@statelyai/sdk 0.7.1 → 0.9.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 +12 -637
- package/dist/assetStorage.d.mts +1 -1
- package/dist/embed.d.mts +1 -1
- package/dist/embed.mjs +1 -1
- package/dist/graph.d.mts +1 -1
- package/dist/{graphToXStateTS-Gzh0ZqbN.mjs → graphToXStateTS-moihsH_U.mjs} +17 -16
- package/dist/index.d.mts +144 -5
- package/dist/index.mjs +2 -2
- package/dist/{inspect-YoEwfiKb.d.mts → inspect-BLlM3qKf.d.mts} +1 -1
- package/dist/inspect.d.mts +2 -2
- package/dist/inspect.mjs +1 -1
- package/dist/{protocol-s9zwsiCW.d.mts → protocol.d.mts} +2 -1
- package/dist/protocol.mjs +5 -0
- package/dist/sync.d.mts +1 -1
- package/dist/sync.mjs +1254 -3
- package/dist/{transport-C8UTS3Fa.mjs → transport-CVZGF0w9.mjs} +2 -4
- package/package.json +5 -18
- package/schemas/statelyai.schema.json +13 -72
- package/dist/cli.d.mts +0 -203
- package/dist/cli.mjs +0 -1760
- package/dist/sync-DLkTmSyA.mjs +0 -2513
- /package/dist/{graph-zuNj3kfa.d.mts → graph-CJ3N2r43.d.mts} +0 -0
package/dist/cli.mjs
DELETED
|
@@ -1,1760 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { createStatelyClient } from "./studio.mjs";
|
|
3
|
-
import { u as getStatelyPragma } from "./graphToXStateTS-Gzh0ZqbN.mjs";
|
|
4
|
-
import { n as pullSync, r as pushLocalMachineLinks, t as planSync } from "./sync-DLkTmSyA.mjs";
|
|
5
|
-
import fs from "node:fs/promises";
|
|
6
|
-
import * as path$1 from "node:path";
|
|
7
|
-
import path from "node:path";
|
|
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";
|
|
14
|
-
import { Args, Command, Flags, flush, handle, run as run$1 } from "@oclif/core";
|
|
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";
|
|
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
|
|
934
|
-
//#region src/projectConfig.ts
|
|
935
|
-
const STATELY_CONFIG_FILE = "statelyai.json";
|
|
936
|
-
const STATELY_CONFIG_SCHEMA_URL = "https://stately.ai/schemas/statelyai.json";
|
|
937
|
-
const STATELY_CONFIG_VERSION = "1.0.0";
|
|
938
|
-
const DEFAULT_SOURCE_EXCLUDES = [
|
|
939
|
-
"**/*.test.*",
|
|
940
|
-
"**/*.spec.*",
|
|
941
|
-
"**/dist/**",
|
|
942
|
-
"**/node_modules/**"
|
|
943
|
-
];
|
|
944
|
-
const CODE_SOURCE_EXTENSIONS = new Set([
|
|
945
|
-
".ts",
|
|
946
|
-
".tsx",
|
|
947
|
-
".js",
|
|
948
|
-
".jsx",
|
|
949
|
-
".mts",
|
|
950
|
-
".cts",
|
|
951
|
-
".mjs",
|
|
952
|
-
".cjs"
|
|
953
|
-
]);
|
|
954
|
-
const execFileAsync$1 = promisify(execFile);
|
|
955
|
-
function createStatelyProjectConfig(options) {
|
|
956
|
-
const defaultXStateVersion = options.defaultXStateVersion ?? 5;
|
|
957
|
-
return {
|
|
958
|
-
$schema: STATELY_CONFIG_SCHEMA_URL,
|
|
959
|
-
version: STATELY_CONFIG_VERSION,
|
|
960
|
-
projectId: options.projectId,
|
|
961
|
-
studioUrl: options.studioUrl,
|
|
962
|
-
defaultXStateVersion,
|
|
963
|
-
sources: []
|
|
964
|
-
};
|
|
965
|
-
}
|
|
966
|
-
function normalizeSourceConfig(source) {
|
|
967
|
-
return {
|
|
968
|
-
include: [...source.include ?? []],
|
|
969
|
-
exclude: source.exclude == null ? [...DEFAULT_SOURCE_EXCLUDES] : [...source.exclude],
|
|
970
|
-
format: source.format ?? "xstate",
|
|
971
|
-
...source.xstateVersion == null ? {} : { xstateVersion: source.xstateVersion }
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
function normalizeProjectConfig(config) {
|
|
975
|
-
return {
|
|
976
|
-
...config,
|
|
977
|
-
sources: (config.sources ?? []).map((source) => normalizeSourceConfig(source))
|
|
978
|
-
};
|
|
979
|
-
}
|
|
980
|
-
async function readStatelyProjectConfig(options = {}) {
|
|
981
|
-
const rootDir = path.resolve(options.cwd ?? process.cwd());
|
|
982
|
-
const configPath = path.resolve(rootDir, options.configPath ?? STATELY_CONFIG_FILE);
|
|
983
|
-
const raw = await fs.readFile(configPath, "utf8");
|
|
984
|
-
return {
|
|
985
|
-
config: normalizeProjectConfig(JSON.parse(raw)),
|
|
986
|
-
configPath,
|
|
987
|
-
rootDir
|
|
988
|
-
};
|
|
989
|
-
}
|
|
990
|
-
async function walkFiles(rootDir, currentDir = rootDir) {
|
|
991
|
-
const entries = await fs.readdir(currentDir, { withFileTypes: true });
|
|
992
|
-
const files = [];
|
|
993
|
-
for (const entry of entries) {
|
|
994
|
-
if (entry.name === ".git") continue;
|
|
995
|
-
const absolutePath = path.join(currentDir, entry.name);
|
|
996
|
-
if (entry.isDirectory()) {
|
|
997
|
-
files.push(...await walkFiles(rootDir, absolutePath));
|
|
998
|
-
continue;
|
|
999
|
-
}
|
|
1000
|
-
if (!entry.isFile()) continue;
|
|
1001
|
-
files.push(path.relative(rootDir, absolutePath).replace(/\\/g, "/"));
|
|
1002
|
-
}
|
|
1003
|
-
return files;
|
|
1004
|
-
}
|
|
1005
|
-
async function filterGitIgnoredPaths(rootDir, relativeFiles) {
|
|
1006
|
-
try {
|
|
1007
|
-
const { stdout } = await execFileAsync$1("git", [
|
|
1008
|
-
"ls-files",
|
|
1009
|
-
"--others",
|
|
1010
|
-
"--ignored",
|
|
1011
|
-
"--exclude-standard",
|
|
1012
|
-
"--directory"
|
|
1013
|
-
], { cwd: rootDir });
|
|
1014
|
-
const ignoredEntries = stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
|
|
1015
|
-
if (ignoredEntries.length === 0) return relativeFiles;
|
|
1016
|
-
return relativeFiles.filter((relativePath) => {
|
|
1017
|
-
return !ignoredEntries.some((ignoredEntry) => {
|
|
1018
|
-
if (ignoredEntry.endsWith("/")) return relativePath.startsWith(ignoredEntry);
|
|
1019
|
-
return relativePath === ignoredEntry;
|
|
1020
|
-
});
|
|
1021
|
-
});
|
|
1022
|
-
} catch {
|
|
1023
|
-
return relativeFiles;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
function isCodeSourceFile(relativePath) {
|
|
1027
|
-
return CODE_SOURCE_EXTENSIONS.has(path.extname(relativePath).toLowerCase());
|
|
1028
|
-
}
|
|
1029
|
-
function expandBraces(pattern) {
|
|
1030
|
-
const match = pattern.match(/\{([^{}]+)\}/);
|
|
1031
|
-
if (!match || match.index == null) return [pattern];
|
|
1032
|
-
const [token, inner] = match;
|
|
1033
|
-
return inner.split(",").flatMap((variant) => expandBraces(`${pattern.slice(0, match.index)}${variant}${pattern.slice(match.index + token.length)}`));
|
|
1034
|
-
}
|
|
1035
|
-
function globToRegExp(pattern) {
|
|
1036
|
-
let regex = "^";
|
|
1037
|
-
for (let index = 0; index < pattern.length; index += 1) {
|
|
1038
|
-
const char = pattern[index];
|
|
1039
|
-
const next = pattern[index + 1];
|
|
1040
|
-
if (char === "*") {
|
|
1041
|
-
if (next === "*") {
|
|
1042
|
-
const slashAfterGlobstar = pattern[index + 2] === "/";
|
|
1043
|
-
regex += slashAfterGlobstar ? "(?:.*/)?" : ".*";
|
|
1044
|
-
index += slashAfterGlobstar ? 2 : 1;
|
|
1045
|
-
continue;
|
|
1046
|
-
}
|
|
1047
|
-
regex += "[^/]*";
|
|
1048
|
-
continue;
|
|
1049
|
-
}
|
|
1050
|
-
if (char === "?") {
|
|
1051
|
-
regex += "[^/]";
|
|
1052
|
-
continue;
|
|
1053
|
-
}
|
|
1054
|
-
if ("\\.[]{}()+-^$|".includes(char)) {
|
|
1055
|
-
regex += `\\${char}`;
|
|
1056
|
-
continue;
|
|
1057
|
-
}
|
|
1058
|
-
regex += char;
|
|
1059
|
-
}
|
|
1060
|
-
regex += "$";
|
|
1061
|
-
return new RegExp(regex);
|
|
1062
|
-
}
|
|
1063
|
-
function matchesGlob(relativePath, pattern) {
|
|
1064
|
-
return expandBraces(pattern).some((expanded) => globToRegExp(expanded).test(relativePath));
|
|
1065
|
-
}
|
|
1066
|
-
function matchesAny(patterns, relativePath) {
|
|
1067
|
-
return (patterns ?? []).some((pattern) => matchesGlob(relativePath, pattern));
|
|
1068
|
-
}
|
|
1069
|
-
async function discoverCodeSourceFiles(options = {}) {
|
|
1070
|
-
const rootDir = path.resolve(options.cwd ?? process.cwd());
|
|
1071
|
-
return (await filterGitIgnoredPaths(rootDir, await walkFiles(rootDir))).filter((relativePath) => isCodeSourceFile(relativePath)).filter((relativePath) => !matchesAny(DEFAULT_SOURCE_EXCLUDES, relativePath)).sort((left, right) => left.localeCompare(right));
|
|
1072
|
-
}
|
|
1073
|
-
function createSuggestedSource(include) {
|
|
1074
|
-
return {
|
|
1075
|
-
include: [include],
|
|
1076
|
-
exclude: [...DEFAULT_SOURCE_EXCLUDES],
|
|
1077
|
-
format: "xstate",
|
|
1078
|
-
xstateVersion: 5
|
|
1079
|
-
};
|
|
1080
|
-
}
|
|
1081
|
-
function suggestStatelySourceConfigs(relativePaths, defaultXStateVersion = 5) {
|
|
1082
|
-
const pending = new Set(relativePaths.map((relativePath) => relativePath.replace(/\\/g, "/")).filter(Boolean));
|
|
1083
|
-
const suggestions = [];
|
|
1084
|
-
const addSuggestion = (include, predicate) => {
|
|
1085
|
-
const matched = [...pending].filter(predicate);
|
|
1086
|
-
if (matched.length === 0) return;
|
|
1087
|
-
suggestions.push({
|
|
1088
|
-
...createSuggestedSource(include),
|
|
1089
|
-
xstateVersion: defaultXStateVersion
|
|
1090
|
-
});
|
|
1091
|
-
for (const matchedPath of matched) pending.delete(matchedPath);
|
|
1092
|
-
};
|
|
1093
|
-
addSuggestion("**/*.machine.ts", (relativePath) => relativePath.endsWith(".machine.ts"));
|
|
1094
|
-
addSuggestion("src/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}", (relativePath) => relativePath.startsWith("src/"));
|
|
1095
|
-
addSuggestion("packages/*/src/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}", (relativePath) => /^packages\/[^/]+\/src\//.test(relativePath));
|
|
1096
|
-
addSuggestion("apps/*/src/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}", (relativePath) => /^apps\/[^/]+\/src\//.test(relativePath));
|
|
1097
|
-
const byDirectory = /* @__PURE__ */ new Map();
|
|
1098
|
-
for (const relativePath of pending) {
|
|
1099
|
-
const directory = path.posix.dirname(relativePath);
|
|
1100
|
-
const key = directory === "." ? relativePath : directory;
|
|
1101
|
-
const bucket = byDirectory.get(key) ?? [];
|
|
1102
|
-
bucket.push(relativePath);
|
|
1103
|
-
byDirectory.set(key, bucket);
|
|
1104
|
-
}
|
|
1105
|
-
for (const [key, matchedPaths] of [...byDirectory.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
|
|
1106
|
-
if (matchedPaths.length > 1 && key !== "." && key !== matchedPaths[0]) {
|
|
1107
|
-
addSuggestion(`${key}/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}`, (relativePath) => relativePath.startsWith(`${key}/`));
|
|
1108
|
-
continue;
|
|
1109
|
-
}
|
|
1110
|
-
const exactPath = matchedPaths[0];
|
|
1111
|
-
addSuggestion(exactPath, (relativePath) => relativePath === exactPath);
|
|
1112
|
-
}
|
|
1113
|
-
return suggestions;
|
|
1114
|
-
}
|
|
1115
|
-
async function discoverStatelySourceFiles(options = {}) {
|
|
1116
|
-
const { config, rootDir } = options.config ? {
|
|
1117
|
-
config: options.config,
|
|
1118
|
-
rootDir: path.resolve(options.cwd ?? process.cwd())
|
|
1119
|
-
} : await readStatelyProjectConfig(options);
|
|
1120
|
-
const relativeFiles = await filterGitIgnoredPaths(rootDir, await walkFiles(rootDir));
|
|
1121
|
-
const discovered = /* @__PURE__ */ new Map();
|
|
1122
|
-
for (const source of config.sources) for (const relativePath of relativeFiles) {
|
|
1123
|
-
if (!matchesAny(source.include, relativePath)) continue;
|
|
1124
|
-
if (matchesAny(source.exclude, relativePath)) continue;
|
|
1125
|
-
const filePath = path.join(rootDir, relativePath);
|
|
1126
|
-
if (!discovered.has(filePath)) discovered.set(filePath, {
|
|
1127
|
-
filePath,
|
|
1128
|
-
relativePath,
|
|
1129
|
-
source
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
return [...discovered.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
//#endregion
|
|
1136
|
-
//#region src/cli.ts
|
|
1137
|
-
const execFileAsync = promisify(execFile);
|
|
1138
|
-
const STATELY_API_KEY_SETTINGS_URL = "https://stately.ai/registry/user/my-settings?tab=API+Key";
|
|
1139
|
-
function loadLocalEnv() {
|
|
1140
|
-
if (typeof process.loadEnvFile !== "function") return;
|
|
1141
|
-
const cwdEnvPath = path.join(process.cwd(), ".env.local");
|
|
1142
|
-
try {
|
|
1143
|
-
process.loadEnvFile(cwdEnvPath);
|
|
1144
|
-
} catch {}
|
|
1145
|
-
}
|
|
1146
|
-
loadLocalEnv();
|
|
1147
|
-
function getEnvApiKey() {
|
|
1148
|
-
const statelyApiKey = process.env.STATELY_API_KEY;
|
|
1149
|
-
if (statelyApiKey) return {
|
|
1150
|
-
apiKey: statelyApiKey,
|
|
1151
|
-
variable: "STATELY_API_KEY"
|
|
1152
|
-
};
|
|
1153
|
-
const publicApiKey = process.env.NEXT_PUBLIC_STATELY_API_KEY;
|
|
1154
|
-
if (publicApiKey) return {
|
|
1155
|
-
apiKey: publicApiKey,
|
|
1156
|
-
variable: "NEXT_PUBLIC_STATELY_API_KEY"
|
|
1157
|
-
};
|
|
1158
|
-
}
|
|
1159
|
-
async function resolveApiKey(explicitApiKey) {
|
|
1160
|
-
if (explicitApiKey) return {
|
|
1161
|
-
apiKey: explicitApiKey,
|
|
1162
|
-
source: "flag",
|
|
1163
|
-
detail: "--api-key"
|
|
1164
|
-
};
|
|
1165
|
-
const envApiKey = getEnvApiKey();
|
|
1166
|
-
if (envApiKey) return {
|
|
1167
|
-
apiKey: envApiKey.apiKey,
|
|
1168
|
-
source: "env",
|
|
1169
|
-
detail: envApiKey.variable
|
|
1170
|
-
};
|
|
1171
|
-
const storedApiKey = await getStoredApiKey();
|
|
1172
|
-
if (storedApiKey) return {
|
|
1173
|
-
apiKey: storedApiKey.apiKey,
|
|
1174
|
-
source: "stored",
|
|
1175
|
-
detail: describeCredentialBackend(storedApiKey.backend, storedApiKey.location)
|
|
1176
|
-
};
|
|
1177
|
-
return { source: "missing" };
|
|
1178
|
-
}
|
|
1179
|
-
function getDefaultBaseUrl() {
|
|
1180
|
-
return process.env.STATELY_API_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? process.env.NEXT_PUBLIC_STATELY_API_URL;
|
|
1181
|
-
}
|
|
1182
|
-
function getDefaultEditorUrl() {
|
|
1183
|
-
return process.env.STATELY_EDITOR_URL ?? process.env.NEXT_PUBLIC_BETA_EDITOR_URL ?? process.env.NEXT_PUBLIC_BASE_URL ?? "https://viz.localhost";
|
|
1184
|
-
}
|
|
1185
|
-
function getResolvedStudioUrl(baseUrl) {
|
|
1186
|
-
return baseUrl ?? getDefaultBaseUrl() ?? "https://stately.ai";
|
|
1187
|
-
}
|
|
1188
|
-
function parseGitHubRemote(remoteUrl) {
|
|
1189
|
-
const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
1190
|
-
if (httpsMatch) {
|
|
1191
|
-
const [, owner, repo] = httpsMatch;
|
|
1192
|
-
if (!owner || !repo) return null;
|
|
1193
|
-
return {
|
|
1194
|
-
url: `https://github.com/${owner}/${repo}`,
|
|
1195
|
-
owner,
|
|
1196
|
-
repo,
|
|
1197
|
-
branch: "",
|
|
1198
|
-
treeSha: ""
|
|
1199
|
-
};
|
|
1200
|
-
}
|
|
1201
|
-
const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
1202
|
-
if (sshMatch) {
|
|
1203
|
-
const [, owner, repo] = sshMatch;
|
|
1204
|
-
if (!owner || !repo) return null;
|
|
1205
|
-
return {
|
|
1206
|
-
url: `https://github.com/${owner}/${repo}`,
|
|
1207
|
-
owner,
|
|
1208
|
-
repo,
|
|
1209
|
-
branch: "",
|
|
1210
|
-
treeSha: ""
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
return null;
|
|
1214
|
-
}
|
|
1215
|
-
async function inferConnectedRepoFromCwd(cwd) {
|
|
1216
|
-
try {
|
|
1217
|
-
const [{ stdout: remoteStdout }, { stdout: branchStdout }, { stdout: treeShaStdout }] = await Promise.all([
|
|
1218
|
-
execFileAsync("git", [
|
|
1219
|
-
"remote",
|
|
1220
|
-
"get-url",
|
|
1221
|
-
"origin"
|
|
1222
|
-
], { cwd }),
|
|
1223
|
-
execFileAsync("git", ["branch", "--show-current"], { cwd }),
|
|
1224
|
-
execFileAsync("git", ["rev-parse", "HEAD"], { cwd })
|
|
1225
|
-
]);
|
|
1226
|
-
const parsedRemote = parseGitHubRemote(remoteStdout.trim());
|
|
1227
|
-
if (!parsedRemote) return;
|
|
1228
|
-
return {
|
|
1229
|
-
...parsedRemote,
|
|
1230
|
-
branch: branchStdout.trim(),
|
|
1231
|
-
treeSha: treeShaStdout.trim()
|
|
1232
|
-
};
|
|
1233
|
-
} catch {
|
|
1234
|
-
return;
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
function inferInitProjectName(cwd, repo) {
|
|
1238
|
-
if (repo?.repo) return repo.repo;
|
|
1239
|
-
return path.basename(cwd);
|
|
1240
|
-
}
|
|
1241
|
-
async function readApiKeyFromStdin() {
|
|
1242
|
-
const chunks = [];
|
|
1243
|
-
for await (const chunk of process.stdin) chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
1244
|
-
return Buffer.concat(chunks).toString("utf8").trim();
|
|
1245
|
-
}
|
|
1246
|
-
async function promptForApiKey() {
|
|
1247
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("No interactive terminal available. Pass --api-key or pipe the key on stdin.");
|
|
1248
|
-
const maskedOutput = new Writable({ write(chunk, _encoding, callback) {
|
|
1249
|
-
if (!maskedOutput.muted) process.stdout.write(chunk);
|
|
1250
|
-
callback();
|
|
1251
|
-
} });
|
|
1252
|
-
maskedOutput.muted = false;
|
|
1253
|
-
const rl = createInterface({
|
|
1254
|
-
input: process.stdin,
|
|
1255
|
-
output: maskedOutput,
|
|
1256
|
-
terminal: true
|
|
1257
|
-
});
|
|
1258
|
-
try {
|
|
1259
|
-
process.stdout.write("Enter your Stately API key: ");
|
|
1260
|
-
maskedOutput.muted = true;
|
|
1261
|
-
const apiKey = (await rl.question("")).trim();
|
|
1262
|
-
maskedOutput.muted = false;
|
|
1263
|
-
process.stdout.write("\n");
|
|
1264
|
-
return apiKey;
|
|
1265
|
-
} finally {
|
|
1266
|
-
rl.close();
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
async function promptYesNo(question, defaultValue = true) {
|
|
1270
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error("No interactive terminal available.");
|
|
1271
|
-
const rl = createInterface({
|
|
1272
|
-
input: process.stdin,
|
|
1273
|
-
output: process.stdout,
|
|
1274
|
-
terminal: true
|
|
1275
|
-
});
|
|
1276
|
-
try {
|
|
1277
|
-
const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
|
|
1278
|
-
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
|
|
1279
|
-
if (!answer) return defaultValue;
|
|
1280
|
-
return answer === "y" || answer === "yes";
|
|
1281
|
-
} finally {
|
|
1282
|
-
rl.close();
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
function normalizeApiKey(value) {
|
|
1286
|
-
const trimmed = value?.trim();
|
|
1287
|
-
return trimmed ? trimmed : void 0;
|
|
1288
|
-
}
|
|
1289
|
-
function pluralize(count, singular, plural = `${singular}s`) {
|
|
1290
|
-
return count === 1 ? singular : plural;
|
|
1291
|
-
}
|
|
1292
|
-
function getMissingApiKeyMessage() {
|
|
1293
|
-
return `No API key configured. Use \`statelyai login\`, set \`STATELY_API_KEY\`, or pass \`--api-key\`.\nGet or create an API key at ${STATELY_API_KEY_SETTINGS_URL}`;
|
|
1294
|
-
}
|
|
1295
|
-
async function fileExists(filePath) {
|
|
1296
|
-
try {
|
|
1297
|
-
await fs.access(filePath);
|
|
1298
|
-
return true;
|
|
1299
|
-
} catch {
|
|
1300
|
-
return false;
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
async function initProject(options) {
|
|
1304
|
-
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
1305
|
-
const studioUrl = getResolvedStudioUrl(options.baseUrl);
|
|
1306
|
-
const configPath = path.resolve(cwd, options.configPath ?? STATELY_CONFIG_FILE);
|
|
1307
|
-
const defaultXStateVersion = Math.max(5, options.defaultXStateVersion ?? 5);
|
|
1308
|
-
if (!options.force && await fileExists(configPath)) throw new Error(`${configPath} already exists. Pass --force to overwrite it.`);
|
|
1309
|
-
const client = options.client ?? createStatelyClient({
|
|
1310
|
-
apiKey: options.apiKey,
|
|
1311
|
-
baseUrl: studioUrl
|
|
1312
|
-
});
|
|
1313
|
-
const inferredRepo = options.project?.repo ?? await inferConnectedRepoFromCwd(cwd);
|
|
1314
|
-
const projectInput = {
|
|
1315
|
-
name: options.project?.name ?? inferInitProjectName(cwd, inferredRepo),
|
|
1316
|
-
visibility: options.project?.visibility ?? "Private",
|
|
1317
|
-
...options.project?.description ? { description: options.project.description } : {},
|
|
1318
|
-
...options.project?.keywords ? { keywords: options.project.keywords } : {},
|
|
1319
|
-
...inferredRepo ? { repo: inferredRepo } : {}
|
|
1320
|
-
};
|
|
1321
|
-
const project = await client.projects.ensure(projectInput);
|
|
1322
|
-
const config = createStatelyProjectConfig({
|
|
1323
|
-
projectId: project.projectId,
|
|
1324
|
-
studioUrl,
|
|
1325
|
-
defaultXStateVersion
|
|
1326
|
-
});
|
|
1327
|
-
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
1328
|
-
return {
|
|
1329
|
-
config,
|
|
1330
|
-
configPath,
|
|
1331
|
-
project
|
|
1332
|
-
};
|
|
1333
|
-
}
|
|
1334
|
-
async function scanProjectSources(options) {
|
|
1335
|
-
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
1336
|
-
const candidateRelativePaths = await discoverCodeSourceFiles({ cwd });
|
|
1337
|
-
const machineRelativePaths = [];
|
|
1338
|
-
for (const relativePath of candidateRelativePaths) {
|
|
1339
|
-
const filePath = path.join(cwd, relativePath);
|
|
1340
|
-
if (looksLikeXStateMachineSource(await fs.readFile(filePath, "utf8"))) machineRelativePaths.push(relativePath);
|
|
1341
|
-
}
|
|
1342
|
-
return suggestStatelySourceConfigs(machineRelativePaths, Math.max(5, options.defaultXStateVersion ?? 5));
|
|
1343
|
-
}
|
|
1344
|
-
function looksLikeXStateMachineSource(source) {
|
|
1345
|
-
const hasXStateImport = /from\s+['"]xstate(?:\/[^'"]+)?['"]/.test(source) || /require\(\s*['"]xstate(?:\/[^'"]+)?['"]\s*\)/.test(source);
|
|
1346
|
-
const hasMachineFactory = /\bcreateMachine\s*\(/.test(source) || /\.createMachine\s*\(/.test(source);
|
|
1347
|
-
return hasXStateImport && hasMachineFactory;
|
|
1348
|
-
}
|
|
1349
|
-
function supportsMachineDiscovery(file) {
|
|
1350
|
-
return file.source.format === "xstate" || file.source.format === "auto";
|
|
1351
|
-
}
|
|
1352
|
-
async function resolveConfiguredProject(options) {
|
|
1353
|
-
const { config, configPath, rootDir } = await readStatelyProjectConfig({
|
|
1354
|
-
cwd: options.cwd,
|
|
1355
|
-
configPath: options.configPath
|
|
1356
|
-
});
|
|
1357
|
-
const studioUrl = options.baseUrl ?? config.studioUrl;
|
|
1358
|
-
return {
|
|
1359
|
-
client: options.client ?? createStatelyClient({
|
|
1360
|
-
apiKey: options.apiKey,
|
|
1361
|
-
baseUrl: studioUrl
|
|
1362
|
-
}),
|
|
1363
|
-
config,
|
|
1364
|
-
configPath,
|
|
1365
|
-
files: await discoverStatelySourceFiles({
|
|
1366
|
-
cwd: rootDir,
|
|
1367
|
-
config
|
|
1368
|
-
})
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
|
-
const sharedFlags = {
|
|
1372
|
-
help: Flags.help({ char: "h" }),
|
|
1373
|
-
"fail-on-changes": Flags.boolean({
|
|
1374
|
-
default: false,
|
|
1375
|
-
description: "Exit with a nonzero code when differences are found"
|
|
1376
|
-
}),
|
|
1377
|
-
"api-key": Flags.string({ description: "Stately API key used for remote source or target resolution" }),
|
|
1378
|
-
"base-url": Flags.string({ description: "Base URL for Stately Studio or a self-hosted deployment" })
|
|
1379
|
-
};
|
|
1380
|
-
function formatChangeList(label, items) {
|
|
1381
|
-
if (items.length === 0) return null;
|
|
1382
|
-
return `${label}: ${items.map((item) => item.id).join(", ")}`;
|
|
1383
|
-
}
|
|
1384
|
-
function formatPlanSummary(plan) {
|
|
1385
|
-
const lines = [
|
|
1386
|
-
`Plan: ${plan.source.locator} -> ${plan.target.locator}`,
|
|
1387
|
-
`Source: ${plan.source.kind} (${plan.source.format})`,
|
|
1388
|
-
`Target: ${plan.target.kind} (${plan.target.format})`,
|
|
1389
|
-
`Has changes: ${plan.summary.hasChanges ? "yes" : "no"}`,
|
|
1390
|
-
`Node changes: ${plan.summary.nodeChanges}`,
|
|
1391
|
-
`Edge changes: ${plan.summary.edgeChanges}`
|
|
1392
|
-
];
|
|
1393
|
-
const nodeSections = [
|
|
1394
|
-
formatChangeList("Added nodes", plan.diff.nodes.added),
|
|
1395
|
-
formatChangeList("Removed nodes", plan.diff.nodes.removed),
|
|
1396
|
-
formatChangeList("Updated nodes", plan.diff.nodes.updated)
|
|
1397
|
-
].filter(Boolean);
|
|
1398
|
-
const edgeSections = [
|
|
1399
|
-
formatChangeList("Added edges", plan.diff.edges.added),
|
|
1400
|
-
formatChangeList("Removed edges", plan.diff.edges.removed),
|
|
1401
|
-
formatChangeList("Updated edges", plan.diff.edges.updated)
|
|
1402
|
-
].filter(Boolean);
|
|
1403
|
-
lines.push(...nodeSections, ...edgeSections);
|
|
1404
|
-
if (plan.warnings.length > 0) lines.push(`Warnings: ${plan.warnings.join("; ")}`);
|
|
1405
|
-
return lines.join("\n");
|
|
1406
|
-
}
|
|
1407
|
-
var BaseSyncCommand = class extends Command {
|
|
1408
|
-
static enableJsonFlag = false;
|
|
1409
|
-
static flags = sharedFlags;
|
|
1410
|
-
};
|
|
1411
|
-
const sharedArgs = {
|
|
1412
|
-
source: Args.string({
|
|
1413
|
-
required: true,
|
|
1414
|
-
description: "Source locator. Supports local file paths, URLs, or machine IDs."
|
|
1415
|
-
}),
|
|
1416
|
-
target: Args.string({
|
|
1417
|
-
required: true,
|
|
1418
|
-
description: "Target locator. Supports local file paths, URLs, or machine IDs."
|
|
1419
|
-
})
|
|
1420
|
-
};
|
|
1421
|
-
var ParsedSyncCommand = class extends BaseSyncCommand {
|
|
1422
|
-
async parseSync(command) {
|
|
1423
|
-
return this.parse(command);
|
|
1424
|
-
}
|
|
1425
|
-
};
|
|
1426
|
-
var PlanCommand = class PlanCommand extends ParsedSyncCommand {
|
|
1427
|
-
static summary = "Plan semantic sync changes between a source and target.";
|
|
1428
|
-
static description = "Resolves the source and target, normalizes both into graph form, and prints a semantic change summary.";
|
|
1429
|
-
static args = sharedArgs;
|
|
1430
|
-
async run() {
|
|
1431
|
-
const { args, flags } = await this.parseSync(PlanCommand);
|
|
1432
|
-
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
1433
|
-
const plan = await planSync({
|
|
1434
|
-
source: args.source,
|
|
1435
|
-
target: args.target,
|
|
1436
|
-
apiKey,
|
|
1437
|
-
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
1438
|
-
});
|
|
1439
|
-
this.log(formatPlanSummary(plan));
|
|
1440
|
-
this.exit(flags["fail-on-changes"] && plan.summary.hasChanges ? 1 : 0);
|
|
1441
|
-
}
|
|
1442
|
-
};
|
|
1443
|
-
var DiffCommand = class DiffCommand extends ParsedSyncCommand {
|
|
1444
|
-
static summary = "Diff a source and target using semantic graph comparison.";
|
|
1445
|
-
static description = "Like plan, but intended for diff-style usage and exit codes in scripts.";
|
|
1446
|
-
static args = sharedArgs;
|
|
1447
|
-
async run() {
|
|
1448
|
-
const { args, flags } = await this.parseSync(DiffCommand);
|
|
1449
|
-
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
1450
|
-
const plan = await planSync({
|
|
1451
|
-
source: args.source,
|
|
1452
|
-
target: args.target,
|
|
1453
|
-
apiKey,
|
|
1454
|
-
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
1455
|
-
});
|
|
1456
|
-
this.log(formatPlanSummary(plan));
|
|
1457
|
-
this.exit(flags["fail-on-changes"] && plan.summary.hasChanges ? 1 : 0);
|
|
1458
|
-
}
|
|
1459
|
-
};
|
|
1460
|
-
var PullCommand = class PullCommand extends ParsedSyncCommand {
|
|
1461
|
-
static summary = "Pull a source locator into a local target file.";
|
|
1462
|
-
static description = "Resolves the source, materializes it in the target format inferred from the target file, and writes the result locally.";
|
|
1463
|
-
static args = {
|
|
1464
|
-
source: Args.string({
|
|
1465
|
-
required: true,
|
|
1466
|
-
description: "Source locator. Supports a local linked file, machine ID, URL, or local file path."
|
|
1467
|
-
}),
|
|
1468
|
-
target: Args.string({
|
|
1469
|
-
required: false,
|
|
1470
|
-
description: "Optional local target path. Defaults to the source file when pulling a linked local file."
|
|
1471
|
-
})
|
|
1472
|
-
};
|
|
1473
|
-
async run() {
|
|
1474
|
-
const { args, flags } = await this.parseSync(PullCommand);
|
|
1475
|
-
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
1476
|
-
const localCandidate = path.resolve(process.cwd(), args.source);
|
|
1477
|
-
let source = args.source;
|
|
1478
|
-
let target = args.target;
|
|
1479
|
-
if (!target && await fileExists(localCandidate)) {
|
|
1480
|
-
const pragma = getStatelyPragma(await fs.readFile(localCandidate, "utf8"), localCandidate);
|
|
1481
|
-
if (!pragma?.id) this.error(`No @statelyai id found in ${localCandidate}. Pass an explicit source machine ID or URL and target path.`);
|
|
1482
|
-
source = pragma.id;
|
|
1483
|
-
target = localCandidate;
|
|
1484
|
-
}
|
|
1485
|
-
if (!target) this.error("Missing target path. Pass `statelyai pull <machine-id|url> <file>` or `statelyai pull <linked-file>`.");
|
|
1486
|
-
this.log(`Pulling ${source} into ${target}...`);
|
|
1487
|
-
const result = await pullSync({
|
|
1488
|
-
source,
|
|
1489
|
-
target,
|
|
1490
|
-
apiKey,
|
|
1491
|
-
baseUrl: flags["base-url"] ?? getDefaultBaseUrl()
|
|
1492
|
-
});
|
|
1493
|
-
this.log(`Pulled: ${result.source.locator} -> ${result.outputPath}\nTarget: ${result.target.kind} (${result.target.format})`);
|
|
1494
|
-
}
|
|
1495
|
-
};
|
|
1496
|
-
var PushCommand = class PushCommand extends Command {
|
|
1497
|
-
static enableJsonFlag = false;
|
|
1498
|
-
static summary = "Create or update remote machines for local source files.";
|
|
1499
|
-
static description = "Uses statelyai.json source discovery by default, creates remote machines for unlabeled local machine sources, updates already-linked machines, and writes returned @statelyai ids back into source files.";
|
|
1500
|
-
static args = { file: Args.string({
|
|
1501
|
-
required: false,
|
|
1502
|
-
description: "Optional local source file to push instead of scanning statelyai.json."
|
|
1503
|
-
}) };
|
|
1504
|
-
static flags = {
|
|
1505
|
-
help: Flags.help({ char: "h" }),
|
|
1506
|
-
"api-key": Flags.string({ description: "Stately API key used to create or update remote machines" }),
|
|
1507
|
-
"base-url": Flags.string({ description: "Base URL for Stately Studio or a self-hosted deployment" }),
|
|
1508
|
-
config: Flags.string({ description: "Path to statelyai.json" })
|
|
1509
|
-
};
|
|
1510
|
-
async run() {
|
|
1511
|
-
const { args, flags } = await this.parse(PushCommand);
|
|
1512
|
-
const resolvedApiKey = await resolveApiKey(flags["api-key"]);
|
|
1513
|
-
if (!resolvedApiKey.apiKey) this.error(getMissingApiKeyMessage());
|
|
1514
|
-
this.log("Resolving configured project and source files...");
|
|
1515
|
-
const { client, config, files } = await resolveConfiguredProject({
|
|
1516
|
-
apiKey: resolvedApiKey.apiKey,
|
|
1517
|
-
baseUrl: flags["base-url"],
|
|
1518
|
-
configPath: flags.config
|
|
1519
|
-
});
|
|
1520
|
-
const candidateFiles = args.file ? [{
|
|
1521
|
-
filePath: path.resolve(args.file),
|
|
1522
|
-
relativePath: path.relative(process.cwd(), path.resolve(args.file)),
|
|
1523
|
-
source: {
|
|
1524
|
-
include: [args.file],
|
|
1525
|
-
format: "xstate",
|
|
1526
|
-
xstateVersion: config.defaultXStateVersion
|
|
1527
|
-
}
|
|
1528
|
-
}] : files.filter(supportsMachineDiscovery);
|
|
1529
|
-
if (candidateFiles.length === 0) {
|
|
1530
|
-
this.log("No matching local machine source files were discovered.");
|
|
1531
|
-
return;
|
|
1532
|
-
}
|
|
1533
|
-
this.log(`Processing ${candidateFiles.length} ${pluralize(candidateFiles.length, "source file")}...`);
|
|
1534
|
-
const linked = [];
|
|
1535
|
-
const refreshed = [];
|
|
1536
|
-
const skipped = [];
|
|
1537
|
-
for (const file of candidateFiles) {
|
|
1538
|
-
this.log(`Pushing ${file.relativePath}...`);
|
|
1539
|
-
if (!supportsMachineDiscovery(file)) {
|
|
1540
|
-
skipped.push(`${file.relativePath}: unsupported format ${file.source.format}`);
|
|
1541
|
-
continue;
|
|
1542
|
-
}
|
|
1543
|
-
const result = await pushLocalMachineLinks({
|
|
1544
|
-
source: file.filePath,
|
|
1545
|
-
apiKey: resolvedApiKey.apiKey,
|
|
1546
|
-
baseUrl: flags["base-url"] ?? config.studioUrl,
|
|
1547
|
-
client,
|
|
1548
|
-
project: { projectId: config.projectId },
|
|
1549
|
-
xstateVersion: Math.max(5, file.source.xstateVersion ?? config.defaultXStateVersion)
|
|
1550
|
-
});
|
|
1551
|
-
if (result.created.length > 0) linked.push(`${file.relativePath}: ${result.created.map(({ machineIndex, machine }) => `${machine.id} [${machineIndex}]`).join(", ")}`);
|
|
1552
|
-
if (result.updated.length > 0) refreshed.push(`${file.relativePath}: ${result.updated.map(({ machineIndex, machine }) => `${machine.id} [${machineIndex}]`).join(", ")}`);
|
|
1553
|
-
for (const entry of result.skipped) skipped.push(`${file.relativePath} [${entry.machineIndex}]: ${entry.reason}`);
|
|
1554
|
-
}
|
|
1555
|
-
if (linked.length > 0) this.log(`Linked:\n${linked.join("\n")}`);
|
|
1556
|
-
if (refreshed.length > 0) this.log(`Updated:\n${refreshed.join("\n")}`);
|
|
1557
|
-
if (skipped.length > 0) this.log(`Skipped:\n${skipped.join("\n")}`);
|
|
1558
|
-
}
|
|
1559
|
-
};
|
|
1560
|
-
var OpenCommand = class OpenCommand extends Command {
|
|
1561
|
-
static enableJsonFlag = false;
|
|
1562
|
-
static summary = "Open a local file in the Stately visual editor.";
|
|
1563
|
-
static description = "Starts a local browser-backed editor session. Saved file changes update the visual editor, and saved visual edits are written back to the source file.";
|
|
1564
|
-
static args = { file: Args.string({
|
|
1565
|
-
required: true,
|
|
1566
|
-
description: "Local source file to edit visually."
|
|
1567
|
-
}) };
|
|
1568
|
-
static flags = {
|
|
1569
|
-
help: Flags.help({ char: "h" }),
|
|
1570
|
-
"api-key": Flags.string({ description: "Stately API key used when the editor server requires auth" }),
|
|
1571
|
-
"editor-url": Flags.string({ description: "Base URL for the Stately editor embed" }),
|
|
1572
|
-
host: Flags.string({
|
|
1573
|
-
description: "Local server host",
|
|
1574
|
-
default: "127.0.0.1"
|
|
1575
|
-
}),
|
|
1576
|
-
port: Flags.integer({
|
|
1577
|
-
description: "Local server port. Defaults to a random available port.",
|
|
1578
|
-
default: 0,
|
|
1579
|
-
min: 0
|
|
1580
|
-
}),
|
|
1581
|
-
open: Flags.boolean({
|
|
1582
|
-
description: "Open the local editor URL in the default browser",
|
|
1583
|
-
default: true,
|
|
1584
|
-
allowNo: true
|
|
1585
|
-
}),
|
|
1586
|
-
debug: Flags.boolean({
|
|
1587
|
-
description: "Log editor protocol messages",
|
|
1588
|
-
default: false
|
|
1589
|
-
})
|
|
1590
|
-
};
|
|
1591
|
-
async run() {
|
|
1592
|
-
const { args, flags } = await this.parse(OpenCommand);
|
|
1593
|
-
const apiKey = (await resolveApiKey(flags["api-key"])).apiKey;
|
|
1594
|
-
await openEditor({
|
|
1595
|
-
fileName: path.resolve(args.file),
|
|
1596
|
-
editorUrl: flags["editor-url"] ?? getDefaultEditorUrl(),
|
|
1597
|
-
host: flags.host,
|
|
1598
|
-
port: flags.port,
|
|
1599
|
-
shouldOpen: flags.open,
|
|
1600
|
-
apiKey,
|
|
1601
|
-
debug: flags.debug
|
|
1602
|
-
});
|
|
1603
|
-
}
|
|
1604
|
-
};
|
|
1605
|
-
var InitCommand = class InitCommand extends Command {
|
|
1606
|
-
static enableJsonFlag = false;
|
|
1607
|
-
static summary = "Create a Stately project and write statelyai.json.";
|
|
1608
|
-
static description = "Creates or reuses a remote Studio project for the current working directory and writes a local statelyai.json configuration file.";
|
|
1609
|
-
static flags = {
|
|
1610
|
-
help: Flags.help({ char: "h" }),
|
|
1611
|
-
"api-key": Flags.string({ description: "Stately API key used to create the remote project" }),
|
|
1612
|
-
"base-url": Flags.string({ description: "Base URL for Stately Studio or a self-hosted deployment" }),
|
|
1613
|
-
name: Flags.string({ description: "Project name to create remotely" }),
|
|
1614
|
-
visibility: Flags.string({
|
|
1615
|
-
description: "Remote project visibility",
|
|
1616
|
-
options: [
|
|
1617
|
-
"Private",
|
|
1618
|
-
"Public",
|
|
1619
|
-
"Unlisted"
|
|
1620
|
-
],
|
|
1621
|
-
default: "Private"
|
|
1622
|
-
}),
|
|
1623
|
-
force: Flags.boolean({
|
|
1624
|
-
description: "Overwrite an existing statelyai.json file",
|
|
1625
|
-
default: false
|
|
1626
|
-
}),
|
|
1627
|
-
scan: Flags.boolean({
|
|
1628
|
-
description: "Scan the repo for machine-bearing files and suggest source globs to save into statelyai.json",
|
|
1629
|
-
default: false
|
|
1630
|
-
})
|
|
1631
|
-
};
|
|
1632
|
-
async run() {
|
|
1633
|
-
const { flags } = await this.parse(InitCommand);
|
|
1634
|
-
const resolvedApiKey = await resolveApiKey(flags["api-key"]);
|
|
1635
|
-
if (!resolvedApiKey.apiKey) this.error(getMissingApiKeyMessage());
|
|
1636
|
-
this.log("Creating or reusing remote project...");
|
|
1637
|
-
const result = await initProject({
|
|
1638
|
-
apiKey: resolvedApiKey.apiKey,
|
|
1639
|
-
baseUrl: flags["base-url"],
|
|
1640
|
-
force: flags.force,
|
|
1641
|
-
project: {
|
|
1642
|
-
...flags.name ? { name: flags.name } : {},
|
|
1643
|
-
visibility: flags.visibility
|
|
1644
|
-
}
|
|
1645
|
-
});
|
|
1646
|
-
if (flags.scan) {
|
|
1647
|
-
this.log("Scanning local source files...");
|
|
1648
|
-
const suggestions = await scanProjectSources({
|
|
1649
|
-
cwd: path.dirname(result.configPath),
|
|
1650
|
-
defaultXStateVersion: result.config.defaultXStateVersion
|
|
1651
|
-
});
|
|
1652
|
-
if (suggestions.length === 0) {
|
|
1653
|
-
this.log(`Initialized project ${result.project.projectId} and wrote ${result.configPath}.\nNo machine source globs were suggested. Edit statelyai.json before running statelyai push.`);
|
|
1654
|
-
return;
|
|
1655
|
-
}
|
|
1656
|
-
this.log(`Initialized project ${result.project.projectId} and wrote ${result.configPath}.`);
|
|
1657
|
-
this.log(`Suggested source globs:\n${suggestions.map((source) => `- ${source.include.join(", ")}`).join("\n")}`);
|
|
1658
|
-
if (await promptYesNo("Save these source globs to statelyai.json?", true)) {
|
|
1659
|
-
const nextConfig = {
|
|
1660
|
-
...result.config,
|
|
1661
|
-
sources: suggestions
|
|
1662
|
-
};
|
|
1663
|
-
await fs.writeFile(result.configPath, `${JSON.stringify(nextConfig, null, 2)}\n`, "utf8");
|
|
1664
|
-
this.log("Saved scanned source globs to statelyai.json.");
|
|
1665
|
-
} else this.log("Left statelyai.json with an empty sources array. Edit it before running statelyai push.");
|
|
1666
|
-
return;
|
|
1667
|
-
}
|
|
1668
|
-
this.log(`Initialized project ${result.project.projectId} and wrote ${result.configPath}.\nNo source globs configured yet. Edit statelyai.json before running statelyai push, or rerun with --scan.`);
|
|
1669
|
-
}
|
|
1670
|
-
};
|
|
1671
|
-
var LoginCommand = class LoginCommand extends Command {
|
|
1672
|
-
static enableJsonFlag = false;
|
|
1673
|
-
static summary = "Store a Stately API key for future CLI use.";
|
|
1674
|
-
static description = "Stores a Stately API key in the OS credential store when available, with a private file fallback.";
|
|
1675
|
-
static flags = {
|
|
1676
|
-
help: Flags.help({ char: "h" }),
|
|
1677
|
-
"api-key": Flags.string({ description: "API key to store without an interactive prompt" }),
|
|
1678
|
-
stdin: Flags.boolean({
|
|
1679
|
-
description: "Read the API key from standard input",
|
|
1680
|
-
default: false
|
|
1681
|
-
})
|
|
1682
|
-
};
|
|
1683
|
-
async run() {
|
|
1684
|
-
const { flags } = await this.parse(LoginCommand);
|
|
1685
|
-
if (flags.stdin && flags["api-key"]) this.error("Pass either --api-key or --stdin, not both.");
|
|
1686
|
-
if (!flags["api-key"] && !flags.stdin && process.stdin.isTTY && process.stdout.isTTY) this.log(`Get or create an API key at ${STATELY_API_KEY_SETTINGS_URL}`);
|
|
1687
|
-
const apiKey = normalizeApiKey(flags["api-key"] ?? (!process.stdin.isTTY || flags.stdin ? await readApiKeyFromStdin() : await promptForApiKey()));
|
|
1688
|
-
if (!apiKey) this.error(`API key cannot be empty.\nGet or create an API key at ${STATELY_API_KEY_SETTINGS_URL}`);
|
|
1689
|
-
const stored = await setStoredApiKey(apiKey);
|
|
1690
|
-
this.log(`Stored API key in ${describeCredentialBackend(stored.backend, stored.location)}.`);
|
|
1691
|
-
}
|
|
1692
|
-
};
|
|
1693
|
-
var LogoutCommand = class extends Command {
|
|
1694
|
-
static enableJsonFlag = false;
|
|
1695
|
-
static summary = "Remove any API key stored by the CLI.";
|
|
1696
|
-
static description = "Deletes the locally stored API key. Environment variables are not changed.";
|
|
1697
|
-
static flags = { help: Flags.help({ char: "h" }) };
|
|
1698
|
-
async run() {
|
|
1699
|
-
const result = await deleteStoredApiKey();
|
|
1700
|
-
if (!result.deleted) {
|
|
1701
|
-
this.log("No stored API key found.");
|
|
1702
|
-
return;
|
|
1703
|
-
}
|
|
1704
|
-
this.log(`Removed stored API key from ${result.locations.join(", ")}.`);
|
|
1705
|
-
}
|
|
1706
|
-
};
|
|
1707
|
-
var AuthStatusCommand = class extends Command {
|
|
1708
|
-
static enableJsonFlag = false;
|
|
1709
|
-
static summary = "Show how the CLI would resolve its API key.";
|
|
1710
|
-
static description = "Reports whether the CLI would use a flag, environment variable, or stored credential.";
|
|
1711
|
-
static flags = { help: Flags.help({ char: "h" }) };
|
|
1712
|
-
async run() {
|
|
1713
|
-
const envApiKey = getEnvApiKey();
|
|
1714
|
-
const storedApiKey = await getStoredApiKey();
|
|
1715
|
-
if (envApiKey) {
|
|
1716
|
-
this.log(`API key source: environment (${envApiKey.variable}).`);
|
|
1717
|
-
if (storedApiKey) this.log(`Stored credential also available in ${describeCredentialBackend(storedApiKey.backend, storedApiKey.location)}.`);
|
|
1718
|
-
return;
|
|
1719
|
-
}
|
|
1720
|
-
if (storedApiKey) {
|
|
1721
|
-
this.log(`API key source: stored credential (${describeCredentialBackend(storedApiKey.backend, storedApiKey.location)}).`);
|
|
1722
|
-
return;
|
|
1723
|
-
}
|
|
1724
|
-
this.log(getMissingApiKeyMessage());
|
|
1725
|
-
}
|
|
1726
|
-
};
|
|
1727
|
-
const COMMANDS = {
|
|
1728
|
-
plan: PlanCommand,
|
|
1729
|
-
diff: DiffCommand,
|
|
1730
|
-
pull: PullCommand,
|
|
1731
|
-
push: PushCommand,
|
|
1732
|
-
open: OpenCommand,
|
|
1733
|
-
init: InitCommand,
|
|
1734
|
-
login: LoginCommand,
|
|
1735
|
-
logout: LogoutCommand,
|
|
1736
|
-
"auth:status": AuthStatusCommand
|
|
1737
|
-
};
|
|
1738
|
-
async function run(argv = process.argv.slice(2), entryUrl = import.meta.url) {
|
|
1739
|
-
const normalizedArgv = argv.length === 1 && argv[0] === "-h" ? ["--help"] : argv;
|
|
1740
|
-
try {
|
|
1741
|
-
await run$1(normalizedArgv, entryUrl);
|
|
1742
|
-
await flush();
|
|
1743
|
-
} catch (error) {
|
|
1744
|
-
await handle(error);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
function isDirectExecution() {
|
|
1748
|
-
const entryPath = process.argv[1];
|
|
1749
|
-
if (!entryPath) return false;
|
|
1750
|
-
const modulePath = fileURLToPath(import.meta.url);
|
|
1751
|
-
try {
|
|
1752
|
-
return fs$1.realpathSync(entryPath) === fs$1.realpathSync(modulePath);
|
|
1753
|
-
} catch {
|
|
1754
|
-
return path.resolve(entryPath) === modulePath;
|
|
1755
|
-
}
|
|
1756
|
-
}
|
|
1757
|
-
if (isDirectExecution()) run();
|
|
1758
|
-
|
|
1759
|
-
//#endregion
|
|
1760
|
-
export { COMMANDS, createStatelyProjectConfig, formatPlanSummary, getEnvApiKey, inferInitProjectName, initProject, resolveApiKey, run, scanProjectSources };
|