@dahoolabs/create 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +45 -2
  2. package/package.json +35 -4
  3. package/src/index.ts +70 -0
  4. package/src/options.ts +49 -0
  5. package/src/prompts.ts +48 -0
  6. package/src/registry.ts +30 -0
  7. package/src/scaffold.ts +57 -0
  8. package/templates/bun-nest-next/.editorconfig +12 -0
  9. package/templates/bun-nest-next/.env.example +6 -0
  10. package/templates/bun-nest-next/.github/workflows/ci.yml +47 -0
  11. package/templates/bun-nest-next/.github/workflows/release-please.yml +18 -0
  12. package/templates/bun-nest-next/.husky/commit-msg +1 -0
  13. package/templates/bun-nest-next/.husky/pre-commit +2 -0
  14. package/templates/bun-nest-next/.release-please-manifest.json +3 -0
  15. package/templates/bun-nest-next/AGENT.md +49 -0
  16. package/templates/bun-nest-next/README.md +21 -0
  17. package/templates/bun-nest-next/_gitignore +15 -0
  18. package/templates/bun-nest-next/apps/api/package.json +29 -0
  19. package/templates/bun-nest-next/apps/api/src/app.module.ts +7 -0
  20. package/templates/bun-nest-next/apps/api/src/health/health.controller.test.ts +19 -0
  21. package/templates/bun-nest-next/apps/api/src/health/health.controller.ts +17 -0
  22. package/templates/bun-nest-next/apps/api/src/health/health.module.ts +7 -0
  23. package/templates/bun-nest-next/apps/api/src/main.ts +21 -0
  24. package/templates/bun-nest-next/apps/api/tsconfig.json +10 -0
  25. package/templates/bun-nest-next/apps/web/app/globals.css +1 -0
  26. package/templates/bun-nest-next/apps/web/app/layout.tsx +17 -0
  27. package/templates/bun-nest-next/apps/web/app/page.tsx +25 -0
  28. package/templates/bun-nest-next/apps/web/next.config.ts +7 -0
  29. package/templates/bun-nest-next/apps/web/package.json +28 -0
  30. package/templates/bun-nest-next/apps/web/postcss.config.mjs +5 -0
  31. package/templates/bun-nest-next/apps/web/tsconfig.json +16 -0
  32. package/templates/bun-nest-next/commitlint.config.ts +6 -0
  33. package/templates/bun-nest-next/e2e/api.spec.ts +7 -0
  34. package/templates/bun-nest-next/e2e/home.spec.ts +6 -0
  35. package/templates/bun-nest-next/package.json +38 -0
  36. package/templates/bun-nest-next/packages/shared/package.json +17 -0
  37. package/templates/bun-nest-next/packages/shared/src/index.ts +5 -0
  38. package/templates/bun-nest-next/packages/shared/tsconfig.json +7 -0
  39. package/templates/bun-nest-next/playwright.config.ts +28 -0
  40. package/templates/bun-nest-next/release-please-config.json +10 -0
  41. package/templates/bun-nest-next/tsconfig.base.json +16 -0
  42. package/templates/bun-nest-next/tsconfig.json +7 -0
  43. package/templates/bun-nest-next/turbo.json +25 -0
package/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
1
+ # @dahoolabs/create
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Scaffold a new Dahoo Labs project.
4
+
5
+ ```sh
6
+ bunx @dahoolabs/create my-app
7
+ # or
8
+ bun create @dahoolabs my-app
9
+ ```
10
+
11
+ ## Options
12
+ | Flag | Description |
13
+ |---|---|
14
+ | `-t, --template <id>` | Template to use (default `bun-nest-next`) |
15
+ | `--no-install` | Skip `bun install` |
16
+ | `--no-git` | Skip `git init` and the initial commit |
17
+ | `-f, --force` | Allow a non-empty target directory |
18
+ | `-y, --yes` | Use defaults, no prompts |
19
+
20
+ ## Templates
21
+ ### `bun-nest-next`
22
+ A Bun workspace + Turborepo monorepo:
23
+ - `apps/api`: NestJS on Bun, with Swagger at `/docs` and a `GET /health` endpoint
24
+ - `apps/web`: Next.js (App Router) + Tailwind CSS v4
25
+ - `packages/shared`: types shared by both apps
26
+
27
+ Every project gets these defaults, and they can't be turned off:
28
+ - Turbo
29
+ - Husky: pre-commit runs check and test; commit-msg runs commitlint
30
+ - commitlint + Commitizen (`bun run commit`)
31
+ - Playwright e2e
32
+ - release-please
33
+ - GitHub Actions CI
34
+ - `AGENT.md`
35
+
36
+ ## Developing
37
+ ```sh
38
+ bun install
39
+ bun run test # unit tests for the CLI
40
+ bun src/index.ts ../../../scratch/demo --yes # try it locally
41
+ ```
42
+
43
+ Template files are copied as-is. `{{projectName}}` and `{{scope}}` are replaced, and `_gitignore` is renamed to `.gitignore`, because npm drops `.gitignore` from published packages.
44
+
45
+ ## Releasing
46
+ Releases are automated with release-please. Merge conventional commits that touch `packages/create` into `main`, then merge the release PR. The workflow publishes to npm using the `NPM_TOKEN` secret.
package/package.json CHANGED
@@ -1,6 +1,37 @@
1
1
  {
2
2
  "name": "@dahoolabs/create",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new Dahoo Labs project",
5
+ "type": "module",
6
+ "bin": {
7
+ "create": "src/index.ts"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "!src/**/*.test.ts",
12
+ "templates"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/dahoolabs/base.git",
17
+ "directory": "packages/create"
18
+ },
19
+ "license": "MIT",
20
+ "engines": {
21
+ "bun": ">=1.3.0"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "scripts": {
27
+ "check": "bun check",
28
+ "test": "bun test ./src"
29
+ },
30
+ "dependencies": {
31
+ "@clack/prompts": "^1.8.1"
32
+ },
33
+ "devDependencies": {
34
+ "@types/bun": "^1.4.3",
35
+ "typescript": "^6.0.3"
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bun
2
+ import * as p from "@clack/prompts";
3
+ import { existsSync, readdirSync } from "node:fs";
4
+ import { relative, resolve } from "node:path";
5
+ import { parseOptions, USAGE } from "./options";
6
+ import { promptOptions } from "./prompts";
7
+ import { getTemplate } from "./registry";
8
+ import { scaffold, toPackageName } from "./scaffold";
9
+
10
+ async function run(cmd: string[], cwd: string): Promise<void> {
11
+ const proc = Bun.spawn(cmd, { cwd, stdout: "inherit", stderr: "inherit" });
12
+ const code = await proc.exited;
13
+ if (code !== 0) throw new Error(`\`${cmd.join(" ")}\` exited with code ${code}`);
14
+ }
15
+
16
+ async function main(): Promise<void> {
17
+ const parsed = parseOptions(process.argv.slice(2));
18
+ if (parsed.help) {
19
+ console.log(USAGE);
20
+ return;
21
+ }
22
+
23
+ p.intro("Dahoo Labs · create");
24
+ const options = await promptOptions(parsed);
25
+ const template = getTemplate(options.template);
26
+ const targetDir = resolve(options.dir);
27
+ const projectName = toPackageName(targetDir);
28
+
29
+ if (existsSync(targetDir) && readdirSync(targetDir).length > 0 && !options.force) {
30
+ p.cancel(`${targetDir} is not empty. Use --force to scaffold into it anyway.`);
31
+ process.exit(1);
32
+ }
33
+
34
+ const spinner = p.spinner();
35
+ spinner.start(`Scaffolding ${template.label}`);
36
+ await scaffold(template.dir, targetDir, { projectName, scope: projectName });
37
+ spinner.stop(`Scaffolded ${template.label} into ${targetDir}`);
38
+
39
+ // git must exist before install so husky's `prepare` script can register the hooks.
40
+ if (options.git) await run(["git", "init", "-q", "-b", "main"], targetDir);
41
+
42
+ if (options.install) {
43
+ p.log.step("Installing dependencies");
44
+ await run(["bun", "install"], targetDir);
45
+ }
46
+
47
+ if (options.git) {
48
+ await run(["git", "add", "-A"], targetDir);
49
+ // Hooks need installed deps; skip them when the user opted out of installing.
50
+ const commit = ["git", "commit", "-q", "-m", "chore: initial scaffold"];
51
+ if (!options.install) commit.push("--no-verify");
52
+ p.log.step("Creating initial commit");
53
+ await run(commit, targetDir);
54
+ }
55
+
56
+ const cd = relative(process.cwd(), targetDir) || ".";
57
+ const steps = [
58
+ cd !== "." && `cd ${cd}`,
59
+ !options.install && "bun install",
60
+ "bun run e2e:install # once, downloads Playwright browsers",
61
+ "bun run dev",
62
+ ].filter(Boolean);
63
+ p.note(steps.join("\n"), "Next steps");
64
+ p.outro("Happy building.");
65
+ }
66
+
67
+ main().catch((error: unknown) => {
68
+ p.cancel(error instanceof Error ? error.message : String(error));
69
+ process.exit(1);
70
+ });
package/src/options.ts ADDED
@@ -0,0 +1,49 @@
1
+ import { parseArgs } from "node:util";
2
+ import { DEFAULT_TEMPLATE } from "./registry";
3
+
4
+ export interface Options {
5
+ dir?: string;
6
+ template: string;
7
+ install: boolean;
8
+ git: boolean;
9
+ force: boolean;
10
+ yes: boolean;
11
+ help: boolean;
12
+ }
13
+
14
+ export const USAGE = `Usage: bunx @dahoolabs/create [dir] [options]
15
+
16
+ Options:
17
+ -t, --template <id> Template to use (default: ${DEFAULT_TEMPLATE})
18
+ --no-install Skip \`bun install\`
19
+ --no-git Skip \`git init\` and the initial commit
20
+ -f, --force Allow scaffolding into a non-empty directory
21
+ -y, --yes Accept defaults, no prompts
22
+ -h, --help Show this help
23
+ `;
24
+
25
+ export function parseOptions(argv: string[]): Options {
26
+ const { values, positionals } = parseArgs({
27
+ args: argv,
28
+ allowPositionals: true,
29
+ allowNegative: true,
30
+ options: {
31
+ template: { type: "string", short: "t", default: DEFAULT_TEMPLATE },
32
+ install: { type: "boolean", default: true },
33
+ git: { type: "boolean", default: true },
34
+ force: { type: "boolean", short: "f", default: false },
35
+ yes: { type: "boolean", short: "y", default: false },
36
+ help: { type: "boolean", short: "h", default: false },
37
+ },
38
+ });
39
+
40
+ return {
41
+ dir: positionals[0],
42
+ template: values.template,
43
+ install: values.install,
44
+ git: values.git,
45
+ force: values.force,
46
+ yes: values.yes,
47
+ help: values.help,
48
+ };
49
+ }
package/src/prompts.ts ADDED
@@ -0,0 +1,48 @@
1
+ import * as p from "@clack/prompts";
2
+ import type { Options } from "./options";
3
+ import { TEMPLATES } from "./registry";
4
+
5
+ function bail(value: unknown): void {
6
+ if (p.isCancel(value)) {
7
+ p.cancel("Cancelled.");
8
+ process.exit(1);
9
+ }
10
+ }
11
+
12
+ /** Fills in anything not given on the command line. Returns options unchanged with --yes. */
13
+ export async function promptOptions(options: Options): Promise<Options & { dir: string }> {
14
+ if (options.yes) return { ...options, dir: options.dir ?? "my-app" };
15
+
16
+ const dir =
17
+ options.dir ??
18
+ (await p.text({
19
+ message: "Where should the project be created?",
20
+ placeholder: "my-app",
21
+ defaultValue: "my-app",
22
+ }));
23
+ bail(dir);
24
+
25
+ const template =
26
+ TEMPLATES.length === 1
27
+ ? options.template
28
+ : await p.select({
29
+ message: "Which template?",
30
+ initialValue: options.template,
31
+ options: TEMPLATES.map((t) => ({ value: t.id, label: t.label, hint: t.hint })),
32
+ });
33
+ bail(template);
34
+
35
+ const install = options.install ? await p.confirm({ message: "Install dependencies?", initialValue: true }) : false;
36
+ bail(install);
37
+
38
+ const git = options.git ? await p.confirm({ message: "Initialize a git repository?", initialValue: true }) : false;
39
+ bail(git);
40
+
41
+ return {
42
+ ...options,
43
+ dir: dir as string,
44
+ template: template as string,
45
+ install: install as boolean,
46
+ git: git as boolean,
47
+ };
48
+ }
@@ -0,0 +1,30 @@
1
+ import { join } from "node:path";
2
+
3
+ export interface Template {
4
+ id: string;
5
+ label: string;
6
+ hint: string;
7
+ dir: string;
8
+ }
9
+
10
+ const TEMPLATES_DIR = join(import.meta.dir, "..", "templates");
11
+
12
+ export const TEMPLATES: Template[] = [
13
+ {
14
+ id: "bun-nest-next",
15
+ label: "Bun + NestJS + Next.js",
16
+ hint: "Turbo monorepo: apps/api (NestJS), apps/web (Next.js), packages/shared",
17
+ dir: join(TEMPLATES_DIR, "bun-nest-next"),
18
+ },
19
+ ];
20
+
21
+ export const DEFAULT_TEMPLATE = "bun-nest-next";
22
+
23
+ export function getTemplate(id: string): Template {
24
+ const template = TEMPLATES.find((t) => t.id === id);
25
+ if (!template) {
26
+ const ids = TEMPLATES.map((t) => t.id).join(", ");
27
+ throw new Error(`Unknown template "${id}". Available: ${ids}`);
28
+ }
29
+ return template;
30
+ }
@@ -0,0 +1,57 @@
1
+ import { cp, readdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { basename, extname, join } from "node:path";
3
+
4
+ export interface TemplateVars {
5
+ projectName: string;
6
+ scope: string;
7
+ }
8
+
9
+ // npm strips these from published tarballs, so templates store them with a leading underscore.
10
+ const RENAMES: Record<string, string> = {
11
+ _gitignore: ".gitignore",
12
+ _npmrc: ".npmrc",
13
+ };
14
+
15
+ const BINARY_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".ico", ".webp", ".woff", ".woff2"]);
16
+
17
+ /** Turns an arbitrary directory name into a valid npm package name. */
18
+ export function toPackageName(name: string): string {
19
+ return (
20
+ basename(name)
21
+ .trim()
22
+ .toLowerCase()
23
+ .replace(/[^a-z0-9-~]+/g, "-")
24
+ .replace(/^[-.~]+|-+$/g, "") || "app"
25
+ );
26
+ }
27
+
28
+ export function render(content: string, vars: TemplateVars): string {
29
+ return content.replace(/\{\{(\w+)\}\}/g, (match, key: string) =>
30
+ key in vars ? vars[key as keyof TemplateVars] : match,
31
+ );
32
+ }
33
+
34
+ export async function scaffold(templateDir: string, targetDir: string, vars: TemplateVars): Promise<void> {
35
+ await cp(templateDir, targetDir, { recursive: true, force: true });
36
+ await processDir(targetDir, vars);
37
+ }
38
+
39
+ async function processDir(dir: string, vars: TemplateVars): Promise<void> {
40
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
41
+ let path = join(dir, entry.name);
42
+ const renamed = RENAMES[entry.name];
43
+ if (renamed) {
44
+ const next = join(dir, renamed);
45
+ await rename(path, next);
46
+ path = next;
47
+ }
48
+
49
+ if (entry.isDirectory()) {
50
+ await processDir(path, vars);
51
+ } else if (!BINARY_EXTENSIONS.has(extname(path))) {
52
+ const content = await readFile(path, "utf8");
53
+ const output = render(content, vars);
54
+ if (output !== content) await writeFile(path, output);
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_style = space
7
+ indent_size = 2
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
@@ -0,0 +1,6 @@
1
+ # apps/api
2
+ PORT=3001
3
+ WEB_ORIGIN=http://localhost:3000
4
+
5
+ # apps/web
6
+ NEXT_PUBLIC_API_URL=http://localhost:3001
@@ -0,0 +1,47 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: ci-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ jobs:
13
+ commitlint:
14
+ if: github.event_name == 'pull_request'
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+ with:
19
+ fetch-depth: 0
20
+ - uses: oven-sh/setup-bun@v2
21
+ - run: bun install --frozen-lockfile
22
+ - run: bunx --bun commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose
23
+
24
+ verify:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v4
28
+ - uses: oven-sh/setup-bun@v2
29
+ - run: bun install --frozen-lockfile
30
+ - run: bun run check
31
+ - run: bun run test
32
+ - run: bun run build
33
+
34
+ e2e:
35
+ runs-on: ubuntu-latest
36
+ steps:
37
+ - uses: actions/checkout@v4
38
+ - uses: oven-sh/setup-bun@v2
39
+ - run: bun install --frozen-lockfile
40
+ - run: bunx playwright install --with-deps chromium
41
+ - run: bun run e2e
42
+ - uses: actions/upload-artifact@v4
43
+ if: failure()
44
+ with:
45
+ name: playwright-report
46
+ path: playwright-report
47
+ retention-days: 7
@@ -0,0 +1,18 @@
1
+ name: Release Please
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+
7
+ permissions:
8
+ contents: write
9
+ pull-requests: write
10
+
11
+ jobs:
12
+ release-please:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: googleapis/release-please-action@v4
16
+ with:
17
+ config-file: release-please-config.json
18
+ manifest-file: .release-please-manifest.json
@@ -0,0 +1 @@
1
+ bunx --bun commitlint --edit "$1"
@@ -0,0 +1,2 @@
1
+ bun run check
2
+ bun run test
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.0.0"
3
+ }
@@ -0,0 +1,49 @@
1
+ # AGENT.md
2
+
3
+ Guidance for AI coding agents (and humans) working in **{{projectName}}**.
4
+
5
+ ## Stack
6
+ - **Runtime / package manager:** Bun (workspaces). Always use `bun` / `bunx`, never npm, yarn, or pnpm.
7
+ - **Monorepo:** Turborepo. Run tasks from the root; Turbo fans them out and caches results.
8
+ - **API:** NestJS on Bun, in `apps/api`. Swagger UI at `/docs`.
9
+ - **Web:** Next.js (App Router) + Tailwind CSS v4, in `apps/web`.
10
+ - **Shared:** `packages/shared` (`@{{scope}}/shared`), for types and contracts used by both apps.
11
+ - **Tests:** `bun test` for unit and integration tests, Playwright for e2e (`e2e/`).
12
+ - **Git:** Husky hooks, commitlint (Conventional Commits), Commitizen, release-please.
13
+
14
+ ## Layout
15
+ ```
16
+ apps/api/src/<feature>/ # one Nest module per feature: module, controller, service, dto, *.test.ts
17
+ apps/web/app/ # Next.js App Router routes
18
+ packages/shared/src/ # shared types; no runtime deps on either app
19
+ e2e/ # Playwright specs that run against the real api and web
20
+ ```
21
+
22
+ ## Commands
23
+ | Task | Command |
24
+ |---|---|
25
+ | Dev servers (api :3001, web :3000) | `bun run dev` |
26
+ | Type check (the static gate; no separate linter) | `bun run check` |
27
+ | Unit tests | `bun run test` |
28
+ | E2E tests (first run `bun run e2e:install`) | `bun run e2e` |
29
+ | Production build | `bun run build` |
30
+ | Commit with a guided prompt | `bun run commit` |
31
+ | Add a dependency to one workspace | `bun add <pkg> --cwd apps/api` |
32
+
33
+ ## Rules
34
+ 1. **Conventional Commits are enforced.** Use `type(scope): subject`. Types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `perf`, `ci`, `build`, `style`, `revert`. Scopes: `api`, `web`, `shared`, `e2e`, `deps`, `ci`, `release`. The commit-msg hook rejects anything else. Never bypass hooks with `--no-verify`.
35
+ 2. **`bun run check` and `bun run test` must pass before committing.** The pre-commit hook runs both.
36
+ 3. **Do not edit `CHANGELOG.md` or bump versions by hand.** release-please does this from commit history. `feat` → minor, `fix` → patch, `feat!` / `BREAKING CHANGE:` → major.
37
+ 4. **Keep the API RESTful and documented.** Every endpoint gets Swagger decorators (`@ApiTags`, `@ApiOkResponse`, …) and DTOs validated with `class-validator`.
38
+ 5. **Layered clean architecture in the API.** Controllers handle HTTP only, services hold business logic, and data access stays behind a repository/provider. Don't put logic in controllers.
39
+ 6. **Shared contracts live in `packages/shared`.** If both apps need a type, define it there instead of duplicating it.
40
+ 7. **Test at the right level.** Unit-test services, integration-test modules with `@nestjs/testing`, and add a Playwright spec for every user-facing flow.
41
+ 8. **Small, focused changes.** One concern per PR/commit. Don't refactor unrelated code in passing.
42
+ 9. **No secrets in git.** Put new env vars in `.env.example` with a safe default.
43
+
44
+ ## Principles & references
45
+ These are the Dahoo Labs engineering principles. Read them before making architectural decisions:
46
+ - [ENGINEERING.md](https://github.com/dahoolabs/base/blob/main/ENGINEERING.md): API design, observability, resilience, scaling, architecture, testing
47
+ - [BACKEND.md](https://github.com/dahoolabs/base/blob/main/BACKEND.md)
48
+ - [FRONTEND.md](https://github.com/dahoolabs/base/blob/main/FRONTEND.md)
49
+ - [AGENTIC_SWE.md](https://github.com/dahoolabs/base/blob/main/AGENTIC_SWE.md)
@@ -0,0 +1,21 @@
1
+ # {{projectName}}
2
+
3
+ Scaffolded with [`@dahoolabs/create`](https://github.com/dahoolabs/base/tree/main/packages/create).
4
+
5
+ ## Getting started
6
+ ```sh
7
+ bun install
8
+ cp .env.example .env
9
+ bun run e2e:install # once, downloads Playwright browsers
10
+ bun run dev # api on :3001 (docs at /docs), web on :3000
11
+ ```
12
+
13
+ ## Scripts
14
+ - `bun run dev`: start api and web in watch mode
15
+ - `bun run check`: type check every workspace
16
+ - `bun run test`: unit tests
17
+ - `bun run e2e`: Playwright end-to-end tests
18
+ - `bun run build`: production build
19
+ - `bun run commit`: guided Conventional Commit
20
+
21
+ See [AGENT.md](./AGENT.md) for conventions.
@@ -0,0 +1,15 @@
1
+ node_modules
2
+ dist
3
+ .next
4
+ .turbo
5
+ out
6
+ coverage
7
+ playwright-report
8
+ test-results
9
+ blob-report
10
+ *.tsbuildinfo
11
+ next-env.d.ts
12
+ .env
13
+ .env.*
14
+ !.env.example
15
+ .DS_Store
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@{{scope}}/api",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun --watch src/main.ts",
8
+ "build": "bun build src/main.ts --target bun --packages external --outdir dist --sourcemap=linked",
9
+ "start": "bun dist/main.js",
10
+ "check": "bun check",
11
+ "test": "bun test ./src"
12
+ },
13
+ "dependencies": {
14
+ "@{{scope}}/shared": "workspace:*",
15
+ "@nestjs/common": "^12.2.0",
16
+ "@nestjs/core": "^12.2.0",
17
+ "@nestjs/platform-express": "^12.2.0",
18
+ "@nestjs/swagger": "^12.0.2",
19
+ "class-transformer": "^0.5.1",
20
+ "class-validator": "^0.15.1",
21
+ "reflect-metadata": "^0.2.2",
22
+ "rxjs": "^7.8.2"
23
+ },
24
+ "devDependencies": {
25
+ "@nestjs/testing": "^12.2.0",
26
+ "@types/bun": "^1.4.3",
27
+ "typescript": "^6.0.3"
28
+ }
29
+ }
@@ -0,0 +1,7 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { HealthModule } from "./health/health.module";
3
+
4
+ @Module({
5
+ imports: [HealthModule],
6
+ })
7
+ export class AppModule {}
@@ -0,0 +1,19 @@
1
+ import "reflect-metadata";
2
+ import { beforeEach, describe, expect, test } from "bun:test";
3
+ import { Test } from "@nestjs/testing";
4
+ import { HealthController } from "./health.controller";
5
+
6
+ describe("HealthController", () => {
7
+ let controller: HealthController;
8
+
9
+ beforeEach(async () => {
10
+ const moduleRef = await Test.createTestingModule({
11
+ controllers: [HealthController],
12
+ }).compile();
13
+ controller = moduleRef.get(HealthController);
14
+ });
15
+
16
+ test("reports ok", () => {
17
+ expect(controller.check().status).toBe("ok");
18
+ });
19
+ });
@@ -0,0 +1,17 @@
1
+ import type { HealthResponse } from "@{{scope}}/shared";
2
+ import { Controller, Get } from "@nestjs/common";
3
+ import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
4
+
5
+ @ApiTags("health")
6
+ @Controller("health")
7
+ export class HealthController {
8
+ @Get()
9
+ @ApiOkResponse({ description: "Service is up" })
10
+ check(): HealthResponse {
11
+ return {
12
+ status: "ok",
13
+ uptime: process.uptime(),
14
+ timestamp: new Date().toISOString(),
15
+ };
16
+ }
17
+ }
@@ -0,0 +1,7 @@
1
+ import { Module } from "@nestjs/common";
2
+ import { HealthController } from "./health.controller";
3
+
4
+ @Module({
5
+ controllers: [HealthController],
6
+ })
7
+ export class HealthModule {}
@@ -0,0 +1,21 @@
1
+ import "reflect-metadata";
2
+ import { ValidationPipe } from "@nestjs/common";
3
+ import { NestFactory } from "@nestjs/core";
4
+ import { DocumentBuilder, SwaggerModule } from "@nestjs/swagger";
5
+ import { AppModule } from "./app.module";
6
+
7
+ async function bootstrap() {
8
+ const app = await NestFactory.create(AppModule);
9
+
10
+ app.enableCors({ origin: process.env.WEB_ORIGIN ?? "http://localhost:3000" });
11
+ app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
12
+
13
+ const config = new DocumentBuilder().setTitle("{{projectName}} API").setVersion("0.0.0").build();
14
+ SwaggerModule.setup("docs", app, () => SwaggerModule.createDocument(app, config));
15
+
16
+ const port = Number(process.env.PORT ?? 3001);
17
+ await app.listen(port);
18
+ console.log(`API listening on http://localhost:${port} (docs at /docs)`);
19
+ }
20
+
21
+ bootstrap();
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "experimentalDecorators": true,
5
+ "emitDecoratorMetadata": true,
6
+ "strictPropertyInitialization": false,
7
+ "types": ["bun"]
8
+ },
9
+ "include": ["src"]
10
+ }
@@ -0,0 +1 @@
1
+ @import "tailwindcss";
@@ -0,0 +1,17 @@
1
+ import type { Metadata } from "next";
2
+ import type { ReactNode } from "react";
3
+ import "./globals.css";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "{{projectName}}",
7
+ };
8
+
9
+ export default function RootLayout({ children }: { children: ReactNode }) {
10
+ return (
11
+ <html lang="en">
12
+ <body className="min-h-screen bg-white text-neutral-900 antialiased dark:bg-neutral-950 dark:text-neutral-100">
13
+ {children}
14
+ </body>
15
+ </html>
16
+ );
17
+ }
@@ -0,0 +1,25 @@
1
+ import type { HealthResponse } from "@{{scope}}/shared";
2
+
3
+ export const dynamic = "force-dynamic";
4
+
5
+ const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3001";
6
+
7
+ async function getHealth(): Promise<HealthResponse | null> {
8
+ try {
9
+ const res = await fetch(`${API_URL}/health`, { cache: "no-store" });
10
+ return res.ok ? ((await res.json()) as HealthResponse) : null;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ export default async function Home() {
17
+ const health = await getHealth();
18
+
19
+ return (
20
+ <main className="mx-auto flex max-w-xl flex-col gap-4 px-4 py-24">
21
+ <h1 className="text-3xl font-semibold">{{projectName}}</h1>
22
+ <p data-testid="api-status">API: {health ? health.status : "unreachable"}</p>
23
+ </main>
24
+ );
25
+ }
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ transpilePackages: ["@{{scope}}/shared"],
5
+ };
6
+
7
+ export default nextConfig;
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@{{scope}}/web",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "bun --bun next dev --port 3000",
8
+ "build": "bun --bun next build",
9
+ "start": "bun --bun next start --port 3000",
10
+ "check": "bun check",
11
+ "test": "bun test ./app --pass-with-no-tests"
12
+ },
13
+ "dependencies": {
14
+ "@{{scope}}/shared": "workspace:*",
15
+ "next": "^16.4.2",
16
+ "react": "^19.3.1",
17
+ "react-dom": "^19.3.1"
18
+ },
19
+ "devDependencies": {
20
+ "@tailwindcss/postcss": "^4.3.3",
21
+ "@types/bun": "^1.4.3",
22
+ "@types/node": "^26.6.5",
23
+ "@types/react": "^19.3.0",
24
+ "@types/react-dom": "^19.3.0",
25
+ "tailwindcss": "^4.3.3",
26
+ "typescript": "^6.0.3"
27
+ }
28
+ }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "lib": ["DOM", "DOM.Iterable", "ES2023"],
5
+ "jsx": "preserve",
6
+ "allowJs": true,
7
+ "incremental": true,
8
+ "types": ["bun"],
9
+ "plugins": [{ "name": "next" }],
10
+ "paths": {
11
+ "@/*": ["./*"]
12
+ }
13
+ },
14
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
15
+ "exclude": ["node_modules", ".next"]
16
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ extends: ["@commitlint/config-conventional"],
3
+ rules: {
4
+ "scope-enum": [2, "always", ["api", "web", "shared", "e2e", "deps", "ci", "release"]],
5
+ },
6
+ };
@@ -0,0 +1,7 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ test("GET /health returns ok", async ({ request }) => {
4
+ const res = await request.get("http://localhost:3001/health");
5
+ expect(res.ok()).toBe(true);
6
+ expect(await res.json()).toMatchObject({ status: "ok" });
7
+ });
@@ -0,0 +1,6 @@
1
+ import { expect, test } from "@playwright/test";
2
+
3
+ test("home page shows the API is up", async ({ page }) => {
4
+ await page.goto("/");
5
+ await expect(page.getByTestId("api-status")).toHaveText("API: ok");
6
+ });
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "packageManager": "bun@1.4.3",
6
+ "workspaces": [
7
+ "apps/*",
8
+ "packages/*"
9
+ ],
10
+ "scripts": {
11
+ "dev": "turbo dev",
12
+ "build": "turbo build",
13
+ "start": "turbo start",
14
+ "check": "turbo check",
15
+ "test": "turbo test",
16
+ "e2e": "playwright test",
17
+ "e2e:install": "playwright install chromium",
18
+ "commit": "cz",
19
+ "prepare": "husky"
20
+ },
21
+ "devDependencies": {
22
+ "@commitlint/cli": "^21.2.4",
23
+ "@commitlint/config-conventional": "^21.2.4",
24
+ "@commitlint/cz-commitlint": "^21.2.4",
25
+ "@playwright/test": "^1.64.1",
26
+ "@types/bun": "^1.4.3",
27
+ "commitizen": "^4.3.2",
28
+ "husky": "^9.1.7",
29
+ "inquirer": "^12.11.1",
30
+ "turbo": "^2.11.10",
31
+ "typescript": "^6.0.3"
32
+ },
33
+ "config": {
34
+ "commitizen": {
35
+ "path": "@commitlint/cz-commitlint"
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@{{scope}}/shared",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "check": "bun check",
11
+ "test": "bun test ./src --pass-with-no-tests"
12
+ },
13
+ "devDependencies": {
14
+ "@types/bun": "^1.4.3",
15
+ "typescript": "^6.0.3"
16
+ }
17
+ }
@@ -0,0 +1,5 @@
1
+ export interface HealthResponse {
2
+ status: "ok";
3
+ uptime: number;
4
+ timestamp: string;
5
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "types": ["bun"]
5
+ },
6
+ "include": ["src"]
7
+ }
@@ -0,0 +1,28 @@
1
+ import { defineConfig, devices } from "@playwright/test";
2
+
3
+ const CI = !!process.env.CI;
4
+
5
+ export default defineConfig({
6
+ testDir: "./e2e",
7
+ fullyParallel: true,
8
+ forbidOnly: CI,
9
+ retries: CI ? 2 : 0,
10
+ reporter: CI ? [["github"], ["html", { open: "never" }]] : "list",
11
+ use: {
12
+ baseURL: "http://localhost:3000",
13
+ trace: "on-first-retry",
14
+ },
15
+ projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
16
+ webServer: [
17
+ {
18
+ command: "bun run --filter=@{{scope}}/api dev",
19
+ url: "http://localhost:3001/health",
20
+ reuseExistingServer: !CI,
21
+ },
22
+ {
23
+ command: "bun run --filter=@{{scope}}/web dev",
24
+ url: "http://localhost:3000",
25
+ reuseExistingServer: !CI,
26
+ },
27
+ ],
28
+ });
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
3
+ "release-type": "node",
4
+ "include-component-in-tag": false,
5
+ "packages": {
6
+ ".": {
7
+ "changelog-path": "CHANGELOG.md"
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "module": "Preserve",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2023"],
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "noFallthroughCasesInSwitch": true,
10
+ "esModuleInterop": true,
11
+ "resolveJsonModule": true,
12
+ "isolatedModules": true,
13
+ "skipLibCheck": true,
14
+ "noEmit": true
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "types": ["bun"]
5
+ },
6
+ "include": ["playwright.config.ts", "commitlint.config.ts", "e2e"]
7
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "$schema": "https://turborepo.com/schema.json",
3
+ "ui": "stream",
4
+ "tasks": {
5
+ "build": {
6
+ "dependsOn": ["^build"],
7
+ "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
8
+ },
9
+ "check": {
10
+ "dependsOn": ["^check"]
11
+ },
12
+ "test": {
13
+ "dependsOn": ["^build"]
14
+ },
15
+ "dev": {
16
+ "cache": false,
17
+ "persistent": true
18
+ },
19
+ "start": {
20
+ "dependsOn": ["build"],
21
+ "cache": false,
22
+ "persistent": true
23
+ }
24
+ }
25
+ }