libretto 0.6.25 → 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.
- package/README.md +1 -1
- package/README.template.md +1 -1
- package/dist/cli/commands/cloud-sharing.js +93 -0
- package/dist/cli/core/deploy-artifact.js +65 -10
- package/dist/cli/core/telemetry.js +32 -3
- package/dist/cli/router.js +4 -1
- package/package.json +1 -1
- package/skills/libretto/SKILL.md +1 -1
- 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/core/telemetry.ts +48 -2
- package/src/cli/router.ts +3 -0
package/README.md
CHANGED
|
@@ -95,7 +95,7 @@ All Libretto state lives in a `.libretto/` directory at your project root. See t
|
|
|
95
95
|
|
|
96
96
|
## Telemetry
|
|
97
97
|
|
|
98
|
-
Libretto records anonymous CLI telemetry to help understand CLI usage and help us prioritize improvements. Each resolved command can send only an install id, timestamp, command event name such as `libretto run`, and
|
|
98
|
+
Libretto records anonymous CLI telemetry to help understand CLI usage and help us prioritize improvements. Each resolved command can send only an install id, timestamp, command event name such as `libretto run`, error boolean, package version, and build channel (`node_modules`, `source`, or `unknown`). Libretto does not send command arguments, URLs, project paths, auth state, API keys, error messages or details, or user identity.
|
|
99
99
|
|
|
100
100
|
The install id is stored in the telemetry file at `~/.libretto/telemetry.json`. The implementation lives in [`src/cli/core/telemetry.ts`](src/cli/core/telemetry.ts).
|
|
101
101
|
|
package/README.template.md
CHANGED
|
@@ -93,7 +93,7 @@ All Libretto state lives in a `.libretto/` directory at your project root. See t
|
|
|
93
93
|
|
|
94
94
|
## Telemetry
|
|
95
95
|
|
|
96
|
-
Libretto records anonymous CLI telemetry to help understand CLI usage and help us prioritize improvements. Each resolved command can send only an install id, timestamp, command event name such as `libretto run`, and
|
|
96
|
+
Libretto records anonymous CLI telemetry to help understand CLI usage and help us prioritize improvements. Each resolved command can send only an install id, timestamp, command event name such as `libretto run`, error boolean, package version, and build channel (`node_modules`, `source`, or `unknown`). Libretto does not send command arguments, URLs, project paths, auth state, API keys, error messages or details, or user identity.
|
|
97
97
|
|
|
98
98
|
The install id is stored in the telemetry file at `~/.libretto/telemetry.json`. The implementation lives in [`{{LIBRETTO_PATH_PREFIX}}src/cli/core/telemetry.ts`]({{LIBRETTO_PATH_PREFIX}}src/cli/core/telemetry.ts).
|
|
99
99
|
|
|
@@ -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) {
|
|
@@ -1,11 +1,38 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { promises as fs } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import { resolveHostedApiUrl } from "./auth-fetch.js";
|
|
6
8
|
const TELEMETRY_FILE_NAME = "telemetry.json";
|
|
7
9
|
const TELEMETRY_ENDPOINT_PATH = "/v1/telemetry/recordCliEvent";
|
|
8
10
|
const TELEMETRY_TIMEOUT_MS = 250;
|
|
11
|
+
function packageRoot() {
|
|
12
|
+
return join(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
13
|
+
}
|
|
14
|
+
function readPackageVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(
|
|
17
|
+
readFileSync(join(packageRoot(), "package.json"), "utf8")
|
|
18
|
+
);
|
|
19
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
20
|
+
} catch {
|
|
21
|
+
return "unknown";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function resolveBuildChannel() {
|
|
25
|
+
const root = packageRoot();
|
|
26
|
+
const workspaceRoot = join(root, "../..");
|
|
27
|
+
if (basename(dirname(root)) === "packages" && existsSync(join(workspaceRoot, "pnpm-workspace.yaml"))) {
|
|
28
|
+
return "source";
|
|
29
|
+
}
|
|
30
|
+
const pathSegments = root.split(/[\\/]+/);
|
|
31
|
+
if (pathSegments.includes("node_modules")) return "node_modules";
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
const packageVersion = readPackageVersion();
|
|
35
|
+
const buildChannel = resolveBuildChannel();
|
|
9
36
|
function telemetryDir() {
|
|
10
37
|
return join(homedir(), ".libretto");
|
|
11
38
|
}
|
|
@@ -39,7 +66,7 @@ function writeTelemetryNotice() {
|
|
|
39
66
|
if (!process.stderr.isTTY) return;
|
|
40
67
|
process.stderr.write(
|
|
41
68
|
[
|
|
42
|
-
"Libretto collects anonymous CLI telemetry: install id, timestamp, command event,
|
|
69
|
+
"Libretto collects anonymous CLI telemetry: install id, timestamp, command event, error status, package version, and build channel only.",
|
|
43
70
|
"Set LIBRETTO_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 to disable it, or set enabled:false in ~/.libretto/telemetry.json."
|
|
44
71
|
].join(" ") + "\n"
|
|
45
72
|
);
|
|
@@ -59,7 +86,9 @@ async function recordCliTelemetryEvent(command, error) {
|
|
|
59
86
|
installId,
|
|
60
87
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
61
88
|
event: `libretto ${command.path.join(" ")}`,
|
|
62
|
-
error
|
|
89
|
+
error,
|
|
90
|
+
packageVersion,
|
|
91
|
+
buildChannel
|
|
63
92
|
});
|
|
64
93
|
}
|
|
65
94
|
async function sendWithTimeout(payload) {
|
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/package.json
CHANGED
package/skills/libretto/SKILL.md
CHANGED
|
@@ -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
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
3
|
import { promises as fs } from "node:fs";
|
|
3
4
|
import { homedir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
+
import { basename, dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import type { SimpleCLICommandMeta, SimpleCLIMiddleware } from "affordance";
|
|
6
8
|
import { resolveHostedApiUrl } from "./auth-fetch.js";
|
|
7
9
|
|
|
@@ -9,6 +11,8 @@ const TELEMETRY_FILE_NAME = "telemetry.json";
|
|
|
9
11
|
const TELEMETRY_ENDPOINT_PATH = "/v1/telemetry/recordCliEvent";
|
|
10
12
|
const TELEMETRY_TIMEOUT_MS = 250;
|
|
11
13
|
|
|
14
|
+
type BuildChannel = "node_modules" | "source" | "unknown";
|
|
15
|
+
|
|
12
16
|
type StoredTelemetryState = {
|
|
13
17
|
installId?: string;
|
|
14
18
|
enabled?: boolean;
|
|
@@ -19,8 +23,48 @@ type CliTelemetryPayload = {
|
|
|
19
23
|
timestamp: string;
|
|
20
24
|
event: string;
|
|
21
25
|
error: boolean;
|
|
26
|
+
packageVersion: string;
|
|
27
|
+
buildChannel: BuildChannel;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type PackageJson = {
|
|
31
|
+
version?: unknown;
|
|
22
32
|
};
|
|
23
33
|
|
|
34
|
+
function packageRoot(): string {
|
|
35
|
+
return join(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readPackageVersion(): string {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(
|
|
41
|
+
readFileSync(join(packageRoot(), "package.json"), "utf8"),
|
|
42
|
+
) as PackageJson;
|
|
43
|
+
return typeof parsed.version === "string" ? parsed.version : "unknown";
|
|
44
|
+
} catch {
|
|
45
|
+
return "unknown";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resolveBuildChannel(): BuildChannel {
|
|
50
|
+
const root = packageRoot();
|
|
51
|
+
const workspaceRoot = join(root, "../..");
|
|
52
|
+
if (
|
|
53
|
+
basename(dirname(root)) === "packages" &&
|
|
54
|
+
existsSync(join(workspaceRoot, "pnpm-workspace.yaml"))
|
|
55
|
+
) {
|
|
56
|
+
return "source";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pathSegments = root.split(/[\\/]+/);
|
|
60
|
+
if (pathSegments.includes("node_modules")) return "node_modules";
|
|
61
|
+
|
|
62
|
+
return "unknown";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const packageVersion = readPackageVersion();
|
|
66
|
+
const buildChannel = resolveBuildChannel();
|
|
67
|
+
|
|
24
68
|
function telemetryDir(): string {
|
|
25
69
|
return join(homedir(), ".libretto");
|
|
26
70
|
}
|
|
@@ -65,7 +109,7 @@ function writeTelemetryNotice(): void {
|
|
|
65
109
|
if (!process.stderr.isTTY) return;
|
|
66
110
|
process.stderr.write(
|
|
67
111
|
[
|
|
68
|
-
"Libretto collects anonymous CLI telemetry: install id, timestamp, command event,
|
|
112
|
+
"Libretto collects anonymous CLI telemetry: install id, timestamp, command event, error status, package version, and build channel only.",
|
|
69
113
|
"Set LIBRETTO_TELEMETRY_DISABLED=1 or DO_NOT_TRACK=1 to disable it, or set enabled:false in ~/.libretto/telemetry.json.",
|
|
70
114
|
].join(" ") + "\n",
|
|
71
115
|
);
|
|
@@ -92,6 +136,8 @@ async function recordCliTelemetryEvent(
|
|
|
92
136
|
timestamp: new Date().toISOString(),
|
|
93
137
|
event: `libretto ${command.path.join(" ")}`,
|
|
94
138
|
error,
|
|
139
|
+
packageVersion,
|
|
140
|
+
buildChannel,
|
|
95
141
|
});
|
|
96
142
|
}
|
|
97
143
|
|
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,
|