insta 0.0.52 → 0.0.54

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
@@ -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
 
@@ -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
- process.exitCode = 1;
546
+ relayExitCode(1);
547
547
  }
548
548
  else {
549
- process.exitCode = exitCode;
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
@@ -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
- process.exit(await connectWithPsql(r.url));
333
+ relayExitCode(await connectWithPsql(r.url));
334
334
  }
335
335
  //# sourceMappingURL=db.js.map
@@ -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
- process.exit();
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
- process.exit(0);
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
- process.exit(0);
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
- process.exit(0);
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
- process.exit(0);
103
+ throw new CliCancel();
104
104
  opts.detail = answer.trim();
105
105
  }
106
106
  }
@@ -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
- process.exit();
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
- process.exit(code);
39
+ relayExitCode(code);
41
40
  }
42
41
  //# sourceMappingURL=run.js.map
@@ -7,7 +7,7 @@ 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.
@@ -26,12 +26,15 @@ export function templateListLines(templates) {
26
26
  }
27
27
  // The info endpoint may list services as an array or keep the manifest's map shape — render both.
28
28
  export function normalizeInfoServices(raw) {
29
- if (Array.isArray(raw)) {
30
- return raw.map((s) => ({ name: s.name ?? '?', type: s.type, port: s.port, volumeGib: s.volumeGib ?? s.volume?.size }));
31
- }
32
- if (raw && typeof raw === 'object') {
33
- return Object.entries(raw).map(([name, s]) => ({ name, type: s?.type, port: s?.port, volumeGib: s?.volumeGib ?? s?.volume?.size }));
34
- }
29
+ const one = (name, s) => ({
30
+ name, type: s?.type, port: s?.port,
31
+ volumeGib: s?.volumeGib ?? s?.volume?.size,
32
+ volume: s?.volume === true || s?.volumeGib != null || s?.volume?.size != null,
33
+ });
34
+ if (Array.isArray(raw))
35
+ return raw.map((s) => one(s?.name ?? '?', s));
36
+ if (raw && typeof raw === 'object')
37
+ return Object.entries(raw).map(([name, s]) => one(name, s));
35
38
  return [];
36
39
  }
37
40
  // Variables may arrive as one array with a `required` flag, or pre-grouped {required, optional}
@@ -64,7 +67,10 @@ export function templateInfoLines(t, bold = (s) => s) {
64
67
  const services = normalizeInfoServices(t.services);
65
68
  if (services.length) {
66
69
  const summary = services.map((s) => {
67
- const bits = [s.type && s.type !== 'compute' ? s.type : undefined, s.port ? `port ${s.port}` : undefined, s.volumeGib ? `${s.volumeGib}Gi volume` : undefined].filter(Boolean);
70
+ // A size only when the registry still carries one: a manifest names no size any more, so
71
+ // "persistent /data" is all there is to say until the service exists.
72
+ const disk = s.volumeGib ? `${s.volumeGib}Gi volume` : s.volume ? 'persistent /data' : undefined;
73
+ const bits = [s.type && s.type !== 'compute' ? s.type : undefined, s.port ? `port ${s.port}` : undefined, disk].filter(Boolean);
68
74
  return `${s.name}${bits.length ? ` (${bits.join(', ')})` : ''}`;
69
75
  });
70
76
  lines.push(`services (${services.length}): ${summary.join(', ')}`);
@@ -290,7 +296,7 @@ async function promptVariable(v) {
290
296
  validate: (s) => (s.trim() ? undefined : 'required'),
291
297
  });
292
298
  if (clack.isCancel(answer))
293
- process.exit(0);
299
+ throw new CliCancel();
294
300
  return answer.trim();
295
301
  }
296
302
  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
- const guard = (fn) => (...a) => fn(...a).then(() => undefined).catch(onError);
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
@@ -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
- process.exit(0);
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
- process.exit(0);
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
- process.exit(0);
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
- process.exit(0);
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
@@ -103,8 +103,16 @@ export function validateManifest(m) {
103
103
  problems.push(`${where}: web services must declare a healthcheck path`);
104
104
  if (svc.healthcheck && !String(svc.healthcheck).startsWith('/'))
105
105
  problems.push(`${where}: healthcheck must be an absolute path (start with /)`);
106
- if (svc.volume !== undefined && (!Number.isInteger(svc.volume?.size) || svc.volume.size < 1)) {
107
- problems.push(`${where}: volume.size must be a whole Gi ≥ 1, got: ${svc.volume?.size}`);
106
+ // Sizing is the platform's, capped for the org's plan (insta-platform#357). Same answers the
107
+ // publish endpoint gives, said here so an author does not upload to find out. Read as unknown:
108
+ // the type above admits only what is SUPPORTED, and the document is a cast over YAML.parse, so
109
+ // a refused shape arrives as a value that type does not describe.
110
+ const authored = svc;
111
+ if (authored.volume !== undefined && authored.volume !== true) {
112
+ problems.push(`${where}: the volume size is the platform's to choose — declare 'volume: true'`);
113
+ }
114
+ if (authored.spec !== undefined) {
115
+ problems.push(`${where}: compute size is the platform's to choose — remove spec`);
108
116
  }
109
117
  const env = svc.env ?? {};
110
118
  for (const group of ['fixed', 'generated', 'required', 'optional']) {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.52",
3
+ "version": "0.0.54",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [