@znight/cli 0.0.0-stage → 0.1.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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +482 -2
  3. package/bin/znight.mjs +17 -0
  4. package/dist/cli.d.mts +1 -0
  5. package/dist/cli.mjs +13 -0
  6. package/dist/cli.mjs.map +1 -0
  7. package/dist/dev/orphan-guard.d.mts +1 -0
  8. package/dist/dev/orphan-guard.mjs +47 -0
  9. package/dist/dev/orphan-guard.mjs.map +1 -0
  10. package/dist/index.d.mts +679 -0
  11. package/dist/index.mjs +1 -0
  12. package/dist/main-BWsiUsxN.mjs +3865 -0
  13. package/dist/main-BWsiUsxN.mjs.map +1 -0
  14. package/dist/orphan-Dn-2xMBd.mjs +45 -0
  15. package/dist/orphan-Dn-2xMBd.mjs.map +1 -0
  16. package/package.json +71 -4
  17. package/src/cli.ts +11 -0
  18. package/src/command.ts +217 -0
  19. package/src/commands/build.ts +159 -0
  20. package/src/commands/dev.ts +249 -0
  21. package/src/commands/generate.ts +304 -0
  22. package/src/commands/new.ts +245 -0
  23. package/src/commands/openapi.ts +160 -0
  24. package/src/commands/schematic.ts +110 -0
  25. package/src/commands/shared.ts +106 -0
  26. package/src/commands/start.ts +79 -0
  27. package/src/context.ts +126 -0
  28. package/src/dev/orphan-guard.ts +49 -0
  29. package/src/dev/orphan.ts +74 -0
  30. package/src/dev/supervisor.ts +175 -0
  31. package/src/errors.ts +64 -0
  32. package/src/index.ts +73 -0
  33. package/src/integrations/compiler.ts +360 -0
  34. package/src/integrations/openapi.ts +97 -0
  35. package/src/integrations/process.ts +180 -0
  36. package/src/integrations/prompts.ts +108 -0
  37. package/src/integrations/resolve.ts +38 -0
  38. package/src/integrations/vite.ts +362 -0
  39. package/src/integrations/watch.ts +243 -0
  40. package/src/main.ts +195 -0
  41. package/src/names.ts +106 -0
  42. package/src/output.ts +110 -0
  43. package/src/runtime.ts +86 -0
  44. package/src/scaffold/create.ts +65 -0
  45. package/src/scaffold/options.ts +103 -0
  46. package/src/scaffold/templates.ts +771 -0
  47. package/src/scaffold/versions.ts +25 -0
  48. package/src/schematics/index.ts +422 -0
  49. package/src/version.ts +20 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Ugho STEPHAN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,483 @@
1
- # Temporary Holding Version
1
+ # @znight/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The `znight` command line of [Znight](../../README.md): create a project, generate code, generate the
4
+ dependency injection container, run the development loop, build and start the application.
5
+
6
+ ```sh
7
+ bunx @znight/cli new my-service # or: npx @znight/cli new my-service, pnpm dlx @znight/cli new my-service
8
+ cd my-service
9
+ bun run dev # or: npm run dev
10
+ ```
11
+
12
+ In a project, install it next to its peers (`znight new` does it for you):
13
+
14
+ ```sh
15
+ # bun
16
+ bun add -d @znight/cli @znight/compiler vite
17
+
18
+ # npm
19
+ npm install -D @znight/cli @znight/compiler vite
20
+ ```
21
+
22
+ `vite`, `@znight/compiler` and `@znight/openapi` are (optional) peer dependencies: the CLI always
23
+ uses the versions installed in your project, and only loads them for the commands that need them
24
+ (`generate`, `dev`, `build`, `openapi`). Runs on Node.js >= 24 and Bun >= 1.3.
25
+
26
+ **Documentation:** [`@znight/cli`](https://github.com/znight-org/znight/blob/main/docs/packages/cli.md)
27
+ and [getting started](https://github.com/znight-org/znight/blob/main/docs/guide/getting-started.md).
28
+ **Examples:** every [example application](https://github.com/znight-org/znight/tree/main/examples)
29
+ runs on the CLI (`generate`, `dev`, `build`, `start` scripts);
30
+ [`storefront-api`](https://github.com/znight-org/znight/tree/main/examples/storefront-api) also
31
+ writes its committed `openapi.json` with `znight openapi` and checks it with `--check`.
32
+
33
+ - [Commands](#commands)
34
+ - [`znight new`](#znight-new)
35
+ - [`znight generate`](#znight-generate)
36
+ - [`znight g`](#znight-g)
37
+ - [`znight dev`](#znight-dev)
38
+ - [`znight build`](#znight-build)
39
+ - [`znight start`](#znight-start)
40
+ - [`znight openapi`](#znight-openapi)
41
+ - [Global options, exit codes, environment](#global-options-exit-codes-environment)
42
+ - [Programmatic API](#programmatic-api)
43
+ - [Compiler contract](#compiler-contract)
44
+
45
+ ## Commands
46
+
47
+ | Command | Description |
48
+ | ----------------------------- | ---------------------------------------------------------------------------------------- |
49
+ | `znight new [name]` | Create a project (interactive, or from flags) |
50
+ | `znight generate` | Write the generated container (`src/.znight/container.generated.ts`) |
51
+ | `znight generate --check` | Exit with code 1 when the generated file is missing or out of date (CI) |
52
+ | `znight generate --watch` | Keep the generated file up to date (for `tsc --watch` and editors) |
53
+ | `znight g <schematic> <name>` | Generate a service, controller, consumer, config, guard, interceptor, factory or job |
54
+ | `znight dev` | Vite build in watch mode, full restart of the application after every rebuild |
55
+ | `znight build` | Production build (`vite build`); `--openapi` also writes `dist/openapi.json` |
56
+ | `znight start` | Run the production build |
57
+ | `znight openapi` | Write the OpenAPI document (`openapi.json`); `--check` exits with code 1 when stale (CI) |
58
+
59
+ `znight <command> --help` prints the options of a command; `znight help <command>` too.
60
+
61
+ ## `znight new`
62
+
63
+ ```sh
64
+ znight new my-service # asks the questions below
65
+ znight new my-service --yes # defaults, no question
66
+ znight new my-service --pm pnpm --adapter fastify --features config,observability,messaging
67
+ znight new my-worker --yes --features config,schedule,openapi
68
+ znight new . --yes --no-install # in the current (empty) directory
69
+ ```
70
+
71
+ In a terminal, the CLI asks for the name, the package manager, the HTTP adapter, the features,
72
+ and whether to install the dependencies and initialise git. Flags answer the questions up front; with
73
+ `--yes` (or without a terminal, e.g. in CI) the defaults are used.
74
+
75
+ | Option | Default | Description |
76
+ | ------------------------------------------ | --------------------------------------------- | -------------------------------------------------------------------------------- |
77
+ | `[name]` | (asked) | npm package name (`my-service`, `@acme/api`), or `.` |
78
+ | `-y, --yes` | `false` | Do not ask questions |
79
+ | `--pm <bun\|npm\|pnpm\|yarn>` | the one running the CLI, else bun on Bun, npm | Package manager |
80
+ | `--adapter <node\|fastify\|express\|hono>` | `node` | HTTP adapter (`node`: the built-in `node:http` server) |
81
+ | `--features <list>` | `config,observability` | `config`, `observability`, `messaging`, `schedule`, `openapi`, or `all` / `none` |
82
+ | `--no-install` | install | Do not install the dependencies |
83
+ | `--no-git` | init | Do not run `git init` (skipped anyway inside a git repository) |
84
+ | `--dir <path>` | `./<name>` (unscoped) | Target directory |
85
+ | `--force` | `false` | Write into a non-empty directory (existing files are overwritten) |
86
+
87
+ The package manager defaults to the one that launched the CLI (`npx`, `pnpm dlx`, `yarn dlx`,
88
+ `bunx`). The target directory must be empty (`.git`, `.DS_Store` and editor folders are fine).
89
+
90
+ ### The generated project
91
+
92
+ ```txt
93
+ my-service/
94
+ ├─ src/
95
+ │ ├─ main.ts # Application.create() with the plugins, then start()
96
+ │ ├─ api.ts # @OpenApiDocument: title, tags of openapi.json (openapi)
97
+ │ └─ app/
98
+ │ ├─ app.config.ts # @Config('app'): PORT, APP_GREETING (config)
99
+ │ ├─ greeting.schemas.ts # Zod schema of the request body
100
+ │ ├─ greeting.service.ts # @Injectable, uses AppConfig and Logger
101
+ │ ├─ greeting.controller.ts # GET /greetings/:name?excited, POST /greetings (validated body)
102
+ │ ├─ greeting.consumer.ts # @Consumer('greetings'), POST /greetings/async (messaging)
103
+ │ ├─ health.controller.ts # GET /health
104
+ │ └─ heartbeat.jobs.ts # @Cron('* * * * *'): a job every minute (schedule)
105
+ ├─ test/
106
+ │ ├─ greeting.service.test.ts # TestBed: real factory, auto-mocked dependencies
107
+ │ ├─ greeting.controller.test.ts # TestApp: in-memory HTTP requests
108
+ │ └─ heartbeat.jobs.test.ts # TestApp + createTestClock(): virtual time (schedule)
109
+ ├─ vite.config.ts # plugins: [znight(options)]
110
+ ├─ znight.config.ts # compiler options, shared by znight(), the Bun preload and znight generate
111
+ ├─ tsconfig.json # strict, experimentalDecorators, moduleResolution bundler, noEmit
112
+ ├─ bunfig.toml # bun only: preloads @znight/compiler/bun before the tests
113
+ ├─ package.json # type module; scripts dev, build, start, generate, typecheck, test
114
+ └─ .gitignore # node_modules, dist, .znight
115
+ ```
116
+
117
+ - `@znight/*` packages are pinned to `^<version of the CLI>` (they are released together).
118
+ - Tests run with `bun test` for Bun projects (the preload regenerates the container), with Vitest
119
+ otherwise (the `znight()` plugin of `vite.config.ts` regenerates it). Both, like
120
+ `znight generate`, read the compiler options of `znight.config.ts`.
121
+ - `typecheck` runs `znight generate && tsc --noEmit`: the generated container is git-ignored.
122
+ - The adapter's peer dependencies are added (`fastify`; `express` + `@types/express`; `hono` +
123
+ `@hono/node-server`), plus `light-my-request` (dev) for `TestApp.inject()` with the `node` and
124
+ `express` adapters. Yarn projects get a `.yarnrc.yml` with `nodeLinker: node-modules`.
125
+ - Bun projects run the application on Bun: their `dev` and `start` scripts pass `--runtime bun`
126
+ (`bun run` runs the `znight` binary, which has a `node` shebang, on Node.js).
127
+ - `schedule` adds `@znight/schedule`, `SchedulePlugin.configure()` **before** `HttpPlugin` (plugins
128
+ stop in reverse order: HTTP stops taking requests first, then running jobs drain), a sample job
129
+ and its test on a virtual clock (`createTestClock()` of `@znight/schedule/testing`).
130
+ - `openapi` adds `@znight/openapi`, `OpenApiPlugin.configure()` after `HttpPlugin` (`/openapi.json`
131
+ and the API reference at `/docs`), `src/api.ts` (`@OpenApiDocument`) and the `openapi` /
132
+ `openapi:check` scripts (commit `openapi.json`, check it in CI).
133
+ - The `TestApp` test configures `ObservabilityPlugin` with `metrics: { server: false }`: with
134
+ `prom-client` installed, the metrics server (`:9464`) would collide with `znight dev` or with other
135
+ test files running in parallel.
136
+
137
+ ## `znight generate`
138
+
139
+ Runs the compiler and writes `src/.znight/container.generated.ts` when its content changed. Compiler
140
+ diagnostics are printed with code frames; any error exits with code 1 (and nothing is written).
141
+
142
+ ```sh
143
+ znight generate # ✓ Generated src/.znight/container.generated.ts (12 providers, 30 files, 48ms)
144
+ znight generate --check # CI: exit 1 when the file is missing or out of date (never writes)
145
+ znight generate --watch # regenerate on every change (incremental: only changed files are re-parsed)
146
+ ```
147
+
148
+ | Option | Description |
149
+ | ------------------------------------------ | ----------------------------------------------------------------------------------------- |
150
+ | `--check` | Do not write; exit 1 when the generated files are missing or stale (each one is named) |
151
+ | `-w, --watch` | Regenerate on changes of the sources and of the files the compiler read (see below) |
152
+ | `--root <dir>` | Project root (default: the current directory) |
153
+ | `--config <file>` | Vite config to read the `znight()` options from, instead of `znight.config.*` (see below) |
154
+ | `--config-loader <bundle\|runner\|native>` | How Vite loads the config file |
155
+
156
+ The compiler options are looked up in this order, so that `znight generate`, `vite build` and the
157
+ Bun preload produce the same file:
158
+
159
+ 1. `znight.config.ts` (or `.mts`, `.js`, `.mjs`) in the project root, or the file named by the
160
+ `ZNIGHT_CONFIG` environment variable — the file the [Bun preload](../compiler/README.md#preload-options)
161
+ reads (`loadZnightConfig()` of the compiler; its preload-only `logLevel` is ignored). Vite is not
162
+ loaded;
163
+ 2. otherwise the options `znight()` compiles with in your Vite config: Vite resolves the config as
164
+ for `znight build`, and the CLI takes the options given to the plugin with what the plugin derives
165
+ from the config — the Vite `root` (the config's `root` is relative to the project root), the string
166
+ entries of `resolve.alias` and the runtime conditions (see [Compiler contract](#compiler-contract));
167
+ `--config <file>` skips step 1 and reads that Vite config;
168
+ 3. otherwise the defaults (without a Vite config, Vite is not even loaded).
169
+
170
+ Share one options object between the Vite plugin and the preload with a `znight.config.ts`:
171
+
172
+ ```ts
173
+ // znight.config.ts
174
+ import type { ZnightOptions } from '@znight/compiler'
175
+
176
+ export default { reportUnused: true } satisfies ZnightOptions
177
+ ```
178
+
179
+ ```ts
180
+ // vite.config.ts
181
+ import { znight } from '@znight/compiler'
182
+ import { defineConfig } from 'vite'
183
+ import options from './znight.config.ts'
184
+
185
+ export default defineConfig({ plugins: [znight(options)] })
186
+ ```
187
+
188
+ `znight generate` compiles in `serve` mode (provider source locations kept) while `vite build` writes
189
+ the disk copy without them: run `znight generate --check` before `znight build`, as below.
190
+
191
+ In CI:
192
+
193
+ ```sh
194
+ npx znight generate --check # or: npx znight generate, before tsc and the tests
195
+ npx tsc --noEmit
196
+ npx znight build
197
+ ```
198
+
199
+ `--check` names every generated file that is missing or out of date: the module and, in library
200
+ mode (`library: true`), `dist/znight.providers.json`. A build that cleans `dist/` (tsdown, `tsc --build
201
+ --clean`...) deletes that manifest, which `package.json` points to: run `znight generate` after such a
202
+ build (the Vite plugin writes it itself at the end of `vite build`).
203
+
204
+ `--watch` watches what the Vite plugin and the Bun preload watch: the `include` directories of the
205
+ project and of its source libraries (recursively), the project root (`package.json`, `tsconfig.json`,
206
+ lockfiles) and the directories of the files the last compile read (library manifests, compiled
207
+ libraries), refreshed after each compile. Only changes that can change the result trigger a compile,
208
+ by the rules of the Vite plugin (`isRelevantChange()` of the compiler: sources the compiler parses,
209
+ `.js` included; resolution config files; files it read; a file created where a relative import did
210
+ not resolve); a directory created, deleted or moved under an `include` directory triggers a rescan,
211
+ since `fs.watch` reports it alone, without its files.
212
+
213
+ ## `znight g`
214
+
215
+ ```sh
216
+ znight g service users/user # src/users/user.service.ts UserService
217
+ znight g controller order # src/order/order.controller.ts OrderController
218
+ znight g consumer orders --path src/messaging
219
+ znight g guard auth --flat # src/auth.guard.ts AuthGuard
220
+ znight g interceptor timing --aop # a method interceptor (@Intercept)
221
+ znight g config payment-gateway # @Config('paymentGateway'): PAYMENT_GATEWAY_* variables
222
+ znight g job reports/digest # src/reports/digest.jobs.ts DigestJobs
223
+ ```
224
+
225
+ | Schematic (alias) | Generates |
226
+ | --------------------- | ----------------------------------------------------------------------------------------------------------------- |
227
+ | `service` (`s`) | an `@Injectable()` class |
228
+ | `controller` (`co`) | an `@Controller('/<name>')` with `GET /` and `GET /:id` |
229
+ | `consumer` (`cs`) | an `@Consumer('<name>')` with a retried `@OnMessage()` handler (a Zod message schema when the project uses `zod`) |
230
+ | `config` (`cf`) | a `@Config('<name>')` class |
231
+ | `guard` (`gu`) | an `@Injectable()` `CanActivate` guard (with a commented `static enforces` line for route decisions) |
232
+ | `interceptor` (`itc`) | an `@Injectable()` `HttpInterceptor` (`--aop`: a `MethodInterceptor` for `@Intercept`) |
233
+ | `factory` (`f`) | a `@Factory()` with a `@Provides()` method |
234
+ | `job` (`j`) | a class of scheduled jobs (`<Name>Jobs`, `<name>.jobs.ts`) with a `@Cron` method (`@znight/schedule`) |
235
+
236
+ Names are converted: files are `kebab-case.<schematic>.ts`, classes `PascalCase<Schematic>`
237
+ (`UserProfile`, `user_profile` and `user-profile.service` all give `user-profile.service.ts` /
238
+ `UserProfileService`). Directories before the last `/` are relative to `src/`; without them the file
239
+ goes to `src/<name>/`. Nothing has to be registered afterwards: the compiler discovers the class.
240
+
241
+ Job names must be unique in the application (ZNIGHT041), so the `@Cron` of `job` is named after the
242
+ location of the file: its directories relative to `src/` (to the project root for a `--path` outside
243
+ `src/`), then the name, without the directory named after it. `znight g job digest` gives
244
+ `digest.daily`, `znight g job reports/digest` gives `reports.digest.daily` and
245
+ `znight g job digest --path src/emails` gives `emails.digest.daily`.
246
+
247
+ | Option | Description |
248
+ | --------------- | ---------------------------------------------------------- |
249
+ | `--path <dir>` | Directory of the file, relative to the current directory |
250
+ | `--flat` | Do not create a directory named after the element |
251
+ | `-f, --force` | Overwrite an existing file (refused otherwise) |
252
+ | `-d, --dry-run` | Print the file instead of writing it |
253
+ | `--aop` | `interceptor`: a method interceptor instead of an HTTP one |
254
+ | `--root <dir>` | Project root (default: the nearest `package.json`) |
255
+
256
+ `znight generate <schematic> <name>` and `znight generate:<schematic> <name>` are aliases.
257
+
258
+ ## `znight dev`
259
+
260
+ ```sh
261
+ znight dev
262
+ znight dev --runtime bun --clear
263
+ znight dev --inspect -- --verbose # arguments after -- go to the application
264
+ ```
265
+
266
+ `znight dev` runs `vite build --watch` with your Vite config (no Vite dev server) and **restarts the
267
+ whole process** after every successful build. There is no partial hot replacement of providers: a
268
+ restart is fast and always consistent.
269
+
270
+ 1. A build succeeds → after `--debounce` ms, the running process receives `SIGTERM` (the application
271
+ stops gracefully), gets `--kill-timeout` ms to exit (then `SIGKILL`), and the new build starts.
272
+ Bursts of builds cause a single restart; restarts never overlap.
273
+ 2. A build fails → Vite prints the error and **the last good process keeps running**.
274
+ 3. The compilation fails (`ZNIGHT` errors) → the `znight()` plugin prints the diagnostics; in watch
275
+ mode it does not fail the build (the bundle contains a module throwing them), so the CLI reads the
276
+ plugin's result (`plugin.api.result`) and does **not** restart: the last good process keeps
277
+ running (a restart still pending for a previous build is cancelled), and nothing starts until a
278
+ build compiles. With `--log-level silent` the CLI prints the diagnostics itself.
279
+ 4. The application crashes or exits → the CLI says so and waits for the next change.
280
+ 5. `Ctrl+C` (or `SIGTERM`, `SIGHUP` when the terminal is closed, `Ctrl+\` / `SIGQUIT`) → the
281
+ watcher closes and the application stops gracefully (`SIGINT` after `Ctrl+C`, `SIGTERM` otherwise);
282
+ a second signal kills it. The application runs in its own session and process group, so it
283
+ receives exactly one signal, and never the terminal's.
284
+ 6. The CLI dies without stopping it (`kill -9`, the OOM killer) → the application stops by itself
285
+ (`SIGTERM`, then `SIGKILL` after `--kill-timeout`) within half a second instead of keeping its
286
+ port: the CLI preloads a small guard into it (`--import` on Node.js, `--preload` on Bun) that
287
+ watches the CLI process. It needs nothing from the application, and the processes the
288
+ application starts itself are left alone.
289
+
290
+ Diagnostics of the compiler are printed on every rebuild by the `znight()` plugin.
291
+
292
+ ```txt
293
+ ✗ ZNIGHT001: Missing provider for 'Clock' required by GreetingService
294
+ --> src/app/greeting.service.ts:12:30
295
+ ✗ Compilation failed with 1 error. The last good build keeps running. Waiting for changes...
296
+ ```
297
+
298
+ | Option | Default | Description |
299
+ | ------------------------------------------ | ----------------- | ------------------------------------------------------------------- |
300
+ | `--root <dir>` | current directory | Project root |
301
+ | `--config <file>` | `vite.config.*` | Vite config file |
302
+ | `--config-loader <bundle\|runner\|native>` | Vite's | How Vite loads the config file |
303
+ | `--mode <mode>` | `development` | Vite mode |
304
+ | `--runtime <auto\|node\|bun>` | `auto` | Runtime of the application (`auto`: the one running the CLI, below) |
305
+ | `--entry <file>` | entry chunk | File to run (default: the entry chunk of the build, `dist/main.js`) |
306
+ | `--inspect` | `false` | Start the application with `--inspect` |
307
+ | `--clear` | `false` | Clear the screen before each rebuild |
308
+ | `--debounce <ms>` | `100` | Delay between a build and the restart |
309
+ | `--kill-timeout <ms>` | `5000` | Time to stop after `SIGTERM` before `SIGKILL` |
310
+ | `--log-level <info\|warn\|error\|silent>` | `warn` | Vite log level |
311
+
312
+ The application runs with `NODE_ENV=development` (unless set when the CLI starts: the `NODE_ENV`
313
+ that Vite sets in the CLI process for its build is not passed on) and, on Node.js, with
314
+ `--enable-source-maps`, so stack traces point to your TypeScript files.
315
+
316
+ `--runtime auto` is the runtime running the CLI. Under `bun run` (or `bunx`) that is Node.js: the
317
+ `znight` binary has a `node` shebang, which Bun honours. Pass `--runtime bun` (as the `dev` and
318
+ `start` scripts of Bun projects created by `znight new` do), or run `bun --bun run dev`.
319
+
320
+ ## `znight build`
321
+
322
+ ```sh
323
+ znight build # ✓ Built dist/main.js 48.20 kB in 1.24s
324
+ znight build --mode staging
325
+ znight build --openapi # also dist/openapi.json (--openapi=<path> for another file)
326
+ znight build --openapi --openapi-prefix /api # HttpPlugin.configure({ prefix: '/api' })
327
+ ```
328
+
329
+ Runs `vite build` with your Vite config. With the defaults of the `znight()` plugin, `src/main.ts`
330
+ is built to `dist/main.js` (Node.js 24 target, source maps). Any compiler error fails the build
331
+ (exit code 1). The `root` of the Vite config is honoured, relative to the project root (`--root`),
332
+ like the Vite CLI run in the project directory; `znight dev` and `znight generate` resolve it the
333
+ same way.
334
+
335
+ | Option | Default | Description |
336
+ | ------------------------------------------ | ----------------- | ------------------------------ |
337
+ | `--root <dir>` | current directory | Project root |
338
+ | `--config <file>` | `vite.config.*` | Vite config file |
339
+ | `--config-loader <bundle\|runner\|native>` | Vite's | How Vite loads the config file |
340
+ | `--mode <mode>` | `production` | Vite mode |
341
+ | `--log-level <info\|warn\|error\|silent>` | `info` | Vite log level |
342
+ | `--openapi[=<path>]` | off | Also emit the OpenAPI document |
343
+ | `--openapi-prefix <path>` | none | With `--openapi`: HTTP prefix |
344
+
345
+ `--openapi` adds the `openapi()` Vite plugin of `@znight/openapi` to the build when the Vite config
346
+ does not register one: after the bundle is written, the document is emitted to `dist/openapi.json`
347
+ (next to `dist/main.js`, ready for `OpenApiPlugin.configure({ document: { file } })`), or to the
348
+ given path.
349
+
350
+ When `HttpPlugin` has a `prefix`, pass it with `--openapi-prefix` (like `znight openapi --prefix`):
351
+ it becomes the `servers` of the document (`[{ "url": "/api" }]`) unless `@OpenApiDocument` declares
352
+ servers, so the file is the document the application serves in `'build'` mode. Without it, the
353
+ document has no `servers`, and the API reference ("Test request", "Try it out") and generated
354
+ clients call `/greetings` instead of `/api/greetings`. When the Vite config registers its own
355
+ `openapi()`, that plugin emits the document with its options: `--openapi=<path>` and
356
+ `--openapi-prefix` are then ignored, with a warning (set `output` and `prefix` on that `openapi()`).
357
+
358
+ ## `znight start`
359
+
360
+ ```sh
361
+ znight start # node --enable-source-maps dist/main.js
362
+ znight start --runtime bun
363
+ znight start --entry dist/worker.js -- --verbose
364
+ ```
365
+
366
+ Runs the production build with `NODE_ENV=production` (unless set). On Linux and macOS the CLI
367
+ **replaces itself** with the runtime (`execve`): same PID, signals and exit code go straight to the
368
+ application, which makes `znight start` a good container entry point. Elsewhere the application runs
369
+ as a child process: `SIGINT`, `SIGTERM` and `SIGHUP` are forwarded and its exit code is returned
370
+ (`128 + n` when it is killed by a signal).
371
+
372
+ | Option | Default | Description |
373
+ | ----------------------------- | ------------------------------- | ----------------------------------------------- |
374
+ | `--root <dir>` | current directory | Project root (the working directory of the app) |
375
+ | `--entry <file>` | `dist/main.js`, `dist/main.mjs` | File to run |
376
+ | `--runtime <auto\|node\|bun>` | `auto` | Runtime (`auto`: the one running the CLI\*) |
377
+
378
+ \* Node.js under `bun run`, see [`znight dev`](#znight-dev).
379
+
380
+ ## `znight openapi`
381
+
382
+ ```sh
383
+ znight openapi # writes openapi.json at the project root
384
+ znight openapi -o docs/openapi.yaml # YAML, chosen by the extension (needs yaml)
385
+ znight openapi --check # CI: exit 1 when the file is missing or stale; never writes
386
+ znight openapi --prefix /api # servers default when @OpenApiDocument declares none
387
+ znight openapi --runtime bun # load the modules with Bun (default: the runtime running the CLI)
388
+ ```
389
+
390
+ Writes the OpenAPI document of the application with `@znight/openapi` (which must be installed in
391
+ the project). The application is compiled like `znight generate` does (compile errors are printed
392
+ and exit with code 1 before any module runs), then its generated module is loaded in a child
393
+ process (Bun: the compiler preload; Node.js: a Vite server in middleware mode), so module side
394
+ effects never reach the CLI. Nothing is instantiated. Outputs are only written when they change;
395
+ the file is byte-identical on Node.js and Bun. Commit it and run `znight openapi --check` in CI: an
396
+ API change shows up in the pull request.
397
+
398
+ | Option | Default | Description |
399
+ | ------------------------------------------ | ----------------- | ------------------------------------------------------------------ |
400
+ | `-o, --output <file>` | `openapi.json` | Output file, repeatable (`.yaml` / `.yml` write YAML) |
401
+ | `--check` | `false` | Do not write: exit with code 1 when an output is missing or stale |
402
+ | `--prefix <path>` | none | HTTP prefix: the `servers` default (`[{ url: prefix }]`) |
403
+ | `--runtime <auto\|node\|bun>` | `auto` | Runtime loading the application modules |
404
+ | `--converters <file>` | none | Module whose default export is a `SchemaConverter[]` |
405
+ | `--root <dir>` | current directory | Project root |
406
+ | `--config <file>` | `vite.config.*` | Vite config file (compiler options and the Node.js module loading) |
407
+ | `--config-loader <bundle\|runner\|native>` | Vite's | How Vite loads the config file |
408
+
409
+ ## Global options, exit codes, environment
410
+
411
+ | Option | Description |
412
+ | ----------------------- | ------------------------------------------------- |
413
+ | `-h, --help` | Help (`znight --help`, `znight <command> --help`) |
414
+ | `-v, --version` | Version |
415
+ | `--color`, `--no-color` | Force or disable colors |
416
+ | `--debug` | Print the stack trace of unexpected errors |
417
+
418
+ | Exit code | Meaning |
419
+ | --------- | ------------------------------------------------------------------------------------------- |
420
+ | `0` | Success |
421
+ | `1` | Failure: compiler errors, failed build, stale generated file, missing build, file exists... |
422
+ | `2` | Invalid usage: unknown command or option (with a suggestion), missing or invalid argument |
423
+ | `130` | Cancelled (prompt cancelled, second `Ctrl+C` in `znight dev`) |
424
+ | other | `znight start`: the exit code of the application |
425
+
426
+ | Variable | Effect |
427
+ | ----------------------- | ----------------------------------------------------- |
428
+ | `NO_COLOR` | Disables colors (any non-empty value) |
429
+ | `FORCE_COLOR` | Forces colors (`0` / `false` disable them) |
430
+ | `ZNIGHT_DEBUG=1` | Same as `--debug` |
431
+ | `CI` | No prompts (`znight new` uses the flags and defaults) |
432
+ | `npm_config_user_agent` | Default package manager of `znight new` |
433
+
434
+ ## Programmatic API
435
+
436
+ ```ts
437
+ import { main, planSchematic, renderProject, SCHEMATICS } from '@znight/cli'
438
+
439
+ const code = await main(['generate', '--check'], { cwd: '/path/to/app' }) // exit code, never throws
440
+
441
+ const files = renderProject({
442
+ name: 'my-service',
443
+ packageManager: 'bun',
444
+ adapter: 'fastify',
445
+ features: ['config', 'observability'],
446
+ znightVersion: '0.1.0',
447
+ }) // [{ path, content }]
448
+ ```
449
+
450
+ `main(argv, options)` accepts overrides for everything it touches (`cwd`, `env`, `stdout`, `stderr`,
451
+ `interactive`, `signals`, and `services`: `loadCompiler`, `loadVite`, `loadPrompts`, `processes`,
452
+ `watch`), which is how the CLI itself is tested. `AppSupervisor` (the restart logic of `znight dev`)
453
+ and the schematics are exported too.
454
+
455
+ ## Compiler contract
456
+
457
+ The CLI drives the project's `@znight/compiler` through its programmatic API (spec §6.2) and only
458
+ relies on these shapes (each is optional; missing pieces are emulated):
459
+
460
+ | Export | Used for |
461
+ | ---------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
462
+ | `generate(options)` | `znight generate`; `written: string[]` tells whether the file changed |
463
+ | `generate({ ...options, check })` | `znight generate --check`; `stale: boolean`, `staleFiles: string[]` (else compared with `diskCode` / `library`) |
464
+ | `createCompiler(options)` | `znight generate --watch`: `generate(changes)` (`{ changed, added, removed }`), `isOutFile(file)`, `watchFiles` |
465
+ | `includeRoots(compiler)` | `--watch`: the directories to watch recursively (else the whole project) |
466
+ | `isRelevantChange(compiler, kind, file, watched, roots)` | `--watch`: the file events that trigger a compile (else every event but the generated module) |
467
+ | `compile(options)` | fallback of the above (writes `diskCode`, else `code`) |
468
+ | `formatDiagnostics(diagnostics, { color, cwd, summary: false })` | diagnostics output (a plain formatter otherwise) |
469
+ | `loadZnightConfig(root)` | `znight generate`: `{ file, options }` of `znight.config.*` / `ZNIGHT_CONFIG` |
470
+
471
+ `znight build` and `znight dev` use the `znight()` Vite plugin of the project's Vite config, found by
472
+ its name (`znight`) among the plugins of the config:
473
+
474
+ - `znight generate` reads `plugin.api.options` (or `plugin.api.getOptions()`) when the project has
475
+ no `znight.config.*`, with the `root`, `aliases` and `runtimeConditions` of the plugin's compiler
476
+ (`plugin.api.compiler.options`, created when Vite resolves the config; without it, the Vite root
477
+ and the string aliases of `resolve.alias`);
478
+ - `znight dev` reads `plugin.api.result` (`{ ok, diagnostics }`) once a build is written, to keep the
479
+ last good process running when the compilation failed.
480
+
481
+ ## License
482
+
483
+ MIT
package/bin/znight.mjs ADDED
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ // Launcher of the `znight` binary. It is committed (unlike dist/) so that package managers link the
3
+ // binary at install time, even in this repository before the packages are built.
4
+ import { existsSync } from 'node:fs'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ const cli = new URL('../dist/cli.mjs', import.meta.url)
8
+
9
+ if (!existsSync(fileURLToPath(cli))) {
10
+ console.error(
11
+ '@znight/cli is not built: dist/cli.mjs is missing.\n' +
12
+ 'In the Znight repository, build the packages first: bun install && bun run build',
13
+ )
14
+ process.exit(1)
15
+ }
16
+
17
+ await import(cli.href)
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ export {}
package/dist/cli.mjs ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import { n as main } from "./main-BWsiUsxN.mjs";
3
+ // src/cli.ts
4
+ /**
5
+ * The `znight` binary. Everything lives in `main()`; this file only wires the process: arguments,
6
+ * exit code, and a safety net for handles left open by third-party code (prompts, watchers).
7
+ */
8
+ const code = await main(process.argv.slice(2));
9
+ process.exitCode = code;
10
+ setTimeout(() => process.exit(code), 1e3).unref();
11
+ export {};
12
+
13
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * The `znight` binary. Everything lives in `main()`; this file only wires the process: arguments,\n * exit code, and a safety net for handles left open by third-party code (prompts, watchers).\n */\nimport { main } from './main.ts'\n\nconst code = await main(process.argv.slice(2))\nprocess.exitCode = code\n// let pending output flush, then exit even if a library left a handle open\nsetTimeout(() => process.exit(code), 1000).unref()\n"],"mappings":";;;;;;;AAOA,MAAM,OAAO,MAAM,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC;AAC7C,QAAQ,WAAW;AAEnB,iBAAiB,QAAQ,KAAK,IAAI,GAAG,GAAI,CAAC,CAAC,MAAM"}
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,47 @@
1
+ import { r as parseOrphanGuard, t as ORPHAN_GUARD_ENV } from "../orphan-Dn-2xMBd.mjs";
2
+ // src/dev/orphan-guard.ts
3
+ /**
4
+ * Preloaded into the application by `znight dev` (see `orphan.ts`): stops the application when the
5
+ * CLI that started it is gone without stopping it (`kill -9`, the OOM killer).
6
+ *
7
+ * Configured by `ZNIGHT_DEV_PARENT` (removed from the environment right away: the processes the
8
+ * application starts, `fork()` included, are left alone), and only in the process the CLI started
9
+ * (whose parent is the CLI). Every poll interval it checks that it was not re-parented and that the
10
+ * CLI still exists (an unref'd timer: it never keeps the application alive). Once the CLI is gone,
11
+ * the application gets SIGTERM (its process group when it leads one, as the CLI would send it), so
12
+ * it stops gracefully, then SIGKILL after the kill timeout.
13
+ */
14
+ const settings = parseOrphanGuard(process.env[ORPHAN_GUARD_ENV]);
15
+ Reflect.deleteProperty(process.env, ORPHAN_GUARD_ENV);
16
+ if (settings !== void 0 && process.ppid === settings.parentPid) {
17
+ const { parentPid, pollMs, killTimeoutMs } = settings;
18
+ const poll = setInterval(() => {
19
+ if (process.ppid === parentPid && isAlive(parentPid)) return;
20
+ clearInterval(poll);
21
+ setTimeout(() => signal("SIGKILL"), killTimeoutMs).unref();
22
+ signal("SIGTERM");
23
+ }, pollMs);
24
+ poll.unref();
25
+ }
26
+ /** Whether `pid` exists (EPERM: it does, but is not the CLI, which runs as the same user). */
27
+ function isAlive(pid) {
28
+ try {
29
+ process.kill(pid, 0);
30
+ return true;
31
+ } catch {
32
+ return false;
33
+ }
34
+ }
35
+ /** Signals the process group of the application (it leads one when detached), else the process. */
36
+ function signal(name) {
37
+ try {
38
+ if (process.platform !== "win32") {
39
+ process.kill(-process.pid, name);
40
+ return;
41
+ }
42
+ } catch {}
43
+ process.kill(process.pid, name);
44
+ }
45
+ export {};
46
+
47
+ //# sourceMappingURL=orphan-guard.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orphan-guard.mjs","names":[],"sources":["../../src/dev/orphan-guard.ts"],"sourcesContent":["/**\n * Preloaded into the application by `znight dev` (see `orphan.ts`): stops the application when the\n * CLI that started it is gone without stopping it (`kill -9`, the OOM killer).\n *\n * Configured by `ZNIGHT_DEV_PARENT` (removed from the environment right away: the processes the\n * application starts, `fork()` included, are left alone), and only in the process the CLI started\n * (whose parent is the CLI). Every poll interval it checks that it was not re-parented and that the\n * CLI still exists (an unref'd timer: it never keeps the application alive). Once the CLI is gone,\n * the application gets SIGTERM (its process group when it leads one, as the CLI would send it), so\n * it stops gracefully, then SIGKILL after the kill timeout.\n */\nimport { ORPHAN_GUARD_ENV, parseOrphanGuard } from './orphan.ts'\n\nconst settings = parseOrphanGuard(process.env[ORPHAN_GUARD_ENV])\nReflect.deleteProperty(process.env, ORPHAN_GUARD_ENV)\n\nif (settings !== undefined && process.ppid === settings.parentPid) {\n const { parentPid, pollMs, killTimeoutMs } = settings\n const poll = setInterval(() => {\n if (process.ppid === parentPid && isAlive(parentPid)) return\n clearInterval(poll)\n setTimeout(() => signal('SIGKILL'), killTimeoutMs).unref()\n signal('SIGTERM')\n }, pollMs)\n poll.unref()\n}\n\n/** Whether `pid` exists (EPERM: it does, but is not the CLI, which runs as the same user). */\nfunction isAlive(pid: number): boolean {\n try {\n process.kill(pid, 0)\n return true\n } catch {\n return false\n }\n}\n\n/** Signals the process group of the application (it leads one when detached), else the process. */\nfunction signal(name: NodeJS.Signals): void {\n try {\n if (process.platform !== 'win32') {\n process.kill(-process.pid, name)\n return\n }\n } catch {\n // not a group leader\n }\n process.kill(process.pid, name)\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAM,WAAW,iBAAiB,QAAQ,IAAI,iBAAiB;AAC/D,QAAQ,eAAe,QAAQ,KAAK,gBAAgB;AAEpD,IAAI,aAAa,KAAA,KAAa,QAAQ,SAAS,SAAS,WAAW;CACjE,MAAM,EAAE,WAAW,QAAQ,kBAAkB;CAC7C,MAAM,OAAO,kBAAkB;EAC7B,IAAI,QAAQ,SAAS,aAAa,QAAQ,SAAS,GAAG;EACtD,cAAc,IAAI;EAClB,iBAAiB,OAAO,SAAS,GAAG,aAAa,CAAC,CAAC,MAAM;EACzD,OAAO,SAAS;CAClB,GAAG,MAAM;CACT,KAAK,MAAM;AACb;;AAGA,SAAS,QAAQ,KAAsB;CACrC,IAAI;EACF,QAAQ,KAAK,KAAK,CAAC;EACnB,OAAO;CACT,QAAQ;EACN,OAAO;CACT;AACF;;AAGA,SAAS,OAAO,MAA4B;CAC1C,IAAI;EACF,IAAI,QAAQ,aAAa,SAAS;GAChC,QAAQ,KAAK,CAAC,QAAQ,KAAK,IAAI;GAC/B;EACF;CACF,QAAQ,CAER;CACA,QAAQ,KAAK,QAAQ,KAAK,IAAI;AAChC"}