libretto 0.6.26 → 0.6.27

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.
@@ -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
- throw new Error(
361
- `Unable to resolve workspace import "${args.path}" from ${match.info.dir}.`
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(args.workspacePackages, args.externalPackages)
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
- return workflows;
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) {
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "libretto",
3
- "version": "0.6.26",
3
+ "version": "0.6.27",
4
4
  "description": "AI-powered browser automation library and CLI built on Playwright",
5
5
  "license": "MIT",
6
6
  "homepage": "https://libretto.sh",
@@ -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.26"
7
+ version: "0.6.27"
8
8
  ---
9
9
 
10
10
  ## How Libretto Works
@@ -4,7 +4,7 @@ description: "Read-only Libretto workflow for diagnosing live browser state with
4
4
  license: MIT
5
5
  metadata:
6
6
  author: saffron-health
7
- version: "0.6.26"
7
+ version: "0.6.27"
8
8
  ---
9
9
 
10
10
  ## How Libretto Read-Only Works
@@ -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 }) => { path: string } | null,
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
- throw new Error(
507
- `Unable to resolve workspace import "${args.path}" from ${match.info.dir}.`,
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<WorkflowDeployMetadata[]> {
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(args.workspacePackages, args.externalPackages),
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
- return workflows;
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,