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
3 changes: 2 additions & 1 deletion src/commands/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function resolveComputeServiceId(services: Array<{ id: string; type: stri

// ---- commands ----

export type ServicesAddOpts = { branch?: string; public?: boolean; image?: string; port?: string; region?: string; alwaysOn?: boolean; volume?: string }
export type ServicesAddOpts = { branch?: string; public?: boolean; image?: string; port?: string; region?: string; alwaysOn?: boolean; volume?: string; json?: boolean }

// 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
Expand Down Expand Up @@ -93,6 +93,7 @@ export async function servicesAdd(type: string, name: string, opts: ServicesAddO
const branch = opts.branch ?? p.branch
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, servicesAddRequestBody(type, name, branch, opts))
if (handleApproval(res)) 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}` : ''}` : ''
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,9 @@ svc.command('add [type] [name]').description('Provision a service on demand (ass
.option('--port <n>', 'compute only: port the image listens on (default 8080)')
.option('--always-on', 'compute only: create as always-on — never scales to zero (all plans; billing is actual usage either way)')
.option('--volume <gi>', 'compute only: attach a persistent /data volume of this many whole Gi (also attachable later: `insta compute volume <name> --size <gi>`; 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')
.option('--json')
.action(guard(async (type, name, o) => {
const a = await resolveServiceArgs(type, name, serviceArgsDeps())
const a = await resolveServiceArgs(type, name, serviceArgsDeps(o.json))
return services.servicesAdd(a.type, a.name, o)
}))
svc.command('list').option('--json').option('--branch <branch>', 'branch (default: current)')
Expand Down
5 changes: 3 additions & 2 deletions src/resolve-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,11 @@ export async function promptServiceName(kind: ServiceKind): Promise<string> {
}

/** Prompts on a real terminal only — an agent's stdin is not one, and must never block. */
export function serviceArgsDeps(): ServiceArgsDeps {
export function serviceArgsDeps(json?: boolean): ServiceArgsDeps {
return {
selectType: promptServiceType,
askName: promptServiceName,
tty: !!process.stdin.isTTY && !!process.stdout.isTTY,
// --json asked for parseable output, so a caller that happens to own a TTY still gets the error.
tty: !json && !!process.stdin.isTTY && !!process.stdout.isTTY,
}
}
14 changes: 14 additions & 0 deletions test/resolve-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
SERVICE_KINDS,
missingArgsMessage,
resolveServiceArgs,
serviceArgsDeps,
serviceKindLines,
type ServiceArgsDeps,
} from '../src/resolve-service.js'
Expand Down Expand Up @@ -60,6 +61,19 @@ test('unknown type: passed through for assertType to report, prompts untouched',
expect(r).toEqual({ type: 'mysql', name: '' })
})

// --json promises parseable stdout; a prompt would corrupt it and hang an agent that owns a TTY.
test('--json opts out of the prompts even on a terminal', () => {
const io = [process.stdin, process.stdout] as Array<{ isTTY?: boolean }>
const saved = io.map((s) => s.isTTY)
for (const s of io) s.isTTY = true
try {
expect(serviceArgsDeps().tty).toBe(true)
expect(serviceArgsDeps(true).tty).toBe(false)
} finally {
io.forEach((s, i) => { s.isTTY = saved[i] })
}
})

test('kind lines stay one per type and mention what each is', () => {
const lines = serviceKindLines()
expect(lines).toHaveLength(SERVICE_TYPES.length)
Expand Down
Loading