insta 0.0.83 → 0.1.1
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 +28 -29
- package/dist/agent.js +2 -2
- package/dist/api.js +19 -4
- package/dist/build-logs.js +1 -1
- package/dist/commands/agent-policy.js +1 -1
- package/dist/commands/auth.js +32 -18
- package/dist/commands/billing.js +5 -5
- package/dist/commands/compute.js +83 -41
- package/dist/commands/db-query.js +18 -14
- package/dist/commands/deploy.js +1 -1
- package/dist/commands/domain.js +105 -3
- package/dist/commands/env.js +1 -1
- package/dist/commands/managed-db.js +34 -0
- package/dist/commands/mcp.js +2 -2
- package/dist/commands/metrics.js +6 -5
- package/dist/commands/observe.js +2 -2
- package/dist/commands/{db.js → postgres.js} +32 -32
- package/dist/commands/regions.js +1 -1
- package/dist/commands/services.js +31 -70
- package/dist/commands/setup.js +8 -7
- package/dist/commands/storage.js +17 -2
- package/dist/commands/template.js +4 -4
- package/dist/commands/upgrade.js +20 -8
- package/dist/config.js +49 -31
- package/dist/deploy-archive.js +2 -2
- package/dist/ensure-skills.js +1 -1
- package/dist/index.js +241 -161
- package/dist/observe/hook.js +1 -1
- package/dist/observe/install.js +1 -1
- package/dist/resolve-service.js +4 -4
- package/dist/telemetry.js +8 -8
- package/dist/util.js +9 -6
- package/package.json +1 -1
package/dist/commands/compute.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
2
2
|
import { info, printJson, handleApproval, relayExitCode, writeFileAtomicSync, resolveThroughSymlink } from '../util.js';
|
|
3
|
-
import { resolveComputeServiceId, resolveSoleService, q, parseVolumeGib } from './services.js';
|
|
3
|
+
import { resolveComputeServiceId, resolveSoleService, q, parseVolumeGib, parseCount } 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 —
|
|
6
6
|
// enough to pick one without a second command. Pure, exported for tests.
|
|
@@ -25,7 +25,7 @@ export function resolveDomainTarget(services, host, group) {
|
|
|
25
25
|
return svc;
|
|
26
26
|
}
|
|
27
27
|
if (compute.length === 0)
|
|
28
|
-
throw new Error('no compute service in this project (add one with `insta
|
|
28
|
+
throw new Error('no compute service in this project (add one with `insta service add compute <name>`)');
|
|
29
29
|
if (compute.length === 1) {
|
|
30
30
|
const only = compute[0];
|
|
31
31
|
if (isWorker(only))
|
|
@@ -43,7 +43,7 @@ const pad = (s, n) => s.padEnd(n);
|
|
|
43
43
|
// An older platform may omit `dns` entirely; every reader treats that as "no records", not a crash.
|
|
44
44
|
const recordsOf = (r) => r.dns ?? [];
|
|
45
45
|
const flags = (c = {}) => `${c.group ? ` --group ${c.group}` : ''}${c.branch ? ` --branch ${c.branch}` : ''}`;
|
|
46
|
-
// After
|
|
46
|
+
// After attach: exactly what to do next, from the records the platform returned — never a
|
|
47
47
|
// hand-built template. No records = say so; a template here would send the customer publishing
|
|
48
48
|
// values the plane never issued. Pure, exported for tests.
|
|
49
49
|
export function domainGuidanceLines(r, ctx = {}) {
|
|
@@ -58,7 +58,7 @@ export function domainGuidanceLines(r, ctx = {}) {
|
|
|
58
58
|
out.push('add these DNS records at your DNS provider:');
|
|
59
59
|
for (const d of records)
|
|
60
60
|
out.push(` ${pad(d.type, 6)} ${pad(d.name, nameW)} -> ${d.value}`);
|
|
61
|
-
out.push(`then: insta
|
|
61
|
+
out.push(`then: insta domain check ${r.hostname}${flags(ctx)}`);
|
|
62
62
|
return out;
|
|
63
63
|
}
|
|
64
64
|
// Whether this provider reports an edge routing target AT ALL. The compute plane's domain view
|
|
@@ -96,10 +96,10 @@ export function domainResolveLine(r) {
|
|
|
96
96
|
}
|
|
97
97
|
return { line: ` ${pad('resolves to', 12)}${r.origin} (${region} router) ok`, ready: true };
|
|
98
98
|
}
|
|
99
|
-
// check
|
|
99
|
+
// check: every stage, what each still needs, and where it routes. Pure, exported for tests.
|
|
100
100
|
export function domainStatusLines(r, ctx = {}) {
|
|
101
101
|
if (r.status === 'not added') {
|
|
102
|
-
return [`${r.hostname} is not attached to ${targetOf(r)} — attach it with: insta
|
|
102
|
+
return [`${r.hostname} is not attached to ${targetOf(r)} — attach it with: insta domain attach ${r.hostname}${flags(ctx)}`];
|
|
103
103
|
}
|
|
104
104
|
const records = recordsOf(r);
|
|
105
105
|
const out = [`${r.hostname} -> ${targetOf(r)}`];
|
|
@@ -131,7 +131,7 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
131
131
|
blockers.push('add the ownership TXT');
|
|
132
132
|
}
|
|
133
133
|
else {
|
|
134
|
-
stage('ownership', 'unchecked', `TXT ${txt.name} -> ${txt.value} (the plane has not checked it yet — re-run check
|
|
134
|
+
stage('ownership', 'unchecked', `TXT ${txt.name} -> ${txt.value} (the plane has not checked it yet — re-run insta domain check)`);
|
|
135
135
|
blockers.push('ownership unchecked');
|
|
136
136
|
}
|
|
137
137
|
}
|
|
@@ -169,7 +169,7 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
169
169
|
blockers.push(`add the ${d.type}`);
|
|
170
170
|
}
|
|
171
171
|
else {
|
|
172
|
-
stage(lbl, 'unchecked', `${d.type} ${d.name} -> ${d.value} (not checked yet — re-run check
|
|
172
|
+
stage(lbl, 'unchecked', `${d.type} ${d.name} -> ${d.value} (not checked yet — re-run insta domain check)`);
|
|
173
173
|
blockers.push(`${d.type} unchecked`);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
@@ -192,7 +192,7 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
192
192
|
blockers.push(`add the ${d.type} ${d.name}`);
|
|
193
193
|
}
|
|
194
194
|
else {
|
|
195
|
-
stage(lbl, 'unchecked', `${where} (not checked yet — re-run check
|
|
195
|
+
stage(lbl, 'unchecked', `${where} (not checked yet — re-run insta domain check)`);
|
|
196
196
|
blockers.push(`${d.type} ${d.name} unchecked`);
|
|
197
197
|
}
|
|
198
198
|
}
|
|
@@ -237,7 +237,7 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
237
237
|
}
|
|
238
238
|
// The platform's 409: the hostname is already bound elsewhere. Domains are never MOVED — the only
|
|
239
239
|
// path is unbind there, then bind here — so the hint names the release step. Three shapes:
|
|
240
|
-
// owner named and present in this project → the exact
|
|
240
|
+
// owner named and present in this project → the exact detach command;
|
|
241
241
|
// owner named but NOT in this project's services → it is held by a deleted (or other-project)
|
|
242
242
|
// service: an operator must release it (the plane has no self-serve orphan release yet);
|
|
243
243
|
// owner not named (today's plane) → the generic release instruction.
|
|
@@ -248,11 +248,11 @@ export function domainConflictMessage(host, e, services, ctx = {}) {
|
|
|
248
248
|
const region = m?.[2];
|
|
249
249
|
// The release command must name the OWNER's group, and the branch the user is working on — a
|
|
250
250
|
// command that defaults back to the linked branch would release nothing.
|
|
251
|
-
const release = (group) => `insta
|
|
251
|
+
const release = (group) => `insta domain detach ${host}${flags({ group, branch: ctx.branch })}`;
|
|
252
252
|
if (owner) {
|
|
253
253
|
const here = services.find((s) => s.type === 'compute' && s.name === owner);
|
|
254
254
|
if (here)
|
|
255
|
-
return `${host} is already attached to ${owner}${region ? ` (${region})` : here.region ? ` (${here.region})` : ''} — domains are not moved; release it first: ${release(owner)}, then re-run
|
|
255
|
+
return `${host} is already attached to ${owner}${region ? ` (${region})` : here.region ? ` (${here.region})` : ''} — domains are not moved; release it first: ${release(owner)}, then re-run insta domain attach`;
|
|
256
256
|
return `${host} is already attached to ${owner}${region ? ` in ${region}` : ''}, which is not a service in this project — it is held by a deleted service (or one in another project); ask an operator to release the hostname before re-binding it`;
|
|
257
257
|
}
|
|
258
258
|
return `${host} is already attached to another compute service — domains are not moved; release it there first (${release('<that service>')}) or, if that service was deleted, ask an operator to release the hostname`;
|
|
@@ -421,6 +421,12 @@ function execCommandIndex(argv) {
|
|
|
421
421
|
const token = argv[cursor];
|
|
422
422
|
if (token === '--agent')
|
|
423
423
|
continue;
|
|
424
|
+
if (token === '--api-url') {
|
|
425
|
+
cursor++;
|
|
426
|
+
continue;
|
|
427
|
+
} // root flag with a value: skip both tokens
|
|
428
|
+
if (token.startsWith('--api-url='))
|
|
429
|
+
continue;
|
|
424
430
|
if (token.startsWith('-'))
|
|
425
431
|
return -1; // a global flag, or `--`: either way not our command path
|
|
426
432
|
return token === 'compute' && argv[cursor + 1] === 'exec' ? cursor : -1;
|
|
@@ -470,7 +476,7 @@ export function resolveExecFallback(services, payload, note = (msg) => process.s
|
|
|
470
476
|
if (head === undefined)
|
|
471
477
|
return { serviceName: undefined, command: [] };
|
|
472
478
|
if (!services.some((service) => service.type === 'compute' && service.name === head)) {
|
|
473
|
-
note(`note: no \`--\` separator was found and \`${head}\` is not a compute service, so it was read as the command. If \`${head}\` was the service, check the name with \`insta
|
|
479
|
+
note(`note: no \`--\` separator was found and \`${head}\` is not a compute service, so it was read as the command. If \`${head}\` was the service, check the name with \`insta service list\`.`);
|
|
474
480
|
return { serviceName: undefined, command: payload };
|
|
475
481
|
}
|
|
476
482
|
// `head` really is a service, so whatever follows it cannot be the command's first token.
|
|
@@ -567,21 +573,38 @@ export async function computeExec(serviceName, command, opts, recovery = {}) {
|
|
|
567
573
|
applyExecResult(res, opts.json);
|
|
568
574
|
}
|
|
569
575
|
// ---- always-on (opt out of scale-to-zero; all plans; billing is actual usage either way) ----
|
|
570
|
-
export async function
|
|
576
|
+
export async function serviceAlwaysOn(type, mode, serviceName, opts) {
|
|
571
577
|
if (mode !== 'on' && mode !== 'off')
|
|
572
578
|
throw new Error('mode must be on|off');
|
|
573
579
|
const api = await ApiClient.load();
|
|
574
580
|
const p = await requireProject();
|
|
575
581
|
const branch = opts.branch ?? p.branch;
|
|
576
582
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
577
|
-
const
|
|
578
|
-
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/always-on`, { enabled: mode === 'on' });
|
|
583
|
+
const svc = resolveSoleService(services, type, serviceName);
|
|
584
|
+
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${svc.id}/always-on`, { enabled: mode === 'on' });
|
|
579
585
|
if (handleApproval(res, opts.json))
|
|
580
586
|
return;
|
|
581
587
|
if (opts.json)
|
|
582
588
|
return printJson(res.body);
|
|
583
589
|
const on = res.body.service?.always_on;
|
|
584
|
-
info(
|
|
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
|
+
}
|
|
592
|
+
export const computeAlwaysOn = (mode, serviceName, opts) => serviceAlwaysOn('compute', mode, serviceName, opts);
|
|
593
|
+
// `insta compute scale <count> [service] [--region <r>]` — POST /services/:id/scale. Count is
|
|
594
|
+
// validated locally (1..10); the paid-plan gate is the backend's and its 403 flows verbatim.
|
|
595
|
+
export async function computeScale(count, serviceName, opts) {
|
|
596
|
+
const machineCount = parseCount(count);
|
|
597
|
+
const api = await ApiClient.load();
|
|
598
|
+
const p = await requireProject();
|
|
599
|
+
const branch = opts.branch ?? p.branch;
|
|
600
|
+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
601
|
+
const svc = resolveSoleService(services, 'compute', serviceName);
|
|
602
|
+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${svc.id}/scale`, { machineCount, region: opts.region });
|
|
603
|
+
if (handleApproval(res, opts.json))
|
|
604
|
+
return;
|
|
605
|
+
if (opts.json)
|
|
606
|
+
return printJson(res.body.service);
|
|
607
|
+
info(`scaled compute ${svc.name} to ${machineCount} replica(s)${opts.region ? ` in ${opts.region}` : ''}`);
|
|
585
608
|
}
|
|
586
609
|
// ---- limits (the resource ceiling; paid plans) ----
|
|
587
610
|
// Parse a human memory value into MB: "512", "512mb", "1gb", "2g", "1.5gb".
|
|
@@ -615,30 +638,47 @@ export function parseCpu(raw) {
|
|
|
615
638
|
// ---- volume (the persistent /data disk; attach any time, grow-only, deletable; never detach) ----
|
|
616
639
|
// Render the volume read. Pure, exported for tests (mirrors serviceListLine). Every plan may view;
|
|
617
640
|
// only growth is paid — that gate is the backend's to enforce, so nothing here pre-blocks.
|
|
618
|
-
export function volumeLines(name, volume, cap) {
|
|
641
|
+
export function volumeLines(name, volume, cap, type = 'compute') {
|
|
619
642
|
if (!volume)
|
|
620
643
|
return [
|
|
621
|
-
|
|
644
|
+
type === 'compute'
|
|
645
|
+
? `compute ${name}: no volume attached (attach one: \`insta compute volume ${name} --size <gi>\` — it mounts at /data on the next deploy)`
|
|
646
|
+
: `${type} ${name}: no volume attached (attach one: \`insta ${type} volume ${name} --size <gi>\` — it mounts at the image's data directory)`,
|
|
622
647
|
];
|
|
648
|
+
// `--delete` exists on `compute volume` only — a managed database's volume IS its data
|
|
649
|
+
// directory, so the group never registered the flag (index.ts) and naming it here would print
|
|
650
|
+
// a command that fails.
|
|
651
|
+
const grow = type === 'compute'
|
|
652
|
+
? 'grow with --size (grow-only), delete with --delete (destroys the data)'
|
|
653
|
+
: 'grow with --size (grow-only); the volume cannot be deleted — remove the service instead';
|
|
623
654
|
return [
|
|
624
|
-
|
|
625
|
-
|
|
655
|
+
`${type} ${name}: volume ${volume.sizeGib}Gi at ${volume.mountPath} (plan max ${cap.volumeGib}Gi)`,
|
|
656
|
+
` billing is actual data stored — the size is a cap, not a price; ${grow}`,
|
|
626
657
|
];
|
|
627
658
|
}
|
|
628
659
|
// Render the PUT result. Pure, exported for tests. `attached` comes from the backend and is what
|
|
629
660
|
// tells a FIRST attach (no disk yet — it mounts on the next deploy) apart from a grow (the live
|
|
630
661
|
// disk was already extended); the wire size is authoritative in both cases.
|
|
631
|
-
export function volumeWriteLine(name, body) {
|
|
662
|
+
export function volumeWriteLine(name, body, type = 'compute') {
|
|
632
663
|
if (body.attached) {
|
|
633
|
-
|
|
664
|
+
// Only a compute service has a deploy step for the mount to wait on; a managed database has
|
|
665
|
+
// no deploy, so its disk is simply mounted.
|
|
666
|
+
const mounts = type === 'compute'
|
|
667
|
+
? `mounts at ${body.volume.mountPath} on the next deploy`
|
|
668
|
+
: `mounts at ${body.volume.mountPath}`;
|
|
669
|
+
return `${type} ${name}: volume ${body.volume.sizeGib}Gi attached — ${mounts} (plan max ${body.cap.volumeGib}Gi)`;
|
|
634
670
|
}
|
|
635
|
-
return
|
|
671
|
+
return `${type} ${name}: volume grown to ${body.volume.sizeGib}Gi at ${body.volume.mountPath} (plan max ${body.cap.volumeGib}Gi)`;
|
|
636
672
|
}
|
|
637
673
|
// Render the DELETE result. Pure, exported for tests. Deleting is the only way off the volume
|
|
638
674
|
// path (there is no detach), so the line says what came back with it: the two constraints the
|
|
639
675
|
// volume imposed.
|
|
640
|
-
export function volumeDeleteLine(name) {
|
|
641
|
-
|
|
676
|
+
export function volumeDeleteLine(name, type = 'compute') {
|
|
677
|
+
// The two constraints a volume imposes — no suspend fast-wake, no scale-out — are compute-plane
|
|
678
|
+
// facts. `--delete` is registered on compute only, so the other branch is unreachable today;
|
|
679
|
+
// it is written type-aware anyway so the line cannot start lying if it ever becomes reachable.
|
|
680
|
+
const regained = type === 'compute' ? '; suspend fast-wake and scale-out are back' : '';
|
|
681
|
+
return `${type} ${name}: volume deleted — the disk and its data are gone${regained}`;
|
|
642
682
|
}
|
|
643
683
|
// Map a DELETE .../volume failure. Pure, exported for tests. An older backend has no DELETE
|
|
644
684
|
// route, and what its 404 looks like depends on who answered: the real platform (Fastify, no
|
|
@@ -661,7 +701,7 @@ export function volumeDeleteError(e) {
|
|
|
661
701
|
// no undo; billing stops now). The paid/cap/machine-count gates all belong to the backend, whose
|
|
662
702
|
// 403/400 messages carry the upgrade hints and must reach the user verbatim (the guard prints
|
|
663
703
|
// ApiError messages as-is).
|
|
664
|
-
export async function
|
|
704
|
+
export async function serviceVolume(type, serviceName, opts) {
|
|
665
705
|
if (opts.delete && opts.mountPath !== undefined)
|
|
666
706
|
throw new Error('--delete cannot be combined with --mount-path');
|
|
667
707
|
if (opts.mountPath !== undefined && !opts.size)
|
|
@@ -672,11 +712,11 @@ export async function computeVolume(serviceName, opts) {
|
|
|
672
712
|
const p = await requireProject();
|
|
673
713
|
const branch = opts.branch ?? p.branch;
|
|
674
714
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
675
|
-
const
|
|
715
|
+
const svc = resolveSoleService(services, type, serviceName);
|
|
676
716
|
if (opts.delete) {
|
|
677
717
|
let res;
|
|
678
718
|
try {
|
|
679
|
-
res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${id}/volume`);
|
|
719
|
+
res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${svc.id}/volume`);
|
|
680
720
|
}
|
|
681
721
|
catch (e) {
|
|
682
722
|
throw volumeDeleteError(e);
|
|
@@ -685,40 +725,41 @@ export async function computeVolume(serviceName, opts) {
|
|
|
685
725
|
return;
|
|
686
726
|
if (opts.json)
|
|
687
727
|
return printJson(res.body);
|
|
688
|
-
info(volumeDeleteLine(res.body.service?.name ??
|
|
728
|
+
info(volumeDeleteLine(res.body.service?.name ?? svc.name, type));
|
|
689
729
|
return;
|
|
690
730
|
}
|
|
691
731
|
if (!opts.size) {
|
|
692
|
-
const r = await api.request('GET', `/projects/${p.projectId}/services/${id}/volume`);
|
|
732
|
+
const r = await api.request('GET', `/projects/${p.projectId}/services/${svc.id}/volume`);
|
|
693
733
|
if (opts.json)
|
|
694
734
|
return printJson(r);
|
|
695
|
-
for (const line of volumeLines(
|
|
735
|
+
for (const line of volumeLines(svc.name, r.volume, r.cap, type))
|
|
696
736
|
info(line);
|
|
697
737
|
return;
|
|
698
738
|
}
|
|
699
739
|
const sizeGib = parseVolumeGib(opts.size);
|
|
700
|
-
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/volume`, { sizeGib, ...(opts.mountPath !== undefined ? { mountPath: opts.mountPath } : {}) });
|
|
740
|
+
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${svc.id}/volume`, { sizeGib, ...(opts.mountPath !== undefined ? { mountPath: opts.mountPath } : {}) });
|
|
701
741
|
if (handleApproval(res, opts.json))
|
|
702
742
|
return;
|
|
703
743
|
if (opts.json)
|
|
704
744
|
return printJson(res.body);
|
|
705
|
-
info(volumeWriteLine(res.body.service?.name ??
|
|
745
|
+
info(volumeWriteLine(res.body.service?.name ?? svc.name, res.body, type));
|
|
706
746
|
}
|
|
747
|
+
export const computeVolume = (serviceName, opts) => serviceVolume('compute', serviceName, opts);
|
|
707
748
|
// Show or set a compute service's ceiling. With no --memory it PRINTS the current limits and the
|
|
708
749
|
// plan cap (so `insta compute limits` is a safe read), which is also what a UI renders as a slider
|
|
709
750
|
// with its plan-limit marker.
|
|
710
|
-
export async function
|
|
751
|
+
export async function serviceLimits(type, serviceName, opts) {
|
|
711
752
|
const api = await ApiClient.load();
|
|
712
753
|
const p = await requireProject();
|
|
713
754
|
const branch = opts.branch ?? p.branch;
|
|
714
755
|
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
715
|
-
const
|
|
756
|
+
const svc = resolveSoleService(services, type, serviceName);
|
|
716
757
|
if (!opts.memory && !opts.cpu) {
|
|
717
|
-
const r = await api.request('GET', `/projects/${p.projectId}/services/${id}/limits`);
|
|
758
|
+
const r = await api.request('GET', `/projects/${p.projectId}/services/${svc.id}/limits`);
|
|
718
759
|
if (opts.json)
|
|
719
760
|
return printJson(r);
|
|
720
|
-
info(
|
|
721
|
-
info(
|
|
761
|
+
info(`${type} ${svc.name}: ceiling ${r.limits.cpu} vCPU / ${fmtMb(r.limits.memoryMb)} (plan max ${r.cap.cpu} vCPU / ${fmtMb(r.cap.memoryMb)})`);
|
|
762
|
+
info(` billing is actual usage — the ceiling caps what the ${type === 'compute' ? 'app' : 'database'} may burn, it is not a price`);
|
|
722
763
|
return;
|
|
723
764
|
}
|
|
724
765
|
if (!opts.memory)
|
|
@@ -726,14 +767,15 @@ export async function computeLimits(serviceName, opts) {
|
|
|
726
767
|
const body = { memoryMb: parseMemoryMb(opts.memory) };
|
|
727
768
|
if (opts.cpu)
|
|
728
769
|
body.cpu = parseCpu(opts.cpu);
|
|
729
|
-
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/limits`, body);
|
|
770
|
+
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${svc.id}/limits`, body);
|
|
730
771
|
if (handleApproval(res, opts.json))
|
|
731
772
|
return;
|
|
732
773
|
if (opts.json)
|
|
733
774
|
return printJson(res.body);
|
|
734
775
|
const l = res.body.limits;
|
|
735
|
-
info(
|
|
776
|
+
info(`${type} ${res.body.service?.name ?? svc.name}: ceiling set to ${l.cpu} vCPU / ${fmtMb(l.memoryMb)}`);
|
|
736
777
|
}
|
|
778
|
+
export const computeLimits = (serviceName, opts) => serviceLimits('compute', serviceName, opts);
|
|
737
779
|
// ---- ssh (interactive sessions) --------------------------------------------
|
|
738
780
|
import { chmodSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
739
781
|
import { homedir } from 'node:os';
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// `insta
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// `insta redis|mysql|mongodb query <service> [args...]` — run a query/command against a MANAGED
|
|
2
|
+
// database through the platform's console exec API. Postgres is not a console target (it has the
|
|
3
|
+
// SQL editor / DATABASE_URL, and `insta postgres url|connect`), so a postgres service is rejected
|
|
4
|
+
// here. The shape logic — path, request body, result rendering — lives in pure, unit-tested seams;
|
|
5
|
+
// the handler just resolves the service and wires them to the API, this repo's pure-seam
|
|
6
|
+
// convention.
|
|
7
7
|
import { ApiClient, requireProject } from '../api.js';
|
|
8
8
|
import { info, printJson, die, handleApproval } from '../util.js';
|
|
9
9
|
import { q } from './services.js';
|
|
@@ -62,11 +62,13 @@ async function dbQueryDeps(deps) {
|
|
|
62
62
|
return { api, project };
|
|
63
63
|
}
|
|
64
64
|
// Resolve <service> (a service NAME) to its id + engine, then dispatch to the console exec API.
|
|
65
|
-
|
|
65
|
+
// `engine` is the group the command was typed under (`insta redis query …`): a service of another
|
|
66
|
+
// type is refused with the right group named, never queried through the wrong renderer.
|
|
67
|
+
export async function dbQuery(service, args, opts = {}, deps, engine) {
|
|
66
68
|
// An empty command is never valid — reject it before loading config or hitting the network,
|
|
67
69
|
// rather than posting an empty statement/argv to the console.
|
|
68
70
|
if (args.length === 0) {
|
|
69
|
-
die(
|
|
71
|
+
die(`usage: insta ${engine ?? '<redis|mysql|mongodb>'} query <service> <query…> (mysql/mongodb: one quoted statement; redis: e.g. GET mykey)`);
|
|
70
72
|
}
|
|
71
73
|
const { api, project: p } = await dbQueryDeps(deps);
|
|
72
74
|
const branch = opts.branch ?? p.branch;
|
|
@@ -75,24 +77,26 @@ export async function dbQuery(service, args, opts = {}, deps) {
|
|
|
75
77
|
if (!svc)
|
|
76
78
|
die(`service not found: ${service}`);
|
|
77
79
|
if (!MANAGED_ENGINES.includes(svc.type)) {
|
|
78
|
-
die('
|
|
80
|
+
die('query is for managed databases (mysql/redis/mongodb); postgres uses `insta postgres url|connect` / the SQL editor');
|
|
79
81
|
}
|
|
80
|
-
|
|
82
|
+
if (engine && svc.type !== engine)
|
|
83
|
+
die(`${service} is a ${svc.type} service — use: insta ${svc.type} query ${service} …`);
|
|
84
|
+
const resolved = svc.type;
|
|
81
85
|
// --database is a mongodb-only selector (execBody drops it for the others). Rejecting it here,
|
|
82
86
|
// rather than silently ignoring it, keeps the documented mongodb-only contract honest.
|
|
83
|
-
if (opts.database !== undefined &&
|
|
87
|
+
if (opts.database !== undefined && resolved !== 'mongodb') {
|
|
84
88
|
die('--database is only supported for mongodb services');
|
|
85
89
|
}
|
|
86
|
-
const res = await api.rawRequest('POST', consoleExecPath(p.projectId, svc.id), execBody(
|
|
90
|
+
const res = await api.rawRequest('POST', consoleExecPath(p.projectId, svc.id), execBody(resolved, args, opts.database));
|
|
87
91
|
if (handleApproval(res, opts.json))
|
|
88
92
|
return;
|
|
89
93
|
if (opts.json)
|
|
90
94
|
return printJson(res.body);
|
|
91
|
-
if (
|
|
95
|
+
if (resolved === 'mysql') {
|
|
92
96
|
for (const line of renderMysqlRows(res.body ?? {}))
|
|
93
97
|
info(line);
|
|
94
98
|
}
|
|
95
|
-
else if (
|
|
99
|
+
else if (resolved === 'redis') {
|
|
96
100
|
info(renderRedisReply(res.body?.reply));
|
|
97
101
|
}
|
|
98
102
|
else {
|
package/dist/commands/deploy.js
CHANGED
|
@@ -60,7 +60,7 @@ async function discoverLane(api, projectId, branch, opts) {
|
|
|
60
60
|
// The route answered and the TARGET is what is missing: say so, or flyctl turns it into "no Dockerfile".
|
|
61
61
|
// Both ways out: the project has several groups and none is named, or it has none at all.
|
|
62
62
|
if (/compute group not found|branch not found/.test(e.message))
|
|
63
|
-
die(`${e.message} — name it with \`--group <name>\` (see \`insta
|
|
63
|
+
die(`${e.message} — name it with \`--group <name>\` (see \`insta service list\`), or add one: \`insta service add compute <name>\``);
|
|
64
64
|
return { lane: 'legacy' };
|
|
65
65
|
}
|
|
66
66
|
throw e;
|
package/dist/commands/domain.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ApiClient } from '../api.js';
|
|
2
2
|
import { info, printJson, handleApproval, die } from '../util.js';
|
|
3
3
|
import { presentUrl, resolveOrgId } from './billing.js';
|
|
4
|
-
import { domainDeps, domainTarget } from './compute.js';
|
|
4
|
+
import { domainDeps, domainTarget, setDomain, checkDomain, removeDomain } from './compute.js';
|
|
5
5
|
const usd = (cents) => `$${(cents / 100).toFixed(2)}`;
|
|
6
6
|
// --org wins; otherwise the linked project's org — the org verbs must not force a project link.
|
|
7
7
|
async function orgDeps(opts, deps) {
|
|
@@ -70,7 +70,9 @@ export async function domainAttach(host, opts, deps) {
|
|
|
70
70
|
const o = ownerOf(name, orders);
|
|
71
71
|
if (o)
|
|
72
72
|
die(`${o.domainName} is not registered yet — its order is ${o.status}: insta domain status ${o.domainName}`);
|
|
73
|
-
|
|
73
|
+
// Not a name this org bought: a domain owned elsewhere. Same verb, the bring-your-own path —
|
|
74
|
+
// the plane issues the edge cert and the DNS records to publish in your own zone are printed.
|
|
75
|
+
return setDomain(name, opts, { api, project: p });
|
|
74
76
|
}
|
|
75
77
|
const branch = opts.branch ?? p.branch;
|
|
76
78
|
const { target } = await domainTarget(api, p.projectId, branch, name, opts.group);
|
|
@@ -90,12 +92,20 @@ export async function domainAttach(host, opts, deps) {
|
|
|
90
92
|
function domainLines(d, linked = true) {
|
|
91
93
|
const out = [`${d.domainName} ${d.status}${d.expiresAt ? ` (expires ${d.expiresAt.slice(0, 10)}${d.autorenew ? ', auto-renews' : ''})` : ''}`];
|
|
92
94
|
// Vacuously true for a domain with no hostnames, which is every domain until something attaches.
|
|
93
|
-
|
|
95
|
+
// Not while delegated: the platform fails every hostname on the way out, and refuses the attach.
|
|
96
|
+
if (linked && !d.delegated && d.hostnames.every((h) => h.state === 'failed')) {
|
|
94
97
|
const names = d.hostnames.map((h) => h.hostname);
|
|
95
98
|
// Attaching the bought name itself re-attaches its www.
|
|
96
99
|
const retry = names.includes(d.domainName) ? names.filter((h) => h !== `www.${d.domainName}`) : names;
|
|
97
100
|
out.push(` nothing serving — ${(retry.length ? retry : [d.domainName]).map((h) => `insta domain attach ${h}`).join('; ')}`);
|
|
98
101
|
}
|
|
102
|
+
// The zone answers elsewhere, so nothing published here resolves and an attach is refused. The
|
|
103
|
+
// repair is a next action, so it follows `linked` like the others: under --org it would name this org.
|
|
104
|
+
if (d.delegated) {
|
|
105
|
+
out.push(` delegated to ${d.nameservers.join(', ')}${linked ? '' : ' — attach is refused'}`);
|
|
106
|
+
if (linked)
|
|
107
|
+
out.push(` attach is refused until: insta domain nameservers reset ${d.domainName}`);
|
|
108
|
+
}
|
|
99
109
|
const w = Math.max(0, ...d.hostnames.map((x) => x.hostname.length));
|
|
100
110
|
for (const h of d.hostnames)
|
|
101
111
|
out.push(` ${h.hostname.padEnd(w)} ${h.state}${h.service ? ` → ${h.service}` : ''}${h.reason ? ` — ${h.reason}` : ''}`);
|
|
@@ -134,6 +144,53 @@ export async function domainStatus(name, opts, deps) {
|
|
|
134
144
|
for (const line of domain ? domainLines(domain, !opts.org) : orderStatusLines(order, !opts.org))
|
|
135
145
|
info(line);
|
|
136
146
|
}
|
|
147
|
+
// ---- nameservers and transferring out ----
|
|
148
|
+
const domainPath = (orgId, domainName) => `/orgs/${orgId}/domains/${encodeURIComponent(domainName)}`;
|
|
149
|
+
export async function domainNameserversSet(domainName, hosts, opts, deps) {
|
|
150
|
+
const nameservers = hosts.flatMap((h) => h.split(/[\s,]+/)).map((h) => h.replace(/\.$/, '')).filter(Boolean);
|
|
151
|
+
if (!nameservers.length)
|
|
152
|
+
die("name at least one nameserver, or `insta domain nameservers reset <domain>` to restore the registrar's own");
|
|
153
|
+
const { api, orgId } = await orgDeps(opts, deps);
|
|
154
|
+
const r = await api.request('PUT', `${domainPath(orgId, domainName)}/nameservers`, { nameservers });
|
|
155
|
+
if (opts.json)
|
|
156
|
+
return printJson(r);
|
|
157
|
+
for (const line of domainLines(r, !opts.org))
|
|
158
|
+
info(line);
|
|
159
|
+
}
|
|
160
|
+
export async function domainNameserversReset(domainName, opts, deps) {
|
|
161
|
+
const { api, orgId } = await orgDeps(opts, deps);
|
|
162
|
+
const r = await api.request('DELETE', `${domainPath(orgId, domainName)}/nameservers`);
|
|
163
|
+
if (opts.json)
|
|
164
|
+
return printJson(r);
|
|
165
|
+
for (const line of domainLines(r, !opts.org))
|
|
166
|
+
info(line);
|
|
167
|
+
}
|
|
168
|
+
export async function domainTransferLock(domainName, mode, opts, deps) {
|
|
169
|
+
if (mode !== 'on' && mode !== 'off')
|
|
170
|
+
die('mode must be on or off');
|
|
171
|
+
const locked = mode === 'on';
|
|
172
|
+
const { api, orgId } = await orgDeps(opts, deps);
|
|
173
|
+
const r = await api.request('PATCH', domainPath(orgId, domainName), { locked });
|
|
174
|
+
if (opts.json)
|
|
175
|
+
return printJson(r);
|
|
176
|
+
info(`${r.domainName} transfer lock ${r.locked ? 'on' : 'off'}`);
|
|
177
|
+
if (!r.locked) {
|
|
178
|
+
// ICANN's post-registration lock outranks this one and nothing here can waive it.
|
|
179
|
+
if (r.transferLockExpiresAt && new Date(r.transferLockExpiresAt).getTime() > Date.now()) {
|
|
180
|
+
info(` ICANN holds the registration until ${r.transferLockExpiresAt.slice(0, 10)} whatever this says`);
|
|
181
|
+
}
|
|
182
|
+
if (!opts.org)
|
|
183
|
+
info(` authorization code: insta domain transfer code ${r.domainName}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
export async function domainTransferCode(domainName, opts, deps) {
|
|
187
|
+
const { api, orgId } = await orgDeps(opts, deps);
|
|
188
|
+
const r = await api.request('POST', `${domainPath(orgId, domainName)}/auth-code`);
|
|
189
|
+
if (opts.json)
|
|
190
|
+
return printJson(r);
|
|
191
|
+
info(r.authCode);
|
|
192
|
+
}
|
|
193
|
+
// ---- records ----
|
|
137
194
|
export function recordLines(records) {
|
|
138
195
|
const w = (pick) => Math.max(...records.map((r) => pick(r).length));
|
|
139
196
|
const idW = w((r) => String(r.id)), typeW = w((r) => r.type), nameW = w((r) => r.fqdn), answerW = w((r) => r.answer), ttlW = w((r) => String(r.ttl));
|
|
@@ -191,4 +248,49 @@ export async function domainRecordsRemove(domainName, id, opts, deps) {
|
|
|
191
248
|
return printJson(r);
|
|
192
249
|
info(`removed record ${id} from ${domainName}`);
|
|
193
250
|
}
|
|
251
|
+
export async function domainCheck(host, opts, deps) {
|
|
252
|
+
return checkDomain(host.trim().toLowerCase(), opts, deps);
|
|
253
|
+
}
|
|
254
|
+
/** The bought domain `host` sits under, or null — including when the question cannot be answered.
|
|
255
|
+
*
|
|
256
|
+
* This lookup is a GUARD on a command that worked without it: a control plane without the
|
|
257
|
+
* org-scoped domains route (an older deployment, insta-oss), a 403, or a transient 5xx must not
|
|
258
|
+
* turn a bring-your-own detach into a hard failure. A failed read means "nothing says this is a
|
|
259
|
+
* bought name", which is exactly how the command behaved before the guard existed. A read that
|
|
260
|
+
* SUCCEEDS is authoritative, and its refusal stands. */
|
|
261
|
+
async function boughtOwner(d, orgId, host) {
|
|
262
|
+
try {
|
|
263
|
+
const { items } = await d.api.request('GET', `/orgs/${orgId}/domains`);
|
|
264
|
+
return ownerOf(host, items);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Release a hostname from its compute service. Bring-your-own only: a hostname under a domain
|
|
272
|
+
* bought through InstaCloud is refused here.
|
|
273
|
+
*
|
|
274
|
+
* `attach` writes a bought hostname into the platform's DOMAINS record (state, serviceId,
|
|
275
|
+
* releaseFrom) and lets a reconciler bind it on the compute plane; this verb's only route,
|
|
276
|
+
* DELETE /projects/:id/compute/domain, unbinds on the compute plane alone and the platform
|
|
277
|
+
* exposes no detach route for the domains record. Running it would leave the record still
|
|
278
|
+
* claiming a binding that no longer exists — and, for an apex, `attach` binds both the name and
|
|
279
|
+
* its www while this takes one hostname. The supported way to move a bought hostname is another
|
|
280
|
+
* `attach`, which releases it from the old service itself.
|
|
281
|
+
*/
|
|
282
|
+
export async function domainDetach(host, opts, deps) {
|
|
283
|
+
const d = await domainDeps(deps);
|
|
284
|
+
const name = host.trim().toLowerCase();
|
|
285
|
+
// Without an org there is no domains list to check against (INSTA_PROJECT_ID-only CI links name
|
|
286
|
+
// none); a bring-your-own detach must keep working there, so the guard is simply not applied.
|
|
287
|
+
if (d.project.orgId) {
|
|
288
|
+
const owner = await boughtOwner(d, d.project.orgId, name);
|
|
289
|
+
if (owner) {
|
|
290
|
+
die(`${name} belongs to ${owner.domainName}, a domain bought through InstaCloud — its binding lives on the domain, not on the compute plane, so detaching it here would leave the domain still claiming it. `
|
|
291
|
+
+ `Move it with \`insta domain attach ${name} --group <other service>\` (attach releases it from the current one), and see where it stands with \`insta domain status ${owner.domainName}\`.`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return removeDomain(name, opts, d);
|
|
295
|
+
}
|
|
194
296
|
//# sourceMappingURL=domain.js.map
|
package/dist/commands/env.js
CHANGED
|
@@ -76,6 +76,6 @@ export async function envUse(name, opts = {}) {
|
|
|
76
76
|
// files were written for the previous environment and are keyed by a different server name, so
|
|
77
77
|
// they keep talking to it until setup is re-run. --env is REQUIRED in the hint: since 0.0.38 a
|
|
78
78
|
// bare `setup agent` forces prod, which would silently undo the switch the user just made.
|
|
79
|
-
info(` re-point this machine's agents at it with: insta setup
|
|
79
|
+
info(` re-point this machine's agents at it with: insta agent setup --env ${target}`);
|
|
80
80
|
}
|
|
81
81
|
//# sourceMappingURL=env.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// `insta redis|mysql|mongodb …` — the managed Fly databases. Their settings verbs (limits / volume /
|
|
2
|
+
// always-on) reuse compute.ts's type-generalized handlers: the platform's /services/:id/{limits,
|
|
3
|
+
// volume,always-on} accept "compute or managed-database" services. `status` cannot: GET
|
|
4
|
+
// /services/:id/state is compute-only on the platform (services.state() throws for anything else),
|
|
5
|
+
// so it reads the project's runtime-health — which covers compute, postgres and the managed DBs in
|
|
6
|
+
// one call — and picks this service's entry.
|
|
7
|
+
import { ApiClient, requireProject } from '../api.js';
|
|
8
|
+
import { info, printJson } from '../util.js';
|
|
9
|
+
import { q, resolveSoleService } from './services.js';
|
|
10
|
+
// pure: `redis cache: standby (1 machine, 0 failing)`. Exported for tests.
|
|
11
|
+
export function statusLine(type, name, entry) {
|
|
12
|
+
if (!entry)
|
|
13
|
+
return `${type} ${name}: unknown (the runtime-health read did not include this service)`;
|
|
14
|
+
const machines = `${entry.machines} machine${entry.machines === 1 ? '' : 's'}`;
|
|
15
|
+
return `${type} ${name}: ${entry.status} (${machines}, ${entry.failing} failing)`;
|
|
16
|
+
}
|
|
17
|
+
async function managedDeps(deps) {
|
|
18
|
+
if (deps)
|
|
19
|
+
return deps;
|
|
20
|
+
const [api, project] = [await ApiClient.load(), await requireProject()];
|
|
21
|
+
return { api, project };
|
|
22
|
+
}
|
|
23
|
+
export async function managedStatus(type, serviceName, opts, deps) {
|
|
24
|
+
const { api, project: p } = await managedDeps(deps);
|
|
25
|
+
const branch = opts.branch ?? p.branch;
|
|
26
|
+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
27
|
+
const svc = resolveSoleService(services, type, serviceName);
|
|
28
|
+
const res = await api.request('GET', `/projects/${p.projectId}/runtime-health${q(branch)}`);
|
|
29
|
+
const entry = res.services?.find((e) => e.serviceId === svc.id);
|
|
30
|
+
if (opts.json)
|
|
31
|
+
return printJson(entry ?? { serviceId: svc.id, status: 'unknown', machines: 0, failing: 0 });
|
|
32
|
+
info(statusLine(type, svc.name, entry));
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=managed-db.js.map
|
package/dist/commands/mcp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `insta
|
|
1
|
+
// `insta config install-mcp` — write the insta-cloud remote MCP server into each coding agent's own
|
|
2
2
|
// config format. Claude Code is NOT handled here — it has a real registry CLI (`claude mcp add`,
|
|
3
3
|
// see setup.ts registerMcp); these are the config-file agents. All entries are OAuth (no
|
|
4
4
|
// credential written): each client discovers the platform AS via RFC 9728 and runs the browser
|
|
@@ -118,7 +118,7 @@ export async function installAgentConfigs(agent, home = os.homedir()) {
|
|
|
118
118
|
}
|
|
119
119
|
return done;
|
|
120
120
|
}
|
|
121
|
-
// `insta
|
|
121
|
+
// `insta config install-mcp [--agent <slug>] [--mcp-token]` — claude-code goes through its registry CLI
|
|
122
122
|
// (registerMcp); everything else is a config-file write. No --agent = claude-code + all detected.
|
|
123
123
|
export async function mcpInstall(opts, register = registerMcp, installConfigs = installAgentConfigs) {
|
|
124
124
|
if (opts.mcpToken && opts.agent && opts.agent !== 'claude-code') {
|