svamp-cli 0.1.48 → 0.1.49

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,514 @@
1
+ function getSandboxEnv() {
2
+ return {
3
+ apiUrl: process.env.SANDBOX_API_URL || "",
4
+ apiKey: process.env.SANDBOX_API_KEY || "",
5
+ namespace: process.env.SANDBOX_NAMESPACE || "",
6
+ sandboxId: process.env.SANDBOX_ID || ""
7
+ };
8
+ }
9
+ function requireSandboxEnv() {
10
+ const env = getSandboxEnv();
11
+ if (!env.apiUrl) {
12
+ throw new Error(
13
+ "SANDBOX_API_URL is not set. Service commands require a sandbox environment.\nThis is normally set by the Svamp provisioner when running in a cloud machine."
14
+ );
15
+ }
16
+ if (!env.apiKey) {
17
+ throw new Error("SANDBOX_API_KEY is not set.");
18
+ }
19
+ if (!env.namespace) {
20
+ throw new Error("SANDBOX_NAMESPACE is not set.");
21
+ }
22
+ return env;
23
+ }
24
+ function requireSandboxApiEnv() {
25
+ const env = getSandboxEnv();
26
+ if (!env.apiUrl) {
27
+ throw new Error(
28
+ "SANDBOX_API_URL is not set. Set it via environment variable or `svamp service config`."
29
+ );
30
+ }
31
+ if (!env.apiKey) {
32
+ throw new Error("SANDBOX_API_KEY is not set.");
33
+ }
34
+ return env;
35
+ }
36
+ async function sandboxFetch(env, path, init) {
37
+ const url = `${env.apiUrl.replace(/\/+$/, "")}${path}`;
38
+ const headers = {
39
+ "Authorization": `Bearer ${env.apiKey}`,
40
+ "Content-Type": "application/json",
41
+ ...init?.headers || {}
42
+ };
43
+ const res = await fetch(url, { ...init, headers });
44
+ if (!res.ok) {
45
+ const body = await res.text().catch(() => "");
46
+ let detail = body;
47
+ try {
48
+ detail = JSON.parse(body).detail || body;
49
+ } catch {
50
+ }
51
+ throw new Error(`${res.status} ${res.statusText}: ${detail}`);
52
+ }
53
+ return res;
54
+ }
55
+ async function createServiceGroup(name, ports, options) {
56
+ const env = requireSandboxEnv();
57
+ const body = {};
58
+ if (ports.length === 1 && options?.subdomain) {
59
+ body.port = ports[0];
60
+ body.subdomain = options.subdomain;
61
+ } else {
62
+ body.ports = ports.map((p) => ({ port: p }));
63
+ }
64
+ if (options?.healthPath) {
65
+ body.health_path = options.healthPath;
66
+ if (options.healthInterval) {
67
+ body.health_interval = options.healthInterval;
68
+ }
69
+ }
70
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}`, {
71
+ method: "POST",
72
+ body: JSON.stringify(body)
73
+ });
74
+ return res.json();
75
+ }
76
+ async function listServiceGroups() {
77
+ const env = requireSandboxEnv();
78
+ const res = await sandboxFetch(env, `/services/${env.namespace}`);
79
+ return res.json();
80
+ }
81
+ async function getServiceGroup(name) {
82
+ const env = requireSandboxEnv();
83
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}`);
84
+ return res.json();
85
+ }
86
+ async function deleteServiceGroup(name) {
87
+ const env = requireSandboxEnv();
88
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}`, {
89
+ method: "DELETE"
90
+ });
91
+ return res.json();
92
+ }
93
+ async function addPort(name, port, subdomain) {
94
+ const env = requireSandboxEnv();
95
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}/ports`, {
96
+ method: "POST",
97
+ body: JSON.stringify({ port, subdomain })
98
+ });
99
+ return res.json();
100
+ }
101
+ async function removePort(name, port) {
102
+ const env = requireSandboxEnv();
103
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}/ports/${port}`, {
104
+ method: "DELETE"
105
+ });
106
+ return res.json();
107
+ }
108
+ async function addBackend(name, sandboxId) {
109
+ const env = requireSandboxEnv();
110
+ const sid = sandboxId || env.sandboxId;
111
+ if (!sid) {
112
+ throw new Error(
113
+ "No sandbox ID provided and SANDBOX_ID is not set.\nUse --sandbox-id <id> to specify which sandbox to add."
114
+ );
115
+ }
116
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}/backends`, {
117
+ method: "POST",
118
+ body: JSON.stringify({ sandbox_id: sid })
119
+ });
120
+ return res.json();
121
+ }
122
+ async function removeBackend(name, sandboxId) {
123
+ const env = requireSandboxEnv();
124
+ const sid = sandboxId || env.sandboxId;
125
+ if (!sid) {
126
+ throw new Error(
127
+ "No sandbox ID provided and SANDBOX_ID is not set.\nUse --sandbox-id <id> to specify which sandbox to remove."
128
+ );
129
+ }
130
+ const res = await sandboxFetch(env, `/services/${env.namespace}/${name}/backends/${sid}`, {
131
+ method: "DELETE"
132
+ });
133
+ return res.json();
134
+ }
135
+
136
+ function getFlag(args, flag) {
137
+ const idx = args.indexOf(flag);
138
+ return idx !== -1 && idx + 1 < args.length ? args[idx + 1] : void 0;
139
+ }
140
+ function getAllFlags(args, flag) {
141
+ const values = [];
142
+ for (let i = 0; i < args.length; i++) {
143
+ if (args[i] === flag && i + 1 < args.length) {
144
+ values.push(args[i + 1]);
145
+ i++;
146
+ }
147
+ }
148
+ return values;
149
+ }
150
+ function hasFlag(args, ...flags) {
151
+ return flags.some((f) => args.includes(f));
152
+ }
153
+ function positionalArgs(args) {
154
+ const result = [];
155
+ for (let i = 0; i < args.length; i++) {
156
+ if (args[i].startsWith("--")) {
157
+ if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
158
+ i++;
159
+ }
160
+ continue;
161
+ }
162
+ result.push(args[i]);
163
+ }
164
+ return result;
165
+ }
166
+ function parsePorts(args) {
167
+ const portStrs = getAllFlags(args, "--port");
168
+ if (portStrs.length === 0) return [];
169
+ const ports = [];
170
+ for (const s of portStrs) {
171
+ const p = parseInt(s, 10);
172
+ if (isNaN(p) || p < 1 || p > 65535) {
173
+ console.error(`Error: invalid port '${s}' \u2014 must be 1-65535`);
174
+ process.exit(1);
175
+ }
176
+ ports.push(p);
177
+ }
178
+ return ports;
179
+ }
180
+ function printServiceGroupInfo(info) {
181
+ console.log(`Service group: ${info.name}`);
182
+ console.log(` Namespace: ${info.namespace}`);
183
+ console.log(` K8s svc: ${info.k8s_service}`);
184
+ console.log(` Created: ${info.created_at}`);
185
+ if (info.health_path) {
186
+ console.log(` Health: ${info.health_path} (every ${info.health_interval}s)`);
187
+ }
188
+ console.log(` Ports (${info.ports.length}):`);
189
+ for (const p of info.ports) {
190
+ console.log(` ${p.port} \u2192 ${p.url}`);
191
+ }
192
+ console.log(` Backends (${info.backends.length}):`);
193
+ for (const b of info.backends) {
194
+ const healthTag = b.healthy === false ? " [unhealthy]" : b.healthy === true ? "" : "";
195
+ const failInfo = b.consecutive_failures ? ` (${b.consecutive_failures} failures)` : "";
196
+ console.log(` - ${b.sandbox_id} (${b.pod_ip})${healthTag}${failInfo}`);
197
+ }
198
+ }
199
+ function printServiceGroupListItem(g) {
200
+ console.log(` ${g.name}`);
201
+ if (g.ports.length > 1) {
202
+ for (const p of g.ports) {
203
+ console.log(` ${p.port} \u2192 ${p.url}`);
204
+ }
205
+ } else {
206
+ console.log(` URL: ${g.url}`);
207
+ console.log(` Port: ${g.port}`);
208
+ }
209
+ console.log(` Backends: ${g.backend_count}`);
210
+ }
211
+ async function serviceCreate(args) {
212
+ const positional = positionalArgs(args);
213
+ const name = positional[0];
214
+ const ports = parsePorts(args);
215
+ const subdomain = getFlag(args, "--subdomain");
216
+ const healthPath = getFlag(args, "--health-path");
217
+ const healthIntervalStr = getFlag(args, "--health-interval");
218
+ if (!name || ports.length === 0) {
219
+ console.error("Usage: svamp service create <name> --port <port> [--port <port2>] [--health-path <path>]");
220
+ process.exit(1);
221
+ }
222
+ const healthInterval = healthIntervalStr ? parseInt(healthIntervalStr, 10) : void 0;
223
+ try {
224
+ const result = await createServiceGroup(name, ports, {
225
+ subdomain,
226
+ healthPath,
227
+ healthInterval
228
+ });
229
+ console.log(`Service group created: ${result.name}`);
230
+ for (const p of result.ports) {
231
+ console.log(` ${p.port} \u2192 ${p.url}`);
232
+ }
233
+ console.log(` K8s svc: ${result.k8s_service}`);
234
+ console.log(` Backends: ${result.backends.length}`);
235
+ } catch (err) {
236
+ console.error(`Error creating service group: ${err.message}`);
237
+ process.exit(1);
238
+ }
239
+ }
240
+ async function serviceList(args) {
241
+ const jsonOutput = hasFlag(args, "--json");
242
+ try {
243
+ const groups = await listServiceGroups();
244
+ if (jsonOutput) {
245
+ console.log(JSON.stringify(groups, null, 2));
246
+ return;
247
+ }
248
+ if (groups.length === 0) {
249
+ console.log("No service groups found.");
250
+ return;
251
+ }
252
+ console.log(`Service groups (${groups.length}):
253
+ `);
254
+ for (const g of groups) {
255
+ printServiceGroupListItem(g);
256
+ console.log();
257
+ }
258
+ } catch (err) {
259
+ console.error(`Error listing service groups: ${err.message}`);
260
+ process.exit(1);
261
+ }
262
+ }
263
+ async function serviceInfo(args) {
264
+ const positional = positionalArgs(args);
265
+ const name = positional[0];
266
+ const jsonOutput = hasFlag(args, "--json");
267
+ if (!name) {
268
+ console.error("Usage: svamp service info <name> [--json]");
269
+ process.exit(1);
270
+ }
271
+ try {
272
+ const info = await getServiceGroup(name);
273
+ if (jsonOutput) {
274
+ console.log(JSON.stringify(info, null, 2));
275
+ return;
276
+ }
277
+ printServiceGroupInfo(info);
278
+ } catch (err) {
279
+ console.error(`Error getting service group: ${err.message}`);
280
+ process.exit(1);
281
+ }
282
+ }
283
+ async function serviceDelete(args) {
284
+ const positional = positionalArgs(args);
285
+ const name = positional[0];
286
+ if (!name) {
287
+ console.error("Usage: svamp service delete <name>");
288
+ process.exit(1);
289
+ }
290
+ try {
291
+ await deleteServiceGroup(name);
292
+ console.log(`Service group '${name}' deleted.`);
293
+ } catch (err) {
294
+ console.error(`Error deleting service group: ${err.message}`);
295
+ process.exit(1);
296
+ }
297
+ }
298
+ async function serviceAddBackend(args) {
299
+ const positional = positionalArgs(args);
300
+ const name = positional[0];
301
+ const sandboxId = getFlag(args, "--sandbox-id");
302
+ if (!name) {
303
+ console.error("Usage: svamp service add-backend <name> [--sandbox-id <id>]");
304
+ process.exit(1);
305
+ }
306
+ try {
307
+ const result = await addBackend(name, sandboxId);
308
+ console.log(`Backend added to '${name}': ${result.sandbox_id} (${result.pod_ip})`);
309
+ console.log(` Total backends: ${result.backend_count}`);
310
+ } catch (err) {
311
+ console.error(`Error adding backend: ${err.message}`);
312
+ process.exit(1);
313
+ }
314
+ }
315
+ async function serviceRemoveBackend(args) {
316
+ const positional = positionalArgs(args);
317
+ const name = positional[0];
318
+ const sandboxId = getFlag(args, "--sandbox-id");
319
+ if (!name) {
320
+ console.error("Usage: svamp service remove-backend <name> [--sandbox-id <id>]");
321
+ process.exit(1);
322
+ }
323
+ try {
324
+ const result = await removeBackend(name, sandboxId);
325
+ console.log(`Backend removed from '${name}': ${result.sandbox_id}`);
326
+ console.log(` Remaining backends: ${result.backend_count}`);
327
+ } catch (err) {
328
+ console.error(`Error removing backend: ${err.message}`);
329
+ process.exit(1);
330
+ }
331
+ }
332
+ async function serviceAddPort(args) {
333
+ const positional = positionalArgs(args);
334
+ const name = positional[0];
335
+ const ports = parsePorts(args);
336
+ const subdomain = getFlag(args, "--subdomain");
337
+ if (!name || ports.length !== 1) {
338
+ console.error("Usage: svamp service add-port <name> --port <port> [--subdomain <sub>]");
339
+ process.exit(1);
340
+ }
341
+ try {
342
+ const result = await addPort(name, ports[0], subdomain);
343
+ console.log(`Port added to '${name}': ${result.port} \u2192 ${result.url}`);
344
+ console.log(` Total ports: ${result.total_ports}`);
345
+ } catch (err) {
346
+ console.error(`Error adding port: ${err.message}`);
347
+ process.exit(1);
348
+ }
349
+ }
350
+ async function serviceRemovePort(args) {
351
+ const positional = positionalArgs(args);
352
+ const name = positional[0];
353
+ const ports = parsePorts(args);
354
+ if (!name || ports.length !== 1) {
355
+ console.error("Usage: svamp service remove-port <name> --port <port>");
356
+ process.exit(1);
357
+ }
358
+ try {
359
+ const result = await removePort(name, ports[0]);
360
+ console.log(`Port removed from '${name}': ${result.port}`);
361
+ console.log(` Remaining ports: ${result.total_ports}`);
362
+ } catch (err) {
363
+ console.error(`Error removing port: ${err.message}`);
364
+ process.exit(1);
365
+ }
366
+ }
367
+ async function serviceExpose(args) {
368
+ const positional = positionalArgs(args);
369
+ const name = positional[0];
370
+ const ports = parsePorts(args);
371
+ const subdomain = getFlag(args, "--subdomain");
372
+ const healthPath = getFlag(args, "--health-path");
373
+ const healthIntervalStr = getFlag(args, "--health-interval");
374
+ if (!name || ports.length === 0) {
375
+ console.error("Usage: svamp service expose <name> --port <port> [--port <port2>] [--health-path <path>]");
376
+ process.exit(1);
377
+ }
378
+ const healthInterval = healthIntervalStr ? parseInt(healthIntervalStr, 10) : void 0;
379
+ const env = getSandboxEnv();
380
+ try {
381
+ const group = await createServiceGroup(name, ports, {
382
+ subdomain,
383
+ healthPath,
384
+ healthInterval
385
+ });
386
+ if (env.sandboxId) {
387
+ const result = await addBackend(name);
388
+ console.log(`Backend added: ${result.sandbox_id} (${result.pod_ip})`);
389
+ console.log(`
390
+ Service is live:`);
391
+ for (const p of group.ports) {
392
+ console.log(` ${p.port} \u2192 ${p.url}`);
393
+ }
394
+ } else {
395
+ console.log(`No SANDBOX_ID detected \u2014 starting reverse tunnel.`);
396
+ const { runTunnel } = await import('./tunnel-BXEroHJF.mjs');
397
+ await runTunnel(name, ports);
398
+ }
399
+ } catch (err) {
400
+ console.error(`Error exposing service: ${err.message}`);
401
+ process.exit(1);
402
+ }
403
+ }
404
+ async function serviceTunnel(args) {
405
+ const positional = positionalArgs(args);
406
+ const name = positional[0];
407
+ const ports = parsePorts(args);
408
+ if (!name || ports.length === 0) {
409
+ console.error("Usage: svamp service tunnel <name> --port <port> [--port <port2>]");
410
+ process.exit(1);
411
+ }
412
+ const { runTunnel } = await import('./tunnel-BXEroHJF.mjs');
413
+ await runTunnel(name, ports);
414
+ }
415
+ async function handleServiceCommand() {
416
+ const args = process.argv.slice(2);
417
+ const serviceArgs = args.slice(1);
418
+ const sub = serviceArgs[0];
419
+ if (!sub || sub === "--help" || sub === "-h") {
420
+ printServiceHelp();
421
+ return;
422
+ }
423
+ const commandArgs = serviceArgs.slice(1);
424
+ if (sub === "create") {
425
+ await serviceCreate(commandArgs);
426
+ } else if (sub === "list" || sub === "ls") {
427
+ await serviceList(commandArgs);
428
+ } else if (sub === "info" || sub === "show") {
429
+ await serviceInfo(commandArgs);
430
+ } else if (sub === "delete" || sub === "rm") {
431
+ await serviceDelete(commandArgs);
432
+ } else if (sub === "add-backend" || sub === "add") {
433
+ await serviceAddBackend(commandArgs);
434
+ } else if (sub === "remove-backend" || sub === "remove") {
435
+ await serviceRemoveBackend(commandArgs);
436
+ } else if (sub === "add-port") {
437
+ await serviceAddPort(commandArgs);
438
+ } else if (sub === "remove-port") {
439
+ await serviceRemovePort(commandArgs);
440
+ } else if (sub === "expose") {
441
+ await serviceExpose(commandArgs);
442
+ } else if (sub === "tunnel") {
443
+ await serviceTunnel(commandArgs);
444
+ } else {
445
+ console.error(`Unknown service command: ${sub}`);
446
+ printServiceHelp();
447
+ process.exit(1);
448
+ }
449
+ }
450
+ function printServiceHelp() {
451
+ console.log(`
452
+ svamp service \u2014 Manage load-balanced service groups
453
+
454
+ Usage:
455
+ svamp service create <name> --port <port> [--port <port2>] [options] Create a service group
456
+ svamp service list [--json] List service groups
457
+ svamp service info <name> [--json] Show service group details
458
+ svamp service delete <name> Delete a service group
459
+ svamp service add-backend <name> [--sandbox-id <id>] Add a pod as backend
460
+ svamp service remove-backend <name> [--sandbox-id <id>] Remove a backend
461
+ svamp service add-port <name> --port <port> [--subdomain <sub>] Add port to existing group
462
+ svamp service remove-port <name> --port <port> Remove port from group
463
+ svamp service expose <name> --port <port> [--port <port2>] [options] Create + join (auto-detects tunnel)
464
+ svamp service tunnel <name> --port <port> [--port <port2>] Tunnel local ports to service group
465
+
466
+ Create/Expose options:
467
+ --health-path <path> Health check endpoint (e.g., /health)
468
+ --health-interval <sec> Health check interval in seconds (default: 30)
469
+ --subdomain <sub> Custom subdomain (single-port only)
470
+
471
+ Service groups expose HTTP services via stable URLs with K8s-native load
472
+ balancing. Multiple pods can serve the same service \u2014 K8s handles traffic
473
+ distribution automatically.
474
+
475
+ Multi-port: Use --port multiple times to expose several ports under one
476
+ service group. Each port gets its own unique subdomain and URL.
477
+
478
+ Auto-detect: 'expose' automatically detects the environment:
479
+ - Cloud sandbox (SANDBOX_ID set): adds pod as K8s backend
480
+ - Local machine (no SANDBOX_ID): starts reverse tunnel
481
+
482
+ Environment variables (set by provisioner):
483
+ SANDBOX_API_URL Agent-sandbox API base URL
484
+ SANDBOX_API_KEY API authentication token
485
+ SANDBOX_NAMESPACE User's sandbox namespace
486
+ SANDBOX_ID This pod's sandbox ID
487
+
488
+ Examples:
489
+ svamp service expose my-api --port 8000
490
+ svamp service expose my-api --port 8000 --port 3000 --health-path /health
491
+ svamp service add-port my-api --port 5000
492
+ svamp service remove-port my-api --port 5000
493
+ svamp service list
494
+ svamp service info my-api
495
+ svamp service delete my-api
496
+ `.trim());
497
+ }
498
+
499
+ var commands = /*#__PURE__*/Object.freeze({
500
+ __proto__: null,
501
+ handleServiceCommand: handleServiceCommand,
502
+ serviceAddBackend: serviceAddBackend,
503
+ serviceAddPort: serviceAddPort,
504
+ serviceCreate: serviceCreate,
505
+ serviceDelete: serviceDelete,
506
+ serviceExpose: serviceExpose,
507
+ serviceInfo: serviceInfo,
508
+ serviceList: serviceList,
509
+ serviceRemoveBackend: serviceRemoveBackend,
510
+ serviceRemovePort: serviceRemovePort,
511
+ serviceTunnel: serviceTunnel
512
+ });
513
+
514
+ export { commands as c, requireSandboxApiEnv as r };