@luckystack/devkit 0.6.7 → 0.7.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.
- package/CHANGELOG.md +90 -0
- package/CLAUDE.md +129 -125
- package/LICENSE +21 -21
- package/README.md +44 -44
- package/dist/{chunk-EU2RFSLO.js → chunk-BZTCJ7JY.js} +1 -1
- package/dist/chunk-BZTCJ7JY.js.map +1 -0
- package/dist/cli/validateDeploy.js +1 -1
- package/dist/cli/validateDeploy.js.map +1 -1
- package/dist/index.js +257 -35
- package/dist/index.js.map +1 -1
- package/dist/supervisor.js +96 -45
- package/dist/supervisor.js.map +1 -1
- package/dist/templates/api.template.ts +54 -54
- package/dist/templates/page_dashboard.template.tsx +35 -35
- package/dist/templates/page_plain.template.tsx +32 -32
- package/dist/templates/sync_client.template.ts +40 -40
- package/dist/templates/sync_client_paired.template.ts +38 -38
- package/dist/templates/sync_client_standalone.template.ts +36 -36
- package/dist/templates/sync_server.template.ts +64 -64
- package/docs/cli.md +245 -245
- package/docs/hot-reload.md +365 -365
- package/docs/loader-pipeline.md +324 -324
- package/docs/runtime-type-resolver.md +258 -258
- package/docs/supervisor.md +292 -292
- package/docs/ts-program-cache.md +200 -199
- package/docs/type-map-generation.md +410 -392
- package/package.json +10 -4
- package/dist/chunk-EU2RFSLO.js.map +0 -1
package/docs/cli.md
CHANGED
|
@@ -1,245 +1,245 @@
|
|
|
1
|
-
# CLI (`luckystack-validate-deploy` + `validateDeploy`)
|
|
2
|
-
|
|
3
|
-
> Dev / build-time only. The CLI is the pre-deploy gate that runs against compiled `services.config.js` + `deploy.config.js` artifacts. The validator itself (`validateDeploy`) is a pure function — safe to call directly from a build script without going through the CLI.
|
|
4
|
-
|
|
5
|
-
`@luckystack/devkit` ships a single bin entry:
|
|
6
|
-
|
|
7
|
-
```json
|
|
8
|
-
{
|
|
9
|
-
"bin": {
|
|
10
|
-
"luckystack-validate-deploy": "./dist/cli/validateDeploy.js"
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
The CLI is a thin wrapper around `validateDeploy()` from `src/validateDeploy.ts`. The validator runs nine rules against the project's `services.config` + `deploy.config` snapshots and returns a structured `ValidateDeployResult` with one finding per detected problem.
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## CLI surface
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
USAGE
|
|
23
|
-
luckystack-validate-deploy --deploy <file> --services <file> [options]
|
|
24
|
-
|
|
25
|
-
REQUIRED
|
|
26
|
-
--deploy, -d <file> Path to compiled deploy.config.js (registers DeployConfig).
|
|
27
|
-
--services, -s <file> Path to compiled services.config.js (registers ServicesConfig).
|
|
28
|
-
|
|
29
|
-
OPTIONS
|
|
30
|
-
--strict Exit 1 on warnings as well as errors.
|
|
31
|
-
--help, -h Show this help.
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Argument parsing (`parseArgs(argv)`):
|
|
35
|
-
|
|
36
|
-
```typescript
|
|
37
|
-
interface CliArgs {
|
|
38
|
-
deploy: string | null;
|
|
39
|
-
services: string | null;
|
|
40
|
-
failOnWarning: boolean;
|
|
41
|
-
}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
Both config paths are required. There is no implicit search of the working directory — the CLI demands an explicit path so build pipelines can't accidentally pick up a stale untracked file.
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## Side-effect import order
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
await importConfig(args.deploy, 'deploy config');
|
|
52
|
-
await importConfig(args.services, 'services config');
|
|
53
|
-
|
|
54
|
-
if (!isDeployConfigRegistered()) {
|
|
55
|
-
// exit 2 with hint
|
|
56
|
-
}
|
|
57
|
-
if (!isServicesConfigRegistered()) {
|
|
58
|
-
// exit 2 with hint
|
|
59
|
-
}
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
The compiled config files are expected to call `registerDeployConfig(...)` / `registerServicesConfig(...)` (re-exported from `@luckystack/core`) as a side effect of import. The CLI imports each file via `pathToFileURL(absolute).href` (Windows-safe), then probes the registry with `isDeployConfigRegistered()` / `isServicesConfigRegistered()`.
|
|
63
|
-
|
|
64
|
-
If a file imports cleanly but doesn't register, the CLI exits with code 2 and a message:
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
[luckystack-validate-deploy] deploy config file did not call registerDeployConfig — nothing to validate.
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
This is the most common authoring mistake: forgetting the registration call leaves a silent empty registry, which would otherwise pass validation with zero findings.
|
|
71
|
-
|
|
72
|
-
`importConfig` wraps the dynamic import in a try/catch and re-throws with a clearer message including the absolute path:
|
|
73
|
-
|
|
74
|
-
```typescript
|
|
75
|
-
const importConfig = async (file: string, label: string): Promise<void> => {
|
|
76
|
-
const abs = path.isAbsolute(file) ? file : path.join(process.cwd(), file);
|
|
77
|
-
try {
|
|
78
|
-
await import(pathToFileURL(abs).href);
|
|
79
|
-
} catch (error) {
|
|
80
|
-
throw new Error(`[luckystack-validate-deploy] failed to import ${label} at ${abs}: ${message}`);
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
---
|
|
86
|
-
|
|
87
|
-
## `validateDeploy(input)` — pure validator
|
|
88
|
-
|
|
89
|
-
Defined in `packages/devkit/src/validateDeploy.ts`. Public types:
|
|
90
|
-
|
|
91
|
-
```typescript
|
|
92
|
-
export type ValidationSeverity = 'error' | 'warning';
|
|
93
|
-
|
|
94
|
-
export interface ValidationFinding {
|
|
95
|
-
severity: ValidationSeverity;
|
|
96
|
-
code: string;
|
|
97
|
-
message: string;
|
|
98
|
-
/** Human-readable location pointer (e.g. `services.config.ts > presets.api`). */
|
|
99
|
-
location?: string;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export interface ValidateDeployInput {
|
|
103
|
-
services: ServicesConfigShape;
|
|
104
|
-
deploy: DeployConfigShape;
|
|
105
|
-
env?: Record<string, string | undefined>; // defaults to process.env
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface ValidateDeployResult {
|
|
109
|
-
ok: boolean;
|
|
110
|
-
findings: ValidationFinding[];
|
|
111
|
-
errorCount: number;
|
|
112
|
-
warningCount: number;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export const validateDeploy = ({ services, deploy, env = process.env }: ValidateDeployInput): ValidateDeployResult;
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
`ServicesConfigShape` / `DeployConfigShape` are re-exported from `@luckystack/core` and define the canonical shape of the two config files. The validator never reads from disk — it works exclusively from the in-memory snapshots passed in.
|
|
119
|
-
|
|
120
|
-
### Rules
|
|
121
|
-
|
|
122
|
-
| # | Rule | Code | Severity |
|
|
123
|
-
|---|---|---|---|
|
|
124
|
-
| 1 | Every service is assigned to exactly one preset | `service-unassigned`, `service-in-multiple-presets` | error |
|
|
125
|
-
| 2 | Every preset references services that exist | `preset-references-unknown-service` | error |
|
|
126
|
-
| 3 | Every environment binding's service matches an actual service | `binding-references-unknown-service` | error |
|
|
127
|
-
| 3a | Every environment binding URL parses as a valid URL | `binding-invalid-url` | error |
|
|
128
|
-
| 3b | Every environment binding URL declares an explicit port | `binding-missing-port` | error |
|
|
129
|
-
| 4 | Every environment's redis/mongo resource keys exist | `unknown-redis-resource`, `unknown-mongo-resource` | error |
|
|
130
|
-
| 5 | Fallback envs must share the same redis/mongo resource keys (shared-resource invariant) | `unknown-fallback-env`, `fallback-redis-mismatch`, `fallback-mongo-mismatch` | error |
|
|
131
|
-
| 6 | `urlEnvKey` values resolve at config time | `missing-resource-env-var` | warning |
|
|
132
|
-
| 7 | `synchronizedEnvKeys` values resolve at config time | `missing-synchronized-env-var` | warning |
|
|
133
|
-
| 8 | Services bound in no environment | `service-bound-in-no-environment` | warning |
|
|
134
|
-
|
|
135
|
-
Each rule pushes one or more `ValidationFinding` records into a single result array; the validator never throws. The CLI iterates findings and prints each one before deciding the exit code.
|
|
136
|
-
|
|
137
|
-
### Findings output
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
const printFinding = (finding: ValidationFinding): void => {
|
|
141
|
-
const tag = finding.severity === 'error'
|
|
142
|
-
? `<red bold>ERROR</red>`
|
|
143
|
-
: `<yellow bold>WARN </yellow>`;
|
|
144
|
-
const code = `<dim>[${finding.code}]</dim>`;
|
|
145
|
-
const location = finding.location ? `\n <dim>at ${finding.location}</dim>` : '';
|
|
146
|
-
const sink = finding.severity === 'error' ? process.stderr : process.stdout;
|
|
147
|
-
sink.write(`${tag} ${code} ${finding.message}${location}\n`);
|
|
148
|
-
};
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
- Errors go to stderr, warnings to stdout — so CI pipelines that only mirror stderr see failures cleanly.
|
|
152
|
-
- ANSI color is only applied when stdout is a TTY (`process.stdout.isTTY`). Piping the CLI through `tee`, redirecting to a file, or running under non-TTY CI yields plain text.
|
|
153
|
-
- The trailing summary line is the same as a single finding: `OK — 0 error(s), 2 warning(s)` (green) or `FAILED — 1 error(s), 0 warning(s)` (red).
|
|
154
|
-
|
|
155
|
-
### Exit codes
|
|
156
|
-
|
|
157
|
-
| Code | Meaning |
|
|
158
|
-
|---|---|
|
|
159
|
-
| `0` | No errors. Warnings are allowed unless `--strict`. |
|
|
160
|
-
| `1` | At least one error finding, OR `--strict` with at least one warning. |
|
|
161
|
-
| `2` | Bad CLI usage: missing required flag, failed import, or config file didn't call its register function. |
|
|
162
|
-
|
|
163
|
-
Designed to be a pre-deploy gate — the recommended placement is the last step of `npm run build` or a dedicated CI job after `tsc -p tsconfig.build.json`.
|
|
164
|
-
|
|
165
|
-
---
|
|
166
|
-
|
|
167
|
-
## Calling from a build script
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
import { validateDeploy } from '@luckystack/devkit';
|
|
171
|
-
import { getServicesConfig, getDeployConfig } from '@luckystack/core';
|
|
172
|
-
|
|
173
|
-
await import('./services.config');
|
|
174
|
-
await import('./deploy.config');
|
|
175
|
-
|
|
176
|
-
const result = validateDeploy({
|
|
177
|
-
services: getServicesConfig(),
|
|
178
|
-
deploy: getDeployConfig(),
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
if (!result.ok) {
|
|
182
|
-
for (const finding of result.findings) {
|
|
183
|
-
console.error(finding);
|
|
184
|
-
}
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
`validateDeploy` is pure and has no I/O. Pass a `env` fixture in tests:
|
|
190
|
-
|
|
191
|
-
```typescript
|
|
192
|
-
const result = validateDeploy({
|
|
193
|
-
services: testServices,
|
|
194
|
-
deploy: testDeploy,
|
|
195
|
-
env: {
|
|
196
|
-
REDIS_URL: 'redis://localhost:6379',
|
|
197
|
-
// MONGO_URL deliberately absent to trigger the warning
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
expect(result.findings.map(f => f.code)).toContain('missing-resource-env-var');
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
The CLI is sugar; library consumers should reach for `validateDeploy` directly and decide their own exit behavior.
|
|
205
|
-
|
|
206
|
-
---
|
|
207
|
-
|
|
208
|
-
## Failure modes
|
|
209
|
-
|
|
210
|
-
| Symptom | Cause | Exit code |
|
|
211
|
-
|---|---|---|
|
|
212
|
-
| `--deploy and --services are required.` | Missing required flag | 2 |
|
|
213
|
-
| `failed to import deploy config at <path>: ...` | The file path is wrong, throws on evaluation, or is missing | 2 |
|
|
214
|
-
| `... did not call registerDeployConfig — nothing to validate` | Import succeeded but the consumer forgot the registration call | 2 |
|
|
215
|
-
| `ERROR [service-unassigned] ...` | A service in `services.services` isn't in any preset | 1 |
|
|
216
|
-
| `ERROR [preset-references-unknown-service] ...` | A preset references a service that doesn't exist | 1 |
|
|
217
|
-
| `ERROR [binding-invalid-url] ...` | A binding URL in `deploy.config.ts > environments.<env>.bindings.<service>` doesn't parse as a valid URL | 1 |
|
|
218
|
-
| `ERROR [binding-missing-port] ...` | A binding URL in `deploy.config.ts > environments.<env>.bindings.<service>` is missing an explicit port — the router requires one to disambiguate per-preset backends | 1 |
|
|
219
|
-
| `ERROR [fallback-redis-mismatch] ...` | Two envs in a fallback chain reference different redis resource keys | 1 |
|
|
220
|
-
| `WARN [missing-resource-env-var] ...` | A resource's `urlEnvKey` is unset at config time (boot may still succeed if env is set at runtime) | 0 (1 with `--strict`) |
|
|
221
|
-
| `FAILED — N error(s)` | Run had at least one error finding | 1 |
|
|
222
|
-
| `OK — 0 error(s), N warning(s)` | No errors; warnings tolerated | 0 |
|
|
223
|
-
|
|
224
|
-
---
|
|
225
|
-
|
|
226
|
-
## Public exports
|
|
227
|
-
|
|
228
|
-
Re-exported from `@luckystack/devkit`:
|
|
229
|
-
|
|
230
|
-
```typescript
|
|
231
|
-
export { validateDeploy } from './validateDeploy';
|
|
232
|
-
export type {
|
|
233
|
-
ValidateDeployInput,
|
|
234
|
-
ValidateDeployResult,
|
|
235
|
-
ValidationFinding,
|
|
236
|
-
ValidationSeverity,
|
|
237
|
-
} from './validateDeploy';
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
Core registry probes used by the CLI live in `@luckystack/core`:
|
|
241
|
-
|
|
242
|
-
- `getServicesConfig()` / `isServicesConfigRegistered()`
|
|
243
|
-
- `getDeployConfig()` / `isDeployConfigRegistered()`
|
|
244
|
-
|
|
245
|
-
These are not re-exported by devkit; consumers calling `validateDeploy` from build scripts should import them directly from core.
|
|
1
|
+
# CLI (`luckystack-validate-deploy` + `validateDeploy`)
|
|
2
|
+
|
|
3
|
+
> Dev / build-time only. The CLI is the pre-deploy gate that runs against compiled `services.config.js` + `deploy.config.js` artifacts. The validator itself (`validateDeploy`) is a pure function — safe to call directly from a build script without going through the CLI.
|
|
4
|
+
|
|
5
|
+
`@luckystack/devkit` ships a single bin entry:
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"bin": {
|
|
10
|
+
"luckystack-validate-deploy": "./dist/cli/validateDeploy.js"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The CLI is a thin wrapper around `validateDeploy()` from `src/validateDeploy.ts`. The validator runs nine rules against the project's `services.config` + `deploy.config` snapshots and returns a structured `ValidateDeployResult` with one finding per detected problem.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## CLI surface
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
USAGE
|
|
23
|
+
luckystack-validate-deploy --deploy <file> --services <file> [options]
|
|
24
|
+
|
|
25
|
+
REQUIRED
|
|
26
|
+
--deploy, -d <file> Path to compiled deploy.config.js (registers DeployConfig).
|
|
27
|
+
--services, -s <file> Path to compiled services.config.js (registers ServicesConfig).
|
|
28
|
+
|
|
29
|
+
OPTIONS
|
|
30
|
+
--strict Exit 1 on warnings as well as errors.
|
|
31
|
+
--help, -h Show this help.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Argument parsing (`parseArgs(argv)`):
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
interface CliArgs {
|
|
38
|
+
deploy: string | null;
|
|
39
|
+
services: string | null;
|
|
40
|
+
failOnWarning: boolean;
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Both config paths are required. There is no implicit search of the working directory — the CLI demands an explicit path so build pipelines can't accidentally pick up a stale untracked file.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Side-effect import order
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
await importConfig(args.deploy, 'deploy config');
|
|
52
|
+
await importConfig(args.services, 'services config');
|
|
53
|
+
|
|
54
|
+
if (!isDeployConfigRegistered()) {
|
|
55
|
+
// exit 2 with hint
|
|
56
|
+
}
|
|
57
|
+
if (!isServicesConfigRegistered()) {
|
|
58
|
+
// exit 2 with hint
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The compiled config files are expected to call `registerDeployConfig(...)` / `registerServicesConfig(...)` (re-exported from `@luckystack/core`) as a side effect of import. The CLI imports each file via `pathToFileURL(absolute).href` (Windows-safe), then probes the registry with `isDeployConfigRegistered()` / `isServicesConfigRegistered()`.
|
|
63
|
+
|
|
64
|
+
If a file imports cleanly but doesn't register, the CLI exits with code 2 and a message:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
[luckystack-validate-deploy] deploy config file did not call registerDeployConfig — nothing to validate.
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This is the most common authoring mistake: forgetting the registration call leaves a silent empty registry, which would otherwise pass validation with zero findings.
|
|
71
|
+
|
|
72
|
+
`importConfig` wraps the dynamic import in a try/catch and re-throws with a clearer message including the absolute path:
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
const importConfig = async (file: string, label: string): Promise<void> => {
|
|
76
|
+
const abs = path.isAbsolute(file) ? file : path.join(process.cwd(), file);
|
|
77
|
+
try {
|
|
78
|
+
await import(pathToFileURL(abs).href);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new Error(`[luckystack-validate-deploy] failed to import ${label} at ${abs}: ${message}`);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## `validateDeploy(input)` — pure validator
|
|
88
|
+
|
|
89
|
+
Defined in `packages/devkit/src/validateDeploy.ts`. Public types:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
export type ValidationSeverity = 'error' | 'warning';
|
|
93
|
+
|
|
94
|
+
export interface ValidationFinding {
|
|
95
|
+
severity: ValidationSeverity;
|
|
96
|
+
code: string;
|
|
97
|
+
message: string;
|
|
98
|
+
/** Human-readable location pointer (e.g. `services.config.ts > presets.api`). */
|
|
99
|
+
location?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ValidateDeployInput {
|
|
103
|
+
services: ServicesConfigShape;
|
|
104
|
+
deploy: DeployConfigShape;
|
|
105
|
+
env?: Record<string, string | undefined>; // defaults to process.env
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface ValidateDeployResult {
|
|
109
|
+
ok: boolean;
|
|
110
|
+
findings: ValidationFinding[];
|
|
111
|
+
errorCount: number;
|
|
112
|
+
warningCount: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const validateDeploy = ({ services, deploy, env = process.env }: ValidateDeployInput): ValidateDeployResult;
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`ServicesConfigShape` / `DeployConfigShape` are re-exported from `@luckystack/core` and define the canonical shape of the two config files. The validator never reads from disk — it works exclusively from the in-memory snapshots passed in.
|
|
119
|
+
|
|
120
|
+
### Rules
|
|
121
|
+
|
|
122
|
+
| # | Rule | Code | Severity |
|
|
123
|
+
|---|---|---|---|
|
|
124
|
+
| 1 | Every service is assigned to exactly one preset | `service-unassigned`, `service-in-multiple-presets` | error |
|
|
125
|
+
| 2 | Every preset references services that exist | `preset-references-unknown-service` | error |
|
|
126
|
+
| 3 | Every environment binding's service matches an actual service | `binding-references-unknown-service` | error |
|
|
127
|
+
| 3a | Every environment binding URL parses as a valid URL | `binding-invalid-url` | error |
|
|
128
|
+
| 3b | Every environment binding URL declares an explicit port | `binding-missing-port` | error |
|
|
129
|
+
| 4 | Every environment's redis/mongo resource keys exist | `unknown-redis-resource`, `unknown-mongo-resource` | error |
|
|
130
|
+
| 5 | Fallback envs must share the same redis/mongo resource keys (shared-resource invariant) | `unknown-fallback-env`, `fallback-redis-mismatch`, `fallback-mongo-mismatch` | error |
|
|
131
|
+
| 6 | `urlEnvKey` values resolve at config time | `missing-resource-env-var` | warning |
|
|
132
|
+
| 7 | `synchronizedEnvKeys` values resolve at config time | `missing-synchronized-env-var` | warning |
|
|
133
|
+
| 8 | Services bound in no environment | `service-bound-in-no-environment` | warning |
|
|
134
|
+
|
|
135
|
+
Each rule pushes one or more `ValidationFinding` records into a single result array; the validator never throws. The CLI iterates findings and prints each one before deciding the exit code.
|
|
136
|
+
|
|
137
|
+
### Findings output
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
const printFinding = (finding: ValidationFinding): void => {
|
|
141
|
+
const tag = finding.severity === 'error'
|
|
142
|
+
? `<red bold>ERROR</red>`
|
|
143
|
+
: `<yellow bold>WARN </yellow>`;
|
|
144
|
+
const code = `<dim>[${finding.code}]</dim>`;
|
|
145
|
+
const location = finding.location ? `\n <dim>at ${finding.location}</dim>` : '';
|
|
146
|
+
const sink = finding.severity === 'error' ? process.stderr : process.stdout;
|
|
147
|
+
sink.write(`${tag} ${code} ${finding.message}${location}\n`);
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- Errors go to stderr, warnings to stdout — so CI pipelines that only mirror stderr see failures cleanly.
|
|
152
|
+
- ANSI color is only applied when stdout is a TTY (`process.stdout.isTTY`). Piping the CLI through `tee`, redirecting to a file, or running under non-TTY CI yields plain text.
|
|
153
|
+
- The trailing summary line is the same as a single finding: `OK — 0 error(s), 2 warning(s)` (green) or `FAILED — 1 error(s), 0 warning(s)` (red).
|
|
154
|
+
|
|
155
|
+
### Exit codes
|
|
156
|
+
|
|
157
|
+
| Code | Meaning |
|
|
158
|
+
|---|---|
|
|
159
|
+
| `0` | No errors. Warnings are allowed unless `--strict`. |
|
|
160
|
+
| `1` | At least one error finding, OR `--strict` with at least one warning. |
|
|
161
|
+
| `2` | Bad CLI usage: missing required flag, failed import, or config file didn't call its register function. |
|
|
162
|
+
|
|
163
|
+
Designed to be a pre-deploy gate — the recommended placement is the last step of `npm run build` or a dedicated CI job after `tsc -p tsconfig.build.json`.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Calling from a build script
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { validateDeploy } from '@luckystack/devkit';
|
|
171
|
+
import { getServicesConfig, getDeployConfig } from '@luckystack/core';
|
|
172
|
+
|
|
173
|
+
await import('./services.config');
|
|
174
|
+
await import('./deploy.config');
|
|
175
|
+
|
|
176
|
+
const result = validateDeploy({
|
|
177
|
+
services: getServicesConfig(),
|
|
178
|
+
deploy: getDeployConfig(),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!result.ok) {
|
|
182
|
+
for (const finding of result.findings) {
|
|
183
|
+
console.error(finding);
|
|
184
|
+
}
|
|
185
|
+
process.exit(1);
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`validateDeploy` is pure and has no I/O. Pass a `env` fixture in tests:
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
const result = validateDeploy({
|
|
193
|
+
services: testServices,
|
|
194
|
+
deploy: testDeploy,
|
|
195
|
+
env: {
|
|
196
|
+
REDIS_URL: 'redis://localhost:6379',
|
|
197
|
+
// MONGO_URL deliberately absent to trigger the warning
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(result.findings.map(f => f.code)).toContain('missing-resource-env-var');
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The CLI is sugar; library consumers should reach for `validateDeploy` directly and decide their own exit behavior.
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## Failure modes
|
|
209
|
+
|
|
210
|
+
| Symptom | Cause | Exit code |
|
|
211
|
+
|---|---|---|
|
|
212
|
+
| `--deploy and --services are required.` | Missing required flag | 2 |
|
|
213
|
+
| `failed to import deploy config at <path>: ...` | The file path is wrong, throws on evaluation, or is missing | 2 |
|
|
214
|
+
| `... did not call registerDeployConfig — nothing to validate` | Import succeeded but the consumer forgot the registration call | 2 |
|
|
215
|
+
| `ERROR [service-unassigned] ...` | A service in `services.services` isn't in any preset | 1 |
|
|
216
|
+
| `ERROR [preset-references-unknown-service] ...` | A preset references a service that doesn't exist | 1 |
|
|
217
|
+
| `ERROR [binding-invalid-url] ...` | A binding URL in `deploy.config.ts > environments.<env>.bindings.<service>` doesn't parse as a valid URL | 1 |
|
|
218
|
+
| `ERROR [binding-missing-port] ...` | A binding URL in `deploy.config.ts > environments.<env>.bindings.<service>` is missing an explicit port — the router requires one to disambiguate per-preset backends | 1 |
|
|
219
|
+
| `ERROR [fallback-redis-mismatch] ...` | Two envs in a fallback chain reference different redis resource keys | 1 |
|
|
220
|
+
| `WARN [missing-resource-env-var] ...` | A resource's `urlEnvKey` is unset at config time (boot may still succeed if env is set at runtime) | 0 (1 with `--strict`) |
|
|
221
|
+
| `FAILED — N error(s)` | Run had at least one error finding | 1 |
|
|
222
|
+
| `OK — 0 error(s), N warning(s)` | No errors; warnings tolerated | 0 |
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Public exports
|
|
227
|
+
|
|
228
|
+
Re-exported from `@luckystack/devkit`:
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
export { validateDeploy } from './validateDeploy';
|
|
232
|
+
export type {
|
|
233
|
+
ValidateDeployInput,
|
|
234
|
+
ValidateDeployResult,
|
|
235
|
+
ValidationFinding,
|
|
236
|
+
ValidationSeverity,
|
|
237
|
+
} from './validateDeploy';
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Core registry probes used by the CLI live in `@luckystack/core`:
|
|
241
|
+
|
|
242
|
+
- `getServicesConfig()` / `isServicesConfigRegistered()`
|
|
243
|
+
- `getDeployConfig()` / `isDeployConfigRegistered()`
|
|
244
|
+
|
|
245
|
+
These are not re-exported by devkit; consumers calling `validateDeploy` from build scripts should import them directly from core.
|