Skip to content

Commit 26b590f

Browse files
authored
Merge pull request #55 from InsForge/feat/branch-scoped-services
Branch-scoped services: --branch flags + insta branch merge
2 parents 9ad59d7 + 8798a45 commit 26b590f

4 files changed

Lines changed: 58 additions & 26 deletions

File tree

src/commands/branch.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,21 @@ export async function branchDelete(name: string): Promise<void> {
3737
if (handleApproval(res)) return
3838
info(`deleted branch ${name}`)
3939
}
40+
41+
// insta branch merge <source> [--into <target>] — structurally merge source's services into target
42+
// (default target: current branch). No data is copied; existing services are left untouched.
43+
export async function branchMerge(source: string, opts: { into?: string } = {}): Promise<void> {
44+
const api = await ApiClient.load()
45+
const p = await requireProject()
46+
const target = opts.into ?? p.branch
47+
if (!target) throw new Error('no target branch — pass --into <branch> (or link a branch first)')
48+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/branches/${encodeURIComponent(target)}/merge`, { from: source })
49+
if (handleApproval(res)) return
50+
const { created = [], skipped = [] } = (res.body ?? {}) as {
51+
created?: Array<{ type: string; name: string }>
52+
skipped?: Array<{ type: string; name: string; reason: string }>
53+
}
54+
info(`merged ${source}${target}: ${created.length} created, ${skipped.length} skipped`)
55+
for (const c of created) info(` + ${c.type}/${c.name}`)
56+
for (const s of skipped) info(` = ${s.type}/${s.name} (${s.reason})`)
57+
}

src/commands/compute.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApiClient, requireProject } from '../api.js'
22
import { info, printJson, handleApproval } from '../util.js'
3-
import { resolveComputeServiceId } from './services.js'
3+
import { resolveComputeServiceId, q } from './services.js'
44

55
type Opts = { branch?: string; group?: string; json?: boolean }
66

@@ -45,12 +45,13 @@ function printDomain(r: any, json?: boolean): void {
4545

4646
// ---- lifecycle (start/stop/suspend/status) ----
4747

48-
type LifeOpts = { json?: boolean }
48+
type LifeOpts = { json?: boolean; branch?: string }
4949

5050
async function lifecycle(verb: 'start' | 'stop' | 'suspend', serviceName: string | undefined, opts: LifeOpts): Promise<void> {
5151
const api = await ApiClient.load()
5252
const p = await requireProject()
53-
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
53+
const branch = opts.branch ?? p.branch
54+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`)
5455
const id = resolveComputeServiceId(services, serviceName)
5556
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/${verb}`)
5657
if (handleApproval(res)) return
@@ -65,7 +66,8 @@ export const computeSuspend = (service: string | undefined, opts: LifeOpts) => l
6566
export async function computeStatus(serviceName: string | undefined, opts: LifeOpts): Promise<void> {
6667
const api = await ApiClient.load()
6768
const p = await requireProject()
68-
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
69+
const branch = opts.branch ?? p.branch
70+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`)
6971
const id = resolveComputeServiceId(services, serviceName)
7072
const r = await api.request('GET', `/projects/${p.projectId}/services/${id}/state`)
7173
if (opts.json) return printJson(r)

src/commands/services.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { info, printJson, handleApproval, renderNextActions } from '../util.js'
55
export const SERVICE_TYPES = ['postgres', 'storage', 'compute'] as const
66
export type ServiceType = (typeof SERVICE_TYPES)[number]
77

8+
export function q(branch?: string): string {
9+
return branch ? `?branch=${encodeURIComponent(branch)}` : ''
10+
}
11+
812
// ---- pure, unit-tested helpers (throw plain Errors; the CLI guard turns them into clean output) ----
913

1014
// Validate a service-type argument against the allowed set for a command.
@@ -41,40 +45,43 @@ export function resolveComputeServiceId(services: Array<{ id: string; type: stri
4145

4246
// ---- commands ----
4347

44-
export async function servicesAdd(type: string, name: string, opts: { public?: boolean } = {}): Promise<void> {
48+
export async function servicesAdd(type: string, name: string, opts: { branch?: string; public?: boolean } = {}): Promise<void> {
4549
assertType(type)
4650
if (opts.public && type !== 'storage') throw new Error('--public is only valid for storage services')
4751
const api = await ApiClient.load()
4852
const p = await requireProject()
49-
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, { type, name, public: !!opts.public })
53+
const branch = opts.branch ?? p.branch
54+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services`, { type, name, ...(branch ? { branch } : {}), public: !!opts.public })
5055
if (handleApproval(res)) return
5156
const svc = res.body.service
5257
const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : ''
53-
info(`added ${type} service ${name} (${svc.id})${access}${svc.domain ? ` — ${svc.domain}` : ''}`)
58+
info(`added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.domain ? ` — ${svc.domain}` : ''}`)
5459
renderNextActions(res.body.nextActions)
5560
}
5661

57-
export async function servicesList(opts: { json?: boolean }): Promise<void> {
62+
export async function servicesList(opts: { json?: boolean; branch?: string }): Promise<void> {
5863
const api = await ApiClient.load()
5964
const p = await requireProject()
60-
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
65+
const branch = opts.branch ?? p.branch
66+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`)
6167
if (opts.json) return printJson(services)
62-
if (!services.length) return info('(no services — add one with `insta services add <postgres|storage|compute> <name>`)')
68+
if (!services.length) return info(`(no services on ${branch ?? 'default'} — add one with \`insta services add <postgres|storage|compute> <name>\`)`)
6369
for (const s of services) {
6470
const extra = s.type === 'compute' ? ` x${s.machine_count}` : s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}` : ''
6571
info(`${s.type}/${s.name} [${s.status}]${extra}${s.domain ? ` ${s.domain}` : ''} ${s.id}`)
6672
}
6773
}
6874

69-
export async function servicesRemove(type: string, name: string): Promise<void> {
75+
export async function servicesRemove(type: string, name: string, opts: { branch?: string } = {}): Promise<void> {
7076
assertType(type)
7177
const api = await ApiClient.load()
7278
const p = await requireProject()
73-
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
79+
const branch = opts.branch ?? p.branch
80+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`)
7481
const id = resolveServiceId(services, type, name)
7582
const res = await api.rawRequest('DELETE', `/projects/${p.projectId}/services/${id}`)
7683
if (handleApproval(res)) return
77-
info(`removed ${type} service ${name}`)
84+
info(`removed ${type} service ${name} from ${branch ?? 'default'}`)
7885
}
7986

8087
// Validate a bucket access-mode argument.
@@ -90,7 +97,7 @@ export async function servicesSetAccess(type: string, name: string, access: stri
9097
const isPublic = parseAccess(access)
9198
const api = await ApiClient.load()
9299
const p = await requireProject()
93-
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
100+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(p.branch)}`)
94101
const id = resolveServiceId(services, type, name)
95102
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/access`, { public: isPublic })
96103
if (handleApproval(res)) return
@@ -99,12 +106,12 @@ export async function servicesSetAccess(type: string, name: string, access: stri
99106
}
100107

101108
// insta services scale compute <name> <number> [region]
102-
export async function servicesScale(type: string, name: string, number: string, region: string | undefined, _opts: { json?: boolean }): Promise<void> {
109+
export async function servicesScale(type: string, name: string, number: string, region: string | undefined, _opts: { json?: boolean; branch?: string }): Promise<void> {
103110
assertType(type, ['compute'])
104111
const machineCount = parseCount(number)
105112
const api = await ApiClient.load()
106113
const p = await requireProject()
107-
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
114+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(_opts.branch ?? p.branch)}`)
108115
const id = resolveServiceId(services, type, name)
109116
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/scale`, { machineCount, region })
110117
if (handleApproval(res)) return
@@ -113,11 +120,11 @@ export async function servicesScale(type: string, name: string, number: string,
113120
}
114121

115122
// insta services upgrade <compute|postgres> <name> <new-spec>
116-
export async function servicesUpgrade(type: string, name: string, spec: string, _opts: { json?: boolean }): Promise<void> {
123+
export async function servicesUpgrade(type: string, name: string, spec: string, _opts: { json?: boolean; branch?: string }): Promise<void> {
117124
assertType(type, ['compute', 'postgres'])
118125
const api = await ApiClient.load()
119126
const p = await requireProject()
120-
const { services } = await api.request('GET', `/projects/${p.projectId}/services`)
127+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(_opts.branch ?? p.branch)}`)
121128
const id = resolveServiceId(services, type, name)
122129
const res = await api.rawRequest('POST', `/projects/${p.projectId}/services/${id}/upgrade`, { spec })
123130
if (handleApproval(res)) return

src/index.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,26 @@ br.command('create <name>').option('--from <branch>', 'parent branch (default: c
8888
br.command('list').option('--json').action(guard((o) => branch.branchList(o)))
8989
br.command('switch <name>').action(guard((name) => branch.branchSwitch(name)))
9090
br.command('delete <name>').action(guard((name) => branch.branchDelete(name)))
91+
br.command('merge <source>').description('Merge a branch service set into another (structural, no data)')
92+
.option('--into <branch>', 'target branch (default: current)').action(guard((source, o) => branch.branchMerge(source, o)))
9193

9294
// ---- services (opt-in postgres/storage/compute) ----
9395
const svc = program.command('services').alias('svc').description('Manage project services (postgres|storage|compute)')
9496
svc.command('add <type> <name>').description('Provision a service on demand (assigns a default domain for postgres/compute)')
97+
.option('--branch <branch>', 'target branch (default: current)')
9598
.option('--public', 'storage only: serve the bucket with anonymous public-read (default private)')
9699
.action(guard((type, name, o) => services.servicesAdd(type, name, o)))
97-
svc.command('list').option('--json').action(guard((o) => services.servicesList(o)))
100+
svc.command('list').option('--json').option('--branch <branch>', 'branch (default: current)')
101+
.action(guard((o) => services.servicesList(o)))
98102
svc.command('remove <type> <name>').description('Remove a service and destroy its resources')
99-
.action(guard((type, name) => services.servicesRemove(type, name)))
103+
.option('--branch <branch>', 'branch (default: current)')
104+
.action(guard((type, name, o) => services.servicesRemove(type, name, o)))
100105
svc.command('set-access <type> <name> <access>').description('Set a storage service bucket access mode (access: public|private)')
101106
.option('--json').action(guard((type, name, access, o) => services.servicesSetAccess(type, name, access, o)))
102107
svc.command('scale <type> <name> <number> [region]').description('Set a compute service machine count (paid plans only)')
103-
.option('--json').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)))
108+
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, number, region, o) => services.servicesScale(type, name, number, region, o)))
104109
svc.command('upgrade <type> <name> <spec>').description('Change a compute/postgres service spec (paid plans only)')
105-
.option('--json').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)))
110+
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((type, name, spec, o) => services.servicesUpgrade(type, name, spec, o)))
106111

107112
// ---- secrets (seam) ----
108113
const sec = program.command('secrets').description('Fetch the credential bundle (secret seam) into .env')
@@ -129,13 +134,13 @@ compute.command('check-domain <host>').description("Show a custom domain's cert
129134
compute.command('remove-domain <host>').description('Detach a custom domain (gated: deploy)')
130135
.option('--branch <b>').option('--group <g>').action(guard((host, o) => computeCmd.removeDomain(host, o)))
131136
compute.command('start [service]').description('Bring a compute service online (persistent — re-enables auto-wake)')
132-
.option('--json').action(guard((service, o) => computeCmd.computeStart(service, o)))
137+
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStart(service, o)))
133138
compute.command('stop [service]').description('Take a compute service offline; traffic will NOT wake it until `start`')
134-
.option('--json').action(guard((service, o) => computeCmd.computeStop(service, o)))
139+
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStop(service, o)))
135140
compute.command('suspend [service]').description('Suspend a compute service (RAM snapshot); stays down until `start`')
136-
.option('--json').action(guard((service, o) => computeCmd.computeSuspend(service, o)))
141+
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeSuspend(service, o)))
137142
compute.command('status [service]').description("Show a compute service's desired vs. live state")
138-
.option('--json').action(guard((service, o) => computeCmd.computeStatus(service, o)))
143+
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStatus(service, o)))
139144

140145
// ---- manifest ----
141146
program.command('manifest').description('Print an agent-legible view of the project environments').option('--json').action(guard((o) => manifest(o)))

0 commit comments

Comments
 (0)