insta 0.0.65 → 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 +1 -0
- package/dist/agent.js +12 -2
- package/dist/api.js +7 -7
- package/dist/commands/auth.js +10 -1
- package/dist/commands/billing.js +2 -2
- package/dist/commands/compute.js +2 -2
- package/dist/commands/domain.js +164 -0
- package/dist/commands/github.js +39 -2
- package/dist/commands/mcp.js +11 -4
- package/dist/commands/setup.js +30 -17
- package/dist/commands/template.js +3 -1
- package/dist/index.js +35 -3
- package/dist/telemetry.js +1 -1
- package/package.json +1 -1
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 |
|
package/dist/agent.js
CHANGED
|
@@ -61,7 +61,13 @@ export async function loadAgentSession(apiUrl, projectId, cwd = process.cwd()) {
|
|
|
61
61
|
throw new Error(guidance);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
// Routes the CLI may call on an account-level (bootstrap) session, by first path segment. Every
|
|
65
|
+
// other route is project-owned: either its path names the project or the caller passes
|
|
66
|
+
// scope.projectId. A miss fails HERE, naming the route, instead of on the platform as a
|
|
67
|
+
// "for a different project" 403 whose setup hint cannot help — keep this list in step with the
|
|
68
|
+
// account-level paths in src/commands/.
|
|
69
|
+
export const ACCOUNT_ROUTES = new Set(['agent', 'auth', 'me', 'orgs', 'regions', 'templates', 'tokens', 'github']);
|
|
70
|
+
export async function agentHeaders(api, method, path, rawBody, scope = {}) {
|
|
65
71
|
if (!mode)
|
|
66
72
|
return {};
|
|
67
73
|
if (canonicalTarget(path) === '/agent/sessions' && method === 'POST')
|
|
@@ -70,9 +76,13 @@ export async function agentHeaders(api, method, path, rawBody) {
|
|
|
70
76
|
};
|
|
71
77
|
const target = canonicalTarget(path);
|
|
72
78
|
const match = target.match(/^\/projects\/([^/?]+)/);
|
|
79
|
+
const projectId = scope.projectId ?? (match ? decodeURIComponent(match[1]) : undefined);
|
|
80
|
+
if (!projectId && !ACCOUNT_ROUTES.has(target.split('/')[1]?.split('?')[0] ?? '')) {
|
|
81
|
+
throw new Error(`${method.toUpperCase()} ${target} is a project route but no project was given — this is an insta CLI bug; please report it with \`insta feedback\``);
|
|
82
|
+
}
|
|
73
83
|
// Account reads/project creation have no project policy yet. Mint a short-lived bootstrap
|
|
74
84
|
// assertion in memory. It cannot access project routes; never downgrade to a human request.
|
|
75
|
-
const session =
|
|
85
|
+
const session = projectId ? await loadAgentSession(api.apiUrl, projectId) : await issueAgentSession(api);
|
|
76
86
|
const timestamp = String(Math.floor(Date.now() / 1000));
|
|
77
87
|
const nonce = randomUUID();
|
|
78
88
|
const proof = [method.toUpperCase(), target, hash(rawBody), session.agentSessionId, timestamp, nonce, mode.source, session.client].join('\n');
|
package/dist/api.js
CHANGED
|
@@ -60,7 +60,7 @@ export class ApiClient {
|
|
|
60
60
|
}
|
|
61
61
|
// Returns parsed body for status < 400 (incl. 202); throws ApiError otherwise.
|
|
62
62
|
async request(method, path, body, opts = {}) {
|
|
63
|
-
const res = await this.raw(method, path, body, opts.auth ?? true);
|
|
63
|
+
const res = await this.raw(method, path, body, opts.auth ?? true, opts);
|
|
64
64
|
if (agentMode() && res.status === 202 && res.body?.status === 'approval_required')
|
|
65
65
|
throw new AgentApprovalRequired(res.body);
|
|
66
66
|
if (res.status >= 400)
|
|
@@ -69,25 +69,25 @@ export class ApiClient {
|
|
|
69
69
|
}
|
|
70
70
|
// Like request but returns {status, body} so callers can branch on 202 (approval_required).
|
|
71
71
|
async rawRequest(method, path, body, opts = {}) {
|
|
72
|
-
const res = await this.raw(method, path, body, opts.auth ?? true);
|
|
72
|
+
const res = await this.raw(method, path, body, opts.auth ?? true, opts);
|
|
73
73
|
if (res.status >= 400)
|
|
74
74
|
throw new ApiError(res.status, res.body?.error ?? `HTTP ${res.status}`, res.body);
|
|
75
75
|
return res;
|
|
76
76
|
}
|
|
77
|
-
async raw(method, path, body, auth) {
|
|
78
|
-
let r = await this.fetch(method, path, body, auth);
|
|
77
|
+
async raw(method, path, body, auth, scope = {}) {
|
|
78
|
+
let r = await this.fetch(method, path, body, auth, scope);
|
|
79
79
|
if (r.status === 401 && auth && this.cfg.refreshToken) {
|
|
80
80
|
if (await this.refresh())
|
|
81
|
-
r = await this.fetch(method, path, body, auth);
|
|
81
|
+
r = await this.fetch(method, path, body, auth, scope);
|
|
82
82
|
}
|
|
83
83
|
return r;
|
|
84
84
|
}
|
|
85
|
-
async fetch(method, path, body, auth) {
|
|
85
|
+
async fetch(method, path, body, auth, scope = {}) {
|
|
86
86
|
const headers = { 'Content-Type': 'application/json', 'Insta-Hints': '1', 'User-Agent': USER_AGENT };
|
|
87
87
|
if (auth && this.cfg.accessToken)
|
|
88
88
|
headers.Authorization = `Bearer ${this.cfg.accessToken}`;
|
|
89
89
|
if (auth)
|
|
90
|
-
Object.assign(headers, await agentHeaders(this, method, path, body === undefined ? '' : JSON.stringify(body)));
|
|
90
|
+
Object.assign(headers, await agentHeaders(this, method, path, body === undefined ? '' : JSON.stringify(body), scope));
|
|
91
91
|
const res = await this.fetchImpl(this.apiUrl + path, {
|
|
92
92
|
method,
|
|
93
93
|
headers,
|
package/dist/commands/auth.js
CHANGED
|
@@ -116,7 +116,8 @@ export async function applyApiKeyLogin(client, key) {
|
|
|
116
116
|
const sleepSeconds = (s) => new Promise((r) => setTimeout(r, s * 1000));
|
|
117
117
|
// Drives the device grant against the platform's Better Auth mount (/api/auth/device*) and
|
|
118
118
|
// returns the approved session token. Injectable poster + wait keep this testable without a
|
|
119
|
-
// network or real timers. Poll errors arrive as ApiError with the OAuth error code as message
|
|
119
|
+
// network or real timers. Poll errors arrive as ApiError with the OAuth error code as message
|
|
120
|
+
// (or a bare `HTTP 429` from the platform's rate limiter, treated as slow_down).
|
|
120
121
|
// `open` (the default browser-login path) launches the verification link locally on top of
|
|
121
122
|
// printing it; without it (--device) the link is print-only, for a browser on another machine.
|
|
122
123
|
export async function deviceGrant(post, wait = sleepSeconds, open) {
|
|
@@ -169,6 +170,14 @@ export async function deviceGrant(post, wait = sleepSeconds, open) {
|
|
|
169
170
|
interval += 5;
|
|
170
171
|
continue;
|
|
171
172
|
} // RFC 8628 §3.5: back off by 5s
|
|
173
|
+
// The platform's per-IP limiter answers a bare HTTP 429 (no OAuth error code) when a poll
|
|
174
|
+
// trips it — seen on prod 2026-09-10 after ~8 min of steady 5s polling. That is the same
|
|
175
|
+
// instruction as slow_down: the code is still pending, so back off and keep waiting rather
|
|
176
|
+
// than abort a login the human may be one click away from approving.
|
|
177
|
+
if (e.status === 429) {
|
|
178
|
+
interval += 5;
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
172
181
|
if (code === 'expired_token')
|
|
173
182
|
break;
|
|
174
183
|
if (code === 'access_denied')
|
package/dist/commands/billing.js
CHANGED
|
@@ -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))
|
package/dist/commands/compute.js
CHANGED
|
@@ -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
|
package/dist/commands/github.js
CHANGED
|
@@ -32,6 +32,14 @@ export function pickCandidate(candidates, rootDir) {
|
|
|
32
32
|
...candidates.map((c) => ` ${(c.rootDir ?? '(repo root)').padEnd(24)} ${c.builder}${c.startCommand ? ` start: ${c.startCommand}` : ''}`),
|
|
33
33
|
].join('\n'));
|
|
34
34
|
}
|
|
35
|
+
// A comma-separated list, because a shell would glob an unquoted `apps/web/**` into filenames — which
|
|
36
|
+
// also means a pattern containing a comma cannot be expressed. The server does the validation.
|
|
37
|
+
export function parseWatchPaths(raw) {
|
|
38
|
+
const out = raw.split(',').map((p) => p.trim()).filter(Boolean);
|
|
39
|
+
if (!out.length)
|
|
40
|
+
throw new Error("watch paths need at least one pattern, e.g. 'apps/web/**,packages/ui/**' (quote them, or the shell expands the *)");
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
35
43
|
// Build/start come from detection only: the platform's nixpacks lane fails a build whose commands differ from it.
|
|
36
44
|
// autoDeploy rides along only when switched off: a public repo 400s on autoDeploy: true.
|
|
37
45
|
export function sourceBody(src, c, o) {
|
|
@@ -46,8 +54,14 @@ export function sourceBody(src, c, o) {
|
|
|
46
54
|
port: o.port !== undefined ? parsePort(o.port) : c.port,
|
|
47
55
|
...(o.repoBranch ? { branch: o.repoBranch } : {}),
|
|
48
56
|
...(o.autoDeploy === false ? { autoDeploy: false } : {}),
|
|
57
|
+
...(o.watchPaths !== undefined ? { watchPaths: parseWatchPaths(o.watchPaths) } : {}),
|
|
49
58
|
};
|
|
50
59
|
}
|
|
60
|
+
// Named as repo-root paths wherever it is printed: every line that carries this also carries root_dir,
|
|
61
|
+
// which the patterns are NOT relative to.
|
|
62
|
+
export function watchPathsClause(paths) {
|
|
63
|
+
return `, but only when a push changes these repo-root paths: ${paths.join(', ')}`;
|
|
64
|
+
}
|
|
51
65
|
export function repoLine(serviceName, s) {
|
|
52
66
|
if (s.type !== 'github')
|
|
53
67
|
return `compute ${serviceName}: no repository connected${s.image ? ` (runs image ${s.image})` : ''} — connect one with \`insta compute connect-repo <owner/repo> ${serviceName}\``;
|
|
@@ -55,7 +69,10 @@ export function repoLine(serviceName, s) {
|
|
|
55
69
|
? 'public repo, deploys are manual (pushes do not redeploy)'
|
|
56
70
|
: s.auto_deploy ? `every push to ${s.branch} redeploys it` : 'auto-deploy off (pushes do not redeploy)';
|
|
57
71
|
const where = s.root_dir ? ` (${s.root_dir}/)` : '';
|
|
58
|
-
|
|
72
|
+
const only = !s.watch_paths?.length ? ''
|
|
73
|
+
: s.auto_deploy ? watchPathsClause(s.watch_paths)
|
|
74
|
+
: `; watch paths ${s.watch_paths.join(', ')} are stored but cannot apply`;
|
|
75
|
+
return `compute ${serviceName}: deploys from ${s.owner}/${s.repo}@${s.branch}${where} — ${how}${only}`;
|
|
59
76
|
}
|
|
60
77
|
export async function findInstalledRepo(api, orgId, ref) {
|
|
61
78
|
if (!orgId)
|
|
@@ -105,11 +122,31 @@ export async function computeConnectRepo(rawRef, serviceName, opts) {
|
|
|
105
122
|
const branch = res.source.branch;
|
|
106
123
|
const where = candidate.rootDir ? `${candidate.rootDir}/, ${candidate.builder}` : candidate.builder;
|
|
107
124
|
const now = res.build.queued ? `building ${branch} now` : `${branch} is already live at this commit`;
|
|
125
|
+
// From the server's answer, not from the flag: what it stored is what a push is matched against.
|
|
126
|
+
const filtered = res.source.watch_paths?.length ? watchPathsClause(res.source.watch_paths) : '';
|
|
108
127
|
const how = src.source === 'public' ? 'deploys are manual from here: pushes will not redeploy (public repo)'
|
|
109
128
|
: opts.autoDeploy === false ? 'auto-deploy is off: redeploy with `insta compute connect-repo` again or from the console'
|
|
110
|
-
: `every push to ${branch} redeploys it`;
|
|
129
|
+
: `every push to ${branch} redeploys it${filtered}`;
|
|
111
130
|
info(`connected ${ref.owner}/${ref.repo} → compute ${svc.name} (${where}): ${now} — ${how}`);
|
|
112
131
|
}
|
|
132
|
+
// Do not reconnect to change these: watch paths do not change what a build produces, and `connect-repo`
|
|
133
|
+
// would rebuild the service.
|
|
134
|
+
export async function computeWatchPaths(serviceName, opts) {
|
|
135
|
+
if (opts.set !== undefined && opts.clear)
|
|
136
|
+
throw new Error('pass --set or --clear, not both');
|
|
137
|
+
const patch = opts.clear ? { watchPaths: null } : opts.set !== undefined ? { watchPaths: parseWatchPaths(opts.set) } : null;
|
|
138
|
+
const api = await ApiClient.load();
|
|
139
|
+
const p = await requireProject();
|
|
140
|
+
const branch = opts.branch ?? p.branch;
|
|
141
|
+
const svc = await targetService(api, p.projectId, branch, serviceName);
|
|
142
|
+
const url = `/projects/${p.projectId}/services/${svc.id}/source${q(branch)}`;
|
|
143
|
+
const { source } = patch
|
|
144
|
+
? await api.request('PATCH', url, patch)
|
|
145
|
+
: await api.request('GET', url);
|
|
146
|
+
if (opts.json)
|
|
147
|
+
return printJson({ service: { id: svc.id, name: svc.name }, source });
|
|
148
|
+
info(patch ? `updated — ${repoLine(svc.name, source)}` : repoLine(svc.name, source));
|
|
149
|
+
}
|
|
113
150
|
export async function computeDisconnectRepo(serviceName, opts) {
|
|
114
151
|
const api = await ApiClient.load();
|
|
115
152
|
const p = await requireProject();
|
package/dist/commands/mcp.js
CHANGED
|
@@ -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
|
|
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
|
|
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 */ }
|
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
-
|
|
207
|
-
|
|
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 (
|
|
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
|
|
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)})
|
|
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 (
|
|
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
|
|
@@ -374,7 +374,9 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
374
374
|
const codeLabel = manifest?.code ?? target;
|
|
375
375
|
if (!quiet)
|
|
376
376
|
info(`deploying template ${codeLabel} to branch ${branchName} (${deploymentId})`);
|
|
377
|
-
|
|
377
|
+
// The poll route is keyed by deployment id, not project: name the project so agent mode signs
|
|
378
|
+
// with the project-bound session (a bootstrap session is rejected as "for a different project").
|
|
379
|
+
const dep = await watchDeployment((id) => api.request('GET', `/template-deployments/${id}`, undefined, { projectId: p.projectId }), deploymentId, quiet ? () => { } : info, deps.wait);
|
|
378
380
|
if (opts.json)
|
|
379
381
|
return printJson(source ? { source, ...dep } : dep);
|
|
380
382
|
info(`template ${codeLabel} deployed to branch ${branchName}`);
|
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
|
|
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 (
|
|
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');
|
|
@@ -252,16 +253,21 @@ const execCmd = compute.command('exec [service]').description("Run a one-shot co
|
|
|
252
253
|
// new option cannot reach the CLI surface while the split still reads it as part of the command.
|
|
253
254
|
for (const [flags, description] of computeCmd.EXEC_OPTIONS)
|
|
254
255
|
execCmd.option(flags, description);
|
|
255
|
-
compute.command('repo [service]').description('Show what a compute service deploys from: the image it runs, or the GitHub repository — owner/repo, the branch it builds, root directory, and whether pushes redeploy it')
|
|
256
|
+
compute.command('repo [service]').description('Show what a compute service deploys from: the image it runs, or the GitHub repository — owner/repo, the branch it builds, root directory, which paths a push must change to redeploy it, and whether pushes redeploy it at all')
|
|
256
257
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => githubCmd.computeRepo(service, o)));
|
|
257
258
|
compute.command('connect-repo <owner/repo> [service]').description("Connect a GitHub repository to an EXISTING compute service: the repo is built (its Dockerfile, or nixpacks when there is none) and deployed into that service, and every later push to the tracked repository branch redeploys it. The repo must be reachable through the org's GitHub App installation — connect GitHub in the console first (Add Service → GitHub Repo) — or be public. Build and start commands come from detection and cannot be set. Connecting again replaces the service's current source")
|
|
258
259
|
.option('--public', 'the repo is public and no GitHub App installation is needed (deploys are manual; pushes cannot redeploy)')
|
|
259
260
|
.option('--root-dir <dir>', 'the directory of the repo to build (a monorepo with several deployable directories lists them and exits 1 without it)')
|
|
260
261
|
.option('--repo-branch <name>', "the repository branch to build (default: the repo's default branch)")
|
|
261
262
|
.option('--no-auto-deploy', 'do not rebuild on pushes; redeploy by connecting again or from the console')
|
|
263
|
+
.option('--watch-paths <patterns>', "only redeploy when a push changes a matching path — a comma-separated list of gitignore patterns relative to the REPOSITORY ROOT, not to --root-dir, e.g. 'apps/web/**,packages/ui/**' (quote them, or the shell expands the *)")
|
|
262
264
|
.option('--port <n>', 'port the app listens on (default: detected)')
|
|
263
265
|
.option('--branch <branch>', 'branch (default: current) — the environment the service is on')
|
|
264
266
|
.option('--json').action(guard((ref, service, o) => githubCmd.computeConnectRepo(ref, service, o)));
|
|
267
|
+
compute.command('watch-paths [service]').description("Show or change which paths make a push redeploy a compute service. No flag prints them. --set narrows to a comma-separated list of gitignore patterns matched against paths relative to the REPOSITORY ROOT (not to the service's root directory), so a monorepo push that touched nothing on the list leaves this service alone — unless GitHub cannot report what a push changed (a force-push, or a comparison of 300 or more files, where its list stops being complete), in which case it deploys rather than risk skipping a real change. --clear removes the filter: every push that deploys this service deploys it again. Neither rebuilds the service — this changes which pushes deploy, not what a deploy builds")
|
|
268
|
+
.option('--set <patterns>', "the patterns, comma-separated, e.g. 'apps/web/**,packages/ui/**' — quote them, or the shell expands the *; a leading ! excludes, under git's rule that a path cannot be re-included once an earlier pattern took its directory")
|
|
269
|
+
.option('--clear', 'remove the filter: every push that deploys this service deploys it again')
|
|
270
|
+
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => githubCmd.computeWatchPaths(service, o)));
|
|
265
271
|
compute.command('disconnect-repo [service]').description('Disconnect the GitHub repository from a compute service. The service keeps running its current image; pushes no longer deploy it, and its build history stays')
|
|
266
272
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => githubCmd.computeDisconnectRepo(service, o)));
|
|
267
273
|
compute.command('volume [service]').description("Show, attach, grow, or delete a compute service's persistent /data volume. No flag: print size, mount path, and the plan cap (any plan). --size on a volumeless service ATTACHES one (any plan at the default 10Gi, the free cap; larger is paid and plan-capped; the disk mounts at /data on the next deploy); on a volume-bearing one it grows (paid plans; grow-only — a provisioned disk cannot shrink). --delete DESTROYS the disk and ALL its data immediately (no detach, no undo; billing stops now, and suspend fast-wake + scale-out return). Billing is actual data stored — the size is a cap, not a price")
|
|
@@ -342,6 +348,32 @@ program.command('logs <target> [group]').description('Service logs (runtime by d
|
|
|
342
348
|
program.command('usage').description('Usage for the current billing cycle by billing dimension (org by default; --proj for one project)')
|
|
343
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')
|
|
344
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 })));
|
|
345
377
|
const bill = program.command('billing').description('Current billing cycle overview (tier / used / included / overage / credits / forecast + per-dimension & per-project breakdown)')
|
|
346
378
|
.option('--org <id>', 'target org (default: linked project\'s org)').option('--json')
|
|
347
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);
|