instabox 0.8.46 → 0.8.48

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 CHANGED
@@ -3,6 +3,7 @@
3
3
  ```
4
4
  npx instabox@latest # sign in, install the CLI and the agent skills (--no-skills: no skills)
5
5
  npx instabox@latest sh # the same, then start a new box and SSH into it
6
+ npx instabox@latest skills install -y # only the agent skills: no sign-in, no network
6
7
  ```
7
8
 
8
9
  `npx instabox@latest` signs you in to instabox with a device code (first run only: open the page it prints on any device and enter the code; it opens the page for you where there is a browser, never with `--no-browser`), 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.
@@ -11,6 +12,10 @@ npx instabox@latest sh # the same, then start a new box and SSH into it
11
12
 
12
13
  A GitHub and a Google sign-in are separate instabox accounts, each with its own boxes. Both this entry point and `instabox login` let the sign-in page offer both. `--google` asks for Google directly, as does `instabox login --provider google`. An agent with no browser and nobody at the keyboard signs itself up instead with `instabox login --agent` once the CLI is installed (docs/agent-auth.md): it prints a claim page and a code for the account owner, who approves later from any device.
13
14
 
15
+ Without a terminal (an agent, CI), nobody can enter a device code, so the sign-in comes from `INSTABOX_API_KEY` (an `ibk_...` key, as the CLI takes it: no browser, no device code, nothing saved), a sign-in already saved, or an agent key from `instabox login --agent`. With none of them it exits at once with status 3 and says how to go on, instead of waiting for a code nobody will enter. `--device-code` asks for the device code anyway, for an agent that relays it to a person. `-y` (`--yes`) asks nothing: no invite-code prompt.
16
+
17
+ `npx instabox@latest skills install [-y] [--claude|--codex|--cursor|--all]` installs only the agent skills, with no sign-in: Claude Code's in `~/.claude/skills`, Codex's and Cursor's in `~/.agents/skills` (both read it, so one copy serves both). With no agent named it installs for all of them, asking first at a terminal unless `-y`. An unknown flag is a usage error, status 2.
18
+
14
19
  Any other command line is the instabox CLI's ([`cmd/instabox`](../cmd/instabox)): `npx instabox@latest ls` runs `instabox ls`, installing the CLI first if it is missing.
15
20
 
16
21
  ## How it works
package/bin/instabox.mjs CHANGED
@@ -10,13 +10,10 @@ import {
10
10
  writeFileSync,
11
11
  rmSync,
12
12
  existsSync,
13
- mkdirSync,
14
- readdirSync,
15
13
  } from 'node:fs';
16
14
  import { createInterface } from 'node:readline/promises';
17
15
  import { tmpdir, homedir } from 'node:os';
18
16
  import { join, dirname } from 'node:path';
19
- import { fileURLToPath } from 'node:url';
20
17
  import { createClient } from '../lib/api.mjs';
21
18
  import { orderBoxes } from '../lib/boxes.mjs';
22
19
  import { guestUserNote, localGuestUser } from '../lib/guestuser.mjs';
@@ -38,6 +35,7 @@ import { browserCommand, canOpenBrowser, deviceLogin } from '../lib/login.mjs';
38
35
  import { makeDirPrivate, sessionArgs } from '../lib/ssh.mjs';
39
36
  import { loadCredentials, saveCredentials } from '../lib/credentials.mjs';
40
37
  import { agentName, track } from '../lib/telemetry.mjs';
38
+ import { AGENT_DIRS, describeSkills, installSkills, skillRoots } from '../lib/skills.mjs';
41
39
 
42
40
  // Same variables, same order as the instabox CLI, so both talk to one server.
43
41
  const API = (
@@ -55,6 +53,15 @@ const CERT_MIN = 120;
55
53
  const PROVIDER = process.argv.includes('--google') ? 'google' : process.argv.includes('--github') ? 'github' : '';
56
54
  // The code is always printed; --no-browser only stops opening the sign-in page.
57
55
  const NO_BROWSER = process.argv.includes('--no-browser');
56
+ // -y: never ask a question (the invite code); the sign-in itself still needs a key or a person.
57
+ const YES = process.argv.includes('-y') || process.argv.includes('--yes');
58
+ // --device-code: print the sign-in code and wait even without a terminal (an agent relaying the
59
+ // code to a person). Without it, no terminal and no credentials fail at once instead of waiting.
60
+ const DEVICE_CODE = process.argv.includes('--device-code');
61
+ // An API key (ibk_...) the way the CLI takes it: it wins over a stored sign-in.
62
+ const API_KEY = (process.env.INSTABOX_API_KEY || '').trim();
63
+ // Exit status when there is no credential and nobody to sign in (2 is a usage error).
64
+ const EXIT_NEEDS_SIGN_IN = 3;
58
65
  const bold = (s) => (process.stdout.isTTY ? `\x1b[1m${s}\x1b[0m` : s);
59
66
  const dim = (s) => (process.stdout.isTTY ? `\x1b[2m${s}\x1b[0m` : s);
60
67
  const green = (s) => (process.stdout.isTTY ? `\x1b[32m${s}\x1b[0m` : s);
@@ -77,8 +84,15 @@ function usage() {
77
84
  --no-browser only print the sign-in page and code; do not open a browser
78
85
  --no-cli do not install or update the instabox CLI
79
86
  --no-skills do not install the instabox agent skills
87
+ -y, --yes ask nothing (no invite-code prompt)
88
+ --device-code sign in with a device code even without a terminal
89
+ npx instabox@latest skills install [--claude|--codex|--cursor|--all] [-y]
90
+ only the agent skills; no sign-in
80
91
  npx instabox@latest <command> run an instabox CLI command (instabox help lists them)
81
92
 
93
+ INSTABOX_API_KEY=ibk_... signs in with that key: no browser, no device code. Without a key
94
+ or a saved sign-in, and with no terminal, it exits at once (status 3).
95
+
82
96
  INSTABOX_IMAGE=<template> picks the template sh starts from (default agent-browser);
83
97
  INSTABOX_IMAGE=snapshot:<name> starts from a snapshot of yours instead.
84
98
 
@@ -139,7 +153,7 @@ async function signInWithInvite() {
139
153
  });
140
154
  if (r.tok) return r.tok;
141
155
  const waitlisted = r.code === 'invite_required' || r.code === 'invite_invalid';
142
- if (waitlisted) invite = await askInvite(r.code === 'invite_invalid');
156
+ if (waitlisted && !YES) invite = await askInvite(r.code === 'invite_invalid');
143
157
  if (waitlisted && invite) continue;
144
158
  if (waitlisted)
145
159
  die(
@@ -149,9 +163,13 @@ async function signInWithInvite() {
149
163
  }
150
164
  }
151
165
 
152
- // The CLI's saved token, a refreshed one, or a device-code sign-in; saved where the CLI reads it.
166
+ // INSTABOX_API_KEY, the CLI's saved token (or an agent's key from `instabox login --agent`), a
167
+ // refreshed one, or a device-code sign-in, saved where the CLI reads it. A key is never saved,
168
+ // refreshed or traded for a sign-in.
153
169
  async function signedIn() {
170
+ if (API_KEY) return { accessToken: API_KEY, apiKey: true };
154
171
  const c = loadCredentials(API);
172
+ if (c?.apiKey) return { accessToken: c.apiKey, apiKey: true };
155
173
  if (c?.accessToken && new Date(c.accessExpiresAt) - Date.now() > 60_000) return c;
156
174
  if (c?.refreshToken && new Date(c.refreshExpiresAt) > Date.now()) {
157
175
  try {
@@ -160,6 +178,16 @@ async function signedIn() {
160
178
  return t;
161
179
  } catch {}
162
180
  }
181
+ // Nobody can enter a code: say what works, now, instead of waiting for a sign-in that never comes.
182
+ if (!DEVICE_CODE && !(process.stdin.isTTY && process.stdout.isTTY)) {
183
+ console.error(` ${bold('instabox needs a sign-in, and there is no terminal to sign in from.')} Either:
184
+ 1. set INSTABOX_API_KEY=ibk_... (make one in the dashboard under API keys, or with
185
+ instabox keys create from a signed-in machine) and run this again, or
186
+ 2. run npx instabox@latest in a terminal to sign in with a device code.
187
+ The agent skills need no sign-in: npx instabox@latest skills install -y
188
+ `);
189
+ process.exit(EXIT_NEEDS_SIGN_IN);
190
+ }
163
191
  const t = await signInWithInvite();
164
192
  freshSignIn = true;
165
193
  saveCredentials(API, t);
@@ -290,96 +318,62 @@ async function ensureBundledCli(bundled, found) {
290
318
  );
291
319
  }
292
320
 
293
- // A skill directory's files, relative to it, sorted.
294
- function skillFiles(dir, rel = '') {
295
- return readdirSync(join(dir, rel), { withFileTypes: true })
296
- .sort((a, b) => a.name.localeCompare(b.name))
297
- .flatMap((e) => {
298
- const r = rel ? `${rel}/${e.name}` : e.name;
299
- return e.isDirectory() ? skillFiles(dir, r) : e.isFile() ? [r] : [];
300
- });
321
+ // Waits for the CLI install started earlier, installs the skills, and prints a line for each.
322
+ async function reportInstalls(cli) {
323
+ const cliLine = await cli;
324
+ if (cliLine) console.log(` ${green('✓')} ${cliLine}`);
325
+ if (process.argv.includes('--no-skills')) return;
326
+ const skillsLine = describeSkills(installSkills(skillRoots([])));
327
+ if (skillsLine) console.log(` ${green('✓')} ${skillsLine} ${dim('(~/.claude/skills, ~/.agents/skills)')}`);
301
328
  }
302
329
 
303
- // Skills this package used to ship under another name. A copy we installed (marked
304
- // `installed-by: instabox`) is removed, so the agent does not see both; the user's own is kept.
305
- const RETIRED_SKILLS = [
306
- 'instabox-migrate', // now instabox-move-repo
307
- 'instabox-move-all', // now instabox-basic-setup
308
- ];
330
+ // `skills install`: the agent skills only. No sign-in, no network, nothing else installed.
331
+ const SKILLS_USAGE = `
332
+ usage: npx instabox@latest skills install [-y] [--claude] [--codex] [--cursor] [--all]
309
333
 
310
- function removeRetiredSkills(roots) {
311
- const removed = new Set();
312
- for (const root of roots)
313
- for (const n of RETIRED_SKILLS) {
314
- try {
315
- if (!readFileSync(join(root, n, 'SKILL.md'), 'utf8').includes('installed-by: instabox')) continue;
316
- rmSync(join(root, n), { recursive: true, force: true });
317
- removed.add(n);
318
- } catch {}
319
- }
320
- return [...removed];
321
- }
334
+ Copies the instabox agent skills into the folders your coding agents read. No sign-in needed.
335
+ --claude Claude Code: ~/.claude/skills
336
+ --codex Codex: ~/.agents/skills
337
+ --cursor Cursor: ~/.agents/skills (Cursor reads it; the same copy as --codex)
338
+ --all all of them (the default when no agent is named)
339
+ -y, --yes do not ask; without a terminal it never asks
340
+ `;
322
341
 
323
- // Agent skills shipped in this package (skills/<name>/SKILL.md, with any scripts beside it) into
324
- // the folders coding agents read. Overwrites only copies we installed earlier (marked
325
- // `installed-by: instabox`).
326
- function installSkills() {
327
- if (process.argv.includes('--no-skills')) return null;
328
- const src = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills');
329
- let names;
330
- try {
331
- names = readdirSync(src).filter((n) => existsSync(join(src, n, 'SKILL.md')));
332
- } catch {
333
- return null;
342
+ async function skillsCommand(argv) {
343
+ const [sub, ...rest] = argv;
344
+ if (sub === undefined || sub === '-h' || sub === '--help' || sub === 'help') {
345
+ console.log(SKILLS_USAGE);
346
+ return;
334
347
  }
335
- const roots = [join(homedir(), '.claude', 'skills'), join(homedir(), '.agents', 'skills')];
336
- // A skill counts as updated if any of its copies changed, else installed if one was new.
337
- const outcome = new Map();
338
- const rank = { unchanged: 0, installed: 1, updated: 2 };
339
- const kept = new Set();
340
- for (const root of roots) {
341
- for (const n of names) {
342
- const dest = join(root, n, 'SKILL.md');
343
- try {
344
- let what = 'installed';
345
- if (existsSync(dest)) {
346
- const old = readFileSync(dest, 'utf8');
347
- if (!old.includes('installed-by: instabox')) {
348
- kept.add(n);
349
- continue;
350
- }
351
- what = 'unchanged';
352
- }
353
- // SKILL.md and whatever else the skill ships (scripts/...).
354
- for (const f of skillFiles(join(src, n))) {
355
- const body = readFileSync(join(src, n, f));
356
- const to = join(root, n, f);
357
- if (existsSync(to) && readFileSync(to).equals(body)) continue;
358
- if (what === 'unchanged') what = 'updated';
359
- mkdirSync(dirname(to), { recursive: true });
360
- writeFileSync(to, body);
361
- }
362
- if (rank[what] > (rank[outcome.get(n)] ?? -1)) outcome.set(n, what);
363
- } catch {}
348
+ if (sub !== 'install') {
349
+ console.error(` instabox: unknown command "skills ${sub}"\n${SKILLS_USAGE}`);
350
+ process.exit(2);
351
+ }
352
+ const agents = [];
353
+ let yes = false;
354
+ for (const a of rest) {
355
+ if (a === '-h' || a === '--help') return console.log(SKILLS_USAGE);
356
+ if (a === '-y' || a === '--yes') yes = true;
357
+ else if (a === '--all') agents.push(...Object.keys(AGENT_DIRS));
358
+ else if (a.startsWith('--') && Object.hasOwn(AGENT_DIRS, a.slice(2))) agents.push(a.slice(2));
359
+ else {
360
+ console.error(` instabox: unknown flag ${a} for skills install\n${SKILLS_USAGE}`);
361
+ process.exit(2);
364
362
  }
365
363
  }
366
- const of = (what) => names.filter((n) => outcome.get(n) === what);
367
- const parts = [];
368
- if (of('updated').length) parts.push(`updated ${of('updated').join(', ')}`);
369
- if (of('installed').length) parts.push(`installed ${of('installed').join(', ')}`);
370
- if (of('unchanged').length) parts.push(`${of('unchanged').join(', ')} already up to date`);
371
- if (kept.size) parts.push(`kept your own ${[...kept].join(', ')}`);
372
- const retired = removeRetiredSkills(roots);
373
- if (retired.length) parts.push(`removed the old ${retired.join(', ')}`);
374
- return parts.length ? `agent skills: ${parts.join('; ')} ${dim('(~/.claude/skills, ~/.agents/skills)')}` : null;
375
- }
376
-
377
- // Waits for the CLI install started earlier, installs the skills, and prints a line for each.
378
- async function reportInstalls(cli) {
379
- const cliLine = await cli;
380
- if (cliLine) console.log(` ${green('✓')} ${cliLine}`);
381
- const skillsLine = installSkills();
382
- if (skillsLine) console.log(` ${green('✓')} ${skillsLine}`);
364
+ const roots = skillRoots(agents);
365
+ const shown = roots.map((r) => r.replace(homedir(), '~')).join(', ');
366
+ // Only a person at a terminal is asked, and only when no agent was named.
367
+ if (!yes && !agents.length && process.stdin.isTTY && process.stdout.isTTY) {
368
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
369
+ const answer = (await rl.question(` Install the instabox agent skills into ${shown}? [Y/n] `)).trim();
370
+ rl.close();
371
+ if (/^n/i.test(answer)) return;
372
+ }
373
+ const r = installSkills(roots);
374
+ if (!r) die('this package carries no skills (run from a checkout? npm pack copies them in)');
375
+ console.log(` ${green('✓')} ${describeSkills(r) ?? 'agent skills: nothing to install'} ${dim(`(${shown})`)}`);
376
+ if (r.failed.length) process.exit(1);
383
377
  }
384
378
 
385
379
  // The commands to run next, one per line, descriptions lined up.
@@ -395,8 +389,17 @@ async function onboard(wantBox) {
395
389
  console.log(`\n ${bold('instabox')} ${dim('· a cloud box in seconds')}\n`);
396
390
 
397
391
  const started = Date.now();
398
- client.use(await signedIn());
399
- const me = await client.call('GET', '/v1/me').catch((e) => die(`instabox did not accept the sign-in: ${e.message}`));
392
+ const session = await signedIn();
393
+ client.use(session);
394
+ const me = await client
395
+ .call('GET', '/v1/me')
396
+ .catch((e) =>
397
+ die(
398
+ API_KEY
399
+ ? `instabox did not accept INSTABOX_API_KEY: ${e.message}`
400
+ : `instabox did not accept the sign-in: ${e.message}`,
401
+ ),
402
+ );
400
403
  // outcome: ready, or quota_full; never names, paths or messages.
401
404
  const report = (outcome, extra = {}) =>
402
405
  track(
@@ -420,7 +423,12 @@ async function onboard(wantBox) {
420
423
  { server: API },
421
424
  );
422
425
  console.log(
423
- ` ${dim('signed in as')} ${bold(me.name || me.login)}${me.login && me.name ? dim(` (${me.login})`) : ''} ${dim('· not you?')} npx instabox@latest logout`,
426
+ ` ${dim('signed in as')} ${bold(me.name || me.login)}${me.login && me.name ? dim(` (${me.login})`) : ''} ` +
427
+ (API_KEY
428
+ ? dim('with INSTABOX_API_KEY')
429
+ : session.apiKey
430
+ ? dim('with an agent key')
431
+ : `${dim('· not you?')} npx instabox@latest logout`),
424
432
  );
425
433
 
426
434
  const cli = ensureCli();
@@ -512,6 +520,7 @@ async function onboard(wantBox) {
512
520
  // this package's own build when it carries one, else the installed CLI, else a fresh install.
513
521
  const args = process.argv.slice(2);
514
522
  if (args.length === 1 && (args[0] === '-h' || args[0] === '--help')) usage();
523
+ else if (args[0] === 'skills') await skillsCommand(args.slice(1));
515
524
  else if (isOnboarding(args)) await onboard(args.includes('sh'));
516
525
  else {
517
526
  let path = bundledCli() || findCli();
package/cli/SHA256SUMS CHANGED
@@ -1,6 +1,6 @@
1
- d5c8ad82b08cd2d83202a61e59290fd30b3bb33b5461b86aebdb407ed542694f instabox-darwin-amd64
2
- 7ba1e6a219e1cda32916953895d2583a24f9b0ebea4a027c428f290f123786a3 instabox-darwin-arm64
3
- 027b1af8ac556c9a6ef72d9ecfa100e447cf0c3e7fa2cd4ced8c53093005db5d instabox-linux-amd64
4
- 5848d4b62ad816107be1b46b218d1d227129b55cdf65b662b74e2a5b09b4f61a instabox-linux-arm64
5
- 1445567ac0abcd19934d50fbbfd8ba3e45c7594b8741b824a41dc143f8f8d370 instabox-windows-amd64.exe
6
- 7c8ef19418468a247452723cfb2a77b6cfa4b4d7cd04683fc354dc869d9bb175 instabox-windows-arm64.exe
1
+ 17b97e7017bee2e9b31b0a37e2ed9e1e7323434179097995cf60e212e2aa85c3 instabox-darwin-amd64
2
+ f88e557ed44edcf832ed792e74c65314ab733ba8fd5a8b2b79e373ac003f5482 instabox-darwin-arm64
3
+ 74542ca4d5b61f328879b7ac0318281896b4dd4af9bf97c8750e98ee569e41b4 instabox-linux-amd64
4
+ 4a62f75a0f5f73e661c160aa2c915040591f2d0e0bfa5eca6d6b93bf8aead2be instabox-linux-arm64
5
+ b72c212cd353552672d173cad436c23f1ccfb023eed969c34e135dfcf4fe0c07 instabox-windows-amd64.exe
6
+ 610f362a6ebae1d894c35388df39f158deb89cfa829c72a1de65bfa9188c5819 instabox-windows-arm64.exe
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/lib/api.mjs CHANGED
@@ -43,6 +43,8 @@ export function createClient(
43
43
 
44
44
  // The CLI's newer token if it already refreshed (a reused refresh token ends the session), else one rotation.
45
45
  async function renew() {
46
+ // An API key has nothing to refresh; a 401 is its answer.
47
+ if (session.apiKey) return false;
46
48
  const c = load();
47
49
  if (c?.accessToken && c.accessToken !== session.accessToken && new Date(c.accessExpiresAt) - Date.now() > 60_000) {
48
50
  session = c;
package/lib/cli.mjs CHANGED
@@ -19,7 +19,16 @@ import { dirname, join, sep } from 'node:path';
19
19
  import { fileURLToPath } from 'node:url';
20
20
  import { buildName, platformFor } from './platforms.mjs';
21
21
 
22
- const OUR_FLAGS = new Set(['--github', '--google', '--no-browser', '--no-cli', '--no-skills']);
22
+ const OUR_FLAGS = new Set([
23
+ '--github',
24
+ '--google',
25
+ '--no-browser',
26
+ '--no-cli',
27
+ '--no-skills',
28
+ '-y',
29
+ '--yes',
30
+ '--device-code',
31
+ ]);
23
32
 
24
33
  // Bare `instabox` or `sh`, with only our flags; anything else is a CLI command.
25
34
  export function isOnboarding(args) {
package/lib/skills.mjs ADDED
@@ -0,0 +1,120 @@
1
+ // The agent skills this package ships (skills/<name>/SKILL.md, with any scripts beside it), copied
2
+ // into the folders coding agents read. Nothing here signs in or talks to the network.
3
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ export const SKILLS_SRC = join(dirname(fileURLToPath(import.meta.url)), '..', 'skills');
9
+
10
+ // Where each agent reads user-level skills. Claude Code reads ~/.claude/skills. Codex and Cursor
11
+ // both read ~/.agents/skills (the user-level location in their docs), so one copy there serves
12
+ // both; a second copy in ~/.cursor/skills would show Cursor every skill twice.
13
+ export const AGENT_DIRS = {
14
+ claude: ['.claude', 'skills'],
15
+ codex: ['.agents', 'skills'],
16
+ cursor: ['.agents', 'skills'],
17
+ };
18
+
19
+ // The skill folders for the chosen agents (every agent when none is chosen), each once.
20
+ export function skillRoots(agents, home = homedir()) {
21
+ const names = agents.length ? agents : Object.keys(AGENT_DIRS);
22
+ return [...new Set(names.map((a) => join(home, ...AGENT_DIRS[a])))];
23
+ }
24
+
25
+ // A skill directory's files, relative to it, sorted.
26
+ function skillFiles(dir, rel = '') {
27
+ return readdirSync(join(dir, rel), { withFileTypes: true })
28
+ .sort((a, b) => a.name.localeCompare(b.name))
29
+ .flatMap((e) => {
30
+ const r = rel ? `${rel}/${e.name}` : e.name;
31
+ return e.isDirectory() ? skillFiles(dir, r) : e.isFile() ? [r] : [];
32
+ });
33
+ }
34
+
35
+ // Skills this package used to ship under another name. A copy we installed (marked
36
+ // `installed-by: instabox`) is removed, so the agent does not see both; the user's own is kept.
37
+ const RETIRED_SKILLS = [
38
+ 'instabox-migrate', // now instabox-move-repo
39
+ 'instabox-move-all', // now instabox-basic-setup
40
+ ];
41
+
42
+ function removeRetiredSkills(roots) {
43
+ const removed = new Set();
44
+ for (const root of roots)
45
+ for (const n of RETIRED_SKILLS) {
46
+ try {
47
+ if (!readFileSync(join(root, n, 'SKILL.md'), 'utf8').includes('installed-by: instabox')) continue;
48
+ rmSync(join(root, n), { recursive: true, force: true });
49
+ removed.add(n);
50
+ } catch {}
51
+ }
52
+ return [...removed];
53
+ }
54
+
55
+ // Copies the skills into each root, overwriting only copies we installed earlier (marked
56
+ // `installed-by: instabox`). Returns null when this package carries no skills, else the skill
57
+ // names by outcome: { installed, updated, unchanged, kept, removed, failed }.
58
+ export function installSkills(roots, src = SKILLS_SRC) {
59
+ let names;
60
+ try {
61
+ names = readdirSync(src).filter((n) => existsSync(join(src, n, 'SKILL.md')));
62
+ } catch {
63
+ return null;
64
+ }
65
+ // A skill counts as updated if any of its copies changed, else installed if one was new.
66
+ const outcome = new Map();
67
+ const rank = { unchanged: 0, installed: 1, updated: 2 };
68
+ const kept = new Set();
69
+ const failed = new Set();
70
+ for (const root of roots) {
71
+ for (const n of names) {
72
+ const dest = join(root, n, 'SKILL.md');
73
+ try {
74
+ let what = 'installed';
75
+ if (existsSync(dest)) {
76
+ const old = readFileSync(dest, 'utf8');
77
+ if (!old.includes('installed-by: instabox')) {
78
+ kept.add(n);
79
+ continue;
80
+ }
81
+ what = 'unchanged';
82
+ }
83
+ // SKILL.md and whatever else the skill ships (scripts/...).
84
+ for (const f of skillFiles(join(src, n))) {
85
+ const body = readFileSync(join(src, n, f));
86
+ const to = join(root, n, f);
87
+ if (existsSync(to) && readFileSync(to).equals(body)) continue;
88
+ if (what === 'unchanged') what = 'updated';
89
+ mkdirSync(dirname(to), { recursive: true });
90
+ writeFileSync(to, body);
91
+ }
92
+ if (rank[what] > (rank[outcome.get(n)] ?? -1)) outcome.set(n, what);
93
+ } catch {
94
+ failed.add(n);
95
+ }
96
+ }
97
+ }
98
+ const of = (what) => names.filter((n) => outcome.get(n) === what && !failed.has(n));
99
+ return {
100
+ installed: of('installed'),
101
+ updated: of('updated'),
102
+ unchanged: of('unchanged'),
103
+ kept: [...kept],
104
+ removed: removeRetiredSkills(roots),
105
+ failed: [...failed],
106
+ };
107
+ }
108
+
109
+ // What installSkills did, as one line, or null when there is nothing to say.
110
+ export function describeSkills(r) {
111
+ if (!r) return null;
112
+ const parts = [];
113
+ if (r.updated.length) parts.push(`updated ${r.updated.join(', ')}`);
114
+ if (r.installed.length) parts.push(`installed ${r.installed.join(', ')}`);
115
+ if (r.unchanged.length) parts.push(`${r.unchanged.join(', ')} already up to date`);
116
+ if (r.kept.length) parts.push(`kept your own ${r.kept.join(', ')}`);
117
+ if (r.removed.length) parts.push(`removed the old ${r.removed.join(', ')}`);
118
+ if (r.failed.length) parts.push(`could not write ${r.failed.join(', ')}`);
119
+ return parts.length ? `agent skills: ${parts.join('; ')}` : null;
120
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instabox",
3
- "version": "0.8.46",
3
+ "version": "0.8.48",
4
4
  "description": "A cloud box for your coding agent, the instabox CLI and its agent skills in one command: npx instabox@latest",
5
5
  "type": "module",
6
6
  "bin": {
@@ -16,11 +16,11 @@ instabox whoami # who is signed in (fails if nobody)
16
16
  instabox login # device code: open the printed page, enter the code
17
17
  ```
18
18
 
19
- A GitHub sign-in and a Google sign-in with the same verified email share one account; with different emails they are separate accounts with separate boxes. Both `instabox login` and `npx instabox@latest` sign in with a device code: they print a page and a code (and open the page when there is a browser); the user opens it on any device, enters the code and chooses GitHub or Google there. It works the same over SSH or on a machine without a browser. Relay the page and the code to the user and wait; the command finishes on its own. For Google directly, use `instabox login --provider google`, or `npx instabox@latest --google`. The CLI shares the sign-in `npx` made.
19
+ A GitHub sign-in and a Google sign-in with the same verified email share one account; with different emails they are separate accounts with separate boxes. Both `instabox login` and `npx instabox@latest` sign in with a device code: they print a page and a code (and open the page when there is a browser); the user opens it on any device, enters the code and chooses GitHub or Google there. It works the same over SSH or on a machine without a browser. Relay the page and the code to the user and wait; the command finishes on its own. Without a terminal, `npx instabox@latest` does not wait for a code: it uses `INSTABOX_API_KEY` when set (no browser, no code), else exits at once with status 3; add `--device-code` to get the page and code to relay. For Google directly, use `instabox login --provider google`, or `npx instabox@latest --google`. The CLI shares the sign-in `npx` made.
20
20
 
21
21
  `instabox update` replaces the CLI with the server's current build (it prints "already up to date" when it is); run it when a command the docs describe is missing, or when a command ends with `instabox X is outdated (latest Y): run instabox update` on stderr (a daily check; `instabox version --json` has `outdated`/`latest`; `INSTABOX_NO_UPDATE_CHECK=1` silences it). `instabox completion bash|zsh|fish|powershell` prints a shell completion script for the user's shell startup file.
22
22
 
23
- For CI or an agent without a browser, use an API key instead: `export INSTABOX_API_KEY=ibk_...` (the user creates it in the dashboard: API keys in the sidebar). Never print, commit or log a key.
23
+ For CI or an agent without a browser, use an API key instead: `export INSTABOX_API_KEY=ibk_...` (the user creates it in the dashboard: API keys in the sidebar). Never print, commit or log a key. Installing or updating these skills needs no sign-in: `npx instabox@latest skills install -y` (`--claude` for `~/.claude/skills`, `--codex` or `--cursor` for `~/.agents/skills`; none named: all).
24
24
 
25
25
  With no key and nobody at the keyboard, sign up by yourself: `instabox login --agent` (optionally `--name 'what this agent is' --scope machines:read,machines:write --expires-in-days 30`). It prints a claim page (`https://dashboard.instabox.sh/dashboard/claim?code=XXXX-XXXX`) and the code; relay both to the user and wait. The user opens the page on any device, signs in or signs up, checks what you ask for and approves; the command then finishes on its own, stores the API key made for you in the user's account, and every later `instabox` command uses it. The code works for 24 hours. If you must do other work meanwhile, `instabox login --agent --no-wait` prints the page and code and exits, and `instabox login --agent` later resumes the same registration. Before the user approves you have no account and no credential: do not retry other commands, wait. A denial or an expired code is an error; register again only if the user asks. The user can also approve from their own terminal with `instabox claim XXXX-XXXX`.
26
26