insta 0.0.79 → 0.0.81

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/README.md CHANGED
@@ -226,6 +226,7 @@ build never reaches a production installer.
226
226
  | `insta db` | `url` (print the postgres DSN) · `connect` (psql session) · `limits` · `stats` · `always-on` · `volume` |
227
227
  | `insta regions` | Regions available for postgres and compute |
228
228
  | `insta manifest` | Agent-legible view of every branch and its URLs |
229
+ | `insta build-logs <id>` | Read source-build output; `--source archive` (default) uses the deploy operation ID, `--source github` uses a GitHub build ID; `--follow` watches output, `--json` returns one snapshot |
229
230
  | `insta metrics` · `logs` · `events` | Service metrics; runtime logs (`--deploy` for deploy events); audit timeline |
230
231
  | `insta usage` · `billing` | Usage by billing dimension; `billing upgrade` · `billing portal` |
231
232
  | `insta approvals` | `list` · `approve` · `deny` |
@@ -0,0 +1,206 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { stripVTControlCharacters } from 'node:util';
3
+ import { ApiError } from './api.js';
4
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
5
+ const terminal = new Set(['succeeded', 'failed', 'canceled', 'unknown', 'live']);
6
+ const entryKey = (step, entry) => createHash('sha256').update(JSON.stringify([step, entry.timestamp, entry.message])).digest('hex');
7
+ class LogsNotReady extends Error {
8
+ }
9
+ export async function readBuildLogs(api, projectId, source, buildId, signal = AbortSignal.timeout(30_000), follow) {
10
+ let bytes = 0;
11
+ let requests = 0;
12
+ let pending = false;
13
+ async function pages(step) {
14
+ const result = [];
15
+ const seen = new Set();
16
+ const tail = step ? follow?.tails.get(step) ?? { counts: new Map() } : undefined;
17
+ let cursor = tail?.cursor;
18
+ do {
19
+ if (++requests > 200)
20
+ throw new Error('build logs exceed the per-read page limit');
21
+ const params = new URLSearchParams({ ...(step ? { step } : {}), ...(cursor ? { cursor } : {}) });
22
+ const { body } = await api.rawRequest('GET', `/projects/${projectId}/builds/${source}/${encodeURIComponent(buildId)}/logs?${params}`, undefined, { signal });
23
+ if (!body || !['ready', 'pending', 'unsupported', 'unavailable'].includes(body.state) || !Array.isArray(body.steps) || !Array.isArray(body.entries))
24
+ throw new Error('invalid build log response');
25
+ bytes += Buffer.byteLength(JSON.stringify(body));
26
+ if (bytes > 16 * 1024 * 1024)
27
+ throw new Error('build logs exceed the 16 MiB per-read limit');
28
+ if (body.state === 'pending')
29
+ pending = true;
30
+ if (step && follow && body.state === 'ready') {
31
+ const counts = new Map(tail.counts);
32
+ const entries = body.entries.map(entry => {
33
+ const key = entryKey(step, entry);
34
+ const occurrence = (counts.get(key) ?? 0) + 1;
35
+ if (counts.size >= 100_000 && !counts.has(key))
36
+ throw new Error('live build logs exceed the display limit');
37
+ counts.set(key, occurrence);
38
+ return { ...entry, occurrence };
39
+ });
40
+ const name = steps.find(item => item.digest === step).name;
41
+ follow.emit({ ...body, output: [{ step, name, entries }] });
42
+ if (body.nextCursor) {
43
+ tail.counts = counts;
44
+ tail.cursor = body.nextCursor;
45
+ }
46
+ follow.tails.set(step, tail);
47
+ result.push({ ...body, entries: [] });
48
+ }
49
+ else
50
+ result.push(body);
51
+ cursor = body.nextCursor || undefined;
52
+ if (cursor && seen.has(cursor))
53
+ throw new Error('build log pagination did not advance');
54
+ if (cursor)
55
+ seen.add(cursor);
56
+ } while (cursor);
57
+ return result;
58
+ }
59
+ const stepPages = await pages();
60
+ const first = stepPages[0];
61
+ if (stepPages.some((page) => page.state !== first.state))
62
+ throw new LogsNotReady('build steps are temporarily unavailable');
63
+ const steps = stepPages.flatMap((page) => page.steps);
64
+ follow?.emit({ ...first, steps, output: [] });
65
+ const output = [];
66
+ for (const step of steps) {
67
+ if (!step.hasLogs)
68
+ continue;
69
+ const logs = await pages(step.digest);
70
+ if (logs.some((page) => page.state !== 'ready' && page.state !== 'pending'))
71
+ throw new LogsNotReady('step output is temporarily unavailable');
72
+ output.push({ step: step.digest, name: step.name, entries: logs.flatMap((page) => page.entries) });
73
+ }
74
+ return { ...first, nextCursor: undefined, state: pending ? 'pending' : first.state, steps, output };
75
+ }
76
+ const safeText = (text) => stripVTControlCharacters(text).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');
77
+ export class BuildLogPrinter {
78
+ seen = new Map();
79
+ diagnostics = new Set();
80
+ lastStep;
81
+ lineStart = true;
82
+ finishLine(write) {
83
+ if (!this.lineStart)
84
+ write('\n');
85
+ this.lineStart = true;
86
+ }
87
+ print(snapshot, write) {
88
+ const errors = [
89
+ ...(snapshot.error ? [{ id: 'build', message: snapshot.error }] : []),
90
+ ...snapshot.steps.filter(step => step.error).map(step => ({ id: step.digest, message: `${step.name}: ${step.error}` })),
91
+ ];
92
+ for (const error of errors) {
93
+ const key = JSON.stringify([error.id, error.message]);
94
+ if (this.diagnostics.has(key))
95
+ continue;
96
+ this.finishLine(write);
97
+ write(safeText(error.message) + '\n');
98
+ this.diagnostics.add(key);
99
+ }
100
+ for (const step of snapshot.output) {
101
+ const counts = new Map();
102
+ for (const entry of step.entries) {
103
+ const key = entryKey(step.step, entry);
104
+ const count = entry.occurrence ?? (counts.get(key) ?? 0) + 1;
105
+ counts.set(key, count);
106
+ if (count <= (this.seen.get(key) ?? 0))
107
+ continue;
108
+ if (this.seen.size >= 100_000)
109
+ throw new Error('live build logs exceed the display limit');
110
+ if (this.lastStep !== step.step) {
111
+ this.finishLine(write);
112
+ write(safeText(step.name) + '\n');
113
+ this.lastStep = step.step;
114
+ }
115
+ const text = safeText(entry.message);
116
+ write(text);
117
+ if (text)
118
+ this.lineStart = text.endsWith('\n');
119
+ }
120
+ for (const [key, count] of counts)
121
+ this.seen.set(key, Math.max(count, this.seen.get(key) ?? 0));
122
+ }
123
+ }
124
+ }
125
+ export function archiveLogWatcher(api, projectId, write, wait = sleep) {
126
+ const printer = new BuildLogPrinter();
127
+ const follow = { tails: new Map(), emit: snapshot => printer.print(snapshot, write) };
128
+ let warned = '';
129
+ let unavailable = false;
130
+ return async (buildId, finished, remainingMs = 30_000) => {
131
+ if (unavailable)
132
+ return;
133
+ const deadline = Date.now() + Math.min(remainingMs, finished ? 18_000 : 3000);
134
+ for (let attempt = 0; attempt < (finished ? 6 : 1); attempt++) {
135
+ if (attempt > 0)
136
+ await wait(Math.min(3000, Math.max(0, deadline - Date.now())));
137
+ const remaining = deadline - Date.now();
138
+ if (remaining <= 0)
139
+ return;
140
+ if (finished && (attempt === 0 || attempt === 5))
141
+ follow.tails.clear();
142
+ try {
143
+ const snapshot = await readBuildLogs(api, projectId, 'archive', buildId, AbortSignal.timeout(remaining), follow);
144
+ if (snapshot.state === 'unsupported') {
145
+ printer.finishLine(write);
146
+ write('Build logs are not supported for this build provider.\n');
147
+ unavailable = true;
148
+ return;
149
+ }
150
+ if (snapshot.state === 'unavailable')
151
+ throw new Error('build logs unavailable');
152
+ warned = '';
153
+ }
154
+ catch (error) {
155
+ if (error instanceof ApiError && error.status === 400)
156
+ follow.tails.clear();
157
+ printer.finishLine(write);
158
+ const message = `Could not read build logs. Retry with: insta build-logs ${buildId}\n`;
159
+ if (warned !== message)
160
+ write(message);
161
+ warned = message;
162
+ }
163
+ }
164
+ };
165
+ }
166
+ export async function followBuildLogs(api, projectId, source, buildId, write, wait = sleep) {
167
+ const printer = new BuildLogPrinter();
168
+ const follow = { tails: new Map(), emit: snapshot => printer.print(snapshot, write) };
169
+ let finalReads = 0;
170
+ let failures = 0;
171
+ for (;;) {
172
+ let snapshot;
173
+ try {
174
+ snapshot = await readBuildLogs(api, projectId, source, buildId, AbortSignal.timeout(30_000), follow);
175
+ failures = 0;
176
+ }
177
+ catch (error) {
178
+ const retryable = error instanceof ApiError ? error.status === 400 || error.status === 429 || error.status >= 500
179
+ : error instanceof LogsNotReady || error instanceof TypeError || (error instanceof Error && ['AbortError', 'TimeoutError'].includes(error.name));
180
+ if (error instanceof ApiError && error.status === 400)
181
+ follow.tails.clear();
182
+ if (!retryable || ++failures >= 5) {
183
+ printer.finishLine(write);
184
+ throw error;
185
+ }
186
+ printer.finishLine(write);
187
+ write('Could not refresh build logs; retrying…\n');
188
+ await wait(3000);
189
+ continue;
190
+ }
191
+ if (snapshot.state === 'unsupported' || snapshot.state === 'unavailable') {
192
+ printer.finishLine(write);
193
+ throw new Error(`Build logs ${snapshot.state}`);
194
+ }
195
+ if (snapshot.state === 'ready' && terminal.has(snapshot.buildState)) {
196
+ if (++finalReads >= 6) {
197
+ printer.finishLine(write);
198
+ return;
199
+ }
200
+ if (finalReads === 1 || finalReads === 5)
201
+ follow.tails.clear();
202
+ }
203
+ await wait(3000);
204
+ }
205
+ }
206
+ //# sourceMappingURL=build-logs.js.map
@@ -5,7 +5,11 @@ import { cycleLine, dimensionLines } from './metrics.js';
5
5
  export async function resolveOrgId(opts) {
6
6
  if (opts.org)
7
7
  return opts.org;
8
- return (await requireProject()).orgId;
8
+ // `ProjectConfig.orgId` is typed string but INSTA_PROJECT_ID resolves a project with no org.
9
+ const orgId = (await requireProject()).orgId;
10
+ if (!orgId)
11
+ die('INSTA_PROJECT_ID names no organization — set INSTA_ORG_ID, or pass --org <id>');
12
+ return orgId;
9
13
  }
10
14
  // Format the billing overview into printable lines (pure, so it's unit-testable).
11
15
  // `org` is the caller's --org, echoed into the portal hint: `billing` and `billing portal` resolve
@@ -0,0 +1,23 @@
1
+ import { ApiClient, requireProject } from '../api.js';
2
+ import { BuildLogPrinter, followBuildLogs, readBuildLogs } from '../build-logs.js';
3
+ import { info, printJson } from '../util.js';
4
+ export async function buildLogs(buildId, opts) {
5
+ if (opts.source !== 'github' && opts.source !== 'archive')
6
+ throw new Error('--source must be github or archive');
7
+ if (opts.follow && opts.json)
8
+ throw new Error('--follow cannot be combined with --json');
9
+ const api = await ApiClient.load();
10
+ const { projectId } = await requireProject();
11
+ if (opts.follow)
12
+ return followBuildLogs(api, projectId, opts.source, buildId, (text) => { process.stdout.write(text); });
13
+ const snapshot = await readBuildLogs(api, projectId, opts.source, buildId);
14
+ if (opts.json)
15
+ return printJson(snapshot);
16
+ const printer = new BuildLogPrinter();
17
+ const write = (text) => { process.stdout.write(text); };
18
+ printer.print(snapshot, write);
19
+ printer.finishLine(write);
20
+ if (snapshot.state !== 'ready')
21
+ info(`Build logs ${snapshot.state}`);
22
+ }
23
+ //# sourceMappingURL=build-logs.js.map
@@ -1,3 +1,4 @@
1
+ import { archiveLogWatcher } from '../build-logs.js';
1
2
  import { resolve, join } from 'node:path';
2
3
  import { existsSync, readFileSync } from 'node:fs';
3
4
  import { ApiClient, ApiError, requireProject } from '../api.js';
@@ -74,7 +75,13 @@ export async function prepareSource(api, projectId, dir, branch, opts, run = opt
74
75
  // flyctl, local-docker and legacy all end in an image, and all three need a Dockerfile.
75
76
  return { image: await buildFromSource(api, projectId, dir, branch, opts, run) };
76
77
  }
77
- const log = note(opts);
78
+ const stream = opts.json ? process.stderr : process.stdout;
79
+ let lineStart = true;
80
+ const finishLine = () => { if (!lineStart)
81
+ stream.write('\n'); lineStart = true; };
82
+ const log = (message) => { finishLine(); note(opts)(message); };
83
+ const writeOutput = (text) => { stream.write(text); if (text)
84
+ lineStart = text.endsWith('\n'); };
78
85
  const absDir = resolve(process.cwd(), dir);
79
86
  const caveat = windowsModeCaveat();
80
87
  if (caveat)
@@ -89,7 +96,8 @@ export async function prepareSource(api, projectId, dir, branch, opts, run = opt
89
96
  // time, because a platform request has to answer inside the ALB's 60s while a build runs minutes.
90
97
  // A repo-connected service refuses this with a 409 the same way it refuses an image deploy, and
91
98
  // the hint that names the FLAG rather than the API field lives here, beside the `/deploy` path.
92
- const out = await deployArchive(api, projectId, ref, branch, opts, Date.now, undefined, log)
99
+ const out = await deployArchive(api, projectId, ref, branch, opts, Date.now, undefined, log, archiveLogWatcher(api, projectId, writeOutput))
100
+ .finally(finishLine)
93
101
  .catch((e) => { throw e instanceof ApiError && e.status === 409 ? new ApiError(e.status, repoConnectedHint(e.message), e.body) : e; });
94
102
  if (!out)
95
103
  return null;
@@ -36,7 +36,10 @@ export async function domainSearch(keyword, opts, deps) {
36
36
  export async function domainBuy(name, opts, deps) {
37
37
  const years = whole('--years', opts.years);
38
38
  const { api, project: p } = await domainDeps(deps);
39
- const res = await api.rawRequest('POST', `/projects/${p.projectId}/domains/orders`, { domainName: name, years });
39
+ const orgId = p.orgId || die('this project link names no organization — set INSTA_ORG_ID');
40
+ // The org route still signs for a PROJECT in agent mode: `domain.purchase` is read there, and a
41
+ // bootstrap session names none, so the platform refuses it.
42
+ const res = await api.rawRequest('POST', `/orgs/${orgId}/domains/orders`, { domainName: name, years }, { projectId: p.projectId });
40
43
  if (handleApproval(res, opts.json))
41
44
  return;
42
45
  if (opts.json)
@@ -57,12 +60,13 @@ export function ownerOf(host, owned) {
57
60
  */
58
61
  export async function domainAttach(host, opts, deps) {
59
62
  const { api, project: p } = await domainDeps(deps);
63
+ const orgId = p.orgId || die('this project link names no organization — set INSTA_ORG_ID');
60
64
  const name = host.trim().toLowerCase();
61
- const { items } = await api.request('GET', `/projects/${p.projectId}/domains`);
65
+ const { items } = await api.request('GET', `/orgs/${orgId}/domains`);
62
66
  const owner = ownerOf(name, items);
63
67
  if (!owner) {
64
68
  // The domains list holds registered names only; a bought name still registering is an order.
65
- const { items: orders } = await api.request('GET', `/projects/${p.projectId}/domains/orders`);
69
+ const { items: orders } = await api.request('GET', `/orgs/${orgId}/domains/orders`);
66
70
  const o = ownerOf(name, orders);
67
71
  if (o)
68
72
  die(`${o.domainName} is not registered yet — its order is ${o.status}: insta domain status ${o.domainName}`);
@@ -83,10 +87,10 @@ export async function domainAttach(host, opts, deps) {
83
87
  info(`then: insta domain status ${owner.domainName}`);
84
88
  }
85
89
  // ---- list / status ----
86
- function domainLines(d) {
90
+ function domainLines(d, linked = true) {
87
91
  const out = [`${d.domainName} ${d.status}${d.expiresAt ? ` (expires ${d.expiresAt.slice(0, 10)}${d.autorenew ? ', auto-renews' : ''})` : ''}`];
88
92
  // Vacuously true for a domain with no hostnames, which is every domain until something attaches.
89
- if (d.hostnames.every((h) => h.state === 'failed')) {
93
+ if (linked && d.hostnames.every((h) => h.state === 'failed')) {
90
94
  const names = d.hostnames.map((h) => h.hostname);
91
95
  // Attaching the bought name itself re-attaches its www.
92
96
  const retry = names.includes(d.domainName) ? names.filter((h) => h !== `www.${d.domainName}`) : names;
@@ -97,30 +101,29 @@ function domainLines(d) {
97
101
  out.push(` ${h.hostname.padEnd(w)} ${h.state}${h.service ? ` → ${h.service}` : ''}${h.reason ? ` — ${h.reason}` : ''}`);
98
102
  return out;
99
103
  }
100
- function orderStatusLines(o) {
104
+ function orderStatusLines(o, linked = true) {
101
105
  const out = [`order ${o.id}: ${o.domainName} — ${o.status}${o.failedReason ? ` — ${o.failedReason}` : ''}`];
102
- if (o.status === 'canceled')
106
+ if (linked && o.status === 'canceled')
103
107
  out.push(` the checkout closed without payment — order again: insta domain buy ${o.domainName}`);
104
108
  return out;
105
109
  }
106
110
  export async function domainList(opts, deps) {
107
- const { api, project: p } = await domainDeps(deps);
108
- const r = await api.request('GET', `/projects/${p.projectId}/domains`);
111
+ const { api, orgId } = await orgDeps(opts, deps);
112
+ const r = await api.request('GET', `/orgs/${orgId}/domains`);
109
113
  if (opts.json)
110
114
  return printJson(r);
111
115
  if (!r.items.length)
112
116
  return info('no domains bought through InstaCloud in this org (search: insta domain search <keyword>)');
113
117
  for (const d of r.items)
114
- for (const line of domainLines(d))
118
+ for (const line of domainLines(d, !opts.org))
115
119
  info(line);
116
120
  }
117
121
  export async function domainStatus(name, opts, deps) {
118
- const { api, project: p } = await domainDeps(deps);
122
+ const { api, orgId } = await orgDeps(opts, deps);
119
123
  const host = name.trim().toLowerCase();
120
- // Both are the ORG's; the project in the path is the scope the agent policy is read at.
121
124
  const [{ items: domains }, { items: orders }] = await Promise.all([
122
- api.request('GET', `/projects/${p.projectId}/domains`),
123
- api.request('GET', `/projects/${p.projectId}/domains/orders`),
125
+ api.request('GET', `/orgs/${orgId}/domains`),
126
+ api.request('GET', `/orgs/${orgId}/domains/orders`),
124
127
  ]);
125
128
  const domain = domains.find((d) => d.domainName === host) ?? null;
126
129
  const order = orders.find((o) => o.domainName === host) ?? null;
@@ -128,7 +131,7 @@ export async function domainStatus(name, opts, deps) {
128
131
  die(`${host} was not bought through this org`);
129
132
  if (opts.json)
130
133
  return printJson({ domain, order });
131
- for (const line of domain ? domainLines(domain) : orderStatusLines(order))
134
+ for (const line of domain ? domainLines(domain, !opts.org) : orderStatusLines(order, !opts.org))
132
135
  info(line);
133
136
  }
134
137
  export function recordLines(records) {
@@ -64,7 +64,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
64
64
  // The operation is idempotent on (target, archive, build kind), so a re-run after approving finds
65
65
  // the one it already started rather than building again: same image, same result, no second
66
66
  // approval for a build that already happened.
67
- export async function deployArchive(api, projectId, ref, branch, opts, now = Date.now, wait = sleep, log = () => { }) {
67
+ export async function deployArchive(api, projectId, ref, branch, opts, now = Date.now, wait = sleep, log = () => { }, watchLogs) {
68
68
  const started = await api.rawRequest('POST', `/projects/${projectId}/archive-deploys`, {
69
69
  branch,
70
70
  group: opts.group,
@@ -79,6 +79,7 @@ export async function deployArchive(api, projectId, ref, branch, opts, now = Dat
79
79
  const operationId = started.body?.operationId;
80
80
  if (typeof operationId !== 'string' || !operationId)
81
81
  throw new Error('the platform accepted the deploy but returned no operation id — re-run the deploy');
82
+ log(`build logs: insta build-logs ${operationId}`);
82
83
  if (started.body?.resumed === true)
83
84
  log('resuming the deploy this archive already started');
84
85
  const deadline = now() + DEPLOY_DEADLINE_MS;
@@ -99,6 +100,7 @@ export async function deployArchive(api, projectId, ref, branch, opts, now = Dat
99
100
  throw remaining <= POLL_REQUEST_TIMEOUT_MS ? overdue() : new Error(`the platform did not answer a status poll within ${POLL_REQUEST_TIMEOUT_MS / 1000}s — check \`insta status\` or re-run`);
100
101
  });
101
102
  const state = res.body?.state;
103
+ await watchLogs?.(operationId, state === 'failed' || state === 'live', Math.max(0, deadline - now()));
102
104
  // A failed operation is an ANSWER, not a transport error: the poll worked, and the sentence
103
105
  // it carries (usually the gateway's own, e.g. "no Dockerfile at ./api") is the one to show.
104
106
  if (state === 'failed') {
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import * as regions from './commands/regions.js';
21
21
  import * as secretsCmd from './commands/secrets.js';
22
22
  import { deploy } from './commands/deploy.js';
23
23
  import { build } from './commands/build.js';
24
+ import { buildLogs } from './commands/build-logs.js';
24
25
  import * as computeCmd from './commands/compute.js';
25
26
  import * as githubCmd from './commands/github.js';
26
27
  import * as dbCmd from './commands/db.js';
@@ -347,6 +348,9 @@ program.command('manifest').description('Print an agent-legible view of the proj
347
348
  // ---- regions ----
348
349
  program.command('regions').description('List regions available for postgres/compute services').option('--json').action(guard((o) => regions.regionsList(o)));
349
350
  // ---- observability ----
351
+ program.command('build-logs <build-id>').description('Read source-build output for a deploy operation or GitHub build')
352
+ .option('--source <source>', 'archive or github', 'archive').option('--follow', 'poll new output until the build ends').option('--json')
353
+ .action(guard((id, opts) => buildLogs(id, opts)));
350
354
  program.command('metrics <target> [group]').description('Service metrics (target: db|compute|redis|mysql|mongodb)')
351
355
  .option('--branch <b>').option('--from <unix>').option('--to <unix>').option('--step <s>').option('--json')
352
356
  .action(guard((target, group, o) => obs.metrics(target, group, o)));
@@ -371,9 +375,11 @@ dom.command('buy <name>').description('Buy a domain — pay at the printed Strip
371
375
  dom.command('attach <hostname>').description('Point a bought domain, or any subdomain of one, at a compute service — `abc.com` binds it and its www, `api.abc.com` binds only that (gated: deploy)')
372
376
  .option('--branch <b>').option('--group <g>', "compute service (default: the branch's sole compute service)").option('--json')
373
377
  .action(guard((hostname, o) => domainCmd.domainAttach(hostname, o)));
374
- dom.command('list').description("Domains bought through InstaCloud in this org — a domain belongs to the org, each of its hostnames to a service").option('--json')
378
+ dom.command('list').description("Domains bought through InstaCloud in this org — a domain belongs to the org, each of its hostnames to a service")
379
+ .option('--org <id>', "target org (default: linked project's org)").option('--json')
375
380
  .action(guard((o) => domainCmd.domainList(o)));
376
- dom.command('status <name>').description("A bought domain's order and attach state").option('--json')
381
+ dom.command('status <name>').description("A bought domain's order and attach state")
382
+ .option('--org <id>', "target org (default: linked project's org)").option('--json')
377
383
  .action(guard((name, o) => domainCmd.domainStatus(name, o)));
378
384
  const rec = dom.command('records').description('DNS records of a bought domain — the zone InstaCloud holds at the registrar');
379
385
  rec.command('list <domain>').description('Every record in the zone, managed ones marked')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.79",
3
+ "version": "0.0.81",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [