Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 18e269da72 |
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"phase": 2,
|
||||
"phase": 3,
|
||||
"stage": "verify",
|
||||
"milestone": "v0.4",
|
||||
"phase_role": "execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-09-13T01:50:00Z"
|
||||
"updated_at": "2026-09-13T04:00:00Z"
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
|----|-------------|----------|-------|--------|
|
||||
| REQ-4-003 | One-liner install script (`curl -fsSL <url> \| bash`): detects linux x64, resolves latest release from Gitea API, downloads binary + checksum, verifies sha256, installs to ~/.local/bin (PATH hint), degrades to source-bootstrap instructions when no binary | critical | 2 | complete |
|
||||
| REQ-4-004 | Binary release pipeline: reproducible linux x64 build script, sha256 checksum sidecar, upload as release assets on every ship from v0.4 onward (ongoing binaries requirement) | critical | 2 | complete |
|
||||
| REQ-4-005 | Install + quickstart documentation: README one-liner quickstart, CLI command reference, fresh-clone-to-running-stack end-to-end verification | high | 3 | pending |
|
||||
| REQ-4-005 | Install + quickstart documentation: README one-liner quickstart, CLI command reference, fresh-clone-to-running-stack end-to-end verification | high | 3 | complete |
|
||||
|
||||
## v0.3 Requirements (Credential Engines)
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
| REQ-4-002 | 1 | complete |
|
||||
| REQ-4-003 | 2 | complete |
|
||||
| REQ-4-004 | 2 | complete |
|
||||
| REQ-4-005 | 3 | pending |
|
||||
| REQ-4-005 | 3 | complete |
|
||||
|
||||
### v0.3 (complete)
|
||||
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
| 0 | Pre-execution | complete | — | — | Specification, clarify, research, plan complete; .ciagent/ files updated for v0.4 |
|
||||
| 1 | Bootstrap CLI core | complete | 0 | REQ-4-001, REQ-4-002 | `nextcraft doctor/bootstrap/verify/dev` work against a fresh clone; unit tests green |
|
||||
| 2 | Binary build + release pipeline | complete | 1 | REQ-4-003, REQ-4-004 | Reproducible linux x64 binary + sha256 checksum; one-liner install script; assets uploaded to the Gitea release |
|
||||
| 3 | Install docs + fresh-clone E2E | pending | 2 | REQ-4-005 | README quickstart verified end-to-end from a clean environment; fresh clone reaches running stack |
|
||||
| 3 | Install docs + fresh-clone E2E | complete | 2 | REQ-4-005 | README quickstart verified end-to-end from a clean environment; fresh clone reaches running stack |
|
||||
| 4 | Final review + ship | pending | 3 | — | Code review clean; audit passes; milestone tagged (v0.3.x final patch); release with binary assets created on Gitea |
|
||||
|
||||
---
|
||||
|
||||
@@ -2,8 +2,62 @@
|
||||
|
||||
AI-native outcome school + marketplace — graduates prove what they can build, not what they can write.
|
||||
|
||||
## Quickstart
|
||||
|
||||
One-liner install (linux x64):
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.coreci.dev/coreci/nextcraft/raw/main/scripts/install.sh | sh
|
||||
```
|
||||
|
||||
That downloads the latest release's `nextcraft` CLI binary, verifies its sha256 checksum, and installs it to `~/.local/bin` (PATH hint printed if needed). Every release ships fresh binaries — re-run the one-liner to upgrade.
|
||||
|
||||
Then, from a clone of this repo:
|
||||
|
||||
```sh
|
||||
nextcraft doctor # check prerequisites: node >= 18, pnpm >= 8, python3 >= 3.11, git, unshare
|
||||
nextcraft bootstrap # pnpm install + ai-service venv + .env from template (idempotent)
|
||||
nextcraft verify # health check: venv imports, uvicorn, ports, env
|
||||
nextcraft dev # run the ai-service dev server on :8420 (web dev server: pnpm dev)
|
||||
```
|
||||
|
||||
The E2E test (`apps/cli/tests/fresh-clone-e2e.test.ts`) proves this exact sequence on a fresh clone.
|
||||
|
||||
### No binary / non-linux?
|
||||
|
||||
The installer degrades to printed source instructions. Manual equivalent:
|
||||
|
||||
```sh
|
||||
git clone https://git.coreci.dev/coreci/nextcraft.git && cd nextcraft
|
||||
pnpm install
|
||||
bash apps/ai-service/scripts/bootstrap.sh
|
||||
cp apps/ai-service/.env.example apps/ai-service/.env
|
||||
pnpm ai:dev
|
||||
```
|
||||
|
||||
## CLI reference (`nextcraft`)
|
||||
|
||||
| Command | What it does | Exit codes |
|
||||
|---------|--------------|------------|
|
||||
| `doctor` | Checks prerequisites on PATH: node >= 18, pnpm >= 8, python3 >= 3.11, git, unshare (sandbox fabric). Every ✗ prints a fix hint. | 0 all pass, 1 any fail |
|
||||
| `bootstrap` | Sets up the monorepo from a fresh clone: (1) locates the repo root, (2) `pnpm install`, (3) ai-service venv via `apps/ai-service/scripts/bootstrap.sh`, (4) copies `.env.example` → `.env` if absent, (5) warns on missing optional keys. Idempotent — safe to re-run. | 0 ok, 1 step failed |
|
||||
| `verify` | Health check: ai-service venv + `import ai_service`, uvicorn importable, `.env` present (warn-only), `AI_PORT` (default 8420) free, workspace `node_modules` present. | 0 ok, 1 failures |
|
||||
| `dev` | Thin passthrough to `apps/ai-service/scripts/dev.sh` (exports secrets from `.ciagent/.env.secrets` if present, runs uvicorn on :8420). Ctrl+C stops it. The web dev server is separate: `pnpm dev`. | child's exit code |
|
||||
| `--help` / `-h` | Usage for the CLI or any command. | 0 |
|
||||
| `--version` | Prints the version this binary was built as (matches the release tag). | 0 |
|
||||
|
||||
Exit-code contract: `0` success, `1` check/step failure (hint printed), `2` usage error.
|
||||
|
||||
## Docs
|
||||
|
||||
- [apps/cli/README.md](apps/cli/README.md) — CLI internals: build, binary pipeline, troubleshooting
|
||||
- [.ciagent/PROJECT.md](.ciagent/PROJECT.md) — product spec and milestone history
|
||||
- [.ciagent/ARCHITECTURE.md](.ciagent/ARCHITECTURE.md) — system architecture
|
||||
|
||||
## Status
|
||||
|
||||
**Milestone v0.1** — UI/UX Prototype (high-fidelity interactive, all mock data)
|
||||
**Milestone v0.4** — Distribution & Bootstrap CLI (one-liner install, `nextcraft` binary releases on every ship)
|
||||
|
||||
Prior: v0.3 Credential Engines (shipped v0.2.8) · v0.2 AI Tutor Architecture (v0.2.0) · v0.1 UI/UX Prototype (v0.1.0)
|
||||
|
||||
Initialized via CIAgent v0.7.0
|
||||
@@ -0,0 +1,48 @@
|
||||
# @nextcraft/cli — nextcraft
|
||||
|
||||
The bootstrap CLI for the Nextcraft monorepo, shipped as a self-contained linux x64 binary (Node SEA) on every release.
|
||||
|
||||
## Commands
|
||||
|
||||
See the [root README quickstart](../../README.md) for the user-facing flow. Internals:
|
||||
|
||||
- `src/index.ts` — argv dispatch, exit-code contract (0 ok / 1 failure / 2 usage), direct-run guard (`argv[0] === argv[1]` detects SEA context — the installer renames the binary, so filename matching is unreliable)
|
||||
- `src/commands/` — doctor / bootstrap / verify / dev; all orchestration delegates to `apps/ai-service/scripts/*.sh` via `src/lib/spawn.ts` (array-args only, SIGTERM→SIGKILL timeout ladder)
|
||||
- `src/checks/` — pure logic: version compare, `.env` template diff
|
||||
- `tests/` — node:test suites: dispatch, checks, spawn, command stubs, real-box doctor integration, install.sh fixture-server E2E (tamper rejection, degradation), release-assets token isolation, fresh-clone E2E
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
pnpm cli:typecheck # tsc --noEmit
|
||||
pnpm cli:test # node:test suites
|
||||
pnpm cli:build # tsc -p tsconfig.build.json -> dist/
|
||||
pnpm --filter @nextcraft/cli build:binary <tag> # SEA binary + sha256 sidecar
|
||||
```
|
||||
|
||||
`build:binary <tag>`: esbuild bundle (CJS, node18 target, version stamped via `NEXTCRAFT_VERSION_STAMP` define — `--version` reports the tag it was built as) → `node --experimental-sea-config` → postject injection into a copy of the system node binary → `dist/nextcraft-linux-x64` + `dist/nextcraft-linux-x64.sha256`. The binary runs without node on PATH (runtime embedded, ~117 MB).
|
||||
|
||||
## Release pipeline
|
||||
|
||||
Every ship from v0.3.2 onward runs `scripts/release-assets.sh <tag>` after tag+merge:
|
||||
|
||||
1. Builds the binary stamped with the tag
|
||||
2. Resolves `GITEA_TOKEN` from `.env*` files ONLY (`.ciagent/.env.secrets` first) — never from shell env
|
||||
3. Attaches `nextcraft-linux-x64` + `nextcraft-linux-x64.sha256` to the Gitea release (bounded retry, best-effort — never blocks the ship)
|
||||
|
||||
`scripts/install.sh` (POSIX sh, dash-safe): platform gate → Gitea latest-release API resolve → exact-name asset match → sha256 verify BEFORE install (mismatch = hard stop) → `~/.local/bin` install → PATH hint. Any failure degrades to printed source-bootstrap instructions.
|
||||
|
||||
## Secrets policy
|
||||
|
||||
The CLI never generates, writes, or echoes secrets. `bootstrap` copies `.env.example` → `.env` only when absent and warns on missing optional keys (mock providers keep the stack runnable keyless). Real keys live only in gitignored `.ciagent/.env.secrets`, exported by `apps/ai-service/scripts/dev.sh`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause / fix |
|
||||
|---------|-------------|
|
||||
| `pnpm not found` in doctor | `corepack enable pnpm` (installs to ~/.local/bin — ensure PATH includes it) |
|
||||
| doctor passes but verify fails on venv | re-run `nextcraft bootstrap` (venv/pip resolution is idempotent) |
|
||||
| `port 8420 busy` in verify | stop the process on :8420 (`kill $(lsof -t -i:8420)`) or set `AI_PORT` |
|
||||
| install.sh says "no binary assets yet" | release predates the binary pipeline (pre-v0.3.2); use source bootstrap |
|
||||
| Binary silent after rename | fixed since v0.3.2 (SEA argv detection); re-download the latest release |
|
||||
| Checksum mismatch on install | do NOT run the download; delete it and retry — report if it persists |
|
||||
@@ -0,0 +1,72 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { mkdtempSync, existsSync, readFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const cliDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const repoRoot = join(cliDir, "..", "..");
|
||||
|
||||
test("fresh-clone E2E: clone -> doctor -> bootstrap -> verify (happy path, transcript)", async () => {
|
||||
const work = mkdtempSync(join(tmpdir(), "nc-e2e-"));
|
||||
const cloneDir = join(work, "nextcraft");
|
||||
|
||||
const clone = spawnSync(
|
||||
"git",
|
||||
["clone", "--quiet", "--no-hardlinks", repoRoot, cloneDir],
|
||||
{ encoding: "utf8", timeout: 120000 },
|
||||
);
|
||||
assert.equal(clone.status, 0, `clone failed: ${clone.stderr}`);
|
||||
|
||||
const binary = join(repoRoot, "apps", "cli", "dist", "nextcraft-linux-x64");
|
||||
const cli = existsSync(binary)
|
||||
? binary
|
||||
: join(cliDir, "node_modules", ".bin", "tsx");
|
||||
|
||||
const transcript: string[] = [];
|
||||
const step = (name: string, args: string[], expect: number, timeoutMs: number) => {
|
||||
const { NODE_TEST_CONTEXT, ...restEnv } = process.env as Record<string, string | undefined>;
|
||||
const env = {
|
||||
...restEnv,
|
||||
PATH: [join(process.env.HOME ?? "/home", ".local/bin"), process.env.PATH].filter(Boolean).join(":"),
|
||||
NEXTCRAFT_VERSION: "v-e2e",
|
||||
} as Record<string, string | undefined>;
|
||||
const run = spawnSync(cli, existsSync(binary) ? args : [join(cliDir, "src", "index.ts"), ...args], {
|
||||
cwd: cloneDir,
|
||||
encoding: "utf8",
|
||||
timeout: timeoutMs,
|
||||
env,
|
||||
});
|
||||
transcript.push(`$ nextcraft ${args.join(" ")} -> exit ${run.status}`);
|
||||
if (run.status !== expect) {
|
||||
transcript.push(run.stdout, run.stderr);
|
||||
assert.fail(
|
||||
`${name} exited ${run.status} (expected ${expect})\nTRANSCRIPT:\n${transcript.join("\n")}\nstderr: ${run.stderr}`,
|
||||
);
|
||||
}
|
||||
return run.stdout + run.stderr;
|
||||
};
|
||||
|
||||
const doctorOut = step("doctor", ["doctor"], 0, 60000);
|
||||
assert.ok(doctorOut.includes("node"), "doctor mentions node");
|
||||
assert.ok(doctorOut.includes("pnpm"), "doctor mentions pnpm");
|
||||
|
||||
step("bootstrap", ["bootstrap"], 0, 600000);
|
||||
|
||||
const verifyOut = step("verify", ["verify"], 0, 120000);
|
||||
assert.ok(verifyOut.includes("venv"), "verify covers venv");
|
||||
assert.ok(existsSync(join(cloneDir, "apps", "ai-service", ".env")), ".env created in clone");
|
||||
assert.ok(existsSync(join(cloneDir, "apps", "ai-service", ".venv")), "venv created in clone");
|
||||
assert.ok(
|
||||
existsSync(join(cloneDir, "node_modules", ".pnpm")),
|
||||
"workspace node_modules present",
|
||||
);
|
||||
|
||||
const envExample = readFileSync(join(repoRoot, "apps", "ai-service", ".env.example"), "utf8");
|
||||
const envClone = readFileSync(join(cloneDir, "apps", "ai-service", ".env"), "utf8");
|
||||
assert.equal(envClone, envExample, ".env content matches template");
|
||||
|
||||
console.log("E2E TRANSCRIPT:\n" + transcript.join("\n"));
|
||||
});
|
||||
Reference in New Issue
Block a user