@douglas-agent/sandbank-cli 0.5.4

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.
Files changed (68) hide show
  1. package/README.md +229 -0
  2. package/dist/cli/api.d.ts +6 -0
  3. package/dist/cli/api.d.ts.map +1 -0
  4. package/dist/cli/api.js +8 -0
  5. package/dist/cli/api.test.d.ts +2 -0
  6. package/dist/cli/api.test.d.ts.map +1 -0
  7. package/dist/cli/api.test.js +31 -0
  8. package/dist/cli/auth.d.ts +25 -0
  9. package/dist/cli/auth.d.ts.map +1 -0
  10. package/dist/cli/auth.js +34 -0
  11. package/dist/cli/auth.test.d.ts +2 -0
  12. package/dist/cli/auth.test.d.ts.map +1 -0
  13. package/dist/cli/auth.test.js +89 -0
  14. package/dist/cli/commands/addons.d.ts +3 -0
  15. package/dist/cli/commands/addons.d.ts.map +1 -0
  16. package/dist/cli/commands/addons.js +54 -0
  17. package/dist/cli/commands/clone.d.ts +3 -0
  18. package/dist/cli/commands/clone.d.ts.map +1 -0
  19. package/dist/cli/commands/clone.js +18 -0
  20. package/dist/cli/commands/commands.test.d.ts +2 -0
  21. package/dist/cli/commands/commands.test.d.ts.map +1 -0
  22. package/dist/cli/commands/commands.test.js +439 -0
  23. package/dist/cli/commands/config.d.ts +3 -0
  24. package/dist/cli/commands/config.d.ts.map +1 -0
  25. package/dist/cli/commands/config.js +57 -0
  26. package/dist/cli/commands/create.d.ts +3 -0
  27. package/dist/cli/commands/create.d.ts.map +1 -0
  28. package/dist/cli/commands/create.js +30 -0
  29. package/dist/cli/commands/destroy.d.ts +3 -0
  30. package/dist/cli/commands/destroy.d.ts.map +1 -0
  31. package/dist/cli/commands/destroy.js +16 -0
  32. package/dist/cli/commands/exec.d.ts +3 -0
  33. package/dist/cli/commands/exec.d.ts.map +1 -0
  34. package/dist/cli/commands/exec.js +22 -0
  35. package/dist/cli/commands/get.d.ts +3 -0
  36. package/dist/cli/commands/get.d.ts.map +1 -0
  37. package/dist/cli/commands/get.js +21 -0
  38. package/dist/cli/commands/help.d.ts +2 -0
  39. package/dist/cli/commands/help.d.ts.map +1 -0
  40. package/dist/cli/commands/help.js +39 -0
  41. package/dist/cli/commands/keep.d.ts +3 -0
  42. package/dist/cli/commands/keep.d.ts.map +1 -0
  43. package/dist/cli/commands/keep.js +25 -0
  44. package/dist/cli/commands/list.d.ts +3 -0
  45. package/dist/cli/commands/list.d.ts.map +1 -0
  46. package/dist/cli/commands/list.js +14 -0
  47. package/dist/cli/commands/login.d.ts +3 -0
  48. package/dist/cli/commands/login.d.ts.map +1 -0
  49. package/dist/cli/commands/login.js +25 -0
  50. package/dist/cli/commands/snapshot.d.ts +3 -0
  51. package/dist/cli/commands/snapshot.d.ts.map +1 -0
  52. package/dist/cli/commands/snapshot.js +78 -0
  53. package/dist/cli/config.d.ts +9 -0
  54. package/dist/cli/config.d.ts.map +1 -0
  55. package/dist/cli/config.js +27 -0
  56. package/dist/cli/config.test.d.ts +2 -0
  57. package/dist/cli/config.test.d.ts.map +1 -0
  58. package/dist/cli/config.test.js +60 -0
  59. package/dist/cli/index.d.ts +8 -0
  60. package/dist/cli/index.d.ts.map +1 -0
  61. package/dist/cli/index.js +78 -0
  62. package/dist/cli/index.test.d.ts +2 -0
  63. package/dist/cli/index.test.d.ts.map +1 -0
  64. package/dist/cli/index.test.js +126 -0
  65. package/dist/index.d.ts +13 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +12 -0
  68. package/package.json +53 -0
@@ -0,0 +1,439 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ // Mock the API client
3
+ const mockX402Fetch = vi.fn();
4
+ const mockX402FetchRaw = vi.fn();
5
+ vi.mock('../api.js', () => ({
6
+ createApiClient: () => ({
7
+ x402Fetch: mockX402Fetch,
8
+ x402FetchRaw: mockX402FetchRaw,
9
+ baseUrl: 'https://cloud.sandbank.dev',
10
+ }),
11
+ printJson: (v) => console.log(JSON.stringify(v, null, 2)),
12
+ }));
13
+ // Mock config for login/config commands
14
+ vi.mock('../config.js', () => {
15
+ let store = {};
16
+ return {
17
+ loadCredentials: vi.fn(() => ({ ...store })),
18
+ saveCredentials: vi.fn((c) => { store = { ...c }; }),
19
+ maskSecret: (v) => v.length > 8 ? v.slice(0, 4) + '...' + v.slice(-4) : '****',
20
+ };
21
+ });
22
+ import { createCommand } from './create.js';
23
+ import { listCommand } from './list.js';
24
+ import { getCommand } from './get.js';
25
+ import { destroyCommand } from './destroy.js';
26
+ import { execCommand } from './exec.js';
27
+ import { cloneCommand } from './clone.js';
28
+ import { keepCommand } from './keep.js';
29
+ import { addonsCommand } from './addons.js';
30
+ import { snapshotCommand } from './snapshot.js';
31
+ import { loginCommand } from './login.js';
32
+ import { configCommand } from './config.js';
33
+ import { helpCommand } from './help.js';
34
+ import { saveCredentials, loadCredentials } from '../config.js';
35
+ // Capture console output
36
+ let stdout = [];
37
+ let stderr = [];
38
+ const origLog = console.log;
39
+ const origErr = console.error;
40
+ const origWrite = process.stdout.write;
41
+ const origErrWrite = process.stderr.write;
42
+ beforeEach(() => {
43
+ stdout = [];
44
+ stderr = [];
45
+ console.log = (...args) => { stdout.push(args.map(String).join(' ')); };
46
+ console.error = (...args) => { stderr.push(args.map(String).join(' ')); };
47
+ process.stdout.write = ((s) => { stdout.push(s); return true; });
48
+ process.stderr.write = ((s) => { stderr.push(s); return true; });
49
+ mockX402Fetch.mockReset();
50
+ vi.mocked(saveCredentials).mockClear();
51
+ });
52
+ afterEach(() => {
53
+ console.log = origLog;
54
+ console.error = origErr;
55
+ process.stdout.write = origWrite;
56
+ process.stderr.write = origErrWrite;
57
+ });
58
+ // Prevent process.exit from actually exiting
59
+ const mockExit = vi.spyOn(process, 'exit').mockImplementation((code) => {
60
+ throw new Error(`process.exit(${code})`);
61
+ });
62
+ describe('helpCommand', () => {
63
+ it('prints help text', () => {
64
+ helpCommand();
65
+ expect(stdout.join('\n')).toContain('sandbank');
66
+ expect(stdout.join('\n')).toContain('create');
67
+ expect(stdout.join('\n')).toContain('exec');
68
+ });
69
+ });
70
+ describe('loginCommand', () => {
71
+ it('saves API key', () => {
72
+ loginCommand([], { apiKey: 'test-key-12345678' });
73
+ expect(vi.mocked(saveCredentials)).toHaveBeenCalled();
74
+ expect(stdout.join('\n')).toContain('Credentials saved');
75
+ });
76
+ it('saves wallet key', () => {
77
+ loginCommand([], { walletKey: '0xabcdef1234567890' });
78
+ expect(vi.mocked(saveCredentials)).toHaveBeenCalled();
79
+ });
80
+ it('saves URL', () => {
81
+ loginCommand([], { url: 'https://custom.dev' });
82
+ expect(vi.mocked(saveCredentials)).toHaveBeenCalled();
83
+ expect(stdout.join('\n')).toContain('https://custom.dev');
84
+ });
85
+ it('exits with error when no flags provided', () => {
86
+ expect(() => loginCommand([], {})).toThrow('process.exit(1)');
87
+ expect(stderr.join('\n')).toContain('Usage');
88
+ });
89
+ });
90
+ describe('configCommand', () => {
91
+ it('shows empty config', () => {
92
+ vi.mocked(loadCredentials).mockReturnValue({});
93
+ configCommand([], {});
94
+ expect(stdout.join('\n')).toContain('No configuration');
95
+ });
96
+ it('shows config as JSON', () => {
97
+ vi.mocked(loadCredentials).mockReturnValue({ apiKey: 'long-key-12345678', url: 'https://x.dev' });
98
+ configCommand([], { json: true });
99
+ const output = stdout.join('\n');
100
+ expect(output).toContain('long...');
101
+ expect(output).toContain('https://x.dev');
102
+ });
103
+ it('shows non-empty config in text mode', () => {
104
+ vi.mocked(loadCredentials).mockReturnValue({ url: 'https://x.dev' });
105
+ configCommand([], {});
106
+ expect(stdout.join('\n')).toContain('url: https://x.dev');
107
+ });
108
+ it('sets a config value', () => {
109
+ configCommand(['set', 'url', 'https://custom.dev'], {});
110
+ expect(vi.mocked(saveCredentials)).toHaveBeenCalled();
111
+ expect(stdout.join('\n')).toContain('url = https://custom.dev');
112
+ });
113
+ it('gets a config value', () => {
114
+ vi.mocked(loadCredentials).mockReturnValue({ url: 'https://test.dev' });
115
+ configCommand(['get', 'url'], {});
116
+ expect(stdout.join('\n')).toContain('https://test.dev');
117
+ });
118
+ it('shows config path', () => {
119
+ configCommand(['path'], {});
120
+ expect(stdout.join('\n')).toContain('credentials.json');
121
+ });
122
+ it('exits on missing set args', () => {
123
+ expect(() => configCommand(['set'], {})).toThrow('process.exit(1)');
124
+ });
125
+ it('exits on missing get key', () => {
126
+ expect(() => configCommand(['get'], {})).toThrow('process.exit(1)');
127
+ });
128
+ });
129
+ describe('createCommand', () => {
130
+ it('creates a box and prints result', async () => {
131
+ mockX402Fetch.mockResolvedValue({ id: 'abc', status: 'running', image: 'codebox' });
132
+ await createCommand([], {});
133
+ expect(mockX402Fetch).toHaveBeenCalledWith('/boxes', expect.objectContaining({ method: 'POST' }));
134
+ expect(stdout.join('\n')).toContain('abc');
135
+ });
136
+ it('creates a box with custom options', async () => {
137
+ mockX402Fetch.mockResolvedValue({ id: 'xyz', status: 'running', image: 'custom' });
138
+ await createCommand(['--image', 'custom', '--cpu', '4', '--memory', '2048'], {});
139
+ const body = JSON.parse(mockX402Fetch.mock.calls[0][1].body);
140
+ expect(body.image).toBe('custom');
141
+ expect(body.cpu).toBe(4);
142
+ expect(body.memory_mb).toBe(2048);
143
+ });
144
+ it('passes timeout option', async () => {
145
+ mockX402Fetch.mockResolvedValue({ id: 'abc', status: 'running', image: 'codebox' });
146
+ await createCommand(['--timeout', '60'], {});
147
+ const body = JSON.parse(mockX402Fetch.mock.calls[0][1].body);
148
+ expect(body.timeout_minutes).toBe(60);
149
+ });
150
+ it('outputs JSON when flag set', async () => {
151
+ mockX402Fetch.mockResolvedValue({ id: 'abc', status: 'running' });
152
+ await createCommand([], { json: true });
153
+ expect(stdout.join('\n')).toContain('"id"');
154
+ });
155
+ });
156
+ describe('listCommand', () => {
157
+ it('lists boxes', async () => {
158
+ mockX402Fetch.mockResolvedValue([
159
+ { id: 'a', status: 'running', image: 'codebox', created_at: '2026-01-01' },
160
+ ]);
161
+ await listCommand([], {});
162
+ expect(stdout.join('\n')).toContain('a');
163
+ expect(stdout.join('\n')).toContain('running');
164
+ });
165
+ it('shows empty message', async () => {
166
+ mockX402Fetch.mockResolvedValue([]);
167
+ await listCommand([], {});
168
+ expect(stdout.join('\n')).toContain('No sandboxes');
169
+ });
170
+ it('outputs JSON', async () => {
171
+ mockX402Fetch.mockResolvedValue([{ id: 'b' }]);
172
+ await listCommand([], { json: true });
173
+ expect(stdout.join('\n')).toContain('"id"');
174
+ });
175
+ });
176
+ describe('getCommand', () => {
177
+ it('gets box details', async () => {
178
+ mockX402Fetch.mockResolvedValue({
179
+ id: 'abc', status: 'running', image: 'codebox',
180
+ cpu: 2, memory_mb: 1024, created_at: '2026-01-01',
181
+ });
182
+ await getCommand(['abc'], {});
183
+ expect(stdout.join('\n')).toContain('abc');
184
+ expect(stdout.join('\n')).toContain('running');
185
+ });
186
+ it('outputs JSON', async () => {
187
+ mockX402Fetch.mockResolvedValue({ id: 'abc', status: 'running' });
188
+ await getCommand(['abc'], { json: true });
189
+ expect(stdout.join('\n')).toContain('"id"');
190
+ });
191
+ it('exits without id', async () => {
192
+ await expect(getCommand([], {})).rejects.toThrow('process.exit(1)');
193
+ });
194
+ it('shows ports when present', async () => {
195
+ mockX402Fetch.mockResolvedValue({
196
+ id: 'x', status: 'running', image: 'codebox',
197
+ cpu: 1, memory_mb: 512, created_at: '2026-01-01',
198
+ ports: { '8080': 10000 },
199
+ });
200
+ await getCommand(['x'], {});
201
+ expect(stdout.join('\n')).toContain('8080');
202
+ });
203
+ });
204
+ describe('destroyCommand', () => {
205
+ it('destroys a box', async () => {
206
+ mockX402Fetch.mockResolvedValue(undefined);
207
+ await destroyCommand(['abc'], {});
208
+ expect(mockX402Fetch).toHaveBeenCalledWith('/boxes/abc', { method: 'DELETE' });
209
+ expect(stdout.join('\n')).toContain('Destroyed abc');
210
+ });
211
+ it('exits without id', async () => {
212
+ await expect(destroyCommand([], {})).rejects.toThrow('process.exit(1)');
213
+ });
214
+ it('outputs JSON', async () => {
215
+ mockX402Fetch.mockResolvedValue(undefined);
216
+ await destroyCommand(['abc'], { json: true });
217
+ expect(stdout.join('\n')).toContain('"destroyed"');
218
+ });
219
+ });
220
+ describe('execCommand', () => {
221
+ it('executes a command', async () => {
222
+ mockX402Fetch.mockResolvedValue({ stdout: 'hello\n', stderr: '', exit_code: 0 });
223
+ await execCommand(['abc', 'echo', 'hello'], {});
224
+ expect(stdout.join('')).toContain('hello');
225
+ });
226
+ it('outputs JSON', async () => {
227
+ mockX402Fetch.mockResolvedValue({ stdout: 'ok', stderr: '', exit_code: 0 });
228
+ await execCommand(['abc', 'echo', 'ok'], { json: true });
229
+ expect(stdout.join('\n')).toContain('"exit_code"');
230
+ });
231
+ it('exits without id or command', async () => {
232
+ await expect(execCommand([], {})).rejects.toThrow('process.exit(1)');
233
+ await expect(execCommand(['abc'], {})).rejects.toThrow('process.exit(1)');
234
+ });
235
+ it('writes stderr', async () => {
236
+ mockX402Fetch.mockResolvedValue({ stdout: '', stderr: 'err\n', exit_code: 0 });
237
+ await execCommand(['abc', 'fail'], {});
238
+ expect(stderr.join('')).toContain('err');
239
+ });
240
+ it('exits with non-zero code', async () => {
241
+ mockX402Fetch.mockResolvedValue({ stdout: '', stderr: '', exit_code: 1 });
242
+ await expect(execCommand(['abc', 'false'], {})).rejects.toThrow('process.exit(1)');
243
+ });
244
+ });
245
+ describe('cloneCommand', () => {
246
+ it('clones a box', async () => {
247
+ mockX402Fetch.mockResolvedValue({ id: 'new', status: 'running' });
248
+ await cloneCommand(['abc'], {});
249
+ expect(mockX402Fetch).toHaveBeenCalledWith('/boxes/abc/clone', expect.any(Object));
250
+ expect(stdout.join('\n')).toContain('Cloned abc → new');
251
+ });
252
+ it('outputs JSON', async () => {
253
+ mockX402Fetch.mockResolvedValue({ id: 'new', status: 'running' });
254
+ await cloneCommand(['abc'], { json: true });
255
+ expect(stdout.join('\n')).toContain('"id"');
256
+ });
257
+ it('uses SANDBANK_BOX_ID as default', async () => {
258
+ const orig = process.env['SANDBANK_BOX_ID'];
259
+ process.env['SANDBANK_BOX_ID'] = 'self';
260
+ mockX402Fetch.mockResolvedValue({ id: 'cloned', status: 'running' });
261
+ await cloneCommand([], {});
262
+ expect(mockX402Fetch).toHaveBeenCalledWith('/boxes/self/clone', expect.any(Object));
263
+ process.env['SANDBANK_BOX_ID'] = orig;
264
+ });
265
+ it('exits without id when not in sandbox', async () => {
266
+ const orig = process.env['SANDBANK_BOX_ID'];
267
+ delete process.env['SANDBANK_BOX_ID'];
268
+ await expect(cloneCommand([], {})).rejects.toThrow('process.exit(1)');
269
+ process.env['SANDBANK_BOX_ID'] = orig;
270
+ });
271
+ });
272
+ describe('keepCommand', () => {
273
+ it('extends timeout', async () => {
274
+ mockX402Fetch.mockResolvedValue({ timeout_minutes: 30 });
275
+ await keepCommand(['abc'], {});
276
+ expect(stdout.join('\n')).toContain('Extended abc by 30 minutes');
277
+ });
278
+ it('outputs JSON', async () => {
279
+ mockX402Fetch.mockResolvedValue({ timeout_minutes: 30 });
280
+ await keepCommand(['abc'], { json: true });
281
+ expect(stdout.join('\n')).toContain('"timeout_minutes"');
282
+ });
283
+ it('uses custom minutes', async () => {
284
+ mockX402Fetch.mockResolvedValue({ timeout_minutes: 60 });
285
+ await keepCommand(['abc', '--minutes', '60'], {});
286
+ const body = JSON.parse(mockX402Fetch.mock.calls[0][1].body);
287
+ expect(body.timeout_minutes).toBe(60);
288
+ });
289
+ it('exits without id', async () => {
290
+ await expect(keepCommand([], {})).rejects.toThrow('process.exit(1)');
291
+ });
292
+ });
293
+ describe('addonsCommand', () => {
294
+ it('creates an addon', async () => {
295
+ const orig = process.env['SANDBANK_BOX_ID'];
296
+ process.env['SANDBANK_BOX_ID'] = 'parent';
297
+ mockX402Fetch.mockResolvedValue({ id: 'a1', type: 'wechatbox', status: 'running', relay_name: 'wechatbox-abc' });
298
+ await addonsCommand(['create', 'wechatbox'], {});
299
+ expect(stdout.join('\n')).toContain('wechatbox');
300
+ expect(stdout.join('\n')).toContain('relay: wechatbox-abc');
301
+ process.env['SANDBANK_BOX_ID'] = orig;
302
+ });
303
+ it('lists addons', async () => {
304
+ const orig = process.env['SANDBANK_BOX_ID'];
305
+ process.env['SANDBANK_BOX_ID'] = 'parent';
306
+ mockX402Fetch.mockResolvedValue([
307
+ { id: 'a1', type: 'logbox', status: 'running', created_at: '2026-01-01' },
308
+ ]);
309
+ await addonsCommand(['list'], {});
310
+ expect(stdout.join('\n')).toContain('logbox');
311
+ process.env['SANDBANK_BOX_ID'] = orig;
312
+ });
313
+ it('shows empty addons', async () => {
314
+ const orig = process.env['SANDBANK_BOX_ID'];
315
+ process.env['SANDBANK_BOX_ID'] = 'parent';
316
+ mockX402Fetch.mockResolvedValue([]);
317
+ await addonsCommand(['list'], {});
318
+ expect(stdout.join('\n')).toContain('No addons');
319
+ process.env['SANDBANK_BOX_ID'] = orig;
320
+ });
321
+ it('exits on missing type', async () => {
322
+ const orig = process.env['SANDBANK_BOX_ID'];
323
+ process.env['SANDBANK_BOX_ID'] = 'parent';
324
+ await expect(addonsCommand(['create'], {})).rejects.toThrow('process.exit(1)');
325
+ process.env['SANDBANK_BOX_ID'] = orig;
326
+ });
327
+ it('exits on missing box id for create', async () => {
328
+ const orig = process.env['SANDBANK_BOX_ID'];
329
+ delete process.env['SANDBANK_BOX_ID'];
330
+ await expect(addonsCommand(['create', 'logbox'], {})).rejects.toThrow('process.exit(1)');
331
+ process.env['SANDBANK_BOX_ID'] = orig;
332
+ });
333
+ it('exits on missing box id for list', async () => {
334
+ const orig = process.env['SANDBANK_BOX_ID'];
335
+ delete process.env['SANDBANK_BOX_ID'];
336
+ await expect(addonsCommand(['list'], {})).rejects.toThrow('process.exit(1)');
337
+ process.env['SANDBANK_BOX_ID'] = orig;
338
+ });
339
+ it('uses --box flag', async () => {
340
+ mockX402Fetch.mockResolvedValue({ id: 'a1', type: 'logbox', status: 'running', relay_name: null });
341
+ await addonsCommand(['create', 'logbox', '--box', 'mybox'], {});
342
+ expect(mockX402Fetch).toHaveBeenCalledWith('/boxes/mybox/addons', expect.any(Object));
343
+ });
344
+ it('creates addon without relay_name', async () => {
345
+ const orig = process.env['SANDBANK_BOX_ID'];
346
+ process.env['SANDBANK_BOX_ID'] = 'parent';
347
+ mockX402Fetch.mockResolvedValue({ id: 'a1', type: 'logbox', status: 'running', relay_name: null });
348
+ await addonsCommand(['create', 'logbox'], {});
349
+ expect(stdout.join('\n')).toContain('logbox');
350
+ expect(stdout.join('\n')).not.toContain('relay:');
351
+ process.env['SANDBANK_BOX_ID'] = orig;
352
+ });
353
+ it('creates addon with --intent flag', async () => {
354
+ const orig = process.env['SANDBANK_BOX_ID'];
355
+ process.env['SANDBANK_BOX_ID'] = 'parent';
356
+ mockX402Fetch.mockResolvedValue({ id: 'a1', type: 'logbox', status: 'running', relay_name: null });
357
+ await addonsCommand(['create', 'logbox', '--intent', 'monitor errors'], {});
358
+ const body = JSON.parse(mockX402Fetch.mock.calls[0][1].body);
359
+ expect(body.intent).toBe('monitor errors');
360
+ process.env['SANDBANK_BOX_ID'] = orig;
361
+ });
362
+ it('outputs JSON for create', async () => {
363
+ const orig = process.env['SANDBANK_BOX_ID'];
364
+ process.env['SANDBANK_BOX_ID'] = 'parent';
365
+ mockX402Fetch.mockResolvedValue({ id: 'a1', type: 'logbox', status: 'running', relay_name: null });
366
+ await addonsCommand(['create', 'logbox'], { json: true });
367
+ expect(stdout.join('\n')).toContain('"id"');
368
+ process.env['SANDBANK_BOX_ID'] = orig;
369
+ });
370
+ it('outputs JSON for list', async () => {
371
+ const orig = process.env['SANDBANK_BOX_ID'];
372
+ process.env['SANDBANK_BOX_ID'] = 'parent';
373
+ mockX402Fetch.mockResolvedValue([{ id: 'a1' }]);
374
+ await addonsCommand(['list'], { json: true });
375
+ expect(stdout.join('\n')).toContain('"id"');
376
+ process.env['SANDBANK_BOX_ID'] = orig;
377
+ });
378
+ it('exits on unknown subcommand', async () => {
379
+ await expect(addonsCommand(['unknown'], {})).rejects.toThrow('process.exit(1)');
380
+ });
381
+ });
382
+ describe('snapshotCommand', () => {
383
+ it('creates a snapshot', async () => {
384
+ mockX402Fetch.mockResolvedValue(undefined);
385
+ await snapshotCommand(['create', 'abc', 'snap1'], {});
386
+ expect(stdout.join('\n')).toContain('Snapshot "snap1" created');
387
+ });
388
+ it('lists snapshots', async () => {
389
+ mockX402Fetch.mockResolvedValue([{ name: 'snap1' }, { name: 'snap2', created_at: '2026-01-01' }]);
390
+ await snapshotCommand(['list', 'abc'], {});
391
+ expect(stdout.join('\n')).toContain('snap1');
392
+ expect(stdout.join('\n')).toContain('snap2');
393
+ });
394
+ it('shows empty snapshots', async () => {
395
+ mockX402Fetch.mockResolvedValue([]);
396
+ await snapshotCommand(['list', 'abc'], {});
397
+ expect(stdout.join('\n')).toContain('No snapshots');
398
+ });
399
+ it('restores a snapshot', async () => {
400
+ mockX402Fetch.mockResolvedValue(undefined);
401
+ await snapshotCommand(['restore', 'abc', 'snap1'], {});
402
+ expect(stdout.join('\n')).toContain('Restored "snap1"');
403
+ });
404
+ it('deletes a snapshot', async () => {
405
+ mockX402Fetch.mockResolvedValue(undefined);
406
+ await snapshotCommand(['delete', 'abc', 'snap1'], {});
407
+ expect(stdout.join('\n')).toContain('Deleted "snap1"');
408
+ });
409
+ it('exits on missing args for create', async () => {
410
+ await expect(snapshotCommand(['create', 'abc'], {})).rejects.toThrow('process.exit(1)');
411
+ });
412
+ it('exits on missing args for list', async () => {
413
+ await expect(snapshotCommand(['list'], {})).rejects.toThrow('process.exit(1)');
414
+ });
415
+ it('exits on missing args for restore', async () => {
416
+ await expect(snapshotCommand(['restore', 'abc'], {})).rejects.toThrow('process.exit(1)');
417
+ });
418
+ it('exits on missing args for delete', async () => {
419
+ await expect(snapshotCommand(['delete', 'abc'], {})).rejects.toThrow('process.exit(1)');
420
+ });
421
+ it('exits on unknown subcommand', async () => {
422
+ await expect(snapshotCommand(['unknown'], {})).rejects.toThrow('process.exit(1)');
423
+ });
424
+ it('outputs JSON for create', async () => {
425
+ mockX402Fetch.mockResolvedValue(undefined);
426
+ await snapshotCommand(['create', 'abc', 's1'], { json: true });
427
+ expect(stdout.join('\n')).toContain('"created"');
428
+ });
429
+ it('outputs JSON for restore', async () => {
430
+ mockX402Fetch.mockResolvedValue(undefined);
431
+ await snapshotCommand(['restore', 'abc', 's1'], { json: true });
432
+ expect(stdout.join('\n')).toContain('"restored"');
433
+ });
434
+ it('outputs JSON for delete', async () => {
435
+ mockX402Fetch.mockResolvedValue(undefined);
436
+ await snapshotCommand(['delete', 'abc', 's1'], { json: true });
437
+ expect(stdout.join('\n')).toContain('"deleted"');
438
+ });
439
+ });
@@ -0,0 +1,3 @@
1
+ import type { CliFlags } from '../auth.js';
2
+ export declare function configCommand(args: string[], flags: CliFlags): void;
3
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/config.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAK1C,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI,CAwDnE"}
@@ -0,0 +1,57 @@
1
+ import { homedir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { loadCredentials, saveCredentials, maskSecret } from '../config.js';
4
+ const SENSITIVE_KEYS = new Set(['apiKey', 'walletKey']);
5
+ export function configCommand(args, flags) {
6
+ const sub = args[0];
7
+ if (sub === 'set') {
8
+ const key = args[1];
9
+ const value = args[2];
10
+ if (!key || value === undefined) {
11
+ console.error('Usage: sandbank config set <key> <value>');
12
+ process.exit(1);
13
+ }
14
+ const creds = loadCredentials();
15
+ creds[key] = value;
16
+ saveCredentials(creds);
17
+ console.log(`${key} = ${SENSITIVE_KEYS.has(key) ? maskSecret(value) : value}`);
18
+ return;
19
+ }
20
+ if (sub === 'get') {
21
+ const key = args[1];
22
+ if (!key) {
23
+ console.error('Usage: sandbank config get <key>');
24
+ process.exit(1);
25
+ }
26
+ const creds = loadCredentials();
27
+ const value = creds[key];
28
+ if (value !== undefined) {
29
+ console.log(value);
30
+ }
31
+ return;
32
+ }
33
+ if (sub === 'path') {
34
+ console.log(join(homedir(), '.sandbank', 'credentials.json'));
35
+ return;
36
+ }
37
+ // Default: show all config
38
+ const creds = loadCredentials();
39
+ if (flags.json) {
40
+ const masked = { ...creds };
41
+ for (const key of SENSITIVE_KEYS) {
42
+ if (masked[key])
43
+ masked[key] = maskSecret(masked[key]);
44
+ }
45
+ console.log(JSON.stringify(masked, null, 2));
46
+ return;
47
+ }
48
+ const entries = Object.entries(creds).filter(([, v]) => v !== undefined);
49
+ if (entries.length === 0) {
50
+ console.log('No configuration. Run: sandbank login --api-key <key>');
51
+ return;
52
+ }
53
+ for (const [key, value] of entries) {
54
+ const display = SENSITIVE_KEYS.has(key) ? maskSecret(value) : value;
55
+ console.log(`${key}: ${display}`);
56
+ }
57
+ }
@@ -0,0 +1,3 @@
1
+ import type { CliFlags } from '../auth.js';
2
+ export declare function createCommand(args: string[], flags: CliFlags): Promise<void>;
3
+ //# sourceMappingURL=create.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"create.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/create.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAY1C,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBlF"}
@@ -0,0 +1,30 @@
1
+ import { createApiClient, printJson } from '../api.js';
2
+ function takeOption(args, name) {
3
+ const idx = args.indexOf(name);
4
+ if (idx === -1)
5
+ return undefined;
6
+ const value = args[idx + 1];
7
+ args.splice(idx, 2);
8
+ return value;
9
+ }
10
+ export async function createCommand(args, flags) {
11
+ const image = takeOption(args, '--image') || 'codebox';
12
+ const cpu = Number(takeOption(args, '--cpu') || 2);
13
+ const memory = Number(takeOption(args, '--memory') || 1024);
14
+ const timeout = takeOption(args, '--timeout');
15
+ const api = createApiClient(flags);
16
+ const body = {
17
+ image,
18
+ cpu,
19
+ memory_mb: memory,
20
+ };
21
+ if (timeout)
22
+ body.timeout_minutes = Number(timeout);
23
+ const box = await api.x402Fetch('/boxes', {
24
+ method: 'POST',
25
+ body: JSON.stringify(body),
26
+ });
27
+ if (flags.json)
28
+ return printJson(box);
29
+ console.log(`${box.id} ${box.status} (${box.image})`);
30
+ }
@@ -0,0 +1,3 @@
1
+ import type { CliFlags } from '../auth.js';
2
+ export declare function destroyCommand(args: string[], flags: CliFlags): Promise<void>;
3
+ //# sourceMappingURL=destroy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"destroy.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/destroy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAG1C,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAenF"}
@@ -0,0 +1,16 @@
1
+ import { createApiClient } from '../api.js';
2
+ export async function destroyCommand(args, flags) {
3
+ const id = args[0];
4
+ if (!id) {
5
+ console.error('Usage: sandbank destroy <id>');
6
+ process.exit(1);
7
+ }
8
+ const api = createApiClient(flags);
9
+ await api.x402Fetch(`/boxes/${id}`, { method: 'DELETE' });
10
+ if (flags.json) {
11
+ console.log(JSON.stringify({ id, destroyed: true }));
12
+ }
13
+ else {
14
+ console.log(`Destroyed ${id}`);
15
+ }
16
+ }
@@ -0,0 +1,3 @@
1
+ import type { CliFlags } from '../auth.js';
2
+ export declare function execCommand(args: string[], flags: CliFlags): Promise<void>;
3
+ //# sourceMappingURL=exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/exec.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAI1C,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBhF"}
@@ -0,0 +1,22 @@
1
+ import { createApiClient, printJson } from '../api.js';
2
+ export async function execCommand(args, flags) {
3
+ const id = args[0];
4
+ const command = args.slice(1).join(' ');
5
+ if (!id || !command) {
6
+ console.error('Usage: sandbank exec <id> <command>');
7
+ process.exit(1);
8
+ }
9
+ const api = createApiClient(flags);
10
+ const result = await api.x402Fetch(`/boxes/${id}/exec`, {
11
+ method: 'POST',
12
+ body: JSON.stringify({ cmd: ['bash', '-c', command] }),
13
+ });
14
+ if (flags.json)
15
+ return printJson(result);
16
+ if (result.stdout)
17
+ process.stdout.write(result.stdout);
18
+ if (result.stderr)
19
+ process.stderr.write(result.stderr);
20
+ if (result.exit_code !== 0)
21
+ process.exit(result.exit_code);
22
+ }
@@ -0,0 +1,3 @@
1
+ import type { CliFlags } from '../auth.js';
2
+ export declare function getCommand(args: string[], flags: CliFlags): Promise<void>;
3
+ //# sourceMappingURL=get.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/get.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAI1C,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAoB/E"}
@@ -0,0 +1,21 @@
1
+ import { createApiClient, printJson } from '../api.js';
2
+ export async function getCommand(args, flags) {
3
+ const id = args[0];
4
+ if (!id) {
5
+ console.error('Usage: sandbank get <id>');
6
+ process.exit(1);
7
+ }
8
+ const api = createApiClient(flags);
9
+ const box = await api.x402Fetch(`/boxes/${id}`);
10
+ if (flags.json)
11
+ return printJson(box);
12
+ console.log(`id: ${box.id}`);
13
+ console.log(`status: ${box.status}`);
14
+ console.log(`image: ${box.image}`);
15
+ console.log(`cpu: ${box.cpu}`);
16
+ console.log(`memory: ${box.memory_mb} MB`);
17
+ console.log(`created: ${box.created_at}`);
18
+ if (box.ports && Object.keys(box.ports).length > 0) {
19
+ console.log(`ports: ${JSON.stringify(box.ports)}`);
20
+ }
21
+ }
@@ -0,0 +1,2 @@
1
+ export declare function helpCommand(): void;
2
+ //# sourceMappingURL=help.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/help.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,IAAI,IAAI,CAsClC"}