Phases 0–4 done. Tree is clean: build green, lint clean, 348/348 tests passing across the workspace. Only Phase 5 (live-cluster verification of the resize bug fix) remains.
cd /home/peter/dev/ix-cli
git checkout ink-rewrite
git log --oneline -15
cd /home/peter/dev/ix-ui
git checkout ink-rewrite # parallel branch; ix-cli consumes via link:Both repos are on the ink-rewrite branch. ix-cli's @agent-ix/ix-ui-cli
dep points at link:../../../ix-ui/packages/cli (set in 4 consumer
package.jsons).
ix on PATH is symlinked to /home/peter/dev/ix-cli/apps/ix, so
ix local … invocations hit whatever was last make build'd.
| Phase | What | Done | Where |
|---|---|---|---|
| 0 | branches, dep swap, link consumer to local ix-ui-cli | ✅ | ix-ui c02750f, ix-cli link: refs |
| 1 | spec rewrite + 4 gates (review, integrity, failure-domain, matrix) | ✅ | ix-ui 0117da9 0e17b27 974ca67 |
| 2 | Ink components + hooks | ✅ | ix-ui 5d9eb76 |
| 3 | test suite on ink-testing-library — 74 tests green, lint clean | ✅ | ix-ui 06a3dc6 |
| 4 | migrate ix-cli consumers to JSX | ✅ | ix-cli 74a900e de3ecd2 1f384e7 5e6b36d + Phase 4 finish (this session) |
| 5 | end-to-end verification (incl. resize-bug repro) | 🔘 | needs live kind cluster |
Migrated the last 4 source files plus surrounding test cleanup:
packages/local/src/app-row-state.ts— reshapedAppInstallRowsfrom adisplay.transition()driver into a snapshot emitter. Constructor signature is now(services, onChange); the class emitsServiceRow<Phase>[]snapshots on every state change. Internal aggregate logic (hook-vs-k8s arbitration, terminal-failure freeze) is unchanged.packages/local/src/commands/up-image.tsx— single-service path is sequential awaits + final<Listing>. Multi-service app path mounts<AppPhaseTable>which runs the imperative pipeline (helm pull → secrets → umbrella install → per-subchart rollout watchers) in auseEffect, pipes theappRowssnapshot into a declarative<PhaseTable services={state}>, and exits viasetTimeout(exit, 0)after the final state paint.packages/local/src/commands/init-cluster.tsx—<InitClusterUI>React component drives a<PhaseTable phases={["run"]}>. Each bootstrap step (kind / cert-manager / ca-issuer / ingress-nginx / wildcard cert / ingress-tls / wait-cert / namespaces+rbac / dns) transitions running → done/failed viasetRow. Final tail is the dnsmasq instructions on success or the error message on failure.packages/local/src/commands/auth-config.tsx— every sub-command (email enable/disable/show/test, password-reset set/show, social add/remove/list/show, registration set/show) is now sequential awaits- final-state
<Listing>. No moremakeListr/startListing.
- final-state
Also: cluster-status.tsx and list.tsx had stray from "ink" imports
left over from group 3 — switched to the Box/Text re-exports on
@agent-ix/ix-ui-cli.
The static-check tests still searched *.ts only; updated walkTs /
grepSrc / readSrc helpers in tests/static-checks.test.ts and
tests/auth-static-checks.test.ts to recognize .tsx too, with a
fallback that swaps .ts → .tsx for legacy paths in the
namespace-allowlist.
Several tests assumed the old API (startListing, list.success,
list.note, list.item, display.transition on a stateful PhaseTable
class). New shared helper at
packages/local/tests/listing-helpers.ts (also copied into
packages/elements/tests/) installs a vi.mock for
@agent-ix/ix-ui-cli that replaces components with string-typed stubs
and records every renderStatic(<Listing …>) call as a ListingCall
with header / status / tail / tailVariant / notes / items / groups / texts. Tests now assert against that record instead of the old
imperative call sequences.
Migrated tests:
tests/app-row-state.test.ts (snapshot-based assertions),
tests/cluster-down.test.ts (with new confirm test seam injected
into runClusterDown),
tests/cluster-status.test.ts,
tests/auth-init.test.ts,
tests/auth-reset-admin.test.ts,
packages/elements/tests/tap-list.test.ts.
Static-check regex updates for the new declarative implementation:
TC-103 (<PhaseTable<Phase> / phases={PHASES} / phaseLabels={…}),
TC-280a (format-tolerant findInstallForHookJob regex),
TC-280c (finalDisplayError ?? err.message instead of the old
display.finish({failed:true,error:…})),
TC-301 (<Item …formatRefreshChange…> instead of list.item(…)).
runClusterDown(config, opts, deps?)— new third argClusterDownDepswith{ confirm?: (clusterName) => Promise<boolean> }. Default uses Ink<ConfirmPrompt>; tests inject a stub. Same shape as the existingIdentityDepsseam inrunAuthInit/runAuthResetAdmin.
# in ix-ui
cd /home/peter/dev/ix-ui && make build && make test && make lint
# 74 tests green, build + lint clean
# in ix-cli
cd /home/peter/dev/ix-cli && make build && make test && make lint
# core 128/129 (1 skipped), elements 50/50, local 170/170 — all green
# build + lint cleanNone of this is blocking the migration; it's runtime verification of the new code on a live cluster.
ix local up auth # multi-service phase table runs
# … resize the terminal narrower mid-run.
# Pre-fix: spinner emits newlines instead of in-place updates.
# Post-fix: PhaseTable rows truncate and re-flow cleanly.If this works, the migration delivered what it was supposed to.
Each of these exercises a different new file. Worth a quick sanity run since they couldn't be tested without a cluster:
ix local init— init-cluster.tsx, single-column PhaseTableix local up <app>— up-image.tsx multi-service path (the bug-trigger)ix local up <service>— up-image.tsx single-service pathix local auth init— final-state Listing with credential notesix local auth reset-admin— sameix local auth invite <email>— sameix local auth config email enable …— auth-config.tsx mutation + rolloutix local cluster status/cluster down/local list— already smoked, sanity only
- TC-100..TC-333 plan in
/home/peter/dev/ix-ui/spec/tests.md— TC-100..210 + TC-300..313 + TC-320..333 + a few TC-OP / TC-EC are implemented. The remaining "🔘 Planned" cases would round out the matrix; not blocking. cluster-up.tsprocess.stdout.write("\n")separators between per-service blank lines — could be folded into a stacked<Listing>layout. Tracked in handoff gotcha #6 from the prior session.
import { Listing, Item, Note, renderStatic } from "@agent-ix/ix-ui-cli";
await renderStatic(
<Listing header="…" status="passed" tail="done">
<Note>{`Username: ${name}`}</Note>
<Item name="foo" description="bar" />
</Listing>
);} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
await renderStatic(
<Listing header="…" status="failed" tail={`Failed: ${msg}`} tailVariant="error" />
);
throw err;
}See packages/core/src/commands/secrets.tsx (promptForPassword),
packages/local/src/credentials.tsx (promptForToken),
packages/local/src/local-secrets.tsx (promptForSecret),
packages/local/src/commands/cluster-down.tsx (defaultConfirm).
For commands that spawn an external tool which prints its own output
(npm install, helm, kubectl apply, kind delete, $EDITOR, cookiecutter,
gh repo create, make), do not wrap the subprocess in Ink. Run it
normally, let stdio inherit. Render a final-state <Listing> after.
The component is declarative — props are the state, not a stateful class. Examples in production:
packages/local/src/commands/init-cluster.tsx— simple single-column state machine with explicitsetRowupdates per step.packages/local/src/commands/up-image.tsxAppPhaseTable— the full reactive pattern:AppInstallRowssnapshot listener feedssetState, React mounts<PhaseTable services={state}>, the imperative pipeline runs inuseEffectand dispatches transitions via the listener. Plus asetFinalStatefor the post-run frame state/tail/tailVariant.
The hooks useExecaPhase, useKubectlRollout, useHelmHookWatcher in
/home/peter/dev/ix-ui/packages/cli/src/hooks/ are available for
simpler cases that don't need the imperative orchestration up-image
needs (terminal failure detection, hook-vs-k8s arbitration, settling
markers).
-
@agent-ix/ix-ui-clionly. Never import directly frominkin consumer code. UseuseRenderResultfrom@agent-ix/ix-ui-cliinstead of ink'suseApp. ix-ui-cli re-exportsBoxandTextfrom ink. -
renderStaticfor one-shot final-state UIs. Adds anExitAfterPaintwrapper that auto-unmounts after the first paint. For interactive flows or live PhaseTables, userender()directly with components that calluseRenderResult().exit()(typically wrapped insetTimeout(exit, 0)aftersetFinalState) when done. -
@types/noderesolution. Roottsconfig.jsonneeds"types": ["node"]under compilerOptions, otherwise vite-plugin-dts fails with "Cannot find name 'node:crypto'" cascading errors. Already set; just don't accidentally remove it. -
JSX whitespace + prettier. Prettier collapses adjacent spaces in
<Text> foo </Text>. For deliberate whitespace use template strings:<Text>{${x}}</Text>. Frame.tsx Tail uses this pattern. -
Static-check regexes are format-sensitive. Prettier may split a call like
findInstallForHookJob(installs, hookFailure.jobName)over multiple lines. Make new static-check regexes whitespace-tolerant (\s*) when asserting on call sites. -
NFR-002 / NFR-003 static greps in
nfr.test.tsscan source forprocess.stdout.write, ANSI literals, raw└──/⊙/⊗/⊕/●/○. These run as part ofpnpm --filter @agent-ix/ix-ui-cli test. Don't add these to consumer code either.cluster-up.tsstill hasprocess.stdout.write("\n")separators — see Phase 5 polish item. -
vite v8 default is browser.
ssr: trueis set inix-ui/packages/cli/vite.config.tsto externalize node built-ins.
/home/peter/dev/ix-ui/spec/ is fully refreshed. FRs FR-001..FR-008 +
FR-009/10/16, NFRs NFR-001..NFR-003, traceability matrix in tests.md
(StR/US/FR/NFR → AC → TC) including Option Permutation Matrix,
Constraint Boundary Tests, and Edge Cases sections per the
spec-matrix template. No old-impl references remain.