insta 0.1.4 → 0.1.5

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
@@ -230,7 +230,7 @@ build never reaches a production installer.
230
230
  | `insta billing` | Current cycle overview; `subscribe <tier>` · `portal` · `usage` |
231
231
  | `insta agent` | `setup` (this machine's coding agents) · `manifest` · `policy …` · `approvals …` · `observe …` · `events` |
232
232
  | `insta config` | `install-mcp` · `regions` · `autoupdate` |
233
- | `insta feedback` | Report an InstaCloud-side hurdle (bug / feature-request / friction) to the team — never for the app you are building; works logged-out |
233
+ | `insta feedback` | Report an InstaCloud-side hurdle (bug / feature-request / friction) to the team — never for the app you are building; on InstaCloud it needs `insta login`, so the team can reply |
234
234
  | `insta upgrade` | Update the CLI |
235
235
 
236
236
  Every command accepts `--api-url <url>` for this invocation only (internal debugging); for `compute exec`, place it before `compute`. `insta --help` documents it.
@@ -5,14 +5,15 @@
5
5
  //
6
6
  // The backend is InstaCloud dogfooding itself: the "InstaCloud Agent Feedback" project runs the
7
7
  // ingest service (InsForge/instacloud-feedback repo) on a postgres + compute pair. It is NOT the
8
- // control-plane API on purpose — feedback must work logged-out, unlinked, and from insta-oss,
9
- // and a control-plane outage is exactly when we most want reports to still arrive.
8
+ // control-plane API on purpose — feedback must work unlinked, from insta-oss, and through a
9
+ // control-plane outage, which is exactly when we most want reports to still arrive.
10
10
  import { readFileSync, statSync } from 'node:fs';
11
11
  import os from 'node:os';
12
12
  import * as clack from '@clack/prompts';
13
+ import { ApiClient, ApiError } from '../api.js';
13
14
  import { readGlobal, readProject } from '../config.js';
14
15
  import { envForApiUrl } from '../env.js';
15
- import { info, printJson, CliCancel } from '../util.js';
16
+ import { info, printJson, refuse, CliCancel } from '../util.js';
16
17
  import { clean } from '../redact.js';
17
18
  import { cliVersion } from '../version.js';
18
19
  export const TYPES = ['bug', 'feature-request', 'friction', 'other'];
@@ -42,6 +43,8 @@ const FEEDBACK_INGEST_TOKEN = process.env.INSTA_FEEDBACK_TOKEN || 'insta-feedbac
42
43
  // the DB wake and persists, so a report can land after a shorter deadline gave up on it).
43
44
  // An expired deadline is reported as UNCONFIRMED, not failed — the report may well be stored.
44
45
  const FEEDBACK_TIMEOUT_MS = 15_000;
46
+ // A slow control plane may cost a report its ticket, never the report itself.
47
+ const ASSERTION_TIMEOUT_MS = 5_000;
45
48
  const MAX_FILE_BYTES = 256 * 1024;
46
49
  function requireEnum(value, allowed, flag) {
47
50
  if (!allowed.includes(value)) {
@@ -156,7 +159,7 @@ export async function buildPayload(opts, ctx) {
156
159
  /** One POST, one bounded attempt (FEEDBACK_TIMEOUT_MS), zero retries — feedback is a side quest and must never hang the CLI.
157
160
  * Transport and server failures come back as a result, not an exception: the caller downgrades
158
161
  * them to a warning so a broken feedback backend can't fail the user's actual task. */
159
- export async function submit(payload, fetchImpl) {
162
+ export async function submit(payload, fetchImpl, assertion) {
160
163
  let res;
161
164
  try {
162
165
  res = await fetchImpl(FEEDBACK_ENDPOINT, {
@@ -164,6 +167,7 @@ export async function submit(payload, fetchImpl) {
164
167
  headers: {
165
168
  'Content-Type': 'application/json',
166
169
  Authorization: `Bearer ${FEEDBACK_INGEST_TOKEN}`,
170
+ ...(assertion ? { 'Insta-User-Assertion': assertion } : {}),
167
171
  },
168
172
  body: JSON.stringify(payload),
169
173
  signal: AbortSignal.timeout(FEEDBACK_TIMEOUT_MS),
@@ -184,7 +188,34 @@ export async function submit(payload, fetchImpl) {
184
188
  return { status: 'error', error: body?.error ?? `HTTP ${res.status}` };
185
189
  return { status: body?.status === 'duplicate' ? 'duplicate' : 'received', id: body?.id ?? null };
186
190
  }
191
+ const SIGNED_OUT = 'not signed in to InstaCloud — run `insta login`, then send this again so the team can reply to you';
192
+ const STAGING = 'not accepted from staging — send InstaCloud feedback from production';
193
+ // Exit 2, not the submit path's 0: the caller can act on this one.
194
+ function refuseFeedback(message, json) {
195
+ if (json)
196
+ printJson({ status: 'refused', submitted: false, error: message });
197
+ refuse([`insta feedback: ${message}`]);
198
+ }
199
+ function inputError(e, json) {
200
+ if (!json)
201
+ throw e;
202
+ printJson({ status: 'error', submitted: false, error: e instanceof Error ? e.message : String(e) });
203
+ process.exitCode = 1;
204
+ }
187
205
  export async function feedback(opts, deps = {}) {
206
+ let api;
207
+ try {
208
+ api = deps.api ?? await ApiClient.load();
209
+ }
210
+ catch (e) {
211
+ return inputError(e, opts.json);
212
+ }
213
+ const env = envForApiUrl(api.apiUrl);
214
+ if (env === 'staging')
215
+ refuseFeedback(STAGING, opts.json);
216
+ // Before the prompts, so nobody types out a report only to be told to sign in.
217
+ if (env === 'prod' && !api.config.accessToken)
218
+ refuseFeedback(SIGNED_OUT, opts.json);
188
219
  const interactive = deps.interactive ?? (!opts.json && !!process.stdin.isTTY && !!process.stdout.isTTY);
189
220
  const missingRequired = !opts.type || !opts.component || !opts.title || (!opts.detail && !opts.file);
190
221
  if (missingRequired && interactive)
@@ -198,13 +229,22 @@ export async function feedback(opts, deps = {}) {
198
229
  payload = await buildPayload(opts, { cliVersion: deps.cliVersion ?? cliVersion() });
199
230
  }
200
231
  catch (e) {
201
- if (!opts.json)
202
- throw e;
203
- printJson({ status: 'error', submitted: false, error: e instanceof Error ? e.message : String(e) });
204
- process.exitCode = 1;
205
- return;
232
+ return inputError(e, opts.json);
233
+ }
234
+ // Fetched after the prompts: it lives five minutes, and a person can take longer than that to type.
235
+ let assertion;
236
+ if (env === 'prod') {
237
+ try {
238
+ // Bearer only: agent evidence adds a session round trip the timeout cannot bound, and 401s signing in cannot fix.
239
+ assertion = (await api.request('GET', '/me/feedback-assertion', undefined, { evidence: false, signal: AbortSignal.timeout(ASSERTION_TIMEOUT_MS) })).token;
240
+ }
241
+ catch (e) {
242
+ if (e instanceof ApiError && e.status === 401)
243
+ refuseFeedback(SIGNED_OUT, opts.json);
244
+ process.stderr.write(`warning: could not confirm who you are (${e instanceof Error ? e.message : String(e)}) — sending anyway, but nobody can reply to this report\n`);
245
+ }
206
246
  }
207
- const result = await submit(payload, deps.fetchImpl ?? fetch);
247
+ const result = await submit(payload, deps.fetchImpl ?? fetch, assertion);
208
248
  if (result.status === 'unconfirmed') {
209
249
  // NOT a failure claim: the request was still in flight at the deadline and the server
210
250
  // finishes what it started, so saying "not submitted" here would be a false negative.
package/dist/index.js CHANGED
@@ -574,7 +574,7 @@ const setupCompat = program.command('setup', { hidden: true }).description('Comp
574
574
  withSetupAgentOptions(setupCompat.command('agent').description('Alias of `insta agent setup`, kept for the console one-liner'));
575
575
  // ---- feedback (agent + human hurdle reports → the InstaCloud team) ----
576
576
  program.command('feedback')
577
- .description('Report an InstaCloud-side hurdle (bug / missing feature / friction) to the InstaCloud team — about the insta toolkit itself, NEVER about the app you are building. Works logged-out and unlinked.')
577
+ .description('Report an InstaCloud-side hurdle (bug / missing feature / friction) to the InstaCloud team — about the insta toolkit itself, NEVER about the app you are building. On InstaCloud it needs `insta login`, so the team can reply; works unlinked.')
578
578
  .option('--type <type>', `what kind of hurdle: ${feedbackCmd.TYPES.join(' | ')}`)
579
579
  .option('--component <component>', `which part of the toolkit: ${feedbackCmd.COMPONENTS.join(' | ')}`)
580
580
  .option('--title <title>', 'one-line summary (≤200 chars)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [