Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions .github/workflows/cli.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: cli

on:
push:
tags:
- 'v*'
paths:
- 'cli/**'
- '.github/workflows/cli.yml'
pull_request:
paths:
- 'cli/**'
- '.github/workflows/cli.yml'

# Builds the standalone setup CLI (cli/) as native binaries for 6 targets and
# publishes them to the same GitHub release that the docker workflow creates.
# The publish job polls for that release because it lands after the (much
# slower) docker image build.

jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Checkout
uses: actions/checkout@v7

- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: '1.4.2'

- name: Install dependencies
working-directory: cli
run: bun install --frozen-lockfile

- name: Typecheck
working-directory: cli
run: bun run typecheck

- name: Lint
working-directory: cli
run: bun run lint

- name: Test
working-directory: cli
run: bun run test

build:
needs: check
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 20

# Cross-compiles from a single Linux runner - Bun embeds the runtime, so
# each target is one fast `bun build --compile` with no toolchain needed.
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Install Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: '1.4.2'

- name: Install dependencies
working-directory: cli
run: bun install --frozen-lockfile

- name: Compile binaries
working-directory: cli
run: |
set -euo pipefail
declare -A targets=(
[linux-x64]='bun-linux-x64'
[linux-arm64]='bun-linux-arm64'
[darwin-x64]='bun-darwin-x64'
[darwin-arm64]='bun-darwin-arm64'
[windows-x64]='bun-windows-x64'
[windows-arm64]='bun-windows-arm64'
)
for name in "${!targets[@]}"; do
ext=""
[[ "$name" == windows-* ]] && ext=".exe"
bun build src/index.ts \
--compile --minify \
--target="${targets[$name]}" \
--define BUILD_VERSION='"${GITHUB_REF_NAME}"' \
--outfile "dist/lode-setup-${name}${ext}"
done
ls -la dist

- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: lode-setup-binaries
path: cli/dist

publish:
needs: [check, build]
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
timeout-minutes: 90

permissions:
contents: write

steps:
- name: Download artifacts
uses: actions/download-artifact@v5
with:
name: lode-setup-binaries
path: cli/dist

# The docker workflow creates the release after its image build
# finishes, which can outlast this pipeline - poll for it.
- name: Wait for the GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
for _ in $(seq 1 450); do
if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
echo "Release ${GITHUB_REF_NAME} found"
exit 0
fi
sleep 10
done
echo "GitHub release ${GITHUB_REF_NAME} was not created within 75 minutes" >&2
exit 1

- name: Upload binaries to the release
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${GITHUB_REF_NAME}" cli/dist/* --clobber
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ logs
# pnpm local store
.pnpm-store

# Bun compile cache
.bun-cache

coverage

# Local env files
Expand Down
10 changes: 10 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ pnpm test:coverage # Run tests with V8 coverage reporting
pnpm typecheck:test # Type-check the test suite (test/tsconfig.json)
```

## The `cli/` project (standalone)

`cli/` is the setup CLI (the `lode-setup` binary): a TypeScript + @clack/prompts app that holds all installer logic. The root `setup.sh` / `setup.ps1` are thin bootstraps that download the prebuilt binary for the platform from the latest GitHub release and exec it.

- **Standalone project**: its own `package.json`, `bun.lock`, `tsconfig.json`, `eslint.config.mjs`, `vitest.config.ts`, and a `pnpm-workspace.yaml` (`packages: []`) that keeps it out of the root workspace. Root `pnpm` commands don't touch it, and root ESLint ignores `cli/**`.
- **Runtime tooling is Bun**: `bun install`, `bun run typecheck|lint|test`; release binaries are built with `bun build --compile --target=bun-<os>-<arch>` (see `.github/workflows/cli.yml`).
- **Commands** (run from `cli/`): `bun install`, `bun run typecheck`, `bun run lint`, `bun run test` (Vitest, `test/**/*.test.ts` mirrors `src/`).
- **CI**: `cli.yml` runs check on PRs/pushes touching `cli/**` and, on `v*` tags, builds 6 platform binaries and uploads them to the same GitHub release that `docker.yml` creates.
- Details: [cli/AGENTS.md](./cli/AGENTS.md)

## Commit Messages

Use **Conventional Commits** format:
Expand Down
2 changes: 2 additions & 0 deletions cli/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
docker-compose*
.env*
51 changes: 51 additions & 0 deletions cli/AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# AGENTS.md - cli/

Setup CLI for Lode (`lode-setup`). Interactive TTY app (TypeScript + @clack/prompts) compiled to standalone native binaries with Bun. The root `setup.sh` / `setup.ps1` are thin bootstraps that download this binary from the latest GitHub release - all installer logic lives here.

## Commands

```bash
bun install --frozen-lockfile
bun run typecheck # tsc --noEmit
bun run lint # eslint . (type-checked flat config)
bun run test # vitest run
# Release build (one of 6 targets: bun-{linux,darwin,windows}-{x64,arm64}):
bun build src/index.ts --compile --minify --target=bun-linux-x64 --define BUILD_VERSION='"1.2.3"' --outfile dist/lode-setup-linux-x64
```

## Structure

```
src/
├── index.ts # Entry: TTY gate + 15-step pipeline + error handling
├── types.ts # All shared types (no inline types in implementation files)
├── constants.ts # Env keys, ports, timeouts, option labels, compose file selection
├── core/ # clack prompt wrappers, docker compose helpers, env/state file
│ # editing, secret generation, downloads, port waits, clipboard,
│ # OSC 8/52, platform helpers
└── steps/ # One file per setup step (banner -> summary)
test/ # Vitest suite mirroring src/
```

## Conventions

- Same code style as the repo root: no semicolons, single quotes, 2-space indent, no trailing commas, 120 char width; comments only where the "why" isn't obvious.
- Strict TS (`strict`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`); ESLint is type-checked (`recommendedTypeChecked` with `project`).
- No `any`, no non-null assertions, no floating promises, `eqeqeq: always`.
- UI strings are plain English (local tool, no i18n).
- Steps never use `console.*` - use the `log` / `note` / `intro` / `outro` wrappers from `core/prompt.ts` (the pre-TTY error in `index.ts` is the only exception).
- Esc at any prompt aborts the whole setup: clack's `CANCEL_SYMBOL` -> `cancel()` + `process.exit(0)` (see `guard` in `core/prompt.ts`).
- State file `.lode-setup` is flat `key=value` (see `core/state.ts`); `.env` edits go through `core/env.ts` (replace / uncomment / append - string-based, never sed-style, so values with `&`/`$` survive).
- `askSelect` in `core/prompt.ts` calls `select<string>` and narrows the result: clack's `Option<T>` is a deferred conditional type, so a generic call site cannot be type-checked.
- @clack/prompts 1.x spinner API: `const s = spinner(); s.start(msg); s.message(msg); s.stop(msg)` - it is a factory function, not `spinner.start()`.
- Hard failures throw `SetupFailure(message, detailLines)`; `index.ts` prints them and exits 1.

## Release pipeline

`.github/workflows/cli.yml`:

- `check` (PRs + `v*` tags): `bun install --frozen-lockfile`, typecheck, lint, test
- `build` (tags): cross-compiles all 6 targets from one ubuntu runner
- `publish` (tags): polls for the GitHub release that `docker.yml` creates (the docker build finishes long after this pipeline), then uploads the 6 binaries

Binary names are part of the bootstrap contract (`lode-setup-<os>-<arch>[.exe]`) - `setup.sh` / `setup.ps1` resolve the asset from the `releases/latest/download/<asset>` redirect, so renaming them breaks both.
Loading
Loading