bank20baht-cli 0.1.3 → 0.2.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.
@@ -1,4 +1,4 @@
1
- import { planCreate } from '../domain/create-plan.js';
1
+ import { planCreate, PRESET_TEMPLATE_SET } from '../domain/create-plan.js';
2
2
  import { validateProjectName } from '../domain/naming.js';
3
3
  export function runCreate(input, deps) {
4
4
  const errors = validateProjectName(input.name);
@@ -7,7 +7,7 @@ export function runCreate(input, deps) {
7
7
  deps.log.error(e);
8
8
  throw new CreateError('Invalid project name.');
9
9
  }
10
- const blueprint = planCreate(input, deps.templates.load('create'), (raw, data) => deps.renderer.render(raw, data));
10
+ const blueprint = planCreate(input, deps.templates.load(PRESET_TEMPLATE_SET[input.preset]), (raw, data) => deps.renderer.render(raw, data));
11
11
  if (input.dryRun) {
12
12
  deps.log.info(`dry-run — would create in ${blueprint.root}:`);
13
13
  for (const f of blueprint.files)
@@ -34,8 +34,19 @@ export function runCreate(input, deps) {
34
34
  deps.log.info(` cd ${input.name}`);
35
35
  if (!input.install)
36
36
  deps.log.info(' bun install');
37
- deps.log.info(' bun run dev # web :5173 + api :3001');
38
- deps.log.info(' bun run lint && bun run test && bun run build');
37
+ for (const line of NEXT_STEPS[input.preset])
38
+ deps.log.info(` ${line}`);
39
39
  }
40
+ const NEXT_STEPS = {
41
+ full: [
42
+ 'bun run dev # web :5173 + api :3001',
43
+ 'bun run lint && bun run test && bun run build',
44
+ ],
45
+ workspace: [
46
+ 'mkdir -p apps/<name> && add a package.json with dev/build/typecheck scripts',
47
+ 'bun run dev # runs every apps/*/ dev script in parallel',
48
+ 'bun run check # lint + format:check + knip + typecheck',
49
+ ],
50
+ };
40
51
  export class CreateError extends Error {
41
52
  }
package/dist/cli.js CHANGED
@@ -12,7 +12,9 @@ import { ShellRunner } from './infrastructure/shell-runner.js';
12
12
  const HELP = `bahtui — bun-workspace DDD scaffolding (React + Elysia + Eden)
13
13
 
14
14
  Usage:
15
- bahtui create <name> [options] scaffold a new monorepo in ./<name>
15
+ bahtui new <name> [options] bare bun workspace in ./<name> — toolchain only
16
+ (eslint, prettier, knip, husky, commitlint), no apps
17
+ bahtui create <name> [options] full monorepo in ./<name> (React + Elysia + Eden)
16
18
  bahtui init [options] write bahtui.json into an existing repo
17
19
  bahtui g model <domain> libs/domain/<d>/model package
18
20
  bahtui g form <domain>/<name> --fields "key:type:mod,…"
@@ -25,8 +27,9 @@ Options:
25
27
  --frontend <react|none> init: default react
26
28
  --backend <elysia|none> init: default elysia
27
29
  --dry-run print what would be generated, write nothing
28
- --no-install create: skip "bun install" (also skips the initial commit)
29
- --no-git create: skip git init + initial commit
30
+ --no-install new/create: skip "bun install" (also skips the initial commit)
31
+ --no-git new/create: skip git init + initial commit
32
+ --skip-install / --skip-git Angular-style aliases of the two above
30
33
  --force overwrite existing files/dirs
31
34
  -h, --help this help
32
35
  -v, --version print version
@@ -50,6 +53,8 @@ const KNOWN_FLAGS = new Set([
50
53
  '--force',
51
54
  '--no-install',
52
55
  '--no-git',
56
+ '--skip-install',
57
+ '--skip-git',
53
58
  ]);
54
59
  /**
55
60
  * Value-carrying flags pulled out before positional parsing. Supports both
@@ -116,16 +121,17 @@ function main(rawArgv) {
116
121
  log,
117
122
  };
118
123
  try {
119
- if (command === 'create') {
124
+ if (command === 'create' || command === 'new') {
120
125
  if (!name) {
121
- log.error('create needs a project name: bahtui create <name>');
126
+ log.error(`${command} needs a project name: bahtui ${command} <name>`);
122
127
  return 1;
123
128
  }
124
129
  runCreate({
125
130
  name,
131
+ preset: command === 'new' ? 'workspace' : 'full',
126
132
  targetDir: resolve(process.cwd(), name),
127
- install: !flags.has('--no-install'),
128
- git: !flags.has('--no-git'),
133
+ install: !flags.has('--no-install') && !flags.has('--skip-install'),
134
+ git: !flags.has('--no-git') && !flags.has('--skip-git'),
129
135
  dryRun,
130
136
  force,
131
137
  }, {
@@ -1,3 +1,7 @@
1
+ export const PRESET_TEMPLATE_SET = {
2
+ full: 'create',
3
+ workspace: 'new',
4
+ };
1
5
  /**
2
6
  * Template naming conventions (the whole contract between templates/ and here):
3
7
  * - `*.hbs` → rendered with the data below, suffix stripped.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "bank20baht-cli",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
- "description": "Scaffold bun-workspace DDD monorepos (React + Elysia + Eden) and bahtui generators.",
5
+ "description": "Scaffold bun workspaces (bahtui new) and DDD monorepos (React + Elysia + Eden, bahtui create) plus bahtui generators.",
6
6
  "license": "MIT",
7
7
  "author": "Nattapong Promthong",
8
8
  "repository": {
@@ -0,0 +1,3 @@
1
+ # Search tool
2
+
3
+ Use the `zvec_grep` MCP tool for code search in this repo instead of Grep/Glob/`grep`/`rg`.
@@ -0,0 +1,36 @@
1
+ # {{name}}
2
+
3
+ Bun-workspace monorepo scaffolded by [`bank20baht-cli`](https://github.com/bank20baht/bahtui)
4
+ (`bahtui new`). Toolchain only — add your own apps under `apps/*` and shared
5
+ libraries under `packages/*`.
6
+
7
+ ## Quickstart
8
+
9
+ ```sh
10
+ bun install
11
+ bun run dev # runs every apps/*/package.json "dev" script in parallel
12
+ bun run check # lint + format:check + knip + typecheck (also the pre-push hook)
13
+ ```
14
+
15
+ ## Layout
16
+
17
+ ```
18
+ apps/<name> runnable applications (each with its own package.json)
19
+ packages/<name> shared libraries consumed by apps via workspace:*
20
+ scripts/dev.ts parallel dev runner with prefixed output (apps/* only)
21
+ scripts/each.ts `bun run scripts/each.ts <script>` in every workspace that has it
22
+ ```
23
+
24
+ Add a workspace by creating `apps/<name>/package.json` (or `packages/<name>`)
25
+ with `dev`, `build` and `typecheck` scripts, then extend `knip.json` with a
26
+ matching workspace entry so dead-code checks cover it.
27
+
28
+ ## Toolchain
29
+
30
+ - **ESLint 10 + typescript-eslint** (`eslint.config.ts`), **Prettier** (no semi,
31
+ single quotes, width 100), **knip** — all run by `bun run check`.
32
+ - **husky** hooks: `pre-commit` → lint-staged, `commit-msg` → commitlint
33
+ (Conventional Commits), `pre-push` → `bun run check`.
34
+ - `tsconfig.base.json` — strict, `moduleResolution: bundler`,
35
+ `verbatimModuleSyntax`, `noUncheckedIndexedAccess`. Extend it from each
36
+ workspace's `tsconfig.json`.
@@ -0,0 +1,9 @@
1
+ node_modules
2
+ .zvec-grep
3
+ dist
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+ *.log
8
+ .DS_Store
9
+ bun.lockb
@@ -0,0 +1 @@
1
+ bunx commitlint --edit "$1"
@@ -0,0 +1 @@
1
+ bunx lint-staged
@@ -0,0 +1 @@
1
+ bun run check
@@ -0,0 +1,9 @@
1
+ {
2
+ "mcpServers": {
3
+ "zvec_grep": {
4
+ "type": "stdio",
5
+ "command": "zg",
6
+ "args": ["server", "--stdio"]
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,3 @@
1
+ node_modules
2
+ dist
3
+ bun.lock
@@ -0,0 +1,6 @@
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "trailingComma": "all",
5
+ "printWidth": 100
6
+ }
File without changes
@@ -0,0 +1,3 @@
1
+ export default {
2
+ extends: ['@commitlint/config-conventional'],
3
+ }
@@ -0,0 +1,24 @@
1
+ import js from '@eslint/js'
2
+ import prettier from 'eslint-config-prettier'
3
+ import tseslint from 'typescript-eslint'
4
+
5
+ export default tseslint.config(
6
+ {
7
+ ignores: ['**/dist/**', '**/node_modules/**', '**/*.gen.ts', '**/.angular/**'],
8
+ },
9
+ js.configs.recommended,
10
+ ...tseslint.configs.recommended,
11
+ {
12
+ languageOptions: {
13
+ parserOptions: { tsconfigRootDir: import.meta.dirname },
14
+ },
15
+ rules: {
16
+ '@typescript-eslint/no-unused-vars': [
17
+ 'error',
18
+ { argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
19
+ ],
20
+ '@typescript-eslint/consistent-type-imports': 'error',
21
+ },
22
+ },
23
+ prettier,
24
+ )
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://unpkg.com/knip@latest/schema.json",
3
+ "workspaces": {
4
+ ".": {
5
+ "entry": ["scripts/*.ts"],
6
+ "project": ["scripts/**/*.ts"]
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "{{name}}",
3
+ "private": true,
4
+ "type": "module",
5
+ "workspaces": ["apps/*", "packages/*"],
6
+ "scripts": {
7
+ "dev": "bun run scripts/dev.ts",
8
+ "build": "bun run scripts/each.ts build",
9
+ "typecheck": "tsc -p tsconfig.json && bun run scripts/each.ts typecheck",
10
+ "lint": "eslint .",
11
+ "lint:fix": "eslint . --fix",
12
+ "format": "prettier --write .",
13
+ "format:check": "prettier --check .",
14
+ "knip": "knip",
15
+ "check": "bun run lint && bun run format:check && bun run knip && bun run typecheck",
16
+ "prepare": "husky"
17
+ },
18
+ "lint-staged": {
19
+ "*.{ts,tsx}": ["eslint --fix", "prettier --write"],
20
+ "*.{json,md,html,css}": ["prettier --write"]
21
+ },
22
+ "devDependencies": {
23
+ "@commitlint/cli": "^21.2.2",
24
+ "@commitlint/config-conventional": "^21.2.2",
25
+ "@eslint/js": "^10.0.1",
26
+ "@types/bun": "^1.3.0",
27
+ "eslint": "^10.10.0",
28
+ "eslint-config-prettier": "^10.1.8",
29
+ "husky": "^9.1.7",
30
+ "jiti": "^2.6.1",
31
+ "knip": "^6.35.1",
32
+ "lint-staged": "^17.5.1",
33
+ "prettier": "^3.9.6",
34
+ "typescript": "^5.9.3",
35
+ "typescript-eslint": "^8.70.0"
36
+ }
37
+ }
File without changes
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Run every `apps/*` workspace `dev` script in parallel with prefixed,
3
+ * live-streamed output. Replaces `bun run --filter '*' dev`, which serializes
4
+ * long-running scripts and never starts the second one.
5
+ */
6
+ import { workspacesWithScript } from './workspaces.ts'
7
+
8
+ const colors = ['\x1b[36m', '\x1b[35m', '\x1b[33m', '\x1b[32m', '\x1b[34m']
9
+ const reset = '\x1b[0m'
10
+
11
+ const targets = workspacesWithScript('dev', ['apps']).map((ws, i) => ({
12
+ ...ws,
13
+ color: colors[i % colors.length],
14
+ }))
15
+
16
+ if (targets.length === 0) {
17
+ console.log('No apps/*/package.json with a "dev" script yet — nothing to run.')
18
+ process.exit(0)
19
+ }
20
+
21
+ const procs = targets.map(({ name, cwd, color }) => {
22
+ const proc = Bun.spawn(['bun', 'run', 'dev'], {
23
+ cwd,
24
+ env: process.env,
25
+ stdout: 'pipe',
26
+ stderr: 'pipe',
27
+ })
28
+
29
+ const prefix = (line: string) => `${color}[${name}]${reset} ${line}`
30
+ const pump = async (stream: ReadableStream<Uint8Array>) => {
31
+ const decoder = new TextDecoder()
32
+ let buf = ''
33
+ for await (const chunk of stream) {
34
+ buf += decoder.decode(chunk, { stream: true })
35
+ const lines = buf.split('\n')
36
+ buf = lines.pop() ?? ''
37
+ for (const line of lines) console.log(prefix(line))
38
+ }
39
+ if (buf) console.log(prefix(buf))
40
+ }
41
+
42
+ pump(proc.stdout)
43
+ pump(proc.stderr)
44
+ return proc
45
+ })
46
+
47
+ const shutdown = () => {
48
+ for (const p of procs) p.kill()
49
+ process.exit(0)
50
+ }
51
+ process.on('SIGINT', shutdown)
52
+ process.on('SIGTERM', shutdown)
53
+
54
+ await Promise.all(procs.map((p) => p.exited))
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `bun run scripts/each.ts <script>` — run `<script>` in every apps/* and
3
+ * packages/* workspace that defines it, one after another, failing on the
4
+ * first non-zero exit. Unlike `bun run --filter '*' <script>` it exits 0 when
5
+ * no workspace defines the script yet, so a fresh repo passes `bun run check`.
6
+ */
7
+ import { workspacesWithScript } from './workspaces.ts'
8
+
9
+ const script = process.argv[2]
10
+ if (!script) {
11
+ console.error('usage: bun run scripts/each.ts <script>')
12
+ process.exit(1)
13
+ }
14
+
15
+ const targets = workspacesWithScript(script)
16
+ if (targets.length === 0) {
17
+ console.log(`No workspace defines "${script}" yet — nothing to run.`)
18
+ process.exit(0)
19
+ }
20
+
21
+ for (const { name, cwd } of targets) {
22
+ console.log(`\x1b[36m[${name}]\x1b[0m bun run ${script}`)
23
+ const proc = Bun.spawnSync(['bun', 'run', script], { cwd, env: process.env, stdio: ['inherit', 'inherit', 'inherit'] })
24
+ if (proc.exitCode !== 0) process.exit(proc.exitCode ?? 1)
25
+ }
@@ -0,0 +1,24 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export interface Workspace {
5
+ name: string
6
+ cwd: string
7
+ }
8
+
9
+ /** Every `apps/*` and `packages/*` directory whose package.json defines `script`. */
10
+ export function workspacesWithScript(script: string, groups = ['apps', 'packages']): Workspace[] {
11
+ const found: Workspace[] = []
12
+ for (const group of groups) {
13
+ if (!existsSync(group)) continue
14
+ for (const entry of readdirSync(group, { withFileTypes: true })) {
15
+ if (!entry.isDirectory()) continue
16
+ const cwd = join(group, entry.name)
17
+ const pkgPath = join(cwd, 'package.json')
18
+ if (!existsSync(pkgPath)) continue
19
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { scripts?: Record<string, string> }
20
+ if (typeof pkg.scripts?.[script] === 'string') found.push({ name: entry.name, cwd })
21
+ }
22
+ }
23
+ return found
24
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "moduleDetection": "force",
7
+ "allowImportingTsExtensions": true,
8
+ "verbatimModuleSyntax": true,
9
+ "esModuleInterop": true,
10
+ "resolveJsonModule": true,
11
+ "strict": true,
12
+ "skipLibCheck": true,
13
+ "noUncheckedIndexedAccess": true,
14
+ "noEmit": true
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "types": ["bun"]
5
+ },
6
+ "include": ["scripts"]
7
+ }