insta 0.0.56 → 0.0.58

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,7 +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
+ | `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. Before you sign in, events carry a random id stored in `~/.insta/telemetry.json`; the first command after you sign in sends one extra event merging that id into your account, and the first command after the session ends (logout, `env use`) retires it |
237
237
 
238
238
  ## Agent skills
239
239
 
@@ -1,4 +1,5 @@
1
1
  import { ApiClient, requireProject } from '../api.js';
2
+ import { pgBadge } from './services.js';
2
3
  import { info, printJson } from '../util.js';
3
4
  /**
4
5
  * The label that names WHERE a resource runs.
@@ -21,7 +22,9 @@ export function resourceLabel(r) {
21
22
  // One `manifest` resource line. Pure, so the label is unit-tested without a network mock.
22
23
  export function resourceLine(r) {
23
24
  const where = r.ref?.url ?? r.ref?.bucket ?? r.ref?.neonProjectId ?? '';
24
- return ` - ${resourceLabel(r)} ${where} [${r.status}]`;
25
+ // Database rows only: the platform stamps ref.pgVersion on insta-db (and legacy neon) resources.
26
+ const pg = r.kind === 'insta-db' || r.kind === 'neon' ? pgBadge(r.ref?.pgVersion) : '';
27
+ return ` - ${resourceLabel(r)} ${where}${pg} [${r.status}]`;
25
28
  }
26
29
  // Agent-legible view of each environment's databases / storage / compute.
27
30
  export async function manifest(opts) {
@@ -118,10 +118,7 @@ export async function servicesAdd(type, name, opts = {}) {
118
118
  if (opts.json)
119
119
  return printJson(res.body.service);
120
120
  const svc = res.body.service;
121
- const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : '';
122
- const img = svc.image ? ` running ${svc.image}${svc.port ? `:${svc.port}` : ''}` : '';
123
- const vol = svc.volume_gib ? ` vol ${svc.volume_gib}Gi at /data` : '';
124
- info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${vol}${svc.domain ? ` — ${svc.domain}` : ''}`);
121
+ info(serviceAddedLine(type, name, branch, svc));
125
122
  // Discoverability: the DB is directly dialable, but its DSN is deliberately absent from the
126
123
  // general `insta secrets` bundle — without this line nothing in the product says how to reach it.
127
124
  if (type === 'postgres') {
@@ -132,13 +129,32 @@ export async function servicesAdd(type, name, opts = {}) {
132
129
  }
133
130
  renderNextActions(res.body.nextActions);
134
131
  }
132
+ // The `services add` success line. Pure, so the badge's placement is unit-tested: a template string
133
+ // nothing asserts on silently loses a segment.
134
+ export function serviceAddedLine(type, name, branch, svc) {
135
+ const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : '';
136
+ const img = svc.image ? ` running ${svc.image}${svc.port ? `:${svc.port}` : ''}` : '';
137
+ const vol = svc.volume_gib ? ` vol ${svc.volume_gib}Gi at /data` : '';
138
+ // The major belongs next to the connect hint: it decides which psql/pg_dump to reach for.
139
+ const pg = svc.type === 'postgres' ? pgBadge(svc.pg_version) : '';
140
+ return `added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${vol}${pg}${svc.domain ? ` — ${svc.domain}` : ''}`;
141
+ }
142
+ // ` pg <major>` for a postgres row, or '' when the platform sent nothing usable. The field arrives
143
+ // as untyped API JSON: only a positive integer renders, so a malformed or nonsense value (true,
144
+ // '16', 16.4, 0, -1) can never print as a version an agent would pick tooling by.
145
+ export function pgBadge(v) {
146
+ return typeof v === 'number' && Number.isInteger(v) && v > 0 ? ` pg ${v}` : '';
147
+ }
135
148
  // Render one `services list` row. Pure, so it's unit-tested without a network mock (mirrors
136
149
  // billingLines in billing.ts). Compute rows show the running image when the platform reports one.
137
150
  export function serviceListLine(s) {
138
151
  const extra = s.type === 'compute'
139
152
  ? ` x${s.machine_count}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}${s.image ? ` running ${s.image}${s.port ? `:${s.port}` : ''}` : ''}`
140
153
  : ['redis', 'mysql', 'mongodb'].includes(s.type) ? ` tcp/${s.port ?? defaultDatabasePort(s.type)}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}`
141
- : s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : '';
154
+ : s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}`
155
+ // Postgres major, so the reader picks matching pg_dump/psql BEFORE connecting (a newer client
156
+ // dumps statements an older server cannot restore). Older platforms send no pg_version.
157
+ : s.type === 'postgres' ? pgBadge(s.pg_version) : '';
142
158
  return `${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`;
143
159
  }
144
160
  export async function servicesList(opts) {
package/dist/telemetry.js CHANGED
@@ -18,6 +18,7 @@ const PROJECT_KEYS = {
18
18
  };
19
19
  // The send is awaited before the process exits, so it gets one bounded attempt and no retry.
20
20
  const SEND_TIMEOUT_MS = 1500;
21
+ const ID_FILE = join(os.homedir(), '.insta', 'telemetry.json');
21
22
  const REDACTED = '[REDACTED]';
22
23
  const oneOf = (values) => (v) => values.includes(v);
23
24
  const ID = (v) => /^(?:[0-9a-f]{8}-[0-9a-f-]{27}|[a-z]+_[\w-]{1,64}|(?=.*\d)[\w-]{1,64})$/i.test(v);
@@ -144,6 +145,7 @@ export function buildCommandEvent(command, args, options, outcome, ctx) {
144
145
  auth_kind: token ? (token.startsWith('insta_') ? 'api_key' : 'session') : null,
145
146
  project_id: ctx.project?.projectId ?? null,
146
147
  org_id: ctx.project?.orgId ?? null,
148
+ ...(ctx.project ? { $groups: { project: ctx.project.projectId, ...(ID(ctx.project.orgId) ? { org: ctx.project.orgId } : {}) } } : {}),
147
149
  tty: ctx.tty,
148
150
  ci: !!ctx.env.CI,
149
151
  agent: detectAgent(ctx.env),
@@ -154,20 +156,22 @@ export function buildCommandEvent(command, args, options, outcome, ctx) {
154
156
  },
155
157
  };
156
158
  }
157
- export async function anonymousId(file = join(os.homedir(), '.insta', 'telemetry.json')) {
159
+ export async function readState(file = ID_FILE) {
158
160
  try {
159
161
  const parsed = JSON.parse(await readFile(file, 'utf8'));
160
162
  if (typeof parsed.anonymousId === 'string' && parsed.anonymousId)
161
- return parsed.anonymousId;
163
+ return parsed;
162
164
  }
163
165
  catch { }
164
- const id = randomUUID();
166
+ return writeState({ anonymousId: randomUUID() }, file);
167
+ }
168
+ async function writeState(state, file = ID_FILE) {
165
169
  try {
166
170
  await mkdir(dirname(file), { recursive: true });
167
- await writeFile(file, JSON.stringify({ anonymousId: id }, null, 2));
171
+ await writeFile(file, JSON.stringify(state, null, 2));
168
172
  }
169
173
  catch { }
170
- return id;
174
+ return state;
171
175
  }
172
176
  export async function sendBatch(key, batch, fetchImpl = fetch) {
173
177
  try {
@@ -201,16 +205,27 @@ export async function trackCommand(cmd, args, outcome, cliVersion, deps = {}) {
201
205
  if (!key)
202
206
  return;
203
207
  const project = await (deps.loadProject ?? readProject)();
208
+ const userId = config.user?.id;
209
+ let state = await readState(deps.idFile);
210
+ // Session gone or switched: the id belongs to the account that left, so later commands get a fresh one.
211
+ if (state.identifiedAs && state.identifiedAs !== userId)
212
+ state = await writeState({ anonymousId: randomUUID() }, deps.idFile);
204
213
  const event = buildCommandEvent(command, args.flat(), cmd.opts(), outcome, {
205
214
  cliVersion,
206
215
  channel: deps.channel ?? detectChannel(),
207
216
  config,
208
217
  project,
209
- anonymousId: await anonymousId(deps.idFile),
218
+ anonymousId: state.anonymousId,
210
219
  env,
211
220
  tty: deps.tty ?? !!process.stdout.isTTY,
212
221
  });
213
- await sendBatch(key, [event], deps.fetchImpl);
222
+ const batch = [event];
223
+ const merge = userId !== undefined && state.identifiedAs !== userId;
224
+ if (merge)
225
+ batch.push({ event: '$identify', distinct_id: userId, timestamp: event.timestamp, properties: { $anon_distinct_id: state.anonymousId } });
226
+ const sent = await sendBatch(key, batch, deps.fetchImpl);
227
+ if (merge && sent)
228
+ await writeState({ ...state, identifiedAs: userId }, deps.idFile);
214
229
  }
215
230
  catch { }
216
231
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.56",
3
+ "version": "0.0.58",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [