libretto 0.5.3-experimental.0 → 0.5.3-experimental.1
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,162 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync
|
|
6
|
+
} from "node:fs";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
11
|
+
function getConfig() {
|
|
12
|
+
const apiUrl = process.env.LIBRETTO_API_URL;
|
|
13
|
+
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
14
|
+
if (!apiUrl) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"LIBRETTO_API_URL environment variable is required."
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (!apiKey) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
"LIBRETTO_API_KEY environment variable is required."
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
return { apiUrl: apiUrl.replace(/\/$/, ""), apiKey };
|
|
25
|
+
}
|
|
26
|
+
async function postJson(apiUrl, apiKey, path, input = {}) {
|
|
27
|
+
return fetch(`${apiUrl}${path}`, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"x-api-key": apiKey,
|
|
31
|
+
"Content-Type": "application/json"
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify({ json: input })
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function buildSourceTarball(sourceDir) {
|
|
37
|
+
const absSourceDir = resolve(sourceDir);
|
|
38
|
+
const pkgJsonPath = join(absSourceDir, "package.json");
|
|
39
|
+
try {
|
|
40
|
+
readFileSync(pkgJsonPath, "utf8");
|
|
41
|
+
} catch {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`No package.json found in ${absSourceDir}. Deploy source must contain a package.json.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const dir = join(tmpdir(), `libretto-deploy-${Date.now()}`);
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
cpSync(absSourceDir, dir, { recursive: true });
|
|
49
|
+
const tarPath = join(dir, "source.tar.gz");
|
|
50
|
+
execSync(
|
|
51
|
+
`tar czf "${tarPath}" --exclude=source.tar.gz --exclude=node_modules --exclude=.git -C "${dir}" .`
|
|
52
|
+
);
|
|
53
|
+
return readFileSync(tarPath).toString("base64");
|
|
54
|
+
}
|
|
55
|
+
async function pollDeployment(apiUrl, apiKey, deploymentId, pollIntervalMs, maxWaitMs) {
|
|
56
|
+
const start = Date.now();
|
|
57
|
+
let status = "building";
|
|
58
|
+
let deployment;
|
|
59
|
+
while (status === "building" && Date.now() - start < maxWaitMs) {
|
|
60
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
61
|
+
const res = await postJson(apiUrl, apiKey, "/v1/deployments/get", {
|
|
62
|
+
id: deploymentId
|
|
63
|
+
});
|
|
64
|
+
const body = await res.json();
|
|
65
|
+
if (res.status !== 200) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
`Failed to get deployment status (${res.status}): ${JSON.stringify(body)}`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
status = body.json.status;
|
|
71
|
+
deployment = body.json;
|
|
72
|
+
process.stdout.write(".");
|
|
73
|
+
}
|
|
74
|
+
console.log();
|
|
75
|
+
if (!deployment) {
|
|
76
|
+
throw new Error("Deployment timed out before receiving a status update.");
|
|
77
|
+
}
|
|
78
|
+
return deployment;
|
|
79
|
+
}
|
|
80
|
+
const deployInput = SimpleCLI.input({
|
|
81
|
+
positionals: [
|
|
82
|
+
SimpleCLI.positional("sourceDir", z.string().default("."), {
|
|
83
|
+
help: "Path to source directory (default: current directory)"
|
|
84
|
+
})
|
|
85
|
+
],
|
|
86
|
+
named: {
|
|
87
|
+
name: SimpleCLI.option(z.string(), {
|
|
88
|
+
help: "Deployment name"
|
|
89
|
+
}),
|
|
90
|
+
description: SimpleCLI.option(z.string().optional(), {
|
|
91
|
+
help: "Deployment description"
|
|
92
|
+
}),
|
|
93
|
+
entryPoint: SimpleCLI.option(z.string().optional(), {
|
|
94
|
+
name: "entry-point",
|
|
95
|
+
help: "Entry point file (default: index.ts)"
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
const deployCommand = SimpleCLI.command({
|
|
100
|
+
description: "[experimental] Deploy workflows to the hosted platform",
|
|
101
|
+
experimental: true
|
|
102
|
+
}).input(deployInput).handle(async ({ input }) => {
|
|
103
|
+
const { apiUrl, apiKey } = getConfig();
|
|
104
|
+
console.log(`Packaging source from ${resolve(input.sourceDir)}...`);
|
|
105
|
+
const source = buildSourceTarball(input.sourceDir);
|
|
106
|
+
const createPayload = {
|
|
107
|
+
name: input.name,
|
|
108
|
+
source
|
|
109
|
+
};
|
|
110
|
+
if (input.description) createPayload.description = input.description;
|
|
111
|
+
if (input.entryPoint) createPayload.entry_point = input.entryPoint;
|
|
112
|
+
console.log("Uploading deployment...");
|
|
113
|
+
const res = await postJson(
|
|
114
|
+
apiUrl,
|
|
115
|
+
apiKey,
|
|
116
|
+
"/v1/deployments/create",
|
|
117
|
+
createPayload
|
|
118
|
+
);
|
|
119
|
+
const body = await res.json();
|
|
120
|
+
if (res.status !== 200) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Failed to create deployment (${res.status}): ${JSON.stringify(body)}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const { deployment_id, name, version, status } = body.json;
|
|
126
|
+
console.log(
|
|
127
|
+
`Deployment created: ${name} v${version} (${deployment_id})`
|
|
128
|
+
);
|
|
129
|
+
console.log(`Status: ${status}`);
|
|
130
|
+
if (status === "building") {
|
|
131
|
+
process.stdout.write("Waiting for build");
|
|
132
|
+
const deployment = await pollDeployment(
|
|
133
|
+
apiUrl,
|
|
134
|
+
apiKey,
|
|
135
|
+
deployment_id,
|
|
136
|
+
1e4,
|
|
137
|
+
5 * 60 * 1e3
|
|
138
|
+
);
|
|
139
|
+
if (deployment.status === "failed") {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Build failed: ${deployment.build_error ?? "unknown error"}`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (deployment.status === "ready") {
|
|
145
|
+
console.log(`Build complete.`);
|
|
146
|
+
if (deployment.workflows?.length) {
|
|
147
|
+
console.log(
|
|
148
|
+
`Workflows: ${deployment.workflows.join(", ")}`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
console.log(
|
|
153
|
+
`Build still in progress (timed out waiting). Check status with deployment ID: ${deployment_id}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return deployment_id;
|
|
158
|
+
});
|
|
159
|
+
export {
|
|
160
|
+
deployCommand,
|
|
161
|
+
deployInput
|
|
162
|
+
};
|
|
@@ -493,6 +493,7 @@ class SimpleCLIApp {
|
|
|
493
493
|
continue;
|
|
494
494
|
}
|
|
495
495
|
const command2 = this.findCommandByPath(routeEntry.path);
|
|
496
|
+
if (command2?.experimental) continue;
|
|
496
497
|
entries.push({
|
|
497
498
|
label: token,
|
|
498
499
|
description: command2?.description
|
|
@@ -637,6 +638,7 @@ function resolveRouteTree(routes, parentPath = [], parentMiddlewares = []) {
|
|
|
637
638
|
routeKey: pathToRouteKey(path),
|
|
638
639
|
path,
|
|
639
640
|
description: command2.config.description,
|
|
641
|
+
experimental: command2.config.experimental,
|
|
640
642
|
input: command2.input,
|
|
641
643
|
middlewares: mergeInheritedMiddlewares(
|
|
642
644
|
parentMiddlewares,
|
package/dist/cli/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { aiCommands } from "./commands/ai.js";
|
|
2
2
|
import { browserCommands } from "./commands/browser.js";
|
|
3
|
+
import { deployCommand } from "./commands/deploy.js";
|
|
3
4
|
import { executionCommands } from "./commands/execution.js";
|
|
4
5
|
import { initCommand } from "./commands/init.js";
|
|
5
6
|
import { logCommands } from "./commands/logs.js";
|
|
@@ -11,7 +12,8 @@ const cliRoutes = {
|
|
|
11
12
|
...logCommands,
|
|
12
13
|
ai: aiCommands,
|
|
13
14
|
init: initCommand,
|
|
14
|
-
snapshot: snapshotCommand
|
|
15
|
+
snapshot: snapshotCommand,
|
|
16
|
+
deploy: deployCommand
|
|
15
17
|
};
|
|
16
18
|
function createCLIApp() {
|
|
17
19
|
return SimpleCLI.define("libretto", cliRoutes);
|
package/package.json
CHANGED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
cpSync,
|
|
4
|
+
mkdirSync,
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
} from "node:fs";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join, resolve } from "node:path";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { SimpleCLI } from "../framework/simple-cli.js";
|
|
12
|
+
|
|
13
|
+
type DeploymentStatus = "building" | "ready" | "failed";
|
|
14
|
+
|
|
15
|
+
type DeploymentResponse = {
|
|
16
|
+
json: {
|
|
17
|
+
deployment_id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
version: number;
|
|
20
|
+
status: DeploymentStatus;
|
|
21
|
+
workflows?: string[] | null;
|
|
22
|
+
build_error?: string | null;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function getConfig() {
|
|
27
|
+
const apiUrl = process.env.LIBRETTO_API_URL;
|
|
28
|
+
const apiKey = process.env.LIBRETTO_API_KEY;
|
|
29
|
+
|
|
30
|
+
if (!apiUrl) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
"LIBRETTO_API_URL environment variable is required.",
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"LIBRETTO_API_KEY environment variable is required.",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { apiUrl: apiUrl.replace(/\/$/, ""), apiKey };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function postJson(
|
|
45
|
+
apiUrl: string,
|
|
46
|
+
apiKey: string,
|
|
47
|
+
path: string,
|
|
48
|
+
input: Record<string, unknown> = {},
|
|
49
|
+
): Promise<Response> {
|
|
50
|
+
return fetch(`${apiUrl}${path}`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: {
|
|
53
|
+
"x-api-key": apiKey,
|
|
54
|
+
"Content-Type": "application/json",
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify({ json: input }),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function buildSourceTarball(sourceDir: string): string {
|
|
61
|
+
const absSourceDir = resolve(sourceDir);
|
|
62
|
+
|
|
63
|
+
const pkgJsonPath = join(absSourceDir, "package.json");
|
|
64
|
+
try {
|
|
65
|
+
readFileSync(pkgJsonPath, "utf8");
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`No package.json found in ${absSourceDir}. Deploy source must contain a package.json.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const dir = join(tmpdir(), `libretto-deploy-${Date.now()}`);
|
|
73
|
+
mkdirSync(dir, { recursive: true });
|
|
74
|
+
|
|
75
|
+
cpSync(absSourceDir, dir, { recursive: true });
|
|
76
|
+
|
|
77
|
+
const tarPath = join(dir, "source.tar.gz");
|
|
78
|
+
execSync(
|
|
79
|
+
`tar czf "${tarPath}" --exclude=source.tar.gz --exclude=node_modules --exclude=.git -C "${dir}" .`,
|
|
80
|
+
);
|
|
81
|
+
return readFileSync(tarPath).toString("base64");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function pollDeployment(
|
|
85
|
+
apiUrl: string,
|
|
86
|
+
apiKey: string,
|
|
87
|
+
deploymentId: string,
|
|
88
|
+
pollIntervalMs: number,
|
|
89
|
+
maxWaitMs: number,
|
|
90
|
+
): Promise<DeploymentResponse["json"]> {
|
|
91
|
+
const start = Date.now();
|
|
92
|
+
let status: DeploymentStatus = "building";
|
|
93
|
+
let deployment: DeploymentResponse["json"] | undefined;
|
|
94
|
+
|
|
95
|
+
while (status === "building" && Date.now() - start < maxWaitMs) {
|
|
96
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
97
|
+
|
|
98
|
+
const res = await postJson(apiUrl, apiKey, "/v1/deployments/get", {
|
|
99
|
+
id: deploymentId,
|
|
100
|
+
});
|
|
101
|
+
const body = (await res.json()) as DeploymentResponse;
|
|
102
|
+
if (res.status !== 200) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Failed to get deployment status (${res.status}): ${JSON.stringify(body)}`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
status = body.json.status;
|
|
108
|
+
deployment = body.json;
|
|
109
|
+
process.stdout.write(".");
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
|
|
113
|
+
if (!deployment) {
|
|
114
|
+
throw new Error("Deployment timed out before receiving a status update.");
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return deployment;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const deployInput = SimpleCLI.input({
|
|
121
|
+
positionals: [
|
|
122
|
+
SimpleCLI.positional("sourceDir", z.string().default("."), {
|
|
123
|
+
help: "Path to source directory (default: current directory)",
|
|
124
|
+
}),
|
|
125
|
+
],
|
|
126
|
+
named: {
|
|
127
|
+
name: SimpleCLI.option(z.string(), {
|
|
128
|
+
help: "Deployment name",
|
|
129
|
+
}),
|
|
130
|
+
description: SimpleCLI.option(z.string().optional(), {
|
|
131
|
+
help: "Deployment description",
|
|
132
|
+
}),
|
|
133
|
+
entryPoint: SimpleCLI.option(z.string().optional(), {
|
|
134
|
+
name: "entry-point",
|
|
135
|
+
help: "Entry point file (default: index.ts)",
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
export const deployCommand = SimpleCLI.command({
|
|
141
|
+
description: "[experimental] Deploy workflows to the hosted platform",
|
|
142
|
+
experimental: true,
|
|
143
|
+
})
|
|
144
|
+
.input(deployInput)
|
|
145
|
+
.handle(async ({ input }) => {
|
|
146
|
+
const { apiUrl, apiKey } = getConfig();
|
|
147
|
+
|
|
148
|
+
console.log(`Packaging source from ${resolve(input.sourceDir)}...`);
|
|
149
|
+
const source = buildSourceTarball(input.sourceDir);
|
|
150
|
+
|
|
151
|
+
const createPayload: Record<string, unknown> = {
|
|
152
|
+
name: input.name,
|
|
153
|
+
source,
|
|
154
|
+
};
|
|
155
|
+
if (input.description) createPayload.description = input.description;
|
|
156
|
+
if (input.entryPoint) createPayload.entry_point = input.entryPoint;
|
|
157
|
+
|
|
158
|
+
console.log("Uploading deployment...");
|
|
159
|
+
const res = await postJson(
|
|
160
|
+
apiUrl,
|
|
161
|
+
apiKey,
|
|
162
|
+
"/v1/deployments/create",
|
|
163
|
+
createPayload,
|
|
164
|
+
);
|
|
165
|
+
const body = (await res.json()) as DeploymentResponse;
|
|
166
|
+
if (res.status !== 200) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Failed to create deployment (${res.status}): ${JSON.stringify(body)}`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const { deployment_id, name, version, status } = body.json;
|
|
173
|
+
console.log(
|
|
174
|
+
`Deployment created: ${name} v${version} (${deployment_id})`,
|
|
175
|
+
);
|
|
176
|
+
console.log(`Status: ${status}`);
|
|
177
|
+
|
|
178
|
+
if (status === "building") {
|
|
179
|
+
process.stdout.write("Waiting for build");
|
|
180
|
+
const deployment = await pollDeployment(
|
|
181
|
+
apiUrl,
|
|
182
|
+
apiKey,
|
|
183
|
+
deployment_id,
|
|
184
|
+
10_000,
|
|
185
|
+
5 * 60 * 1000,
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (deployment.status === "failed") {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Build failed: ${deployment.build_error ?? "unknown error"}`,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (deployment.status === "ready") {
|
|
195
|
+
console.log(`Build complete.`);
|
|
196
|
+
if (deployment.workflows?.length) {
|
|
197
|
+
console.log(
|
|
198
|
+
`Workflows: ${deployment.workflows.join(", ")}`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
console.log(
|
|
203
|
+
`Build still in progress (timed out waiting). Check status with deployment ID: ${deployment_id}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return deployment_id;
|
|
209
|
+
});
|
|
@@ -4,6 +4,7 @@ type RecordUnknown = Record<string, unknown>;
|
|
|
4
4
|
|
|
5
5
|
export type SimpleCLICommandConfig = {
|
|
6
6
|
description: string;
|
|
7
|
+
experimental?: boolean;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
export type SimpleCLIInputRaw = {
|
|
@@ -111,6 +112,7 @@ export type SimpleCLIResolvedCommand = {
|
|
|
111
112
|
};
|
|
112
113
|
|
|
113
114
|
type InternalResolvedCommand = SimpleCLIResolvedCommand & {
|
|
115
|
+
experimental?: boolean;
|
|
114
116
|
input?: SimpleCLIInput<unknown>;
|
|
115
117
|
middlewares: AnySimpleCLIMiddleware[];
|
|
116
118
|
handler: SimpleCLIHandler<unknown, SimpleCLIContext, unknown>;
|
|
@@ -858,6 +860,7 @@ export class SimpleCLIApp {
|
|
|
858
860
|
}
|
|
859
861
|
|
|
860
862
|
const command = this.findCommandByPath(routeEntry.path);
|
|
863
|
+
if (command?.experimental) continue;
|
|
861
864
|
entries.push({
|
|
862
865
|
label: token,
|
|
863
866
|
description: command?.description,
|
|
@@ -1077,6 +1080,7 @@ function resolveRouteTree(
|
|
|
1077
1080
|
routeKey: pathToRouteKey(path),
|
|
1078
1081
|
path,
|
|
1079
1082
|
description: command.config.description,
|
|
1083
|
+
experimental: command.config.experimental,
|
|
1080
1084
|
input: command.input,
|
|
1081
1085
|
middlewares: mergeInheritedMiddlewares(
|
|
1082
1086
|
parentMiddlewares,
|
package/src/cli/router.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { aiCommands } from "./commands/ai.js";
|
|
2
2
|
import { browserCommands } from "./commands/browser.js";
|
|
3
|
+
import { deployCommand } from "./commands/deploy.js";
|
|
3
4
|
import { executionCommands } from "./commands/execution.js";
|
|
4
5
|
import { initCommand } from "./commands/init.js";
|
|
5
6
|
import { logCommands } from "./commands/logs.js";
|
|
@@ -13,6 +14,7 @@ export const cliRoutes = {
|
|
|
13
14
|
ai: aiCommands,
|
|
14
15
|
init: initCommand,
|
|
15
16
|
snapshot: snapshotCommand,
|
|
17
|
+
deploy: deployCommand,
|
|
16
18
|
};
|
|
17
19
|
|
|
18
20
|
export function createCLIApp() {
|