Skip to content

fix: name the host, the reason, and the setting when a request fails - #200

Open
tonychang04 wants to merge 2 commits into
mainfrom
fix/name-the-target
Open

fix: name the host, the reason, and the setting when a request fails#200
tonychang04 wants to merge 2 commits into
mainfrom
fix/name-the-target

Conversation

@tonychang04

@tonychang04 tonychang04 commented Sep 10, 2026

Copy link
Copy Markdown
Member

The incident

A real session today, verbatim:

$ insta login --device
error: fetch failed

That is the entire output. The cause: ~/.insta/config.json still held
apiUrl: https://api.98-87-8-168.sslip.io, an insta-oss test box saved earlier
by insta login --api-url. The box had since been terminated, so the fetch
failed at the network layer. The cloud API was fine the whole time. There was
no way to see any of this without opening the config file by hand.

Earlier in the same session, against a live insta-oss daemon:

$ insta login --device
error: device and OAuth login are cloud-only; use insta login --api-key or --email (HTTP 501)

Also true, also unhelpful: it never named the host that answered, so it was not
obvious the CLI was talking to a daemon rather than to InstaCloud.

The CLI knew the host it dialled and which setting pointed it there. It said
neither.

Before / after

Unreachable host, the exact incident config:

error: fetch failed
error: cannot reach api.98-87-8-168.sslip.io (connect timeout)
  target: https://api.98-87-8-168.sslip.io (saved by `insta login --api-url`)
  for InstaCloud: insta env use prod

Same host via the env var, the second line changes and nothing else:

  target: https://api.98-87-8-168.sslip.io (INSTA_API_URL)
  for InstaCloud: unset INSTA_API_URL

Cloud-only flow against a daemon:

error: device and OAuth login are cloud-only; use insta login --api-key or --email (HTTP 501)
  target: https://api.3-208-30-50.sslip.io (INSTA_API_URL)
  for InstaCloud: unset INSTA_API_URL

insta env show on a custom target:

env:     custom
api:     https://api.3-208-30-50.sslip.io
source:  saved by `insta login --api-url`
mcp:     https://mcp.instacloud.com/mcp (insta-cloud, cloud fallback)
skills:  InsForge/insta-skills (cloud fallback)
switch:  insta env use prod

On the cloud default, errors gain nothing: targetLines() returns [] for
prod, so the normal path is byte-identical to today.

What changed

  • NetworkError replaces undici's leaked TypeError: fetch failed. It names
    the host and translates the cause code into a reason (connect timeout,
    DNS lookup failed, connection refused, TLS cases). The original error is
    kept as cause, so telemetry.ts still lifts cause.code.
  • Any error against a target that is not the cloud default carries two lines
    under it: the URL with the setting that chose it, and the one command back to
    InstaCloud.
  • env show and status gain a source: line. status stops reporting an
    unreachable host as (not logged in), which was a flat misdiagnosis and sent
    the user hunting for a login problem that did not exist.
  • env show labels the mcp and skills rows as the cloud fallback they are
    when apiUrl is custom. Previously a cloud MCP host printed unlabelled
    directly beneath an insta-oss API host, which reads as a matched pair and is
    exactly the "CLI on one deployment, agents on another" failure env.ts's own
    header says must never happen.
  • --json on both commands gains source (and status gains unreachable).
    Additive only; no field was removed or renamed. No command or flag changed.

What I deliberately did not build

The obvious design is: add GET /info returning {product, version}, call it
on login and env use, store the answer beside the URL, and label the target
from the stored kind. Do not do this. Three reasons, in order of weight:

  1. The probe fails exactly when it is needed. The incident is an
    unreachable host. A design whose identification step is a request to that
    host degrades to "unknown" in precisely the case that motivated it, having
    spent a round trip to learn nothing.
  2. Provenance needs no probe at all, because it is fully derivable.
    env use only ever writes a host from the env table, so a persisted
    apiUrl that is not one of those can only have come from login --api-url
    or a hand edit. INSTA_API_URL and INSTA_ENV are readable from the
    environment. That is the entire decision tree, it is pure, it costs nothing,
    and it works with the network down. See src/target.ts.
  3. A stored kind goes stale and lies. Boxes get rebuilt, sslip.io URLs get
    reused. A label that says insta-oss about a host that is now something
    else is worse than no label, because it is confidently wrong.

Two corollaries worth writing down:

  • Refusing cloud-only flows locally, up front, is not buildable without the
    probe, and inherits its staleness. It is also unnecessary: once the 501 names
    the host, it is a good message. The remaining gap is one line of copy on the
    daemon, not machinery here.
  • "Product kind" is the wrong axis. The question a confused user asks is
    "which box am I on", and the host is that answer.
    api.98-87-8-168.sslip.io identifies the box; insta-oss does not.

Nothing here is stored and nothing is probed, so there is no new state to
migrate, invalidate, or get wrong.

Note for whoever does the deferred part

If we do eventually want product identity, it is two fields on an existing
route, not a new one
. GET /healthz is already unauthenticated on both sides
(insta-platform/src/server.ts, insta-oss/src/server.ts) and both return
byte-identical {"ok":true}.

Gotcha: the platform's /healthz declares a TypeBox response schema
(Type.Object({ ok: Type.Optional(Type.Boolean()) })). Fastify serializes
strictly, so an added product field is silently stripped unless the schema
is extended too. It will look like the server is ignoring you.

If that lands, probe it only from env show / status (which already do
network I/O and are the "where am I" commands), never on every command, never
blockingly on login, and never store the answer.

Related, not fixed here

  • insta-oss/src/auth.ts is the one notCloud message in the daemon that
    omits the product name; every other one ends "insta-oss is a single-tenant
    local runtime". One-word fix, daemon side.
  • .insta/project.json records no target, so a cloud project id stays "linked"
    while the CLI points at a daemon. Arguably a bigger "which box" hazard than
    the product label.
  • COMPATIBILITY.md already notes that insta-mcp maps every status >= 500 to
    platform_error: "upstream error, retry", so the daemon's 501 guidance never
    reaches an agent at all.

Testing

npm run typecheck clean. 864 tests pass, new test/target.test.ts covers
provenance derivation, the line builder, the cause-code translation tables, and
the client throwing NetworkError on a dead transport.

test/github-source.integration.test.ts fails on this machine and also fails
identically on unmodified main
because the local git predates
--initial-branch. Not related to this change.

QA against the shipped binary and a live daemon

The suite was not enough. The compiled binary was driven against a genuinely
terminated box (api.98-87-8-168.sslip.io), a live insta-oss daemon from merged
main (api.3-208-30-50.sslip.io, project demo, branches main/feat/rest),
staging, and the cloud. Three defects came out of that, all in the second
commit, none of them reproducible under npm test:

  1. The suite runs on Node; we ship Bun. The compiled binaries put the
    failure code on the error itself, with no cause, in Bun's own CamelCase, so
    every reason fell through to a raw ConnectionRefused. Bun also reports a
    connect timeout as ConnectionRefused, so it cannot distinguish a
    terminated box from a refused port. It now renders as the honest "could not
    connect" instead of a confident lie about the exact host this feature exists
    for. Node's own ECONNREFUSED keeps the precise wording.
  2. --api-url at a dead host advised a no-op. With prod already persisted,
    for InstaCloud: insta env use prod would print "already on prod". The
    advice now undoes whatever actually chose the host: drop --api-url.
  3. --json published prose with backticks in it. source is now a stable
    token (saved-api-url, env-api-url, saved-env, env-name, default,
    flag) with the prose kept for the terminal.

Verified good, real output:

$ insta login --device                       # released 0.0.65
error: fetch failed

$ insta login --device                       # this branch
error: cannot reach api.98-87-8-168.sslip.io (could not connect)
  target: https://api.98-87-8-168.sslip.io (saved by `insta login --api-url`)
  for InstaCloud: insta env use prod
$ insta services list                        # real 404 from the live daemon
error: project not found (HTTP 404)
  target: https://api.3-208-30-50.sslip.io (saved by `insta login --api-url`)
  for InstaCloud: insta env use prod

Staging gets one line and no recovery line, since staging is InstaCloud:

error: unauthorized (HTTP 401)
  target: https://api.staging.instacloud.com (INSTA_ENV=staging)

The cloud is byte-identical to the released binary: error: unauthorized (HTTP 401) and nothing more. env show makes no network call, so it still
answers "where am I" with the host down.

Incidentally confirmed while probing: the live daemon's GET /healthz returns
exactly {"ok":true}, the same bytes as the cloud's. Today there is no way at
all to tell the two apart from a response.

Honest remaining gaps

Two states where a user still cannot tell where they are, neither introduced
here and neither fixed here:

  • A local die() carries no target. In agent mode, agent session missing, expired, or for another project/environment names neither the project nor the
    environment, though its own wording says both matter. It is thrown from
    agent.ts as a plain Error, so it never passes through the client that knows
    the target. Fixing it properly means restructuring that catch to tell "no
    session file" apart from "session for another host": that is the agent auth
    path and does not belong in this PR.
  • The cloud direction of the linked-project problem is still silent. Link a
    project on a daemon, switch back to prod, and you get a bare
    project not found (HTTP 404): prod deliberately suppresses the target lines,
    and they would not help anyway, because the useful fact is where the project
    is, not where you are. The real fix is .insta/project.json recording the
    target that minted it. Deliberately not bolted on here as a half-measure.

One divergence worth recording: the daemon answers branch list for an unknown
project with {branches: []} and exit 0, where services list and
secrets list on the same id return 404. A missing project reads as an empty
one on that route.

🤖 Generated with Claude Code

`insta login --device` against a terminated insta-oss box printed exactly
`error: fetch failed`. The CLI knew the host it dialled and which setting
pointed it there, and said neither, so the only way to find out was to read
~/.insta/config.json.

Three changes, all on the failure path:

- A transport failure now throws NetworkError instead of leaking undici's
  `TypeError: fetch failed`: it names the host and translates the cause code
  into a reason ("connect timeout", "DNS lookup failed", ...). The original
  error stays as `cause`, so telemetry still lifts `cause.code`.
- Any error against a target that is not the cloud default carries two lines
  under it: the URL with the setting that chose it (INSTA_API_URL, INSTA_ENV,
  `login --api-url`, `env use`), and the one command back to InstaCloud.
  On prod nothing is added, so the common error is unchanged.
- `env show` and `status` grow a `source:` line, and `status` stops reporting
  an unreachable host as "(not logged in)". `env show` also marks the mcp and
  skills rows as the cloud fallback they are when apiUrl is custom.

Provenance is derived, never stored and never probed. `env use` only ever
writes a host from the env table, so a persisted apiUrl outside it can only
have come from `login --api-url`. Nothing asks the host what it is: the case
this exists for is a host that does not answer.

Before:
  error: fetch failed

After:
  error: cannot reach api.98-87-8-168.sslip.io (connect timeout)
    target: https://api.98-87-8-168.sslip.io (saved by `insta login --api-url`)
    for InstaCloud: insta env use prod

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

8 issues found across 8 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/api.ts">

<violation number="1" location="src/api.ts:17">
P2: When deploy remaps a 409 error, it drops the new `ApiError.context`, so non-default target failures omit the URL and provenance. Preserve `e.context` when constructing the replacement error.</violation>

<violation number="2" location="src/api.ts:95">
P2: When `login --env <name>` fails before persistence, `targetContext()` reports the target as `--api-url flag` because it derives provenance without the command's `--env` option. Pass the explicit target provenance into `ApiClient` so the error identifies `--env=<name>` correctly.</violation>

<violation number="3" location="src/api.ts:112">
P2: When the peer closes the connection while `res.text()` reads the response body, the new catch has already finished, so the CLI still leaks `TypeError: fetch failed` without the host or reason. Include body consumption in the `NetworkError` try/catch path.</violation>
</file>

<file name="src/target.ts">

<violation number="1" location="src/target.ts:45">
P2: When `login --api-url` is given a known environment URL, this labels the persisted target as `env use` even though login writes it too. Record provenance when persisting or use an ambiguous label for stored named URLs.</violation>

<violation number="2" location="src/target.ts:52">
P2: When `INSTA_API_URL` overrides an already-custom stored URL, unsetting it leaves the CLI on that same custom host, so the displayed `for InstaCloud` recovery is false. Build recovery from both override and stored provenance, or print the steps needed to clear the override and select prod.</violation>

<violation number="3" location="src/target.ts:53">
P3: The `unset INSTA_ENV` recovery branch is unreachable. `source === 'INSTA_ENV=...'` only happens when apiUrl resolves to a named environment's host, so `t.env` is always truthy; every consumer of `recovery` (targetLines, auth.ts, env.ts) prints it only when `env` is null/custom. Remove the branch and let INSTA_ENV fall through to the default recovery, or drop the env-gating and show it for named environments.</violation>

<violation number="4" location="src/target.ts:74">
P2: When the resolved target is staging, `t.env` is non-null, so this suppresses the recovery command even though staging is not the cloud default. Add the recovery line for every `t.env !== DEFAULT_ENV` target.</violation>
</file>

<file name="src/commands/env.ts">

<violation number="1" location="src/commands/env.ts:25">
P2: When a custom `apiUrl` is combined with `INSTA_MCP_URL` or `INSTA_SKILLS_REPO`, `env show` still calls the selected values cloud fallbacks. Check each override before adding its fallback label.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread src/api.ts
Comment on lines +112 to 126
let res: Response
try {
res = await this.fetchImpl(this.apiUrl + path, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
})
} catch (e) {
// A transport failure, not an HTTP status: there is no response to parse and no 401 to
// refresh past, so it goes straight out as a NetworkError naming the host and the setting.
throw new NetworkError(this.apiUrl, (e as { cause?: unknown })?.cause ?? e, await this.targetContext())
}
const text = await res.text()
let parsed: any = null
try { parsed = text ? JSON.parse(text) : null } catch { parsed = { raw: text } }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the peer closes the connection while res.text() reads the response body, the new catch has already finished, so the CLI still leaks TypeError: fetch failed without the host or reason. Include body consumption in the NetworkError try/catch path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/api.ts, line 112:

<comment>When the peer closes the connection while `res.text()` reads the response body, the new catch has already finished, so the CLI still leaks `TypeError: fetch failed` without the host or reason. Include body consumption in the `NetworkError` try/catch path.</comment>

<file context>
@@ -80,11 +109,18 @@ export class ApiClient {
-      headers,
-      body: body === undefined ? undefined : JSON.stringify(body),
-    })
+    let res: Response
+    try {
+      res = await this.fetchImpl(this.apiUrl + path, {
</file context>

Comment thread src/api.ts
// body carries the parsed error payload for callers that branch on machine-readable errors
// (e.g. template deploy's missing_variables); the message stays the human line.
constructor(public status: number, msg: string, public body?: any) { super(msg); this.name = 'ApiError' }
constructor(public status: number, msg: string, public body?: any, public context?: ErrorContext) { super(msg); this.name = 'ApiError' }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When deploy remaps a 409 error, it drops the new ApiError.context, so non-default target failures omit the URL and provenance. Preserve e.context when constructing the replacement error.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/api.ts, line 17:

<comment>When deploy remaps a 409 error, it drops the new `ApiError.context`, so non-default target failures omit the URL and provenance. Preserve `e.context` when constructing the replacement error.</comment>

<file context>
@@ -5,11 +5,30 @@ import { autoResolveProject, promptChoice, type ProjectItem } from './resolve-pr
   // body carries the parsed error payload for callers that branch on machine-readable errors
   // (e.g. template deploy's missing_variables); the message stays the human line.
-  constructor(public status: number, msg: string, public body?: any) { super(msg); this.name = 'ApiError' }
+  constructor(public status: number, msg: string, public body?: any, public context?: ErrorContext) { super(msg); this.name = 'ApiError' }
+}
+
</file context>

Comment thread src/api.ts
private noteCache?: ErrorContext
private async targetContext(): Promise<ErrorContext> {
if (!this.noteCache) {
try { this.noteCache = targetLines(await describeTarget(this.apiUrl)) } catch { this.noteCache = [] }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When login --env <name> fails before persistence, targetContext() reports the target as --api-url flag because it derives provenance without the command's --env option. Pass the explicit target provenance into ApiClient so the error identifies --env=<name> correctly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/api.ts, line 95:

<comment>When `login --env <name>` fails before persistence, `targetContext()` reports the target as `--api-url flag` because it derives provenance without the command's `--env` option. Pass the explicit target provenance into `ApiClient` so the error identifies `--env=<name>` correctly.</comment>

<file context>
@@ -57,17 +76,27 @@ export class ApiClient {
+  private noteCache?: ErrorContext
+  private async targetContext(): Promise<ErrorContext> {
+    if (!this.noteCache) {
+      try { this.noteCache = targetLines(await describeTarget(this.apiUrl)) } catch { this.noteCache = [] }
+    }
+    return this.noteCache
</file context>

Comment thread src/target.ts Outdated
else source = '--api-url flag'

const recovery =
source === 'INSTA_API_URL' ? 'unset INSTA_API_URL'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When INSTA_API_URL overrides an already-custom stored URL, unsetting it leaves the CLI on that same custom host, so the displayed for InstaCloud recovery is false. Build recovery from both override and stored provenance, or print the steps needed to clear the override and select prod.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/target.ts, line 52:

<comment>When `INSTA_API_URL` overrides an already-custom stored URL, unsetting it leaves the CLI on that same custom host, so the displayed `for InstaCloud` recovery is false. Build recovery from both override and stored provenance, or print the steps needed to clear the override and select prod.</comment>

<file context>
@@ -0,0 +1,104 @@
+  else source = '--api-url flag'
+
+  const recovery =
+    source === 'INSTA_API_URL' ? 'unset INSTA_API_URL'
+      : source.startsWith('INSTA_ENV=') ? 'unset INSTA_ENV'
+        : 'insta env use prod'
</file context>

Comment thread src/target.ts
export function targetLines(t: Target): string[] {
if (t.env === DEFAULT_ENV) return []
const lines = [` target: ${t.apiUrl} (${t.source})`]
if (!t.env) lines.push(` for InstaCloud: ${t.recovery}`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the resolved target is staging, t.env is non-null, so this suppresses the recovery command even though staging is not the cloud default. Add the recovery line for every t.env !== DEFAULT_ENV target.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/target.ts, line 74:

<comment>When the resolved target is staging, `t.env` is non-null, so this suppresses the recovery command even though staging is not the cloud default. Add the recovery line for every `t.env !== DEFAULT_ENV` target.</comment>

<file context>
@@ -0,0 +1,104 @@
+export function targetLines(t: Target): string[] {
+  if (t.env === DEFAULT_ENV) return []
+  const lines = [`  target: ${t.apiUrl} (${t.source})`]
+  if (!t.env) lines.push(`  for InstaCloud: ${t.recovery}`)
+  return lines
+}
</file context>
Suggested change
if (!t.env) lines.push(` for InstaCloud: ${t.recovery}`)
if (t.env !== DEFAULT_ENV) lines.push(` for InstaCloud: ${t.recovery}`)

Comment thread src/target.ts Outdated
else if (i.stored && normalizeUrl(i.stored) === want) {
// A stored host the env table knows was written by `env use` (or by `login --env`, which
// writes the same value); anything else was a literal URL the user typed at `login --api-url`.
source = envForApiUrl(i.stored) ? 'saved by `insta env use`' : 'saved by `insta login --api-url`'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When login --api-url is given a known environment URL, this labels the persisted target as env use even though login writes it too. Record provenance when persisting or use an ambiguous label for stored named URLs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/target.ts, line 45:

<comment>When `login --api-url` is given a known environment URL, this labels the persisted target as `env use` even though login writes it too. Record provenance when persisting or use an ambiguous label for stored named URLs.</comment>

<file context>
@@ -0,0 +1,104 @@
+  else if (i.stored && normalizeUrl(i.stored) === want) {
+    // A stored host the env table knows was written by `env use` (or by `login --env`, which
+    // writes the same value); anything else was a literal URL the user typed at `login --api-url`.
+    source = envForApiUrl(i.stored) ? 'saved by `insta env use`' : 'saved by `insta login --api-url`'
+  } else if (i.stored === null && env === DEFAULT_ENV) source = 'built-in default'
+  // Nothing in the environment or on disk accounts for this URL, so it came from the flag the
</file context>

Comment thread src/commands/env.ts
Comment on lines +25 to +26
info(`mcp: ${mcpUrl} (${mcpServer}${env ? '' : ', cloud fallback'})`)
info(`skills: ${skills}${env ? '' : ' (cloud fallback)'}`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a custom apiUrl is combined with INSTA_MCP_URL or INSTA_SKILLS_REPO, env show still calls the selected values cloud fallbacks. Check each override before adding its fallback label.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/env.ts, line 25:

<comment>When a custom `apiUrl` is combined with `INSTA_MCP_URL` or `INSTA_SKILLS_REPO`, `env show` still calls the selected values cloud fallbacks. Check each override before adding its fallback label.</comment>

<file context>
@@ -7,17 +7,24 @@
+  info(`source:  ${target.source}`)
+  // A custom apiUrl has no matching mcp/skills entry, so resolveEnv falls back to the cloud's.
+  // Say so: an unlabelled cloud mcp host under an insta-oss api host reads as a matched pair.
+  info(`mcp:     ${mcpUrl} (${mcpServer}${env ? '' : ', cloud fallback'})`)
+  info(`skills:  ${skills}${env ? '' : ' (cloud fallback)'}`)
+  if (!env) info(`switch:  ${target.recovery}`)
</file context>
Suggested change
info(`mcp: ${mcpUrl} (${mcpServer}${env ? '' : ', cloud fallback'})`)
info(`skills: ${skills}${env ? '' : ' (cloud fallback)'}`)
info(`mcp: ${mcpUrl} (${mcpServer}${env || process.env.INSTA_MCP_URL ? '' : ', cloud fallback'})`)
info(`skills: ${skills}${env || process.env.INSTA_SKILLS_REPO ? '' : ' (cloud fallback)'}`)

Comment thread src/target.ts Outdated

const recovery =
source === 'INSTA_API_URL' ? 'unset INSTA_API_URL'
: source.startsWith('INSTA_ENV=') ? 'unset INSTA_ENV'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The unset INSTA_ENV recovery branch is unreachable. source === 'INSTA_ENV=...' only happens when apiUrl resolves to a named environment's host, so t.env is always truthy; every consumer of recovery (targetLines, auth.ts, env.ts) prints it only when env is null/custom. Remove the branch and let INSTA_ENV fall through to the default recovery, or drop the env-gating and show it for named environments.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/target.ts, line 53:

<comment>The `unset INSTA_ENV` recovery branch is unreachable. `source === 'INSTA_ENV=...'` only happens when apiUrl resolves to a named environment's host, so `t.env` is always truthy; every consumer of `recovery` (targetLines, auth.ts, env.ts) prints it only when `env` is null/custom. Remove the branch and let INSTA_ENV fall through to the default recovery, or drop the env-gating and show it for named environments.</comment>

<file context>
@@ -0,0 +1,104 @@
+
+  const recovery =
+    source === 'INSTA_API_URL' ? 'unset INSTA_API_URL'
+      : source.startsWith('INSTA_ENV=') ? 'unset INSTA_ENV'
+        : 'insta env use prod'
+
</file context>

None of these reproduce under `npm test`, because the suite runs on Node and
never drives the shipped artifact.

- Bun, not just Node. The compiled binaries install.sh serves put the failure
  code on the error ITSELF, with no `cause`, spelled in Bun's own CamelCase, so
  every reason fell through to a raw `ConnectionRefused`. Bun also reports a
  connect TIMEOUT as `ConnectionRefused`, so it cannot tell a terminated box
  from a refused port; it now renders as the honest "could not connect" rather
  than a confident lie about the one host this feature exists for. Node's
  ECONNREFUSED really does mean refused and keeps the precise wording.
- `--api-url` at a dead host, with prod already persisted, advised
  "insta env use prod" — a no-op that prints "already on prod". The advice now
  undoes whatever actually chose the host: "drop --api-url".
- `--json` published `source` as prose containing backticks. It now publishes a
  stable token (`saved-api-url`, `env-api-url`, ...) and keeps the prose for the
  terminal, so an agent is not branching on a sentence we will reword.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant