@etus/bhono-app 0.1.3 → 0.1.4

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.
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ function buildProgram() {
13
13
  return new Command()
14
14
  .name('bhono-app')
15
15
  .description('Create a new project from the Etus boilerplate')
16
- .version('0.1.3')
16
+ .version('0.1.4')
17
17
  .argument('<project-name>', 'Name of the project')
18
18
  .option('-d, --domain <domain>', 'Production domain')
19
19
  .option('-m, --modules <modules>', 'Comma-separated modules to include')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@etus/bhono-app",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "dist",
@@ -1,9 +1,10 @@
1
1
  #!/bin/bash
2
2
 
3
- # Hono Multi-Tenant SaaS Boilerplate - Development Environment Setup
4
- # This script initializes the development environment for future coding agents
3
+ # BHono - Development Environment Setup
4
+ # This script bootstraps dependencies, configures Cloudflare bindings,
5
+ # seeds the local D1 (sqlite) database, and starts the dev server.
5
6
 
6
- set -e
7
+ set -euo pipefail
7
8
 
8
9
  # Colors for output
9
10
  RED='\033[0;31m'
@@ -12,110 +13,278 @@ YELLOW='\033[1;33m'
12
13
  BLUE='\033[0;34m'
13
14
  NC='\033[0m' # No Color
14
15
 
15
- echo -e "${BLUE}========================================${NC}"
16
- echo -e "${BLUE} Hono Boilerplate - Dev Environment ${NC}"
17
- echo -e "${BLUE}========================================${NC}"
18
- echo ""
16
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17
+ cd "$ROOT_DIR"
18
+
19
+ UPDATE_PACKAGES=0
20
+ SKIP_DEV=0
21
+ SKIP_PROVISION=0
22
+
23
+ while [[ $# -gt 0 ]]; do
24
+ case "$1" in
25
+ --update)
26
+ UPDATE_PACKAGES=1
27
+ ;;
28
+ --skip-dev)
29
+ SKIP_DEV=1
30
+ ;;
31
+ --no-provision)
32
+ SKIP_PROVISION=1
33
+ ;;
34
+ *)
35
+ echo -e "${YELLOW}Ignoring unknown argument: $1${NC}"
36
+ ;;
37
+ esac
38
+ shift
39
+ done
40
+
41
+ log_info() { echo -e "${BLUE}$*${NC}"; }
42
+ log_ok() { echo -e "${GREEN}$*${NC}"; }
43
+ log_warn() { echo -e "${YELLOW}$*${NC}"; }
44
+ log_err() { echo -e "${RED}$*${NC}"; }
45
+
46
+ log_info "========================================"
47
+ log_info " BHono - Dev Environment Setup "
48
+ log_info "========================================"
19
49
 
20
50
  # Check for required tools
21
- echo -e "${YELLOW}Checking required tools...${NC}"
51
+ log_info "Checking required tools..."
22
52
 
23
- if ! command -v node &> /dev/null; then
24
- echo -e "${RED}Error: Node.js is not installed${NC}"
25
- echo "Please install Node.js 18+ from https://nodejs.org/"
26
- exit 1
53
+ if ! command -v node >/dev/null 2>&1; then
54
+ log_err "Error: Node.js is not installed"
55
+ log_err "Install Node.js 18+ from https://nodejs.org/"
56
+ exit 1
27
57
  fi
28
58
 
29
- if ! command -v pnpm &> /dev/null; then
30
- echo -e "${YELLOW}pnpm not found. Installing...${NC}"
31
- npm install -g pnpm
59
+ if ! command -v pnpm >/dev/null 2>&1; then
60
+ log_warn "pnpm not found. Installing..."
61
+ npm install -g pnpm
32
62
  fi
33
63
 
34
- NODE_VERSION=$(node -v | cut -d'v' -f2 | cut -d'.' -f1)
35
- if [ "$NODE_VERSION" -lt 18 ]; then
36
- echo -e "${RED}Error: Node.js 18+ is required (found v${NODE_VERSION})${NC}"
37
- exit 1
64
+ NODE_MAJOR=$(node -p "Number(process.versions.node.split('.')[0])")
65
+ if [[ "$NODE_MAJOR" -lt 18 ]]; then
66
+ log_err "Error: Node.js 18+ is required (found v${NODE_MAJOR})"
67
+ exit 1
38
68
  fi
39
69
 
40
- echo -e "${GREEN}Node.js $(node -v) detected${NC}"
41
- echo -e "${GREEN}pnpm $(pnpm -v) detected${NC}"
70
+ log_ok "Node.js $(node -v) detected"
71
+ log_ok "pnpm $(pnpm -v) detected"
42
72
 
43
73
  # Install dependencies
44
- echo ""
45
- echo -e "${YELLOW}Installing dependencies...${NC}"
74
+ log_info "Installing dependencies..."
46
75
  pnpm install
47
76
 
48
- # Check for .env file
49
- if [ ! -f .env ]; then
50
- if [ -f .env.example ]; then
51
- echo ""
52
- echo -e "${YELLOW}Creating .env from .env.example...${NC}"
53
- cp .env.example .env
54
- echo -e "${YELLOW}Please update .env with your actual values:${NC}"
55
- echo " - GOOGLE_CLIENT_ID"
56
- echo " - GOOGLE_CLIENT_SECRET"
57
- echo " - JWT_SECRET (min 32 chars)"
58
- echo " - SENDGRID_API_KEY"
59
- else
60
- echo -e "${YELLOW}Warning: No .env file found. Create one based on required environment variables.${NC}"
77
+ if [[ "$UPDATE_PACKAGES" -eq 1 ]]; then
78
+ log_info "Updating dependencies..."
79
+ pnpm update
80
+ fi
81
+
82
+ # Check for .env files
83
+ if [[ ! -f .env && -f .env.example ]]; then
84
+ log_info "Creating .env from .env.example..."
85
+ cp .env.example .env
86
+ log_warn "Update .env with real values (GOOGLE_CLIENT_ID/SECRET, JWT_SECRET, SENDGRID_API_KEY)."
87
+ fi
88
+
89
+ if [[ ! -f .dev.vars && -f .dev.vars.example ]]; then
90
+ log_info "Creating .dev.vars from .dev.vars.example..."
91
+ cp .dev.vars.example .dev.vars
92
+ fi
93
+
94
+ # Determine project name
95
+ PROJECT_NAME_RAW=$(node -e "
96
+ const fs = require('fs');
97
+ const path = require('path');
98
+ let name = '';
99
+ try { name = JSON.parse(fs.readFileSync('etus.config.json','utf8')).name || ''; } catch (e) {}
100
+ if (!name) { try { name = JSON.parse(fs.readFileSync('package.json','utf8')).name || ''; } catch (e) {} }
101
+ if (!name) name = path.basename(process.cwd());
102
+ console.log(name);
103
+ " 2>/dev/null || echo "")
104
+
105
+ PROJECT_NAME="${PROJECT_NAME_RAW##*/}"
106
+ PROJECT_NAME=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+|-+$//g')
107
+ if [[ -z "$PROJECT_NAME" ]]; then
108
+ PROJECT_NAME=$(basename "$ROOT_DIR" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+|-+$//g')
109
+ fi
110
+
111
+ log_ok "Project name: $PROJECT_NAME"
112
+
113
+ # Ensure wrangler.json exists
114
+ if [[ ! -f config/wrangler.json ]]; then
115
+ log_err "Missing config/wrangler.json"
116
+ exit 1
117
+ fi
118
+
119
+ # Update wrangler.json placeholders
120
+ PROJECT_NAME="$PROJECT_NAME" node - <<'NODE'
121
+ const fs = require('fs');
122
+ const path = 'config/wrangler.json';
123
+ const data = JSON.parse(fs.readFileSync(path, 'utf8'));
124
+ const projectName = process.env.PROJECT_NAME || '';
125
+
126
+ function replacePlaceholders(value) {
127
+ if (typeof value === 'string') {
128
+ return value.split('{{projectName}}').join(projectName);
129
+ }
130
+ if (Array.isArray(value)) {
131
+ return value.map(replacePlaceholders);
132
+ }
133
+ if (value && typeof value === 'object') {
134
+ for (const key of Object.keys(value)) {
135
+ value[key] = replacePlaceholders(value[key]);
136
+ }
137
+ return value;
138
+ }
139
+ return value;
140
+ }
141
+
142
+ replacePlaceholders(data);
143
+ if (!data.name || data.name.includes('{{projectName}}')) {
144
+ data.name = projectName;
145
+ }
146
+
147
+ fs.writeFileSync(path, JSON.stringify(data, null, 2));
148
+ NODE
149
+
150
+ WRANGLER="pnpm exec wrangler"
151
+ if ! $WRANGLER --version >/dev/null 2>&1; then
152
+ log_warn "Wrangler not available. Ensure dependencies are installed."
153
+ fi
154
+
155
+ # Resolve names from wrangler.json
156
+ DB_NAME=$(node -e "const c=require('./config/wrangler.json'); console.log((c.d1_databases&&c.d1_databases[0]&&c.d1_databases[0].database_name)||'');")
157
+ if [[ -z "$DB_NAME" ]]; then
158
+ DB_NAME="${PROJECT_NAME}-db"
159
+ fi
160
+
161
+ R2_BUCKET=$(node -e "const c=require('./config/wrangler.json'); console.log((c.r2_buckets&&c.r2_buckets[0]&&c.r2_buckets[0].bucket_name)||'');")
162
+ if [[ -z "$R2_BUCKET" ]]; then
163
+ R2_BUCKET="${PROJECT_NAME}-storage"
164
+ fi
165
+
166
+ KV_NAME="${PROJECT_NAME}-sessions"
167
+
168
+ # Read existing IDs from wrangler.json (if present)
169
+ D1_ID=$(node -e "const c=require('./config/wrangler.json'); console.log((c.d1_databases&&c.d1_databases[0]&&c.d1_databases[0].database_id)||'');")
170
+ KV_ID=$(node -e "const c=require('./config/wrangler.json'); console.log((c.kv_namespaces&&c.kv_namespaces[0]&&c.kv_namespaces[0].id)||'');")
171
+ if [[ "$D1_ID" == "TO_BE_PROVISIONED" ]]; then D1_ID=""; fi
172
+ if [[ "$KV_ID" == "TO_BE_PROVISIONED" ]]; then KV_ID=""; fi
173
+
174
+ if [[ "$SKIP_PROVISION" -eq 0 ]]; then
175
+ if $WRANGLER whoami >/dev/null 2>&1; then
176
+ log_info "Provisioning Cloudflare resources (D1, KV, R2)..."
177
+
178
+ # D1
179
+ D1_LIST=$($WRANGLER d1 list --json || echo '[]')
180
+ D1_ID=$(node -e "const fs=require('fs'); const list=JSON.parse(fs.readFileSync(0,'utf8')||'[]'); const name=process.env.DB_NAME; const item=list.find(x=>x.name===name); console.log(item?.uuid||item?.id||'');" <<< "$D1_LIST")
181
+ if [[ -z "$D1_ID" ]]; then
182
+ D1_CREATE=$($WRANGLER d1 create "$DB_NAME" --json)
183
+ D1_ID=$(node -e "const obj=JSON.parse(process.env.JSON||'{}'); console.log(obj.uuid||obj.id||'');" JSON="$D1_CREATE")
184
+ fi
185
+
186
+ # KV
187
+ KV_LIST=$($WRANGLER kv namespace list --json || echo '[]')
188
+ KV_ID=$(node -e "const fs=require('fs'); const list=JSON.parse(fs.readFileSync(0,'utf8')||'[]'); const name=process.env.KV_NAME; const item=list.find(x=>x.title===name||x.name===name); console.log(item?.id||'');" <<< "$KV_LIST")
189
+ if [[ -z "$KV_ID" ]]; then
190
+ KV_CREATE=$($WRANGLER kv namespace create "$KV_NAME" --json)
191
+ KV_ID=$(node -e "const obj=JSON.parse(process.env.JSON||'{}'); console.log(obj.id||'');" JSON="$KV_CREATE")
61
192
  fi
193
+
194
+ # R2
195
+ R2_LIST=$($WRANGLER r2 bucket list --json || echo '[]')
196
+ R2_EXISTS=$(node -e "const fs=require('fs'); const list=JSON.parse(fs.readFileSync(0,'utf8')||'[]'); const name=process.env.R2_NAME; const item=list.find(x=>x.name===name); console.log(item? 'yes':'no');" R2_NAME="$R2_BUCKET" <<< "$R2_LIST")
197
+ if [[ "$R2_EXISTS" != "yes" ]]; then
198
+ $WRANGLER r2 bucket create "$R2_BUCKET" >/dev/null
199
+ fi
200
+
201
+ log_ok "Cloudflare resources ready."
202
+ else
203
+ log_warn "Wrangler not logged in. Skipping remote provisioning."
204
+ fi
62
205
  fi
63
206
 
207
+ # If no remote IDs, generate local-only IDs for dev
208
+ if [[ -z "$D1_ID" ]]; then
209
+ D1_ID=$(node -e "console.log(require('crypto').randomUUID())")
210
+ log_warn "Using local D1 id: $D1_ID"
211
+ fi
212
+
213
+ if [[ -z "$KV_ID" ]]; then
214
+ KV_ID=$(node -e "console.log(require('crypto').randomUUID())")
215
+ log_warn "Using local KV id: $KV_ID"
216
+ fi
217
+
218
+ # Update wrangler.json with IDs
219
+ D1_ID="$D1_ID" KV_ID="$KV_ID" PROJECT_NAME="$PROJECT_NAME" node - <<'NODE'
220
+ const fs = require('fs');
221
+ const path = 'config/wrangler.json';
222
+ const data = JSON.parse(fs.readFileSync(path, 'utf8'));
223
+
224
+ const d1Id = process.env.D1_ID || '';
225
+ const kvId = process.env.KV_ID || '';
226
+
227
+ if (Array.isArray(data.d1_databases) && data.d1_databases[0]) {
228
+ data.d1_databases[0].database_id = d1Id;
229
+ }
230
+
231
+ if (Array.isArray(data.kv_namespaces) && data.kv_namespaces[0]) {
232
+ data.kv_namespaces[0].id = kvId;
233
+ }
234
+
235
+ fs.writeFileSync(path, JSON.stringify(data, null, 2));
236
+ NODE
237
+
64
238
  # Generate Cloudflare types
65
- echo ""
66
- echo -e "${YELLOW}Generating Cloudflare types...${NC}"
67
- pnpm cf-typegen 2>/dev/null || echo -e "${YELLOW}Skipping cf-typegen (wrangler not configured)${NC}"
68
-
69
- # Run database migrations (local)
70
- echo ""
71
- echo -e "${YELLOW}Running database migrations...${NC}"
72
- pnpm db:migrate:local 2>/dev/null || echo -e "${YELLOW}Skipping migrations (D1 not available locally)${NC}"
73
-
74
- # Seed database (optional)
75
- echo ""
76
- echo -e "${YELLOW}Do you want to seed the database with test data? (y/n)${NC}"
77
- read -r -t 10 SEED_RESPONSE || SEED_RESPONSE="n"
78
- if [[ "$SEED_RESPONSE" =~ ^[Yy]$ ]]; then
79
- pnpm db:seed 2>/dev/null || echo -e "${YELLOW}Skipping seed (D1 not available locally)${NC}"
80
- fi
81
-
82
- # Run tests to verify setup
83
- echo ""
84
- echo -e "${YELLOW}Running tests to verify setup...${NC}"
85
- pnpm test:run --reporter=dot 2>/dev/null || echo -e "${YELLOW}Tests skipped (run 'pnpm test' manually)${NC}"
86
-
87
- # Print summary
88
- echo ""
89
- echo -e "${GREEN}========================================${NC}"
90
- echo -e "${GREEN} Environment Setup Complete! ${NC}"
91
- echo -e "${GREEN}========================================${NC}"
92
- echo ""
93
- echo -e "${BLUE}Available commands:${NC}"
94
- echo ""
95
- echo -e " ${GREEN}pnpm dev${NC} Start development server (Vite + Wrangler)"
96
- echo -e " ${GREEN}pnpm build${NC} Build for production"
97
- echo -e " ${GREEN}pnpm deploy${NC} Deploy to Cloudflare Workers"
98
- echo ""
99
- echo -e " ${GREEN}pnpm db:migrate:local${NC} Apply migrations locally"
100
- echo -e " ${GREEN}pnpm db:migrate:remote${NC} Apply migrations to production"
101
- echo -e " ${GREEN}pnpm db:seed${NC} Seed test data"
102
- echo ""
103
- echo -e " ${GREEN}pnpm test${NC} Run unit tests (watch mode)"
104
- echo -e " ${GREEN}pnpm test:run${NC} Run unit tests (single run)"
105
- echo -e " ${GREEN}pnpm test:e2e${NC} Run Playwright E2E tests"
106
- echo -e " ${GREEN}pnpm test:e2e:ui${NC} Run E2E tests with interactive UI"
107
- echo -e " ${GREEN}pnpm test:coverage${NC} Generate coverage report"
108
- echo ""
109
- echo -e " ${GREEN}pnpm lint${NC} Run ESLint"
110
- echo -e " ${GREEN}pnpm cf-typegen${NC} Generate Cloudflare types"
111
- echo ""
112
- echo -e "${BLUE}Access the application:${NC}"
113
- echo ""
114
- echo -e " Local dev server: ${GREEN}http://localhost:5173${NC}"
115
- echo -e " API documentation: ${GREEN}http://localhost:5173/api/swagger${NC}"
116
- echo -e " OpenAPI JSON: ${GREEN}http://localhost:5173/api/doc${NC}"
117
- echo -e " Health check: ${GREEN}http://localhost:5173/health${NC}"
118
- echo ""
119
- echo -e "${YELLOW}To start development, run:${NC}"
120
- echo -e " ${GREEN}pnpm dev${NC}"
121
- echo ""
239
+ log_info "Generating Cloudflare types..."
240
+ pnpm cf-typegen >/dev/null 2>&1 || log_warn "Skipping cf-typegen (wrangler not configured)"
241
+
242
+ # Ensure local D1 exists and update drizzle config
243
+ log_info "Preparing local D1 database..."
244
+ $WRANGLER d1 execute "$DB_NAME" --local --command "SELECT 1;" >/dev/null 2>&1 || true
245
+
246
+ D1_ID_FROM_CONFIG=$(node -e "const c=require('./config/wrangler.json'); console.log((c.d1_databases&&c.d1_databases[0]&&c.d1_databases[0].database_id)||'');")
247
+ if [[ -n "$D1_ID_FROM_CONFIG" ]]; then
248
+ LOCAL_DB_PATH=".wrangler/state/v3/d1/miniflare-D1DatabaseObject/${D1_ID_FROM_CONFIG}.sqlite"
249
+ DRIZZLE_DB_URL="../${LOCAL_DB_PATH}"
250
+
251
+ if [[ -f config/drizzle.config.ts ]]; then
252
+ DRIZZLE_DB_URL="$DRIZZLE_DB_URL" node - <<'NODE'
253
+ const fs = require('fs');
254
+ const path = 'config/drizzle.config.ts';
255
+ const nextUrl = process.env.DRIZZLE_DB_URL;
256
+ let content = fs.readFileSync(path, 'utf8');
257
+ const pattern = /url:\s*['"][^'"]*\.sqlite['"]/;
258
+ if (pattern.test(content)) {
259
+ content = content.replace(pattern, `url: '${nextUrl}'`);
260
+ } else {
261
+ // Fallback: append url if not found
262
+ content = content.replace('dbCredentials: {', `dbCredentials: {\n url: '${nextUrl}',`);
263
+ }
264
+ fs.writeFileSync(path, content);
265
+ NODE
266
+ log_ok "Updated config/drizzle.config.ts with local DB path."
267
+ fi
268
+ else
269
+ log_warn "D1 id not found in wrangler.json. Skipping drizzle config update."
270
+ fi
271
+
272
+ # Push schema (no migrations) and seed
273
+ log_info "Pushing schema to local D1..."
274
+ pnpm db:push >/dev/null 2>&1 || log_warn "Schema push skipped (check drizzle config and local D1)."
275
+
276
+ log_info "Generating seed data..."
277
+ pnpm db:seed >/dev/null
278
+
279
+ log_info "Seeding local D1..."
280
+ $WRANGLER d1 execute "$DB_NAME" --local --file=seed.sql >/dev/null 2>&1 || log_warn "Seed failed (check local D1)."
281
+
282
+ log_ok "Local D1 ready with seed data."
283
+ log_info "Seed data is defined in src/server/db/seed.ts (customize as needed)."
284
+
285
+ if [[ "$SKIP_DEV" -eq 0 ]]; then
286
+ log_info "Starting dev server..."
287
+ pnpm dev
288
+ else
289
+ log_ok "Setup complete. Run 'pnpm dev' to start the server."
290
+ fi