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.
@@ -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 metrics <db|compute|redis|mysql|mongodb> [group]
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`). Any Fly-backed
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 logs <db|compute|redis|mysql|mongodb> [group]
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)
@@ -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 (opts.group)
20
- qs.set('group', opts.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 ${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)'}`);
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 (opts.group)
77
- qs.set('group', opts.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 ${opts.group ?? 'default'}: no manageable instance (this service manages its own resources)`);
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 ${opts.group ?? 'default'}: ceiling ${cpu} vCPU / ${fmtMib(mib)}`);
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 ${opts.group ?? 'default'}: provider reported no ceiling — set one with --cpu/--memory`);
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 ${opts.group ?? 'default'}: ceiling set to ${cpu} / ${mem}`);
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 (opts.group)
175
- qs.set('group', opts.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(opts.group ?? 'default', res.body))
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 (opts.group)
208
- qs.set('group', opts.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 ${opts.group ?? 'default'}: no manageable instance (this service manages its own storage)`);
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(opts.group ?? 'default', read.body))
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 ${opts.group ?? 'default'}: volume ${typeof vg === 'number' ? `grown to ${vg}Gi` : `set to ${sizeGib}Gi`}`);
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 --group) and its connection string. Two reads: the
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, group, json) {
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', group);
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 services list\` shows status)`);
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 db url)"`), like `storage get --json` keeps stdout parseable.
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, opts.group, opts.json);
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 db url`')
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, opts.group, opts.json);
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=db.js.map
332
+ //# sourceMappingURL=postgres.js.map
@@ -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 services` — manage a project's opt-in services (postgres | storage | compute | redis | mysql | mongodb).
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 services add ${type} <name>\`)`);
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('--always-on / --no-always-on is only valid for compute services (for postgres, use `insta db always-on on|off` after creation)');
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('--volume is only valid for compute services (postgres has one by default — grow it with `insta db volume --size`)');
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 --group so it survives multiple postgres services.
133
- const flags = `${opts.branch ? ` --branch ${opts.branch}` : ''} --group ${name}`;
134
- info(` connect: \`insta db url${flags}\` prints the connection string, \`insta db connect${flags}\` opens psql (--group optional with a single postgres service)`);
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 services add <postgres|storage|compute|redis|mysql|mongodb> <name>\`)`);
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
@@ -1,5 +1,6 @@
1
- // `insta setup agent` — make this machine's coding agents InstaCloud-native in one step
2
- // (the Railway `railway setup agent` pattern). Installs the `insta` skill USER-GLOBALLY for
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 agent` a complete one-liner) ----
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 agent --mcp-token` again');
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 agent` means PRODUCTION,
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 agent)');
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 agent`');
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.
@@ -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 './db.js'; // the repo's tested bytes formatter — don't grow a third copy
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, compute check-domain) — no table library.
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 compute <name>`, re-run the deploy to retry, or remove them with `insta services remove <type> <name>`',
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 db url` prints the postgres DSN; bind service credentials into compute with `insta secrets bind`; `insta secrets` refreshes user-defined secrets in .env');
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
@@ -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.