create-bcp-app 0.1.25 → 0.1.27

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/README.md CHANGED
@@ -8,19 +8,72 @@ cd my-app
8
8
  npm run dev
9
9
  ```
10
10
 
11
+ ## Project-local BCP CLI
12
+
13
+ `create-bcp-app` installs BCP Framework as a **project-local dependency**. It does not install the framework CLI globally.
14
+
15
+ Generated npm scripts can use `bcp` directly because npm automatically prepends the project's `node_modules/.bin` directory to `PATH` while scripts are running:
16
+
17
+ ```json
18
+ {
19
+ "scripts": {
20
+ "dev": "bcp dev",
21
+ "build": "bcp build",
22
+ "start": "bcp start",
23
+ "routes": "bcp routes",
24
+ "update": "bcp update"
25
+ }
26
+ }
27
+ ```
28
+
29
+ For direct PowerShell usage, invoke the local CLI through `npm exec`:
30
+
31
+ ```powershell
32
+ npm exec -- bcp-framework --version
33
+ npm exec -- bcp-framework doctor
34
+ npm exec -- bcp-framework inspect
35
+ npm exec -- bcp-framework routes
36
+ npm exec -- bcp-framework dev
37
+ npm exec -- bcp-framework build
38
+ ```
39
+
40
+ Typing `bcp-framework` directly in a normal PowerShell session may return `CommandNotFoundException` because PowerShell does not automatically add `node_modules/.bin` to its normal command search path.
41
+
42
+ The Windows command shim can also be executed explicitly:
43
+
44
+ ```powershell
45
+ .\node_modules\.bin\bcp-framework.cmd --version
46
+ ```
47
+
48
+ BCP Framework publishes the `bcp-framework` alias because Microsoft SQL Server can install another Windows executable named `bcp.exe`.
49
+
50
+ Recommended convention:
51
+
52
+ ```text
53
+ Inside npm scripts -> bcp ...
54
+ Direct PowerShell usage -> npm exec -- bcp-framework ...
55
+ ```
56
+
57
+ Using the project-local CLI also keeps the CLI version aligned with the exact BCP Framework dependency installed by the project.
58
+
11
59
  When running interactively, the generator asks:
12
60
 
13
61
  ```text
14
- Use Tailwind CSS? (Y/n)
62
+ Use Tailwind CSS?
15
63
  Select a database:
16
- 1) None
17
- 2) MySQL
18
- 3) PostgreSQL
19
- 4) SQLite
20
- 5) MongoDB
64
+ None
65
+ MySQL
66
+ PostgreSQL
67
+ SQLite
68
+ MongoDB
21
69
  Select authentication:
22
- 1) None
23
- 2) JWT Cookie
70
+ None
71
+ JWT Cookie
72
+ Select storage provider:
73
+ None
74
+ Local Server
75
+ Amazon S3
76
+ Cloudflare R2
24
77
  ```
25
78
 
26
79
  ## Generated application defaults
@@ -53,7 +106,7 @@ After a newer stable BCP release is published, update the project with:
53
106
  npm run update
54
107
  ```
55
108
 
56
- This resolves the npm `latest` release through BCP's updater, updates the exact framework dependency and refreshes the active package-manager lockfile. Use `bcp update --check` to inspect an available update without changing files.
109
+ This resolves the npm `latest` release through BCP's updater, updates the exact framework dependency and refreshes the active package-manager lockfile. Use `bcp update --check` inside an npm script or `npm exec -- bcp-framework update --check` from PowerShell to inspect an available update without changing files.
57
110
 
58
111
  ## Generated optional setup
59
112
 
@@ -96,18 +149,7 @@ import "bcp/server-only";
96
149
 
97
150
  This prevents database code from being used in a page/client module graph. Query the database from an API route or a route-level server data loader and consume only serializable results from hydrated pages.
98
151
 
99
- For MySQL, the generated helper exports a reusable `db` pool and caches that pool on `globalThis` during development so hot reloads do not create a new connection pool every time:
100
-
101
- ```ts
102
- import {
103
- db,
104
- } from "@/lib/database";
105
-
106
- const [rows] =
107
- await db.query(
108
- "SELECT 1"
109
- );
110
- ```
152
+ For MySQL, the generated helper exports the BCP database primitives through `bcp/database`.
111
153
 
112
154
  The generated MySQL `.env.example` uses separate connection settings:
113
155
 
@@ -119,7 +161,77 @@ DB_PASSWORD=
119
161
  DB_NAME=bcp_app
120
162
  ```
121
163
 
122
- The generator intentionally installs database drivers directly and does not force an ORM.
164
+ The generator intentionally does not force an ORM.
165
+
166
+ ### Storage provider
167
+
168
+ Selecting a storage provider creates:
169
+
170
+ ```text
171
+ lib/storage.ts
172
+ ```
173
+
174
+ and adds the provider-specific settings to `.env.example`.
175
+
176
+ #### Local Server
177
+
178
+ ```bash
179
+ npx create-bcp-app my-app --storage local
180
+ ```
181
+
182
+ Generated environment setting:
183
+
184
+ ```dotenv
185
+ STORAGE_LOCAL_DIRECTORY=./storage
186
+ ```
187
+
188
+ The generated helper uses `createLocalStorage()` and adds `storage/` to `.gitignore` so uploaded files are not accidentally committed.
189
+
190
+ #### Amazon S3
191
+
192
+ ```bash
193
+ npx create-bcp-app my-app --storage amazon-s3
194
+ ```
195
+
196
+ Generated environment settings:
197
+
198
+ ```dotenv
199
+ AWS_S3_BUCKET=
200
+ AWS_REGION=ap-southeast-1
201
+ AWS_ACCESS_KEY_ID=
202
+ AWS_SECRET_ACCESS_KEY=
203
+ AWS_SESSION_TOKEN=
204
+ AWS_S3_PREFIX=
205
+ ```
206
+
207
+ The generated helper uses `createS3Storage()`. Explicit credentials can be left unset when the deployment uses the AWS SDK server-side credential provider chain, such as an IAM role.
208
+
209
+ #### Cloudflare R2
210
+
211
+ ```bash
212
+ npx create-bcp-app my-app --storage cloudflare-r2
213
+ ```
214
+
215
+ Generated environment settings:
216
+
217
+ ```dotenv
218
+ R2_ACCOUNT_ID=
219
+ R2_BUCKET=
220
+ R2_ACCESS_KEY_ID=
221
+ R2_SECRET_ACCESS_KEY=
222
+ R2_PREFIX=
223
+ ```
224
+
225
+ The generated helper uses the S3-compatible BCP adapter with:
226
+
227
+ ```text
228
+ region: auto
229
+ endpoint: https://<account-id>.r2.cloudflarestorage.com
230
+ ```
231
+
232
+ Storage credentials are server-only. Do not expose them through `BCP_PUBLIC_*` variables.
233
+
234
+ The selected preset only scaffolds the initial provider configuration. Application code can later switch to another BCP storage adapter without changing the generic storage APIs used by routes and services.
123
235
 
124
236
  ### JWT Cookie authentication
125
237
 
@@ -169,7 +281,8 @@ POST /api/auth/logout
169
281
  --no-tailwind Disable Tailwind CSS without prompting
170
282
  --database <database> none | mysql | postgresql | sqlite | mongodb
171
283
  --auth <preset> none | jwt-cookie
172
- -y, --yes Accept defaults (Tailwind enabled, no database, no auth)
284
+ --storage <provider> none | local | amazon-s3 | cloudflare-r2
285
+ -y, --yes Accept defaults (Tailwind enabled, no database, no auth, no storage)
173
286
  --bcp <specifier> Override dependencies.bcp
174
287
  -h, --help Show help
175
288
  ```
@@ -177,14 +290,15 @@ POST /api/auth/logout
177
290
  Examples:
178
291
 
179
292
  ```bash
180
- npx create-bcp-app my-app --tailwind --database mysql --auth jwt-cookie
293
+ npx create-bcp-app my-app --tailwind --database mysql --auth jwt-cookie --storage local
294
+ npx create-bcp-app my-app --storage amazon-s3
295
+ npx create-bcp-app my-app --storage cloudflare-r2
181
296
  npx create-bcp-app my-app --no-tailwind --database mongodb
182
- npx create-bcp-app my-app --auth jwt-cookie
183
297
  npx create-bcp-app my-app --yes
184
298
  ```
185
299
 
186
300
  The `--bcp` option is primarily useful for prerelease and local package testing, for example:
187
301
 
188
302
  ```bash
189
- npx create-bcp-app my-app --bcp file:../bcp-0.1.9.tgz
303
+ npx create-bcp-app my-app --bcp file:../bcp-0.1.27.tgz
190
304
  ```
@@ -18,6 +18,11 @@ import {
18
18
  normalizeAuth,
19
19
  normalizeDatabase,
20
20
  } from "../src/project-options.mjs";
21
+ import {
22
+ formatStorageProvider,
23
+ normalizeStorageProvider,
24
+ STORAGE_CHOICES,
25
+ } from "../src/storage-options.mjs";
21
26
 
22
27
  const ANSI = {
23
28
  reset: "\u001B[0m",
@@ -71,6 +76,7 @@ async function main() {
71
76
  tailwind: resolved.tailwind,
72
77
  database: resolved.database,
73
78
  auth: resolved.auth,
79
+ storage: resolved.storage,
74
80
  packageManager: "npm",
75
81
  });
76
82
 
@@ -87,6 +93,7 @@ function parseArguments(args) {
87
93
  let tailwind = undefined;
88
94
  let database = undefined;
89
95
  let auth = undefined;
96
+ let storage = undefined;
90
97
  let acceptDefaults = false;
91
98
 
92
99
  for (
@@ -147,6 +154,20 @@ function parseArguments(args) {
147
154
  continue;
148
155
  }
149
156
 
157
+ if (value === "--storage") {
158
+ const nextValue = args[index + 1];
159
+
160
+ if (!nextValue) {
161
+ throw new Error(
162
+ "--storage requires a value."
163
+ );
164
+ }
165
+
166
+ storage = normalizeStorageProvider(nextValue);
167
+ index++;
168
+ continue;
169
+ }
170
+
150
171
  if (value === "--bcp") {
151
172
  const nextValue = args[index + 1];
152
173
 
@@ -183,6 +204,7 @@ function parseArguments(args) {
183
204
  tailwind,
184
205
  database,
185
206
  auth,
207
+ storage,
186
208
  acceptDefaults,
187
209
  };
188
210
  }
@@ -191,11 +213,13 @@ async function resolveOptions({
191
213
  tailwind: selectedTailwind,
192
214
  database: selectedDatabase,
193
215
  auth: selectedAuth,
216
+ storage: selectedStorage,
194
217
  acceptDefaults,
195
218
  }) {
196
219
  let tailwind = selectedTailwind;
197
220
  let database = selectedDatabase;
198
221
  let auth = selectedAuth;
222
+ let storage = selectedStorage;
199
223
 
200
224
  if (acceptDefaults) {
201
225
  return {
@@ -206,6 +230,9 @@ async function resolveOptions({
206
230
  auth: normalizeAuth(
207
231
  auth ?? "none"
208
232
  ),
233
+ storage: normalizeStorageProvider(
234
+ storage ?? "none"
235
+ ),
209
236
  };
210
237
  }
211
238
 
@@ -223,6 +250,9 @@ async function resolveOptions({
223
250
  auth: normalizeAuth(
224
251
  auth ?? "none"
225
252
  ),
253
+ storage: normalizeStorageProvider(
254
+ storage ?? "none"
255
+ ),
226
256
  };
227
257
  }
228
258
 
@@ -242,10 +272,15 @@ async function resolveOptions({
242
272
  auth = await askAuth();
243
273
  }
244
274
 
275
+ if (storage === undefined) {
276
+ storage = await askStorage();
277
+ }
278
+
245
279
  return {
246
280
  tailwind: Boolean(tailwind),
247
281
  database: normalizeDatabase(database),
248
282
  auth: normalizeAuth(auth),
283
+ storage: normalizeStorageProvider(storage),
249
284
  };
250
285
  }
251
286
 
@@ -302,6 +337,21 @@ function askAuth() {
302
337
  );
303
338
  }
304
339
 
340
+ function askStorage() {
341
+ return askTerminalSelect(
342
+ "Select storage provider:",
343
+ STORAGE_CHOICES.map(
344
+ (value) => ({
345
+ value,
346
+ label:
347
+ formatStorageProvider(
348
+ value
349
+ ),
350
+ })
351
+ )
352
+ );
353
+ }
354
+
305
355
  function askTerminalSelect(
306
356
  label,
307
357
  choices
@@ -486,6 +536,9 @@ function printSuccess(
486
536
  console.log(
487
537
  `Authentication: ${formatAuthName(result.auth)}`
488
538
  );
539
+ console.log(
540
+ `Storage: ${formatStorageProvider(result.storage)}`
541
+ );
489
542
  console.log("");
490
543
  console.log("Next steps:");
491
544
  console.log(
@@ -496,6 +549,10 @@ function printSuccess(
496
549
  console.log(" npm install");
497
550
  }
498
551
 
552
+ if (result.storage !== "none") {
553
+ console.log(" Copy .env.example to .env and configure storage credentials");
554
+ }
555
+
499
556
  console.log(" npm run dev");
500
557
  console.log("");
501
558
  }
@@ -533,6 +590,7 @@ Interactive setup:
533
590
  - Choose Tailwind CSS: Yes or No
534
591
  - Choose a database: None, MySQL, PostgreSQL, SQLite or MongoDB
535
592
  - Choose authentication: None or JWT Cookie
593
+ - Choose storage: None, Local Server, Amazon S3 or Cloudflare R2
536
594
 
537
595
  Options:
538
596
  --no-install Create files without running npm install
@@ -540,14 +598,16 @@ Options:
540
598
  --no-tailwind Disable Tailwind CSS without prompting
541
599
  --database <database> none | mysql | postgresql | sqlite | mongodb
542
600
  --auth <preset> none | jwt-cookie
543
- -y, --yes Accept defaults (Tailwind enabled, no database, no auth)
601
+ --storage <provider> none | local | amazon-s3 | cloudflare-r2
602
+ -y, --yes Accept defaults (Tailwind enabled, no database, no auth, no storage)
544
603
  --bcp <specifier> Override the package specifier stored under dependencies.bcp
545
604
  -h, --help Show this help message
546
605
 
547
606
  Examples:
548
607
  npx create-bcp-app my-app
549
- npx create-bcp-app my-app --tailwind --database mysql --auth jwt-cookie
550
- npx create-bcp-app my-app --no-tailwind --database mongodb
608
+ npx create-bcp-app my-app --tailwind --database mysql --auth jwt-cookie --storage local
609
+ npx create-bcp-app my-app --storage amazon-s3
610
+ npx create-bcp-app my-app --storage cloudflare-r2
551
611
  npx create-bcp-app my-app --yes
552
612
  npx create-bcp-app my-app --no-install
553
613
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-bcp-app",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Create a new BCP Framework application.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/index.d.mts CHANGED
@@ -9,6 +9,12 @@ export type BcpAuthPreset =
9
9
  | "none"
10
10
  | "jwt-cookie";
11
11
 
12
+ export type BcpStorageProvider =
13
+ | "none"
14
+ | "local"
15
+ | "amazon-s3"
16
+ | "cloudflare-r2";
17
+
12
18
  export interface CreateBcpAppOptions {
13
19
  projectDirectory: string;
14
20
  install?: boolean;
@@ -16,6 +22,7 @@ export interface CreateBcpAppOptions {
16
22
  tailwind?: boolean;
17
23
  database?: BcpDatabase;
18
24
  auth?: BcpAuthPreset;
25
+ storage?: BcpStorageProvider;
19
26
  packageManager?: "npm";
20
27
  }
21
28
 
@@ -26,6 +33,7 @@ export interface CreateBcpAppResult {
26
33
  tailwind: boolean;
27
34
  database: BcpDatabase;
28
35
  auth: BcpAuthPreset;
36
+ storage: BcpStorageProvider;
29
37
  }
30
38
 
31
39
  export interface ResolveNpmInvocationOptions {
package/src/index.mjs CHANGED
@@ -12,6 +12,10 @@ import {
12
12
  normalizeAuth,
13
13
  normalizeDatabase,
14
14
  } from "./project-options.mjs";
15
+ import {
16
+ applyStorageOption,
17
+ normalizeStorageProvider,
18
+ } from "./storage-options.mjs";
15
19
 
16
20
  const packageRoot =
17
21
  path.resolve(
@@ -47,6 +51,10 @@ export async function createBcpApp(
47
51
  normalizeAuth(
48
52
  options.auth
49
53
  );
54
+ const storage =
55
+ normalizeStorageProvider(
56
+ options.storage
57
+ );
50
58
 
51
59
  ensureTargetDirectory(
52
60
  targetDirectory
@@ -128,6 +136,11 @@ export async function createBcpApp(
128
136
  database,
129
137
  auth,
130
138
  });
139
+ const selectedStorage =
140
+ applyStorageOption({
141
+ targetDirectory,
142
+ storage,
143
+ });
131
144
 
132
145
  configureFrameworkDatabaseEntry(
133
146
  targetDirectory,
@@ -171,6 +184,8 @@ export async function createBcpApp(
171
184
  selectedOptions.database,
172
185
  auth:
173
186
  selectedOptions.auth,
187
+ storage:
188
+ selectedStorage,
174
189
  };
175
190
  }
176
191
 
@@ -0,0 +1,341 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ export const STORAGE_CHOICES = [
5
+ "none",
6
+ "local",
7
+ "amazon-s3",
8
+ "cloudflare-r2",
9
+ ];
10
+
11
+ const STORAGE_PRESETS = {
12
+ local: {
13
+ env: [
14
+ "STORAGE_LOCAL_DIRECTORY=./storage",
15
+ ],
16
+ gitIgnore: [
17
+ "storage/",
18
+ ],
19
+ source: `import "bcp/server-only";
20
+
21
+ import {
22
+ createLocalStorage,
23
+ } from "bcp/server";
24
+
25
+ export const storage =
26
+ createLocalStorage({
27
+ directory:
28
+ process.env.STORAGE_LOCAL_DIRECTORY ??
29
+ "./storage",
30
+ });
31
+ `,
32
+ },
33
+ "amazon-s3": {
34
+ env: [
35
+ "AWS_S3_BUCKET=",
36
+ "AWS_REGION=ap-southeast-1",
37
+ "AWS_ACCESS_KEY_ID=",
38
+ "AWS_SECRET_ACCESS_KEY=",
39
+ "AWS_SESSION_TOKEN=",
40
+ "AWS_S3_PREFIX=",
41
+ ],
42
+ gitIgnore: [],
43
+ source: `import "bcp/server-only";
44
+
45
+ import {
46
+ createS3Storage,
47
+ } from "bcp/server";
48
+
49
+ const bucket =
50
+ requireEnvironmentVariable(
51
+ "AWS_S3_BUCKET"
52
+ );
53
+
54
+ export const storage =
55
+ createS3Storage({
56
+ bucket,
57
+ region:
58
+ process.env.AWS_REGION ??
59
+ "ap-southeast-1",
60
+ prefix:
61
+ optionalEnvironmentVariable(
62
+ "AWS_S3_PREFIX"
63
+ ),
64
+ accessKeyId:
65
+ optionalEnvironmentVariable(
66
+ "AWS_ACCESS_KEY_ID"
67
+ ),
68
+ secretAccessKey:
69
+ optionalEnvironmentVariable(
70
+ "AWS_SECRET_ACCESS_KEY"
71
+ ),
72
+ sessionToken:
73
+ optionalEnvironmentVariable(
74
+ "AWS_SESSION_TOKEN"
75
+ ),
76
+ });
77
+
78
+ function requireEnvironmentVariable(
79
+ name: string
80
+ ): string {
81
+ const value =
82
+ process.env[name]
83
+ ?.trim();
84
+
85
+ if (!value) {
86
+ throw new Error(
87
+ \`BCP storage: \${name} is required.\`
88
+ );
89
+ }
90
+
91
+ return value;
92
+ }
93
+
94
+ function optionalEnvironmentVariable(
95
+ name: string
96
+ ): string | undefined {
97
+ const value =
98
+ process.env[name]
99
+ ?.trim();
100
+
101
+ return value || undefined;
102
+ }
103
+ `,
104
+ },
105
+ "cloudflare-r2": {
106
+ env: [
107
+ "R2_ACCOUNT_ID=",
108
+ "R2_BUCKET=",
109
+ "R2_ACCESS_KEY_ID=",
110
+ "R2_SECRET_ACCESS_KEY=",
111
+ "R2_PREFIX=",
112
+ ],
113
+ gitIgnore: [],
114
+ source: `import "bcp/server-only";
115
+
116
+ import {
117
+ createS3Storage,
118
+ } from "bcp/server";
119
+
120
+ const accountId =
121
+ requireEnvironmentVariable(
122
+ "R2_ACCOUNT_ID"
123
+ );
124
+ const bucket =
125
+ requireEnvironmentVariable(
126
+ "R2_BUCKET"
127
+ );
128
+
129
+ export const storage =
130
+ createS3Storage({
131
+ bucket,
132
+ region:
133
+ "auto",
134
+ endpoint:
135
+ \`https://\${accountId}.r2.cloudflarestorage.com\`,
136
+ prefix:
137
+ optionalEnvironmentVariable(
138
+ "R2_PREFIX"
139
+ ),
140
+ accessKeyId:
141
+ requireEnvironmentVariable(
142
+ "R2_ACCESS_KEY_ID"
143
+ ),
144
+ secretAccessKey:
145
+ requireEnvironmentVariable(
146
+ "R2_SECRET_ACCESS_KEY"
147
+ ),
148
+ });
149
+
150
+ function requireEnvironmentVariable(
151
+ name: string
152
+ ): string {
153
+ const value =
154
+ process.env[name]
155
+ ?.trim();
156
+
157
+ if (!value) {
158
+ throw new Error(
159
+ \`BCP storage: \${name} is required.\`
160
+ );
161
+ }
162
+
163
+ return value;
164
+ }
165
+
166
+ function optionalEnvironmentVariable(
167
+ name: string
168
+ ): string | undefined {
169
+ const value =
170
+ process.env[name]
171
+ ?.trim();
172
+
173
+ return value || undefined;
174
+ }
175
+ `,
176
+ },
177
+ };
178
+
179
+ export function normalizeStorageProvider(
180
+ value
181
+ ) {
182
+ const normalized =
183
+ String(
184
+ value ?? "none"
185
+ )
186
+ .trim()
187
+ .toLowerCase();
188
+
189
+ if (
190
+ !STORAGE_CHOICES.includes(
191
+ normalized
192
+ )
193
+ ) {
194
+ throw new Error(
195
+ `Unsupported storage provider: ${normalized}. Choose one of: ${STORAGE_CHOICES.join(", ")}.`
196
+ );
197
+ }
198
+
199
+ return normalized;
200
+ }
201
+
202
+ export function applyStorageOption({
203
+ targetDirectory,
204
+ storage = "none",
205
+ }) {
206
+ const selectedStorage =
207
+ normalizeStorageProvider(
208
+ storage
209
+ );
210
+
211
+ if (
212
+ selectedStorage === "none"
213
+ ) {
214
+ return selectedStorage;
215
+ }
216
+
217
+ const preset =
218
+ STORAGE_PRESETS[
219
+ selectedStorage
220
+ ];
221
+
222
+ writeFile(
223
+ targetDirectory,
224
+ "lib/storage.ts",
225
+ preset.source
226
+ );
227
+ appendEnvExample(
228
+ targetDirectory,
229
+ preset.env,
230
+ `Storage (${formatStorageProvider(selectedStorage)})`
231
+ );
232
+
233
+ for (const entry of preset.gitIgnore) {
234
+ appendGitIgnore(
235
+ targetDirectory,
236
+ entry
237
+ );
238
+ }
239
+
240
+ return selectedStorage;
241
+ }
242
+
243
+ export function formatStorageProvider(
244
+ value
245
+ ) {
246
+ const names = {
247
+ none: "None",
248
+ local: "Local Server",
249
+ "amazon-s3": "Amazon S3",
250
+ "cloudflare-r2": "Cloudflare R2",
251
+ };
252
+
253
+ return names[value] ?? value;
254
+ }
255
+
256
+ function appendEnvExample(
257
+ targetDirectory,
258
+ lines,
259
+ section
260
+ ) {
261
+ const filePath =
262
+ path.join(
263
+ targetDirectory,
264
+ ".env.example"
265
+ );
266
+ const current =
267
+ fs.existsSync(
268
+ filePath
269
+ )
270
+ ? fs.readFileSync(
271
+ filePath,
272
+ "utf8"
273
+ ).trimEnd()
274
+ : "";
275
+
276
+ fs.writeFileSync(
277
+ filePath,
278
+ `${current}${current ? "\n\n" : ""}# ${section}\n${lines.join("\n")}\n`,
279
+ "utf8"
280
+ );
281
+ }
282
+
283
+ function appendGitIgnore(
284
+ targetDirectory,
285
+ entry
286
+ ) {
287
+ const filePath =
288
+ path.join(
289
+ targetDirectory,
290
+ ".gitignore"
291
+ );
292
+ const current =
293
+ fs.existsSync(
294
+ filePath
295
+ )
296
+ ? fs.readFileSync(
297
+ filePath,
298
+ "utf8"
299
+ ).trimEnd()
300
+ : "";
301
+ const lines =
302
+ new Set(
303
+ current
304
+ .split(/\r?\n/)
305
+ .filter(Boolean)
306
+ );
307
+
308
+ lines.add(entry);
309
+
310
+ fs.writeFileSync(
311
+ filePath,
312
+ `${Array.from(lines).join("\n")}\n`,
313
+ "utf8"
314
+ );
315
+ }
316
+
317
+ function writeFile(
318
+ targetDirectory,
319
+ relativePath,
320
+ content
321
+ ) {
322
+ const filePath =
323
+ path.join(
324
+ targetDirectory,
325
+ relativePath
326
+ );
327
+
328
+ fs.mkdirSync(
329
+ path.dirname(
330
+ filePath
331
+ ),
332
+ {
333
+ recursive: true,
334
+ }
335
+ );
336
+ fs.writeFileSync(
337
+ filePath,
338
+ content,
339
+ "utf8"
340
+ );
341
+ }
@@ -10,7 +10,9 @@ npm run dev
10
10
 
11
11
  Open `http://localhost:3000`.
12
12
 
13
- ## Commands
13
+ ## Project commands
14
+
15
+ Generated projects use the project-local BCP Framework CLI through npm scripts:
14
16
 
15
17
  ```bash
16
18
  npm run dev
@@ -18,6 +20,65 @@ npm run routes
18
20
  npm run typecheck
19
21
  npm run build
20
22
  npm start
23
+ npm run update
24
+ ```
25
+
26
+ The generated scripts call commands such as `bcp dev`, `bcp build` and `bcp start`. This works because npm automatically adds the project's `node_modules/.bin` directory to `PATH` while an npm script is running.
27
+
28
+ ## Direct CLI usage
29
+
30
+ BCP Framework is installed as a **project-local dependency**. It is not installed globally by `create-bcp-app`.
31
+
32
+ Because of that, typing this directly in a normal PowerShell session may not work:
33
+
34
+ ```powershell
35
+ bcp-framework doctor
36
+ ```
37
+
38
+ PowerShell does not automatically add `node_modules/.bin` to its normal command search path.
39
+
40
+ Use `npm exec` when you want to invoke the project-local CLI directly:
41
+
42
+ ```powershell
43
+ npm exec -- bcp-framework --version
44
+ npm exec -- bcp-framework doctor
45
+ npm exec -- bcp-framework inspect
46
+ npm exec -- bcp-framework routes
47
+ npm exec -- bcp-framework dev
48
+ npm exec -- bcp-framework build
49
+ ```
50
+
51
+ You can also execute the Windows command shim explicitly:
52
+
53
+ ```powershell
54
+ .\node_modules\.bin\bcp-framework.cmd --version
55
+ ```
56
+
57
+ Using the project-local CLI is recommended because it guarantees that the CLI version matches the BCP Framework version installed by this application.
58
+
59
+ ### Why `bcp-framework` instead of `bcp` in PowerShell?
60
+
61
+ Microsoft SQL Server can install another Windows executable named `bcp.exe`. To avoid that command-name collision, BCP Framework publishes the additional `bcp-framework` alias.
62
+
63
+ Use this convention:
64
+
65
+ ```text
66
+ Inside npm scripts -> bcp ...
67
+ Direct PowerShell usage -> npm exec -- bcp-framework ...
68
+ ```
69
+
70
+ ## Production
71
+
72
+ Create the standalone production build:
73
+
74
+ ```bash
75
+ npm run build
76
+ ```
77
+
78
+ Start it with:
79
+
80
+ ```bash
81
+ npm start
21
82
  ```
22
83
 
23
84
  The production build is written to `.bcp-framework/build` and runs as a standalone Node.js server.