insta 0.0.26 → 0.0.28

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 CHANGED
@@ -79,8 +79,8 @@ plane's address, not the CLI's loopback address.
79
79
  ### Services are branch-scoped
80
80
 
81
81
  A project holds services (`postgres`, `storage`, `compute`) and each branch owns its own
82
- set. `insta branch create feature-x` forks the parent's services: a Neon branch per
83
- Postgres, a copy-on-write bucket per storage, a clone of every compute service. From there
82
+ set. `insta branch create feature-x` forks the parent's services: a copy-on-write database
83
+ branch per Postgres, a copy-on-write bucket per storage, a clone of every compute service. From there
84
84
  the two branches diverge independently. A project is capped at 10 branches.
85
85
 
86
86
  ### Credentials come from the secret seam, not a file you maintain
@@ -207,8 +207,9 @@ build never reaches a production installer.
207
207
  The `insta` skill and its task guides live in
208
208
  [InsForge/insta-skills](https://github.com/InsForge/insta-skills). `insta setup agent`
209
209
  installs it user-globally for every coding agent on the machine. `insta project create` and
210
- `insta project link` additionally install the stack skills (Neon Postgres, Tigris, Better
211
- Auth) into the project, along with the `insta observe` credential-audit hook.
210
+ `insta project link` additionally install the stack skills (Tigris, Better Auth) into the
211
+ project, along with the `insta observe` credential-audit hook. Postgres needs no stack
212
+ skill — it's plain Postgres, reached directly via `DATABASE_URL`.
212
213
 
213
214
  ## Contributing
214
215
 
package/dist/api.js CHANGED
@@ -11,6 +11,13 @@ export class ApiError extends Error {
11
11
  this.name = 'ApiError';
12
12
  }
13
13
  }
14
+ // Store a durable insta_ key as the credential: set it as the bearer and drop any refresh token (an insta_ key never rotates; a stale one would leak to /auth/refresh on a 401).
15
+ export function storeApiKeyCredential(cfg, token, user) {
16
+ cfg.accessToken = token;
17
+ delete cfg.refreshToken;
18
+ if (user)
19
+ cfg.user = user;
20
+ }
14
21
  export class ApiClient {
15
22
  cfg;
16
23
  constructor(cfg) {
@@ -27,6 +34,10 @@ export class ApiClient {
27
34
  if (user)
28
35
  this.cfg.user = user;
29
36
  }
37
+ // Adopt a durable insta_ key as the credential (non-interactive `login --api-key`).
38
+ setApiKey(token, user) {
39
+ storeApiKeyCredential(this.cfg, token, user);
40
+ }
30
41
  clearSession() {
31
42
  delete this.cfg.accessToken;
32
43
  delete this.cfg.refreshToken;
@@ -17,6 +17,13 @@ function targetApiUrl(opts) {
17
17
  return ENVS[want].api;
18
18
  }
19
19
  export async function login(opts) {
20
+ // Login modes are exclusive — pick one. Check presence (not truthiness) so an explicit
21
+ // empty --api-key= is rejected by validation rather than silently falling through.
22
+ if (opts.apiKey !== undefined) {
23
+ if (opts.device || opts.oauth || opts.email)
24
+ die('choose one login mode: --api-key, --device, --oauth, or --email');
25
+ return loginApiKey(opts.apiKey, opts);
26
+ }
20
27
  if (opts.device)
21
28
  return loginDevice(opts);
22
29
  if (opts.oauth)
@@ -65,6 +72,36 @@ export async function loginDevice(opts) {
65
72
  await api.persist();
66
73
  info(`logged in as ${me.user.email ?? me.user.id} @ ${api.apiUrl}`);
67
74
  }
75
+ // Non-interactive login with a durable insta_ key (minted via POST /tokens): store it and confirm against /me. No browser, no polling.
76
+ export async function loginApiKey(key, opts) {
77
+ const api = await ApiClient.load();
78
+ const target = targetApiUrl(opts);
79
+ if (target)
80
+ api.setApiUrl(target);
81
+ const user = await applyApiKeyLogin(api, key);
82
+ await api.persist();
83
+ info(`logged in as ${user.email ?? user.id} @ ${api.apiUrl}`);
84
+ }
85
+ // Verify an insta_ key and store it: set it first so the /me probe is authed with the key itself, then re-store with the resolved user (401 → bad/revoked).
86
+ export async function applyApiKeyLogin(client, key) {
87
+ key = key.trim(); // tolerate a trailing newline / stray whitespace from `--api-key "$(cat token)"`
88
+ if (!key.startsWith('insta_'))
89
+ throw new Error('--api-key expects an insta_ token (mint one with POST /tokens)');
90
+ client.setApiKey(key);
91
+ let me;
92
+ try {
93
+ me = await client.request('GET', '/me');
94
+ }
95
+ catch (e) {
96
+ if (e instanceof ApiError && e.status === 401)
97
+ throw new Error('that insta_ API key was rejected (invalid or revoked) — check it or mint a new one');
98
+ throw e;
99
+ }
100
+ if (!me?.user)
101
+ throw new Error('unexpected response while verifying the API key');
102
+ client.setApiKey(key, me.user);
103
+ return me.user;
104
+ }
68
105
  const sleepSeconds = (s) => new Promise((r) => setTimeout(r, s * 1000));
69
106
  // Drives the device grant against the platform's Better Auth mount (/api/auth/device*) and
70
107
  // returns the approved session token. Injectable poster + wait keep this testable without a
@@ -1,6 +1,6 @@
1
1
  import { ApiClient, requireProject } from '../api.js';
2
2
  import { info, printJson, handleApproval } from '../util.js';
3
- import { resolveComputeServiceId, q } from './services.js';
3
+ import { resolveComputeServiceId, q, parseVolumeGib } from './services.js';
4
4
  // Attach a developer-owned custom domain to a branch's compute service. Fly issues the cert + routes
5
5
  // it; the platform returns the DNS records to set in your OWN zone.
6
6
  export async function setDomain(host, opts) {
@@ -116,6 +116,46 @@ export function parseCpu(raw) {
116
116
  throw new Error(`invalid cpu: ${raw} (provider sizes: ${CPU_SIZES.join(', ')})`);
117
117
  return n;
118
118
  }
119
+ // ---- volume (the persistent /data disk; attach is create-time only) ----
120
+ // Render the volume read. Pure, exported for tests (mirrors serviceListLine). Every plan may view;
121
+ // only growth is paid — that gate is the backend's to enforce, so nothing here pre-blocks.
122
+ export function volumeLines(name, volume, cap) {
123
+ if (!volume)
124
+ return [
125
+ `compute ${name}: no volume attached (attach is create-time only: \`insta services add compute <name> --volume <gi>\`)`,
126
+ ];
127
+ return [
128
+ `compute ${name}: volume ${volume.sizeGib}Gi at ${volume.mountPath} (plan max ${cap.volumeGib}Gi)`,
129
+ ' billing is actual data stored — the size is a cap, not a price; grow with --size (grow-only)',
130
+ ];
131
+ }
132
+ // Show or grow a compute service's /data volume. No --size: a safe read (size + mount path + the
133
+ // plan cap). --size: grow via PUT .../volume — paid and grow-only, but both gates belong to the
134
+ // backend, whose 403/400 messages carry the upgrade hints and must reach the user verbatim (the
135
+ // guard prints ApiError messages as-is).
136
+ export async function computeVolume(serviceName, opts) {
137
+ const api = await ApiClient.load();
138
+ const p = await requireProject();
139
+ const branch = opts.branch ?? p.branch;
140
+ const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
141
+ const id = resolveComputeServiceId(services, serviceName);
142
+ if (!opts.size) {
143
+ const r = await api.request('GET', `/projects/${p.projectId}/services/${id}/volume`);
144
+ if (opts.json)
145
+ return printJson(r);
146
+ for (const line of volumeLines(serviceName ?? id, r.volume, r.cap))
147
+ info(line);
148
+ return;
149
+ }
150
+ const sizeGib = parseVolumeGib(opts.size);
151
+ const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/volume`, { sizeGib });
152
+ if (handleApproval(res))
153
+ return;
154
+ if (opts.json)
155
+ return printJson(res.body);
156
+ const v = res.body.volume;
157
+ info(`compute ${res.body.service?.name ?? serviceName ?? id}: volume grown to ${v.sizeGib}Gi at ${v.mountPath} (plan max ${res.body.cap.volumeGib}Gi)`);
158
+ }
119
159
  // Show or set a compute service's ceiling. With no --memory it PRINTS the current limits and the
120
160
  // plan cap (so `insta compute limits` is a safe read), which is also what a UI renders as a slider
121
161
  // with its plan-limit marker.
@@ -1,5 +1,6 @@
1
1
  import { ApiClient, ApiError, requireProject } from '../api.js';
2
2
  import { info, printJson, handleApproval } from '../util.js';
3
+ import { parseVolumeGib } from './services.js';
3
4
  // Toggle a postgres service between scale-to-zero (the default: instance suspends when idle,
4
5
  // cold-starts on the next connection) and always-on (instance stays warm; idle RAM bills at
5
6
  // actual usage). Thin wrapper over PATCH /database/settings {scaleToZero} — insta-db-backed
@@ -116,4 +117,61 @@ export async function dbLimits(opts) {
116
117
  const mem = typeof res.body?.memoryMib === 'number' ? fmtMib(res.body.memoryMib) : (opts.memory ?? 'unchanged');
117
118
  info(`postgres ${opts.group ?? 'default'}: ceiling set to ${cpu} / ${mem}`);
118
119
  }
120
+ // Render the instance's volume from a database/instance read. Pure, exported for tests. Reads the
121
+ // CANONICAL volume* names only — storageSize/storageGiB are deprecated aliases the platform drops
122
+ // next release, so depending on them here would be a scheduled breakage.
123
+ export function dbVolumeLines(group, body) {
124
+ const gib = typeof body?.volumeGib === 'number' ? `${body.volumeGib}Gi` : (typeof body?.volumeSize === 'string' ? body.volumeSize : undefined);
125
+ if (gib === undefined)
126
+ return [`postgres ${group}: provider reported no volume size`];
127
+ const cap = body?.cap?.volumeGib;
128
+ const region = typeof body?.region === 'string' ? ` ${body.region}` : '';
129
+ return [
130
+ `postgres ${group}: volume ${gib}${typeof cap === 'number' ? ` (plan max ${cap}Gi)` : ''}${region}`,
131
+ ' billing is actual data stored — the size is a cap, not a price; grow with --size (grow-only)',
132
+ ];
133
+ }
134
+ // Show or grow a postgres service's provisioned volume (block disk; insta-db-backed only). Viewing
135
+ // is available on every plan; growth is paid and grow-only — both gates are the backend's to
136
+ // enforce, so nothing here pre-blocks: its 403/400 messages carry the upgrade hints and are wrapped
137
+ // with context but kept verbatim.
138
+ export async function dbVolume(opts) {
139
+ const api = await ApiClient.load();
140
+ const p = await requireProject();
141
+ const qs = new URLSearchParams();
142
+ const branch = opts.branch ?? p.branch;
143
+ if (branch)
144
+ qs.set('branch', branch);
145
+ if (opts.group)
146
+ qs.set('group', opts.group);
147
+ const suffix = qs.toString() ? `?${qs}` : '';
148
+ if (!opts.size) {
149
+ const read = await fetchDbInstance(api, p.projectId, suffix);
150
+ if (read.kind === 'no-instance') {
151
+ info(`postgres ${opts.group ?? 'default'}: no manageable instance (Neon-backed services manage their own storage)`);
152
+ return;
153
+ }
154
+ if (opts.json)
155
+ return printJson(read.body);
156
+ for (const line of dbVolumeLines(opts.group ?? 'default', read.body))
157
+ info(line);
158
+ return;
159
+ }
160
+ const sizeGib = parseVolumeGib(opts.size);
161
+ let res;
162
+ try {
163
+ res = await api.rawRequest('PATCH', `/projects/${p.projectId}/database/settings${suffix}`, { volumeSize: `${sizeGib}Gi` });
164
+ }
165
+ catch (e) {
166
+ if (e instanceof ApiError)
167
+ throw new Error(`growing the volume failed (${e.status}): ${e.message}`);
168
+ throw e;
169
+ }
170
+ if (handleApproval(res))
171
+ return;
172
+ if (opts.json)
173
+ return printJson(res.body);
174
+ const vg = res.body?.volumeGib;
175
+ info(`postgres ${opts.group ?? 'default'}: volume ${typeof vg === 'number' ? `grown to ${vg}Gi` : `set to ${sizeGib}Gi`}`);
176
+ }
119
177
  //# sourceMappingURL=db.js.map
@@ -23,6 +23,19 @@ export function parseCount(raw) {
23
23
  throw new Error(`count must be a positive integer, got: ${raw}`);
24
24
  return n;
25
25
  }
26
+ // Parse a volume size in whole Gi: "10" or "10Gi" (suffix case-insensitive — unlike the db
27
+ // quantity strings this is not a provider pass-through; the wire value is an integer). Volumes
28
+ // are provisioned block disks, so fractional and Mi values are rejected locally with an example
29
+ // rather than travelling to the server as junk (the parseCpu lesson: NaN serializes to null).
30
+ export function parseVolumeGib(raw) {
31
+ const m = /^\s*(\d+)\s*(gi|gib|g)?\s*$/i.exec(raw);
32
+ if (!m)
33
+ throw new Error(`invalid volume size: ${raw} (whole Gi — try 1 or 10)`);
34
+ const n = Number(m[1]);
35
+ if (n < 1)
36
+ throw new Error(`invalid volume size: ${raw} (whole Gi — try 1 or 10)`);
37
+ return n;
38
+ }
26
39
  // Resolve a service id from a `services list` result by (type, name).
27
40
  export function resolveServiceId(services, type, name) {
28
41
  const svc = services.find((s) => s.type === type && s.name === name);
@@ -54,6 +67,7 @@ export function servicesAddRequestBody(type, name, branch, opts) {
54
67
  ...(opts.image ? { image: opts.image } : {}), ...(opts.port ? { port: Number(opts.port) } : {}),
55
68
  ...(opts.region ? { region: opts.region } : {}),
56
69
  ...(opts.alwaysOn ? { alwaysOn: true } : {}),
70
+ ...(opts.volume !== undefined ? { volumeGib: parseVolumeGib(opts.volume) } : {}),
57
71
  };
58
72
  }
59
73
  export async function servicesAdd(type, name, opts = {}) {
@@ -68,6 +82,11 @@ export async function servicesAdd(type, name, opts = {}) {
68
82
  throw new Error('--port is only valid for compute services');
69
83
  if (opts.alwaysOn && type !== 'compute')
70
84
  throw new Error('--always-on is only valid for compute services (for postgres, use `insta db always-on on` after creation)');
85
+ if (opts.volume !== undefined) {
86
+ if (type !== 'compute')
87
+ throw new Error('--volume is only valid for compute services (postgres has one by default — grow it with `insta db volume --size`)');
88
+ parseVolumeGib(opts.volume); // junk fails here, before any config/network access
89
+ }
71
90
  const api = await ApiClient.load();
72
91
  const p = await requireProject();
73
92
  const branch = opts.branch ?? p.branch;
@@ -77,14 +96,15 @@ export async function servicesAdd(type, name, opts = {}) {
77
96
  const svc = res.body.service;
78
97
  const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : '';
79
98
  const img = svc.image ? ` running ${svc.image}${svc.port ? `:${svc.port}` : ''}` : '';
80
- info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${svc.domain ? ` — ${svc.domain}` : ''}`);
99
+ const vol = svc.volume_gib ? ` vol ${svc.volume_gib}Gi at /data` : '';
100
+ info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${vol}${svc.domain ? ` — ${svc.domain}` : ''}`);
81
101
  renderNextActions(res.body.nextActions);
82
102
  }
83
103
  // Render one `services list` row. Pure, so it's unit-tested without a network mock (mirrors
84
104
  // billingLines in billing.ts). Compute rows show the running image when the platform reports one.
85
105
  export function serviceListLine(s) {
86
106
  const extra = s.type === 'compute'
87
- ? ` x${s.machine_count}${s.image ? ` running ${s.image}${s.port ? `:${s.port}` : ''}` : ''}`
107
+ ? ` x${s.machine_count}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}${s.image ? ` running ${s.image}${s.port ? `:${s.port}` : ''}` : ''}`
88
108
  : s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : '';
89
109
  return `${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`;
90
110
  }
@@ -3,7 +3,7 @@
3
3
  // every agent the skills tool knows: the skill is pure product knowledge with brand-gated
4
4
  // triggers — no project state in it (the project binding is carried by ./.insta/project.json
5
5
  // at command time), so one machine-level copy is strictly better than per-project copies.
6
- // Stack skills (neon/tigris/better-auth) intentionally stay per-project: their presence in a
6
+ // Stack skills (tigris/better-auth) intentionally stay per-project: their presence in a
7
7
  // project doubles as its stack manifest — that install happens on `project create|link`.
8
8
  import { spawn } from 'node:child_process';
9
9
  import os from 'node:os';
@@ -1,6 +1,7 @@
1
1
  // Install the related agent skills into a linked project so the developer's coding agent has
2
- // context for what InstaCloud provisions — the `insta` CLI itself plus the three services you build
3
- // directly against (Neon Postgres, Tigris storage, Better Auth). Runs `npx skills add`
2
+ // context for what InstaCloud provisions — the `insta` CLI itself plus the services you build
3
+ // directly against (Tigris storage, Better Auth). Postgres gets no vendor skill: instadb is
4
+ // plain Postgres, and agents talk to it directly via DATABASE_URL. Runs `npx skills add`
4
5
  // (vercel-labs/skills) fully non-interactively. Best-effort: a failure (offline, npx missing, a
5
6
  // repo moved) prints a manual fallback and never blocks or fails the host command — same contract
6
7
  // as the observe-hook install.
@@ -13,8 +14,8 @@ import { DEFAULT_ENV, ENVS } from './env.js';
13
14
  // Codex → .agents/skills/ (.github/skills/ is the third well-known dir). These are regenerable
14
15
  // agent context, not the developer's source — keep them out of git.
15
16
  const SKILL_DIRS = ['.claude/skills/', '.agents/skills/', '.github/skills/'];
16
- // `skills` prints a full-screen ASCII banner at the top of every `add`, so our 4 invocations
17
- // would stack 4 banners. It skips the banner when it detects an agent driving it rather than a
17
+ // `skills` prints a full-screen ASCII banner at the top of every `add`, so our 3 invocations
18
+ // would stack 3 banners. It skips the banner when it detects an agent driving it rather than a
18
19
  // human — AI_AGENT is its first-checked signal (any non-empty value ⇒ agent mode). Setting it is
19
20
  // honest here (this IS programmatic, not an interactive prompt) and, because we already pin the
20
21
  // agents/skills/-y, has no effect on the install beyond quieting the banner. Preserve a caller's
@@ -32,10 +33,9 @@ const defaultRunner = (cmd, args, inherit = false) => new Promise((resolve) => {
32
33
  const AGENT_FLAGS = ['-a', 'claude-code', '-a', 'codex', '-y', '--copy'];
33
34
  // `instaSpec` is the insta skill source for the resolved environment (`owner/repo[@ref]`), so a
34
35
  // project created against staging gets the staging skill text. The third-party stack skills are
35
- // environment-independent — they document Neon/Tigris/Better Auth, not our control plane.
36
+ // environment-independent — they document Tigris/Better Auth, not our control plane.
36
37
  const skillTargets = (instaSpec) => [
37
38
  { label: 'insta', args: ['skills', 'add', instaSpec, '-s', 'insta', ...AGENT_FLAGS] },
38
- { label: 'neon-postgres', args: ['skills', 'add', 'neondatabase/agent-skills', '-s', 'neon-postgres', ...AGENT_FLAGS] },
39
39
  { label: 'tigris', args: ['skills', 'add', 'tigrisdata/skills',
40
40
  '-s', 'tigris-object-operations', '-s', 'file-storage', '-s', 'tigris-sdk-guide',
41
41
  '-s', 'tigris-security-access-control', '-s', 'tigris-image-optimization',
@@ -63,7 +63,7 @@ export async function installSkills(deps) {
63
63
  targets = skillTargets(skills);
64
64
  }
65
65
  catch { /* keep production defaults */ }
66
- print(' installing related agent skills (insta, neon-postgres, tigris, better-auth) …');
66
+ print(' installing related agent skills (insta, tigris, better-auth) …');
67
67
  for (const s of targets) {
68
68
  // Don't stream: the `skills` tool's clack UI (clone spinner, banners) is noise. Run it
69
69
  // silent (stdio 'ignore') and let the per-skill ✓/failed line below be the clean output —
package/dist/index.js CHANGED
@@ -54,11 +54,12 @@ function resolveVersion() {
54
54
  }
55
55
  program.name('insta').description('InstaCloud CLI — manage projects, branches, secrets, deploys').version(resolveVersion());
56
56
  // ---- auth ----
57
- program.command('login').description('Log in with email + password, --oauth <github|google> (browser), or --device (headless)')
57
+ program.command('login').description('Log in with email + password, --oauth <github|google> (browser), --device (headless), or --api-key <insta_…> (headless, durable token)')
58
58
  .option('--email <email>', 'account email')
59
59
  .option('--password <password>', 'account password (else $INSTA_PASSWORD or prompt)')
60
60
  .option('--oauth <provider>', 'browser OAuth login: github | google')
61
61
  .option('--device', 'device-code login: approve from a browser on any other machine (VMs, SSH, CI)')
62
+ .option('--api-key <key>', 'non-interactive login with a durable insta_ API token (headless agents / CI)')
62
63
  .option('--api-url <url>', 'control-plane API base URL')
63
64
  .option('--env <name>', `deployment environment: ${ENV_NAMES.join(' | ')}`)
64
65
  .action(guard((o) => auth.login(o)));
@@ -114,6 +115,7 @@ svc.command('add <type> <name>').description('Provision a service on demand (ass
114
115
  .option('--image <url>', 'compute only: run this container image at creation')
115
116
  .option('--port <n>', 'compute only: port the image listens on (default 8080)')
116
117
  .option('--always-on', 'compute only: create as always-on — never scales to zero (all plans; billing is actual usage either way)')
118
+ .option('--volume <gi>', 'compute only: attach a persistent /data volume of this many whole Gi (create-time only; any plan may attach at the default 1; larger sizes are paid and plan-capped). Volume services keep 1 machine and stop (cold wake) instead of suspend when idle')
117
119
  .action(guard((type, name, o) => services.servicesAdd(type, name, o)));
118
120
  svc.command('list').option('--json').option('--branch <branch>', 'branch (default: current)')
119
121
  .action(guard((o) => services.servicesList(o)));
@@ -169,8 +171,11 @@ compute.command('limits [service]').description("Show or set a compute service's
169
171
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeLimits(service, o)));
170
172
  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')
171
173
  .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((mode, service, o) => computeCmd.computeAlwaysOn(mode, service, o)));
174
+ compute.command('volume [service]').description("Show or grow a compute service's persistent /data volume. No --size: print size, mount path, and the plan cap (any plan). --size grows it (paid plans; grow-only — a provisioned disk cannot shrink). Attach is create-time only: `insta services add compute <name> --volume <gi>`. Billing is actual data stored — the size is a cap, not a price")
175
+ .option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)')
176
+ .option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeVolume(service, o)));
172
177
  // ---- db (postgres service controls) ----
173
- const db = program.command('db').description('Postgres service controls (limits / always-on / scale-to-zero)');
178
+ const db = program.command('db').description('Postgres service controls (limits / volume / always-on / scale-to-zero)');
174
179
  db.command('limits').description("Show or set a postgres service's resource ceiling (paid plans; insta-db-backed only). Moves both directions")
175
180
  .option('--cpu <n>', "vCPU ceiling, e.g. 2 or 2500m").option('--memory <size>', "memory ceiling, e.g. 4Gi")
176
181
  .option('--json').option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
@@ -178,6 +183,10 @@ db.command('limits').description("Show or set a postgres service's resource ceil
178
183
  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')
179
184
  .option('--json').option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
180
185
  .action(guard((mode, o) => dbCmd.dbAlwaysOn(mode, o)));
186
+ db.command('volume').description("Show or grow a postgres service's provisioned volume (block disk; insta-db-backed only). No --size: print size and the plan cap (any plan). --size grows it (paid plans; grow-only — a provisioned disk cannot shrink). Billing is actual data stored — the size is a cap, not a price")
187
+ .option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)')
188
+ .option('--json').option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
189
+ .action(guard((o) => dbCmd.dbVolume(o)));
181
190
  // ---- manifest ----
182
191
  program.command('manifest').description('Print an agent-legible view of the project environments').option('--json').action(guard((o) => manifest(o)));
183
192
  // ---- regions ----
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.26",
3
+ "version": "0.0.28",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [