insta 0.0.78 → 0.0.80
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/api.js +4 -5
- package/dist/commands/auth.js +1 -1
- package/dist/commands/billing.js +7 -5
- package/dist/commands/compute.js +19 -32
- package/dist/commands/db.js +6 -9
- package/dist/commands/deploy.js +4 -4
- package/dist/commands/domain.js +18 -15
- package/dist/commands/feedback.js +2 -2
- package/dist/commands/manifest.js +1 -2
- package/dist/commands/metrics.js +1 -2
- package/dist/commands/secrets.js +1 -1
- package/dist/commands/services.js +2 -3
- package/dist/commands/setup.js +4 -4
- package/dist/commands/ssh-config.js +29 -30
- package/dist/commands/template.js +1 -1
- package/dist/commands/upgrade.js +3 -4
- package/dist/config.js +5 -6
- package/dist/flyctl-build.js +2 -2
- package/dist/github-source.js +5 -6
- package/dist/index.js +6 -8
- package/dist/observe/hook.js +1 -1
- package/dist/observe/install.js +1 -1
- package/dist/pack-ignore.js +2 -2
- package/dist/resolve-project.js +2 -2
- package/dist/template-manifest.js +1 -1
- package/dist/util.js +7 -9
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -67,11 +67,9 @@ export class ApiClient {
|
|
|
67
67
|
}
|
|
68
68
|
// Returns parsed body for status < 400 (incl. 202); throws ApiError otherwise.
|
|
69
69
|
async request(method, path, body, opts = {}) {
|
|
70
|
-
const res = await this.
|
|
70
|
+
const res = await this.rawRequest(method, path, body, opts);
|
|
71
71
|
if (agentMode() && res.status === 202 && res.body?.status === 'approval_required')
|
|
72
72
|
throw new AgentApprovalRequired(res.body);
|
|
73
|
-
if (res.status >= 400)
|
|
74
|
-
throw new ApiError(res.status, res.body?.error ?? `HTTP ${res.status}`, res.body);
|
|
75
73
|
return res.body;
|
|
76
74
|
}
|
|
77
75
|
// Like request but returns {status, body} so callers can branch on 202 (approval_required).
|
|
@@ -93,12 +91,13 @@ export class ApiClient {
|
|
|
93
91
|
const headers = { 'Content-Type': 'application/json', 'Insta-Hints': '1', 'User-Agent': USER_AGENT };
|
|
94
92
|
if (auth && this.cfg.accessToken)
|
|
95
93
|
headers.Authorization = `Bearer ${this.cfg.accessToken}`;
|
|
94
|
+
const payload = body === undefined ? undefined : JSON.stringify(body);
|
|
96
95
|
if (auth && scope.evidence !== false)
|
|
97
|
-
Object.assign(headers, await agentHeaders(this, method, path,
|
|
96
|
+
Object.assign(headers, await agentHeaders(this, method, path, payload ?? '', scope));
|
|
98
97
|
const res = await this.fetchImpl(this.apiUrl + path, {
|
|
99
98
|
method,
|
|
100
99
|
headers,
|
|
101
|
-
body:
|
|
100
|
+
body: payload,
|
|
102
101
|
signal: scope.signal,
|
|
103
102
|
});
|
|
104
103
|
const text = await res.text();
|
package/dist/commands/auth.js
CHANGED
|
@@ -193,7 +193,7 @@ export async function deviceGrant(post, wait = sleepSeconds, open) {
|
|
|
193
193
|
continue;
|
|
194
194
|
} // RFC 8628 §3.5: back off by 5s
|
|
195
195
|
// The platform's per-IP limiter answers a bare HTTP 429 (no OAuth error code) when a poll
|
|
196
|
-
// trips it
|
|
196
|
+
// trips it. That is the same
|
|
197
197
|
// instruction as slow_down: the code is still pending, so back off and keep waiting rather
|
|
198
198
|
// than abort a login the human may be one click away from approving.
|
|
199
199
|
if (e.status === 429) {
|
package/dist/commands/billing.js
CHANGED
|
@@ -5,7 +5,11 @@ import { cycleLine, dimensionLines } from './metrics.js';
|
|
|
5
5
|
export async function resolveOrgId(opts) {
|
|
6
6
|
if (opts.org)
|
|
7
7
|
return opts.org;
|
|
8
|
-
|
|
8
|
+
// `ProjectConfig.orgId` is typed string but INSTA_PROJECT_ID resolves a project with no org.
|
|
9
|
+
const orgId = (await requireProject()).orgId;
|
|
10
|
+
if (!orgId)
|
|
11
|
+
die('INSTA_PROJECT_ID names no organization — set INSTA_ORG_ID, or pass --org <id>');
|
|
12
|
+
return orgId;
|
|
9
13
|
}
|
|
10
14
|
// Format the billing overview into printable lines (pure, so it's unit-testable).
|
|
11
15
|
// `org` is the caller's --org, echoed into the portal hint: `billing` and `billing portal` resolve
|
|
@@ -85,10 +89,8 @@ export async function billing(opts) {
|
|
|
85
89
|
}
|
|
86
90
|
// insta billing upgrade <tier> — start a Stripe Checkout to subscribe the org to a paid tier.
|
|
87
91
|
export async function billingUpgrade(tier, opts) {
|
|
88
|
-
// pro|team, matching what POST /orgs/:orgId/billing/checkout
|
|
89
|
-
//
|
|
90
|
-
// here, and `enterprise` is per-deal and 400s at the server. The suspension hint above now names
|
|
91
|
-
// the org's own tier, so a Team org was being sent to a command that rejected it.
|
|
92
|
+
// pro|team, matching what POST /orgs/:orgId/billing/checkout accepts: `team` is a real
|
|
93
|
+
// self-serve tier, and `enterprise` is per-deal and 400s at the server.
|
|
92
94
|
if (tier !== 'pro' && tier !== 'team')
|
|
93
95
|
die('tier must be pro|team');
|
|
94
96
|
const api = await ApiClient.load();
|
package/dist/commands/compute.js
CHANGED
|
@@ -64,8 +64,7 @@ export function domainGuidanceLines(r, ctx = {}) {
|
|
|
64
64
|
// Whether this provider reports an edge routing target AT ALL. The compute plane's domain view
|
|
65
65
|
// always carries an `ssl` status, so a plane answer without `origin` is a daemon too old to report
|
|
66
66
|
// where the hostname resolves — we cannot confirm routing, and must not call it serving. A Fly
|
|
67
|
-
// answer carries no `ssl` and has no per-hostname origin concept, so its own verdict stands
|
|
68
|
-
// (r2d2 round 1 Critical: "no target reported" was previously treated as ready for both).
|
|
67
|
+
// answer carries no `ssl` and has no per-hostname origin concept, so its own verdict stands.
|
|
69
68
|
const reportsOrigin = (r) => r.ssl !== undefined;
|
|
70
69
|
// Where the hostname actually resolves — the region-specific origin the plane requested vs what
|
|
71
70
|
// Cloudflare holds. Each shape carries its action; absent fields are reported as absent.
|
|
@@ -116,9 +115,8 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
116
115
|
// A record is settled only when the platform says `ok` — or, for a provider that reports no
|
|
117
116
|
// per-record status at all (Fly), when it vouched for the whole set with `configured`. missing,
|
|
118
117
|
// mismatch and never-checked are each outstanding and each add a blocker. Applying this to only
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
// still-pending validation record, could ride under a `serving https://…` line.
|
|
118
|
+
// some records lets an apex whose AAAA is missing, or a still-pending validation record, ride
|
|
119
|
+
// under a `serving https://…` line.
|
|
122
120
|
const verdictOf = (d) => d.status ?? (r.configured ? 'ok' : 'unchecked');
|
|
123
121
|
if (txt) {
|
|
124
122
|
const st = verdictOf(txt);
|
|
@@ -139,7 +137,7 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
139
137
|
}
|
|
140
138
|
else if (reportsOrigin(r)) {
|
|
141
139
|
// The stage is drawn even with no record to draw it from: an omitted stage reads as "not
|
|
142
|
-
// required", when in fact the platform told us nothing to publish
|
|
140
|
+
// required", when in fact the platform told us nothing to publish. Only the plane
|
|
143
141
|
// proves ownership by TXT, so only a plane answer missing one is a problem.
|
|
144
142
|
stage('ownership', 'unknown', 'the platform returned no ownership TXT for this domain — nothing to publish yet; ask an operator');
|
|
145
143
|
blockers.push('no ownership TXT from the platform');
|
|
@@ -148,10 +146,10 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
148
146
|
stage('ownership', 'n/a', '(this provider does not use an ownership TXT)');
|
|
149
147
|
}
|
|
150
148
|
if (routing.length === 0) {
|
|
151
|
-
// No routing record for the hostname — whatever ELSE came back.
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
149
|
+
// No routing record for the hostname — whatever ELSE came back. Keyed on an entirely empty
|
|
150
|
+
// record set, a payload carrying only the ownership TXT skips the stage and adds no blocker,
|
|
151
|
+
// so a `configured: true` answer with a live cert and a confirmed origin prints `serving`
|
|
152
|
+
// for a hostname with nothing pointing at us.
|
|
155
153
|
stage('cname', 'unknown', 'the platform returned no routing record for this domain — nothing to publish yet; ask an operator');
|
|
156
154
|
blockers.push('no routing record from the platform');
|
|
157
155
|
}
|
|
@@ -213,7 +211,7 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
213
211
|
}
|
|
214
212
|
const resolve = domainResolveLine(r);
|
|
215
213
|
out.push(resolve.line);
|
|
216
|
-
// An error STATE is a blocker whether or not the plane sent a reason with it
|
|
214
|
+
// An error STATE is a blocker whether or not the plane sent a reason with it: a row
|
|
217
215
|
// that says `error` has not been observed serving, and saying otherwise is the blackhole lie.
|
|
218
216
|
if (r.status === 'error') {
|
|
219
217
|
stage('error', r.status, r.errorReason || '(the plane reported an error state with no reason)');
|
|
@@ -230,7 +228,7 @@ export function domainStatusLines(r, ctx = {}) {
|
|
|
230
228
|
// `serving` is claimed only when every stage above agreed: the provider says configured, the
|
|
231
229
|
// routing target is confirmed, and NOTHING is outstanding. A blocker beside a `configured: true`
|
|
232
230
|
// answer means the record set and the verdict disagree — report the disagreement, never paper
|
|
233
|
-
// over it with a URL the user would then trust
|
|
231
|
+
// over it with a URL the user would then trust.
|
|
234
232
|
if (r.configured && blockers.length === 0)
|
|
235
233
|
stage('serving', `https://${r.hostname}`, '');
|
|
236
234
|
else
|
|
@@ -249,7 +247,7 @@ export function domainConflictMessage(host, e, services, ctx = {}) {
|
|
|
249
247
|
const owner = m?.[1] && m[1] !== 'another' ? m[1] : undefined;
|
|
250
248
|
const region = m?.[2];
|
|
251
249
|
// The release command must name the OWNER's group, and the branch the user is working on — a
|
|
252
|
-
// command that defaults back to the linked branch would release nothing
|
|
250
|
+
// command that defaults back to the linked branch would release nothing.
|
|
253
251
|
const release = (group) => `insta compute remove-domain ${host}${flags({ group, branch: ctx.branch })}`;
|
|
254
252
|
if (owner) {
|
|
255
253
|
const here = services.find((s) => s.type === 'compute' && s.name === owner);
|
|
@@ -379,14 +377,6 @@ export async function computeStatus(serviceName, opts) {
|
|
|
379
377
|
info(`compute ${serviceName ?? id}: desired=${r.desiredState} live=${r.state}`);
|
|
380
378
|
}
|
|
381
379
|
// ---- exec (one-shot command; no interactive shell/PTY) ----
|
|
382
|
-
// `insta compute exec [service] -- <command> [args…]`: the command must reach the platform
|
|
383
|
-
// byte-for-byte and can itself contain dashes or another `--`, so it can't be a normal commander
|
|
384
|
-
// positional — with `service` optional, commander flattens everything past the literal `--` into
|
|
385
|
-
// one operand list and has no way to tell "no service, command starts here" apart from "service IS
|
|
386
|
-
// the first command token". Splitting argv on the first literal `--` after `compute exec`
|
|
387
|
-
// ourselves, before commander ever parses it, removes the ambiguity; this is the only place in the
|
|
388
|
-
// whole CLI a bare `--` has this meaning, so nothing else is affected. Exported for a direct,
|
|
389
|
-
// network-free unit test — this split is the seam most likely to regress.
|
|
390
380
|
/** The options `insta compute exec` declares — the one source of truth. index.ts builds the
|
|
391
381
|
* commander command from this list, and the payload scan below uses it to know where the CLI's
|
|
392
382
|
* own arguments stop. Adding an option here reaches both. */
|
|
@@ -650,9 +640,9 @@ export function volumeWriteLine(name, body) {
|
|
|
650
640
|
export function volumeDeleteLine(name) {
|
|
651
641
|
return `compute ${name}: volume deleted — the disk and its data are gone; suspend fast-wake and scale-out are back`;
|
|
652
642
|
}
|
|
653
|
-
// Map a DELETE .../volume failure. Pure, exported for tests
|
|
654
|
-
//
|
|
655
|
-
//
|
|
643
|
+
// Map a DELETE .../volume failure. Pure, exported for tests. An older backend has no DELETE
|
|
644
|
+
// route, and what its 404 looks like depends on who answered: the real platform (Fastify, no
|
|
645
|
+
// custom notFound handler) sends its
|
|
656
646
|
// default body {"message":"Route DELETE:/… not found","error":"Not Found"} → ApiError message
|
|
657
647
|
// "Not Found"; a proxy or bodyless 404 leaves ApiError's own "HTTP 404" fallback. BOTH are the
|
|
658
648
|
// generic route-miss shape and mean version skew, not a bug — parroting them would send the user
|
|
@@ -1007,9 +997,7 @@ const sshKeygenVerifyCert = (certPath) => {
|
|
|
1007
997
|
* So the authority is `ssh-keygen -L`, run against a temporary file, and the
|
|
1008
998
|
* real file is only replaced once it passes. Spawning it here costs nothing
|
|
1009
999
|
* new: certNeedsRenewal already runs the same binary on this same path, every
|
|
1010
|
-
* time a certificate exists.
|
|
1011
|
-
* that a subprocess did not belong on the renewal path -- that reasoning was
|
|
1012
|
-
* simply wrong about what the path already does.)
|
|
1000
|
+
* time a certificate exists.
|
|
1013
1001
|
*
|
|
1014
1002
|
* A missing ssh-keygen is a REFUSAL, not a pass: it means we cannot confirm,
|
|
1015
1003
|
* and an unconfirmable certificate must not displace one that works. Nothing
|
|
@@ -1720,11 +1708,10 @@ function processAlive(pid) {
|
|
|
1720
1708
|
* move or remove the lock, so there is no window in which the file is
|
|
1721
1709
|
* missing for something to slip in through.
|
|
1722
1710
|
*
|
|
1723
|
-
* And the takeover NEVER UNLINKS THE PATH
|
|
1724
|
-
*
|
|
1725
|
-
*
|
|
1726
|
-
*
|
|
1727
|
-
* Here the breaker writes its token INTO the inode the two names share.
|
|
1711
|
+
* And the takeover NEVER UNLINKS THE PATH: an unlink-then-create is not tied
|
|
1712
|
+
* to the inode that was verified, so a holder releasing in between lets a
|
|
1713
|
+
* third process create a fresh lock at the path, which the breaker then
|
|
1714
|
+
* deletes. Here the breaker writes its token INTO the inode the two names share.
|
|
1728
1715
|
* `path` keeps its inode throughout, so there is no moment at which the lock
|
|
1729
1716
|
* at `path` can have become somebody else's between the check and the act:
|
|
1730
1717
|
* the holder judged dead cannot release, every other breaker is behind the
|
package/dist/commands/db.js
CHANGED
|
@@ -6,9 +6,7 @@ import { parseVolumeGib, q, resolveSoleService } from './services.js';
|
|
|
6
6
|
// Toggle a postgres service between scale-to-zero (the default: instance suspends when idle,
|
|
7
7
|
// cold-starts on the next connection) and always-on (instance stays warm; idle RAM bills at
|
|
8
8
|
// actual usage). Thin wrapper over PATCH /database/settings {scaleToZero} — insta-db-backed
|
|
9
|
-
// postgres only.
|
|
10
|
-
// insta-db) and this code is retained, not live — Neon-backed services managed their own
|
|
11
|
-
// autosuspend and the platform returned an error for them.
|
|
9
|
+
// postgres only.
|
|
12
10
|
export async function dbAlwaysOn(mode, opts) {
|
|
13
11
|
if (mode !== 'on' && mode !== 'off')
|
|
14
12
|
throw new Error('mode must be on|off');
|
|
@@ -55,9 +53,8 @@ export async function fetchDbInstance(api, projectId, suffix) {
|
|
|
55
53
|
return { kind: 'ok', body: res.body };
|
|
56
54
|
}
|
|
57
55
|
catch (e) {
|
|
58
|
-
// The platform answers a provider-shaped 502 for services with no manageable instance
|
|
59
|
-
//
|
|
60
|
-
// not live): a soft case, not a failure. Everything else stays an error — an expired
|
|
56
|
+
// The platform answers a provider-shaped 502 for services with no manageable instance:
|
|
57
|
+
// a soft case, not a failure. Everything else stays an error — an expired
|
|
61
58
|
// token must not render as "no ceiling set" — but wrapped so the user sees what failed.
|
|
62
59
|
if (e instanceof ApiError && e.status === 502)
|
|
63
60
|
return { kind: 'no-instance' };
|
|
@@ -121,9 +118,6 @@ export async function dbLimits(opts) {
|
|
|
121
118
|
const mem = typeof res.body?.memoryMib === 'number' ? fmtMib(res.body.memoryMib) : (opts.memory ?? 'unchanged');
|
|
122
119
|
info(`postgres ${opts.group ?? 'default'}: ceiling set to ${cpu} / ${mem}`);
|
|
123
120
|
}
|
|
124
|
-
// Render the instance's volume from a database/instance read. Pure, exported for tests. Reads the
|
|
125
|
-
// CANONICAL volume* names only — storageSize/storageGiB are deprecated aliases the platform drops
|
|
126
|
-
// next release, so depending on them here would be a scheduled breakage.
|
|
127
121
|
// Bytes → human units, one decimal above KiB. Local because the metrics payload is the only
|
|
128
122
|
// bytes-denominated read in this file (fmtMib serves the MiB-denominated resize path).
|
|
129
123
|
export function fmtBytes(n) {
|
|
@@ -185,6 +179,9 @@ export async function dbStats(opts) {
|
|
|
185
179
|
for (const line of dbStatsLines(opts.group ?? 'default', res.body))
|
|
186
180
|
info(line);
|
|
187
181
|
}
|
|
182
|
+
// Render the instance's volume from a database/instance read. Pure, exported for tests. Reads the
|
|
183
|
+
// CANONICAL volume* names only — storageSize/storageGiB are deprecated aliases the platform drops
|
|
184
|
+
// next release, so depending on them here would be a scheduled breakage.
|
|
188
185
|
export function dbVolumeLines(group, body) {
|
|
189
186
|
const gib = typeof body?.volumeGib === 'number' ? `${body.volumeGib}Gi` : (typeof body?.volumeSize === 'string' ? body.volumeSize : undefined);
|
|
190
187
|
if (gib === undefined)
|
package/dist/commands/deploy.js
CHANGED
|
@@ -38,9 +38,9 @@ async function discoverLane(api, projectId, branch, opts) {
|
|
|
38
38
|
if (!known.includes(tag)) {
|
|
39
39
|
die(`this platform answered with a source-build lane this CLI does not know (${JSON.stringify(tag)}) — upgrade with \`insta upgrade\``);
|
|
40
40
|
}
|
|
41
|
-
// The tag alone is not the contract: each branch carries a payload this code then trusts
|
|
42
|
-
//
|
|
43
|
-
//
|
|
41
|
+
// The tag alone is not the contract: each branch carries a payload this code then trusts, so
|
|
42
|
+
// it is validated too — an `archive` with malformed limits must not fall back to local
|
|
43
|
+
// defaults the SERVER does not enforce, and a `none` must carry its reason.
|
|
44
44
|
if (tag === 'archive') {
|
|
45
45
|
const l = lane.limits;
|
|
46
46
|
const positive = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v > 0;
|
|
@@ -112,7 +112,7 @@ export function dockerfileExposedPort(dockerfile) {
|
|
|
112
112
|
// This message is for the target that still REQUIRES a Dockerfile: a Fly-backed service, where a
|
|
113
113
|
// directory deploy builds the Dockerfile in the directory and dies without one. On insta-compute
|
|
114
114
|
// the archive lane carries the directory to the gateway and nixpacks builds it, so this dead end is
|
|
115
|
-
//
|
|
115
|
+
// not universal. It names every way forward instead of the bare "add one".
|
|
116
116
|
//
|
|
117
117
|
// It deliberately does NOT say "save the Dockerfile `insta build --explain` prints": that file is
|
|
118
118
|
// not standalone — it COPYs `.nixpacks/nixpkgs-<hash>.nix` support files nixpacks writes beside it,
|
package/dist/commands/domain.js
CHANGED
|
@@ -36,7 +36,10 @@ export async function domainSearch(keyword, opts, deps) {
|
|
|
36
36
|
export async function domainBuy(name, opts, deps) {
|
|
37
37
|
const years = whole('--years', opts.years);
|
|
38
38
|
const { api, project: p } = await domainDeps(deps);
|
|
39
|
-
const
|
|
39
|
+
const orgId = p.orgId || die('this project link names no organization — set INSTA_ORG_ID');
|
|
40
|
+
// The org route still signs for a PROJECT in agent mode: `domain.purchase` is read there, and a
|
|
41
|
+
// bootstrap session names none, so the platform refuses it.
|
|
42
|
+
const res = await api.rawRequest('POST', `/orgs/${orgId}/domains/orders`, { domainName: name, years }, { projectId: p.projectId });
|
|
40
43
|
if (handleApproval(res, opts.json))
|
|
41
44
|
return;
|
|
42
45
|
if (opts.json)
|
|
@@ -57,12 +60,13 @@ export function ownerOf(host, owned) {
|
|
|
57
60
|
*/
|
|
58
61
|
export async function domainAttach(host, opts, deps) {
|
|
59
62
|
const { api, project: p } = await domainDeps(deps);
|
|
63
|
+
const orgId = p.orgId || die('this project link names no organization — set INSTA_ORG_ID');
|
|
60
64
|
const name = host.trim().toLowerCase();
|
|
61
|
-
const { items } = await api.request('GET', `/
|
|
65
|
+
const { items } = await api.request('GET', `/orgs/${orgId}/domains`);
|
|
62
66
|
const owner = ownerOf(name, items);
|
|
63
67
|
if (!owner) {
|
|
64
68
|
// The domains list holds registered names only; a bought name still registering is an order.
|
|
65
|
-
const { items: orders } = await api.request('GET', `/
|
|
69
|
+
const { items: orders } = await api.request('GET', `/orgs/${orgId}/domains/orders`);
|
|
66
70
|
const o = ownerOf(name, orders);
|
|
67
71
|
if (o)
|
|
68
72
|
die(`${o.domainName} is not registered yet — its order is ${o.status}: insta domain status ${o.domainName}`);
|
|
@@ -83,10 +87,10 @@ export async function domainAttach(host, opts, deps) {
|
|
|
83
87
|
info(`then: insta domain status ${owner.domainName}`);
|
|
84
88
|
}
|
|
85
89
|
// ---- list / status ----
|
|
86
|
-
function domainLines(d) {
|
|
90
|
+
function domainLines(d, linked = true) {
|
|
87
91
|
const out = [`${d.domainName} ${d.status}${d.expiresAt ? ` (expires ${d.expiresAt.slice(0, 10)}${d.autorenew ? ', auto-renews' : ''})` : ''}`];
|
|
88
92
|
// Vacuously true for a domain with no hostnames, which is every domain until something attaches.
|
|
89
|
-
if (d.hostnames.every((h) => h.state === 'failed')) {
|
|
93
|
+
if (linked && d.hostnames.every((h) => h.state === 'failed')) {
|
|
90
94
|
const names = d.hostnames.map((h) => h.hostname);
|
|
91
95
|
// Attaching the bought name itself re-attaches its www.
|
|
92
96
|
const retry = names.includes(d.domainName) ? names.filter((h) => h !== `www.${d.domainName}`) : names;
|
|
@@ -97,30 +101,29 @@ function domainLines(d) {
|
|
|
97
101
|
out.push(` ${h.hostname.padEnd(w)} ${h.state}${h.service ? ` → ${h.service}` : ''}${h.reason ? ` — ${h.reason}` : ''}`);
|
|
98
102
|
return out;
|
|
99
103
|
}
|
|
100
|
-
function orderStatusLines(o) {
|
|
104
|
+
function orderStatusLines(o, linked = true) {
|
|
101
105
|
const out = [`order ${o.id}: ${o.domainName} — ${o.status}${o.failedReason ? ` — ${o.failedReason}` : ''}`];
|
|
102
|
-
if (o.status === 'canceled')
|
|
106
|
+
if (linked && o.status === 'canceled')
|
|
103
107
|
out.push(` the checkout closed without payment — order again: insta domain buy ${o.domainName}`);
|
|
104
108
|
return out;
|
|
105
109
|
}
|
|
106
110
|
export async function domainList(opts, deps) {
|
|
107
|
-
const { api,
|
|
108
|
-
const r = await api.request('GET', `/
|
|
111
|
+
const { api, orgId } = await orgDeps(opts, deps);
|
|
112
|
+
const r = await api.request('GET', `/orgs/${orgId}/domains`);
|
|
109
113
|
if (opts.json)
|
|
110
114
|
return printJson(r);
|
|
111
115
|
if (!r.items.length)
|
|
112
116
|
return info('no domains bought through InstaCloud in this org (search: insta domain search <keyword>)');
|
|
113
117
|
for (const d of r.items)
|
|
114
|
-
for (const line of domainLines(d))
|
|
118
|
+
for (const line of domainLines(d, !opts.org))
|
|
115
119
|
info(line);
|
|
116
120
|
}
|
|
117
121
|
export async function domainStatus(name, opts, deps) {
|
|
118
|
-
const { api,
|
|
122
|
+
const { api, orgId } = await orgDeps(opts, deps);
|
|
119
123
|
const host = name.trim().toLowerCase();
|
|
120
|
-
// Both are the ORG's; the project in the path is the scope the agent policy is read at.
|
|
121
124
|
const [{ items: domains }, { items: orders }] = await Promise.all([
|
|
122
|
-
api.request('GET', `/
|
|
123
|
-
api.request('GET', `/
|
|
125
|
+
api.request('GET', `/orgs/${orgId}/domains`),
|
|
126
|
+
api.request('GET', `/orgs/${orgId}/domains/orders`),
|
|
124
127
|
]);
|
|
125
128
|
const domain = domains.find((d) => d.domainName === host) ?? null;
|
|
126
129
|
const order = orders.find((o) => o.domainName === host) ?? null;
|
|
@@ -128,7 +131,7 @@ export async function domainStatus(name, opts, deps) {
|
|
|
128
131
|
die(`${host} was not bought through this org`);
|
|
129
132
|
if (opts.json)
|
|
130
133
|
return printJson({ domain, order });
|
|
131
|
-
for (const line of domain ? domainLines(domain) : orderStatusLines(order))
|
|
134
|
+
for (const line of domain ? domainLines(domain, !opts.org) : orderStatusLines(order, !opts.org))
|
|
132
135
|
info(line);
|
|
133
136
|
}
|
|
134
137
|
export function recordLines(records) {
|
|
@@ -39,7 +39,7 @@ const FEEDBACK_ENDPOINT = process.env.INSTA_FEEDBACK_URL ||
|
|
|
39
39
|
'https://feedback.instacloud.com/v1/feedback';
|
|
40
40
|
const FEEDBACK_INGEST_TOKEN = process.env.INSTA_FEEDBACK_TOKEN || 'insta-feedback-public-v1';
|
|
41
41
|
// 15s gives the backend's scale-to-zero cold start room to answer (the ingest service waits out
|
|
42
|
-
// the DB wake and persists, so a report can land after
|
|
42
|
+
// the DB wake and persists, so a report can land after a shorter deadline gave up on it).
|
|
43
43
|
// An expired deadline is reported as UNCONFIRMED, not failed — the report may well be stored.
|
|
44
44
|
const FEEDBACK_TIMEOUT_MS = 15_000;
|
|
45
45
|
const MAX_FILE_BYTES = 256 * 1024;
|
|
@@ -153,7 +153,7 @@ export async function buildPayload(opts, ctx) {
|
|
|
153
153
|
branch: project?.branch,
|
|
154
154
|
};
|
|
155
155
|
}
|
|
156
|
-
/** One POST,
|
|
156
|
+
/** One POST, one bounded attempt (FEEDBACK_TIMEOUT_MS), zero retries — feedback is a side quest and must never hang the CLI.
|
|
157
157
|
* Transport and server failures come back as a result, not an exception: the caller downgrades
|
|
158
158
|
* them to a warning so a broken feedback backend can't fail the user's actual task. */
|
|
159
159
|
export async function submit(payload, fetchImpl) {
|
|
@@ -6,8 +6,7 @@ import { info, printJson } from '../util.js';
|
|
|
6
6
|
*
|
|
7
7
|
* `kind` is the platform's internal resource kind, and for compute it is always 'fly' — 'fly' is
|
|
8
8
|
* the compute SEAT, occupied by the microvm plane on any environment that has cut over. Printing
|
|
9
|
-
* it
|
|
10
|
-
* Fly (staging, 2026-08-25: `fly(api)` for a row serving from warm pod insta-warm-00178a-16).
|
|
9
|
+
* it tells users and agents that a microvm-backed service runs on Fly.
|
|
11
10
|
*
|
|
12
11
|
* So for compute rows the label is the platform's explicit `provider`, and when that is absent --
|
|
13
12
|
* an older platform, or a row whose provider the platform itself could not determine -- we fall
|
package/dist/commands/metrics.js
CHANGED
|
@@ -98,8 +98,7 @@ function printDimensions(dims) {
|
|
|
98
98
|
// insta usage — usage across the 5 billing dimensions (cpu/memory/volume/egress/storage) for the
|
|
99
99
|
// current billing cycle. Shows the whole ORG by default (with a per-project breakdown); pass --proj
|
|
100
100
|
// [id] for a single project (the linked one, or a given id). Billed dimensions, not raw provider
|
|
101
|
-
// meters.
|
|
102
|
-
// though the adapter code is retained, not live.)
|
|
101
|
+
// meters.
|
|
103
102
|
export async function usage(opts) {
|
|
104
103
|
const api = await ApiClient.load();
|
|
105
104
|
const p = await requireProject();
|
package/dist/commands/secrets.js
CHANGED
|
@@ -165,7 +165,7 @@ async function readStdin() {
|
|
|
165
165
|
// Every platform SkipReason (applyTargets.ts) except 'unknown-service', which means the plan
|
|
166
166
|
// could not resolve the service, not that it correctly chose to skip it.
|
|
167
167
|
const BENIGN_SKIP_REASONS = new Set(['no-image', 'other-branch', 'caller-deploying', 'skip-deploy']);
|
|
168
|
-
//
|
|
168
|
+
// Three outcomes: an unwritten entry is a hard failure; a written entry with a
|
|
169
169
|
// failed or unexplained-skip service is durable but not fully live; anything else is success.
|
|
170
170
|
export function applyVerdict(entries, services) {
|
|
171
171
|
if (entries.some((e) => !e.written))
|
|
@@ -85,9 +85,8 @@ export function servicesAddRequestBody(type, name, branch, opts) {
|
|
|
85
85
|
type, name, ...(branch ? { branch } : {}), public: !!opts.public,
|
|
86
86
|
...(opts.image ? { image: opts.image } : {}), ...(opts.port ? { port: parsePort(opts.port) } : {}),
|
|
87
87
|
...(opts.region ? { region: opts.region } : {}),
|
|
88
|
-
// Sent whenever the flag was given, false included: compute is born always-on by default
|
|
89
|
-
//
|
|
90
|
-
// false. Omitted means the platform default.
|
|
88
|
+
// Sent whenever the flag was given, false included: compute is born always-on by default,
|
|
89
|
+
// so `--no-always-on` must reach the API as an explicit false. Omitted means the platform default.
|
|
91
90
|
...(opts.alwaysOn !== undefined ? { alwaysOn: opts.alwaysOn } : {}),
|
|
92
91
|
...(opts.volume !== undefined ? { volumeGib: parseVolumeGib(opts.volume) } : {}),
|
|
93
92
|
...(opts.mountPath !== undefined ? { volumeMountPath: opts.mountPath } : {}),
|
package/dist/commands/setup.js
CHANGED
|
@@ -21,7 +21,7 @@ import { projectCreate, projectLink, slugifyName } from './project.js';
|
|
|
21
21
|
import { envUse } from './env.js';
|
|
22
22
|
import { installAgentConfigs } from './mcp.js';
|
|
23
23
|
import { detectChannel } from './upgrade.js';
|
|
24
|
-
export { resolveSpawnable
|
|
24
|
+
export { resolveSpawnable } from '../spawn.js';
|
|
25
25
|
// The `skills` tool we shell out to prints a clack UI: a frame-by-frame clone spinner, an
|
|
26
26
|
// "Installing to all N agents" banner, a full N-line install-path box, and a third-party
|
|
27
27
|
// "Security Risk Assessment" that flags our OWN first-party skill as "Critical Risk". Streamed
|
|
@@ -65,7 +65,7 @@ export function parseInstalledAgents(output) {
|
|
|
65
65
|
// The tool boxes each line ("│ → ~/.claude/skills/insta │"), so don't anchor to EOL.
|
|
66
66
|
// Separator-agnostic: on Windows the tool prints C:\Users\…\.claude\skills\insta — a
|
|
67
67
|
// forward-slash-only match found nothing there, collapsing the summary to a nameless
|
|
68
|
-
// "Agents set up"
|
|
68
|
+
// "Agents set up".
|
|
69
69
|
const m = plain.match(/→\s*(\S+)[\\/]skills[\\/][A-Za-z0-9_-]+/);
|
|
70
70
|
if (m && m[1])
|
|
71
71
|
paths.add(m[1]);
|
|
@@ -256,7 +256,7 @@ export function requireMcpRegistration(status) {
|
|
|
256
256
|
/** The environment `setup agent` should target, and whether the machine must be switched to it
|
|
257
257
|
* first. Pure — decides only; the caller performs the switch.
|
|
258
258
|
*
|
|
259
|
-
* The contract
|
|
259
|
+
* The contract: the public one-liner `npx -y insta setup agent` means PRODUCTION,
|
|
260
260
|
* full stop — a leftover `insta env use staging` from last month must not silently give a new
|
|
261
261
|
* onboarding run staging skills. Staging is an explicit ask: `--env staging` (or $INSTA_ENV).
|
|
262
262
|
* Two deliberate exceptions leave the machine alone:
|
|
@@ -442,7 +442,7 @@ export async function setupAgent(opts, run = defaultRunner, mint, installConfigs
|
|
|
442
442
|
// --project / --create: bind this directory to a project inside the SAME process. Never split
|
|
443
443
|
// this back into `setup agent && insta project <cmd>` as one paste: no shell joiner survives
|
|
444
444
|
// every Windows shell, and in shells without bracketed paste the queued second line is eaten
|
|
445
|
-
// as the answer to the login prompt above
|
|
445
|
+
// as the answer to the login prompt above. Both need the session: without
|
|
446
446
|
// one the manual command is the hint, never a hang; a failure (bad id, no access, name taken)
|
|
447
447
|
// is a REAL error — binding a project is the entire point of the flag — so it sets the exit
|
|
448
448
|
// code instead of pretending setup succeeded.
|
|
@@ -139,24 +139,29 @@ export function renderConfigBlock(o) {
|
|
|
139
139
|
// IdentitiesOnly is not tidiness. SSH offers public keys ONE AT A TIME,
|
|
140
140
|
// so a user with several keys is identified non-deterministically -- the
|
|
141
141
|
// server sees whichever key happened to be offered first, which may not be
|
|
142
|
-
// the one carrying our certificate.
|
|
143
|
-
//
|
|
144
|
-
// unexplainable auth failures.
|
|
142
|
+
// the one carrying our certificate. Without this line a developer with a
|
|
143
|
+
// full ssh-agent gets intermittent, unexplainable auth failures.
|
|
145
144
|
' IdentitiesOnly yes');
|
|
146
145
|
// Connection multiplexing collapses scp, an IDE's several connections and a
|
|
147
146
|
// second terminal onto ONE connection; without it a single developer can
|
|
148
147
|
// reach the per-service session cap in an afternoon.
|
|
149
148
|
//
|
|
150
|
-
//
|
|
149
|
+
// The socket is keyed on %C -- a hash of (local host, remote host, port,
|
|
150
|
+
// user) -- not %r@%h:%p. A ControlPath is a Unix-domain socket, whose path
|
|
151
|
+
// is capped at 104 bytes on macOS (108 on Linux), and the route-key user
|
|
152
|
+
// plus the regional gateway hostname sailed past that: a real prod alias
|
|
153
|
+
// rendered a 109-byte path and every `ssh` died on `ControlPath too long`.
|
|
154
|
+
// %C is a fixed 40 hex chars however long the host and user grow, so the
|
|
155
|
+
// path can no longer overflow; it also drops the `:` the old token carried.
|
|
156
|
+
//
|
|
157
|
+
// OMITTED ON WINDOWS, where it is not an optimisation but a broken config:
|
|
151
158
|
// Win32-OpenSSH does not implement ControlMaster (PowerShell/Win32-OpenSSH
|
|
152
|
-
// #1328, #405) and
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
// `ssh` and skip on Windows, so this branch is asserted on the rendered
|
|
157
|
-
// text instead.
|
|
159
|
+
// #1328, #405) and FAILS the connection rather than ignoring the directive,
|
|
160
|
+
// so every alias would be unusable on a platform this repo runs CI for. The
|
|
161
|
+
// effective-config tests need a real `ssh` and skip on Windows, so this
|
|
162
|
+
// branch is asserted on the rendered text instead.
|
|
158
163
|
if ((o.platform ?? process.platform) !== 'win32') {
|
|
159
|
-
lines.push(' ControlMaster auto', ' ControlPath ~/.insta/ssh/cm-%
|
|
164
|
+
lines.push(' ControlMaster auto', ' ControlPath ~/.insta/ssh/cm-%C', ' ControlPersist 10m');
|
|
160
165
|
}
|
|
161
166
|
if (o.ensureCertCommand) {
|
|
162
167
|
// Renewal happens while OpenSSH PARSES the config, before it connects, so
|
|
@@ -328,11 +333,10 @@ function isEcdsaBody(curve, point, wantCurve, pointLen) {
|
|
|
328
333
|
/** Exactly ONE OpenSSH public-key record: `<type> <base64>` with an optional
|
|
329
334
|
* comment, and nothing else -- no second line, no leading directive.
|
|
330
335
|
*
|
|
331
|
-
*
|
|
332
|
-
*
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* part of it is honoured. */
|
|
336
|
+
* Parsing to the three fields we will actually write, and rebuilding the line
|
|
337
|
+
* from THOSE, means a value either is one key record or is refused; there is
|
|
338
|
+
* no third outcome where part of it is honoured (an embedded newline would
|
|
339
|
+
* otherwise smuggle additional known_hosts lines through). */
|
|
336
340
|
export function parseCAPublicKey(value) {
|
|
337
341
|
if (typeof value !== 'string')
|
|
338
342
|
throw new Error('the platform returned no ssh certificate authority key');
|
|
@@ -349,22 +353,18 @@ export function parseCAPublicKey(value) {
|
|
|
349
353
|
if (!/^[A-Za-z0-9+/]+={0,3}$/.test(blob) || blob.length < 32) {
|
|
350
354
|
throw new Error('refusing a certificate authority key whose body is not base64');
|
|
351
355
|
}
|
|
352
|
-
// The blob's OWN type must agree with the text field
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
// and an anchor built from that installs silently, then fails at connect
|
|
359
|
-
// time, where the message points at known_hosts rather than at the response
|
|
360
|
-
// that produced it.
|
|
356
|
+
// The blob's OWN type must agree with the text field, and the WHOLE blob is
|
|
357
|
+
// checked, not just its first field: a first-field check rejects arbitrary
|
|
358
|
+
// base64 and still accepts a correct type name followed by noise -- and an
|
|
359
|
+
// anchor built from that installs silently, then fails at connect time,
|
|
360
|
+
// where the message points at known_hosts rather than at the response that
|
|
361
|
+
// produced it.
|
|
361
362
|
const fields = sshBlobFields(blob);
|
|
362
363
|
if (!fields || fields.length < 2 || fields[0].toString('utf8') !== type) {
|
|
363
364
|
throw new Error(`refusing a certificate authority key whose body does not match its type ${JSON.stringify(type.slice(0, 32))}`);
|
|
364
365
|
}
|
|
365
|
-
// And the fields must be the ones THIS type has
|
|
366
|
-
//
|
|
367
|
-
// any well-formed fields passed -- see CA_KEY_SHAPES.
|
|
366
|
+
// And the fields must be the ones THIS type has (see CA_KEY_SHAPES): a
|
|
367
|
+
// correct RSA or ECDSA type name followed by any well-formed fields must not pass.
|
|
368
368
|
if (!shape(fields)) {
|
|
369
369
|
throw new Error(`refusing a certificate authority key whose body is not the shape of ${JSON.stringify(type.slice(0, 32))}`);
|
|
370
370
|
}
|
|
@@ -662,8 +662,7 @@ export function planCertAuthority(existing, hostPattern, caKey) {
|
|
|
662
662
|
export function revertCertAuthority(current, plan) {
|
|
663
663
|
const kept = current.split('\n').filter((l) => l.trimEnd() !== plan.line);
|
|
664
664
|
// Only the ONE trailing blank the split leaves behind a final newline. A
|
|
665
|
-
// blank line before that is the user's -- trailing blanks included
|
|
666
|
-
// loop popping every empty tail line was deleting on the failure path.
|
|
665
|
+
// blank line before that is the user's -- trailing blanks included.
|
|
667
666
|
if (kept.length > 0 && kept[kept.length - 1] === '')
|
|
668
667
|
kept.pop();
|
|
669
668
|
// Only the anchors that are genuinely gone: a concurrent install may already
|
|
@@ -329,7 +329,7 @@ export async function templateDeploy(target, opts = {}, deps = {}) {
|
|
|
329
329
|
manifest = fetched.manifest;
|
|
330
330
|
source = fetched.source;
|
|
331
331
|
vars = collectManifestVariables(manifest);
|
|
332
|
-
//
|
|
332
|
+
// Exactly ONE line in front of today's output. A second "deploying template …" line
|
|
333
333
|
// would read as a duplicate of the "deploying template <code> to branch <branch>" line below,
|
|
334
334
|
// so the manifest's code@version rides on this one instead.
|
|
335
335
|
if (!quiet) {
|
package/dist/commands/upgrade.js
CHANGED
|
@@ -9,9 +9,8 @@
|
|
|
9
9
|
// `resolveLatest()`, reading npm's `latest` dist-tag, the same thing `npx insta@latest` and
|
|
10
10
|
// `npm i -g insta` resolve. Both the background check and `insta upgrade` go through it, and the
|
|
11
11
|
// binary channel then installs THAT version by tag (INSTA_VERSION=v<latest>) rather than asking
|
|
12
|
-
// GitHub independently for /releases/latest. Two independent resolvers is how the two paths
|
|
13
|
-
//
|
|
14
|
-
// neither npm nor GitHub).
|
|
12
|
+
// GitHub independently for /releases/latest. Two independent resolvers is how the two paths
|
|
13
|
+
// drift apart: a version can be merged but never tagged, so it exists on neither npm nor GitHub.
|
|
15
14
|
//
|
|
16
15
|
// A CACHE MUST NOT LIE. ~/.insta/update-check.json is a cache of that one answer, never a second
|
|
17
16
|
// source. Every path that learns the real latest rewrites it (including a successful upgrade),
|
|
@@ -348,7 +347,7 @@ export async function autoupdate(mode) {
|
|
|
348
347
|
* `upgrade`/`autoupdate` are the update machinery itself. Every `__` command
|
|
349
348
|
* is internal machinery rather than a user at a prompt, and the rule is
|
|
350
349
|
* written as a PREFIX so the next such command inherits it instead of
|
|
351
|
-
* rediscovering it
|
|
350
|
+
* rediscovering it:
|
|
352
351
|
* `__ssh-ensure-cert` is run by OpenSSH while it PARSES ssh_config, on every
|
|
353
352
|
* ssh, scp, `ssh -G` and IDE connection, so a nudge here is written straight
|
|
354
353
|
* into the ssh session's stderr and an auto-upgrade spawns a detached process
|
package/dist/config.js
CHANGED
|
@@ -16,9 +16,8 @@ const PROJECT_FILE = 'project.json';
|
|
|
16
16
|
// CLI treat the shared link as foreign. It records the project id too, because project.json is
|
|
17
17
|
// committed and this file is not: a pull or checkout can replace the project underneath it.
|
|
18
18
|
const LINK_PLANE_FILE = 'link-plane.json';
|
|
19
|
-
// The cloud API default.
|
|
20
|
-
//
|
|
21
|
-
// Only affects fresh installs: a persisted apiUrl (from a prior login) or INSTA_API_URL wins below.
|
|
19
|
+
// The cloud API default. Only affects fresh installs: a persisted apiUrl (from a prior login) or
|
|
20
|
+
// INSTA_API_URL wins below.
|
|
22
21
|
const DEFAULT_API = ENVS[DEFAULT_ENV].api;
|
|
23
22
|
export async function readGlobal() {
|
|
24
23
|
// Precedence, most explicit first:
|
|
@@ -147,7 +146,7 @@ export async function findProjectRoot(cwd = process.cwd()) {
|
|
|
147
146
|
}
|
|
148
147
|
/** The link that applies to `cwd`, and whether it was made against a DIFFERENT control plane. A
|
|
149
148
|
* project id means nothing on another control plane (cloud, staging and every insta-oss box each
|
|
150
|
-
* have their own)
|
|
149
|
+
* have their own). */
|
|
151
150
|
export async function resolveProjectLink(cwd = process.cwd()) {
|
|
152
151
|
// Linkless targeting (CI / one-offs / agents): INSTA_PROJECT_ID resolves the project with no
|
|
153
152
|
// link file, and beats one when both exist — an explicit parameter outranks ambient state.
|
|
@@ -170,8 +169,8 @@ export async function resolveProjectLink(cwd = process.cwd()) {
|
|
|
170
169
|
catch {
|
|
171
170
|
return null;
|
|
172
171
|
}
|
|
173
|
-
// No sidecar
|
|
174
|
-
//
|
|
172
|
+
// No sidecar (a teammate who just cloned): nothing says which control plane the link belongs
|
|
173
|
+
// to, so it resolves unchecked.
|
|
175
174
|
const record = await readLinkPlane(root);
|
|
176
175
|
if (record) {
|
|
177
176
|
const current = safeUrl((await readGlobal()).apiUrl);
|
package/dist/flyctl-build.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Build a source directory into an image and push it to Fly's registry, using a short-lived,
|
|
2
2
|
// app-scoped deploy token minted by the platform (the CLI never holds a standing Fly credential).
|
|
3
|
-
// Shells out to `flyctl deploy --build-only --push` (remote builder).
|
|
3
|
+
// Shells out to `flyctl deploy --build-only --push` (remote builder).
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { existsSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
@@ -80,7 +80,7 @@ export async function ensureFlyctl() {
|
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
82
|
if (process.platform === 'linux') {
|
|
83
|
-
// A fresh Linux machine (CI containers included
|
|
83
|
+
// A fresh Linux machine (CI containers included) has no flyctl
|
|
84
84
|
// and no brew; without this branch `insta deploy <dir>` dead-ends on a hand-install of a
|
|
85
85
|
// third-party CLI. Official installer, pinned into ~/.fly; the current process extends its
|
|
86
86
|
// own PATH because the installer's shell-profile edit can't reach an already-running process.
|
package/dist/github-source.js
CHANGED
|
@@ -7,7 +7,6 @@ import { tmpdir } from 'node:os';
|
|
|
7
7
|
import { join, sep } from 'node:path';
|
|
8
8
|
import { parseManifestYaml, MANIFEST_FILE } from './template-manifest.js';
|
|
9
9
|
const GITHUB_HOST = /^(?:https?:\/\/)?(?:www\.)?github\.com\//i;
|
|
10
|
-
// An explicit address: a scheme, or an scp-style user@host:path. Never a local path.
|
|
11
10
|
// A scheme (with or without its slashes) or an scp-style user@host:path. The slashes are optional
|
|
12
11
|
// because `https:/github.com/o/r`, a URL that lost one, must be named as a bad address rather than
|
|
13
12
|
// resolved as a directory called `https:`. Two or more scheme characters are required so a Windows
|
|
@@ -49,7 +48,7 @@ function decodedSegments(parts, target) {
|
|
|
49
48
|
}
|
|
50
49
|
/** Parse a github.com URL into owner, repo and the still-unsplit ref+path tail.
|
|
51
50
|
* null = not URL-shaped, so the caller's local-directory and registry modes still get a look.
|
|
52
|
-
* A URL-shaped target that is not a github.com repository URL throws
|
|
51
|
+
* A URL-shaped target that is not a github.com repository URL throws. */
|
|
53
52
|
export function parseGitHubTemplateUrl(target) {
|
|
54
53
|
if (!GITHUB_HOST.test(target)) {
|
|
55
54
|
// Name a non-GitHub address for what it is; let everything else reach local/registry mode.
|
|
@@ -140,7 +139,7 @@ function releaseChild(child) {
|
|
|
140
139
|
}
|
|
141
140
|
catch { /* already gone */ }
|
|
142
141
|
}
|
|
143
|
-
/** spawnFn is injected so the timeout path is testable without a network
|
|
142
|
+
/** spawnFn is injected so the timeout path is testable without a network. */
|
|
144
143
|
export function makeGitRunner(spawnFn = nodeSpawn) {
|
|
145
144
|
return (args, opts) => new Promise((resolve) => {
|
|
146
145
|
// Spawned directly, NOT through resolveSpawnable: that wrapper exists for npm-installed
|
|
@@ -232,7 +231,7 @@ export function splitRefAndPath(refAndPath, refs) {
|
|
|
232
231
|
}
|
|
233
232
|
/** One round trip answers two questions: the default branch, and where the ref ends. The commit
|
|
234
233
|
* is NOT taken from here — an annotated tag lists its tag object, and a branch can move before
|
|
235
|
-
* the clone. fetchGitHubTemplate reads it from the checkout instead
|
|
234
|
+
* the clone. fetchGitHubTemplate reads it from the checkout instead. */
|
|
236
235
|
export async function resolveGitHubRef(t, run) {
|
|
237
236
|
// HEAD is listed EXPLICITLY: adding refspecs otherwise drops the symref line that names the
|
|
238
237
|
// default branch (measured on this repo: 375 refs unfiltered, 188 with the filter, and no
|
|
@@ -285,7 +284,7 @@ export async function fetchGitHubTemplate(target, run = defaultGitRunner, parse
|
|
|
285
284
|
const dir = mkdtempSync(join(tmpdir(), 'insta-tpl-gh-'));
|
|
286
285
|
try {
|
|
287
286
|
// The short name, not resolved.qualifiedRef: `--branch` rejects a fully-qualified ref, and
|
|
288
|
-
// git's own short-name tie-break already agrees with splitRefAndPath
|
|
287
|
+
// git's own short-name tie-break already agrees with splitRefAndPath.
|
|
289
288
|
const cloned = await run(['clone', '--depth', '1', '--quiet', '--branch', resolved.ref, repoUrl(target), dir], { timeoutMs: CLONE_TIMEOUT_MS });
|
|
290
289
|
if (cloned.timedOut)
|
|
291
290
|
throw new Error(`timed out after ${CLONE_TIMEOUT_MS / 1000}s cloning https://github.com/${target.owner}/${target.repo}`);
|
|
@@ -294,7 +293,7 @@ export async function fetchGitHubTemplate(target, run = defaultGitRunner, parse
|
|
|
294
293
|
if (cloned.code !== 0)
|
|
295
294
|
throw new Error(unreadableRepoMessage(target, cloned.stderr));
|
|
296
295
|
// The deployed commit is the one in the checkout: an annotated tag's listing entry is its tag
|
|
297
|
-
// object, and a branch can move between ls-remote and here
|
|
296
|
+
// object, and a branch can move between ls-remote and here.
|
|
298
297
|
const head = await run(['-C', dir, 'rev-parse', 'HEAD'], { timeoutMs: LS_REMOTE_TIMEOUT_MS });
|
|
299
298
|
// Same three outcomes the other two calls distinguish, so a timeout or a missing git here is
|
|
300
299
|
// not reported as an unreadable repository.
|
package/dist/index.js
CHANGED
|
@@ -371,9 +371,11 @@ dom.command('buy <name>').description('Buy a domain — pay at the printed Strip
|
|
|
371
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)')
|
|
372
372
|
.option('--branch <b>').option('--group <g>', "compute service (default: the branch's sole compute service)").option('--json')
|
|
373
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")
|
|
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")
|
|
375
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
375
376
|
.action(guard((o) => domainCmd.domainList(o)));
|
|
376
|
-
dom.command('status <name>').description("A bought domain's order and attach state")
|
|
377
|
+
dom.command('status <name>').description("A bought domain's order and attach state")
|
|
378
|
+
.option('--org <id>', "target org (default: linked project's org)").option('--json')
|
|
377
379
|
.action(guard((name, o) => domainCmd.domainStatus(name, o)));
|
|
378
380
|
const rec = dom.command('records').description('DNS records of a bought domain — the zone InstaCloud holds at the registrar');
|
|
379
381
|
rec.command('list <domain>').description('Every record in the zone, managed ones marked')
|
|
@@ -448,12 +450,8 @@ program.command('autoupdate [mode]').description('Show or set auto-update: on |
|
|
|
448
450
|
program.command('__update-check', { hidden: true }).action(guard(() => selfUpdate.backgroundCheck(cliVersion())));
|
|
449
451
|
// The ssh_config renewal hook. Hidden, and named with the `__` prefix that
|
|
450
452
|
// trackCommand skips, because OpenSSH runs it while PARSING the config on EVERY
|
|
451
|
-
// ssh/scp/`ssh -G`/IDE connection
|
|
452
|
-
// path
|
|
453
|
-
// early -- reading config, possibly creating ~/.insta/telemetry.json, and
|
|
454
|
-
// making a PostHog request with a timeout of up to 1.5s. That is a network
|
|
455
|
-
// round trip on the critical path of every ordinary ssh, which is exactly what
|
|
456
|
-
// the hook was specified not to do.
|
|
453
|
+
// ssh/scp/`ssh -G`/IDE connection: a telemetry round trip here would sit on the
|
|
454
|
+
// critical path of every ordinary ssh.
|
|
457
455
|
program.command('__ssh-ensure-cert <alias>', { hidden: true })
|
|
458
456
|
.action(guard((alias) => computeCmd.ensureCertForAlias(alias)));
|
|
459
457
|
selfUpdate.maybeUpdate(cliVersion(), process.argv);
|
package/dist/observe/hook.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// PostToolUse hook: reads a tool-use event on stdin (Claude Code / Codex), scans every string
|
|
2
|
-
// surface for credential exposure, and appends findings to ./.insta/audit.jsonl.
|
|
2
|
+
// surface for credential exposure, and appends findings to ./.insta/audit.jsonl.
|
|
3
3
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
package/dist/observe/install.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Install the observe hook into a project's agent harness (Claude Code / Codex) and materialize
|
|
2
|
-
// the standalone hook + scanner into ./.insta/observe.
|
|
2
|
+
// the standalone hook + scanner into ./.insta/observe.
|
|
3
3
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
package/dist/pack-ignore.js
CHANGED
|
@@ -139,8 +139,8 @@ function posixClass(name, negated) {
|
|
|
139
139
|
// literal. docker hands the class to Go's regexp: a `]` right after the opening `[` (or after
|
|
140
140
|
// the `^`) is a MEMBER, not the close, `\` quotes the next character, and only `^` negates, so a
|
|
141
141
|
// `!` is an ordinary member. Unlike `*` and `?`, these Go regexp classes CAN match a separator.
|
|
142
|
-
//
|
|
143
|
-
//
|
|
142
|
+
// moby/patternmatcher compile() preserves bracket expressions without adding a separator
|
|
143
|
+
// exclusion (private[^x]token matches private/token).
|
|
144
144
|
function bracket(p, start) {
|
|
145
145
|
let j = start + 1;
|
|
146
146
|
let negated = false;
|
package/dist/resolve-project.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// "One command, just works": when a command needs a project and the directory isn't linked
|
|
2
2
|
// (and no INSTA_PROJECT_ID is set), resolve it instead of lecturing about `project link` —
|
|
3
3
|
// one project auto-selects silently, several get a one-keystroke picker, and either way the
|
|
4
|
-
// choice is SAVED so this happens at most once per directory
|
|
5
|
-
//
|
|
4
|
+
// choice is SAVED so this happens at most once per directory, and the committed link file shares
|
|
5
|
+
// it with the team.
|
|
6
6
|
import { createInterface } from 'node:readline/promises';
|
|
7
7
|
export async function autoResolveProject(orgId, deps) {
|
|
8
8
|
const projects = await deps.listProjects();
|
|
@@ -125,7 +125,7 @@ export function validateManifest(m) {
|
|
|
125
125
|
problems.push(`${where}: web services must declare a healthcheck path`);
|
|
126
126
|
if (svc.healthcheck && !String(svc.healthcheck).startsWith('/'))
|
|
127
127
|
problems.push(`${where}: healthcheck must be an absolute path (start with /)`);
|
|
128
|
-
// Sizing is the platform's, capped for the org's plan
|
|
128
|
+
// Sizing is the platform's, capped for the org's plan. Same answers the
|
|
129
129
|
// publish endpoint gives, said here so an author does not upload to find out. Read as unknown:
|
|
130
130
|
// the type above admits only what is SUPPORTED, and the document is a cast over YAML.parse, so
|
|
131
131
|
// a refused shape arrives as a value that type does not describe.
|
package/dist/util.js
CHANGED
|
@@ -101,8 +101,8 @@ export function resolveThroughSymlink(path) {
|
|
|
101
101
|
/** Linux allows 40; the exact number does not matter, only that the walk ends. */
|
|
102
102
|
const MAX_SYMLINK_HOPS = 40;
|
|
103
103
|
/** How to launch the default browser for `url` on `platform`. Pure so the Windows encoding is
|
|
104
|
-
* testable. On Windows NO shell may ever parse the URL: cmd.exe splits at bare `&` (
|
|
105
|
-
*
|
|
104
|
+
* testable. On Windows NO shell may ever parse the URL: cmd.exe splits at bare `&` (quoting
|
|
105
|
+
* fixes that) but ALSO expands `%…%` sequences even inside quotes, and a percent-encoded
|
|
106
106
|
* OAuth redirect (`http%3A%2F%2F127.0.0.1…`) is nothing but such sequences. So the launch goes
|
|
107
107
|
* through PowerShell's -EncodedCommand: a pure-ASCII script travels as base64(UTF-16LE) — no
|
|
108
108
|
* argument parsing anywhere — and the URL itself rides as a second base64 payload INSIDE that
|
|
@@ -113,11 +113,10 @@ export function openUrlSpawn(url, platform = process.platform,
|
|
|
113
113
|
// directory before PATH, so a planted powershell.exe beside the user's shell would win.
|
|
114
114
|
systemRoot = process.env.SYSTEMROOT ?? process.env.windir ?? 'C:\\Windows') {
|
|
115
115
|
if (platform === 'win32') {
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
// base64 alphabet), so no byte of any URL can terminate anything.
|
|
116
|
+
// Interpolating the URL into a quoted literal is not enough — PowerShell honors smart quotes
|
|
117
|
+
// (U+2018–U+201B) as string delimiters too, so ASCII-only escaping still leaves a breakout.
|
|
118
|
+
// The script below is pure ASCII by construction (the base64 alphabet), so no byte of any
|
|
119
|
+
// URL can terminate anything.
|
|
121
120
|
const urlB64 = Buffer.from(url, 'utf8').toString('base64');
|
|
122
121
|
const script = `Start-Process ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${urlB64}')))`;
|
|
123
122
|
return {
|
|
@@ -149,8 +148,7 @@ export function openUrl(url) {
|
|
|
149
148
|
}
|
|
150
149
|
export class CliExit extends Error {
|
|
151
150
|
constructor() {
|
|
152
|
-
//
|
|
153
|
-
// process.exit(1) by throwing `Error('exit 1')`.
|
|
151
|
+
// Command-unit tests assert this exact message.
|
|
154
152
|
super('exit 1');
|
|
155
153
|
this.name = 'CliExit';
|
|
156
154
|
}
|