quilt-sdk 0.0.6 → 0.0.8
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 +50 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +606 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/client.d.ts +2 -2
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +20 -20
- package/dist/core/client.js.map +1 -1
- package/dist/core/http.js +2 -2
- package/dist/generated/platform-contract.d.ts +9329 -1570
- package/dist/generated/platform-contract.d.ts.map +1 -1
- package/dist/generated/platform-contract.js +350 -29
- package/dist/generated/platform-contract.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/modules/agent.d.ts +5 -33
- package/dist/modules/agent.d.ts.map +1 -1
- package/dist/modules/agent.js.map +1 -1
- package/dist/modules/containers.d.ts +16 -35
- package/dist/modules/containers.d.ts.map +1 -1
- package/dist/modules/containers.js +7 -7
- package/dist/modules/containers.js.map +1 -1
- package/dist/modules/functions.d.ts +4 -32
- package/dist/modules/functions.d.ts.map +1 -1
- package/dist/modules/functions.js.map +1 -1
- package/dist/modules/images.d.ts +24 -0
- package/dist/modules/images.d.ts.map +1 -0
- package/dist/modules/images.js +42 -0
- package/dist/modules/images.js.map +1 -0
- package/dist/modules/platform.d.ts +10 -13
- package/dist/modules/platform.d.ts.map +1 -1
- package/dist/modules/platform.js +7 -7
- package/dist/modules/platform.js.map +1 -1
- package/dist/realtime/events.js +1 -1
- package/dist/realtime/terminal.js +1 -1
- package/package.json +12 -8
- package/dist/modules/master.d.ts +0 -10
- package/dist/modules/master.d.ts.map +0 -1
- package/dist/modules/master.js +0 -19
- package/dist/modules/master.js.map +0 -1
package/README.md
CHANGED
|
@@ -10,12 +10,21 @@ Type-safe TypeScript SDK for Quilt production APIs.
|
|
|
10
10
|
npm install quilt-sdk
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
Local package install exposes the SDK CLI through `npx quilt` and `npx quilt-sdk`.
|
|
14
|
+
|
|
13
15
|
or
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
18
|
bun add quilt-sdk
|
|
17
19
|
```
|
|
18
20
|
|
|
21
|
+
Global install exposes the same CLI directly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g quilt-sdk
|
|
25
|
+
quilt health
|
|
26
|
+
```
|
|
27
|
+
|
|
19
28
|
## Requirements
|
|
20
29
|
|
|
21
30
|
- Node `>=20.10.0`
|
|
@@ -65,6 +74,7 @@ Primary SDK surfaces:
|
|
|
65
74
|
- `client.system` for health, info, and activity
|
|
66
75
|
- `client.containers` for container lifecycle, exec, logs, metrics, snapshots, network, and GUI URLs
|
|
67
76
|
- `client.platform` for cross-cutting routes such as operations, env maps, archives, jobs, ICC, OCI, and helper control flows
|
|
77
|
+
- `client.images` for OCI pull, inspect, history, remove, build-context upload, and OCI image builds
|
|
68
78
|
- `client.volumes` for volume lifecycle and file browsing
|
|
69
79
|
- `client.clusters` for cluster, node, workload, placement, and join-token control-plane flows
|
|
70
80
|
- `client.agent` for join-token and node-token authenticated agent calls
|
|
@@ -108,6 +118,41 @@ const accepted = await client.containers.create(
|
|
|
108
118
|
);
|
|
109
119
|
```
|
|
110
120
|
|
|
121
|
+
Build a local Docker context into Quilt's OCI store:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import { readFile } from "node:fs/promises";
|
|
125
|
+
import { QuiltClient } from "quilt-sdk";
|
|
126
|
+
|
|
127
|
+
const client = QuiltClient.connect({
|
|
128
|
+
baseUrl: process.env.QUILT_BASE_URL ?? "https://backend.quilt.sh",
|
|
129
|
+
apiKey: process.env.QUILT_API_KEY,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const upload = await client.images.uploadBuildContext(
|
|
133
|
+
(await readFile("./context.tar.gz")).toString("base64"),
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
const accepted = await client.images.build({
|
|
137
|
+
context_id: upload.context_id,
|
|
138
|
+
image_reference: "docker.io/acme/my-app:latest",
|
|
139
|
+
dockerfile_path: "Dockerfile",
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## CLI
|
|
144
|
+
|
|
145
|
+
The npm package now ships a CLI backed by the same SDK client.
|
|
146
|
+
|
|
147
|
+
Examples:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
npx quilt --api-key "$QUILT_API_KEY" health
|
|
151
|
+
npx quilt --api-key "$QUILT_API_KEY" oci pull --reference docker.io/library/alpine:3.20
|
|
152
|
+
npx quilt --api-key "$QUILT_API_KEY" build --context . --dockerfile Dockerfile --tag docker.io/acme/my-app:latest
|
|
153
|
+
npx quilt --api-key "$QUILT_API_KEY" container create --image docker.io/acme/my-app:latest --oci --wait -- sleep 60
|
|
154
|
+
```
|
|
155
|
+
|
|
111
156
|
## Public Types
|
|
112
157
|
|
|
113
158
|
The SDK ships declarations automatically through the package export.
|
|
@@ -138,6 +183,11 @@ Use the SDK in the same execution style the backend expects:
|
|
|
138
183
|
- invoke a shell explicitly when shell parsing is required
|
|
139
184
|
- prefer typed module methods over `client.raw(...)`
|
|
140
185
|
|
|
186
|
+
Recent contract-alignment details now reflected in the SDK:
|
|
187
|
+
|
|
188
|
+
- `client.containers.create()` and `createBatch()` accept the full backend request shape, including `gpu_count` and `gpu_ids`
|
|
189
|
+
- snapshot responses include `source_container_name` when the backend captured it at snapshot creation time
|
|
190
|
+
|
|
141
191
|
## Realtime
|
|
142
192
|
|
|
143
193
|
### SSE
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import process from "node:process";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
import { Writable } from "node:stream";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
import { create as createTarball } from "tar";
|
|
11
|
+
import { QuiltApiError, QuiltClient } from "./index.js";
|
|
12
|
+
class MutedOutput extends Writable {
|
|
13
|
+
delegate;
|
|
14
|
+
muted = false;
|
|
15
|
+
constructor(delegate) {
|
|
16
|
+
super();
|
|
17
|
+
this.delegate = delegate;
|
|
18
|
+
}
|
|
19
|
+
_write(chunk, encoding, callback) {
|
|
20
|
+
if (!this.muted) {
|
|
21
|
+
this.delegate.write(chunk, encoding);
|
|
22
|
+
}
|
|
23
|
+
callback();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function main() {
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
28
|
+
loadLocalEnv(argv);
|
|
29
|
+
const global = parseGlobalOptions(argv);
|
|
30
|
+
const auth = await ensureAuth(global.auth);
|
|
31
|
+
const client = QuiltClient.connect({
|
|
32
|
+
baseUrl: global.baseUrl,
|
|
33
|
+
auth,
|
|
34
|
+
});
|
|
35
|
+
const ctx = {
|
|
36
|
+
client,
|
|
37
|
+
json: global.json,
|
|
38
|
+
};
|
|
39
|
+
const commands = buildCommands();
|
|
40
|
+
const key = commandKey(global.rest);
|
|
41
|
+
const handler = commands.get(key);
|
|
42
|
+
if (!handler) {
|
|
43
|
+
printUsage();
|
|
44
|
+
process.exitCode = 1;
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
await handler(ctx, global.rest.slice(key.split(" ").length));
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
renderError(error);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function buildCommands() {
|
|
56
|
+
return new Map([
|
|
57
|
+
["health", healthCommand],
|
|
58
|
+
["build", buildCommand],
|
|
59
|
+
["container create", containerCreateCommand],
|
|
60
|
+
["container list", containerListCommand],
|
|
61
|
+
["container get", containerGetCommand],
|
|
62
|
+
["container logs", containerLogsCommand],
|
|
63
|
+
["container remove", containerRemoveCommand],
|
|
64
|
+
["container exec", containerExecCommand],
|
|
65
|
+
["oci pull", ociPullCommand],
|
|
66
|
+
["oci list", ociListCommand],
|
|
67
|
+
["oci inspect", ociInspectCommand],
|
|
68
|
+
["oci history", ociHistoryCommand],
|
|
69
|
+
["operation get", operationGetCommand],
|
|
70
|
+
["operation wait", operationWaitCommand],
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
function commandKey(args) {
|
|
74
|
+
if (args.length >= 2) {
|
|
75
|
+
const two = `${args[0]} ${args[1]}`;
|
|
76
|
+
if (two === "container create" ||
|
|
77
|
+
two === "container list" ||
|
|
78
|
+
two === "container get" ||
|
|
79
|
+
two === "container logs" ||
|
|
80
|
+
two === "container remove" ||
|
|
81
|
+
two === "container exec" ||
|
|
82
|
+
two === "oci pull" ||
|
|
83
|
+
two === "oci list" ||
|
|
84
|
+
two === "oci inspect" ||
|
|
85
|
+
two === "oci history" ||
|
|
86
|
+
two === "operation get" ||
|
|
87
|
+
two === "operation wait") {
|
|
88
|
+
return two;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return args[0] ?? "";
|
|
92
|
+
}
|
|
93
|
+
function parseGlobalOptions(argv) {
|
|
94
|
+
let json = false;
|
|
95
|
+
let baseUrl = process.env["QUILT_API_URL"] ?? process.env["QUILT_BASE_URL"] ?? "https://backend.quilt.sh";
|
|
96
|
+
let apiKey = process.env["QUILT_API_KEY"];
|
|
97
|
+
let token = process.env["QUILT_TOKEN"] ?? process.env["QUILT_JWT"];
|
|
98
|
+
const rest = [];
|
|
99
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
100
|
+
const value = argv[index];
|
|
101
|
+
if (value === "--json") {
|
|
102
|
+
json = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (value === "--base-url") {
|
|
106
|
+
baseUrl = requiredValue(argv, ++index, "--base-url");
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (value === "--api-key") {
|
|
110
|
+
apiKey = requiredValue(argv, ++index, "--api-key");
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (value === "--token") {
|
|
114
|
+
token = requiredValue(argv, ++index, "--token");
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
rest.push(...argv.slice(index));
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
const auth = apiKey
|
|
121
|
+
? { type: "apiKey", apiKey }
|
|
122
|
+
: token
|
|
123
|
+
? { type: "bearer", token }
|
|
124
|
+
: { type: "none" };
|
|
125
|
+
return { baseUrl, auth, json, rest };
|
|
126
|
+
}
|
|
127
|
+
function loadLocalEnv(argv) {
|
|
128
|
+
const cwd = process.cwd();
|
|
129
|
+
const preparse = {
|
|
130
|
+
noEnv: argv.includes("--no-env"),
|
|
131
|
+
envFile: firstValue(argv, "--env-file"),
|
|
132
|
+
};
|
|
133
|
+
if (preparse.noEnv) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
for (const fileName of [".env", ".env.local"]) {
|
|
137
|
+
loadEnvFile(path.join(cwd, fileName));
|
|
138
|
+
}
|
|
139
|
+
if (preparse.envFile) {
|
|
140
|
+
loadEnvFile(path.resolve(cwd, preparse.envFile));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function loadEnvFile(filePath) {
|
|
144
|
+
if (!existsSync(filePath)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const content = readFileSync(filePath, "utf8");
|
|
148
|
+
for (const [key, value] of Object.entries(parseDotEnv(content))) {
|
|
149
|
+
if (process.env[key] === undefined) {
|
|
150
|
+
process.env[key] = value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function parseDotEnv(content) {
|
|
155
|
+
const parsed = {};
|
|
156
|
+
for (const rawLine of content.split(/\r?\n/u)) {
|
|
157
|
+
const line = rawLine.trim();
|
|
158
|
+
if (!line || line.startsWith("#")) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const exportLine = line.startsWith("export ") ? line.slice(7).trim() : line;
|
|
162
|
+
const separator = exportLine.indexOf("=");
|
|
163
|
+
if (separator === -1) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const key = exportLine.slice(0, separator).trim();
|
|
167
|
+
let value = exportLine.slice(separator + 1).trim();
|
|
168
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
169
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
170
|
+
value = value.slice(1, -1);
|
|
171
|
+
}
|
|
172
|
+
parsed[key] = value;
|
|
173
|
+
}
|
|
174
|
+
return parsed;
|
|
175
|
+
}
|
|
176
|
+
async function ensureAuth(auth) {
|
|
177
|
+
if (auth.type !== "none") {
|
|
178
|
+
return auth;
|
|
179
|
+
}
|
|
180
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
181
|
+
return auth;
|
|
182
|
+
}
|
|
183
|
+
const output = new MutedOutput(process.stderr);
|
|
184
|
+
const rl = createInterface({
|
|
185
|
+
input: process.stdin,
|
|
186
|
+
output,
|
|
187
|
+
terminal: true,
|
|
188
|
+
});
|
|
189
|
+
try {
|
|
190
|
+
process.stderr.write("Enter your API key: ");
|
|
191
|
+
output.muted = true;
|
|
192
|
+
const answer = (await rl.question("")).trim();
|
|
193
|
+
process.stderr.write("\n");
|
|
194
|
+
return answer.length > 0 ? { type: "apiKey", apiKey: answer } : auth;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
output.muted = false;
|
|
198
|
+
rl.close();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function healthCommand(ctx) {
|
|
202
|
+
const response = await ctx.client.system.health();
|
|
203
|
+
printResult(ctx, response);
|
|
204
|
+
}
|
|
205
|
+
async function buildCommand(ctx, args) {
|
|
206
|
+
const parsed = parseOptions(args, {
|
|
207
|
+
"--context": ".",
|
|
208
|
+
"--dockerfile": "Dockerfile",
|
|
209
|
+
"--tag": "",
|
|
210
|
+
"--target": "",
|
|
211
|
+
"--build-arg": [],
|
|
212
|
+
"--no-wait": false,
|
|
213
|
+
});
|
|
214
|
+
const tag = stringOption(parsed, "--tag");
|
|
215
|
+
if (!tag) {
|
|
216
|
+
throw new Error("build requires --tag <image-reference>");
|
|
217
|
+
}
|
|
218
|
+
const contextDir = path.resolve(stringOption(parsed, "--context") ?? ".");
|
|
219
|
+
const archive = await createContextArchive(contextDir);
|
|
220
|
+
const upload = await ctx.client.images.uploadBuildContext(archive.toString("base64"));
|
|
221
|
+
if (!upload) {
|
|
222
|
+
throw new Error("build context upload returned no response body");
|
|
223
|
+
}
|
|
224
|
+
const targetStage = optionalString(parsed, "--target");
|
|
225
|
+
const buildBody = {
|
|
226
|
+
context_id: upload.context_id,
|
|
227
|
+
image_reference: tag,
|
|
228
|
+
dockerfile_path: stringOption(parsed, "--dockerfile") ?? "Dockerfile",
|
|
229
|
+
build_args: buildArgMap(arrayOption(parsed, "--build-arg")),
|
|
230
|
+
...(targetStage ? { target_stage: targetStage } : {}),
|
|
231
|
+
};
|
|
232
|
+
const accepted = await ctx.client.images.build(buildBody);
|
|
233
|
+
if (booleanOption(parsed, "--no-wait")) {
|
|
234
|
+
printResult(ctx, {
|
|
235
|
+
context_id: upload.context_id,
|
|
236
|
+
size_bytes: upload.size_bytes,
|
|
237
|
+
operation_id: accepted.operation_id,
|
|
238
|
+
status_url: accepted.status_url,
|
|
239
|
+
});
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const result = await waitForOperation(ctx.client, accepted);
|
|
243
|
+
printResult(ctx, result);
|
|
244
|
+
}
|
|
245
|
+
async function containerCreateCommand(ctx, args) {
|
|
246
|
+
const parsed = parseOptions(args, {
|
|
247
|
+
"--name": "",
|
|
248
|
+
"--image": "prod",
|
|
249
|
+
"--oci": false,
|
|
250
|
+
"--env": [],
|
|
251
|
+
"--memory": "",
|
|
252
|
+
"--cpu": "",
|
|
253
|
+
"--volume": [],
|
|
254
|
+
"--workdir": "",
|
|
255
|
+
"--wait": false,
|
|
256
|
+
"--": [],
|
|
257
|
+
});
|
|
258
|
+
const image = stringOption(parsed, "--image") ?? "prod";
|
|
259
|
+
const name = optionalString(parsed, "--name");
|
|
260
|
+
const oci = booleanOption(parsed, "--oci") || !["prod", "prod-gui"].includes(image);
|
|
261
|
+
const command = arrayOption(parsed, "--");
|
|
262
|
+
const memoryLimitMb = numberOption(parsed, "--memory");
|
|
263
|
+
const cpuLimitPercent = numberOption(parsed, "--cpu");
|
|
264
|
+
const workdir = optionalString(parsed, "--workdir");
|
|
265
|
+
const environment = keyValueMap(arrayOption(parsed, "--env"));
|
|
266
|
+
const volumes = arrayOption(parsed, "--volume");
|
|
267
|
+
const containerBody = {
|
|
268
|
+
...(name ? { name } : {}),
|
|
269
|
+
image,
|
|
270
|
+
...(oci ? { oci: true } : {}),
|
|
271
|
+
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
|
272
|
+
...(memoryLimitMb !== undefined ? { memory_limit_mb: memoryLimitMb } : {}),
|
|
273
|
+
...(cpuLimitPercent !== undefined ? { cpu_limit_percent: cpuLimitPercent } : {}),
|
|
274
|
+
...(volumes.length > 0 ? { volumes } : {}),
|
|
275
|
+
...(workdir ? { working_directory: workdir } : {}),
|
|
276
|
+
...(command.length > 0 ? { command } : {}),
|
|
277
|
+
};
|
|
278
|
+
const accepted = await ctx.client.containers.create(containerBody, "async");
|
|
279
|
+
if (!booleanOption(parsed, "--wait")) {
|
|
280
|
+
printResult(ctx, accepted);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const result = await waitForOperation(ctx.client, asAcceptedOperation(accepted));
|
|
284
|
+
printResult(ctx, result);
|
|
285
|
+
}
|
|
286
|
+
async function containerListCommand(ctx, args) {
|
|
287
|
+
const parsed = parseOptions(args, { "--state": "" });
|
|
288
|
+
const state = optionalString(parsed, "--state");
|
|
289
|
+
const response = await ctx.client.containers.list(state ? { state } : undefined);
|
|
290
|
+
printResult(ctx, response);
|
|
291
|
+
}
|
|
292
|
+
async function containerGetCommand(ctx, args) {
|
|
293
|
+
const identifier = args[0];
|
|
294
|
+
if (!identifier) {
|
|
295
|
+
throw new Error("container get requires <id-or-name>");
|
|
296
|
+
}
|
|
297
|
+
printResult(ctx, await ctx.client.containers.get(identifier));
|
|
298
|
+
}
|
|
299
|
+
async function containerLogsCommand(ctx, args) {
|
|
300
|
+
const identifier = args[0];
|
|
301
|
+
if (!identifier) {
|
|
302
|
+
throw new Error("container logs requires <id-or-name>");
|
|
303
|
+
}
|
|
304
|
+
const parsed = parseOptions(args.slice(1), { "--limit": "" });
|
|
305
|
+
const limit = numberOption(parsed, "--limit");
|
|
306
|
+
printResult(ctx, await ctx.client.containers.logs(identifier, limit ? { limit } : undefined));
|
|
307
|
+
}
|
|
308
|
+
async function containerRemoveCommand(ctx, args) {
|
|
309
|
+
const identifier = args[0];
|
|
310
|
+
if (!identifier) {
|
|
311
|
+
throw new Error("container remove requires <id-or-name>");
|
|
312
|
+
}
|
|
313
|
+
const parsed = parseOptions(args.slice(1), { "--wait": false });
|
|
314
|
+
const accepted = await ctx.client.containers.remove(identifier, "async");
|
|
315
|
+
if (!booleanOption(parsed, "--wait")) {
|
|
316
|
+
printResult(ctx, accepted);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
printResult(ctx, await waitForOperation(ctx.client, asAcceptedOperation(accepted)));
|
|
320
|
+
}
|
|
321
|
+
async function containerExecCommand(ctx, args) {
|
|
322
|
+
const identifier = args[0];
|
|
323
|
+
if (!identifier) {
|
|
324
|
+
throw new Error("container exec requires <id-or-name>");
|
|
325
|
+
}
|
|
326
|
+
const parsed = parseOptions(args.slice(1), {
|
|
327
|
+
"--workdir": "",
|
|
328
|
+
"--capture-output": false,
|
|
329
|
+
"--detach": false,
|
|
330
|
+
"--timeout-ms": "",
|
|
331
|
+
"--": [],
|
|
332
|
+
});
|
|
333
|
+
const command = arrayOption(parsed, "--");
|
|
334
|
+
if (command.length === 0) {
|
|
335
|
+
throw new Error("container exec requires a command after --");
|
|
336
|
+
}
|
|
337
|
+
const workdir = optionalString(parsed, "--workdir");
|
|
338
|
+
const timeoutMs = numberOption(parsed, "--timeout-ms");
|
|
339
|
+
printResult(ctx, await ctx.client.containers.exec(identifier, {
|
|
340
|
+
command,
|
|
341
|
+
...(workdir ? { workdir } : {}),
|
|
342
|
+
...(booleanOption(parsed, "--capture-output") ? { capture_output: true } : {}),
|
|
343
|
+
...(booleanOption(parsed, "--detach") ? { detach: true } : {}),
|
|
344
|
+
...(timeoutMs !== undefined ? { timeout_ms: timeoutMs } : {}),
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
async function ociPullCommand(ctx, args) {
|
|
348
|
+
const parsed = parseOptions(args, {
|
|
349
|
+
"--reference": "",
|
|
350
|
+
"--force": false,
|
|
351
|
+
"--platform": "",
|
|
352
|
+
"--username": "",
|
|
353
|
+
"--password": "",
|
|
354
|
+
});
|
|
355
|
+
const reference = stringOption(parsed, "--reference");
|
|
356
|
+
if (!reference) {
|
|
357
|
+
throw new Error("oci pull requires --reference <oci-reference>");
|
|
358
|
+
}
|
|
359
|
+
const pullBody = {
|
|
360
|
+
reference,
|
|
361
|
+
force: booleanOption(parsed, "--force"),
|
|
362
|
+
};
|
|
363
|
+
const platform = optionalString(parsed, "--platform");
|
|
364
|
+
if (platform) {
|
|
365
|
+
pullBody.platform = platform;
|
|
366
|
+
}
|
|
367
|
+
const username = optionalString(parsed, "--username");
|
|
368
|
+
if (username) {
|
|
369
|
+
pullBody.registry_username = username;
|
|
370
|
+
}
|
|
371
|
+
const password = optionalString(parsed, "--password");
|
|
372
|
+
if (password) {
|
|
373
|
+
pullBody.registry_password = password;
|
|
374
|
+
}
|
|
375
|
+
printResult(ctx, await ctx.client.images.pull(pullBody));
|
|
376
|
+
}
|
|
377
|
+
async function ociListCommand(ctx, args) {
|
|
378
|
+
const parsed = parseOptions(args, { "--filter": "", "--include-digests": false });
|
|
379
|
+
const listQuery = {};
|
|
380
|
+
const filter = optionalString(parsed, "--filter");
|
|
381
|
+
if (filter) {
|
|
382
|
+
listQuery.filter = filter;
|
|
383
|
+
}
|
|
384
|
+
if (booleanOption(parsed, "--include-digests")) {
|
|
385
|
+
listQuery.include_digests = true;
|
|
386
|
+
}
|
|
387
|
+
printResult(ctx, await ctx.client.images.list(listQuery));
|
|
388
|
+
}
|
|
389
|
+
async function ociInspectCommand(ctx, args) {
|
|
390
|
+
const reference = firstValue(args, "--reference");
|
|
391
|
+
if (!reference) {
|
|
392
|
+
throw new Error("oci inspect requires --reference <oci-reference>");
|
|
393
|
+
}
|
|
394
|
+
printResult(ctx, await ctx.client.images.inspect(reference));
|
|
395
|
+
}
|
|
396
|
+
async function ociHistoryCommand(ctx, args) {
|
|
397
|
+
const reference = firstValue(args, "--reference");
|
|
398
|
+
if (!reference) {
|
|
399
|
+
throw new Error("oci history requires --reference <oci-reference>");
|
|
400
|
+
}
|
|
401
|
+
printResult(ctx, await ctx.client.images.history(reference));
|
|
402
|
+
}
|
|
403
|
+
async function operationGetCommand(ctx, args) {
|
|
404
|
+
const operationId = args[0];
|
|
405
|
+
if (!operationId) {
|
|
406
|
+
throw new Error("operation get requires <operation-id>");
|
|
407
|
+
}
|
|
408
|
+
printResult(ctx, await ctx.client.platform.getOperationStatus(operationId));
|
|
409
|
+
}
|
|
410
|
+
async function operationWaitCommand(ctx, args) {
|
|
411
|
+
const operationId = args[0];
|
|
412
|
+
if (!operationId) {
|
|
413
|
+
throw new Error("operation wait requires <operation-id>");
|
|
414
|
+
}
|
|
415
|
+
const parsed = parseOptions(args.slice(1), { "--timeout-ms": "", "--interval-ms": "" });
|
|
416
|
+
printResult(ctx, await ctx.client.awaitOperation(operationId, {
|
|
417
|
+
timeoutMs: numberOption(parsed, "--timeout-ms") ?? 300_000,
|
|
418
|
+
intervalMs: numberOption(parsed, "--interval-ms") ?? 1_000,
|
|
419
|
+
}));
|
|
420
|
+
}
|
|
421
|
+
async function createContextArchive(contextDir) {
|
|
422
|
+
const dockerignorePath = path.join(contextDir, ".dockerignore");
|
|
423
|
+
const ignore = await readDockerignore(dockerignorePath);
|
|
424
|
+
const tempDir = await mkdtemp(path.join(tmpdir(), "quilt-sdk-build-"));
|
|
425
|
+
const archivePath = path.join(tempDir, "context.tar.gz");
|
|
426
|
+
try {
|
|
427
|
+
await createTarball({
|
|
428
|
+
file: archivePath,
|
|
429
|
+
gzip: true,
|
|
430
|
+
cwd: contextDir,
|
|
431
|
+
portable: true,
|
|
432
|
+
filter: (entryPath) => shouldIncludeEntry(entryPath, ignore),
|
|
433
|
+
}, ["."]);
|
|
434
|
+
return await readFile(archivePath);
|
|
435
|
+
}
|
|
436
|
+
finally {
|
|
437
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function readDockerignore(filePath) {
|
|
441
|
+
try {
|
|
442
|
+
const raw = await readFile(filePath, "utf8");
|
|
443
|
+
return raw
|
|
444
|
+
.split(/\r?\n/u)
|
|
445
|
+
.map((line) => line.trim())
|
|
446
|
+
.filter((line) => line.length > 0 && !line.startsWith("#"));
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
function shouldIncludeEntry(entryPath, ignore) {
|
|
453
|
+
const normalized = entryPath.replaceAll("\\", "/").replace(/^\.\/?/u, "");
|
|
454
|
+
if (normalized.length === 0) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
for (const pattern of ignore) {
|
|
458
|
+
const candidate = pattern.replaceAll("\\", "/").replace(/^\.\/?/u, "");
|
|
459
|
+
if (normalized === candidate || normalized.startsWith(`${candidate}/`)) {
|
|
460
|
+
return false;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
async function waitForOperation(client, accepted) {
|
|
466
|
+
return await client.awaitOperation(accepted.operation_id, { timeoutMs: 600_000 });
|
|
467
|
+
}
|
|
468
|
+
function parseOptions(args, defaults) {
|
|
469
|
+
const values = new Map();
|
|
470
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
471
|
+
values.set(key, Array.isArray(value) ? [...value] : value);
|
|
472
|
+
}
|
|
473
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
474
|
+
const token = args[index];
|
|
475
|
+
if (token === undefined) {
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
if (token === "--") {
|
|
479
|
+
values.set("--", args.slice(index + 1));
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
if (!token.startsWith("--")) {
|
|
483
|
+
const positional = values.get("_") ?? [];
|
|
484
|
+
positional.push(token);
|
|
485
|
+
values.set("_", positional);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const current = values.get(token);
|
|
489
|
+
if (typeof current === "boolean") {
|
|
490
|
+
values.set(token, true);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (Array.isArray(current)) {
|
|
494
|
+
const next = requiredValue(args, ++index, token);
|
|
495
|
+
values.set(token, [...current, next]);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
values.set(token, requiredValue(args, ++index, token));
|
|
499
|
+
}
|
|
500
|
+
return values;
|
|
501
|
+
}
|
|
502
|
+
function requiredValue(args, index, flag) {
|
|
503
|
+
const value = args[index];
|
|
504
|
+
if (!value) {
|
|
505
|
+
throw new Error(`${flag} requires a value`);
|
|
506
|
+
}
|
|
507
|
+
return value;
|
|
508
|
+
}
|
|
509
|
+
function asAcceptedOperation(value) {
|
|
510
|
+
if (typeof value === "object" &&
|
|
511
|
+
value !== null &&
|
|
512
|
+
"operation_id" in value &&
|
|
513
|
+
"status_url" in value) {
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
throw new Error("Expected an operation response from the API");
|
|
517
|
+
}
|
|
518
|
+
function stringOption(values, key) {
|
|
519
|
+
const value = values.get(key);
|
|
520
|
+
return typeof value === "string" ? value : undefined;
|
|
521
|
+
}
|
|
522
|
+
function optionalString(values, key) {
|
|
523
|
+
const value = stringOption(values, key);
|
|
524
|
+
return value && value.length > 0 ? value : undefined;
|
|
525
|
+
}
|
|
526
|
+
function booleanOption(values, key) {
|
|
527
|
+
return values.get(key) === true;
|
|
528
|
+
}
|
|
529
|
+
function arrayOption(values, key) {
|
|
530
|
+
const value = values.get(key);
|
|
531
|
+
return Array.isArray(value) ? value : [];
|
|
532
|
+
}
|
|
533
|
+
function numberOption(values, key) {
|
|
534
|
+
const value = optionalString(values, key);
|
|
535
|
+
return value ? Number(value) : undefined;
|
|
536
|
+
}
|
|
537
|
+
function keyValueMap(values) {
|
|
538
|
+
const entries = values.map((entry) => {
|
|
539
|
+
const [key, value] = splitPair(entry);
|
|
540
|
+
return [key, value];
|
|
541
|
+
});
|
|
542
|
+
return Object.fromEntries(entries);
|
|
543
|
+
}
|
|
544
|
+
function buildArgMap(values) {
|
|
545
|
+
return keyValueMap(values);
|
|
546
|
+
}
|
|
547
|
+
function splitPair(entry) {
|
|
548
|
+
const separator = entry.indexOf("=");
|
|
549
|
+
if (separator === -1) {
|
|
550
|
+
throw new Error(`Expected KEY=VALUE, got '${entry}'`);
|
|
551
|
+
}
|
|
552
|
+
return [entry.slice(0, separator), entry.slice(separator + 1)];
|
|
553
|
+
}
|
|
554
|
+
function firstValue(args, flag) {
|
|
555
|
+
const index = args.indexOf(flag);
|
|
556
|
+
return index === -1 ? undefined : args[index + 1];
|
|
557
|
+
}
|
|
558
|
+
function printResult(ctx, value) {
|
|
559
|
+
if (ctx.json) {
|
|
560
|
+
console.log(JSON.stringify(value, null, 2));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (typeof value === "string") {
|
|
564
|
+
console.log(value);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
console.log(JSON.stringify(value, null, 2));
|
|
568
|
+
}
|
|
569
|
+
function renderError(error) {
|
|
570
|
+
if (error instanceof QuiltApiError) {
|
|
571
|
+
console.error(error.message);
|
|
572
|
+
if (error.body) {
|
|
573
|
+
console.error(JSON.stringify(error.body, null, 2));
|
|
574
|
+
}
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
if (error instanceof Error) {
|
|
578
|
+
console.error(error.message);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
console.error(String(error));
|
|
582
|
+
}
|
|
583
|
+
function printUsage() {
|
|
584
|
+
const scriptName = path.basename(fileURLToPath(import.meta.url), path.extname(fileURLToPath(import.meta.url)));
|
|
585
|
+
console.log([
|
|
586
|
+
`Usage: ${scriptName} [--base-url URL] [--api-key KEY|--token JWT] [--json] <command>`,
|
|
587
|
+
"",
|
|
588
|
+
"Commands:",
|
|
589
|
+
" health",
|
|
590
|
+
" build --tag <oci-ref> [--context dir] [--dockerfile Dockerfile] [--build-arg KEY=VALUE] [--target stage] [--no-wait]",
|
|
591
|
+
" container create [--name name] [--image ref-or-alias] [--oci] [--env KEY=VALUE] [--memory MB] [--cpu PERCENT] [--volume spec] [--workdir dir] [--wait] -- <command...>",
|
|
592
|
+
" container list [--state state]",
|
|
593
|
+
" container get <id-or-name>",
|
|
594
|
+
" container logs <id-or-name> [--limit N]",
|
|
595
|
+
" container remove <id-or-name> [--wait]",
|
|
596
|
+
" container exec <id-or-name> [--workdir dir] [--capture-output] [--detach] [--timeout-ms N] -- <command...>",
|
|
597
|
+
" oci pull --reference <oci-ref> [--force] [--platform platform] [--username user] [--password pass]",
|
|
598
|
+
" oci list [--filter value] [--include-digests]",
|
|
599
|
+
" oci inspect --reference <oci-ref>",
|
|
600
|
+
" oci history --reference <oci-ref>",
|
|
601
|
+
" operation get <operation-id>",
|
|
602
|
+
" operation wait <operation-id> [--timeout-ms N] [--interval-ms N]",
|
|
603
|
+
].join("\n"));
|
|
604
|
+
}
|
|
605
|
+
void main();
|
|
606
|
+
//# sourceMappingURL=cli.js.map
|