sentinelayer-cli 0.1.2 → 0.3.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 +996 -996
- package/bin/create-sentinelayer.js +5 -5
- package/bin/sentinelayer-cli.js +4 -4
- package/bin/sl.js +5 -5
- package/package.json +62 -54
- package/src/agents/jules/config/definition.js +209 -209
- package/src/agents/jules/config/system-prompt.js +175 -175
- package/src/agents/jules/error-intake.js +51 -51
- package/src/agents/jules/fix-cycle.js +377 -377
- package/src/agents/jules/loop.js +367 -367
- package/src/agents/jules/pulse.js +319 -319
- package/src/agents/jules/stream.js +186 -186
- package/src/agents/jules/swarm/file-scanner.js +74 -74
- package/src/agents/jules/swarm/index.js +11 -11
- package/src/agents/jules/swarm/orchestrator.js +362 -362
- package/src/agents/jules/swarm/pattern-hunter.js +123 -123
- package/src/agents/jules/swarm/sub-agent.js +308 -308
- package/src/agents/jules/tools/auth-audit.js +226 -222
- package/src/agents/jules/tools/dispatch.js +327 -327
- package/src/agents/jules/tools/file-edit.js +180 -180
- package/src/agents/jules/tools/file-read.js +100 -100
- package/src/agents/jules/tools/frontend-analyze.js +570 -570
- package/src/agents/jules/tools/glob.js +168 -168
- package/src/agents/jules/tools/grep.js +228 -228
- package/src/agents/jules/tools/index.js +29 -29
- package/src/agents/jules/tools/path-guards.js +161 -161
- package/src/agents/jules/tools/runtime-audit.js +493 -493
- package/src/agents/jules/tools/shell.js +383 -383
- package/src/ai/aidenid.js +972 -945
- package/src/ai/client.js +508 -508
- package/src/ai/domain-target-store.js +268 -268
- package/src/ai/identity-store.js +270 -270
- package/src/ai/site-store.js +145 -145
- package/src/audit/agents/architecture.js +180 -180
- package/src/audit/agents/compliance.js +179 -179
- package/src/audit/agents/documentation.js +165 -165
- package/src/audit/agents/performance.js +145 -145
- package/src/audit/agents/security.js +215 -215
- package/src/audit/agents/testing.js +172 -172
- package/src/audit/orchestrator.js +557 -557
- package/src/audit/package.js +204 -204
- package/src/audit/registry.js +284 -284
- package/src/audit/replay.js +103 -103
- package/src/auth/http.js +113 -113
- package/src/auth/service.js +891 -848
- package/src/auth/session-store.js +359 -345
- package/src/cli.js +252 -252
- package/src/commands/ai/identity-lifecycle.js +1338 -1337
- package/src/commands/ai/provision-governance.js +1272 -1246
- package/src/commands/ai/shared.js +147 -147
- package/src/commands/ai.js +11 -11
- package/src/commands/apply.js +12 -12
- package/src/commands/audit.js +1166 -1166
- package/src/commands/auth.js +375 -366
- package/src/commands/chat.js +191 -191
- package/src/commands/config.js +184 -184
- package/src/commands/cost.js +311 -311
- package/src/commands/daemon/core.js +850 -850
- package/src/commands/daemon/extended.js +1048 -1048
- package/src/commands/daemon/shared.js +213 -213
- package/src/commands/daemon.js +11 -11
- package/src/commands/guide.js +174 -174
- package/src/commands/ingest.js +58 -58
- package/src/commands/init.js +55 -55
- package/src/commands/legacy-args.js +10 -10
- package/src/commands/mcp.js +461 -404
- package/src/commands/omargate.js +15 -15
- package/src/commands/persona.js +20 -20
- package/src/commands/plugin.js +260 -260
- package/src/commands/policy.js +132 -132
- package/src/commands/prompt.js +238 -238
- package/src/commands/review.js +704 -704
- package/src/commands/scan.js +866 -788
- package/src/commands/spec.js +716 -716
- package/src/commands/swarm.js +651 -651
- package/src/commands/telemetry.js +202 -202
- package/src/commands/watch.js +510 -510
- package/src/config/agent-dictionary.js +182 -182
- package/src/config/io.js +56 -56
- package/src/config/paths.js +18 -18
- package/src/config/schema.js +55 -55
- package/src/config/service.js +184 -184
- package/src/cost/budget.js +235 -235
- package/src/cost/history.js +188 -188
- package/src/cost/tracker.js +171 -171
- package/src/daemon/artifact-lineage.js +534 -534
- package/src/daemon/assignment-ledger.js +770 -770
- package/src/daemon/ast-parser-layer.js +258 -258
- package/src/daemon/budget-governor.js +633 -633
- package/src/daemon/callgraph-overlay.js +646 -646
- package/src/daemon/error-worker.js +626 -626
- package/src/daemon/hybrid-mapper.js +929 -929
- package/src/daemon/jira-lifecycle.js +632 -632
- package/src/daemon/operator-control.js +657 -657
- package/src/daemon/reliability-lane.js +471 -471
- package/src/daemon/watchdog.js +971 -971
- package/src/guide/generator.js +316 -316
- package/src/ingest/engine.js +918 -918
- package/src/legacy-cli.js +2548 -2435
- package/src/mcp/registry.js +695 -695
- package/src/memory/blackboard.js +301 -301
- package/src/memory/retrieval.js +581 -581
- package/src/plugin/manifest.js +553 -553
- package/src/policy/packs.js +144 -144
- package/src/prompt/generator.js +118 -106
- package/src/review/ai-review.js +669 -669
- package/src/review/local-review.js +1284 -1284
- package/src/review/replay.js +235 -235
- package/src/review/report.js +664 -664
- package/src/review/spec-binding.js +487 -487
- package/src/scaffold/generator.js +67 -0
- package/src/scaffold/templates.js +150 -0
- package/src/scan/generator.js +418 -351
- package/src/scan/gh-secrets.js +107 -0
- package/src/spec/generator.js +519 -519
- package/src/spec/regenerate.js +237 -237
- package/src/spec/templates.js +91 -91
- package/src/swarm/dashboard.js +247 -247
- package/src/swarm/factory.js +363 -363
- package/src/swarm/pentest.js +934 -934
- package/src/swarm/registry.js +419 -419
- package/src/swarm/report.js +158 -158
- package/src/swarm/runtime.js +576 -576
- package/src/swarm/scenario-dsl.js +272 -272
- package/src/telemetry/ledger.js +302 -302
- package/src/telemetry/sync.js +96 -59
- package/src/ui/markdown.js +220 -220
|
@@ -1,272 +1,272 @@
|
|
|
1
|
-
import fsp from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
const SUPPORTED_ACTIONS = new Set(["goto", "click", "fill", "wait", "screenshot"]);
|
|
5
|
-
|
|
6
|
-
function normalizeString(value) {
|
|
7
|
-
return String(value || "").trim();
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function decodeQuoted(value) {
|
|
11
|
-
return String(value || "")
|
|
12
|
-
.replace(/^"/, "")
|
|
13
|
-
.replace(/"$/, "")
|
|
14
|
-
.replace(/\\"/g, '"')
|
|
15
|
-
.replace(/\\n/g, "\n")
|
|
16
|
-
.replace(/\\t/g, "\t");
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function tokenizeLine(line = "") {
|
|
20
|
-
const tokens = [];
|
|
21
|
-
let cursor = 0;
|
|
22
|
-
while (cursor < line.length) {
|
|
23
|
-
while (cursor < line.length && /\s/.test(line[cursor])) {
|
|
24
|
-
cursor += 1;
|
|
25
|
-
}
|
|
26
|
-
if (cursor >= line.length) {
|
|
27
|
-
break;
|
|
28
|
-
}
|
|
29
|
-
if (line[cursor] === "#") {
|
|
30
|
-
break;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (line[cursor] === '"') {
|
|
34
|
-
let token = '"';
|
|
35
|
-
cursor += 1;
|
|
36
|
-
let escaped = false;
|
|
37
|
-
while (cursor < line.length) {
|
|
38
|
-
const char = line[cursor];
|
|
39
|
-
token += char;
|
|
40
|
-
cursor += 1;
|
|
41
|
-
if (escaped) {
|
|
42
|
-
escaped = false;
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (char === "\\") {
|
|
46
|
-
escaped = true;
|
|
47
|
-
continue;
|
|
48
|
-
}
|
|
49
|
-
if (char === '"') {
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (!token.endsWith('"')) {
|
|
54
|
-
throw new Error("Unterminated quoted string.");
|
|
55
|
-
}
|
|
56
|
-
tokens.push(decodeQuoted(token));
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const start = cursor;
|
|
61
|
-
while (cursor < line.length && !/\s/.test(line[cursor])) {
|
|
62
|
-
cursor += 1;
|
|
63
|
-
}
|
|
64
|
-
tokens.push(line.slice(start, cursor));
|
|
65
|
-
}
|
|
66
|
-
return tokens;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function parseAction(tokens = [], lineNumber = 0) {
|
|
70
|
-
const type = normalizeString(tokens[0]).toLowerCase();
|
|
71
|
-
if (!SUPPORTED_ACTIONS.has(type)) {
|
|
72
|
-
throw new Error(
|
|
73
|
-
`Line ${lineNumber}: unsupported action '${type}'. Supported: ${[...SUPPORTED_ACTIONS].join(
|
|
74
|
-
", "
|
|
75
|
-
)}.`
|
|
76
|
-
);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (type === "goto") {
|
|
80
|
-
if (tokens.length < 2) {
|
|
81
|
-
throw new Error(`Line ${lineNumber}: action goto requires <url>.`);
|
|
82
|
-
}
|
|
83
|
-
return {
|
|
84
|
-
type,
|
|
85
|
-
url: normalizeString(tokens[1]),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (type === "click") {
|
|
90
|
-
if (tokens.length < 2) {
|
|
91
|
-
throw new Error(`Line ${lineNumber}: action click requires <selector>.`);
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
type,
|
|
95
|
-
selector: normalizeString(tokens[1]),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
if (type === "fill") {
|
|
100
|
-
if (tokens.length < 3) {
|
|
101
|
-
throw new Error(`Line ${lineNumber}: action fill requires <selector> <text>.`);
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
type,
|
|
105
|
-
selector: normalizeString(tokens[1]),
|
|
106
|
-
text: normalizeString(tokens[2]),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (type === "wait") {
|
|
111
|
-
if (tokens.length < 2) {
|
|
112
|
-
throw new Error(`Line ${lineNumber}: action wait requires <ms>.`);
|
|
113
|
-
}
|
|
114
|
-
const ms = Number(tokens[1]);
|
|
115
|
-
if (!Number.isFinite(ms) || ms < 0) {
|
|
116
|
-
throw new Error(`Line ${lineNumber}: action wait requires non-negative milliseconds.`);
|
|
117
|
-
}
|
|
118
|
-
return {
|
|
119
|
-
type,
|
|
120
|
-
ms: Math.floor(ms),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (type === "screenshot") {
|
|
125
|
-
return {
|
|
126
|
-
type,
|
|
127
|
-
path: normalizeString(tokens[1] || ""),
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
throw new Error(`Line ${lineNumber}: unsupported action '${type}'.`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function parseScenarioDsl(content = "", { source = "" } = {}) {
|
|
135
|
-
const raw = String(content || "");
|
|
136
|
-
const lines = raw.split(/\r?\n/);
|
|
137
|
-
const spec = {
|
|
138
|
-
id: "",
|
|
139
|
-
startUrl: "",
|
|
140
|
-
tags: [],
|
|
141
|
-
actions: [],
|
|
142
|
-
source: normalizeString(source),
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
for (let index = 0; index < lines.length; index += 1) {
|
|
146
|
-
const lineNumber = index + 1;
|
|
147
|
-
const line = lines[index];
|
|
148
|
-
const trimmed = normalizeString(line);
|
|
149
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
150
|
-
continue;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let tokens;
|
|
154
|
-
try {
|
|
155
|
-
tokens = tokenizeLine(line);
|
|
156
|
-
} catch (error) {
|
|
157
|
-
throw new Error(`Line ${lineNumber}: ${error.message}`);
|
|
158
|
-
}
|
|
159
|
-
if (tokens.length === 0) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
const command = normalizeString(tokens[0]).toLowerCase();
|
|
163
|
-
|
|
164
|
-
if (command === "scenario") {
|
|
165
|
-
if (tokens.length < 2) {
|
|
166
|
-
throw new Error(`Line ${lineNumber}: scenario requires <id>.`);
|
|
167
|
-
}
|
|
168
|
-
spec.id = normalizeString(tokens[1]);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (command === "start_url") {
|
|
173
|
-
if (tokens.length < 2) {
|
|
174
|
-
throw new Error(`Line ${lineNumber}: start_url requires <url>.`);
|
|
175
|
-
}
|
|
176
|
-
spec.startUrl = normalizeString(tokens[1]);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (command === "tag") {
|
|
181
|
-
if (tokens.length < 2) {
|
|
182
|
-
throw new Error(`Line ${lineNumber}: tag requires <value>.`);
|
|
183
|
-
}
|
|
184
|
-
spec.tags.push(normalizeString(tokens[1]));
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (command === "action") {
|
|
189
|
-
const action = parseAction(tokens.slice(1), lineNumber);
|
|
190
|
-
spec.actions.push(action);
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
throw new Error(`Line ${lineNumber}: unknown command '${command}'.`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return spec;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function validateScenarioSpec(spec = {}) {
|
|
201
|
-
const errors = [];
|
|
202
|
-
const id = normalizeString(spec.id);
|
|
203
|
-
if (!id) {
|
|
204
|
-
errors.push("scenario id is required.");
|
|
205
|
-
}
|
|
206
|
-
if (!Array.isArray(spec.actions) || spec.actions.length === 0) {
|
|
207
|
-
errors.push("at least one action is required.");
|
|
208
|
-
}
|
|
209
|
-
for (const action of spec.actions || []) {
|
|
210
|
-
if (!SUPPORTED_ACTIONS.has(normalizeString(action.type).toLowerCase())) {
|
|
211
|
-
errors.push(`unsupported action type: ${action.type}`);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return {
|
|
215
|
-
valid: errors.length === 0,
|
|
216
|
-
errors,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
export async function parseScenarioFile(filePath = "") {
|
|
221
|
-
const normalizedPath = normalizeString(filePath);
|
|
222
|
-
if (!normalizedPath) {
|
|
223
|
-
throw new Error("scenario file path is required.");
|
|
224
|
-
}
|
|
225
|
-
const resolved = path.resolve(process.cwd(), normalizedPath);
|
|
226
|
-
const content = await fsp.readFile(resolved, "utf-8");
|
|
227
|
-
const spec = parseScenarioDsl(content, { source: resolved });
|
|
228
|
-
return {
|
|
229
|
-
spec,
|
|
230
|
-
filePath: resolved,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function renderScenarioTemplate({
|
|
235
|
-
scenarioId = "nightly_smoke",
|
|
236
|
-
startUrl = "https://example.com",
|
|
237
|
-
} = {}) {
|
|
238
|
-
const id = normalizeString(scenarioId) || "nightly_smoke";
|
|
239
|
-
const url = normalizeString(startUrl) || "https://example.com";
|
|
240
|
-
return `# Sentinelayer swarm scenario DSL
|
|
241
|
-
scenario "${id}"
|
|
242
|
-
start_url "${url}"
|
|
243
|
-
tag "smoke"
|
|
244
|
-
tag "runtime"
|
|
245
|
-
|
|
246
|
-
action goto "${url}"
|
|
247
|
-
action wait 500
|
|
248
|
-
action screenshot "${id}-home.png"
|
|
249
|
-
`;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export async function writeScenarioTemplate({
|
|
253
|
-
scenarioId,
|
|
254
|
-
targetPath = ".",
|
|
255
|
-
outputFile = "",
|
|
256
|
-
startUrl = "https://example.com",
|
|
257
|
-
} = {}) {
|
|
258
|
-
const normalizedTargetPath = path.resolve(String(targetPath || "."));
|
|
259
|
-
const resolvedOutputFile = normalizeString(outputFile)
|
|
260
|
-
? path.resolve(normalizedTargetPath, outputFile)
|
|
261
|
-
: path.join(normalizedTargetPath, ".sentinelayer", "scenarios", `${scenarioId}.sls`);
|
|
262
|
-
const content = renderScenarioTemplate({
|
|
263
|
-
scenarioId,
|
|
264
|
-
startUrl,
|
|
265
|
-
});
|
|
266
|
-
await fsp.mkdir(path.dirname(resolvedOutputFile), { recursive: true });
|
|
267
|
-
await fsp.writeFile(resolvedOutputFile, `${content.trim()}\n`, "utf-8");
|
|
268
|
-
return {
|
|
269
|
-
filePath: resolvedOutputFile,
|
|
270
|
-
content,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const SUPPORTED_ACTIONS = new Set(["goto", "click", "fill", "wait", "screenshot"]);
|
|
5
|
+
|
|
6
|
+
function normalizeString(value) {
|
|
7
|
+
return String(value || "").trim();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function decodeQuoted(value) {
|
|
11
|
+
return String(value || "")
|
|
12
|
+
.replace(/^"/, "")
|
|
13
|
+
.replace(/"$/, "")
|
|
14
|
+
.replace(/\\"/g, '"')
|
|
15
|
+
.replace(/\\n/g, "\n")
|
|
16
|
+
.replace(/\\t/g, "\t");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function tokenizeLine(line = "") {
|
|
20
|
+
const tokens = [];
|
|
21
|
+
let cursor = 0;
|
|
22
|
+
while (cursor < line.length) {
|
|
23
|
+
while (cursor < line.length && /\s/.test(line[cursor])) {
|
|
24
|
+
cursor += 1;
|
|
25
|
+
}
|
|
26
|
+
if (cursor >= line.length) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (line[cursor] === "#") {
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (line[cursor] === '"') {
|
|
34
|
+
let token = '"';
|
|
35
|
+
cursor += 1;
|
|
36
|
+
let escaped = false;
|
|
37
|
+
while (cursor < line.length) {
|
|
38
|
+
const char = line[cursor];
|
|
39
|
+
token += char;
|
|
40
|
+
cursor += 1;
|
|
41
|
+
if (escaped) {
|
|
42
|
+
escaped = false;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (char === "\\") {
|
|
46
|
+
escaped = true;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (char === '"') {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!token.endsWith('"')) {
|
|
54
|
+
throw new Error("Unterminated quoted string.");
|
|
55
|
+
}
|
|
56
|
+
tokens.push(decodeQuoted(token));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const start = cursor;
|
|
61
|
+
while (cursor < line.length && !/\s/.test(line[cursor])) {
|
|
62
|
+
cursor += 1;
|
|
63
|
+
}
|
|
64
|
+
tokens.push(line.slice(start, cursor));
|
|
65
|
+
}
|
|
66
|
+
return tokens;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseAction(tokens = [], lineNumber = 0) {
|
|
70
|
+
const type = normalizeString(tokens[0]).toLowerCase();
|
|
71
|
+
if (!SUPPORTED_ACTIONS.has(type)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Line ${lineNumber}: unsupported action '${type}'. Supported: ${[...SUPPORTED_ACTIONS].join(
|
|
74
|
+
", "
|
|
75
|
+
)}.`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (type === "goto") {
|
|
80
|
+
if (tokens.length < 2) {
|
|
81
|
+
throw new Error(`Line ${lineNumber}: action goto requires <url>.`);
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
type,
|
|
85
|
+
url: normalizeString(tokens[1]),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (type === "click") {
|
|
90
|
+
if (tokens.length < 2) {
|
|
91
|
+
throw new Error(`Line ${lineNumber}: action click requires <selector>.`);
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
type,
|
|
95
|
+
selector: normalizeString(tokens[1]),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (type === "fill") {
|
|
100
|
+
if (tokens.length < 3) {
|
|
101
|
+
throw new Error(`Line ${lineNumber}: action fill requires <selector> <text>.`);
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
type,
|
|
105
|
+
selector: normalizeString(tokens[1]),
|
|
106
|
+
text: normalizeString(tokens[2]),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (type === "wait") {
|
|
111
|
+
if (tokens.length < 2) {
|
|
112
|
+
throw new Error(`Line ${lineNumber}: action wait requires <ms>.`);
|
|
113
|
+
}
|
|
114
|
+
const ms = Number(tokens[1]);
|
|
115
|
+
if (!Number.isFinite(ms) || ms < 0) {
|
|
116
|
+
throw new Error(`Line ${lineNumber}: action wait requires non-negative milliseconds.`);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
type,
|
|
120
|
+
ms: Math.floor(ms),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (type === "screenshot") {
|
|
125
|
+
return {
|
|
126
|
+
type,
|
|
127
|
+
path: normalizeString(tokens[1] || ""),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error(`Line ${lineNumber}: unsupported action '${type}'.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function parseScenarioDsl(content = "", { source = "" } = {}) {
|
|
135
|
+
const raw = String(content || "");
|
|
136
|
+
const lines = raw.split(/\r?\n/);
|
|
137
|
+
const spec = {
|
|
138
|
+
id: "",
|
|
139
|
+
startUrl: "",
|
|
140
|
+
tags: [],
|
|
141
|
+
actions: [],
|
|
142
|
+
source: normalizeString(source),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
146
|
+
const lineNumber = index + 1;
|
|
147
|
+
const line = lines[index];
|
|
148
|
+
const trimmed = normalizeString(line);
|
|
149
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let tokens;
|
|
154
|
+
try {
|
|
155
|
+
tokens = tokenizeLine(line);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
throw new Error(`Line ${lineNumber}: ${error.message}`);
|
|
158
|
+
}
|
|
159
|
+
if (tokens.length === 0) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const command = normalizeString(tokens[0]).toLowerCase();
|
|
163
|
+
|
|
164
|
+
if (command === "scenario") {
|
|
165
|
+
if (tokens.length < 2) {
|
|
166
|
+
throw new Error(`Line ${lineNumber}: scenario requires <id>.`);
|
|
167
|
+
}
|
|
168
|
+
spec.id = normalizeString(tokens[1]);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (command === "start_url") {
|
|
173
|
+
if (tokens.length < 2) {
|
|
174
|
+
throw new Error(`Line ${lineNumber}: start_url requires <url>.`);
|
|
175
|
+
}
|
|
176
|
+
spec.startUrl = normalizeString(tokens[1]);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (command === "tag") {
|
|
181
|
+
if (tokens.length < 2) {
|
|
182
|
+
throw new Error(`Line ${lineNumber}: tag requires <value>.`);
|
|
183
|
+
}
|
|
184
|
+
spec.tags.push(normalizeString(tokens[1]));
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (command === "action") {
|
|
189
|
+
const action = parseAction(tokens.slice(1), lineNumber);
|
|
190
|
+
spec.actions.push(action);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw new Error(`Line ${lineNumber}: unknown command '${command}'.`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return spec;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function validateScenarioSpec(spec = {}) {
|
|
201
|
+
const errors = [];
|
|
202
|
+
const id = normalizeString(spec.id);
|
|
203
|
+
if (!id) {
|
|
204
|
+
errors.push("scenario id is required.");
|
|
205
|
+
}
|
|
206
|
+
if (!Array.isArray(spec.actions) || spec.actions.length === 0) {
|
|
207
|
+
errors.push("at least one action is required.");
|
|
208
|
+
}
|
|
209
|
+
for (const action of spec.actions || []) {
|
|
210
|
+
if (!SUPPORTED_ACTIONS.has(normalizeString(action.type).toLowerCase())) {
|
|
211
|
+
errors.push(`unsupported action type: ${action.type}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
valid: errors.length === 0,
|
|
216
|
+
errors,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export async function parseScenarioFile(filePath = "") {
|
|
221
|
+
const normalizedPath = normalizeString(filePath);
|
|
222
|
+
if (!normalizedPath) {
|
|
223
|
+
throw new Error("scenario file path is required.");
|
|
224
|
+
}
|
|
225
|
+
const resolved = path.resolve(process.cwd(), normalizedPath);
|
|
226
|
+
const content = await fsp.readFile(resolved, "utf-8");
|
|
227
|
+
const spec = parseScenarioDsl(content, { source: resolved });
|
|
228
|
+
return {
|
|
229
|
+
spec,
|
|
230
|
+
filePath: resolved,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function renderScenarioTemplate({
|
|
235
|
+
scenarioId = "nightly_smoke",
|
|
236
|
+
startUrl = "https://example.com",
|
|
237
|
+
} = {}) {
|
|
238
|
+
const id = normalizeString(scenarioId) || "nightly_smoke";
|
|
239
|
+
const url = normalizeString(startUrl) || "https://example.com";
|
|
240
|
+
return `# Sentinelayer swarm scenario DSL
|
|
241
|
+
scenario "${id}"
|
|
242
|
+
start_url "${url}"
|
|
243
|
+
tag "smoke"
|
|
244
|
+
tag "runtime"
|
|
245
|
+
|
|
246
|
+
action goto "${url}"
|
|
247
|
+
action wait 500
|
|
248
|
+
action screenshot "${id}-home.png"
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export async function writeScenarioTemplate({
|
|
253
|
+
scenarioId,
|
|
254
|
+
targetPath = ".",
|
|
255
|
+
outputFile = "",
|
|
256
|
+
startUrl = "https://example.com",
|
|
257
|
+
} = {}) {
|
|
258
|
+
const normalizedTargetPath = path.resolve(String(targetPath || "."));
|
|
259
|
+
const resolvedOutputFile = normalizeString(outputFile)
|
|
260
|
+
? path.resolve(normalizedTargetPath, outputFile)
|
|
261
|
+
: path.join(normalizedTargetPath, ".sentinelayer", "scenarios", `${scenarioId}.sls`);
|
|
262
|
+
const content = renderScenarioTemplate({
|
|
263
|
+
scenarioId,
|
|
264
|
+
startUrl,
|
|
265
|
+
});
|
|
266
|
+
await fsp.mkdir(path.dirname(resolvedOutputFile), { recursive: true });
|
|
267
|
+
await fsp.writeFile(resolvedOutputFile, `${content.trim()}\n`, "utf-8");
|
|
268
|
+
return {
|
|
269
|
+
filePath: resolvedOutputFile,
|
|
270
|
+
content,
|
|
271
|
+
};
|
|
272
|
+
}
|