@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/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 fs$1, { watch } from "node:fs";
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 fetch(url, {
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
- return error instanceof Error ? error.message : fallback;
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("&", "&amp;").replaceAll("\"", "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
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 getDefaultApiKey() {
659
- return process.env.STATELY_API_KEY ?? process.env.NEXT_PUBLIC_STATELY_API_KEY;
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 ?? "http://localhost:3001";
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: flags["api-key"] ?? getDefaultApiKey(),
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: flags["api-key"] ?? getDefaultApiKey(),
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: flags["api-key"] ?? getDefaultApiKey(),
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: flags["api-key"] ?? getDefaultApiKey(),
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 };