libretto 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/cli.js +83 -223
- package/dist/cli/commands/ai.js +32 -18
- package/dist/cli/commands/browser.js +126 -85
- package/dist/cli/commands/execution.js +147 -108
- package/dist/cli/commands/init.js +234 -131
- package/dist/cli/commands/logs.js +90 -65
- package/dist/cli/commands/shared.js +50 -0
- package/dist/cli/commands/snapshot.js +62 -37
- package/dist/cli/core/ai-config.js +29 -44
- package/dist/cli/core/api-snapshot-analyzer.js +74 -0
- package/dist/cli/core/context.js +1 -1
- package/dist/cli/core/snapshot-analyzer.js +200 -87
- package/dist/cli/core/snapshot-api-config.js +137 -0
- package/dist/cli/framework/simple-cli.js +776 -0
- package/dist/cli/router.js +29 -0
- package/dist/shared/condense-dom/condense-dom.cjs +462 -0
- package/dist/shared/condense-dom/condense-dom.d.cts +34 -0
- package/dist/shared/condense-dom/condense-dom.d.ts +34 -0
- package/dist/shared/condense-dom/condense-dom.js +438 -0
- package/dist/shared/llm/ai-sdk-adapter.cjs +5 -1
- package/dist/shared/llm/ai-sdk-adapter.js +5 -1
- package/dist/shared/llm/client.cjs +106 -27
- package/dist/shared/llm/client.d.cts +8 -1
- package/dist/shared/llm/client.d.ts +8 -1
- package/dist/shared/llm/client.js +89 -23
- package/dist/shared/llm/types.d.cts +2 -1
- package/dist/shared/llm/types.d.ts +2 -1
- package/package.json +7 -4
- /package/{.agents/skills → skills}/libretto/SKILL.md +0 -0
- /package/{.agents/skills → skills}/libretto/code-generation-rules.md +0 -0
- /package/{.agents/skills → skills}/libretto/integration-approach-selection.md +0 -0
|
@@ -1,67 +1,175 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
import { appendFileSync, cpSync, existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { readAiConfig } from "../core/ai-config.js";
|
|
7
|
+
import { REPO_ROOT } from "../core/context.js";
|
|
4
8
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from "
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
loadSnapshotEnv,
|
|
10
|
+
resolveSnapshotApiModel
|
|
11
|
+
} from "../core/snapshot-api-config.js";
|
|
12
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
|
+
import { hasProviderCredentials } from "../../shared/llm/client.js";
|
|
14
|
+
const PROVIDER_CHOICES = [
|
|
15
|
+
{
|
|
16
|
+
key: "1",
|
|
17
|
+
label: "OpenAI",
|
|
18
|
+
envVar: "OPENAI_API_KEY",
|
|
19
|
+
envHint: "Get your key at https://platform.openai.com/api-keys"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
key: "2",
|
|
23
|
+
label: "Anthropic",
|
|
24
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
25
|
+
envHint: "Get your key at https://console.anthropic.com/settings/keys"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
key: "3",
|
|
29
|
+
label: "Google Gemini",
|
|
30
|
+
envVar: "GEMINI_API_KEY",
|
|
31
|
+
envHint: "Get your key at https://aistudio.google.com/apikey"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "4",
|
|
35
|
+
label: "Google Vertex AI",
|
|
36
|
+
envVar: "GOOGLE_CLOUD_PROJECT",
|
|
37
|
+
envHint: "Requires gcloud auth application-default login and a GCP project ID"
|
|
38
|
+
}
|
|
39
|
+
];
|
|
40
|
+
function promptUser(rl, question) {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
rl.question(question, (answer) => {
|
|
43
|
+
resolve(answer.trim());
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function askYesNo(question) {
|
|
48
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
49
|
+
return new Promise((resolve) => {
|
|
50
|
+
rl.question(`${question} (y/N) `, (answer) => {
|
|
51
|
+
rl.close();
|
|
52
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
13
55
|
}
|
|
14
|
-
function
|
|
56
|
+
function safeReadAiConfig() {
|
|
15
57
|
try {
|
|
16
|
-
|
|
17
|
-
if (!stats.isFile()) return false;
|
|
18
|
-
if (process.platform === "win32") {
|
|
19
|
-
const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
20
|
-
const extensions = pathExt.split(";").map((ext) => ext.trim().toUpperCase()).filter(Boolean);
|
|
21
|
-
const fileExt = extname(filePath).toUpperCase();
|
|
22
|
-
return extensions.includes(fileExt);
|
|
23
|
-
}
|
|
24
|
-
accessSync(filePath, constants.X_OK);
|
|
25
|
-
return true;
|
|
58
|
+
return readAiConfig();
|
|
26
59
|
} catch {
|
|
27
|
-
return
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
function isCommandDefined(command) {
|
|
31
|
-
if (!command) return false;
|
|
32
|
-
if (command.includes("/") || command.includes("\\")) {
|
|
33
|
-
return isRunnableFile(command);
|
|
34
|
-
}
|
|
35
|
-
const pathEnv = process.env.PATH ?? "";
|
|
36
|
-
if (!pathEnv) return false;
|
|
37
|
-
const pathEntries = pathEnv.split(delimiter).filter(Boolean);
|
|
38
|
-
if (process.platform === "win32") {
|
|
39
|
-
const pathExt = process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD";
|
|
40
|
-
const extensions = pathExt.split(";").map((ext) => ext.trim()).filter(Boolean);
|
|
41
|
-
const hasExtension = /\.[^./\\]+$/.test(command);
|
|
42
|
-
const candidates = hasExtension ? [command] : extensions.map(
|
|
43
|
-
(ext) => ext.startsWith(".") ? `${command}${ext}` : `${command}.${ext}`
|
|
44
|
-
);
|
|
45
|
-
return pathEntries.some(
|
|
46
|
-
(dir) => candidates.some((candidate) => isRunnableFile(join(dir, candidate)))
|
|
47
|
-
);
|
|
60
|
+
return null;
|
|
48
61
|
}
|
|
49
|
-
return pathEntries.some((dir) => isRunnableFile(join(dir, command)));
|
|
50
62
|
}
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
function printSnapshotApiStatus() {
|
|
64
|
+
const config = safeReadAiConfig();
|
|
65
|
+
const selection = resolveSnapshotApiModel(config);
|
|
66
|
+
const envPath = join(REPO_ROOT, ".env");
|
|
67
|
+
console.log("\nSnapshot analysis:");
|
|
68
|
+
console.log(
|
|
69
|
+
" Libretto uses direct API calls for snapshot analysis when supported credentials are available."
|
|
54
70
|
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
console.log(
|
|
71
|
+
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
72
|
+
if (selection && hasProviderCredentials(selection.provider)) {
|
|
73
|
+
console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
|
|
74
|
+
console.log(" Snapshot objectives will use the API analyzer by default.");
|
|
75
|
+
console.log(" No further action required.");
|
|
76
|
+
return;
|
|
59
77
|
}
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
console.log(" \u2717 No snapshot API credentials detected.");
|
|
79
|
+
console.log(" Add one provider to .env:");
|
|
80
|
+
console.log(" OPENAI_API_KEY=...");
|
|
81
|
+
console.log(" ANTHROPIC_API_KEY=...");
|
|
82
|
+
console.log(" GEMINI_API_KEY=... # or GOOGLE_GENERATIVE_AI_API_KEY");
|
|
83
|
+
console.log(
|
|
84
|
+
" GOOGLE_CLOUD_PROJECT=... # plus application default credentials for Vertex"
|
|
85
|
+
);
|
|
62
86
|
console.log(
|
|
63
|
-
|
|
87
|
+
" Or run `npx libretto ai configure <provider>` to set a specific model."
|
|
64
88
|
);
|
|
89
|
+
console.log(" Run `npx libretto init` interactively to set up credentials.");
|
|
90
|
+
}
|
|
91
|
+
async function runInteractiveApiSetup() {
|
|
92
|
+
const config = safeReadAiConfig();
|
|
93
|
+
const selection = resolveSnapshotApiModel(config);
|
|
94
|
+
const envPath = join(REPO_ROOT, ".env");
|
|
95
|
+
console.log("\nSnapshot analysis setup:");
|
|
96
|
+
console.log(" Libretto uses direct API calls for snapshot analysis.");
|
|
97
|
+
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
98
|
+
if (selection && hasProviderCredentials(selection.provider)) {
|
|
99
|
+
console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
|
|
100
|
+
console.log(" Snapshot objectives will use the API analyzer by default.");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log(" \u2717 No snapshot API credentials detected.\n");
|
|
104
|
+
const rl = createInterface({
|
|
105
|
+
input: process.stdin,
|
|
106
|
+
output: process.stdout
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
console.log(" Which API provider would you like to use for snapshot analysis?\n");
|
|
110
|
+
for (const choice of PROVIDER_CHOICES) {
|
|
111
|
+
console.log(` ${choice.key}) ${choice.label}`);
|
|
112
|
+
}
|
|
113
|
+
console.log(" s) Skip for now\n");
|
|
114
|
+
const answer = await promptUser(rl, " Choice: ");
|
|
115
|
+
if (answer.toLowerCase() === "s" || !answer) {
|
|
116
|
+
console.log("\n Skipped. You can set up API credentials later by rerunning `npx libretto init`.");
|
|
117
|
+
console.log(" Or add credentials directly to your .env file:");
|
|
118
|
+
console.log(" OPENAI_API_KEY=...");
|
|
119
|
+
console.log(" ANTHROPIC_API_KEY=...");
|
|
120
|
+
console.log(" GEMINI_API_KEY=...");
|
|
121
|
+
console.log(
|
|
122
|
+
" Or run `npx libretto ai configure <provider>` to set a specific model."
|
|
123
|
+
);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
|
|
127
|
+
if (!selected) {
|
|
128
|
+
console.log(`
|
|
129
|
+
Unknown choice "${answer}". Skipping API setup.`);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
console.log(`
|
|
133
|
+
${selected.label} selected.`);
|
|
134
|
+
console.log(` ${selected.envHint}
|
|
135
|
+
`);
|
|
136
|
+
const apiKeyValue = await promptUser(
|
|
137
|
+
rl,
|
|
138
|
+
` Enter your ${selected.envVar}: `
|
|
139
|
+
);
|
|
140
|
+
if (!apiKeyValue) {
|
|
141
|
+
console.log("\n No value entered. Skipping API key setup.");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
let envContent = "";
|
|
145
|
+
if (existsSync(envPath)) {
|
|
146
|
+
envContent = readFileSync(envPath, "utf-8");
|
|
147
|
+
}
|
|
148
|
+
const envLine = `${selected.envVar}=${apiKeyValue}`;
|
|
149
|
+
if (envContent.includes(`${selected.envVar}=`)) {
|
|
150
|
+
const updated = envContent.replace(
|
|
151
|
+
new RegExp(`^${selected.envVar}=.*$`, "m"),
|
|
152
|
+
() => envLine
|
|
153
|
+
);
|
|
154
|
+
writeFileSync(envPath, updated);
|
|
155
|
+
console.log(`
|
|
156
|
+
\u2713 Updated ${selected.envVar} in ${envPath}`);
|
|
157
|
+
} else {
|
|
158
|
+
const separator = envContent && !envContent.endsWith("\n") ? "\n" : "";
|
|
159
|
+
appendFileSync(envPath, `${separator}${envLine}
|
|
160
|
+
`);
|
|
161
|
+
console.log(`
|
|
162
|
+
\u2713 Added ${selected.envVar} to ${envPath}`);
|
|
163
|
+
}
|
|
164
|
+
loadSnapshotEnv();
|
|
165
|
+
process.env[selected.envVar] = apiKeyValue;
|
|
166
|
+
const newSelection = resolveSnapshotApiModel(safeReadAiConfig());
|
|
167
|
+
if (newSelection && hasProviderCredentials(newSelection.provider)) {
|
|
168
|
+
console.log(` \u2713 Snapshot API ready: ${newSelection.model}`);
|
|
169
|
+
}
|
|
170
|
+
} finally {
|
|
171
|
+
rl.close();
|
|
172
|
+
}
|
|
65
173
|
}
|
|
66
174
|
function installBrowsers() {
|
|
67
175
|
console.log("\nInstalling Playwright Chromium...");
|
|
@@ -77,90 +185,85 @@ function installBrowsers() {
|
|
|
77
185
|
);
|
|
78
186
|
}
|
|
79
187
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
let
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
188
|
+
function getPackageSkillsDir() {
|
|
189
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
190
|
+
let dir = dirname(thisFile);
|
|
191
|
+
while (dir !== dirname(dir)) {
|
|
192
|
+
if (existsSync(join(dir, "skills", "libretto"))) {
|
|
193
|
+
return join(dir, "skills", "libretto");
|
|
194
|
+
}
|
|
195
|
+
dir = dirname(dir);
|
|
87
196
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
197
|
+
throw new Error("Could not locate libretto skill files in package");
|
|
198
|
+
}
|
|
199
|
+
async function copySkills() {
|
|
200
|
+
const cwd = process.cwd();
|
|
201
|
+
const agentDirs = [];
|
|
202
|
+
if (existsSync(join(cwd, ".agents"))) {
|
|
203
|
+
agentDirs.push({
|
|
204
|
+
name: ".agents",
|
|
205
|
+
skillDest: join(cwd, ".agents", "skills", "libretto")
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (existsSync(join(cwd, ".claude"))) {
|
|
209
|
+
agentDirs.push({
|
|
210
|
+
name: ".claude",
|
|
211
|
+
skillDest: join(cwd, ".claude", "skills", "libretto")
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (agentDirs.length === 0) {
|
|
215
|
+
console.log("\nSkills: No .agents/ or .claude/ directory found \u2014 skipping skill copy.");
|
|
101
216
|
return;
|
|
102
217
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
console.log(
|
|
111
|
-
` Detected available commands: ${availableCommands.join(", ")}`
|
|
112
|
-
);
|
|
113
|
-
} else {
|
|
114
|
-
console.log(
|
|
115
|
-
" No codex, claude, or gemini analyzer command was detected on PATH."
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
console.log(" Reconfigure with:");
|
|
119
|
-
printAiConfigureCommands(" ");
|
|
120
|
-
printDifferentAnalyzerHint(" ");
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
console.log(
|
|
124
|
-
` \u2713 Configured (${config.preset}): ${formatCommandPrefix(config.commandPrefix)}`
|
|
125
|
-
);
|
|
126
|
-
console.log(" Analysis commands are ready to use.");
|
|
127
|
-
printDifferentAnalyzerHint(" ");
|
|
218
|
+
const dirNames = agentDirs.map((d) => d.name).join(" and ");
|
|
219
|
+
const existing = agentDirs.filter((d) => existsSync(d.skillDest));
|
|
220
|
+
const verb = existing.length > 0 ? "Overwrite" : "Install";
|
|
221
|
+
const proceed = await askYesNo(`
|
|
222
|
+
${verb} libretto skills in ${dirNames}?`);
|
|
223
|
+
if (!proceed) {
|
|
224
|
+
console.log(" Skipping skill copy.");
|
|
128
225
|
return;
|
|
129
226
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
);
|
|
135
|
-
|
|
136
|
-
|
|
227
|
+
let sourceDir;
|
|
228
|
+
try {
|
|
229
|
+
sourceDir = getPackageSkillsDir();
|
|
230
|
+
} catch (e) {
|
|
231
|
+
console.error(` \u2717 ${e instanceof Error ? e.message : String(e)}`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
for (const { name, skillDest } of agentDirs) {
|
|
235
|
+
cpSync(sourceDir, skillDest, { recursive: true });
|
|
236
|
+
const fileCount = readdirSync(skillDest).length;
|
|
237
|
+
console.log(` \u2713 Copied ${fileCount} skill files to ${name}/skills/libretto/`);
|
|
137
238
|
}
|
|
138
|
-
console.log(" Configure one with:");
|
|
139
|
-
printAiConfigureCommands(" ");
|
|
140
|
-
printDifferentAnalyzerHint(" ");
|
|
141
|
-
console.log(" Optionally provide a custom command prefix with '-- ...'.");
|
|
142
|
-
}
|
|
143
|
-
function registerInitCommand(yargs) {
|
|
144
|
-
return yargs.command(
|
|
145
|
-
"init",
|
|
146
|
-
"Initialize libretto in the current project",
|
|
147
|
-
(cmd) => cmd.option("skip-browsers", {
|
|
148
|
-
type: "boolean",
|
|
149
|
-
default: false,
|
|
150
|
-
describe: "Skip Playwright Chromium installation"
|
|
151
|
-
}),
|
|
152
|
-
(argv) => {
|
|
153
|
-
console.log("Initializing libretto...\n");
|
|
154
|
-
if (!argv["skip-browsers"]) {
|
|
155
|
-
installBrowsers();
|
|
156
|
-
} else {
|
|
157
|
-
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
158
|
-
}
|
|
159
|
-
checkAiRuntimeConfiguration();
|
|
160
|
-
console.log("\n\u2713 libretto init complete");
|
|
161
|
-
}
|
|
162
|
-
);
|
|
163
239
|
}
|
|
240
|
+
const initInput = SimpleCLI.input({
|
|
241
|
+
positionals: [],
|
|
242
|
+
named: {
|
|
243
|
+
skipBrowsers: SimpleCLI.flag({
|
|
244
|
+
name: "skip-browsers",
|
|
245
|
+
help: "Skip Playwright Chromium installation"
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
const initCommand = SimpleCLI.command({
|
|
250
|
+
description: "Initialize libretto in the current project"
|
|
251
|
+
}).input(initInput).handle(async ({ input }) => {
|
|
252
|
+
console.log("Initializing libretto...\n");
|
|
253
|
+
if (!input.skipBrowsers) {
|
|
254
|
+
installBrowsers();
|
|
255
|
+
} else {
|
|
256
|
+
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
257
|
+
}
|
|
258
|
+
if (process.stdin.isTTY) {
|
|
259
|
+
await copySkills();
|
|
260
|
+
await runInteractiveApiSetup();
|
|
261
|
+
} else {
|
|
262
|
+
printSnapshotApiStatus();
|
|
263
|
+
}
|
|
264
|
+
console.log("\n\u2713 libretto init complete");
|
|
265
|
+
});
|
|
164
266
|
export {
|
|
165
|
-
|
|
267
|
+
initCommand,
|
|
268
|
+
initInput
|
|
166
269
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { listOpenPages } from "../core/browser.js";
|
|
2
3
|
import { withSessionLogger } from "../core/context.js";
|
|
3
4
|
import {
|
|
@@ -8,6 +9,14 @@ import {
|
|
|
8
9
|
readActionLog,
|
|
9
10
|
readNetworkLog
|
|
10
11
|
} from "../core/telemetry.js";
|
|
12
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
|
+
import {
|
|
14
|
+
integerOption,
|
|
15
|
+
loadSessionStateMiddleware,
|
|
16
|
+
pageOption,
|
|
17
|
+
resolveSessionMiddleware,
|
|
18
|
+
sessionOption
|
|
19
|
+
} from "./shared.js";
|
|
11
20
|
async function resolvePageId(session, pageId) {
|
|
12
21
|
if (!pageId) return void 0;
|
|
13
22
|
const pages = await withSessionLogger(
|
|
@@ -22,72 +31,88 @@ async function resolvePageId(session, pageId) {
|
|
|
22
31
|
}
|
|
23
32
|
return pageId;
|
|
24
33
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
34
|
+
const networkInput = SimpleCLI.input({
|
|
35
|
+
positionals: [],
|
|
36
|
+
named: {
|
|
37
|
+
session: sessionOption(),
|
|
38
|
+
last: integerOption(),
|
|
39
|
+
filter: SimpleCLI.option(z.string().optional()),
|
|
40
|
+
method: SimpleCLI.option(z.string().optional()),
|
|
41
|
+
page: pageOption(),
|
|
42
|
+
clear: SimpleCLI.flag()
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
const networkCommand = SimpleCLI.command({
|
|
46
|
+
description: "View captured network requests"
|
|
47
|
+
}).input(networkInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ input, ctx }) => {
|
|
48
|
+
if (input.clear) {
|
|
49
|
+
clearNetworkLog(ctx.session);
|
|
50
|
+
console.log("Network log cleared.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const pageId = await resolvePageId(ctx.session, input.page);
|
|
54
|
+
const entries = readNetworkLog(ctx.session, {
|
|
55
|
+
last: input.last,
|
|
56
|
+
filter: input.filter,
|
|
57
|
+
method: input.method,
|
|
58
|
+
pageId
|
|
59
|
+
});
|
|
60
|
+
if (entries.length === 0) {
|
|
61
|
+
console.log("No network requests captured.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
console.log(formatNetworkEntry(entry));
|
|
66
|
+
}
|
|
67
|
+
console.log(`
|
|
55
68
|
${entries.length} request(s) shown.`);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
69
|
+
});
|
|
70
|
+
const actionsInput = SimpleCLI.input({
|
|
71
|
+
positionals: [],
|
|
72
|
+
named: {
|
|
73
|
+
session: sessionOption(),
|
|
74
|
+
last: integerOption(),
|
|
75
|
+
filter: SimpleCLI.option(z.string().optional()),
|
|
76
|
+
action: SimpleCLI.option(z.string().optional()),
|
|
77
|
+
source: SimpleCLI.option(z.string().optional()),
|
|
78
|
+
page: pageOption(),
|
|
79
|
+
clear: SimpleCLI.flag()
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
const actionsCommand = SimpleCLI.command({
|
|
83
|
+
description: "View captured actions"
|
|
84
|
+
}).input(actionsInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ input, ctx }) => {
|
|
85
|
+
if (input.clear) {
|
|
86
|
+
clearActionLog(ctx.session);
|
|
87
|
+
console.log("Action log cleared.");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const pageId = await resolvePageId(ctx.session, input.page);
|
|
91
|
+
const entries = readActionLog(ctx.session, {
|
|
92
|
+
last: input.last,
|
|
93
|
+
filter: input.filter,
|
|
94
|
+
action: input.action,
|
|
95
|
+
source: input.source,
|
|
96
|
+
pageId
|
|
97
|
+
});
|
|
98
|
+
if (entries.length === 0) {
|
|
99
|
+
console.log("No actions captured.");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
console.log(formatActionEntry(entry));
|
|
104
|
+
}
|
|
105
|
+
console.log(`
|
|
87
106
|
${entries.length} action(s) shown.`);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
});
|
|
108
|
+
const logCommands = {
|
|
109
|
+
network: networkCommand,
|
|
110
|
+
actions: actionsCommand
|
|
111
|
+
};
|
|
91
112
|
export {
|
|
92
|
-
|
|
113
|
+
actionsCommand,
|
|
114
|
+
actionsInput,
|
|
115
|
+
logCommands,
|
|
116
|
+
networkCommand,
|
|
117
|
+
networkInput
|
|
93
118
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
SESSION_DEFAULT,
|
|
4
|
+
readSessionStateOrThrow,
|
|
5
|
+
validateSessionName
|
|
6
|
+
} from "../core/session.js";
|
|
7
|
+
import {
|
|
8
|
+
SimpleCLI
|
|
9
|
+
} from "../framework/simple-cli.js";
|
|
10
|
+
function createSessionSchema() {
|
|
11
|
+
return z.string().default(SESSION_DEFAULT).superRefine((value, ctx) => {
|
|
12
|
+
try {
|
|
13
|
+
validateSessionName(value);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
ctx.addIssue({
|
|
16
|
+
code: z.ZodIssueCode.custom,
|
|
17
|
+
message: err instanceof Error ? err.message : String(err)
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function sessionOption(help = "Use a named session") {
|
|
23
|
+
return SimpleCLI.option(createSessionSchema(), { help });
|
|
24
|
+
}
|
|
25
|
+
function pageOption(help = "Target a specific page id") {
|
|
26
|
+
return SimpleCLI.option(z.string().optional(), { help });
|
|
27
|
+
}
|
|
28
|
+
function integerOption(help) {
|
|
29
|
+
return SimpleCLI.option(z.coerce.number().int().optional(), { help });
|
|
30
|
+
}
|
|
31
|
+
const resolveSessionMiddleware = async ({ input, ctx }) => {
|
|
32
|
+
return {
|
|
33
|
+
...ctx,
|
|
34
|
+
session: input.session
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
const loadSessionStateMiddleware = async ({ ctx }) => {
|
|
38
|
+
return {
|
|
39
|
+
...ctx,
|
|
40
|
+
sessionState: readSessionStateOrThrow(ctx.session)
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
export {
|
|
44
|
+
createSessionSchema,
|
|
45
|
+
integerOption,
|
|
46
|
+
loadSessionStateMiddleware,
|
|
47
|
+
pageOption,
|
|
48
|
+
resolveSessionMiddleware,
|
|
49
|
+
sessionOption
|
|
50
|
+
};
|