create-nwire 0.12.1 → 0.13.1

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 (66) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +6 -2
  3. package/package.json +1 -1
  4. package/templates/enterprise/AGENTS.md +11 -11
  5. package/templates/enterprise/README.md +15 -10
  6. package/templates/enterprise/__tests__/auto-moderate.test.ts +22 -6
  7. package/templates/enterprise/__tests__/submit-flow.test.ts +22 -6
  8. package/templates/enterprise/app/api.ts +5 -1
  9. package/templates/enterprise/app/app.ts +23 -15
  10. package/templates/enterprise/app/main.ts +20 -45
  11. package/templates/enterprise/config/app.ts +18 -0
  12. package/templates/enterprise/config/env.ts +32 -0
  13. package/templates/enterprise/config/http.ts +15 -0
  14. package/templates/enterprise/modules/posts/actions/approve-post.ts +4 -3
  15. package/templates/enterprise/modules/posts/actions/reject-post.ts +3 -3
  16. package/templates/enterprise/modules/posts/actions/submit-post.ts +4 -3
  17. package/templates/enterprise/modules/posts/events/post-was-approved.ts +1 -1
  18. package/templates/enterprise/modules/posts/events/post-was-rejected.ts +2 -1
  19. package/templates/enterprise/modules/posts/events/post-was-submitted.ts +2 -1
  20. package/templates/enterprise/modules/posts/projections/queue-dashboard.ts +34 -27
  21. package/templates/enterprise/modules/posts/queries/posts-by-author.ts +7 -5
  22. package/templates/enterprise/modules/posts/routes/approve-post.ts +3 -3
  23. package/templates/enterprise/modules/posts/routes/get-post.ts +3 -3
  24. package/templates/enterprise/modules/posts/routes/list-queue.ts +4 -4
  25. package/templates/enterprise/modules/posts/routes/posts-by-author.ts +28 -0
  26. package/templates/enterprise/modules/posts/routes/reject-post.ts +3 -3
  27. package/templates/enterprise/modules/posts/routes/submit-post.ts +3 -3
  28. package/templates/enterprise/modules/posts/workflows/auto-moderate.ts +3 -3
  29. package/templates/enterprise/package.json +10 -11
  30. package/templates/enterprise/tsconfig.json +1 -1
  31. package/templates/mcp/AGENTS.md +73 -0
  32. package/templates/mcp/__tests__/mcp-server.test.ts +205 -0
  33. package/templates/mcp/_gitignore +5 -0
  34. package/templates/mcp/_npmrc +4 -0
  35. package/templates/mcp/_pnpm-workspace.yaml +12 -0
  36. package/templates/mcp/app/app.ts +24 -0
  37. package/templates/mcp/app/main.ts +197 -0
  38. package/templates/mcp/app/store/facts-store.ts +17 -0
  39. package/templates/mcp/app/tools/add-fact.ts +27 -0
  40. package/templates/mcp/app/tools/list-facts.ts +17 -0
  41. package/templates/mcp/app/tools/lookup-fact.ts +28 -0
  42. package/templates/mcp/app/tools.ts +19 -0
  43. package/templates/mcp/config/app.ts +16 -0
  44. package/templates/mcp/config/env.ts +27 -0
  45. package/templates/mcp/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  46. package/templates/mcp/package.json +25 -0
  47. package/templates/mcp/tsconfig.json +15 -0
  48. package/templates/mcp/vitest.config.ts +8 -0
  49. package/templates/minimal/AGENTS.md +16 -9
  50. package/templates/minimal/README.md +20 -10
  51. package/templates/minimal/__tests__/hello.test.ts +3 -3
  52. package/templates/minimal/app/app.ts +21 -0
  53. package/templates/minimal/app/main.ts +15 -24
  54. package/templates/minimal/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  55. package/templates/minimal/package.json +7 -8
  56. package/templates/service/AGENTS.md +13 -11
  57. package/templates/service/README.md +5 -3
  58. package/templates/service/__tests__/todo-api.test.ts +5 -5
  59. package/templates/service/app/app.ts +25 -0
  60. package/templates/service/app/main.ts +20 -30
  61. package/templates/service/app/store/todo-store.ts +6 -22
  62. package/templates/service/config/app.ts +18 -0
  63. package/templates/service/config/env.ts +32 -0
  64. package/templates/service/config/http.ts +15 -0
  65. package/templates/service/package.json +8 -9
  66. package/templates/service/tsconfig.json +1 -1
@@ -14,7 +14,9 @@ import { post } from "@nwire/wires/http";
14
14
  import { httpKoa } from "@nwire/koa";
15
15
 
16
16
  const app = createApp({ appName: "svc" });
17
- app.wire(post("/things", { body: z.object({ name: z.string() }) }), async (input) => ({ ok: input.name }));
17
+ app.wire(post("/things", { body: z.object({ name: z.string() }) }), async (input) => ({
18
+ ok: input.name,
19
+ }));
18
20
  await endpoint("svc", { port: 3000 }).use(httpKoa()).mount(app).run();
19
21
  ```
20
22
 
@@ -26,15 +28,15 @@ their request shapes in `app/resources/`, errors in `app/errors/`.
26
28
  The single most common mistake is importing a primitive from the wrong
27
29
  package. Use exactly:
28
30
 
29
- | Primitive | Package |
30
- |---|---|
31
- | `createApp`, `appCompose`, `definePlugin` | `@nwire/app` |
32
- | `endpoint` | `@nwire/endpoint` |
33
- | `get` `post` `put` `patch` `del` | `@nwire/wires/http` |
34
- | `httpKoa` | `@nwire/koa` |
35
- | `defineHandler`, `defineResource`, `defineError`, `Unauthorized`/`Forbidden`/`NotFound`/`Conflict`/`BadRequest` | `@nwire/handler` |
36
- | `defineEvent` | `@nwire/messages` |
37
- | `defineAction`, `defineActor`, `defineProjection`, `defineQuery`, `defineWorkflow`, `createForgePlugin` | `@nwire/forge` |
31
+ | Primitive | Package |
32
+ | --------------------------------------------------------------------------------------------------------------- | ------------------- |
33
+ | `createApp`, `appCompose`, `definePlugin` | `@nwire/app` |
34
+ | `endpoint` | `@nwire/endpoint` |
35
+ | `get` `post` `put` `patch` `del` | `@nwire/wires/http` |
36
+ | `httpKoa` | `@nwire/koa` |
37
+ | `defineHandler`, `defineResource`, `defineError`, `Unauthorized`/`Forbidden`/`NotFound`/`Conflict`/`BadRequest` | `@nwire/handler` |
38
+ | `defineEvent` | `@nwire/messages` |
39
+ | `defineAction`, `defineActor`, `defineProjection`, `defineQuery`, `defineWorkflow`, `forgePlugins` | `@nwire/forge` |
38
40
 
39
41
  `defineResource` and `defineError` are in **`@nwire/handler`**, not
40
42
  `@nwire/forge`. This service template does not depend on `@nwire/forge` —
@@ -56,7 +58,7 @@ listener file (not `on` — `on` is for lifecycle hooks).
56
58
 
57
59
  ## Commands
58
60
 
59
- - `pnpm dev` — run the app (HTTP on :3000).
61
+ - `pnpm dev` — wire + Studio on http://localhost:4000.
60
62
  - `pnpm test` — run the test suite.
61
63
  - `pnpm doctor` — health-check the project setup.
62
64
  - `pnpm studio` — open the live trace/inspect console.
@@ -16,18 +16,20 @@ pnpm dev
16
16
 
17
17
  ## Try
18
18
 
19
+ `pnpm dev` serves the wire + Studio on http://localhost:4000.
20
+
19
21
  ```bash
20
22
  # create a todo
21
- curl -X POST http://localhost:3000/api/todos \
23
+ curl -X POST http://localhost:4000/api/todos \
22
24
  -H "content-type: application/json" \
23
25
  -H "x-user-id: alice" \
24
26
  -d '{"text":"buy milk"}'
25
27
 
26
28
  # list this user's todos
27
- curl -H "x-user-id: alice" http://localhost:3000/api/todos
29
+ curl -H "x-user-id: alice" http://localhost:4000/api/todos
28
30
 
29
31
  # complete one
30
- curl -X POST http://localhost:3000/api/todos/<id>/complete \
32
+ curl -X POST http://localhost:4000/api/todos/<id>/complete \
31
33
  -H "x-user-id: alice"
32
34
  ```
33
35
 
@@ -1,22 +1,22 @@
1
1
  /**
2
2
  * Smoke test — the four CRUD operations + the requireUser middleware.
3
3
  *
4
- * Boots the app under httpKoa against an ephemeral port and dispatches
5
- * fetch requests exactly the way clients will.
4
+ * Boots the app from `app.ts` (the pure value) against an ephemeral port.
5
+ * `app` is complete on import — routes are wired in the module body of
6
+ * `app.ts`. Never imports `main.ts` — that file runs a real server and
7
+ * hangs the test runner.
6
8
  */
7
9
 
8
10
  import { describe, it, expect, beforeAll, afterAll } from "vitest";
9
11
  import { endpoint } from "@nwire/endpoint";
10
12
  import { httpKoa } from "@nwire/koa";
11
- import { buildApp } from "../app/main";
13
+ import { app } from "../app/app";
12
14
  import { requireUser } from "../app/middleware/require-user";
13
15
 
14
16
  let running: Awaited<ReturnType<ReturnType<typeof endpoint>["run"]>>;
15
- let app: ReturnType<typeof buildApp>;
16
17
  let url: string;
17
18
 
18
19
  beforeAll(async () => {
19
- app = buildApp();
20
20
  const koa = httpKoa({ port: 0, prefix: "/api", middleware: [requireUser] });
21
21
  running = await endpoint("todo-test", {
22
22
  exitOnShutdown: false,
@@ -0,0 +1,25 @@
1
+ /**
2
+ * App — the bounded context value.
3
+ *
4
+ * `createApp` installs plugins and wires all routes in this module body.
5
+ * The exported `app` is COMPLETE on import — no caller needs to wire or
6
+ * start it. No ports are bound here; that is `main.ts`'s job.
7
+ *
8
+ * Tests import `{ app }` from this file and build an isolated endpoint.
9
+ * Never import `main.ts` from tests: it boots a real server and hangs
10
+ * the runner.
11
+ *
12
+ * Graduate: swap `TodoStore` for `@nwire/drizzle` or `@nwire/mongo`.
13
+ * The plugin shape (`provide("todos", { boot, shutdown, healthCheck })`)
14
+ * stays the same; handlers keep calling `resolve("todos")`.
15
+ */
16
+
17
+ import { createApp } from "@nwire/app";
18
+ import { todoStorePlugin } from "./store/todo-store";
19
+ import { wires } from "./api";
20
+
21
+ export const app = createApp({ appName: "{{PROJECT_NAME}}" }).with(todoStorePlugin);
22
+
23
+ for (const { binding, handler } of wires) {
24
+ app.wire(binding, handler);
25
+ }
@@ -1,42 +1,32 @@
1
1
  /**
2
- * Entry — boot the app under HTTP.
2
+ * Entry — the only file that boots a real HTTP server.
3
3
  *
4
- * 1. createApp constructs the bounded context; `.with(plugin)` installs
5
- * the TodoStore on the container.
6
- * 2. app.wire(...) pairs every route binding with its handler.
7
- * 3. endpoint().use(httpKoa({ middleware: [requireUser] })).mount(app).run()
8
- * runs the HTTP adapter under graceful drain + K8s probes.
4
+ * pnpm dev # wire + Studio on http://localhost:4000
9
5
  *
10
- * Run: pnpm dev
11
- * Try: curl -X POST http://localhost:3000/api/todos \
12
- * -H "content-type: application/json" \
13
- * -H "x-user-id: alice" \
14
- * -d '{"text":"buy milk"}'
15
- * curl -H "x-user-id: alice" http://localhost:3000/api/todos
6
+ * curl -X POST http://localhost:4000/api/todos \
7
+ * -H "content-type: application/json" \
8
+ * -H "x-user-id: alice" \
9
+ * -d '{"text":"buy milk"}'
10
+ * curl -H "x-user-id: alice" http://localhost:4000/api/todos
11
+ *
12
+ * `app` is a pure value — routes are wired in `./app` on import.
13
+ * This file adds the one side effect: binding a port and serving traffic.
14
+ * Tests import `{ app }` from `./app` and build their own ephemeral
15
+ * endpoint — never import this file from tests or it boots a server and
16
+ * hangs the runner.
16
17
  */
17
18
 
18
- import { createApp, definePlugin } from "@nwire/app";
19
19
  import { endpoint } from "@nwire/endpoint";
20
20
  import { httpKoa } from "@nwire/koa";
21
-
22
- import { wires } from "./api";
21
+ import { appConfig } from "../config/app";
22
+ import { httpConfig } from "../config/http";
23
23
  import { requireUser } from "./middleware/require-user";
24
- import { TodoStore } from "./store/todo-store";
25
-
26
- const todoStorePlugin = definePlugin("todo-store", ({ bind }) => {
27
- bind("todos", new TodoStore());
28
- });
24
+ import { app } from "./app";
29
25
 
30
- export function buildApp() {
31
- const app = createApp({ appName: "{{PROJECT_NAME}}" }).with(todoStorePlugin);
32
- for (const { binding, handler } of wires) {
33
- app.wire(binding, handler);
34
- }
35
- return app;
36
- }
26
+ const cfg = appConfig();
27
+ const http = httpConfig();
37
28
 
38
- const app = buildApp();
39
- await endpoint("{{PROJECT_NAME}}", { port: 3000 })
40
- .use(httpKoa({ prefix: "/api", middleware: [requireUser] }))
29
+ await endpoint(cfg.name, { port: cfg.port, banner: cfg.banner })
30
+ .use(httpKoa({ prefix: http.prefix, middleware: [requireUser] }))
41
31
  .mount(app)
42
32
  .run();
@@ -99,32 +99,16 @@ export class TodoStore {
99
99
  // ─── The plugin (optional — used when you add the forge battery) ────────
100
100
 
101
101
  /**
102
- * Plugin shape: `provide("todos", { boot, shutdown })`.
102
+ * Plugin shape: `bind("todos", store)`.
103
103
  *
104
- * The framework calls `boot()` during app start, registers the result
105
- * on the container under the name we chose, and calls `shutdown(store)`
106
- * during graceful drain. From any handler: `resolve("todos")`.
104
+ * `bind` registers the store on the container under the name we chose.
105
+ * From any handler: `resolve("todos")`. For lifecycle teardown, use
106
+ * `dispose(() => ...)` called on graceful shutdown.
107
107
  *
108
108
  * This same shape works whether the store is in-memory (here), a Pool
109
109
  * (Postgres via Drizzle), or a MongoClient. The plugin wrapper handles
110
110
  * the lifecycle; the store implementation handles its own concerns.
111
- *
112
- * this template's main.ts uses the simpler `container.register(...)` path because we
113
- * don't have a forge app yet. The plugin is exported so forge graduation
114
- * is `import { todoStorePlugin } from "./store/todo-store"`.
115
111
  */
116
- export const todoStorePlugin = definePlugin("todos", ({ provide }) => {
117
- provide("todos", {
118
- boot: () => new TodoStore(),
119
- shutdown: (store) => {
120
- // For an in-memory store there's nothing to close. A real adapter
121
- // would call `pool.end()` / `client.close()` here.
122
- void store;
123
- },
124
- healthCheck: (store) => {
125
- // Trivial — proves the store is alive. A real adapter would
126
- // SELECT 1 against the DB. Lightship calls this on every /ready.
127
- if (store.size() < 0) throw new Error("store corrupt");
128
- },
129
- });
112
+ export const todoStorePlugin = definePlugin("todos", ({ bind }) => {
113
+ bind("todos", new TodoStore());
130
114
  });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * App-level settings — the process identity and lifecycle budgets the
3
+ * endpoint owns. A function of the typed `env`, so the same config file
4
+ * reads differently per environment without an `if` in sight.
5
+ */
6
+
7
+ import { env, type Env } from "./env";
8
+
9
+ export const appConfig = (e: Env = env) => ({
10
+ /** Endpoint + app name — shows in the banner, logs, and telemetry. */
11
+ name: "{{PROJECT_NAME}}",
12
+ /** Port the HTTP transport binds. */
13
+ port: e.PORT,
14
+ /** Print the boot banner outside of tests. */
15
+ banner: e.NODE_ENV !== "test",
16
+ });
17
+
18
+ export type AppConfig = ReturnType<typeof appConfig>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Typed environment — read `process.env` once, through a schema, and never
3
+ * touch it again from app code. Missing or malformed values fail loudly at
4
+ * boot with a readable message, not three layers deep at first request.
5
+ *
6
+ * `config/*.ts` files take this typed `env` and shape it into the settings
7
+ * each concern needs.
8
+ */
9
+
10
+ import { z } from "zod";
11
+
12
+ const EnvSchema = z.object({
13
+ NODE_ENV: z.enum(["development", "test", "production"]).default("development"),
14
+ PORT: z.coerce.number().int().min(0).max(65_535).default(3000),
15
+ HTTP_PREFIX: z.string().default("/api"),
16
+ });
17
+
18
+ export type Env = z.output<typeof EnvSchema>;
19
+
20
+ export function loadEnv(source: NodeJS.ProcessEnv = process.env): Env {
21
+ const parsed = EnvSchema.safeParse(source);
22
+ if (!parsed.success) {
23
+ const summary = parsed.error.issues
24
+ .map((i) => ` ${i.path.join(".") || "(root)"}: ${i.message}`)
25
+ .join("\n");
26
+ throw new Error(`Invalid environment:\n${summary}`);
27
+ }
28
+ return parsed.data;
29
+ }
30
+
31
+ /** The validated environment, loaded once. */
32
+ export const env: Env = loadEnv();
@@ -0,0 +1,15 @@
1
+ /**
2
+ * HTTP transport settings — prefix, OpenAPI, and the dev inspect surface
3
+ * Studio reads. A function of the typed `env`, like every config file here.
4
+ */
5
+
6
+ import { env, type Env } from "./env";
7
+
8
+ export const httpConfig = (e: Env = env) => ({
9
+ /** Mounted under this prefix — e.g. `POST /api/todos`. */
10
+ prefix: e.HTTP_PREFIX,
11
+ /** Expose detailed errors in dev; redact in production. */
12
+ exposeErrors: e.NODE_ENV !== "production",
13
+ });
14
+
15
+ export type HttpConfig = ReturnType<typeof httpConfig>;
@@ -4,27 +4,26 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "vite-node app/main.ts",
7
+ "dev": "nwire dev",
8
8
  "test": "vitest run",
9
9
  "doctor": "nwire doctor",
10
10
  "studio": "nwire studio",
11
11
  "cache": "nwire cache"
12
12
  },
13
13
  "dependencies": {
14
- "@nwire/app": "^0.12.1",
15
- "@nwire/endpoint": "^0.12.1",
16
- "@nwire/handler": "^0.12.1",
17
- "@nwire/koa": "^0.12.1",
18
- "@nwire/wires": "^0.12.1",
14
+ "@nwire/app": "^0.13.1",
15
+ "@nwire/endpoint": "^0.13.1",
16
+ "@nwire/handler": "^0.13.1",
17
+ "@nwire/koa": "^0.13.1",
18
+ "@nwire/wires": "^0.13.1",
19
19
  "koa": "^2.16.1",
20
20
  "zod": "^4.0.0"
21
21
  },
22
22
  "devDependencies": {
23
+ "@nwire/cli": "^0.13.1",
23
24
  "@types/koa": "^2.15.0",
24
25
  "@types/node": "^22.19.9",
25
26
  "typescript": "^5.9.0",
26
- "vite-node": "^3.2.4",
27
- "vitest": "^4.0.18",
28
- "@nwire/cli": "^0.12.1"
27
+ "vitest": "^4.0.18"
29
28
  }
30
29
  }
@@ -11,5 +11,5 @@
11
11
  "noEmit": true,
12
12
  "types": ["node"]
13
13
  },
14
- "include": ["app/**/*", "__tests__/**/*"]
14
+ "include": ["app/**/*", "config/**/*", "__tests__/**/*"]
15
15
  }