@jlmx/starter 1.1.3 → 1.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.
Files changed (3) hide show
  1. package/README.md +38 -10
  2. package/dist/index.js +162 -85
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -4,16 +4,44 @@ CLI to scaffold new projects with my preferred stack, optimized for Cloudflare d
4
4
 
5
5
  ## Usage
6
6
 
7
+ Interactive mode — prompts for all options:
8
+
9
+ ```bash
10
+ bunx @jlmx/starter
11
+ ```
12
+
13
+ Pass a project name to skip the name prompt:
14
+
7
15
  ```bash
8
16
  bunx @jlmx/starter my-app
9
17
  ```
10
18
 
11
- Or run interactively:
19
+ One-liner with flags — fully non-interactive:
12
20
 
13
21
  ```bash
14
- bunx @jlmx/starter
22
+ bunx @jlmx/starter my-app --template nextjs --features tailwind,fonts,cloudflare --bindings d1,r2 --ai claude
23
+ ```
24
+
25
+ Accept all defaults with no prompts:
26
+
27
+ ```bash
28
+ bunx @jlmx/starter my-app --yes
15
29
  ```
16
30
 
31
+ ## Options
32
+
33
+ | Flag | Values | Description |
34
+ |------|--------|-------------|
35
+ | `--template` | `tanstack-start`, `nextjs` | Framework template |
36
+ | `--features` | `tailwind,fonts,auth,cloudflare` | Comma-separated feature list |
37
+ | `--bindings` | `d1,r2,kv,ai,queues` | Cloudflare bindings (requires `cloudflare` feature) |
38
+ | `--ai` | `none`, `agents`, `claude`, `both` | AI coding assistant instructions |
39
+ | `-y, --yes` | — | Accept all defaults, skip all prompts |
40
+ | `-h, --help` | — | Show help |
41
+ | `-v, --version` | — | Show version |
42
+
43
+ Defaults (used with `--yes`): `tanstack-start`, features `tailwind,fonts,cloudflare`, no bindings, no AI instructions.
44
+
17
45
  ## Templates
18
46
 
19
47
  - **TanStack Start** - Full-stack React with TanStack Router
@@ -22,28 +50,28 @@ bunx @jlmx/starter
22
50
  ## Features
23
51
 
24
52
  All projects include:
25
- - **Tailwind CSS** - Custom design system with `tailwindcss-animate`
26
- - **Custom Fonts** - Inter + JetBrains Mono via Fontsource
27
53
  - **Biome** - Fast linting and formatting (replaces ESLint + Prettier)
28
54
  - **Logger** - Structured logging utility
29
55
  - **Result Type** - Explicit error handling (neverthrow pattern)
30
56
 
31
57
  Optional:
58
+ - **Tailwind CSS** - Custom design system with `tailwindcss-animate`
59
+ - **Custom Fonts** - Inter + JetBrains Mono via Fontsource
32
60
  - **Authentication** - Better Auth + Drizzle ORM
33
61
  - **Cloudflare Workers** - Edge deployment with D1, R2, KV, AI, Queues
34
62
  - **AI Instructions** - AGENTS.md and/or CLAUDE.md for coding assistants
35
63
 
36
64
  ## Cloudflare Bindings
37
65
 
38
- When Cloudflare is selected, you can enable:
66
+ When `cloudflare` is selected, you can enable:
39
67
 
40
68
  | Binding | Description |
41
69
  |---------|-------------|
42
- | D1 | SQLite database at the edge |
43
- | R2 | S3-compatible object storage |
44
- | KV | Key-value storage |
45
- | AI | Workers AI models |
46
- | Queues | Message queues |
70
+ | `d1` | SQLite database at the edge |
71
+ | `r2` | S3-compatible object storage |
72
+ | `kv` | Key-value storage |
73
+ | `ai` | Workers AI models |
74
+ | `queues` | Message queues |
47
75
 
48
76
  ## Development
49
77
 
package/dist/index.js CHANGED
@@ -181,12 +181,18 @@ Usage:
181
181
  bunx @jlmx/starter [project-name] [options]
182
182
 
183
183
  Options:
184
- -h, --help Show this help message
185
- -v, --version Show version number
184
+ -h, --help Show this help message
185
+ -v, --version Show version number
186
+ -y, --yes Accept defaults (no prompts)
187
+ --template <name> tanstack-start | nextjs
188
+ --features <list> Comma-separated: tailwind,fonts,auth,cloudflare
189
+ --bindings <list> Comma-separated: d1,r2,kv,ai,queues
190
+ --ai <value> none | agents | claude | both
186
191
 
187
192
  Examples:
188
193
  bunx @jlmx/starter my-app
189
- bunx @jlmx/starter
194
+ bunx @jlmx/starter my-app --template nextjs --features tailwind,fonts,cloudflare --bindings d1,r2 --ai claude
195
+ bunx @jlmx/starter my-app --yes
190
196
 
191
197
  Templates:
192
198
  tanstack-start Full-stack React with TanStack Router
@@ -215,10 +221,19 @@ async function main() {
215
221
  console.info(VERSION);
216
222
  process.exit(0);
217
223
  }
224
+ const useDefaults = args.includes("-y") || args.includes("--yes");
225
+ const DEFAULT_TEMPLATE = "tanstack-start";
226
+ const DEFAULT_FEATURES = ["tailwind", "fonts", "cloudflare"];
227
+ const DEFAULT_BINDINGS = [];
228
+ const DEFAULT_AI = "none";
218
229
  console.clear();
219
230
  p.intro("@jlmx/starter");
220
231
  let projectName = args.find((arg) => !arg.startsWith("-"));
221
232
  if (!projectName) {
233
+ if (useDefaults) {
234
+ p.log.error("--yes requires a project name as the first argument");
235
+ process.exit(1);
236
+ }
222
237
  const nameResult = await p.text({
223
238
  message: "Project name:",
224
239
  placeholder: "my-app",
@@ -235,40 +250,69 @@ async function main() {
235
250
  }
236
251
  projectName = nameResult;
237
252
  }
238
- const template = await p.select({
239
- message: "Select a template:",
240
- options: [
241
- {
242
- value: "tanstack-start",
243
- label: "TanStack Start",
244
- hint: "Full-stack React with TanStack Router"
245
- },
246
- {
247
- value: "nextjs",
248
- label: "Next.js",
249
- hint: "Full-stack React with App Router"
250
- }
251
- ]
252
- });
253
- if (p.isCancel(template)) {
254
- p.cancel("Cancelled");
255
- process.exit(0);
253
+ const templateFlag = getFlagValue(args, "--template");
254
+ const validTemplates = ["tanstack-start", "nextjs"];
255
+ let selectedTemplate;
256
+ if (templateFlag) {
257
+ if (!validTemplates.includes(templateFlag)) {
258
+ p.log.error(`Invalid template "${templateFlag}". Choose: ${validTemplates.join(", ")}`);
259
+ process.exit(1);
260
+ }
261
+ selectedTemplate = templateFlag;
262
+ } else if (useDefaults) {
263
+ selectedTemplate = DEFAULT_TEMPLATE;
264
+ } else {
265
+ const template = await p.select({
266
+ message: "Select a template:",
267
+ options: [
268
+ {
269
+ value: "tanstack-start",
270
+ label: "TanStack Start",
271
+ hint: "Full-stack React with TanStack Router"
272
+ },
273
+ {
274
+ value: "nextjs",
275
+ label: "Next.js",
276
+ hint: "Full-stack React with App Router"
277
+ }
278
+ ]
279
+ });
280
+ if (p.isCancel(template)) {
281
+ p.cancel("Cancelled");
282
+ process.exit(0);
283
+ }
284
+ selectedTemplate = template;
256
285
  }
257
- const features = await p.multiselect({
258
- message: "Select features:",
259
- options: [
260
- { value: "tailwind", label: "Tailwind CSS", hint: "design system + tailwindcss-animate" },
261
- { value: "fonts", label: "Custom Fonts", hint: "Inter + JetBrains Mono" },
262
- { value: "auth", label: "Authentication", hint: "Better Auth + D1" },
263
- { value: "cloudflare", label: "Cloudflare Workers", hint: "deployment config" }
264
- ],
265
- initialValues: ["tailwind", "fonts", "cloudflare"]
266
- });
267
- if (p.isCancel(features)) {
268
- p.cancel("Cancelled");
269
- process.exit(0);
286
+ const featuresFlag = getFlagValue(args, "--features");
287
+ const validFeatures = ["tailwind", "fonts", "auth", "cloudflare"];
288
+ let selectedFeatures;
289
+ if (featuresFlag) {
290
+ const parsed = featuresFlag.split(",").map((f) => f.trim());
291
+ const invalid = parsed.filter((f) => !validFeatures.includes(f));
292
+ if (invalid.length > 0) {
293
+ p.log.error(`Invalid features: ${invalid.join(", ")}. Choose from: ${validFeatures.join(", ")}`);
294
+ process.exit(1);
295
+ }
296
+ selectedFeatures = parsed;
297
+ } else if (useDefaults) {
298
+ selectedFeatures = [...DEFAULT_FEATURES];
299
+ } else {
300
+ const features = await p.multiselect({
301
+ message: "Select features:",
302
+ options: [
303
+ { value: "tailwind", label: "Tailwind CSS", hint: "design system + tailwindcss-animate" },
304
+ { value: "fonts", label: "Custom Fonts", hint: "Inter + JetBrains Mono" },
305
+ { value: "auth", label: "Authentication", hint: "Better Auth + D1" },
306
+ { value: "cloudflare", label: "Cloudflare Workers", hint: "deployment config" }
307
+ ],
308
+ initialValues: ["tailwind", "fonts", "cloudflare"]
309
+ });
310
+ if (p.isCancel(features)) {
311
+ p.cancel("Cancelled");
312
+ process.exit(0);
313
+ }
314
+ selectedFeatures = features;
270
315
  }
271
- const selectedFeatures = features;
272
316
  let cfBindings = [];
273
317
  if (selectedFeatures.includes("auth")) {
274
318
  if (!selectedFeatures.includes("cloudflare")) {
@@ -277,65 +321,91 @@ async function main() {
277
321
  }
278
322
  }
279
323
  if (selectedFeatures.includes("cloudflare")) {
324
+ const bindingsFlag = getFlagValue(args, "--bindings");
325
+ const validBindings = ["d1", "r2", "kv", "ai", "queues"];
280
326
  const needsD1 = selectedFeatures.includes("auth");
281
- const bindings = await p.multiselect({
282
- message: "Cloudflare platform features:",
327
+ if (bindingsFlag !== undefined) {
328
+ const parsed = bindingsFlag === "" ? [] : bindingsFlag.split(",").map((b) => b.trim());
329
+ const invalid = parsed.filter((b) => !validBindings.includes(b));
330
+ if (invalid.length > 0) {
331
+ p.log.error(`Invalid bindings: ${invalid.join(", ")}. Choose from: ${validBindings.join(", ")}`);
332
+ process.exit(1);
333
+ }
334
+ cfBindings = parsed;
335
+ } else if (useDefaults) {
336
+ cfBindings = [...DEFAULT_BINDINGS];
337
+ } else {
338
+ const bindings = await p.multiselect({
339
+ message: "Cloudflare platform features:",
340
+ options: [
341
+ {
342
+ value: "d1",
343
+ label: "D1 Database",
344
+ hint: needsD1 ? "required for auth" : "SQLite at the edge"
345
+ },
346
+ { value: "r2", label: "R2 Storage", hint: "S3-compatible object storage" },
347
+ { value: "kv", label: "KV Store", hint: "key-value storage" },
348
+ { value: "ai", label: "Workers AI", hint: "run AI models" },
349
+ { value: "queues", label: "Queues", hint: "message queues" }
350
+ ],
351
+ initialValues: needsD1 ? ["d1"] : [],
352
+ required: false
353
+ });
354
+ if (p.isCancel(bindings)) {
355
+ p.cancel("Cancelled");
356
+ process.exit(0);
357
+ }
358
+ cfBindings = bindings;
359
+ }
360
+ if (needsD1 && !cfBindings.includes("d1")) {
361
+ cfBindings.push("d1");
362
+ p.log.info("D1 Database enabled (required for auth)");
363
+ }
364
+ }
365
+ const aiFlag = getFlagValue(args, "--ai");
366
+ const validAi = ["none", "agents", "claude", "both"];
367
+ let selectedAiInstructions;
368
+ if (aiFlag) {
369
+ if (!validAi.includes(aiFlag)) {
370
+ p.log.error(`Invalid --ai value "${aiFlag}". Choose: ${validAi.join(", ")}`);
371
+ process.exit(1);
372
+ }
373
+ selectedAiInstructions = aiFlag;
374
+ } else if (useDefaults) {
375
+ selectedAiInstructions = DEFAULT_AI;
376
+ } else {
377
+ const aiInstructions = await p.select({
378
+ message: "AI coding assistant instructions:",
283
379
  options: [
284
380
  {
285
- value: "d1",
286
- label: "D1 Database",
287
- hint: needsD1 ? "required for auth" : "SQLite at the edge"
381
+ value: "none",
382
+ label: "None",
383
+ hint: "skip AI instructions"
288
384
  },
289
- { value: "r2", label: "R2 Storage", hint: "S3-compatible object storage" },
290
- { value: "kv", label: "KV Store", hint: "key-value storage" },
291
- { value: "ai", label: "Workers AI", hint: "run AI models" },
292
- { value: "queues", label: "Queues", hint: "message queues" }
293
- ],
294
- initialValues: needsD1 ? ["d1"] : [],
295
- required: false
385
+ {
386
+ value: "agents",
387
+ label: "AGENTS.md",
388
+ hint: "generic (Codex, Cursor, etc.)"
389
+ },
390
+ {
391
+ value: "claude",
392
+ label: "CLAUDE.md",
393
+ hint: "Claude Code specific"
394
+ },
395
+ {
396
+ value: "both",
397
+ label: "Both",
398
+ hint: "AGENTS.md + CLAUDE.md symlink"
399
+ }
400
+ ]
296
401
  });
297
- if (p.isCancel(bindings)) {
402
+ if (p.isCancel(aiInstructions)) {
298
403
  p.cancel("Cancelled");
299
404
  process.exit(0);
300
405
  }
301
- cfBindings = bindings;
302
- if (needsD1 && !cfBindings.includes("d1")) {
303
- cfBindings.push("d1");
304
- p.log.info("D1 Database enabled (required for auth)");
305
- }
406
+ selectedAiInstructions = aiInstructions;
306
407
  }
307
- const aiInstructions = await p.select({
308
- message: "AI coding assistant instructions:",
309
- options: [
310
- {
311
- value: "none",
312
- label: "None",
313
- hint: "skip AI instructions"
314
- },
315
- {
316
- value: "agents",
317
- label: "AGENTS.md",
318
- hint: "generic (Codex, Cursor, etc.)"
319
- },
320
- {
321
- value: "claude",
322
- label: "CLAUDE.md",
323
- hint: "Claude Code specific"
324
- },
325
- {
326
- value: "both",
327
- label: "Both",
328
- hint: "AGENTS.md + CLAUDE.md symlink"
329
- }
330
- ]
331
- });
332
- if (p.isCancel(aiInstructions)) {
333
- p.cancel("Cancelled");
334
- process.exit(0);
335
- }
336
- const selectedAiInstructions = aiInstructions;
337
408
  const projectPath = resolve(process.cwd(), projectName);
338
- const selectedTemplate = template;
339
409
  const s = p.spinner();
340
410
  if (selectedTemplate === "tanstack-start") {
341
411
  s.start("Creating TanStack Start project...");
@@ -617,4 +687,11 @@ ${envVarPrefix}BETTER_AUTH_URL=http://localhost:3000
617
687
 
618
688
  ${nextSteps}`);
619
689
  }
620
- main().catch(console.error);
690
+ main().catch((err) => {
691
+ if (err instanceof Error) {
692
+ console.error(`Error: ${err.message}`);
693
+ } else {
694
+ console.error(err);
695
+ }
696
+ process.exit(1);
697
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jlmx/starter",
3
- "version": "1.1.3",
3
+ "version": "1.2.0",
4
4
  "description": "Scaffold new projects with jlmx's preferred stack",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "scripts": {
15
15
  "build": "bun build ./src/index.ts --outdir ./dist --target bun --packages external",
16
16
  "dev": "bun run ./src/index.ts",
17
+ "test": "bun test",
17
18
  "lint": "biome check .",
18
19
  "lint:fix": "biome check --write .",
19
20
  "format": "biome format --write .",