libretto 0.6.26 → 0.6.28
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/commands/cloud-sharing.js +93 -0
- package/dist/cli/core/deploy-artifact.js +65 -10
- package/dist/cli/router.js +4 -1
- package/docs/releasing.md +9 -6
- package/package.json +5 -2
- package/skills/libretto/SKILL.md +3 -12
- package/skills/libretto/references/shipped-source-and-documentation.md +13 -0
- package/skills/libretto-readonly/SKILL.md +1 -1
- package/src/cli/commands/cloud-sharing.ts +114 -0
- package/src/cli/core/deploy-artifact.ts +95 -11
- package/src/cli/router.ts +3 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SimpleCLI } from "affordance";
|
|
3
|
+
import { orpcCall, resolveApiUrl } from "../core/auth-fetch.js";
|
|
4
|
+
function requireCloudApiKey() {
|
|
5
|
+
const apiKey = process.env.LIBRETTO_API_KEY?.trim();
|
|
6
|
+
if (!apiKey) {
|
|
7
|
+
throw new Error(
|
|
8
|
+
"LIBRETTO_API_KEY is required to share Libretto Cloud workflow code. Issue one with `libretto cloud auth api-key issue --label <label>`."
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
apiUrl: resolveApiUrl(null),
|
|
13
|
+
credential: { source: "env-api-key", apiKey }
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const shareWorkflowCommand = SimpleCLI.command({
|
|
17
|
+
description: "Share one hosted workflow's code publicly"
|
|
18
|
+
}).input(SimpleCLI.input({
|
|
19
|
+
positionals: [
|
|
20
|
+
SimpleCLI.positional("workflow", z.string().min(1), {
|
|
21
|
+
help: "Hosted workflow name to share"
|
|
22
|
+
})
|
|
23
|
+
],
|
|
24
|
+
named: {
|
|
25
|
+
refresh: SimpleCLI.flag({
|
|
26
|
+
help: "Refresh an existing share from the workflow's current deployment"
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
})).handle(async ({ input }) => {
|
|
30
|
+
const { apiUrl, credential } = requireCloudApiKey();
|
|
31
|
+
const response = await orpcCall({
|
|
32
|
+
apiUrl,
|
|
33
|
+
path: "/v1/workflows/share",
|
|
34
|
+
input: { workflow: input.workflow, refresh: input.refresh },
|
|
35
|
+
credential
|
|
36
|
+
});
|
|
37
|
+
if (response.status === "existing") {
|
|
38
|
+
console.log(`Workflow is already shared: ${response.workflow}`);
|
|
39
|
+
console.log("Use --refresh to update the shared code from the current deployment.");
|
|
40
|
+
} else if (response.status === "refreshed") {
|
|
41
|
+
console.log(`Refreshed shared workflow: ${response.workflow}`);
|
|
42
|
+
} else {
|
|
43
|
+
console.log(`Shared workflow: ${response.workflow}`);
|
|
44
|
+
}
|
|
45
|
+
console.log(`Marketplace URL: ${response.marketplace_url}`);
|
|
46
|
+
console.log(`Code URL: ${response.code_url}`);
|
|
47
|
+
return response.marketplace_url;
|
|
48
|
+
});
|
|
49
|
+
const codeSharingStatusCommand = SimpleCLI.command({
|
|
50
|
+
description: "Show whether tenant code sharing is enabled"
|
|
51
|
+
}).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => {
|
|
52
|
+
const { apiUrl, credential } = requireCloudApiKey();
|
|
53
|
+
const response = await orpcCall({
|
|
54
|
+
apiUrl,
|
|
55
|
+
path: "/v1/tenant/codeSharing",
|
|
56
|
+
input: {},
|
|
57
|
+
credential
|
|
58
|
+
});
|
|
59
|
+
console.log(`Code sharing: ${response.enabled ? "enabled" : "disabled"}`);
|
|
60
|
+
return response.enabled;
|
|
61
|
+
});
|
|
62
|
+
async function updateCodeSharing(enabled) {
|
|
63
|
+
const { apiUrl, credential } = requireCloudApiKey();
|
|
64
|
+
const response = await orpcCall({
|
|
65
|
+
apiUrl,
|
|
66
|
+
path: "/v1/tenant/updateCodeSharing",
|
|
67
|
+
input: { enabled },
|
|
68
|
+
credential
|
|
69
|
+
});
|
|
70
|
+
console.log(`Code sharing: ${response.enabled ? "enabled" : "disabled"}`);
|
|
71
|
+
return response.enabled;
|
|
72
|
+
}
|
|
73
|
+
const enableCodeSharingCommand = SimpleCLI.command({
|
|
74
|
+
description: "Enable public workflow code sharing for this tenant"
|
|
75
|
+
}).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => updateCodeSharing(true));
|
|
76
|
+
const disableCodeSharingCommand = SimpleCLI.command({
|
|
77
|
+
description: "Disable public workflow code sharing for this tenant"
|
|
78
|
+
}).input(SimpleCLI.input({ positionals: [], named: {} })).handle(async () => updateCodeSharing(false));
|
|
79
|
+
const codeSharingCommands = SimpleCLI.group({
|
|
80
|
+
description: "Manage tenant workflow code sharing",
|
|
81
|
+
routes: {
|
|
82
|
+
status: codeSharingStatusCommand,
|
|
83
|
+
enable: enableCodeSharingCommand,
|
|
84
|
+
disable: disableCodeSharingCommand
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
export {
|
|
88
|
+
codeSharingCommands,
|
|
89
|
+
codeSharingStatusCommand,
|
|
90
|
+
disableCodeSharingCommand,
|
|
91
|
+
enableCodeSharingCommand,
|
|
92
|
+
shareWorkflowCommand
|
|
93
|
+
};
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import { createRequire, Module } from "node:module";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
15
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { gzipSync } from "node:zlib";
|
|
18
18
|
import { build } from "esbuild";
|
|
@@ -337,7 +337,7 @@ function resolveWorkspaceSourcePath(info, subpath) {
|
|
|
337
337
|
const directSubpath = subpath === "." ? "index" : subpath.slice(2);
|
|
338
338
|
return resolvePathCandidates(info.dir, directSubpath);
|
|
339
339
|
}
|
|
340
|
-
function workspaceSourcePlugin(workspacePackages, externalPackages) {
|
|
340
|
+
function workspaceSourcePlugin(workspacePackages, externalPackages, unresolvedWorkspaceImports) {
|
|
341
341
|
return {
|
|
342
342
|
name: "workspace-source-resolver",
|
|
343
343
|
setup(buildApi) {
|
|
@@ -357,9 +357,8 @@ function workspaceSourcePlugin(workspacePackages, externalPackages) {
|
|
|
357
357
|
match.subpath
|
|
358
358
|
);
|
|
359
359
|
if (!resolvedPath) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
);
|
|
360
|
+
unresolvedWorkspaceImports?.set(args.path, match.info.dir);
|
|
361
|
+
return { path: args.path, external: true };
|
|
363
362
|
}
|
|
364
363
|
return { path: resolvedPath };
|
|
365
364
|
});
|
|
@@ -438,6 +437,29 @@ function writeDeployMetadata(args) {
|
|
|
438
437
|
JSON.stringify({ workflows: args.workflows }, null, 2) + "\n"
|
|
439
438
|
);
|
|
440
439
|
}
|
|
440
|
+
function toPortableRelativePath(args) {
|
|
441
|
+
const relPath = relative(args.absSourceDir, args.absPath).replaceAll("\\", "/");
|
|
442
|
+
if (relPath.startsWith("../") || relPath === ".." || relPath.startsWith("/")) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Deploy entry point must be inside the source directory to support cloud code sharing: ${args.absPath}`
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
return relPath;
|
|
448
|
+
}
|
|
449
|
+
function writeShareableSourceFiles(args) {
|
|
450
|
+
const relPaths = [...new Set(args.absSourcePaths.map(
|
|
451
|
+
(absPath) => toPortableRelativePath({
|
|
452
|
+
absPath,
|
|
453
|
+
absSourceDir: args.absSourceDir
|
|
454
|
+
})
|
|
455
|
+
))].sort();
|
|
456
|
+
for (const relPath of relPaths) {
|
|
457
|
+
const targetPath = join(args.outputDir, ".libretto-share", "source", relPath);
|
|
458
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
459
|
+
cpSync(resolve(args.absSourceDir, relPath), targetPath);
|
|
460
|
+
}
|
|
461
|
+
return relPaths;
|
|
462
|
+
}
|
|
441
463
|
function shouldVendorCurrentLibretto(versionSpec) {
|
|
442
464
|
return versionSpec.startsWith("file:") || versionSpec.startsWith("link:") || versionSpec.startsWith("workspace:") || versionSpec.startsWith("portal:") || versionSpec.includes("&path:");
|
|
443
465
|
}
|
|
@@ -693,6 +715,7 @@ ${exportLines}
|
|
|
693
715
|
}
|
|
694
716
|
async function writeBundledDeployEntrypoint(args) {
|
|
695
717
|
try {
|
|
718
|
+
const unresolvedWorkspaceImports = /* @__PURE__ */ new Map();
|
|
696
719
|
const implementationBuild = await build({
|
|
697
720
|
absWorkingDir: args.absSourceDir,
|
|
698
721
|
bundle: true,
|
|
@@ -701,13 +724,25 @@ async function writeBundledDeployEntrypoint(args) {
|
|
|
701
724
|
format: "cjs",
|
|
702
725
|
outfile: "prebundled.cjs",
|
|
703
726
|
platform: "node",
|
|
727
|
+
metafile: true,
|
|
704
728
|
plugins: [
|
|
705
|
-
workspaceSourcePlugin(
|
|
729
|
+
workspaceSourcePlugin(
|
|
730
|
+
args.workspacePackages,
|
|
731
|
+
args.externalPackages,
|
|
732
|
+
unresolvedWorkspaceImports
|
|
733
|
+
)
|
|
706
734
|
],
|
|
707
735
|
splitting: false,
|
|
708
736
|
target: "node20",
|
|
709
737
|
write: false
|
|
710
738
|
});
|
|
739
|
+
const [unresolvedImport] = unresolvedWorkspaceImports;
|
|
740
|
+
if (unresolvedImport) {
|
|
741
|
+
const [importPath, packageDir] = unresolvedImport;
|
|
742
|
+
throw new Error(
|
|
743
|
+
`Unable to resolve workspace import "${importPath}" from ${packageDir}.`
|
|
744
|
+
);
|
|
745
|
+
}
|
|
711
746
|
const bundledImplementation = implementationBuild.outputFiles?.find(
|
|
712
747
|
(file) => file.path.endsWith("prebundled.cjs")
|
|
713
748
|
);
|
|
@@ -730,7 +765,13 @@ async function writeBundledDeployEntrypoint(args) {
|
|
|
730
765
|
workflows
|
|
731
766
|
})
|
|
732
767
|
);
|
|
733
|
-
|
|
768
|
+
const shareableSourceFiles = Object.keys(implementationBuild.metafile?.inputs ?? {}).map(
|
|
769
|
+
(inputPath) => isAbsolute(inputPath) ? resolve(inputPath) : resolve(args.absSourceDir, inputPath)
|
|
770
|
+
).filter((absPath) => {
|
|
771
|
+
const relPath = relative(args.absSourceDir, absPath);
|
|
772
|
+
return relPath !== "" && !relPath.startsWith("../") && relPath !== ".." && !isAbsolute(relPath);
|
|
773
|
+
});
|
|
774
|
+
return { shareableSourceFiles, workflows };
|
|
734
775
|
} catch (error) {
|
|
735
776
|
throw new Error(
|
|
736
777
|
`Failed to bundle deploy entry point ${args.absEntryPoint}.
|
|
@@ -754,7 +795,7 @@ async function createHostedDeployPackage(args) {
|
|
|
754
795
|
const workspacePackages = discoverWorkspacePackages(absSourceDir);
|
|
755
796
|
let callerOwnsTempRoot = false;
|
|
756
797
|
try {
|
|
757
|
-
const workflows = await writeBundledDeployEntrypoint({
|
|
798
|
+
const { shareableSourceFiles, workflows } = await writeBundledDeployEntrypoint({
|
|
758
799
|
absEntryPoint,
|
|
759
800
|
absSourceDir,
|
|
760
801
|
deploymentName: args.deploymentName,
|
|
@@ -762,6 +803,20 @@ async function createHostedDeployPackage(args) {
|
|
|
762
803
|
outputDir,
|
|
763
804
|
workspacePackages
|
|
764
805
|
});
|
|
806
|
+
const sourceFiles = writeShareableSourceFiles({
|
|
807
|
+
absSourceDir,
|
|
808
|
+
absSourcePaths: [...shareableSourceFiles, absEntryPoint],
|
|
809
|
+
outputDir
|
|
810
|
+
});
|
|
811
|
+
const sourceFile = toPortableRelativePath({
|
|
812
|
+
absPath: absEntryPoint,
|
|
813
|
+
absSourceDir
|
|
814
|
+
});
|
|
815
|
+
const workflowsWithShareableSource = workflows.map((workflow) => ({
|
|
816
|
+
...workflow,
|
|
817
|
+
sourceFile,
|
|
818
|
+
sourceFiles
|
|
819
|
+
}));
|
|
765
820
|
if (librettoDependency === "file:./libretto") {
|
|
766
821
|
copyCurrentLibrettoPackage(outputDir);
|
|
767
822
|
}
|
|
@@ -772,7 +827,7 @@ async function createHostedDeployPackage(args) {
|
|
|
772
827
|
outputDir,
|
|
773
828
|
sourceDir: absSourceDir
|
|
774
829
|
});
|
|
775
|
-
writeDeployMetadata({ outputDir, workflows });
|
|
830
|
+
writeDeployMetadata({ outputDir, workflows: workflowsWithShareableSource });
|
|
776
831
|
callerOwnsTempRoot = true;
|
|
777
832
|
return {
|
|
778
833
|
cleanup: () => {
|
|
@@ -780,7 +835,7 @@ async function createHostedDeployPackage(args) {
|
|
|
780
835
|
},
|
|
781
836
|
entryPoint: "index.js",
|
|
782
837
|
outputDir,
|
|
783
|
-
workflows
|
|
838
|
+
workflows: workflowsWithShareableSource
|
|
784
839
|
};
|
|
785
840
|
} finally {
|
|
786
841
|
if (!callerOwnsTempRoot) {
|
package/dist/cli/router.js
CHANGED
|
@@ -2,6 +2,7 @@ import { authCommands } from "./commands/auth.js";
|
|
|
2
2
|
import { billingCommands } from "./commands/billing.js";
|
|
3
3
|
import { browserCommands } from "./commands/browser.js";
|
|
4
4
|
import { cloudCredentialCommands } from "./commands/cloud-credentials.js";
|
|
5
|
+
import { codeSharingCommands, shareWorkflowCommand } from "./commands/cloud-sharing.js";
|
|
5
6
|
import { deployCommand } from "./commands/deploy.js";
|
|
6
7
|
import { executionCommands } from "./commands/execution.js";
|
|
7
8
|
import { experimentsCommand } from "./commands/experiments.js";
|
|
@@ -23,7 +24,9 @@ const cliRoutes = {
|
|
|
23
24
|
auth: authCommands,
|
|
24
25
|
billing: billingCommands,
|
|
25
26
|
credentials: cloudCredentialCommands,
|
|
26
|
-
profiles: profileCommands
|
|
27
|
+
profiles: profileCommands,
|
|
28
|
+
share: shareWorkflowCommand,
|
|
29
|
+
sharing: codeSharingCommands
|
|
27
30
|
}
|
|
28
31
|
}),
|
|
29
32
|
experiments: experimentsCommand,
|
package/docs/releasing.md
CHANGED
|
@@ -66,10 +66,13 @@ The root `scripts/prepare-release.sh` script does the following:
|
|
|
66
66
|
1. Checks that the working tree is clean.
|
|
67
67
|
2. Updates local `main` from `origin/main`.
|
|
68
68
|
3. Runs `pnpm install --frozen-lockfile`, `pnpm --filter libretto type-check`, and `pnpm --filter libretto test`.
|
|
69
|
-
4.
|
|
70
|
-
5.
|
|
71
|
-
6.
|
|
72
|
-
7.
|
|
69
|
+
4. Checks whether `packages/affordance` changed since the current Libretto release tag. If it changed, the script runs Affordance type-check/tests and bumps `packages/affordance/package.json` by one patch version when the current Affordance version is already published to npm.
|
|
70
|
+
5. Bumps the version in `packages/libretto/package.json`.
|
|
71
|
+
6. Creates a release branch.
|
|
72
|
+
7. Commits the version bump.
|
|
73
|
+
8. Pushes the branch and opens a PR to `main` with the `release` label.
|
|
74
|
+
|
|
75
|
+
Affordance is published before Libretto in `.github/workflows/release.yml`. Keeping this automatic patch bump in the release PR prevents Libretto from depending on unpublished Affordance APIs while still avoiding unnecessary Affordance releases when its package contents did not change.
|
|
73
76
|
|
|
74
77
|
Release PRs also run the eval workflow. That workflow records score, duration, token, cost, and tool-call metrics for review. Scores are informational: low scores do not fail the workflow, but setup/runtime failures and zero completed records do.
|
|
75
78
|
|
|
@@ -80,12 +83,12 @@ After the release PR merges, `.github/workflows/release.yml` runs on `main`.
|
|
|
80
83
|
The workflow:
|
|
81
84
|
|
|
82
85
|
1. Reads the version from `packages/libretto/package.json`.
|
|
83
|
-
2. Checks whether that version already exists on npm and in GitHub Releases.
|
|
86
|
+
2. Checks whether that version already exists on npm and in GitHub Releases. It also checks whether the current `packages/affordance/package.json` version and matching `create-libretto` version already exist on npm.
|
|
84
87
|
3. Runs install, type-check, and tests for the `libretto` package in a verification job.
|
|
85
88
|
4. Publishes `affordance` first, then `libretto@X.Y.Z`, then `create-libretto@X.Y.Z` with trusted publishing.
|
|
86
89
|
5. Creates GitHub release `vX.Y.Z` with generated release notes if it does not already exist.
|
|
87
90
|
|
|
88
|
-
This makes the workflow safe to re-run after partial failures. For example, if npm publish succeeds but GitHub release creation fails, a re-run will skip npm and only create the missing release.
|
|
91
|
+
This makes the workflow safe to re-run after partial failures. For example, if npm publish succeeds but GitHub release creation fails, a re-run will skip npm and only create the missing release. It also means Affordance can still be published if its version is missing even when the Libretto npm package and GitHub release for the current Libretto version already exist.
|
|
89
92
|
|
|
90
93
|
## Eval gating on release PRs
|
|
91
94
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "libretto",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.28",
|
|
4
4
|
"description": "AI-powered browser automation library and CLI built on Playwright",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://libretto.sh",
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/saffron-health/libretto"
|
|
10
10
|
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/saffron-health/libretto/issues"
|
|
13
|
+
},
|
|
11
14
|
"type": "module",
|
|
12
15
|
"publishConfig": {
|
|
13
16
|
"access": "public"
|
|
@@ -67,7 +70,7 @@
|
|
|
67
70
|
"playwright": "^1.58.2",
|
|
68
71
|
"tsx": "^4.21.0",
|
|
69
72
|
"zod": "^4.3.6",
|
|
70
|
-
"affordance": "^0.2.
|
|
73
|
+
"affordance": "^0.2.1"
|
|
71
74
|
},
|
|
72
75
|
"scripts": {
|
|
73
76
|
"sync:mirrors": "node ../dev-tools/scripts/sync-mirrors.mjs",
|
package/skills/libretto/SKILL.md
CHANGED
|
@@ -4,7 +4,7 @@ description: "Browser automation CLI for building, maintaining, and running brow
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: saffron-health
|
|
7
|
-
version: "0.6.
|
|
7
|
+
version: "0.6.28"
|
|
8
8
|
---
|
|
9
9
|
|
|
10
10
|
## How Libretto Works
|
|
@@ -15,17 +15,7 @@ metadata:
|
|
|
15
15
|
|
|
16
16
|
## Shipped Source & Documentation
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Full documentation is published at [libretto.sh](https://libretto.sh). Available pages:
|
|
21
|
-
|
|
22
|
-
- Get started: [quickstart](https://libretto.sh/docs/get-started/quickstart), [first workflow](https://libretto.sh/docs/get-started/first-workflow), [deploying](https://libretto.sh/docs/get-started/deploying)
|
|
23
|
-
- Fundamentals: [core concepts](https://libretto.sh/docs/understand-libretto/core-concepts), [how workflow generation works](https://libretto.sh/docs/understand-libretto/how-workflow-generation-works), [automation and bot detection](https://libretto.sh/docs/understand-libretto/automation-and-bot-detection), [website authentication](https://libretto.sh/docs/understand-libretto/website-authentication)
|
|
24
|
-
- Workflow guides: [one-shot generation](https://libretto.sh/docs/guides/one-shot-workflow-generation), [interactive building](https://libretto.sh/docs/guides/interactive-workflow-building), [debugging workflows](https://libretto.sh/docs/guides/debugging-workflows), [convert to network requests](https://libretto.sh/docs/guides/convert-to-network-requests)
|
|
25
|
-
- CLI reference: [open and connect](https://libretto.sh/docs/reference/cli/open-and-connect), [sessions](https://libretto.sh/docs/reference/cli/sessions), [profiles](https://libretto.sh/docs/reference/cli/profiles), [snapshot](https://libretto.sh/docs/reference/cli/snapshot), [exec](https://libretto.sh/docs/reference/cli/exec), [run and resume](https://libretto.sh/docs/reference/cli/run-and-resume), [session logs](https://libretto.sh/docs/reference/cli/session-logs), [pages](https://libretto.sh/docs/reference/cli/pages)
|
|
26
|
-
- Library API: [workflow](https://libretto.sh/docs/reference/runtime/workflow), [network requests](https://libretto.sh/docs/reference/runtime/network-requests), [file downloads](https://libretto.sh/docs/reference/runtime/file-downloads)
|
|
27
|
-
- Libretto Cloud Hosting: [overview](https://libretto.sh/docs/libretto-cloud-hosting/overview), [authentication](https://libretto.sh/docs/libretto-cloud-hosting/authentication), [deployments](https://libretto.sh/docs/libretto-cloud-hosting/deployments), [stealth](https://libretto.sh/docs/libretto-cloud-hosting/stealth)
|
|
28
|
-
- Alternative providers: [overview](https://libretto.sh/docs/alternative-providers/overview), [Kernel](https://libretto.sh/docs/alternative-providers/kernel), [Browserbase](https://libretto.sh/docs/alternative-providers/browserbase), [Steel](https://libretto.sh/docs/alternative-providers/steel), [GCP](https://libretto.sh/docs/alternative-providers/gcp), [AWS](https://libretto.sh/docs/alternative-providers/aws)
|
|
18
|
+
Read `references/shipped-source-and-documentation.md` for shipped source details, published documentation links, and implementation context beyond what this skill file covers.
|
|
29
19
|
|
|
30
20
|
## Default Integration Approach
|
|
31
21
|
|
|
@@ -48,6 +38,7 @@ Prefer to enter sites at a user-facing URL (homepage, login, etc.) on the first
|
|
|
48
38
|
- Use the package manager convention for the target project. The examples use `npx libretto`; pnpm, yarn, and bun projects should use their equivalent package-manager execution form.
|
|
49
39
|
- Use `npx libretto setup` for first-time workspace onboarding. It installs Chromium and syncs skills.
|
|
50
40
|
- Use `npx libretto status` to inspect open sessions without triggering setup.
|
|
41
|
+
- Use `npx libretto update` to upgrade the project-local Libretto package. Use `npx libretto update --dry-run` to preview the package-manager command first.
|
|
51
42
|
|
|
52
43
|
## Experiments
|
|
53
44
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Shipped Source & Documentation
|
|
2
|
+
|
|
3
|
+
The npm package includes `src/` (full TypeScript source) and `docs/` for deeper understanding of internals and design decisions. Resolve paths from the package root, such as `node_modules/libretto/`.
|
|
4
|
+
|
|
5
|
+
Full documentation is published at [libretto.sh](https://libretto.sh). Available pages:
|
|
6
|
+
|
|
7
|
+
- Get started: [quickstart](https://libretto.sh/docs/get-started/quickstart), [first workflow](https://libretto.sh/docs/get-started/first-workflow), [deploying](https://libretto.sh/docs/get-started/deploying)
|
|
8
|
+
- Fundamentals: [core concepts](https://libretto.sh/docs/understand-libretto/core-concepts), [how workflow generation works](https://libretto.sh/docs/understand-libretto/how-workflow-generation-works), [automation and bot detection](https://libretto.sh/docs/understand-libretto/automation-and-bot-detection), [website authentication](https://libretto.sh/docs/understand-libretto/website-authentication)
|
|
9
|
+
- Workflow guides: [one-shot generation](https://libretto.sh/docs/guides/one-shot-workflow-generation), [interactive building](https://libretto.sh/docs/guides/interactive-workflow-building), [debugging workflows](https://libretto.sh/docs/guides/debugging-workflows), [convert to network requests](https://libretto.sh/docs/guides/convert-to-network-requests)
|
|
10
|
+
- CLI reference: [open and connect](https://libretto.sh/docs/reference/cli/open-and-connect), [sessions](https://libretto.sh/docs/reference/cli/sessions), [profiles](https://libretto.sh/docs/reference/cli/profiles), [snapshot](https://libretto.sh/docs/reference/cli/snapshot), [exec](https://libretto.sh/docs/reference/cli/exec), [run and resume](https://libretto.sh/docs/reference/cli/run-and-resume), [session logs](https://libretto.sh/docs/reference/cli/session-logs), [pages](https://libretto.sh/docs/reference/cli/pages)
|
|
11
|
+
- Library API: [workflow](https://libretto.sh/docs/reference/runtime/workflow), [network requests](https://libretto.sh/docs/reference/runtime/network-requests), [file downloads](https://libretto.sh/docs/reference/runtime/file-downloads)
|
|
12
|
+
- Libretto Cloud Hosting: [overview](https://libretto.sh/docs/libretto-cloud-hosting/overview), [authentication](https://libretto.sh/docs/libretto-cloud-hosting/authentication), [deployments](https://libretto.sh/docs/libretto-cloud-hosting/deployments), [stealth](https://libretto.sh/docs/libretto-cloud-hosting/stealth)
|
|
13
|
+
- Alternative providers: [overview](https://libretto.sh/docs/alternative-providers/overview), [Kernel](https://libretto.sh/docs/alternative-providers/kernel), [Browserbase](https://libretto.sh/docs/alternative-providers/browserbase), [Steel](https://libretto.sh/docs/alternative-providers/steel), [GCP](https://libretto.sh/docs/alternative-providers/gcp), [AWS](https://libretto.sh/docs/alternative-providers/aws)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { SimpleCLI } from "affordance";
|
|
3
|
+
import { orpcCall, resolveApiUrl } from "../core/auth-fetch.js";
|
|
4
|
+
|
|
5
|
+
type CodeSharingStatusResponse = {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type ShareWorkflowResponse = {
|
|
10
|
+
id: string;
|
|
11
|
+
status: "created" | "existing" | "refreshed";
|
|
12
|
+
workflow: string;
|
|
13
|
+
marketplace_url: string;
|
|
14
|
+
code_url: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function requireCloudApiKey() {
|
|
18
|
+
const apiKey = process.env.LIBRETTO_API_KEY?.trim();
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"LIBRETTO_API_KEY is required to share Libretto Cloud workflow code. Issue one with `libretto cloud auth api-key issue --label <label>`.",
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
apiUrl: resolveApiUrl(null),
|
|
26
|
+
credential: { source: "env-api-key" as const, apiKey },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const shareWorkflowCommand = SimpleCLI.command({
|
|
31
|
+
description: "Share one hosted workflow's code publicly",
|
|
32
|
+
})
|
|
33
|
+
.input(SimpleCLI.input({
|
|
34
|
+
positionals: [
|
|
35
|
+
SimpleCLI.positional("workflow", z.string().min(1), {
|
|
36
|
+
help: "Hosted workflow name to share",
|
|
37
|
+
}),
|
|
38
|
+
],
|
|
39
|
+
named: {
|
|
40
|
+
refresh: SimpleCLI.flag({
|
|
41
|
+
help: "Refresh an existing share from the workflow's current deployment",
|
|
42
|
+
}),
|
|
43
|
+
},
|
|
44
|
+
}))
|
|
45
|
+
.handle(async ({ input }) => {
|
|
46
|
+
const { apiUrl, credential } = requireCloudApiKey();
|
|
47
|
+
const response = await orpcCall<ShareWorkflowResponse>({
|
|
48
|
+
apiUrl,
|
|
49
|
+
path: "/v1/workflows/share",
|
|
50
|
+
input: { workflow: input.workflow, refresh: input.refresh },
|
|
51
|
+
credential,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (response.status === "existing") {
|
|
55
|
+
console.log(`Workflow is already shared: ${response.workflow}`);
|
|
56
|
+
console.log("Use --refresh to update the shared code from the current deployment.");
|
|
57
|
+
} else if (response.status === "refreshed") {
|
|
58
|
+
console.log(`Refreshed shared workflow: ${response.workflow}`);
|
|
59
|
+
} else {
|
|
60
|
+
console.log(`Shared workflow: ${response.workflow}`);
|
|
61
|
+
}
|
|
62
|
+
console.log(`Marketplace URL: ${response.marketplace_url}`);
|
|
63
|
+
console.log(`Code URL: ${response.code_url}`);
|
|
64
|
+
return response.marketplace_url;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const codeSharingStatusCommand = SimpleCLI.command({
|
|
68
|
+
description: "Show whether tenant code sharing is enabled",
|
|
69
|
+
})
|
|
70
|
+
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
71
|
+
.handle(async () => {
|
|
72
|
+
const { apiUrl, credential } = requireCloudApiKey();
|
|
73
|
+
const response = await orpcCall<CodeSharingStatusResponse>({
|
|
74
|
+
apiUrl,
|
|
75
|
+
path: "/v1/tenant/codeSharing",
|
|
76
|
+
input: {},
|
|
77
|
+
credential,
|
|
78
|
+
});
|
|
79
|
+
console.log(`Code sharing: ${response.enabled ? "enabled" : "disabled"}`);
|
|
80
|
+
return response.enabled;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
async function updateCodeSharing(enabled: boolean): Promise<boolean> {
|
|
84
|
+
const { apiUrl, credential } = requireCloudApiKey();
|
|
85
|
+
const response = await orpcCall<CodeSharingStatusResponse>({
|
|
86
|
+
apiUrl,
|
|
87
|
+
path: "/v1/tenant/updateCodeSharing",
|
|
88
|
+
input: { enabled },
|
|
89
|
+
credential,
|
|
90
|
+
});
|
|
91
|
+
console.log(`Code sharing: ${response.enabled ? "enabled" : "disabled"}`);
|
|
92
|
+
return response.enabled;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const enableCodeSharingCommand = SimpleCLI.command({
|
|
96
|
+
description: "Enable public workflow code sharing for this tenant",
|
|
97
|
+
})
|
|
98
|
+
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
99
|
+
.handle(async () => updateCodeSharing(true));
|
|
100
|
+
|
|
101
|
+
export const disableCodeSharingCommand = SimpleCLI.command({
|
|
102
|
+
description: "Disable public workflow code sharing for this tenant",
|
|
103
|
+
})
|
|
104
|
+
.input(SimpleCLI.input({ positionals: [], named: {} }))
|
|
105
|
+
.handle(async () => updateCodeSharing(false));
|
|
106
|
+
|
|
107
|
+
export const codeSharingCommands = SimpleCLI.group({
|
|
108
|
+
description: "Manage tenant workflow code sharing",
|
|
109
|
+
routes: {
|
|
110
|
+
status: codeSharingStatusCommand,
|
|
111
|
+
enable: enableCodeSharingCommand,
|
|
112
|
+
disable: disableCodeSharingCommand,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "node:fs";
|
|
13
13
|
import { createRequire, Module } from "node:module";
|
|
14
14
|
import { tmpdir } from "node:os";
|
|
15
|
-
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
15
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { gzipSync } from "node:zlib";
|
|
18
18
|
import { build } from "esbuild";
|
|
@@ -57,6 +57,8 @@ export type WorkflowDeployMetadata = {
|
|
|
57
57
|
credentialNames: string[];
|
|
58
58
|
authProfileName?: string;
|
|
59
59
|
authProfileRefresh?: boolean;
|
|
60
|
+
sourceFile?: string;
|
|
61
|
+
sourceFiles?: string[];
|
|
60
62
|
};
|
|
61
63
|
|
|
62
64
|
type BuildHostedDeployTarballArgs = {
|
|
@@ -473,13 +475,17 @@ function resolveWorkspaceSourcePath(
|
|
|
473
475
|
function workspaceSourcePlugin(
|
|
474
476
|
workspacePackages: Map<string, WorkspacePackage>,
|
|
475
477
|
externalPackages: ReadonlySet<string>,
|
|
478
|
+
unresolvedWorkspaceImports?: Map<string, string>,
|
|
476
479
|
) {
|
|
477
480
|
return {
|
|
478
481
|
name: "workspace-source-resolver",
|
|
479
482
|
setup(buildApi: {
|
|
480
483
|
onResolve: (
|
|
481
484
|
options: { filter: RegExp },
|
|
482
|
-
callback: (args: { path: string }) =>
|
|
485
|
+
callback: (args: { path: string }) =>
|
|
486
|
+
| { path: string }
|
|
487
|
+
| { path: string; external: true }
|
|
488
|
+
| null,
|
|
483
489
|
) => void;
|
|
484
490
|
}) {
|
|
485
491
|
// Workspace imports are treated as bundle input, so their code is
|
|
@@ -503,9 +509,8 @@ function workspaceSourcePlugin(
|
|
|
503
509
|
match.subpath,
|
|
504
510
|
);
|
|
505
511
|
if (!resolvedPath) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
);
|
|
512
|
+
unresolvedWorkspaceImports?.set(args.path, match.info.dir);
|
|
513
|
+
return { path: args.path, external: true };
|
|
509
514
|
}
|
|
510
515
|
|
|
511
516
|
return { path: resolvedPath };
|
|
@@ -618,6 +623,40 @@ function writeDeployMetadata(args: {
|
|
|
618
623
|
);
|
|
619
624
|
}
|
|
620
625
|
|
|
626
|
+
function toPortableRelativePath(args: {
|
|
627
|
+
absSourceDir: string;
|
|
628
|
+
absPath: string;
|
|
629
|
+
}): string {
|
|
630
|
+
const relPath = relative(args.absSourceDir, args.absPath).replaceAll("\\", "/");
|
|
631
|
+
if (relPath.startsWith("../") || relPath === ".." || relPath.startsWith("/")) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
`Deploy entry point must be inside the source directory to support cloud code sharing: ${args.absPath}`,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
return relPath;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function writeShareableSourceFiles(args: {
|
|
640
|
+
absSourceDir: string;
|
|
641
|
+
absSourcePaths: readonly string[];
|
|
642
|
+
outputDir: string;
|
|
643
|
+
}): string[] {
|
|
644
|
+
const relPaths = [...new Set(args.absSourcePaths.map((absPath) =>
|
|
645
|
+
toPortableRelativePath({
|
|
646
|
+
absPath,
|
|
647
|
+
absSourceDir: args.absSourceDir,
|
|
648
|
+
}),
|
|
649
|
+
))].sort();
|
|
650
|
+
|
|
651
|
+
for (const relPath of relPaths) {
|
|
652
|
+
const targetPath = join(args.outputDir, ".libretto-share", "source", relPath);
|
|
653
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
654
|
+
cpSync(resolve(args.absSourceDir, relPath), targetPath);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return relPaths;
|
|
658
|
+
}
|
|
659
|
+
|
|
621
660
|
function shouldVendorCurrentLibretto(versionSpec: string): boolean {
|
|
622
661
|
return (
|
|
623
662
|
versionSpec.startsWith("file:") ||
|
|
@@ -963,8 +1002,12 @@ async function writeBundledDeployEntrypoint(args: {
|
|
|
963
1002
|
externalPackages: ReadonlySet<string>;
|
|
964
1003
|
outputDir: string;
|
|
965
1004
|
workspacePackages: Map<string, WorkspacePackage>;
|
|
966
|
-
}): Promise<
|
|
1005
|
+
}): Promise<{
|
|
1006
|
+
shareableSourceFiles: string[];
|
|
1007
|
+
workflows: WorkflowDeployMetadata[];
|
|
1008
|
+
}> {
|
|
967
1009
|
try {
|
|
1010
|
+
const unresolvedWorkspaceImports = new Map<string, string>();
|
|
968
1011
|
// The implementation bundle is CommonJS so the bootstrap can load it lazily
|
|
969
1012
|
// with createRequire() after workflow discovery, while external packages
|
|
970
1013
|
// continue to load through normal Node module resolution.
|
|
@@ -976,13 +1019,25 @@ async function writeBundledDeployEntrypoint(args: {
|
|
|
976
1019
|
format: "cjs",
|
|
977
1020
|
outfile: "prebundled.cjs",
|
|
978
1021
|
platform: "node",
|
|
1022
|
+
metafile: true,
|
|
979
1023
|
plugins: [
|
|
980
|
-
workspaceSourcePlugin(
|
|
1024
|
+
workspaceSourcePlugin(
|
|
1025
|
+
args.workspacePackages,
|
|
1026
|
+
args.externalPackages,
|
|
1027
|
+
unresolvedWorkspaceImports,
|
|
1028
|
+
),
|
|
981
1029
|
],
|
|
982
1030
|
splitting: false,
|
|
983
1031
|
target: "node20",
|
|
984
1032
|
write: false,
|
|
985
1033
|
});
|
|
1034
|
+
const [unresolvedImport] = unresolvedWorkspaceImports;
|
|
1035
|
+
if (unresolvedImport) {
|
|
1036
|
+
const [importPath, packageDir] = unresolvedImport;
|
|
1037
|
+
throw new Error(
|
|
1038
|
+
`Unable to resolve workspace import "${importPath}" from ${packageDir}.`,
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
986
1041
|
|
|
987
1042
|
const bundledImplementation = implementationBuild.outputFiles?.find(
|
|
988
1043
|
(file) => file.path.endsWith("prebundled.cjs"),
|
|
@@ -1008,7 +1063,22 @@ async function writeBundledDeployEntrypoint(args: {
|
|
|
1008
1063
|
workflows,
|
|
1009
1064
|
}),
|
|
1010
1065
|
);
|
|
1011
|
-
|
|
1066
|
+
const shareableSourceFiles = Object.keys(implementationBuild.metafile?.inputs ?? {})
|
|
1067
|
+
.map((inputPath) =>
|
|
1068
|
+
isAbsolute(inputPath)
|
|
1069
|
+
? resolve(inputPath)
|
|
1070
|
+
: resolve(args.absSourceDir, inputPath),
|
|
1071
|
+
)
|
|
1072
|
+
.filter((absPath) => {
|
|
1073
|
+
const relPath = relative(args.absSourceDir, absPath);
|
|
1074
|
+
return (
|
|
1075
|
+
relPath !== "" &&
|
|
1076
|
+
!relPath.startsWith("../") &&
|
|
1077
|
+
relPath !== ".." &&
|
|
1078
|
+
!isAbsolute(relPath)
|
|
1079
|
+
);
|
|
1080
|
+
});
|
|
1081
|
+
return { shareableSourceFiles, workflows };
|
|
1012
1082
|
} catch (error) {
|
|
1013
1083
|
throw new Error(
|
|
1014
1084
|
`Failed to bundle deploy entry point ${args.absEntryPoint}.\n${formatBuildError(error)}`,
|
|
@@ -1040,7 +1110,7 @@ export async function createHostedDeployPackage(
|
|
|
1040
1110
|
let callerOwnsTempRoot = false;
|
|
1041
1111
|
|
|
1042
1112
|
try {
|
|
1043
|
-
const workflows = await writeBundledDeployEntrypoint({
|
|
1113
|
+
const { shareableSourceFiles, workflows } = await writeBundledDeployEntrypoint({
|
|
1044
1114
|
absEntryPoint,
|
|
1045
1115
|
absSourceDir,
|
|
1046
1116
|
deploymentName: args.deploymentName,
|
|
@@ -1048,6 +1118,20 @@ export async function createHostedDeployPackage(
|
|
|
1048
1118
|
outputDir,
|
|
1049
1119
|
workspacePackages,
|
|
1050
1120
|
});
|
|
1121
|
+
const sourceFiles = writeShareableSourceFiles({
|
|
1122
|
+
absSourceDir,
|
|
1123
|
+
absSourcePaths: [...shareableSourceFiles, absEntryPoint],
|
|
1124
|
+
outputDir,
|
|
1125
|
+
});
|
|
1126
|
+
const sourceFile = toPortableRelativePath({
|
|
1127
|
+
absPath: absEntryPoint,
|
|
1128
|
+
absSourceDir,
|
|
1129
|
+
});
|
|
1130
|
+
const workflowsWithShareableSource = workflows.map((workflow) => ({
|
|
1131
|
+
...workflow,
|
|
1132
|
+
sourceFile,
|
|
1133
|
+
sourceFiles,
|
|
1134
|
+
}));
|
|
1051
1135
|
|
|
1052
1136
|
if (librettoDependency === "file:./libretto") {
|
|
1053
1137
|
copyCurrentLibrettoPackage(outputDir);
|
|
@@ -1063,7 +1147,7 @@ export async function createHostedDeployPackage(
|
|
|
1063
1147
|
outputDir,
|
|
1064
1148
|
sourceDir: absSourceDir,
|
|
1065
1149
|
});
|
|
1066
|
-
writeDeployMetadata({ outputDir, workflows });
|
|
1150
|
+
writeDeployMetadata({ outputDir, workflows: workflowsWithShareableSource });
|
|
1067
1151
|
|
|
1068
1152
|
// Success transfers ownership of the temp directory to the caller, who is
|
|
1069
1153
|
// responsible for invoking cleanup() after the tarball/upload step.
|
|
@@ -1074,7 +1158,7 @@ export async function createHostedDeployPackage(
|
|
|
1074
1158
|
},
|
|
1075
1159
|
entryPoint: "index.js",
|
|
1076
1160
|
outputDir,
|
|
1077
|
-
workflows,
|
|
1161
|
+
workflows: workflowsWithShareableSource,
|
|
1078
1162
|
};
|
|
1079
1163
|
} finally {
|
|
1080
1164
|
// On any failure before we return, this function still owns the temp dir
|
package/src/cli/router.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { authCommands } from "./commands/auth.js";
|
|
|
2
2
|
import { billingCommands } from "./commands/billing.js";
|
|
3
3
|
import { browserCommands } from "./commands/browser.js";
|
|
4
4
|
import { cloudCredentialCommands } from "./commands/cloud-credentials.js";
|
|
5
|
+
import { codeSharingCommands, shareWorkflowCommand } from "./commands/cloud-sharing.js";
|
|
5
6
|
import { deployCommand } from "./commands/deploy.js";
|
|
6
7
|
import { executionCommands } from "./commands/execution.js";
|
|
7
8
|
import { experimentsCommand } from "./commands/experiments.js";
|
|
@@ -25,6 +26,8 @@ export const cliRoutes = {
|
|
|
25
26
|
billing: billingCommands,
|
|
26
27
|
credentials: cloudCredentialCommands,
|
|
27
28
|
profiles: profileCommands,
|
|
29
|
+
share: shareWorkflowCommand,
|
|
30
|
+
sharing: codeSharingCommands,
|
|
28
31
|
},
|
|
29
32
|
}),
|
|
30
33
|
experiments: experimentsCommand,
|