@pristine-ts/cli 1.0.439 → 2.0.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 (220) hide show
  1. package/dist/bin/pristine.cjs +7 -0
  2. package/dist/lib/cjs/bin.js +15 -1
  3. package/dist/lib/cjs/bin.js.map +1 -1
  4. package/dist/lib/cjs/bootstrap/app-module-loader.js +321 -0
  5. package/dist/lib/cjs/bootstrap/app-module-loader.js.map +1 -0
  6. package/dist/lib/cjs/bootstrap/bootstrap.js +31 -0
  7. package/dist/lib/cjs/bootstrap/bootstrap.js.map +1 -0
  8. package/dist/lib/cjs/bootstrap/build-manifest-checker.js +67 -0
  9. package/dist/lib/cjs/bootstrap/build-manifest-checker.js.map +1 -0
  10. package/dist/lib/cjs/bootstrap/build-manifest-reader.js +44 -0
  11. package/dist/lib/cjs/bootstrap/build-manifest-reader.js.map +1 -0
  12. package/dist/lib/cjs/bootstrap/build-manifest-staleness.enum.js +23 -0
  13. package/dist/lib/cjs/bootstrap/build-manifest-staleness.enum.js.map +1 -0
  14. package/dist/lib/cjs/bootstrap/build-manifest-writer.js +55 -0
  15. package/dist/lib/cjs/bootstrap/build-manifest-writer.js.map +1 -0
  16. package/dist/lib/cjs/bootstrap/build-manifest.js +31 -0
  17. package/dist/lib/cjs/bootstrap/build-manifest.js.map +1 -0
  18. package/dist/lib/cjs/bootstrap/build-runner.js +44 -0
  19. package/dist/lib/cjs/bootstrap/build-runner.js.map +1 -0
  20. package/dist/lib/cjs/bootstrap/build-staleness-prompt.js +90 -0
  21. package/dist/lib/cjs/bootstrap/build-staleness-prompt.js.map +1 -0
  22. package/dist/lib/cjs/bootstrap/dynamic-importer.js +43 -0
  23. package/dist/lib/cjs/bootstrap/dynamic-importer.js.map +1 -0
  24. package/dist/lib/cjs/bootstrap/init-prompt.js +127 -0
  25. package/dist/lib/cjs/bootstrap/init-prompt.js.map +1 -0
  26. package/dist/lib/cjs/bootstrap/loaded-app-module.js +29 -0
  27. package/dist/lib/cjs/bootstrap/loaded-app-module.js.map +1 -0
  28. package/dist/lib/cjs/bootstrap/loaded-plugin.js +22 -0
  29. package/dist/lib/cjs/bootstrap/loaded-plugin.js.map +1 -0
  30. package/dist/lib/cjs/bootstrap/plugin-loader.js +149 -0
  31. package/dist/lib/cjs/bootstrap/plugin-loader.js.map +1 -0
  32. package/dist/lib/cjs/bootstrap/source-hasher.js +41 -0
  33. package/dist/lib/cjs/bootstrap/source-hasher.js.map +1 -0
  34. package/dist/lib/cjs/cli.js +94 -111
  35. package/dist/lib/cjs/cli.js.map +1 -1
  36. package/dist/lib/cjs/cli.module.js +9 -1
  37. package/dist/lib/cjs/cli.module.js.map +1 -1
  38. package/dist/lib/cjs/commands/build-alias.command.js +50 -0
  39. package/dist/lib/cjs/commands/build-alias.command.js.map +1 -0
  40. package/dist/lib/cjs/commands/build.command.js +173 -0
  41. package/dist/lib/cjs/commands/build.command.js.map +1 -0
  42. package/dist/lib/cjs/commands/commands.js +15 -0
  43. package/dist/lib/cjs/commands/commands.js.map +1 -1
  44. package/dist/lib/cjs/commands/config-print.command.js +75 -0
  45. package/dist/lib/cjs/commands/config-print.command.js.map +1 -0
  46. package/dist/lib/cjs/commands/help-alias.command.js +52 -0
  47. package/dist/lib/cjs/commands/help-alias.command.js.map +1 -0
  48. package/dist/lib/cjs/commands/help.command.js +44 -10
  49. package/dist/lib/cjs/commands/help.command.js.map +1 -1
  50. package/dist/lib/cjs/commands/info-alias.command.js +50 -0
  51. package/dist/lib/cjs/commands/info-alias.command.js.map +1 -0
  52. package/dist/lib/cjs/commands/info.command.js +162 -0
  53. package/dist/lib/cjs/commands/info.command.js.map +1 -0
  54. package/dist/lib/cjs/commands/init-alias.command.js +51 -0
  55. package/dist/lib/cjs/commands/init-alias.command.js.map +1 -0
  56. package/dist/lib/cjs/commands/init.command-options.js +53 -0
  57. package/dist/lib/cjs/commands/init.command-options.js.map +1 -0
  58. package/dist/lib/cjs/commands/init.command.js +249 -0
  59. package/dist/lib/cjs/commands/init.command.js.map +1 -0
  60. package/dist/lib/cjs/commands/list-alias.command.js +50 -0
  61. package/dist/lib/cjs/commands/list-alias.command.js.map +1 -0
  62. package/dist/lib/cjs/commands/list.command.js +7 -1
  63. package/dist/lib/cjs/commands/list.command.js.map +1 -1
  64. package/dist/lib/cjs/commands/start-alias.command.js +51 -0
  65. package/dist/lib/cjs/commands/start-alias.command.js.map +1 -0
  66. package/dist/lib/cjs/commands/start.command-options.js +34 -0
  67. package/dist/lib/cjs/commands/start.command-options.js.map +1 -0
  68. package/dist/lib/cjs/commands/start.command.js +169 -0
  69. package/dist/lib/cjs/commands/start.command.js.map +1 -0
  70. package/dist/lib/cjs/commands/verify-alias.command.js +50 -0
  71. package/dist/lib/cjs/commands/verify-alias.command.js.map +1 -0
  72. package/dist/lib/cjs/commands/verify.command.js +71 -0
  73. package/dist/lib/cjs/commands/verify.command.js.map +1 -0
  74. package/dist/lib/cjs/config/config-loader.js +136 -0
  75. package/dist/lib/cjs/config/config-loader.js.map +1 -0
  76. package/dist/lib/cjs/config/config-provenance.enum.js +15 -0
  77. package/dist/lib/cjs/config/config-provenance.enum.js.map +1 -0
  78. package/dist/lib/cjs/config/config.js +22 -0
  79. package/dist/lib/cjs/config/config.js.map +1 -0
  80. package/dist/lib/cjs/config/define-config.js +18 -0
  81. package/dist/lib/cjs/config/define-config.js.map +1 -0
  82. package/dist/lib/cjs/config/pristine-config.interface.js +3 -0
  83. package/dist/lib/cjs/config/pristine-config.interface.js.map +1 -0
  84. package/dist/lib/cjs/config/resolved-pristine-config.js +25 -0
  85. package/dist/lib/cjs/config/resolved-pristine-config.js.map +1 -0
  86. package/dist/lib/cjs/event-handlers/cli.event-handler.js +69 -48
  87. package/dist/lib/cjs/event-handlers/cli.event-handler.js.map +1 -1
  88. package/dist/lib/cjs/tsconfig.cjs.tsbuildinfo +1 -0
  89. package/dist/lib/esm/bin.js +15 -1
  90. package/dist/lib/esm/bin.js.map +1 -1
  91. package/dist/lib/esm/bootstrap/app-module-loader.js +315 -0
  92. package/dist/lib/esm/bootstrap/app-module-loader.js.map +1 -0
  93. package/dist/lib/esm/bootstrap/bootstrap.js +15 -0
  94. package/dist/lib/esm/bootstrap/bootstrap.js.map +1 -0
  95. package/dist/lib/esm/bootstrap/build-manifest-checker.js +61 -0
  96. package/dist/lib/esm/bootstrap/build-manifest-checker.js.map +1 -0
  97. package/dist/lib/esm/bootstrap/build-manifest-reader.js +38 -0
  98. package/dist/lib/esm/bootstrap/build-manifest-reader.js.map +1 -0
  99. package/dist/lib/esm/bootstrap/build-manifest-staleness.enum.js +20 -0
  100. package/dist/lib/esm/bootstrap/build-manifest-staleness.enum.js.map +1 -0
  101. package/dist/lib/esm/bootstrap/build-manifest-writer.js +49 -0
  102. package/dist/lib/esm/bootstrap/build-manifest-writer.js.map +1 -0
  103. package/dist/lib/esm/bootstrap/build-manifest.js +27 -0
  104. package/dist/lib/esm/bootstrap/build-manifest.js.map +1 -0
  105. package/dist/lib/esm/bootstrap/build-runner.js +41 -0
  106. package/dist/lib/esm/bootstrap/build-runner.js.map +1 -0
  107. package/dist/lib/esm/bootstrap/build-staleness-prompt.js +87 -0
  108. package/dist/lib/esm/bootstrap/build-staleness-prompt.js.map +1 -0
  109. package/dist/lib/esm/bootstrap/dynamic-importer.js +40 -0
  110. package/dist/lib/esm/bootstrap/dynamic-importer.js.map +1 -0
  111. package/dist/lib/esm/bootstrap/init-prompt.js +124 -0
  112. package/dist/lib/esm/bootstrap/init-prompt.js.map +1 -0
  113. package/dist/lib/esm/bootstrap/loaded-app-module.js +25 -0
  114. package/dist/lib/esm/bootstrap/loaded-app-module.js.map +1 -0
  115. package/dist/lib/esm/bootstrap/loaded-plugin.js +18 -0
  116. package/dist/lib/esm/bootstrap/loaded-plugin.js.map +1 -0
  117. package/dist/lib/esm/bootstrap/plugin-loader.js +143 -0
  118. package/dist/lib/esm/bootstrap/plugin-loader.js.map +1 -0
  119. package/dist/lib/esm/bootstrap/source-hasher.js +35 -0
  120. package/dist/lib/esm/bootstrap/source-hasher.js.map +1 -0
  121. package/dist/lib/esm/cli.js +109 -0
  122. package/dist/lib/esm/cli.js.map +1 -0
  123. package/dist/lib/esm/cli.module.js +7 -0
  124. package/dist/lib/esm/cli.module.js.map +1 -1
  125. package/dist/lib/esm/commands/build-alias.command.js +47 -0
  126. package/dist/lib/esm/commands/build-alias.command.js.map +1 -0
  127. package/dist/lib/esm/commands/build.command.js +167 -0
  128. package/dist/lib/esm/commands/build.command.js.map +1 -0
  129. package/dist/lib/esm/commands/commands.js +15 -0
  130. package/dist/lib/esm/commands/commands.js.map +1 -1
  131. package/dist/lib/esm/commands/config-print.command.js +69 -0
  132. package/dist/lib/esm/commands/config-print.command.js.map +1 -0
  133. package/dist/lib/esm/commands/help-alias.command.js +49 -0
  134. package/dist/lib/esm/commands/help-alias.command.js.map +1 -0
  135. package/dist/lib/esm/commands/help.command.js +44 -10
  136. package/dist/lib/esm/commands/help.command.js.map +1 -1
  137. package/dist/lib/esm/commands/info-alias.command.js +47 -0
  138. package/dist/lib/esm/commands/info-alias.command.js.map +1 -0
  139. package/dist/lib/esm/commands/info.command.js +156 -0
  140. package/dist/lib/esm/commands/info.command.js.map +1 -0
  141. package/dist/lib/esm/commands/init-alias.command.js +48 -0
  142. package/dist/lib/esm/commands/init-alias.command.js.map +1 -0
  143. package/dist/lib/esm/commands/init.command-options.js +49 -0
  144. package/dist/lib/esm/commands/init.command-options.js.map +1 -0
  145. package/dist/lib/esm/commands/init.command.js +243 -0
  146. package/dist/lib/esm/commands/init.command.js.map +1 -0
  147. package/dist/lib/esm/commands/list-alias.command.js +47 -0
  148. package/dist/lib/esm/commands/list-alias.command.js.map +1 -0
  149. package/dist/lib/esm/commands/list.command.js +8 -2
  150. package/dist/lib/esm/commands/list.command.js.map +1 -1
  151. package/dist/lib/esm/commands/start-alias.command.js +48 -0
  152. package/dist/lib/esm/commands/start-alias.command.js.map +1 -0
  153. package/dist/lib/esm/commands/start.command-options.js +30 -0
  154. package/dist/lib/esm/commands/start.command-options.js.map +1 -0
  155. package/dist/lib/esm/commands/start.command.js +166 -0
  156. package/dist/lib/esm/commands/start.command.js.map +1 -0
  157. package/dist/lib/esm/commands/verify-alias.command.js +47 -0
  158. package/dist/lib/esm/commands/verify-alias.command.js.map +1 -0
  159. package/dist/lib/esm/commands/verify.command.js +68 -0
  160. package/dist/lib/esm/commands/verify.command.js.map +1 -0
  161. package/dist/lib/esm/config/config-loader.js +130 -0
  162. package/dist/lib/esm/config/config-loader.js.map +1 -0
  163. package/dist/lib/esm/config/config-provenance.enum.js +12 -0
  164. package/dist/lib/esm/config/config-provenance.enum.js.map +1 -0
  165. package/dist/lib/esm/config/config.js +6 -0
  166. package/dist/lib/esm/config/config.js.map +1 -0
  167. package/dist/lib/esm/config/define-config.js +14 -0
  168. package/dist/lib/esm/config/define-config.js.map +1 -0
  169. package/dist/lib/esm/config/pristine-config.interface.js +2 -0
  170. package/dist/lib/esm/config/pristine-config.interface.js.map +1 -0
  171. package/dist/lib/esm/config/resolved-pristine-config.js +21 -0
  172. package/dist/lib/esm/config/resolved-pristine-config.js.map +1 -0
  173. package/dist/lib/esm/event-handlers/cli.event-handler.js +69 -48
  174. package/dist/lib/esm/event-handlers/cli.event-handler.js.map +1 -1
  175. package/dist/lib/esm/tsconfig.tsbuildinfo +1 -0
  176. package/dist/types/bootstrap/app-module-loader.d.ts +83 -0
  177. package/dist/types/bootstrap/bootstrap.d.ts +14 -0
  178. package/dist/types/bootstrap/build-manifest-checker.d.ts +19 -0
  179. package/dist/types/bootstrap/build-manifest-reader.d.ts +14 -0
  180. package/dist/types/bootstrap/build-manifest-staleness.enum.d.ts +18 -0
  181. package/dist/types/bootstrap/build-manifest-writer.d.ts +19 -0
  182. package/dist/types/bootstrap/build-manifest.d.ts +29 -0
  183. package/dist/types/bootstrap/build-runner.d.ts +16 -0
  184. package/dist/types/bootstrap/build-staleness-prompt.d.ts +27 -0
  185. package/dist/types/bootstrap/dynamic-importer.d.ts +13 -0
  186. package/dist/types/bootstrap/init-prompt.d.ts +38 -0
  187. package/dist/types/bootstrap/loaded-app-module.d.ts +40 -0
  188. package/dist/types/bootstrap/loaded-plugin.d.ts +20 -0
  189. package/dist/types/bootstrap/plugin-loader.d.ts +35 -0
  190. package/dist/types/bootstrap/source-hasher.d.ts +15 -0
  191. package/dist/types/cli.d.ts +12 -0
  192. package/dist/types/cli.module.d.ts +3 -0
  193. package/dist/types/commands/build-alias.command.d.ts +15 -0
  194. package/dist/types/commands/build.command.d.ts +47 -0
  195. package/dist/types/commands/commands.d.ts +15 -0
  196. package/dist/types/commands/config-print.command.d.ts +18 -0
  197. package/dist/types/commands/help-alias.command.d.ts +17 -0
  198. package/dist/types/commands/help.command.d.ts +19 -1
  199. package/dist/types/commands/info-alias.command.d.ts +15 -0
  200. package/dist/types/commands/info.command.d.ts +41 -0
  201. package/dist/types/commands/init-alias.command.d.ts +16 -0
  202. package/dist/types/commands/init.command-options.d.ts +22 -0
  203. package/dist/types/commands/init.command.d.ts +60 -0
  204. package/dist/types/commands/list-alias.command.d.ts +15 -0
  205. package/dist/types/commands/list.command.d.ts +6 -0
  206. package/dist/types/commands/start-alias.command.d.ts +16 -0
  207. package/dist/types/commands/start.command-options.d.ts +11 -0
  208. package/dist/types/commands/start.command.d.ts +47 -0
  209. package/dist/types/commands/verify-alias.command.d.ts +15 -0
  210. package/dist/types/commands/verify.command.d.ts +24 -0
  211. package/dist/types/config/config-loader.d.ts +40 -0
  212. package/dist/types/config/config-provenance.enum.d.ts +10 -0
  213. package/dist/types/config/config.d.ts +5 -0
  214. package/dist/types/config/define-config.d.ts +14 -0
  215. package/dist/types/config/pristine-config.interface.d.ts +67 -0
  216. package/dist/types/config/resolved-pristine-config.d.ts +28 -0
  217. package/dist/types/event-handlers/cli.event-handler.d.ts +30 -3
  218. package/dist/types/interfaces/command.interface.d.ts +31 -3
  219. package/package.json +16 -12
  220. package/readme.md +1023 -17
package/readme.md CHANGED
@@ -1,33 +1,1039 @@
1
- # CLI module
1
+ # `@pristine-ts/cli`
2
2
 
3
- The CLI Module allows you to create your own Commands by simply implementing the `CommandInterface`.
3
+ The Pristine CLI a `pristine` binary for your project, plus everything you need to add
4
+ your own commands, build and start your app, and verify that your AppModule is healthy.
4
5
 
5
- Then, you tag your class using the `@tag(ServiceDefinitionTagEnum.Command)` decorator.
6
+ If you've used `ng`, `nest`, or `vite`, this is the equivalent for Pristine apps.
6
7
 
7
- You specify the expected arguments using an Options Class that is validated automatically for you using the
8
- `class-validator` library.
8
+ ---
9
9
 
10
- You will need to modify your project's `package.json` by adding this:
10
+ ## Table of contents
11
11
 
12
+ - [Install](#install)
13
+ - [A 5-minute tour](#a-5-minute-tour)
14
+ - [Recipes](#recipes)
15
+ - [Add a command to your app](#recipe-add-a-command-to-your-app)
16
+ - [Build your TypeScript](#recipe-build-your-typescript)
17
+ - [Start your app in production](#recipe-start-your-app-in-production)
18
+ - [Host an HTTP (or HTTPS) server](#recipe-host-an-http-or-https-server)
19
+ - [Verify your AppModule on every CI run](#recipe-verify-your-appmodule-on-every-ci-run)
20
+ - [Pull commands in from a separate package (plugins)](#recipe-pull-commands-in-from-a-separate-package-plugins)
21
+ - [Configuration reference](#configuration-reference)
22
+ - [How `pristine` finds your AppModule](#how-pristine-finds-your-appmodule)
23
+ - [Built-in commands](#built-in-commands)
24
+ - [Production deployment](#production-deployment)
25
+ - [Architecture & design notes](#architecture--design-notes)
26
+ - [Migrating from older versions](#migrating-from-older-versions)
27
+ - [What changed](#what-changed-versus-pre-10440)
28
+
29
+ ---
30
+
31
+ ## Install
32
+
33
+ You have two good options. Pick the one that matches how you'll invoke the CLI.
34
+
35
+ ### Option A — Local install (recommended for project-bound usage)
36
+
37
+ ```sh
38
+ npm install --save-dev @pristine-ts/cli
39
+ ```
40
+
41
+ Use it from your `package.json` scripts — no `npx` needed because npm puts
42
+ `node_modules/.bin/` on PATH for `npm run` invocations:
43
+
44
+ ```json
45
+ {
46
+ "scripts": {
47
+ "build": "pristine build",
48
+ "start": "pristine start",
49
+ "verify": "pristine verify"
50
+ }
51
+ }
52
+ ```
53
+
54
+ ```sh
55
+ npm run build
56
+ npm run start
57
+ ```
58
+
59
+ For one-off invocations from the terminal, use `npx pristine ...`.
60
+
61
+ ### Option B — Global install (if you want bare `pristine` in any terminal)
62
+
63
+ ```sh
64
+ npm install -g @pristine-ts/cli
65
+ pristine list # works in any directory
66
+ ```
67
+
68
+ The bin is self-contained, so the global install pulls everything it needs and `pristine`
69
+ becomes available everywhere. This is the Angular/Nest/Vue CLI pattern.
70
+
71
+ > **Tip**: if you want bare `pristine` in your terminal **without** a global install, add
72
+ > `./node_modules/.bin` to your shell PATH per-project. `direnv` works well:
73
+ > create a `.envrc` with `PATH_add node_modules/.bin`, run `direnv allow`, done.
74
+
75
+ ---
76
+
77
+ ## A 5-minute tour
78
+
79
+ Let's walk through what the CLI looks like in practice. Assume you have a brand-new
80
+ project with just `package.json` and `tsconfig.json`.
81
+
82
+ ### 1. Install the CLI
83
+
84
+ ```sh
85
+ npm install --save-dev @pristine-ts/cli
86
+ ```
87
+
88
+ ### 2. Run `pristine init`
89
+
90
+ ```sh
91
+ npx pristine init
92
+ ```
93
+
94
+ `pristine init` is the canonical setup command. It interactively (or via flags in CI):
95
+
96
+ - Asks where your AppModule source file lives (default: `src/app.module.ts`)
97
+ - Asks where the compiled output should land (default: `dist/app.module.js`)
98
+ - Asks which tsconfig to use (default: `tsconfig.json`) and build format (default: `esm`)
99
+ - Writes `pristine.config.ts` with both `sourcePath` AND `outputPath` populated
100
+ - Optionally scaffolds a starter AppModule at the source path (only if it doesn't exist)
101
+ - Optionally adds `build`/`start`/`verify` scripts to your `package.json` (only ones that
102
+ don't already exist — never overwritten)
103
+ - Adds `.pristine/` to your `.gitignore` if one is present
104
+
105
+ The generated config:
106
+
107
+ ```ts
108
+ import {defineConfig} from "@pristine-ts/cli";
109
+
110
+ export default defineConfig({
111
+ appModule: {
112
+ sourcePath: "src/app.module.ts",
113
+ outputPath: "dist/app.module.js",
114
+ },
115
+ build: {
116
+ tsconfig: "tsconfig.json",
117
+ format: "esm",
118
+ },
119
+ });
12
120
  ```
13
121
 
14
- ...
15
- "pristine": {
16
- "appModule": {
17
- "cjsPath": "RELATIVE_PATH_TO_YOUR_MODULE_COMPILE_FOR_CJS"
122
+ For non-interactive use:
123
+
124
+ ```sh
125
+ pristine init \
126
+ --source-path=src/app.module.ts \
127
+ --output-path=dist/app.module.js \
128
+ --tsconfig=tsconfig.json \
129
+ --format=esm \
130
+ --scaffold \
131
+ --scripts
132
+ ```
133
+
134
+ ### 3. Build your project
135
+
136
+ ```sh
137
+ npx pristine build
138
+ ```
139
+
140
+ `pristine build` runs `tsc` for you and produces `dist/`.
141
+
142
+ ### 4. See what's loaded
143
+
144
+ ```sh
145
+ npx pristine info
146
+ ```
147
+
148
+ Output looks like:
149
+
150
+ ```
151
+ Pristine CLI
152
+ Version: 1.0.440
153
+ Node: v22.18.0
154
+ Platform: darwin arm64 (24.6.0)
155
+ CWD: /Users/you/projects/my-app
156
+
157
+ Configuration
158
+ Config file: /Users/you/projects/my-app/pristine.config.ts
159
+ AppModule path: dist/app.module.js (from config file)
160
+
161
+ AppModule: my-app
162
+ Imported modules (5):
163
+ - my-app
164
+ - pristine.cli
165
+ - pristine.common
166
+ - pristine.core
167
+ - pristine.logging
168
+ ```
169
+
170
+ ### 5. Verify your AppModule boots cleanly
171
+
172
+ ```sh
173
+ npx pristine verify
174
+ ```
175
+
176
+ This actually starts a kernel from your AppModule, captures every phase outcome (module
177
+ registration, config load, after-init, etc.), runs every registered `InstantiationTest`,
178
+ and exits non-zero if anything fails. Drop it in CI to catch boot regressions before they
179
+ ship.
180
+
181
+ ### 6. Run your app
182
+
183
+ ```sh
184
+ npx pristine start
185
+ ```
186
+
187
+ Boots your AppModule, registers SIGTERM/SIGINT handlers, keeps the process alive. If your
188
+ AppModule imports `@pristine-ts/http`, `pristine start` automatically launches an HTTP
189
+ server on `0.0.0.0:3000` (configurable). Send `SIGTERM` and watch graceful shutdown happen.
190
+
191
+ That's the whole loop. The rest of the README is recipes for specific things you'll want
192
+ to do, plus reference material when you need to look something up.
193
+
194
+ ---
195
+
196
+ ## Recipes
197
+
198
+ ### Recipe: Add a command to your app
199
+
200
+ You want to type `pristine sync-products` and have your own code run.
201
+
202
+ **1. Write the command class.** It's an injectable class implementing `CommandInterface`,
203
+ decorated with `@tag(ServiceDefinitionTagEnum.Command)`:
204
+
205
+ ```ts
206
+ // src/commands/sync-products.command.ts
207
+ import {moduleScoped, ServiceDefinitionTagEnum, tag} from "@pristine-ts/common";
208
+ import {injectable} from "tsyringe";
209
+ import {CommandInterface, ConsoleManager, ExitCodeEnum} from "@pristine-ts/cli";
210
+ import {AppModuleKeyname} from "../app.module.keyname";
211
+ import {ProductService} from "../services/product.service";
212
+
213
+ @tag(ServiceDefinitionTagEnum.Command)
214
+ @moduleScoped(AppModuleKeyname)
215
+ @injectable()
216
+ export class SyncProductsCommand implements CommandInterface<null> {
217
+ optionsType = null;
218
+ name = "sync-products";
219
+ description = "Re-sync the local product cache from upstream.";
220
+
221
+ constructor(
222
+ private readonly consoleManager: ConsoleManager,
223
+ private readonly productService: ProductService,
224
+ ) {}
225
+
226
+ async run(): Promise<ExitCodeEnum | number> {
227
+ const count = await this.productService.syncAll();
228
+ this.consoleManager.writeSuccess(`Synced ${count} products.`);
229
+ return ExitCodeEnum.Success;
230
+ }
231
+ }
232
+ ```
233
+
234
+ **2. Make sure your AppModule imports it.** The simplest way: include it in your AppModule's
235
+ `importServices`:
236
+
237
+ ```ts
238
+ // src/app.module.ts
239
+ import {AppModuleInterface} from "@pristine-ts/common";
240
+ import {CliModule} from "@pristine-ts/cli";
241
+ import {CoreModule} from "@pristine-ts/core";
242
+ import {SyncProductsCommand} from "./commands/sync-products.command";
243
+
244
+ export const AppModule: AppModuleInterface = {
245
+ keyname: "my-app",
246
+ importModules: [CoreModule, CliModule],
247
+ importServices: [SyncProductsCommand],
248
+ };
249
+ ```
250
+
251
+ **3. Build and run.**
252
+
253
+ ```sh
254
+ npx pristine build
255
+ npx pristine sync-products
256
+ ```
257
+
258
+ Output:
259
+
260
+ ```
261
+ ✔ Success: Synced 142 products.
262
+ [status:'Success', code:'0'] - Command 'sync-products' exited.
263
+ ```
264
+
265
+ #### With typed CLI flags
266
+
267
+ Need `--limit=100 --dry-run`? Define an options class with `class-validator` decorators:
268
+
269
+ ```ts
270
+ // src/commands/sync-products.command-options.ts
271
+ import "reflect-metadata";
272
+ import {IsBoolean, IsNumber, IsOptional} from "@pristine-ts/class-validator";
273
+
274
+ export class SyncProductsOptions {
275
+ @IsOptional() @IsNumber() limit?: number;
276
+ @IsOptional() @IsBoolean() "dry-run"?: boolean;
277
+ }
278
+ ```
279
+
280
+ Then in the command, set `optionsType` to a fresh instance and read `args` in `run`:
281
+
282
+ ```ts
283
+ @injectable()
284
+ export class SyncProductsCommand implements CommandInterface<SyncProductsOptions> {
285
+ optionsType = new SyncProductsOptions();
286
+ name = "sync-products";
287
+ description = "Re-sync the local product cache from upstream.";
288
+
289
+ async run(args: SyncProductsOptions): Promise<ExitCodeEnum | number> {
290
+ const limit = args.limit ?? Infinity;
291
+ const dryRun = args["dry-run"] === true;
292
+ // ...
293
+ }
294
+ }
295
+ ```
296
+
297
+ `npx pristine sync-products --limit=50 --dry-run` parses, validates, and passes the typed
298
+ options into `run`. Validation failures exit non-zero and print the constraint errors.
299
+
300
+ ---
301
+
302
+ ### Recipe: Build your TypeScript
303
+
304
+ `pristine build` is a `tsc` wrapper that ALSO writes a build manifest at
305
+ `.pristine/build-manifest.json` so downstream commands can detect when the build is
306
+ stale (source edited, output deleted, paths reconfigured).
307
+
308
+ For most projects, the only thing you need is the default config produced by `pristine init`:
309
+
310
+ ```ts
311
+ // pristine.config.ts
312
+ import {defineConfig} from "@pristine-ts/cli";
313
+
314
+ export default defineConfig({
315
+ appModule: {path: "dist/app.module.js"},
316
+ build: {
317
+ outDir: "dist",
318
+ tsconfig: "tsconfig.json",
319
+ format: "esm", // "esm" | "cjs" | "both"
320
+ clean: true, // wipe outDir before each build
321
+ },
322
+ });
323
+ ```
324
+
325
+ ```sh
326
+ npx pristine build
327
+ ```
328
+
329
+ #### Building both ESM and CJS
330
+
331
+ If you publish a library that needs both:
332
+
333
+ ```ts
334
+ build: {
335
+ format: "both", // runs tsconfig.json then tsconfig.cjs.json sequentially
336
+ }
337
+ ```
338
+
339
+ You need a `tsconfig.cjs.json` sibling that targets CommonJS. The CLI looks for it
340
+ automatically when format is `"both"` or `"cjs"`.
341
+
342
+ #### Custom tsconfig path
343
+
344
+ ```ts
345
+ build: {
346
+ tsconfig: "tsconfig.build.json",
347
+ }
348
+ ```
349
+
350
+ #### The build manifest
351
+
352
+ After a successful build, `pristine build` writes
353
+ `<project>/.pristine/build-manifest.json`:
354
+
355
+ ```json
356
+ {
357
+ "appModuleSourcePath": "/abs/path/src/app.module.ts",
358
+ "appModuleOutputPath": "/abs/path/dist/app.module.js",
359
+ "sourceHash": "sha256:...",
360
+ "builtAt": "2026-05-11T00:00:00.000Z"
361
+ }
362
+ ```
363
+
364
+ Every command that loads your AppModule (`pristine start`, `pristine verify`, etc.) reads
365
+ this file to confirm the compiled output matches your current source. If the manifest is
366
+ **stale** (source edited since last build, output deleted, paths reconfigured), the CLI:
367
+
368
+ - **In a TTY**: prints what's stale and prompts: "Run `pristine build` now to refresh? [Y/n]".
369
+ On Yes, runs the build inline and continues. On No, exits.
370
+ - **Non-TTY** (CI, Docker): prints the same explanation and exits non-zero. CI never
371
+ auto-rebuilds — that hides bugs.
372
+
373
+ Examples of stale states and what they mean:
374
+
375
+ | Reason | What happened |
376
+ |--------|---------------|
377
+ | `Missing` | No manifest yet. Run `pristine build`. |
378
+ | `SourcePathChanged` | You edited `appModule.sourcePath` in the config. Rebuild. |
379
+ | `OutputPathChanged` | You edited `appModule.outputPath` in the config. Rebuild. |
380
+ | `SourceContentChanged` | The source file's bytes don't match the hash from the last build. Rebuild. |
381
+ | `OutputMissing` | The compiled file referenced by the manifest is no longer on disk. Rebuild. |
382
+
383
+ The manifest only ships when both `appModule.sourcePath` and `appModule.outputPath` are
384
+ configured (which `pristine init` does for you). Without them, `pristine build` still works
385
+ as a thin `tsc` wrapper but doesn't produce a manifest, and downstream commands skip the
386
+ staleness check.
387
+
388
+ ---
389
+
390
+ ### Recipe: Start your app in production
391
+
392
+ `pristine start` is a real production entry point — boots your AppModule, runs every
393
+ registered `RuntimeServer` (HTTP, etc.), handles SIGTERM/SIGINT with graceful shutdown.
394
+
395
+ #### The simplest case
396
+
397
+ ```sh
398
+ npx pristine start
399
+ ```
400
+
401
+ If your AppModule has no HTTP/queue/etc. modules imported, this just boots the kernel and
402
+ waits for a signal. Useful as a worker-style entry that consumes events through other
403
+ mechanisms (cron, CLI args, etc.).
404
+
405
+ #### With graceful shutdown
406
+
407
+ Add an `onShutdown` hook to your modules to release resources cleanly when the process
408
+ gets SIGTERM:
409
+
410
+ ```ts
411
+ import {ModuleInterface} from "@pristine-ts/common";
412
+
413
+ export const DatabaseModule: ModuleInterface = {
414
+ keyname: "my-app.database",
415
+ // ... onInit, providerRegistrations, etc.
416
+ onShutdown: async (container) => {
417
+ const pool = container.resolve(DatabaseConnectionPool);
418
+ await pool.drain(); // wait for in-flight queries
419
+ await pool.close(); // release sockets
420
+ },
421
+ };
422
+ ```
423
+
424
+ When `pristine start` receives SIGTERM:
425
+ 1. Signal handler fires.
426
+ 2. `Kernel.stop()` walks every imported module's `onShutdown` in outer-to-inner order
427
+ (your AppModule first, leaf dependencies last) so a higher-level module can still call
428
+ into its dependencies during teardown.
429
+ 3. Each hook gets a 10-second timeout. Misbehaving hooks log a warning and shutdown
430
+ continues.
431
+ 4. After all hooks complete, the process exits 0.
432
+ 5. If shutdown takes longer than 30 seconds total, the process is force-killed (so
433
+ Kubernetes / ECS / systemd are never stuck waiting).
434
+
435
+ A second SIGTERM/SIGINT during shutdown bypasses the rest of the wait and exits
436
+ immediately with code 130.
437
+
438
+ #### In a Dockerfile
439
+
440
+ ```dockerfile
441
+ FROM node:22-slim
442
+ WORKDIR /app
443
+ COPY package*.json ./
444
+ RUN npm ci --omit=dev
445
+ RUN npm install -g @pristine-ts/cli
446
+ COPY dist/ ./dist/
447
+ COPY pristine.config.js ./ # if your config file is .ts, compile it or use .js
448
+ CMD ["pristine", "start"]
449
+ ```
450
+
451
+ `pristine start` is the canonical container entry. If you'd rather use bare Node, that
452
+ also works: `CMD ["node", "dist/main.js"]`. Both are supported; `pristine start` adds
453
+ graceful-shutdown wiring you'd otherwise have to write yourself.
454
+
455
+ ---
456
+
457
+ ### Recipe: Host an HTTP (or HTTPS) server
458
+
459
+ If your AppModule imports `@pristine-ts/http`, `pristine start` automatically launches the
460
+ built-in `KernelHttpServer`. Every incoming request goes through `kernel.handle()` →
461
+ the `@pristine-ts/networking` `Router` → your controllers. No glue code needed.
462
+
463
+ **1. Set up the AppModule.**
464
+
465
+ ```ts
466
+ // src/app.module.ts
467
+ import {AppModuleInterface} from "@pristine-ts/common";
468
+ import {CoreModule} from "@pristine-ts/core";
469
+ import {HttpModule} from "@pristine-ts/http";
470
+ import {NetworkingModule} from "@pristine-ts/networking";
471
+ import {DogsController} from "./controllers/dogs.controller";
472
+
473
+ export const AppModule: AppModuleInterface = {
474
+ keyname: "my-app",
475
+ importModules: [CoreModule, HttpModule, NetworkingModule],
476
+ importServices: [DogsController],
477
+ };
478
+ ```
479
+
480
+ **2. Write a controller.** Standard Pristine — nothing CLI-specific:
481
+
482
+ ```ts
483
+ // src/controllers/dogs.controller.ts
484
+ import {injectable} from "tsyringe";
485
+ import {controller, HttpMethod, route} from "@pristine-ts/networking";
486
+
487
+ @injectable()
488
+ @controller("/dogs")
489
+ export class DogsController {
490
+ @route(HttpMethod.Get, "")
491
+ list() {
492
+ return [{name: "Peach"}, {name: "Banjo"}];
493
+ }
494
+ }
495
+ ```
496
+
497
+ **3. Build and start.**
498
+
499
+ ```sh
500
+ npx pristine build
501
+ npx pristine start
502
+ # Pristine app running with 1 server(s): http. Send SIGTERM (or Ctrl+C) to stop.
503
+
504
+ curl http://localhost:3000/dogs
505
+ # [{"name":"Peach"},{"name":"Banjo"}]
506
+ ```
507
+
508
+ #### Customizing port and address
509
+
510
+ Three layers, highest priority first. Use whichever fits.
511
+
512
+ ```sh
513
+ # CLI flag — one-off override
514
+ npx pristine start --port=4000 --address=127.0.0.1
515
+ ```
516
+
517
+ ```ts
518
+ // Config file — project-level default
519
+ export default defineConfig({
520
+ appModule: {path: "dist/app.module.js"},
521
+ kernelConfiguration: {
522
+ "pristine.http.kernel-server.port": 4000,
523
+ "pristine.http.kernel-server.address": "127.0.0.1",
524
+ },
525
+ });
526
+ ```
527
+
528
+ ```sh
529
+ # Environment variable — deploy-time override
530
+ PRISTINE_HTTP_KERNEL_SERVER_PORT=4000 \
531
+ PRISTINE_HTTP_KERNEL_SERVER_ADDRESS=0.0.0.0 \
532
+ pristine start
533
+ ```
534
+
535
+ Defaults: `0.0.0.0:3000`.
536
+
537
+ #### Switching to HTTPS
538
+
539
+ Set the TLS key and cert paths and the server flips from `http.Server` to `https.Server`
540
+ automatically:
541
+
542
+ ```sh
543
+ PRISTINE_HTTP_KERNEL_SERVER_TLS_KEY_PATH=/etc/ssl/key.pem \
544
+ PRISTINE_HTTP_KERNEL_SERVER_TLS_CERT_PATH=/etc/ssl/cert.pem \
545
+ pristine start
546
+ # KernelHttpServer: listening on https://0.0.0.0:3000
547
+ ```
548
+
549
+ Or via config:
550
+
551
+ ```ts
552
+ kernelConfiguration: {
553
+ "pristine.http.kernel-server.tls.key-path": "/etc/ssl/key.pem",
554
+ "pristine.http.kernel-server.tls.cert-path": "/etc/ssl/cert.pem",
555
+ }
556
+ ```
557
+
558
+ When both paths are set to non-empty values, the server reads the PEM files at boot and
559
+ serves HTTPS. The `name` it reports flips from `"http"` to `"https"`.
560
+
561
+ #### Graceful drain
562
+
563
+ When SIGTERM hits, `KernelHttpServer.stop()` is called via `HttpModule.onShutdown` — it
564
+ calls `server.close()` (refuses new connections, lets in-flight requests finish), waits up
565
+ to 10 seconds for connection drain, then force-closes any remaining sockets. Matches what
566
+ container orchestrators expect during a rolling deploy.
567
+
568
+ #### Adding more server types
569
+
570
+ Any module can register a long-running server by implementing `RuntimeServerInterface`
571
+ and tagging it. `pristine start` discovers and launches it alongside the HTTP server:
572
+
573
+ ```ts
574
+ @tag(ServiceDefinitionTagEnum.RuntimeServer)
575
+ @moduleScoped(MyModuleKeyname)
576
+ @injectable()
577
+ export class GrpcServer implements RuntimeServerInterface {
578
+ name = "grpc";
579
+ async start(overrides) { /* ... */ }
580
+ async stop() { /* ... */ }
581
+ }
582
+ ```
583
+
584
+ This is how future gRPC, websocket, or queue-listener modules will plug into `pristine
585
+ start` — no `@pristine-ts/cli` changes required.
586
+
587
+ ---
588
+
589
+ ### Recipe: Verify your AppModule on every CI run
590
+
591
+ `pristine verify` runs a fresh kernel boot of your AppModule on a throw-away kernel,
592
+ captures per-phase outcomes (module registration, config check/load, after-init, etc.),
593
+ and runs every registered `InstantiationTestInterface`. Returns non-zero if anything
594
+ fails. Perfect for CI.
595
+
596
+ #### In your CI pipeline
597
+
598
+ ```yaml
599
+ # .github/workflows/ci.yml
600
+ - run: npm ci
601
+ - run: npm run build
602
+ - run: npx pristine verify
603
+ ```
604
+
605
+ #### Adding your own boot-time health checks
606
+
607
+ Want CI to fail if your DB credentials are wrong? Implement `InstantiationTestInterface`:
608
+
609
+ ```ts
610
+ // src/health/database-connectivity.test.ts
611
+ import {tag, ServiceDefinitionTagEnum} from "@pristine-ts/common";
612
+ import {injectable, DependencyContainer} from "tsyringe";
613
+ import {InstantiationTestInterface, InstantiationTestResultInterface} from "@pristine-ts/core";
614
+ import {DatabaseClient} from "../database/database.client";
615
+
616
+ @tag(ServiceDefinitionTagEnum.InstantiationTest)
617
+ @injectable()
618
+ export class DatabaseConnectivityTest implements InstantiationTestInterface {
619
+ name = "database connectivity";
620
+ description = "Pings the database to confirm credentials and network reachability.";
621
+
622
+ async run(container: DependencyContainer): Promise<InstantiationTestResultInterface> {
623
+ try {
624
+ await container.resolve(DatabaseClient).ping();
625
+ return {passed: true};
626
+ } catch (e) {
627
+ return {passed: false, message: (e as Error).message};
18
628
  }
629
+ }
19
630
  }
20
- ...
631
+ ```
632
+
633
+ Register it the same way you would any service (via `importServices` or by `@tag`
634
+ self-registration). `pristine verify` will discover and run it automatically.
635
+
636
+ To skip the health-test phase (and only verify the boot phases):
21
637
 
638
+ ```sh
639
+ npx pristine verify --skip-tests
22
640
  ```
23
641
 
24
- Then, you add your command to the `package.json` such as:
642
+ ---
643
+
644
+ ### Recipe: Pull commands in from a separate package (plugins)
645
+
646
+ Custom commands can live in their own npm package. Useful for tooling-only commands
647
+ (generators, codemods, linters) you don't want loaded into your runtime AppModule.
25
648
 
649
+ **Plugin author** publishes a package that exports one or more `*Module` symbols:
650
+
651
+ ```ts
652
+ // my-plugin/src/index.ts
653
+ import {ModuleInterface} from "@pristine-ts/common";
654
+ import {moduleScoped, ServiceDefinitionTagEnum, tag} from "@pristine-ts/common";
655
+ import {injectable} from "tsyringe";
656
+ import {CommandInterface, ConsoleManager, ExitCodeEnum} from "@pristine-ts/cli";
657
+
658
+ @tag(ServiceDefinitionTagEnum.Command)
659
+ @moduleScoped("my-plugin")
660
+ @injectable()
661
+ export class HelloCommand implements CommandInterface<null> {
662
+ optionsType = null;
663
+ name = "my-plugin:hello";
664
+ description = "Says hello.";
665
+ constructor(private readonly consoleManager: ConsoleManager) {}
666
+ async run() { this.consoleManager.writeLine("Hello!"); return ExitCodeEnum.Success; }
667
+ }
668
+
669
+ export const MyPluginModule: ModuleInterface = {
670
+ keyname: "my-plugin",
671
+ // No providerRegistrations needed — the @tag decorator self-registers HelloCommand
672
+ // as soon as the file is imported (which happens when this module is loaded).
673
+ };
26
674
  ```
27
- "scripts": {
28
- ...
29
- "cli": "pristine YOUR_COMMAND_HERE YOUR_ARGUMENTS_HERE"
30
- ...
675
+
676
+ **Consumer** installs and declares it:
677
+
678
+ ```sh
679
+ npm install --save-dev my-plugin
680
+ ```
681
+
682
+ ```ts
683
+ // pristine.config.ts
684
+ import {defineConfig} from "@pristine-ts/cli";
685
+
686
+ export default defineConfig({
687
+ appModule: {path: "dist/app.module.js"},
688
+ plugins: [
689
+ "my-plugin",
690
+ // Or: {name: "@my-org/codegen", options: {/* reserved for future use */}},
691
+ ],
692
+ });
693
+ ```
694
+
695
+ ```sh
696
+ npx pristine my-plugin:hello
697
+ # Hello!
698
+ ```
699
+
700
+ Plugins are resolved from the **consumer's** `node_modules` (via `createRequire` anchored
701
+ at the config file's location), so monorepos with hoisted deps work out of the box.
702
+
703
+ #### Failure modes
704
+
705
+ - Missing plugin → clear stderr error, CLI continues without it (built-in commands like
706
+ `pristine p:config:print` still work for debugging).
707
+ - Plugin exports no `*Module` symbols → loud error (silent loading is a footgun).
708
+ - Two commands collide on `name` → stderr warning at boot listing the count, first
709
+ registered match dispatches. Rename one to fix.
710
+
711
+ #### Diagnostics
712
+
713
+ `pristine info` lists every loaded plugin under a dedicated `Plugins (N)` section.
714
+
715
+ ---
716
+
717
+ ## Configuration reference
718
+
719
+ The canonical config file is **`pristine.config.ts`** at your project root.
720
+
721
+ ```ts
722
+ import {defineConfig} from "@pristine-ts/cli";
723
+
724
+ export default defineConfig({
725
+ appModule: {
726
+ path: "dist/app.module.js", // required for non-trivial setups
727
+ export: "AppModule", // default; override only for unusual setups
728
+ },
729
+
730
+ build: {
731
+ outDir: "dist", // tsc's outDir is what actually controls output
732
+ tsconfig: "tsconfig.json",
733
+ format: "esm", // "esm" | "cjs" | "both"
734
+ clean: false, // wipe outDir before each build
31
735
  },
32
736
 
33
- ```
737
+ start: {
738
+ // Reserved for upcoming features (entry, watch, nodeArgs).
739
+ },
740
+
741
+ plugins: [
742
+ "my-plugin",
743
+ {name: "@my-org/codegen"},
744
+ ],
745
+
746
+ kernelConfiguration: {
747
+ // Any configuration value your modules expect — these are passed through to
748
+ // `kernel.start(appModule, kernelConfiguration)` so they take effect during boot.
749
+ "pristine.http.kernel-server.port": 4000,
750
+ "pristine.logging.logSeverityLevelConfiguration": 1,
751
+ },
752
+ });
753
+ ```
754
+
755
+ All fields are optional. The CLI applies sensible defaults wherever a field is absent.
756
+
757
+ ### Supported file formats
758
+
759
+ The CLI looks for these names in order, walking **up** from `process.cwd()` until it
760
+ finds a match (so a CLI invocation from `packages/foo/` in a monorepo finds the root
761
+ config):
762
+
763
+ 1. `pristine.config.ts` — recommended; full IDE autocomplete via `defineConfig`
764
+ 2. `pristine.config.mts`
765
+ 3. `pristine.config.cts`
766
+ 4. `pristine.config.js`
767
+ 5. `pristine.config.mjs`
768
+ 6. `pristine.config.cjs`
769
+
770
+ `.ts` configs load at runtime via `jiti` — no separate compile step needed.
771
+
772
+ ### Inspecting the resolved config
773
+
774
+ ```sh
775
+ npx pristine p:config:print
776
+ ```
777
+
778
+ Prints the loaded config as JSON, plus the file path it came from and per-field
779
+ provenance markers. Use this when discovery is doing something unexpected.
780
+
781
+ ---
782
+
783
+ ## How `pristine` finds your AppModule
784
+
785
+ When the CLI starts, it walks this cascade. The first match wins.
786
+
787
+ ```
788
+ 1. pristine.config.ts → appModule.path
789
+ ↓ (not set?)
790
+ 2. package.json → pristine.appModule.path
791
+ ↓ (deprecated alias: pristine.appModule.cjsPath, prints warning)
792
+ ↓ (not set?)
793
+ 3. .pristine/last-app-module ← cached selection from a previous TTY prompt
794
+ ↓ (not set?)
795
+ 4. Convention scan: dist/, dist/lib/cjs/, dist/lib/esm/, build/, .
796
+ for *.module.{js,mjs,cjs}
797
+ ├── named app.module.* → score 0
798
+ └── exports an AppModule symbol → score 10
799
+ ── one match? → use it
800
+ ── multiple equally-ranked + TTY? → prompt
801
+ ── multiple equally-ranked + no TTY? → exit with actionable error
802
+ ↓ (no candidates?)
803
+ 5. Legacy node_modules/@pristine-ts/* scan (synthetic AppModule)
804
+ ↓ (still nothing?)
805
+ 6. Built-in CliModule fallback (so p:help etc. always work)
806
+ ```
807
+
808
+ If a configured AppModule path can't be loaded (file missing, import error), the CLI
809
+ warns to stderr and falls back to the CliModule fallback. Built-in commands like
810
+ `pristine p:config:print` still work so you can debug.
811
+
812
+ ### Module formats
813
+
814
+ The loader accepts:
815
+
816
+ | Extension | Loaded as | Example |
817
+ |-----------|-----------|---------|
818
+ | `.js` (CJS) | CommonJS | tsc's default output |
819
+ | `.cjs` | CommonJS (explicit) | |
820
+ | `.mjs` | ESM | |
821
+ | `.js` in a `"type": "module"` package | ESM (via package context) | |
822
+
823
+ All loaded via Node's real dynamic `import()` (with `pathToFileURL` for absolute paths).
824
+
825
+ ---
826
+
827
+ ## Built-in commands
828
+
829
+ Every framework-reserved command has a canonical `p:`-prefixed name and a top-level
830
+ alias. Use whichever you prefer.
831
+
832
+ | Command | Alias | What it does |
833
+ |---------|-------|--------------|
834
+ | `pristine p:init` | `init` | Scaffold a new project setup interactively (or via flags). Writes `pristine.config.ts`, optional starter AppModule, optional npm scripts. Refuses to overwrite an existing config. |
835
+ | `pristine p:help` | `help` | Print usage and list every registered command (built-in + custom) with descriptions. |
836
+ | `pristine p:list` | `list` | Print every registered command name (compact form). |
837
+ | `pristine p:info` | `info` | Print framework version, Node, OS, resolved config path, AppModule location, imported module list. Useful for support tickets. |
838
+ | `pristine p:build` | `build` | Compile your TypeScript via `tsc` and write the build manifest. Reads `build.{outDir,tsconfig,format,clean}` and `appModule.{sourcePath,outputPath}` from config. |
839
+ | `pristine p:start` | `start` | Boot the AppModule and run until SIGTERM/SIGINT. Auto-starts every registered `RuntimeServer` (HTTP, etc.). Production-grade. Supports `--port` / `--address`. Prompts to rebuild if the manifest is stale. |
840
+ | `pristine p:verify` | `verify` | Boot a fresh kernel of your AppModule, run all registered `InstantiationTest`s. Exits non-zero on failure. `--skip-tests` skips the test phase. |
841
+ | `pristine p:config:init` | — | Legacy helper that migrates a `pristine.appModule.{path,cjsPath}` field from `package.json` to a minimal config file. Prefer `pristine init` for new projects. |
842
+ | `pristine p:config:print` | — | Print the resolved config + file path it loaded from + per-field provenance. |
843
+
844
+ `config:*` commands intentionally don't have top-level aliases — they're sub-commands by
845
+ design.
846
+
847
+ ---
848
+
849
+ ## Production deployment
850
+
851
+ You have two equally supported entry points.
852
+
853
+ ### Option A — `node dist/main.js`
854
+
855
+ Traditional. Your `dist/main.js` is the entry. No `@pristine-ts/cli` needed in the deploy
856
+ unit. Lifecycle, signal handling, and graceful shutdown are your responsibility.
857
+
858
+ ### Option B — `pristine start`
859
+
860
+ `pristine start` is itself a production-grade entry. It boots your AppModule, starts every
861
+ registered `RuntimeServer` (HTTP server, etc.), handles SIGTERM/SIGINT with graceful
862
+ shutdown, enforces a hard-exit timeout, and keeps the event loop alive on its own.
863
+
864
+ Install once on the host:
865
+
866
+ ```sh
867
+ npm install -g @pristine-ts/cli
868
+ ```
869
+
870
+ Then in your Dockerfile / systemd unit / process manager:
871
+
872
+ ```sh
873
+ pristine start
874
+ ```
875
+
876
+ If `@pristine-ts/http` is in your AppModule, an HTTP server is launched automatically.
877
+ Configure port/address/TLS via env vars (see [HTTP recipe](#recipe-host-an-http-or-https-server)).
878
+
879
+ ---
880
+
881
+ ## Architecture & design notes
882
+
883
+ The `pristine` bin file is a thin shim:
884
+
885
+ ```js
886
+ require("reflect-metadata");
887
+ require("@pristine-ts/cli").bootstrap();
888
+ ```
889
+
890
+ This is deliberate. An earlier design bundled the entire CLI into the bin file; this
891
+ caused a "TypeInfo not known for X" error in real consumer projects because tsyringe's
892
+ decorator metadata is keyed by class identity, and bundling produced a *second* set of
893
+ class identities (the bundled copy) that didn't share metadata with the consumer's
894
+ `node_modules`-loaded copy.
895
+
896
+ The current design loads `@pristine-ts/cli` from the consumer's `node_modules`. This
897
+ guarantees that whichever `@pristine-ts/cli` class your AppModule imports is the same
898
+ physical class the bin reaches for — single identity, single decorator metadata
899
+ registration, no mismatch.
900
+
901
+ Side effect: `reflect-metadata`, `tsyringe`, and `class-transformer` are NOT declared as
902
+ direct dependencies of `@pristine-ts/cli`. They come transitively through
903
+ `@pristine-ts/common` (which every Pristine package depends on). Declaring them directly
904
+ would cause npm to install duplicate copies in `packages/cli/node_modules/`, and
905
+ `reflect-metadata` in particular keeps its decorator WeakMap inside the module closure —
906
+ two copies = two WeakMaps = silently lost metadata.
907
+
908
+ The bin itself is bundled with `esbuild` for fast startup, but with all `@pristine-ts/*`
909
+ packages marked external so the cross-realm trap doesn't reappear.
910
+
911
+ ---
912
+
913
+ ## Migrating from older versions
914
+
915
+ ### From the `package.json` `pristine.appModule.cjsPath` field
916
+
917
+ The old setup required:
918
+
919
+ ```json
920
+ {
921
+ "pristine": {
922
+ "appModule": { "cjsPath": "dist/lib/cjs/app.module.js" }
923
+ }
924
+ }
925
+ ```
926
+
927
+ Both `pristine.appModule.cjsPath` (deprecated, prints warning) and `pristine.appModule.path`
928
+ (new, format-agnostic) still work for one minor version cycle. To migrate cleanly:
929
+
930
+ ```sh
931
+ npx pristine p:config:init
932
+ ```
933
+
934
+ The command detects the existing `pristine.appModule.{path,cjsPath}` field, generates a
935
+ `pristine.config.ts` with the path migrated, and tells you to delete the `pristine` field
936
+ from `package.json`.
937
+
938
+ ### From manual bootstrap in `main.ts`
939
+
940
+ If your old setup looked like:
941
+
942
+ ```ts
943
+ // main.ts (old)
944
+ import {Kernel} from "@pristine-ts/core";
945
+ import http from "http";
946
+ import {AppModule} from "./app.module";
947
+
948
+ const kernel = new Kernel();
949
+ await kernel.start(AppModule);
950
+
951
+ http.createServer(async (req, res) => {
952
+ // ... wire req → kernel.handle → res
953
+ }).listen(3000);
954
+ ```
955
+
956
+ You can drop all of that and just use `pristine start`. Make sure `@pristine-ts/http` is
957
+ in your AppModule's `importModules` and the server starts automatically with the same
958
+ routing pipeline (no behavior changes).
959
+
960
+ ---
961
+
962
+ ## What changed (versus pre-1.0.440)
963
+
964
+ **Phase 7 (this release):**
965
+
966
+ - **`pristine init` command.** Interactive (or flag-driven) scaffold: writes
967
+ `pristine.config.ts` with both `sourcePath` and `outputPath`, optionally creates a starter
968
+ AppModule, optionally adds `build`/`start`/`verify` scripts to `package.json`, optionally
969
+ adds `.pristine/` to `.gitignore`. Never overwrites existing files.
970
+ - **Explicit source + output paths in config.** `appModule.path` deprecated;
971
+ `appModule.sourcePath` (what `pristine build` compiles) and `appModule.outputPath`
972
+ (what runtime commands load) replace it. Old `path` field still works for one minor
973
+ cycle with a warning.
974
+ - **Build manifest at `.pristine/build-manifest.json`.** Written atomically by
975
+ `pristine build` after successful compile. Records source path, output path, source
976
+ content hash, build timestamp.
977
+ - **Staleness detection.** `pristine start`/`verify`/etc. read the manifest before loading
978
+ the AppModule. Stale manifests (source edited, output missing, paths reconfigured) are
979
+ detected and surfaced with a specific reason. In a TTY, the user is prompted to rebuild
980
+ inline; in CI, the bin exits non-zero with the explanation.
981
+ - **Legacy `path` field still works.** With a deprecation warning pointing users at
982
+ `pristine init` for migration.
983
+
984
+ **Phase 6:**
985
+
986
+ - **End-to-end smoke tests for the bin.** `tests/cli` exercises every command via the
987
+ actual built `pristine` binary spawned by jest.
988
+ - **`pristine.config.ts` migration in `tests/cli`.** The old `package.json`
989
+ `pristine.appModule.cjsPath` field was removed in favor of a real `pristine.config.ts`.
990
+ - **CI runs the e2e suite.** `npm run e2e` invokes `tests/cli`'s suite alongside
991
+ `tests/e2e`.
992
+
993
+ **Phase 5:**
994
+
995
+ - **Plugin discovery via `pristine.config.ts`'s `plugins` array.** Tooling-only command
996
+ packages can be opted into without polluting the runtime AppModule.
997
+ - **Plugin failure is non-fatal.** A missing or broken plugin warns to stderr and the CLI
998
+ continues with built-in commands intact.
999
+ - **`pristine info` lists loaded plugins.**
1000
+ - **Command-name collisions warn loudly** at boot.
1001
+
1002
+ **Phase 4:**
1003
+
1004
+ - **`pristine start` hosts HTTP servers automatically** when `@pristine-ts/http` is
1005
+ imported. HTTPS support via TLS file paths. CLI flag overrides for `--port` / `--address`.
1006
+ - **`pristine start` is a real production entry point.** SIGTERM/SIGINT → graceful
1007
+ `Kernel.stop()` → `onShutdown` hooks → exit. Hard-exit timeout protection.
1008
+ - **`RuntimeServerInterface`** added so any module can plug a long-running server into
1009
+ `pristine start`.
1010
+ - **`pristine info`** prints framework + runtime metadata + the imported module graph.
1011
+ - **`pristine build`** wraps `tsc`. Format `"both"` runs ESM and CJS sequentially.
1012
+ - **`pristine help`** is generated from the live command registry.
1013
+ - **Top-level aliases** (`pristine help`, `list`, `verify`, `info`, `build`, `start`).
1014
+ - **`CommandInterface.description`** added (optional one-line summary).
1015
+
1016
+ **Phase 3:**
1017
+
1018
+ - **`pristine.config.ts` is the canonical config location.** Loaded via `jiti` — no
1019
+ separate compile step.
1020
+ - **`p:config:init`** generates a starter config and migrates from `package.json`.
1021
+ - **`p:config:print`** prints the resolved config with provenance markers.
1022
+ - **Deprecation warning** on `pristine.appModule.cjsPath` in `package.json`.
1023
+
1024
+ **Phase 2:**
1025
+
1026
+ - **ESM (`.mjs`) AppModules supported.** Path resolved through `pathToFileURL` so Windows
1027
+ paths and ESM resolution are correct.
1028
+ - **Convention-based AppModule discovery.** Greenfield projects with `dist/app.module.js`
1029
+ need zero configuration.
1030
+ - **Multi-candidate detection with TTY prompt / non-TTY error.** Selections are cached so
1031
+ re-runs skip the prompt.
1032
+ - **Resilient bootstrap.** A broken AppModule path no longer prevents built-in commands
1033
+ from running.
1034
+
1035
+ **Phase 1:**
1036
+
1037
+ - **The bin is bundled** (esbuild, single file) but with all `@pristine-ts/*` packages
1038
+ marked external (cross-realm safety — see [Architecture](#architecture--design-notes)).
1039
+ - **Bundled bin is published with the executable bit set.**