libretto 0.6.9 → 0.6.10
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 +2 -0
- package/dist/cli/commands/auth.js +535 -0
- package/dist/cli/commands/billing.js +74 -0
- package/dist/cli/commands/browser.js +8 -3
- package/dist/cli/commands/deploy.js +2 -7
- package/dist/cli/commands/execution.js +99 -136
- package/dist/cli/commands/snapshot.js +38 -126
- package/dist/cli/core/ai-model.js +0 -3
- package/dist/cli/core/auth-fetch.js +195 -0
- package/dist/cli/core/auth-storage.js +52 -0
- package/dist/cli/core/browser.js +128 -202
- package/dist/cli/core/daemon/config.js +6 -0
- package/dist/cli/core/daemon/daemon.js +298 -0
- package/dist/cli/core/daemon/exec.js +86 -0
- package/dist/cli/core/daemon/index.js +16 -0
- package/dist/cli/core/daemon/ipc.js +171 -0
- package/dist/cli/core/daemon/pages.js +15 -0
- package/dist/cli/core/daemon/snapshot.js +86 -0
- package/dist/cli/core/daemon/spawn.js +90 -0
- package/dist/cli/core/exec-compiler.js +111 -0
- package/dist/cli/core/prompt.js +72 -0
- package/dist/cli/core/providers/libretto-cloud.js +2 -6
- package/dist/cli/core/readonly-exec.js +1 -1
- package/dist/cli/router.js +4 -0
- package/dist/cli/workers/run-integration-runtime.js +0 -5
- package/dist/shared/state/session-state.d.ts +1 -0
- package/dist/shared/state/session-state.js +2 -1
- package/docs/browser-automation-approaches.md +435 -0
- package/docs/releasing.md +117 -0
- package/package.json +4 -3
- package/skills/libretto/SKILL.md +14 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/cli.ts +2 -0
- package/src/cli/commands/auth.ts +787 -0
- package/src/cli/commands/billing.ts +133 -0
- package/src/cli/commands/browser.ts +8 -2
- package/src/cli/commands/deploy.ts +2 -7
- package/src/cli/commands/execution.ts +126 -186
- package/src/cli/commands/snapshot.ts +46 -143
- package/src/cli/core/ai-model.ts +4 -5
- package/src/cli/core/auth-fetch.ts +283 -0
- package/src/cli/core/auth-storage.ts +102 -0
- package/src/cli/core/browser.ts +159 -242
- package/src/cli/core/daemon/config.ts +46 -0
- package/src/cli/core/daemon/daemon.ts +429 -0
- package/src/cli/core/daemon/exec.ts +128 -0
- package/src/cli/core/daemon/index.ts +24 -0
- package/src/cli/core/daemon/ipc.ts +294 -0
- package/src/cli/core/daemon/pages.ts +21 -0
- package/src/cli/core/daemon/snapshot.ts +114 -0
- package/src/cli/core/daemon/spawn.ts +171 -0
- package/src/cli/core/exec-compiler.ts +169 -0
- package/src/cli/core/prompt.ts +94 -0
- package/src/cli/core/providers/libretto-cloud.ts +2 -6
- package/src/cli/core/readonly-exec.ts +2 -1
- package/src/cli/router.ts +4 -0
- package/src/cli/workers/run-integration-runtime.ts +0 -6
- package/src/shared/state/session-state.ts +1 -0
- package/dist/cli/core/browser-daemon.js +0 -122
- package/src/cli/core/browser-daemon.ts +0 -198
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hosted-platform billing commands. Stripe is the source of truth
|
|
3
|
+
* for the plan catalog and is also where every tenant — including
|
|
4
|
+
* Free — has a live Subscription. The Stripe Customer Portal is the
|
|
5
|
+
* single management UI: it shows the user's current plan and lets
|
|
6
|
+
* them switch between any of the configured Subscription Update
|
|
7
|
+
* products (Free / Pro / Team).
|
|
8
|
+
*
|
|
9
|
+
* libretto experimental billing portal → Stripe Customer Portal
|
|
10
|
+
* libretto experimental billing status → plan + usage + period end
|
|
11
|
+
*
|
|
12
|
+
* `libretto init` is unchanged. New tenants start on Free automatically
|
|
13
|
+
* (with a real Stripe Customer + Free Subscription created at signup).
|
|
14
|
+
*
|
|
15
|
+
* Auth: requires a session cookie (or LIBRETTO_API_KEY).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
19
|
+
import {
|
|
20
|
+
NOT_AUTHENTICATED_MESSAGE,
|
|
21
|
+
orpcCall,
|
|
22
|
+
pickCredential,
|
|
23
|
+
resolveApiUrl,
|
|
24
|
+
} from "../core/auth-fetch.js";
|
|
25
|
+
import { readAuthState } from "../core/auth-storage.js";
|
|
26
|
+
|
|
27
|
+
// Marketing-site URL — used for BAA requests and Enterprise contact.
|
|
28
|
+
const CONTACT_URL = "https://libretto.sh";
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Types — mirrored from api/src/routes/billing/subscription.ts
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
type SubscriptionResponse = {
|
|
35
|
+
plan: string;
|
|
36
|
+
status: string;
|
|
37
|
+
currentPeriodEnd: string | null;
|
|
38
|
+
cancelAtPeriodEnd: boolean;
|
|
39
|
+
browserHoursUsedThisPeriod: number;
|
|
40
|
+
browserHoursLimit: number | null;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type OpenPlansPageResponse = {
|
|
44
|
+
url: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Shared helpers
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
async function requireAuth(): Promise<{ apiUrl: string; credential: ReturnType<typeof pickCredential> }> {
|
|
52
|
+
const stored = await readAuthState();
|
|
53
|
+
const apiUrl = resolveApiUrl(stored);
|
|
54
|
+
const credential = pickCredential(stored);
|
|
55
|
+
if (credential.source === "none") {
|
|
56
|
+
throw new Error(NOT_AUTHENTICATED_MESSAGE);
|
|
57
|
+
}
|
|
58
|
+
return { apiUrl, credential };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatLimit(limit: number | null): string {
|
|
62
|
+
return limit === null ? "∞" : String(limit);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// portal: always opens the Stripe Customer Portal — every tenant has a
|
|
67
|
+
// live Stripe Subscription (Free or paid) so the portal always has
|
|
68
|
+
// something to show. It's where users see their current plan and switch
|
|
69
|
+
// to another. We DON'T branch on plan / status here.
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export const billingPortalCommand = SimpleCLI.command({
|
|
73
|
+
description: "Open the libretto plans page (current plan + switch options)",
|
|
74
|
+
experimental: true,
|
|
75
|
+
})
|
|
76
|
+
.handle(async () => {
|
|
77
|
+
const { apiUrl, credential } = await requireAuth();
|
|
78
|
+
const { url } = await orpcCall<OpenPlansPageResponse>({
|
|
79
|
+
apiUrl,
|
|
80
|
+
path: "/v1/billing/openPlansPage",
|
|
81
|
+
credential,
|
|
82
|
+
});
|
|
83
|
+
console.log("Open this URL in your browser to choose or change your plan:");
|
|
84
|
+
console.log(` ${url}`);
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(
|
|
87
|
+
"(Shows all tiers with features, your current plan, and a Manage payment / invoices link.)",
|
|
88
|
+
);
|
|
89
|
+
console.log(
|
|
90
|
+
`For a BAA or Enterprise pricing, contact us at ${CONTACT_URL}.`,
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// status
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
export const billingStatusCommand = SimpleCLI.command({
|
|
99
|
+
description: "Print the current plan, status, and browser-hour usage",
|
|
100
|
+
experimental: true,
|
|
101
|
+
})
|
|
102
|
+
.handle(async () => {
|
|
103
|
+
const { apiUrl, credential } = await requireAuth();
|
|
104
|
+
const sub = await orpcCall<SubscriptionResponse>({
|
|
105
|
+
apiUrl,
|
|
106
|
+
path: "/v1/billing/subscription",
|
|
107
|
+
credential,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const used = sub.browserHoursUsedThisPeriod.toFixed(2);
|
|
111
|
+
const limit = formatLimit(sub.browserHoursLimit);
|
|
112
|
+
|
|
113
|
+
console.log(`Plan: ${sub.plan} (${sub.status})`);
|
|
114
|
+
console.log(`Usage: ${used} / ${limit} browser hours this period`);
|
|
115
|
+
if (sub.currentPeriodEnd) {
|
|
116
|
+
console.log(`Period: ends ${sub.currentPeriodEnd.slice(0, 10)}`);
|
|
117
|
+
}
|
|
118
|
+
if (sub.cancelAtPeriodEnd) {
|
|
119
|
+
console.log("Note: cancellation scheduled at period end.");
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Group export
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
export const billingCommands = SimpleCLI.group({
|
|
128
|
+
description: "Hosted-platform subscription + usage commands",
|
|
129
|
+
routes: {
|
|
130
|
+
portal: billingPortalCommand,
|
|
131
|
+
status: billingStatusCommand,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
@@ -80,6 +80,10 @@ export const openInput = SimpleCLI.input({
|
|
|
80
80
|
name: "write-access",
|
|
81
81
|
help: "Create the session in write-access mode (overrides config default)",
|
|
82
82
|
}),
|
|
83
|
+
authProfile: SimpleCLI.option(z.string().optional(), {
|
|
84
|
+
name: "auth-profile",
|
|
85
|
+
help: "Override the domain used for auth profile lookup (e.g. use login.example.com's profile when opening app.example.com)",
|
|
86
|
+
}),
|
|
83
87
|
viewport: SimpleCLI.option(z.string().optional(), {
|
|
84
88
|
help: "Viewport size as WIDTHxHEIGHT (e.g. 1920x1080)",
|
|
85
89
|
}),
|
|
@@ -91,7 +95,7 @@ export const openInput = SimpleCLI.input({
|
|
|
91
95
|
})
|
|
92
96
|
.refine(
|
|
93
97
|
(input) => Boolean(input.url),
|
|
94
|
-
`Usage: libretto open <url> [--headless] [--read-only|--write-access] [--viewport WxH] [--session <name>]`,
|
|
98
|
+
`Usage: libretto open <url> [--headless] [--read-only|--write-access] [--auth-profile <domain>] [--viewport WxH] [--session <name>]`,
|
|
95
99
|
)
|
|
96
100
|
.refine(
|
|
97
101
|
(input) => !(input.headed && input.headless),
|
|
@@ -103,7 +107,8 @@ export const openInput = SimpleCLI.input({
|
|
|
103
107
|
);
|
|
104
108
|
|
|
105
109
|
export const openCommand = SimpleCLI.command({
|
|
106
|
-
description:
|
|
110
|
+
description:
|
|
111
|
+
"Launch browser and open URL (headed by default). Automatically loads a saved auth profile for the URL's domain if one exists.",
|
|
107
112
|
})
|
|
108
113
|
.input(openInput)
|
|
109
114
|
.use(withAutoSession())
|
|
@@ -120,6 +125,7 @@ export const openCommand = SimpleCLI.command({
|
|
|
120
125
|
input.readOnly,
|
|
121
126
|
input.writeAccess,
|
|
122
127
|
),
|
|
128
|
+
authProfileDomain: input.authProfile,
|
|
123
129
|
});
|
|
124
130
|
} else {
|
|
125
131
|
const provider = getCloudProviderApi(providerName);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { HOSTED_API_URL } from "../core/auth-fetch.js";
|
|
3
4
|
import { buildHostedDeployTarball } from "../core/deploy-artifact.js";
|
|
4
5
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
5
6
|
|
|
@@ -19,21 +20,15 @@ function generateDeploymentName(): string {
|
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
function getConfig() {
|
|
22
|
-
const apiUrl = process.env.LIBRETTO_API_URL;
|
|
23
23
|
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
24
24
|
|
|
25
|
-
if (!apiUrl) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
"LIBRETTO_API_URL environment variable is required.",
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
25
|
if (!apiKey) {
|
|
31
26
|
throw new Error(
|
|
32
27
|
"LIBRETTO_API_KEY environment variable is required.",
|
|
33
28
|
);
|
|
34
29
|
}
|
|
35
30
|
|
|
36
|
-
return { apiUrl:
|
|
31
|
+
return { apiUrl: HOSTED_API_URL, apiKey };
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
async function postJson(
|
|
@@ -17,18 +17,24 @@ import {
|
|
|
17
17
|
assertSessionAllowsCommand,
|
|
18
18
|
clearSessionState,
|
|
19
19
|
readSessionState,
|
|
20
|
+
readSessionStateOrThrow,
|
|
20
21
|
setSessionStatus,
|
|
21
22
|
type SessionState,
|
|
22
23
|
} from "../core/session.js";
|
|
23
24
|
import { warnIfInstalledSkillOutOfDate } from "../core/skill-version.js";
|
|
25
|
+
import { readLibrettoConfig } from "../core/config.js";
|
|
26
|
+
import { resolveProviderName, getCloudProviderApi } from "../core/providers/index.js";
|
|
27
|
+
import {
|
|
28
|
+
compileExecFunction,
|
|
29
|
+
stripEmptyCatchHandlers,
|
|
30
|
+
} from "../core/exec-compiler.js";
|
|
31
|
+
import { DaemonClient } from "../core/daemon/index.js";
|
|
32
|
+
import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
|
|
24
33
|
import {
|
|
25
34
|
readActionLog,
|
|
26
35
|
readNetworkLog,
|
|
27
36
|
wrapPageForActionLogging,
|
|
28
37
|
} from "../core/telemetry.js";
|
|
29
|
-
import { readLibrettoConfig } from "../core/config.js";
|
|
30
|
-
import { resolveProviderName, getCloudProviderApi } from "../core/providers/index.js";
|
|
31
|
-
import { createReadonlyExecHelpers } from "../core/readonly-exec.js";
|
|
32
38
|
import type { RunIntegrationWorkerRequest } from "../workers/run-integration-worker-protocol.js";
|
|
33
39
|
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
34
40
|
import {
|
|
@@ -38,173 +44,85 @@ import {
|
|
|
38
44
|
withRequiredSession,
|
|
39
45
|
} from "./shared.js";
|
|
40
46
|
|
|
41
|
-
type ExecFunction = (...args: unknown[]) => Promise<unknown>;
|
|
42
47
|
type RunIntegrationCommandRequest = RunIntegrationWorkerRequest & {
|
|
43
48
|
tsconfigPath?: string;
|
|
44
49
|
};
|
|
45
50
|
type ExecMode = "exec" | "readonly-exec";
|
|
46
51
|
|
|
47
|
-
type StripTypeScriptTypesFn = (
|
|
48
|
-
code: string,
|
|
49
|
-
options?: { mode?: "strip" | "transform" },
|
|
50
|
-
) => string;
|
|
51
|
-
|
|
52
|
-
const stripTypeScriptTypes = (
|
|
53
|
-
moduleBuiltin as { stripTypeScriptTypes?: StripTypeScriptTypesFn }
|
|
54
|
-
).stripTypeScriptTypes;
|
|
55
52
|
const require = moduleBuiltin.createRequire(import.meta.url);
|
|
56
53
|
const tsxCliPath = require.resolve("tsx/cli");
|
|
57
54
|
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const warning = args[0];
|
|
65
|
-
const typeOrOptions = args[1];
|
|
66
|
-
const warningMessage =
|
|
67
|
-
typeof warning === "string"
|
|
68
|
-
? warning
|
|
69
|
-
: warning instanceof Error
|
|
70
|
-
? warning.message
|
|
71
|
-
: "";
|
|
72
|
-
const warningType =
|
|
73
|
-
typeof typeOrOptions === "string"
|
|
74
|
-
? typeOrOptions
|
|
75
|
-
: typeof typeOrOptions === "object" &&
|
|
76
|
-
typeOrOptions !== null &&
|
|
77
|
-
"type" in typeOrOptions &&
|
|
78
|
-
typeof (typeOrOptions as { type?: unknown }).type === "string"
|
|
79
|
-
? ((typeOrOptions as { type?: string }).type ?? "")
|
|
80
|
-
: "";
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
warningType === "ExperimentalWarning" &&
|
|
84
|
-
warningMessage.includes("stripTypeScriptTypes")
|
|
85
|
-
) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
originalEmitWarning(...args);
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
try {
|
|
92
|
-
return action();
|
|
93
|
-
} finally {
|
|
94
|
-
mutableProcess.emitWarning = originalEmitWarning;
|
|
55
|
+
function writeDaemonExecOutput(output?: { stdout: string; stderr: string }) {
|
|
56
|
+
if (output?.stdout) {
|
|
57
|
+
process.stdout.write(output.stdout);
|
|
58
|
+
}
|
|
59
|
+
if (output?.stderr) {
|
|
60
|
+
process.stderr.write(output.stderr);
|
|
95
61
|
}
|
|
96
62
|
}
|
|
97
63
|
|
|
98
|
-
function
|
|
64
|
+
async function execViaDaemon(
|
|
99
65
|
code: string,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
66
|
+
session: string,
|
|
67
|
+
daemonSocketPath: string,
|
|
68
|
+
logger: LoggerApi,
|
|
69
|
+
options: {
|
|
70
|
+
visualize?: boolean;
|
|
71
|
+
pageId?: string;
|
|
72
|
+
mode?: ExecMode;
|
|
73
|
+
},
|
|
74
|
+
): Promise<void> {
|
|
75
|
+
const mode = options.mode ?? "exec";
|
|
76
|
+
const { cleaned: cleanedCode, strippedCount } = stripEmptyCatchHandlers(code);
|
|
77
|
+
if (strippedCount > 0) {
|
|
78
|
+
console.log("(Stripped `.catch(() => {})` — letting errors bubble up)");
|
|
79
|
+
}
|
|
80
|
+
logger.info(`${mode}-start`, {
|
|
81
|
+
session,
|
|
82
|
+
codeLength: cleanedCode.length,
|
|
83
|
+
codePreview: cleanedCode.slice(0, 200),
|
|
84
|
+
visualize: options.visualize,
|
|
85
|
+
pageId: options.pageId,
|
|
86
|
+
via: "daemon",
|
|
87
|
+
});
|
|
103
88
|
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
89
|
+
const client = new DaemonClient(daemonSocketPath);
|
|
90
|
+
|
|
91
|
+
const response =
|
|
92
|
+
mode === "exec"
|
|
93
|
+
? await client.exec({
|
|
94
|
+
code: cleanedCode,
|
|
95
|
+
pageId: options.pageId,
|
|
96
|
+
visualize: options.visualize,
|
|
97
|
+
})
|
|
98
|
+
: await client.readonlyExec({
|
|
99
|
+
code: cleanedCode,
|
|
100
|
+
pageId: options.pageId,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (!response.ok) {
|
|
104
|
+
writeDaemonExecOutput(response.output);
|
|
105
|
+
throw new Error(response.message);
|
|
106
|
+
}
|
|
113
107
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
helperNames: string[],
|
|
117
|
-
): ExecFunction {
|
|
118
|
-
const typeStripped = compileTypeScriptExecFunction(code, helperNames);
|
|
119
|
-
if (typeStripped) return typeStripped;
|
|
120
|
-
|
|
121
|
-
const AsyncFunction = Object.getPrototypeOf(async function () {})
|
|
122
|
-
.constructor as new (...args: string[]) => ExecFunction;
|
|
123
|
-
return new AsyncFunction(...helperNames, code);
|
|
124
|
-
}
|
|
108
|
+
const { result, output } = response.data;
|
|
109
|
+
writeDaemonExecOutput(output);
|
|
125
110
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
let result = "";
|
|
138
|
-
let i = 0;
|
|
139
|
-
|
|
140
|
-
while (i < code.length) {
|
|
141
|
-
// Single-line comment
|
|
142
|
-
if (code[i] === "/" && code[i + 1] === "/") {
|
|
143
|
-
const end = code.indexOf("\n", i);
|
|
144
|
-
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 1);
|
|
145
|
-
result += slice;
|
|
146
|
-
i += slice.length;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
// Multi-line comment
|
|
150
|
-
if (code[i] === "/" && code[i + 1] === "*") {
|
|
151
|
-
const end = code.indexOf("*/", i + 2);
|
|
152
|
-
const slice = end === -1 ? code.slice(i) : code.slice(i, end + 2);
|
|
153
|
-
result += slice;
|
|
154
|
-
i += slice.length;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
// String literals
|
|
158
|
-
if (code[i] === '"' || code[i] === "'" || code[i] === "`") {
|
|
159
|
-
const quote = code[i];
|
|
160
|
-
let j = i + 1;
|
|
161
|
-
while (j < code.length) {
|
|
162
|
-
if (code[j] === "\\" && quote !== "`") {
|
|
163
|
-
j += 2;
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
if (code[j] === "\\" && quote === "`") {
|
|
167
|
-
j += 2;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
if (code[j] === quote) {
|
|
171
|
-
j++;
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
// Template literal interpolation — skip nested braces
|
|
175
|
-
if (quote === "`" && code[j] === "$" && code[j + 1] === "{") {
|
|
176
|
-
let depth = 1;
|
|
177
|
-
j += 2;
|
|
178
|
-
while (j < code.length && depth > 0) {
|
|
179
|
-
if (code[j] === "{") depth++;
|
|
180
|
-
else if (code[j] === "}") depth--;
|
|
181
|
-
j++;
|
|
182
|
-
}
|
|
183
|
-
continue;
|
|
184
|
-
}
|
|
185
|
-
j++;
|
|
186
|
-
}
|
|
187
|
-
result += code.slice(i, j);
|
|
188
|
-
i = j;
|
|
189
|
-
continue;
|
|
190
|
-
}
|
|
191
|
-
// Try to match the catch pattern at the current position
|
|
192
|
-
catchRe.lastIndex = i;
|
|
193
|
-
const match = catchRe.exec(code);
|
|
194
|
-
if (match && match.index === i) {
|
|
195
|
-
strippedCount++;
|
|
196
|
-
i += match[0].length;
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
// Regular character
|
|
200
|
-
result += code[i];
|
|
201
|
-
i++;
|
|
111
|
+
logger.info(`${mode}-success`, {
|
|
112
|
+
session,
|
|
113
|
+
hasResult: result !== undefined,
|
|
114
|
+
via: "daemon",
|
|
115
|
+
});
|
|
116
|
+
if (result !== undefined) {
|
|
117
|
+
console.log(
|
|
118
|
+
typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
console.log("Executed successfully");
|
|
202
122
|
}
|
|
203
|
-
|
|
204
|
-
return { cleaned: result, strippedCount };
|
|
205
123
|
}
|
|
206
124
|
|
|
207
|
-
async function
|
|
125
|
+
async function execViaCdpFallback(
|
|
208
126
|
code: string,
|
|
209
127
|
session: string,
|
|
210
128
|
logger: LoggerApi,
|
|
@@ -212,7 +130,7 @@ async function runExec(
|
|
|
212
130
|
visualize?: boolean;
|
|
213
131
|
pageId?: string;
|
|
214
132
|
mode?: ExecMode;
|
|
215
|
-
}
|
|
133
|
+
},
|
|
216
134
|
): Promise<void> {
|
|
217
135
|
const visualize = options.visualize ?? false;
|
|
218
136
|
const pageId = options.pageId;
|
|
@@ -227,7 +145,9 @@ async function runExec(
|
|
|
227
145
|
codePreview: cleanedCode.slice(0, 200),
|
|
228
146
|
visualize,
|
|
229
147
|
pageId,
|
|
148
|
+
via: "cdp-fallback",
|
|
230
149
|
});
|
|
150
|
+
|
|
231
151
|
const {
|
|
232
152
|
browser,
|
|
233
153
|
context,
|
|
@@ -235,7 +155,6 @@ async function runExec(
|
|
|
235
155
|
pageId: resolvedPageId,
|
|
236
156
|
} = await connect(session, logger, 10000, {
|
|
237
157
|
pageId,
|
|
238
|
-
requireSinglePage: true,
|
|
239
158
|
});
|
|
240
159
|
|
|
241
160
|
const STALL_THRESHOLD_MS = 60_000;
|
|
@@ -251,6 +170,7 @@ async function runExec(
|
|
|
251
170
|
session,
|
|
252
171
|
silenceMs,
|
|
253
172
|
codePreview: cleanedCode.slice(0, 200),
|
|
173
|
+
via: "cdp-fallback",
|
|
254
174
|
});
|
|
255
175
|
console.warn(
|
|
256
176
|
`[stall-warning] No Playwright activity for ${Math.round(silenceMs / 1000)}s — ${mode} may be hung (code: ${cleanedCode.slice(0, 100)}...)`,
|
|
@@ -264,6 +184,7 @@ async function runExec(
|
|
|
264
184
|
session,
|
|
265
185
|
duration: Date.now() - execStartTs,
|
|
266
186
|
codePreview: cleanedCode.slice(0, 200),
|
|
187
|
+
via: "cdp-fallback",
|
|
267
188
|
});
|
|
268
189
|
};
|
|
269
190
|
process.on("SIGINT", sigintHandler);
|
|
@@ -277,24 +198,24 @@ async function runExec(
|
|
|
277
198
|
}
|
|
278
199
|
|
|
279
200
|
try {
|
|
201
|
+
const execState: Record<string, unknown> = {};
|
|
280
202
|
const helpers =
|
|
281
203
|
mode === "readonly-exec"
|
|
282
204
|
? createReadonlyExecHelpers(page, { onActivity })
|
|
283
|
-
:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
205
|
+
: {
|
|
206
|
+
page,
|
|
207
|
+
context,
|
|
208
|
+
state: execState,
|
|
209
|
+
browser,
|
|
210
|
+
networkLog: (
|
|
287
211
|
opts: {
|
|
288
212
|
last?: number;
|
|
289
213
|
filter?: string;
|
|
290
214
|
method?: string;
|
|
291
215
|
pageId?: string;
|
|
292
216
|
} = {},
|
|
293
|
-
) =>
|
|
294
|
-
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
const actionLog = (
|
|
217
|
+
) => readNetworkLog(session, opts),
|
|
218
|
+
actionLog: (
|
|
298
219
|
opts: {
|
|
299
220
|
last?: number;
|
|
300
221
|
filter?: string;
|
|
@@ -302,33 +223,25 @@ async function runExec(
|
|
|
302
223
|
source?: string;
|
|
303
224
|
pageId?: string;
|
|
304
225
|
} = {},
|
|
305
|
-
) =>
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
actionLog,
|
|
316
|
-
console,
|
|
317
|
-
setTimeout,
|
|
318
|
-
setInterval,
|
|
319
|
-
clearTimeout,
|
|
320
|
-
clearInterval,
|
|
321
|
-
fetch,
|
|
322
|
-
URL,
|
|
323
|
-
Buffer,
|
|
324
|
-
};
|
|
325
|
-
})();
|
|
226
|
+
) => readActionLog(session, opts),
|
|
227
|
+
console,
|
|
228
|
+
setTimeout,
|
|
229
|
+
setInterval,
|
|
230
|
+
clearTimeout,
|
|
231
|
+
clearInterval,
|
|
232
|
+
fetch,
|
|
233
|
+
URL,
|
|
234
|
+
Buffer,
|
|
235
|
+
};
|
|
326
236
|
|
|
327
237
|
const helperNames = Object.keys(helpers);
|
|
328
238
|
const fn = compileExecFunction(cleanedCode, helperNames);
|
|
329
|
-
|
|
330
239
|
const result = await fn(...Object.values(helpers));
|
|
331
|
-
logger.info(`${mode}-success`, {
|
|
240
|
+
logger.info(`${mode}-success`, {
|
|
241
|
+
session,
|
|
242
|
+
hasResult: result !== undefined,
|
|
243
|
+
via: "cdp-fallback",
|
|
244
|
+
});
|
|
332
245
|
if (result !== undefined) {
|
|
333
246
|
console.log(
|
|
334
247
|
typeof result === "string" ? result : JSON.stringify(result, null, 2),
|
|
@@ -341,6 +254,7 @@ async function runExec(
|
|
|
341
254
|
error: err,
|
|
342
255
|
session,
|
|
343
256
|
codePreview: cleanedCode.slice(0, 200),
|
|
257
|
+
via: "cdp-fallback",
|
|
344
258
|
});
|
|
345
259
|
throw err;
|
|
346
260
|
} finally {
|
|
@@ -350,6 +264,32 @@ async function runExec(
|
|
|
350
264
|
}
|
|
351
265
|
}
|
|
352
266
|
|
|
267
|
+
async function runExec(
|
|
268
|
+
code: string,
|
|
269
|
+
session: string,
|
|
270
|
+
logger: LoggerApi,
|
|
271
|
+
options: {
|
|
272
|
+
visualize?: boolean;
|
|
273
|
+
pageId?: string;
|
|
274
|
+
mode?: ExecMode;
|
|
275
|
+
} = {},
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
const state = readSessionStateOrThrow(session);
|
|
278
|
+
if (!state.daemonSocketPath) {
|
|
279
|
+
// Compatibility fallback for failed runs created before `run` became
|
|
280
|
+
// daemon-backed: those session states can have a live CDP endpoint/port but
|
|
281
|
+
// no daemon socket. Keep `exec` inspection working until such sessions are
|
|
282
|
+
// gone. Context: https://www.notion.so/Make-libretto-run-daemon-backed-for-failed-workflow-inspection-352ac9fb35f181c1b7d3f08c0a735e9d
|
|
283
|
+
logger.warn(`${options.mode ?? "exec"}-daemon-socket-missing-cdp-fallback`, {
|
|
284
|
+
session,
|
|
285
|
+
hasCdpEndpoint: Boolean(state.cdpEndpoint),
|
|
286
|
+
port: state.port,
|
|
287
|
+
});
|
|
288
|
+
return execViaCdpFallback(code, session, logger, options);
|
|
289
|
+
}
|
|
290
|
+
return execViaDaemon(code, session, state.daemonSocketPath, logger, options);
|
|
291
|
+
}
|
|
292
|
+
|
|
353
293
|
function parseJsonArg(label: string, raw: string): unknown {
|
|
354
294
|
try {
|
|
355
295
|
return JSON.parse(raw);
|