From 02aee9f783dc30f12b85fdbffa5d7ba2a2e8ee23 Mon Sep 17 00:00:00 2001 From: jwfing Date: Mon, 20 Jul 2026 15:43:15 -0700 Subject: [PATCH 1/2] feat(cli): services add --region + insta regions command Co-Authored-By: Claude Opus 4.8 (1M context) --- src/commands/regions.ts | 10 ++++++++++ src/commands/services.ts | 14 ++++++++++---- src/index.ts | 5 +++++ test/services-add-body.test.ts | 16 ++++++++++++++++ 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 src/commands/regions.ts create mode 100644 test/services-add-body.test.ts diff --git a/src/commands/regions.ts b/src/commands/regions.ts new file mode 100644 index 0000000..35030c8 --- /dev/null +++ b/src/commands/regions.ts @@ -0,0 +1,10 @@ +import { ApiClient } from '../api.js' +import { info, printJson } from '../util.js' + +// insta regions — list the regions a postgres/compute service can be created in. +export async function regionsList(opts: { json?: boolean } = {}): Promise { + const api = await ApiClient.load() + const { regions } = await api.request('GET', '/regions') + if (opts.json) return printJson(regions) + for (const r of regions) info(`${r.slug} ${r.label}`) +} diff --git a/src/commands/services.ts b/src/commands/services.ts index 8b31a2e..eaf4503 100644 --- a/src/commands/services.ts +++ b/src/commands/services.ts @@ -45,17 +45,23 @@ export function resolveComputeServiceId(services: Array<{ id: string; type: stri // ---- commands ---- -export async function servicesAdd(type: string, name: string, opts: { branch?: string; public?: boolean } = {}): Promise { - assertType(type) +// Pure: build the POST body for `services add`, enforcing client-side guards (mirrors the platform). +export function buildAddServiceBody(type: string, name: string, opts: { branch?: string; public?: boolean; region?: string }): Record { if (opts.public && type !== 'storage') throw new Error('--public is only valid for storage services') + if (opts.region && type === 'storage') throw new Error('--region is not valid for storage services') + return { type, name, ...(opts.branch ? { branch: opts.branch } : {}), ...(opts.region ? { region: opts.region } : {}), public: !!opts.public } +} + +export async function servicesAdd(type: string, name: string, opts: { branch?: string; public?: boolean; region?: string } = {}): Promise { + assertType(type) const api = await ApiClient.load() const p = await requireProject() const branch = opts.branch ?? p.branch - const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, { type, name, ...(branch ? { branch } : {}), public: !!opts.public }) + const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, buildAddServiceBody(type, name, { ...opts, branch })) if (handleApproval(res)) return const svc = res.body.service const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : '' - info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.domain ? ` — ${svc.domain}` : ''}`) + info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${svc.domain ? ` — ${svc.domain}` : ''}`) renderNextActions(res.body.nextActions) } diff --git a/src/index.ts b/src/index.ts index fdff722..fb7a95d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ import * as org from './commands/org.js' import * as project from './commands/project.js' import * as branch from './commands/branch.js' import * as services from './commands/services.js' +import * as regions from './commands/regions.js' import * as secretsCmd from './commands/secrets.js' import { deploy } from './commands/deploy.js' import * as computeCmd from './commands/compute.js' @@ -104,6 +105,7 @@ br.command('merge ').description('Merge a branch service set into anothe const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute)') svc.command('add ').description('Provision a service on demand (assigns a default domain for postgres/compute)') .option('--branch ', 'target branch (default: current)') + .option('--region ', 'region for postgres/compute, e.g. us-east (see `insta regions`)') .option('--public', 'storage only: serve the bucket with anonymous public-read (default private)') .action(guard((type, name, o) => services.servicesAdd(type, name, o))) svc.command('list').option('--json').option('--branch ', 'branch (default: current)') @@ -159,6 +161,9 @@ compute.command('status [service]').description("Show a compute service's desire // ---- manifest ---- program.command('manifest').description('Print an agent-legible view of the project environments').option('--json').action(guard((o) => manifest(o))) +// ---- regions ---- +program.command('regions').description('List regions available for postgres/compute services').option('--json').action(guard((o) => regions.regionsList(o))) + // ---- observability ---- program.command('metrics [group]').description('Service metrics (target: db|compute)') .option('--branch ').option('--from ').option('--to ').option('--step ').option('--json') diff --git a/test/services-add-body.test.ts b/test/services-add-body.test.ts new file mode 100644 index 0000000..c8ab1d3 --- /dev/null +++ b/test/services-add-body.test.ts @@ -0,0 +1,16 @@ +import { test, expect } from 'vitest' +import { buildAddServiceBody } from '../src/commands/services.js' + +test('includes region for postgres/compute', () => { + expect(buildAddServiceBody('postgres', 'db', { region: 'us-east' })).toEqual({ type: 'postgres', name: 'db', region: 'us-east', public: false }) + expect(buildAddServiceBody('compute', 'api', { region: 'eu-central', branch: 'feat' })).toEqual({ type: 'compute', name: 'api', branch: 'feat', region: 'eu-central', public: false }) +}) + +test('omits region when not provided', () => { + expect(buildAddServiceBody('postgres', 'db', {})).toEqual({ type: 'postgres', name: 'db', public: false }) +}) + +test('rejects region on storage, and --public on non-storage', () => { + expect(() => buildAddServiceBody('storage', 'files', { region: 'us-east' })).toThrow(/--region is not valid for storage/) + expect(() => buildAddServiceBody('postgres', 'db', { public: true })).toThrow(/--public is only valid for storage/) +}) From 59362f4812c4389a6dc6bb9066dcb6b274dc64a5 Mon Sep 17 00:00:00 2001 From: jwfing Date: Mon, 20 Jul 2026 15:49:47 -0700 Subject: [PATCH 2/2] fix(cli): validate services-add flags before network/project resolution Co-Authored-By: Claude Opus 4.8 (1M context) --- src/commands/services.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/services.ts b/src/commands/services.ts index eaf4503..75b04c0 100644 --- a/src/commands/services.ts +++ b/src/commands/services.ts @@ -54,10 +54,11 @@ export function buildAddServiceBody(type: string, name: string, opts: { branch?: export async function servicesAdd(type: string, name: string, opts: { branch?: string; public?: boolean; region?: string } = {}): Promise { assertType(type) + const base = buildAddServiceBody(type, name, opts) // validate client-side BEFORE any I/O const api = await ApiClient.load() const p = await requireProject() const branch = opts.branch ?? p.branch - const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, buildAddServiceBody(type, name, { ...opts, branch })) + const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, { ...base, ...(branch ? { branch } : {}) }) if (handleApproval(res)) return const svc = res.body.service const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : ''