statelyai 0.10.1 → 0.11.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 +21 -12
- package/dist/bin.mjs +1 -1
- package/dist/{cli-DV60djkN.mjs → cli-Dw84PDUH.mjs} +939 -557
- package/dist/index.d.mts +939 -1
- package/dist/index.mjs +2 -2
- package/package.json +9 -7
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import fs, { promises, watch } from "node:fs";
|
|
1
|
+
import { Args, Command, Flags, flush, handle, run } from "@oclif/core";
|
|
3
2
|
import fsPromises from "node:fs/promises";
|
|
4
|
-
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
5
3
|
import * as path$1 from "node:path";
|
|
6
4
|
import path from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { execFile, execFileSync, spawn } from "node:child_process";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import * as crypto from "node:crypto";
|
|
9
|
+
import fs, { promises, watch } from "node:fs";
|
|
7
10
|
import { createInterface } from "node:readline/promises";
|
|
8
11
|
import { Writable } from "node:stream";
|
|
9
12
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
-
import { promisify } from "node:util";
|
|
11
|
-
import { Args, Command, Flags, flush, handle, run } from "@oclif/core";
|
|
12
13
|
import * as http from "node:http";
|
|
13
14
|
import * as ts from "typescript";
|
|
14
15
|
import { EditorHostWorkspaceSession, StudioApiError, buildAuthorizeUrl, createPkcePair, createRemoteEditorSyncBackend, createStatelyClient, discoverAuthorizationServer, discoverProtectedResource, exchangeCodeForToken, findStatelyPragmaAttachments, getStatelyPragma, refreshToken, registerOAuthClient } from "@statelyai/sdk";
|
|
@@ -16,121 +17,814 @@ import { WebSocket, WebSocketServer } from "ws";
|
|
|
16
17
|
import os from "node:os";
|
|
17
18
|
import { PullSourceReconciliationError, planSync, pullSync, pushLocalMachineLinks } from "@statelyai/sdk/sync";
|
|
18
19
|
|
|
19
|
-
//#region
|
|
20
|
-
const
|
|
20
|
+
//#region ../api-contract/dist/verification.mjs
|
|
21
|
+
const count = z.number().int().nonnegative();
|
|
22
|
+
const metric = (value) => z.object({
|
|
23
|
+
value: z.union([value, z.literal("n/a")]),
|
|
24
|
+
detail: z.string()
|
|
25
|
+
});
|
|
26
|
+
const coverageBucket = z.object({
|
|
27
|
+
covered: count,
|
|
28
|
+
total: count,
|
|
29
|
+
uncovered: z.array(z.string())
|
|
30
|
+
}).refine((bucket) => bucket.covered <= bucket.total, "Covered count exceeds total");
|
|
31
|
+
const verificationCoverageSchema = z.object({
|
|
32
|
+
states: coverageBucket,
|
|
33
|
+
transitions: coverageBucket,
|
|
34
|
+
eventCases: coverageBucket,
|
|
35
|
+
percent: z.object({
|
|
36
|
+
states: z.number().min(0).max(100),
|
|
37
|
+
transitions: z.number().min(0).max(100),
|
|
38
|
+
eventCases: z.number().min(0).max(100),
|
|
39
|
+
overall: z.number().min(0).max(100)
|
|
40
|
+
})
|
|
41
|
+
});
|
|
42
|
+
const witnessStep = z.object({
|
|
43
|
+
index: count,
|
|
44
|
+
event: z.object({ type: z.string() }).catchall(z.unknown()),
|
|
45
|
+
expectedState: z.unknown()
|
|
46
|
+
});
|
|
47
|
+
const predicateResult = z.object({
|
|
48
|
+
state: z.string(),
|
|
49
|
+
status: z.enum(["passed", "failed"]),
|
|
50
|
+
signals: z.array(z.string()),
|
|
51
|
+
message: z.string().optional()
|
|
52
|
+
});
|
|
53
|
+
const verificationDivergenceSchema = z.object({
|
|
54
|
+
version: z.literal(1),
|
|
55
|
+
id: z.string(),
|
|
56
|
+
kind: z.enum(["expectation", "negative-probe"]),
|
|
57
|
+
classification: z.enum([
|
|
58
|
+
"unclassified",
|
|
59
|
+
"model-gap",
|
|
60
|
+
"behavior-gap",
|
|
61
|
+
"flake"
|
|
62
|
+
]),
|
|
63
|
+
pathId: z.string().optional(),
|
|
64
|
+
witness: z.object({
|
|
65
|
+
path: z.array(witnessStep),
|
|
66
|
+
stepCount: count
|
|
67
|
+
}),
|
|
68
|
+
minimalWitness: z.object({
|
|
69
|
+
steps: z.array(witnessStep),
|
|
70
|
+
pathLength: count,
|
|
71
|
+
candidatesTried: count,
|
|
72
|
+
reproduced: z.boolean()
|
|
73
|
+
}).optional(),
|
|
74
|
+
expected: z.object({
|
|
75
|
+
state: z.string(),
|
|
76
|
+
predicateResults: z.array(predicateResult),
|
|
77
|
+
refusal: z.object({
|
|
78
|
+
stateUnchanged: z.literal(true),
|
|
79
|
+
noNavigationOrMutation: z.literal(true)
|
|
80
|
+
}).optional()
|
|
81
|
+
}),
|
|
82
|
+
observed: z.object({
|
|
83
|
+
predicateResults: z.array(predicateResult),
|
|
84
|
+
snapshot: z.object({
|
|
85
|
+
url: z.string(),
|
|
86
|
+
title: z.string(),
|
|
87
|
+
visibleText: z.string(),
|
|
88
|
+
domHash: z.string(),
|
|
89
|
+
mutationCount: count.optional(),
|
|
90
|
+
requestCount: count.optional()
|
|
91
|
+
}),
|
|
92
|
+
error: z.string().optional()
|
|
93
|
+
}),
|
|
94
|
+
screenshotPath: z.string(),
|
|
95
|
+
provenance: z.object({
|
|
96
|
+
suite: z.string(),
|
|
97
|
+
modelVersion: z.string(),
|
|
98
|
+
modelHash: z.string(),
|
|
99
|
+
appBaseUrl: z.string(),
|
|
100
|
+
timestamp: z.string(),
|
|
101
|
+
traceFileRef: z.string()
|
|
102
|
+
}),
|
|
103
|
+
weakOracles: z.boolean()
|
|
104
|
+
}).passthrough();
|
|
105
|
+
const verificationPathSchema = z.object({
|
|
106
|
+
type: z.literal("path-result"),
|
|
107
|
+
id: z.string(),
|
|
108
|
+
status: z.enum(["passed", "failed"]),
|
|
109
|
+
witnessedStates: z.array(z.string()),
|
|
110
|
+
witnessedTransitions: z.array(z.string())
|
|
111
|
+
}).passthrough();
|
|
112
|
+
/** MBT version-one run report. Optional path/probe records support older reports. */
|
|
113
|
+
const verificationRunReportSchema = z.object({
|
|
114
|
+
version: z.literal(1),
|
|
115
|
+
suite: z.string(),
|
|
116
|
+
status: z.enum(["passed", "failed"]),
|
|
117
|
+
verdict: z.enum([
|
|
118
|
+
"conformant",
|
|
119
|
+
"divergent",
|
|
120
|
+
"inconclusive"
|
|
121
|
+
]),
|
|
122
|
+
coverage: z.enum(["shortest", "simple"]),
|
|
123
|
+
scenario: z.string().optional(),
|
|
124
|
+
startedAt: z.iso.datetime({ offset: true }),
|
|
125
|
+
completedAt: z.iso.datetime({ offset: true }),
|
|
126
|
+
durationMs: z.number().nonnegative(),
|
|
127
|
+
exitCode: z.number().int(),
|
|
128
|
+
weakOracles: z.boolean(),
|
|
129
|
+
phases: z.array(z.object({
|
|
130
|
+
name: z.enum(["exploration", "verification"]),
|
|
131
|
+
status: z.enum(["completed", "skipped"]),
|
|
132
|
+
aiFree: z.boolean()
|
|
133
|
+
})),
|
|
134
|
+
probePlan: z.object({
|
|
135
|
+
cap: count,
|
|
136
|
+
totalCandidates: count,
|
|
137
|
+
samplingRule: z.string()
|
|
138
|
+
}),
|
|
139
|
+
metrics: z.object({
|
|
140
|
+
fitness: metric(z.number()).optional(),
|
|
141
|
+
precision: metric(z.number()),
|
|
142
|
+
soundnessRatio: metric(z.number()),
|
|
143
|
+
frontierSize: metric(z.number()),
|
|
144
|
+
determinism: metric(z.boolean())
|
|
145
|
+
}).passthrough(),
|
|
146
|
+
modelCoverage: verificationCoverageSchema,
|
|
147
|
+
executedCoverage: verificationCoverageSchema,
|
|
148
|
+
paths: z.array(verificationPathSchema).optional(),
|
|
149
|
+
probes: z.array(z.object({
|
|
150
|
+
type: z.literal("probe-result"),
|
|
151
|
+
id: z.string(),
|
|
152
|
+
state: z.string(),
|
|
153
|
+
event: z.string(),
|
|
154
|
+
status: z.enum(["passed", "failed"])
|
|
155
|
+
})).optional(),
|
|
156
|
+
divergences: z.array(verificationDivergenceSchema),
|
|
157
|
+
ledger: z.array(z.object({
|
|
158
|
+
type: z.literal("divergence"),
|
|
159
|
+
timestamp: z.string(),
|
|
160
|
+
divergence: verificationDivergenceSchema
|
|
161
|
+
}))
|
|
162
|
+
}).passthrough().superRefine((report, context) => {
|
|
163
|
+
if (report.divergences.length > 0 !== (report.verdict === "divergent")) context.addIssue({
|
|
164
|
+
code: "custom",
|
|
165
|
+
path: ["verdict"],
|
|
166
|
+
message: "Verdict must agree with recorded divergences"
|
|
167
|
+
});
|
|
168
|
+
if (report.verdict === "conformant" && (report.status !== "passed" || report.exitCode !== 0 || report.paths?.some((item) => item.status === "failed") || report.probes?.some((item) => item.status === "failed") || report.metrics.determinism.value === false)) context.addIssue({
|
|
169
|
+
code: "custom",
|
|
170
|
+
path: ["verdict"],
|
|
171
|
+
message: "Conformant reports cannot contain failed execution results"
|
|
172
|
+
});
|
|
173
|
+
if (report.status === "passed" && (report.verdict !== "conformant" || report.exitCode !== 0)) context.addIssue({
|
|
174
|
+
code: "custom",
|
|
175
|
+
path: ["status"],
|
|
176
|
+
message: "A passed report requires a conformant verdict and zero exit code"
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
const verificationFailOnSchema = z.enum(["divergent", "inconclusive"]);
|
|
180
|
+
/** References are inert data. The host resolves them inside its execution boundary. */
|
|
181
|
+
const verificationRequestSchema = z.object({
|
|
182
|
+
model: z.discriminatedUnion("kind", [z.object({
|
|
183
|
+
kind: z.literal("source"),
|
|
184
|
+
content: z.string().min(1),
|
|
185
|
+
name: z.string().optional()
|
|
186
|
+
}), z.object({
|
|
187
|
+
kind: z.literal("machine"),
|
|
188
|
+
machineId: z.string().min(1)
|
|
189
|
+
})]),
|
|
190
|
+
bindings: z.object({
|
|
191
|
+
content: z.string(),
|
|
192
|
+
name: z.string().optional()
|
|
193
|
+
}).optional(),
|
|
194
|
+
baseline: z.discriminatedUnion("kind", [z.object({
|
|
195
|
+
kind: z.literal("report"),
|
|
196
|
+
report: verificationRunReportSchema
|
|
197
|
+
}), z.object({
|
|
198
|
+
kind: z.literal("reference"),
|
|
199
|
+
id: z.string().min(1)
|
|
200
|
+
})]).optional()
|
|
201
|
+
});
|
|
202
|
+
const verificationExecutionPolicy = {
|
|
203
|
+
aiFree: true,
|
|
204
|
+
network: "deny",
|
|
205
|
+
env: { MBT_VERIFY_AI_FREE: "1" }
|
|
206
|
+
};
|
|
207
|
+
/** Strict mode fails on divergences and inconclusive runs; default fails on divergences. */
|
|
208
|
+
function verificationExitCode(report, failOn = "divergent") {
|
|
209
|
+
return report.verdict === "divergent" || failOn === "inconclusive" && report.verdict === "inconclusive" ? 1 : 0;
|
|
210
|
+
}
|
|
211
|
+
function verificationWitnessEvents(divergence) {
|
|
212
|
+
return (divergence.minimalWitness?.reproduced ? divergence.minimalWitness.steps : divergence.witness.path).map((step) => step.event.type);
|
|
213
|
+
}
|
|
214
|
+
function formatVerificationSummary(report) {
|
|
215
|
+
const violations = report.divergences.filter((item) => item.kind === "negative-probe").length;
|
|
216
|
+
return [`${report.verdict}: ${report.divergences.length} divergence(s), ${violations} negative-probe violation(s), ${report.executedCoverage.percent.overall}% executed coverage`, ...report.divergences.map((item) => `${item.id}: witness: [${verificationWitnessEvents(item).join(", ")}]`)].join("\n");
|
|
217
|
+
}
|
|
21
218
|
|
|
22
219
|
//#endregion
|
|
23
|
-
//#region src/
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
220
|
+
//#region src/projectConfig.ts
|
|
221
|
+
const STATELY_CONFIG_FILE = "statelyai.json";
|
|
222
|
+
const STATELY_CONFIG_SCHEMA_URL = "https://stately.ai/schemas/statelyai.json";
|
|
223
|
+
const STATELY_CONFIG_VERSION = "1.0.0";
|
|
224
|
+
/**
|
|
225
|
+
* Base URL of the deployment that hosts the owned resource contract
|
|
226
|
+
* (`/api/v1/*`). Matches the SDK default.
|
|
227
|
+
*/
|
|
228
|
+
const DEFAULT_STATELY_API_BASE_URL = "https://editor.stately.ai";
|
|
229
|
+
/**
|
|
230
|
+
* Hosts of the legacy hosted Studio. `statelyai.json#studioUrl` values that
|
|
231
|
+
* point at them predate the editor-hosted API and must be redirected.
|
|
232
|
+
*/
|
|
233
|
+
const HOSTED_STUDIO_HOSTNAMES = new Set(["stately.ai", "www.stately.ai"]);
|
|
234
|
+
/**
|
|
235
|
+
* Maps a configured `statelyai.json#studioUrl` (or a `--base-url` flag) to the
|
|
236
|
+
* base URL of the editor API that serves our owned contract.
|
|
237
|
+
*
|
|
238
|
+
* Hosted legacy values (`https://stately.ai`, `http://www.stately.ai/`, ...)
|
|
239
|
+
* resolve to {@link DEFAULT_STATELY_API_BASE_URL}. Anything else is used as-is
|
|
240
|
+
* (minus trailing slashes): self-hosted editor deployments already point at the
|
|
241
|
+
* server that hosts `/api/v1`.
|
|
242
|
+
*
|
|
243
|
+
* This is a read-only compatibility shim: the CLI never rewrites
|
|
244
|
+
* `statelyai.json` because of it.
|
|
245
|
+
*/
|
|
246
|
+
function resolveApiBaseUrl(studioUrl) {
|
|
247
|
+
const trimmed = studioUrl?.trim();
|
|
248
|
+
if (!trimmed) return DEFAULT_STATELY_API_BASE_URL;
|
|
249
|
+
const parsed = tryParseUrl(trimmed) ?? tryParseUrl(`https://${trimmed}`);
|
|
250
|
+
if (parsed && HOSTED_STUDIO_HOSTNAMES.has(parsed.hostname.toLowerCase())) return DEFAULT_STATELY_API_BASE_URL;
|
|
251
|
+
return trimmed.replace(/\/+$/, "");
|
|
252
|
+
}
|
|
253
|
+
function tryParseUrl(value) {
|
|
254
|
+
try {
|
|
255
|
+
return new URL(value);
|
|
256
|
+
} catch {
|
|
257
|
+
return;
|
|
48
258
|
}
|
|
49
|
-
|
|
50
|
-
|
|
259
|
+
}
|
|
260
|
+
const DEFAULT_SOURCE_EXCLUDES = ["**/*.test.*", "**/*.spec.*"];
|
|
261
|
+
const CODE_SOURCE_EXTENSIONS = new Set([
|
|
262
|
+
".ts",
|
|
263
|
+
".tsx",
|
|
264
|
+
".js",
|
|
265
|
+
".jsx",
|
|
266
|
+
".mts",
|
|
267
|
+
".cts",
|
|
268
|
+
".mjs",
|
|
269
|
+
".cjs"
|
|
270
|
+
]);
|
|
271
|
+
const execFileAsync$1 = promisify(execFile);
|
|
272
|
+
const ALWAYS_IGNORED_DIRECTORIES = new Set([
|
|
273
|
+
".git",
|
|
274
|
+
"node_modules",
|
|
275
|
+
"dist",
|
|
276
|
+
"build",
|
|
277
|
+
"out",
|
|
278
|
+
"coverage",
|
|
279
|
+
".next"
|
|
280
|
+
]);
|
|
281
|
+
function createStatelyProjectConfig(options) {
|
|
282
|
+
const defaultXStateVersion = options.defaultXStateVersion ?? 5;
|
|
283
|
+
return {
|
|
284
|
+
$schema: STATELY_CONFIG_SCHEMA_URL,
|
|
285
|
+
version: STATELY_CONFIG_VERSION,
|
|
286
|
+
projectId: options.projectId ?? "",
|
|
287
|
+
studioUrl: options.studioUrl ?? "https://stately.ai",
|
|
288
|
+
defaultXStateVersion,
|
|
289
|
+
include: [],
|
|
290
|
+
exclude: [...DEFAULT_SOURCE_EXCLUDES]
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async function writeStatelyProjectConfig(configPath, config) {
|
|
294
|
+
const serialized = { ...config };
|
|
295
|
+
if (!config.projectId) {
|
|
296
|
+
delete serialized.projectId;
|
|
297
|
+
delete serialized.studioUrl;
|
|
51
298
|
}
|
|
52
|
-
};
|
|
53
|
-
function rawDataToBuffer(data) {
|
|
54
|
-
if (Array.isArray(data)) return Buffer.concat(data);
|
|
55
|
-
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
56
|
-
return Buffer.from(data);
|
|
299
|
+
await fsPromises.writeFile(configPath, `${JSON.stringify(serialized, null, 2)}\n`, "utf8");
|
|
57
300
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
301
|
+
function normalizeProjectConfig(config) {
|
|
302
|
+
const firstSource = config.sources?.[0];
|
|
303
|
+
return {
|
|
304
|
+
$schema: config.$schema ?? STATELY_CONFIG_SCHEMA_URL,
|
|
305
|
+
version: config.version ?? STATELY_CONFIG_VERSION,
|
|
306
|
+
projectId: config.projectId ?? "",
|
|
307
|
+
studioUrl: config.studioUrl ?? "https://stately.ai",
|
|
308
|
+
defaultXStateVersion: Math.max(5, config.defaultXStateVersion ?? 5),
|
|
309
|
+
...config.sourceUpdateStrategy === "preserve" || config.sourceUpdateStrategy === "rewrite" ? { sourceUpdateStrategy: config.sourceUpdateStrategy } : {},
|
|
310
|
+
include: [...config.include ?? firstSource?.include ?? []],
|
|
311
|
+
exclude: config.exclude == null ? [...firstSource?.exclude ?? DEFAULT_SOURCE_EXCLUDES] : [...config.exclude],
|
|
312
|
+
...config.newMachinesDir ? { newMachinesDir: config.newMachinesDir } : {}
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function getConfigRewriteStatus(config) {
|
|
316
|
+
if (!config.sources) return { needsRewrite: false };
|
|
317
|
+
if (config.sources.length > 1) return {
|
|
318
|
+
needsRewrite: true,
|
|
319
|
+
reason: "multiple-legacy-sources"
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
needsRewrite: true,
|
|
323
|
+
reason: "legacy-sources"
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
async function readStatelyProjectConfig(options = {}) {
|
|
327
|
+
const rootDir = path.resolve(options.cwd ?? process.cwd());
|
|
328
|
+
const configPath = path.resolve(rootDir, options.configPath ?? STATELY_CONFIG_FILE);
|
|
329
|
+
const raw = await fsPromises.readFile(configPath, "utf8");
|
|
330
|
+
const parsed = JSON.parse(raw);
|
|
331
|
+
const config = normalizeProjectConfig(parsed);
|
|
332
|
+
const rewriteStatus = getConfigRewriteStatus(parsed);
|
|
333
|
+
if (options.rewriteIfNeeded && rewriteStatus.reason === "legacy-sources") {
|
|
334
|
+
await writeStatelyProjectConfig(configPath, config);
|
|
335
|
+
return {
|
|
336
|
+
config,
|
|
337
|
+
configPath,
|
|
338
|
+
rootDir,
|
|
339
|
+
rewriteStatus: {
|
|
340
|
+
needsRewrite: false,
|
|
341
|
+
rewritten: true
|
|
342
|
+
}
|
|
66
343
|
};
|
|
67
|
-
const machines = options.machines ?? [{
|
|
68
|
-
id: `${options.rootUri}#0`,
|
|
69
|
-
fileName: options.fileName,
|
|
70
|
-
machineIndex: 0
|
|
71
|
-
}];
|
|
72
|
-
this.workspaceWatcher = createSharedWorkspaceWatcher(options.rootDir);
|
|
73
|
-
this.session = new EditorHostWorkspaceSession({
|
|
74
|
-
machines: machines.map((machine) => ({
|
|
75
|
-
id: machine.id,
|
|
76
|
-
name: machine.name,
|
|
77
|
-
source: {
|
|
78
|
-
rootUri: fileNameToUri(machine.fileName),
|
|
79
|
-
rootFileName: machine.fileName,
|
|
80
|
-
rootDir: options.rootDir,
|
|
81
|
-
machineIndex: machine.machineIndex,
|
|
82
|
-
collectDocuments: (extraUris) => collectSourceDocuments({
|
|
83
|
-
rootFileName: machine.fileName,
|
|
84
|
-
extraUris: [...extraUris ?? []]
|
|
85
|
-
}),
|
|
86
|
-
applyReplacements: async (replacements, rootUri) => {
|
|
87
|
-
await applyFileReplacements(replacements, rootUri);
|
|
88
|
-
return [...new Set(replacements.map((replacement) => replacement.uri ?? rootUri))];
|
|
89
|
-
},
|
|
90
|
-
watchSavedDocuments: (listener) => this.workspaceWatcher.subscribe(listener)
|
|
91
|
-
}
|
|
92
|
-
})),
|
|
93
|
-
currentMachineId: machines[0]?.id,
|
|
94
|
-
workspaceName: path$1.basename(options.rootDir),
|
|
95
|
-
projectWorkspaceOpen: false,
|
|
96
|
-
sourceUpdateStrategy: options.sourceUpdateStrategy ?? "preserve",
|
|
97
|
-
sync: createRemoteEditorSyncBackend({
|
|
98
|
-
editorUrl: options.editorUrl,
|
|
99
|
-
apiKey: options.apiKey,
|
|
100
|
-
fetch: (input, init) => fetchEditorHost(input instanceof URL ? input : new URL(String(input)), init)
|
|
101
|
-
}),
|
|
102
|
-
send,
|
|
103
|
-
apiKey: options.apiKey,
|
|
104
|
-
client: {
|
|
105
|
-
name: "statelyai",
|
|
106
|
-
version: CLI_VERSION
|
|
107
|
-
},
|
|
108
|
-
showError: (message) => console.error(`Stately Editor: ${message}`)
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
receiveMessage(message) {
|
|
112
|
-
this.log("recv", message.type, message);
|
|
113
|
-
return this.session.receive(message);
|
|
114
344
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
function redactDebugPayload(payload) {
|
|
124
|
-
if (Array.isArray(payload)) return payload.map(redactDebugPayload);
|
|
125
|
-
if (!payload || typeof payload !== "object") return payload;
|
|
126
|
-
return Object.fromEntries(Object.entries(payload).map(([key, value]) => [key, /^(?:apiKey|accessToken|refreshToken|token|authorization)$/i.test(key) ? "[REDACTED]" : redactDebugPayload(value)]));
|
|
345
|
+
return {
|
|
346
|
+
config,
|
|
347
|
+
configPath,
|
|
348
|
+
rootDir,
|
|
349
|
+
rewriteStatus
|
|
350
|
+
};
|
|
127
351
|
}
|
|
128
|
-
async function
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
352
|
+
async function walkFiles(rootDir, currentDir = rootDir) {
|
|
353
|
+
const entries = await fsPromises.readdir(currentDir, { withFileTypes: true });
|
|
354
|
+
const files = [];
|
|
355
|
+
for (const entry of entries) {
|
|
356
|
+
if (ALWAYS_IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
357
|
+
const absolutePath = path.join(currentDir, entry.name);
|
|
358
|
+
if (entry.isDirectory()) {
|
|
359
|
+
files.push(...await walkFiles(rootDir, absolutePath));
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (!entry.isFile()) continue;
|
|
363
|
+
files.push(path.relative(rootDir, absolutePath).replace(/\\/g, "/"));
|
|
364
|
+
}
|
|
365
|
+
return files;
|
|
366
|
+
}
|
|
367
|
+
async function filterGitIgnoredPaths(rootDir, relativeFiles) {
|
|
368
|
+
if (relativeFiles.length === 0) return relativeFiles;
|
|
369
|
+
try {
|
|
370
|
+
const { stdout } = await execFileAsync$1("git", ["check-ignore", ...relativeFiles], { cwd: rootDir });
|
|
371
|
+
const ignoredEntries = stdout.split(/\r?\n/).map((entry) => entry.trim()).filter(Boolean);
|
|
372
|
+
if (ignoredEntries.length === 0) return relativeFiles;
|
|
373
|
+
const ignoredPaths = new Set(ignoredEntries);
|
|
374
|
+
return relativeFiles.filter((relativePath) => {
|
|
375
|
+
return !ignoredPaths.has(relativePath);
|
|
376
|
+
});
|
|
377
|
+
} catch {
|
|
378
|
+
return relativeFiles;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function isCodeSourceFile(relativePath) {
|
|
382
|
+
return CODE_SOURCE_EXTENSIONS.has(path.extname(relativePath).toLowerCase());
|
|
383
|
+
}
|
|
384
|
+
function expandBraces(pattern) {
|
|
385
|
+
const match = pattern.match(/\{([^{}]+)\}/);
|
|
386
|
+
if (!match || match.index == null) return [pattern];
|
|
387
|
+
const [token, inner] = match;
|
|
388
|
+
return inner.split(",").flatMap((variant) => expandBraces(`${pattern.slice(0, match.index)}${variant}${pattern.slice(match.index + token.length)}`));
|
|
389
|
+
}
|
|
390
|
+
function globToRegExp(pattern) {
|
|
391
|
+
let regex = "^";
|
|
392
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
393
|
+
const char = pattern[index];
|
|
394
|
+
const next = pattern[index + 1];
|
|
395
|
+
if (char === "*") {
|
|
396
|
+
if (next === "*") {
|
|
397
|
+
const slashAfterGlobstar = pattern[index + 2] === "/";
|
|
398
|
+
regex += slashAfterGlobstar ? "(?:.*/)?" : ".*";
|
|
399
|
+
index += slashAfterGlobstar ? 2 : 1;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
regex += "[^/]*";
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (char === "?") {
|
|
406
|
+
regex += "[^/]";
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if ("\\.[]{}()+-^$|".includes(char)) {
|
|
410
|
+
regex += `\\${char}`;
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
regex += char;
|
|
414
|
+
}
|
|
415
|
+
regex += "$";
|
|
416
|
+
return new RegExp(regex);
|
|
417
|
+
}
|
|
418
|
+
function matchesGlob(relativePath, pattern) {
|
|
419
|
+
return expandBraces(pattern).some((expanded) => globToRegExp(expanded).test(relativePath));
|
|
420
|
+
}
|
|
421
|
+
function matchesAny(patterns, relativePath) {
|
|
422
|
+
return (patterns ?? []).some((pattern) => matchesGlob(relativePath, pattern));
|
|
423
|
+
}
|
|
424
|
+
function isTrackedByStatelyProjectConfig(config, relativePath) {
|
|
425
|
+
return matchesAny(config.include, relativePath) && !matchesAny(config.exclude ?? DEFAULT_SOURCE_EXCLUDES, relativePath);
|
|
426
|
+
}
|
|
427
|
+
async function discoverCodeSourceFiles(options = {}) {
|
|
428
|
+
const rootDir = path.resolve(options.cwd ?? process.cwd());
|
|
429
|
+
return (await filterGitIgnoredPaths(rootDir, await walkFiles(rootDir))).filter((relativePath) => isCodeSourceFile(relativePath)).filter((relativePath) => !matchesAny(DEFAULT_SOURCE_EXCLUDES, relativePath)).sort((left, right) => left.localeCompare(right));
|
|
430
|
+
}
|
|
431
|
+
function createSuggestedConfig(include) {
|
|
432
|
+
return {
|
|
433
|
+
include: [include],
|
|
434
|
+
exclude: [...DEFAULT_SOURCE_EXCLUDES]
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
function toExtensionToken(extname) {
|
|
438
|
+
return extname.startsWith(".") ? extname.slice(1) : extname;
|
|
439
|
+
}
|
|
440
|
+
function formatExtensionPattern(extnames) {
|
|
441
|
+
const unique = [...new Set(extnames.map(toExtensionToken))].sort();
|
|
442
|
+
if (unique.length === 1) return `.${unique[0]}`;
|
|
443
|
+
return `.{${unique.join(",")}}`;
|
|
444
|
+
}
|
|
445
|
+
function getSuggestionRoot(relativePath) {
|
|
446
|
+
if (relativePath.startsWith("src/")) return "src/";
|
|
447
|
+
const packageMatch = relativePath.match(/^packages\/([^/]+)\/src\//);
|
|
448
|
+
if (packageMatch) return `packages/${packageMatch[1]}/src/`;
|
|
449
|
+
const appMatch = relativePath.match(/^apps\/([^/]+)\/src\//);
|
|
450
|
+
if (appMatch) return `apps/${appMatch[1]}/src/`;
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
function getCommonDirectory(relativePaths) {
|
|
454
|
+
const [firstDirectory, ...remainingDirectories] = relativePaths.map((relativePath) => path.posix.dirname(relativePath).split("/"));
|
|
455
|
+
if (!firstDirectory) return;
|
|
456
|
+
const commonSegments = [];
|
|
457
|
+
for (const [index, segment] of firstDirectory.entries()) {
|
|
458
|
+
if (!remainingDirectories.every((directory) => directory[index] === segment)) break;
|
|
459
|
+
commonSegments.push(segment);
|
|
460
|
+
}
|
|
461
|
+
return commonSegments.length > 0 ? commonSegments.join("/") : void 0;
|
|
462
|
+
}
|
|
463
|
+
function suggestGroupedInclude(relativePaths, root) {
|
|
464
|
+
const extPattern = formatExtensionPattern(relativePaths.map((relativePath) => path.posix.extname(relativePath)));
|
|
465
|
+
if (relativePaths.map((relativePath) => path.posix.basename(relativePath, path.posix.extname(relativePath))).every((basename) => basename.toLowerCase().endsWith("machine"))) return `${root}**/*machine${extPattern}`;
|
|
466
|
+
const commonDirectory = getCommonDirectory(relativePaths);
|
|
467
|
+
if (commonDirectory) return `${commonDirectory}/${relativePaths.every((relativePath) => path.posix.dirname(relativePath) === commonDirectory) ? "*" : "**/*"}${extPattern}`;
|
|
468
|
+
return `${root}**/*${extPattern}`;
|
|
469
|
+
}
|
|
470
|
+
function suggestStatelySourceConfigs(relativePaths) {
|
|
471
|
+
const pending = new Set(relativePaths.map((relativePath) => relativePath.replace(/\\/g, "/")).filter(Boolean));
|
|
472
|
+
const suggestions = [];
|
|
473
|
+
const addSuggestion = (include, predicate) => {
|
|
474
|
+
const matched = [...pending].filter(predicate);
|
|
475
|
+
if (matched.length === 0) return;
|
|
476
|
+
suggestions.push(createSuggestedConfig(include));
|
|
477
|
+
for (const matchedPath of matched) pending.delete(matchedPath);
|
|
478
|
+
};
|
|
479
|
+
const groupedRoots = /* @__PURE__ */ new Map();
|
|
480
|
+
for (const relativePath of pending) {
|
|
481
|
+
const root = getSuggestionRoot(relativePath);
|
|
482
|
+
if (!root) continue;
|
|
483
|
+
const bucket = groupedRoots.get(root) ?? [];
|
|
484
|
+
bucket.push(relativePath);
|
|
485
|
+
groupedRoots.set(root, bucket);
|
|
486
|
+
}
|
|
487
|
+
for (const [root, groupedPaths] of [...groupedRoots.entries()].sort((left, right) => left[0].localeCompare(right[0]))) addSuggestion(suggestGroupedInclude(groupedPaths, root), (relativePath) => groupedPaths.includes(relativePath));
|
|
488
|
+
const byDirectory = /* @__PURE__ */ new Map();
|
|
489
|
+
for (const relativePath of pending) {
|
|
490
|
+
const directory = path.posix.dirname(relativePath);
|
|
491
|
+
const key = directory === "." ? relativePath : directory;
|
|
492
|
+
const bucket = byDirectory.get(key) ?? [];
|
|
493
|
+
bucket.push(relativePath);
|
|
494
|
+
byDirectory.set(key, bucket);
|
|
495
|
+
}
|
|
496
|
+
for (const [key, matchedPaths] of [...byDirectory.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
|
|
497
|
+
if (matchedPaths.length > 1 && key !== "." && key !== matchedPaths[0]) {
|
|
498
|
+
addSuggestion(`${key}/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}`, (relativePath) => relativePath.startsWith(`${key}/`));
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const exactPath = matchedPaths[0];
|
|
502
|
+
addSuggestion(exactPath, (relativePath) => relativePath === exactPath);
|
|
503
|
+
}
|
|
504
|
+
return suggestions;
|
|
505
|
+
}
|
|
506
|
+
async function discoverStatelySourceFiles(options = {}) {
|
|
507
|
+
const { config, rootDir } = options.config ? {
|
|
508
|
+
config: options.config,
|
|
509
|
+
rootDir: path.resolve(options.cwd ?? process.cwd())
|
|
510
|
+
} : await readStatelyProjectConfig(options);
|
|
511
|
+
const relativeFiles = await filterGitIgnoredPaths(rootDir, await walkFiles(rootDir));
|
|
512
|
+
const discovered = /* @__PURE__ */ new Map();
|
|
513
|
+
const source = {
|
|
514
|
+
include: config.include,
|
|
515
|
+
exclude: config.exclude ?? DEFAULT_SOURCE_EXCLUDES,
|
|
516
|
+
format: "xstate",
|
|
517
|
+
xstateVersion: config.defaultXStateVersion
|
|
518
|
+
};
|
|
519
|
+
for (const relativePath of relativeFiles) {
|
|
520
|
+
if (!matchesAny(source.include, relativePath)) continue;
|
|
521
|
+
if (matchesAny(source.exclude, relativePath)) continue;
|
|
522
|
+
const filePath = path.join(rootDir, relativePath);
|
|
523
|
+
if (!discovered.has(filePath)) discovered.set(filePath, {
|
|
524
|
+
filePath,
|
|
525
|
+
relativePath,
|
|
526
|
+
source
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return [...discovered.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/verificationJUnit.ts
|
|
534
|
+
function xml(text) {
|
|
535
|
+
return text.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uFFFE\uFFFF]/gu, "�").replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
536
|
+
}
|
|
537
|
+
/** One case per recorded path/probe. Unattributed divergences remain separate cases. */
|
|
538
|
+
function formatVerificationJUnit(report) {
|
|
539
|
+
if (!report.paths) throw new Error("JUnit requires path records. This report predates path reporting.");
|
|
540
|
+
const records = [...report.paths, ...report.probes ?? []];
|
|
541
|
+
const counts = /* @__PURE__ */ new Map();
|
|
542
|
+
for (const record of records) counts.set(record.id, (counts.get(record.id) ?? 0) + 1);
|
|
543
|
+
const hasUniqueRecord = (id) => id !== void 0 && counts.get(id) === 1;
|
|
544
|
+
const cases = [];
|
|
545
|
+
let errors = 0;
|
|
546
|
+
const failures = (items) => items.map((item) => `<failure type="${xml(item.kind)}" message="${xml(`${item.id}; witness: [${verificationWitnessEvents(item).join(", ")}]`)}">${xml(item.observed.error ?? `Expected ${item.expected.state}`)}</failure>`).join("");
|
|
547
|
+
for (const record of records) {
|
|
548
|
+
const divergences = report.divergences.filter((item) => item.pathId === record.id && hasUniqueRecord(item.pathId));
|
|
549
|
+
const failedWithoutWitness = record.status === "failed" && !divergences.length;
|
|
550
|
+
if (failedWithoutWitness) errors++;
|
|
551
|
+
cases.push(`<testcase name="${xml(record.id)}" classname="${xml(report.suite)}">${failures(divergences)}${failedWithoutWitness ? "<error type=\"unattributed_path_failure\" message=\"Path failed without a linked divergence; consult the run report.\"/>" : ""}</testcase>`);
|
|
552
|
+
}
|
|
553
|
+
for (const item of report.divergences.filter((item) => !hasUniqueRecord(item.pathId))) cases.push(`<testcase name="${xml(item.id)}" classname="${xml(report.suite)}">${failures([item])}</testcase>`);
|
|
554
|
+
if (report.verdict === "inconclusive" || report.status === "failed" && !errors && !report.divergences.length) {
|
|
555
|
+
errors++;
|
|
556
|
+
cases.push("<testcase name=\"verification incomplete\"><error type=\"inconclusive\" message=\"Verification did not produce a conclusive successful run.\"/></testcase>");
|
|
557
|
+
}
|
|
558
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="${xml(report.suite)}" tests="${cases.length}" failures="${report.divergences.length}" errors="${errors}" time="${report.durationMs / 1e3}">\n${cases.join("\n")}\n</testsuite>\n`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
//#endregion
|
|
562
|
+
//#region src/verification.ts
|
|
563
|
+
const maxInputBytes = 10 * 1024 * 1024;
|
|
564
|
+
async function readInput(file) {
|
|
565
|
+
if ((await fsPromises.stat(file)).size > maxInputBytes) throw new Error(`Verification input exceeds ${maxInputBytes} bytes: ${file}`);
|
|
566
|
+
return fsPromises.readFile(file, "utf8");
|
|
567
|
+
}
|
|
568
|
+
async function localFile(root, reference) {
|
|
569
|
+
const file = path.resolve(root, reference);
|
|
570
|
+
try {
|
|
571
|
+
if (!(await fsPromises.stat(file)).isFile()) throw new Error(`Not a file: ${reference}`);
|
|
572
|
+
return file;
|
|
573
|
+
} catch (error) {
|
|
574
|
+
if (error.code !== "ENOENT") throw error;
|
|
575
|
+
if (reference.includes("/") || reference.includes("\\") || path.extname(reference) || reference.startsWith(".")) throw new Error(`Verification input not found: ${reference}`);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/** Read local source as data; never import or execute model/bindings modules. */
|
|
580
|
+
async function resolveVerificationRequest(options) {
|
|
581
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
582
|
+
const inputFiles = [];
|
|
583
|
+
let model = options.model;
|
|
584
|
+
if (!model) {
|
|
585
|
+
const project = await readStatelyProjectConfig({
|
|
586
|
+
cwd,
|
|
587
|
+
configPath: options.configPath
|
|
588
|
+
});
|
|
589
|
+
const files = await discoverStatelySourceFiles({
|
|
590
|
+
cwd: project.rootDir,
|
|
591
|
+
config: project.config
|
|
592
|
+
});
|
|
593
|
+
if (files.length !== 1) throw new Error(`Expected one configured model, found ${files.length}. Select one with --model.`);
|
|
594
|
+
model = files[0].filePath;
|
|
595
|
+
}
|
|
596
|
+
const modelFile = await localFile(cwd, model);
|
|
597
|
+
const request = { model: modelFile ? {
|
|
598
|
+
kind: "source",
|
|
599
|
+
content: await readInput(modelFile),
|
|
600
|
+
name: path.basename(modelFile)
|
|
601
|
+
} : {
|
|
602
|
+
kind: "machine",
|
|
603
|
+
machineId: model.replace(/^machine:/, "")
|
|
604
|
+
} };
|
|
605
|
+
if (modelFile) inputFiles.push(modelFile);
|
|
606
|
+
if (options.bindings) {
|
|
607
|
+
const file = path.resolve(cwd, options.bindings);
|
|
608
|
+
request.bindings = {
|
|
609
|
+
content: await readInput(file),
|
|
610
|
+
name: path.basename(file)
|
|
611
|
+
};
|
|
612
|
+
inputFiles.push(file);
|
|
613
|
+
}
|
|
614
|
+
if (options.baseline) {
|
|
615
|
+
const file = await localFile(cwd, options.baseline);
|
|
616
|
+
request.baseline = file ? {
|
|
617
|
+
kind: "report",
|
|
618
|
+
report: verificationRunReportSchema.parse(JSON.parse(await readInput(file)))
|
|
619
|
+
} : {
|
|
620
|
+
kind: "reference",
|
|
621
|
+
id: options.baseline
|
|
622
|
+
};
|
|
623
|
+
if (file) inputFiles.push(file);
|
|
624
|
+
}
|
|
625
|
+
return {
|
|
626
|
+
request: verificationRequestSchema.parse(request),
|
|
627
|
+
inputFiles
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
async function verifyProject(options = {}) {
|
|
631
|
+
const cwd = path.resolve(options.cwd ?? process.cwd());
|
|
632
|
+
let report;
|
|
633
|
+
let inputFiles;
|
|
634
|
+
if (options.fromReport) {
|
|
635
|
+
if (options.model || options.bindings || options.baseline || options.configPath) throw new Error("--from-report cannot be combined with model, bindings, baseline, or config inputs.");
|
|
636
|
+
const file = path.resolve(cwd, options.fromReport);
|
|
637
|
+
report = verificationRunReportSchema.parse(JSON.parse(await readInput(file)));
|
|
638
|
+
inputFiles = [file];
|
|
639
|
+
} else {
|
|
640
|
+
if (!options.execute) throw new Error("Live verification is not configured. Use --from-report <file> to process an existing run report, or supply a VerificationExecutor to verifyProject().");
|
|
641
|
+
const resolved = await resolveVerificationRequest(options);
|
|
642
|
+
inputFiles = resolved.inputFiles;
|
|
643
|
+
report = verificationRunReportSchema.parse(await options.execute(resolved.request, verificationExecutionPolicy));
|
|
644
|
+
}
|
|
645
|
+
const format = options.reportFormat ?? "json";
|
|
646
|
+
const output = format === "junit" ? formatVerificationJUnit(report) : `${JSON.stringify(report, null, 2)}\n`;
|
|
647
|
+
const destination = options.report ?? `statelyai-verify-report.${format === "junit" ? "xml" : "json"}`;
|
|
648
|
+
if (destination !== "-") {
|
|
649
|
+
const file = path.resolve(cwd, destination);
|
|
650
|
+
const existing = await fsPromises.realpath(file).catch((error) => {
|
|
651
|
+
if (error.code === "ENOENT") return file;
|
|
652
|
+
throw error;
|
|
653
|
+
});
|
|
654
|
+
for (const input of inputFiles) if (existing === await fsPromises.realpath(input)) throw new Error("Report output cannot overwrite a verification input.");
|
|
655
|
+
await fsPromises.mkdir(path.dirname(file), { recursive: true });
|
|
656
|
+
await fsPromises.writeFile(file, output, "utf8");
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
report,
|
|
660
|
+
output,
|
|
661
|
+
destination,
|
|
662
|
+
summary: `${options.fromReport ? "Saved report: " : ""}${formatVerificationSummary(report)}`,
|
|
663
|
+
exitCode: verificationExitCode(report, options.failOn)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
//#endregion
|
|
668
|
+
//#region src/verifyCommand.ts
|
|
669
|
+
var VerifyCommand = class VerifyCommand extends Command {
|
|
670
|
+
static enableJsonFlag = false;
|
|
671
|
+
static summary = "Validate a verification report, print its verdict, and export JSON or JUnit.";
|
|
672
|
+
static description = "Saved-report processing works offline. Live execution requires a configured verification adapter.";
|
|
673
|
+
static flags = {
|
|
674
|
+
model: Flags.string({ description: "Model file or registry machine ID (live execution)" }),
|
|
675
|
+
bindings: Flags.string({ description: "Bindings file (live execution)" }),
|
|
676
|
+
baseline: Flags.string({ description: "Baseline report file or reference ID (live execution)" }),
|
|
677
|
+
config: Flags.string({ description: "Path to statelyai.json (live execution)" }),
|
|
678
|
+
"from-report": Flags.string({ description: "Process an existing run report without executing verification" }),
|
|
679
|
+
report: Flags.string({ description: "Output file, or - for stdout; defaults to statelyai-verify-report.json/xml" }),
|
|
680
|
+
"report-format": Flags.string({
|
|
681
|
+
options: ["json", "junit"],
|
|
682
|
+
default: "json"
|
|
683
|
+
}),
|
|
684
|
+
"fail-on": Flags.string({
|
|
685
|
+
options: ["divergent", "inconclusive"],
|
|
686
|
+
default: "divergent",
|
|
687
|
+
description: "inconclusive also fails on divergences"
|
|
688
|
+
})
|
|
689
|
+
};
|
|
690
|
+
async run() {
|
|
691
|
+
const { flags } = await this.parse(VerifyCommand);
|
|
692
|
+
let result;
|
|
693
|
+
try {
|
|
694
|
+
result = await verifyProject({
|
|
695
|
+
model: flags.model,
|
|
696
|
+
bindings: flags.bindings,
|
|
697
|
+
baseline: flags.baseline,
|
|
698
|
+
configPath: flags.config,
|
|
699
|
+
fromReport: flags["from-report"],
|
|
700
|
+
report: flags.report,
|
|
701
|
+
reportFormat: flags["report-format"],
|
|
702
|
+
failOn: flags["fail-on"]
|
|
703
|
+
});
|
|
704
|
+
} catch (error) {
|
|
705
|
+
this.error(error instanceof Error ? error.message : String(error), { exit: 2 });
|
|
706
|
+
}
|
|
707
|
+
this.log(result.destination === "-" ? result.output.trimEnd() : result.summary);
|
|
708
|
+
if (result.exitCode) this.exit(result.exitCode);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/version.ts
|
|
714
|
+
const CLI_VERSION = "0.11.0";
|
|
715
|
+
|
|
716
|
+
//#endregion
|
|
717
|
+
//#region src/cliHost.ts
|
|
718
|
+
const DEFAULT_SYNC_MAX_FILES = 150;
|
|
719
|
+
const DEFAULT_SYNC_MAX_BYTES = 2e6;
|
|
720
|
+
const supportedDocumentPattern = /\.(?:c|m)?(?:jsx?|tsx?)$|\.jsonc?$|\.ya?ml$|\.mmd$|\.mermaid$/i;
|
|
721
|
+
var WebSocketClient = class {
|
|
722
|
+
listeners = /* @__PURE__ */ new Set();
|
|
723
|
+
constructor(socket) {
|
|
724
|
+
this.socket = socket;
|
|
725
|
+
socket.on("message", (data, isBinary) => {
|
|
726
|
+
if (isBinary) return;
|
|
727
|
+
let message;
|
|
728
|
+
try {
|
|
729
|
+
message = JSON.parse(rawDataToBuffer(data).toString("utf8"));
|
|
730
|
+
} catch {
|
|
731
|
+
socket.close(1007, "Invalid JSON payload");
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
for (const listener of this.listeners) listener(message);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
onMessage(listener) {
|
|
738
|
+
this.listeners.add(listener);
|
|
739
|
+
}
|
|
740
|
+
send(message) {
|
|
741
|
+
if (this.socket.readyState === WebSocket.OPEN) this.socket.send(JSON.stringify(message));
|
|
742
|
+
}
|
|
743
|
+
close() {
|
|
744
|
+
if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) this.socket.close();
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
function rawDataToBuffer(data) {
|
|
748
|
+
if (Array.isArray(data)) return Buffer.concat(data);
|
|
749
|
+
if (data instanceof ArrayBuffer) return Buffer.from(data);
|
|
750
|
+
return Buffer.from(data);
|
|
751
|
+
}
|
|
752
|
+
var RemoteEditorSession = class {
|
|
753
|
+
session;
|
|
754
|
+
workspaceWatcher;
|
|
755
|
+
constructor(options, sendMessage) {
|
|
756
|
+
this.options = options;
|
|
757
|
+
const send = (message) => {
|
|
758
|
+
this.log("send", message.type, message);
|
|
759
|
+
sendMessage(message);
|
|
760
|
+
};
|
|
761
|
+
const machines = options.machines ?? [{
|
|
762
|
+
id: `${options.rootUri}#0`,
|
|
763
|
+
fileName: options.fileName,
|
|
764
|
+
machineIndex: 0
|
|
765
|
+
}];
|
|
766
|
+
this.workspaceWatcher = createSharedWorkspaceWatcher(options.rootDir);
|
|
767
|
+
this.session = new EditorHostWorkspaceSession({
|
|
768
|
+
machines: machines.map((machine) => ({
|
|
769
|
+
id: machine.id,
|
|
770
|
+
name: machine.name,
|
|
771
|
+
source: {
|
|
772
|
+
rootUri: fileNameToUri(machine.fileName),
|
|
773
|
+
rootFileName: machine.fileName,
|
|
774
|
+
rootDir: options.rootDir,
|
|
775
|
+
machineIndex: machine.machineIndex,
|
|
776
|
+
collectDocuments: (extraUris) => collectSourceDocuments({
|
|
777
|
+
rootFileName: machine.fileName,
|
|
778
|
+
extraUris: [...extraUris ?? []]
|
|
779
|
+
}),
|
|
780
|
+
applyReplacements: async (replacements, rootUri) => {
|
|
781
|
+
await applyFileReplacements(replacements, rootUri);
|
|
782
|
+
return [...new Set(replacements.map((replacement) => replacement.uri ?? rootUri))];
|
|
783
|
+
},
|
|
784
|
+
watchSavedDocuments: (listener) => this.workspaceWatcher.subscribe(listener)
|
|
785
|
+
}
|
|
786
|
+
})),
|
|
787
|
+
currentMachineId: machines[0]?.id,
|
|
788
|
+
workspaceName: path$1.basename(options.rootDir),
|
|
789
|
+
projectWorkspaceOpen: false,
|
|
790
|
+
sourceUpdateStrategy: options.sourceUpdateStrategy ?? "preserve",
|
|
791
|
+
sync: createRemoteEditorSyncBackend({
|
|
792
|
+
editorUrl: options.editorUrl,
|
|
793
|
+
apiKey: options.apiKey,
|
|
794
|
+
fetch: (input, init) => fetchEditorHost(input instanceof URL ? input : new URL(String(input)), init)
|
|
795
|
+
}),
|
|
796
|
+
send,
|
|
797
|
+
apiKey: options.apiKey,
|
|
798
|
+
client: {
|
|
799
|
+
name: "statelyai",
|
|
800
|
+
version: CLI_VERSION
|
|
801
|
+
},
|
|
802
|
+
showError: (message) => console.error(`Stately Editor: ${message}`)
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
receiveMessage(message) {
|
|
806
|
+
this.log("recv", message.type, message);
|
|
807
|
+
return this.session.receive(message);
|
|
808
|
+
}
|
|
809
|
+
dispose() {
|
|
810
|
+
this.session.dispose();
|
|
811
|
+
this.workspaceWatcher.dispose();
|
|
812
|
+
}
|
|
813
|
+
log(direction, type, payload) {
|
|
814
|
+
if (this.options.debug) console.error(`[${direction}] ${type}`, redactDebugPayload(payload) ?? "");
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
function redactDebugPayload(payload) {
|
|
818
|
+
if (Array.isArray(payload)) return payload.map(redactDebugPayload);
|
|
819
|
+
if (!payload || typeof payload !== "object") return payload;
|
|
820
|
+
return Object.fromEntries(Object.entries(payload).map(([key, value]) => [key, /^(?:apiKey|accessToken|refreshToken|token|authorization)$/i.test(key) ? "[REDACTED]" : redactDebugPayload(value)]));
|
|
821
|
+
}
|
|
822
|
+
async function openEditor(options) {
|
|
823
|
+
const fileName = path$1.resolve(options.fileName);
|
|
824
|
+
const machines = (options.machines ?? [{
|
|
825
|
+
id: `${fileNameToUri(fileName)}#0`,
|
|
826
|
+
fileName,
|
|
827
|
+
machineIndex: 0
|
|
134
828
|
}]).map((machine) => ({
|
|
135
829
|
...machine,
|
|
136
830
|
fileName: path$1.resolve(machine.fileName)
|
|
@@ -624,99 +1318,32 @@ async function writeFileStoredCredential(credential) {
|
|
|
624
1318
|
location: credentialsPath
|
|
625
1319
|
};
|
|
626
1320
|
}
|
|
627
|
-
async function deleteFileCredentials() {
|
|
628
|
-
try {
|
|
629
|
-
await promises.unlink(getCredentialsFilePath());
|
|
630
|
-
return true;
|
|
631
|
-
} catch {
|
|
632
|
-
return false;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
async function readMacOSStoredCredential() {
|
|
636
|
-
if (process.platform !== "darwin") return;
|
|
637
|
-
try {
|
|
638
|
-
const { stdout } = await execFile$1("security", [
|
|
639
|
-
"find-generic-password",
|
|
640
|
-
"-s",
|
|
641
|
-
SERVICE_NAME,
|
|
642
|
-
"-a",
|
|
643
|
-
ACCOUNT_NAME,
|
|
644
|
-
"-w"
|
|
645
|
-
]);
|
|
646
|
-
return parseStoredCredential(stdout, "macos-keychain", "macOS Keychain");
|
|
647
|
-
} catch {
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
async function writeMacOSKeychain(apiKey) {
|
|
652
|
-
const stored = await writeMacOSStoredCredential({
|
|
653
|
-
type: "api_key",
|
|
654
|
-
token: apiKey
|
|
655
|
-
});
|
|
656
|
-
return stored ? {
|
|
657
|
-
apiKey,
|
|
658
|
-
backend: stored.backend,
|
|
659
|
-
location: stored.location
|
|
660
|
-
} : void 0;
|
|
661
|
-
}
|
|
662
|
-
async function writeMacOSStoredCredential(credential) {
|
|
663
|
-
if (process.platform !== "darwin") return;
|
|
664
|
-
const payload = {
|
|
665
|
-
version: 1,
|
|
666
|
-
credential
|
|
667
|
-
};
|
|
668
|
-
try {
|
|
669
|
-
await execFile$1("security", [
|
|
670
|
-
"add-generic-password",
|
|
671
|
-
"-U",
|
|
672
|
-
"-s",
|
|
673
|
-
SERVICE_NAME,
|
|
674
|
-
"-a",
|
|
675
|
-
ACCOUNT_NAME,
|
|
676
|
-
"-w",
|
|
677
|
-
JSON.stringify(payload)
|
|
678
|
-
]);
|
|
679
|
-
return {
|
|
680
|
-
...credential ? { credential } : {},
|
|
681
|
-
backend: "macos-keychain",
|
|
682
|
-
location: "macOS Keychain"
|
|
683
|
-
};
|
|
684
|
-
} catch {
|
|
685
|
-
return;
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
async function deleteMacOSKeychain() {
|
|
689
|
-
if (process.platform !== "darwin") return false;
|
|
1321
|
+
async function deleteFileCredentials() {
|
|
690
1322
|
try {
|
|
691
|
-
await
|
|
692
|
-
"delete-generic-password",
|
|
693
|
-
"-s",
|
|
694
|
-
SERVICE_NAME,
|
|
695
|
-
"-a",
|
|
696
|
-
ACCOUNT_NAME
|
|
697
|
-
]);
|
|
1323
|
+
await promises.unlink(getCredentialsFilePath());
|
|
698
1324
|
return true;
|
|
699
1325
|
} catch {
|
|
700
1326
|
return false;
|
|
701
1327
|
}
|
|
702
1328
|
}
|
|
703
|
-
async function
|
|
704
|
-
if (process.platform !== "
|
|
1329
|
+
async function readMacOSStoredCredential() {
|
|
1330
|
+
if (process.platform !== "darwin") return;
|
|
705
1331
|
try {
|
|
706
|
-
const { stdout } = await execFile$1("
|
|
707
|
-
"
|
|
708
|
-
"
|
|
1332
|
+
const { stdout } = await execFile$1("security", [
|
|
1333
|
+
"find-generic-password",
|
|
1334
|
+
"-s",
|
|
709
1335
|
SERVICE_NAME,
|
|
710
|
-
"
|
|
711
|
-
ACCOUNT_NAME
|
|
1336
|
+
"-a",
|
|
1337
|
+
ACCOUNT_NAME,
|
|
1338
|
+
"-w"
|
|
712
1339
|
]);
|
|
713
|
-
return parseStoredCredential(stdout, "
|
|
1340
|
+
return parseStoredCredential(stdout, "macos-keychain", "macOS Keychain");
|
|
714
1341
|
} catch {
|
|
715
1342
|
return;
|
|
716
1343
|
}
|
|
717
1344
|
}
|
|
718
|
-
async function
|
|
719
|
-
const stored = await
|
|
1345
|
+
async function writeMacOSKeychain(apiKey) {
|
|
1346
|
+
const stored = await writeMacOSStoredCredential({
|
|
720
1347
|
type: "api_key",
|
|
721
1348
|
token: apiKey
|
|
722
1349
|
});
|
|
@@ -726,394 +1353,148 @@ async function writeLinuxSecretTool(apiKey) {
|
|
|
726
1353
|
location: stored.location
|
|
727
1354
|
} : void 0;
|
|
728
1355
|
}
|
|
729
|
-
async function
|
|
730
|
-
if (process.platform !== "
|
|
1356
|
+
async function writeMacOSStoredCredential(credential) {
|
|
1357
|
+
if (process.platform !== "darwin") return;
|
|
731
1358
|
const payload = {
|
|
732
1359
|
version: 1,
|
|
733
1360
|
credential
|
|
734
1361
|
};
|
|
735
1362
|
try {
|
|
736
|
-
|
|
737
|
-
"
|
|
738
|
-
"
|
|
739
|
-
"
|
|
740
|
-
"service",
|
|
741
|
-
SERVICE_NAME,
|
|
742
|
-
"account",
|
|
743
|
-
ACCOUNT_NAME
|
|
744
|
-
], { input: JSON.stringify(payload) });
|
|
745
|
-
return {
|
|
746
|
-
...credential ? { credential } : {},
|
|
747
|
-
backend: "linux-secret-tool",
|
|
748
|
-
location: "Secret Service (secret-tool)"
|
|
749
|
-
};
|
|
750
|
-
} catch {
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
async function deleteLinuxSecretTool() {
|
|
755
|
-
if (process.platform !== "linux") return false;
|
|
756
|
-
try {
|
|
757
|
-
await execFile$1("secret-tool", [
|
|
758
|
-
"clear",
|
|
759
|
-
"service",
|
|
1363
|
+
await execFile$1("security", [
|
|
1364
|
+
"add-generic-password",
|
|
1365
|
+
"-U",
|
|
1366
|
+
"-s",
|
|
760
1367
|
SERVICE_NAME,
|
|
761
|
-
"
|
|
762
|
-
ACCOUNT_NAME
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
return
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
async function setStoredApiKey(apiKey) {
|
|
774
|
-
if (shouldUseFileBackendOnly()) return writeFileCredentials(apiKey);
|
|
775
|
-
return await writeMacOSKeychain(apiKey) ?? await writeLinuxSecretTool(apiKey) ?? await writeFileCredentials(apiKey);
|
|
776
|
-
}
|
|
777
|
-
async function setStoredCredential(credential) {
|
|
778
|
-
if (shouldUseFileBackendOnly()) return writeFileStoredCredential(credential);
|
|
779
|
-
return await writeMacOSStoredCredential(credential) ?? await writeLinuxStoredCredential(credential) ?? await writeFileStoredCredential(credential);
|
|
780
|
-
}
|
|
781
|
-
async function deleteStoredApiKey() {
|
|
782
|
-
if (shouldUseFileBackendOnly()) {
|
|
783
|
-
const deleted = await deleteFileCredentials();
|
|
784
|
-
return {
|
|
785
|
-
deleted,
|
|
786
|
-
locations: deleted ? [getCredentialsFilePath()] : []
|
|
787
|
-
};
|
|
788
|
-
}
|
|
789
|
-
const locations = [];
|
|
790
|
-
if (await deleteMacOSKeychain()) locations.push("macOS Keychain");
|
|
791
|
-
if (await deleteLinuxSecretTool()) locations.push("Secret Service (secret-tool)");
|
|
792
|
-
if (await deleteFileCredentials()) locations.push(getCredentialsFilePath());
|
|
793
|
-
return {
|
|
794
|
-
deleted: locations.length > 0,
|
|
795
|
-
locations
|
|
796
|
-
};
|
|
797
|
-
}
|
|
798
|
-
function describeCredentialBackend(backend, location) {
|
|
799
|
-
switch (backend) {
|
|
800
|
-
case "macos-keychain": return location;
|
|
801
|
-
case "linux-secret-tool": return location;
|
|
802
|
-
case "file": return `file (${location})`;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
//#endregion
|
|
807
|
-
//#region src/projectConfig.ts
|
|
808
|
-
const STATELY_CONFIG_FILE = "statelyai.json";
|
|
809
|
-
const STATELY_CONFIG_SCHEMA_URL = "https://stately.ai/schemas/statelyai.json";
|
|
810
|
-
const STATELY_CONFIG_VERSION = "1.0.0";
|
|
811
|
-
/**
|
|
812
|
-
* Base URL of the deployment that hosts the owned resource contract
|
|
813
|
-
* (`/api/v1/*`). Matches the SDK default.
|
|
814
|
-
*/
|
|
815
|
-
const DEFAULT_STATELY_API_BASE_URL = "https://editor.stately.ai";
|
|
816
|
-
/**
|
|
817
|
-
* Hosts of the legacy hosted Studio. `statelyai.json#studioUrl` values that
|
|
818
|
-
* point at them predate the editor-hosted API and must be redirected.
|
|
819
|
-
*/
|
|
820
|
-
const HOSTED_STUDIO_HOSTNAMES = new Set(["stately.ai", "www.stately.ai"]);
|
|
821
|
-
/**
|
|
822
|
-
* Maps a configured `statelyai.json#studioUrl` (or a `--base-url` flag) to the
|
|
823
|
-
* base URL of the editor API that serves our owned contract.
|
|
824
|
-
*
|
|
825
|
-
* Hosted legacy values (`https://stately.ai`, `http://www.stately.ai/`, ...)
|
|
826
|
-
* resolve to {@link DEFAULT_STATELY_API_BASE_URL}. Anything else is used as-is
|
|
827
|
-
* (minus trailing slashes): self-hosted editor deployments already point at the
|
|
828
|
-
* server that hosts `/api/v1`.
|
|
829
|
-
*
|
|
830
|
-
* This is a read-only compatibility shim: the CLI never rewrites
|
|
831
|
-
* `statelyai.json` because of it.
|
|
832
|
-
*/
|
|
833
|
-
function resolveApiBaseUrl(studioUrl) {
|
|
834
|
-
const trimmed = studioUrl?.trim();
|
|
835
|
-
if (!trimmed) return DEFAULT_STATELY_API_BASE_URL;
|
|
836
|
-
const parsed = tryParseUrl(trimmed) ?? tryParseUrl(`https://${trimmed}`);
|
|
837
|
-
if (parsed && HOSTED_STUDIO_HOSTNAMES.has(parsed.hostname.toLowerCase())) return DEFAULT_STATELY_API_BASE_URL;
|
|
838
|
-
return trimmed.replace(/\/+$/, "");
|
|
839
|
-
}
|
|
840
|
-
function tryParseUrl(value) {
|
|
841
|
-
try {
|
|
842
|
-
return new URL(value);
|
|
843
|
-
} catch {
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
const DEFAULT_SOURCE_EXCLUDES = ["**/*.test.*", "**/*.spec.*"];
|
|
848
|
-
const CODE_SOURCE_EXTENSIONS = new Set([
|
|
849
|
-
".ts",
|
|
850
|
-
".tsx",
|
|
851
|
-
".js",
|
|
852
|
-
".jsx",
|
|
853
|
-
".mts",
|
|
854
|
-
".cts",
|
|
855
|
-
".mjs",
|
|
856
|
-
".cjs"
|
|
857
|
-
]);
|
|
858
|
-
const execFileAsync$1 = promisify(execFile);
|
|
859
|
-
const ALWAYS_IGNORED_DIRECTORIES = new Set([
|
|
860
|
-
".git",
|
|
861
|
-
"node_modules",
|
|
862
|
-
"dist",
|
|
863
|
-
"build",
|
|
864
|
-
"out",
|
|
865
|
-
"coverage",
|
|
866
|
-
".next"
|
|
867
|
-
]);
|
|
868
|
-
function createStatelyProjectConfig(options) {
|
|
869
|
-
const defaultXStateVersion = options.defaultXStateVersion ?? 5;
|
|
870
|
-
return {
|
|
871
|
-
$schema: STATELY_CONFIG_SCHEMA_URL,
|
|
872
|
-
version: STATELY_CONFIG_VERSION,
|
|
873
|
-
projectId: options.projectId ?? "",
|
|
874
|
-
studioUrl: options.studioUrl ?? "https://stately.ai",
|
|
875
|
-
defaultXStateVersion,
|
|
876
|
-
include: [],
|
|
877
|
-
exclude: [...DEFAULT_SOURCE_EXCLUDES]
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
async function writeStatelyProjectConfig(configPath, config) {
|
|
881
|
-
const serialized = { ...config };
|
|
882
|
-
if (!config.projectId) {
|
|
883
|
-
delete serialized.projectId;
|
|
884
|
-
delete serialized.studioUrl;
|
|
885
|
-
}
|
|
886
|
-
await fsPromises.writeFile(configPath, `${JSON.stringify(serialized, null, 2)}\n`, "utf8");
|
|
887
|
-
}
|
|
888
|
-
function normalizeProjectConfig(config) {
|
|
889
|
-
const firstSource = config.sources?.[0];
|
|
890
|
-
return {
|
|
891
|
-
$schema: config.$schema ?? STATELY_CONFIG_SCHEMA_URL,
|
|
892
|
-
version: config.version ?? STATELY_CONFIG_VERSION,
|
|
893
|
-
projectId: config.projectId ?? "",
|
|
894
|
-
studioUrl: config.studioUrl ?? "https://stately.ai",
|
|
895
|
-
defaultXStateVersion: Math.max(5, config.defaultXStateVersion ?? 5),
|
|
896
|
-
...config.sourceUpdateStrategy === "preserve" || config.sourceUpdateStrategy === "rewrite" ? { sourceUpdateStrategy: config.sourceUpdateStrategy } : {},
|
|
897
|
-
include: [...config.include ?? firstSource?.include ?? []],
|
|
898
|
-
exclude: config.exclude == null ? [...firstSource?.exclude ?? DEFAULT_SOURCE_EXCLUDES] : [...config.exclude],
|
|
899
|
-
...config.newMachinesDir ? { newMachinesDir: config.newMachinesDir } : {}
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
function getConfigRewriteStatus(config) {
|
|
903
|
-
if (!config.sources) return { needsRewrite: false };
|
|
904
|
-
if (config.sources.length > 1) return {
|
|
905
|
-
needsRewrite: true,
|
|
906
|
-
reason: "multiple-legacy-sources"
|
|
907
|
-
};
|
|
908
|
-
return {
|
|
909
|
-
needsRewrite: true,
|
|
910
|
-
reason: "legacy-sources"
|
|
911
|
-
};
|
|
912
|
-
}
|
|
913
|
-
async function readStatelyProjectConfig(options = {}) {
|
|
914
|
-
const rootDir = path.resolve(options.cwd ?? process.cwd());
|
|
915
|
-
const configPath = path.resolve(rootDir, options.configPath ?? STATELY_CONFIG_FILE);
|
|
916
|
-
const raw = await fsPromises.readFile(configPath, "utf8");
|
|
917
|
-
const parsed = JSON.parse(raw);
|
|
918
|
-
const config = normalizeProjectConfig(parsed);
|
|
919
|
-
const rewriteStatus = getConfigRewriteStatus(parsed);
|
|
920
|
-
if (options.rewriteIfNeeded && rewriteStatus.reason === "legacy-sources") {
|
|
921
|
-
await writeStatelyProjectConfig(configPath, config);
|
|
922
|
-
return {
|
|
923
|
-
config,
|
|
924
|
-
configPath,
|
|
925
|
-
rootDir,
|
|
926
|
-
rewriteStatus: {
|
|
927
|
-
needsRewrite: false,
|
|
928
|
-
rewritten: true
|
|
929
|
-
}
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
return {
|
|
933
|
-
config,
|
|
934
|
-
configPath,
|
|
935
|
-
rootDir,
|
|
936
|
-
rewriteStatus
|
|
937
|
-
};
|
|
938
|
-
}
|
|
939
|
-
async function walkFiles(rootDir, currentDir = rootDir) {
|
|
940
|
-
const entries = await fsPromises.readdir(currentDir, { withFileTypes: true });
|
|
941
|
-
const files = [];
|
|
942
|
-
for (const entry of entries) {
|
|
943
|
-
if (ALWAYS_IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
944
|
-
const absolutePath = path.join(currentDir, entry.name);
|
|
945
|
-
if (entry.isDirectory()) {
|
|
946
|
-
files.push(...await walkFiles(rootDir, absolutePath));
|
|
947
|
-
continue;
|
|
948
|
-
}
|
|
949
|
-
if (!entry.isFile()) continue;
|
|
950
|
-
files.push(path.relative(rootDir, absolutePath).replace(/\\/g, "/"));
|
|
1368
|
+
"-a",
|
|
1369
|
+
ACCOUNT_NAME,
|
|
1370
|
+
"-w",
|
|
1371
|
+
JSON.stringify(payload)
|
|
1372
|
+
]);
|
|
1373
|
+
return {
|
|
1374
|
+
...credential ? { credential } : {},
|
|
1375
|
+
backend: "macos-keychain",
|
|
1376
|
+
location: "macOS Keychain"
|
|
1377
|
+
};
|
|
1378
|
+
} catch {
|
|
1379
|
+
return;
|
|
951
1380
|
}
|
|
952
|
-
return files;
|
|
953
1381
|
}
|
|
954
|
-
async function
|
|
955
|
-
if (
|
|
1382
|
+
async function deleteMacOSKeychain() {
|
|
1383
|
+
if (process.platform !== "darwin") return false;
|
|
956
1384
|
try {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
1385
|
+
await execFile$1("security", [
|
|
1386
|
+
"delete-generic-password",
|
|
1387
|
+
"-s",
|
|
1388
|
+
SERVICE_NAME,
|
|
1389
|
+
"-a",
|
|
1390
|
+
ACCOUNT_NAME
|
|
1391
|
+
]);
|
|
1392
|
+
return true;
|
|
964
1393
|
} catch {
|
|
965
|
-
return
|
|
1394
|
+
return false;
|
|
966
1395
|
}
|
|
967
1396
|
}
|
|
968
|
-
function
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const next = pattern[index + 1];
|
|
982
|
-
if (char === "*") {
|
|
983
|
-
if (next === "*") {
|
|
984
|
-
const slashAfterGlobstar = pattern[index + 2] === "/";
|
|
985
|
-
regex += slashAfterGlobstar ? "(?:.*/)?" : ".*";
|
|
986
|
-
index += slashAfterGlobstar ? 2 : 1;
|
|
987
|
-
continue;
|
|
988
|
-
}
|
|
989
|
-
regex += "[^/]*";
|
|
990
|
-
continue;
|
|
991
|
-
}
|
|
992
|
-
if (char === "?") {
|
|
993
|
-
regex += "[^/]";
|
|
994
|
-
continue;
|
|
995
|
-
}
|
|
996
|
-
if ("\\.[]{}()+-^$|".includes(char)) {
|
|
997
|
-
regex += `\\${char}`;
|
|
998
|
-
continue;
|
|
999
|
-
}
|
|
1000
|
-
regex += char;
|
|
1397
|
+
async function readLinuxStoredCredential() {
|
|
1398
|
+
if (process.platform !== "linux") return;
|
|
1399
|
+
try {
|
|
1400
|
+
const { stdout } = await execFile$1("secret-tool", [
|
|
1401
|
+
"lookup",
|
|
1402
|
+
"service",
|
|
1403
|
+
SERVICE_NAME,
|
|
1404
|
+
"account",
|
|
1405
|
+
ACCOUNT_NAME
|
|
1406
|
+
]);
|
|
1407
|
+
return parseStoredCredential(stdout, "linux-secret-tool", "Secret Service (secret-tool)");
|
|
1408
|
+
} catch {
|
|
1409
|
+
return;
|
|
1001
1410
|
}
|
|
1002
|
-
regex += "$";
|
|
1003
|
-
return new RegExp(regex);
|
|
1004
|
-
}
|
|
1005
|
-
function matchesGlob(relativePath, pattern) {
|
|
1006
|
-
return expandBraces(pattern).some((expanded) => globToRegExp(expanded).test(relativePath));
|
|
1007
|
-
}
|
|
1008
|
-
function matchesAny(patterns, relativePath) {
|
|
1009
|
-
return (patterns ?? []).some((pattern) => matchesGlob(relativePath, pattern));
|
|
1010
1411
|
}
|
|
1011
|
-
function
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
return
|
|
1412
|
+
async function writeLinuxSecretTool(apiKey) {
|
|
1413
|
+
const stored = await writeLinuxStoredCredential({
|
|
1414
|
+
type: "api_key",
|
|
1415
|
+
token: apiKey
|
|
1416
|
+
});
|
|
1417
|
+
return stored ? {
|
|
1418
|
+
apiKey,
|
|
1419
|
+
backend: stored.backend,
|
|
1420
|
+
location: stored.location
|
|
1421
|
+
} : void 0;
|
|
1017
1422
|
}
|
|
1018
|
-
function
|
|
1019
|
-
return
|
|
1020
|
-
|
|
1021
|
-
|
|
1423
|
+
async function writeLinuxStoredCredential(credential) {
|
|
1424
|
+
if (process.platform !== "linux") return;
|
|
1425
|
+
const payload = {
|
|
1426
|
+
version: 1,
|
|
1427
|
+
credential
|
|
1022
1428
|
};
|
|
1429
|
+
try {
|
|
1430
|
+
execFileSync("secret-tool", [
|
|
1431
|
+
"store",
|
|
1432
|
+
"--label",
|
|
1433
|
+
"statelyai API key",
|
|
1434
|
+
"service",
|
|
1435
|
+
SERVICE_NAME,
|
|
1436
|
+
"account",
|
|
1437
|
+
ACCOUNT_NAME
|
|
1438
|
+
], { input: JSON.stringify(payload) });
|
|
1439
|
+
return {
|
|
1440
|
+
...credential ? { credential } : {},
|
|
1441
|
+
backend: "linux-secret-tool",
|
|
1442
|
+
location: "Secret Service (secret-tool)"
|
|
1443
|
+
};
|
|
1444
|
+
} catch {
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1023
1447
|
}
|
|
1024
|
-
function
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1448
|
+
async function deleteLinuxSecretTool() {
|
|
1449
|
+
if (process.platform !== "linux") return false;
|
|
1450
|
+
try {
|
|
1451
|
+
await execFile$1("secret-tool", [
|
|
1452
|
+
"clear",
|
|
1453
|
+
"service",
|
|
1454
|
+
SERVICE_NAME,
|
|
1455
|
+
"account",
|
|
1456
|
+
ACCOUNT_NAME
|
|
1457
|
+
]);
|
|
1458
|
+
return true;
|
|
1459
|
+
} catch {
|
|
1460
|
+
return false;
|
|
1461
|
+
}
|
|
1031
1462
|
}
|
|
1032
|
-
function
|
|
1033
|
-
if (
|
|
1034
|
-
|
|
1035
|
-
if (packageMatch) return `packages/${packageMatch[1]}/src/`;
|
|
1036
|
-
const appMatch = relativePath.match(/^apps\/([^/]+)\/src\//);
|
|
1037
|
-
if (appMatch) return `apps/${appMatch[1]}/src/`;
|
|
1038
|
-
return null;
|
|
1463
|
+
async function getStoredCredential() {
|
|
1464
|
+
if (shouldUseFileBackendOnly()) return readFileStoredCredential();
|
|
1465
|
+
return await readMacOSStoredCredential() ?? await readLinuxStoredCredential() ?? await readFileStoredCredential();
|
|
1039
1466
|
}
|
|
1040
|
-
function
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
const commonSegments = [];
|
|
1044
|
-
for (const [index, segment] of firstDirectory.entries()) {
|
|
1045
|
-
if (!remainingDirectories.every((directory) => directory[index] === segment)) break;
|
|
1046
|
-
commonSegments.push(segment);
|
|
1047
|
-
}
|
|
1048
|
-
return commonSegments.length > 0 ? commonSegments.join("/") : void 0;
|
|
1467
|
+
async function setStoredApiKey(apiKey) {
|
|
1468
|
+
if (shouldUseFileBackendOnly()) return writeFileCredentials(apiKey);
|
|
1469
|
+
return await writeMacOSKeychain(apiKey) ?? await writeLinuxSecretTool(apiKey) ?? await writeFileCredentials(apiKey);
|
|
1049
1470
|
}
|
|
1050
|
-
function
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
const commonDirectory = getCommonDirectory(relativePaths);
|
|
1054
|
-
if (commonDirectory) return `${commonDirectory}/${relativePaths.every((relativePath) => path.posix.dirname(relativePath) === commonDirectory) ? "*" : "**/*"}${extPattern}`;
|
|
1055
|
-
return `${root}**/*${extPattern}`;
|
|
1471
|
+
async function setStoredCredential(credential) {
|
|
1472
|
+
if (shouldUseFileBackendOnly()) return writeFileStoredCredential(credential);
|
|
1473
|
+
return await writeMacOSStoredCredential(credential) ?? await writeLinuxStoredCredential(credential) ?? await writeFileStoredCredential(credential);
|
|
1056
1474
|
}
|
|
1057
|
-
function
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
for (const matchedPath of matched) pending.delete(matchedPath);
|
|
1065
|
-
};
|
|
1066
|
-
const groupedRoots = /* @__PURE__ */ new Map();
|
|
1067
|
-
for (const relativePath of pending) {
|
|
1068
|
-
const root = getSuggestionRoot(relativePath);
|
|
1069
|
-
if (!root) continue;
|
|
1070
|
-
const bucket = groupedRoots.get(root) ?? [];
|
|
1071
|
-
bucket.push(relativePath);
|
|
1072
|
-
groupedRoots.set(root, bucket);
|
|
1073
|
-
}
|
|
1074
|
-
for (const [root, groupedPaths] of [...groupedRoots.entries()].sort((left, right) => left[0].localeCompare(right[0]))) addSuggestion(suggestGroupedInclude(groupedPaths, root), (relativePath) => groupedPaths.includes(relativePath));
|
|
1075
|
-
const byDirectory = /* @__PURE__ */ new Map();
|
|
1076
|
-
for (const relativePath of pending) {
|
|
1077
|
-
const directory = path.posix.dirname(relativePath);
|
|
1078
|
-
const key = directory === "." ? relativePath : directory;
|
|
1079
|
-
const bucket = byDirectory.get(key) ?? [];
|
|
1080
|
-
bucket.push(relativePath);
|
|
1081
|
-
byDirectory.set(key, bucket);
|
|
1082
|
-
}
|
|
1083
|
-
for (const [key, matchedPaths] of [...byDirectory.entries()].sort((left, right) => left[0].localeCompare(right[0]))) {
|
|
1084
|
-
if (matchedPaths.length > 1 && key !== "." && key !== matchedPaths[0]) {
|
|
1085
|
-
addSuggestion(`${key}/**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}`, (relativePath) => relativePath.startsWith(`${key}/`));
|
|
1086
|
-
continue;
|
|
1087
|
-
}
|
|
1088
|
-
const exactPath = matchedPaths[0];
|
|
1089
|
-
addSuggestion(exactPath, (relativePath) => relativePath === exactPath);
|
|
1475
|
+
async function deleteStoredApiKey() {
|
|
1476
|
+
if (shouldUseFileBackendOnly()) {
|
|
1477
|
+
const deleted = await deleteFileCredentials();
|
|
1478
|
+
return {
|
|
1479
|
+
deleted,
|
|
1480
|
+
locations: deleted ? [getCredentialsFilePath()] : []
|
|
1481
|
+
};
|
|
1090
1482
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
const relativeFiles = await filterGitIgnoredPaths(rootDir, await walkFiles(rootDir));
|
|
1099
|
-
const discovered = /* @__PURE__ */ new Map();
|
|
1100
|
-
const source = {
|
|
1101
|
-
include: config.include,
|
|
1102
|
-
exclude: config.exclude ?? DEFAULT_SOURCE_EXCLUDES,
|
|
1103
|
-
format: "xstate",
|
|
1104
|
-
xstateVersion: config.defaultXStateVersion
|
|
1483
|
+
const locations = [];
|
|
1484
|
+
if (await deleteMacOSKeychain()) locations.push("macOS Keychain");
|
|
1485
|
+
if (await deleteLinuxSecretTool()) locations.push("Secret Service (secret-tool)");
|
|
1486
|
+
if (await deleteFileCredentials()) locations.push(getCredentialsFilePath());
|
|
1487
|
+
return {
|
|
1488
|
+
deleted: locations.length > 0,
|
|
1489
|
+
locations
|
|
1105
1490
|
};
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
relativePath,
|
|
1113
|
-
source
|
|
1114
|
-
});
|
|
1491
|
+
}
|
|
1492
|
+
function describeCredentialBackend(backend, location) {
|
|
1493
|
+
switch (backend) {
|
|
1494
|
+
case "macos-keychain": return location;
|
|
1495
|
+
case "linux-secret-tool": return location;
|
|
1496
|
+
case "file": return `file (${location})`;
|
|
1115
1497
|
}
|
|
1116
|
-
return [...discovered.values()].sort((left, right) => left.relativePath.localeCompare(right.relativePath));
|
|
1117
1498
|
}
|
|
1118
1499
|
|
|
1119
1500
|
//#endregion
|
|
@@ -2654,6 +3035,7 @@ var WhoamiCommand = class extends Command {
|
|
|
2654
3035
|
}
|
|
2655
3036
|
};
|
|
2656
3037
|
const COMMANDS = {
|
|
3038
|
+
verify: VerifyCommand,
|
|
2657
3039
|
plan: PlanCommand,
|
|
2658
3040
|
diff: DiffCommand,
|
|
2659
3041
|
pull: PullCommand,
|
|
@@ -2688,4 +3070,4 @@ function isDirectExecution() {
|
|
|
2688
3070
|
if (isDirectExecution()) run$1();
|
|
2689
3071
|
|
|
2690
3072
|
//#endregion
|
|
2691
|
-
export { scanProjectMachines as C, run$1 as S,
|
|
3073
|
+
export { scanProjectMachines as C, formatVerificationJUnit as D, verifyProject as E, createStatelyProjectConfig as O, run$1 as S, resolveVerificationRequest as T, isFileGitDirty as _, discoverMachinesInSource as a, resolveCredentialStatus as b, formatDiscoveredMachines as c, formatProjectStatus as d, formatStoredCredentialSource as f, initProject as g, inferInitProjectName as h, discoverLinkedPullTargets as i, formatMissingNewMachineDirMessage as l, getEnvCredential as m, buildOAuthLoginStart as n, discoverOAuthLogin as o, getEnvApiKey as p, classifyPushCandidates as r, discoverRemoteProjectMachineTargets as s, COMMANDS as t, formatPlanSummary as u, resolveApiKey as v, scanProjectSources as w, resolveProjectStatus as x, resolveConfiguredPullTargets as y };
|