@tarcisiopgs/lisa 1.35.0 → 1.37.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,16 +32,10 @@ import {
32
32
  resolveModels,
33
33
  runValidationCommands,
34
34
  runWithFallback
35
- } from "./chunk-6VIN5PMW.js";
35
+ } from "./chunk-XXVTKBC5.js";
36
36
  import {
37
- divider,
38
- error,
39
- initLogFile,
40
- kanbanEmitter,
41
- log,
42
- ok,
43
- warn
44
- } from "./chunk-V44FTYWZ.js";
37
+ kanbanEmitter
38
+ } from "./chunk-VS6R5KBO.js";
45
39
  import {
46
40
  appendRawEntry,
47
41
  migrateGuardrails
@@ -62,8 +56,17 @@ import {
62
56
  stopSpinner
63
57
  } from "./chunk-72CYGBT4.js";
64
58
  import {
59
+ divider,
60
+ error,
61
+ initLogFile,
62
+ log,
63
+ ok,
64
+ warn
65
+ } from "./chunk-HPWL5JRW.js";
66
+ import {
67
+ LisaError,
65
68
  formatError
66
- } from "./chunk-7JT7DTSS.js";
69
+ } from "./chunk-CTMZ666K.js";
67
70
  import {
68
71
  fetchPrFeedback,
69
72
  formatPrFeedbackEntry
@@ -111,7 +114,7 @@ var configSchema = object({
111
114
  base_branch: string().optional(),
112
115
  workspace: string().optional()
113
116
  }).passthrough();
114
- var ConfigValidationError = class extends Error {
117
+ var ConfigValidationError = class extends LisaError {
115
118
  constructor(message) {
116
119
  super(message);
117
120
  this.name = "ConfigValidationError";
@@ -255,6 +258,7 @@ function loadConfig(cwd = process.cwd()) {
255
258
  const rawReviewMonitor = parsed.review_monitor;
256
259
  const rawReactions = parsed.reactions;
257
260
  const rawSpecCompliance = parsed.spec_compliance;
261
+ const rawPlanValidation = parsed.plan_validation;
258
262
  const rawProgress = parsed.progress_comments;
259
263
  const rawPr = parsed.pr;
260
264
  const config = {
@@ -309,6 +313,10 @@ function loadConfig(cwd = process.cwd()) {
309
313
  max_retries: rawSpecCompliance.max_retries,
310
314
  block_on_failure: rawSpecCompliance.block_on_failure
311
315
  } : void 0,
316
+ plan_validation: rawPlanValidation ? {
317
+ enabled: rawPlanValidation.enabled ?? false,
318
+ max_iterations: rawPlanValidation.max_iterations
319
+ } : void 0,
312
320
  progress_comments: rawProgress ? { enabled: rawProgress.enabled ?? false } : void 0,
313
321
  pr: parsePrConfig(rawPr),
314
322
  provider_options: {
@@ -852,13 +860,13 @@ function killProviderForIssue(issueId) {
852
860
  }, 5e3);
853
861
  }
854
862
  function setupEventListeners() {
855
- kanbanEmitter.on("loop:pause", () => {
863
+ const onPause = () => {
856
864
  _loopPaused = true;
857
- });
858
- kanbanEmitter.on("loop:resume", () => {
865
+ };
866
+ const onResume = () => {
859
867
  _loopPaused = false;
860
- });
861
- kanbanEmitter.on("loop:pause-provider", (issueId) => {
868
+ };
869
+ const onPauseProvider = (issueId) => {
862
870
  if (issueId) {
863
871
  const pid = activeProviderPids.get(issueId);
864
872
  if (pid) {
@@ -878,8 +886,8 @@ function setupEventListeners() {
878
886
  }
879
887
  }
880
888
  kanbanEmitter.emit("provider:paused", issueId);
881
- });
882
- kanbanEmitter.on("loop:resume-provider", (issueId) => {
889
+ };
890
+ const onResumeProvider = (issueId) => {
883
891
  if (issueId) {
884
892
  const pid = activeProviderPids.get(issueId);
885
893
  if (pid && providerPausedSet.has(issueId)) {
@@ -902,8 +910,8 @@ function setupEventListeners() {
902
910
  providerPausedSet.clear();
903
911
  }
904
912
  kanbanEmitter.emit("provider:resumed", issueId);
905
- });
906
- kanbanEmitter.on("loop:kill", (issueId) => {
913
+ };
914
+ const onKill = (issueId) => {
907
915
  if (issueId) {
908
916
  userKilledSet.add(issueId);
909
917
  killProviderForIssue(issueId);
@@ -914,8 +922,8 @@ function setupEventListeners() {
914
922
  killProviderForIssue(firstId);
915
923
  }
916
924
  }
917
- });
918
- kanbanEmitter.on("loop:skip", (issueId) => {
925
+ };
926
+ const onSkip = (issueId) => {
919
927
  if (issueId) {
920
928
  userSkippedSet.add(issueId);
921
929
  killProviderForIssue(issueId);
@@ -926,15 +934,33 @@ function setupEventListeners() {
926
934
  killProviderForIssue(firstId);
927
935
  }
928
936
  }
929
- });
930
- kanbanEmitter.on("loop:run", () => {
937
+ };
938
+ const onRun = () => {
931
939
  resolveIdle();
932
- });
933
- kanbanEmitter.on("loop:quit", () => {
940
+ };
941
+ const onQuit = () => {
934
942
  _userQuitFromWatchPrompt = true;
935
943
  setShuttingDown(true);
936
944
  resolveIdle();
937
- });
945
+ };
946
+ kanbanEmitter.on("loop:pause", onPause);
947
+ kanbanEmitter.on("loop:resume", onResume);
948
+ kanbanEmitter.on("loop:pause-provider", onPauseProvider);
949
+ kanbanEmitter.on("loop:resume-provider", onResumeProvider);
950
+ kanbanEmitter.on("loop:kill", onKill);
951
+ kanbanEmitter.on("loop:skip", onSkip);
952
+ kanbanEmitter.on("loop:run", onRun);
953
+ kanbanEmitter.on("loop:quit", onQuit);
954
+ return () => {
955
+ kanbanEmitter.off("loop:pause", onPause);
956
+ kanbanEmitter.off("loop:resume", onResume);
957
+ kanbanEmitter.off("loop:pause-provider", onPauseProvider);
958
+ kanbanEmitter.off("loop:resume-provider", onResumeProvider);
959
+ kanbanEmitter.off("loop:kill", onKill);
960
+ kanbanEmitter.off("loop:skip", onSkip);
961
+ kanbanEmitter.off("loop:run", onRun);
962
+ kanbanEmitter.off("loop:quit", onQuit);
963
+ };
938
964
  }
939
965
 
940
966
  // src/session/reconciliation.ts
@@ -1798,7 +1824,14 @@ var WORKTREES_DIR = ".worktrees";
1798
1824
  function generateBranchName(issueId, title) {
1799
1825
  const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").substring(0, 40).replace(/^-|-$/g, "");
1800
1826
  const safeId = issueId.toLowerCase().replace(/[^a-z0-9-]/g, "-");
1801
- return `feat/${safeId}-${slug}`;
1827
+ let branch = `feat/${safeId}-${slug}`;
1828
+ branch = branch.replace(/\.\./g, "");
1829
+ branch = branch.replace(/@\{/g, "");
1830
+ branch = branch.replace(/^[./]+/, "").replace(/[./]+$/, "");
1831
+ if (!branch || branch === "feat/" || branch === "feat") {
1832
+ branch = `feat/${safeId}-${Date.now()}`;
1833
+ }
1834
+ return branch;
1802
1835
  }
1803
1836
  async function cleanupOrphanedWorktree(repoRoot, branchName) {
1804
1837
  const { stdout: branchList } = await execa2("git", ["branch", "--list", branchName], {
@@ -2078,28 +2111,11 @@ async function monitorCi(branch, config, issue, models, cwd, logFile, workspace,
2078
2111
  // src/session/hooks.ts
2079
2112
  import { spawn as spawn2 } from "child_process";
2080
2113
  var DEFAULT_HOOK_TIMEOUT = 6e4;
2081
- var SENSITIVE_ENV_PATTERNS = [
2082
- /^GITHUB_TOKEN$/,
2083
- /^GH_TOKEN$/,
2084
- /^GITLAB_TOKEN$/,
2085
- /^BITBUCKET_.*(TOKEN|PASSWORD|SECRET)/i,
2086
- /^LINEAR_API_KEY$/,
2087
- /^TRELLO_(API_KEY|TOKEN)$/,
2088
- /^PLANE_API_TOKEN$/,
2089
- /^SHORTCUT_API_TOKEN$/,
2090
- /^JIRA_(API_TOKEN|TOKEN)$/,
2091
- /^AWS_(SECRET|SESSION).*KEY/i,
2092
- /^ANTHROPIC_API_KEY$/,
2093
- /^OPENAI_API_KEY$/,
2094
- /^GOOGLE_API_KEY$/,
2095
- /^GEMINI_API_KEY$/,
2096
- /^NPM_TOKEN$/,
2097
- /^PYPI_TOKEN$/
2098
- ];
2114
+ var SAFE_ENV_PATTERN = /^(PATH|HOME|USER|LANG|LANGUAGE|LC_.+|SHELL|TERM|TERM_PROGRAM|PWD|OLDPWD|TMPDIR|TMP|TEMP|EDITOR|VISUAL|CI|LISA_.+|NODE_ENV|NODE_PATH|NPM_.+|PNPM_.+|YARN_.+|XDG_.+|COLORTERM|FORCE_COLOR|NO_COLOR|COLUMNS|LINES|HOSTNAME|LOGNAME|SHLVL)$/;
2099
2115
  function sanitizeEnv(env) {
2100
2116
  const result = {};
2101
2117
  for (const [key, value] of Object.entries(env)) {
2102
- if (value !== void 0 && !SENSITIVE_ENV_PATTERNS.some((p) => p.test(key))) {
2118
+ if (value !== void 0 && SAFE_ENV_PATTERN.test(key)) {
2103
2119
  result[key] = value;
2104
2120
  }
2105
2121
  }
@@ -3428,6 +3444,8 @@ async function runConcurrentLoop(config, source, models, workspace, opts) {
3428
3444
  let consecutiveExhaustions = 0;
3429
3445
  const MAX_CONSECUTIVE_EXHAUSTIONS = 3;
3430
3446
  const slotPool = Array.from({ length: concurrency }, (_, i) => i);
3447
+ let watchStartTime = null;
3448
+ const watchTimeout = config.loop.watch_timeout ?? 0;
3431
3449
  const processIssue = async (issue, session, slotIndex) => {
3432
3450
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
3433
3451
  const logFile = resolve7(getLogsDir(workspace), `session_${session}_${timestamp}.log`);
@@ -3456,8 +3474,12 @@ async function runConcurrentLoop(config, source, models, workspace, opts) {
3456
3474
  } catch (err) {
3457
3475
  error(`Unhandled error in session for ${issue.id}: ${formatError(err)}`);
3458
3476
  await revertIssueStatus(issue, source, config);
3459
- activeCleanups.delete(issue.id);
3477
+ userKilledSet.delete(issue.id);
3478
+ userSkippedSet.delete(issue.id);
3479
+ providerPausedSet.delete(issue.id);
3460
3480
  activeProviderPids.delete(issue.id);
3481
+ activeCleanups.delete(issue.id);
3482
+ claimedIssueIds.delete(issue.id);
3461
3483
  if (config.bell !== false) notify(2);
3462
3484
  return;
3463
3485
  }
@@ -3520,10 +3542,20 @@ async function runConcurrentLoop(config, source, models, workspace, opts) {
3520
3542
  }
3521
3543
  if (issue && claimedIssueIds.has(issue.id)) {
3522
3544
  log(`Issue ${issue.id} already claimed by another worker. Skipping.`);
3545
+ await sleep(Math.max(config.loop.cooldown * 1e3, 2e3));
3523
3546
  break;
3524
3547
  }
3525
3548
  if (!issue) {
3526
3549
  if (opts.watch) {
3550
+ if (watchStartTime === null) watchStartTime = Date.now();
3551
+ if (watchTimeout > 0) {
3552
+ const elapsed = (Date.now() - watchStartTime) / 1e3;
3553
+ if (elapsed >= watchTimeout) {
3554
+ ok(`Watch mode timeout reached (${watchTimeout}s). Stopping.`);
3555
+ noMoreIssues = true;
3556
+ break;
3557
+ }
3558
+ }
3527
3559
  if (activeWorkers.size === 0) {
3528
3560
  if (completedCount > 0) {
3529
3561
  ok(`All issues resolved. Prompting user to continue watching...`);
@@ -3561,6 +3593,7 @@ async function runConcurrentLoop(config, source, models, workspace, opts) {
3561
3593
  break;
3562
3594
  }
3563
3595
  kanbanEmitter.emit("work:resumed");
3596
+ watchStartTime = null;
3564
3597
  sessionCounter = tentativeSession;
3565
3598
  const session = sessionCounter;
3566
3599
  claimedIssueIds.add(issue.id);
@@ -3924,6 +3957,8 @@ async function runSequentialLoop(config, source, models, workspace, opts) {
3924
3957
  const MAX_CONSECUTIVE_FETCH_ERRORS = 3;
3925
3958
  let consecutiveExhaustions = 0;
3926
3959
  const MAX_CONSECUTIVE_EXHAUSTIONS = 3;
3960
+ let watchStartTime = null;
3961
+ const watchTimeout = config.loop.watch_timeout ?? 0;
3927
3962
  while (true) {
3928
3963
  session++;
3929
3964
  if (opts.limit > 0 && session > opts.limit) {
@@ -3985,6 +4020,14 @@ async function runSequentialLoop(config, source, models, workspace, opts) {
3985
4020
  break;
3986
4021
  }
3987
4022
  if (opts.watch) {
4023
+ if (watchStartTime === null) watchStartTime = Date.now();
4024
+ if (watchTimeout > 0) {
4025
+ const elapsed = (Date.now() - watchStartTime) / 1e3;
4026
+ if (elapsed >= watchTimeout) {
4027
+ ok(`Watch mode timeout reached (${watchTimeout}s). Stopping.`);
4028
+ break;
4029
+ }
4030
+ }
3988
4031
  if (completedCount > 0) {
3989
4032
  ok(`All issues resolved. Prompting user to continue watching...`);
3990
4033
  kanbanEmitter.emit("work:watch-prompt");
@@ -4024,6 +4067,7 @@ async function runSequentialLoop(config, source, models, workspace, opts) {
4024
4067
  continue;
4025
4068
  }
4026
4069
  kanbanEmitter.emit("work:resumed");
4070
+ watchStartTime = null;
4027
4071
  ok(`Picked up: ${issue.id} \u2014 ${issue.title}`);
4028
4072
  setTitle(`Lisa \u2014 ${issue.id}`);
4029
4073
  const cachedPrUrls = loadPrUrls(workspace, issue.id);
@@ -4246,7 +4290,7 @@ async function runDemoLoop() {
4246
4290
  }
4247
4291
 
4248
4292
  // src/loop/index.ts
4249
- setupEventListeners();
4293
+ var cleanupEventListeners = setupEventListeners();
4250
4294
  async function runLoop(config, opts) {
4251
4295
  const source = createSource(config.source);
4252
4296
  const models = resolveModels(config);
@@ -4318,5 +4362,6 @@ export {
4318
4362
  checkoutBaseBranches,
4319
4363
  listSessionRecords,
4320
4364
  runDemoLoop,
4365
+ cleanupEventListeners,
4321
4366
  runLoop
4322
4367
  };
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/errors.ts
4
+ function formatError(err) {
5
+ if (err instanceof Error) {
6
+ const cause = err.cause ? ` (caused by: ${formatError(err.cause)})` : "";
7
+ return `${err.message}${cause}`;
8
+ }
9
+ return String(err);
10
+ }
11
+ var LisaError = class extends Error {
12
+ constructor(message, options) {
13
+ super(message, options);
14
+ this.name = "LisaError";
15
+ }
16
+ };
17
+ var SourceError = class extends LisaError {
18
+ constructor(message, source, statusCode, options) {
19
+ super(message, options);
20
+ this.source = source;
21
+ this.statusCode = statusCode;
22
+ this.name = "SourceError";
23
+ }
24
+ };
25
+
26
+ export {
27
+ formatError,
28
+ LisaError,
29
+ SourceError
30
+ };
@@ -2,9 +2,12 @@
2
2
  import {
3
3
  isGhCliAvailable
4
4
  } from "./chunk-YMV4CBQE.js";
5
+ import {
6
+ verbose
7
+ } from "./chunk-HPWL5JRW.js";
5
8
  import {
6
9
  formatError
7
- } from "./chunk-7JT7DTSS.js";
10
+ } from "./chunk-CTMZ666K.js";
8
11
 
9
12
  // src/cli/detection.ts
10
13
  import { execSync } from "child_process";
@@ -17,7 +20,8 @@ function getVersion() {
17
20
  const pkgPath = resolvePath(new URL(".", import.meta.url).pathname, "../package.json");
18
21
  const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
19
22
  return pkg.version;
20
- } catch {
23
+ } catch (err) {
24
+ verbose(`Failed to read package.json version: ${formatError(err)}`);
21
25
  return "0.0.0";
22
26
  }
23
27
  }
@@ -32,7 +36,8 @@ async function isCursorFreePlan() {
32
36
  try {
33
37
  execSync(`${b} --version`, { stdio: "ignore" });
34
38
  return true;
35
- } catch {
39
+ } catch (err) {
40
+ verbose(`Cursor binary "${b}" not found: ${formatError(err)}`);
36
41
  return false;
37
42
  }
38
43
  });
@@ -49,7 +54,8 @@ async function isCursorFreePlan() {
49
54
  } finally {
50
55
  try {
51
56
  rmSync(tmpDir, { recursive: true, force: true });
52
- } catch {
57
+ } catch (err) {
58
+ verbose(`Failed to clean up temp dir ${tmpDir}: ${formatError(err)}`);
53
59
  }
54
60
  }
55
61
  }
@@ -75,7 +81,8 @@ function fetchCursorModels() {
75
81
  try {
76
82
  execSync(`${b} --version`, { stdio: "ignore" });
77
83
  return true;
78
- } catch {
84
+ } catch (err) {
85
+ verbose(`Cursor binary "${b}" not available: ${formatError(err)}`);
79
86
  return false;
80
87
  }
81
88
  });
@@ -85,7 +92,8 @@ function fetchCursorModels() {
85
92
  const all = clean.split("\n").map((l) => l.trim()).filter((l) => l.includes(" - ")).map((l) => (l.split(" - ")[0] ?? "").trim()).filter(Boolean);
86
93
  const filtered = CURSOR_PREFERRED_MODELS.filter((m) => all.includes(m));
87
94
  return filtered.length > 0 ? filtered : CURSOR_PREFERRED_MODELS;
88
- } catch {
95
+ } catch (err) {
96
+ verbose(`Failed to fetch Cursor models: ${formatError(err)}`);
89
97
  return CURSOR_PREFERRED_MODELS;
90
98
  }
91
99
  }
@@ -98,7 +106,8 @@ function fetchOpenCodeModels() {
98
106
  const raw = execSync("opencode models", { encoding: "utf-8", timeout: 1e4 });
99
107
  const clean = raw.replace(/\x1b\[[0-9;]*[mGKHFA-Z]/g, "");
100
108
  return clean.split("\n").map((l) => l.trim()).filter((m) => /^[a-z0-9][\w.-]*\/.+/i.test(m));
101
- } catch {
109
+ } catch (err) {
110
+ verbose(`Failed to fetch OpenCode models: ${formatError(err)}`);
102
111
  return [];
103
112
  }
104
113
  }
@@ -120,7 +129,8 @@ async function detectPlatform() {
120
129
  const platformLabel = detectedPlatform === "cli" || detectedPlatform === "token" ? "GitHub" : detectedPlatform === "gitlab" ? "GitLab" : "Bitbucket";
121
130
  clack.log.info(`Detected ${platformLabel} remote`);
122
131
  }
123
- } catch {
132
+ } catch (err) {
133
+ verbose(`Platform detection from git remote failed: ${formatError(err)}`);
124
134
  }
125
135
  const initialValue = detectedPlatform ?? "cli";
126
136
  const selected = await clack.select({
@@ -210,7 +220,8 @@ function detectDefaultBranch(repoPath) {
210
220
  encoding: "utf-8"
211
221
  }).trim();
212
222
  return ref.replace("origin/", "");
213
- } catch {
223
+ } catch (err) {
224
+ verbose(`Failed to detect default branch, falling back to "main": ${formatError(err)}`);
214
225
  return "main";
215
226
  }
216
227
  }
@@ -219,7 +230,8 @@ function getGitRepoName(repoPath) {
219
230
  const url = execSync("git remote get-url origin", { cwd: repoPath, encoding: "utf-8" }).trim();
220
231
  const match = url.match(/\/([^/]+?)(?:\.git)?$/) ?? url.match(/:([^/]+?)(?:\.git)?$/);
221
232
  return match?.[1] ?? null;
222
- } catch {
233
+ } catch (err) {
234
+ verbose(`Failed to get git repo name for ${repoPath}: ${formatError(err)}`);
223
235
  return null;
224
236
  }
225
237
  }
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/output/logger.ts
4
+ import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "fs";
5
+ import { dirname } from "path";
6
+ import pc from "picocolors";
7
+ var logFilePath = null;
8
+ var outputMode = "default";
9
+ var logLevel = "default";
10
+ function setOutputMode(mode) {
11
+ outputMode = mode;
12
+ }
13
+ function getOutputMode() {
14
+ return outputMode;
15
+ }
16
+ function setLogLevel(level) {
17
+ logLevel = level;
18
+ }
19
+ function shouldPrintToConsole() {
20
+ return outputMode !== "tui" && logLevel !== "quiet";
21
+ }
22
+ function initLogFile(path) {
23
+ const dir = dirname(path);
24
+ if (!existsSync(dir)) {
25
+ mkdirSync(dir, { recursive: true });
26
+ }
27
+ writeFileSync(path, `[${timestamp()}] Log started
28
+ `);
29
+ logFilePath = path;
30
+ }
31
+ function timestamp() {
32
+ return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour12: false });
33
+ }
34
+ function writeToFile(level, message) {
35
+ if (logFilePath) {
36
+ appendFileSync(logFilePath, `[${timestamp()}] [${level}] ${message}
37
+ `);
38
+ }
39
+ }
40
+ function log(message) {
41
+ if (shouldPrintToConsole()) {
42
+ console.error(`${pc.cyan("[lisa]")} ${pc.dim(timestamp())} ${message}`);
43
+ }
44
+ writeToFile("info", message);
45
+ }
46
+ function warn(message) {
47
+ if (shouldPrintToConsole()) {
48
+ console.error(`${pc.yellow("[lisa]")} ${pc.dim(timestamp())} ${message}`);
49
+ }
50
+ writeToFile("warn", message);
51
+ }
52
+ function error(message) {
53
+ if (shouldPrintToConsole()) {
54
+ console.error(`${pc.red("[lisa]")} ${pc.dim(timestamp())} ${message}`);
55
+ }
56
+ writeToFile("error", message);
57
+ }
58
+ function ok(message) {
59
+ if (shouldPrintToConsole()) {
60
+ console.error(`${pc.green("[lisa]")} ${pc.dim(timestamp())} ${message}`);
61
+ }
62
+ writeToFile("ok", message);
63
+ }
64
+ function verbose(message) {
65
+ if (logLevel !== "verbose") return;
66
+ if (shouldPrintToConsole()) {
67
+ console.error(`${pc.dim("[lisa]")} ${pc.dim(timestamp())} ${pc.dim(message)}`);
68
+ }
69
+ writeToFile("verbose", message);
70
+ }
71
+ function divider(session) {
72
+ log(`${"\u2501".repeat(3)} Session ${session} ${"\u2501".repeat(3)}`);
73
+ }
74
+ function banner() {
75
+ if (outputMode !== "default" || logLevel === "quiet") return;
76
+ const title = " lisa \u266A autonomous issue resolver ";
77
+ const border = "\u2500".repeat(title.length);
78
+ console.error(pc.yellow(`
79
+ \u250C${border}\u2510`));
80
+ console.error(pc.yellow(` \u2502`) + pc.bold(pc.white(title)) + pc.yellow("\u2502"));
81
+ console.error(pc.yellow(` \u2514${border}\u2518
82
+ `));
83
+ }
84
+ function updateNotice(update) {
85
+ if (outputMode !== "default" || logLevel === "quiet") return;
86
+ const msg = `Update available ${pc.dim(update.currentVersion)} \u2192 ${pc.green(pc.bold(update.latestVersion))}`;
87
+ const cmd = `Run ${pc.cyan("npm i -g @tarcisiopgs/lisa")} to update`;
88
+ const lines = [msg, cmd];
89
+ const strip = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
90
+ const maxLen = Math.max(...lines.map((l) => strip(l).length));
91
+ const pad = (s) => s + " ".repeat(maxLen - strip(s).length);
92
+ console.error(pc.yellow(` \u250C${"\u2500".repeat(maxLen + 2)}\u2510`));
93
+ for (const line of lines) {
94
+ console.error(pc.yellow(" \u2502 ") + pad(line) + pc.yellow(" \u2502"));
95
+ }
96
+ console.error(pc.yellow(` \u2514${"\u2500".repeat(maxLen + 2)}\u2518
97
+ `));
98
+ }
99
+
100
+ export {
101
+ setOutputMode,
102
+ getOutputMode,
103
+ setLogLevel,
104
+ initLogFile,
105
+ log,
106
+ warn,
107
+ error,
108
+ ok,
109
+ verbose,
110
+ divider,
111
+ banner,
112
+ updateNotice
113
+ };