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 +1 -1
- package/dist/commands/feedback.js +50 -10
- package/dist/index.js +1 -1
- package/package.json +1 -1
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;
|
|
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
|
|
9
|
-
//
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
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)')
|