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 +6 -3
- package/dist/bin.mjs +1 -1
- package/dist/{cli-DkWxDq0T.mjs → cli-D4Id6WsS.mjs} +235 -41
- package/dist/index.d.mts +39 -1
- package/dist/index.mjs +2 -2
- package/package.json +2 -2
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
|
@@ -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
|
|
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 {
|
|
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
|
-
|
|
1618
|
-
if (linkedTargets.length === 0) {
|
|
1619
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1645
|
-
|
|
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(
|
|
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({
|
|
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(
|
|
1737
|
-
if (wouldUpdate.length > 0) this.log(
|
|
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(
|
|
1787
|
-
if (refreshed.length > 0) this.log(
|
|
1788
|
-
if (skipped.length > 0) this.log(
|
|
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 {
|
|
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
|
|
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.
|
|
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.
|
|
24
|
+
"@statelyai/sdk": "0.10.1"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"tsdown": "0.21.0-beta.2",
|