@statelyai/sdk 0.5.0 → 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 +157 -83
- package/dist/api.d.mts +20 -0
- package/dist/api.mjs +56 -0
- package/dist/cli.d.mts +100 -2
- package/dist/cli.mjs +1225 -14
- 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 -126
- package/dist/index.mjs +4 -3
- package/dist/{inspect-WUC2inmJ.d.mts → inspect-DIxB2Tr3.d.mts} +1 -1
- package/dist/inspect.d.mts +2 -2
- package/dist/inspect.mjs +1 -1
- package/dist/patchTypes.d.mts +25 -1
- package/dist/{protocol-B1cNV7QB.d.mts → protocol-CEbWQPYe.d.mts} +74 -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-lomH7b0v.mjs → transport-C0eTgNNu.mjs} +19 -1
- package/package.json +25 -17
- package/schemas/statelyai.schema.json +128 -0
- package/dist/graphToXStateTS-BSUj97r0.mjs +0 -557
- 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,12 +1,965 @@
|
|
|
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";
|
|
5
|
+
import fs from "node:fs/promises";
|
|
6
|
+
import * as path$1 from "node:path";
|
|
3
7
|
import path from "node:path";
|
|
4
|
-
import
|
|
5
|
-
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";
|
|
13
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
14
|
import { Args, Command, Flags, flush, handle, run as run$1 } from "@oclif/core";
|
|
7
|
-
import
|
|
15
|
+
import * as crypto from "node:crypto";
|
|
16
|
+
import * as http from "node:http";
|
|
17
|
+
import * as https from "node:https";
|
|
18
|
+
import os from "node:os";
|
|
8
19
|
|
|
20
|
+
//#region src/cliHost.ts
|
|
21
|
+
const DEFAULT_SYNC_MAX_FILES = 150;
|
|
22
|
+
const DEFAULT_SYNC_MAX_BYTES = 2e6;
|
|
23
|
+
const supportedDocumentPattern = /\.(?:c|m)?(?:jsx?|tsx?)$|\.jsonc?$|\.ya?ml$|\.mmd$|\.mermaid$/i;
|
|
24
|
+
var WebSocketClient = class {
|
|
25
|
+
buffer = Buffer.alloc(0);
|
|
26
|
+
listeners = /* @__PURE__ */ new Set();
|
|
27
|
+
constructor(socket) {
|
|
28
|
+
this.socket = socket;
|
|
29
|
+
socket.on("data", (chunk) => {
|
|
30
|
+
const nextChunk = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
31
|
+
this.buffer = Buffer.concat([this.buffer, nextChunk]);
|
|
32
|
+
this.readFrames();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
onMessage(listener) {
|
|
36
|
+
this.listeners.add(listener);
|
|
37
|
+
}
|
|
38
|
+
send(message) {
|
|
39
|
+
this.socket.write(encodeWebSocketFrame(JSON.stringify(message)));
|
|
40
|
+
}
|
|
41
|
+
close() {
|
|
42
|
+
this.socket.end();
|
|
43
|
+
}
|
|
44
|
+
readFrames() {
|
|
45
|
+
while (this.buffer.length >= 2) {
|
|
46
|
+
const first = this.buffer[0];
|
|
47
|
+
const second = this.buffer[1];
|
|
48
|
+
const opcode = first & 15;
|
|
49
|
+
const masked = (second & 128) !== 0;
|
|
50
|
+
let length = second & 127;
|
|
51
|
+
let offset = 2;
|
|
52
|
+
if (length === 126) {
|
|
53
|
+
if (this.buffer.length < offset + 2) return;
|
|
54
|
+
length = this.buffer.readUInt16BE(offset);
|
|
55
|
+
offset += 2;
|
|
56
|
+
} else if (length === 127) {
|
|
57
|
+
if (this.buffer.length < offset + 8) return;
|
|
58
|
+
const bigLength = this.buffer.readBigUInt64BE(offset);
|
|
59
|
+
if (bigLength > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
60
|
+
this.socket.destroy(/* @__PURE__ */ new Error("WebSocket frame is too large."));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
length = Number(bigLength);
|
|
64
|
+
offset += 8;
|
|
65
|
+
}
|
|
66
|
+
const maskLength = masked ? 4 : 0;
|
|
67
|
+
if (this.buffer.length < offset + maskLength + length) return;
|
|
68
|
+
const mask = masked ? this.buffer.subarray(offset, offset + 4) : void 0;
|
|
69
|
+
offset += maskLength;
|
|
70
|
+
const payload = Buffer.from(this.buffer.subarray(offset, offset + length));
|
|
71
|
+
this.buffer = this.buffer.subarray(offset + length);
|
|
72
|
+
if (mask) for (let index = 0; index < payload.length; index++) payload[index] = payload[index] ^ mask[index % 4];
|
|
73
|
+
if (opcode === 8) {
|
|
74
|
+
this.close();
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (opcode === 9) {
|
|
78
|
+
this.socket.write(encodeWebSocketFrame(payload, 10));
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (opcode !== 1) continue;
|
|
82
|
+
const message = JSON.parse(payload.toString("utf8"));
|
|
83
|
+
for (const listener of this.listeners) listener(message);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var RemoteEditorSession = class {
|
|
88
|
+
currentGraph;
|
|
89
|
+
lastPersistedGraph;
|
|
90
|
+
currentSourceLocations;
|
|
91
|
+
currentFormat;
|
|
92
|
+
ready = false;
|
|
93
|
+
pendingMessages = [];
|
|
94
|
+
pendingRetrievals = /* @__PURE__ */ new Map();
|
|
95
|
+
retrieveRequestCount = 0;
|
|
96
|
+
watchedSourceUris = /* @__PURE__ */ new Set();
|
|
97
|
+
selfEditCount = 0;
|
|
98
|
+
watcher;
|
|
99
|
+
constructor(options, sendMessage) {
|
|
100
|
+
this.options = options;
|
|
101
|
+
this.sendMessage = sendMessage;
|
|
102
|
+
this.watcher = watchWorkspace(this.options.rootDir, (uri) => {
|
|
103
|
+
this.handleSavedDocument(uri);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
dispose() {
|
|
107
|
+
for (const pending of this.pendingRetrievals.values()) pending.reject(/* @__PURE__ */ new Error("Editor session disposed before export completed."));
|
|
108
|
+
this.pendingRetrievals.clear();
|
|
109
|
+
this.watcher?.dispose();
|
|
110
|
+
}
|
|
111
|
+
async receiveMessage(msg) {
|
|
112
|
+
this.log("recv", msg.type, msg);
|
|
113
|
+
switch (msg.type) {
|
|
114
|
+
case "@statelyai.ready":
|
|
115
|
+
await this.initializeEditor();
|
|
116
|
+
break;
|
|
117
|
+
case "@statelyai.loaded":
|
|
118
|
+
this.currentGraph = msg.graph;
|
|
119
|
+
this.lastPersistedGraph = msg.graph;
|
|
120
|
+
this.currentSourceLocations = msg.sourceLocations ?? this.currentSourceLocations;
|
|
121
|
+
this.updateSourceLocations(this.currentSourceLocations);
|
|
122
|
+
break;
|
|
123
|
+
case "@statelyai.change":
|
|
124
|
+
this.currentGraph = msg.graph;
|
|
125
|
+
this.currentSourceLocations = msg.sourceLocations ?? this.currentSourceLocations;
|
|
126
|
+
this.updateSourceLocations(this.currentSourceLocations);
|
|
127
|
+
break;
|
|
128
|
+
case "@statelyai.save":
|
|
129
|
+
await this.saveFromEditor(msg);
|
|
130
|
+
break;
|
|
131
|
+
case "@statelyai.retrieved":
|
|
132
|
+
this.resolveRetrievedSource(msg);
|
|
133
|
+
break;
|
|
134
|
+
case "@statelyai.clipboardRead":
|
|
135
|
+
this.postMessage({
|
|
136
|
+
type: "@statelyai.clipboardReadResponse",
|
|
137
|
+
requestId: msg.requestId,
|
|
138
|
+
text: ""
|
|
139
|
+
});
|
|
140
|
+
break;
|
|
141
|
+
case "@statelyai.error":
|
|
142
|
+
this.handleErrorMessage(msg);
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
async initializeEditor() {
|
|
147
|
+
this.ready = true;
|
|
148
|
+
try {
|
|
149
|
+
const update = await this.parseRootDocument();
|
|
150
|
+
this.currentFormat = update.format;
|
|
151
|
+
this.currentSourceLocations = update.sourceLocations;
|
|
152
|
+
this.updateSourceLocations(update.sourceLocations);
|
|
153
|
+
this.postMessage({
|
|
154
|
+
type: "@statelyai.init",
|
|
155
|
+
machine: update.machine,
|
|
156
|
+
format: update.format,
|
|
157
|
+
sourceLocations: update.sourceLocations,
|
|
158
|
+
mode: "editing",
|
|
159
|
+
theme: "light",
|
|
160
|
+
unsavedIndicator: { enabled: true },
|
|
161
|
+
leftPanels: [],
|
|
162
|
+
rightPanels: [],
|
|
163
|
+
activePanels: []
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
this.showError(formatError(error, "Failed to initialize the visual editor."));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
for (const pending of this.pendingMessages) this.sendMessage(pending);
|
|
170
|
+
this.pendingMessages = [];
|
|
171
|
+
}
|
|
172
|
+
async handleSavedDocument(uri) {
|
|
173
|
+
if (uri !== this.options.rootUri && !this.watchedSourceUris.has(uri)) return;
|
|
174
|
+
if (this.selfEditCount > 0) return;
|
|
175
|
+
try {
|
|
176
|
+
const update = await this.parseRootDocument();
|
|
177
|
+
this.currentFormat = update.format;
|
|
178
|
+
this.currentSourceLocations = update.sourceLocations;
|
|
179
|
+
this.updateSourceLocations(update.sourceLocations);
|
|
180
|
+
this.postMessage({
|
|
181
|
+
type: "@statelyai.update",
|
|
182
|
+
machine: update.machine,
|
|
183
|
+
format: update.format,
|
|
184
|
+
sourceLocations: update.sourceLocations
|
|
185
|
+
});
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.showError(formatError(error, "Failed to read the saved document."));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async saveFromEditor(msg) {
|
|
191
|
+
const validationErrors = getValidationErrors(msg.validations);
|
|
192
|
+
if (validationErrors.length > 0) {
|
|
193
|
+
const message = formatValidationErrorMessage(validationErrors);
|
|
194
|
+
this.postMessage({
|
|
195
|
+
type: "@statelyai.toast",
|
|
196
|
+
message,
|
|
197
|
+
toastType: "error"
|
|
198
|
+
});
|
|
199
|
+
this.showError(message);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this.currentSourceLocations = msg.sourceLocations ?? this.currentSourceLocations;
|
|
203
|
+
this.updateSourceLocations(this.currentSourceLocations);
|
|
204
|
+
try {
|
|
205
|
+
const serialized = await this.getSerializedPayloadForCurrentFormat();
|
|
206
|
+
const documents = await collectSourceDocuments({
|
|
207
|
+
rootFileName: this.options.fileName,
|
|
208
|
+
rootDir: this.options.rootDir,
|
|
209
|
+
extraUris: sourceLocationUris(this.currentSourceLocations)
|
|
210
|
+
});
|
|
211
|
+
const response = await this.postSync("/api/editor-sync/apply", {
|
|
212
|
+
rootUri: this.options.rootUri,
|
|
213
|
+
rootFileName: this.options.fileName,
|
|
214
|
+
rootDir: this.options.rootDir,
|
|
215
|
+
documents,
|
|
216
|
+
machineConfig: msg.machineConfig,
|
|
217
|
+
patches: msg.patches,
|
|
218
|
+
prevGraph: this.lastPersistedGraph ?? this.currentGraph,
|
|
219
|
+
nextGraph: msg.graph,
|
|
220
|
+
sourceLocations: this.currentSourceLocations,
|
|
221
|
+
serialized
|
|
222
|
+
});
|
|
223
|
+
if (response.replacements.length === 0) {
|
|
224
|
+
const message = "Unable to apply Viz changes to the source file automatically.";
|
|
225
|
+
this.postMessage({
|
|
226
|
+
type: "@statelyai.toast",
|
|
227
|
+
message,
|
|
228
|
+
toastType: "error"
|
|
229
|
+
});
|
|
230
|
+
this.showError(message);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
await this.runWithSelfEdit(async () => {
|
|
234
|
+
await applyFileReplacements(response.replacements, this.options.rootUri);
|
|
235
|
+
});
|
|
236
|
+
this.currentGraph = msg.graph;
|
|
237
|
+
this.lastPersistedGraph = msg.graph;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const message = formatError(error, "Failed to apply Viz changes.");
|
|
240
|
+
this.postMessage({
|
|
241
|
+
type: "@statelyai.toast",
|
|
242
|
+
message,
|
|
243
|
+
toastType: "error"
|
|
244
|
+
});
|
|
245
|
+
this.showError(message);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
async parseRootDocument() {
|
|
249
|
+
const documents = await collectSourceDocuments({
|
|
250
|
+
rootFileName: this.options.fileName,
|
|
251
|
+
rootDir: this.options.rootDir,
|
|
252
|
+
extraUris: sourceLocationUris(this.currentSourceLocations)
|
|
253
|
+
});
|
|
254
|
+
return this.postSync("/api/editor-sync/parse", {
|
|
255
|
+
rootUri: this.options.rootUri,
|
|
256
|
+
rootFileName: this.options.fileName,
|
|
257
|
+
rootDir: this.options.rootDir,
|
|
258
|
+
documents
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
async postSync(pathname, body) {
|
|
262
|
+
const url = new URL(pathname, normalizedBaseUrl(this.options.editorUrl));
|
|
263
|
+
const headers = { "Content-Type": "application/json" };
|
|
264
|
+
if (this.options.apiKey) headers.Authorization = `Bearer ${this.options.apiKey}`;
|
|
265
|
+
const response = await fetchEditorHost(url, {
|
|
266
|
+
method: "POST",
|
|
267
|
+
headers,
|
|
268
|
+
body: JSON.stringify(body)
|
|
269
|
+
});
|
|
270
|
+
const data = await response.json().catch(() => null);
|
|
271
|
+
if (!response.ok) {
|
|
272
|
+
const message = data && typeof data === "object" && "error" in data && typeof data.error === "string" ? data.error : `HTTP ${response.status}`;
|
|
273
|
+
throw new Error(message);
|
|
274
|
+
}
|
|
275
|
+
return data;
|
|
276
|
+
}
|
|
277
|
+
async getSerializedPayloadForCurrentFormat() {
|
|
278
|
+
const format = this.currentFormat;
|
|
279
|
+
if (!format || format === "xstate") return;
|
|
280
|
+
try {
|
|
281
|
+
const serialized = await this.requestSerialized(format);
|
|
282
|
+
return { [format]: serialized };
|
|
283
|
+
} catch {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
requestSerialized(format) {
|
|
288
|
+
const requestId = `retrieve-${++this.retrieveRequestCount}`;
|
|
289
|
+
return new Promise((resolve, reject) => {
|
|
290
|
+
this.pendingRetrievals.set(requestId, {
|
|
291
|
+
resolve,
|
|
292
|
+
reject
|
|
293
|
+
});
|
|
294
|
+
this.postMessage({
|
|
295
|
+
type: "@statelyai.retrieve",
|
|
296
|
+
requestId,
|
|
297
|
+
format
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
resolveRetrievedSource(msg) {
|
|
302
|
+
const pending = this.pendingRetrievals.get(msg.requestId);
|
|
303
|
+
if (!pending) return;
|
|
304
|
+
this.pendingRetrievals.delete(msg.requestId);
|
|
305
|
+
const serialized = typeof msg.data === "string" ? msg.data : JSON.stringify(msg.data, null, 2);
|
|
306
|
+
pending.resolve(serialized);
|
|
307
|
+
}
|
|
308
|
+
handleErrorMessage(msg) {
|
|
309
|
+
if (msg.requestId) {
|
|
310
|
+
const pending = this.pendingRetrievals.get(msg.requestId);
|
|
311
|
+
if (pending) {
|
|
312
|
+
this.pendingRetrievals.delete(msg.requestId);
|
|
313
|
+
pending.reject(new Error(msg.message));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
this.showError(msg.message);
|
|
318
|
+
}
|
|
319
|
+
updateSourceLocations(sourceLocations) {
|
|
320
|
+
this.watchedSourceUris.clear();
|
|
321
|
+
for (const location of sourceLocations?.states ?? []) if (location.uri !== this.options.rootUri) this.watchedSourceUris.add(location.uri);
|
|
322
|
+
}
|
|
323
|
+
async runWithSelfEdit(operation) {
|
|
324
|
+
this.selfEditCount++;
|
|
325
|
+
try {
|
|
326
|
+
return await operation();
|
|
327
|
+
} finally {
|
|
328
|
+
this.selfEditCount--;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
postMessage(msg) {
|
|
332
|
+
this.log("send", msg.type, msg);
|
|
333
|
+
if (!this.ready) {
|
|
334
|
+
this.pendingMessages.push(msg);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.sendMessage(msg);
|
|
338
|
+
}
|
|
339
|
+
showError(message) {
|
|
340
|
+
console.error(`Stately Editor: ${message}`);
|
|
341
|
+
}
|
|
342
|
+
log(direction, type, payload) {
|
|
343
|
+
if (this.options.debug) console.error(`[${direction}] ${type}`, payload ?? "");
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
async function openEditor(options) {
|
|
347
|
+
const fileName = path$1.resolve(options.fileName);
|
|
348
|
+
await fs.access(fileName);
|
|
349
|
+
await assertEditorHostAvailable(options.editorUrl);
|
|
350
|
+
const rootUri = fileNameToUri(fileName);
|
|
351
|
+
const rootDir = path$1.dirname(fileName);
|
|
352
|
+
let activeClient;
|
|
353
|
+
let session;
|
|
354
|
+
const server = http.createServer((request, response) => {
|
|
355
|
+
if (new URL(request.url ?? "/", `http://${request.headers.host}`).pathname !== "/") {
|
|
356
|
+
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
357
|
+
response.end("Not found");
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
361
|
+
response.end(getCliWebviewContent({
|
|
362
|
+
editorUrl: options.editorUrl,
|
|
363
|
+
apiKey: options.apiKey
|
|
364
|
+
}));
|
|
365
|
+
});
|
|
366
|
+
server.on("upgrade", (request, socket) => {
|
|
367
|
+
if (request.url !== "/ws") {
|
|
368
|
+
socket.end("HTTP/1.1 404 Not Found\r\n\r\n");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
const key = request.headers["sec-websocket-key"];
|
|
372
|
+
if (typeof key !== "string") {
|
|
373
|
+
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const accept = crypto.createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
|
|
377
|
+
socket.write([
|
|
378
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
379
|
+
"Upgrade: websocket",
|
|
380
|
+
"Connection: Upgrade",
|
|
381
|
+
`Sec-WebSocket-Accept: ${accept}`,
|
|
382
|
+
"\r\n"
|
|
383
|
+
].join("\r\n"));
|
|
384
|
+
activeClient?.close();
|
|
385
|
+
session?.dispose();
|
|
386
|
+
activeClient = new WebSocketClient(socket);
|
|
387
|
+
session = new RemoteEditorSession({
|
|
388
|
+
...options,
|
|
389
|
+
fileName,
|
|
390
|
+
rootUri,
|
|
391
|
+
rootDir
|
|
392
|
+
}, (message) => activeClient?.send(message));
|
|
393
|
+
activeClient.onMessage((message) => {
|
|
394
|
+
session?.receiveMessage(message);
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
await listen(server, options.port, options.host);
|
|
398
|
+
const address = server.address();
|
|
399
|
+
const port = typeof address === "object" && address ? address.port : options.port;
|
|
400
|
+
const editorPageUrl = `http://${options.host}:${port}`;
|
|
401
|
+
console.log(`Stately visual editor: ${editorPageUrl}`);
|
|
402
|
+
console.log(`Editing ${fileName}`);
|
|
403
|
+
if (options.shouldOpen) openBrowser(editorPageUrl);
|
|
404
|
+
const shutdown = () => {
|
|
405
|
+
session?.dispose();
|
|
406
|
+
activeClient?.close();
|
|
407
|
+
server.close();
|
|
408
|
+
};
|
|
409
|
+
process.once("SIGINT", () => {
|
|
410
|
+
shutdown();
|
|
411
|
+
process.exit(0);
|
|
412
|
+
});
|
|
413
|
+
process.once("SIGTERM", () => {
|
|
414
|
+
shutdown();
|
|
415
|
+
process.exit(0);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
async function collectSourceDocuments(options) {
|
|
419
|
+
const files = new Set([options.rootFileName]);
|
|
420
|
+
const maxFiles = Number.parseInt(process.env.STATELY_SYNC_MAX_FILES ?? "", 10) || DEFAULT_SYNC_MAX_FILES;
|
|
421
|
+
const maxBytes = Number.parseInt(process.env.STATELY_SYNC_MAX_BYTES ?? "", 10) || DEFAULT_SYNC_MAX_BYTES;
|
|
422
|
+
for (const uri of options.extraUris ?? []) files.add(sourceUriToFileName(uri));
|
|
423
|
+
await collectDirectoryFiles(options.rootDir, files, maxFiles);
|
|
424
|
+
const documents = [];
|
|
425
|
+
let totalBytes = 0;
|
|
426
|
+
for (const fileName of files) {
|
|
427
|
+
if (!isSupportedDocument(fileName)) continue;
|
|
428
|
+
const text = await fs.readFile(fileName, "utf8").catch(() => null);
|
|
429
|
+
if (text === null) continue;
|
|
430
|
+
totalBytes += Buffer.byteLength(text);
|
|
431
|
+
if (totalBytes > maxBytes) throw new Error(`Source payload exceeds ${maxBytes} bytes.`);
|
|
432
|
+
documents.push({
|
|
433
|
+
uri: fileNameToUri(fileName),
|
|
434
|
+
fileName,
|
|
435
|
+
text
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return documents;
|
|
439
|
+
}
|
|
440
|
+
async function collectDirectoryFiles(rootDir, files, maxFiles) {
|
|
441
|
+
const queue = [rootDir];
|
|
442
|
+
while (queue.length > 0 && files.size < maxFiles) {
|
|
443
|
+
const dir = queue.shift();
|
|
444
|
+
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
445
|
+
for (const entry of entries) {
|
|
446
|
+
if (files.size >= maxFiles) break;
|
|
447
|
+
if (entry.name.startsWith(".") || shouldSkipDirectory(entry.name)) continue;
|
|
448
|
+
const fileName = path$1.join(dir, entry.name);
|
|
449
|
+
if (entry.isDirectory()) queue.push(fileName);
|
|
450
|
+
else if (entry.isFile() && isSupportedDocument(fileName)) files.add(fileName);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function shouldSkipDirectory(name) {
|
|
455
|
+
return [
|
|
456
|
+
"node_modules",
|
|
457
|
+
"dist",
|
|
458
|
+
"out",
|
|
459
|
+
"build",
|
|
460
|
+
"coverage",
|
|
461
|
+
".git"
|
|
462
|
+
].includes(name);
|
|
463
|
+
}
|
|
464
|
+
function isSupportedDocument(fileName) {
|
|
465
|
+
return supportedDocumentPattern.test(fileName);
|
|
466
|
+
}
|
|
467
|
+
async function applyFileReplacements(replacements, defaultUri) {
|
|
468
|
+
const replacementsByUri = /* @__PURE__ */ new Map();
|
|
469
|
+
for (const replacement of replacements) {
|
|
470
|
+
const uri = replacement.uri ?? defaultUri;
|
|
471
|
+
const existing = replacementsByUri.get(uri) ?? [];
|
|
472
|
+
existing.push(replacement);
|
|
473
|
+
replacementsByUri.set(uri, existing);
|
|
474
|
+
}
|
|
475
|
+
for (const [uri, uriReplacements] of replacementsByUri) {
|
|
476
|
+
const fileName = sourceUriToFileName(uri);
|
|
477
|
+
const nextText = applyReplacementsToText(await fs.readFile(fileName, "utf8"), uriReplacements);
|
|
478
|
+
await fs.writeFile(fileName, nextText, "utf8");
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function applyReplacementsToText(text, replacements) {
|
|
482
|
+
let updated = text;
|
|
483
|
+
const sorted = [...replacements].sort(compareReplacementRangesDescending);
|
|
484
|
+
for (const replacement of sorted) {
|
|
485
|
+
const lineStarts = getLineStarts(updated);
|
|
486
|
+
const start = offsetAt(lineStarts, replacement.range.startLine, replacement.range.startChar);
|
|
487
|
+
const end = offsetAt(lineStarts, replacement.range.endLine, replacement.range.endChar);
|
|
488
|
+
updated = `${updated.slice(0, start)}${replacement.newText}${updated.slice(end)}`;
|
|
489
|
+
}
|
|
490
|
+
return updated;
|
|
491
|
+
}
|
|
492
|
+
function watchWorkspace(rootDir, listener) {
|
|
493
|
+
let watcher;
|
|
494
|
+
const timers = /* @__PURE__ */ new Map();
|
|
495
|
+
const queue = (fileName) => {
|
|
496
|
+
const existing = timers.get(fileName);
|
|
497
|
+
if (existing) clearTimeout(existing);
|
|
498
|
+
timers.set(fileName, setTimeout(() => {
|
|
499
|
+
timers.delete(fileName);
|
|
500
|
+
listener(fileNameToUri(fileName));
|
|
501
|
+
}, 75));
|
|
502
|
+
};
|
|
503
|
+
try {
|
|
504
|
+
watcher = watch(rootDir, { recursive: true }, (_event, fileName) => {
|
|
505
|
+
if (fileName) queue(path$1.join(rootDir, fileName.toString()));
|
|
506
|
+
});
|
|
507
|
+
} catch {
|
|
508
|
+
watcher = watch(rootDir, (_event, fileName) => {
|
|
509
|
+
if (fileName) queue(path$1.join(rootDir, fileName.toString()));
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return { dispose() {
|
|
513
|
+
watcher?.close();
|
|
514
|
+
for (const timer of timers.values()) clearTimeout(timer);
|
|
515
|
+
timers.clear();
|
|
516
|
+
} };
|
|
517
|
+
}
|
|
518
|
+
function getCliWebviewContent(options) {
|
|
519
|
+
const baseUrl = normalizedBaseUrl(options.editorUrl);
|
|
520
|
+
const url = new URL(`${baseUrl}/embed`);
|
|
521
|
+
if (options.apiKey) url.searchParams.set("api_key", options.apiKey);
|
|
522
|
+
return `<!DOCTYPE html>
|
|
523
|
+
<html lang="en" style="height:100%;margin:0">
|
|
524
|
+
<head>
|
|
525
|
+
<meta charset="UTF-8">
|
|
526
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
527
|
+
<style>
|
|
528
|
+
body { margin: 0; padding: 0; height: 100vh; overflow: hidden; }
|
|
529
|
+
iframe { border: 0; width: 100%; height: 100%; display: block; }
|
|
530
|
+
</style>
|
|
531
|
+
</head>
|
|
532
|
+
<body>
|
|
533
|
+
<iframe
|
|
534
|
+
id="stately-editor"
|
|
535
|
+
src="${escapeAttribute(url.toString())}"
|
|
536
|
+
allow="clipboard-read; clipboard-write"
|
|
537
|
+
></iframe>
|
|
538
|
+
<script>
|
|
539
|
+
const iframe = document.getElementById('stately-editor');
|
|
540
|
+
const socket = new WebSocket(
|
|
541
|
+
(location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/ws'
|
|
542
|
+
);
|
|
543
|
+
const pending = [];
|
|
544
|
+
|
|
545
|
+
function sendToCli(message) {
|
|
546
|
+
if (socket.readyState === WebSocket.OPEN) {
|
|
547
|
+
socket.send(JSON.stringify(message));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
pending.push(message);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
socket.addEventListener('open', () => {
|
|
554
|
+
while (pending.length > 0) {
|
|
555
|
+
socket.send(JSON.stringify(pending.shift()));
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
socket.addEventListener('message', (event) => {
|
|
560
|
+
iframe.contentWindow?.postMessage(JSON.parse(event.data), '*');
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
window.addEventListener('message', (event) => {
|
|
564
|
+
if (!event.data?.type?.startsWith?.('@statelyai.')) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
sendToCli(event.data);
|
|
568
|
+
});
|
|
569
|
+
<\/script>
|
|
570
|
+
</body>
|
|
571
|
+
</html>`;
|
|
572
|
+
}
|
|
573
|
+
function encodeWebSocketFrame(payload, opcode = 1) {
|
|
574
|
+
const data = typeof payload === "string" ? Buffer.from(payload) : payload;
|
|
575
|
+
const headerLength = data.length < 126 ? 2 : data.length <= 65535 ? 4 : 10;
|
|
576
|
+
const frame = Buffer.alloc(headerLength + data.length);
|
|
577
|
+
frame[0] = 128 | opcode;
|
|
578
|
+
if (data.length < 126) {
|
|
579
|
+
frame[1] = data.length;
|
|
580
|
+
data.copy(frame, 2);
|
|
581
|
+
} else if (data.length <= 65535) {
|
|
582
|
+
frame[1] = 126;
|
|
583
|
+
frame.writeUInt16BE(data.length, 2);
|
|
584
|
+
data.copy(frame, 4);
|
|
585
|
+
} else {
|
|
586
|
+
frame[1] = 127;
|
|
587
|
+
frame.writeBigUInt64BE(BigInt(data.length), 2);
|
|
588
|
+
data.copy(frame, 10);
|
|
589
|
+
}
|
|
590
|
+
return frame;
|
|
591
|
+
}
|
|
592
|
+
function listen(server, port, host) {
|
|
593
|
+
return new Promise((resolve) => {
|
|
594
|
+
server.listen(port, host, resolve);
|
|
595
|
+
});
|
|
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
|
+
}
|
|
650
|
+
function openBrowser(url) {
|
|
651
|
+
spawn(process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open", process.platform === "win32" ? [
|
|
652
|
+
"/c",
|
|
653
|
+
"start",
|
|
654
|
+
"",
|
|
655
|
+
url
|
|
656
|
+
] : [url], {
|
|
657
|
+
detached: true,
|
|
658
|
+
stdio: "ignore"
|
|
659
|
+
}).unref();
|
|
660
|
+
}
|
|
661
|
+
function getValidationErrors(validations) {
|
|
662
|
+
return (validations ?? []).filter((validation) => validation.level === "error");
|
|
663
|
+
}
|
|
664
|
+
function formatValidationErrorMessage(errors) {
|
|
665
|
+
const [firstError] = errors;
|
|
666
|
+
if (!firstError) return "Cannot save Viz changes because the graph has validation errors.";
|
|
667
|
+
if (errors.length === 1) return `Cannot save Viz changes: ${firstError.message}`;
|
|
668
|
+
return `Cannot save Viz changes: ${errors.length} validation errors. First: ${firstError.message}`;
|
|
669
|
+
}
|
|
670
|
+
function sourceLocationUris(sourceLocations) {
|
|
671
|
+
return [
|
|
672
|
+
...sourceLocations?.root ? [sourceLocations.root.uri] : [],
|
|
673
|
+
...(sourceLocations?.states ?? []).map((state) => state.uri),
|
|
674
|
+
...(sourceLocations?.staticValues ?? []).map((value) => value.uri)
|
|
675
|
+
];
|
|
676
|
+
}
|
|
677
|
+
function fileNameToUri(fileName) {
|
|
678
|
+
return pathToFileURL(path$1.resolve(fileName)).toString();
|
|
679
|
+
}
|
|
680
|
+
function sourceUriToFileName(uriOrPath) {
|
|
681
|
+
if (uriOrPath.startsWith("file://")) return fileURLToPath(uriOrPath);
|
|
682
|
+
if (/^[a-z][a-z\d+.-]*:/i.test(uriOrPath)) throw new Error(`Unsupported non-file URI: ${uriOrPath}`);
|
|
683
|
+
return path$1.resolve(uriOrPath);
|
|
684
|
+
}
|
|
685
|
+
function getLineStarts(text) {
|
|
686
|
+
const lineStarts = [0];
|
|
687
|
+
for (let index = 0; index < text.length; index += 1) if (text.charCodeAt(index) === 10) lineStarts.push(index + 1);
|
|
688
|
+
return lineStarts;
|
|
689
|
+
}
|
|
690
|
+
function offsetAt(lineStarts, line, character) {
|
|
691
|
+
const lineStart = lineStarts[line];
|
|
692
|
+
if (lineStart === void 0) return lineStarts[lineStarts.length - 1] + character;
|
|
693
|
+
return lineStart + character;
|
|
694
|
+
}
|
|
695
|
+
function compareReplacementRangesDescending(left, right) {
|
|
696
|
+
if (left.range.startLine !== right.range.startLine) return right.range.startLine - left.range.startLine;
|
|
697
|
+
return right.range.startChar - left.range.startChar;
|
|
698
|
+
}
|
|
699
|
+
function normalizedBaseUrl(value) {
|
|
700
|
+
return value.replace(/\/+$/, "");
|
|
701
|
+
}
|
|
702
|
+
function formatError(error, 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;
|
|
720
|
+
}
|
|
721
|
+
function escapeAttribute(value) {
|
|
722
|
+
return value.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
723
|
+
}
|
|
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
|
+
|
|
933
|
+
//#endregion
|
|
9
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
|
+
}
|
|
10
963
|
function loadLocalEnv() {
|
|
11
964
|
if (typeof process.loadEnvFile !== "function") return;
|
|
12
965
|
const cwdEnvPath = path.join(process.cwd(), ".env.local");
|
|
@@ -15,14 +968,170 @@ function loadLocalEnv() {
|
|
|
15
968
|
} catch {}
|
|
16
969
|
}
|
|
17
970
|
loadLocalEnv();
|
|
18
|
-
function
|
|
19
|
-
|
|
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" };
|
|
20
1002
|
}
|
|
21
1003
|
function getDefaultBaseUrl() {
|
|
22
1004
|
return process.env.STATELY_API_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? process.env.NEXT_PUBLIC_STATELY_API_URL;
|
|
23
1005
|
}
|
|
24
1006
|
function getDefaultEditorUrl() {
|
|
25
|
-
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
|
+
};
|
|
26
1135
|
}
|
|
27
1136
|
const sharedFlags = {
|
|
28
1137
|
help: Flags.help({ char: "h" }),
|
|
@@ -85,10 +1194,11 @@ var PlanCommand = class PlanCommand extends ParsedSyncCommand {
|
|
|
85
1194
|
static args = sharedArgs;
|
|
86
1195
|
async run() {
|
|
87
1196
|
const { args, flags } = await this.parseSync(PlanCommand);
|
|
1197
|
+
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
88
1198
|
const plan = await planSync({
|
|
89
1199
|
source: args.source,
|
|
90
1200
|
target: args.target,
|
|
91
|
-
apiKey
|
|
1201
|
+
apiKey,
|
|
92
1202
|
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
93
1203
|
});
|
|
94
1204
|
this.log(formatPlanSummary(plan));
|
|
@@ -101,10 +1211,11 @@ var DiffCommand = class DiffCommand extends ParsedSyncCommand {
|
|
|
101
1211
|
static args = sharedArgs;
|
|
102
1212
|
async run() {
|
|
103
1213
|
const { args, flags } = await this.parseSync(DiffCommand);
|
|
1214
|
+
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
104
1215
|
const plan = await planSync({
|
|
105
1216
|
source: args.source,
|
|
106
1217
|
target: args.target,
|
|
107
|
-
apiKey
|
|
1218
|
+
apiKey,
|
|
108
1219
|
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
109
1220
|
});
|
|
110
1221
|
this.log(formatPlanSummary(plan));
|
|
@@ -117,10 +1228,11 @@ var PullCommand = class PullCommand extends ParsedSyncCommand {
|
|
|
117
1228
|
static args = sharedArgs;
|
|
118
1229
|
async run() {
|
|
119
1230
|
const { args, flags } = await this.parseSync(PullCommand);
|
|
1231
|
+
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
120
1232
|
const result = await pullSync({
|
|
121
1233
|
source: args.source,
|
|
122
1234
|
target: args.target,
|
|
123
|
-
apiKey
|
|
1235
|
+
apiKey,
|
|
124
1236
|
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
125
1237
|
});
|
|
126
1238
|
this.log(`Pulled: ${result.source.locator} -> ${result.outputPath}\nTarget: ${result.target.kind} (${result.target.format})`);
|
|
@@ -136,7 +1248,7 @@ var OpenCommand = class OpenCommand extends Command {
|
|
|
136
1248
|
}) };
|
|
137
1249
|
static flags = {
|
|
138
1250
|
help: Flags.help({ char: "h" }),
|
|
139
|
-
"api-key": Flags.string({ description: "Stately API key used
|
|
1251
|
+
"api-key": Flags.string({ description: "Stately API key used when the editor server requires auth" }),
|
|
140
1252
|
"editor-url": Flags.string({ description: "Base URL for the Stately editor embed" }),
|
|
141
1253
|
host: Flags.string({
|
|
142
1254
|
description: "Local server host",
|
|
@@ -159,22 +1271,121 @@ var OpenCommand = class OpenCommand extends Command {
|
|
|
159
1271
|
};
|
|
160
1272
|
async run() {
|
|
161
1273
|
const { args, flags } = await this.parse(OpenCommand);
|
|
1274
|
+
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
162
1275
|
await openEditor({
|
|
163
1276
|
fileName: path.resolve(args.file),
|
|
164
1277
|
editorUrl: flags["editor-url"] ?? getDefaultEditorUrl(),
|
|
165
1278
|
host: flags.host,
|
|
166
1279
|
port: flags.port,
|
|
167
1280
|
shouldOpen: flags.open,
|
|
168
|
-
apiKey
|
|
1281
|
+
apiKey,
|
|
169
1282
|
debug: flags.debug
|
|
170
1283
|
});
|
|
171
1284
|
}
|
|
172
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
|
+
};
|
|
173
1380
|
const COMMANDS = {
|
|
174
1381
|
plan: PlanCommand,
|
|
175
1382
|
diff: DiffCommand,
|
|
176
1383
|
pull: PullCommand,
|
|
177
|
-
open: OpenCommand
|
|
1384
|
+
open: OpenCommand,
|
|
1385
|
+
init: InitCommand,
|
|
1386
|
+
login: LoginCommand,
|
|
1387
|
+
logout: LogoutCommand,
|
|
1388
|
+
"auth:status": AuthStatusCommand
|
|
178
1389
|
};
|
|
179
1390
|
async function run(argv = process.argv.slice(2), entryUrl = import.meta.url) {
|
|
180
1391
|
const normalizedArgv = argv.length === 1 && argv[0] === "-h" ? ["--help"] : argv;
|
|
@@ -190,7 +1401,7 @@ function isDirectExecution() {
|
|
|
190
1401
|
if (!entryPath) return false;
|
|
191
1402
|
const modulePath = fileURLToPath(import.meta.url);
|
|
192
1403
|
try {
|
|
193
|
-
return fs.realpathSync(entryPath) === fs.realpathSync(modulePath);
|
|
1404
|
+
return fs$1.realpathSync(entryPath) === fs$1.realpathSync(modulePath);
|
|
194
1405
|
} catch {
|
|
195
1406
|
return path.resolve(entryPath) === modulePath;
|
|
196
1407
|
}
|
|
@@ -198,4 +1409,4 @@ function isDirectExecution() {
|
|
|
198
1409
|
if (isDirectExecution()) run();
|
|
199
1410
|
|
|
200
1411
|
//#endregion
|
|
201
|
-
export { COMMANDS, formatPlanSummary, run };
|
|
1412
|
+
export { COMMANDS, createStatelyProjectConfig, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, resolveApiKey, run };
|