Skip to content

fix(cli): agent-UX + billing-visibility follow-ups (RND-590/592/593/594/595)#166

Open
mpjunior92 wants to merge 67 commits into
masterfrom
matheuspereirajunior/cli-agent-ux-billing-followups
Open

fix(cli): agent-UX + billing-visibility follow-ups (RND-590/592/593/594/595)#166
mpjunior92 wants to merge 67 commits into
masterfrom
matheuspereirajunior/cli-agent-ux-billing-followups

Conversation

@mpjunior92

@mpjunior92 mpjunior92 commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Follow-up CLI/SDK fixes on top of #164, addressing five UX/DX tickets plus a billing-display bug. Each ticket is its own commit; live-verified against both deployed backends (compute-tee on sepolia/mainnet, ecloud-platform on sepolia-dev).

Changes

RND-590 — deploy guards against duplicate same-named billable apps (59c69d9)
deploy enumerated name collisions only against the local YAML registry, so a clean machine/CI (empty registry) silently provisioned a second billable app. Now enumerates the callers apps on-chain (getAllAppsByDeveloper), resolves profile names via /info, and errors (exit 2, pointing to app upgrade ) when a live app already uses the name. --force-new` bypasses. The check is fail-open: a read failure warns and proceeds.

RND-592 — app status --wait docs (c435144)
Align the deploy skill guidance and --wait help text with shipped behavior: --wait only blocks while the app is transitioning and returns immediately for settled statuses.

RND-593 — reconcile release digest after upgrade (b19e4ad)
After app upgrade, the release digest is served by a lagging Ponder indexer, so an agent could read the old digest and retry a successful upgrade. New reconcileReleaseDigest SDK helper polls until the reported digest matches the just-deployed one (or warns "propagation in progress", exit 0). The digest is surfaced out of the SDK so the exact target is known on both verifiable and non-verifiable paths. --fresh (from the ticket) was dropped after verifying in the server source that the digest comes from a GraphQL POST with no cache layer — re-reading, not cache-busting, is the fix.

RND-594 — surface TLS-off (DOMAIN unset) (3158617)
When DOMAIN is unset the app runs but nothing binds ports 80/443, with no signal. deploy/upgrade now warn pre-flight; app info shows a static TLS line (gated so it stays out of the denser app list). DOMAIN is encrypted, so the info line is informational.

RND-595 — billing status credit split (6785e88)
billing status printed one ambiguous "Credits" line. Now shows a grouped funds block: Promotional (+expiry) / Paid / Total from the billing-api 3-way split (new SDK getAccountCredits), plus Wallet ETH + USDC — with a note that Stripe credits are separate from on-chain funds. Each read is best-effort.

Billing display fix (1ed75f5) + reconcile doc (cfcd923)
Pre-existing line-item bug: the renderer took the last two words of the Stripe description as the SKU, yielding garbage like Compute (/ month)). Now parses the SKU correctly and uses structured price/quantity fields; preserves the (correct) hourly rate, verified against the published pricing table. Also shortens wallet ETH display to 4 dp.

New billing status output

Subscription Status:
  Wallet: 0x540d6701c396f77c3601FC34585107497ED71495
  Status: ✓ Active
  Product: compute
  Current Period: 20/05/2026 - 20/06/2026

  Line Items:
    • Compute (Pro 1): $0.00 (0 hours × $0.074/hour)
    • Compute (Starter 2): $0.00 (0 hours × $0.041/hour)
    • Compute (Starter 1): $0.00 (0 hours × $0.027/hour)
    • Compute (Pro 2): $0.00 (0 hours × $0.118/hour)
    • Compute (Enterprise 1): $0.00 (0 hours × $0.329/hour)
    • Compute (Enterprise 2): $0.00 (0 hours × $0.664/hour)

Credits (Stripe):
  Promotional: $9.98 (expires 19/07/2026)
  Paid:        $10.00
  Total:       $19.98
  (Stripe credits pay compute usage — separate from the on-chain wallet below)

Wallet (sepolia):
  ETH:  0.0989 ETH
  USDC: 10 USDC

Payment & Invoices:
  https://billing.stripe.com/p/session/...

The old format collapsed all of this into a single ambiguous Credits: $19.98 line, with line items rendered as Compute (/ month)).

Testing

  • SDK: 56 tests pass. CLI: 160 tests pass.
  • Live-verified billing status (both backends), app status --wait (both), and the credit split / line items with a real wallet.

🤖 Generated with Claude Code

seanmcgary and others added 30 commits May 20, 2026 12:33
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rewrote the top-up command to support both USDC and credit card payment methods.
Users can now choose between on-chain USDC payment or credit card checkout.

Changes:
- Added method flag to select payment method (usdc or card)
- Extracted USDC flow into handleUsdc() method
- Added handleCard() method for credit card checkout flow
- Added pollForCredits() helper to share polling logic
- Updated description and examples
- Integrated with new SDK methods: getPaymentMethods and purchaseCredits

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
### New CLI Commands
**User-facing:**
- `ecloud billing redeem-coupon [--code CODE]` — redeem a coupon code
for credits (prompts interactively if no `--code` flag)
**Admin (requires admin privileges):**
- `ecloud admin coupons create --amount <dollars>` — create a new coupon
- `ecloud admin coupons list [--active] [--redeemed] [--limit N]
[--offset N]` — list coupons with optional filters
- `ecloud admin coupons get <id>` — get coupon details
- `ecloud admin coupons deactivate <id>` — deactivate a coupon
- `ecloud admin coupons redeem <id> --address <wallet>` — redeem a
coupon on behalf of a user
- `ecloud admin admins add <address>` — grant admin privileges
- `ecloud admin admins remove <address>` — revoke admin privileges
- `ecloud admin admins list` — list all admins
### SDK Changes
- `BillingApiClient` — added methods for all admin and coupon REST
endpoints
- `AdminModule` — new module wrapping admin API surface
(`createAdminModule`)
- `BillingModule` — added `redeemCoupon(code)` method
- New types: `AdminCoupon`, `AdminUser`, `CreateCouponResponse`,
`ListCouponsResponse`, `GetCouponResponse`, `AddAdminResponse`,
`ListAdminsResponse`, `RedeemCouponResponse`
The `billing cancel` command was cherry-picking only `private-key` and
`verbose` from commonFlags, leaving `--environment`, `--rpc-url`,
`--max-fee-per-gas`, `--max-priority-fee`, and `--nonce` undeclared.
This made the command unusable in CI/scripted flows because there was
no non-interactive way to pick the environment.

Spread `...commonFlags` like the other billing subcommands
(status, subscribe, top-up, list-cards) so all common flags are
accepted; keep `product` and `force` flags on top.
The 'ecloud compute app upgrade' command would hang indefinitely after
submitting the on-chain transaction whenever the orchestrator was silent
(15+ minutes observed in the wild) — there was no deadline, no progress
between status transitions, and no recovery hint.

- Bound watchUntilUpgradeComplete with a deadline (default 10 minutes,
  overridable via ECLOUD_WATCH_TIMEOUT_SECONDS env var or an explicit
  timeoutSeconds option).
- Log every status transition on its own line with elapsed seconds,
  mirroring watchUntilRunning.
- On timeout, throw a typed WatchUpgradeTimeoutError carrying appId,
  lastStatus, elapsedSeconds, and timeoutSeconds.
- CLI catches the timeout, prints a recovery hint pointing the user to
  'ecloud compute app info <id>' along with txHash and appId, and exits
  non-zero. Success path is unchanged.
watchUntilRunning previously only logged on status transitions, so when
the orchestrator silently kept the app in Unknown the user saw a single
"Status: Unknown (1s)" line forever (visible especially over non-TTY
stdout where carriage-return overwrites are invisible). The loop also
had no timeout, so the CLI would hang indefinitely.

Add a 30s heartbeat that re-emits the current status with elapsed time,
plus a configurable timeout (default 10 minutes, override via
ECLOUD_WATCH_TIMEOUT_SECONDS) that throws a typed WatchTimeoutError.
The CLI deploy command catches it and prints a hint pointing at
'ecloud compute app info <id>' before exiting non-zero.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sepolia-dev AppController was upgraded to v1.5.x (eigenx-contracts
KMS-006, PR #15), which added a 4th field `containerPolicy` to the on-chain
`Release` struct. This changed the `createApp` selector from 0xa60daa8f to
0x5e92a19f (and likewise createAppWithIsolatedBilling / upgradeApp). The SDK
still shipped the 3-field ABI, so every deploy/upgrade encoded the old
selector and the upgraded contract reverted with empty revert data — opaque
"execution reverted" with no decodable reason.

Changes:
- Replace the vendored AppController.json with the v1.5.x ABI generated from
  eigenx-contracts master (createApp/createAppWithIsolatedBilling/upgradeApp
  now take the 4-field Release; adds createEmptyApp, confirmUpgrade, etc.).
  All SDK-used functions remain present.
- Add ContainerPolicy / EnvVar types + EMPTY_CONTAINER_POLICY default, and an
  optional `containerPolicy` field on Release (backwards-compatible: callers
  that omit it get an empty policy that preserves the image's own
  entrypoint/env).
- Encode containerPolicy at both release-encoding sites in caller.ts
  (prepareDeployBatch / prepareUpgradeBatch) via a shared helper.
- Add release-encoding regression test pinning the 0x5e92a19f selector and the
  4-field encoding so this drift cannot silently return.

Verified E2E on sepolia-dev: deploy now passes the on-chain createApp step
(app created on-chain, status STARTED) where it previously bare-reverted. The
follow-on "Failed state" during TEE provisioning is unrelated to this ABI fix.

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

The first commit swapped the vendored ABI wholesale to v1.5.x, which would
have broken sepolia / mainnet-alpha — both still run AppController v1.4.0
(3-field Release, createApp selector 0xa60daa8f). Only sepolia-dev is on
v1.5.x (4-field Release + containerPolicy, selector 0x5e92a19f). Verified
on-chain via each controller's version() and createApp selector.

Make the SDK support BOTH formats, selected per environment:
- Keep the v1.5 ABI as AppController.json and re-add the v1.4 ABI as
  AppController.v1_4.json (the two also differ on getApps AppConfig shape, so
  the whole ABI is selected, not just the create/upgrade entries).
- Add `releaseAbiVersion?: "v1.4" | "v1.5"` to EnvironmentConfig; set
  sepolia-dev = v1.5, sepolia + mainnet-alpha = v1.4. Omitted defaults to v1.5.
- caller.ts: appControllerAbiFor(env) picks the ABI; releaseForViem(release,
  env) includes containerPolicy only on v1.5. All read/lifecycle calls also
  route through the version-aware ABI.
- Drop the now-unused `Address` import in environment.ts (pre-existing lint
  error in a file this change touches).

Tests: per-version encoding (0xa60daa8f vs 0x5e92a19f, containerPolicy
round-trip, arity guards) + env→ABI selection through getEnvironmentConfig.
31 SDK tests pass; tsc + eslint clean on changed files.

E2E verified on both: sepolia-dev (v1.5) and sepolia-prod (v1.4) each created
an app on-chain (status STARTED) where the wholesale-swap build would have
reverted on one of them. (Provisioning "Failed state" for the bare nginx test
image is unrelated to the ABI.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nblocks sepolia-dev without breaking prod (#165)

Deploys/upgrades on **sepolia-dev** have been failing since the
AppController was upgraded to **v1.5.x** (2026-05-28) with an opaque
`execution reverted`. **sepolia / mainnet-alpha remain on v1.4.0**, so
the fix must support **both** ABIs, selected per environment.

## Root cause

eigenx-contracts **KMS-006** added a 4th field `containerPolicy` to the
on-chain `Release` struct on v1.5.x:

```
v1.4.x: Release = (rmsRelease, publicEnv, encryptedEnv)                  // createApp 0xa60daa8f
v1.5.x: Release = (rmsRelease, publicEnv, encryptedEnv, ContainerPolicy) // createApp 0x5e92a19f
```

The SDK shipped only the 3-field ABI. Against v1.5.x (sepolia-dev) it
encoded the now-nonexistent `0xa60daa8f` selector → empty-data revert.

Verified on-chain per environment:
| Env | Controller | `version()` | Release |
|---|---|---|---|
| sepolia-dev | `0xa86DC1C…` | 1.5.1 | 4-field |
| sepolia | `0x0dd810a6…` | 1.4.0 | 3-field |
| mainnet-alpha | `0xc38d35Fc…` | 1.4.0 | 3-field |

## Design — support both, select per environment

- Vendor **both** ABIs: `AppController.json` (v1.5) and
`AppController.v1_4.json` (v1.4). They also diverge on the `getApps`
`AppConfig` shape, so the whole ABI is selected, not just
create/upgrade.
- Add `releaseAbiVersion?: "v1.4" | "v1.5"` to `EnvironmentConfig`:
**sepolia-dev = v1.5**, **sepolia + mainnet-alpha = v1.4** (omitted
defaults to v1.5). Future prod upgrade = one-line config flip.
- `caller.ts`: `appControllerAbiFor(env)` selects the ABI;
`releaseForViem(release, env)` adds `containerPolicy` only on v1.5. Add
`ContainerPolicy`/`EnvVar` types + `EMPTY_CONTAINER_POLICY` (empty
policy preserves the image’s own entrypoint/env; current deploy path
supplies none).

## Testing

- [x] Unit: per-version encoding asserts `0xa60daa8f` (v1.4) and
`0x5e92a19f` (v1.5), containerPolicy round-trip, and arity guards
- [x] Unit: env→ABI selection through real `getEnvironmentConfig`
(sepolia-dev → v1.5, sepolia/mainnet-alpha → v1.4)
- [x] 31 SDK tests pass; `tsc --noEmit` clean; `eslint` clean on changed
files
- [x] E2E sepolia-dev (v1.5): deploy created app on-chain, status
`STARTED`
- [x] E2E sepolia-prod (v1.4): deploy created app on-chain (tx
`0x8dc3c88e…`), status `STARTED` — the case the wholesale-swap approach
would have reverted
- [x] Both test apps terminated afterward

### Known follow-on (out of scope)
The bare-`nginx` test image enters **Failed state** during TEE
provisioning (off-chain watcher, identical on both envs) — a runtime
concern unrelated to this ABI fix.

### Branch target
Targets `release/v1.0.0` as requested. Same drift exists on `master`;
forward-port (or re-sync `master` from eigenx-contracts bindings). The
new `createEmptyApp` two-step flow is present in the v1.5 ABI but not
adopted — this is the minimal fix to keep the existing one-shot
`createApp` deploy path valid across both contract versions.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
mpjunior92 and others added 26 commits June 3, 2026 17:42
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One-shot status via getStatuses; --json emits { appId, status }. --wait blocks
via the bounded watchDeployment machinery (honors --watch-timeout /
ECLOUD_WATCH_TIMEOUT_SECONDS), catches WatchTimeoutError with a recovery hint,
then prints a final status read. Gives agents a supported wait mechanism
instead of tight-looping 'app info'.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…pp info' (RND-592)

Rewrite Gate 3 to use the bounded 'app status --wait' instead of a bare
'app info' poll loop (the tight loop that trips rate limits). Add --json to the
supported-flags list and update the upgrade gate. Also brings SKILL.md into
prettier compliance (was pre-existing non-conforming; whitespace-only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onfirm behavior

The PR #162 merge changed confirmWithDefault to return the default in non-TTY
mode instead of throwing, so promptUseVerifiableBuild(false) now resolves to
false (regular build) rather than erroring. Update the two tests that asserted
the old throw behavior. Mainnet deploy confirm stays safe: it's gated on
!flags.force and defaults to false, so non-TTY without --force cancels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…f throwing (RND-589)

Optional yes/no confirms must not block a non-interactive run. In non-TTY mode
confirmWithDefault now returns its default rather than throwing 'Use --force'.
Safe for the mainnet deploy confirm: that path is gated on !flags.force and
defaults to false, so a non-interactive mainnet deploy without --force cancels
rather than auto-proceeding. Makes the committed tests (which assert this) match
the code.

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

Callers keying off exit status can now tell which stage failed:
  2 = invalid/missing input (pre-build)
  3 = build/push failed (no on-chain tx attempted)
  4 = build OK but on-chain tx failed (image already pushed; re-run reuses it)

Wrap prepare* (build) and execute* (on-chain) in stage-labeled try/catch in
both deploy.ts and upgrade.ts; the non-interactive precheck now exits 2.
On-chain failure message states the image was already built+pushed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ND-591)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e-flight check (RND-597)

Three gaps that let an arm64 image deploy and crash on first request in the TEE:

1. digest.ts: the 'architecture undetectable' branch assumed linux/amd64 and
   returned without verifying. Now throws createPlatformErrorMessage (fail closed).
2. prepare.ts: verify the remote --image-ref is linux/amd64 (docker manifest
   inspect, no pull) BEFORE layerRemoteImageIfNeeded, so an arm64 ref fails in
   ~seconds with the buildx/--verifiable remediation instead of after a
   multi-minute pull+layer+push.
3. dockerhub.ts: resolveDockerHubImageDigest (prebuilt verifiable images) now
   asserts a linux/amd64 manifest — checks the index for an amd64 entry, or the
   config blob's architecture for single-platform — rejecting otherwise.

--verifiable --repo --commit is unchanged (server-side build, no local Docker;
never hits these paths).

Tests: digest.test.ts (undetectable→throw, single-platform arm64, multi-platform
no-amd64) + dockerhub.test.ts (multi/single platform accept+reject). 40 SDK +
99 CLI tests pass. E2E sepolia-dev: arm64 --image-ref rejected pre-push in ~16s
(exit 3); amd64 passes pre-flight and proceeds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…RND-596)

Compute credits do not pay on-chain gas (paid by the EOA via EIP-7702), so an
agent funded only with credits had its deploy tx revert with no machine-readable
pre-check. Add a typed pre-flight gate in the SDK so BOTH CLI and SDK/agent paths
are protected:

- New InsufficientGasError + assertSufficientGas(publicClient, address,
  gasEstimate) in common/gas/insufficientGas.ts; threshold is gasEstimate.maxCostWei
  (not zero — dust below cost still fails). Exported from the SDK index.
- Call it at all 4 prepare* gas-estimate sites (deploy + upgrade, normal +
  verifiable), right after estimateBatchGas, before returning.
- billing status now shows the wallet's on-chain ETH (best-effort) so the
  credit-vs-gas gap is visible pre-deploy.

Tests: assertSufficientGas (above/equal/dust-below/error-shape). 44 SDK + 99 CLI
tests pass. E2E sepolia-dev: a 0.0044-ETH wallet is blocked with 'needs ~0.0198
ETH ... credits do not pay on-chain gas'; billing status shows the ETH line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Strip RND-* ticket references from code comments and test names across
the CLI and SDK packages. Ticket tracking belongs in the PR/commit/branch,
not in committed source. Comment-only and describe()-label changes — no
behavior change; all affected tests pass.

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

Replace bare `let prepared, gasEstimate;` / `let res;` (implicit any) with
explicit annotations in the deploy and upgrade command flows. None take
undefined — the catch block calls this.error(..., {exit}) (returns never),
so the vars are definitely assigned. Types sourced from the SDK's exported
Prepare{Deploy,Upgrade}Result / GasEstimate and Awaited<ReturnType<...>>.

No behavior change; deploy/upgrade tests + prettier pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The optional-input helpers (env-file, log-visibility, resource-usage-
monitoring, dockerfile) called isNonInteractive() internally, re-deriving
the decision from process.stdin/CI on every call. That dropped the
--non-interactive flag: the bare call only sees CI + !isTTY, so
--non-interactive on a real TTY (CI unset) still prompted.

Resolve isNonInteractive(flags) once at the command boundary in deploy/
upgrade and thread the boolean into each helper as a parameter — matching
the existing getInstanceTypeInteractive(..., nonInteractive) shape. No
global state: helpers are now pure and unit-testable by passing the bool
(no process.stdin mocking), and --non-interactive is honored everywhere.

Tests: added a block asserting each helper honors the injected decision on
a TTY (the dropped-flag case), plus a sanity check that nonInteractive=false
still reaches the prompt. 104 CLI tests pass; eslint + prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wallet-ETH balance read in `billing status` was wrapped in an empty
catch that discarded the error. It lumped three distinct failures —
malformed --private-key, bad environment config, and transient RPC errors
— into a silent no-op: the user saw no ETH line and no reason why.

Keep it best-effort (must not abort `billing status`) but warn with the
reason via this.warn, matching the existing pattern in app list/info.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
getDockerfileInteractive / getEnvFileInteractive / getLogSettingsInteractive
/ getResourceUsageMonitoringInteractive / getInstanceTypeInteractive each
take a nonInteractive argument now, so the "Interactive" suffix read
contradictorily (getDockerfileInteractive(..., nonInteractive)). These
helpers resolve a value from flag/default/prompt in either mode, so rename
to getDockerfile / getEnvFile / getLogSettings / getResourceUsageMonitoring
/ getInstanceType and update their doc comments. The genuinely
interactive-only helpers (getImageReferenceInteractive, getEnvironmentInteractive,
etc.) keep their suffix.

Also clarify exitCodes.ts: exit 1 is oclif's default for unclassified
errors, not one we define — the doc comment listed it alongside our 2/3/4.

No behavior change; 106 CLI tests pass; eslint + prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…exit code

The old exitCodes test only asserted the EXIT_CODES constants and a trivial
errorMessage helper — it would pass even if the codes were never wired into
the commands. And the six this.error(..., { exit }) blocks (3 stages x deploy
/upgrade) duplicated the message + code mapping inline.

Extract a pure stageFailure(operation, stage, err) -> { message, exit } that
owns the mapping, and call it from both commands. Now the real logic is unit-
tested directly (invalid-input -> 2, build -> 3 "no X was attempted",
onchain -> 4 "image already pushed, re-run reuses it"), with operation-specific
wording, and the duplication is gone.

110 CLI tests pass; eslint + prettier clean; no new tsc errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…; dedup instance-type fetch

Two correctness fixes surfaced by review:

1. Insufficient-gas failures were misclassified as exit 3 ("build failed,
   no deployment attempted"). assertSufficientGas runs inside prepare*() AFTER
   the image is built+pushed, so an InsufficientGasError surfaces through the
   build try/catch — but the image already exists. stageFailure now reclassifies
   InsufficientGasError as on-chain (exit 4, "re-run reuses the pushed image"),
   matching the documented invariant and giving agents an accurate signal.

2. Non-interactive deploy/upgrade with --dockerfile but no --image-ref slipped
   past the all-at-once required-input check, then threw at the interactive
   --image-ref prompt as an unclassified exit 1. collectMissingRequiredInputs
   now requires --image-ref (the push destination) when building from a local
   Dockerfile, so it's reported as invalid-input (exit 2) up front.

Also dedup: fetchAvailableInstanceTypes was byte-identical in deploy.ts and
upgrade.ts — extracted to utils/instanceTypes.ts. (This also removes one
duplicate copy of a pre-existing SkuInfo fallback-shape tsc error: 43 -> 41.)

113 CLI tests pass; eslint + prettier clean; no new tsc errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The platform-rejection message from digest.ts (hit by the prepare.ts
pre-flight for a plain --image-ref) still pointed at `docker build` + "use
the SDK", while the CLI's prebuilt-verifiable path (dockerhub.ts) already
recommended `docker buildx ... --push` OR a server-side verifiable build
(--verifiable --repo <repo> --commit <sha>).

An arm64 --image-ref is exactly the case where the verifiable-build escape
hatch is most useful, so both arm64 entry points now give the same
remediation. Updated createPlatformErrorMessage to match dockerhub.ts and
asserted both `buildx` and `--verifiable --repo --commit` appear.

SDK + CLI unit suites pass; prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…keep --json pure

Addresses review feedback on #164:

- --wait now does a one-shot read first and only blocks while the app is
  in a transitional status (created/deploying/upgrading/resuming/stopping/
  terminating), matched case-insensitively. Settled statuses (Running,
  Stopped, Terminated, Suspended, Failed) return immediately instead of
  polling until the watch timeout.
- --wait --json no longer corrupts stdout: the SDK compute module now
  accepts a logger override, and the command routes SDK progress output
  ("Waiting for app to start...", "Status: ...") to stderr in JSON mode so
  stdout stays a single JSON object.

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

Align the deploy SKILL.md agent guidance and the --wait flag help text with
the shipped behavior: --wait only blocks while the app is transitioning
(Deploying/Upgrading/etc.) and returns immediately for already-settled
statuses (Running/Stopped/Terminated/Suspended/Failed).

Completes the agent-facing docs acceptance criterion; the command, --wait
timeout handling, and 502/503/504 retry already landed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enumerate the caller's apps on-chain (getAllAppsByDeveloper), filter out
terminated, resolve profile names via the coordinator-DB-backed /info, and
error (exit 2) when a live app already uses the requested name — pointing to
'app upgrade <addr>'. --force-new bypasses. The check is fail-open: any read
failure warns and proceeds rather than blocking a legitimate deploy.

Fixes the duplicate-billable-app leak where the name check only consulted the
local YAML registry, so a clean machine/CI (empty registry) never detected an
existing same-named app and silently provisioned a second one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
After `app upgrade`, poll the release digest until it matches the digest just
deployed (or warn "indexer propagation in progress"), so an agent never reads
the stale pre-upgrade digest and retries an already-successful upgrade.

The expected digest is surfaced out of the SDK (prepareRelease ->
PreparedUpgrade -> CLI) so an exact target is known on both the verifiable
(sha256:) and non-verifiable (0x bytes32) paths; reconcileReleaseDigest
normalizes either form. Adds the reconcileReleaseDigest SDK helper (polls
getApp, never throws on timeout) and exposes it on the app module.

Verified root cause in compute-tee: GET /apps/:id serves the digest from a
Ponder indexer via GraphQL POST with no cache layer (API code has no cache on
this path; the GCP HTTP LB has enable_cdn=false). So re-reading -- not
cache-busting -- is the fix; the ticket's --fresh/--no-cache criterion is
dropped as a no-op against this backend.

Closes RND-593.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…p info

When DOMAIN is unset the app runs but nothing binds ports 80/443, so HTTP(S)
requests are refused with no signal — a "Running" web app that is unreachable.

- deploy/upgrade now warn pre-flight (after the env file is finalized, so an
  inline --env DOMAIN=... still counts as on) when isTlsEnabledFromEnvFile is
  false, pointing to `ecloud compute app configure tls`.
- app info shows a static TLS line (gated behind a showTls option so it stays
  out of the denser app list). DOMAIN is encrypted (private env), so the line
  is informational rather than a live on/off read.

No behavior change when DOMAIN is set.

Closes RND-594.

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

billing status previously printed one ambiguous "Credits" line (the Stripe
balance) and a lone wallet-ETH line, so an agent couldn't tell applied promo
credit from spendable funds. Now it shows a grouped funds block:

- Credits (Stripe): Promotional (+expiry), Paid, Total — pulled from the
  billing-api 3-way split (new SDK billing.getAccountCredits → the deployed
  GET /accounts/:eth/credits route), not recomputed in the CLI — with a note
  that Stripe credits are separate from the on-chain wallet.
- Wallet (<env>): ETH (gas) + USDC (via getTopUpInfo, 6 decimals).

Each read is best-effort: a failure warns and degrades (credits → single
Stripe line; wallet → omit), and core subscription status always prints.

Verified live against both deployed billing-API versions: compute-tee
(sepolia/mainnet prod, GCP) and ecloud-platform (sepolia-dev, AWS) — both
return the promotional/paid/total split on the unprefixed /accounts/:eth/credits
route.

Closes RND-595.

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

Two display fixes in `billing status`, surfaced while testing RND-595:

1. Line items: the Stripe description format is '<qty> × <SKU> (at $price /
   month)', but the renderer took the last two words as the SKU, yielding
   garbage like 'Compute (/ month)): ...'. Now parse the SKU with a regex
   (verbatim fallback if it stops matching) and use the structured
   quantity/price/subtotal fields. Price is the hourly rate (verified against
   the published pricing table; the description's '/ month' text is a
   Stripe-side mislabel), so 'hours × $price/hour' is preserved. Dropped the
   dead sepolia/mainnet branch.

2. Wallet ETH: render at most 4 decimal places (e.g. 0.0989 ETH) instead of the
   full 18-decimal wei value — the leading digits are what matter for gas.

Both extracted as pure, unit-tested helpers in billingFormat.ts. Live-verified
on sepolia (compute-tee); line items are a no-op on sepolia-dev (ecloud-platform
returns none).
@mpjunior92 mpjunior92 self-assigned this Jun 11, 2026
@mpjunior92 mpjunior92 requested a review from mmurrs June 11, 2026 17:01
Base automatically changed from release/v1.0.0 to master July 1, 2026 17:42
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.

2 participants