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 e31802f..4a32bd2 100644 --- a/src/commands/services.ts +++ b/src/commands/services.ts @@ -45,7 +45,7 @@ export function resolveComputeServiceId(services: Array<{ id: string; type: stri // ---- commands ---- -export type ServicesAddOpts = { branch?: string; public?: boolean; image?: string; port?: string } +export type ServicesAddOpts = { branch?: string; public?: boolean; image?: string; port?: string; region?: string } // Map service-add options to the platform POST body. Pure, so it's unit-tested without a network // mock (mirrors deployRequestBody in deploy.ts). Validation (which options are valid for which @@ -54,12 +54,14 @@ export function servicesAddRequestBody(type: string, name: string, branch: strin return { type, name, ...(branch ? { branch } : {}), public: !!opts.public, ...(opts.image ? { image: opts.image } : {}), ...(opts.port ? { port: Number(opts.port) } : {}), + ...(opts.region ? { region: opts.region } : {}), } } export async function servicesAdd(type: string, name: string, opts: ServicesAddOpts = {}): Promise { assertType(type) 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') if (opts.image && type !== 'compute') throw new Error('--image is only valid for compute services') if (opts.port && type !== 'compute') throw new Error('--port is only valid for compute services') const api = await ApiClient.load() @@ -70,7 +72,7 @@ export async function servicesAdd(type: string, name: string, opts: ServicesAddO const svc = res.body.service const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : '' const img = svc.image ? ` running ${svc.image}${svc.port ? `:${svc.port}` : ''}` : '' - info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${img}${svc.domain ? ` — ${svc.domain}` : ''}`) + info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${svc.domain ? ` — ${svc.domain}` : ''}`) renderNextActions(res.body.nextActions) } diff --git a/src/index.ts b/src/index.ts index 4c19bff..b090722 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)') .option('--image ', 'compute only: run this container image at creation') .option('--port ', 'compute only: port the image listens on (default 8080)') @@ -161,6 +163,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.test.ts b/test/services.test.ts index 9b035ca..8f8b34c 100644 --- a/test/services.test.ts +++ b/test/services.test.ts @@ -95,6 +95,10 @@ describe('servicesAddRequestBody', () => { it('carries --public through unchanged', () => { expect(servicesAddRequestBody('storage', 'bkt', 'main', { public: true })).toMatchObject({ public: true }) }) + it('sends region when passed, omits it when absent', () => { + expect(servicesAddRequestBody('postgres', 'db', 'main', { region: 'us-east' })).toMatchObject({ region: 'us-east' }) + expect(servicesAddRequestBody('postgres', 'db', 'main', {})).not.toHaveProperty('region') + }) }) describe('servicesAdd validation (throws before any network/config access)', () => { @@ -107,6 +111,9 @@ describe('servicesAdd validation (throws before any network/config access)', () it('rejects --public for a non-storage type', async () => { await expect(servicesAdd('compute', 'api', { public: true })).rejects.toThrow(/--public is only valid for storage services/) }) + it('rejects --region for a storage type', async () => { + await expect(servicesAdd('storage', 'bkt', { region: 'us-east' })).rejects.toThrow(/--region is not valid for storage services/) + }) it('rejects an unknown service type before any option checks', async () => { await expect(servicesAdd('lambda', 'x', {})).rejects.toThrow(/postgres\|storage\|compute/) })