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
2 changes: 1 addition & 1 deletion src/commands/compute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,7 @@ export async function computeAlwaysOn(mode: string, serviceName: string | undefi
if (handleApproval(res, opts.json)) return
if (opts.json) return printJson(res.body)
const on = res.body.service?.always_on
info(`compute ${res.body.service?.name ?? id}: always-on ${on ? 'ENABLED — machines stay warm (no cold starts; idle RAM bills at actual usage)' : 'disabled — scales to zero when idle (default)'}`)
info(`compute ${res.body.service?.name ?? id}: always-on ${on ? 'ENABLED — machines stay warm (no cold starts; idle RAM bills at actual usage)' : 'disabled — scales to zero when idle'}`)
}

// ---- limits (the resource ceiling; paid plans) ----
Expand Down
8 changes: 6 additions & 2 deletions src/commands/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ export function servicesAddRequestBody(type: string, name: string, branch: strin
type, name, ...(branch ? { branch } : {}), public: !!opts.public,
...(opts.image ? { image: opts.image } : {}), ...(opts.port ? { port: parsePort(opts.port) } : {}),
...(opts.region ? { region: opts.region } : {}),
...(opts.alwaysOn ? { alwaysOn: true } : {}),
// Sent whenever the flag was given, false included: compute is born always-on by default
// (insta-platform #385, 2026-09-07), so `--no-always-on` must reach the API as an explicit
// false. Omitted means the platform default.
...(opts.alwaysOn !== undefined ? { alwaysOn: opts.alwaysOn } : {}),
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
...(opts.volume !== undefined ? { volumeGib: parseVolumeGib(opts.volume) } : {}),
}
}
Expand All @@ -109,7 +112,8 @@ export async function servicesAdd(type: string, name: string, opts: ServicesAddO
if (type !== 'compute') throw new Error('--port is only valid for compute services')
parsePort(opts.port) // junk fails here, before any config/network access
}
if (opts.alwaysOn && type !== 'compute') throw new Error('--always-on is only valid for compute services (for postgres, use `insta db always-on on` after creation)')
// Presence, not truthiness: `--no-always-on` is an explicit false and is just as compute-only.
if (opts.alwaysOn !== undefined && type !== 'compute') throw new Error('--always-on / --no-always-on is only valid for compute services (for postgres, use `insta db always-on on|off` after creation)')
if (opts.volume !== undefined) {
if (type !== 'compute') throw new Error('--volume is only valid for compute services (postgres has one by default — grow it with `insta db volume --size`)')
parseVolumeGib(opts.volume) // junk fails here, before any config/network access
Expand Down
5 changes: 3 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ svc.command('add [type] [name]').description('Provision a service on demand (ass
.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)')
.option('--always-on', 'compute only: create as always-on — never scales to zero (all plans; billing is actual usage either way)')
.option('--always-on', 'compute only: create as always-on — never scales to zero (the default for new compute services; all plans; billing is actual usage either way)')
.option('--no-always-on', 'compute only: create as scale-to-zero — idle machines suspend and wake on the next request')
.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 10 (the free cap, on every plan); 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) => {
Expand Down Expand Up @@ -236,7 +237,7 @@ compute.command('status [service]').description("Show a compute service's desire
compute.command('limits [service]').description("Show or set a compute service's resource ceiling (any plan within the free cap; raising above it needs a paid plan). --memory is the dial; cpu derives from it unless --cpu is given. Billing is actual usage — the ceiling caps what the app may burn, it is not a price")
.option('--memory <size>', 'memory ceiling, e.g. 512mb or 1gb').option('--cpu <n>', 'vCPU ceiling override (provider sizes: 1, 2, 4, 6, 8)')
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeLimits(service, o)))
compute.command('always-on <mode> [service]').description('Set a compute service always-on (mode: on|off). on = machines never scale to zero; off = default scale-to-zero. All plans; billing is actual usage either way')
compute.command('always-on <mode> [service]').description('Set a compute service always-on (mode: on|off). on = machines never scale to zero (the default for new compute services); off = scale-to-zero. All plans; billing is actual usage either way')
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((mode, service, o) => computeCmd.computeAlwaysOn(mode, service, o)))
const execCmd = compute.command('exec [service]').description("Run a one-shot command inside a compute service's machine (`insta compute exec [service] -- <command> [args…]`) — no interactive shell/PTY: `command` is argv, no shell is invoked (use [\"sh\", \"-c\", \"...\"] for shell features). Wakes the machine first if it's scaled to zero — expect a few seconds of latency, billed as uptime, not an error. Exits with the remote command's own exit code (agents rely on this)")
.action(guard((service, o) => computeCmd.computeExec(service, execCommand, o, { windowsFallback: execWindowsFallback })))
Expand Down
13 changes: 11 additions & 2 deletions test/services.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,8 +136,12 @@ describe('servicesAddRequestBody', () => {
expect(servicesAddRequestBody('postgres', 'db', 'main', { region: 'us-east' })).toMatchObject({ region: 'us-east' })
expect(servicesAddRequestBody('postgres', 'db', 'main', {})).not.toHaveProperty('region')
})
it('sends alwaysOn only when the flag is set (absent means the scale-to-zero default)', () => {
it('sends alwaysOn in BOTH states when the flag is given, omits it when absent (absent = the platform default, always-on for compute)', () => {
expect(servicesAddRequestBody('compute', 'api', 'main', { alwaysOn: true })).toMatchObject({ alwaysOn: true })
// --no-always-on must reach the API as an explicit false: an omitted key is reinterpreted as
// the always-on default by the platform (insta-platform #385), so dropping false would create
// exactly the always-on, idle-billed service the user opted out of.
expect(servicesAddRequestBody('compute', 'api', 'main', { alwaysOn: false })).toMatchObject({ alwaysOn: false })
expect(servicesAddRequestBody('compute', 'api', 'main', {})).not.toHaveProperty('alwaysOn')
})
})
Expand All @@ -150,7 +154,12 @@ describe('servicesAdd validation (throws before any network/config access)', ()
await expect(servicesAdd('postgres', 'db', { port: '3000' })).rejects.toThrow(/--port is only valid for compute services/)
})
it('rejects --always-on for a non-compute type, pointing at the db command instead', async () => {
await expect(servicesAdd('postgres', 'db', { alwaysOn: true })).rejects.toThrow(/--always-on is only valid for compute services/)
await expect(servicesAdd('postgres', 'db', { alwaysOn: true })).rejects.toThrow(/--always-on \/ --no-always-on is only valid for compute services/)
})
it('rejects --no-always-on for a non-compute type too: an explicit false is just as compute-only', async () => {
// Presence check, not truthiness: a truthiness check would let `postgres db --no-always-on`
// reach the platform with alwaysOn:false instead of failing before config/network access.
await expect(servicesAdd('postgres', 'db', { alwaysOn: false })).rejects.toThrow(/--always-on \/ --no-always-on is only valid for compute services/)
})
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/)
Expand Down
Loading