statelyai 0.5.0 → 0.5.2

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-DB0A-O4Y.mjs";
3
3
 
4
4
  //#region src/bin.ts
5
5
  run();
@@ -10,7 +10,6 @@ import { promisify } from "node:util";
10
10
  import { Args, Command, Flags, flush, handle, run } from "@oclif/core";
11
11
  import * as crypto from "node:crypto";
12
12
  import * as http from "node:http";
13
- import * as https from "node:https";
14
13
  import os from "node:os";
15
14
  import { StudioApiError, createStatelyClient, getStatelyPragma } from "@statelyai/sdk";
16
15
  import { planSync, pullSync, pushLocalMachineLinks } from "@statelyai/sdk/sync";
@@ -594,33 +593,7 @@ function listen(server, port, host) {
594
593
  }
595
594
  async function fetchEditorHost(input, init) {
596
595
  const url = typeof input === "string" ? new URL(input) : input;
597
- if (!isLocalVizHost(url.toString())) return fetch(url, init);
598
- const body = typeof init?.body === "string" ? init.body : init?.body instanceof URLSearchParams ? init.body.toString() : void 0;
599
- return new Promise((resolve, reject) => {
600
- const request = https.request(url, {
601
- method: init?.method ?? "GET",
602
- headers: init?.headers,
603
- rejectUnauthorized: false
604
- }, (response) => {
605
- const chunks = [];
606
- response.on("data", (chunk) => {
607
- chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
608
- });
609
- response.on("end", () => {
610
- const text = Buffer.concat(chunks).toString("utf8");
611
- resolve({
612
- ok: typeof response.statusCode === "number" && response.statusCode >= 200 && response.statusCode < 300,
613
- status: response.statusCode ?? 0,
614
- async json() {
615
- return JSON.parse(text);
616
- }
617
- });
618
- });
619
- });
620
- request.on("error", reject);
621
- if (body !== void 0) request.write(body);
622
- request.end();
623
- });
596
+ return fetch(url, init);
624
597
  }
625
598
  async function assertEditorHostAvailable(editorUrl) {
626
599
  const baseUrl = normalizedBaseUrl(editorUrl);
@@ -634,16 +607,7 @@ async function assertEditorHostAvailable(editorUrl) {
634
607
  if (response.status === 404) throw new Error(formatEditorHostError(baseUrl, response.status));
635
608
  }
636
609
  function formatEditorHostError(baseUrl, status, cause) {
637
- const hint = isLocalVizHost(baseUrl) ? " Start the local editor app with `pnpm dev` on https://viz.localhost, or pass `--editor-url https://viz.localhost`." : " Start the editor host, set `STATELY_EDITOR_URL`, or pass `--editor-url <url>`.";
638
- return `Cannot reach a usable editor host at ${baseUrl}.${status ? ` HTTP ${status} from /embed.` : ""}${cause instanceof Error && cause.message ? ` ${cause.message}` : ""} The CLI loads editor URL defaults from your environment, including \`.env.local\`.${hint}`;
639
- }
640
- function isLocalVizHost(value) {
641
- try {
642
- const url = new URL(value);
643
- return url.protocol === "https:" && url.hostname === "viz.localhost";
644
- } catch {
645
- return false;
646
- }
610
+ return `Cannot reach a usable editor host at ${baseUrl}.${status ? ` HTTP ${status} from /embed.` : ""}${cause instanceof Error && cause.message ? ` ${cause.message}` : ""} The CLI loads editor URL defaults from your environment, including \`.env.local\`. Start the editor host, set \`STATELY_EDITOR_URL\`, or pass \`--editor-url <url>\`.`;
647
611
  }
648
612
  function openBrowser(url) {
649
613
  spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? [
@@ -976,17 +940,44 @@ function normalizeProjectConfig(config) {
976
940
  defaultXStateVersion: Math.max(5, config.defaultXStateVersion ?? 5),
977
941
  include: [...config.include ?? firstSource?.include ?? []],
978
942
  exclude: config.exclude == null ? [...firstSource?.exclude ?? DEFAULT_SOURCE_EXCLUDES] : [...config.exclude],
979
- ...config.newMachineDir ? { newMachineDir: config.newMachineDir } : {}
943
+ ...config.newMachinesDir ? { newMachinesDir: config.newMachinesDir } : {}
944
+ };
945
+ }
946
+ function getConfigRewriteStatus(config) {
947
+ if (!config.sources) return { needsRewrite: false };
948
+ if (config.sources.length > 1) return {
949
+ needsRewrite: true,
950
+ reason: "multiple-legacy-sources"
951
+ };
952
+ return {
953
+ needsRewrite: true,
954
+ reason: "legacy-sources"
980
955
  };
981
956
  }
982
957
  async function readStatelyProjectConfig(options = {}) {
983
958
  const rootDir = path.resolve(options.cwd ?? process.cwd());
984
959
  const configPath = path.resolve(rootDir, options.configPath ?? STATELY_CONFIG_FILE);
985
960
  const raw = await fsPromises.readFile(configPath, "utf8");
961
+ const parsed = JSON.parse(raw);
962
+ const config = normalizeProjectConfig(parsed);
963
+ const rewriteStatus = getConfigRewriteStatus(parsed);
964
+ if (options.rewriteIfNeeded && rewriteStatus.reason === "legacy-sources") {
965
+ await fsPromises.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
966
+ return {
967
+ config,
968
+ configPath,
969
+ rootDir,
970
+ rewriteStatus: {
971
+ needsRewrite: false,
972
+ rewritten: true
973
+ }
974
+ };
975
+ }
986
976
  return {
987
- config: normalizeProjectConfig(JSON.parse(raw)),
977
+ config,
988
978
  configPath,
989
- rootDir
979
+ rootDir,
980
+ rewriteStatus
990
981
  };
991
982
  }
992
983
  async function walkFiles(rootDir, currentDir = rootDir) {
@@ -1162,6 +1153,12 @@ async function discoverStatelySourceFiles(options = {}) {
1162
1153
  const execFileAsync = promisify(execFile);
1163
1154
  const STATELY_API_KEY_SETTINGS_URL = "https://stately.ai/registry/user/my-settings?tab=API+Key";
1164
1155
  const DEFAULT_NEW_MACHINE_FILE_SUFFIX = ".machine.ts";
1156
+ const ANSI_RESET = "\x1B[0m";
1157
+ const ANSI_BOLD = "\x1B[1m";
1158
+ const ANSI_DIM = "\x1B[2m";
1159
+ const ANSI_CYAN = "\x1B[36m";
1160
+ const ANSI_GREEN = "\x1B[32m";
1161
+ const ANSI_YELLOW = "\x1B[33m";
1165
1162
  function loadLocalEnv() {
1166
1163
  if (typeof process.loadEnvFile !== "function") return;
1167
1164
  const cwdEnvPath = path.join(process.cwd(), ".env.local");
@@ -1206,7 +1203,7 @@ function getDefaultBaseUrl() {
1206
1203
  return process.env.STATELY_API_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? process.env.NEXT_PUBLIC_STATELY_API_URL;
1207
1204
  }
1208
1205
  function getDefaultEditorUrl() {
1209
- return process.env.STATELY_EDITOR_URL ?? process.env.NEXT_PUBLIC_BETA_EDITOR_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? "https://viz.localhost";
1206
+ return process.env.STATELY_EDITOR_URL ?? process.env.NEXT_PUBLIC_BETA_EDITOR_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? "https://editor.stately.ai";
1210
1207
  }
1211
1208
  function getResolvedStudioUrl(baseUrl) {
1212
1209
  return baseUrl ?? getDefaultBaseUrl() ?? "https://stately.ai";
@@ -1315,8 +1312,42 @@ function normalizeApiKey(value) {
1315
1312
  function pluralize(count, singular, plural = `${singular}s`) {
1316
1313
  return count === 1 ? singular : plural;
1317
1314
  }
1315
+ function supportsColor() {
1316
+ return Boolean(process.stdout.isTTY) && process.env.NO_COLOR == null && process.env.CI !== "true";
1317
+ }
1318
+ function colorize(text, ...codes) {
1319
+ if (!supportsColor()) return text;
1320
+ return `${codes.join("")}${text}${ANSI_RESET}`;
1321
+ }
1322
+ function formatSectionLabel(label, tone = "info") {
1323
+ return colorize(label, ANSI_BOLD, tone === "success" ? ANSI_GREEN : tone === "warning" ? ANSI_YELLOW : ANSI_CYAN);
1324
+ }
1325
+ function formatSecondaryText(text) {
1326
+ return colorize(text, ANSI_DIM);
1327
+ }
1328
+ function inferSuggestedNewMachineDir(config) {
1329
+ const include = config.include[0];
1330
+ if (!include) return "src";
1331
+ const groupedRootMatch = include.match(/^(src|packages\/[^/]+\/src|apps\/[^/]+\/src)(?:\/|$)/);
1332
+ if (groupedRootMatch?.[1]) return groupedRootMatch[1];
1333
+ const globIndex = include.indexOf("**/");
1334
+ if (globIndex > 0) return include.slice(0, globIndex).replace(/\/$/, "") || "src";
1335
+ const wildcardIndex = include.indexOf("*");
1336
+ if (wildcardIndex > 0) return path.posix.dirname(include.slice(0, wildcardIndex)) || "src";
1337
+ return path.posix.dirname(include) === "." ? "src" : path.posix.dirname(include);
1338
+ }
1339
+ function formatMissingNewMachineDirMessage(options) {
1340
+ const suggestedDir = inferSuggestedNewMachineDir(options.config);
1341
+ const countLabel = pluralize(options.remoteMachineCount, "remote-only project machine");
1342
+ return [
1343
+ `Found ${options.remoteMachineCount} ${countLabel}, but statelyai.json does not set newMachinesDir, so they will be skipped.`,
1344
+ "To import new machines created in Studio, add this to statelyai.json:",
1345
+ ` "newMachinesDir": "${suggestedDir}"`,
1346
+ "Then run `statelyai pull` again."
1347
+ ].join("\n");
1348
+ }
1318
1349
  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";
1350
+ 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
1351
  }
1321
1352
  function getMissingApiKeyMessage() {
1322
1353
  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 +1449,34 @@ async function discoverLinkedPullTargets(files) {
1418
1449
  return linkedTargets;
1419
1450
  }
1420
1451
  async function discoverRemoteProjectMachineTargets(options) {
1421
- const newMachineDir = options.config.newMachineDir?.trim();
1452
+ const newMachineDir = options.config.newMachinesDir?.trim();
1422
1453
  if (!newMachineDir) return [];
1423
1454
  const targets = [];
1455
+ const reservedRelativePaths = /* @__PURE__ */ new Set();
1424
1456
  for (const machine of options.project.machines) {
1425
- if (options.linkedMachineIds.has(machine.machineId)) continue;
1457
+ const candidate = machine;
1458
+ const machineId = typeof candidate.machineId === "string" ? candidate.machineId : typeof candidate.id === "string" ? candidate.id : void 0;
1459
+ if (!machineId || options.linkedMachineIds.has(machineId)) continue;
1426
1460
  const baseDirectory = path.join(options.rootDir, newMachineDir);
1427
- const stem = toMachineFileStem(machine.name);
1461
+ 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;
1462
+ if ((!machineName || machineName === machineId) && options.client?.machines?.get) try {
1463
+ const remoteMachine = await options.client.machines.get(machineId);
1464
+ const fetchedName = typeof remoteMachine?.name === "string" && remoteMachine.name.trim().length > 0 ? remoteMachine.name : void 0;
1465
+ if (fetchedName) machineName = fetchedName;
1466
+ } catch {}
1467
+ machineName ??= machineId;
1468
+ const stem = toMachineFileStem(machineName);
1428
1469
  let candidateFileName = `${stem}${DEFAULT_NEW_MACHINE_FILE_SUFFIX}`;
1429
1470
  let attempt = 2;
1430
1471
  for (;;) {
1431
1472
  const filePath = path.join(baseDirectory, candidateFileName);
1432
1473
  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)) {
1474
+ if (!isTrackedByStatelyProjectConfig(options.config, relativePath)) throw new Error(`Generated path ${relativePath} for remote machine ${machineName} is not covered by include/exclude globs.`);
1475
+ if (!reservedRelativePaths.has(relativePath) && !await fileExists(filePath)) {
1476
+ reservedRelativePaths.add(relativePath);
1435
1477
  targets.push({
1436
- machineId: machine.machineId,
1437
- machineName: machine.name,
1478
+ machineId,
1479
+ machineName,
1438
1480
  filePath,
1439
1481
  relativePath
1440
1482
  });
@@ -1453,7 +1495,8 @@ async function initProject(options) {
1453
1495
  const defaultXStateVersion = Math.max(5, options.defaultXStateVersion ?? 5);
1454
1496
  const existingConfig = !options.force ? await readStatelyProjectConfig({
1455
1497
  cwd,
1456
- configPath: options.configPath
1498
+ configPath: options.configPath,
1499
+ rewriteIfNeeded: true
1457
1500
  }).catch(() => void 0) : void 0;
1458
1501
  const client = options.client ?? createStatelyClient({
1459
1502
  apiKey: options.apiKey,
@@ -1521,9 +1564,10 @@ function supportsMachineDiscovery(file) {
1521
1564
  return file.source.format === "xstate" || file.source.format === "auto";
1522
1565
  }
1523
1566
  async function resolveConfiguredProject(options) {
1524
- const { config, configPath, rootDir } = await readStatelyProjectConfig({
1567
+ const { config, configPath, rootDir, rewriteStatus } = await readStatelyProjectConfig({
1525
1568
  cwd: options.cwd,
1526
- configPath: options.configPath
1569
+ configPath: options.configPath,
1570
+ rewriteIfNeeded: true
1527
1571
  });
1528
1572
  const studioUrl = options.baseUrl ?? config.studioUrl;
1529
1573
  return {
@@ -1536,7 +1580,37 @@ async function resolveConfiguredProject(options) {
1536
1580
  files: await discoverStatelySourceFiles({
1537
1581
  cwd: rootDir,
1538
1582
  config
1539
- })
1583
+ }),
1584
+ rewriteStatus
1585
+ };
1586
+ }
1587
+ async function resolveConfiguredPullTargets(options) {
1588
+ const { client, files, config, configPath, rewriteStatus } = await resolveConfiguredProject(options);
1589
+ const linkedTargets = await discoverLinkedPullTargets(files);
1590
+ const linkedMachineIds = new Set(linkedTargets.map((linkedTarget) => linkedTarget.machineId));
1591
+ const project = await client.projects.get(config.projectId);
1592
+ const skipped = [];
1593
+ let remoteOnlyTargets = [];
1594
+ try {
1595
+ remoteOnlyTargets = await discoverRemoteProjectMachineTargets({
1596
+ rootDir: path.dirname(configPath),
1597
+ config,
1598
+ project,
1599
+ linkedMachineIds,
1600
+ client
1601
+ });
1602
+ } catch (error) {
1603
+ skipped.push(error instanceof Error ? error.message : String(error));
1604
+ }
1605
+ return {
1606
+ client,
1607
+ config,
1608
+ configPath,
1609
+ linkedTargets,
1610
+ project,
1611
+ remoteOnlyTargets,
1612
+ skipped,
1613
+ rewriteStatus
1540
1614
  };
1541
1615
  }
1542
1616
  const sharedFlags = {
@@ -1654,37 +1728,33 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1654
1728
  const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
1655
1729
  if (!args.source) {
1656
1730
  if (!apiKey) this.error(getMissingApiKeyMessage());
1657
- this.log("Resolving configured project and linked source files...");
1658
- const { files, config, configPath } = await resolveConfiguredProject({
1731
+ this.log(colorize("Resolving configured project and linked source files...", ANSI_CYAN));
1732
+ const { config, linkedTargets, project, remoteOnlyTargets, skipped, rewriteStatus } = await resolveConfiguredPullTargets({
1659
1733
  apiKey,
1660
1734
  baseUrl: flags["base-url"],
1661
1735
  configPath: flags.config
1662
1736
  });
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
- }
1737
+ 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
1738
  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.");
1739
+ if (project.machines.length > 0 && !config.newMachinesDir) {
1740
+ this.log(formatSectionLabel("Skipped:", "warning"));
1741
+ this.log(formatMissingNewMachineDirMessage({
1742
+ config,
1743
+ remoteMachineCount: project.machines.length
1744
+ }));
1681
1745
  return;
1682
1746
  }
1683
1747
  this.log("No linked local machine files or remote-only project machines were discovered.");
1684
1748
  return;
1685
1749
  }
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")}` : ""}...`);
1750
+ if (project.machines.length > linkedTargets.length && !config.newMachinesDir) {
1751
+ this.log(formatSectionLabel("Skipped:", "warning"));
1752
+ this.log(formatMissingNewMachineDirMessage({
1753
+ config,
1754
+ remoteMachineCount: project.machines.length - linkedTargets.length
1755
+ }));
1756
+ }
1757
+ 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
1758
  const pulled = [];
1689
1759
  const created = [];
1690
1760
  for (const linkedTarget of linkedTargets) {
@@ -1693,7 +1763,7 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1693
1763
  skipped.push(`${linkedTarget.file.relativePath}: local git changes detected (pass --force to overwrite)`);
1694
1764
  continue;
1695
1765
  }
1696
- this.log(`Pulling ${linkedTarget.file.relativePath}...`);
1766
+ this.log(colorize(`Pulling ${linkedTarget.file.relativePath}...`, ANSI_CYAN));
1697
1767
  try {
1698
1768
  const result = await pullSync({
1699
1769
  source: linkedTarget.machineId,
@@ -1701,13 +1771,13 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1701
1771
  apiKey,
1702
1772
  baseUrl: flags["base-url"] ?? config.studioUrl
1703
1773
  });
1704
- pulled.push(`${linkedTarget.file.relativePath}: ${result.source.locator}`);
1774
+ pulled.push(`${linkedTarget.file.relativePath}: ${formatSecondaryText(result.source.locator)}`);
1705
1775
  } catch (error) {
1706
1776
  skipped.push(`${linkedTarget.file.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
1707
1777
  }
1708
1778
  }
1709
1779
  for (const remoteTarget of remoteOnlyTargets) {
1710
- this.log(`Pulling new remote machine ${remoteTarget.machineName} into ${remoteTarget.relativePath}...`);
1780
+ this.log(colorize(`Pulling new remote machine ${remoteTarget.machineName} into ${remoteTarget.relativePath}...`, ANSI_CYAN));
1711
1781
  try {
1712
1782
  await fsPromises.mkdir(path.dirname(remoteTarget.filePath), { recursive: true });
1713
1783
  const result = await pullSync({
@@ -1716,14 +1786,14 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1716
1786
  apiKey,
1717
1787
  baseUrl: flags["base-url"] ?? config.studioUrl
1718
1788
  });
1719
- created.push(`${remoteTarget.relativePath}: ${result.source.locator}`);
1789
+ created.push(`${remoteTarget.relativePath}: ${formatSecondaryText(result.source.locator)}`);
1720
1790
  } catch (error) {
1721
1791
  skipped.push(`${remoteTarget.relativePath}: ${error instanceof Error ? error.message : String(error)}`);
1722
1792
  }
1723
1793
  }
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")}`);
1794
+ if (pulled.length > 0) this.log(`${formatSectionLabel("Pulled:", "success")}\n${pulled.join("\n")}`);
1795
+ if (created.length > 0) this.log(`${formatSectionLabel("Created:", "success")}\n${created.join("\n")}`);
1796
+ if (skipped.length > 0) this.log(`${formatSectionLabel("Skipped:", "warning")}\n${skipped.join("\n")}`);
1727
1797
  return;
1728
1798
  }
1729
1799
  const localCandidate = path.resolve(process.cwd(), args.source);
@@ -1738,14 +1808,14 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
1738
1808
  if (!target) this.error("Missing target path. Pass `statelyai pull <machine-id|url> <file>` or `statelyai pull <linked-file>`.");
1739
1809
  if (!apiKey) this.error(getMissingApiKeyMessage());
1740
1810
  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}...`);
1811
+ this.log(colorize(`Pulling ${source} into ${target}...`, ANSI_CYAN));
1742
1812
  const result = await pullSync({
1743
1813
  source,
1744
1814
  target,
1745
1815
  apiKey,
1746
1816
  baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
1747
1817
  });
1748
- this.log(`Pulled: ${result.source.locator} -> ${result.outputPath}\nTarget: ${result.target.kind} (${result.target.format})`);
1818
+ this.log(`${formatSectionLabel("Pulled:", "success")} ${formatSecondaryText(result.source.locator)} -> ${result.outputPath}\nTarget: ${result.target.kind} (${result.target.format})`);
1749
1819
  }
1750
1820
  };
1751
1821
  var PushCommand = class PushCommand extends Command {
@@ -1770,22 +1840,27 @@ var PushCommand = class PushCommand extends Command {
1770
1840
  const { args, flags } = await this.parse(PushCommand);
1771
1841
  const resolvedApiKey = flags["dry-run"] ? { source: "missing" } : await resolveApiKey(flags["api-key"]);
1772
1842
  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 });
1843
+ this.log(colorize("Resolving configured project and source files...", ANSI_CYAN));
1844
+ const { client, config, files, rewriteStatus } = flags["dry-run"] ? await (async () => {
1845
+ const { config, rootDir, rewriteStatus } = await readStatelyProjectConfig({
1846
+ configPath: flags.config,
1847
+ rewriteIfNeeded: true
1848
+ });
1776
1849
  return {
1777
1850
  client: void 0,
1778
1851
  config,
1779
1852
  files: await discoverStatelySourceFiles({
1780
1853
  cwd: rootDir,
1781
1854
  config
1782
- })
1855
+ }),
1856
+ rewriteStatus
1783
1857
  };
1784
1858
  })() : await resolveConfiguredProject({
1785
1859
  apiKey: resolvedApiKey.apiKey,
1786
1860
  baseUrl: flags["base-url"],
1787
1861
  configPath: flags.config
1788
1862
  });
1863
+ 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
1864
  const candidateFiles = args.file ? [{
1790
1865
  filePath: path.resolve(args.file),
1791
1866
  relativePath: path.relative(process.cwd(), path.resolve(args.file)),
@@ -1797,33 +1872,33 @@ var PushCommand = class PushCommand extends Command {
1797
1872
  }
1798
1873
  }] : files.filter(supportsMachineDiscovery);
1799
1874
  if (candidateFiles.length === 0) {
1800
- this.log("No matching local machine source files were discovered.");
1875
+ this.log(colorize("No matching local machine source files were discovered.", ANSI_YELLOW));
1801
1876
  return;
1802
1877
  }
1803
1878
  const { machineFiles } = await classifyPushCandidates(candidateFiles);
1804
1879
  if (machineFiles.length === 0) {
1805
- this.log(`Matched ${candidateFiles.length} ${pluralize(candidateFiles.length, "file")}, but no local machine sources were discovered.`);
1880
+ this.log(colorize(`Matched ${candidateFiles.length} ${pluralize(candidateFiles.length, "file")}, but no local machine sources were discovered.`, ANSI_YELLOW));
1806
1881
  return;
1807
1882
  }
1808
- this.log(`Discovered ${machineFiles.length} ${pluralize(machineFiles.length, "file with a machine")} from ${candidateFiles.length} matched ${pluralize(candidateFiles.length, "file")}.`);
1883
+ this.log(colorize(`Discovered ${machineFiles.length} ${pluralize(machineFiles.length, "file with a machine")} from ${candidateFiles.length} matched ${pluralize(candidateFiles.length, "file")}.`, ANSI_CYAN));
1809
1884
  if (flags["dry-run"]) {
1810
1885
  const wouldLink = [];
1811
1886
  const wouldUpdate = [];
1812
1887
  for (const file of machineFiles) {
1813
1888
  const pragma = getStatelyPragma(await fsPromises.readFile(file.filePath, "utf8"), file.filePath);
1814
- if (pragma?.id) wouldUpdate.push(`${file.relativePath}: ${pragma.id}`);
1889
+ if (pragma?.id) wouldUpdate.push(`${file.relativePath}: ${formatSecondaryText(pragma.id)}`);
1815
1890
  else wouldLink.push(file.relativePath);
1816
1891
  }
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")}`);
1892
+ if (wouldLink.length > 0) this.log(`${formatSectionLabel("Would link:")}\n${wouldLink.join("\n")}`);
1893
+ if (wouldUpdate.length > 0) this.log(`${formatSectionLabel("Would update:")}\n${wouldUpdate.join("\n")}`);
1819
1894
  return;
1820
1895
  }
1821
- this.log(`Processing ${machineFiles.length} ${pluralize(machineFiles.length, "source file")}...`);
1896
+ this.log(colorize(`Processing ${machineFiles.length} ${pluralize(machineFiles.length, "source file")}...`, ANSI_CYAN));
1822
1897
  const linked = [];
1823
1898
  const refreshed = [];
1824
1899
  const skipped = [];
1825
1900
  for (const file of machineFiles) {
1826
- this.log(`Pushing ${file.relativePath}...`);
1901
+ this.log(colorize(`Pushing ${file.relativePath}...`, ANSI_CYAN));
1827
1902
  if (!supportsMachineDiscovery(file)) {
1828
1903
  skipped.push(`${file.relativePath}: unsupported format ${file.source.format}`);
1829
1904
  continue;
@@ -1848,7 +1923,7 @@ var PushCommand = class PushCommand extends Command {
1848
1923
  skipped.push(`${file.relativePath}: linked remote machine is missing or inaccessible.`);
1849
1924
  continue;
1850
1925
  }
1851
- this.log(`Relinking ${file.relativePath} as a new remote machine...`);
1926
+ this.log(colorize(`Relinking ${file.relativePath} as a new remote machine...`, ANSI_YELLOW));
1852
1927
  result = await pushLocalMachineLinks({
1853
1928
  source: file.filePath,
1854
1929
  apiKey: resolvedApiKey.apiKey,
@@ -1860,13 +1935,13 @@ var PushCommand = class PushCommand extends Command {
1860
1935
  });
1861
1936
  } else throw error;
1862
1937
  }
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(", ")}`);
1938
+ 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(", ")}`);
1939
+ 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
1940
  for (const entry of result.skipped) skipped.push(`${file.relativePath} [${entry.machineIndex}]: ${entry.reason}`);
1866
1941
  }
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")}`);
1942
+ if (linked.length > 0) this.log(`${formatSectionLabel("Linked:", "success")}\n${linked.join("\n")}`);
1943
+ if (refreshed.length > 0) this.log(`${formatSectionLabel("Updated:", "success")}\n${refreshed.join("\n")}`);
1944
+ if (skipped.length > 0) this.log(`${formatSectionLabel("Skipped:", "warning")}\n${skipped.join("\n")}`);
1870
1945
  }
1871
1946
  };
1872
1947
  var OpenCommand = class OpenCommand extends Command {
@@ -1975,7 +2050,7 @@ var InitCommand = class InitCommand extends Command {
1975
2050
  ...result.config,
1976
2051
  include: suggestions.flatMap((suggestion) => suggestion.include),
1977
2052
  exclude: [...new Set(suggestions.flatMap((suggestion) => suggestion.exclude ?? []))],
1978
- ...inferredNewMachineDir ? { newMachineDir: inferredNewMachineDir } : {}
2053
+ ...inferredNewMachineDir ? { newMachinesDir: inferredNewMachineDir } : {}
1979
2054
  };
1980
2055
  await fsPromises.writeFile(result.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
1981
2056
  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 +2149,4 @@ function isDirectExecution() {
2074
2149
  if (isDirectExecution()) run$1();
2075
2150
 
2076
2151
  //#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 };
2152
+ 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-DB0A-O4Y.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.2",
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",