@dunx/create-app 3.0.2 → 3.0.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/README.md CHANGED
@@ -13,18 +13,52 @@ bunx @dunx/create-app my-api
13
13
  ```
14
14
  cd my-api
15
15
  bun install
16
- bun run start # http://localhost:3000/greetings
16
+ bun run dev # http://localhost:3000/greetings, restarting on a change
17
17
  ```
18
18
 
19
+ ## The questions
20
+
21
+ There is no flag for choosing features. The command opens a list:
22
+
23
+ ```
24
+ ? Features 2 chosen, 1 pulled in
25
+ ○ notes CRUD routes with zod validation. The smallest real feature.
26
+ ◉ openapi OpenAPI 3.1 from the routes own schemas, plus the Swagger UI page.
27
+ ◈ database drizzle over bun:sqlite, with a schema, seeds and migrations.
28
+ ❯ ◉ users A repository, a service and validated routes over the database.
29
+ ○ auth better-auth mounted, with SessionGuard and an audit trail.
30
+ database comes along as a requirement.
31
+ Space toggles. ↑↓ moves. a all, n none. Enter continues.
32
+ ```
33
+
34
+ | Key | Does |
35
+ | --------------- | --------------------------------------------- |
36
+ | `↑` `↓`, `k` `j`, Tab | Move the cursor |
37
+ | Space | Toggle the feature under it |
38
+ | `a` / `n` | Everything / nothing |
39
+ | Enter | Take the selection |
40
+ | Ctrl+C, Esc | Stop, having written nothing |
41
+
42
+ `◉` is chosen, `◈` is pulled in by something else you chose, `○` is neither. The
43
+ two lines under the list update as you go: what your selection drags in, and which
44
+ of it needs Redis or Postgres running to do anything.
45
+
46
+ Three more questions appear only when there is something to ask: a directory, when
47
+ the command line named none; a package name, when the directory's is one npm would
48
+ reject; and whether to write into a directory that already has files in it.
49
+
19
50
  ## Options
20
51
 
21
- | Flag | Default | Meaning |
22
- | ------------------- | ------------------ | -------------------------------------------------- |
23
- | `--name <name>` | the directory name | Package name for the generated app |
24
- | `--template <name>` | `minimal` | Which template to write |
25
- | `--force` | off | Write into a directory that already has files |
26
- | `--yes`, `-y` | | Accepted and ignored; nothing here ever prompts |
27
- | `--help` | | Print usage |
52
+ | Flag | Default | Meaning |
53
+ | --------------- | ------------------ | --------------------------------------------- |
54
+ | `--name <name>` | the directory name | Package name for the generated app |
55
+ | `--force` | off | Write into a directory that already has files |
56
+ | `--yes`, `-y` | off | Skip the questions, take the minimal template |
57
+ | `--help` | | Print usage |
58
+
59
+ **Piped, redirected or in CI it asks nothing** and writes the minimal template, so
60
+ a script never hangs on a question nothing can answer. To choose features without
61
+ a terminal, call [`scaffold`](#programmatic-use) rather than passing flags.
28
62
 
29
63
  The name is validated against npm's rules **before** anything is created, because
30
64
  an invalid one would otherwise surface as a confusing `bun install` failure inside
@@ -38,9 +72,31 @@ bunx @dunx/create-app .
38
72
  ```
39
73
 
40
74
  `.git`, `.gitkeep`, `.DS_Store` and `LICENSE` do not count as contents, so a fresh
41
- repo or a clone of an empty GitHub repository is a valid target without `--force`.
75
+ repo or a clone of an empty GitHub repository is a valid target without a question.
42
76
  Nothing else is ignored: `.gitignore` and `README.md` both come out of the template,
43
- and overwriting your copy of either is what `--force` is there to ask about.
77
+ and overwriting your copy of either is what the last question asks about.
78
+
79
+ ## What a composed app looks like
80
+
81
+ ```
82
+ my-api/
83
+ src/
84
+ main.ts exports createApp, and serves it when run directly
85
+ app.module.ts the root module, importing every feature
86
+ config.ts one validation function, flat env in and a shaped object out
87
+ users/ one directory per feature you chose
88
+ package.json dev, start, test, typecheck
89
+ bunfig.toml the transform preload
90
+ ```
91
+
92
+ Three files are generated for the selection and the feature directories are copied.
93
+ `main.ts` is one file rather than two: a test imports `createApp` from it, and the
94
+ `import.meta.main` block at the bottom is what stops that starting a server.
95
+
96
+ **There is no worker entry point, even with queues.** `QueueModule` is given
97
+ `consume: true`, so the container opens the bullmq workers at `onInit` and closes
98
+ them before the connections the handlers use. A handler marked `background: true` is
99
+ forked by bullmq itself into `src/jobs/jobs.processor.ts`.
44
100
 
45
101
  ## What it generates
46
102
 
@@ -49,7 +105,12 @@ The `minimal` template, the same app as
49
105
  server, and the `bunfig.toml` preload line that makes constructor injection work.
50
106
 
51
107
  Its `src/` is a **byte-for-byte copy** of that example, and a test in this package
52
- fails if the two ever drift. The example is the one CI boots, so keeping them
108
+ fails if the two ever drift.
109
+
110
+ Every app also gets an `AGENTS.md` naming its layout, its commands and the rules
111
+ dunx fails at boot over, plus a `CLAUDE.md` pointing at it. Both link
112
+ <https://petarzarkov.github.io/dunx/setup.md>, which is served per release, rather
113
+ than copying the framework's own instructions into your repository. The example is the one CI boots, so keeping them
53
114
  identical is what makes the template trustworthy rather than merely plausible.
54
115
 
55
116
  ## Two details
@@ -80,9 +141,14 @@ import { scaffold } from '@dunx/create-app';
80
141
  const { directory, files } = await scaffold({
81
142
  target: 'my-api',
82
143
  name: '@acme/my-api',
144
+ features: ['users', 'openapi'],
83
145
  });
84
146
  ```
85
147
 
148
+ This is the scripted path the removed `--with` flag used to be. `features` takes
149
+ the same names the list shows, in any order, and pulls in what they require;
150
+ `FEATURES` exports the set. Omitting it writes the minimal template.
151
+
86
152
  `scaffold` throws `ScaffoldError` for anything the caller can fix - an unknown
87
153
  template, an unusable package name, a non-empty target without `force` - and lets
88
154
  everything else propagate.
@@ -0,0 +1,5 @@
1
+ import type { Feature } from './features.js';
2
+ export declare const CLAUDE_POINTER = "# CLAUDE.md\n\nThe instructions for this application live in `AGENTS.md`, imported below so every\nagent reads one file.\n\n@AGENTS.md\n";
3
+ export declare const agents: (name: string, features: readonly Feature[]) => string;
4
+ /** Both files, written for a fixed template and a composed app alike. */
5
+ export declare const agentFiles: (name: string, features: readonly Feature[]) => Readonly<Record<string, string>>;
@@ -0,0 +1,14 @@
1
+ import type { Style } from './style.js';
2
+ /**
3
+ * The header the questions run under.
4
+ *
5
+ * Printed once, before the first prompt, and only when there is a terminal to
6
+ * print it into: a logo piped into a log file is noise. A terminal too narrow for
7
+ * the logo gets the one-line form rather than a wrapped one, which is what
8
+ * `Bun.stringWidth` is measured against.
9
+ */
10
+ export declare class Banner {
11
+ #private;
12
+ constructor(style: Style);
13
+ lines(width: number, version: string): readonly string[];
14
+ }
@@ -221,7 +221,7 @@ var FEATURES = [
221
221
  {
222
222
  name: "jobs",
223
223
  source: "jobs",
224
- summary: "bullmq queues and a worker, over Bun.RedisClient.",
224
+ summary: "bullmq queues over Bun.RedisClient, background handlers forked.",
225
225
  requires: ["images"],
226
226
  module: { klass: "JobsModule", from: "./jobs/jobs.module.js" },
227
227
  dependencies: ["@dunx/infra", "bullmq", "ioredis", "zod"],
@@ -318,6 +318,119 @@ import { basename, dirname, join, resolve } from "path";
318
318
  import { fileURLToPath } from "url";
319
319
  var {Glob } = globalThis.Bun;
320
320
 
321
+ // src/agents.ts
322
+ var SETUP_URL = "https://petarzarkov.github.io/dunx/setup.md";
323
+ var LLMS_URL = "https://petarzarkov.github.io/dunx/llms.txt";
324
+ var CLAUDE_POINTER = `# CLAUDE.md
325
+
326
+ The instructions for this application live in \`AGENTS.md\`, imported below so every
327
+ agent reads one file.
328
+
329
+ @AGENTS.md
330
+ `;
331
+ var RULES = `## Rules that produce a boot error when broken
332
+
333
+ - **No \`@Injectable()\`, no \`@Inject()\`.** Listing a class in a module's
334
+ \`providers\` is what makes it injectable. dunx uses TC39 standard decorators,
335
+ which have no parameter decorators. For a value with no constructor parameter to
336
+ hang off, use \`inject(Token)\` in a field initializer.
337
+ - **Do not add \`reflect-metadata\`, \`experimentalDecorators\` or
338
+ \`emitDecoratorMetadata\`.** \`bunfig.toml\` preloads \`@dunx/transform\`, which
339
+ records each class's constructor parameter types. Removing that line makes every
340
+ provider fail at boot.
341
+ - **A constructor parameter whose type is erased fails at boot, naming the
342
+ parameter.** An interface, a primitive, a union, a class type parameter, or a
343
+ \`import type\` at an injection site all record as \`unresolved\`. Inject a class,
344
+ and drop \`type\` from the import.
345
+ - **Relative imports carry \`.js\`**: \`'./users.service.js'\`, never
346
+ \`'./users.service'\`.
347
+ - **A module's \`exports\` is its public surface.** The container is scoped per
348
+ module, so a provider another module injects has to be exported by the module that
349
+ declares it.
350
+ - **\`bun\` only.** No \`npm\`, \`npx\`, \`yarn\` or \`pnpm\`; run tools with \`bunx\`.
351
+ `;
352
+ var layout = (features) => features.length === 0 ? `- \`src/main.ts\` - the entry point
353
+ - \`src/app.module.ts\` - the root module; \`controllers\` get routes, \`providers\` do not
354
+ - \`src/greetings.controller.ts\`, \`src/greetings.service.ts\` - one route and its provider
355
+ - \`src/app.test.ts\` - the whole app behind a real server on port 0
356
+ - \`bunfig.toml\` - the preload line constructor injection needs
357
+ ` : `- \`src/main.ts\` - exports \`createApp\`, and serves it when run directly
358
+ - \`src/app.module.ts\` - the root module, importing every feature
359
+ - \`src/config.ts\` - one validation function, flat env in and a shaped object out
360
+ ${features.map((feature) => `- \`src/${feature.source}/\` - ${feature.name}`).join(`
361
+ `)}
362
+ - \`bunfig.toml\` - the preload line constructor injection needs
363
+
364
+ \`main.ts\`, \`app.module.ts\` and \`config.ts\` were generated for the features chosen
365
+ at scaffold time. Everything else was copied from dunx's \`examples/full\`. The
366
+ \`*.demo.ts\` files are that example's scripted walkthroughs; delete one and its
367
+ \`providers\` entry to drop it.
368
+
369
+ A test imports \`createApp\` from \`./main.js\`; the \`import.meta.main\` block at the
370
+ bottom is what stops that starting a server.
371
+ `;
372
+ var agents = (name, features) => {
373
+ const services = features.filter((feature) => feature.service !== undefined);
374
+ const hasJobs = features.some((feature) => feature.name === "jobs");
375
+ return `# ${name}
376
+
377
+ Notes for an agent working in this application. It is a
378
+ [dunx](https://github.com/petarzarkov/dunx) app, scaffolded by
379
+ \`bunx @dunx/create-app\`.
380
+
381
+ ## Commands
382
+
383
+ \`\`\`bash
384
+ bun install
385
+ bun run dev # http://localhost:3000, restarting on a change
386
+ bun run start
387
+ bun test
388
+ bun run typecheck
389
+ \`\`\`${hasJobs ? `
390
+
391
+ **There is no worker command.** \`QueueModule\` is given \`consume: true\`, so the
392
+ container opens the workers at \`onInit\` and closes them before the connections they
393
+ use. A handler marked \`background: true\` is forked by bullmq into
394
+ \`src/jobs/jobs.processor.ts\`, which nobody runs by hand.` : ""}
395
+
396
+ ## Layout
397
+
398
+ ${layout(features)}
399
+ ${features.length === 0 ? "" : `## What is wired up
400
+
401
+ ${features.map((feature) => `- **${feature.name}** - ${feature.summary}`).join(`
402
+ `)}
403
+
404
+ `}${services.length === 0 ? "" : `## Services
405
+
406
+ Each of these reports itself degraded rather than failing the boot, so the app
407
+ starts without them.
408
+
409
+ ${services.map((feature) => `- **${feature.name}** needs ${feature.service}`).join(`
410
+ `)}
411
+
412
+ `}${RULES}
413
+ ## Reading this app instead of grepping it
414
+
415
+ \`\`\`bash
416
+ bunx @dunx/mcp ./src/app.module.ts
417
+ \`\`\`
418
+
419
+ An MCP server over stdio answering what routes, providers, modules and gateways
420
+ exist, and which constructor parameters would fail to resolve. It reads the module
421
+ graph and never boots the app.
422
+
423
+ ## The framework's own instructions
424
+
425
+ - <${SETUP_URL}> - installing, wiring and verifying a dunx app
426
+ - <${LLMS_URL}> - every dunx document, as raw markdown
427
+ `;
428
+ };
429
+ var agentFiles = (name, features) => ({
430
+ "AGENTS.md": agents(name, features),
431
+ "CLAUDE.md": CLAUDE_POINTER
432
+ });
433
+
321
434
  // src/generate.ts
322
435
  var HEADER = (name) => `// Generated by @dunx/create-app for ${name}. Yours to edit.
323
436
  `;
@@ -344,13 +457,11 @@ var manifest = (features) => {
344
457
  dependencies[dep] = DUNX.test(dep) ? "__DUNX_VERSION__" : versionOf(dep);
345
458
  }
346
459
  const scripts = {
460
+ dev: "bun --watch src/main.ts",
347
461
  start: "bun src/main.ts",
348
462
  test: "bun test",
349
463
  typecheck: "tsc --noEmit"
350
464
  };
351
- if (features.some((feature) => feature.name === "jobs")) {
352
- scripts["worker"] = "bun src/worker.ts";
353
- }
354
465
  return `${JSON.stringify({
355
466
  name: "__DUNX_APP_NAME__",
356
467
  version: "0.1.0",
@@ -477,15 +588,17 @@ ${chosen.map(([, group]) => ` ${group.map}`).join(`
477
588
  `;
478
589
  };
479
590
  var has = (features, name) => features.some((feature) => feature.name === name);
480
- var bootstrap = (name, features) => {
591
+ var main = (name, features) => {
481
592
  const openapi = has(features, "openapi");
482
593
  const websockets = has(features, "websockets");
483
594
  const http = has(features, "http");
595
+ const health = has(features, "health");
484
596
  const documentsAuth = openapi && has(features, "auth");
485
597
  const assets = has(features, "assets");
486
598
  const throttle = has(features, "throttle");
487
599
  const imports = [
488
600
  ...documentsAuth ? ["import { Auth, betterAuthDocument } from '@dunx/auth';"] : [],
601
+ "import { Logger } from '@dunx/core';",
489
602
  `import { ${[
490
603
  "HttpFactory",
491
604
  ...websockets ? ["RedisRelay"] : [],
@@ -495,12 +608,9 @@ var bootstrap = (name, features) => {
495
608
  ].join(", ")} } from '@dunx/http';`,
496
609
  ...openapi ? ["import { OpenApiModule } from '@dunx/openapi';"] : [],
497
610
  "import { AppModule } from './app.module.js';",
498
- `import { ${[
499
- ...http ? ["AppConfigService"] : [],
500
- ...websockets ? ["RELAY_CHANNEL"] : []
501
- ].join(", ")} } from './config.js';`,
611
+ `import { ${["AppConfigService", ...websockets ? ["RELAY_CHANNEL"] : []].join(", ")} } from './config.js';`,
502
612
  ...http ? ["import { RequestTrailMiddleware } from './http/request-trail.js';"] : []
503
- ].filter((line) => !line.includes("{ }"));
613
+ ];
504
614
  const root = documentsAuth ? `OpenApiModule.forRootAsync({
505
615
  root: AppModule,
506
616
  inject: [Auth] as const,
@@ -538,12 +648,20 @@ var bootstrap = (name, features) => {
538
648
  ] : [],
539
649
  ...throttle ? ["app.use(ThrottleGuard);"] : []
540
650
  ];
651
+ const urls = [
652
+ ...openapi ? [
653
+ "logger.info(`docs ${new URL('api/docs', url).href}`);",
654
+ "logger.info(`openapi ${new URL('api/openapi.json', url).href}`);"
655
+ ] : [],
656
+ ...health ? ["logger.info(`health ${new URL('api/health', url).href}`);"] : []
657
+ ];
541
658
  return `${HEADER(name)}${imports.join(`
542
659
  `)}
543
660
 
544
661
  /**
545
- * One app, built the same way for \`bun start\` and for the tests - so what the
546
- * tests exercise is what actually serves.
662
+ * One app for \`bun run start\`, \`bun run dev\` and the tests, and one file:
663
+ * \`createApp\` is exported for a caller that wants the shape without a server, and
664
+ * the block at the bottom serves it when this file is the entry point.
547
665
  *
548
666
  * \`create()\` boots the container and discovers routes and gateways; \`listen()\` is
549
667
  * what builds the \`Bun.serve\` route table. Everything between the two still gets to
@@ -564,23 +682,8 @@ ${shaping.map((line) => ` ${line}`).join(`
564
682
 
565
683
  return app;
566
684
  };
567
- `;
568
- };
569
- var main = (name, features) => {
570
- const health = has(features, "health");
571
- const openapi = has(features, "openapi");
572
- const lines = [
573
- ...openapi ? [
574
- "logger.info(`docs ${new URL('api/docs', url).href}`);",
575
- "logger.info(`openapi ${new URL('api/openapi.json', url).href}`);"
576
- ] : [],
577
- ...health ? ["logger.info(`health ${new URL('api/health', url).href}`);"] : []
578
- ];
579
- return `${HEADER(name)}import { Logger } from '@dunx/core';
580
- import { createApp } from './bootstrap.js';
581
- import { AppConfigService } from './config.js';
582
685
 
583
- async function bootstrap(): Promise<void> {
686
+ const start = async (): Promise<void> => {
584
687
  const app = await createApp();
585
688
  app.enableShutdownHooks();
586
689
 
@@ -589,31 +692,24 @@ async function bootstrap(): Promise<void> {
589
692
  const url = await app.listen(config.get('port'));
590
693
 
591
694
  logger.info(\`listening on \${url}\`);
592
- ${lines.map((line) => ` ${line}`).join(`
593
- `)}${lines.length > 0 ? `
695
+ ${urls.map((line) => ` ${line}`).join(`
696
+ `)}${urls.length > 0 ? `
594
697
  ` : ""}
595
698
  // Nothing else to do: the server holds the process open, and the shutdown hooks
596
699
  // resolve this once a signal arrives.
597
700
  await app.closed;
598
- }
599
-
600
- bootstrap().catch((error: unknown) => {
601
- console.error('failed to start', error);
602
- process.exit(1);
603
- });
604
- `;
605
701
  };
606
- var worker = (name) => `${HEADER(name)}import { AppFactory } from '@dunx/core';
607
- import { AppModule } from './app.module.js';
608
702
 
609
- /**
610
- * A queue needs a process to drain it, and it is deliberately not the web one: a
611
- * worker that shares the server's event loop competes with request handling.
612
- */
613
- const app = await AppFactory.create(AppModule);
614
- app.enableShutdownHooks();
615
- await app.closed;
703
+ // False when a test imports this file for \`createApp\` alone, which is what lets one
704
+ // module be both the entry point and the app's definition.
705
+ if (import.meta.main) {
706
+ start().catch((error: unknown) => {
707
+ console.error('failed to start', error);
708
+ process.exit(1);
709
+ });
710
+ }
616
711
  `;
712
+ };
617
713
  var envExample = (groups) => {
618
714
  const lines = groups.flatMap((group) => CONFIG_GROUPS[group]?.env ?? []).map((entry) => `${entry.name}=${entry.value}`);
619
715
  return lines.length === 0 ? `# Every variable has a default, so this file is optional.
@@ -630,6 +726,7 @@ Scaffolded with \`bunx @dunx/create-app\`.
630
726
 
631
727
  \`\`\`bash
632
728
  bun install
729
+ bun run dev # restarts on a change
633
730
  bun run start
634
731
  \`\`\`
635
732
 
@@ -648,17 +745,19 @@ ${services.map((feature) => `- **${feature.name}** needs ${feature.service}`).jo
648
745
 
649
746
  `}## Layout
650
747
 
651
- - \`src/main.ts\` - the entry point
652
- - \`src/bootstrap.ts\` - builds the app; shared by \`start\` and the tests
748
+ - \`src/main.ts\` - exports \`createApp\`, and serves it when run directly
653
749
  - \`src/app.module.ts\` - the root module, importing every feature
654
750
  - \`src/config.ts\` - one validation function, flat env in and a shaped object out
655
751
  ${features.map((feature) => `- \`src/${feature.source}/\` - ${feature.name}`).join(`
656
752
  `)}
657
753
 
658
- \`main.ts\`, \`bootstrap.ts\`, \`app.module.ts\` and \`config.ts\` were generated for the
659
- features you chose; everything else is copied from dunx's \`examples/full\`, which is
660
- run and toured in CI on every push. The \`*.demo.ts\` files are that example's
661
- scripted walkthroughs - delete one and its \`providers\` entry when you do not want it.
754
+ \`main.ts\`, \`app.module.ts\` and \`config.ts\` were generated for the features you
755
+ chose; everything else is copied from dunx's \`examples/full\`, which is run and
756
+ toured in CI on every push. The \`*.demo.ts\` files are that example's scripted
757
+ walkthroughs - delete one and its \`providers\` entry when you do not want it.
758
+
759
+ A test imports \`createApp\` from \`./main.js\` and never starts a server: the
760
+ \`import.meta.main\` block at the bottom of the file is false for an import.
662
761
 
663
762
  ## Constructor injection
664
763
 
@@ -681,13 +780,14 @@ var IGNORED_WHEN_EMPTY = new Set([
681
780
  ".gitkeep",
682
781
  "LICENSE"
683
782
  ]);
783
+ var blockingEntries = (directory) => existsSync(directory) ? readdirSync(directory).filter((entry) => !IGNORED_WHEN_EMPTY.has(entry)).sort() : [];
684
784
 
685
785
  class ScaffoldError extends Error {
686
786
  name = "ScaffoldError";
687
787
  }
688
788
  var templatesRoot = () => resolve(dirname(fileURLToPath(import.meta.url)), "..", "templates");
689
789
  var isValidPackageName = (name) => /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);
690
- var readPackageVersion = async () => {
790
+ var packageVersion = async () => {
691
791
  const file = Bun.file(join(templatesRoot(), "..", "package.json"));
692
792
  const json = await file.json();
693
793
  return json.version ?? "0.0.0";
@@ -695,19 +795,14 @@ var readPackageVersion = async () => {
695
795
  var fill = (contents, name, version) => contents.replaceAll(VERSION_PLACEHOLDER, version).replaceAll("__DUNX_APP_NAME__", name);
696
796
  var generated = (name, features) => {
697
797
  const groups = configGroupsFor(features);
698
- const files = {
798
+ return {
699
799
  "package.json": manifest(features),
700
800
  "README.md": readme(name, features),
701
801
  ".env.example": envExample(groups),
702
802
  "src/main.ts": main(name, features),
703
- "src/bootstrap.ts": bootstrap(name, features),
704
803
  "src/app.module.ts": appModule(name, features),
705
804
  "src/config.ts": config(name, groups)
706
805
  };
707
- if (features.some((feature) => feature.name === "jobs")) {
708
- files["src/worker.ts"] = worker(name);
709
- }
710
- return files;
711
806
  };
712
807
  var scaffold = async (options) => {
713
808
  const template = options.template ?? "minimal";
@@ -727,15 +822,15 @@ var scaffold = async (options) => {
727
822
  if (!isValidPackageName(name)) {
728
823
  throw new ScaffoldError(`"${name}" is not a usable package name. Pass --name to choose one.`);
729
824
  }
730
- if (existsSync(directory) && options.force !== true) {
731
- const blocking = readdirSync(directory).filter((entry) => !IGNORED_WHEN_EMPTY.has(entry));
825
+ if (options.force !== true) {
826
+ const blocking = blockingEntries(directory);
732
827
  if (blocking.length > 0) {
733
- const shown = blocking.sort().slice(0, 3).join(", ");
828
+ const shown = blocking.slice(0, 3).join(", ");
734
829
  const rest = blocking.length > 3 ? `, +${blocking.length - 3} more` : "";
735
830
  throw new ScaffoldError(`${directory} is not empty (${shown}${rest}). ` + `Pass --force to write into it anyway.`);
736
831
  }
737
832
  }
738
- const version = options.version ?? `^${await readPackageVersion()}`;
833
+ const version = options.version ?? `^${await packageVersion()}`;
739
834
  const written = [];
740
835
  const copyTree = async (from, into) => {
741
836
  for await (const relative of new Glob("**/*").scan({
@@ -751,12 +846,19 @@ var scaffold = async (options) => {
751
846
  written.push(target);
752
847
  }
753
848
  };
849
+ const writeAll = async (files) => {
850
+ for (const [target, contents] of Object.entries(files)) {
851
+ await Bun.write(join(directory, target), fill(contents, name, version));
852
+ written.push(target);
853
+ }
854
+ };
754
855
  if (!composing) {
755
856
  const source = join(templatesRoot(), template);
756
857
  if (!existsSync(source)) {
757
858
  throw new ScaffoldError(`Template "${template}" is missing from ${source}.`);
758
859
  }
759
860
  await copyTree(source, ".");
861
+ await writeAll(agentFiles(name, []));
760
862
  return {
761
863
  directory,
762
864
  name,
@@ -777,10 +879,10 @@ var scaffold = async (options) => {
777
879
  }
778
880
  await copyTree(from, join("src", feature.source));
779
881
  }
780
- for (const [target, contents] of Object.entries(generated(name, features))) {
781
- await Bun.write(join(directory, target), fill(contents, name, version));
782
- written.push(target);
783
- }
882
+ await writeAll({
883
+ ...generated(name, features),
884
+ ...agentFiles(name, features)
885
+ });
784
886
  return {
785
887
  directory,
786
888
  name,
@@ -790,4 +892,4 @@ var scaffold = async (options) => {
790
892
  };
791
893
  };
792
894
 
793
- export { FEATURES, featureNames, impliedBy, TEMPLATES, VERSION_PLACEHOLDER, ScaffoldError, scaffold };
895
+ export { FEATURES, featureNames, resolveFeatures, impliedBy, TEMPLATES, VERSION_PLACEHOLDER, blockingEntries, ScaffoldError, isValidPackageName, packageVersion, scaffold };