insta 0.0.24 → 0.0.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/auth.js +78 -2
- package/dist/commands/compute.js +59 -0
- package/dist/commands/db.js +93 -1
- package/dist/index.js +10 -2
- package/package.json +1 -1
package/dist/commands/auth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createServer } from 'node:http';
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
|
-
import { ApiClient, linkedProject } from '../api.js';
|
|
3
|
+
import { ApiClient, ApiError, linkedProject } from '../api.js';
|
|
4
4
|
import { ENVS, ENV_NAMES, envForApiUrl, isEnvName } from '../env.js';
|
|
5
5
|
import { info, die, printJson, promptPassword, openUrl } from '../util.js';
|
|
6
6
|
/** --api-url and --env both set the target host; --api-url wins (more specific), matching the
|
|
@@ -17,6 +17,8 @@ function targetApiUrl(opts) {
|
|
|
17
17
|
return ENVS[want].api;
|
|
18
18
|
}
|
|
19
19
|
export async function login(opts) {
|
|
20
|
+
if (opts.device)
|
|
21
|
+
return loginDevice(opts);
|
|
20
22
|
if (opts.oauth)
|
|
21
23
|
return loginOauth(opts.oauth, opts);
|
|
22
24
|
const api = await ApiClient.load();
|
|
@@ -24,7 +26,7 @@ export async function login(opts) {
|
|
|
24
26
|
if (target)
|
|
25
27
|
api.setApiUrl(target);
|
|
26
28
|
if (!opts.email)
|
|
27
|
-
die('--email is required (or use --oauth <github|google
|
|
29
|
+
die('--email is required (or use --oauth <github|google>; on a headless machine, --device)');
|
|
28
30
|
const password = opts.password ?? process.env.INSTA_PASSWORD ?? (await promptPassword());
|
|
29
31
|
const res = await api.request('POST', '/auth/login', { email: opts.email, password }, { auth: false });
|
|
30
32
|
api.setSession(res, res.user);
|
|
@@ -47,6 +49,80 @@ export async function loginOauth(provider, opts) {
|
|
|
47
49
|
await api.persist();
|
|
48
50
|
info(`logged in as ${me.user.email ?? me.user.id} @ ${api.apiUrl}`);
|
|
49
51
|
}
|
|
52
|
+
// RFC 8628 device authorization — login from a machine with no usable browser (VM, SSH box, CI
|
|
53
|
+
// container). The loopback --oauth flow can never work there: its callback targets 127.0.0.1 on
|
|
54
|
+
// THIS machine. Here the roles invert — we mint a code, print a link the human opens on ANY
|
|
55
|
+
// device, and poll the platform until they approve in the console.
|
|
56
|
+
export async function loginDevice(opts) {
|
|
57
|
+
const api = await ApiClient.load();
|
|
58
|
+
const target = targetApiUrl(opts);
|
|
59
|
+
if (target)
|
|
60
|
+
api.setApiUrl(target);
|
|
61
|
+
const token = await deviceGrant((path, body) => api.request('POST', path, body, { auth: false }));
|
|
62
|
+
api.setSession({ accessToken: token, refreshToken: token });
|
|
63
|
+
const me = await api.request('GET', '/me');
|
|
64
|
+
api.setSession({ accessToken: token, refreshToken: token }, me.user);
|
|
65
|
+
await api.persist();
|
|
66
|
+
info(`logged in as ${me.user.email ?? me.user.id} @ ${api.apiUrl}`);
|
|
67
|
+
}
|
|
68
|
+
const sleepSeconds = (s) => new Promise((r) => setTimeout(r, s * 1000));
|
|
69
|
+
// Drives the device grant against the platform's Better Auth mount (/api/auth/device*) and
|
|
70
|
+
// returns the approved session token. Injectable poster + wait keep this testable without a
|
|
71
|
+
// network or real timers. Poll errors arrive as ApiError with the OAuth error code as message.
|
|
72
|
+
export async function deviceGrant(post, wait = sleepSeconds) {
|
|
73
|
+
const start = (await post('/api/auth/device/code', { client_id: 'insta-cli' }));
|
|
74
|
+
// A missing/garbage expires_in must fail loudly here — carried into the deadline arithmetic it
|
|
75
|
+
// becomes NaN, every `Date.now() < deadline` is false, and login dies as a bogus instant expiry.
|
|
76
|
+
// Cap the lifetime too: a huge-but-finite value (Number.MAX_VALUE) overflows the ms conversion
|
|
77
|
+
// to Infinity and would otherwise pin the CLI polling forever.
|
|
78
|
+
const expiresIn = Number(start.expires_in);
|
|
79
|
+
if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
80
|
+
throw new Error('malformed device authorization response (missing expires_in) — is the platform up to date?');
|
|
81
|
+
}
|
|
82
|
+
const lifetime = Math.min(expiresIn, 3600); // no device code sensibly outlives an hour
|
|
83
|
+
info('to log in, open this link in a browser on any device:');
|
|
84
|
+
info(` ${start.verification_uri_complete ?? start.verification_uri}`);
|
|
85
|
+
info(`and check it shows this code: ${start.user_code}`);
|
|
86
|
+
info(`waiting for approval… (expires in ${Math.round(lifetime / 60)}m, ctrl-c to abort)`);
|
|
87
|
+
// Absent OR non-finite interval = the RFC 8628 §3.2 default 5s: NaN would fire the timer
|
|
88
|
+
// instantly and Infinity gets truncated to ~1ms by Node — both hot-poll the token endpoint.
|
|
89
|
+
const rawInterval = Number(start.interval);
|
|
90
|
+
let interval = Number.isFinite(rawInterval) ? Math.max(rawInterval, 1) : 5;
|
|
91
|
+
const deadline = Date.now() + lifetime * 1000;
|
|
92
|
+
while (Date.now() < deadline) {
|
|
93
|
+
await wait(interval);
|
|
94
|
+
let grant = null;
|
|
95
|
+
try {
|
|
96
|
+
grant = (await post('/api/auth/device/token', {
|
|
97
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
98
|
+
device_code: start.device_code,
|
|
99
|
+
client_id: 'insta-cli',
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
if (!(e instanceof ApiError))
|
|
104
|
+
continue; // transport blip (dropped SSH/CI link) — keep polling until deadline
|
|
105
|
+
const code = e.message;
|
|
106
|
+
if (code === 'authorization_pending')
|
|
107
|
+
continue;
|
|
108
|
+
if (code === 'slow_down') {
|
|
109
|
+
interval += 5;
|
|
110
|
+
continue;
|
|
111
|
+
} // RFC 8628 §3.5: back off by 5s
|
|
112
|
+
if (code === 'expired_token')
|
|
113
|
+
break;
|
|
114
|
+
if (code === 'access_denied')
|
|
115
|
+
throw new Error('login request was denied in the console');
|
|
116
|
+
throw e; // a definite API-level error (invalid_grant, …) — not retryable
|
|
117
|
+
}
|
|
118
|
+
// Validated OUTSIDE the try: a 200 without a token is a malformed response that must fail
|
|
119
|
+
// loudly, not be mistaken for a transport blip and retried into an empty stored session.
|
|
120
|
+
if (!grant?.access_token)
|
|
121
|
+
throw new Error('malformed token response (missing access_token)');
|
|
122
|
+
return grant.access_token;
|
|
123
|
+
}
|
|
124
|
+
throw new Error('device login expired before it was approved — run `insta login --device` again');
|
|
125
|
+
}
|
|
50
126
|
// Start a loopback server, open the browser at the platform bridge, and await the token.
|
|
51
127
|
function browserOauth(apiUrl, provider) {
|
|
52
128
|
return new Promise((resolve, reject) => {
|
package/dist/commands/compute.js
CHANGED
|
@@ -87,4 +87,63 @@ export async function computeAlwaysOn(mode, serviceName, opts) {
|
|
|
87
87
|
const on = res.body.service?.always_on;
|
|
88
88
|
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)'}`);
|
|
89
89
|
}
|
|
90
|
+
// ---- limits (the resource ceiling; paid plans) ----
|
|
91
|
+
// Parse a human memory value into MB: "512", "512mb", "1gb", "2g", "1.5gb".
|
|
92
|
+
// Exported for unit tests — this is the only place a user-typed size becomes a number.
|
|
93
|
+
export function parseMemoryMb(raw) {
|
|
94
|
+
const m = /^\s*(\d+(?:\.\d+)?)\s*(g|gb|gi|gib|m|mb|mi|mib)?\s*$/i.exec(raw);
|
|
95
|
+
if (!m)
|
|
96
|
+
throw new Error(`invalid memory: ${raw} (try 512mb, 1gb, 2gb)`);
|
|
97
|
+
const n = Number(m[1]);
|
|
98
|
+
const unit = (m[2] ?? 'mb').toLowerCase();
|
|
99
|
+
const mb = unit.startsWith('g') ? n * 1024 : n;
|
|
100
|
+
if (!(mb > 0))
|
|
101
|
+
throw new Error(`invalid memory: ${raw}`);
|
|
102
|
+
return Math.round(mb);
|
|
103
|
+
}
|
|
104
|
+
// Whole and half GB collapse (1536 → "1.5 GB"); anything else stays exact in MB — a display that
|
|
105
|
+
// rounds 1536 to "2 GB" claims a ceiling the API did not set.
|
|
106
|
+
export const fmtMb = (mb) => (mb >= 1024 && mb % 512 === 0 ? `${mb / 1024} GB` : `${mb} MB`);
|
|
107
|
+
// The --cpu override, through a throwing parser like every other user-typed number in this repo
|
|
108
|
+
// (parseCount, parseMemoryMb). A bare Number() turns a typo into NaN, which JSON.stringify
|
|
109
|
+
// serializes as null — the server then sees {cpu: null} instead of the user seeing an error.
|
|
110
|
+
// Enforces the provider grid the help text advertises: the server would reject 100 anyway, but a
|
|
111
|
+
// value the client KNOWS is invalid should fail locally, matching what --help promises.
|
|
112
|
+
const CPU_SIZES = [1, 2, 4, 6, 8];
|
|
113
|
+
export function parseCpu(raw) {
|
|
114
|
+
const n = Number(raw);
|
|
115
|
+
if (!CPU_SIZES.includes(n))
|
|
116
|
+
throw new Error(`invalid cpu: ${raw} (provider sizes: ${CPU_SIZES.join(', ')})`);
|
|
117
|
+
return n;
|
|
118
|
+
}
|
|
119
|
+
// Show or set a compute service's ceiling. With no --memory it PRINTS the current limits and the
|
|
120
|
+
// plan cap (so `insta compute limits` is a safe read), which is also what a UI renders as a slider
|
|
121
|
+
// with its plan-limit marker.
|
|
122
|
+
export async function computeLimits(serviceName, opts) {
|
|
123
|
+
const api = await ApiClient.load();
|
|
124
|
+
const p = await requireProject();
|
|
125
|
+
const branch = opts.branch ?? p.branch;
|
|
126
|
+
const { services } = await api.request('GET', `/projects/${p.projectId}/services${q(branch)}`);
|
|
127
|
+
const id = resolveComputeServiceId(services, serviceName);
|
|
128
|
+
if (!opts.memory && !opts.cpu) {
|
|
129
|
+
const r = await api.request('GET', `/projects/${p.projectId}/services/${id}/limits`);
|
|
130
|
+
if (opts.json)
|
|
131
|
+
return printJson(r);
|
|
132
|
+
info(`compute ${serviceName ?? id}: ceiling ${r.limits.cpu} vCPU / ${fmtMb(r.limits.memoryMb)} (plan max ${r.cap.cpu} vCPU / ${fmtMb(r.cap.memoryMb)})`);
|
|
133
|
+
info(' billing is actual usage — the ceiling caps what the app may burn, it is not a price');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!opts.memory)
|
|
137
|
+
throw new Error('--memory is required when setting limits (cpu is derived from it; pass --cpu only to override)');
|
|
138
|
+
const body = { memoryMb: parseMemoryMb(opts.memory) };
|
|
139
|
+
if (opts.cpu)
|
|
140
|
+
body.cpu = parseCpu(opts.cpu);
|
|
141
|
+
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/limits`, body);
|
|
142
|
+
if (handleApproval(res))
|
|
143
|
+
return;
|
|
144
|
+
if (opts.json)
|
|
145
|
+
return printJson(res.body);
|
|
146
|
+
const l = res.body.limits;
|
|
147
|
+
info(`compute ${res.body.service?.name ?? id}: ceiling set to ${l.cpu} vCPU / ${fmtMb(l.memoryMb)}`);
|
|
148
|
+
}
|
|
90
149
|
//# sourceMappingURL=compute.js.map
|
package/dist/commands/db.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ApiClient, requireProject } from '../api.js';
|
|
1
|
+
import { ApiClient, ApiError, requireProject } from '../api.js';
|
|
2
2
|
import { info, printJson, handleApproval } from '../util.js';
|
|
3
3
|
// Toggle a postgres service between scale-to-zero (the default: instance suspends when idle,
|
|
4
4
|
// cold-starts on the next connection) and always-on (instance stays warm; idle RAM bills at
|
|
@@ -24,4 +24,96 @@ export async function dbAlwaysOn(mode, opts) {
|
|
|
24
24
|
const s2z = res.body?.scaleToZero;
|
|
25
25
|
info(`postgres ${opts.group ?? 'default'}: always-on ${s2z === false ? 'ENABLED — instance stays warm (no cold starts; idle RAM bills at actual usage)' : 'disabled — scales to zero when idle (default; first connection after idle cold-starts)'}`);
|
|
26
26
|
}
|
|
27
|
+
// Validated pass-throughs for the provider's quantity strings. The insta-db resize API takes
|
|
28
|
+
// k8s-style quantities (cpu: "2", "2500m"; memory: "4Gi", "2048Mi"), so unlike the compute path
|
|
29
|
+
// there is no unit conversion here — but junk must still fail LOCALLY with an example, not travel
|
|
30
|
+
// to the server as-is. CASE-EXACT deliberately: k8s quantities are case-sensitive ("4gi" is
|
|
31
|
+
// rejected server-side), and local validation that accepts a form the backend refuses would
|
|
32
|
+
// defeat its own purpose.
|
|
33
|
+
export function parseDbCpu(raw) {
|
|
34
|
+
if (!/^\d+(\.\d+)?m?$/.test(raw.trim()))
|
|
35
|
+
throw new Error(`invalid cpu: ${raw} (try 2, 4, or 2500m)`);
|
|
36
|
+
return raw.trim();
|
|
37
|
+
}
|
|
38
|
+
export function parseDbMemory(raw) {
|
|
39
|
+
if (!/^\d+(\.\d+)?(Gi|Mi|G|M)$/.test(raw.trim()))
|
|
40
|
+
throw new Error(`invalid memory: ${raw} (try 4Gi or 8Gi)`);
|
|
41
|
+
return raw.trim();
|
|
42
|
+
}
|
|
43
|
+
// MiB → display without lying: whole/half GiB collapse, anything else stays exact in MiB
|
|
44
|
+
// (1536 MiB is "1.5 GiB", 1300 MiB is "1300 MiB" — never "1 GiB").
|
|
45
|
+
export function fmtMib(mib) {
|
|
46
|
+
return mib >= 1024 && mib % 512 === 0 ? `${mib / 1024} GiB` : `${mib} MiB`;
|
|
47
|
+
}
|
|
48
|
+
export async function fetchDbInstance(api, projectId, suffix) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await api.rawRequest('GET', `/projects/${projectId}/database/instance${suffix}`);
|
|
51
|
+
return { kind: 'ok', body: res.body };
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
// The platform answers a provider-shaped 502 for services with no manageable instance
|
|
55
|
+
// (Neon-backed): a soft case, not a failure. Everything else stays an error — an expired
|
|
56
|
+
// token must not render as "no ceiling set" — but wrapped so the user sees what failed.
|
|
57
|
+
if (e instanceof ApiError && e.status === 502)
|
|
58
|
+
return { kind: 'no-instance' };
|
|
59
|
+
if (e instanceof ApiError)
|
|
60
|
+
throw new Error(`reading the instance failed (${e.status}): ${e.message}`);
|
|
61
|
+
throw e;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Show or set a postgres service's resource ceiling (insta-db-backed only). Paid plans — the
|
|
65
|
+
// ceiling is the tier lever now that billing follows actual usage. Moves both directions:
|
|
66
|
+
// unlike storage it is a cgroup limit, not a provisioned volume.
|
|
67
|
+
export async function dbLimits(opts) {
|
|
68
|
+
const api = await ApiClient.load();
|
|
69
|
+
const p = await requireProject();
|
|
70
|
+
const qs = new URLSearchParams();
|
|
71
|
+
const branch = opts.branch ?? p.branch;
|
|
72
|
+
if (branch)
|
|
73
|
+
qs.set('branch', branch);
|
|
74
|
+
if (opts.group)
|
|
75
|
+
qs.set('group', opts.group);
|
|
76
|
+
const suffix = qs.toString() ? `?${qs}` : '';
|
|
77
|
+
if (!opts.cpu && !opts.memory) {
|
|
78
|
+
const read = await fetchDbInstance(api, p.projectId, suffix);
|
|
79
|
+
if (read.kind === 'no-instance') {
|
|
80
|
+
info(`postgres ${opts.group ?? 'default'}: no manageable instance (Neon-backed services manage their own resources)`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (opts.json)
|
|
84
|
+
return printJson(read.body);
|
|
85
|
+
const cpuMilli = read.body?.cpuMilli;
|
|
86
|
+
const mib = read.body?.memoryMib;
|
|
87
|
+
if (typeof cpuMilli === 'number' && typeof mib === 'number') {
|
|
88
|
+
const cpu = cpuMilli % 1000 === 0 ? `${cpuMilli / 1000}` : `${cpuMilli}m`;
|
|
89
|
+
info(`postgres ${opts.group ?? 'default'}: ceiling ${cpu} vCPU / ${fmtMib(mib)}`);
|
|
90
|
+
info(' billing is actual usage — the ceiling caps what the database may burn, it is not a price');
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
info(`postgres ${opts.group ?? 'default'}: provider reported no ceiling — set one with --cpu/--memory`);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const body = {};
|
|
98
|
+
if (opts.cpu)
|
|
99
|
+
body.cpu = parseDbCpu(opts.cpu);
|
|
100
|
+
if (opts.memory)
|
|
101
|
+
body.memory = parseDbMemory(opts.memory);
|
|
102
|
+
let res;
|
|
103
|
+
try {
|
|
104
|
+
res = await api.rawRequest('PATCH', `/projects/${p.projectId}/database/settings${suffix}`, body);
|
|
105
|
+
}
|
|
106
|
+
catch (e) {
|
|
107
|
+
if (e instanceof ApiError)
|
|
108
|
+
throw new Error(`setting the ceiling failed (${e.status}): ${e.message}`);
|
|
109
|
+
throw e;
|
|
110
|
+
}
|
|
111
|
+
if (handleApproval(res))
|
|
112
|
+
return;
|
|
113
|
+
if (opts.json)
|
|
114
|
+
return printJson(res.body);
|
|
115
|
+
const cpu = typeof res.body?.cpuMilli === 'number' ? `${res.body.cpuMilli / 1000} vCPU` : (opts.cpu ?? 'unchanged');
|
|
116
|
+
const mem = typeof res.body?.memoryMib === 'number' ? fmtMib(res.body.memoryMib) : (opts.memory ?? 'unchanged');
|
|
117
|
+
info(`postgres ${opts.group ?? 'default'}: ceiling set to ${cpu} / ${mem}`);
|
|
118
|
+
}
|
|
27
119
|
//# sourceMappingURL=db.js.map
|
package/dist/index.js
CHANGED
|
@@ -54,10 +54,11 @@ function resolveVersion() {
|
|
|
54
54
|
}
|
|
55
55
|
program.name('insta').description('InstaCloud CLI — manage projects, branches, secrets, deploys').version(resolveVersion());
|
|
56
56
|
// ---- auth ----
|
|
57
|
-
program.command('login').description('Log in with email + password,
|
|
57
|
+
program.command('login').description('Log in with email + password, --oauth <github|google> (browser), or --device (headless)')
|
|
58
58
|
.option('--email <email>', 'account email')
|
|
59
59
|
.option('--password <password>', 'account password (else $INSTA_PASSWORD or prompt)')
|
|
60
60
|
.option('--oauth <provider>', 'browser OAuth login: github | google')
|
|
61
|
+
.option('--device', 'device-code login: approve from a browser on any other machine (VMs, SSH, CI)')
|
|
61
62
|
.option('--api-url <url>', 'control-plane API base URL')
|
|
62
63
|
.option('--env <name>', `deployment environment: ${ENV_NAMES.join(' | ')}`)
|
|
63
64
|
.action(guard((o) => auth.login(o)));
|
|
@@ -163,10 +164,17 @@ compute.command('suspend [service]').description('Suspend a compute service (RAM
|
|
|
163
164
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeSuspend(service, o)));
|
|
164
165
|
compute.command('status [service]').description("Show a compute service's desired vs. live state")
|
|
165
166
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeStatus(service, o)));
|
|
167
|
+
compute.command('limits [service]').description("Show or set a compute service's resource ceiling (paid plans). --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")
|
|
168
|
+
.option('--memory <size>', 'memory ceiling, e.g. 512mb or 1gb').option('--cpu <n>', 'vCPU ceiling override (provider sizes: 1, 2, 4, 6, 8)')
|
|
169
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeLimits(service, o)));
|
|
166
170
|
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')
|
|
167
171
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((mode, service, o) => computeCmd.computeAlwaysOn(mode, service, o)));
|
|
168
172
|
// ---- db (postgres service controls) ----
|
|
169
|
-
const db = program.command('db').description('Postgres service controls (always-on / scale-to-zero)');
|
|
173
|
+
const db = program.command('db').description('Postgres service controls (limits / always-on / scale-to-zero)');
|
|
174
|
+
db.command('limits').description("Show or set a postgres service's resource ceiling (paid plans; insta-db-backed only). Moves both directions")
|
|
175
|
+
.option('--cpu <n>', "vCPU ceiling, e.g. 2 or 2500m").option('--memory <size>', "memory ceiling, e.g. 4Gi")
|
|
176
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
|
|
177
|
+
.action(guard((o) => dbCmd.dbLimits(o)));
|
|
170
178
|
db.command('always-on <mode>').description('Set a postgres service always-on (mode: on|off). on = instance stays warm, no cold starts; off = default scale-to-zero (idle instance suspends; first connection cold-starts). insta-db-backed services only')
|
|
171
179
|
.option('--json').option('--branch <branch>', 'branch (default: current)').option('--group <g>', 'postgres service name (default: the sole/default one)')
|
|
172
180
|
.action(guard((mode, o) => dbCmd.dbAlwaysOn(mode, o)));
|