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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "insta",
"version": "0.0.56",
"version": "0.0.57",
"type": "module",
"description": "InstaCloud CLI — a thin client of the platform control-plane API.",
"keywords": [
Expand Down
9 changes: 7 additions & 2 deletions src/commands/manifest.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ApiClient, requireProject } from '../api.js'
import { pgBadge } from './services.js'
import { info, printJson } from '../util.js'

// A resource row as the platform's project-detail read returns it.
Expand All @@ -10,7 +11,9 @@ export type ManifestResource = {
name?: string | null
branchId?: string | null
status?: string
ref?: { url?: string; bucket?: string; neonProjectId?: string } | null
// pgVersion: the Postgres MAJOR a database row runs (root and branch rows alike); absent on
// older platforms and on legacy rows that never recorded one.
ref?: { url?: string; bucket?: string; neonProjectId?: string; pgVersion?: number } | null
}

/**
Expand All @@ -35,7 +38,9 @@ export function resourceLabel(r: ManifestResource): string {
// One `manifest` resource line. Pure, so the label is unit-tested without a network mock.
export function resourceLine(r: ManifestResource): string {
const where = r.ref?.url ?? r.ref?.bucket ?? r.ref?.neonProjectId ?? ''
return ` - ${resourceLabel(r)} ${where} [${r.status}]`
// Database rows only: the platform stamps ref.pgVersion on insta-db (and legacy neon) resources.
const pg = r.kind === 'insta-db' || r.kind === 'neon' ? pgBadge(r.ref?.pgVersion) : ''
return ` - ${resourceLabel(r)} ${where}${pg} [${r.status}]`
}

// Agent-legible view of each environment's databases / storage / compute.
Expand Down
30 changes: 24 additions & 6 deletions src/commands/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,7 @@ export async function servicesAdd(type: string, name: string, opts: ServicesAddO
if (handleApproval(res, opts.json)) return
if (opts.json) return printJson(res.body.service)
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}` : ''}` : ''
const vol = svc.volume_gib ? ` vol ${svc.volume_gib}Gi at /data` : ''
info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${vol}${svc.domain ? ` — ${svc.domain}` : ''}`)
info(serviceAddedLine(type, name, branch, svc))
// Discoverability: the DB is directly dialable, but its DSN is deliberately absent from the
// general `insta secrets` bundle — without this line nothing in the product says how to reach it.
if (type === 'postgres') {
Expand All @@ -136,13 +133,34 @@ export async function servicesAdd(type: string, name: string, opts: ServicesAddO
renderNextActions(res.body.nextActions)
}

// The `services add` success line. Pure, so the badge's placement is unit-tested: a template string
// nothing asserts on silently loses a segment.
export function serviceAddedLine(type: string, name: string, branch: string | undefined, svc: { id: string; type: string; public?: boolean; image?: string; port?: number; volume_gib?: number | null; region?: string; domain?: string; pg_version?: number | null }): string {
const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : ''
const img = svc.image ? ` running ${svc.image}${svc.port ? `:${svc.port}` : ''}` : ''
const vol = svc.volume_gib ? ` vol ${svc.volume_gib}Gi at /data` : ''
// The major belongs next to the connect hint: it decides which psql/pg_dump to reach for.
const pg = svc.type === 'postgres' ? pgBadge(svc.pg_version) : ''
return `added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${vol}${pg}${svc.domain ? ` — ${svc.domain}` : ''}`
}

// ` pg <major>` for a postgres row, or '' when the platform sent nothing usable. The field arrives
// as untyped API JSON: only a positive integer renders, so a malformed or nonsense value (true,
// '16', 16.4, 0, -1) can never print as a version an agent would pick tooling by.
export function pgBadge(v: unknown): string {
return typeof v === 'number' && Number.isInteger(v) && v > 0 ? ` pg ${v}` : ''
}

// Render one `services list` row. Pure, so it's unit-tested without a network mock (mirrors
// billingLines in billing.ts). Compute rows show the running image when the platform reports one.
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 {
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; pg_version?: 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}` : ''}` : ''}`
: ['redis', 'mysql', 'mongodb'].includes(s.type) ? ` tcp/${s.port ?? defaultDatabasePort(s.type)}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}`
: s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : ''
: s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}`
// Postgres major, so the reader picks matching pg_dump/psql BEFORE connecting (a newer client
// dumps statements an older server cannot restore). Older platforms send no pg_version.
: s.type === 'postgres' ? pgBadge(s.pg_version) : ''
return `${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`
}

Expand Down
12 changes: 12 additions & 0 deletions test/manifest-label.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,18 @@ describe('resourceLabel', () => {
})

describe('resourceLine', () => {
it('names the Postgres major on a database row when the platform sends one', () => {
expect(resourceLine({ kind: 'insta-db', name: 'db', status: 'active', ref: { pgVersion: 16 } })).toBe(' - insta-db(db) pg 16 [active]')
expect(resourceLine({ kind: 'insta-db', name: 'db', status: 'active', ref: {} })).toBe(' - insta-db(db) [active]')
})
it('never badges a non-database row, whatever its ref carries', () => {
expect(resourceLine({ kind: 's3', name: 'assets', status: 'active', ref: { bucket: 'b', pgVersion: 16 } })).toBe(' - s3(assets) b [active]')
})
it('omits the badge when ref.pgVersion is not a positive integer (untyped API JSON)', () => {
for (const bad of [true, '16', 16.4, NaN, 0, -1] as unknown[]) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
expect(resourceLine({ kind: 'insta-db', name: 'db', status: 'active', ref: { pgVersion: bad as number } })).toBe(' - insta-db(db) [active]')
}
})
it('renders the microvm row exactly as staging should have shown it', () => {
expect(resourceLine({
kind: 'fly', provider: 'microvm', name: 'api', status: 'active',
Expand Down
28 changes: 27 additions & 1 deletion test/services.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest'
import {
assertType, assertServiceName, parseCount, parsePort, parseAccess, resolveServiceId, resolveComputeServiceId, SERVICE_TYPES,
servicesAddRequestBody, servicesAdd, serviceListLine,
servicesAddRequestBody, servicesAdd, serviceListLine, serviceAddedLine,
} from '../src/commands/services.js'

describe('assertType', () => {
Expand Down Expand Up @@ -163,7 +163,33 @@ describe('servicesAdd validation (throws before any network/config access)', ()
})
})

describe('serviceAddedLine', () => {
it('carries the Postgres major next to the connect hint for a postgres service', () => {
expect(serviceAddedLine('postgres', 'db', 'main', { id: 'svc_pg', type: 'postgres', region: 'us-east', domain: 'db.example.test', pg_version: 16 }))
.toBe('added postgres service db on main (svc_pg) us-east pg 16 — db.example.test')
})
it('never badges a non-postgres service, and omits the badge when the platform sent no major', () => {
expect(serviceAddedLine('storage', 'assets', undefined, { id: 'svc_s3', type: 'storage', public: false, pg_version: 16 }))
.toBe('added storage service assets on default (svc_s3) [private]')
expect(serviceAddedLine('postgres', 'db', 'main', { id: 'svc_pg', type: 'postgres', pg_version: null })).not.toContain('pg ')
})
})

describe('serviceListLine', () => {
it('shows the Postgres major on a postgres row, so the reader picks matching client tooling', () => {
const line = serviceListLine({ type: 'postgres', name: 'db', status: 'active', id: 'svc_pg', domain: 'db.example.test', pg_version: 16 })
expect(line).toContain('pg 16')
expect(line).toContain('db.example.test')
})
it('omits the badge when the platform sent no pg_version (older platform, legacy row)', () => {
expect(serviceListLine({ type: 'postgres', name: 'db', status: 'active', id: 'svc_pg', pg_version: null })).not.toContain('pg ')
expect(serviceListLine({ type: 'storage', name: 'assets', status: 'active', id: 'svc_s3', pg_version: 16 })).not.toContain('pg 16')
})
it('omits the badge when pg_version is not a positive integer (the API JSON is untyped on the wire)', () => {
for (const bad of [true, '16', 16.4, NaN, {}, 0, -1] as unknown[]) {
expect(serviceListLine({ type: 'postgres', name: 'db', status: 'active', id: 'svc_pg', pg_version: bad as number })).not.toContain('pg ')
}
})
it('renders a compute row with the running image when present', () => {
const line = serviceListLine({ type: 'compute', name: 'api', status: 'active', id: 'svc_1', machine_count: 1, image: 'ghcr.io/acme/api:latest', port: 8080 })
expect(line).toBe('compute/api [active] x1 running ghcr.io/acme/api:latest:8080 svc_1')
Expand Down
Loading