libretto 0.2.7 → 0.3.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/{skill → .agents/skills/libretto}/SKILL.md +20 -18
- package/{skill → .agents/skills/libretto}/code-generation-rules.md +3 -3
- package/{skill → .agents/skills/libretto}/integration-approach-selection.md +3 -3
- package/LICENSE +21 -0
- package/README.md +41 -125
- package/dist/cli/cli.js +148 -59
- package/dist/cli/commands/execution.js +45 -16
- package/dist/cli/commands/init.js +120 -49
- package/dist/cli/core/browser.js +4 -0
- package/dist/cli/core/context.js +5 -6
- package/dist/cli/index.js +0 -0
- package/dist/cli/workers/run-integration-runtime.js +10 -3
- package/dist/cli/workers/run-integration-worker.js +2 -1
- package/dist/index.cjs +28 -0
- package/dist/index.js +17 -0
- package/package.json +28 -81
- package/bin/libretto.mjs +0 -18
- package/scripts/postinstall.mjs +0 -48
package/dist/cli/cli.js
CHANGED
|
@@ -12,9 +12,11 @@ import {
|
|
|
12
12
|
ensureLibrettoSetup
|
|
13
13
|
} from "./core/context.js";
|
|
14
14
|
import {
|
|
15
|
-
|
|
15
|
+
listSessionsWithStateFile,
|
|
16
16
|
validateSessionName
|
|
17
17
|
} from "./core/session.js";
|
|
18
|
+
const AUTO_SESSION_COMMANDS = /* @__PURE__ */ new Set(["open", "run"]);
|
|
19
|
+
const SESSION_OPTIONAL_COMMANDS = /* @__PURE__ */ new Set(["help", "--help", "-h", "init", "ai"]);
|
|
18
20
|
const CLI_COMMANDS = /* @__PURE__ */ new Set([
|
|
19
21
|
"open",
|
|
20
22
|
"run",
|
|
@@ -36,10 +38,10 @@ function printUsage() {
|
|
|
36
38
|
console.log(`Usage: libretto-cli <command> [--session <name>]
|
|
37
39
|
|
|
38
40
|
Commands:
|
|
39
|
-
init [--skip-browsers] Initialize libretto (
|
|
41
|
+
init [--skip-browsers] Initialize libretto (install browsers, check AI setup)
|
|
40
42
|
open <url> [--headless] Launch browser and open URL (headed by default)
|
|
41
43
|
Automatically loads saved profile if available
|
|
42
|
-
run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless] Run an exported Libretto workflow from a file
|
|
44
|
+
run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless] Run an exported Libretto workflow from a file
|
|
43
45
|
ai configure [preset] [-- <command prefix...>] Configure AI runtime for analysis commands
|
|
44
46
|
save <url|domain> Save current browser session (cookies, localStorage, etc.)
|
|
45
47
|
exec <code> [--visualize] Execute Playwright typescript code (--visualize enables ghost cursor + highlight)
|
|
@@ -51,7 +53,9 @@ Commands:
|
|
|
51
53
|
close [--all] [--force] Close the browser for the session, or all tracked sessions with --all
|
|
52
54
|
|
|
53
55
|
Options:
|
|
54
|
-
--session <name> Use a named session
|
|
56
|
+
--session <name> Use a named session
|
|
57
|
+
If omitted for open/run, a session id is auto-generated
|
|
58
|
+
All other stateful commands require --session
|
|
55
59
|
Built-in sessions: default, dev-server, browser-agent
|
|
56
60
|
|
|
57
61
|
Examples:
|
|
@@ -107,55 +111,116 @@ function filterSessionArgs(args) {
|
|
|
107
111
|
}
|
|
108
112
|
function parseSessionForLog(rawArgs) {
|
|
109
113
|
const idx = rawArgs.indexOf("--session");
|
|
110
|
-
if (idx < 0) return
|
|
114
|
+
if (idx < 0) return null;
|
|
111
115
|
const value = rawArgs[idx + 1];
|
|
112
116
|
if (!value || value.startsWith("--") || CLI_COMMANDS.has(value)) {
|
|
113
|
-
return
|
|
117
|
+
return null;
|
|
114
118
|
}
|
|
115
119
|
try {
|
|
116
120
|
validateSessionName(value);
|
|
117
121
|
return value;
|
|
118
122
|
} catch {
|
|
119
|
-
return
|
|
123
|
+
return null;
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
function hasExplicitSession(rawArgs) {
|
|
127
|
+
return rawArgs.includes("--session");
|
|
128
|
+
}
|
|
129
|
+
function randomSessionId() {
|
|
130
|
+
const digits = Math.floor(Math.random() * 1e4).toString().padStart(4, "0");
|
|
131
|
+
return `ses-${digits}`;
|
|
132
|
+
}
|
|
133
|
+
function generateSessionId() {
|
|
134
|
+
const activeSessions = new Set(listSessionsWithStateFile());
|
|
135
|
+
for (let attempt = 0; attempt < 1e4; attempt += 1) {
|
|
136
|
+
const candidate = randomSessionId();
|
|
137
|
+
if (!activeSessions.has(candidate)) {
|
|
138
|
+
return candidate;
|
|
139
|
+
}
|
|
130
140
|
}
|
|
131
|
-
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Could not generate an available session id. Close an existing session and try again."
|
|
143
|
+
);
|
|
132
144
|
}
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
function hasExecCodeArg(filteredArgs) {
|
|
146
|
+
for (let i = 1; i < filteredArgs.length; i += 1) {
|
|
147
|
+
const token = filteredArgs[i];
|
|
148
|
+
if (!token) continue;
|
|
149
|
+
if (token === "--") {
|
|
150
|
+
return filteredArgs.length > i + 1;
|
|
151
|
+
}
|
|
152
|
+
if (token === "--visualize") {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (token === "--page") {
|
|
156
|
+
const maybeValue = filteredArgs[i + 1];
|
|
157
|
+
if (maybeValue && !maybeValue.startsWith("--")) {
|
|
158
|
+
i += 1;
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (token.startsWith("--page=")) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (token.startsWith("-")) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
142
171
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
function commandNeedsSession(command, rawArgs, filteredArgs) {
|
|
173
|
+
if (AUTO_SESSION_COMMANDS.has(command)) return false;
|
|
174
|
+
if (SESSION_OPTIONAL_COMMANDS.has(command)) return false;
|
|
175
|
+
if (command === "close" && rawArgs.includes("--all")) return false;
|
|
176
|
+
if (command === "close" && rawArgs.includes("--force")) return false;
|
|
177
|
+
if (command === "exec" && !hasExecCodeArg(filteredArgs)) return false;
|
|
178
|
+
if (command === "save" && filteredArgs.length <= 1) return false;
|
|
179
|
+
if (!CLI_COMMANDS.has(command)) return false;
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
function resolveSessionArgs(rawArgs) {
|
|
183
|
+
const filtered = filterSessionArgs(rawArgs);
|
|
184
|
+
const command = filtered[0];
|
|
185
|
+
const explicitSession = parseSessionForLog(rawArgs);
|
|
186
|
+
if (!command) {
|
|
187
|
+
return {
|
|
188
|
+
args: rawArgs,
|
|
189
|
+
generatedSession: null,
|
|
190
|
+
resolvedSession: explicitSession
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (hasExplicitSession(rawArgs)) {
|
|
194
|
+
return {
|
|
195
|
+
args: rawArgs,
|
|
196
|
+
generatedSession: null,
|
|
197
|
+
resolvedSession: explicitSession
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (!AUTO_SESSION_COMMANDS.has(command)) {
|
|
201
|
+
return {
|
|
202
|
+
args: rawArgs,
|
|
203
|
+
generatedSession: null,
|
|
204
|
+
resolvedSession: null
|
|
205
|
+
};
|
|
149
206
|
}
|
|
207
|
+
const generatedSession = generateSessionId();
|
|
208
|
+
return {
|
|
209
|
+
args: [...rawArgs, "--session", generatedSession],
|
|
210
|
+
generatedSession,
|
|
211
|
+
resolvedSession: generatedSession
|
|
212
|
+
};
|
|
150
213
|
}
|
|
151
214
|
function createParser(logger) {
|
|
152
215
|
let parser = yargs(hideBin(process.argv)).scriptName("libretto-cli").parserConfiguration({ "populate--": true }).option("session", {
|
|
153
216
|
type: "string",
|
|
154
|
-
default: SESSION_DEFAULT,
|
|
155
217
|
describe: "Use a named session",
|
|
156
|
-
global: true
|
|
218
|
+
global: true,
|
|
219
|
+
requiresArg: true
|
|
157
220
|
}).middleware((argv) => {
|
|
158
|
-
|
|
221
|
+
if (argv.session !== void 0) {
|
|
222
|
+
validateSessionName(String(argv.session));
|
|
223
|
+
}
|
|
159
224
|
}).exitProcess(false).help(false).version(false).fail((msg, err) => {
|
|
160
225
|
if (err) throw err;
|
|
161
226
|
throw new Error(msg || "Command failed");
|
|
@@ -175,33 +240,57 @@ function createParser(logger) {
|
|
|
175
240
|
async function runLibrettoCLI() {
|
|
176
241
|
const rawArgs = process.argv.slice(2);
|
|
177
242
|
let exitCode = 0;
|
|
243
|
+
let effectiveArgs = rawArgs;
|
|
244
|
+
let generatedSession = null;
|
|
245
|
+
let resolvedSession = null;
|
|
246
|
+
({
|
|
247
|
+
args: effectiveArgs,
|
|
248
|
+
generatedSession,
|
|
249
|
+
resolvedSession
|
|
250
|
+
} = resolveSessionArgs(rawArgs));
|
|
178
251
|
ensureLibrettoSetup();
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!CLI_COMMANDS.has(command)) {
|
|
189
|
-
console.error(`Unknown command: ${command}
|
|
252
|
+
const args = filterSessionArgs(effectiveArgs);
|
|
253
|
+
const command = args[0];
|
|
254
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
255
|
+
printUsage();
|
|
256
|
+
process.exit(exitCode);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (!CLI_COMMANDS.has(command)) {
|
|
260
|
+
console.error(`Unknown command: ${command}
|
|
190
261
|
`);
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
262
|
+
printUsage();
|
|
263
|
+
process.exit(1);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (!hasExplicitSession(effectiveArgs) && commandNeedsSession(command, effectiveArgs, args)) {
|
|
267
|
+
console.error(
|
|
268
|
+
[
|
|
269
|
+
`Missing required --session for "${command}".`,
|
|
270
|
+
"Pass --session <name>, or use open/run without --session to auto-create one."
|
|
271
|
+
].join("\n")
|
|
272
|
+
);
|
|
273
|
+
process.exit(1);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const sessionForLogger = resolvedSession ?? "cli";
|
|
277
|
+
const logger = createLoggerForSession(sessionForLogger);
|
|
278
|
+
try {
|
|
279
|
+
const parser = createParser(logger);
|
|
280
|
+
await parser.parseAsync(effectiveArgs);
|
|
281
|
+
} catch (err) {
|
|
282
|
+
logger.error("cli-error", {
|
|
283
|
+
error: err,
|
|
284
|
+
args: rawArgs,
|
|
285
|
+
effectiveArgs,
|
|
286
|
+
generatedSession
|
|
287
|
+
});
|
|
288
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
289
|
+
console.error(message);
|
|
290
|
+
exitCode = 1;
|
|
291
|
+
} finally {
|
|
292
|
+
await closeLogger(logger);
|
|
293
|
+
}
|
|
205
294
|
process.exit(exitCode);
|
|
206
295
|
}
|
|
207
296
|
export {
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
wrapPageForActionLogging
|
|
22
22
|
} from "../core/telemetry.js";
|
|
23
23
|
const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
|
|
24
|
+
const require2 = moduleBuiltin.createRequire(import.meta.url);
|
|
25
|
+
const tsxCliPath = require2.resolve("tsx/cli");
|
|
24
26
|
function withSuppressedStripTypeScriptWarning(action) {
|
|
25
27
|
const mutableProcess = process;
|
|
26
28
|
const originalEmitWarning = mutableProcess.emitWarning;
|
|
@@ -209,20 +211,25 @@ function readJsonFileIfExists(path) {
|
|
|
209
211
|
return null;
|
|
210
212
|
}
|
|
211
213
|
}
|
|
212
|
-
function
|
|
214
|
+
function readFailureSignal(path) {
|
|
213
215
|
const raw = readJsonFileIfExists(path);
|
|
214
216
|
if (!raw || typeof raw !== "object") return null;
|
|
215
217
|
const message = raw.message;
|
|
216
|
-
|
|
218
|
+
if (typeof message !== "string") return null;
|
|
219
|
+
const phase = raw.phase;
|
|
220
|
+
return {
|
|
221
|
+
message,
|
|
222
|
+
phase: phase === "setup" || phase === "workflow" ? phase : void 0
|
|
223
|
+
};
|
|
217
224
|
}
|
|
218
|
-
async function
|
|
225
|
+
async function waitForFailureSignal(path, timeoutMs = 1e3) {
|
|
219
226
|
const deadline = Date.now() + timeoutMs;
|
|
220
227
|
while (Date.now() < deadline) {
|
|
221
|
-
const
|
|
222
|
-
if (
|
|
228
|
+
const failure = readFailureSignal(path);
|
|
229
|
+
if (failure) return failure;
|
|
223
230
|
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
224
231
|
}
|
|
225
|
-
return
|
|
232
|
+
return readFailureSignal(path);
|
|
226
233
|
}
|
|
227
234
|
function streamOutputSince(path, offset) {
|
|
228
235
|
if (!existsSync(path)) return offset;
|
|
@@ -248,8 +255,12 @@ async function waitForWorkflowOutcome(args) {
|
|
|
248
255
|
outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
|
|
249
256
|
if (existsSync(signalPaths.failedSignalPath)) {
|
|
250
257
|
outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
|
|
251
|
-
const
|
|
252
|
-
return {
|
|
258
|
+
const failure = await waitForFailureSignal(signalPaths.failedSignalPath);
|
|
259
|
+
return {
|
|
260
|
+
status: "failed",
|
|
261
|
+
message: failure?.message,
|
|
262
|
+
failurePhase: failure?.phase
|
|
263
|
+
};
|
|
253
264
|
}
|
|
254
265
|
if (existsSync(signalPaths.completedSignalPath)) {
|
|
255
266
|
outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
|
|
@@ -339,8 +350,20 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
339
350
|
const workerEntryPath = fileURLToPath(
|
|
340
351
|
new URL("../workers/run-integration-worker.js", import.meta.url)
|
|
341
352
|
);
|
|
342
|
-
const payload = JSON.stringify(
|
|
343
|
-
|
|
353
|
+
const payload = JSON.stringify({
|
|
354
|
+
integrationPath: args.integrationPath,
|
|
355
|
+
exportName: args.exportName,
|
|
356
|
+
session: args.session,
|
|
357
|
+
params: args.params,
|
|
358
|
+
headless: args.headless,
|
|
359
|
+
authProfileDomain: args.authProfileDomain
|
|
360
|
+
});
|
|
361
|
+
const worker = spawn(process.execPath, [
|
|
362
|
+
tsxCliPath,
|
|
363
|
+
...args.tsconfigPath ? ["--tsconfig", args.tsconfigPath] : [],
|
|
364
|
+
workerEntryPath,
|
|
365
|
+
payload
|
|
366
|
+
], {
|
|
344
367
|
detached: true,
|
|
345
368
|
stdio: "ignore",
|
|
346
369
|
env: process.env
|
|
@@ -357,10 +380,14 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
357
380
|
}
|
|
358
381
|
if (outcome.status === "failed") {
|
|
359
382
|
setSessionStatus(args.session, "failed", logger);
|
|
360
|
-
|
|
361
|
-
|
|
383
|
+
const message = outcome.message ?? "Workflow failed during run.";
|
|
384
|
+
if (outcome.failurePhase === "workflow") {
|
|
385
|
+
throw new Error(
|
|
386
|
+
`${message}
|
|
362
387
|
Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-run the workflow.`
|
|
363
|
-
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
throw new Error(message);
|
|
364
391
|
}
|
|
365
392
|
if (outcome.status === "exited") {
|
|
366
393
|
setSessionStatus(args.session, "exited", logger);
|
|
@@ -394,9 +421,9 @@ function registerExecutionCommands(yargs, logger) {
|
|
|
394
421
|
).command(
|
|
395
422
|
"run [integrationFile] [integrationExport]",
|
|
396
423
|
"Run an exported Libretto workflow from a file",
|
|
397
|
-
(cmd) => cmd.option("params", { type: "string" }).option("params-file", { type: "string" }).option("headed", { type: "boolean", default: false }).option("headless", { type: "boolean", default: false }).option("auth-profile", { type: "string", describe: "Domain for local auth profile (e.g. apps.example.com)" }),
|
|
424
|
+
(cmd) => cmd.option("params", { type: "string" }).option("params-file", { type: "string" }).option("tsconfig", { type: "string" }).option("headed", { type: "boolean", default: false }).option("headless", { type: "boolean", default: false }).option("auth-profile", { type: "string", describe: "Domain for local auth profile (e.g. apps.example.com)" }),
|
|
398
425
|
async (argv) => {
|
|
399
|
-
const usage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless]";
|
|
426
|
+
const usage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]";
|
|
400
427
|
const integrationPath = argv.integrationFile;
|
|
401
428
|
const exportName = argv.integrationExport;
|
|
402
429
|
const legacyDebug = argv.debug;
|
|
@@ -438,13 +465,15 @@ function registerExecutionCommands(yargs, logger) {
|
|
|
438
465
|
}
|
|
439
466
|
const headlessMode = hasHeadedFlag ? false : hasHeadlessFlag ? true : void 0;
|
|
440
467
|
const authProfileDomain = argv["auth-profile"];
|
|
468
|
+
const tsconfigPath = argv.tsconfig;
|
|
441
469
|
await runIntegrationFromFile({
|
|
442
470
|
integrationPath,
|
|
443
471
|
exportName,
|
|
444
472
|
session,
|
|
445
473
|
params,
|
|
446
474
|
headless: headlessMode ?? false,
|
|
447
|
-
authProfileDomain
|
|
475
|
+
authProfileDomain,
|
|
476
|
+
tsconfigPath
|
|
448
477
|
}, logger);
|
|
449
478
|
}
|
|
450
479
|
).command(
|
|
@@ -1,36 +1,68 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join,
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
1
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
2
|
+
import { join, delimiter, extname } from "node:path";
|
|
4
3
|
import { spawnSync } from "node:child_process";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"Could not find skill/ or skills/ directory in the libretto package."
|
|
15
|
-
);
|
|
4
|
+
import {
|
|
5
|
+
AI_CONFIG_PRESETS,
|
|
6
|
+
AiPresetSchema,
|
|
7
|
+
formatCommandPrefix,
|
|
8
|
+
readAiConfig
|
|
9
|
+
} from "../core/ai-config.js";
|
|
10
|
+
const AI_RUNTIME_PRESETS = AiPresetSchema.options;
|
|
11
|
+
function getPresetCommand(preset) {
|
|
12
|
+
return AI_CONFIG_PRESETS[preset][0] ?? "";
|
|
16
13
|
}
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
function isRunnableFile(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
const stats = statSync(filePath);
|
|
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;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
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
|
+
);
|
|
23
48
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
49
|
+
return pathEntries.some((dir) => isRunnableFile(join(dir, command)));
|
|
50
|
+
}
|
|
51
|
+
function detectAvailableAiRuntimeCommands() {
|
|
52
|
+
return AI_RUNTIME_PRESETS.filter(
|
|
53
|
+
(preset) => isCommandDefined(getPresetCommand(preset))
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function printAiConfigureCommands(prefix = " ") {
|
|
57
|
+
for (const preset of AI_RUNTIME_PRESETS) {
|
|
58
|
+
console.log(`${prefix}npx libretto ai configure ${preset}`);
|
|
32
59
|
}
|
|
33
60
|
}
|
|
61
|
+
function printDifferentAnalyzerHint(prefix = " ") {
|
|
62
|
+
console.log(
|
|
63
|
+
`${prefix}Use npx libretto ai configure <gemini|claude|codex> to configure a different AI analyzer.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
34
66
|
function installBrowsers() {
|
|
35
67
|
console.log("\nInstalling Playwright Chromium...");
|
|
36
68
|
const result = spawnSync("npx", ["playwright", "install", "chromium"], {
|
|
@@ -45,21 +77,68 @@ function installBrowsers() {
|
|
|
45
77
|
);
|
|
46
78
|
}
|
|
47
79
|
}
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
function checkAiRuntimeConfiguration() {
|
|
81
|
+
let config = null;
|
|
82
|
+
let configReadError = null;
|
|
83
|
+
try {
|
|
84
|
+
config = readAiConfig();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
configReadError = error instanceof Error ? error.message : String(error);
|
|
87
|
+
}
|
|
88
|
+
const availableCommands = detectAvailableAiRuntimeCommands();
|
|
89
|
+
console.log("\nAI runtime configuration:");
|
|
90
|
+
console.log(
|
|
91
|
+
" Libretto can use your coding agent as a subagent to analyze snapshots and other page signals."
|
|
92
|
+
);
|
|
93
|
+
console.log(
|
|
94
|
+
" This is optional, but it significantly improves page understanding and debugging performance."
|
|
95
|
+
);
|
|
96
|
+
if (configReadError) {
|
|
97
|
+
console.log(` \u2717 Could not read AI config: ${configReadError}`);
|
|
98
|
+
console.log(" Reconfigure with:");
|
|
99
|
+
printAiConfigureCommands(" ");
|
|
100
|
+
printDifferentAnalyzerHint(" ");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (config) {
|
|
104
|
+
const configuredCommand = config.commandPrefix[0];
|
|
105
|
+
if (!isCommandDefined(configuredCommand)) {
|
|
106
|
+
console.log(
|
|
107
|
+
` \u2717 Configured command not found: ${configuredCommand ?? "(empty)"}`
|
|
108
|
+
);
|
|
109
|
+
if (availableCommands.length > 0) {
|
|
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
|
+
}
|
|
59
123
|
console.log(
|
|
60
|
-
|
|
124
|
+
` \u2713 Configured (${config.preset}): ${formatCommandPrefix(config.commandPrefix)}`
|
|
61
125
|
);
|
|
126
|
+
console.log(" Analysis commands are ready to use.");
|
|
127
|
+
printDifferentAnalyzerHint(" ");
|
|
128
|
+
return;
|
|
62
129
|
}
|
|
130
|
+
console.log(" \u2717 No AI config set.");
|
|
131
|
+
if (availableCommands.length > 0) {
|
|
132
|
+
console.log(
|
|
133
|
+
` Detected available commands: ${availableCommands.join(", ")}`
|
|
134
|
+
);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(" No codex, claude, or gemini analyzer command was detected on PATH.");
|
|
137
|
+
}
|
|
138
|
+
console.log(" Configure one with:");
|
|
139
|
+
printAiConfigureCommands(" ");
|
|
140
|
+
printDifferentAnalyzerHint(" ");
|
|
141
|
+
console.log(" Optionally provide a custom command prefix with '-- ...'.");
|
|
63
142
|
}
|
|
64
143
|
function registerInitCommand(yargs) {
|
|
65
144
|
return yargs.command(
|
|
@@ -72,20 +151,12 @@ function registerInitCommand(yargs) {
|
|
|
72
151
|
}),
|
|
73
152
|
(argv) => {
|
|
74
153
|
console.log("Initializing libretto...\n");
|
|
75
|
-
console.log("Copying skill files...");
|
|
76
|
-
try {
|
|
77
|
-
copySkills();
|
|
78
|
-
} catch (err) {
|
|
79
|
-
console.error(
|
|
80
|
-
` \u2717 ${err instanceof Error ? err.message : String(err)}`
|
|
81
|
-
);
|
|
82
|
-
}
|
|
83
154
|
if (!argv["skip-browsers"]) {
|
|
84
155
|
installBrowsers();
|
|
85
156
|
} else {
|
|
86
157
|
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
87
158
|
}
|
|
88
|
-
|
|
159
|
+
checkAiRuntimeConfiguration();
|
|
89
160
|
console.log("\n\u2713 libretto init complete");
|
|
90
161
|
}
|
|
91
162
|
);
|
package/dist/cli/core/browser.js
CHANGED
|
@@ -257,6 +257,10 @@ const NETWORK_LOG = '${escapedNetworkLogPath}';
|
|
|
257
257
|
const ACTIONS_LOG = '${escapedActionsLogPath}';
|
|
258
258
|
mkdirSync(dirname(NETWORK_LOG), { recursive: true });
|
|
259
259
|
|
|
260
|
+
// tsx/esbuild may emit __name() wrappers in Function#toString output.
|
|
261
|
+
const __name = (target, value) =>
|
|
262
|
+
Object.defineProperty(target, 'name', { value, configurable: true });
|
|
263
|
+
|
|
260
264
|
${installSessionTelemetry.toString()}
|
|
261
265
|
|
|
262
266
|
function logAction(entry) {
|
package/dist/cli/core/context.js
CHANGED
|
@@ -2,9 +2,13 @@ import { Logger, createFileLogSink } from "../../shared/logger/index.js";
|
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { cwd } from "node:process";
|
|
4
4
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
6
|
import { validateSessionName } from "./session.js";
|
|
7
7
|
function getRepoRoot() {
|
|
8
|
+
const override = process.env.LIBRETTO_REPO_ROOT?.trim();
|
|
9
|
+
if (override) {
|
|
10
|
+
return resolve(override);
|
|
11
|
+
}
|
|
8
12
|
const result = spawnSync("git", ["rev-parse", "--show-toplevel"], {
|
|
9
13
|
encoding: "utf-8"
|
|
10
14
|
});
|
|
@@ -53,11 +57,6 @@ function ensureLibrettoSetup() {
|
|
|
53
57
|
if (!existsSync(LIBRETTO_GITIGNORE_PATH)) {
|
|
54
58
|
writeFileSync(LIBRETTO_GITIGNORE_PATH, LIBRETTO_GITIGNORE_CONTENT, "utf-8");
|
|
55
59
|
}
|
|
56
|
-
const agentsSkillsDir = join(REPO_ROOT, ".agents", "skills", "libretto");
|
|
57
|
-
const claudeSkillsDir = join(REPO_ROOT, ".claude", "skills", "libretto");
|
|
58
|
-
if (!existsSync(agentsSkillsDir) && !existsSync(claudeSkillsDir)) {
|
|
59
|
-
console.log("[libretto] Skills not installed. Run 'npx libretto init' to complete setup.");
|
|
60
|
-
}
|
|
61
60
|
}
|
|
62
61
|
function createLoggerForSession(session) {
|
|
63
62
|
validateSessionName(session);
|
package/dist/cli/index.js
CHANGED
|
File without changes
|