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/metrics.js
CHANGED
|
@@ -60,7 +60,7 @@ function humanBytes(v, base) {
|
|
|
60
60
|
const shown = i === 0 && Number.isInteger(value) ? String(value) : value.toFixed(1);
|
|
61
61
|
return `${shown} ${units[i]}`;
|
|
62
62
|
}
|
|
63
|
-
// insta
|
|
63
|
+
// shared handler behind `insta <compute|postgres|redis|mysql|mongodb> metrics [service]`
|
|
64
64
|
export async function metrics(component, group, opts) {
|
|
65
65
|
const api = await ApiClient.load();
|
|
66
66
|
const p = await requireProject();
|
|
@@ -82,7 +82,7 @@ export function cycleLine(res) {
|
|
|
82
82
|
const day = (sec) => new Date(sec * 1000).toISOString().slice(0, 10);
|
|
83
83
|
return `billing cycle ${day(res.from)} → ${day(res.to - 86400)}`;
|
|
84
84
|
}
|
|
85
|
-
// Pure: format the per-dimension lines (label: qty unit (cost)). Shared by `insta usage` and
|
|
85
|
+
// Pure: format the per-dimension lines (label: qty unit (cost)). Shared by `insta billing usage` and
|
|
86
86
|
// `insta billing` so both render dimensions identically.
|
|
87
87
|
export function dimensionLines(dims) {
|
|
88
88
|
return dims.map((d) => {
|
|
@@ -95,7 +95,7 @@ function printDimensions(dims) {
|
|
|
95
95
|
for (const l of dimensionLines(dims))
|
|
96
96
|
info(l);
|
|
97
97
|
}
|
|
98
|
-
// insta usage — usage across the 5 billing dimensions (cpu/memory/volume/egress/storage) for the
|
|
98
|
+
// insta billing usage — usage across the 5 billing dimensions (cpu/memory/volume/egress/storage) for the
|
|
99
99
|
// current billing cycle. Shows the whole ORG by default (with a per-project breakdown); pass --proj
|
|
100
100
|
// [id] for a single project (the linked one, or a given id). Billed dimensions, not raw provider
|
|
101
101
|
// meters.
|
|
@@ -128,7 +128,8 @@ export async function usage(opts) {
|
|
|
128
128
|
info(` ${pr.name}: $${Number(pr.totalCostUsd ?? 0).toFixed(4)}`);
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
|
-
// pure: platform path for a deploy-events request (used by `insta logs --deploy`
|
|
131
|
+
// pure: platform path for a deploy-events request (used by `insta compute logs --deploy` and the
|
|
132
|
+
// equivalent on redis/mysql/mongodb). Any Fly-backed
|
|
132
133
|
// component (compute or a managed database) has machine lifecycle events; omitted → the platform
|
|
133
134
|
// defaults to compute.
|
|
134
135
|
export function deployEventsPath(projectId, opts) {
|
|
@@ -169,7 +170,7 @@ export function resolveLogWindow(opts, now = Math.floor(Date.now() / 1000)) {
|
|
|
169
170
|
throw new Error('--to is before --from');
|
|
170
171
|
return { from, to };
|
|
171
172
|
}
|
|
172
|
-
// insta
|
|
173
|
+
// shared handler behind `insta <compute|postgres|redis|mysql|mongodb> logs [service]`
|
|
173
174
|
export async function logs(component, group, opts) {
|
|
174
175
|
const windowFlags = opts.from !== undefined || opts.to !== undefined || opts.since !== undefined;
|
|
175
176
|
if (opts.deploy && windowFlags)
|
package/dist/commands/observe.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `insta observe` — the local credential-audit hook. install wires a PostToolUse hook into the
|
|
1
|
+
// `insta agent observe` — the local credential-audit hook. install wires a PostToolUse hook into the
|
|
2
2
|
// agent harness; report renders the local audit; sync uploads findings into the project timeline
|
|
3
3
|
// (idempotent via a stable dedup key, matching the platform's audit-event ingest).
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
@@ -67,7 +67,7 @@ export async function observeInstall() {
|
|
|
67
67
|
if (hint)
|
|
68
68
|
info(hint);
|
|
69
69
|
info('it scans agent tool-use for credential exposure; findings append to ./.insta/audit.jsonl');
|
|
70
|
-
info('run `insta observe report` to review, `insta observe sync` to upload to the project timeline');
|
|
70
|
+
info('run `insta agent observe report` to review, `insta agent observe sync` to upload to the project timeline');
|
|
71
71
|
}
|
|
72
72
|
export async function observeUninstall() {
|
|
73
73
|
uninstallObserve(await installRoot());
|
|
@@ -7,7 +7,7 @@ import { parseVolumeGib, q, resolveSoleService } from './services.js';
|
|
|
7
7
|
// cold-starts on the next connection) and always-on (instance stays warm; idle RAM bills at
|
|
8
8
|
// actual usage). Thin wrapper over PATCH /database/settings {scaleToZero} — insta-db-backed
|
|
9
9
|
// postgres only.
|
|
10
|
-
export async function dbAlwaysOn(mode, opts) {
|
|
10
|
+
export async function dbAlwaysOn(mode, service, opts) {
|
|
11
11
|
if (mode !== 'on' && mode !== 'off')
|
|
12
12
|
throw new Error('mode must be on|off');
|
|
13
13
|
const api = await ApiClient.load();
|
|
@@ -16,15 +16,15 @@ export async function dbAlwaysOn(mode, opts) {
|
|
|
16
16
|
const branch = opts.branch ?? p.branch;
|
|
17
17
|
if (branch)
|
|
18
18
|
qs.set('branch', branch);
|
|
19
|
-
if (
|
|
20
|
-
qs.set('group',
|
|
19
|
+
if (service)
|
|
20
|
+
qs.set('group', service);
|
|
21
21
|
const res = await api.rawRequest('PATCH', `/projects/${p.projectId}/database/settings${qs.toString() ? `?${qs}` : ''}`, { scaleToZero: mode !== 'on' });
|
|
22
22
|
if (handleApproval(res, opts.json))
|
|
23
23
|
return;
|
|
24
24
|
if (opts.json)
|
|
25
25
|
return printJson(res.body);
|
|
26
26
|
const s2z = res.body?.scaleToZero;
|
|
27
|
-
info(`postgres ${
|
|
27
|
+
info(`postgres ${service ?? '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)'}`);
|
|
28
28
|
}
|
|
29
29
|
// Validated pass-throughs for the provider's quantity strings. The insta-db resize API takes
|
|
30
30
|
// k8s-style quantities (cpu: "2", "2500m"; memory: "4Gi", "2048Mi"), so unlike the compute path
|
|
@@ -66,20 +66,20 @@ export async function fetchDbInstance(api, projectId, suffix) {
|
|
|
66
66
|
// Show or set a postgres service's resource ceiling (insta-db-backed only). Paid plans — the
|
|
67
67
|
// ceiling is the tier lever now that billing follows actual usage. Moves both directions:
|
|
68
68
|
// unlike storage it is a cgroup limit, not a provisioned volume.
|
|
69
|
-
export async function dbLimits(opts) {
|
|
69
|
+
export async function dbLimits(service, opts) {
|
|
70
70
|
const api = await ApiClient.load();
|
|
71
71
|
const p = await requireProject();
|
|
72
72
|
const qs = new URLSearchParams();
|
|
73
73
|
const branch = opts.branch ?? p.branch;
|
|
74
74
|
if (branch)
|
|
75
75
|
qs.set('branch', branch);
|
|
76
|
-
if (
|
|
77
|
-
qs.set('group',
|
|
76
|
+
if (service)
|
|
77
|
+
qs.set('group', service);
|
|
78
78
|
const suffix = qs.toString() ? `?${qs}` : '';
|
|
79
79
|
if (!opts.cpu && !opts.memory) {
|
|
80
80
|
const read = await fetchDbInstance(api, p.projectId, suffix);
|
|
81
81
|
if (read.kind === 'no-instance') {
|
|
82
|
-
info(`postgres ${
|
|
82
|
+
info(`postgres ${service ?? 'default'}: no manageable instance (this service manages its own resources)`);
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
if (opts.json)
|
|
@@ -88,11 +88,11 @@ export async function dbLimits(opts) {
|
|
|
88
88
|
const mib = read.body?.memoryMib;
|
|
89
89
|
if (typeof cpuMilli === 'number' && typeof mib === 'number') {
|
|
90
90
|
const cpu = cpuMilli % 1000 === 0 ? `${cpuMilli / 1000}` : `${cpuMilli}m`;
|
|
91
|
-
info(`postgres ${
|
|
91
|
+
info(`postgres ${service ?? 'default'}: ceiling ${cpu} vCPU / ${fmtMib(mib)}`);
|
|
92
92
|
info(' billing is actual usage — the ceiling caps what the database may burn, it is not a price');
|
|
93
93
|
}
|
|
94
94
|
else {
|
|
95
|
-
info(`postgres ${
|
|
95
|
+
info(`postgres ${service ?? 'default'}: provider reported no ceiling — set one with --cpu/--memory`);
|
|
96
96
|
}
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
@@ -116,7 +116,7 @@ export async function dbLimits(opts) {
|
|
|
116
116
|
return printJson(res.body);
|
|
117
117
|
const cpu = typeof res.body?.cpuMilli === 'number' ? `${res.body.cpuMilli / 1000} vCPU` : (opts.cpu ?? 'unchanged');
|
|
118
118
|
const mem = typeof res.body?.memoryMib === 'number' ? fmtMib(res.body.memoryMib) : (opts.memory ?? 'unchanged');
|
|
119
|
-
info(`postgres ${
|
|
119
|
+
info(`postgres ${service ?? 'default'}: ceiling set to ${cpu} / ${mem}`);
|
|
120
120
|
}
|
|
121
121
|
// Bytes → human units, one decimal above KiB. Local because the metrics payload is the only
|
|
122
122
|
// bytes-denominated read in this file (fmtMib serves the MiB-denominated resize path).
|
|
@@ -164,19 +164,19 @@ export function dbStatsLines(group, body) {
|
|
|
164
164
|
// anywhere, and the code that handled it is retained, not live. Neon-backed: the platform read
|
|
165
165
|
// over a direct SQL connection, so a one-shot call could wake a suspended endpoint — acceptable
|
|
166
166
|
// for an explicit command, which is why nothing here polls.
|
|
167
|
-
export async function dbStats(opts) {
|
|
167
|
+
export async function dbStats(service, opts) {
|
|
168
168
|
const api = await ApiClient.load();
|
|
169
169
|
const p = await requireProject();
|
|
170
170
|
const qs = new URLSearchParams();
|
|
171
171
|
const branch = opts.branch ?? p.branch;
|
|
172
172
|
if (branch)
|
|
173
173
|
qs.set('branch', branch);
|
|
174
|
-
if (
|
|
175
|
-
qs.set('group',
|
|
174
|
+
if (service)
|
|
175
|
+
qs.set('group', service);
|
|
176
176
|
const res = await api.rawRequest('GET', `/projects/${p.projectId}/database/metrics${qs.toString() ? `?${qs}` : ''}`);
|
|
177
177
|
if (opts.json)
|
|
178
178
|
return printJson(res.body);
|
|
179
|
-
for (const line of dbStatsLines(
|
|
179
|
+
for (const line of dbStatsLines(service ?? 'default', res.body))
|
|
180
180
|
info(line);
|
|
181
181
|
}
|
|
182
182
|
// Render the instance's volume from a database/instance read. Pure, exported for tests. Reads the
|
|
@@ -197,25 +197,25 @@ export function dbVolumeLines(group, body) {
|
|
|
197
197
|
// is available on every plan; growth is paid and grow-only — both gates are the backend's to
|
|
198
198
|
// enforce, so nothing here pre-blocks: its 403/400 messages carry the upgrade hints and are wrapped
|
|
199
199
|
// with context but kept verbatim.
|
|
200
|
-
export async function dbVolume(opts) {
|
|
200
|
+
export async function dbVolume(service, opts) {
|
|
201
201
|
const api = await ApiClient.load();
|
|
202
202
|
const p = await requireProject();
|
|
203
203
|
const qs = new URLSearchParams();
|
|
204
204
|
const branch = opts.branch ?? p.branch;
|
|
205
205
|
if (branch)
|
|
206
206
|
qs.set('branch', branch);
|
|
207
|
-
if (
|
|
208
|
-
qs.set('group',
|
|
207
|
+
if (service)
|
|
208
|
+
qs.set('group', service);
|
|
209
209
|
const suffix = qs.toString() ? `?${qs}` : '';
|
|
210
210
|
if (!opts.size) {
|
|
211
211
|
const read = await fetchDbInstance(api, p.projectId, suffix);
|
|
212
212
|
if (read.kind === 'no-instance') {
|
|
213
|
-
info(`postgres ${
|
|
213
|
+
info(`postgres ${service ?? 'default'}: no manageable instance (this service manages its own storage)`);
|
|
214
214
|
return;
|
|
215
215
|
}
|
|
216
216
|
if (opts.json)
|
|
217
217
|
return printJson(read.body);
|
|
218
|
-
for (const line of dbVolumeLines(
|
|
218
|
+
for (const line of dbVolumeLines(service ?? 'default', read.body))
|
|
219
219
|
info(line);
|
|
220
220
|
return;
|
|
221
221
|
}
|
|
@@ -234,9 +234,9 @@ export async function dbVolume(opts) {
|
|
|
234
234
|
if (opts.json)
|
|
235
235
|
return printJson(res.body);
|
|
236
236
|
const vg = res.body?.volumeGib;
|
|
237
|
-
info(`postgres ${
|
|
237
|
+
info(`postgres ${service ?? 'default'}: volume ${typeof vg === 'number' ? `grown to ${vg}Gi` : `set to ${sizeGib}Gi`}`);
|
|
238
238
|
}
|
|
239
|
-
// Resolve the postgres service (sole, or
|
|
239
|
+
// Resolve the postgres service (sole, or the named one) and its connection string. Two reads: the
|
|
240
240
|
// branch's services list names the service; GET /services/:id/credentials (gated secrets.read)
|
|
241
241
|
// carries the value. Provider-minted credentials are canonical within their source service
|
|
242
242
|
// (DATABASE_URL) and deliberately absent from the general `insta secrets` bundle, so this is the
|
|
@@ -244,25 +244,25 @@ export async function dbVolume(opts) {
|
|
|
244
244
|
// already branch-scoped by the list. Returns null when the read parked on an approval
|
|
245
245
|
// (handleApproval already spoke). Takes the client as an argument so tests drive it with a stub,
|
|
246
246
|
// per this repo's pure-seam convention.
|
|
247
|
-
export async function resolveDbUrl(api, projectId, branch,
|
|
247
|
+
export async function resolveDbUrl(api, projectId, branch, service, json) {
|
|
248
248
|
const { services } = await api.request('GET', `/projects/${projectId}/services${q(branch)}`);
|
|
249
|
-
const svc = resolveSoleService(services, 'postgres',
|
|
249
|
+
const svc = resolveSoleService(services, 'postgres', service);
|
|
250
250
|
const res = await api.rawRequest('GET', `/projects/${projectId}/services/${svc.id}/credentials`);
|
|
251
251
|
if (handleApproval(res, json))
|
|
252
252
|
return null;
|
|
253
253
|
const url = res.body?.credentials?.DATABASE_URL;
|
|
254
254
|
if (typeof url !== 'string' || !url) {
|
|
255
|
-
throw new Error(`postgres ${svc.name} has no DATABASE_URL credential yet — still provisioning? (\`insta
|
|
255
|
+
throw new Error(`postgres ${svc.name} has no DATABASE_URL credential yet — still provisioning? (\`insta service list\` shows status)`);
|
|
256
256
|
}
|
|
257
257
|
return { serviceName: svc.name, url };
|
|
258
258
|
}
|
|
259
259
|
// Print the postgres connection string: the bare DSN on stdout, nothing else — pipe-friendly
|
|
260
|
-
// (`psql "$(insta
|
|
261
|
-
export async function dbUrl(opts) {
|
|
260
|
+
// (`psql "$(insta postgres url)"`), like `storage get --json` keeps stdout parseable.
|
|
261
|
+
export async function dbUrl(service, opts) {
|
|
262
262
|
const api = await ApiClient.load();
|
|
263
263
|
const p = await requireProject();
|
|
264
264
|
const branch = opts.branch ?? p.branch;
|
|
265
|
-
const r = await resolveDbUrl(api, p.projectId, branch,
|
|
265
|
+
const r = await resolveDbUrl(api, p.projectId, branch, service, opts.json);
|
|
266
266
|
if (!r)
|
|
267
267
|
return;
|
|
268
268
|
if (opts.json)
|
|
@@ -308,7 +308,7 @@ export async function connectWithPsql(url, spawnImpl = spawn) {
|
|
|
308
308
|
return await new Promise((resolve, reject) => {
|
|
309
309
|
const child = spawnImpl('psql', [], { stdio: 'inherit', env });
|
|
310
310
|
child.on('error', (e) => reject(e.code === 'ENOENT'
|
|
311
|
-
? new Error('psql not found on PATH — install the postgres client, or print the DSN with `insta
|
|
311
|
+
? new Error('psql not found on PATH — install the postgres client, or print the DSN with `insta postgres url`')
|
|
312
312
|
: e));
|
|
313
313
|
// Signal death reports code null — map to the conventional 128+signo (full table from
|
|
314
314
|
// os.constants) so the advertised exit-status passthrough holds for Ctrl-C'd/killed sessions.
|
|
@@ -318,15 +318,15 @@ export async function connectWithPsql(url, spawnImpl = spawn) {
|
|
|
318
318
|
// Open an interactive psql session on the postgres service. The DSN never touches disk or argv
|
|
319
319
|
// history beyond the child process. Exits with psql's own exit code (agents rely on this, as
|
|
320
320
|
// with `compute exec`).
|
|
321
|
-
export async function dbConnect(opts) {
|
|
321
|
+
export async function dbConnect(service, opts) {
|
|
322
322
|
const api = await ApiClient.load();
|
|
323
323
|
const p = await requireProject();
|
|
324
324
|
const branch = opts.branch ?? p.branch;
|
|
325
|
-
const r = await resolveDbUrl(api, p.projectId, branch,
|
|
325
|
+
const r = await resolveDbUrl(api, p.projectId, branch, service, opts.json);
|
|
326
326
|
if (!r)
|
|
327
327
|
return;
|
|
328
328
|
// stderr: stdout belongs to psql (the `insta run` rule).
|
|
329
329
|
process.stderr.write(`psql → postgres/${r.serviceName}${branch ? ` (branch ${branch})` : ''} — a suspended instance wakes on connect, so the first prompt can take a few seconds\n`);
|
|
330
330
|
relayExitCode(await connectWithPsql(r.url));
|
|
331
331
|
}
|
|
332
|
-
//# sourceMappingURL=
|
|
332
|
+
//# sourceMappingURL=postgres.js.map
|
package/dist/commands/regions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ApiClient } from '../api.js';
|
|
2
2
|
import { info, printJson } from '../util.js';
|
|
3
|
-
// insta regions — list the regions a postgres/compute service can be created in.
|
|
3
|
+
// insta config regions — list the regions a postgres/compute service can be created in.
|
|
4
4
|
export async function regionsList(opts = {}, deps) {
|
|
5
5
|
const api = deps?.api ?? await ApiClient.load();
|
|
6
6
|
const { regions } = await api.request('GET', '/regions');
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `insta
|
|
1
|
+
// `insta service` — manage a project's opt-in services (postgres | storage | compute | redis | mysql | mongodb).
|
|
2
2
|
import { ApiClient, requireProject } from '../api.js';
|
|
3
3
|
import { info, printJson, handleApproval, renderNextActions } from '../util.js';
|
|
4
4
|
export const SERVICE_TYPES = ['postgres', 'storage', 'compute', 'redis', 'mysql', 'mongodb'];
|
|
@@ -16,6 +16,28 @@ export function assertServiceName(name) {
|
|
|
16
16
|
if (!SERVICE_NAME_RE.test(name))
|
|
17
17
|
throw new Error('service name must be lower-kebab (a-z, 0-9, -)');
|
|
18
18
|
}
|
|
19
|
+
// Every service type that owns an always-on / volume verb of its OWN. The creation-time flags are
|
|
20
|
+
// compute-only, so the refusal has to name the group the user actually typed: pointing a redis or
|
|
21
|
+
// mongodb user at `insta postgres …` sends them at a different service type (and, on a project
|
|
22
|
+
// with no postgres, at nothing at all).
|
|
23
|
+
const DB_TYPES = ['postgres', 'redis', 'mysql', 'mongodb'];
|
|
24
|
+
const hasOwnGroup = (type) => DB_TYPES.includes(type);
|
|
25
|
+
export function alwaysOnTypeError(type) {
|
|
26
|
+
const base = '--always-on / --no-always-on is only valid for compute services';
|
|
27
|
+
return hasOwnGroup(type) ? `${base} (for ${type}, use \`insta ${type} always-on on|off\` after creation)` : base;
|
|
28
|
+
}
|
|
29
|
+
export function volumeTypeError(type) {
|
|
30
|
+
const base = '--volume is only valid for compute services';
|
|
31
|
+
// insta-db-backed postgres is provisioned WITH its disk (dbVolume goes through
|
|
32
|
+
// PATCH /database/settings), so there is nothing to attach — only to grow. A managed Fly
|
|
33
|
+
// database can be volumeless, and `--size` attaches there (ServicesService.setVolumeSize's
|
|
34
|
+
// attach branch runs for isFlyRuntimeType), which is what `volumeLines` already prints.
|
|
35
|
+
if (type === 'postgres')
|
|
36
|
+
return `${base} (postgres has one by default — grow it with \`insta postgres volume --size <gi>\`)`;
|
|
37
|
+
if (hasOwnGroup(type))
|
|
38
|
+
return `${base} (attach or grow one after creation with \`insta ${type} volume --size <gi>\`)`;
|
|
39
|
+
return base;
|
|
40
|
+
}
|
|
19
41
|
const MAX_COMPUTE_REPLICAS = 10;
|
|
20
42
|
// Parse a replica count inside the compute plane's current safety ceiling.
|
|
21
43
|
export function parseCount(raw) {
|
|
@@ -65,7 +87,7 @@ export function resolveSoleService(services, type, name) {
|
|
|
65
87
|
return svc;
|
|
66
88
|
}
|
|
67
89
|
if (of.length === 0)
|
|
68
|
-
throw new Error(`no ${type} service in this project (add one with \`insta
|
|
90
|
+
throw new Error(`no ${type} service in this project (add one with \`insta service add ${type} <name>\`)`);
|
|
69
91
|
if (of.length > 1)
|
|
70
92
|
throw new Error(`multiple ${type} services — specify one: ${of.map((s) => s.name).join(', ')}`);
|
|
71
93
|
return of[0];
|
|
@@ -107,12 +129,12 @@ export async function servicesAdd(type, name, opts = {}) {
|
|
|
107
129
|
}
|
|
108
130
|
// Presence, not truthiness: `--no-always-on` is an explicit false and is just as compute-only.
|
|
109
131
|
if (opts.alwaysOn !== undefined && type !== 'compute')
|
|
110
|
-
throw new Error(
|
|
132
|
+
throw new Error(alwaysOnTypeError(type));
|
|
111
133
|
if (opts.mountPath !== undefined && (type !== 'compute' || opts.volume === undefined))
|
|
112
134
|
throw new Error('--mount-path requires --volume on a compute service');
|
|
113
135
|
if (opts.volume !== undefined) {
|
|
114
136
|
if (type !== 'compute')
|
|
115
|
-
throw new Error(
|
|
137
|
+
throw new Error(volumeTypeError(type));
|
|
116
138
|
parseVolumeGib(opts.volume); // junk fails here, before any config/network access
|
|
117
139
|
}
|
|
118
140
|
const api = await ApiClient.load();
|
|
@@ -129,9 +151,10 @@ export async function servicesAdd(type, name, opts = {}) {
|
|
|
129
151
|
// general `insta secrets` bundle — without this line nothing in the product says how to reach it.
|
|
130
152
|
if (type === 'postgres') {
|
|
131
153
|
// The hint must be runnable as printed: carry --branch when the service was created on a
|
|
132
|
-
// branch other than the linked one, and
|
|
133
|
-
|
|
134
|
-
|
|
154
|
+
// branch other than the linked one, and the service name so it survives multiple postgres
|
|
155
|
+
// services (a trailing positional on `postgres url`/`postgres connect`, not a flag).
|
|
156
|
+
const branchFlag = opts.branch ? ` --branch ${opts.branch}` : '';
|
|
157
|
+
info(` connect: \`insta postgres url ${name}${branchFlag}\` prints the connection string, \`insta postgres connect ${name}${branchFlag}\` opens psql ([service] optional with a single postgres service)`);
|
|
135
158
|
}
|
|
136
159
|
renderNextActions(res.body.nextActions);
|
|
137
160
|
}
|
|
@@ -171,7 +194,7 @@ export async function servicesList(opts) {
|
|
|
171
194
|
if (opts.json)
|
|
172
195
|
return printJson(services);
|
|
173
196
|
if (!services.length)
|
|
174
|
-
return info(`(no services on ${branch ?? 'default'} — add one with \`insta
|
|
197
|
+
return info(`(no services on ${branch ?? 'default'} — add one with \`insta service add <postgres|storage|compute|redis|mysql|mongodb> <name>\`)`);
|
|
175
198
|
for (const s of services)
|
|
176
199
|
info(serviceListLine(s));
|
|
177
200
|
}
|
|
@@ -212,66 +235,4 @@ export function parseAccess(raw) {
|
|
|
212
235
|
return false;
|
|
213
236
|
throw new Error(`access must be public|private, got: ${raw}`);
|
|
214
237
|
}
|
|
215
|
-
// insta services set-access storage <name> <public|private>
|
|
216
|
-
export async function servicesSetAccess(type, name, access, _opts) {
|
|
217
|
-
assertType(type, ['storage']);
|
|
218
|
-
const isPublic = parseAccess(access);
|
|
219
|
-
const api = await ApiClient.load();
|
|
220
|
-
const p = await requireProject();
|
|
221
|
-
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(p.branch)}`);
|
|
222
|
-
const id = resolveServiceId(services, type, name);
|
|
223
|
-
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/access`, { public: isPublic });
|
|
224
|
-
if (handleApproval(res, _opts.json))
|
|
225
|
-
return;
|
|
226
|
-
if (_opts.json)
|
|
227
|
-
return printJson(res.body.service);
|
|
228
|
-
info(`set storage ${name} access to ${access}`);
|
|
229
|
-
}
|
|
230
|
-
// insta services scale compute <name> <number> [region]
|
|
231
|
-
export async function servicesScale(type, name, number, region, _opts) {
|
|
232
|
-
assertType(type, ['compute']);
|
|
233
|
-
const machineCount = parseCount(number);
|
|
234
|
-
const api = await ApiClient.load();
|
|
235
|
-
const p = await requireProject();
|
|
236
|
-
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(_opts.branch ?? p.branch)}`);
|
|
237
|
-
const id = resolveServiceId(services, type, name);
|
|
238
|
-
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/scale`, { machineCount, region });
|
|
239
|
-
if (handleApproval(res, _opts.json))
|
|
240
|
-
return;
|
|
241
|
-
if (_opts.json)
|
|
242
|
-
return printJson(res.body.service);
|
|
243
|
-
info(`scaled compute ${name} to ${machineCount} replica(s)${region ? ` in ${region}` : ''}`);
|
|
244
|
-
}
|
|
245
|
-
// insta services upgrade <compute|postgres> <name> <new-spec>
|
|
246
|
-
export async function servicesUpgrade(type, name, spec, _opts) {
|
|
247
|
-
assertType(type, ['compute', 'postgres']);
|
|
248
|
-
const api = await ApiClient.load();
|
|
249
|
-
const p = await requireProject();
|
|
250
|
-
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(_opts.branch ?? p.branch)}`);
|
|
251
|
-
const id = resolveServiceId(services, type, name);
|
|
252
|
-
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/upgrade`, { spec });
|
|
253
|
-
if (handleApproval(res, _opts.json))
|
|
254
|
-
return;
|
|
255
|
-
if (_opts.json)
|
|
256
|
-
return printJson(res.body.service);
|
|
257
|
-
info(`upgraded ${type} ${name} to ${spec}`);
|
|
258
|
-
}
|
|
259
|
-
// insta services secrets <type> <name> — the secret names bound to a service.
|
|
260
|
-
export async function servicesSecrets(type, name, opts = {}) {
|
|
261
|
-
assertType(type);
|
|
262
|
-
const api = await ApiClient.load();
|
|
263
|
-
const p = await requireProject();
|
|
264
|
-
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(opts.branch ?? p.branch)}`);
|
|
265
|
-
const id = resolveServiceId(services, type, name);
|
|
266
|
-
const res = await api.rawRequest('GET', `/projects/${p.projectId}/services/${id}/secrets`);
|
|
267
|
-
if (handleApproval(res, opts.json))
|
|
268
|
-
return;
|
|
269
|
-
const { secrets } = res.body;
|
|
270
|
-
if (opts.json)
|
|
271
|
-
return printJson(secrets);
|
|
272
|
-
if (!secrets.length)
|
|
273
|
-
return info(`(no secrets bound to ${type}/${name})`);
|
|
274
|
-
for (const n of secrets)
|
|
275
|
-
info(n);
|
|
276
|
-
}
|
|
277
238
|
//# sourceMappingURL=services.js.map
|
package/dist/commands/setup.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// `insta setup
|
|
2
|
-
// (
|
|
1
|
+
// `insta agent setup` — make this machine's coding agents InstaCloud-native in one step.
|
|
2
|
+
// (`setup agent` remains a permanent hidden alias; the canonical order is `agent setup`, per the
|
|
3
|
+
// noun-first tree — see the Command architecture rules.) Installs the `insta` skill USER-GLOBALLY for
|
|
3
4
|
// every agent the skills tool knows: the skill is pure product knowledge with brand-gated
|
|
4
5
|
// triggers — no project state in it (the project binding is carried by ./.insta/project.json
|
|
5
6
|
// at command time), so one machine-level copy is strictly better than per-project copies.
|
|
@@ -89,7 +90,7 @@ export function summarizeInstall(output) {
|
|
|
89
90
|
: `${count} agent${count === 1 ? '' : 's'}`;
|
|
90
91
|
return `✓ Agents — ${list}`;
|
|
91
92
|
}
|
|
92
|
-
// ---- CLI self-install (makes `npx -y insta setup
|
|
93
|
+
// ---- CLI self-install (makes `npx -y insta agent setup` a complete one-liner) ----
|
|
93
94
|
// Under npx the CLI runs from the npm cache and vanishes when the process exits — but the skill
|
|
94
95
|
// installed below tells every agent to run `insta …`, which then wouldn't exist. So when this
|
|
95
96
|
// process came from the npm channel and no DURABLE `insta` is on PATH, install ourselves
|
|
@@ -226,7 +227,7 @@ export async function registerMcp(run = defaultRunner, mint = defaultMinter, use
|
|
|
226
227
|
if (useToken) {
|
|
227
228
|
const token = await mint();
|
|
228
229
|
if (!token) {
|
|
229
|
-
info(' MCP not registered (--mcp-token needs a login) — run `insta login`, then `insta setup
|
|
230
|
+
info(' MCP not registered (--mcp-token needs a login) — run `insta login`, then `insta agent setup --mcp-token` again');
|
|
230
231
|
return 'no-token';
|
|
231
232
|
}
|
|
232
233
|
args.push('--header', `Authorization: Bearer ${token}`);
|
|
@@ -256,7 +257,7 @@ export function requireMcpRegistration(status) {
|
|
|
256
257
|
/** The environment `setup agent` should target, and whether the machine must be switched to it
|
|
257
258
|
* first. Pure — decides only; the caller performs the switch.
|
|
258
259
|
*
|
|
259
|
-
* The contract: the public one-liner `npx -y insta setup
|
|
260
|
+
* The contract: the public one-liner `npx -y insta agent setup` means PRODUCTION,
|
|
260
261
|
* full stop — a leftover `insta env use staging` from last month must not silently give a new
|
|
261
262
|
* onboarding run staging skills. Staging is an explicit ask: `--env staging` (or $INSTA_ENV).
|
|
262
263
|
* Two deliberate exceptions leave the machine alone:
|
|
@@ -473,10 +474,10 @@ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs
|
|
|
473
474
|
}
|
|
474
475
|
if (loggedIn) {
|
|
475
476
|
if (await enroll())
|
|
476
|
-
info('✓ Project agent session ready (expires in 24 hours; refresh with insta setup
|
|
477
|
+
info('✓ Project agent session ready (expires in 24 hours; refresh with insta agent setup)');
|
|
477
478
|
}
|
|
478
479
|
else {
|
|
479
|
-
info(' project agent session not created — run `insta login`, then `insta setup
|
|
480
|
+
info(' project agent session not created — run `insta login`, then `insta agent setup`');
|
|
480
481
|
}
|
|
481
482
|
// THE summary line. The restart note exists because config-file agents only read their MCP
|
|
482
483
|
// config at startup; the skill files need no restart.
|
package/dist/commands/storage.js
CHANGED
|
@@ -6,8 +6,8 @@ import { Readable } from 'node:stream';
|
|
|
6
6
|
import { pipeline } from 'node:stream/promises';
|
|
7
7
|
import { ApiClient, requireProject } from '../api.js';
|
|
8
8
|
import { info, printJson, handleApproval } from '../util.js';
|
|
9
|
-
import { q, resolveSoleService } from './services.js';
|
|
10
|
-
import { fmtBytes } from './
|
|
9
|
+
import { q, resolveSoleService, parseAccess } from './services.js';
|
|
10
|
+
import { fmtBytes } from './postgres.js'; // the repo's tested bytes formatter — don't grow a third copy
|
|
11
11
|
function qs(params) {
|
|
12
12
|
const u = new URLSearchParams();
|
|
13
13
|
for (const [k, v] of Object.entries(params))
|
|
@@ -170,4 +170,19 @@ export async function storageDelete(key, opts) {
|
|
|
170
170
|
return printJson(res.body);
|
|
171
171
|
info(`deleted ${key} from storage/${svc.name} (branch ${branch})`);
|
|
172
172
|
}
|
|
173
|
+
// `insta storage set-access <public|private> [--service <name>]` — PUT /services/:id/access.
|
|
174
|
+
// public = anonymous public-read on the bucket; private is the default a bucket is born with.
|
|
175
|
+
export async function storageSetAccess(access, opts) {
|
|
176
|
+
const isPublic = parseAccess(access);
|
|
177
|
+
const api = await ApiClient.load();
|
|
178
|
+
const p = await requireProject();
|
|
179
|
+
const branch = opts.branch ?? p.branch;
|
|
180
|
+
const svc = await storageTarget(api, p.projectId, branch, opts.service);
|
|
181
|
+
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${svc.id}/access`, { public: isPublic });
|
|
182
|
+
if (handleApproval(res, opts.json))
|
|
183
|
+
return;
|
|
184
|
+
if (opts.json)
|
|
185
|
+
return printJson(res.body.service);
|
|
186
|
+
info(`set storage ${svc.name} access to ${access}`);
|
|
187
|
+
}
|
|
173
188
|
//# sourceMappingURL=storage.js.map
|
|
@@ -12,7 +12,7 @@ import { info, printJson, handleApproval, renderNextActions, CliCancel } from '.
|
|
|
12
12
|
import { MANIFEST_FILE, collectManifestVariables, loadTemplateManifest } from '../template-manifest.js';
|
|
13
13
|
import { parseGitHubTemplateUrl, fetchGitHubTemplate } from '../github-source.js';
|
|
14
14
|
// One aligned row per template; numeric columns right-aligned. Plain padded columns, as the rest
|
|
15
|
-
// of the CLI (storage list,
|
|
15
|
+
// of the CLI (storage list, domain check) — no table library.
|
|
16
16
|
export function templateListLines(templates) {
|
|
17
17
|
if (!templates.length)
|
|
18
18
|
return ['(no templates published yet)'];
|
|
@@ -233,7 +233,7 @@ export function partialMessage(dep) {
|
|
|
233
233
|
...serviceStateLines(dep),
|
|
234
234
|
...(dep?.error ? [` ${dep.error}`] : []),
|
|
235
235
|
...(dep?.logsTail ? ['--- log tail ---', String(dep.logsTail).trimEnd()] : []),
|
|
236
|
-
'created services are kept — inspect with `insta logs
|
|
236
|
+
'created services are kept — inspect with `insta compute logs <name>`, re-run the deploy to retry, or remove them with `insta service remove <type> <name>`',
|
|
237
237
|
].join('\n');
|
|
238
238
|
}
|
|
239
239
|
export function failureMessage(dep, fallbackStep) {
|
|
@@ -279,7 +279,7 @@ export async function watchDeployment(getDeployment, id, out = info, wait = slee
|
|
|
279
279
|
}
|
|
280
280
|
await wait(2);
|
|
281
281
|
}
|
|
282
|
-
throw new Error(`timed out after ${Math.round(timeoutMs / 60_000)}m waiting for template deployment ${id} — check \`insta events\``);
|
|
282
|
+
throw new Error(`timed out after ${Math.round(timeoutMs / 60_000)}m waiting for template deployment ${id} — check \`insta agent events\``);
|
|
283
283
|
}
|
|
284
284
|
// ---- commands ----
|
|
285
285
|
export async function templateList(opts) {
|
|
@@ -391,7 +391,7 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
391
391
|
for (const u of deploymentUrls(dep))
|
|
392
392
|
info(` ${u}`);
|
|
393
393
|
// Provider credentials are not in the `insta secrets` bundle — point at the paths that exist.
|
|
394
|
-
info('next: `insta
|
|
394
|
+
info('next: `insta postgres url` prints the postgres DSN; bind service credentials into compute with `insta secrets bind`; `insta secrets` refreshes user-defined secrets in .env');
|
|
395
395
|
renderNextActions(dep.nextActions);
|
|
396
396
|
}
|
|
397
397
|
//# sourceMappingURL=template.js.map
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// release installer; npm via `npm i -g`). A background version check (detached, cached in
|
|
3
3
|
// ~/.insta/update-check.json) powers an update nudge — and, since the CLI is young and moves
|
|
4
4
|
// fast, AUTO-UPDATE IS ON BY DEFAULT: when a newer version is known, a quiet upgrade runs in the
|
|
5
|
-
// background. `insta autoupdate off` (or INSTA_NO_AUTOUPDATE=1) disables that, leaving just the
|
|
5
|
+
// background. `insta config autoupdate off` (or INSTA_NO_AUTOUPDATE=1) disables that, leaving just the
|
|
6
6
|
// stderr nudge.
|
|
7
7
|
//
|
|
8
8
|
// ONE SOURCE OF TRUTH. "What is the latest insta?" is answered in exactly one place —
|
|
@@ -20,7 +20,7 @@ import { spawn } from 'node:child_process';
|
|
|
20
20
|
import { dirname, join } from 'node:path';
|
|
21
21
|
import { homedir } from 'node:os';
|
|
22
22
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
23
|
-
import { readGlobal, writeGlobal } from '../config.js';
|
|
23
|
+
import { readGlobal, readPersistedGlobal, writeGlobal } from '../config.js';
|
|
24
24
|
import { resolveSpawnable } from '../spawn.js';
|
|
25
25
|
import { info } from '../util.js';
|
|
26
26
|
const INSTALL_SH = 'https://raw.githubusercontent.com/InsForge/instacloud-cli/main/install.sh';
|
|
@@ -329,16 +329,22 @@ export async function backgroundCheck(current, deps = {}) {
|
|
|
329
329
|
}
|
|
330
330
|
return 'auto';
|
|
331
331
|
}
|
|
332
|
-
// `insta autoupdate [on|off]` — toggle / show the auto-update preference (default: on).
|
|
332
|
+
// `insta config autoupdate [on|off]` — toggle / show the auto-update preference (default: on).
|
|
333
333
|
export async function autoupdate(mode) {
|
|
334
|
-
const cfg = await readGlobal();
|
|
335
334
|
if (mode === 'on' || mode === 'off') {
|
|
335
|
+
// Read-modify-write must use the PERSISTED config (like `env use` does), not readGlobal()'s
|
|
336
|
+
// runtime view: readGlobal() folds in a --api-url/INSTA_API_URL override and, when that override
|
|
337
|
+
// points at a different deployment, scrubs the stored session. Writing that view back to disk
|
|
338
|
+
// here would silently re-point ~/.insta/config.json at the override and log the user out just
|
|
339
|
+
// from toggling autoupdate.
|
|
340
|
+
const cfg = await readPersistedGlobal();
|
|
336
341
|
await writeGlobal({ ...cfg, autoUpdate: mode === 'on' });
|
|
337
342
|
info(`autoupdate ${mode}`);
|
|
338
343
|
return;
|
|
339
344
|
}
|
|
345
|
+
const cfg = await readGlobal();
|
|
340
346
|
const enabled = cfg.autoUpdate !== false && !process.env.INSTA_NO_AUTOUPDATE;
|
|
341
|
-
info(`autoupdate: ${enabled ? 'on' : 'off'} (default on while the CLI is pre-1.0 — \`insta autoupdate off\` to disable)`);
|
|
347
|
+
info(`autoupdate: ${enabled ? 'on' : 'off'} (default on while the CLI is pre-1.0 — \`insta config autoupdate off\` to disable)`);
|
|
342
348
|
}
|
|
343
349
|
// Called once at CLI start-up. Never blocks: reads the cache synchronously, prints at most one
|
|
344
350
|
// stderr line, and (when due) spawns detached children for the registry check / quiet upgrade.
|
|
@@ -353,11 +359,17 @@ export async function autoupdate(mode) {
|
|
|
353
359
|
* into the ssh session's stderr and an auto-upgrade spawns a detached process
|
|
354
360
|
* mid-connection. Same prefix rule trackCommand already applies to telemetry.
|
|
355
361
|
*/
|
|
356
|
-
export function skipsUpdateCheck(cmd) {
|
|
362
|
+
export function skipsUpdateCheck(cmd, sub) {
|
|
363
|
+
// The autoupdate PREFERENCE moved to `insta config autoupdate` in the command re-organization,
|
|
364
|
+
// so the level-1 name alone stopped matching it: `insta config autoupdate off` would run the
|
|
365
|
+
// very check it is being typed to switch off (and could auto-upgrade before the handler lands).
|
|
366
|
+
// The retired top-level spelling stays exempt too — it costs nothing and cannot regress.
|
|
367
|
+
if (cmd === 'config' && sub === 'autoupdate')
|
|
368
|
+
return true;
|
|
357
369
|
return cmd === 'upgrade' || cmd === 'autoupdate' || !!cmd?.startsWith('__');
|
|
358
370
|
}
|
|
359
371
|
export function maybeUpdate(current, argv) {
|
|
360
|
-
if (skipsUpdateCheck(argv[2]))
|
|
372
|
+
if (skipsUpdateCheck(argv[2], argv[3]))
|
|
361
373
|
return;
|
|
362
374
|
const channel = detectChannel();
|
|
363
375
|
const cache = readCache();
|
|
@@ -373,7 +385,7 @@ export function maybeUpdate(current, argv) {
|
|
|
373
385
|
else if (action === 'auto') {
|
|
374
386
|
writeCache({ ...cache, lastAutoAt: now });
|
|
375
387
|
respawnDetached(['upgrade']);
|
|
376
|
-
console.error(`↑ auto-updating insta ${current} → ${cache.latest} in the background (\`insta autoupdate off\` to disable)`);
|
|
388
|
+
console.error(`↑ auto-updating insta ${current} → ${cache.latest} in the background (\`insta config autoupdate off\` to disable)`);
|
|
377
389
|
}
|
|
378
390
|
}
|
|
379
391
|
// Re-invoke this same CLI (binary or node+script) detached, output discarded.
|