@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 ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }