insta 0.1.6 → 0.1.8
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/compute.js +30 -0
- package/dist/commands/feedback.js +63 -7
- package/dist/index.js +9 -2
- package/dist/telemetry.js +5 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -231,7 +231,7 @@ build never reaches a production installer.
|
|
|
231
231
|
| `insta billing` | Current cycle overview; `subscribe <tier>` · `portal` · `usage` |
|
|
232
232
|
| `insta agent` | `setup` (this machine's coding agents) · `manifest` · `policy …` · `approvals …` · `observe …` · `events` |
|
|
233
233
|
| `insta config` | `install-mcp` · `regions` · `autoupdate` |
|
|
234
|
-
| `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
|
+
| `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, and prints the ticket it opened; `status <ticket-id>` — New / In Progress / Resolved / Closed (the replies are in the console) |
|
|
235
235
|
| `insta upgrade` | Update the CLI |
|
|
236
236
|
|
|
237
237
|
Every command accepts `--api-url <url>` for this invocation only (internal debugging); for `compute exec`, place it before `compute`. `insta --help` documents it.
|
package/dist/commands/compute.js
CHANGED
|
@@ -590,9 +590,25 @@ export async function serviceAlwaysOn(type, mode, serviceName, opts) {
|
|
|
590
590
|
info(`${type} ${res.body.service?.name ?? svc.name}: always-on ${on ? 'ENABLED — machines stay warm (no cold starts; idle RAM bills at actual usage)' : 'disabled — scales to zero when idle'}`);
|
|
591
591
|
}
|
|
592
592
|
export const computeAlwaysOn = (mode, serviceName, opts) => serviceAlwaysOn('compute', mode, serviceName, opts);
|
|
593
|
+
const INSTANCE_ID = /^inst-[a-f0-9]{12}$/;
|
|
594
|
+
// A lone positional is the service even when it is all digits: service names may be.
|
|
595
|
+
export function removeTarget(count, service, opts) {
|
|
596
|
+
if (!INSTANCE_ID.test(opts.remove))
|
|
597
|
+
throw new Error(`invalid instance id: ${opts.remove} (expected inst-<12 hex digits>, the \`instance\` field of \`insta compute logs --json\`)`);
|
|
598
|
+
if (opts.region !== undefined)
|
|
599
|
+
throw new Error('--region does not apply to --remove');
|
|
600
|
+
if (service !== undefined) {
|
|
601
|
+
throw new Error(count !== undefined && count.trim() !== '' && Number.isFinite(Number(count)) ? '--remove lowers the replica count by one; pass no count' : `unexpected argument: ${service} (usage: insta compute scale [service] --remove <instance>)`);
|
|
602
|
+
}
|
|
603
|
+
return count;
|
|
604
|
+
}
|
|
593
605
|
// `insta compute scale <count> [service] [--region <r>]` — POST /services/:id/scale. Count is
|
|
594
606
|
// validated locally (1..10); the paid-plan gate is the backend's and its 403 flows verbatim.
|
|
595
607
|
export async function computeScale(count, serviceName, opts) {
|
|
608
|
+
if (opts.remove !== undefined)
|
|
609
|
+
return computeRemoveInstance(removeTarget(count, serviceName, { remove: opts.remove, region: opts.region }), opts.remove, opts);
|
|
610
|
+
if (count === undefined)
|
|
611
|
+
throw new Error('a replica count is required (or --remove <instance> to drop one named instance)');
|
|
596
612
|
const machineCount = parseCount(count);
|
|
597
613
|
const api = await ApiClient.load();
|
|
598
614
|
const p = await requireProject();
|
|
@@ -606,6 +622,20 @@ export async function computeScale(count, serviceName, opts) {
|
|
|
606
622
|
return printJson(res.body.service);
|
|
607
623
|
info(`scaled compute ${svc.name} to ${machineCount} replica(s)${opts.region ? ` in ${opts.region}` : ''}`);
|
|
608
624
|
}
|
|
625
|
+
// `insta compute scale [service] --remove <instance>` — DELETE /services/:id/instances/:instance.
|
|
626
|
+
async function computeRemoveInstance(serviceName, instance, opts) {
|
|
627
|
+
const api = await ApiClient.load();
|
|
628
|
+
const p = await requireProject();
|
|
629
|
+
const branch = opts.branch ?? p.branch;
|
|
630
|
+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
631
|
+
const svc = resolveSoleService(services, 'compute', serviceName);
|
|
632
|
+
const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${svc.id}/instances/${instance}`);
|
|
633
|
+
if (handleApproval(res, opts.json))
|
|
634
|
+
return;
|
|
635
|
+
if (opts.json)
|
|
636
|
+
return printJson(res.body.service);
|
|
637
|
+
info(`removed ${instance} from compute ${svc.name}; ${res.body.service?.machine_count ?? '?'} replica(s) remain`);
|
|
638
|
+
}
|
|
609
639
|
// ---- limits (the resource ceiling; paid plans) ----
|
|
610
640
|
// Parse a human memory value into MB: "512", "512mb", "1gb", "2g", "1.5gb".
|
|
611
641
|
// Exported for unit tests — this is the only place a user-typed size becomes a number.
|
|
@@ -13,7 +13,7 @@ import * as clack from '@clack/prompts';
|
|
|
13
13
|
import { ApiClient, ApiError } from '../api.js';
|
|
14
14
|
import { readGlobal, readProject } from '../config.js';
|
|
15
15
|
import { envForApiUrl } from '../env.js';
|
|
16
|
-
import { info, printJson, refuse, CliCancel } from '../util.js';
|
|
16
|
+
import { info, printJson, refuse, CliCancel, CliExit } from '../util.js';
|
|
17
17
|
import { clean } from '../redact.js';
|
|
18
18
|
import { cliVersion } from '../version.js';
|
|
19
19
|
export const TYPES = ['bug', 'feature-request', 'friction', 'other'];
|
|
@@ -186,20 +186,24 @@ export async function submit(payload, fetchImpl, assertion) {
|
|
|
186
186
|
catch { /* non-JSON body — fall through to status handling */ }
|
|
187
187
|
if (!res.ok)
|
|
188
188
|
return { status: 'error', error: body?.error ?? `HTTP ${res.status}` };
|
|
189
|
-
return { status: body?.status === 'duplicate' ? 'duplicate' : 'received', id: body?.id ?? null };
|
|
189
|
+
return { status: body?.status === 'duplicate' ? 'duplicate' : 'received', id: body?.id ?? null, ticket: body?.ticket };
|
|
190
190
|
}
|
|
191
191
|
const SIGNED_OUT = 'not signed in to InstaCloud — run `insta login`, then send this again so the team can reply to you';
|
|
192
192
|
const STAGING = 'not accepted from staging — send InstaCloud feedback from production';
|
|
193
|
+
// Bearer only: agent evidence adds a session round trip the timeout cannot bound, and 401s signing in cannot fix.
|
|
194
|
+
async function userAssertion(api) {
|
|
195
|
+
return (await api.request('GET', '/me/feedback-assertion', undefined, { evidence: false, signal: AbortSignal.timeout(ASSERTION_TIMEOUT_MS) })).token;
|
|
196
|
+
}
|
|
193
197
|
// Exit 2, not the submit path's 0: the caller can act on this one.
|
|
194
198
|
function refuseFeedback(message, json) {
|
|
195
199
|
if (json)
|
|
196
200
|
printJson({ status: 'refused', submitted: false, error: message });
|
|
197
201
|
refuse([`insta feedback: ${message}`]);
|
|
198
202
|
}
|
|
199
|
-
function inputError(e, json) {
|
|
203
|
+
function inputError(e, json, fields = { submitted: false }) {
|
|
200
204
|
if (!json)
|
|
201
205
|
throw e;
|
|
202
|
-
printJson({ status: 'error',
|
|
206
|
+
printJson({ status: 'error', ...fields, error: e instanceof Error ? e.message : String(e) });
|
|
203
207
|
process.exitCode = 1;
|
|
204
208
|
}
|
|
205
209
|
export async function feedback(opts, deps = {}) {
|
|
@@ -235,8 +239,7 @@ export async function feedback(opts, deps = {}) {
|
|
|
235
239
|
let assertion;
|
|
236
240
|
if (env === 'prod') {
|
|
237
241
|
try {
|
|
238
|
-
|
|
239
|
-
assertion = (await api.request('GET', '/me/feedback-assertion', undefined, { evidence: false, signal: AbortSignal.timeout(ASSERTION_TIMEOUT_MS) })).token;
|
|
242
|
+
assertion = await userAssertion(api);
|
|
240
243
|
}
|
|
241
244
|
catch (e) {
|
|
242
245
|
if (e instanceof ApiError && e.status === 401)
|
|
@@ -262,13 +265,66 @@ export async function feedback(opts, deps = {}) {
|
|
|
262
265
|
return;
|
|
263
266
|
}
|
|
264
267
|
if (opts.json)
|
|
265
|
-
return printJson({ status: result.status, id: result.id });
|
|
268
|
+
return printJson({ status: result.status, id: result.id, ticket: result.ticket });
|
|
266
269
|
if (result.status === 'duplicate') {
|
|
267
270
|
info(`already reported this week — bumped its count instead (id: ${result.id})`);
|
|
268
271
|
}
|
|
269
272
|
else {
|
|
270
273
|
info(`feedback submitted (id: ${result.id}) — thank you!`);
|
|
271
274
|
}
|
|
275
|
+
if (result.ticket) {
|
|
276
|
+
info(`ticket ${result.ticket.id} — the team's replies are in the console: ${result.ticket.url}`);
|
|
277
|
+
info(`check its status with \`insta feedback status ${result.ticket.id}\``);
|
|
278
|
+
}
|
|
272
279
|
info('PII (emails, tokens, keys, home paths) was redacted before sending.');
|
|
273
280
|
}
|
|
281
|
+
const STATUS_LABEL = { open: 'New', in_progress: 'In Progress', resolved: 'Resolved', closed: 'Closed' };
|
|
282
|
+
const SIGNED_OUT_STATUS = 'not signed in to InstaCloud — run `insta login` to check your ticket';
|
|
283
|
+
function refuseStatus(message, json) {
|
|
284
|
+
if (json)
|
|
285
|
+
printJson({ status: 'refused', error: message });
|
|
286
|
+
refuse([`insta feedback status: ${message}`]);
|
|
287
|
+
}
|
|
288
|
+
export async function feedbackStatus(id, opts, deps = {}) {
|
|
289
|
+
try {
|
|
290
|
+
const api = deps.api ?? await ApiClient.load();
|
|
291
|
+
const env = envForApiUrl(api.apiUrl);
|
|
292
|
+
if (env !== 'prod')
|
|
293
|
+
refuseStatus('ticket status is only available on production InstaCloud', opts.json);
|
|
294
|
+
if (!api.config.accessToken)
|
|
295
|
+
refuseStatus(SIGNED_OUT_STATUS, opts.json);
|
|
296
|
+
let assertion;
|
|
297
|
+
try {
|
|
298
|
+
assertion = await userAssertion(api);
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
if (e instanceof ApiError && e.status === 401)
|
|
302
|
+
refuseStatus(SIGNED_OUT_STATUS, opts.json);
|
|
303
|
+
throw e;
|
|
304
|
+
}
|
|
305
|
+
// Relative, so an endpoint served under a path prefix keeps it.
|
|
306
|
+
const res = await (deps.fetchImpl ?? fetch)(new URL(`tickets/${encodeURIComponent(id)}`, FEEDBACK_ENDPOINT), {
|
|
307
|
+
headers: { Authorization: `Bearer ${FEEDBACK_INGEST_TOKEN}`, 'Insta-User-Assertion': assertion },
|
|
308
|
+
signal: AbortSignal.timeout(FEEDBACK_TIMEOUT_MS),
|
|
309
|
+
});
|
|
310
|
+
if (res.status === 404)
|
|
311
|
+
throw new Error(`no ticket ${id} of yours — use the ticket id \`insta feedback\` printed`);
|
|
312
|
+
// The platform vouched for this login a moment ago: a 401 here is the service's, not a signed-out user.
|
|
313
|
+
if (res.status === 401)
|
|
314
|
+
throw new Error('the feedback service could not verify who you are — signing in again will not fix it');
|
|
315
|
+
const body = (await res.json().catch(() => ({})));
|
|
316
|
+
if (!res.ok || !body.status)
|
|
317
|
+
throw new Error(`the feedback service answered ${res.status}${body.error ? `: ${body.error}` : ''}`);
|
|
318
|
+
if (opts.json)
|
|
319
|
+
return printJson({ id: body.id, status: body.status, url: body.url });
|
|
320
|
+
info(`ticket ${body.id}: ${STATUS_LABEL[body.status] ?? body.status}`);
|
|
321
|
+
info(`open it in the console: ${body.url}`);
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
// A refusal has already printed and set exit 2; handled again it would print twice and exit 1.
|
|
325
|
+
if (e instanceof CliExit)
|
|
326
|
+
throw e;
|
|
327
|
+
return inputError(e, opts.json, {});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
274
330
|
//# sourceMappingURL=feedback.js.map
|
package/dist/index.js
CHANGED
|
@@ -365,8 +365,9 @@ compute.command('restart [service]').description("Restart a compute service by r
|
|
|
365
365
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeRestart(service, o)));
|
|
366
366
|
compute.command('status [service]').description("Show a compute service's desired vs. live state")
|
|
367
367
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStatus(service, o)));
|
|
368
|
-
compute.command('scale
|
|
368
|
+
compute.command('scale [count] [service]').description('Set a compute service same-region replica count, 1 to 10 (paid plans only). --remove <instance> instead drops that one instance and runs one fewer; instance ids are the `instance` field of `insta compute logs --json`. The last instance cannot be removed')
|
|
369
369
|
.option('--region <region>', 'region to scale in (default: the service region)')
|
|
370
|
+
.option('--remove <instance>', 'drop this instance (inst-<12 hex>) and run one fewer replica')
|
|
370
371
|
.option('--json').option('--branch <branch>', 'branch (default: current)')
|
|
371
372
|
.action(guard((count, service, o) => computeCmd.computeScale(count, service, o)));
|
|
372
373
|
compute.command('limits [service]').description("Show or set a compute service's resource ceiling (any plan within the free cap; raising above it needs a paid plan). --memory is the dial; cpu derives from it unless --cpu is given. Billing is actual usage — the ceiling caps what the app may burn, it is not a price")
|
|
@@ -591,7 +592,7 @@ cfg.command('autoupdate [mode]').description('Show or set auto-update: on | off
|
|
|
591
592
|
const setupCompat = program.command('setup', { hidden: true }).description('Compatibility alias: `insta setup agent` is `insta agent setup`');
|
|
592
593
|
withSetupAgentOptions(setupCompat.command('agent').description('Alias of `insta agent setup`, kept for the console one-liner'));
|
|
593
594
|
// ---- feedback (agent + human hurdle reports → the InstaCloud team) ----
|
|
594
|
-
program.command('feedback')
|
|
595
|
+
const fb = program.command('feedback')
|
|
595
596
|
.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.')
|
|
596
597
|
.option('--type <type>', `what kind of hurdle: ${feedbackCmd.TYPES.join(' | ')}`)
|
|
597
598
|
.option('--component <component>', `which part of the toolkit: ${feedbackCmd.COMPONENTS.join(' | ')}`)
|
|
@@ -607,6 +608,12 @@ program.command('feedback')
|
|
|
607
608
|
.option('--severity <severity>', `${feedbackCmd.SEVERITIES.join(' | ')} (default: minor)`)
|
|
608
609
|
.option('--json')
|
|
609
610
|
.action(guard((o) => feedbackCmd.feedback(o)));
|
|
611
|
+
// As with billing: a mistyped subcommand must fail, not open the report wizard.
|
|
612
|
+
fb.allowExcessArguments(false);
|
|
613
|
+
fb.command('status <ticket-id>')
|
|
614
|
+
.description('Show the status of a ticket `insta feedback` opened: New, In Progress, Resolved or Closed. The replies are read and answered in the console.')
|
|
615
|
+
.option('--json')
|
|
616
|
+
.action(guard((id, o) => feedbackCmd.feedbackStatus(id, o)));
|
|
610
617
|
// ---- self-update ----
|
|
611
618
|
program.command('upgrade').description('Update the insta CLI to the latest release (binary or npm install)')
|
|
612
619
|
.action(guard(() => selfUpdate.upgrade(cliVersion())));
|
package/dist/telemetry.js
CHANGED
|
@@ -34,7 +34,7 @@ const SAFE_ARGS = {
|
|
|
34
34
|
'env use': { 0: ENV }, 'project link': { 0: ID },
|
|
35
35
|
'service add': { 0: SERVICE }, 'service remove': { 0: SERVICE }, 'service rename': { 0: SERVICE },
|
|
36
36
|
'storage set-access': { 0: oneOf(['public', 'private']) },
|
|
37
|
-
'compute scale': { 0:
|
|
37
|
+
'compute scale': { 0: (v) => /^\d+$/.test(v) },
|
|
38
38
|
'compute always-on': { 0: ON_OFF }, 'postgres always-on': { 0: ON_OFF },
|
|
39
39
|
'redis always-on': { 0: ON_OFF }, 'mysql always-on': { 0: ON_OFF }, 'mongodb always-on': { 0: ON_OFF },
|
|
40
40
|
'template info': { 0: SLUG }, 'billing subscribe': { 0: oneOf(['pro', 'team']) },
|
|
@@ -69,8 +69,9 @@ export function redactOptions(opts) {
|
|
|
69
69
|
}
|
|
70
70
|
return out;
|
|
71
71
|
}
|
|
72
|
-
export function redactArgs(command, args) {
|
|
73
|
-
|
|
72
|
+
export function redactArgs(command, args, options = {}) {
|
|
73
|
+
// with --remove the count's slot holds the service name
|
|
74
|
+
const checks = command === 'compute scale' && options.remove !== undefined ? {} : SAFE_ARGS[command] ?? {};
|
|
74
75
|
return args.map((a, i) => (a === undefined ? null : typeof a === 'string' && checks[i]?.(a) ? a : REDACTED));
|
|
75
76
|
}
|
|
76
77
|
/** The deployment a `login --env|--api-url` targets. It is persisted only when the login succeeds, so
|
|
@@ -125,7 +126,7 @@ export function buildCommandEvent(command, args, options, outcome, ctx) {
|
|
|
125
126
|
timestamp: new Date().toISOString(),
|
|
126
127
|
properties: {
|
|
127
128
|
command,
|
|
128
|
-
args: redactArgs(command, args),
|
|
129
|
+
args: redactArgs(command, args, options),
|
|
129
130
|
options: redactOptions(options),
|
|
130
131
|
success: !cancelled && (outcome.exitCode === 0 || ranChild),
|
|
131
132
|
cancelled,
|