instabox 0.5.2 → 0.5.3
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 +2 -1
- package/bin/instabox.mjs +15 -30
- package/lib/cli.mjs +73 -13
- package/lib/guestuser.mjs +59 -0
- package/lib/login.mjs +7 -0
- package/lib/ssh.mjs +26 -0
- package/package.json +1 -1
- package/skills/instabox/SKILL.md +3 -3
- package/skills/instabox-snapshots/SKILL.md +8 -4
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ npx instabox@latest # sign in, install the CLI and the agent skills
|
|
|
5
5
|
npx instabox@latest sh # the same, then start a new box and SSH into it
|
|
6
6
|
```
|
|
7
7
|
|
|
8
|
-
`npx instabox@latest` signs you in to instabox in your browser (first run only; confirm its current Instabox account or choose another), installs the `instabox` CLI already signed in or updates an out-of-date one (
|
|
8
|
+
`npx instabox@latest` signs you in to instabox in your browser (first run only; confirm its current Instabox account or choose another), installs the `instabox` CLI already signed in or updates an out-of-date one (skip with `--no-cli`), installs or updates the agent skills from [`../skills`](../skills) in `~/.claude/skills` and `~/.agents/skills` (skip with `--no-skills`), and prints the next steps. It does not touch your boxes.
|
|
9
9
|
|
|
10
10
|
`npx instabox@latest sh` does all that, then starts a new box (`box-<hex>`) and drops you into it over SSH, no questions. Run by an agent (no terminal), it starts the box and prints `instabox ssh <box> -- <command>` instead of opening a shell. At the box limit it starts nothing and lists your boxes, to remove one (`instabox rm <box>`) or open one (`instabox ssh <box>`).
|
|
11
11
|
|
|
@@ -19,6 +19,7 @@ Any other command line is the instabox CLI's ([`cmd/instabox`](../cmd/instabox))
|
|
|
19
19
|
- `sh` always starts a new box, with no timeout and no idle auto-suspend of its own. An existing box is entered with the CLI: `instabox ssh <box>`.
|
|
20
20
|
- An installed CLI is checked against what the server's `/install.sh` would install: the binary's sha256 against the server's `/dl/SHA256SUMS` (release builds are named after a commit, so their versions do not sort). One the server no longer serves is replaced in place through the same installer; a development build (`instabox dev`, or `-dirty`) is left alone with a one-line notice of the install command.
|
|
21
21
|
- Skills are written only over copies marked `installed-by: instabox`; the output says which were installed, updated, already up to date, or kept because they are the user's own.
|
|
22
|
+
- On Windows, where `/install.sh` does not run, the CLI is installed by `lib/cli.mjs` itself: `/dl/instabox-windows-<arch>.exe`, checked against `SHA256SUMS` and `--version`, renamed to `%LOCALAPPDATA%\Programs\instabox\instabox.exe`. The output gives the PowerShell line that puts that folder on the user's PATH; `npx instabox@latest <command>` finds it without that.
|
|
22
23
|
- The bin is also called `instabox`, so `lib/cli.mjs` finds the real CLI the way the insta CLI does: PATH without `node_modules` dirs and without links back to this script, then the installer's own targets.
|
|
23
24
|
- `prepack` copies `../skills` into the package; every skill there ships on the next publish.
|
|
24
25
|
|
package/bin/instabox.mjs
CHANGED
|
@@ -20,8 +20,10 @@ import { join, dirname } from 'node:path';
|
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { createClient } from '../lib/api.mjs';
|
|
22
22
|
import { orderBoxes } from '../lib/boxes.mjs';
|
|
23
|
+
import { guestUserNote, localGuestUser } from '../lib/guestuser.mjs';
|
|
23
24
|
import { checkCli, findCli, installCli, installCommand, isOnboarding, real, runCli } from '../lib/cli.mjs';
|
|
24
|
-
import { callbackHandler } from '../lib/login.mjs';
|
|
25
|
+
import { browserCommand, callbackHandler } from '../lib/login.mjs';
|
|
26
|
+
import { sessionArgs } from '../lib/ssh.mjs';
|
|
25
27
|
import { loadCredentials, saveCredentials } from '../lib/credentials.mjs';
|
|
26
28
|
|
|
27
29
|
// Same variables, same order as the instabox CLI, so both talk to one server.
|
|
@@ -66,12 +68,7 @@ function usage() {
|
|
|
66
68
|
const client = createClient(API);
|
|
67
69
|
|
|
68
70
|
function openBrowser(url) {
|
|
69
|
-
const cmd =
|
|
70
|
-
process.platform === 'darwin'
|
|
71
|
-
? ['open', [url]]
|
|
72
|
-
: process.platform === 'win32'
|
|
73
|
-
? ['cmd', ['/c', 'start', '', url]]
|
|
74
|
-
: ['xdg-open', [url]];
|
|
71
|
+
const cmd = browserCommand(url);
|
|
75
72
|
try {
|
|
76
73
|
spawn(cmd[0], cmd[1], { stdio: 'ignore', detached: true })
|
|
77
74
|
.on('error', () => {})
|
|
@@ -253,16 +250,18 @@ async function newBox() {
|
|
|
253
250
|
};
|
|
254
251
|
if (img) body.imageId = img.id;
|
|
255
252
|
else body.templateId = IMAGE;
|
|
253
|
+
// The box's Linux user is named after yours, so `ssh <box>` over your own network works. A
|
|
254
|
+
// box from one of your own snapshots keeps the snapshot's user.
|
|
255
|
+
const user = localGuestUser();
|
|
256
|
+
if (user.name && (!img || img.official)) body.guestUser = user.name;
|
|
256
257
|
const op = await waitOp(await client.call('POST', '/v1/machines', body));
|
|
257
|
-
return { id: op.machineId, name: body.name };
|
|
258
|
+
return { id: op.machineId, name: body.name, user };
|
|
258
259
|
}
|
|
259
260
|
|
|
260
261
|
// Runs while the box starts; never fatal. Returns a line to print, or null.
|
|
261
262
|
async function ensureCli() {
|
|
262
263
|
if (process.argv.includes('--no-cli')) return null;
|
|
263
264
|
const found = findCli();
|
|
264
|
-
// The installer is a sh script: on Windows an existing CLI is only reported, never checked.
|
|
265
|
-
if (found && process.platform === 'win32') return `instabox CLI already installed ${dim('· try')} instabox ls`;
|
|
266
265
|
if (found) {
|
|
267
266
|
const { state, version } = await checkCli(found, API);
|
|
268
267
|
const v = version ? ` ${version}` : '';
|
|
@@ -285,7 +284,6 @@ async function ensureCli() {
|
|
|
285
284
|
return `instabox CLI${v} is out of date and could not be updated ${dim(`(${r.error}) · to update:`)} ${installCommand(API)}`;
|
|
286
285
|
return `instabox CLI updated${v ? ` from${v}` : ''}${r.version ? ` to ${r.version}` : ''} at ${found} ${dim('· try')} instabox ls`;
|
|
287
286
|
}
|
|
288
|
-
if (process.platform === 'win32') return null;
|
|
289
287
|
const r = await installCli(API);
|
|
290
288
|
if (r.error) return `instabox CLI not installed ${dim(`(${r.error})`)}`;
|
|
291
289
|
return (
|
|
@@ -433,6 +431,9 @@ async function onboard(wantBox) {
|
|
|
433
431
|
const ready = `ready in ${((Date.now() - t0) / 1000).toFixed(1)}s`;
|
|
434
432
|
if (!tty) {
|
|
435
433
|
console.log(`\n ${green('✓')} box ${bold(box.name)} ${ready}`);
|
|
434
|
+
const got = await client.call('GET', '/v1/machines/' + enc(box.id)).catch(() => null);
|
|
435
|
+
const note = guestUserNote(box.user, box.name, got?.guestUser);
|
|
436
|
+
if (note) console.log(dim(` ${note}`));
|
|
436
437
|
await reportInstalls(cli);
|
|
437
438
|
return nextSteps([
|
|
438
439
|
[`instabox ssh ${box.name} -- <command>`, 'run a command in it'],
|
|
@@ -466,6 +467,8 @@ async function onboard(wantBox) {
|
|
|
466
467
|
|
|
467
468
|
const pauses = m.timeoutAt ? dim(` · pauses itself in ${Math.max(1, Math.round(ttlSeconds / 60))} min`) : '';
|
|
468
469
|
process.stdout.write(`\r ${green('✓')} box ${bold(m.name)} ${ready}${pauses}\n`);
|
|
470
|
+
const note = guestUserNote(box.user, m.name, m.guestUser);
|
|
471
|
+
if (note) console.log(dim(` ${note}`));
|
|
469
472
|
await reportInstalls(cli);
|
|
470
473
|
console.log(dim(` type ${'exit'} to leave · stuck? press Enter, then ~.`));
|
|
471
474
|
// The box's prompt is ~37 columns; narrower terminals make bash scroll the input line sideways.
|
|
@@ -473,24 +476,7 @@ async function onboard(wantBox) {
|
|
|
473
476
|
console.log(dim(` your terminal is ${process.stdout.columns} columns wide; widen it for a readable prompt`));
|
|
474
477
|
console.log();
|
|
475
478
|
|
|
476
|
-
const args =
|
|
477
|
-
'-i',
|
|
478
|
-
key,
|
|
479
|
-
'-o',
|
|
480
|
-
`CertificateFile=${key}-cert.pub`,
|
|
481
|
-
'-o',
|
|
482
|
-
`UserKnownHostsFile=${join(dir, 'known_hosts')}`,
|
|
483
|
-
'-o',
|
|
484
|
-
'IdentitiesOnly=yes',
|
|
485
|
-
'-o',
|
|
486
|
-
'StrictHostKeyChecking=yes',
|
|
487
|
-
'-o',
|
|
488
|
-
'LogLevel=ERROR',
|
|
489
|
-
'-p',
|
|
490
|
-
String(c.port),
|
|
491
|
-
'-t',
|
|
492
|
-
`${c.username}@${c.host}`,
|
|
493
|
-
];
|
|
479
|
+
const args = sessionArgs({ dir, key, port: c.port, username: c.username, host: c.host });
|
|
494
480
|
const child = spawn('ssh', args, { stdio: 'inherit' });
|
|
495
481
|
child.on('exit', (code) => {
|
|
496
482
|
console.log(`\n ${dim(`left ${m.name}. Back in:`)} instabox ssh ${m.name}\n`);
|
|
@@ -505,7 +491,6 @@ else if (isOnboarding(args)) await onboard(args.includes('sh'));
|
|
|
505
491
|
else {
|
|
506
492
|
let path = findCli();
|
|
507
493
|
if (!path) {
|
|
508
|
-
if (process.platform === 'win32') die('the instabox CLI runs on macOS and Linux only');
|
|
509
494
|
console.error(dim(' installing the instabox CLI…'));
|
|
510
495
|
const r = await installCli(API);
|
|
511
496
|
if (r.error) die(`could not install the instabox CLI: ${r.error}`);
|
package/lib/cli.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// The Go instabox CLI: find it, install it, hand it the command line.
|
|
2
2
|
import { execFile, spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { createReadStream, realpathSync, statSync } from 'node:fs';
|
|
4
|
+
import { createReadStream, mkdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
-
import {
|
|
6
|
+
import { join, sep } from 'node:path';
|
|
7
7
|
|
|
8
8
|
const OUR_FLAGS = new Set(['--github', '--google', '--no-cli', '--no-skills']);
|
|
9
9
|
|
|
@@ -20,29 +20,36 @@ export const real = (p) => {
|
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Windows has no execute bit: any instabox.exe file runs.
|
|
24
|
+
function runnable(p, platform) {
|
|
24
25
|
try {
|
|
25
26
|
const s = statSync(p);
|
|
26
|
-
return s.isFile() && (s.mode & 0o111) !== 0;
|
|
27
|
+
return s.isFile() && (platform === 'win32' || (s.mode & 0o111) !== 0);
|
|
27
28
|
} catch {
|
|
28
29
|
return false;
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
// Where installCli puts the CLI on Windows, which has no /install.sh.
|
|
34
|
+
export function windowsInstallDir(env = process.env) {
|
|
35
|
+
const local = env.LOCALAPPDATA || join(env.USERPROFILE || homedir(), 'AppData', 'Local');
|
|
36
|
+
return join(local, 'Programs', 'instabox');
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
// Skips node_modules dirs and links back to us, like insta's findDurableOnPath.
|
|
33
|
-
export function findCli({ env = process.env, self = process.argv[1] } = {}) {
|
|
40
|
+
export function findCli({ env = process.env, self = process.argv[1], platform = process.platform } = {}) {
|
|
34
41
|
const me = real(self);
|
|
42
|
+
const win = platform === 'win32';
|
|
35
43
|
const home = env.HOME || homedir();
|
|
36
44
|
const dirs = [
|
|
37
|
-
...(env.PATH || '').split(
|
|
45
|
+
...(env.PATH || '').split(win ? ';' : ':'),
|
|
38
46
|
env.INSTABOX_INSTALL_DIR,
|
|
39
|
-
'/usr/local/bin',
|
|
40
|
-
join(home, '.local', 'bin'),
|
|
47
|
+
...(win ? [windowsInstallDir(env)] : ['/usr/local/bin', join(home, '.local', 'bin')]),
|
|
41
48
|
];
|
|
42
49
|
for (const dir of dirs) {
|
|
43
50
|
if (!dir || dir.toLowerCase().includes('node_modules')) continue;
|
|
44
|
-
const p = join(dir, 'instabox');
|
|
45
|
-
if (!runnable(p)) continue;
|
|
51
|
+
const p = join(dir, win ? 'instabox.exe' : 'instabox');
|
|
52
|
+
if (!runnable(p, platform)) continue;
|
|
46
53
|
const r = real(p);
|
|
47
54
|
if (r === me || r.includes(`${sep}node_modules${sep}`)) continue;
|
|
48
55
|
return p;
|
|
@@ -55,8 +62,12 @@ const INSTALL = 's=$(curl -fsSL "$1/install.sh") && printf \'%s\\n\' "$s" | sh';
|
|
|
55
62
|
|
|
56
63
|
// The command a person runs to install or update the CLI themselves; with dir, one that puts it
|
|
57
64
|
// in that directory (for a directory they cannot write to without sudo).
|
|
58
|
-
export const installCommand = (api, dir) =>
|
|
59
|
-
|
|
65
|
+
export const installCommand = (api, dir, platform = process.platform) =>
|
|
66
|
+
platform === 'win32'
|
|
67
|
+
? 'npx instabox@latest'
|
|
68
|
+
: dir
|
|
69
|
+
? `curl -fsSL ${api}/install.sh | sudo env INSTABOX_INSTALL_DIR=${dir} sh`
|
|
70
|
+
: `curl -fsSL ${api}/install.sh | sh`;
|
|
60
71
|
|
|
61
72
|
// What `instabox --version` prints after "instabox ", or null when it does not run. Async, so a
|
|
62
73
|
// slow or hung binary does not hold up the rest of onboarding.
|
|
@@ -115,7 +126,8 @@ export async function checkCli(path, api, { fetch = globalThis.fetch } = {}) {
|
|
|
115
126
|
|
|
116
127
|
// path is null, never a bare `instabox` that could resolve back to this script. dir replaces the
|
|
117
128
|
// CLI in that directory (INSTABOX_INSTALL_DIR), so an update lands where the old one is found.
|
|
118
|
-
export function installCli(api, { dir } = {}) {
|
|
129
|
+
export function installCli(api, { dir, platform = process.platform, ...win } = {}) {
|
|
130
|
+
if (platform === 'win32') return installWindows(api, { dir, ...win });
|
|
119
131
|
return new Promise((resolve) => {
|
|
120
132
|
let out = '';
|
|
121
133
|
const env = dir ? { ...process.env, INSTABOX_INSTALL_DIR: dir } : process.env;
|
|
@@ -148,3 +160,51 @@ export function runCli(path, args) {
|
|
|
148
160
|
}
|
|
149
161
|
process.exit(r.status ?? 1);
|
|
150
162
|
}
|
|
163
|
+
|
|
164
|
+
const WINDOWS_ARCH = { x64: 'amd64', arm64: 'arm64' };
|
|
165
|
+
|
|
166
|
+
// Keep its checks in step with /install.sh's, which Windows cannot run.
|
|
167
|
+
async function installWindows(
|
|
168
|
+
api,
|
|
169
|
+
{ dir, env = process.env, arch = process.arch, fetch = globalThis.fetch, version = cliVersion } = {},
|
|
170
|
+
) {
|
|
171
|
+
if (!WINDOWS_ARCH[arch]) return { error: `no instabox CLI for Windows on ${arch} (x64 and arm64 only)` };
|
|
172
|
+
const name = `instabox-windows-${WINDOWS_ARCH[arch]}.exe`;
|
|
173
|
+
const to = dir || env.INSTABOX_INSTALL_DIR || windowsInstallDir(env);
|
|
174
|
+
const tmp = join(to, `.instabox.install.${process.pid}.exe`);
|
|
175
|
+
try {
|
|
176
|
+
const get = async (file) => {
|
|
177
|
+
const res = await fetch(`${api}/dl/${file}`, { signal: AbortSignal.timeout(300_000) });
|
|
178
|
+
if (!res.ok) throw new Error(`download ${file}: HTTP ${res.status}`);
|
|
179
|
+
return Buffer.from(await res.arrayBuffer());
|
|
180
|
+
};
|
|
181
|
+
const [body, sums] = await Promise.all([get(name), get('SHA256SUMS')]);
|
|
182
|
+
const want = String(sums)
|
|
183
|
+
.split('\n')
|
|
184
|
+
.map((l) => l.trim().split(/\s+/))
|
|
185
|
+
.find(([, n]) => n === name || n === `*${name}`)?.[0];
|
|
186
|
+
if (!/^[0-9a-f]{64}$/.test(want || '')) return { error: `no valid checksum for ${name} in SHA256SUMS` };
|
|
187
|
+
const got = createHash('sha256').update(body).digest('hex');
|
|
188
|
+
if (got !== want) return { error: `checksum mismatch for ${name}; run it again if a deploy was in progress` };
|
|
189
|
+
mkdirSync(to, { recursive: true });
|
|
190
|
+
writeFileSync(tmp, body, { mode: 0o755 });
|
|
191
|
+
const v = await version(tmp);
|
|
192
|
+
if (!v) {
|
|
193
|
+
rmSync(tmp, { force: true });
|
|
194
|
+
return { error: 'the downloaded instabox does not run (instabox --version failed)' };
|
|
195
|
+
}
|
|
196
|
+
const path = join(to, 'instabox.exe');
|
|
197
|
+
renameSync(tmp, path);
|
|
198
|
+
const onPath = (env.PATH || '').split(';').some((d) => d.replace(/[\\/]+$/, '').toLowerCase() === to.toLowerCase());
|
|
199
|
+
return {
|
|
200
|
+
path,
|
|
201
|
+
version: v,
|
|
202
|
+
pathHint: onPath
|
|
203
|
+
? null
|
|
204
|
+
: `[Environment]::SetEnvironmentVariable('Path', [Environment]::GetEnvironmentVariable('Path', 'User') + ';${to.replaceAll("'", "''")}', 'User')`,
|
|
205
|
+
};
|
|
206
|
+
} catch (e) {
|
|
207
|
+
rmSync(tmp, { force: true });
|
|
208
|
+
return { error: e.message };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The box's Linux user (docs/guest-user-plan.md): the person's local username, normalized the
|
|
2
|
+
// way the server and the instabox CLI do (pkg/instaboxapi/guestuser.go; the shared test vectors
|
|
3
|
+
// keep them equal). A name that is not valid after normalization is not sent: the box gets `user`.
|
|
4
|
+
import { userInfo } from 'node:os';
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_GUEST_USER = 'user';
|
|
7
|
+
|
|
8
|
+
const PATTERN = /^[a-z_][a-z0-9_-]{0,31}$/;
|
|
9
|
+
|
|
10
|
+
// root, instabox and the users and groups of the Debian base rootfs; names starting with
|
|
11
|
+
// `systemd-` too. Nothing else (pkg/instaboxapi/guestuser.go): `ubuntu`, `admin`, `docker`, … are
|
|
12
|
+
// allowed, and the guest falls back to `user` when its rootfs already has the name.
|
|
13
|
+
const RESERVED = new Set(
|
|
14
|
+
`root daemon bin sys sync games man lp mail news uucp proxy www-data backup list irc
|
|
15
|
+
_gnats gnats nobody nogroup _apt messagebus sshd polkitd tss
|
|
16
|
+
sudo adm staff users tty disk shadow utmp operator dialout audio video plugdev netdev
|
|
17
|
+
input render kvm instabox`.split(/\s+/),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export function validGuestUser(name) {
|
|
21
|
+
return PATTERN.test(name) && !RESERVED.has(name) && !name.startsWith('systemd-');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// The guest user for a local username: trimmed, without a `DOMAIN\` prefix or an `@domain`
|
|
25
|
+
// suffix, lowercased, spaces made `_`. null when that is not a valid guest user.
|
|
26
|
+
export function normalizeGuestUser(local) {
|
|
27
|
+
let s = String(local ?? '').trim();
|
|
28
|
+
const b = s.lastIndexOf('\\');
|
|
29
|
+
if (b >= 0) s = s.slice(b + 1);
|
|
30
|
+
const a = s.lastIndexOf('@');
|
|
31
|
+
if (a >= 0) s = s.slice(0, a);
|
|
32
|
+
s = s.trim().toLowerCase().replaceAll(' ', '_');
|
|
33
|
+
return validGuestUser(s) ? s : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// This computer's username and its guest user: { local, name } (name null when it cannot be
|
|
37
|
+
// one); local is '' when the OS does not say.
|
|
38
|
+
export function localGuestUser() {
|
|
39
|
+
let local = '';
|
|
40
|
+
try {
|
|
41
|
+
local = userInfo().username || '';
|
|
42
|
+
} catch {}
|
|
43
|
+
if (!local) local = process.env.USER || process.env.USERNAME || '';
|
|
44
|
+
return { local, name: local ? normalizeGuestUser(local) : null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The user of boxes from templates that predate per-box users; nothing to say about it.
|
|
48
|
+
export const LEGACY_GUEST_USER = 'dev';
|
|
49
|
+
|
|
50
|
+
// The one line to print about the box's user after a create, or null when there is nothing to
|
|
51
|
+
// say: plain `ssh <box>` over your own network sends the local name, which matches only when
|
|
52
|
+
// normalization left it as it was. actual is the box's guestUser as the server reports it.
|
|
53
|
+
export function guestUserNote({ local, name }, box, actual) {
|
|
54
|
+
if (!local || actual === LEGACY_GUEST_USER) return null;
|
|
55
|
+
const user = actual || name || DEFAULT_GUEST_USER;
|
|
56
|
+
if (!name) return `your username ${local} cannot be a Linux user name; the box's user is ${user}`;
|
|
57
|
+
if (name === local && (!actual || actual === name)) return null;
|
|
58
|
+
return `user ${user} (ssh ${user}@${box} over your own network; instabox ssh ${box} as usual)`;
|
|
59
|
+
}
|
package/lib/login.mjs
CHANGED
|
@@ -36,3 +36,10 @@ export function callbackHandler({ state, exchange, finish, page }) {
|
|
|
36
36
|
finish(result);
|
|
37
37
|
};
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
// The command that opens url in the browser. On Windows never `cmd /c start`: cmd ends the URL at its first &.
|
|
41
|
+
export function browserCommand(url, platform = process.platform) {
|
|
42
|
+
if (platform === 'darwin') return ['open', [url]];
|
|
43
|
+
if (platform === 'win32') return ['rundll32', ['url.dll,FileProtocolHandler', url]];
|
|
44
|
+
return ['xdg-open', [url]];
|
|
45
|
+
}
|
package/lib/ssh.mjs
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// ssh's argv for a session with this run's throwaway key, certificate and known_hosts.
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
// ssh splits an -o value at whitespace and reads ' as an open quote; Windows profile paths often have either.
|
|
5
|
+
export const sshConfigArg = (path) => (/[ \t']/.test(path) ? `"${path}"` : path);
|
|
6
|
+
|
|
7
|
+
export function sessionArgs({ dir, key, port, username, host }) {
|
|
8
|
+
return [
|
|
9
|
+
'-i',
|
|
10
|
+
key,
|
|
11
|
+
'-o',
|
|
12
|
+
`CertificateFile=${sshConfigArg(`${key}-cert.pub`)}`,
|
|
13
|
+
'-o',
|
|
14
|
+
`UserKnownHostsFile=${sshConfigArg(join(dir, 'known_hosts'))}`,
|
|
15
|
+
'-o',
|
|
16
|
+
'IdentitiesOnly=yes',
|
|
17
|
+
'-o',
|
|
18
|
+
'StrictHostKeyChecking=yes',
|
|
19
|
+
'-o',
|
|
20
|
+
'LogLevel=ERROR',
|
|
21
|
+
'-p',
|
|
22
|
+
String(port),
|
|
23
|
+
'-t',
|
|
24
|
+
`${username}@${host}`,
|
|
25
|
+
];
|
|
26
|
+
}
|
package/package.json
CHANGED
package/skills/instabox/SKILL.md
CHANGED
|
@@ -7,7 +7,7 @@ metadata:
|
|
|
7
7
|
|
|
8
8
|
# instabox CLI
|
|
9
9
|
|
|
10
|
-
An instabox sandbox is a Linux VM (Debian, user `
|
|
10
|
+
An instabox sandbox is a Linux VM (Debian, one user with passwordless sudo: named after your local username when created with `instabox create`, `user` from the dashboard, `dev` on older boxes; `instabox get <box>` shows it). It has a root disk and a persistent `/data` disk; HOME is `/data/home`. Boxes are private to the signed-in account.
|
|
11
11
|
|
|
12
12
|
## Sign in
|
|
13
13
|
|
|
@@ -38,7 +38,7 @@ instabox create --name work # new box; waits until it is run
|
|
|
38
38
|
instabox ls # your boxes and their states
|
|
39
39
|
instabox ssh work # interactive shell (leave with: exit)
|
|
40
40
|
instabox exec work -- uname -a # one command, no SSH client needed
|
|
41
|
-
instabox exec work --cwd /data/home -- ls -la # exec runs `bash -lc` as
|
|
41
|
+
instabox exec work --cwd /data/home -- ls -la # exec runs `bash -lc` as the box's user
|
|
42
42
|
instabox files work /data/home # list a directory (or print a text file)
|
|
43
43
|
instabox auto-suspend work 10m # pause after 10 idle minutes (default 30m; 0: never)
|
|
44
44
|
instabox timeout work 2h --on-timeout pause # pause (keeps memory) or kill at the deadline
|
|
@@ -49,7 +49,7 @@ instabox rm work # delete the box
|
|
|
49
49
|
|
|
50
50
|
- Names or IDs (`m_...`) both work wherever `<id-or-name>` is accepted.
|
|
51
51
|
- `create` options: `--template T` or `--snapshot S`, `--auto-suspend D|0`, `--timeout D [--on-timeout pause|kill]`, `--meta K=V`, `--no-vault`, `--no-wait`. Every box is the same size (`instabox usage` shows it); there are no size flags.
|
|
52
|
-
- A new box pauses itself after 30 idle minutes (no agent working, no terminal use, little CPU) and wakes on the next `exec`, `ssh` or `
|
|
52
|
+
- A new box pauses itself after 30 idle minutes (no agent working, no terminal use, little CPU) and wakes on the next `exec`, `ssh`, `files`, public port request or `instabox wake <box>`; a paused box keeps its memory and processes. `--auto-suspend 0` keeps a box always on (a server that must keep answering); `--auto-suspend 2h` changes the idle time.
|
|
53
53
|
- Use `exec` to run a command and read its output, and `ssh` for an interactive shell.
|
|
54
54
|
- `exec` returns the command's exit code and takes `--stdin` to pipe input. It stops the command after `--timeout` seconds: 60 by default, 600 at most. Run anything longer (installs, builds, test suites) over SSH, which has no time limit and also returns the exit code: `instabox ssh work -- 'cd /data/workspace/app && npm ci'`.
|
|
55
55
|
- The box's HOME has `MACHINE.md` (which box it is, its size, what survives a stop, the installed tools) and `AGENTS.md`. Both are worth reading before working in the box; they appear a moment after it starts.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: instabox-snapshots
|
|
3
|
-
description: Use when the user wants to save an instabox box's state and load it later — "save a snapshot", "checkpoint this box", "restore/load from a snapshot", "start a new box from my setup", "clone this box". Covers instabox snapshot save/ls/files/rm
|
|
3
|
+
description: Use when the user wants to save an instabox box's state and load it later — "save a snapshot", "checkpoint this box", "restore/load from a snapshot", "start a new box from my setup", "clone this box". Covers instabox snapshot save/ls/files/rm and create --snapshot.
|
|
4
4
|
metadata:
|
|
5
5
|
installed-by: instabox
|
|
6
6
|
---
|
|
@@ -40,13 +40,17 @@ instabox snapshot files work-ready /etc/os-release # print a text file
|
|
|
40
40
|
|
|
41
41
|
This reads the snapshot's root disk only; `/data` (and HOME) are not browsable here. To check files under `/data`, load the snapshot into a box and use `instabox exec`.
|
|
42
42
|
|
|
43
|
-
## Copy a box
|
|
43
|
+
## Copy a box
|
|
44
|
+
|
|
45
|
+
There is no direct copy: save a snapshot of it, create the new box from that, and delete the snapshot if you will not use it again.
|
|
44
46
|
|
|
45
47
|
```sh
|
|
46
|
-
instabox
|
|
48
|
+
instabox snapshot save work --name work-copy
|
|
49
|
+
instabox create --name work2 --snapshot work-copy
|
|
50
|
+
instabox snapshot rm work-copy # optional; work2 is not affected
|
|
47
51
|
```
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
The copy does not inherit `--no-vault`. If the box was created with `--no-vault`, pass it again: `instabox create --name work2 --snapshot work-copy --no-vault`.
|
|
50
54
|
|
|
51
55
|
## Clean up
|
|
52
56
|
|