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
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ It will:
3. **Rename the skeleton** — `Shop → Clinic`, `Shopkeeper → Vet`, etc. — consistently across Ruby migrations, Swift models, Kotlin data classes, policies, tests, and localized copy.
4. **Adapt or replace the domain module** — keep `ItemTag` for walk-in queue variants; strip and insert a new resource for non-queue SaaS.
5. **Drive the build green** — `bin/rails test`, `xcodebuild test`, `./gradlew test` must all pass before the agent exits.
6. **Validate the output** across three layers (structural, runtime, semantic). Details in [`docs/SPEC.md`](./docs/SPEC.md) section 6.
6. **Validate the output** across three layers (structural, runtime, semantic) and write a self-contained HTML + JSON [validation report](#validation-report). Details in [`docs/SPEC.md`](./docs/SPEC.md) section 6.

## Demo

Expand Down Expand Up @@ -107,9 +107,11 @@ npx nativeapptemplate-agent "a personal task tracker with due dates"

# Generated output appears under ./out/<slug>/
tree ./out/clinic-queue/
# ├── rails/ ← Rails 8.1 API, git-initialized, buildable
# ├── ios/ ← SwiftUI iOS project, buildable
# └── android/ ← Jetpack Compose Android project, buildable
# ├── rails/ ← Rails 8.1 API, git-initialized, buildable
# ├── ios/ ← SwiftUI iOS project, buildable
# ├── android/ ← Jetpack Compose Android project, buildable
# ├── report.json ← machine-readable validation result
# └── validation-report.html ← self-contained visual report (open in a browser)
```

The agent will also be available as a Claude Code plugin.
Expand Down Expand Up @@ -154,6 +156,29 @@ The agent doesn't just generate code and exit — it validates the output.

See [`docs/SPEC.md`](./docs/SPEC.md) for the full design.

## Validation report

Every run writes a report of the validation results to the output directory:

- **`out/<slug>/validation-report.html`** — a self-contained HTML report (screenshots base64-embedded, no external assets, no JavaScript) you can open in a browser, attach to a PR, or drop into a demo. It shows the overall verdict, a platform×layer matrix, Layer 1 leftover-token findings, Layer 2 build commands + `stderr`, Layer 3 home-screen screenshots with the vision judge's per-criterion rationales (plus the Stage 2 filmstrip when `NATIVEAPPTEMPLATE_VISUAL=2`), the reviewer's contract diff, and the domain rename plan.
- **`out/<slug>/report.json`** — the same data, machine-readable, for CI gating or programmatic use. The full schema lives in [`docs/validation-report.md`](./docs/validation-report.md).

The CLI **exits non-zero when validation fails**, so a shell `&&` chain or CI step catches it:

```bash
npx nativeapptemplate-agent "a walk-in clinic queue" && echo "validation passed"
```

Report flags:

| Flag | Default | Effect |
|---|---|---|
| `--no-report` | — | Skip writing the report |
| `--report-format=html\|json\|both` | `both` | Which artifact(s) to write |
| `--report-embed=true\|false` | `true` | Embed screenshots as `data:` URIs (single portable file) vs. copy to `report-assets/` |
| `--report-open` | — | Open the HTML in your browser when the run finishes (macOS) |
| `--exit-zero` | — | Always exit 0, even on validation failure (e.g. when you only want the report) |

## Security

`ANTHROPIC_API_KEY` is the only sensitive secret the agent needs.
Expand Down
16 changes: 12 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,17 @@ import { loadDotenvIfPresent } from "./env.js";

loadDotenvIfPresent();

type ParsedArgs = { spec: string; report: DispatchReportOptions; open: boolean };
export type ParsedArgs = { spec: string; report: DispatchReportOptions; open: boolean; exitZero: boolean };

function parseArgs(argv: readonly string[]): ParsedArgs {
export function parseArgs(argv: readonly string[]): ParsedArgs {
const specParts: string[] = [];
const report: DispatchReportOptions = {};
let open = false;
let exitZero = false;
for (const arg of argv) {
if (arg === "--no-report") report.enabled = false;
else if (arg === "--report-open") open = true;
else if (arg === "--exit-zero") exitZero = true;
else if (arg.startsWith("--report-format=")) {
const value = arg.slice("--report-format=".length);
if (value === "html" || value === "json" || value === "both") report.format = value;
Expand All @@ -25,15 +27,15 @@ function parseArgs(argv: readonly string[]): ParsedArgs {
specParts.push(arg);
}
}
return { spec: specParts.join(" ").trim(), report, open };
return { spec: specParts.join(" ").trim(), report, open, exitZero };
}

export async function main(spec?: string): Promise<void> {
const parsed = parseArgs(process.argv.slice(2));
const input = (spec ?? parsed.spec).trim();
if (!input) {
console.error(
'Usage: nativeapptemplate-agent "your spec here" [--no-report] [--report-format=html|json|both] [--report-embed=true|false] [--report-open]',
'Usage: nativeapptemplate-agent "your spec here" [--no-report] [--report-format=html|json|both] [--report-embed=true|false] [--report-open] [--exit-zero]',
);
process.exitCode = 1;
return;
Expand All @@ -56,6 +58,12 @@ export async function main(spec?: string): Promise<void> {
} else if (result.reportPaths.jsonPath) {
console.log(`report: ${result.reportPaths.jsonPath}`);
}

// Non-zero exit on validation failure so CI / shell `&&` chains catch
// it. --exit-zero opts out (e.g. when you only want the report).
if (!result.overallPass && !parsed.exitZero) {
process.exitCode = 1;
}
}

// Entry guard: run main() when this file is the program entry point. Resolve
Expand Down
27 changes: 27 additions & 0 deletions tests/smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1296,3 +1296,30 @@ test("writeReport with embed=false externalizes screenshots to report-assets/",
// The copied asset exists on disk.
assert.ok(readFileSync(join(dir, "report-assets", "ios-home.png")).length > 0);
});

test("parseArgs splits the spec from report + exit flags", async () => {
const { parseArgs } = await import("../src/index.js");
const parsed = parseArgs(["a", "walk-in", "queue", "--no-report", "--report-format=json", "--report-embed=false", "--report-open", "--exit-zero"]);
assert.equal(parsed.spec, "a walk-in queue");
assert.equal(parsed.report.enabled, false);
assert.equal(parsed.report.format, "json");
assert.equal(parsed.report.embed, false);
assert.equal(parsed.open, true);
assert.equal(parsed.exitZero, true);
});

test("parseArgs defaults: a bare spec leaves report options unset", async () => {
const { parseArgs } = await import("../src/index.js");
const parsed = parseArgs(["just", "a", "spec"]);
assert.equal(parsed.spec, "just a spec");
assert.deepEqual(parsed.report, {});
assert.equal(parsed.open, false);
assert.equal(parsed.exitZero, false);
});

test("parseArgs ignores an invalid --report-format value", async () => {
const { parseArgs } = await import("../src/index.js");
const parsed = parseArgs(["spec", "--report-format=xml"]);
assert.equal(parsed.spec, "spec");
assert.equal(parsed.report.format, undefined);
});
Loading