libretto 0.4.0 → 0.4.2
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 -16
- package/dist/cli/commands/browser.js +126 -85
- package/dist/cli/commands/execution.js +147 -108
- package/dist/cli/commands/init.js +103 -40
- package/dist/cli/commands/logs.js +90 -65
- package/dist/cli/commands/shared.js +50 -0
- package/dist/cli/commands/snapshot.js +31 -16
- package/dist/cli/framework/simple-cli.js +776 -0
- package/dist/cli/router.js +29 -0
- package/package.json +2 -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
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import * as moduleBuiltin from "node:module";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import { installInstrumentation } from "../../shared/instrumentation/index.js";
|
|
6
7
|
import {
|
|
7
8
|
connect,
|
|
@@ -12,7 +13,6 @@ import {
|
|
|
12
13
|
assertSessionAvailableForStart,
|
|
13
14
|
clearSessionState,
|
|
14
15
|
readSessionState,
|
|
15
|
-
readSessionStateOrThrow,
|
|
16
16
|
setSessionStatus
|
|
17
17
|
} from "../core/session.js";
|
|
18
18
|
import {
|
|
@@ -20,6 +20,13 @@ import {
|
|
|
20
20
|
readNetworkLog,
|
|
21
21
|
wrapPageForActionLogging
|
|
22
22
|
} from "../core/telemetry.js";
|
|
23
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
24
|
+
import {
|
|
25
|
+
loadSessionStateMiddleware,
|
|
26
|
+
pageOption,
|
|
27
|
+
resolveSessionMiddleware,
|
|
28
|
+
sessionOption
|
|
29
|
+
} from "./shared.js";
|
|
23
30
|
const stripTypeScriptTypes = moduleBuiltin.stripTypeScriptTypes;
|
|
24
31
|
const require2 = moduleBuiltin.createRequire(import.meta.url);
|
|
25
32
|
const tsxCliPath = require2.resolve("tsx/cli");
|
|
@@ -63,7 +70,6 @@ function compileExecFunction(code, helperNames) {
|
|
|
63
70
|
return new AsyncFunction(...helperNames, code);
|
|
64
71
|
}
|
|
65
72
|
async function runExec(code, session, logger, visualize = false, pageId) {
|
|
66
|
-
readSessionStateOrThrow(session);
|
|
67
73
|
logger.info("exec-start", {
|
|
68
74
|
session,
|
|
69
75
|
codeLength: code.length,
|
|
@@ -211,25 +217,24 @@ function readJsonFileIfExists(path) {
|
|
|
211
217
|
return null;
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
|
-
function
|
|
220
|
+
function readFailureDetails(path) {
|
|
215
221
|
const raw = readJsonFileIfExists(path);
|
|
216
222
|
if (!raw || typeof raw !== "object") return null;
|
|
217
223
|
const message = raw.message;
|
|
218
|
-
if (typeof message !== "string") return null;
|
|
219
224
|
const phase = raw.phase;
|
|
220
225
|
return {
|
|
221
|
-
message,
|
|
226
|
+
message: typeof message === "string" ? message : void 0,
|
|
222
227
|
phase: phase === "setup" || phase === "workflow" ? phase : void 0
|
|
223
228
|
};
|
|
224
229
|
}
|
|
225
|
-
async function
|
|
230
|
+
async function waitForFailureDetails(path, timeoutMs = 1e3) {
|
|
226
231
|
const deadline = Date.now() + timeoutMs;
|
|
227
232
|
while (Date.now() < deadline) {
|
|
228
|
-
const
|
|
229
|
-
if (
|
|
233
|
+
const details = readFailureDetails(path);
|
|
234
|
+
if (details?.message) return details;
|
|
230
235
|
await new Promise((resolveWait) => setTimeout(resolveWait, 25));
|
|
231
236
|
}
|
|
232
|
-
return
|
|
237
|
+
return readFailureDetails(path);
|
|
233
238
|
}
|
|
234
239
|
function streamOutputSince(path, offset) {
|
|
235
240
|
if (!existsSync(path)) return offset;
|
|
@@ -255,11 +260,11 @@ async function waitForWorkflowOutcome(args) {
|
|
|
255
260
|
outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
|
|
256
261
|
if (existsSync(signalPaths.failedSignalPath)) {
|
|
257
262
|
outputOffset = streamOutputSince(signalPaths.outputSignalPath, outputOffset);
|
|
258
|
-
const
|
|
263
|
+
const failureDetails = await waitForFailureDetails(signalPaths.failedSignalPath);
|
|
259
264
|
return {
|
|
260
265
|
status: "failed",
|
|
261
|
-
message:
|
|
262
|
-
|
|
266
|
+
message: failureDetails?.message,
|
|
267
|
+
phase: failureDetails?.phase
|
|
263
268
|
};
|
|
264
269
|
}
|
|
265
270
|
if (existsSync(signalPaths.completedSignalPath)) {
|
|
@@ -277,8 +282,7 @@ async function waitForWorkflowOutcome(args) {
|
|
|
277
282
|
await new Promise((resolveWait) => setTimeout(resolveWait, 250));
|
|
278
283
|
}
|
|
279
284
|
}
|
|
280
|
-
async function runResume(session, logger) {
|
|
281
|
-
const state = readSessionStateOrThrow(session);
|
|
285
|
+
async function runResume(session, logger, sessionState) {
|
|
282
286
|
const {
|
|
283
287
|
pausedSignalPath,
|
|
284
288
|
resumeSignalPath,
|
|
@@ -291,9 +295,9 @@ async function runResume(session, logger) {
|
|
|
291
295
|
`Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call pause() first.`
|
|
292
296
|
);
|
|
293
297
|
}
|
|
294
|
-
if (!isProcessRunning(
|
|
298
|
+
if (!isProcessRunning(sessionState.pid)) {
|
|
295
299
|
throw new Error(
|
|
296
|
-
`No active paused workflow found for session "${session}" (worker pid ${
|
|
300
|
+
`No active paused workflow found for session "${session}" (worker pid ${sessionState.pid} is not running).`
|
|
297
301
|
);
|
|
298
302
|
}
|
|
299
303
|
clearSignalIfExists(pausedSignalPath);
|
|
@@ -316,7 +320,7 @@ async function runResume(session, logger) {
|
|
|
316
320
|
console.log(`Resume signal sent for session "${session}".`);
|
|
317
321
|
const outcome = await waitForWorkflowOutcome({
|
|
318
322
|
session,
|
|
319
|
-
pid:
|
|
323
|
+
pid: sessionState.pid
|
|
320
324
|
});
|
|
321
325
|
if (outcome.status === "completed") {
|
|
322
326
|
setSessionStatus(session, "completed", logger);
|
|
@@ -340,7 +344,6 @@ async function runResume(session, logger) {
|
|
|
340
344
|
}
|
|
341
345
|
async function runIntegrationFromFile(args, logger) {
|
|
342
346
|
await stopExistingFailedRunSession(args.session, logger);
|
|
343
|
-
assertSessionAvailableForStart(args.session, logger);
|
|
344
347
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
345
348
|
clearSignalIfExists(signalPaths.pausedSignalPath);
|
|
346
349
|
clearSignalIfExists(signalPaths.resumeSignalPath);
|
|
@@ -380,14 +383,13 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
380
383
|
}
|
|
381
384
|
if (outcome.status === "failed") {
|
|
382
385
|
setSessionStatus(args.session, "failed", logger);
|
|
383
|
-
|
|
384
|
-
if (outcome.failurePhase === "workflow") {
|
|
386
|
+
if (outcome.phase === "workflow") {
|
|
385
387
|
throw new Error(
|
|
386
|
-
`${message}
|
|
388
|
+
`${outcome.message ?? "Workflow failed during run."}
|
|
387
389
|
Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-run the workflow.`
|
|
388
390
|
);
|
|
389
391
|
}
|
|
390
|
-
throw new Error(message);
|
|
392
|
+
throw new Error(outcome.message ?? "Workflow failed during run.");
|
|
391
393
|
}
|
|
392
394
|
if (outcome.status === "exited") {
|
|
393
395
|
setSessionStatus(args.session, "exited", logger);
|
|
@@ -396,95 +398,132 @@ Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-ru
|
|
|
396
398
|
);
|
|
397
399
|
}
|
|
398
400
|
setSessionStatus(args.session, "completed", logger);
|
|
401
|
+
console.log("Integration completed.");
|
|
402
|
+
}
|
|
403
|
+
const execInput = SimpleCLI.input({
|
|
404
|
+
positionals: [
|
|
405
|
+
SimpleCLI.positional("codeParts", z.array(z.string()).default([]), {
|
|
406
|
+
help: "Playwright TypeScript code to execute",
|
|
407
|
+
variadic: true
|
|
408
|
+
})
|
|
409
|
+
],
|
|
410
|
+
named: {
|
|
411
|
+
session: sessionOption(),
|
|
412
|
+
visualize: SimpleCLI.flag({ help: "Enable ghost cursor + highlight visualization" }),
|
|
413
|
+
page: pageOption()
|
|
414
|
+
}
|
|
415
|
+
}).refine(
|
|
416
|
+
(input) => input.codeParts.length > 0,
|
|
417
|
+
"Usage: libretto-cli exec <code> [--session <name>] [--visualize]"
|
|
418
|
+
);
|
|
419
|
+
function createExecCommand(logger) {
|
|
420
|
+
return SimpleCLI.command({
|
|
421
|
+
description: "Execute Playwright TypeScript code"
|
|
422
|
+
}).input(execInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ input, ctx }) => {
|
|
423
|
+
await runExec(
|
|
424
|
+
input.codeParts.join(" "),
|
|
425
|
+
ctx.session,
|
|
426
|
+
logger,
|
|
427
|
+
input.visualize,
|
|
428
|
+
input.page
|
|
429
|
+
);
|
|
430
|
+
});
|
|
399
431
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
"
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
432
|
+
const runUsage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--tsconfig <path>] [--headed|--headless]";
|
|
433
|
+
const runInput = SimpleCLI.input({
|
|
434
|
+
positionals: [
|
|
435
|
+
SimpleCLI.positional("integrationFile", z.string().optional(), {
|
|
436
|
+
help: "Path to the integration file"
|
|
437
|
+
}),
|
|
438
|
+
SimpleCLI.positional("integrationExport", z.string().optional(), {
|
|
439
|
+
help: "Named workflow export to run"
|
|
440
|
+
})
|
|
441
|
+
],
|
|
442
|
+
named: {
|
|
443
|
+
session: sessionOption(),
|
|
444
|
+
params: SimpleCLI.option(z.string().optional(), {
|
|
445
|
+
help: "Inline JSON params"
|
|
446
|
+
}),
|
|
447
|
+
paramsFile: SimpleCLI.option(z.string().optional(), {
|
|
448
|
+
name: "params-file",
|
|
449
|
+
help: "Path to a JSON params file"
|
|
450
|
+
}),
|
|
451
|
+
tsconfig: SimpleCLI.option(z.string().optional(), {
|
|
452
|
+
help: "Path to a tsconfig used for workflow module resolution"
|
|
453
|
+
}),
|
|
454
|
+
headed: SimpleCLI.flag({ help: "Run in headed mode" }),
|
|
455
|
+
headless: SimpleCLI.flag({ help: "Run in headless mode" }),
|
|
456
|
+
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
457
|
+
name: "auth-profile",
|
|
458
|
+
help: "Domain for local auth profile (e.g. apps.example.com)"
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
}).refine(
|
|
462
|
+
(input) => Boolean(input.integrationFile && input.integrationExport),
|
|
463
|
+
runUsage
|
|
464
|
+
).refine((input) => !(input.params && input.paramsFile), "Pass either --params or --params-file, not both.").refine((input) => !(input.headed && input.headless), "Cannot pass both --headed and --headless.");
|
|
465
|
+
function resolveRunParams(rawInlineParams, paramsFile) {
|
|
466
|
+
if (paramsFile) {
|
|
467
|
+
let content;
|
|
468
|
+
try {
|
|
469
|
+
content = readFileSync(paramsFile, "utf8");
|
|
470
|
+
} catch {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`Could not read --params-file "${paramsFile}". Ensure the file exists and is readable.`
|
|
419
473
|
);
|
|
420
474
|
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const headlessMode = hasHeadedFlag ? false : hasHeadlessFlag ? true : void 0;
|
|
467
|
-
const authProfileDomain = argv["auth-profile"];
|
|
468
|
-
const tsconfigPath = argv.tsconfig;
|
|
469
|
-
await runIntegrationFromFile({
|
|
470
|
-
integrationPath,
|
|
471
|
-
exportName,
|
|
472
|
-
session,
|
|
473
|
-
params,
|
|
474
|
-
headless: headlessMode ?? false,
|
|
475
|
-
authProfileDomain,
|
|
476
|
-
tsconfigPath
|
|
477
|
-
}, logger);
|
|
478
|
-
}
|
|
479
|
-
).command(
|
|
480
|
-
"resume",
|
|
481
|
-
"Resume a paused workflow for the current session",
|
|
482
|
-
(cmd) => cmd,
|
|
483
|
-
async (argv) => {
|
|
484
|
-
await runResume(String(argv.session), logger);
|
|
485
|
-
}
|
|
486
|
-
);
|
|
475
|
+
return parseJsonArg("--params-file", content);
|
|
476
|
+
}
|
|
477
|
+
if (rawInlineParams) {
|
|
478
|
+
return parseJsonArg("--params", rawInlineParams);
|
|
479
|
+
}
|
|
480
|
+
return {};
|
|
481
|
+
}
|
|
482
|
+
function createRunCommand(logger) {
|
|
483
|
+
return SimpleCLI.command({
|
|
484
|
+
description: "Run an exported Libretto workflow from a file"
|
|
485
|
+
}).input(runInput).use(resolveSessionMiddleware).handle(async ({ input, ctx }) => {
|
|
486
|
+
await stopExistingFailedRunSession(ctx.session, logger);
|
|
487
|
+
assertSessionAvailableForStart(ctx.session, logger);
|
|
488
|
+
const params = resolveRunParams(input.params, input.paramsFile);
|
|
489
|
+
const headlessMode = input.headed ? false : input.headless ? true : void 0;
|
|
490
|
+
await runIntegrationFromFile({
|
|
491
|
+
integrationPath: input.integrationFile,
|
|
492
|
+
exportName: input.integrationExport,
|
|
493
|
+
session: ctx.session,
|
|
494
|
+
params,
|
|
495
|
+
tsconfigPath: input.tsconfig,
|
|
496
|
+
headless: headlessMode ?? false,
|
|
497
|
+
authProfileDomain: input.authProfile
|
|
498
|
+
}, logger);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const resumeInput = SimpleCLI.input({
|
|
502
|
+
positionals: [],
|
|
503
|
+
named: {
|
|
504
|
+
session: sessionOption()
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
function createResumeCommand(logger) {
|
|
508
|
+
return SimpleCLI.command({
|
|
509
|
+
description: "Resume a paused workflow for the current session"
|
|
510
|
+
}).input(resumeInput).use(resolveSessionMiddleware).use(loadSessionStateMiddleware).handle(async ({ ctx }) => {
|
|
511
|
+
await runResume(ctx.session, logger, ctx.sessionState);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
function createExecutionCommands(logger) {
|
|
515
|
+
return {
|
|
516
|
+
exec: createExecCommand(logger),
|
|
517
|
+
run: createRunCommand(logger),
|
|
518
|
+
resume: createResumeCommand(logger)
|
|
519
|
+
};
|
|
487
520
|
}
|
|
488
521
|
export {
|
|
489
|
-
|
|
522
|
+
createExecCommand,
|
|
523
|
+
createExecutionCommands,
|
|
524
|
+
createResumeCommand,
|
|
525
|
+
createRunCommand,
|
|
526
|
+
execInput,
|
|
527
|
+
resumeInput,
|
|
528
|
+
runInput
|
|
490
529
|
};
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
|
-
import { existsSync, readFileSync,
|
|
2
|
+
import { appendFileSync, cpSync, existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
} from "../core/ai-config.js";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { readAiConfig } from "../core/ai-config.js";
|
|
8
7
|
import { REPO_ROOT } from "../core/context.js";
|
|
9
8
|
import {
|
|
10
9
|
loadSnapshotEnv,
|
|
11
10
|
resolveSnapshotApiModel
|
|
12
11
|
} from "../core/snapshot-api-config.js";
|
|
12
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
13
13
|
import { hasProviderCredentials } from "../../shared/llm/client.js";
|
|
14
14
|
const PROVIDER_CHOICES = [
|
|
15
15
|
{
|
|
@@ -44,6 +44,15 @@ function promptUser(rl, question) {
|
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
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
|
+
});
|
|
55
|
+
}
|
|
47
56
|
function safeReadAiConfig() {
|
|
48
57
|
try {
|
|
49
58
|
return readAiConfig();
|
|
@@ -61,9 +70,7 @@ function printSnapshotApiStatus() {
|
|
|
61
70
|
);
|
|
62
71
|
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
63
72
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
64
|
-
console.log(
|
|
65
|
-
` \u2713 Ready: ${selection.model} (${selection.source})`
|
|
66
|
-
);
|
|
73
|
+
console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
|
|
67
74
|
console.log(" Snapshot objectives will use the API analyzer by default.");
|
|
68
75
|
console.log(" No further action required.");
|
|
69
76
|
return;
|
|
@@ -86,14 +93,10 @@ async function runInteractiveApiSetup() {
|
|
|
86
93
|
const selection = resolveSnapshotApiModel(config);
|
|
87
94
|
const envPath = join(REPO_ROOT, ".env");
|
|
88
95
|
console.log("\nSnapshot analysis setup:");
|
|
89
|
-
console.log(
|
|
90
|
-
" Libretto uses direct API calls for snapshot analysis."
|
|
91
|
-
);
|
|
96
|
+
console.log(" Libretto uses direct API calls for snapshot analysis.");
|
|
92
97
|
console.log(` Credentials are loaded from process env and ${envPath}.`);
|
|
93
98
|
if (selection && hasProviderCredentials(selection.provider)) {
|
|
94
|
-
console.log(
|
|
95
|
-
` \u2713 Ready: ${selection.model} (${selection.source})`
|
|
96
|
-
);
|
|
99
|
+
console.log(` \u2713 Ready: ${selection.model} (${selection.source})`);
|
|
97
100
|
console.log(" Snapshot objectives will use the API analyzer by default.");
|
|
98
101
|
return;
|
|
99
102
|
}
|
|
@@ -120,7 +123,7 @@ async function runInteractiveApiSetup() {
|
|
|
120
123
|
);
|
|
121
124
|
return;
|
|
122
125
|
}
|
|
123
|
-
const selected = PROVIDER_CHOICES.find((
|
|
126
|
+
const selected = PROVIDER_CHOICES.find((choice) => choice.key === answer);
|
|
124
127
|
if (!selected) {
|
|
125
128
|
console.log(`
|
|
126
129
|
Unknown choice "${answer}". Skipping API setup.`);
|
|
@@ -130,7 +133,10 @@ async function runInteractiveApiSetup() {
|
|
|
130
133
|
${selected.label} selected.`);
|
|
131
134
|
console.log(` ${selected.envHint}
|
|
132
135
|
`);
|
|
133
|
-
const apiKeyValue = await promptUser(
|
|
136
|
+
const apiKeyValue = await promptUser(
|
|
137
|
+
rl,
|
|
138
|
+
` Enter your ${selected.envVar}: `
|
|
139
|
+
);
|
|
134
140
|
if (!apiKeyValue) {
|
|
135
141
|
console.log("\n No value entered. Skipping API key setup.");
|
|
136
142
|
return;
|
|
@@ -179,31 +185,88 @@ function installBrowsers() {
|
|
|
179
185
|
);
|
|
180
186
|
}
|
|
181
187
|
}
|
|
182
|
-
function
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
(
|
|
187
|
-
|
|
188
|
-
default: false,
|
|
189
|
-
describe: "Skip Playwright Chromium installation"
|
|
190
|
-
}),
|
|
191
|
-
async (argv) => {
|
|
192
|
-
console.log("Initializing libretto...\n");
|
|
193
|
-
if (!argv["skip-browsers"]) {
|
|
194
|
-
installBrowsers();
|
|
195
|
-
} else {
|
|
196
|
-
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
197
|
-
}
|
|
198
|
-
if (process.stdin.isTTY) {
|
|
199
|
-
await runInteractiveApiSetup();
|
|
200
|
-
} else {
|
|
201
|
-
printSnapshotApiStatus();
|
|
202
|
-
}
|
|
203
|
-
console.log("\n\u2713 libretto init complete");
|
|
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");
|
|
204
194
|
}
|
|
205
|
-
|
|
195
|
+
dir = dirname(dir);
|
|
196
|
+
}
|
|
197
|
+
throw new Error("Could not locate libretto skill files in package");
|
|
206
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.");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
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.");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
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
|
+
if (existsSync(skillDest)) {
|
|
236
|
+
rmSync(skillDest, { recursive: true });
|
|
237
|
+
}
|
|
238
|
+
cpSync(sourceDir, skillDest, { recursive: true });
|
|
239
|
+
const fileCount = readdirSync(skillDest).length;
|
|
240
|
+
console.log(` \u2713 Copied ${fileCount} skill files to ${name}/skills/libretto/`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const initInput = SimpleCLI.input({
|
|
244
|
+
positionals: [],
|
|
245
|
+
named: {
|
|
246
|
+
skipBrowsers: SimpleCLI.flag({
|
|
247
|
+
name: "skip-browsers",
|
|
248
|
+
help: "Skip Playwright Chromium installation"
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
const initCommand = SimpleCLI.command({
|
|
253
|
+
description: "Initialize libretto in the current project"
|
|
254
|
+
}).input(initInput).handle(async ({ input }) => {
|
|
255
|
+
console.log("Initializing libretto...\n");
|
|
256
|
+
if (!input.skipBrowsers) {
|
|
257
|
+
installBrowsers();
|
|
258
|
+
} else {
|
|
259
|
+
console.log("\nSkipping browser installation (--skip-browsers)");
|
|
260
|
+
}
|
|
261
|
+
if (process.stdin.isTTY) {
|
|
262
|
+
await copySkills();
|
|
263
|
+
await runInteractiveApiSetup();
|
|
264
|
+
} else {
|
|
265
|
+
printSnapshotApiStatus();
|
|
266
|
+
}
|
|
267
|
+
console.log("\n\u2713 libretto init complete");
|
|
268
|
+
});
|
|
207
269
|
export {
|
|
208
|
-
|
|
270
|
+
initCommand,
|
|
271
|
+
initInput
|
|
209
272
|
};
|