insta 0.0.75 → 0.0.76
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/compute.js +5 -1
- package/dist/commands/domain.js +42 -16
- package/dist/commands/services.js +5 -2
- package/dist/commands/template.js +10 -2
- package/dist/index.js +10 -7
- package/package.json +1 -1
package/dist/commands/compute.js
CHANGED
|
@@ -672,6 +672,10 @@ export function volumeDeleteError(e) {
|
|
|
672
672
|
// 403/400 messages carry the upgrade hints and must reach the user verbatim (the guard prints
|
|
673
673
|
// ApiError messages as-is).
|
|
674
674
|
export async function computeVolume(serviceName, opts) {
|
|
675
|
+
if (opts.delete && opts.mountPath !== undefined)
|
|
676
|
+
throw new Error('--delete cannot be combined with --mount-path');
|
|
677
|
+
if (opts.mountPath !== undefined && !opts.size)
|
|
678
|
+
throw new Error('--mount-path requires --size when attaching a volume');
|
|
675
679
|
if (opts.delete && opts.size)
|
|
676
680
|
throw new Error('--delete cannot be combined with --size (one changes the volume, the other destroys it)');
|
|
677
681
|
const api = await ApiClient.load();
|
|
@@ -703,7 +707,7 @@ export async function computeVolume(serviceName, opts) {
|
|
|
703
707
|
return;
|
|
704
708
|
}
|
|
705
709
|
const sizeGib = parseVolumeGib(opts.size);
|
|
706
|
-
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/volume`, { sizeGib });
|
|
710
|
+
const res = await api.rawRequest('PUT', `/projects/${p.projectId}/services/${id}/volume`, { sizeGib, ...(opts.mountPath !== undefined ? { mountPath: opts.mountPath } : {}) });
|
|
707
711
|
if (handleApproval(res, opts.json))
|
|
708
712
|
return;
|
|
709
713
|
if (opts.json)
|
package/dist/commands/domain.js
CHANGED
|
@@ -35,8 +35,6 @@ export async function domainSearch(keyword, opts, deps) {
|
|
|
35
35
|
}
|
|
36
36
|
export async function domainBuy(name, opts, deps) {
|
|
37
37
|
const { api, project: p } = await domainDeps(deps);
|
|
38
|
-
const branch = opts.branch ?? p.branch;
|
|
39
|
-
const { target } = await domainTarget(api, p.projectId, branch, name, opts.group);
|
|
40
38
|
// JSON.stringify drops undefined but keeps NaN as null, which the platform rejects as a type
|
|
41
39
|
// error rather than a bad term — so a malformed --years is refused here, with the reason.
|
|
42
40
|
let years;
|
|
@@ -45,38 +43,65 @@ export async function domainBuy(name, opts, deps) {
|
|
|
45
43
|
if (!Number.isInteger(years))
|
|
46
44
|
die(`--years must be a whole number of years, not ${opts.years}`);
|
|
47
45
|
}
|
|
48
|
-
const res = await api.rawRequest('POST', `/projects/${p.projectId}/domains/orders`, { domainName: name, years
|
|
46
|
+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/domains/orders`, { domainName: name, years });
|
|
49
47
|
if (handleApproval(res, opts.json))
|
|
50
48
|
return;
|
|
51
49
|
if (opts.json)
|
|
52
50
|
return printJson(res.body);
|
|
53
51
|
const { order } = res.body;
|
|
54
52
|
info(`${order.domainName} — ${usd(order.priceCents)} for ${order.years} year${order.years === 1 ? '' : 's'}${order.renewalPriceCents !== null ? `, then ${usd(order.renewalPriceCents)}/yr` : ''}`);
|
|
55
|
-
info(`attaches to ${target.name}${order.branch ? ` (branch ${order.branch})` : ''} as ${order.domainName} and www.${order.domainName} once paid`);
|
|
56
53
|
presentUrl(order.checkoutUrl, 'Complete the payment in your browser:', opts.open);
|
|
57
|
-
info(`then: insta domain
|
|
54
|
+
info(`then attach it: insta domain attach ${order.domainName}`);
|
|
55
|
+
}
|
|
56
|
+
// The bought name `host` sits under. At most one name can match: only apex names are sold, so no bought
|
|
57
|
+
// domain is ever a subdomain of another.
|
|
58
|
+
export function ownerOf(host, owned) {
|
|
59
|
+
return owned.find((d) => host === d.domainName || host.endsWith(`.${d.domainName}`)) ?? null;
|
|
58
60
|
}
|
|
59
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Point one hostname at a compute service. `host` is a bought name — which binds it and its www —
|
|
63
|
+
* or any subdomain of one, which binds only that; the rest of the domain is left as it is.
|
|
64
|
+
*/
|
|
65
|
+
export async function domainAttach(host, opts, deps) {
|
|
60
66
|
const { api, project: p } = await domainDeps(deps);
|
|
67
|
+
const name = host.trim().toLowerCase();
|
|
68
|
+
const { items } = await api.request('GET', `/projects/${p.projectId}/domains`);
|
|
69
|
+
const owner = ownerOf(name, items);
|
|
70
|
+
if (!owner) {
|
|
71
|
+
// The domains list holds registered names only; a bought name still registering is an order.
|
|
72
|
+
const { items: orders } = await api.request('GET', `/projects/${p.projectId}/domains/orders`);
|
|
73
|
+
const o = ownerOf(name, orders);
|
|
74
|
+
if (o)
|
|
75
|
+
die(`${o.domainName} is not registered yet — its order is ${o.status}: insta domain status ${o.domainName}`);
|
|
76
|
+
die(`no domain this org bought covers ${name} — for a domain you own elsewhere: insta compute set-domain ${name}`);
|
|
77
|
+
}
|
|
61
78
|
const branch = opts.branch ?? p.branch;
|
|
62
79
|
const { target } = await domainTarget(api, p.projectId, branch, name, opts.group);
|
|
63
|
-
const
|
|
80
|
+
const hostname = name === owner.domainName ? undefined : name;
|
|
81
|
+
const res = await api.rawRequest('POST', `/projects/${p.projectId}/domains/${encodeURIComponent(owner.domainName)}/attach`, { hostname, branch, group: target.name });
|
|
64
82
|
if (handleApproval(res, opts.json))
|
|
65
83
|
return;
|
|
66
84
|
if (opts.json)
|
|
67
85
|
return printJson(res.body);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
86
|
+
// What THIS call asked for. Reading it back off the answer would also name a hostname some
|
|
87
|
+
// earlier attach left pending, which this command did not touch.
|
|
88
|
+
const asked = hostname ? [hostname] : [owner.domainName, `www.${owner.domainName}`];
|
|
89
|
+
info(`${asked.join(' and ')} will attach to ${target.name}`);
|
|
90
|
+
info(`then: insta domain status ${owner.domainName}`);
|
|
71
91
|
}
|
|
72
92
|
// ---- list / status ----
|
|
73
93
|
function domainLines(d) {
|
|
74
|
-
const out = [`${d.domainName} ${d.status}${d.
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
const out = [`${d.domainName} ${d.status}${d.expiresAt ? ` (expires ${d.expiresAt.slice(0, 10)}${d.autorenew ? ', auto-renews' : ''})` : ''}`];
|
|
95
|
+
// Vacuously true for a domain with no hostnames, which is every domain until something attaches.
|
|
96
|
+
if (d.hostnames.every((h) => h.state === 'failed')) {
|
|
97
|
+
const names = d.hostnames.map((h) => h.hostname);
|
|
98
|
+
// Attaching the bought name itself re-attaches its www.
|
|
99
|
+
const retry = names.includes(d.domainName) ? names.filter((h) => h !== `www.${d.domainName}`) : names;
|
|
100
|
+
out.push(` nothing serving — ${(retry.length ? retry : [d.domainName]).map((h) => `insta domain attach ${h}`).join('; ')}`);
|
|
101
|
+
}
|
|
77
102
|
const w = Math.max(0, ...d.hostnames.map((x) => x.hostname.length));
|
|
78
103
|
for (const h of d.hostnames)
|
|
79
|
-
out.push(` ${h.hostname.padEnd(w)} ${h.state}${h.reason ? ` — ${h.reason}` : ''}`);
|
|
104
|
+
out.push(` ${h.hostname.padEnd(w)} ${h.state}${h.service ? ` → ${h.service}` : ''}${h.reason ? ` — ${h.reason}` : ''}`);
|
|
80
105
|
return out;
|
|
81
106
|
}
|
|
82
107
|
function orderStatusLines(o) {
|
|
@@ -91,7 +116,7 @@ export async function domainList(opts, deps) {
|
|
|
91
116
|
if (opts.json)
|
|
92
117
|
return printJson(r);
|
|
93
118
|
if (!r.items.length)
|
|
94
|
-
return info('no domains bought through InstaCloud in this
|
|
119
|
+
return info('no domains bought through InstaCloud in this org (search: insta domain search <keyword>)');
|
|
95
120
|
for (const d of r.items)
|
|
96
121
|
for (const line of domainLines(d))
|
|
97
122
|
info(line);
|
|
@@ -99,6 +124,7 @@ export async function domainList(opts, deps) {
|
|
|
99
124
|
export async function domainStatus(name, opts, deps) {
|
|
100
125
|
const { api, project: p } = await domainDeps(deps);
|
|
101
126
|
const host = name.trim().toLowerCase();
|
|
127
|
+
// Both are the ORG's; the project in the path is the scope the agent policy is read at.
|
|
102
128
|
const [{ items: domains }, { items: orders }] = await Promise.all([
|
|
103
129
|
api.request('GET', `/projects/${p.projectId}/domains`),
|
|
104
130
|
api.request('GET', `/projects/${p.projectId}/domains/orders`),
|
|
@@ -106,7 +132,7 @@ export async function domainStatus(name, opts, deps) {
|
|
|
106
132
|
const domain = domains.find((d) => d.domainName === host) ?? null;
|
|
107
133
|
const order = orders.find((o) => o.domainName === host) ?? null;
|
|
108
134
|
if (!domain && !order)
|
|
109
|
-
die(`${host} was not bought through this
|
|
135
|
+
die(`${host} was not bought through this org`);
|
|
110
136
|
if (opts.json)
|
|
111
137
|
return printJson({ domain, order });
|
|
112
138
|
for (const line of domain ? domainLines(domain) : orderStatusLines(order))
|
|
@@ -90,6 +90,7 @@ export function servicesAddRequestBody(type, name, branch, opts) {
|
|
|
90
90
|
// false. Omitted means the platform default.
|
|
91
91
|
...(opts.alwaysOn !== undefined ? { alwaysOn: opts.alwaysOn } : {}),
|
|
92
92
|
...(opts.volume !== undefined ? { volumeGib: parseVolumeGib(opts.volume) } : {}),
|
|
93
|
+
...(opts.mountPath !== undefined ? { volumeMountPath: opts.mountPath } : {}),
|
|
93
94
|
};
|
|
94
95
|
}
|
|
95
96
|
export async function servicesAdd(type, name, opts = {}) {
|
|
@@ -108,6 +109,8 @@ export async function servicesAdd(type, name, opts = {}) {
|
|
|
108
109
|
// Presence, not truthiness: `--no-always-on` is an explicit false and is just as compute-only.
|
|
109
110
|
if (opts.alwaysOn !== undefined && type !== 'compute')
|
|
110
111
|
throw new Error('--always-on / --no-always-on is only valid for compute services (for postgres, use `insta db always-on on|off` after creation)');
|
|
112
|
+
if (opts.mountPath !== undefined && (type !== 'compute' || opts.volume === undefined))
|
|
113
|
+
throw new Error('--mount-path requires --volume on a compute service');
|
|
111
114
|
if (opts.volume !== undefined) {
|
|
112
115
|
if (type !== 'compute')
|
|
113
116
|
throw new Error('--volume is only valid for compute services (postgres has one by default — grow it with `insta db volume --size`)');
|
|
@@ -138,7 +141,7 @@ export async function servicesAdd(type, name, opts = {}) {
|
|
|
138
141
|
export function serviceAddedLine(type, name, branch, svc) {
|
|
139
142
|
const access = svc.type === 'storage' ? ` [${svc.public ? 'public' : 'private'}]` : '';
|
|
140
143
|
const img = svc.image ? ` running ${svc.image}${svc.port ? `:${svc.port}` : ''}` : '';
|
|
141
|
-
const vol = svc.volume_gib ? ` vol ${svc.volume_gib}Gi at /data` : '';
|
|
144
|
+
const vol = svc.volume_gib ? ` vol ${svc.volume_gib}Gi at ${svc.volume_mount_path ?? "/data"}` : '';
|
|
142
145
|
// The major belongs next to the connect hint: it decides which psql/pg_dump to reach for.
|
|
143
146
|
const pg = svc.type === 'postgres' ? pgBadge(svc.pg_version) : '';
|
|
144
147
|
return `added ${type} service ${name} on ${branch ?? 'default'} (${svc.id})${access}${svc.region ? ` ${svc.region}` : ''}${img}${vol}${pg}${svc.domain ? ` — ${svc.domain}` : ''}`;
|
|
@@ -153,7 +156,7 @@ export function pgBadge(v) {
|
|
|
153
156
|
// billingLines in billing.ts). Compute rows show the running image when the platform reports one.
|
|
154
157
|
export function serviceListLine(s) {
|
|
155
158
|
const extra = s.type === 'compute'
|
|
156
|
-
? ` x${s.machine_count}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}${s.image ? ` running ${s.image}${s.port ? `:${s.port}` : ''}` : ''}`
|
|
159
|
+
? ` x${s.machine_count}${s.volume_gib ? ` vol ${s.volume_gib}Gi at ${s.volume_mount_path ?? '/data'}` : ''}${s.image ? ` running ${s.image}${s.port ? `:${s.port}` : ''}` : ''}`
|
|
157
160
|
: ['redis', 'mysql', 'mongodb'].includes(s.type) ? ` tcp/${s.port ?? defaultDatabasePort(s.type)}${s.volume_gib ? ` vol ${s.volume_gib}Gi` : ''}`
|
|
158
161
|
: s.type === 'storage' ? ` ${s.public ? 'public' : 'private'}`
|
|
159
162
|
// Postgres major, so the reader picks matching pg_dump/psql BEFORE connecting (a newer client
|
|
@@ -352,7 +352,14 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
352
352
|
const onAutoResolved = quiet ? undefined : (v) => info(` ${v.name}: ${v.generate ? `platform-generated (${v.generate})` : `default (${v.default})`}`);
|
|
353
353
|
const variables = await resolveVariables(vars, given, { tty, ask, onAutoResolved });
|
|
354
354
|
// The endpoint takes the branch NAME directly (branchId is its uuid alias) — no lookup needed.
|
|
355
|
-
|
|
355
|
+
// Presence, not truthiness: `--region ''` must reach the platform's 400 rather than be dropped
|
|
356
|
+
// into the default region. An omitted flag still sends no key, so a retry keeps the recorded one.
|
|
357
|
+
const body = {
|
|
358
|
+
...(mode.kind === 'registry' ? { templateCode: mode.code } : { manifest }),
|
|
359
|
+
branch: branchName,
|
|
360
|
+
variables,
|
|
361
|
+
...(opts.region !== undefined ? { region: opts.region } : {}),
|
|
362
|
+
};
|
|
356
363
|
let res;
|
|
357
364
|
try {
|
|
358
365
|
res = await api.rawRequest('POST', `/projects/${p.projectId}/template-deployments`, body);
|
|
@@ -371,9 +378,10 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
371
378
|
if (handleApproval(res, opts.json))
|
|
372
379
|
return;
|
|
373
380
|
const deploymentId = res.body.deploymentId ?? (res.body.deployment ?? res.body).id;
|
|
381
|
+
const acceptedRegion = (res.body.deployment ?? res.body).region;
|
|
374
382
|
const codeLabel = manifest?.code ?? target;
|
|
375
383
|
if (!quiet)
|
|
376
|
-
info(`deploying template ${codeLabel} to branch ${branchName} (${deploymentId})`);
|
|
384
|
+
info(`deploying template ${codeLabel} to branch ${branchName}${acceptedRegion ? ` in ${acceptedRegion}` : ''} (${deploymentId})`);
|
|
377
385
|
// The poll route is keyed by deployment id, not project: name the project so agent mode signs
|
|
378
386
|
// with the project-bound session (a bootstrap session is rejected as "for a different project").
|
|
379
387
|
const dep = await watchDeployment((id) => api.request('GET', `/template-deployments/${id}`, undefined, { projectId: p.projectId }), deploymentId, quiet ? () => { } : info, deps.wait);
|
package/dist/index.js
CHANGED
|
@@ -150,7 +150,8 @@ svc.command('add [type] [name]').description('Provision a service on demand (ass
|
|
|
150
150
|
.option('--port <n>', 'compute only: port the image listens on (default 8080)')
|
|
151
151
|
.option('--always-on', 'compute only: create as always-on — never scales to zero (the default for new compute services; all plans; billing is actual usage either way)')
|
|
152
152
|
.option('--no-always-on', 'compute only: create as scale-to-zero — idle machines suspend and wake on the next request')
|
|
153
|
-
.option('--
|
|
153
|
+
.option('--mount-path <path>', 'compute only: container mount path for a new volume (requires --volume; default /data; fixed after attachment)')
|
|
154
|
+
.option('--volume <gi>', 'compute only: attach a persistent volume of this many whole Gi (also attachable later: `insta compute volume <name> --size <gi>`). Any plan may attach up to its own plan cap (10Gi free, 50Gi paid by default; the bare `insta compute volume <name>` read prints it as plan max); a size above the free cap is paid. Volume services keep 1 machine and stop (cold wake) instead of suspend when idle')
|
|
154
155
|
.option('--json')
|
|
155
156
|
.action(guard(async (type, name, o) => {
|
|
156
157
|
const a = await resolveServiceArgs(type, name, serviceArgsDeps(o.json), o);
|
|
@@ -278,7 +279,8 @@ compute.command('watch-paths [service]').description("Show or change which paths
|
|
|
278
279
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => githubCmd.computeWatchPaths(service, o)));
|
|
279
280
|
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')
|
|
280
281
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => githubCmd.computeDisconnectRepo(service, o)));
|
|
281
|
-
compute.command('volume [service]').description("Show, attach, grow, or delete a compute service's persistent
|
|
282
|
+
compute.command('volume [service]').description("Show, attach, grow, or delete a compute service's persistent volume. No flag: print size, mount path, and the plan cap (any plan). --size on a volumeless service ATTACHES one (any plan up to its own plan cap — 10Gi free, 50Gi paid by default — which is also what a disk with no size named is born at; a size above the free cap is paid; the disk mounts at --mount-path (default /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")
|
|
283
|
+
.option('--mount-path <path>', 'container mount path for a new volume (requires --size; default /data; fixed after attachment)')
|
|
282
284
|
.option('--size <gi>', 'new size in whole Gi, e.g. 10 (must be ≥ the current size)')
|
|
283
285
|
.option('--delete', 'destroy the volume and ALL its data (irreversible; download anything you need first)')
|
|
284
286
|
.option('--json').option('--branch <branch>', 'branch (default: current)').action(guard((service, o) => computeCmd.computeVolume(service, o)));
|
|
@@ -335,6 +337,7 @@ tpl.command('info <code>').description('Show a template: version, upstream pin,
|
|
|
335
337
|
.option('--json').action(guard((code, o) => template.templateInfo(code, o)));
|
|
336
338
|
tpl.command('deploy <code-or-dir-or-url>').description('Deploy a template onto a branch — a registry code, a local directory containing insta.template.yaml (a path-looking target is always read as a directory), or a github.com URL (https://github.com/<owner>/<repo>[/tree/<ref>[/<dir>]]) whose manifest is fetched with your own git credentials. Missing required variables are prompted for on a terminal; generator-backed (secret:N) and defaulted ones are resolved by the platform')
|
|
337
339
|
.option('--branch <b>', 'target branch (default: current)')
|
|
340
|
+
.option('--region <region>', 'region for every service the template creates, e.g. us-east (see `insta regions`)')
|
|
338
341
|
.option('--set <NAME=value>', 'set a template variable (repeatable)', (v, prev) => [...prev, v], [])
|
|
339
342
|
.option('-y, --yes', 'non-interactive: missing required variables fail with a --set list instead of prompting')
|
|
340
343
|
.option('--json')
|
|
@@ -361,14 +364,14 @@ const dom = program.command('domain').description('Buy a domain through InstaClo
|
|
|
361
364
|
dom.command('search <keyword>').description('Search purchasable names with prices (a label like "myapp" or a full name like "myapp.com")')
|
|
362
365
|
.option('--tlds <list>', 'comma-separated TLDs to include').option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
363
366
|
.action(guard((keyword, o) => domainCmd.domainSearch(keyword, o)));
|
|
364
|
-
dom.command('buy <name>').description('Buy a domain
|
|
365
|
-
.option('--years <n>', 'registration term in years (default 1)')
|
|
367
|
+
dom.command('buy <name>').description('Buy a domain — pay at the printed Stripe Checkout link. It serves nothing until you attach it (gated: domain.purchase)')
|
|
368
|
+
.option('--years <n>', 'registration term in years (default 1)')
|
|
366
369
|
.option('--no-open', 'print the checkout URL instead of opening a browser').option('--json')
|
|
367
370
|
.action(guard((name, o) => domainCmd.domainBuy(name, o)));
|
|
368
|
-
dom.command('attach <
|
|
371
|
+
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)')
|
|
369
372
|
.option('--branch <b>').option('--group <g>', "compute service (default: the branch's sole compute service)").option('--json')
|
|
370
|
-
.action(guard((
|
|
371
|
-
dom.command('list').description(
|
|
373
|
+
.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')
|
|
372
375
|
.action(guard((o) => domainCmd.domainList(o)));
|
|
373
376
|
dom.command('status <name>').description("A bought domain's order and attach state").option('--json')
|
|
374
377
|
.action(guard((name, o) => domainCmd.domainStatus(name, o)));
|