instabox 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/instabox.mjs +43 -2
- package/lib/api.mjs +8 -1
- package/lib/telemetry.mjs +97 -0
- package/package.json +1 -1
package/bin/instabox.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { checkCli, findCli, installCli, installCommand, isOnboarding, real, runC
|
|
|
25
25
|
import { browserCommand, callbackHandler } from '../lib/login.mjs';
|
|
26
26
|
import { makeDirPrivate, sessionArgs } from '../lib/ssh.mjs';
|
|
27
27
|
import { loadCredentials, saveCredentials } from '../lib/credentials.mjs';
|
|
28
|
+
import { agentName, track } from '../lib/telemetry.mjs';
|
|
28
29
|
|
|
29
30
|
// Same variables, same order as the instabox CLI, so both talk to one server.
|
|
30
31
|
const API = (
|
|
@@ -61,11 +62,22 @@ function usage() {
|
|
|
61
62
|
--no-cli do not install or update the instabox CLI
|
|
62
63
|
--no-skills do not install the instabox agent skills
|
|
63
64
|
npx instabox@latest <command> run an instabox CLI command (instabox help lists them)
|
|
65
|
+
|
|
66
|
+
Usage data (steps and outcomes, never names or arguments) goes to the instabox team;
|
|
67
|
+
INSTABOX_NO_TELEMETRY=1 or DO_NOT_TRACK=1 turns it off.
|
|
64
68
|
`);
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
// ---- instabox API ------------------------------------------------------------------------------
|
|
68
|
-
const
|
|
72
|
+
const VERSION = (() => {
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
|
|
75
|
+
} catch {
|
|
76
|
+
return 'unknown';
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
const client = createClient(API, { userAgent: `instabox-npm/${VERSION}` });
|
|
80
|
+
let freshSignIn = false;
|
|
69
81
|
|
|
70
82
|
function openBrowser(url) {
|
|
71
83
|
const cmd = browserCommand(url);
|
|
@@ -210,6 +222,7 @@ async function signedIn() {
|
|
|
210
222
|
} catch {}
|
|
211
223
|
}
|
|
212
224
|
const t = await signInWithInvite();
|
|
225
|
+
freshSignIn = true;
|
|
213
226
|
saveCredentials(API, t);
|
|
214
227
|
return t;
|
|
215
228
|
}
|
|
@@ -396,8 +409,31 @@ function nextSteps(rows) {
|
|
|
396
409
|
async function onboard(wantBox) {
|
|
397
410
|
console.log(`\n ${bold('instabox')} ${dim('· a cloud box in seconds')}\n`);
|
|
398
411
|
|
|
412
|
+
const started = Date.now();
|
|
399
413
|
client.use(await signedIn());
|
|
400
414
|
const me = await client.call('GET', '/v1/me').catch((e) => die(`instabox did not accept the sign-in: ${e.message}`));
|
|
415
|
+
// outcome: ready, or quota_full; never names, paths or messages.
|
|
416
|
+
const report = (outcome, extra = {}) =>
|
|
417
|
+
track(
|
|
418
|
+
'npx_onboard',
|
|
419
|
+
me.userId,
|
|
420
|
+
{
|
|
421
|
+
outcome,
|
|
422
|
+
want_box: wantBox,
|
|
423
|
+
fresh_sign_in: freshSignIn,
|
|
424
|
+
duration_ms: Date.now() - started,
|
|
425
|
+
no_cli: process.argv.includes('--no-cli'),
|
|
426
|
+
no_skills: process.argv.includes('--no-skills'),
|
|
427
|
+
tty: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
428
|
+
agent: agentName(),
|
|
429
|
+
os: process.platform,
|
|
430
|
+
arch: process.arch,
|
|
431
|
+
node_version: process.versions.node,
|
|
432
|
+
npm_version: VERSION,
|
|
433
|
+
...extra,
|
|
434
|
+
},
|
|
435
|
+
{ server: API },
|
|
436
|
+
);
|
|
401
437
|
console.log(
|
|
402
438
|
` ${dim('signed in as')} ${bold(me.name || me.login)}${me.login && me.name ? dim(` (${me.login})`) : ''} ${dim('· not you?')} npx instabox@latest logout`,
|
|
403
439
|
);
|
|
@@ -405,6 +441,7 @@ async function onboard(wantBox) {
|
|
|
405
441
|
const cli = ensureCli();
|
|
406
442
|
if (!wantBox) {
|
|
407
443
|
await reportInstalls(cli);
|
|
444
|
+
await report('ready');
|
|
408
445
|
return nextSteps([
|
|
409
446
|
['npx instabox@latest sh', 'start a new box and SSH into it'],
|
|
410
447
|
['instabox ls', 'list your boxes'],
|
|
@@ -426,15 +463,18 @@ async function onboard(wantBox) {
|
|
|
426
463
|
// The way out of the limit is `instabox rm`, so the CLI and the skills land before the exit.
|
|
427
464
|
console.log();
|
|
428
465
|
await reportInstalls(cli);
|
|
466
|
+
await report('quota_full');
|
|
429
467
|
return quotaFull(e);
|
|
430
468
|
});
|
|
431
|
-
const
|
|
469
|
+
const boxMs = Date.now() - t0;
|
|
470
|
+
const ready = `ready in ${(boxMs / 1000).toFixed(1)}s`;
|
|
432
471
|
if (!tty) {
|
|
433
472
|
console.log(`\n ${green('✓')} box ${bold(box.name)} ${ready}`);
|
|
434
473
|
const got = await client.call('GET', '/v1/machines/' + enc(box.id)).catch(() => null);
|
|
435
474
|
const note = guestUserNote(box.user, box.name, got?.guestUser);
|
|
436
475
|
if (note) console.log(dim(` ${note}`));
|
|
437
476
|
await reportInstalls(cli);
|
|
477
|
+
await report('ready', { box_ready_ms: boxMs });
|
|
438
478
|
return nextSteps([
|
|
439
479
|
[`instabox ssh ${box.name} -- <command>`, 'run a command in it'],
|
|
440
480
|
[`instabox ssh ${box.name}`, 'open a shell in it, from a terminal'],
|
|
@@ -472,6 +512,7 @@ async function onboard(wantBox) {
|
|
|
472
512
|
const note = guestUserNote(box.user, m.name, m.guestUser);
|
|
473
513
|
if (note) console.log(dim(` ${note}`));
|
|
474
514
|
await reportInstalls(cli);
|
|
515
|
+
await report('ready', { box_ready_ms: boxMs, ssh: true });
|
|
475
516
|
console.log(dim(` type ${'exit'} to leave · stuck? press Enter, then ~.`));
|
|
476
517
|
// The box's prompt is ~37 columns; narrower terminals make bash scroll the input line sideways.
|
|
477
518
|
if (process.stdout.isTTY && process.stdout.columns > 0 && process.stdout.columns < 60)
|
package/lib/api.mjs
CHANGED
|
@@ -12,12 +12,19 @@ export class ApiError extends Error {
|
|
|
12
12
|
|
|
13
13
|
export function createClient(
|
|
14
14
|
server,
|
|
15
|
-
{
|
|
15
|
+
{
|
|
16
|
+
fetch = globalThis.fetch,
|
|
17
|
+
load = () => loadCredentials(server),
|
|
18
|
+
save = (t) => saveCredentials(server, t),
|
|
19
|
+
userAgent = '',
|
|
20
|
+
} = {},
|
|
16
21
|
) {
|
|
17
22
|
let session = null;
|
|
18
23
|
|
|
19
24
|
const send = (token, method, path, body, key) => {
|
|
20
25
|
const headers = { Accept: 'application/json' };
|
|
26
|
+
// instabox-npm/<version>: the server attributes the requests to this package.
|
|
27
|
+
if (userAgent) headers['User-Agent'] = userAgent;
|
|
21
28
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
22
29
|
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
|
23
30
|
if (key) headers['Idempotency-Key'] = key;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Usage telemetry (docs/analytics.md); telemetry.json is shared with cmd/instabox/telemetry.go.
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { configDir } from './credentials.mjs';
|
|
6
|
+
|
|
7
|
+
// A write-only capture key, not a secret.
|
|
8
|
+
export const TELEMETRY_KEY = 'phc_nPznuUD5U36v4aSQVHaV4mbKwGTpjjR8Xt9Ukb5mATfY';
|
|
9
|
+
export const TELEMETRY_HOST = 'https://us.i.posthog.com';
|
|
10
|
+
export const DEFAULT_SERVER = 'https://api.instabox.sh';
|
|
11
|
+
// The instabox CLI prints the same when it creates telemetry.json (cmd/instabox/telemetry.go).
|
|
12
|
+
const NOTICE =
|
|
13
|
+
'instabox sends usage data (command names and outcomes, never arguments) to help improve it;\n' +
|
|
14
|
+
'set INSTABOX_NO_TELEMETRY=1 or DO_NOT_TRACK=1 to turn it off.\n';
|
|
15
|
+
|
|
16
|
+
export function telemetryDisabled(env = process.env) {
|
|
17
|
+
const on = (k) =>
|
|
18
|
+
!['', '0', 'false'].includes(
|
|
19
|
+
String(env[k] ?? '')
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase(),
|
|
22
|
+
);
|
|
23
|
+
return on('DO_NOT_TRACK') || on('INSTABOX_NO_TELEMETRY');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function agentName(env = process.env) {
|
|
27
|
+
if (env.CLAUDECODE) return 'claude-code';
|
|
28
|
+
if (env.CURSOR_TRACE_ID) return 'cursor';
|
|
29
|
+
if (env.CODEX_SANDBOX) return 'codex';
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readState(env) {
|
|
34
|
+
try {
|
|
35
|
+
const st = JSON.parse(readFileSync(join(configDir(env), 'telemetry.json'), 'utf8'));
|
|
36
|
+
return st?.anonymousId ? st : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeState(env, st) {
|
|
43
|
+
const dir = configDir(env);
|
|
44
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
45
|
+
chmodSync(dir, 0o700);
|
|
46
|
+
const tmp = join(dir, `.telemetry.${randomUUID()}.tmp`);
|
|
47
|
+
writeFileSync(tmp, JSON.stringify(st) + '\n', { mode: 0o600 });
|
|
48
|
+
renameSync(tmp, join(dir, 'telemetry.json'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Sends event for the account userId. Never throws; waits at most 1.5 s.
|
|
52
|
+
export async function track(
|
|
53
|
+
event,
|
|
54
|
+
userId,
|
|
55
|
+
properties,
|
|
56
|
+
{
|
|
57
|
+
server,
|
|
58
|
+
env = process.env,
|
|
59
|
+
fetch = globalThis.fetch,
|
|
60
|
+
key = TELEMETRY_KEY,
|
|
61
|
+
host = TELEMETRY_HOST,
|
|
62
|
+
stderr = process.stderr,
|
|
63
|
+
} = {},
|
|
64
|
+
) {
|
|
65
|
+
if (!key || !userId || String(server).replace(/\/+$/, '') !== DEFAULT_SERVER || telemetryDisabled(env)) return;
|
|
66
|
+
try {
|
|
67
|
+
let st = readState(env);
|
|
68
|
+
if (!st) {
|
|
69
|
+
st = { anonymousId: randomUUID() };
|
|
70
|
+
writeState(env, st);
|
|
71
|
+
if (stderr.isTTY) stderr.write(NOTICE);
|
|
72
|
+
}
|
|
73
|
+
// Another account than the last one: a fresh anonymous ID to link.
|
|
74
|
+
if (st.userId && st.userId !== userId) st = { anonymousId: randomUUID() };
|
|
75
|
+
const id = String(userId);
|
|
76
|
+
const lib = { $lib: 'instabox-npm' };
|
|
77
|
+
const batch = [
|
|
78
|
+
{
|
|
79
|
+
event,
|
|
80
|
+
distinct_id: id,
|
|
81
|
+
uuid: randomUUID(),
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
properties: { ...properties, ...lib },
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
const merge = st.userId !== userId || !st.merged;
|
|
87
|
+
if (merge)
|
|
88
|
+
batch.push({ event: '$identify', distinct_id: id, properties: { $anon_distinct_id: st.anonymousId, ...lib } });
|
|
89
|
+
const res = await fetch(host + '/batch/', {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: { 'Content-Type': 'application/json' },
|
|
92
|
+
body: JSON.stringify({ api_key: key, batch }),
|
|
93
|
+
signal: AbortSignal.timeout(1500),
|
|
94
|
+
});
|
|
95
|
+
if (res.ok && merge) writeState(env, { anonymousId: st.anonymousId, userId, merged: true });
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|