Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions src/commands/services.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// `insta services` — manage a project's opt-in services (postgres | storage | compute).
// `insta services` — manage a project's opt-in services (postgres | storage | compute | redis).
import { ApiClient, requireProject } from '../api.js'
import { info, printJson, handleApproval, renderNextActions } from '../util.js'

export const SERVICE_TYPES = ['postgres', 'storage', 'compute'] as const
export const SERVICE_TYPES = ['postgres', 'storage', 'compute', 'redis'] as const
export type ServiceType = (typeof SERVICE_TYPES)[number]
const SERVICE_NAME_RE = /^[a-z0-9][a-z0-9-]{0,38}$/

Expand Down Expand Up @@ -125,7 +125,8 @@ export async function servicesAdd(type: string, name: string, opts: ServicesAddO
export function serviceListLine(s: { type: string; name: string; status: string; id: string; domain?: string; machine_count?: number; public?: boolean; image?: string; port?: number; volume_gib?: number | null }): string {
const extra = s.type === 'compute'
? ` x${s.machine_count}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}${s.image ? ` running ${s.image}${s.port ? `:${s.port}` : ''}` : ''}`
: s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : ''
: s.type === 'redis' ? ` tcp/${s.port ?? 6379}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}`
: s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : ''
return `${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`
}

Expand All @@ -135,7 +136,7 @@ export async function servicesList(opts: { json?: boolean; branch?: string }): P
const branch = opts.branch ?? p.branch
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`)
if (opts.json) return printJson(services)
if (!services.length) return info(`(no services on ${branch ?? 'default'} — add one with \`insta services add <postgres|storage|compute> <name>\`)`)
if (!services.length) return info(`(no services on ${branch ?? 'default'} — add one with \`insta services add <postgres|storage|compute|redis> <name>\`)`)
for (const s of services) info(serviceListLine(s))
}

Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ br.command('delete <name>').action(guard((name) => branch.branchDelete(name)))
br.command('merge <source>').description('Merge a branch service set into another (structural, no data)')
.option('--into <branch>', 'target branch (default: current)').action(guard((source, o) => branch.branchMerge(source, o)))

// ---- services (opt-in postgres/storage/compute) ----
const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute)')
// ---- services (opt-in postgres/storage/compute/redis) ----
const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute|redis)')
// [type] [name] are optional so the command can answer "what can I add?" — a terminal is walked
// through the dashboard's Add Service kinds, anything else gets that list back as an error
// (resolve-service.ts). Picking Docker Image also fills in --image/--port from the answers.
svc.command('add [type] [name]').description('Provision a service on demand (assigns a default domain for postgres/compute); with no type/name, a terminal picks from the service kinds')
.option('--branch <branch>', 'target branch (default: current)')
.option('--region <region>', 'region for postgres/compute, e.g. us-east (see `insta regions`)')
.option('--region <region>', 'region for postgres/compute/redis, e.g. us-east (see `insta regions`)')
.option('--public', 'storage only: serve the bucket with anonymous public-read (default private)')
.option('--image <url>', 'compute only: run this container image at creation')
.option('--port <n>', 'compute only: port the image listens on (default 8080)')
Expand Down
3 changes: 2 additions & 1 deletion src/resolve-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// `insta services add` with no type (or no name): the kinds are otherwise only discoverable by
// guessing wrong and reading `type must be postgres|storage|compute`, so missing arguments answer
// guessing wrong and reading `type must be postgres|storage|compute|redis`, so missing arguments answer
// "what can I add?" instead. The list mirrors the dashboard's Add Service menu (frontend
// `add-service-button.tsx`) — Docker Image sits BESIDE Empty Service, not under it, because
// picking an image is a different intent rather than a compute flag. An agent gets the same list
Expand All @@ -22,6 +22,7 @@ export type ServiceKind = {
export const SERVICE_KINDS: readonly ServiceKind[] = [
{ id: 'image', label: 'Docker Image', type: 'compute', hint: 'run an existing container image', needsImage: true },
{ id: 'postgres', label: 'Postgres', type: 'postgres', hint: 'relational DB, usable as soon as it is added', defaultName: 'main-db' },
{ id: 'redis', label: 'Redis', type: 'redis', hint: 'private Redis-compatible cache', defaultName: 'cache' },
{ id: 'storage', label: 'Storage', type: 'storage', hint: 'S3-compatible bucket, private by default', defaultName: 'assets' },
{ id: 'compute', label: 'Empty Service', type: 'compute', hint: 'an app to deploy code to (empty until `insta deploy`)', defaultName: 'compute' },
]
Expand Down
5 changes: 4 additions & 1 deletion test/resolve-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ test('every service type is reachable from some kind', () => {

// The dashboard's Add Service lists Docker Image beside Empty Service, not under it.
test('Docker Image is its own kind, at the same level as Empty Service', () => {
expect(SERVICE_KINDS.map((k) => k.label)).toEqual(['Docker Image', 'Postgres', 'Storage', 'Empty Service'])
expect(SERVICE_KINDS.map((k) => k.label)).toEqual(['Docker Image', 'Postgres', 'Redis', 'Storage', 'Empty Service'])
expect(kind('image').needsImage).toBe(true)
expect(kind('image').type).toBe('compute')
expect(kind('compute').needsImage).toBeUndefined()
Expand All @@ -44,6 +44,7 @@ test('Docker Image is its own kind, at the same level as Empty Service', () => {
// Default names are the dashboard dialog's placeholders — they must not drift apart.
test('default names match the Add Service placeholders', () => {
expect(kind('postgres').defaultName).toBe('main-db')
expect(kind('redis').defaultName).toBe('cache')
expect(kind('storage').defaultName).toBe('assets')
expect(kind('compute').defaultName).toBe('compute')
expect(kind('image').defaultName).toBeUndefined()
Expand Down Expand Up @@ -95,6 +96,7 @@ test('no TTY: throws, and the message lists every kind with its command', async
const msg = missingArgsMessage()
for (const k of SERVICE_KINDS) expect(msg).toContain(k.label)
expect(msg).toContain('insta services add postgres main-db')
expect(msg).toContain('insta services add redis cache')
expect(msg).toContain('--image <ref>')
})

Expand All @@ -111,6 +113,7 @@ test('kind lines stay one per kind and carry a runnable command', () => {
const lines = serviceKindLines()
expect(lines).toHaveLength(SERVICE_KINDS.length)
expect(lines.join('\n')).toContain('insta services add storage assets')
expect(lines.join('\n')).toContain('insta services add redis cache')
})

// Same rules as the dashboard's helpers, so a ref names the service identically in both.
Expand Down
Loading