@unikernelai/sandbox-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +290 -0
- package/package.json +38 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { UnikernelsClient, DEFAULT_BASE_URL, } from "@unikernelai/sandbox";
|
|
7
|
+
// ─── Config file: ~/.unik/config.json ───────────────────────────────────────
|
|
8
|
+
const CONFIG_DIR = join(homedir(), ".unik");
|
|
9
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
10
|
+
function loadConfig() {
|
|
11
|
+
if (!existsSync(CONFIG_FILE))
|
|
12
|
+
return {};
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function saveConfig(cfg) {
|
|
21
|
+
if (!existsSync(CONFIG_DIR))
|
|
22
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
23
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n", "utf8");
|
|
24
|
+
}
|
|
25
|
+
function getClient(overrides = {}) {
|
|
26
|
+
const cfg = loadConfig();
|
|
27
|
+
const apiKey = overrides.apiKey ?? cfg.api_key;
|
|
28
|
+
const authToken = overrides.authToken ?? cfg.auth_token;
|
|
29
|
+
const baseUrl = overrides.baseUrl ?? cfg.base_url ?? DEFAULT_BASE_URL;
|
|
30
|
+
if (!apiKey && !authToken) {
|
|
31
|
+
console.error("Error: no credentials found.\n" +
|
|
32
|
+
" Set an API key: unik auth set-key <key>\n" +
|
|
33
|
+
" Or set a JWT: unik auth set-token <jwt>\n" +
|
|
34
|
+
" Or login: unik auth login");
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
return new UnikernelsClient({ apiKey, authToken, baseUrl });
|
|
38
|
+
}
|
|
39
|
+
function jsonOut(data) {
|
|
40
|
+
console.log(JSON.stringify(data, null, 2));
|
|
41
|
+
}
|
|
42
|
+
function readStdin() {
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
let buf = "";
|
|
45
|
+
process.stdin.setEncoding("utf8");
|
|
46
|
+
process.stdin.on("data", (chunk) => (buf += chunk));
|
|
47
|
+
process.stdin.on("end", () => resolve(buf.trim()));
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// ─── CLI definition ──────────────────────────────────────────────────────────
|
|
51
|
+
program
|
|
52
|
+
.name("unik")
|
|
53
|
+
.description("Unikraft sandbox CLI — run code in unikernel-isolated sandboxes")
|
|
54
|
+
.version("1.0.0");
|
|
55
|
+
// ── auth ─────────────────────────────────────────────────────────────────────
|
|
56
|
+
const auth = program.command("auth").description("Manage credentials");
|
|
57
|
+
auth
|
|
58
|
+
.command("set-key <key>")
|
|
59
|
+
.description("Save an API key to ~/.unik/config.json")
|
|
60
|
+
.action((key) => {
|
|
61
|
+
const cfg = loadConfig();
|
|
62
|
+
cfg.api_key = key;
|
|
63
|
+
delete cfg.auth_token;
|
|
64
|
+
saveConfig(cfg);
|
|
65
|
+
console.log(`✓ API key saved (prefix: ${key.slice(0, 12)}...)`);
|
|
66
|
+
});
|
|
67
|
+
auth
|
|
68
|
+
.command("set-token <jwt>")
|
|
69
|
+
.description("Save a JWT bearer token (for admin operations like creating API keys)")
|
|
70
|
+
.action((jwt) => {
|
|
71
|
+
const cfg = loadConfig();
|
|
72
|
+
cfg.auth_token = jwt;
|
|
73
|
+
saveConfig(cfg);
|
|
74
|
+
console.log("✓ JWT saved");
|
|
75
|
+
});
|
|
76
|
+
auth
|
|
77
|
+
.command("set-url <url>")
|
|
78
|
+
.description("Override the base URL (default: DEFAULT_BASE_URL)")
|
|
79
|
+
.action((url) => {
|
|
80
|
+
const cfg = loadConfig();
|
|
81
|
+
cfg.base_url = url;
|
|
82
|
+
saveConfig(cfg);
|
|
83
|
+
console.log(`✓ Base URL set to ${url}`);
|
|
84
|
+
});
|
|
85
|
+
auth
|
|
86
|
+
.command("whoami")
|
|
87
|
+
.description("Show stored credentials")
|
|
88
|
+
.action(() => {
|
|
89
|
+
const cfg = loadConfig();
|
|
90
|
+
console.log("Base URL:", cfg.base_url ?? DEFAULT_BASE_URL);
|
|
91
|
+
console.log("API key: ", cfg.api_key ? cfg.api_key.slice(0, 16) + "..." : "(none)");
|
|
92
|
+
console.log("JWT: ", cfg.auth_token ? cfg.auth_token.slice(0, 20) + "..." : "(none)");
|
|
93
|
+
});
|
|
94
|
+
auth
|
|
95
|
+
.command("logout")
|
|
96
|
+
.description("Clear stored credentials")
|
|
97
|
+
.action(() => {
|
|
98
|
+
saveConfig({});
|
|
99
|
+
console.log("✓ Credentials cleared");
|
|
100
|
+
});
|
|
101
|
+
// ── api-keys ─────────────────────────────────────────────────────────────────
|
|
102
|
+
const keys = program.command("keys").description("Manage API keys (requires JWT)");
|
|
103
|
+
keys
|
|
104
|
+
.command("create")
|
|
105
|
+
.description("Create a new API key and save it locally")
|
|
106
|
+
.option("-l, --label <label>", "Human-readable label for the key")
|
|
107
|
+
.option("-e, --expires <date>", "Expiry date (ISO 8601, e.g. 2026-12-31T00:00:00Z)")
|
|
108
|
+
.option("--no-save", "Print the key without saving to ~/.unik/config.json")
|
|
109
|
+
.action(async (opts) => {
|
|
110
|
+
const client = getClient();
|
|
111
|
+
const key = await client.createApiKey({
|
|
112
|
+
label: opts.label,
|
|
113
|
+
expires_at: opts.expires,
|
|
114
|
+
});
|
|
115
|
+
console.log("\n✓ API key created:");
|
|
116
|
+
console.log(" Key: ", key.key);
|
|
117
|
+
console.log(" ID: ", key.id);
|
|
118
|
+
console.log(" Prefix: ", key.prefix);
|
|
119
|
+
if (key.expires_at)
|
|
120
|
+
console.log(" Expires:", key.expires_at);
|
|
121
|
+
if (opts.save !== false) {
|
|
122
|
+
const cfg = loadConfig();
|
|
123
|
+
cfg.api_key = key.key;
|
|
124
|
+
delete cfg.auth_token;
|
|
125
|
+
saveConfig(cfg);
|
|
126
|
+
console.log("\n✓ Key saved to ~/.unik/config.json. Future commands will use it automatically.");
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log("\n ⚠ Store this key — it will NOT be shown again.");
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
keys
|
|
133
|
+
.command("list")
|
|
134
|
+
.description("List all API keys for this account")
|
|
135
|
+
.action(async () => {
|
|
136
|
+
const client = getClient();
|
|
137
|
+
const list = await client.listApiKeys();
|
|
138
|
+
if (list.length === 0) {
|
|
139
|
+
console.log("No API keys found.");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
console.log(`\n${list.length} key(s):\n`);
|
|
143
|
+
for (const k of list) {
|
|
144
|
+
const status = k.revoked_at ? "REVOKED" : "active";
|
|
145
|
+
console.log(` ${k.prefix}... [${status}] id=${k.id} label=${k.label ?? "-"} created=${k.created_at.slice(0, 10)}`);
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
keys
|
|
149
|
+
.command("revoke <id>")
|
|
150
|
+
.description("Revoke an API key by its ID")
|
|
151
|
+
.action(async (id) => {
|
|
152
|
+
const client = getClient();
|
|
153
|
+
await client.revokeApiKey(id);
|
|
154
|
+
console.log(`✓ Key ${id} revoked`);
|
|
155
|
+
});
|
|
156
|
+
// ── run ───────────────────────────────────────────────────────────────────────
|
|
157
|
+
program
|
|
158
|
+
.command("run <language> [code]")
|
|
159
|
+
.description("Execute code directly (stateless). Language: python | nodejs | shell.\n" +
|
|
160
|
+
" Pass code as argument or pipe via stdin:\n" +
|
|
161
|
+
" unik run python 'print(\"hello\")'\n" +
|
|
162
|
+
" echo 'print(1+1)' | unik run python\n" +
|
|
163
|
+
" cat script.py | unik run python")
|
|
164
|
+
.option("-t, --timeout <ms>", "Timeout in milliseconds (max 8000)", "8000")
|
|
165
|
+
.option("--json", "Output raw JSON response")
|
|
166
|
+
.action(async (language, codeArg, opts) => {
|
|
167
|
+
const code = codeArg ?? (await readStdin());
|
|
168
|
+
if (!code) {
|
|
169
|
+
console.error("Error: provide code as argument or pipe via stdin");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
const client = getClient();
|
|
173
|
+
const result = await client.execute({
|
|
174
|
+
language: language,
|
|
175
|
+
code,
|
|
176
|
+
timeout_ms: parseInt(opts.timeout),
|
|
177
|
+
});
|
|
178
|
+
if (opts.json) {
|
|
179
|
+
jsonOut(result);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (result.stdout)
|
|
183
|
+
process.stdout.write(result.stdout);
|
|
184
|
+
if (result.stderr)
|
|
185
|
+
process.stderr.write(result.stderr);
|
|
186
|
+
if (result.status !== "success") {
|
|
187
|
+
console.error(`\n✗ Exited with status: ${result.status} (${result.execution_time_ms}ms)`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
// ── sandbox ───────────────────────────────────────────────────────────────────
|
|
192
|
+
const sbx = program.command("sandbox").description("Manage persistent sandbox sessions");
|
|
193
|
+
sbx
|
|
194
|
+
.command("create")
|
|
195
|
+
.description("Create a new sandbox and print its ID")
|
|
196
|
+
.option("--json", "Output raw JSON")
|
|
197
|
+
.action(async (opts) => {
|
|
198
|
+
const client = getClient();
|
|
199
|
+
const sandbox = await client.createSandbox();
|
|
200
|
+
if (opts.json) {
|
|
201
|
+
jsonOut({ id: sandbox.id });
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
console.log(sandbox.id);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
sbx
|
|
208
|
+
.command("exec <id> <language> [code]")
|
|
209
|
+
.description("Run code inside an existing sandbox")
|
|
210
|
+
.option("-t, --timeout <ms>", "Timeout in milliseconds (max 8000)", "8000")
|
|
211
|
+
.option("--json", "Output raw JSON")
|
|
212
|
+
.action(async (id, language, codeArg, opts) => {
|
|
213
|
+
const code = codeArg ?? (await readStdin());
|
|
214
|
+
if (!code) {
|
|
215
|
+
console.error("Error: provide code as argument or pipe via stdin");
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const client = getClient();
|
|
219
|
+
const sandbox = client.getSandboxHandle(id);
|
|
220
|
+
const result = await sandbox.execute({
|
|
221
|
+
language: language,
|
|
222
|
+
code,
|
|
223
|
+
timeout_ms: parseInt(opts.timeout),
|
|
224
|
+
});
|
|
225
|
+
if (opts.json) {
|
|
226
|
+
jsonOut(result);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (result.stdout)
|
|
230
|
+
process.stdout.write(result.stdout);
|
|
231
|
+
if (result.stderr)
|
|
232
|
+
process.stderr.write(result.stderr);
|
|
233
|
+
if (result.status !== "success")
|
|
234
|
+
process.exit(1);
|
|
235
|
+
});
|
|
236
|
+
sbx
|
|
237
|
+
.command("delete <id>")
|
|
238
|
+
.description("Delete a sandbox")
|
|
239
|
+
.action(async (id) => {
|
|
240
|
+
const client = getClient();
|
|
241
|
+
await client.deleteSandbox(id);
|
|
242
|
+
console.log(`✓ Sandbox ${id} deleted`);
|
|
243
|
+
});
|
|
244
|
+
sbx
|
|
245
|
+
.command("upload <id> <path> [content]")
|
|
246
|
+
.description("Upload a file into a sandbox (base64 content, or pipe stdin)")
|
|
247
|
+
.action(async (id, path, contentArg) => {
|
|
248
|
+
const raw = contentArg ?? (await readStdin());
|
|
249
|
+
const b64 = Buffer.from(raw).toString("base64");
|
|
250
|
+
const client = getClient();
|
|
251
|
+
const sandbox = client.getSandboxHandle(id);
|
|
252
|
+
const file = await sandbox.uploadFile(path, b64);
|
|
253
|
+
console.log(`✓ Uploaded ${file.path} (${file.size_bytes} bytes)`);
|
|
254
|
+
});
|
|
255
|
+
sbx
|
|
256
|
+
.command("ls <id> [path]")
|
|
257
|
+
.description("List files in a sandbox")
|
|
258
|
+
.action(async (id, path = "") => {
|
|
259
|
+
const client = getClient();
|
|
260
|
+
const sandbox = client.getSandboxHandle(id);
|
|
261
|
+
const files = await sandbox.listFiles(path);
|
|
262
|
+
if (files.length === 0) {
|
|
263
|
+
console.log("(empty)");
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
for (const f of files) {
|
|
267
|
+
console.log(` ${f.path} (${f.size_bytes}B)`);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
// ── status ────────────────────────────────────────────────────────────────────
|
|
271
|
+
program
|
|
272
|
+
.command("status")
|
|
273
|
+
.description("Check if the server is healthy")
|
|
274
|
+
.option("--json", "Output raw JSON")
|
|
275
|
+
.action(async (opts) => {
|
|
276
|
+
const cfg = loadConfig();
|
|
277
|
+
const baseUrl = cfg.base_url ?? DEFAULT_BASE_URL;
|
|
278
|
+
const res = await fetch(`${baseUrl}/readyz`).then((r) => r.json());
|
|
279
|
+
if (opts.json) {
|
|
280
|
+
jsonOut(res);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
const r = res;
|
|
284
|
+
console.log("Status:", r.status);
|
|
285
|
+
for (const [name, check] of Object.entries(r.checks ?? {})) {
|
|
286
|
+
console.log(` ${check.ok ? "✓" : "✗"} ${name}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
program.parseAsync(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@unikernelai/sandbox-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI for the Unikraft sandbox API — run code, manage sandboxes, provision API keys",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"unik": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/cli.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc -p tsconfig.json",
|
|
15
|
+
"dev": "node --loader ts-node/esm src/cli.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@unikernelai/sandbox": "^1.1.0",
|
|
19
|
+
"commander": "^12.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^20.0.0",
|
|
23
|
+
"typescript": "^5.4.0"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"sandbox",
|
|
27
|
+
"unikraft",
|
|
28
|
+
"code-execution",
|
|
29
|
+
"cli"
|
|
30
|
+
],
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
}
|
|
38
|
+
}
|