@statelyai/sdk 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -82
- package/dist/api.d.mts +20 -0
- package/dist/api.mjs +56 -0
- package/dist/cli.d.mts +100 -2
- package/dist/cli.mjs +584 -13
- package/dist/embed.d.mts +21 -3
- package/dist/embed.mjs +52 -2
- package/dist/graph.d.mts +1 -1
- package/dist/graph.mjs +20 -7
- package/dist/graphToXStateTS-CvXM8wHL.mjs +344 -0
- package/dist/index.d.mts +29 -140
- package/dist/index.mjs +4 -3
- package/dist/{inspect-ttRIjoCu.d.mts → inspect-DIxB2Tr3.d.mts} +1 -1
- package/dist/inspect.d.mts +2 -2
- package/dist/inspect.mjs +1 -1
- package/dist/{protocol-BPuwbNCz.d.mts → protocol-CEbWQPYe.d.mts} +70 -5
- package/dist/studio.d.mts +112 -2
- package/dist/studio.mjs +73 -11
- package/dist/sync.d.mts +19 -3
- package/dist/sync.mjs +128 -6
- package/dist/{transport-DoCHBLTu.mjs → transport-C0eTgNNu.mjs} +14 -0
- package/package.json +24 -15
- package/schemas/statelyai.schema.json +128 -0
- package/dist/graphToXStateTS-CtecEESq.mjs +0 -568
- package/dist/studio-D2uQhrvX.d.mts +0 -54
- /package/dist/{graph-BfezxFKJ.d.mts → graph-CB-ALrdk.d.mts} +0 -0
package/dist/cli.mjs
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { createStatelyClient } from "./studio.mjs";
|
|
3
|
+
import "./graphToXStateTS-CvXM8wHL.mjs";
|
|
2
4
|
import { planSync, pullSync } from "./sync.mjs";
|
|
3
5
|
import fs from "node:fs/promises";
|
|
4
6
|
import * as path$1 from "node:path";
|
|
5
7
|
import path from "node:path";
|
|
6
|
-
import
|
|
8
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
import fs$1, { promises, watch } from "node:fs";
|
|
11
|
+
import { createInterface } from "node:readline/promises";
|
|
12
|
+
import { Writable } from "node:stream";
|
|
7
13
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
8
14
|
import { Args, Command, Flags, flush, handle, run as run$1 } from "@oclif/core";
|
|
9
15
|
import * as crypto from "node:crypto";
|
|
10
|
-
import { spawn } from "node:child_process";
|
|
11
16
|
import * as http from "node:http";
|
|
17
|
+
import * as https from "node:https";
|
|
18
|
+
import os from "node:os";
|
|
12
19
|
|
|
13
20
|
//#region src/cliHost.ts
|
|
14
21
|
const DEFAULT_SYNC_MAX_FILES = 150;
|
|
@@ -255,7 +262,7 @@ var RemoteEditorSession = class {
|
|
|
255
262
|
const url = new URL(pathname, normalizedBaseUrl(this.options.editorUrl));
|
|
256
263
|
const headers = { "Content-Type": "application/json" };
|
|
257
264
|
if (this.options.apiKey) headers.Authorization = `Bearer ${this.options.apiKey}`;
|
|
258
|
-
const response = await
|
|
265
|
+
const response = await fetchEditorHost(url, {
|
|
259
266
|
method: "POST",
|
|
260
267
|
headers,
|
|
261
268
|
body: JSON.stringify(body)
|
|
@@ -339,6 +346,7 @@ var RemoteEditorSession = class {
|
|
|
339
346
|
async function openEditor(options) {
|
|
340
347
|
const fileName = path$1.resolve(options.fileName);
|
|
341
348
|
await fs.access(fileName);
|
|
349
|
+
await assertEditorHostAvailable(options.editorUrl);
|
|
342
350
|
const rootUri = fileNameToUri(fileName);
|
|
343
351
|
const rootDir = path$1.dirname(fileName);
|
|
344
352
|
let activeClient;
|
|
@@ -586,6 +594,59 @@ function listen(server, port, host) {
|
|
|
586
594
|
server.listen(port, host, resolve);
|
|
587
595
|
});
|
|
588
596
|
}
|
|
597
|
+
async function fetchEditorHost(input, init) {
|
|
598
|
+
const url = typeof input === "string" ? new URL(input) : input;
|
|
599
|
+
if (!isLocalVizHost(url.toString())) return fetch(url, init);
|
|
600
|
+
const body = typeof init?.body === "string" ? init.body : init?.body instanceof URLSearchParams ? init.body.toString() : void 0;
|
|
601
|
+
return new Promise((resolve, reject) => {
|
|
602
|
+
const request = https.request(url, {
|
|
603
|
+
method: init?.method ?? "GET",
|
|
604
|
+
headers: init?.headers,
|
|
605
|
+
rejectUnauthorized: false
|
|
606
|
+
}, (response) => {
|
|
607
|
+
const chunks = [];
|
|
608
|
+
response.on("data", (chunk) => {
|
|
609
|
+
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
610
|
+
});
|
|
611
|
+
response.on("end", () => {
|
|
612
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
613
|
+
resolve({
|
|
614
|
+
ok: typeof response.statusCode === "number" && response.statusCode >= 200 && response.statusCode < 300,
|
|
615
|
+
status: response.statusCode ?? 0,
|
|
616
|
+
async json() {
|
|
617
|
+
return JSON.parse(text);
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
request.on("error", reject);
|
|
623
|
+
if (body !== void 0) request.write(body);
|
|
624
|
+
request.end();
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
async function assertEditorHostAvailable(editorUrl) {
|
|
628
|
+
const baseUrl = normalizedBaseUrl(editorUrl);
|
|
629
|
+
const embedUrl = new URL("/embed", baseUrl);
|
|
630
|
+
let response;
|
|
631
|
+
try {
|
|
632
|
+
response = await fetchEditorHost(embedUrl, { redirect: "manual" });
|
|
633
|
+
} catch (error) {
|
|
634
|
+
throw new Error(formatEditorHostError(baseUrl, void 0, error));
|
|
635
|
+
}
|
|
636
|
+
if (response.status === 404) throw new Error(formatEditorHostError(baseUrl, response.status));
|
|
637
|
+
}
|
|
638
|
+
function formatEditorHostError(baseUrl, status, cause) {
|
|
639
|
+
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>`.";
|
|
640
|
+
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}`;
|
|
641
|
+
}
|
|
642
|
+
function isLocalVizHost(value) {
|
|
643
|
+
try {
|
|
644
|
+
const url = new URL(value);
|
|
645
|
+
return url.protocol === "https:" && url.hostname === "viz.localhost";
|
|
646
|
+
} catch {
|
|
647
|
+
return false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
589
650
|
function openBrowser(url) {
|
|
590
651
|
spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? [
|
|
591
652
|
"/c",
|
|
@@ -639,14 +700,266 @@ function normalizedBaseUrl(value) {
|
|
|
639
700
|
return value.replace(/\/+$/, "");
|
|
640
701
|
}
|
|
641
702
|
function formatError(error, fallback) {
|
|
642
|
-
|
|
703
|
+
if (!(error instanceof Error)) return fallback;
|
|
704
|
+
const details = collectErrorDetails(error);
|
|
705
|
+
return details.length > 0 ? `${error.message} (${details.join(" | ")})` : error.message;
|
|
706
|
+
}
|
|
707
|
+
function collectErrorDetails(error) {
|
|
708
|
+
const details = [];
|
|
709
|
+
const seen = /* @__PURE__ */ new Set();
|
|
710
|
+
let current = error;
|
|
711
|
+
while (current && typeof current === "object" && !seen.has(current)) {
|
|
712
|
+
seen.add(current);
|
|
713
|
+
const code = "code" in current && typeof current.code === "string" ? current.code : void 0;
|
|
714
|
+
const message = "message" in current && typeof current.message === "string" ? current.message : void 0;
|
|
715
|
+
if (code && message) details.push(`${code}: ${message}`);
|
|
716
|
+
else if (code) details.push(code);
|
|
717
|
+
current = "cause" in current ? current.cause : void 0;
|
|
718
|
+
}
|
|
719
|
+
return details;
|
|
643
720
|
}
|
|
644
721
|
function escapeAttribute(value) {
|
|
645
722
|
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
646
723
|
}
|
|
647
724
|
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/credentials.ts
|
|
727
|
+
const execFile$1 = promisify(execFile);
|
|
728
|
+
const SERVICE_NAME = "statelyai";
|
|
729
|
+
const ACCOUNT_NAME = "api-key";
|
|
730
|
+
const CREDENTIALS_FILE_NAME = "credentials.json";
|
|
731
|
+
function getConfigDir() {
|
|
732
|
+
const override = process.env.STATELYAI_CONFIG_DIR;
|
|
733
|
+
if (override) return path.resolve(override);
|
|
734
|
+
if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", SERVICE_NAME);
|
|
735
|
+
if (process.platform === "win32") return path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), SERVICE_NAME);
|
|
736
|
+
return path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), SERVICE_NAME);
|
|
737
|
+
}
|
|
738
|
+
function getCredentialsFilePath() {
|
|
739
|
+
return path.join(getConfigDir(), CREDENTIALS_FILE_NAME);
|
|
740
|
+
}
|
|
741
|
+
function shouldUseFileBackendOnly() {
|
|
742
|
+
return process.env.STATELYAI_CREDENTIALS_BACKEND === "file";
|
|
743
|
+
}
|
|
744
|
+
async function readFileCredentials() {
|
|
745
|
+
try {
|
|
746
|
+
const raw = await promises.readFile(getCredentialsFilePath(), "utf8");
|
|
747
|
+
const parsed = JSON.parse(raw);
|
|
748
|
+
if (typeof parsed.apiKey !== "string" || parsed.apiKey.length === 0) return;
|
|
749
|
+
return {
|
|
750
|
+
apiKey: parsed.apiKey,
|
|
751
|
+
backend: "file",
|
|
752
|
+
location: getCredentialsFilePath()
|
|
753
|
+
};
|
|
754
|
+
} catch {
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
async function writeFileCredentials(apiKey) {
|
|
759
|
+
const configDir = getConfigDir();
|
|
760
|
+
const credentialsPath = getCredentialsFilePath();
|
|
761
|
+
await promises.mkdir(configDir, {
|
|
762
|
+
recursive: true,
|
|
763
|
+
mode: 448
|
|
764
|
+
});
|
|
765
|
+
await promises.writeFile(credentialsPath, `${JSON.stringify({ apiKey }, null, 2)}\n`, {
|
|
766
|
+
encoding: "utf8",
|
|
767
|
+
mode: 384
|
|
768
|
+
});
|
|
769
|
+
await promises.chmod(credentialsPath, 384);
|
|
770
|
+
return {
|
|
771
|
+
apiKey,
|
|
772
|
+
backend: "file",
|
|
773
|
+
location: credentialsPath
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
async function deleteFileCredentials() {
|
|
777
|
+
try {
|
|
778
|
+
await promises.unlink(getCredentialsFilePath());
|
|
779
|
+
return true;
|
|
780
|
+
} catch {
|
|
781
|
+
return false;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
async function readMacOSKeychain() {
|
|
785
|
+
if (process.platform !== "darwin") return;
|
|
786
|
+
try {
|
|
787
|
+
const { stdout } = await execFile$1("security", [
|
|
788
|
+
"find-generic-password",
|
|
789
|
+
"-s",
|
|
790
|
+
SERVICE_NAME,
|
|
791
|
+
"-a",
|
|
792
|
+
ACCOUNT_NAME,
|
|
793
|
+
"-w"
|
|
794
|
+
]);
|
|
795
|
+
const apiKey = stdout.trim();
|
|
796
|
+
if (!apiKey) return;
|
|
797
|
+
return {
|
|
798
|
+
apiKey,
|
|
799
|
+
backend: "macos-keychain",
|
|
800
|
+
location: "macOS Keychain"
|
|
801
|
+
};
|
|
802
|
+
} catch {
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
async function writeMacOSKeychain(apiKey) {
|
|
807
|
+
if (process.platform !== "darwin") return;
|
|
808
|
+
try {
|
|
809
|
+
await execFile$1("security", [
|
|
810
|
+
"add-generic-password",
|
|
811
|
+
"-U",
|
|
812
|
+
"-s",
|
|
813
|
+
SERVICE_NAME,
|
|
814
|
+
"-a",
|
|
815
|
+
ACCOUNT_NAME,
|
|
816
|
+
"-w",
|
|
817
|
+
apiKey
|
|
818
|
+
]);
|
|
819
|
+
return {
|
|
820
|
+
apiKey,
|
|
821
|
+
backend: "macos-keychain",
|
|
822
|
+
location: "macOS Keychain"
|
|
823
|
+
};
|
|
824
|
+
} catch {
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
async function deleteMacOSKeychain() {
|
|
829
|
+
if (process.platform !== "darwin") return false;
|
|
830
|
+
try {
|
|
831
|
+
await execFile$1("security", [
|
|
832
|
+
"delete-generic-password",
|
|
833
|
+
"-s",
|
|
834
|
+
SERVICE_NAME,
|
|
835
|
+
"-a",
|
|
836
|
+
ACCOUNT_NAME
|
|
837
|
+
]);
|
|
838
|
+
return true;
|
|
839
|
+
} catch {
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
async function readLinuxSecretTool() {
|
|
844
|
+
if (process.platform !== "linux") return;
|
|
845
|
+
try {
|
|
846
|
+
const { stdout } = await execFile$1("secret-tool", [
|
|
847
|
+
"lookup",
|
|
848
|
+
"service",
|
|
849
|
+
SERVICE_NAME,
|
|
850
|
+
"account",
|
|
851
|
+
ACCOUNT_NAME
|
|
852
|
+
]);
|
|
853
|
+
const apiKey = stdout.trim();
|
|
854
|
+
if (!apiKey) return;
|
|
855
|
+
return {
|
|
856
|
+
apiKey,
|
|
857
|
+
backend: "linux-secret-tool",
|
|
858
|
+
location: "Secret Service (secret-tool)"
|
|
859
|
+
};
|
|
860
|
+
} catch {
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
async function writeLinuxSecretTool(apiKey) {
|
|
865
|
+
if (process.platform !== "linux") return;
|
|
866
|
+
try {
|
|
867
|
+
execFileSync("secret-tool", [
|
|
868
|
+
"store",
|
|
869
|
+
"--label",
|
|
870
|
+
"statelyai API key",
|
|
871
|
+
"service",
|
|
872
|
+
SERVICE_NAME,
|
|
873
|
+
"account",
|
|
874
|
+
ACCOUNT_NAME
|
|
875
|
+
], { input: apiKey });
|
|
876
|
+
return {
|
|
877
|
+
apiKey,
|
|
878
|
+
backend: "linux-secret-tool",
|
|
879
|
+
location: "Secret Service (secret-tool)"
|
|
880
|
+
};
|
|
881
|
+
} catch {
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
async function deleteLinuxSecretTool() {
|
|
886
|
+
if (process.platform !== "linux") return false;
|
|
887
|
+
try {
|
|
888
|
+
await execFile$1("secret-tool", [
|
|
889
|
+
"clear",
|
|
890
|
+
"service",
|
|
891
|
+
SERVICE_NAME,
|
|
892
|
+
"account",
|
|
893
|
+
ACCOUNT_NAME
|
|
894
|
+
]);
|
|
895
|
+
return true;
|
|
896
|
+
} catch {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
async function getStoredApiKey() {
|
|
901
|
+
if (shouldUseFileBackendOnly()) return readFileCredentials();
|
|
902
|
+
return await readMacOSKeychain() ?? await readLinuxSecretTool() ?? await readFileCredentials();
|
|
903
|
+
}
|
|
904
|
+
async function setStoredApiKey(apiKey) {
|
|
905
|
+
if (shouldUseFileBackendOnly()) return writeFileCredentials(apiKey);
|
|
906
|
+
return await writeMacOSKeychain(apiKey) ?? await writeLinuxSecretTool(apiKey) ?? await writeFileCredentials(apiKey);
|
|
907
|
+
}
|
|
908
|
+
async function deleteStoredApiKey() {
|
|
909
|
+
if (shouldUseFileBackendOnly()) {
|
|
910
|
+
const deleted = await deleteFileCredentials();
|
|
911
|
+
return {
|
|
912
|
+
deleted,
|
|
913
|
+
locations: deleted ? [getCredentialsFilePath()] : []
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
const locations = [];
|
|
917
|
+
if (await deleteMacOSKeychain()) locations.push("macOS Keychain");
|
|
918
|
+
if (await deleteLinuxSecretTool()) locations.push("Secret Service (secret-tool)");
|
|
919
|
+
if (await deleteFileCredentials()) locations.push(getCredentialsFilePath());
|
|
920
|
+
return {
|
|
921
|
+
deleted: locations.length > 0,
|
|
922
|
+
locations
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function describeCredentialBackend(backend, location) {
|
|
926
|
+
switch (backend) {
|
|
927
|
+
case "macos-keychain": return location;
|
|
928
|
+
case "linux-secret-tool": return location;
|
|
929
|
+
case "file": return `file (${location})`;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
648
933
|
//#endregion
|
|
649
934
|
//#region src/cli.ts
|
|
935
|
+
const execFileAsync = promisify(execFile);
|
|
936
|
+
const STATELY_CONFIG_FILE = "statelyai.json";
|
|
937
|
+
const STATELY_CONFIG_SCHEMA_URL = "https://stately.ai/schemas/statelyai.json";
|
|
938
|
+
const STATELY_CONFIG_VERSION = "1.0.0";
|
|
939
|
+
function getDefaultSources(defaultXStateVersion) {
|
|
940
|
+
return [{
|
|
941
|
+
include: ["**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}"],
|
|
942
|
+
exclude: [
|
|
943
|
+
"**/*.test.*",
|
|
944
|
+
"**/*.spec.*",
|
|
945
|
+
"**/dist/**",
|
|
946
|
+
"**/node_modules/**"
|
|
947
|
+
],
|
|
948
|
+
format: "xstate",
|
|
949
|
+
xstateVersion: defaultXStateVersion
|
|
950
|
+
}];
|
|
951
|
+
}
|
|
952
|
+
function createStatelyProjectConfig(options) {
|
|
953
|
+
const defaultXStateVersion = options.defaultXStateVersion ?? 5;
|
|
954
|
+
return {
|
|
955
|
+
$schema: STATELY_CONFIG_SCHEMA_URL,
|
|
956
|
+
version: STATELY_CONFIG_VERSION,
|
|
957
|
+
projectId: options.projectId,
|
|
958
|
+
studioUrl: options.studioUrl,
|
|
959
|
+
defaultXStateVersion,
|
|
960
|
+
sources: getDefaultSources(defaultXStateVersion)
|
|
961
|
+
};
|
|
962
|
+
}
|
|
650
963
|
function loadLocalEnv() {
|
|
651
964
|
if (typeof process.loadEnvFile !== "function") return;
|
|
652
965
|
const cwdEnvPath = path.join(process.cwd(), ".env.local");
|
|
@@ -655,14 +968,170 @@ function loadLocalEnv() {
|
|
|
655
968
|
} catch {}
|
|
656
969
|
}
|
|
657
970
|
loadLocalEnv();
|
|
658
|
-
function
|
|
659
|
-
|
|
971
|
+
function getEnvApiKey() {
|
|
972
|
+
const statelyApiKey = process.env.STATELY_API_KEY;
|
|
973
|
+
if (statelyApiKey) return {
|
|
974
|
+
apiKey: statelyApiKey,
|
|
975
|
+
variable: "STATELY_API_KEY"
|
|
976
|
+
};
|
|
977
|
+
const publicApiKey = process.env.NEXT_PUBLIC_STATELY_API_KEY;
|
|
978
|
+
if (publicApiKey) return {
|
|
979
|
+
apiKey: publicApiKey,
|
|
980
|
+
variable: "NEXT_PUBLIC_STATELY_API_KEY"
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
async function resolveApiKey(explicitApiKey) {
|
|
984
|
+
if (explicitApiKey) return {
|
|
985
|
+
apiKey: explicitApiKey,
|
|
986
|
+
source: "flag",
|
|
987
|
+
detail: "--api-key"
|
|
988
|
+
};
|
|
989
|
+
const envApiKey = getEnvApiKey();
|
|
990
|
+
if (envApiKey) return {
|
|
991
|
+
apiKey: envApiKey.apiKey,
|
|
992
|
+
source: "env",
|
|
993
|
+
detail: envApiKey.variable
|
|
994
|
+
};
|
|
995
|
+
const storedApiKey = await getStoredApiKey();
|
|
996
|
+
if (storedApiKey) return {
|
|
997
|
+
apiKey: storedApiKey.apiKey,
|
|
998
|
+
source: "stored",
|
|
999
|
+
detail: describeCredentialBackend(storedApiKey.backend, storedApiKey.location)
|
|
1000
|
+
};
|
|
1001
|
+
return { source: "missing" };
|
|
660
1002
|
}
|
|
661
1003
|
function getDefaultBaseUrl() {
|
|
662
1004
|
return process.env.STATELY_API_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? process.env.NEXT_PUBLIC_STATELY_API_URL;
|
|
663
1005
|
}
|
|
664
1006
|
function getDefaultEditorUrl() {
|
|
665
|
-
return process.env.STATELY_EDITOR_URL ?? process.env.NEXT_PUBLIC_BETA_EDITOR_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? "
|
|
1007
|
+
return process.env.STATELY_EDITOR_URL ?? process.env.NEXT_PUBLIC_BETA_EDITOR_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? "https://viz.localhost";
|
|
1008
|
+
}
|
|
1009
|
+
function getResolvedStudioUrl(baseUrl) {
|
|
1010
|
+
return baseUrl ?? getDefaultBaseUrl() ?? "https://stately.ai";
|
|
1011
|
+
}
|
|
1012
|
+
function parseGitHubRemote(remoteUrl) {
|
|
1013
|
+
const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
1014
|
+
if (httpsMatch) {
|
|
1015
|
+
const [, owner, repo] = httpsMatch;
|
|
1016
|
+
if (!owner || !repo) return null;
|
|
1017
|
+
return {
|
|
1018
|
+
url: `https://github.com/${owner}/${repo}`,
|
|
1019
|
+
owner,
|
|
1020
|
+
repo,
|
|
1021
|
+
branch: "",
|
|
1022
|
+
treeSha: ""
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
1026
|
+
if (sshMatch) {
|
|
1027
|
+
const [, owner, repo] = sshMatch;
|
|
1028
|
+
if (!owner || !repo) return null;
|
|
1029
|
+
return {
|
|
1030
|
+
url: `https://github.com/${owner}/${repo}`,
|
|
1031
|
+
owner,
|
|
1032
|
+
repo,
|
|
1033
|
+
branch: "",
|
|
1034
|
+
treeSha: ""
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
async function inferConnectedRepoFromCwd(cwd) {
|
|
1040
|
+
try {
|
|
1041
|
+
const [{ stdout: remoteStdout }, { stdout: branchStdout }, { stdout: treeShaStdout }] = await Promise.all([
|
|
1042
|
+
execFileAsync("git", [
|
|
1043
|
+
"remote",
|
|
1044
|
+
"get-url",
|
|
1045
|
+
"origin"
|
|
1046
|
+
], { cwd }),
|
|
1047
|
+
execFileAsync("git", ["branch", "--show-current"], { cwd }),
|
|
1048
|
+
execFileAsync("git", ["rev-parse", "HEAD"], { cwd })
|
|
1049
|
+
]);
|
|
1050
|
+
const parsedRemote = parseGitHubRemote(remoteStdout.trim());
|
|
1051
|
+
if (!parsedRemote) return;
|
|
1052
|
+
return {
|
|
1053
|
+
...parsedRemote,
|
|
1054
|
+
branch: branchStdout.trim(),
|
|
1055
|
+
treeSha: treeShaStdout.trim()
|
|
1056
|
+
};
|
|
1057
|
+
} catch {
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function inferInitProjectName(cwd, repo) {
|
|
1062
|
+
if (repo?.repo) return repo.repo;
|
|
1063
|
+
return path.basename(cwd);
|
|
1064
|
+
}
|
|
1065
|
+
async function readApiKeyFromStdin() {
|
|
1066
|
+
const chunks = [];
|
|
1067
|
+
for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
1068
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
1069
|
+
}
|
|
1070
|
+
async function promptForApiKey() {
|
|
1071
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("No interactive terminal available. Pass --api-key or pipe the key on stdin.");
|
|
1072
|
+
const maskedOutput = new Writable({ write(chunk, _encoding, callback) {
|
|
1073
|
+
if (!maskedOutput.muted) process.stdout.write(chunk);
|
|
1074
|
+
callback();
|
|
1075
|
+
} });
|
|
1076
|
+
maskedOutput.muted = false;
|
|
1077
|
+
const rl = createInterface({
|
|
1078
|
+
input: process.stdin,
|
|
1079
|
+
output: maskedOutput,
|
|
1080
|
+
terminal: true
|
|
1081
|
+
});
|
|
1082
|
+
try {
|
|
1083
|
+
process.stdout.write("Enter your Stately API key: ");
|
|
1084
|
+
maskedOutput.muted = true;
|
|
1085
|
+
const apiKey = (await rl.question("")).trim();
|
|
1086
|
+
maskedOutput.muted = false;
|
|
1087
|
+
process.stdout.write("\n");
|
|
1088
|
+
return apiKey;
|
|
1089
|
+
} finally {
|
|
1090
|
+
rl.close();
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
function normalizeApiKey(value) {
|
|
1094
|
+
const trimmed = value?.trim();
|
|
1095
|
+
return trimmed ? trimmed : void 0;
|
|
1096
|
+
}
|
|
1097
|
+
async function fileExists(filePath) {
|
|
1098
|
+
try {
|
|
1099
|
+
await fs.access(filePath);
|
|
1100
|
+
return true;
|
|
1101
|
+
} catch {
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
async function initProject(options) {
|
|
1106
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
1107
|
+
const studioUrl = getResolvedStudioUrl(options.baseUrl);
|
|
1108
|
+
const configPath = path.resolve(cwd, options.configPath ?? STATELY_CONFIG_FILE);
|
|
1109
|
+
const defaultXStateVersion = Math.max(5, options.defaultXStateVersion ?? 5);
|
|
1110
|
+
if (!options.force && await fileExists(configPath)) throw new Error(`${configPath} already exists. Pass --force to overwrite it.`);
|
|
1111
|
+
const client = options.client ?? createStatelyClient({
|
|
1112
|
+
apiKey: options.apiKey,
|
|
1113
|
+
baseUrl: studioUrl
|
|
1114
|
+
});
|
|
1115
|
+
const inferredRepo = options.project?.repo ?? await inferConnectedRepoFromCwd(cwd);
|
|
1116
|
+
const projectInput = {
|
|
1117
|
+
name: options.project?.name ?? inferInitProjectName(cwd, inferredRepo),
|
|
1118
|
+
visibility: options.project?.visibility ?? "Private",
|
|
1119
|
+
...options.project?.description ? { description: options.project.description } : {},
|
|
1120
|
+
...options.project?.keywords ? { keywords: options.project.keywords } : {},
|
|
1121
|
+
...inferredRepo ? { repo: inferredRepo } : {}
|
|
1122
|
+
};
|
|
1123
|
+
const project = await client.projects.ensure(projectInput);
|
|
1124
|
+
const config = createStatelyProjectConfig({
|
|
1125
|
+
projectId: project.projectId,
|
|
1126
|
+
studioUrl,
|
|
1127
|
+
defaultXStateVersion
|
|
1128
|
+
});
|
|
1129
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
1130
|
+
return {
|
|
1131
|
+
config,
|
|
1132
|
+
configPath,
|
|
1133
|
+
project
|
|
1134
|
+
};
|
|
666
1135
|
}
|
|
667
1136
|
const sharedFlags = {
|
|
668
1137
|
help: Flags.help({ char: "h" }),
|
|
@@ -725,10 +1194,11 @@ var PlanCommand = class PlanCommand extends ParsedSyncCommand {
|
|
|
725
1194
|
static args = sharedArgs;
|
|
726
1195
|
async run() {
|
|
727
1196
|
const { args, flags } = await this.parseSync(PlanCommand);
|
|
1197
|
+
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
728
1198
|
const plan = await planSync({
|
|
729
1199
|
source: args.source,
|
|
730
1200
|
target: args.target,
|
|
731
|
-
apiKey
|
|
1201
|
+
apiKey,
|
|
732
1202
|
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
733
1203
|
});
|
|
734
1204
|
this.log(formatPlanSummary(plan));
|
|
@@ -741,10 +1211,11 @@ var DiffCommand = class DiffCommand extends ParsedSyncCommand {
|
|
|
741
1211
|
static args = sharedArgs;
|
|
742
1212
|
async run() {
|
|
743
1213
|
const { args, flags } = await this.parseSync(DiffCommand);
|
|
1214
|
+
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
744
1215
|
const plan = await planSync({
|
|
745
1216
|
source: args.source,
|
|
746
1217
|
target: args.target,
|
|
747
|
-
apiKey
|
|
1218
|
+
apiKey,
|
|
748
1219
|
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
749
1220
|
});
|
|
750
1221
|
this.log(formatPlanSummary(plan));
|
|
@@ -757,10 +1228,11 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
|
|
|
757
1228
|
static args = sharedArgs;
|
|
758
1229
|
async run() {
|
|
759
1230
|
const { args, flags } = await this.parseSync(PullCommand);
|
|
1231
|
+
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
760
1232
|
const result = await pullSync({
|
|
761
1233
|
source: args.source,
|
|
762
1234
|
target: args.target,
|
|
763
|
-
apiKey
|
|
1235
|
+
apiKey,
|
|
764
1236
|
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
765
1237
|
});
|
|
766
1238
|
this.log(`Pulled: ${result.source.locator} -> ${result.outputPath}\nTarget: ${result.target.kind} (${result.target.format})`);
|
|
@@ -799,22 +1271,121 @@ var OpenCommand = class OpenCommand extends Command {
|
|
|
799
1271
|
};
|
|
800
1272
|
async run() {
|
|
801
1273
|
const { args, flags } = await this.parse(OpenCommand);
|
|
1274
|
+
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
802
1275
|
await openEditor({
|
|
803
1276
|
fileName: path.resolve(args.file),
|
|
804
1277
|
editorUrl: flags["editor-url"] ?? getDefaultEditorUrl(),
|
|
805
1278
|
host: flags.host,
|
|
806
1279
|
port: flags.port,
|
|
807
1280
|
shouldOpen: flags.open,
|
|
808
|
-
apiKey
|
|
1281
|
+
apiKey,
|
|
809
1282
|
debug: flags.debug
|
|
810
1283
|
});
|
|
811
1284
|
}
|
|
812
1285
|
};
|
|
1286
|
+
var InitCommand = class InitCommand extends Command {
|
|
1287
|
+
static enableJsonFlag = false;
|
|
1288
|
+
static summary = "Create a Stately project and write statelyai.json.";
|
|
1289
|
+
static description = "Creates or reuses a remote Studio project for the current working directory and writes a local statelyai.json configuration file.";
|
|
1290
|
+
static flags = {
|
|
1291
|
+
help: Flags.help({ char: "h" }),
|
|
1292
|
+
"api-key": Flags.string({ description: "Stately API key used to create the remote project" }),
|
|
1293
|
+
"base-url": Flags.string({ description: "Base URL for Stately Studio or a self-hosted deployment" }),
|
|
1294
|
+
name: Flags.string({ description: "Project name to create remotely" }),
|
|
1295
|
+
visibility: Flags.string({
|
|
1296
|
+
description: "Remote project visibility",
|
|
1297
|
+
options: [
|
|
1298
|
+
"Private",
|
|
1299
|
+
"Public",
|
|
1300
|
+
"Unlisted"
|
|
1301
|
+
],
|
|
1302
|
+
default: "Private"
|
|
1303
|
+
}),
|
|
1304
|
+
force: Flags.boolean({
|
|
1305
|
+
description: "Overwrite an existing statelyai.json file",
|
|
1306
|
+
default: false
|
|
1307
|
+
})
|
|
1308
|
+
};
|
|
1309
|
+
async run() {
|
|
1310
|
+
const { flags } = await this.parse(InitCommand);
|
|
1311
|
+
const resolvedApiKey = await resolveApiKey(flags["api-key"]);
|
|
1312
|
+
if (!resolvedApiKey.apiKey) this.error("No API key configured. Use `statelyai login`, set `STATELY_API_KEY`, or pass `--api-key`.");
|
|
1313
|
+
const result = await initProject({
|
|
1314
|
+
apiKey: resolvedApiKey.apiKey,
|
|
1315
|
+
baseUrl: flags["base-url"],
|
|
1316
|
+
force: flags.force,
|
|
1317
|
+
project: {
|
|
1318
|
+
...flags.name ? { name: flags.name } : {},
|
|
1319
|
+
visibility: flags.visibility
|
|
1320
|
+
}
|
|
1321
|
+
});
|
|
1322
|
+
this.log(`Initialized project ${result.project.projectId} and wrote ${result.configPath}.`);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
var LoginCommand = class LoginCommand extends Command {
|
|
1326
|
+
static enableJsonFlag = false;
|
|
1327
|
+
static summary = "Store a Stately API key for future CLI use.";
|
|
1328
|
+
static description = "Stores a Stately API key in the OS credential store when available, with a private file fallback.";
|
|
1329
|
+
static flags = {
|
|
1330
|
+
help: Flags.help({ char: "h" }),
|
|
1331
|
+
"api-key": Flags.string({ description: "API key to store without an interactive prompt" }),
|
|
1332
|
+
stdin: Flags.boolean({
|
|
1333
|
+
description: "Read the API key from standard input",
|
|
1334
|
+
default: false
|
|
1335
|
+
})
|
|
1336
|
+
};
|
|
1337
|
+
async run() {
|
|
1338
|
+
const { flags } = await this.parse(LoginCommand);
|
|
1339
|
+
if (flags.stdin && flags["api-key"]) this.error("Pass either --api-key or --stdin, not both.");
|
|
1340
|
+
const apiKey = normalizeApiKey(flags["api-key"] ?? (!process.stdin.isTTY || flags.stdin ? await readApiKeyFromStdin() : await promptForApiKey()));
|
|
1341
|
+
if (!apiKey) this.error("API key cannot be empty.");
|
|
1342
|
+
const stored = await setStoredApiKey(apiKey);
|
|
1343
|
+
this.log(`Stored API key in ${describeCredentialBackend(stored.backend, stored.location)}.`);
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
var LogoutCommand = class extends Command {
|
|
1347
|
+
static enableJsonFlag = false;
|
|
1348
|
+
static summary = "Remove any API key stored by the CLI.";
|
|
1349
|
+
static description = "Deletes the locally stored API key. Environment variables are not changed.";
|
|
1350
|
+
static flags = { help: Flags.help({ char: "h" }) };
|
|
1351
|
+
async run() {
|
|
1352
|
+
const result = await deleteStoredApiKey();
|
|
1353
|
+
if (!result.deleted) {
|
|
1354
|
+
this.log("No stored API key found.");
|
|
1355
|
+
return;
|
|
1356
|
+
}
|
|
1357
|
+
this.log(`Removed stored API key from ${result.locations.join(", ")}.`);
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
var AuthStatusCommand = class extends Command {
|
|
1361
|
+
static enableJsonFlag = false;
|
|
1362
|
+
static summary = "Show how the CLI would resolve its API key.";
|
|
1363
|
+
static description = "Reports whether the CLI would use a flag, environment variable, or stored credential.";
|
|
1364
|
+
static flags = { help: Flags.help({ char: "h" }) };
|
|
1365
|
+
async run() {
|
|
1366
|
+
const envApiKey = getEnvApiKey();
|
|
1367
|
+
const storedApiKey = await getStoredApiKey();
|
|
1368
|
+
if (envApiKey) {
|
|
1369
|
+
this.log(`API key source: environment (${envApiKey.variable}).`);
|
|
1370
|
+
if (storedApiKey) this.log(`Stored credential also available in ${describeCredentialBackend(storedApiKey.backend, storedApiKey.location)}.`);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
if (storedApiKey) {
|
|
1374
|
+
this.log(`API key source: stored credential (${describeCredentialBackend(storedApiKey.backend, storedApiKey.location)}).`);
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
this.log("No API key configured. Use `statelyai login`, set `STATELY_API_KEY`, or pass `--api-key`.");
|
|
1378
|
+
}
|
|
1379
|
+
};
|
|
813
1380
|
const COMMANDS = {
|
|
814
1381
|
plan: PlanCommand,
|
|
815
1382
|
diff: DiffCommand,
|
|
816
1383
|
pull: PullCommand,
|
|
817
|
-
open: OpenCommand
|
|
1384
|
+
open: OpenCommand,
|
|
1385
|
+
init: InitCommand,
|
|
1386
|
+
login: LoginCommand,
|
|
1387
|
+
logout: LogoutCommand,
|
|
1388
|
+
"auth:status": AuthStatusCommand
|
|
818
1389
|
};
|
|
819
1390
|
async function run(argv = process.argv.slice(2), entryUrl = import.meta.url) {
|
|
820
1391
|
const normalizedArgv = argv.length === 1 && argv[0] === "-h" ? ["--help"] : argv;
|
|
@@ -838,4 +1409,4 @@ function isDirectExecution() {
|
|
|
838
1409
|
if (isDirectExecution()) run();
|
|
839
1410
|
|
|
840
1411
|
//#endregion
|
|
841
|
-
export { COMMANDS, formatPlanSummary, run };
|
|
1412
|
+
export { COMMANDS, createStatelyProjectConfig, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, resolveApiKey, run };
|