insta 0.0.66 → 0.0.67

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
@@ -220,6 +220,7 @@ build never reaches a production installer.
220
220
  | `insta run <cmd>` | Run a command with the branch bundle injected, nothing written to disk |
221
221
  | `insta deploy [dir]` | Deploy a source directory (built remotely) or `--image <url>` |
222
222
  | `insta compute` | `start` · `stop` · `suspend` · `status` · `set-domain` · `check-domain` · `remove-domain` |
223
+ | `insta domain` | Buy a domain through InstaCloud: `search` · `buy` · `attach` · `list` · `status` · `contact` |
223
224
  | `insta db` | `url` (print the postgres DSN) · `connect` (psql session) · `limits` · `stats` · `always-on` · `volume` |
224
225
  | `insta regions` | Regions available for postgres and compute |
225
226
  | `insta manifest` | Agent-legible view of every branch and its URLs |
@@ -2,7 +2,7 @@ import { ApiClient, requireProject } from '../api.js';
2
2
  import { die, info, openUrl, printJson } from '../util.js';
3
3
  import { cycleLine, dimensionLines } from './metrics.js';
4
4
  // Resolve the target org: explicit --org, else the linked project's org.
5
- async function resolveOrgId(opts) {
5
+ export async function resolveOrgId(opts) {
6
6
  if (opts.org)
7
7
  return opts.org;
8
8
  return (await requireProject()).orgId;
@@ -108,7 +108,7 @@ export async function billingPortal(opts) {
108
108
  // "opening", not "opened": a launcher that starts and then fails reports it asynchronously,
109
109
  // so openUrl's true return is an attempt, not a confirmation (see util.ts) — and the URL is
110
110
  // already printed above for exactly that case.
111
- function presentUrl(url, label, open) {
111
+ export function presentUrl(url, label, open) {
112
112
  info(label);
113
113
  info(` ${url}`);
114
114
  if (open !== false && openUrl(url))
@@ -262,11 +262,11 @@ export function domainConflictMessage(host, e, services, ctx = {}) {
262
262
  // Resolve branch + target service, so every domain verb names the service AND its region, and an
263
263
  // ambiguous project is refused with the list instead of the platform's `default` fallback picking
264
264
  // one silently.
265
- async function domainTarget(api, projectId, branch, host, group) {
265
+ export async function domainTarget(api, projectId, branch, host, group) {
266
266
  const { services } = await api.request('GET', `/projects/${projectId}/services${q(branch)}`);
267
267
  return { target: resolveDomainTarget(services, host, group), services };
268
268
  }
269
- async function domainDeps(deps) {
269
+ export async function domainDeps(deps) {
270
270
  if (deps)
271
271
  return deps;
272
272
  const [api, project] = [await ApiClient.load(), await requireProject()];
@@ -0,0 +1,164 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { ApiClient } from '../api.js';
3
+ import { info, printJson, handleApproval, die } from '../util.js';
4
+ import { presentUrl, resolveOrgId } from './billing.js';
5
+ import { domainDeps, domainTarget } from './compute.js';
6
+ const usd = (cents) => `$${(cents / 100).toFixed(2)}`;
7
+ // --org wins; otherwise the linked project's org — the org verbs must not force a project link.
8
+ async function orgDeps(opts, deps) {
9
+ if (deps)
10
+ return { api: deps.api, orgId: opts.org ?? deps.project.orgId };
11
+ return { api: await ApiClient.load(), orgId: await resolveOrgId(opts) };
12
+ }
13
+ export function searchLines(results) {
14
+ if (!results.length)
15
+ return ['no results'];
16
+ const w = Math.max(...results.map((r) => r.domainName.length));
17
+ return results.map((r) => r.purchasable
18
+ ? ` ${r.domainName.padEnd(w)} ${usd(r.priceCents)}${r.renewalPriceCents !== undefined ? ` (renews ${usd(r.renewalPriceCents)}/yr)` : ''}`
19
+ : ` ${r.domainName.padEnd(w)} unavailable${r.reason ? ` — ${r.reason}` : ''}`);
20
+ }
21
+ export async function domainSearch(keyword, opts, deps) {
22
+ const { api, orgId } = await orgDeps(opts, deps);
23
+ const qs = new URLSearchParams({ q: keyword });
24
+ if (opts.tlds)
25
+ qs.set('tlds', opts.tlds);
26
+ const r = await api.request('GET', `/orgs/${orgId}/domains/search?${qs}`);
27
+ if (opts.json)
28
+ return printJson(r);
29
+ for (const line of searchLines(r.results))
30
+ info(line);
31
+ const buyable = r.results.find((x) => x.purchasable);
32
+ if (buyable)
33
+ info(`buy one: insta domain buy ${buyable.domainName}`);
34
+ }
35
+ const CONTACT_KEYS = ['firstName', 'lastName', 'companyName', 'address1', 'address2', 'city', 'state', 'zip', 'country', 'email', 'phone'];
36
+ export function contactFromOpts(opts, file) {
37
+ if (file !== undefined)
38
+ return file;
39
+ const out = {};
40
+ for (const k of CONTACT_KEYS)
41
+ if (opts[k] !== undefined)
42
+ out[k] = opts[k];
43
+ return Object.keys(out).length ? out : undefined;
44
+ }
45
+ async function readContactFile(path) {
46
+ if (!path)
47
+ return undefined;
48
+ try {
49
+ return JSON.parse(await readFile(path, 'utf8'));
50
+ }
51
+ catch (e) {
52
+ throw new Error(`cannot read ${path} as JSON: ${e.message}`);
53
+ }
54
+ }
55
+ function contactLines(c) {
56
+ if (!c)
57
+ return ['no registrant contact set — set one with: insta domain contact set --first-name … (or --contact-file contact.json)'];
58
+ return [
59
+ `${c.firstName} ${c.lastName}${c.companyName ? ` (${c.companyName} — the organization is the legal registrant)` : ''}`,
60
+ `${c.address1}${c.address2 ? `, ${c.address2}` : ''}, ${c.city}, ${c.state} ${c.zip}, ${c.country}`,
61
+ `${c.email} ${c.phone}`,
62
+ ];
63
+ }
64
+ export async function domainContactShow(opts, deps) {
65
+ const { api, orgId } = await orgDeps(opts, deps);
66
+ const r = await api.request('GET', `/orgs/${orgId}/domains/contact`);
67
+ if (opts.json)
68
+ return printJson(r);
69
+ for (const line of contactLines(r.contact))
70
+ info(line);
71
+ }
72
+ export async function domainContactSet(opts, deps) {
73
+ const { api, orgId } = await orgDeps(opts, deps);
74
+ const contact = contactFromOpts(opts, await readContactFile(opts.contactFile));
75
+ if (!contact)
76
+ die('pass the contact as flags (--first-name … --phone) or as --contact-file <contact.json>');
77
+ const r = await api.request('PUT', `/orgs/${orgId}/domains/contact`, contact);
78
+ if (opts.json)
79
+ return printJson(r);
80
+ info('registrant contact saved:');
81
+ for (const line of contactLines(r.contact))
82
+ info(` ${line}`);
83
+ }
84
+ export async function domainBuy(name, opts, deps) {
85
+ const { api, project: p } = await domainDeps(deps);
86
+ const branch = opts.branch ?? p.branch;
87
+ const { target } = await domainTarget(api, p.projectId, branch, name, opts.group);
88
+ const contact = await readContactFile(opts.contactFile);
89
+ // JSON.stringify drops undefined but keeps NaN as null, which the platform rejects as a type
90
+ // error rather than a bad term — so a malformed --years is refused here, with the reason.
91
+ let years;
92
+ if (opts.years !== undefined) {
93
+ years = Number(opts.years);
94
+ if (!Number.isInteger(years))
95
+ die(`--years must be a whole number of years, not ${opts.years}`);
96
+ }
97
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/domains/orders`, { domainName: name, years, branch, group: target.name, contact });
98
+ if (handleApproval(res, opts.json))
99
+ return;
100
+ if (opts.json)
101
+ return printJson(res.body);
102
+ const { order } = res.body;
103
+ info(`${order.domainName} — ${usd(order.priceCents)} for ${order.years} year${order.years === 1 ? '' : 's'}${order.renewalPriceCents !== null ? `, then ${usd(order.renewalPriceCents)}/yr` : ''}`);
104
+ info(`attaches to ${target.name}${order.branch ? ` (branch ${order.branch})` : ''} as ${order.domainName} and www.${order.domainName} once paid`);
105
+ presentUrl(order.checkoutUrl, 'Complete the payment in your browser:', opts.open);
106
+ info(`then: insta domain status ${order.domainName}`);
107
+ }
108
+ export async function domainAttach(name, opts, deps) {
109
+ const { api, project: p } = await domainDeps(deps);
110
+ const branch = opts.branch ?? p.branch;
111
+ const { target } = await domainTarget(api, p.projectId, branch, name, opts.group);
112
+ const res = await api.rawRequest('POST', `/projects/${p.projectId}/domains/${encodeURIComponent(name)}/attach`, { branch, group: target.name });
113
+ if (handleApproval(res, opts.json))
114
+ return;
115
+ if (opts.json)
116
+ return printJson(res.body);
117
+ const d = res.body;
118
+ info(`${d.domainName} will attach to ${target.name} as ${d.hostnames.map((h) => h.hostname).join(' and ')}`);
119
+ info(`then: insta domain status ${d.domainName}`);
120
+ }
121
+ // ---- list / status ----
122
+ function domainLines(d) {
123
+ const out = [`${d.domainName} ${d.status}${d.service ? ` → ${d.service}` : ''}${d.expiresAt ? ` (expires ${d.expiresAt.slice(0, 10)}${d.autorenew ? ', auto-renews' : ''})` : ''}`];
124
+ if (d.status === 'detached' || d.status === 'attach_failed')
125
+ out.push(` attach it again: insta domain attach ${d.domainName}`);
126
+ const w = Math.max(0, ...d.hostnames.map((x) => x.hostname.length));
127
+ for (const h of d.hostnames)
128
+ out.push(` ${h.hostname.padEnd(w)} ${h.state}${h.reason ? ` — ${h.reason}` : ''}`);
129
+ return out;
130
+ }
131
+ function orderStatusLines(o) {
132
+ const out = [`order ${o.id}: ${o.domainName} — ${o.status}${o.failedReason ? ` — ${o.failedReason}` : ''}`];
133
+ if (o.status === 'canceled')
134
+ out.push(` the checkout closed without payment — order again: insta domain buy ${o.domainName}`);
135
+ return out;
136
+ }
137
+ export async function domainList(opts, deps) {
138
+ const { api, project: p } = await domainDeps(deps);
139
+ const r = await api.request('GET', `/projects/${p.projectId}/domains`);
140
+ if (opts.json)
141
+ return printJson(r);
142
+ if (!r.items.length)
143
+ return info('no domains bought through InstaCloud in this project (search: insta domain search <keyword>)');
144
+ for (const d of r.items)
145
+ for (const line of domainLines(d))
146
+ info(line);
147
+ }
148
+ export async function domainStatus(name, opts, deps) {
149
+ const { api, project: p } = await domainDeps(deps);
150
+ const host = name.trim().toLowerCase();
151
+ const [{ items: domains }, { items: orders }] = await Promise.all([
152
+ api.request('GET', `/projects/${p.projectId}/domains`),
153
+ api.request('GET', `/projects/${p.projectId}/domains/orders`),
154
+ ]);
155
+ const domain = domains.find((d) => d.domainName === host) ?? null;
156
+ const order = orders.find((o) => o.domainName === host) ?? null;
157
+ if (!domain && !order)
158
+ die(`${host} was not bought through this project`);
159
+ if (opts.json)
160
+ return printJson({ domain, order });
161
+ for (const line of domain ? domainLines(domain) : orderStatusLines(order))
162
+ info(line);
163
+ }
164
+ //# sourceMappingURL=domain.js.map
@@ -8,7 +8,7 @@ import { existsSync } from 'node:fs';
8
8
  import os from 'node:os';
9
9
  import path from 'node:path';
10
10
  import { info } from '../util.js';
11
- import { MCP_SERVER_NAME, registerMcp, resolveMcpTarget } from './setup.js';
11
+ import { MCP_SERVER_NAME, registerMcp, requireMcpRegistration, resolveMcpTarget } from './setup.js';
12
12
  export const MCP_AGENT_TARGETS = ['cursor', 'codex', 'opencode', 'copilot', 'factory-droid'];
13
13
  export function configPath(slug, home) {
14
14
  switch (slug) {
@@ -120,13 +120,20 @@ export async function installAgentConfigs(agent, home = os.homedir()) {
120
120
  }
121
121
  // `insta mcp install [--agent <slug>] [--mcp-token]` — claude-code goes through its registry CLI
122
122
  // (registerMcp); everything else is a config-file write. No --agent = claude-code + all detected.
123
- export async function mcpInstall(opts) {
123
+ export async function mcpInstall(opts, register = registerMcp, installConfigs = installAgentConfigs) {
124
+ if (opts.mcpToken && opts.agent && opts.agent !== 'claude-code') {
125
+ throw new Error('--mcp-token supports Claude Code only; other clients use OAuth');
126
+ }
124
127
  if (!opts.agent || opts.agent === 'claude-code') {
125
- await registerMcp(undefined, undefined, !!opts.mcpToken);
128
+ const status = await register(undefined, undefined, !!opts.mcpToken);
129
+ // Missing Claude is an optional discovery miss only for the default OAuth install. A
130
+ // targeted install, token request, or attempted-but-failed add must not report success.
131
+ if ((opts.agent || opts.mcpToken || status === 'failed') && !requireMcpRegistration(status))
132
+ return;
126
133
  if (opts.agent)
127
134
  return;
128
135
  }
129
- const done = await installAgentConfigs(opts.agent);
136
+ const done = await installConfigs(opts.agent);
130
137
  if (done.length)
131
138
  info(`✓ MCP — configured for ${done.join(', ')} (restart those tools to pick it up)`);
132
139
  else if (opts.agent) { /* messages already printed */ }
@@ -14,7 +14,7 @@ import { ApiClient } from '../api.js';
14
14
  import { setupProjectAgentSession } from '../agent.js';
15
15
  import { readPersistedGlobal, resolveEnv } from '../config.js';
16
16
  import { DEFAULT_ENV, ENVS, ENV_NAMES, envForApiUrl, envFromEnvVar, isEnvName, mcpServerName } from '../env.js';
17
- import { info, openUrl } from '../util.js';
17
+ import { fail, info, openUrl } from '../util.js';
18
18
  import { isRunnableFile, resolveSpawnable } from '../spawn.js';
19
19
  import { loginDevice } from './auth.js';
20
20
  import { projectCreate, projectLink, slugifyName } from './project.js';
@@ -203,18 +203,16 @@ export async function resolveMcpTarget() {
203
203
  const { env, mcpUrl } = await resolveEnv();
204
204
  return { name: mcpServerName(env ?? DEFAULT_ENV), url: mcpUrl };
205
205
  }
206
- const defaultMinter = async () => {
207
- try {
208
- const api = await ApiClient.load();
209
- if (!api.config.accessToken)
210
- return null;
211
- const { token } = await api.request('POST', '/tokens', { name: `mcp-${os.hostname()}` });
212
- return token ?? null;
213
- }
214
- catch {
206
+ export async function mintMcpToken(api) {
207
+ if (!api.config.accessToken)
215
208
  return null;
209
+ const result = await api.request('POST', '/tokens', { name: `mcp-${os.hostname()}` });
210
+ if (typeof result?.token !== 'string' || !result.token.trim()) {
211
+ throw new Error('MCP token creation did not return a token; registration was not written');
216
212
  }
217
- };
213
+ return result.token;
214
+ }
215
+ const defaultMinter = async () => mintMcpToken(await ApiClient.load());
218
216
  export async function registerMcp(run = defaultRunner, mint = defaultMinter, useToken = false, announce = true) {
219
217
  const { name, url } = await resolveMcpTarget();
220
218
  if (!(await run('claude', ['--version'])).ok)
@@ -241,10 +239,20 @@ export async function registerMcp(run = defaultRunner, mint = defaultMinter, use
241
239
  if (announce) {
242
240
  info(`✓ MCP — ${name} registered with Claude Code (\`claude mcp list\` to verify)`);
243
241
  if (!useToken)
244
- info(' first use: run `/mcp` in Claude Code and authorize in the browser (headless machines: `insta setup agent --mcp-token`)');
242
+ info(' first use: run `/mcp` in Claude Code and authorize in the browser (--mcp-token requires token-creation permission)');
245
243
  }
246
244
  return 'new';
247
245
  }
246
+ /** Callers decide when an optional probe becomes a required registration (after the interactive
247
+ * login retry in setup). An OAuth registration for another client cannot satisfy --mcp-token. */
248
+ export function requireMcpRegistration(status) {
249
+ if (status === 'new' || status === 'existing')
250
+ return true;
251
+ fail(status === 'no-claude'
252
+ ? 'Claude Code MCP registration incomplete: Claude Code is not available on PATH'
253
+ : 'Claude Code MCP registration incomplete; see the error above');
254
+ return false;
255
+ }
248
256
  /** The environment `setup agent` should target, and whether the machine must be switched to it
249
257
  * first. Pure — decides only; the caller performs the switch.
250
258
  *
@@ -409,7 +417,8 @@ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs
409
417
  // Default into login on an interactive terminal (see shouldOfferLogin) BEFORE the MCP summary:
410
418
  // a --mcp-token registration needs the session to mint, so a post-login retry must land in the
411
419
  // same combined line instead of announcing Claude Code separately. Best-effort: a declined
412
- // prompt or a failed browser flow leaves a completed setup plus the manual hint, never an error.
420
+ // prompt or a failed browser flow leaves the manual hint. Explicit --mcp-token still requires
421
+ // registration to finish; without that flag, login remains optional.
413
422
  const stored = await readStored();
414
423
  let loggedIn = !!(stored.accessToken || stored.user);
415
424
  if (shouldOfferLogin(!!opts.yes, loggedIn, loginFlow.stdinTty, loginFlow.stdoutTty)) {
@@ -417,15 +426,19 @@ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs
417
426
  try {
418
427
  await loginFlow.login();
419
428
  loggedIn = true;
420
- if (opts.mcpToken)
421
- claude = await registerMcp(run, mint, true, false);
422
429
  }
423
430
  catch (e) {
424
- info(` login did not complete (${e instanceof Error ? e.message : String(e)}) — no problem, setup itself is done.`);
431
+ info(` login did not complete (${e instanceof Error ? e.message : String(e)})`);
425
432
  info(' run `insta login` to try again — the sign-in link it prints works from a browser on any device.');
426
433
  }
434
+ // Keep token-creation errors outside the browser-login catch. A signed-in caller can be
435
+ // forbidden from minting; relabeling that as a login failure hides the real platform error.
436
+ if (opts.mcpToken && loggedIn)
437
+ claude = await registerMcp(run, mint, true, false);
427
438
  }
428
439
  }
440
+ if (opts.mcpToken && !requireMcpRegistration(claude))
441
+ return;
429
442
  // --project / --create: bind this directory to a project inside the SAME process. Never split
430
443
  // this back into `setup agent && insta project <cmd>` as one paste: no shell joiner survives
431
444
  // every Windows shell, and in shells without bracketed paste the queued second line is eaten
@@ -470,7 +483,7 @@ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs
470
483
  const mcpOk = claude === 'new' || claude === 'existing' || others.length > 0;
471
484
  info(`${summarizeInstall(res.output ?? '')} — ready to use InstaCloud${mcpOk ? ' (CLI + skill + MCP; restart any open tools)' : ''}`);
472
485
  if (claude === 'new' && !opts.mcpToken) {
473
- info(' Claude Code first use: run `/mcp` and authorize in the browser (headless machines: `insta setup agent --mcp-token`)');
486
+ info(' Claude Code first use: run `/mcp` and authorize in the browser (--mcp-token requires token-creation permission)');
474
487
  }
475
488
  // The user's next move: one concrete action, not a concept. The agents drive `insta` themselves
476
489
  // (project create/link, deploys, login via the device flow), so the human just asks for the
package/dist/index.js CHANGED
@@ -32,6 +32,7 @@ import * as govern from './commands/govern.js';
32
32
  import * as observe from './commands/observe.js';
33
33
  import * as obs from './commands/metrics.js';
34
34
  import { billing, billingUpgrade, billingPortal } from './commands/billing.js';
35
+ import * as domainCmd from './commands/domain.js';
35
36
  import * as selfUpdate from './commands/upgrade.js';
36
37
  import * as feedbackCmd from './commands/feedback.js';
37
38
  function onError(e) {
@@ -107,7 +108,7 @@ const setupCmd = program.command('setup').description('Set up this machine for I
107
108
  setupCmd.command('agent').description('Install the insta CLI (if missing), the insta skill for all coding agents, and the MCP server — targets production; pass --env staging for the staging deployment')
108
109
  .option('-y, --yes', 'non-interactive')
109
110
  .option('--env <prod|staging>', 'deployment to set this machine up for (default: prod — switches and persists, like `insta env use`)')
110
- .option('--mcp-token', 'register the MCP server with a minted insta_ API token instead of OAuth (headless machines / CI)')
111
+ .option('--mcp-token', 'register Claude Code with a minted insta_ API token instead of OAuth (requires login and token-creation permission)')
111
112
  .option('--project <id>', 'also link this directory to an existing project after setup (flows through login first if needed)')
112
113
  .option('--create [name]', 'also create a new project and link this directory after setup (default name: this directory; mutually exclusive with --project)')
113
114
  .action(guard((o) => setup.setupAgent(o)));
@@ -115,7 +116,7 @@ setupCmd.command('agent').description('Install the insta CLI (if missing), the i
115
116
  const mcpCmd = program.command('mcp').description('insta-cloud remote MCP server integration');
116
117
  mcpCmd.command('install').description('Register the remote MCP server with coding agents (default: Claude Code + all detected)')
117
118
  .option('--agent <slug>', 'one agent: claude-code, cursor, codex, opencode, copilot, factory-droid')
118
- .option('--mcp-token', 'claude-code only: minted insta_ API token instead of OAuth (headless machines / CI)')
119
+ .option('--mcp-token', 'claude-code only: minted insta_ API token instead of OAuth (requires login and token-creation permission)')
119
120
  .action(guard((o) => mcp.mcpInstall(o)));
120
121
  // ---- org ----
121
122
  const orgCmd = program.command('org').description('Manage organizations');
@@ -347,6 +348,32 @@ program.command('logs <target> [group]').description('Service logs (runtime by d
347
348
  program.command('usage').description('Usage for the current billing cycle by billing dimension (org by default; --proj for one project)')
348
349
  .option('--from <unix>').option('--to <unix>').option('--proj [id]', 'show one project (the linked one, or a given id) instead of the whole org').option('--json')
349
350
  .action(guard((o) => obs.usage(o)));
351
+ // ---- domains bought through InstaCloud (BYO domains: `insta compute set-domain`) ----
352
+ const dom = program.command('domain').description('Buy a domain through InstaCloud and attach it to a compute service (your own domain: `insta compute set-domain`)');
353
+ dom.command('search <keyword>').description('Search purchasable names with prices (a label like "myapp" or a full name like "myapp.com")')
354
+ .option('--tlds <list>', 'comma-separated TLDs to include').option('--org <id>', "target org (default: linked project's org)").option('--json')
355
+ .action(guard((keyword, o) => domainCmd.domainSearch(keyword, o)));
356
+ dom.command('buy <name>').description('Buy a domain and attach it to a branch compute service — pay at the printed Stripe Checkout link (gated: domain.purchase)')
357
+ .option('--years <n>', 'registration term in years (default 1)').option('--branch <b>').option('--group <g>', "compute service (default: the branch's sole compute service)")
358
+ .option('--contact-file <path>', 'registrant contact as JSON (default: the org contact from `insta domain contact set`)')
359
+ .option('--no-open', 'print the checkout URL instead of opening a browser').option('--json')
360
+ .action(guard((name, o) => domainCmd.domainBuy(name, o)));
361
+ dom.command('attach <name>').description('Attach a bought domain whose service was deleted (or whose attach failed) to a compute service (gated: deploy)')
362
+ .option('--branch <b>').option('--group <g>', "compute service (default: the branch's sole compute service)").option('--json')
363
+ .action(guard((name, o) => domainCmd.domainAttach(name, o)));
364
+ dom.command('list').description('Domains bought through InstaCloud in this project, with attach state per hostname').option('--json')
365
+ .action(guard((o) => domainCmd.domainList(o)));
366
+ dom.command('status <name>').description("A bought domain's order and attach state").option('--json')
367
+ .action(guard((name, o) => domainCmd.domainStatus(name, o)));
368
+ const domContact = dom.command('contact').description("Show the org's default registrant contact (the legal registrant of every domain bought with it)")
369
+ .option('--org <id>').option('--json').action(guard((o) => domainCmd.domainContactShow(o)));
370
+ domContact.command('set').description('Set the org default registrant contact (admin) from flags or --contact-file <path>; --company-name makes that organization the legal registrant')
371
+ .option('--first-name <s>').option('--last-name <s>').option('--company-name <s>').option('--address1 <s>').option('--address2 <s>').option('--city <s>').option('--state <s>').option('--zip <s>')
372
+ .option('--country <cc>', 'ISO 3166-1 alpha-2, e.g. US').option('--email <s>').option('--phone <e164>', 'E.164, e.g. +14155550100')
373
+ .option('--contact-file <path>', 'JSON file with the contact fields').option('--org <id>').option('--json')
374
+ // `contact --org X set` parks --org on the GROUP (enablePositionalOptions); without this merge the
375
+ // wrong org's registrant contact is written, and that field is legal ownership.
376
+ .action(guard((o) => domainCmd.domainContactSet({ ...domContact.opts(), ...o })));
350
377
  const bill = program.command('billing').description('Current billing cycle overview (tier / used / included / overage / credits / forecast + per-dimension & per-project breakdown)')
351
378
  .option('--org <id>', 'target org (default: linked project\'s org)').option('--json')
352
379
  .action(guard((o) => billing(o)));
package/dist/telemetry.js CHANGED
@@ -46,7 +46,7 @@ const SAFE_OPTIONS = {
46
46
  agent: oneOf(['claude-code', 'cursor', 'codex', 'opencode', 'copilot', 'factory-droid']),
47
47
  type: oneOf(TYPES), component: oneOf(COMPONENTS), severity: oneOf(SEVERITIES),
48
48
  status: oneOf(['pending', 'granted', 'denied', 'consumed']),
49
- limit: NUMBER, step: NUMBER, since: NUMBER, port: NUMBER, memory: NUMBER, cpu: NUMBER, size: NUMBER, volume: NUMBER,
49
+ limit: NUMBER, step: NUMBER, since: NUMBER, port: NUMBER, memory: NUMBER, cpu: NUMBER, size: NUMBER, volume: NUMBER, years: NUMBER,
50
50
  };
51
51
  export function telemetryDisabled(env = process.env) {
52
52
  return !!(env.DO_NOT_TRACK || env.INSTA_NO_TELEMETRY);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insta",
3
- "version": "0.0.66",
3
+ "version": "0.0.67",
4
4
  "type": "module",
5
5
  "description": "InstaCloud CLI — a thin client of the platform control-plane API.",
6
6
  "keywords": [