libretto 0.6.24 → 0.6.25
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 +9 -1
- package/README.template.md +9 -1
- package/dist/cli/commands/browser.js +17 -10
- package/dist/cli/commands/cloud-credentials.js +70 -0
- package/dist/cli/commands/deploy.js +24 -2
- package/dist/cli/commands/execution.js +9 -30
- package/dist/cli/commands/import-chrome-profiles.js +46 -0
- package/dist/cli/commands/profiles.js +71 -0
- package/dist/cli/commands/shared.js +1 -3
- package/dist/cli/core/browser.js +89 -75
- package/dist/cli/core/daemon/daemon.js +47 -35
- package/dist/cli/core/daemon/ipc.js +3 -0
- package/dist/cli/core/deploy-artifact.js +85 -22
- package/dist/cli/core/profiles.js +47 -0
- package/dist/cli/core/prompt.js +9 -0
- package/dist/cli/core/providers/libretto-cloud.js +6 -2
- package/dist/cli/core/session-logs.js +325 -0
- package/dist/cli/core/telemetry.js +83 -313
- package/dist/cli/core/workflow-runner/runner.js +65 -0
- package/dist/cli/router.js +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +12 -0
- package/dist/shared/workflow/auth-profile-name.d.ts +3 -0
- package/dist/shared/workflow/auth-profile-name.js +29 -0
- package/dist/shared/workflow/auth-profile-state.d.ts +20 -0
- package/dist/shared/workflow/auth-profile-state.js +105 -0
- package/dist/shared/workflow/authenticate.d.ts +17 -0
- package/dist/shared/workflow/authenticate.js +37 -0
- package/dist/shared/workflow/credentials.d.ts +5 -0
- package/dist/shared/workflow/credentials.js +68 -0
- package/dist/shared/workflow/workflow.d.ts +16 -1
- package/dist/shared/workflow/workflow.js +56 -4
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +3 -4
- package/skills/libretto/references/auth-profiles.md +61 -11
- package/skills/libretto/references/code-generation-rules.md +31 -1
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/browser.ts +19 -11
- package/src/cli/commands/cloud-credentials.ts +82 -0
- package/src/cli/commands/deploy.ts +41 -2
- package/src/cli/commands/execution.ts +10 -31
- package/src/cli/commands/import-chrome-profiles.ts +46 -0
- package/src/cli/commands/profiles.ts +90 -0
- package/src/cli/commands/shared.ts +4 -8
- package/src/cli/core/browser.ts +102 -91
- package/src/cli/core/daemon/config.ts +4 -1
- package/src/cli/core/daemon/daemon.ts +52 -44
- package/src/cli/core/daemon/ipc.ts +15 -0
- package/src/cli/core/deploy-artifact.ts +131 -32
- package/src/cli/core/profiles.ts +53 -0
- package/src/cli/core/prompt.ts +15 -0
- package/src/cli/core/providers/libretto-cloud.ts +6 -2
- package/src/cli/core/providers/types.ts +4 -1
- package/src/cli/core/session-logs.ts +445 -0
- package/src/cli/core/telemetry.ts +105 -422
- package/src/cli/core/workflow-runner/runner.ts +86 -1
- package/src/cli/router.ts +8 -0
- package/src/index.ts +10 -0
- package/src/shared/workflow/auth-profile-name.ts +27 -0
- package/src/shared/workflow/auth-profile-state.ts +144 -0
- package/src/shared/workflow/authenticate.ts +63 -0
- package/src/shared/workflow/credentials.ts +91 -0
- package/src/shared/workflow/workflow.ts +89 -4
package/dist/cli/core/browser.js
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
chromium
|
|
3
3
|
} from "playwright";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import {
|
|
5
|
+
existsSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
unlinkSync
|
|
8
|
+
} from "node:fs";
|
|
7
9
|
import { createServer } from "node:net";
|
|
10
|
+
import { join } from "node:path";
|
|
8
11
|
import { isWindowsNamedPipePath } from "../../shared/ipc/socket-transport.js";
|
|
9
|
-
import { getSessionProviderClosePath
|
|
12
|
+
import { getSessionProviderClosePath } from "./context.js";
|
|
10
13
|
import { readLibrettoConfig } from "./config.js";
|
|
14
|
+
import {
|
|
15
|
+
captureAuthProfileStorageState,
|
|
16
|
+
parseAuthProfileSites
|
|
17
|
+
} from "../../shared/workflow/auth-profile-state.js";
|
|
18
|
+
import {
|
|
19
|
+
formatMissingLocalAuthProfileMessage,
|
|
20
|
+
getProfilePath,
|
|
21
|
+
hasProfile,
|
|
22
|
+
normalizeProfileName,
|
|
23
|
+
writeProfile
|
|
24
|
+
} from "./profiles.js";
|
|
11
25
|
import {
|
|
12
26
|
getCloudProviderApi,
|
|
13
27
|
getProviderStartupTimeoutMs
|
|
@@ -80,12 +94,6 @@ function normalizeUrl(url) {
|
|
|
80
94
|
function normalizeDomain(url) {
|
|
81
95
|
return url.hostname.replace(/^www\./, "");
|
|
82
96
|
}
|
|
83
|
-
function getProfilePath(domain) {
|
|
84
|
-
return join(PROFILES_DIR, `${domain}.json`);
|
|
85
|
-
}
|
|
86
|
-
function hasProfile(domain) {
|
|
87
|
-
return existsSync(getProfilePath(domain));
|
|
88
|
-
}
|
|
89
97
|
async function tryConnectToCDP(endpoint, logger, timeoutMs = 5e3) {
|
|
90
98
|
logger.info("cdp-connect-attempt", { endpoint, timeoutMs });
|
|
91
99
|
try {
|
|
@@ -304,30 +312,34 @@ async function runOpen(rawUrl, headed, session, logger, options) {
|
|
|
304
312
|
const port = await pickFreePort();
|
|
305
313
|
const runLogPath = logFileForSession(session);
|
|
306
314
|
const browserMode = headed ? "headed" : "headless";
|
|
307
|
-
const
|
|
308
|
-
if (
|
|
309
|
-
const authProfilePath = getProfilePath(
|
|
310
|
-
if (!
|
|
315
|
+
const authProfileName = options?.authProfileName ? normalizeProfileName(options.authProfileName) : void 0;
|
|
316
|
+
if (authProfileName) {
|
|
317
|
+
const authProfilePath = getProfilePath(authProfileName);
|
|
318
|
+
if (!hasProfile(authProfileName)) {
|
|
311
319
|
throw new Error(
|
|
312
|
-
|
|
320
|
+
formatMissingLocalAuthProfileMessage({
|
|
321
|
+
profileName: authProfileName,
|
|
322
|
+
profilePath: authProfilePath,
|
|
323
|
+
session
|
|
324
|
+
})
|
|
313
325
|
);
|
|
314
326
|
}
|
|
315
327
|
}
|
|
316
328
|
const supportsSavedProfile = parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:";
|
|
317
|
-
const
|
|
318
|
-
const profilePath =
|
|
319
|
-
const useProfile =
|
|
329
|
+
const profileName = authProfileName ?? (supportsSavedProfile ? normalizeDomain(parsedUrl) : void 0);
|
|
330
|
+
const profilePath = profileName ? getProfilePath(profileName) : void 0;
|
|
331
|
+
const useProfile = profileName ? hasProfile(profileName) : false;
|
|
320
332
|
logger.info("open-launching", {
|
|
321
333
|
url,
|
|
322
334
|
mode: browserMode,
|
|
323
335
|
session,
|
|
324
336
|
port,
|
|
325
|
-
|
|
337
|
+
profileName,
|
|
326
338
|
useProfile,
|
|
327
339
|
profilePath: useProfile ? profilePath : void 0
|
|
328
340
|
});
|
|
329
341
|
if (useProfile) {
|
|
330
|
-
console.log(`Loading saved profile
|
|
342
|
+
console.log(`Loading saved profile ${profileName}`);
|
|
331
343
|
}
|
|
332
344
|
console.log(`Launching ${browserMode} browser (session: ${session})...`);
|
|
333
345
|
const { pid, socketPath: daemonSocketPath, client } = await DaemonClient.spawn({
|
|
@@ -449,67 +461,70 @@ async function runOpenWithProvider(rawUrl, providerName, session, logger, access
|
|
|
449
461
|
});
|
|
450
462
|
console.log(`Browser open (${providerName}): ${url}`);
|
|
451
463
|
}
|
|
452
|
-
async function runSave(
|
|
453
|
-
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
464
|
+
async function runSave(profileName, session, logger, options = { sites: "" }) {
|
|
465
|
+
const normalizedProfileName = normalizeProfileName(profileName);
|
|
466
|
+
const sites = parseAuthProfileSites(options.sites);
|
|
467
|
+
if (sites.length === 0) {
|
|
468
|
+
throw new Error("Pass at least one site with --sites <site[,site]>.");
|
|
469
|
+
}
|
|
470
|
+
logger.info("save-start", { profileName: normalizedProfileName, session, sites });
|
|
471
|
+
const state = readSessionStateOrThrow(session);
|
|
472
|
+
if (!state.daemonSocketPath) {
|
|
473
|
+
throw new Error(
|
|
474
|
+
`Session "${session}" has no daemon socket. Close and reopen the session, then run libretto save again.`
|
|
462
475
|
);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return cookie;
|
|
469
|
-
});
|
|
470
|
-
await cdpSession.detach();
|
|
471
|
-
const origins = [];
|
|
472
|
-
for (const ctx of browser.contexts()) {
|
|
473
|
-
for (const pg of ctx.pages()) {
|
|
474
|
-
try {
|
|
475
|
-
const origin = new URL(pg.url()).origin;
|
|
476
|
-
const localStorage = await pg.evaluate(() => {
|
|
477
|
-
const items = [];
|
|
478
|
-
for (let i = 0; i < window.localStorage.length; i++) {
|
|
479
|
-
const key = window.localStorage.key(i);
|
|
480
|
-
if (key) {
|
|
481
|
-
items.push({
|
|
482
|
-
name: key,
|
|
483
|
-
value: window.localStorage.getItem(key) || ""
|
|
484
|
-
});
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return items;
|
|
488
|
-
});
|
|
489
|
-
if (localStorage.length > 0) {
|
|
490
|
-
origins.push({ origin, localStorage });
|
|
491
|
-
}
|
|
492
|
-
} catch {
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
const state = { cookies, origins };
|
|
497
|
-
await mkdir(dirname(profilePath), { recursive: true });
|
|
498
|
-
await writeFile(profilePath, JSON.stringify(state, null, 2));
|
|
476
|
+
}
|
|
477
|
+
const client = await DaemonClient.connect(state.daemonSocketPath);
|
|
478
|
+
try {
|
|
479
|
+
const storageState = await client.captureAuthProfileStorageState({ sites });
|
|
480
|
+
const profilePath = await writeProfile(normalizedProfileName, storageState);
|
|
499
481
|
logger.info("save-success", {
|
|
500
|
-
|
|
482
|
+
profileName: normalizedProfileName,
|
|
483
|
+
sites,
|
|
501
484
|
profilePath,
|
|
502
|
-
cookieCount: cookies
|
|
503
|
-
originCount: origins
|
|
485
|
+
cookieCount: storageState.cookies?.length ?? 0,
|
|
486
|
+
originCount: storageState.origins?.length ?? 0
|
|
504
487
|
});
|
|
505
|
-
console.log(`Profile saved
|
|
488
|
+
console.log(`Profile saved: ${normalizedProfileName}`);
|
|
506
489
|
console.log(` Location: ${profilePath}`);
|
|
507
|
-
console.log(`
|
|
490
|
+
console.log(` Sites: ${sites.join(", ")}`);
|
|
491
|
+
console.log(
|
|
492
|
+
` Cookies: ${storageState.cookies?.length ?? 0}, Origins: ${storageState.origins?.length ?? 0}`
|
|
493
|
+
);
|
|
508
494
|
} catch (err) {
|
|
509
|
-
logger.error("save-error", { error: err,
|
|
495
|
+
logger.error("save-error", { error: err, profileName, session, sites });
|
|
510
496
|
throw err;
|
|
511
497
|
} finally {
|
|
512
|
-
|
|
498
|
+
client.destroy();
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
async function runFetchChromeProfile(profileName, cdpUrl, logger, options) {
|
|
502
|
+
const normalizedProfileName = normalizeProfileName(profileName);
|
|
503
|
+
const sites = parseAuthProfileSites(options.sites);
|
|
504
|
+
if (sites.length === 0) {
|
|
505
|
+
throw new Error("Pass at least one site with --sites <site[,site]>.");
|
|
506
|
+
}
|
|
507
|
+
logger.info("fetch-chrome-profile-start", {
|
|
508
|
+
profileName: normalizedProfileName,
|
|
509
|
+
cdpUrl,
|
|
510
|
+
sites
|
|
511
|
+
});
|
|
512
|
+
const browser = await chromium.connectOverCDP(cdpUrl);
|
|
513
|
+
try {
|
|
514
|
+
const context = browser.contexts()[0];
|
|
515
|
+
if (!context) {
|
|
516
|
+
throw new Error("Connected Chrome instance has no browser context.");
|
|
517
|
+
}
|
|
518
|
+
const state = await captureAuthProfileStorageState(context, sites);
|
|
519
|
+
const profilePath = await writeProfile(normalizedProfileName, state);
|
|
520
|
+
console.log(`Profile fetched: ${normalizedProfileName}`);
|
|
521
|
+
console.log(` Location: ${profilePath}`);
|
|
522
|
+
console.log(` Sites: ${sites.join(", ")}`);
|
|
523
|
+
console.log(
|
|
524
|
+
` Cookies: ${state.cookies?.length ?? 0}, Origins: ${state.origins?.length ?? 0}`
|
|
525
|
+
);
|
|
526
|
+
} finally {
|
|
527
|
+
disconnectBrowser(browser, logger);
|
|
513
528
|
}
|
|
514
529
|
}
|
|
515
530
|
async function runClose(session, logger) {
|
|
@@ -1007,9 +1022,7 @@ function getScreenshotBaseName(title) {
|
|
|
1007
1022
|
export {
|
|
1008
1023
|
connect,
|
|
1009
1024
|
disconnectBrowser,
|
|
1010
|
-
getProfilePath,
|
|
1011
1025
|
getScreenshotBaseName,
|
|
1012
|
-
hasProfile,
|
|
1013
1026
|
normalizeDomain,
|
|
1014
1027
|
normalizeUrl,
|
|
1015
1028
|
resolvePath,
|
|
@@ -1017,6 +1030,7 @@ export {
|
|
|
1017
1030
|
runClose,
|
|
1018
1031
|
runCloseAll,
|
|
1019
1032
|
runConnect,
|
|
1033
|
+
runFetchChromeProfile,
|
|
1020
1034
|
runOpen,
|
|
1021
1035
|
runOpenWithProvider,
|
|
1022
1036
|
runPages,
|
|
@@ -23,13 +23,13 @@ import {
|
|
|
23
23
|
import {
|
|
24
24
|
getDaemonSocketPath
|
|
25
25
|
} from "./ipc.js";
|
|
26
|
-
import { wrapPageForActionLogging } from "../
|
|
26
|
+
import { wrapPageForActionLogging } from "../session-logs.js";
|
|
27
27
|
import {
|
|
28
|
+
formatMissingLocalAuthProfileMessage,
|
|
28
29
|
getProfilePath,
|
|
29
30
|
hasProfile,
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
} from "../browser.js";
|
|
31
|
+
normalizeProfileName
|
|
32
|
+
} from "../profiles.js";
|
|
33
33
|
import { handlePages } from "./pages.js";
|
|
34
34
|
import { handleExec, handleReadonlyExec } from "./exec.js";
|
|
35
35
|
import { DaemonExecRepl } from "./exec-repl.js";
|
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
} from "../workflow-runtime.js";
|
|
49
49
|
import { WorkflowController } from "../workflow-runner/runner.js";
|
|
50
50
|
import { validateWorkflowInput } from "../../../shared/workflow/workflow.js";
|
|
51
|
+
import { captureAuthProfileStorageState } from "../../../shared/workflow/auth-profile-state.js";
|
|
51
52
|
function isOperationalPage(page) {
|
|
52
53
|
const url = page.url();
|
|
53
54
|
return !url.startsWith("devtools://") && !url.startsWith("chrome-error://");
|
|
@@ -68,26 +69,14 @@ class UserFacingStartupError extends Error {
|
|
|
68
69
|
this.name = "UserFacingStartupError";
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
|
-
function getMissingLocalAuthProfileError(args) {
|
|
72
|
-
return [
|
|
73
|
-
`Local auth profile not found for domain "${args.normalizedDomain}".`,
|
|
74
|
-
`Expected profile file: ${args.profilePath}`,
|
|
75
|
-
"To create it:",
|
|
76
|
-
` 1. libretto open https://${args.normalizedDomain} --headed --session ${args.session}`,
|
|
77
|
-
" 2. Log in manually in the browser window.",
|
|
78
|
-
` 3. libretto save ${args.normalizedDomain} --session ${args.session}`
|
|
79
|
-
].join("\n");
|
|
80
|
-
}
|
|
81
72
|
function resolveAuthProfileStorageStatePath(args) {
|
|
82
|
-
if (!args.
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
)
|
|
86
|
-
const profilePath = getProfilePath(normalizedDomain);
|
|
87
|
-
if (!hasProfile(normalizedDomain)) {
|
|
73
|
+
if (!args.authProfileName) return void 0;
|
|
74
|
+
const profileName = normalizeProfileName(args.authProfileName);
|
|
75
|
+
const profilePath = getProfilePath(profileName);
|
|
76
|
+
if (!hasProfile(profileName)) {
|
|
88
77
|
throw new UserFacingStartupError(
|
|
89
|
-
|
|
90
|
-
|
|
78
|
+
formatMissingLocalAuthProfileMessage({
|
|
79
|
+
profileName,
|
|
91
80
|
profilePath,
|
|
92
81
|
session: args.session
|
|
93
82
|
})
|
|
@@ -253,6 +242,10 @@ class BrowserDaemon {
|
|
|
253
242
|
// ── Launch mode ────────────────────────────────────────────────────
|
|
254
243
|
static async launchBrowser(args) {
|
|
255
244
|
const { session, browser: config } = args;
|
|
245
|
+
const storageStatePath = config.storageStatePath ?? resolveAuthProfileStorageStatePath({
|
|
246
|
+
authProfileName: args.workflow?.authProfileName,
|
|
247
|
+
session
|
|
248
|
+
});
|
|
256
249
|
const windowPositionArg = config.windowPosition ? `--window-position=${config.windowPosition.x},${config.windowPosition.y}` : void 0;
|
|
257
250
|
const browser = await chromium.launch({
|
|
258
251
|
headless: !config.headed,
|
|
@@ -264,10 +257,6 @@ class BrowserDaemon {
|
|
|
264
257
|
...windowPositionArg ? [windowPositionArg] : []
|
|
265
258
|
]
|
|
266
259
|
});
|
|
267
|
-
const storageStatePath = config.storageStatePath ?? resolveAuthProfileStorageStatePath({
|
|
268
|
-
authProfileDomain: args.workflow?.authProfileDomain,
|
|
269
|
-
session
|
|
270
|
-
});
|
|
271
260
|
const context = await browser.newContext({
|
|
272
261
|
...storageStatePath ? { storageState: storageStatePath } : {},
|
|
273
262
|
viewport: {
|
|
@@ -331,7 +320,10 @@ class BrowserDaemon {
|
|
|
331
320
|
getProviderSession: () => providerSession
|
|
332
321
|
});
|
|
333
322
|
try {
|
|
334
|
-
providerSession = await provider.createSession(
|
|
323
|
+
providerSession = await provider.createSession({
|
|
324
|
+
authProfileName: config.authProfileName,
|
|
325
|
+
authProfilePersist: config.authProfilePersist
|
|
326
|
+
});
|
|
335
327
|
const browser = await chromium.connectOverCDP(
|
|
336
328
|
providerSession.cdpEndpoint
|
|
337
329
|
);
|
|
@@ -471,6 +463,9 @@ class BrowserDaemon {
|
|
|
471
463
|
pages: () => this.withRequestTimeout(() => handlePages(this.pageById, this.page)),
|
|
472
464
|
exec: (args) => this.runExec(args),
|
|
473
465
|
readonlyExec: (args) => this.runReadonlyExec(args),
|
|
466
|
+
captureAuthProfileStorageState: (args) => this.withRequestTimeout(
|
|
467
|
+
() => captureAuthProfileStorageState(this.context, args.sites)
|
|
468
|
+
),
|
|
474
469
|
snapshot: (args) => this.runSnapshot(args),
|
|
475
470
|
getWorkflowStatus: () => this.getWorkflowStatus(),
|
|
476
471
|
resumeWorkflow: () => this.resumeWorkflow(),
|
|
@@ -584,6 +579,7 @@ class BrowserDaemon {
|
|
|
584
579
|
page: this.page,
|
|
585
580
|
context: this.context,
|
|
586
581
|
logger: this.logger,
|
|
582
|
+
refreshLocalAuthProfiles: !this.externallyManaged,
|
|
587
583
|
onLog: (event) => {
|
|
588
584
|
void this.broadcast("workflowOutput", event);
|
|
589
585
|
},
|
|
@@ -680,36 +676,52 @@ async function main() {
|
|
|
680
676
|
const config = JSON.parse(process.argv[2]);
|
|
681
677
|
const headed = config.browser.kind === "launch" ? config.browser.headed : false;
|
|
682
678
|
let loadedWorkflow;
|
|
679
|
+
let workflowConfig = config.workflow;
|
|
680
|
+
let browserConfig = config.browser;
|
|
683
681
|
if (config.workflow) {
|
|
684
682
|
try {
|
|
685
683
|
loadedWorkflow = await loadDefaultWorkflow(
|
|
686
684
|
getAbsoluteIntegrationPath(config.workflow.integrationPath)
|
|
687
685
|
);
|
|
688
686
|
validateWorkflowInput(loadedWorkflow, config.workflow.params ?? {});
|
|
687
|
+
const authProfileName = loadedWorkflow.authProfileName;
|
|
688
|
+
const authProfilePersist = loadedWorkflow.authProfileRefresh === true;
|
|
689
|
+
workflowConfig = {
|
|
690
|
+
...config.workflow,
|
|
691
|
+
authProfileName,
|
|
692
|
+
authProfilePersist
|
|
693
|
+
};
|
|
694
|
+
if (config.browser.kind === "provider") {
|
|
695
|
+
browserConfig = {
|
|
696
|
+
...config.browser,
|
|
697
|
+
authProfileName,
|
|
698
|
+
authProfilePersist
|
|
699
|
+
};
|
|
700
|
+
}
|
|
689
701
|
} catch (error) {
|
|
690
702
|
throw new UserFacingStartupError(
|
|
691
703
|
error instanceof Error ? error.message : String(error)
|
|
692
704
|
);
|
|
693
705
|
}
|
|
694
706
|
}
|
|
695
|
-
const daemon =
|
|
707
|
+
const daemon = browserConfig.kind === "provider" ? await BrowserDaemon.connectToProvider({
|
|
696
708
|
session: config.session,
|
|
697
709
|
experiments: config.experiments,
|
|
698
|
-
browser:
|
|
699
|
-
}) :
|
|
710
|
+
browser: browserConfig
|
|
711
|
+
}) : browserConfig.kind === "connect" ? await BrowserDaemon.connectToEndpoint({
|
|
700
712
|
session: config.session,
|
|
701
713
|
experiments: config.experiments,
|
|
702
|
-
browser:
|
|
714
|
+
browser: browserConfig
|
|
703
715
|
}) : await BrowserDaemon.launchBrowser({
|
|
704
716
|
session: config.session,
|
|
705
717
|
experiments: config.experiments,
|
|
706
|
-
browser:
|
|
707
|
-
workflow:
|
|
718
|
+
browser: browserConfig,
|
|
719
|
+
workflow: workflowConfig
|
|
708
720
|
});
|
|
709
|
-
if (
|
|
721
|
+
if (workflowConfig) {
|
|
710
722
|
void waitForSessionState(config.session).then(
|
|
711
723
|
() => daemon.startWorkflow({
|
|
712
|
-
workflow:
|
|
724
|
+
workflow: workflowConfig,
|
|
713
725
|
headed,
|
|
714
726
|
loadedWorkflow
|
|
715
727
|
})
|
|
@@ -232,6 +232,9 @@ class DaemonClient {
|
|
|
232
232
|
async readonlyExec(args) {
|
|
233
233
|
return this.ipc.call.readonlyExec(args);
|
|
234
234
|
}
|
|
235
|
+
async captureAuthProfileStorageState(args) {
|
|
236
|
+
return this.ipc.call.captureAuthProfileStorageState(args);
|
|
237
|
+
}
|
|
235
238
|
async snapshot(args = {}) {
|
|
236
239
|
return this.ipc.call.snapshot(args);
|
|
237
240
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
getWorkflowsFromModuleExports,
|
|
21
21
|
LIBRETTO_WORKFLOW_BRAND
|
|
22
22
|
} from "../../shared/workflow/workflow.js";
|
|
23
|
+
import { normalizeCredentialNames } from "../../shared/workflow/credentials.js";
|
|
23
24
|
const DEFAULT_RUNTIME_EXTERNALS = [
|
|
24
25
|
"libretto",
|
|
25
26
|
"playwright",
|
|
@@ -27,6 +28,7 @@ const DEFAULT_RUNTIME_EXTERNALS = [
|
|
|
27
28
|
"chromium-bidi"
|
|
28
29
|
];
|
|
29
30
|
const BUILT_IN_MANIFEST_DEPENDENCIES = ["libretto"];
|
|
31
|
+
const DEPLOY_METADATA_FILENAME = ".libretto-workflows.json";
|
|
30
32
|
const SOURCE_FILE_EXTENSIONS = [
|
|
31
33
|
"",
|
|
32
34
|
".ts",
|
|
@@ -430,6 +432,12 @@ function writeDeployManifest(args) {
|
|
|
430
432
|
) + "\n"
|
|
431
433
|
);
|
|
432
434
|
}
|
|
435
|
+
function writeDeployMetadata(args) {
|
|
436
|
+
writeFileSync(
|
|
437
|
+
join(args.outputDir, DEPLOY_METADATA_FILENAME),
|
|
438
|
+
JSON.stringify({ workflows: args.workflows }, null, 2) + "\n"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
433
441
|
function shouldVendorCurrentLibretto(versionSpec) {
|
|
434
442
|
return versionSpec.startsWith("file:") || versionSpec.startsWith("link:") || versionSpec.startsWith("workspace:") || versionSpec.startsWith("portal:") || versionSpec.includes("&path:");
|
|
435
443
|
}
|
|
@@ -498,11 +506,15 @@ function createExternalDiscoveryStub() {
|
|
|
498
506
|
}
|
|
499
507
|
});
|
|
500
508
|
}
|
|
501
|
-
function createDiscoveryLibrettoModule(
|
|
509
|
+
function createDiscoveryLibrettoModule(workflowsByName) {
|
|
502
510
|
const moduleShape = {
|
|
503
511
|
LIBRETTO_WORKFLOW_BRAND,
|
|
504
|
-
workflow: (name) => {
|
|
505
|
-
|
|
512
|
+
workflow: (name, definitionOrHandler) => {
|
|
513
|
+
workflowsByName.set(name, {
|
|
514
|
+
name,
|
|
515
|
+
...extractDiscoveryCredentialMetadata(definitionOrHandler),
|
|
516
|
+
...extractDiscoveryAuthProfileMetadata(definitionOrHandler)
|
|
517
|
+
});
|
|
506
518
|
return {
|
|
507
519
|
[LIBRETTO_WORKFLOW_BRAND]: true,
|
|
508
520
|
name,
|
|
@@ -524,14 +536,37 @@ function createDiscoveryLibrettoModule(workflowNames) {
|
|
|
524
536
|
}
|
|
525
537
|
});
|
|
526
538
|
}
|
|
527
|
-
function
|
|
539
|
+
function extractDiscoveryCredentialMetadata(definitionOrHandler) {
|
|
540
|
+
if (!definitionOrHandler || typeof definitionOrHandler !== "object" || !("credentials" in definitionOrHandler)) {
|
|
541
|
+
return { credentialNames: [] };
|
|
542
|
+
}
|
|
543
|
+
const rawCredentials = definitionOrHandler.credentials;
|
|
544
|
+
return {
|
|
545
|
+
credentialNames: Array.isArray(rawCredentials) ? normalizeCredentialNames(rawCredentials) : []
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function extractDiscoveryAuthProfileMetadata(definitionOrHandler) {
|
|
549
|
+
if (!definitionOrHandler || typeof definitionOrHandler !== "object" || !("authProfile" in definitionOrHandler)) {
|
|
550
|
+
return {};
|
|
551
|
+
}
|
|
552
|
+
const authProfile = definitionOrHandler.authProfile;
|
|
553
|
+
if (typeof authProfile === "string") return { authProfileName: authProfile };
|
|
554
|
+
if (!authProfile || typeof authProfile !== "object") return {};
|
|
555
|
+
const record = authProfile;
|
|
556
|
+
if (typeof record.name !== "string") return {};
|
|
557
|
+
return {
|
|
558
|
+
authProfileName: record.name,
|
|
559
|
+
...typeof record.refresh === "boolean" ? { authProfileRefresh: record.refresh } : {}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
function discoverBundledWorkflows(args) {
|
|
528
563
|
const discoveryPath = join(
|
|
529
564
|
args.absSourceDir,
|
|
530
565
|
`.libretto-deploy-discovery-${process.pid}-${Date.now()}.cjs`
|
|
531
566
|
);
|
|
532
567
|
const originalRequire = Module.prototype.require;
|
|
533
|
-
const
|
|
534
|
-
const discoveryLibrettoModule = createDiscoveryLibrettoModule(
|
|
568
|
+
const workflowsByName = /* @__PURE__ */ new Map();
|
|
569
|
+
const discoveryLibrettoModule = createDiscoveryLibrettoModule(workflowsByName);
|
|
535
570
|
let loadedModuleExports = null;
|
|
536
571
|
try {
|
|
537
572
|
writeFileSync(discoveryPath, args.bundleBuffer);
|
|
@@ -556,10 +591,10 @@ ${formatBuildError(error)}`
|
|
|
556
591
|
delete require2.cache?.[discoveryPath];
|
|
557
592
|
rmSync(discoveryPath, { force: true });
|
|
558
593
|
}
|
|
559
|
-
const
|
|
560
|
-
(left, right) => left.localeCompare(right)
|
|
594
|
+
const discoveredWorkflows = [...workflowsByName.values()].sort(
|
|
595
|
+
(left, right) => left.name.localeCompare(right.name)
|
|
561
596
|
);
|
|
562
|
-
if (
|
|
597
|
+
if (discoveredWorkflows.length === 0) {
|
|
563
598
|
throw new Error(
|
|
564
599
|
`No workflows were found in ${args.absEntryPoint}. Import the workflow files you want to deploy from the entry point, or export a workflow directly from it.`
|
|
565
600
|
);
|
|
@@ -569,15 +604,15 @@ ${formatBuildError(error)}`
|
|
|
569
604
|
(workflow) => workflow.name
|
|
570
605
|
)
|
|
571
606
|
);
|
|
572
|
-
const nonExportedWorkflowNames =
|
|
573
|
-
(
|
|
607
|
+
const nonExportedWorkflowNames = discoveredWorkflows.filter(
|
|
608
|
+
(workflow) => !exportedWorkflowNames.has(workflow.name)
|
|
574
609
|
);
|
|
575
610
|
if (nonExportedWorkflowNames.length > 0) {
|
|
576
611
|
throw new Error(
|
|
577
|
-
`Workflows discovered in ${args.absEntryPoint} must be exported from the deploy entry point. Re-export them from the entry point or export them through a \`workflows\` object. Non-exported workflows: ${nonExportedWorkflowNames.join(", ")}`
|
|
612
|
+
`Workflows discovered in ${args.absEntryPoint} must be exported from the deploy entry point. Re-export them from the entry point or export them through a \`workflows\` object. Non-exported workflows: ${nonExportedWorkflowNames.map((workflow) => workflow.name).join(", ")}`
|
|
578
613
|
);
|
|
579
614
|
}
|
|
580
|
-
return
|
|
615
|
+
return discoveredWorkflows;
|
|
581
616
|
}
|
|
582
617
|
function createBootstrapSource(args) {
|
|
583
618
|
const bundleHash = createHash("sha256").update(args.bundleBuffer).digest("hex").slice(0, 16);
|
|
@@ -585,8 +620,12 @@ function createBootstrapSource(args) {
|
|
|
585
620
|
"base64"
|
|
586
621
|
);
|
|
587
622
|
const outputPrefix = `${normalizePackageName(args.deploymentName)}-`;
|
|
588
|
-
const exportLines = args.
|
|
589
|
-
(
|
|
623
|
+
const exportLines = args.workflows.map(
|
|
624
|
+
(workflow, index) => `export const ${getGeneratedWorkflowExportName(index)} = createWorkflowProxy(${JSON.stringify(workflow.name)}, ${JSON.stringify({
|
|
625
|
+
credentialNames: workflow.credentialNames,
|
|
626
|
+
authProfileName: workflow.authProfileName,
|
|
627
|
+
authProfileRefresh: workflow.authProfileRefresh
|
|
628
|
+
})});`
|
|
590
629
|
).join("\n");
|
|
591
630
|
return `import { createRequire } from "node:module";
|
|
592
631
|
import { existsSync, writeFileSync } from "node:fs";
|
|
@@ -616,8 +655,8 @@ function ensureBundleFile() {
|
|
|
616
655
|
return BUNDLE_FILENAME;
|
|
617
656
|
}
|
|
618
657
|
|
|
619
|
-
function createWorkflowProxy(workflowName) {
|
|
620
|
-
|
|
658
|
+
function createWorkflowProxy(workflowName, metadata) {
|
|
659
|
+
const handler = async (ctx, input) => {
|
|
621
660
|
const impl = nativeRequire(ensureBundleFile());
|
|
622
661
|
const target = getWorkflowFromModuleExports(impl, workflowName);
|
|
623
662
|
if (!target || typeof target.run !== "function") {
|
|
@@ -626,6 +665,26 @@ function createWorkflowProxy(workflowName) {
|
|
|
626
665
|
);
|
|
627
666
|
}
|
|
628
667
|
return await target.run(ctx, input);
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
if (!metadata?.authProfileName) {
|
|
671
|
+
return workflow(workflowName, {
|
|
672
|
+
credentials: Array.isArray(metadata?.credentialNames)
|
|
673
|
+
? metadata.credentialNames
|
|
674
|
+
: [],
|
|
675
|
+
handler,
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return workflow(workflowName, {
|
|
680
|
+
credentials: Array.isArray(metadata.credentialNames)
|
|
681
|
+
? metadata.credentialNames
|
|
682
|
+
: [],
|
|
683
|
+
authProfile: {
|
|
684
|
+
name: metadata.authProfileName,
|
|
685
|
+
...(typeof metadata.authProfileRefresh === "boolean" ? { refresh: metadata.authProfileRefresh } : {}),
|
|
686
|
+
},
|
|
687
|
+
handler,
|
|
629
688
|
});
|
|
630
689
|
}
|
|
631
690
|
|
|
@@ -657,7 +716,7 @@ async function writeBundledDeployEntrypoint(args) {
|
|
|
657
716
|
"Bundler did not produce a deployment implementation file."
|
|
658
717
|
);
|
|
659
718
|
}
|
|
660
|
-
const
|
|
719
|
+
const workflows = discoverBundledWorkflows({
|
|
661
720
|
absEntryPoint: args.absEntryPoint,
|
|
662
721
|
absSourceDir: args.absSourceDir,
|
|
663
722
|
bundleBuffer: Buffer.from(bundledImplementation.contents),
|
|
@@ -668,9 +727,10 @@ async function writeBundledDeployEntrypoint(args) {
|
|
|
668
727
|
createBootstrapSource({
|
|
669
728
|
bundleBuffer: Buffer.from(bundledImplementation.contents),
|
|
670
729
|
deploymentName: args.deploymentName,
|
|
671
|
-
|
|
730
|
+
workflows
|
|
672
731
|
})
|
|
673
732
|
);
|
|
733
|
+
return workflows;
|
|
674
734
|
} catch (error) {
|
|
675
735
|
throw new Error(
|
|
676
736
|
`Failed to bundle deploy entry point ${args.absEntryPoint}.
|
|
@@ -694,7 +754,7 @@ async function createHostedDeployPackage(args) {
|
|
|
694
754
|
const workspacePackages = discoverWorkspacePackages(absSourceDir);
|
|
695
755
|
let callerOwnsTempRoot = false;
|
|
696
756
|
try {
|
|
697
|
-
await writeBundledDeployEntrypoint({
|
|
757
|
+
const workflows = await writeBundledDeployEntrypoint({
|
|
698
758
|
absEntryPoint,
|
|
699
759
|
absSourceDir,
|
|
700
760
|
deploymentName: args.deploymentName,
|
|
@@ -712,13 +772,15 @@ async function createHostedDeployPackage(args) {
|
|
|
712
772
|
outputDir,
|
|
713
773
|
sourceDir: absSourceDir
|
|
714
774
|
});
|
|
775
|
+
writeDeployMetadata({ outputDir, workflows });
|
|
715
776
|
callerOwnsTempRoot = true;
|
|
716
777
|
return {
|
|
717
778
|
cleanup: () => {
|
|
718
779
|
rmSync(tempRoot, { force: true, recursive: true });
|
|
719
780
|
},
|
|
720
781
|
entryPoint: "index.js",
|
|
721
|
-
outputDir
|
|
782
|
+
outputDir,
|
|
783
|
+
workflows
|
|
722
784
|
};
|
|
723
785
|
} finally {
|
|
724
786
|
if (!callerOwnsTempRoot) {
|
|
@@ -735,7 +797,8 @@ async function buildHostedDeployTarball(args) {
|
|
|
735
797
|
});
|
|
736
798
|
return {
|
|
737
799
|
entryPoint: deployPackage.entryPoint,
|
|
738
|
-
source: readFileSync(tarPath).toString("base64")
|
|
800
|
+
source: readFileSync(tarPath).toString("base64"),
|
|
801
|
+
workflows: deployPackage.workflows
|
|
739
802
|
};
|
|
740
803
|
} finally {
|
|
741
804
|
deployPackage.cleanup();
|