insta 0.0.24 → 0.0.25
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/dist/commands/compute.js +59 -0
- package/dist/commands/db.js +93 -1
- package/dist/index.js +8 -1
- package/package.json +1 -1
package/dist/commands/compute.js
CHANGED
|
@@ -87,4 +87,63 @@ export async function computeAlwaysOn(mode, serviceName, opts) {
|
|
|
87
87
|
const on = res.body.service?.always_on;
|
|
88
88
|
info(`compute ${res.body.service?.name ?? id}: always-on ${on ? 'ENABLED — machines stay warm (no cold starts; idle RAM bills at actual usage)' : 'disabled — scales to zero when idle (default)'}`);
|
|
89
89
|
}
|
|
90
|
+
// ---- limits (the resource ceiling; paid plans) ----
|
|
91
|
+
// Parse a human memory value into MB: "512", "512mb", "1gb", "2g", "1.5gb".
|
|
92
|
+
// Exported for unit tests — this is the only place a user-typed size becomes a number.
|
|
93
|
+
export function parseMemoryMb(raw) {
|
|
94
|
+
const m = /^\s*(\d+(?:\.\d+)?)\s*(g|gb|gi|gib|m|mb|mi|mib)?\s*$/i.exec(raw);
|
|
95
|
+
if (!m)
|
|
96
|
+
throw new Error(`invalid memory: ${raw} (try 512mb, 1gb, 2gb)`);
|
|
97
|
+
const n = Number(m[1]);
|
|
98
|
+
const unit = (m[2] ?? 'mb').toLowerCase();
|
|
99
|
+
const mb = unit.startsWith('g') ? n * 1024 : n;
|
|
100
|
+
if (!(mb > 0))
|
|
101
|
+
throw new Error(`invalid memory: ${raw}`);
|
|
102
|
+
return Math.round(mb);
|
|
103
|
+
}
|
|
104
|
+
// Whole and half GB collapse (1536 → "1.5 GB"); anything else stays exact in MB — a display that
|
|
105
|
+
// rounds 1536 to "2 GB" claims a ceiling the API did not set.
|
|
106
|
+
export const fmtMb = (mb) => (mb >= 1024 && mb % 512 === 0 ? `${mb / 1024} GB` : `${mb} MB`);
|
|
107
|
+
// The --cpu override, through a throwing parser like every other user-typed number in this repo
|
|
108
|
+
// (parseCount, parseMemoryMb). A bare Number() turns a typo into NaN, which JSON.stringify
|
|
109
|
+
// serializes as null — the server then sees {cpu: null} instead of the user seeing an error.
|
|
110
|
+
// Enforces the provider grid the help text advertises: the server would reject 100 anyway, but a
|
|
111
|
+
// value the client KNOWS is invalid should fail locally, matching what --help promises.
|
|
112
|
+
const CPU_SIZES = [1, 2, 4, 6, 8];
|
|
113
|
+
export function parseCpu(raw) {
|
|
114
|
+
const n = Number(raw);
|
|
115
|
+
if (!CPU_SIZES.includes(n))
|
|
116
|
+
throw new Error(`invalid cpu: ${raw} (provider sizes: ${CPU_SIZES.join(', ')})`);
|
|
117
|
+
return n;
|
|
118
|
+
}
|
|
119
|
+
// Show or set a compute service's ceiling. With no --memory it PRINTS the current limits and the
|
|
120
|
+
// plan cap (so `insta compute limits` is a safe read), which is also what a UI renders as a slider
|
|
121
|
+
// with its plan-limit marker.
|
|
122
|
+
export async function computeLimits(serviceName, opts) {
|
|
123
|
+
const api = await ApiClient.load();
|
|
124
|
+
const p = await requireProject();
|
|
125
|
+
const branch = opts.branch ?? p.branch;
|
|
126
|
+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
127
|
+
const id = resolveComputeServiceId(services, serviceName);
|
|
128
|
+
if (!opts.memory && !opts.cpu) {
|
|
129
|
+
const r = await api.request('GET', `/projects/${p.projectId}/services/${id}/limits`);
|
|
130
|
+
if (opts.json)
|
|
131
|
+
return printJson(r);
|
|
132
|
+
info(`compute ${serviceName ?? id}: ceiling ${r.limits.cpu} vCPU / ${fmtMb(r.limits.memoryMb)} (plan max ${r.cap.cpu} vCPU / ${fmtMb(r.cap.memoryMb)})`);
|
|
133
|
+
info(' billing is actual usage — the ceiling caps what the app may burn, it is not a price');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!opts.memory)
|
|
137
|
+
throw new Error('--memory is required when setting limits (cpu is derived from it; pass --cpu only to override)');
|
|
138
|
+
const body = { memoryMb: parseMemoryMb(opts.memory) };
|
|
139
|
+
if (opts.cpu)
|
|
140
|
+
body.cpu = parseCpu(opts.cpu);
|
|
141
|
+
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/limits`, body);
|
|
142
|
+
if (handleApproval(res))
|
|
143
|
+
return;
|
|
144
|
+
if (opts.json)
|
|
145
|
+
return printJson(res.body);
|
|
146
|
+
const l = res.body.limits;
|
|
147
|
+
info(`compute ${res.body.service?.name ?? id}: ceiling set to ${l.cpu} vCPU / ${fmtMb(l.memoryMb)}`);
|
|
148
|
+
}
|
|
90
149
|
//# sourceMappingURL=compute.js.map
|
package/dist/commands/db.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiClient, requireProject } from '../api.js';
|
|
1
|
+
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
2
2
|
import { info, printJson, handleApproval } from '../util.js';
|
|
3
3
|
// Toggle a postgres service between scale-to-zero (the default: instance suspends when idle,
|
|
4
4
|
// cold-starts on the next connection) and always-on (instance stays warm; idle RAM bills at
|
|
@@ -24,4 +24,96 @@ export async function dbAlwaysOn(mode, opts) {
|
|
|
24
24
|
const s2z = res.body?.scaleToZero;
|
|
25
25
|
info(`postgres ${opts.group ?? 'default'}: always-on ${s2z === false ? 'ENABLED — instance stays warm (no cold starts; idle RAM bills at actual usage)' : 'disabled — scales to zero when idle (default; first connection after idle cold-starts)'}`);
|
|
26
26
|
}
|
|
27
|
+
// Validated pass-throughs for the provider's quantity strings. The insta-db resize API takes
|
|
28
|
+
// k8s-style quantities (cpu: "2", "2500m"; memory: "4Gi", "2048Mi"), so unlike the compute path
|
|
29
|
+
// there is no unit conversion here — but junk must still fail LOCALLY with an example, not travel
|
|
30
|
+
// to the server as-is. CASE-EXACT deliberately: k8s quantities are case-sensitive ("4gi" is
|
|
31
|
+
// rejected server-side), and local validation that accepts a form the backend refuses would
|
|
32
|
+
// defeat its own purpose.
|
|
33
|
+
export function parseDbCpu(raw) {
|
|
34
|
+
if (!/^\d+(\.\d+)?m?$/.test(raw.trim()))
|
|
35
|
+
throw new Error(`invalid cpu: ${raw} (try 2, 4, or 2500m)`);
|
|
36
|
+
return raw.trim();
|
|
37
|
+
}
|
|
38
|
+
export function parseDbMemory(raw) {
|
|
39
|
+
if (!/^\d+(\.\d+)?(Gi|Mi|G|M)$/.test(raw.trim()))
|
|
40
|
+
throw new Error(`invalid memory: ${raw} (try 4Gi or 8Gi)`);
|
|
41
|
+
return raw.trim();
|
|
42
|
+
}
|
|
43
|
+
// MiB → display without lying: whole/half GiB collapse, anything else stays exact in MiB
|
|
44
|
+
// (1536 MiB is "1.5 GiB", 1300 MiB is "1300 MiB" — never "1 GiB").
|
|
45
|
+
export function fmtMib(mib) {
|
|
46
|
+
return mib >= 1024 && mib % 512 === 0 ? `${mib / 1024} GiB` : `${mib} MiB`;
|
|
47
|
+
}
|
|
48
|
+
export async function fetchDbInstance(api, projectId, suffix) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await api.rawRequest('GET', `/projects/${projectId}/database/instance${suffix}`);
|
|
51
|
+
return { kind: 'ok', body: res.body };
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
// The platform answers a provider-shaped 502 for services with no manageable instance
|
|
55
|
+
// (Neon-backed): a soft case, not a failure. Everything else stays an error — an expired
|
|
56
|
+
// token must not render as "no ceiling set" — but wrapped so the user sees what failed.
|
|
57
|
+
if (e instanceof ApiError && e.status === 502)
|
|
58
|
+
return { kind: 'no-instance' };
|
|
59
|
+
if (e instanceof ApiError)
|
|
60
|
+
throw new Error(`reading the instance failed (${e.status}): ${e.message}`);
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Show or set a postgres service's resource ceiling (insta-db-backed only). Paid plans — the
|
|
65
|
+
// ceiling is the tier lever now that billing follows actual usage. Moves both directions:
|
|
66
|
+
// unlike storage it is a cgroup limit, not a provisioned volume.
|
|
67
|
+
export async function dbLimits(opts) {
|
|
68
|
+
const api = await ApiClient.load();
|
|
69
|
+
const p = await requireProject();
|
|
70
|
+
const qs = new URLSearchParams();
|
|
71
|
+
const branch = opts.branch ?? p.branch;
|
|
72
|
+
if (branch)
|
|
73
|
+
qs.set('branch', branch);
|
|
74
|
+
if (opts.group)
|
|
75
|
+
qs.set('group', opts.group);
|
|
76
|
+
const suffix = qs.toString() ? `?${qs}` : '';
|
|
77
|
+
if (!opts.cpu && !opts.memory) {
|
|
78
|
+
const read = await fetchDbInstance(api, p.projectId, suffix);
|
|
79
|
+
if (read.kind === 'no-instance') {
|
|
80
|
+
info(`postgres ${opts.group ?? 'default'}: no manageable instance (Neon-backed services manage their own resources)`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (opts.json)
|
|
84
|
+
return printJson(read.body);
|
|
85
|
+
const cpuMilli = read.body?.cpuMilli;
|
|
86
|
+
const mib = read.body?.memoryMib;
|
|
87
|
+
if (typeof cpuMilli === 'number' && typeof mib === 'number') {
|
|
88
|
+
const cpu = cpuMilli % 1000 === 0 ? `${cpuMilli / 1000}` : `${cpuMilli}m`;
|
|
89
|
+
info(`postgres ${opts.group ?? 'default'}: ceiling ${cpu} vCPU / ${fmtMib(mib)}`);
|
|
90
|
+
info(' billing is actual usage — the ceiling caps what the database may burn, it is not a price');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
info(`postgres ${opts.group ?? 'default'}: provider reported no ceiling — set one with --cpu/--memory`);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const body = {};
|
|
98
|
+
if (opts.cpu)
|
|
99
|
+
body.cpu = parseDbCpu(opts.cpu);
|
|
100
|
+
if (opts.memory)
|
|
101
|
+
body.memory = parseDbMemory(opts.memory);
|
|
102
|
+
let res;
|
|
103
|
+
try {
|
|
104
|
+
res = await api.rawRequest('PATCH', `/projects/${p.projectId}/database/settings${suffix}`, body);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
if (e instanceof ApiError)
|
|
108
|
+
throw new Error(`setting the ceiling failed (${e.status}): ${e.message}`);
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
if (handleApproval(res))
|
|
112
|
+
return;
|
|
113
|
+
if (opts.json)
|
|
114
|
+
return printJson(res.body);
|
|
115
|
+
const cpu = typeof res.body?.cpuMilli === 'number' ? `${res.body.cpuMilli / 1000} vCPU` : (opts.cpu ?? 'unchanged');
|
|
116
|
+
const mem = typeof res.body?.memoryMib === 'number' ? fmtMib(res.body.memoryMib) : (opts.memory ?? 'unchanged');
|
|
117
|
+
info(`postgres ${opts.group ?? 'default'}: ceiling set to ${cpu} / ${mem}`);
|
|
118
|
+
}
|
|
27
119
|
//# sourceMappingURL=db.js.map
|
package/dist/index.js
CHANGED
|
@@ -163,10 +163,17 @@ compute.command('suspend [service]').description('Suspend a compute service (RAM
|
|
|
163
163
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeSuspend(service, o)));
|
|
164
164
|
compute.command('status [service]').description("Show a compute service's desired vs. live state")
|
|
165
165
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStatus(service, o)));
|
|
166
|
+
compute.command('limits [service]').description("Show or set a compute service's resource ceiling (paid plans). --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")
|
|
167
|
+
.option('--memory <size>', 'memory ceiling, e.g. 512mb or 1gb').option('--cpu <n>', 'vCPU ceiling override (provider sizes: 1, 2, 4, 6, 8)')
|
|
168
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeLimits(service, o)));
|
|
166
169
|
compute.command('always-on <mode> [service]').description('Set a compute service always-on (mode: on|off). on = machines never scale to zero; off = default scale-to-zero. All plans; billing is actual usage either way')
|
|
167
170
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((mode, service, o) => computeCmd.computeAlwaysOn(mode, service, o)));
|
|
168
171
|
// ---- db (postgres service controls) ----
|
|
169
|
-
const db = program.command('db').description('Postgres service controls (always-on / scale-to-zero)');
|
|
172
|
+
const db = program.command('db').description('Postgres service controls (limits / always-on / scale-to-zero)');
|
|
173
|
+
db.command('limits').description("Show or set a postgres service's resource ceiling (paid plans; insta-db-backed only). Moves both directions")
|
|
174
|
+
.option('--cpu <n>', "vCPU ceiling, e.g. 2 or 2500m").option('--memory <size>', "memory ceiling, e.g. 4Gi")
|
|
175
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
|
|
176
|
+
.action(guard((o) => dbCmd.dbLimits(o)));
|
|
170
177
|
db.command('always-on <mode>').description('Set a postgres service always-on (mode: on|off). on = instance stays warm, no cold starts; off = default scale-to-zero (idle instance suspends; first connection cold-starts). insta-db-backed services only')
|
|
171
178
|
.option('--json').option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
|
|
172
179
|
.action(guard((mode, o) => dbCmd.dbAlwaysOn(mode, o)));
|