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/dist/cli/cli.js CHANGED
@@ -12,9 +12,11 @@ import {
12
12
  ensureLibrettoSetup
13
13
  } from "./core/context.js";
14
14
  import {
15
- SESSION_DEFAULT,
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 (copy skills, install browsers)
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 (default: "default")
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 SESSION_DEFAULT;
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 SESSION_DEFAULT;
117
+ return null;
114
118
  }
115
119
  try {
116
120
  validateSessionName(value);
117
121
  return value;
118
122
  } catch {
119
- return SESSION_DEFAULT;
123
+ return null;
120
124
  }
121
125
  }
122
- function validateLegacySessionArg(rawArgs) {
123
- const idx = rawArgs.indexOf("--session");
124
- if (idx < 0) return;
125
- const value = rawArgs[idx + 1];
126
- if (!value || value.startsWith("--") || CLI_COMMANDS.has(value)) {
127
- throw new Error(
128
- "Usage: libretto-cli <command> [--session <name>]\nMissing or invalid --session value."
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
- validateSessionName(value);
141
+ throw new Error(
142
+ "Could not generate an available session id. Close an existing session and try again."
143
+ );
132
144
  }
133
- function initializeLogger(rawArgs) {
134
- const sessionForLog = parseSessionForLog(rawArgs);
135
- const logger = createLoggerForSession(sessionForLog);
136
- logger.info("cli-start", {
137
- args: rawArgs,
138
- cwd: process.cwd(),
139
- session: sessionForLog
140
- });
141
- return logger;
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
- async function withCliLogger(rawArgs, run) {
144
- const logger = initializeLogger(rawArgs);
145
- try {
146
- return await run(logger);
147
- } finally {
148
- await closeLogger(logger);
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
- validateSessionName(String(argv.session));
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
- await withCliLogger(rawArgs, async (logger) => {
180
- try {
181
- validateLegacySessionArg(rawArgs);
182
- const args = filterSessionArgs(rawArgs);
183
- const command = args[0];
184
- if (!command || command === "--help" || command === "-h" || command === "help") {
185
- printUsage();
186
- return;
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
- printUsage();
192
- exitCode = 1;
193
- return;
194
- }
195
- const parser = createParser(logger);
196
- logger.info("cli-command", { command, args });
197
- await parser.parseAsync();
198
- } catch (err) {
199
- logger.error("cli-error", { error: err, args: rawArgs });
200
- const message = err instanceof Error ? err.message : String(err);
201
- console.error(message);
202
- exitCode = 1;
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 readFailureMessage(path) {
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
- return typeof message === "string" ? message : null;
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 waitForFailureMessage(path, timeoutMs = 1e3) {
225
+ async function waitForFailureSignal(path, timeoutMs = 1e3) {
219
226
  const deadline = Date.now() + timeoutMs;
220
227
  while (Date.now() < deadline) {
221
- const message = readFailureMessage(path);
222
- if (message) return message;
228
+ const failure = readFailureSignal(path);
229
+ if (failure) return failure;
223
230
  await new Promise((resolveWait) => setTimeout(resolveWait, 25));
224
231
  }
225
- return readFailureMessage(path);
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 message = await waitForFailureMessage(signalPaths.failedSignalPath);
252
- return { status: "failed", message: message ?? void 0 };
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(args);
343
- const worker = spawn(process.execPath, [workerEntryPath, payload], {
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
- throw new Error(
361
- `${outcome.message ?? "Workflow failed during run."}
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 { existsSync, mkdirSync, cpSync, readdirSync } from "node:fs";
2
- import { join, dirname } from "node:path";
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 { REPO_ROOT } from "../core/context.js";
6
- function getSkillSourceDir() {
7
- const thisDir = dirname(fileURLToPath(import.meta.url));
8
- const pkgRoot = join(thisDir, "..", "..", "..");
9
- const skillDir = join(pkgRoot, "skill");
10
- if (existsSync(skillDir)) return skillDir;
11
- const skillsDir = join(pkgRoot, "skills");
12
- if (existsSync(skillsDir)) return skillsDir;
13
- throw new Error(
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 copySkills() {
18
- const src = getSkillSourceDir();
19
- const files = readdirSync(src);
20
- if (files.length === 0) {
21
- console.log(" No skill files found to copy.");
22
- return;
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
- const targets = [
25
- join(REPO_ROOT, ".agents", "skills", "libretto"),
26
- join(REPO_ROOT, ".claude", "skills", "libretto")
27
- ];
28
- for (const target of targets) {
29
- mkdirSync(target, { recursive: true });
30
- cpSync(src, target, { recursive: true });
31
- console.log(` \u2713 Copied skill files to ${target}`);
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 checkSnapshotLLM() {
49
- const hasAnyCreds = process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY;
50
- console.log("\nSnapshot LLM configuration:");
51
- if (hasAnyCreds) {
52
- console.log(" \u2713 LLM credentials detected");
53
- } else {
54
- console.log(" \u2717 No LLM credentials found.");
55
- console.log(" Set one of the following environment variables:");
56
- console.log(" GOOGLE_CLOUD_PROJECT (for Vertex AI / Gemini)");
57
- console.log(" ANTHROPIC_API_KEY (for Claude)");
58
- console.log(" OPENAI_API_KEY (for GPT)");
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
- " Then configure via: npx libretto ai configure <preset>"
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
- checkSnapshotLLM();
159
+ checkAiRuntimeConfiguration();
89
160
  console.log("\n\u2713 libretto init complete");
90
161
  }
91
162
  );
@@ -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) {
@@ -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