libretto 0.2.4 → 0.2.6
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/README.md +34 -3
- package/dist/cli/cli.js +4 -2
- package/dist/cli/commands/browser.js +26 -3
- package/dist/cli/commands/execution.js +50 -11
- package/dist/cli/core/browser.js +131 -6
- package/dist/cli/core/session.js +13 -13
- package/dist/cli/workers/run-integration-runtime.js +64 -59
- package/dist/cli/workers/run-integration-worker-protocol.js +12 -0
- package/dist/cli/workers/run-integration-worker.js +13 -30
- package/dist/index.cjs +2 -12
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3 -15
- package/dist/shared/debug/index.cjs +4 -6
- package/dist/shared/debug/index.d.cts +1 -2
- package/dist/shared/debug/index.d.ts +1 -2
- package/dist/shared/debug/index.js +3 -8
- package/dist/shared/debug/pause.cjs +58 -24
- package/dist/shared/debug/pause.d.cts +13 -20
- package/dist/shared/debug/pause.d.ts +13 -20
- package/dist/shared/debug/pause.js +46 -21
- package/dist/shared/llm/ai-sdk-adapter.cjs +7 -1
- package/dist/shared/llm/ai-sdk-adapter.js +7 -1
- package/dist/shared/run/api.cjs +0 -7
- package/dist/shared/run/api.d.cts +0 -1
- package/dist/shared/run/api.d.ts +0 -1
- package/dist/shared/run/api.js +0 -8
- package/dist/shared/workflow/workflow.d.cts +11 -24
- package/dist/shared/workflow/workflow.d.ts +11 -24
- package/package.json +10 -4
- package/skill/SKILL.md +18 -5
- package/skill/code-generation-rules.md +43 -10
package/README.md
CHANGED
|
@@ -9,17 +9,21 @@ pnpm add libretto playwright zod
|
|
|
9
9
|
npx libretto init
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
> **pnpm users:**
|
|
13
|
-
>
|
|
12
|
+
> **pnpm users:** if your workspace uses `onlyBuiltDependencies`, add both
|
|
13
|
+
> `libretto` and `playwright` to allow their postinstall scripts to run
|
|
14
|
+
> (libretto's postinstall copies skill files and installs Playwright Chromium):
|
|
14
15
|
>
|
|
15
16
|
> ```jsonc
|
|
16
17
|
> // package.json
|
|
17
18
|
> {
|
|
18
19
|
> "pnpm": {
|
|
19
|
-
> "onlyBuiltDependencies": ["playwright"]
|
|
20
|
+
> "onlyBuiltDependencies": ["libretto", "playwright"]
|
|
20
21
|
> }
|
|
21
22
|
> }
|
|
22
23
|
> ```
|
|
24
|
+
>
|
|
25
|
+
> If the postinstall was skipped (e.g., `libretto` wasn't in the allowlist),
|
|
26
|
+
> run `npx libretto init` manually after install to complete setup.
|
|
23
27
|
|
|
24
28
|
## Quick Start
|
|
25
29
|
|
|
@@ -119,6 +123,33 @@ Run `npx libretto help` for the full list.
|
|
|
119
123
|
| `libretto/run` | `launchBrowser` |
|
|
120
124
|
| `libretto/state` | Session state serialization and parsing |
|
|
121
125
|
|
|
126
|
+
## Using Recovery Helpers
|
|
127
|
+
|
|
128
|
+
The recovery module (`libretto/recovery`) provides `detectSubmissionError` and
|
|
129
|
+
`executeRecoveryAgent` for handling form submission errors. Both accept an
|
|
130
|
+
`LLMClient` — create one with `createLLMClientFromModel` and pass it directly:
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { detectSubmissionError, executeRecoveryAgent } from "libretto/recovery";
|
|
134
|
+
import { createLLMClientFromModel } from "libretto/llm";
|
|
135
|
+
import { openai } from "@ai-sdk/openai";
|
|
136
|
+
|
|
137
|
+
const llmClient = createLLMClientFromModel(openai("gpt-4o"));
|
|
138
|
+
|
|
139
|
+
// Detect if a submission produced an error
|
|
140
|
+
const error = await detectSubmissionError(
|
|
141
|
+
page, submissionError, "eligibility check failed", llmClient, knownErrors, logger,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Or run the full recovery agent to retry with corrections
|
|
145
|
+
const result = await executeRecoveryAgent(
|
|
146
|
+
page, error, llmClient, recoveryOptions, logger,
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
No need to write custom wrappers — `createLLMClientFromModel` bridges any
|
|
151
|
+
Vercel AI SDK provider into the `LLMClient` interface that recovery helpers expect.
|
|
152
|
+
|
|
122
153
|
## Links
|
|
123
154
|
|
|
124
155
|
- [GitHub](https://github.com/saffron-health/libretto)
|
package/dist/cli/cli.js
CHANGED
|
@@ -39,7 +39,7 @@ Commands:
|
|
|
39
39
|
init [--skip-browsers] Initialize libretto (copy skills, install browsers)
|
|
40
40
|
open <url> [--headless] Launch browser and open URL (headed by default)
|
|
41
41
|
Automatically loads saved profile if available
|
|
42
|
-
run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless]
|
|
42
|
+
run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless] Run an exported Libretto workflow from a file
|
|
43
43
|
ai configure [preset] [-- <command prefix...>] Configure AI runtime for analysis commands
|
|
44
44
|
save <url|domain> Save current browser session (cookies, localStorage, etc.)
|
|
45
45
|
exec <code> [--visualize] Execute Playwright typescript code (--visualize enables ghost cursor + highlight)
|
|
@@ -48,7 +48,7 @@ Commands:
|
|
|
48
48
|
actions [--last N] [--filter regex] [--action TYPE] [--source SOURCE] [--clear] View captured actions
|
|
49
49
|
pages List open pages in the active session
|
|
50
50
|
resume Resume a paused workflow in the active session
|
|
51
|
-
close
|
|
51
|
+
close [--all] [--force] Close the browser for the session, or all tracked sessions with --all
|
|
52
52
|
|
|
53
53
|
Options:
|
|
54
54
|
--session <name> Use a named session (default: "default")
|
|
@@ -71,6 +71,8 @@ Examples:
|
|
|
71
71
|
libretto-cli snapshot --objective "Find the submit button" --context "Submitting a referral form, already filled in patient details"
|
|
72
72
|
libretto-cli resume --session default
|
|
73
73
|
libretto-cli close
|
|
74
|
+
libretto-cli close --all
|
|
75
|
+
libretto-cli close --all --force
|
|
74
76
|
|
|
75
77
|
# Multiple sessions
|
|
76
78
|
libretto-cli open https://site1.com --session test1
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
runClose as runCloseWithLogger,
|
|
3
|
+
runCloseAll as runCloseAllWithLogger,
|
|
3
4
|
runOpen,
|
|
4
5
|
runPages,
|
|
5
6
|
runSave
|
|
@@ -44,9 +45,31 @@ function registerBrowserCommands(yargs, logger) {
|
|
|
44
45
|
}
|
|
45
46
|
).command("pages", "List open pages in the session", (cmd) => cmd, async (argv) => {
|
|
46
47
|
await runPages(String(argv.session), logger);
|
|
47
|
-
}).command(
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
}).command(
|
|
49
|
+
"close",
|
|
50
|
+
"Close the browser",
|
|
51
|
+
(cmd) => cmd.option("all", {
|
|
52
|
+
type: "boolean",
|
|
53
|
+
default: false,
|
|
54
|
+
describe: "Close all tracked sessions in this workspace"
|
|
55
|
+
}).option("force", {
|
|
56
|
+
type: "boolean",
|
|
57
|
+
default: false,
|
|
58
|
+
describe: "Force kill sessions that ignore SIGTERM (requires --all)"
|
|
59
|
+
}),
|
|
60
|
+
async (argv) => {
|
|
61
|
+
const closeAll = Boolean(argv.all);
|
|
62
|
+
const force = Boolean(argv.force);
|
|
63
|
+
if (force && !closeAll) {
|
|
64
|
+
throw new Error("Usage: libretto-cli close --all [--force]");
|
|
65
|
+
}
|
|
66
|
+
if (closeAll) {
|
|
67
|
+
await runCloseAllWithLogger(logger, { force });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
await runCloseWithLogger(String(argv.session), logger);
|
|
71
|
+
}
|
|
72
|
+
);
|
|
50
73
|
}
|
|
51
74
|
async function runClose(session) {
|
|
52
75
|
await withSessionLogger(session, async (logger) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
3
|
import * as moduleBuiltin from "node:module";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { installInstrumentation } from "../../shared/instrumentation/index.js";
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
import { getPauseSignalPaths } from "../core/pause-signals.js";
|
|
11
11
|
import {
|
|
12
12
|
assertSessionAvailableForStart,
|
|
13
|
+
clearSessionState,
|
|
14
|
+
readSessionState,
|
|
13
15
|
readSessionStateOrThrow,
|
|
14
16
|
setSessionStatus
|
|
15
17
|
} from "../core/session.js";
|
|
@@ -170,6 +172,35 @@ function isProcessRunning(pid) {
|
|
|
170
172
|
return false;
|
|
171
173
|
}
|
|
172
174
|
}
|
|
175
|
+
async function stopExistingFailedRunSession(session, logger) {
|
|
176
|
+
const existingState = readSessionState(session, logger);
|
|
177
|
+
if (!existingState || existingState.status !== "failed") {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
logger.info("run-release-existing-failed-session", {
|
|
181
|
+
session,
|
|
182
|
+
pid: existingState.pid,
|
|
183
|
+
port: existingState.port
|
|
184
|
+
});
|
|
185
|
+
clearSessionState(session, logger);
|
|
186
|
+
const stopDeadline = Date.now() + 3e3;
|
|
187
|
+
while (isProcessRunning(existingState.pid) && Date.now() < stopDeadline) {
|
|
188
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
189
|
+
}
|
|
190
|
+
if (isProcessRunning(existingState.pid)) {
|
|
191
|
+
logger.warn("run-release-existing-failed-session-timeout", {
|
|
192
|
+
session,
|
|
193
|
+
pid: existingState.pid
|
|
194
|
+
});
|
|
195
|
+
console.warn(
|
|
196
|
+
`Existing failed workflow process for session "${session}" (pid ${existingState.pid}) is still shutting down; continuing.`
|
|
197
|
+
);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
console.log(
|
|
201
|
+
`Closed existing failed workflow process for session "${session}" (pid ${existingState.pid}).`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
173
204
|
function readJsonFileIfExists(path) {
|
|
174
205
|
if (!existsSync(path)) return null;
|
|
175
206
|
try {
|
|
@@ -246,7 +277,7 @@ async function runResume(session, logger) {
|
|
|
246
277
|
} = getPauseSignalPaths(session);
|
|
247
278
|
if (!existsSync(pausedSignalPath)) {
|
|
248
279
|
throw new Error(
|
|
249
|
-
`Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call
|
|
280
|
+
`Session "${session}" is not paused. Run "libretto-cli run ... --session ${session}" and call pause() first.`
|
|
250
281
|
);
|
|
251
282
|
}
|
|
252
283
|
if (!isProcessRunning(state.pid)) {
|
|
@@ -297,6 +328,7 @@ async function runResume(session, logger) {
|
|
|
297
328
|
console.log("Workflow paused.");
|
|
298
329
|
}
|
|
299
330
|
async function runIntegrationFromFile(args, logger) {
|
|
331
|
+
await stopExistingFailedRunSession(args.session, logger);
|
|
300
332
|
assertSessionAvailableForStart(args.session, logger);
|
|
301
333
|
const signalPaths = getPauseSignalPaths(args.session);
|
|
302
334
|
clearSignalIfExists(signalPaths.pausedSignalPath);
|
|
@@ -308,12 +340,11 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
308
340
|
new URL("../workers/run-integration-worker.js", import.meta.url)
|
|
309
341
|
);
|
|
310
342
|
const payload = JSON.stringify(args);
|
|
311
|
-
const worker =
|
|
343
|
+
const worker = spawn(process.execPath, [workerEntryPath, payload], {
|
|
312
344
|
detached: true,
|
|
313
|
-
stdio:
|
|
345
|
+
stdio: "ignore",
|
|
314
346
|
env: process.env
|
|
315
347
|
});
|
|
316
|
-
worker.disconnect();
|
|
317
348
|
worker.unref();
|
|
318
349
|
const outcome = await waitForWorkflowOutcome({
|
|
319
350
|
session: args.session,
|
|
@@ -326,7 +357,10 @@ async function runIntegrationFromFile(args, logger) {
|
|
|
326
357
|
}
|
|
327
358
|
if (outcome.status === "failed") {
|
|
328
359
|
setSessionStatus(args.session, "failed", logger);
|
|
329
|
-
throw new Error(
|
|
360
|
+
throw new Error(
|
|
361
|
+
`${outcome.message ?? "Workflow failed during run."}
|
|
362
|
+
Browser is still open. You can use \`exec\` to inspect it. Call \`run\` to re-run the workflow.`
|
|
363
|
+
);
|
|
330
364
|
}
|
|
331
365
|
if (outcome.status === "exited") {
|
|
332
366
|
setSessionStatus(args.session, "exited", logger);
|
|
@@ -360,11 +394,17 @@ function registerExecutionCommands(yargs, logger) {
|
|
|
360
394
|
).command(
|
|
361
395
|
"run [integrationFile] [integrationExport]",
|
|
362
396
|
"Run an exported Libretto workflow from a file",
|
|
363
|
-
(cmd) => cmd.option("params", { type: "string" }).option("params-file", { type: "string" }).option("headed", { type: "boolean", default: false }).option("headless", { type: "boolean", default: false }).option("
|
|
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)" }),
|
|
364
398
|
async (argv) => {
|
|
365
|
-
const usage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless]
|
|
399
|
+
const usage = "Usage: libretto-cli run <integrationFile> <integrationExport> [--params <json> | --params-file <path>] [--headed|--headless]";
|
|
366
400
|
const integrationPath = argv.integrationFile;
|
|
367
401
|
const exportName = argv.integrationExport;
|
|
402
|
+
const legacyDebug = argv.debug;
|
|
403
|
+
if (legacyDebug !== void 0) {
|
|
404
|
+
throw new Error(
|
|
405
|
+
"The --debug flag has been removed. Run the command without --debug."
|
|
406
|
+
);
|
|
407
|
+
}
|
|
368
408
|
if (!integrationPath || !exportName) {
|
|
369
409
|
throw new Error(usage);
|
|
370
410
|
}
|
|
@@ -397,15 +437,14 @@ function registerExecutionCommands(yargs, logger) {
|
|
|
397
437
|
throw new Error("Cannot pass both --headed and --headless.");
|
|
398
438
|
}
|
|
399
439
|
const headlessMode = hasHeadedFlag ? false : hasHeadlessFlag ? true : void 0;
|
|
400
|
-
const
|
|
401
|
-
const debugMode = debugFlag !== void 0 ? debugFlag : process.env.LIBRETTO_DEBUG === "true";
|
|
440
|
+
const authProfileDomain = argv["auth-profile"];
|
|
402
441
|
await runIntegrationFromFile({
|
|
403
442
|
integrationPath,
|
|
404
443
|
exportName,
|
|
405
444
|
session,
|
|
406
445
|
params,
|
|
407
446
|
headless: headlessMode ?? false,
|
|
408
|
-
|
|
447
|
+
authProfileDomain
|
|
409
448
|
}, logger);
|
|
410
449
|
}
|
|
411
450
|
).command(
|
package/dist/cli/core/browser.js
CHANGED
|
@@ -12,12 +12,15 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
assertSessionAvailableForStart,
|
|
14
14
|
clearSessionState,
|
|
15
|
+
listSessionsWithStateFile,
|
|
15
16
|
readSessionStateOrThrow,
|
|
16
17
|
logFileForSession,
|
|
17
18
|
readSessionState,
|
|
18
19
|
writeSessionState
|
|
19
20
|
} from "./session.js";
|
|
20
21
|
import { installSessionTelemetry } from "./session-telemetry.js";
|
|
22
|
+
const CLOSE_WAIT_MS = 1500;
|
|
23
|
+
const FORCE_CLOSE_WAIT_MS = 300;
|
|
21
24
|
async function pickFreePort() {
|
|
22
25
|
return await new Promise((resolve2, reject) => {
|
|
23
26
|
const server = createServer();
|
|
@@ -488,16 +491,137 @@ async function runClose(session, logger) {
|
|
|
488
491
|
return;
|
|
489
492
|
}
|
|
490
493
|
logger.info("close-killing", { session, pid: state.pid, port: state.port });
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
} catch (err) {
|
|
494
|
-
logger.warn("close-kill-failed", { error: err, session, pid: state.pid });
|
|
495
|
-
}
|
|
496
|
-
await new Promise((r) => setTimeout(r, 1500));
|
|
494
|
+
sendSignalToProcessGroupOrPid(state.pid, "SIGTERM", logger, session);
|
|
495
|
+
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
497
496
|
clearSessionState(session, logger);
|
|
498
497
|
logger.info("close-success", { session });
|
|
499
498
|
console.log(`Browser closed (session: ${session}).`);
|
|
500
499
|
}
|
|
500
|
+
function waitForCloseSignalWindow(ms) {
|
|
501
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
502
|
+
}
|
|
503
|
+
function isPidRunning(pid) {
|
|
504
|
+
try {
|
|
505
|
+
process.kill(pid, 0);
|
|
506
|
+
return true;
|
|
507
|
+
} catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function sendSignalToProcessGroupOrPid(pid, signal, logger, session) {
|
|
512
|
+
try {
|
|
513
|
+
process.kill(pid, signal);
|
|
514
|
+
logger.info("close-signal-pid", { session, pid, signal });
|
|
515
|
+
} catch (pidErr) {
|
|
516
|
+
const pidCode = pidErr.code;
|
|
517
|
+
if (pidCode !== "ESRCH") {
|
|
518
|
+
logger.warn("close-signal-pid-failed", {
|
|
519
|
+
session,
|
|
520
|
+
pid,
|
|
521
|
+
signal,
|
|
522
|
+
error: pidErr
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
function formatSessionList(targets) {
|
|
528
|
+
return targets.map((target) => `"${target.session}"`).join(", ");
|
|
529
|
+
}
|
|
530
|
+
function resolveClosableSessions(logger) {
|
|
531
|
+
const sessions = listSessionsWithStateFile();
|
|
532
|
+
const closable = [];
|
|
533
|
+
let clearedUnreadableStates = 0;
|
|
534
|
+
for (const session of sessions) {
|
|
535
|
+
const state = readSessionState(session, logger);
|
|
536
|
+
if (!state) {
|
|
537
|
+
clearSessionState(session, logger);
|
|
538
|
+
clearedUnreadableStates += 1;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
closable.push({
|
|
542
|
+
session,
|
|
543
|
+
pid: state.pid,
|
|
544
|
+
port: state.port
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
return { closable, clearedUnreadableStates };
|
|
548
|
+
}
|
|
549
|
+
function clearStoppedSessionStates(sessions, logger) {
|
|
550
|
+
let cleared = 0;
|
|
551
|
+
for (const session of sessions) {
|
|
552
|
+
if (!isPidRunning(session.pid)) {
|
|
553
|
+
clearSessionState(session.session, logger);
|
|
554
|
+
cleared += 1;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return cleared;
|
|
558
|
+
}
|
|
559
|
+
async function runCloseAll(logger, options) {
|
|
560
|
+
const force = Boolean(options?.force);
|
|
561
|
+
logger.info("close-all-start", { force });
|
|
562
|
+
const { closable, clearedUnreadableStates } = resolveClosableSessions(logger);
|
|
563
|
+
if (closable.length === 0) {
|
|
564
|
+
if (clearedUnreadableStates > 0) {
|
|
565
|
+
console.log(
|
|
566
|
+
`Cleared ${clearedUnreadableStates} unreadable session state file(s).`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
console.log("No browser sessions found.");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
for (const target of closable) {
|
|
573
|
+
logger.info("close-all-sigterm", {
|
|
574
|
+
session: target.session,
|
|
575
|
+
pid: target.pid,
|
|
576
|
+
port: target.port
|
|
577
|
+
});
|
|
578
|
+
sendSignalToProcessGroupOrPid(target.pid, "SIGTERM", logger, target.session);
|
|
579
|
+
}
|
|
580
|
+
await waitForCloseSignalWindow(CLOSE_WAIT_MS);
|
|
581
|
+
let survivors = closable.filter((target) => isPidRunning(target.pid));
|
|
582
|
+
if (survivors.length > 0 && !force) {
|
|
583
|
+
const closed = clearStoppedSessionStates(closable, logger);
|
|
584
|
+
throw new Error(
|
|
585
|
+
[
|
|
586
|
+
`Failed to close ${survivors.length} session(s) gracefully: ${formatSessionList(survivors)}.`,
|
|
587
|
+
`Closed ${closed} session(s).`,
|
|
588
|
+
"Retry with: libretto-cli close --all --force"
|
|
589
|
+
].join("\n")
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
let forceKilled = 0;
|
|
593
|
+
if (survivors.length > 0) {
|
|
594
|
+
for (const survivor of survivors) {
|
|
595
|
+
logger.warn("close-all-sigkill", {
|
|
596
|
+
session: survivor.session,
|
|
597
|
+
pid: survivor.pid
|
|
598
|
+
});
|
|
599
|
+
sendSignalToProcessGroupOrPid(survivor.pid, "SIGKILL", logger, survivor.session);
|
|
600
|
+
forceKilled += 1;
|
|
601
|
+
}
|
|
602
|
+
await waitForCloseSignalWindow(FORCE_CLOSE_WAIT_MS);
|
|
603
|
+
survivors = survivors.filter((target) => isPidRunning(target.pid));
|
|
604
|
+
if (survivors.length > 0) {
|
|
605
|
+
const closed = clearStoppedSessionStates(closable, logger);
|
|
606
|
+
throw new Error(
|
|
607
|
+
[
|
|
608
|
+
`Failed to force-close ${survivors.length} session(s): ${formatSessionList(survivors)}.`,
|
|
609
|
+
`Closed ${closed} session(s).`
|
|
610
|
+
].join("\n")
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
clearStoppedSessionStates(closable, logger);
|
|
615
|
+
if (clearedUnreadableStates > 0) {
|
|
616
|
+
console.log(
|
|
617
|
+
`Cleared ${clearedUnreadableStates} unreadable session state file(s).`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
console.log(`Closed ${closable.length} session(s).`);
|
|
621
|
+
if (forceKilled > 0) {
|
|
622
|
+
console.log(`Force-killed ${forceKilled} session(s).`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
501
625
|
function resolvePath(filePath) {
|
|
502
626
|
return join(process.cwd(), filePath);
|
|
503
627
|
}
|
|
@@ -517,6 +641,7 @@ export {
|
|
|
517
641
|
normalizeUrl,
|
|
518
642
|
resolvePath,
|
|
519
643
|
runClose,
|
|
644
|
+
runCloseAll,
|
|
520
645
|
runOpen,
|
|
521
646
|
runPages,
|
|
522
647
|
runSave
|
package/dist/cli/core/session.js
CHANGED
|
@@ -14,7 +14,6 @@ import {
|
|
|
14
14
|
} from "./context.js";
|
|
15
15
|
import {
|
|
16
16
|
SESSION_STATE_VERSION,
|
|
17
|
-
SessionStatusSchema,
|
|
18
17
|
parseSessionStateContent,
|
|
19
18
|
serializeSessionState
|
|
20
19
|
} from "../../shared/state/index.js";
|
|
@@ -65,11 +64,19 @@ function readSessionState(session, logger) {
|
|
|
65
64
|
return null;
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
|
-
function
|
|
67
|
+
function listSessionsWithStateFile() {
|
|
69
68
|
if (!existsSync(LIBRETTO_SESSIONS_DIR)) return [];
|
|
70
|
-
return readdirSync(LIBRETTO_SESSIONS_DIR).filter(
|
|
71
|
-
|
|
72
|
-
|
|
69
|
+
return readdirSync(LIBRETTO_SESSIONS_DIR).filter((session) => {
|
|
70
|
+
try {
|
|
71
|
+
validateSessionName(session);
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return existsSync(getSessionStatePath(session));
|
|
76
|
+
}).sort();
|
|
77
|
+
}
|
|
78
|
+
function listActiveSessions() {
|
|
79
|
+
return listSessionsWithStateFile();
|
|
73
80
|
}
|
|
74
81
|
function throwSessionNotFoundError(session) {
|
|
75
82
|
const active = listActiveSessions();
|
|
@@ -128,9 +135,6 @@ function clearSessionState(session, logger) {
|
|
|
128
135
|
unlinkSync(stateFile);
|
|
129
136
|
logger?.info("session-state-cleared", { session, stateFile });
|
|
130
137
|
}
|
|
131
|
-
function isSessionStatus(value) {
|
|
132
|
-
return SessionStatusSchema.safeParse(value).success;
|
|
133
|
-
}
|
|
134
138
|
function isPidRunning(pid) {
|
|
135
139
|
try {
|
|
136
140
|
process.kill(pid, 0);
|
|
@@ -151,11 +155,6 @@ function setSessionStatus(session, status, logger) {
|
|
|
151
155
|
function assertSessionAvailableForStart(session, logger) {
|
|
152
156
|
const existingState = readSessionState(session, logger);
|
|
153
157
|
if (!existingState) return;
|
|
154
|
-
if (isSessionStatus(existingState.status)) {
|
|
155
|
-
if (existingState.status === "completed" || existingState.status === "failed" || existingState.status === "exited") {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
158
|
if (!isPidRunning(existingState.pid)) {
|
|
160
159
|
setSessionStatus(session, "exited", logger);
|
|
161
160
|
return;
|
|
@@ -174,6 +173,7 @@ export {
|
|
|
174
173
|
assertSessionStateExistsOrThrow,
|
|
175
174
|
clearSessionState,
|
|
176
175
|
getStateFilePath,
|
|
176
|
+
listSessionsWithStateFile,
|
|
177
177
|
logFileForSession,
|
|
178
178
|
readSessionState,
|
|
179
179
|
readSessionStateOrThrow,
|