insta 0.0.53 → 0.0.55
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 +1 -0
- package/dist/commands/compute.js +3 -3
- package/dist/commands/db.js +2 -2
- package/dist/commands/deploy.js +2 -3
- package/dist/commands/feedback.js +5 -5
- package/dist/commands/run.js +3 -4
- package/dist/commands/services.js +6 -4
- package/dist/commands/template.js +9 -7
- package/dist/index.js +19 -4
- package/dist/resolve-service.js +5 -4
- package/dist/telemetry.js +217 -0
- package/dist/util.js +14 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -233,6 +233,7 @@ build never reaches a production installer.
|
|
|
233
233
|
| `INSTA_PROJECT_ID` · `INSTA_ORG_ID` · `INSTA_BRANCH` | Target a project, org or branch without linking |
|
|
234
234
|
| `INSTA_PASSWORD` | Password for non-interactive login |
|
|
235
235
|
| `INSTA_NO_AUTOUPDATE` | Disable self-update |
|
|
236
|
+
| `INSTA_NO_TELEMETRY` · `DO_NOT_TRACK` | Disable usage analytics. Each command sends one event (command, flags, outcome, version, OS) to the same PostHog project as the console. Only ids, enums and numbers among the arguments are kept — names, branches, keys, paths, secret values, free text and error messages never leave the machine; custom API hosts report nothing |
|
|
236
237
|
|
|
237
238
|
## Agent skills
|
|
238
239
|
|
package/dist/commands/compute.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
2
|
-
import { info, printJson, handleApproval } from '../util.js';
|
|
2
|
+
import { info, printJson, handleApproval, relayExitCode } from '../util.js';
|
|
3
3
|
import { resolveComputeServiceId, q, parseVolumeGib } from './services.js';
|
|
4
4
|
export const isWorker = (s) => s.port === 0;
|
|
5
5
|
// One line per compute service for the disambiguation error: name, region, default URL, status —
|
|
@@ -543,10 +543,10 @@ export function applyExecResult(res, json) {
|
|
|
543
543
|
// visible. Normal codes pass through untouched.
|
|
544
544
|
if (exitCode < 0 || exitCode > 255) {
|
|
545
545
|
process.stderr.write(`note: remote exit code ${exitCode} out of range — exiting 1\n`);
|
|
546
|
-
|
|
546
|
+
relayExitCode(1);
|
|
547
547
|
}
|
|
548
548
|
else {
|
|
549
|
-
|
|
549
|
+
relayExitCode(exitCode);
|
|
550
550
|
}
|
|
551
551
|
}
|
|
552
552
|
// One HTTP round trip, not a shell session: no PTY, no interactivity, stdout/stderr come back as
|
package/dist/commands/db.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { constants as osConstants } from 'node:os';
|
|
3
3
|
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
4
|
-
import { info, printJson, handleApproval } from '../util.js';
|
|
4
|
+
import { info, printJson, handleApproval, relayExitCode } from '../util.js';
|
|
5
5
|
import { parseVolumeGib, q, resolveSoleService } from './services.js';
|
|
6
6
|
// Toggle a postgres service between scale-to-zero (the default: instance suspends when idle,
|
|
7
7
|
// cold-starts on the next connection) and always-on (instance stays warm; idle RAM bills at
|
|
@@ -330,6 +330,6 @@ export async function dbConnect(opts) {
|
|
|
330
330
|
return;
|
|
331
331
|
// stderr: stdout belongs to psql (the `insta run` rule).
|
|
332
332
|
process.stderr.write(`psql → postgres/${r.serviceName}${branch ? ` (branch ${branch})` : ''} — a suspended instance wakes on connect, so the first prompt can take a few seconds\n`);
|
|
333
|
-
|
|
333
|
+
relayExitCode(await connectWithPsql(r.url));
|
|
334
334
|
}
|
|
335
335
|
//# sourceMappingURL=db.js.map
|
package/dist/commands/deploy.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { resolve, join } from 'node:path';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
4
|
-
import { info, die, printJson, handleApproval, renderNextActions } from '../util.js';
|
|
4
|
+
import { info, die, printJson, handleApproval, renderNextActions, CliExit } from '../util.js';
|
|
5
5
|
import { flyctlBuildAndPush, ensureFlyctl, defaultBuildRunner, stderrBuildRunner } from '../flyctl-build.js';
|
|
6
6
|
// With --json, stdout must carry exactly one JSON document (the deploy result), so every progress
|
|
7
7
|
// line moves to stderr.
|
|
@@ -116,9 +116,8 @@ export async function buildFromSource(api, projectId, dir, branch, opts, run = o
|
|
|
116
116
|
log(` built ${built}`);
|
|
117
117
|
return built;
|
|
118
118
|
}
|
|
119
|
-
// exit() with no argument honors the exit code handleApproval just set (2).
|
|
120
119
|
if (handleApproval(tok, opts.json))
|
|
121
|
-
|
|
120
|
+
throw new CliExit();
|
|
122
121
|
const { token, flyApp } = tok.body;
|
|
123
122
|
await ensureFlyctl(); // cloud path only — the local path needs docker, which the daemon requires anyway
|
|
124
123
|
const port = opts.port ? Number(opts.port) : 8080;
|
|
@@ -12,7 +12,7 @@ import os from 'node:os';
|
|
|
12
12
|
import * as clack from '@clack/prompts';
|
|
13
13
|
import { readGlobal, readProject } from '../config.js';
|
|
14
14
|
import { envForApiUrl } from '../env.js';
|
|
15
|
-
import { info, printJson } from '../util.js';
|
|
15
|
+
import { info, printJson, CliCancel } from '../util.js';
|
|
16
16
|
import { clean } from '../redact.js';
|
|
17
17
|
export const TYPES = ['bug', 'feature-request', 'friction', 'other'];
|
|
18
18
|
export const COMPONENTS = ['cli', 'mcp', 'platform', 'skills', 'docs', 'other'];
|
|
@@ -73,7 +73,7 @@ async function promptMissing(opts) {
|
|
|
73
73
|
],
|
|
74
74
|
});
|
|
75
75
|
if (clack.isCancel(answer))
|
|
76
|
-
|
|
76
|
+
throw new CliCancel();
|
|
77
77
|
opts.type = answer;
|
|
78
78
|
}
|
|
79
79
|
if (!opts.component) {
|
|
@@ -82,7 +82,7 @@ async function promptMissing(opts) {
|
|
|
82
82
|
options: COMPONENTS.map((c) => ({ value: c, label: c })),
|
|
83
83
|
});
|
|
84
84
|
if (clack.isCancel(answer))
|
|
85
|
-
|
|
85
|
+
throw new CliCancel();
|
|
86
86
|
opts.component = answer;
|
|
87
87
|
}
|
|
88
88
|
if (!opts.title) {
|
|
@@ -91,7 +91,7 @@ async function promptMissing(opts) {
|
|
|
91
91
|
validate: (v) => (v.trim() ? undefined : 'required'),
|
|
92
92
|
});
|
|
93
93
|
if (clack.isCancel(answer))
|
|
94
|
-
|
|
94
|
+
throw new CliCancel();
|
|
95
95
|
opts.title = answer.trim();
|
|
96
96
|
}
|
|
97
97
|
if (!opts.detail && !opts.file) {
|
|
@@ -100,7 +100,7 @@ async function promptMissing(opts) {
|
|
|
100
100
|
validate: (v) => (v.trim() ? undefined : 'required'),
|
|
101
101
|
});
|
|
102
102
|
if (clack.isCancel(answer))
|
|
103
|
-
|
|
103
|
+
throw new CliCancel();
|
|
104
104
|
opts.detail = answer.trim();
|
|
105
105
|
}
|
|
106
106
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// as the process does.
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { ApiClient, requireProject } from '../api.js';
|
|
7
|
-
import { die, handleApproval } from '../util.js';
|
|
7
|
+
import { CliExit, die, handleApproval, relayExitCode } from '../util.js';
|
|
8
8
|
/** Core, dependency-injected for tests: spawn cmd with the bundle in env, return its exit code. */
|
|
9
9
|
export async function runWithSecrets(cmd, args, deps) {
|
|
10
10
|
const bundle = await deps.fetchBundle();
|
|
@@ -28,15 +28,14 @@ export async function run(cmdAndArgs, opts) {
|
|
|
28
28
|
const code = await runWithSecrets(cmd, rest, {
|
|
29
29
|
fetchBundle: async () => {
|
|
30
30
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/secrets?branch=${encodeURIComponent(branch)}`);
|
|
31
|
-
// exit() with no argument honors the exit code handleApproval just set (2).
|
|
32
31
|
if (handleApproval(res))
|
|
33
|
-
|
|
32
|
+
throw new CliExit();
|
|
34
33
|
// stderr, not stdout: `insta run`'s stdout belongs entirely to the child command (that's why
|
|
35
34
|
// run has no --json — wrapping would break the child's own output contract).
|
|
36
35
|
process.stderr.write(`running with ${Object.keys(res.body.secrets).length} injected secrets (branch ${branch}) — nothing written to disk\n`);
|
|
37
36
|
return res.body.secrets;
|
|
38
37
|
},
|
|
39
38
|
});
|
|
40
|
-
|
|
39
|
+
relayExitCode(code);
|
|
41
40
|
}
|
|
42
41
|
//# sourceMappingURL=run.js.map
|
|
@@ -16,11 +16,13 @@ export function assertServiceName(name) {
|
|
|
16
16
|
if (!SERVICE_NAME_RE.test(name))
|
|
17
17
|
throw new Error('service name must be lower-kebab (a-z, 0-9, -)');
|
|
18
18
|
}
|
|
19
|
-
|
|
19
|
+
const MAX_COMPUTE_REPLICAS = 10;
|
|
20
|
+
// Parse a replica count inside the compute plane's current safety ceiling.
|
|
20
21
|
export function parseCount(raw) {
|
|
21
22
|
const n = Number(raw);
|
|
22
|
-
if (!Number.isInteger(n) || n < 1)
|
|
23
|
-
throw new Error(`count must be
|
|
23
|
+
if (!Number.isInteger(n) || n < 1 || n > MAX_COMPUTE_REPLICAS) {
|
|
24
|
+
throw new Error(`count must be an integer between 1 and ${MAX_COMPUTE_REPLICAS}, got: ${raw}`);
|
|
25
|
+
}
|
|
24
26
|
return n;
|
|
25
27
|
}
|
|
26
28
|
// Parse a TCP port. Junk fails here rather than reaching the API as NaN (the parseCpu lesson).
|
|
@@ -216,7 +218,7 @@ export async function servicesScale(type, name, number, region, _opts) {
|
|
|
216
218
|
return;
|
|
217
219
|
if (_opts.json)
|
|
218
220
|
return printJson(res.body.service);
|
|
219
|
-
info(`scaled compute ${name} to ${machineCount}
|
|
221
|
+
info(`scaled compute ${name} to ${machineCount} replica(s)${region ? ` in ${region}` : ''}`);
|
|
220
222
|
}
|
|
221
223
|
// insta services upgrade <compute|postgres> <name> <new-spec>
|
|
222
224
|
export async function servicesUpgrade(type, name, spec, _opts) {
|
|
@@ -7,18 +7,20 @@ import { existsSync } from 'node:fs';
|
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import * as clack from '@clack/prompts';
|
|
9
9
|
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
10
|
-
import { info, printJson, handleApproval, renderNextActions } from '../util.js';
|
|
10
|
+
import { info, printJson, handleApproval, renderNextActions, CliCancel } from '../util.js';
|
|
11
11
|
import { MANIFEST_FILE, collectManifestVariables, loadTemplateManifest } from '../template-manifest.js';
|
|
12
12
|
// One aligned row per template; numeric columns right-aligned. Plain padded columns, as the rest
|
|
13
13
|
// of the CLI (storage list, compute check-domain) — no table library.
|
|
14
14
|
export function templateListLines(templates) {
|
|
15
15
|
if (!templates.length)
|
|
16
16
|
return ['(no templates published yet)'];
|
|
17
|
-
const head = ['CODE', 'VERSION', 'CATEGORY', '
|
|
17
|
+
const head = ['CODE', 'VERSION', 'CATEGORY', 'PROJECTS', 'SUCCESS', 'NAME'];
|
|
18
18
|
const numeric = [false, false, false, true, true, false];
|
|
19
19
|
const rows = templates.map((t) => [
|
|
20
20
|
t.code, t.version ?? '', t.category ?? '-',
|
|
21
|
-
String(t.
|
|
21
|
+
String(t.totalProjects ?? 0),
|
|
22
|
+
// null = nothing has concluded yet; the platform never sends 0 for that.
|
|
23
|
+
t.successRate == null ? '-' : `${t.successRate}%`,
|
|
22
24
|
t.tagline ? `${t.name} — ${t.tagline}` : (t.name ?? ''),
|
|
23
25
|
]);
|
|
24
26
|
const widths = head.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
|
|
@@ -148,12 +150,12 @@ export async function resolveVariables(vars, given, opts = {}) {
|
|
|
148
150
|
return values;
|
|
149
151
|
}
|
|
150
152
|
// The platform's machine-readable "you forgot these" answer to the POST (error=missing_variables,
|
|
151
|
-
// missing: [{name, key, description}]
|
|
152
|
-
//
|
|
153
|
+
// missing: [{name, key, description}]) — turned back into promptable variables. null = some other
|
|
154
|
+
// error, not ours to interpret.
|
|
153
155
|
export function missingVariablesFrom(body) {
|
|
154
156
|
if ((body?.error ?? body?.code) !== 'missing_variables')
|
|
155
157
|
return null;
|
|
156
|
-
const list = body?.missing ??
|
|
158
|
+
const list = body?.missing ?? [];
|
|
157
159
|
if (!Array.isArray(list))
|
|
158
160
|
return [];
|
|
159
161
|
return list.map((v) => ({ name: String(v.name ?? v.key ?? v), required: true, description: v.description }));
|
|
@@ -296,7 +298,7 @@ async function promptVariable(v) {
|
|
|
296
298
|
validate: (s) => (s.trim() ? undefined : 'required'),
|
|
297
299
|
});
|
|
298
300
|
if (clack.isCancel(answer))
|
|
299
|
-
|
|
301
|
+
throw new CliCancel();
|
|
300
302
|
return answer.trim();
|
|
301
303
|
}
|
|
302
304
|
export async function templateDeploy(target, opts = {}, deps = {}) {
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { Command } from 'commander';
|
|
4
4
|
import { ApiError } from './api.js';
|
|
5
|
-
import { CliExit, fail } from './util.js';
|
|
5
|
+
import { CliCancel, CliExit, fail, relayedExitCode } from './util.js';
|
|
6
|
+
import { trackCommand } from './telemetry.js';
|
|
6
7
|
import * as auth from './commands/auth.js';
|
|
7
8
|
import * as envCmd_ from './commands/env.js';
|
|
8
9
|
import { ENV_NAMES } from './env.js';
|
|
@@ -31,14 +32,28 @@ import { billing, billingUpgrade, billingPortal } from './commands/billing.js';
|
|
|
31
32
|
import * as selfUpdate from './commands/upgrade.js';
|
|
32
33
|
import * as feedbackCmd from './commands/feedback.js';
|
|
33
34
|
function onError(e) {
|
|
34
|
-
if (e instanceof CliExit)
|
|
35
|
+
if (e instanceof CliExit || e instanceof CliCancel)
|
|
35
36
|
return;
|
|
36
37
|
if (e instanceof ApiError)
|
|
37
38
|
return fail(`${e.message} (HTTP ${e.status})`);
|
|
38
39
|
fail(e instanceof Error ? e.message : String(e));
|
|
39
40
|
}
|
|
40
41
|
// Wrap an async action so rejections surface as clean CLI errors.
|
|
41
|
-
|
|
42
|
+
// commander appends (options, command) to every action's arguments, so the command is always last.
|
|
43
|
+
const guard = (fn) => async (...a) => {
|
|
44
|
+
const started = Date.now();
|
|
45
|
+
let error;
|
|
46
|
+
try {
|
|
47
|
+
await fn(...a);
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
error = e;
|
|
51
|
+
onError(e);
|
|
52
|
+
}
|
|
53
|
+
await trackCommand(a[a.length - 1], a.slice(0, -2), {
|
|
54
|
+
error, durationMs: Date.now() - started, exitCode: Number(process.exitCode ?? 0), childExitCode: relayedExitCode(),
|
|
55
|
+
}, resolveVersion());
|
|
56
|
+
};
|
|
42
57
|
const program = new Command();
|
|
43
58
|
// Positional options: some command groups (e.g. `secrets`, `billing`) declare a flag (like
|
|
44
59
|
// --branch or --org) both on the group itself (for its own default action) and on a subcommand
|
|
@@ -145,7 +160,7 @@ svc.command('rename <type> <name> <new-name>').description('Rename a service and
|
|
|
145
160
|
.action(guard((type, name, newName, o) => services.servicesRename(type, name, newName, o)));
|
|
146
161
|
svc.command('set-access <type> <name> <access>').description('Set a storage service bucket access mode (access: public|private)')
|
|
147
162
|
.option('--json').action(guard((type, name, access, o) => services.servicesSetAccess(type, name, access, o)));
|
|
148
|
-
svc.command('scale <type> <name> <number> [region]').description('Set a compute service
|
|
163
|
+
svc.command('scale <type> <name> <number> [region]').description('Set a compute service same-region replica count from 1 to 10 (paid plans only)')
|
|
149
164
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)));
|
|
150
165
|
svc.command('upgrade <type> <name> <spec>').description('Change a compute service spec (paid plans only). Postgres upgrades are rejected by the platform — use `insta db limits` instead')
|
|
151
166
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)));
|
package/dist/resolve-service.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// as an error, because nothing was created and a silent exit 0 would read as success.
|
|
7
7
|
import * as clack from '@clack/prompts';
|
|
8
8
|
import { SERVICE_TYPES, assertServiceName, parsePort } from './commands/services.js';
|
|
9
|
+
import { CliCancel } from './util.js';
|
|
9
10
|
// Same order, labels and default names as the dashboard's Add Service menu. Github Repo is left
|
|
10
11
|
// out: the platform has no repo path yet, so a CLI entry could only say "coming soon".
|
|
11
12
|
export const SERVICE_KINDS = [
|
|
@@ -98,7 +99,7 @@ export async function promptServiceKind(kinds) {
|
|
|
98
99
|
options: kinds.map((k) => ({ value: k.id, label: k.label, hint: k.hint })),
|
|
99
100
|
});
|
|
100
101
|
if (clack.isCancel(picked))
|
|
101
|
-
|
|
102
|
+
throw new CliCancel();
|
|
102
103
|
// Resolve against the list that was displayed — a subset must not fall through to the registry.
|
|
103
104
|
return kinds.find((k) => k.id === picked);
|
|
104
105
|
}
|
|
@@ -109,7 +110,7 @@ export async function promptImageRef() {
|
|
|
109
110
|
validate: (v) => (normalizeImageRef(v) ? undefined : 'an image reference is required'),
|
|
110
111
|
});
|
|
111
112
|
if (clack.isCancel(answer))
|
|
112
|
-
|
|
113
|
+
throw new CliCancel();
|
|
113
114
|
return answer;
|
|
114
115
|
}
|
|
115
116
|
export async function promptServiceName(kind, suggested) {
|
|
@@ -128,7 +129,7 @@ export async function promptServiceName(kind, suggested) {
|
|
|
128
129
|
},
|
|
129
130
|
});
|
|
130
131
|
if (clack.isCancel(answer))
|
|
131
|
-
|
|
132
|
+
throw new CliCancel();
|
|
132
133
|
return answer.trim();
|
|
133
134
|
}
|
|
134
135
|
export async function promptPort(fallback) {
|
|
@@ -147,7 +148,7 @@ export async function promptPort(fallback) {
|
|
|
147
148
|
},
|
|
148
149
|
});
|
|
149
150
|
if (clack.isCancel(answer))
|
|
150
|
-
|
|
151
|
+
throw new CliCancel();
|
|
151
152
|
return answer.trim();
|
|
152
153
|
}
|
|
153
154
|
/** Prompts on a real terminal only — an agent's stdin is not one, and must never block. */
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import { ApiError } from './api.js';
|
|
6
|
+
import { readGlobal, readProject } from './config.js';
|
|
7
|
+
import { ENVS, envForApiUrl, isEnvName, normalizeUrl } from './env.js';
|
|
8
|
+
import { COMPONENTS, SEVERITIES, TYPES } from './commands/feedback.js';
|
|
9
|
+
import { SERVICE_TYPES } from './commands/services.js';
|
|
10
|
+
import { detectChannel } from './commands/upgrade.js';
|
|
11
|
+
import { CliCancel, CliExit } from './util.js';
|
|
12
|
+
export const POSTHOG_HOST = 'https://us.i.posthog.com';
|
|
13
|
+
// The console's project keys (insta-frontend src/lib/analytics.ts), so a CLI event lands on the
|
|
14
|
+
// person the console identified. Write-only capture tokens: safe to ship in a public binary.
|
|
15
|
+
const PROJECT_KEYS = {
|
|
16
|
+
prod: 'phc_yHWfNfkDuQpJ34yKQ4equid3j64zj3tBRtpej3b5i8QH',
|
|
17
|
+
staging: 'phc_BeXdaHFfeaCJFAH26TyEi3LB9xwHj23U6LdfKWP46G4U',
|
|
18
|
+
};
|
|
19
|
+
// The send is awaited before the process exits, so it gets one bounded attempt and no retry.
|
|
20
|
+
const SEND_TIMEOUT_MS = 1500;
|
|
21
|
+
const REDACTED = '[REDACTED]';
|
|
22
|
+
const oneOf = (values) => (v) => values.includes(v);
|
|
23
|
+
const ID = (v) => /^(?:[0-9a-f]{8}-[0-9a-f-]{27}|[a-z]+_[\w-]{1,64}|(?=.*\d)[\w-]{1,64})$/i.test(v);
|
|
24
|
+
const SLUG = (v) => /^[a-z0-9][a-z0-9-]{0,63}$/.test(v);
|
|
25
|
+
const NUMBER = (v) => /^\d+(?:\.\d+)?[a-z]{0,3}$/i.test(v);
|
|
26
|
+
const REGION = (v) => /^[a-z]{2,3}(?:-[a-z0-9]+)+$/.test(v);
|
|
27
|
+
const SERVICE = oneOf(SERVICE_TYPES);
|
|
28
|
+
// `login`/`env use` accept the name case-insensitively; so does this.
|
|
29
|
+
const ENV = (v) => isEnvName(v.trim().toLowerCase());
|
|
30
|
+
const ON_OFF = oneOf(['on', 'off']);
|
|
31
|
+
const TARGET = oneOf(['db', 'compute', 'redis', 'mysql', 'mongodb']);
|
|
32
|
+
const POLICY_ACTION = (v) => /^(?:secrets|deploy|project|branch|service|storage)(?:\.[a-zA-Z]+)?$/.test(v);
|
|
33
|
+
const SAFE_ARGS = {
|
|
34
|
+
'env use': { 0: ENV }, 'project link': { 0: ID },
|
|
35
|
+
'services add': { 0: SERVICE }, 'services remove': { 0: SERVICE }, 'services rename': { 0: SERVICE }, 'services secrets': { 0: SERVICE },
|
|
36
|
+
'services set-access': { 0: SERVICE, 2: oneOf(['public', 'private']) },
|
|
37
|
+
'services scale': { 0: SERVICE, 2: NUMBER, 3: REGION }, 'services upgrade': { 0: SERVICE, 2: SLUG },
|
|
38
|
+
'compute always-on': { 0: ON_OFF }, 'db always-on': { 0: ON_OFF }, metrics: { 0: TARGET }, logs: { 0: TARGET },
|
|
39
|
+
'template info': { 0: SLUG }, 'billing upgrade': { 0: oneOf(['pro', 'team']) },
|
|
40
|
+
'approvals approve': { 0: ID }, 'approvals deny': { 0: ID },
|
|
41
|
+
'policy set': { 0: POLICY_ACTION, 1: oneOf(['allow', 'deny', 'approve']) }, autoupdate: { 0: ON_OFF },
|
|
42
|
+
};
|
|
43
|
+
const SAFE_OPTIONS = {
|
|
44
|
+
org: ID, project: ID, region: REGION, env: ENV, oauth: oneOf(['github', 'google']),
|
|
45
|
+
agent: oneOf(['claude-code', 'cursor', 'codex', 'opencode', 'copilot', 'factory-droid']),
|
|
46
|
+
type: oneOf(TYPES), component: oneOf(COMPONENTS), severity: oneOf(SEVERITIES),
|
|
47
|
+
status: oneOf(['pending', 'granted', 'denied', 'consumed']),
|
|
48
|
+
limit: NUMBER, step: NUMBER, since: NUMBER, port: NUMBER, memory: NUMBER, cpu: NUMBER, size: NUMBER, volume: NUMBER,
|
|
49
|
+
};
|
|
50
|
+
export function telemetryDisabled(env = process.env) {
|
|
51
|
+
return !!(env.DO_NOT_TRACK || env.INSTA_NO_TELEMETRY);
|
|
52
|
+
}
|
|
53
|
+
/** The PostHog project of the environment `apiUrl` belongs to; a custom host (insta-oss, a preview
|
|
54
|
+
* deployment) captures nothing. */
|
|
55
|
+
export function telemetryKey(apiUrl) {
|
|
56
|
+
const name = envForApiUrl(apiUrl);
|
|
57
|
+
return name ? PROJECT_KEYS[name] : undefined;
|
|
58
|
+
}
|
|
59
|
+
export function redactOptions(opts) {
|
|
60
|
+
const out = {};
|
|
61
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
62
|
+
if (typeof v === 'boolean' || typeof v === 'number')
|
|
63
|
+
out[k] = v;
|
|
64
|
+
else if (typeof v === 'string' && SAFE_OPTIONS[k]?.(v))
|
|
65
|
+
out[k] = v;
|
|
66
|
+
else
|
|
67
|
+
out[k] = REDACTED;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
export function redactArgs(command, args) {
|
|
72
|
+
const checks = SAFE_ARGS[command] ?? {};
|
|
73
|
+
return args.map((a, i) => (a === undefined ? null : typeof a === 'string' && checks[i]?.(a) ? a : REDACTED));
|
|
74
|
+
}
|
|
75
|
+
/** The deployment a `login --env|--api-url` targets. It is persisted only when the login succeeds, so
|
|
76
|
+
* a failed attempt must be routed from the options, never from the previous configuration. */
|
|
77
|
+
export function loginTarget(command, opts) {
|
|
78
|
+
if (command !== 'login')
|
|
79
|
+
return undefined;
|
|
80
|
+
if (typeof opts.apiUrl === 'string')
|
|
81
|
+
return opts.apiUrl;
|
|
82
|
+
const env = typeof opts.env === 'string' ? opts.env.trim().toLowerCase() : '';
|
|
83
|
+
return isEnvName(env) ? ENVS[env].api : undefined;
|
|
84
|
+
}
|
|
85
|
+
/** `secrets set`, `services add`, … — the subcommand chain without the program name. */
|
|
86
|
+
export function commandPath(cmd) {
|
|
87
|
+
const names = [];
|
|
88
|
+
for (let c = cmd; c?.parent; c = c.parent)
|
|
89
|
+
names.unshift(c.name());
|
|
90
|
+
return names.join(' ');
|
|
91
|
+
}
|
|
92
|
+
export function detectAgent(env = process.env) {
|
|
93
|
+
if (env.CLAUDECODE)
|
|
94
|
+
return 'claude-code';
|
|
95
|
+
if (env.CURSOR_TRACE_ID)
|
|
96
|
+
return 'cursor';
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
function errorProps(error) {
|
|
100
|
+
if (error === undefined || error instanceof CliCancel)
|
|
101
|
+
return {};
|
|
102
|
+
if (error instanceof ApiError)
|
|
103
|
+
return { error_type: 'api', http_status: error.status };
|
|
104
|
+
if (error instanceof CliExit)
|
|
105
|
+
return { error_type: 'cli' };
|
|
106
|
+
const e = error;
|
|
107
|
+
return { error_type: e?.name ?? 'unknown', ...(e?.cause?.code ? { error_code: e.cause.code } : {}) };
|
|
108
|
+
}
|
|
109
|
+
function hostOf(url) {
|
|
110
|
+
try {
|
|
111
|
+
return new URL(url).host;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function buildCommandEvent(command, args, options, outcome, ctx) {
|
|
118
|
+
const token = ctx.config.accessToken;
|
|
119
|
+
const cancelled = outcome.error instanceof CliCancel;
|
|
120
|
+
const ranChild = outcome.childExitCode !== undefined && outcome.error === undefined;
|
|
121
|
+
return {
|
|
122
|
+
event: 'cli_command',
|
|
123
|
+
distinct_id: ctx.config.user?.id ?? ctx.anonymousId,
|
|
124
|
+
timestamp: new Date().toISOString(),
|
|
125
|
+
properties: {
|
|
126
|
+
command,
|
|
127
|
+
args: redactArgs(command, args),
|
|
128
|
+
options: redactOptions(options),
|
|
129
|
+
success: !cancelled && (outcome.exitCode === 0 || ranChild),
|
|
130
|
+
cancelled,
|
|
131
|
+
exit_code: outcome.exitCode,
|
|
132
|
+
child_exit_code: outcome.childExitCode ?? null,
|
|
133
|
+
duration_ms: outcome.durationMs,
|
|
134
|
+
...errorProps(outcome.error),
|
|
135
|
+
cli_version: ctx.cliVersion,
|
|
136
|
+
channel: ctx.channel,
|
|
137
|
+
node_version: process.version,
|
|
138
|
+
os: os.platform(),
|
|
139
|
+
os_release: os.release(),
|
|
140
|
+
arch: os.arch(),
|
|
141
|
+
env: envForApiUrl(ctx.config.apiUrl) ?? 'custom',
|
|
142
|
+
api_host: hostOf(ctx.config.apiUrl),
|
|
143
|
+
logged_in: !!token,
|
|
144
|
+
auth_kind: token ? (token.startsWith('insta_') ? 'api_key' : 'session') : null,
|
|
145
|
+
project_id: ctx.project?.projectId ?? null,
|
|
146
|
+
org_id: ctx.project?.orgId ?? null,
|
|
147
|
+
tty: ctx.tty,
|
|
148
|
+
ci: !!ctx.env.CI,
|
|
149
|
+
agent: detectAgent(ctx.env),
|
|
150
|
+
term_program: ctx.env.TERM_PROGRAM ?? null,
|
|
151
|
+
$lib: 'insta-cli',
|
|
152
|
+
$lib_version: ctx.cliVersion,
|
|
153
|
+
...(ctx.config.user?.id ? {} : { $process_person_profile: false }),
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
export async function anonymousId(file = join(os.homedir(), '.insta', 'telemetry.json')) {
|
|
158
|
+
try {
|
|
159
|
+
const parsed = JSON.parse(await readFile(file, 'utf8'));
|
|
160
|
+
if (typeof parsed.anonymousId === 'string' && parsed.anonymousId)
|
|
161
|
+
return parsed.anonymousId;
|
|
162
|
+
}
|
|
163
|
+
catch { }
|
|
164
|
+
const id = randomUUID();
|
|
165
|
+
try {
|
|
166
|
+
await mkdir(dirname(file), { recursive: true });
|
|
167
|
+
await writeFile(file, JSON.stringify({ anonymousId: id }, null, 2));
|
|
168
|
+
}
|
|
169
|
+
catch { }
|
|
170
|
+
return id;
|
|
171
|
+
}
|
|
172
|
+
export async function sendBatch(key, batch, fetchImpl = fetch) {
|
|
173
|
+
try {
|
|
174
|
+
const res = await fetchImpl(`${POSTHOG_HOST}/batch/`, {
|
|
175
|
+
method: 'POST',
|
|
176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
177
|
+
body: JSON.stringify({ api_key: key, batch }),
|
|
178
|
+
signal: AbortSignal.timeout(SEND_TIMEOUT_MS),
|
|
179
|
+
});
|
|
180
|
+
return res.ok;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/** Report one finished command. Never throws and never changes the exit code: analytics must not
|
|
187
|
+
* fail the user's actual task. */
|
|
188
|
+
export async function trackCommand(cmd, args, outcome, cliVersion, deps = {}) {
|
|
189
|
+
try {
|
|
190
|
+
const env = deps.env ?? process.env;
|
|
191
|
+
if (telemetryDisabled(env))
|
|
192
|
+
return;
|
|
193
|
+
const command = commandPath(cmd);
|
|
194
|
+
if (command.startsWith('__'))
|
|
195
|
+
return;
|
|
196
|
+
let config = await (deps.loadConfig ?? readGlobal)();
|
|
197
|
+
const target = loginTarget(command, cmd.opts());
|
|
198
|
+
if (target && normalizeUrl(target) !== normalizeUrl(config.apiUrl))
|
|
199
|
+
config = { apiUrl: target };
|
|
200
|
+
const key = telemetryKey(config.apiUrl);
|
|
201
|
+
if (!key)
|
|
202
|
+
return;
|
|
203
|
+
const project = await (deps.loadProject ?? readProject)();
|
|
204
|
+
const event = buildCommandEvent(command, args.flat(), cmd.opts(), outcome, {
|
|
205
|
+
cliVersion,
|
|
206
|
+
channel: deps.channel ?? detectChannel(),
|
|
207
|
+
config,
|
|
208
|
+
project,
|
|
209
|
+
anonymousId: await anonymousId(deps.idFile),
|
|
210
|
+
env,
|
|
211
|
+
tty: deps.tty ?? !!process.stdout.isTTY,
|
|
212
|
+
});
|
|
213
|
+
await sendBatch(key, [event], deps.fetchImpl);
|
|
214
|
+
}
|
|
215
|
+
catch { }
|
|
216
|
+
}
|
|
217
|
+
//# sourceMappingURL=telemetry.js.map
|
package/dist/util.js
CHANGED
|
@@ -56,6 +56,20 @@ export class CliExit extends Error {
|
|
|
56
56
|
this.name = 'CliExit';
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
let relayedCode;
|
|
60
|
+
/** A child's exit status the CLI passes through as its own (run, db connect, compute exec). */
|
|
61
|
+
export function relayExitCode(code) {
|
|
62
|
+
relayedCode = code;
|
|
63
|
+
process.exitCode = code;
|
|
64
|
+
}
|
|
65
|
+
export function relayedExitCode() { return relayedCode; }
|
|
66
|
+
/** The user cancelled an interactive prompt: exit 0 with nothing printed. */
|
|
67
|
+
export class CliCancel extends Error {
|
|
68
|
+
constructor() {
|
|
69
|
+
super('cancelled');
|
|
70
|
+
this.name = 'CliCancel';
|
|
71
|
+
}
|
|
72
|
+
}
|
|
59
73
|
export function fail(msg) {
|
|
60
74
|
process.stderr.write(`error: ${msg}\n`);
|
|
61
75
|
process.exitCode = 1;
|