@titannio/webtoolkit-cli 1.3.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +639 -0
  3. package/dist/bin.d.ts +2 -0
  4. package/dist/bin.js +268 -0
  5. package/dist/bundle-audit.d.ts +7 -0
  6. package/dist/bundle-audit.js +111 -0
  7. package/dist/cleaner.d.ts +15 -0
  8. package/dist/cleaner.js +359 -0
  9. package/dist/config-reference.d.ts +8 -0
  10. package/dist/config-reference.js +805 -0
  11. package/dist/config.d.ts +306 -0
  12. package/dist/config.js +173 -0
  13. package/dist/dev-grid.d.ts +7 -0
  14. package/dist/dev-grid.js +181 -0
  15. package/dist/dev-watch.d.ts +13 -0
  16. package/dist/dev-watch.js +184 -0
  17. package/dist/environment.d.ts +10 -0
  18. package/dist/environment.js +172 -0
  19. package/dist/guard-registry.d.ts +1 -0
  20. package/dist/guard-registry.js +17 -0
  21. package/dist/guard-runner.d.ts +4 -0
  22. package/dist/guard-runner.js +36 -0
  23. package/dist/guards/any-guard.d.ts +16 -0
  24. package/dist/guards/any-guard.js +121 -0
  25. package/dist/guards/assert-no-tests-in-dist.d.ts +1 -0
  26. package/dist/guards/assert-no-tests-in-dist.js +56 -0
  27. package/dist/guards/check-mojibake.d.ts +52 -0
  28. package/dist/guards/check-mojibake.js +378 -0
  29. package/dist/guards/code-pattern-guard.d.ts +71 -0
  30. package/dist/guards/code-pattern-guard.js +654 -0
  31. package/dist/guards/dal-service-repository-check.d.ts +13 -0
  32. package/dist/guards/dal-service-repository-check.js +264 -0
  33. package/dist/guards/dependency-cruiser-guard.d.ts +14 -0
  34. package/dist/guards/dependency-cruiser-guard.js +69 -0
  35. package/dist/guards/documentation-guard.d.ts +3 -0
  36. package/dist/guards/documentation-guard.js +370 -0
  37. package/dist/guards/guard-config.d.ts +19 -0
  38. package/dist/guards/guard-config.js +87 -0
  39. package/dist/guards/internal-link-guard.d.ts +12 -0
  40. package/dist/guards/internal-link-guard.js +272 -0
  41. package/dist/guards/package-surface-guard.d.ts +37 -0
  42. package/dist/guards/package-surface-guard.js +234 -0
  43. package/dist/guards/pnpm-workspace-config.d.ts +2 -0
  44. package/dist/guards/pnpm-workspace-config.js +40 -0
  45. package/dist/guards/rebuild-preflight.d.ts +29 -0
  46. package/dist/guards/rebuild-preflight.js +137 -0
  47. package/dist/guards/repository-hygiene-guard.d.ts +12 -0
  48. package/dist/guards/repository-hygiene-guard.js +70 -0
  49. package/dist/guards/schema-guard.d.ts +10 -0
  50. package/dist/guards/schema-guard.js +160 -0
  51. package/dist/guards/singleton-deps-guard.d.ts +21 -0
  52. package/dist/guards/singleton-deps-guard.js +183 -0
  53. package/dist/guards/tsconfig-guard.d.ts +5 -0
  54. package/dist/guards/tsconfig-guard.js +105 -0
  55. package/dist/guards/workspace-manifest-guard.d.ts +21 -0
  56. package/dist/guards/workspace-manifest-guard.js +210 -0
  57. package/dist/jsdoc-report.d.ts +7 -0
  58. package/dist/jsdoc-report.js +456 -0
  59. package/dist/process.d.ts +20 -0
  60. package/dist/process.js +86 -0
  61. package/dist/ready-service.d.ts +7 -0
  62. package/dist/ready-service.js +135 -0
  63. package/dist/release-gate.d.ts +7 -0
  64. package/dist/release-gate.js +35 -0
  65. package/dist/repo-check.d.ts +7 -0
  66. package/dist/repo-check.js +121 -0
  67. package/dist/tasks.d.ts +17 -0
  68. package/dist/tasks.js +195 -0
  69. package/dist/upgrade.d.ts +10 -0
  70. package/dist/upgrade.js +674 -0
  71. package/dist/validate.d.ts +7 -0
  72. package/dist/validate.js +51 -0
  73. package/dist/workspace-tests.d.ts +33 -0
  74. package/dist/workspace-tests.js +529 -0
  75. package/package.json +57 -0
@@ -0,0 +1,805 @@
1
+ import path from 'node:path';
2
+ import { Ajv2020 } from 'ajv/dist/2020.js';
3
+ import { builtinGuards } from './guard-registry.js';
4
+ const stringArray = (description, options = {}) => ({
5
+ type: 'array',
6
+ items: {
7
+ type: 'string',
8
+ minLength: 1,
9
+ ...(options.format ? { format: options.format } : {}),
10
+ },
11
+ ...(options.minItems === undefined ? {} : { minItems: options.minItems }),
12
+ description,
13
+ });
14
+ const projectPath = (description) => ({
15
+ type: 'string',
16
+ minLength: 1,
17
+ format: 'project-path',
18
+ ...(description ? { description } : {}),
19
+ });
20
+ const projectPaths = (description, minItems = 1) => (stringArray(description, { minItems, format: 'project-path' }));
21
+ const regexPatterns = (description) => (stringArray(description, { format: 'regex' }));
22
+ const builtinGuardNames = Object.keys(builtinGuards).sort();
23
+ export const configSchema = {
24
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
25
+ title: 'WebToolkit CLI configuration',
26
+ description: 'Reference for .webtoolkit-cli/config.json.',
27
+ type: 'object',
28
+ additionalProperties: false,
29
+ properties: {
30
+ packageManager: {
31
+ type: 'string',
32
+ default: 'pnpm',
33
+ description: 'Package-manager command used by CLI engines.',
34
+ examples: ['pnpm'],
35
+ },
36
+ cleaner: { $ref: '#/$defs/cleaner' },
37
+ tasks: {
38
+ type: 'object',
39
+ description: 'Named task recipes used by build, check, and run:<name>.',
40
+ additionalProperties: { $ref: '#/$defs/task' },
41
+ examples: [{ build: { steps: [{ label: 'TypeScript', command: 'pnpm', args: ['exec', 'tsc', '--noEmit'] }] } }],
42
+ },
43
+ guards: { $ref: '#/$defs/guards' },
44
+ documentation: { $ref: '#/$defs/documentation' },
45
+ workspaceTests: { $ref: '#/$defs/workspaceTests' },
46
+ repoCheck: { $ref: '#/$defs/repoCheck' },
47
+ releaseGate: { $ref: '#/$defs/releaseGate' },
48
+ validate: { $ref: '#/$defs/validate' },
49
+ jsdocReport: { $ref: '#/$defs/jsdocReport' },
50
+ bundleAudit: { $ref: '#/$defs/bundleAudit' },
51
+ upgrade: { $ref: '#/$defs/upgrade' },
52
+ devWatch: { $ref: '#/$defs/devWatch' },
53
+ devGrid: { $ref: '#/$defs/devGrid' },
54
+ environment: { $ref: '#/$defs/environment' },
55
+ },
56
+ $defs: {
57
+ taskStep: {
58
+ type: 'object',
59
+ required: ['label'],
60
+ oneOf: [
61
+ {
62
+ required: ['command'],
63
+ properties: {
64
+ command: { type: 'string' },
65
+ builtinGuard: false,
66
+ },
67
+ },
68
+ {
69
+ required: ['builtinGuard'],
70
+ properties: {
71
+ command: false,
72
+ builtinGuard: { type: 'string' },
73
+ },
74
+ },
75
+ ],
76
+ additionalProperties: false,
77
+ properties: {
78
+ label: { type: 'string', minLength: 1, description: 'Human-readable step name.' },
79
+ builtinGuard: {
80
+ type: 'string',
81
+ enum: builtinGuardNames,
82
+ description: 'Builtin guard name; command is unnecessary when set.',
83
+ },
84
+ command: { type: 'string', minLength: 1, description: 'Executable to spawn.' },
85
+ args: stringArray('Command arguments, one shell token per item.'),
86
+ cwd: projectPath('Optional project-relative working directory.'),
87
+ env: { type: 'object', additionalProperties: { type: 'string' }, description: 'Environment variables for the step.' },
88
+ appendArgs: { type: 'boolean', default: false, description: 'Append extra CLI arguments to this step.' },
89
+ outputMode: { type: 'string', enum: ['inherit', 'buffered'], default: 'inherit', description: 'Whether output streams live or only on failure.' },
90
+ },
91
+ },
92
+ commandStep: {
93
+ type: 'object',
94
+ required: ['label', 'command'],
95
+ additionalProperties: false,
96
+ properties: {
97
+ label: { type: 'string', minLength: 1 },
98
+ command: { type: 'string', minLength: 1 },
99
+ args: stringArray('Command arguments, one shell token per item.'),
100
+ cwd: projectPath('Optional project-relative working directory.'),
101
+ env: { type: 'object', additionalProperties: { type: 'string' } },
102
+ appendArgs: { type: 'boolean', default: false },
103
+ outputMode: { type: 'string', enum: ['inherit', 'buffered'], default: 'inherit' },
104
+ },
105
+ },
106
+ task: {
107
+ type: 'object',
108
+ required: ['steps'],
109
+ additionalProperties: false,
110
+ properties: {
111
+ title: { type: 'string', description: 'Heading printed before the task.' },
112
+ failFast: { type: 'boolean', default: true, description: 'Skip later steps after a failure.' },
113
+ outputMode: { type: 'string', enum: ['inherit', 'buffered'], default: 'inherit' },
114
+ steps: { type: 'array', minItems: 1, items: { $ref: '#/$defs/taskStep' } },
115
+ },
116
+ },
117
+ pathScanGuard: {
118
+ type: 'object',
119
+ required: ['includePaths'],
120
+ additionalProperties: false,
121
+ properties: {
122
+ includePaths: projectPaths('Project-relative directories scanned by the guard.'),
123
+ excludePatterns: regexPatterns('Additional regular expressions excluded after the CLI safe base exclusions.'),
124
+ },
125
+ },
126
+ guards: {
127
+ type: 'object',
128
+ description: 'Consumer-owned policy for configurable builtin guards.',
129
+ additionalProperties: false,
130
+ properties: {
131
+ any: { $ref: '#/$defs/pathScanGuard' },
132
+ internalLink: { $ref: '#/$defs/pathScanGuard' },
133
+ schema: {
134
+ type: 'object',
135
+ required: ['centralDirectory', 'includePaths', 'builders'],
136
+ additionalProperties: false,
137
+ properties: {
138
+ centralDirectory: projectPath('Project-relative directory where schemas may be defined.'),
139
+ includePaths: projectPaths('Project-relative directories scanned by the guard.'),
140
+ builders: stringArray('Zod builder names treated as schema definitions.', { minItems: 1 }),
141
+ excludePatterns: regexPatterns('Additional regular expressions excluded after the CLI safe base exclusions.'),
142
+ },
143
+ },
144
+ rebuildPreflight: {
145
+ type: 'object',
146
+ required: ['targets'],
147
+ additionalProperties: false,
148
+ properties: {
149
+ targets: {
150
+ type: 'object',
151
+ minProperties: 1,
152
+ additionalProperties: {
153
+ type: 'object',
154
+ required: ['warningTitle', 'turboFilters', 'relevantBuildPackages'],
155
+ additionalProperties: false,
156
+ properties: {
157
+ warningTitle: { type: 'string' },
158
+ turboFilters: stringArray('Package filters passed to Turbo.', { minItems: 1 }),
159
+ relevantBuildPackages: stringArray('Build packages whose cache status is relevant.'),
160
+ },
161
+ },
162
+ },
163
+ },
164
+ },
165
+ tsconfig: { $ref: '#/$defs/tsconfigGuard' },
166
+ dalServiceRepository: { $ref: '#/$defs/dalServiceRepositoryGuard' },
167
+ codePattern: { $ref: '#/$defs/codePatternGuard' },
168
+ packageSurface: { $ref: '#/$defs/packageSurfaceGuard' },
169
+ repositoryHygiene: { $ref: '#/$defs/repositoryHygieneGuard' },
170
+ workspaceManifest: { $ref: '#/$defs/workspaceManifestGuard' },
171
+ },
172
+ examples: [{
173
+ any: { includePaths: ['apps', 'packages'] },
174
+ internalLink: { includePaths: ['apps/web/src'] },
175
+ schema: {
176
+ centralDirectory: 'packages/contracts/src/schemas',
177
+ includePaths: ['apps/api/src', 'apps/web/src'],
178
+ builders: ['object', 'enum', 'array', 'nativeEnum'],
179
+ },
180
+ repositoryHygiene: {
181
+ forbiddenPathPatterns: ['(^|/)\\.env($|\\.)', '\\.(pem|key|p12)$'],
182
+ allowedPathPatterns: ['(^|/)\\.env\\.example$'],
183
+ },
184
+ packageSurface: {
185
+ packageDirectories: ['packages/library'],
186
+ forbiddenPublishedPatterns: ['(^|/)__tests__/', '\\.(test|spec)\\.'],
187
+ },
188
+ tsconfig: {
189
+ packageScope: '@acme',
190
+ configs: [{ path: 'tsconfig.json', compilerOptions: { strict: true } }],
191
+ },
192
+ workspaceManifest: {
193
+ packageRoots: ['apps', 'packages'],
194
+ requireWorkspaceProtocol: true,
195
+ peerRequirements: [],
196
+ },
197
+ }],
198
+ },
199
+ repositoryHygieneGuard: {
200
+ type: 'object',
201
+ required: ['forbiddenPathPatterns', 'allowedPathPatterns'],
202
+ additionalProperties: false,
203
+ properties: {
204
+ forbiddenPathPatterns: stringArray('Regular expressions forbidden in normalized Git-tracked project paths.', { minItems: 1, format: 'regex' }),
205
+ allowedPathPatterns: regexPatterns('Explicit exceptions checked before forbidden tracked-path patterns.'),
206
+ },
207
+ },
208
+ packageSurfaceGuard: {
209
+ type: 'object',
210
+ required: ['packageDirectories', 'forbiddenPublishedPatterns'],
211
+ additionalProperties: false,
212
+ properties: {
213
+ packageDirectories: projectPaths('Project-relative package directories verified after build.'),
214
+ forbiddenPublishedPatterns: regexPatterns('Regular expressions forbidden in normalized npm package paths.'),
215
+ },
216
+ },
217
+ tsconfigGuard: {
218
+ type: 'object',
219
+ required: ['configs'],
220
+ additionalProperties: false,
221
+ properties: {
222
+ packageScope: { type: 'string', description: 'Scoped package prefix whose aliases must use a slash.' },
223
+ configs: {
224
+ type: 'array',
225
+ minItems: 1,
226
+ items: {
227
+ type: 'object',
228
+ required: ['path'],
229
+ additionalProperties: false,
230
+ properties: {
231
+ path: projectPath(),
232
+ requiredIncludes: projectPaths('Values required in the tsconfig include array.', 0),
233
+ compilerOptions: {
234
+ type: 'object',
235
+ additionalProperties: {
236
+ anyOf: [{ type: 'string' }, { type: 'number' }, { type: 'boolean' }],
237
+ },
238
+ },
239
+ publicAliases: stringArray('Aliases that must not map directly to src internals.'),
240
+ },
241
+ },
242
+ },
243
+ textFiles: {
244
+ type: 'array',
245
+ items: {
246
+ type: 'object',
247
+ required: ['path', 'forbiddenStrings'],
248
+ additionalProperties: false,
249
+ properties: {
250
+ path: projectPath(),
251
+ forbiddenStrings: stringArray('Literal strings forbidden in the file.', { minItems: 1 }),
252
+ },
253
+ },
254
+ },
255
+ },
256
+ },
257
+ dalServiceRepositoryGuard: {
258
+ type: 'object',
259
+ required: ['sourceDirectory', 'tsconfig', 'layers', 'forbiddenDependencies'],
260
+ additionalProperties: false,
261
+ properties: {
262
+ sourceDirectory: projectPath(),
263
+ tsconfig: projectPath(),
264
+ excludePatterns: regexPatterns('Additional regular expressions excluded after the CLI safe base exclusions.'),
265
+ layers: {
266
+ type: 'array',
267
+ minItems: 1,
268
+ items: {
269
+ type: 'object',
270
+ required: ['name', 'paths'],
271
+ additionalProperties: false,
272
+ properties: {
273
+ name: { type: 'string' },
274
+ paths: projectPaths('Path prefixes relative to sourceDirectory.'),
275
+ exclude: projectPaths('Path prefixes excluded from this layer.', 0),
276
+ },
277
+ },
278
+ },
279
+ forbiddenDependencies: {
280
+ type: 'object',
281
+ minProperties: 1,
282
+ additionalProperties: { type: 'array', items: { type: 'string' } },
283
+ },
284
+ },
285
+ },
286
+ codePatternGuard: {
287
+ type: 'object',
288
+ required: ['tsconfig', 'modelsDirectory', 'rules'],
289
+ additionalProperties: false,
290
+ properties: {
291
+ tsconfig: projectPath(),
292
+ modelsDirectory: projectPath(),
293
+ rules: {
294
+ type: 'object',
295
+ minProperties: 1,
296
+ additionalProperties: {
297
+ type: 'object',
298
+ required: ['includePaths'],
299
+ additionalProperties: false,
300
+ properties: {
301
+ includePaths: projectPaths('Project-relative directories scanned by the rule.'),
302
+ allowedPathPatterns: regexPatterns('Regular expressions exempted from the rule.'),
303
+ },
304
+ },
305
+ },
306
+ },
307
+ },
308
+ workspaceManifestGuard: {
309
+ type: 'object',
310
+ required: ['packageRoots', 'requireWorkspaceProtocol', 'peerRequirements'],
311
+ additionalProperties: false,
312
+ properties: {
313
+ packageRoots: projectPaths('Directories whose direct children contain workspace package manifests.'),
314
+ requireWorkspaceProtocol: { type: 'boolean' },
315
+ peerRequirements: {
316
+ type: 'array',
317
+ items: {
318
+ type: 'object',
319
+ required: ['dependency', 'providers', 'consumers'],
320
+ additionalProperties: false,
321
+ properties: {
322
+ dependency: { type: 'string', minLength: 1 },
323
+ providers: projectPaths('Workspace packages that must expose the dependency as a peer.'),
324
+ consumers: projectPaths('Workspace packages that must declare the dependency directly at runtime.'),
325
+ },
326
+ },
327
+ },
328
+ },
329
+ },
330
+ cleanerLevel: {
331
+ type: 'object',
332
+ additionalProperties: false,
333
+ properties: {
334
+ label: { type: 'string' },
335
+ removeEmptyDirs: { type: 'boolean' },
336
+ removableDirNames: stringArray('Artifact directory names removable at repository or workspace roots.'),
337
+ removableFileNames: stringArray('Exact removable file names.'),
338
+ removableFileSuffixes: stringArray('Removable file suffixes.'),
339
+ removableFilePrefixes: stringArray('Removable file prefixes.'),
340
+ removableFilePatterns: regexPatterns('Regular expressions matched against file names.'),
341
+ removableSpecificFiles: projectPaths('Exact project-relative removable files.', 0),
342
+ },
343
+ },
344
+ cleaner: {
345
+ type: 'object',
346
+ description: 'Cleanup discovery and per-level overrides.',
347
+ additionalProperties: false,
348
+ properties: {
349
+ workspaceRootNames: stringArray('Directories whose direct children are workspace roots.'),
350
+ protectedRootNames: stringArray('Top-level directories protected from empty-directory cleanup.'),
351
+ skipEmptyDirNames: stringArray('Directory names skipped during empty-directory cleanup.'),
352
+ skipArtifactDirNames: stringArray('Directory names skipped while scanning artifacts.'),
353
+ levels: {
354
+ type: 'object',
355
+ additionalProperties: false,
356
+ properties: {
357
+ empty: { $ref: '#/$defs/cleanerLevel' },
358
+ cache: { $ref: '#/$defs/cleanerLevel' },
359
+ deep: { $ref: '#/$defs/cleanerLevel' },
360
+ nuclear: { $ref: '#/$defs/cleanerLevel' },
361
+ },
362
+ },
363
+ },
364
+ examples: [{ workspaceRootNames: ['apps', 'packages'], protectedRootNames: ['apps', 'scripts'] }],
365
+ },
366
+ documentationMetadataRule: {
367
+ type: 'object',
368
+ additionalProperties: false,
369
+ properties: {
370
+ equals: { type: 'string', description: 'Required value; supports {basename} and {stem}.' },
371
+ unique: { type: 'boolean', default: false, description: 'Require a distinct value across the collection.' },
372
+ repositoryPaths: { type: 'boolean', default: false, description: 'Treat inline-code values as repository-relative paths that must exist.' },
373
+ minItems: { type: 'integer', minimum: 0, default: 1, description: 'Minimum inline-code paths when repositoryPaths is enabled.' },
374
+ },
375
+ },
376
+ documentationPairedDocuments: {
377
+ type: 'object',
378
+ required: ['target'],
379
+ additionalProperties: false,
380
+ properties: {
381
+ target: projectPath('Paired-document path template; supports {basename} and {stem}.'),
382
+ index: projectPath('Optional index that must link to each paired document exactly once.'),
383
+ table: {
384
+ type: 'object',
385
+ required: ['header', 'fileColumn'],
386
+ additionalProperties: false,
387
+ properties: {
388
+ header: stringArray('Exact Markdown table header cells.', { minItems: 1 }),
389
+ fileColumn: { type: 'string', description: 'Column containing one or more inline-code repository file paths.' },
390
+ minRows: { type: 'integer', minimum: 0, default: 1 },
391
+ },
392
+ },
393
+ finalSection: {
394
+ type: 'object',
395
+ required: ['heading'],
396
+ additionalProperties: false,
397
+ properties: {
398
+ heading: { type: 'string', description: 'Exact final H2 heading, without ##.' },
399
+ minItems: { type: 'integer', minimum: 0, default: 1, description: 'Minimum list items beneath the section.' },
400
+ },
401
+ },
402
+ },
403
+ },
404
+ documentationCollection: {
405
+ type: 'object',
406
+ required: ['files'],
407
+ additionalProperties: false,
408
+ properties: {
409
+ files: projectPaths('Glob patterns selecting collection documents.'),
410
+ exclude: projectPaths('Glob patterns excluded from the collection.', 0),
411
+ index: projectPath('Index that must link to every collection document exactly once.'),
412
+ metadata: {
413
+ type: 'object',
414
+ description: 'Required blockquote metadata fields and their validation rules.',
415
+ additionalProperties: { $ref: '#/$defs/documentationMetadataRule' },
416
+ },
417
+ pairedDocuments: { $ref: '#/$defs/documentationPairedDocuments' },
418
+ },
419
+ },
420
+ documentation: {
421
+ type: 'object',
422
+ description: 'Declarative Markdown, collection, paired-document, and coverage-inventory checks.',
423
+ required: ['files'],
424
+ additionalProperties: false,
425
+ properties: {
426
+ files: projectPaths('Glob patterns selecting Markdown files to inspect.'),
427
+ excludeDirectories: stringArray('Directory names excluded from repository scanning.'),
428
+ checks: {
429
+ type: 'object',
430
+ additionalProperties: false,
431
+ properties: {
432
+ singleH1: { type: 'boolean', default: true },
433
+ headingOrder: { type: 'boolean', default: true },
434
+ localLinks: { type: 'boolean', default: true },
435
+ reachability: {
436
+ type: 'object',
437
+ required: ['entrypoints', 'files'],
438
+ additionalProperties: false,
439
+ properties: {
440
+ entrypoints: projectPaths('Glob patterns selecting reachability roots.'),
441
+ files: projectPaths('Glob patterns selecting documents that must be reachable.'),
442
+ },
443
+ },
444
+ },
445
+ },
446
+ requiredFiles: projectPaths('Exact repository-relative files that must exist.', 0),
447
+ collections: { type: 'array', minItems: 1, items: { $ref: '#/$defs/documentationCollection' } },
448
+ inventories: {
449
+ type: 'array',
450
+ minItems: 1,
451
+ items: {
452
+ type: 'object',
453
+ required: ['document', 'sources'],
454
+ additionalProperties: false,
455
+ properties: {
456
+ document: projectPath('Markdown inventory document.'),
457
+ sources: projectPaths('Glob or exact path patterns whose matches must be listed as inline code.'),
458
+ minMatches: { type: 'integer', minimum: 0, default: 0 },
459
+ },
460
+ },
461
+ },
462
+ },
463
+ examples: [{
464
+ files: ['docs/**/*.md', '**/README.md'],
465
+ checks: {
466
+ reachability: { entrypoints: ['**/README.md'], files: ['docs/**/*.md'] },
467
+ },
468
+ }],
469
+ },
470
+ workspaceTarget: {
471
+ type: 'object',
472
+ required: ['name', 'package', 'path'],
473
+ additionalProperties: false,
474
+ properties: {
475
+ name: { type: 'string' },
476
+ package: { type: 'string' },
477
+ path: projectPath(),
478
+ },
479
+ },
480
+ workspaceTests: {
481
+ type: 'object',
482
+ description: 'Workspace targets and failure-report behavior for test commands.',
483
+ required: ['workspaces'],
484
+ additionalProperties: false,
485
+ properties: {
486
+ workspaces: { type: 'array', minItems: 1, items: { $ref: '#/$defs/workspaceTarget' } },
487
+ errorLogFile: { ...projectPath(), default: 'tests_output_errors.log' },
488
+ testFilePattern: { type: 'string', format: 'regex', default: '\\.(test|spec)\\.(ts|tsx|js|jsx)$' },
489
+ ignoreDirNames: stringArray('Directory names skipped while counting tests.'),
490
+ maxFailureExcerptLines: { type: 'integer', minimum: 1 },
491
+ },
492
+ examples: [{ workspaces: [{ name: 'Core', package: '@acme/core', path: 'packages/core' }] }],
493
+ },
494
+ repoCheck: {
495
+ type: 'object',
496
+ description: 'Ordered repository-quality checks.',
497
+ required: ['steps'],
498
+ additionalProperties: false,
499
+ properties: {
500
+ title: { type: 'string' },
501
+ failFast: { type: 'boolean', default: false },
502
+ steps: { type: 'array', minItems: 1, items: { $ref: '#/$defs/taskStep' } },
503
+ },
504
+ examples: [{ steps: [{ label: 'Documentation', builtinGuard: 'documentation' }] }],
505
+ },
506
+ releaseGate: {
507
+ type: 'object',
508
+ description: 'Critical release stages.',
509
+ required: ['stages'],
510
+ additionalProperties: false,
511
+ properties: {
512
+ stages: {
513
+ type: 'array',
514
+ minItems: 1,
515
+ items: {
516
+ type: 'object',
517
+ required: ['name'],
518
+ additionalProperties: false,
519
+ properties: {
520
+ name: { type: 'string' },
521
+ command: { type: 'string' },
522
+ args: stringArray('Command arguments.'),
523
+ package: { type: 'string' },
524
+ script: { type: 'string' },
525
+ files: projectPaths('Focused Vitest files.', 0),
526
+ },
527
+ },
528
+ },
529
+ },
530
+ },
531
+ validate: {
532
+ type: 'object',
533
+ description: 'Main and post-validation steps.',
534
+ required: ['steps'],
535
+ additionalProperties: false,
536
+ properties: {
537
+ steps: { type: 'array', minItems: 1, items: { $ref: '#/$defs/taskStep' } },
538
+ postSteps: { type: 'array', items: { $ref: '#/$defs/taskStep' } },
539
+ },
540
+ },
541
+ jsdocReport: {
542
+ type: 'object',
543
+ description: 'JSDoc scan paths and report behavior.',
544
+ required: ['includePaths'],
545
+ additionalProperties: false,
546
+ properties: {
547
+ includePaths: projectPaths('Project-relative directories scanned by default.'),
548
+ excludePatterns: regexPatterns('Regular expressions excluded from scanning.'),
549
+ reportFile: projectPath(),
550
+ maxLineLength: { type: 'integer', minimum: 1 },
551
+ promptForReport: { type: 'boolean' },
552
+ },
553
+ },
554
+ bundleAudit: {
555
+ type: 'object',
556
+ description: 'Frontend build directories and size thresholds.',
557
+ required: ['appDirs'],
558
+ additionalProperties: false,
559
+ properties: {
560
+ appDirs: projectPaths('Frontend app directories containing dist/assets.'),
561
+ top: { type: 'integer', minimum: 1 },
562
+ rawWarningBytes: { type: 'integer', minimum: 0 },
563
+ },
564
+ },
565
+ upgrade: {
566
+ type: 'object',
567
+ description: 'Dependency-upgrade policy.',
568
+ additionalProperties: false,
569
+ properties: {
570
+ defaultCooldownDays: { type: 'integer', minimum: 0 },
571
+ protectedDependencyUpstreamHints: {
572
+ type: 'object',
573
+ additionalProperties: { type: 'array', items: { type: 'string' } },
574
+ },
575
+ protectedOverridesFile: projectPath(),
576
+ singletonGuardCommand: { $ref: '#/$defs/commandStep' },
577
+ },
578
+ },
579
+ devApp: {
580
+ type: 'object',
581
+ required: ['displayName', 'port'],
582
+ additionalProperties: false,
583
+ properties: {
584
+ displayName: { type: 'string' },
585
+ port: { type: 'integer', minimum: 1, maximum: 65535 },
586
+ filter: { type: 'string' },
587
+ color: { type: 'string' },
588
+ },
589
+ },
590
+ devWatch: {
591
+ type: 'object',
592
+ description: 'Development app ports and package filters.',
593
+ required: ['apps', 'defaultApps'],
594
+ additionalProperties: false,
595
+ properties: {
596
+ apps: { type: 'object', minProperties: 1, additionalProperties: { $ref: '#/$defs/devApp' } },
597
+ defaultApps: stringArray('App keys started when --apps is omitted.', { minItems: 1 }),
598
+ backendApp: { type: 'string' },
599
+ host: { type: 'string', default: '127.0.0.1' },
600
+ backendPortCleanupGraceMs: { type: 'integer', minimum: 0 },
601
+ },
602
+ },
603
+ devGridPane: {
604
+ type: 'object',
605
+ required: ['title', 'command'],
606
+ additionalProperties: false,
607
+ properties: {
608
+ title: { type: 'string' },
609
+ command: { type: 'string' },
610
+ silentCommand: { type: 'string' },
611
+ fontSize: { type: 'integer', minimum: 1 },
612
+ },
613
+ },
614
+ devGridRow: {
615
+ type: 'object',
616
+ required: ['panes'],
617
+ additionalProperties: false,
618
+ properties: {
619
+ panes: { type: 'array', minItems: 1, items: { $ref: '#/$defs/devGridPane' } },
620
+ },
621
+ },
622
+ devGridLayout: {
623
+ type: 'object',
624
+ required: ['rows'],
625
+ additionalProperties: false,
626
+ properties: {
627
+ rows: { type: 'array', minItems: 1, items: { $ref: '#/$defs/devGridRow' } },
628
+ },
629
+ },
630
+ devGrid: {
631
+ type: 'object',
632
+ description: 'Terminal row layout and fallback scripts for development.',
633
+ required: ['layout'],
634
+ additionalProperties: false,
635
+ properties: {
636
+ layout: { $ref: '#/$defs/devGridLayout' },
637
+ fallbackScript: { type: 'string' },
638
+ silentFallbackScript: { type: 'string' },
639
+ preflightCommand: { $ref: '#/$defs/commandStep' },
640
+ },
641
+ },
642
+ environment: {
643
+ type: 'object',
644
+ description: 'Node, package-manager, and Corepack policy.',
645
+ additionalProperties: false,
646
+ properties: {
647
+ requiredNodeMajor: { type: 'integer', minimum: 1 },
648
+ packageManager: { type: 'string' },
649
+ corepackHome: { type: 'string' },
650
+ },
651
+ },
652
+ },
653
+ };
654
+ function staysInsideRoot(pathApi, root, candidate) {
655
+ const resolved = pathApi.resolve(root, candidate);
656
+ const relative = pathApi.relative(root, resolved);
657
+ return relative !== '..' && !relative.startsWith(`..${pathApi.sep}`) && !pathApi.isAbsolute(relative);
658
+ }
659
+ function isSafeProjectPath(value) {
660
+ if (path.posix.isAbsolute(value) || path.win32.isAbsolute(value))
661
+ return false;
662
+ return (staysInsideRoot(path.posix, '/project', value.replaceAll('\\', '/')) &&
663
+ staysInsideRoot(path.win32, 'C:\\project', value));
664
+ }
665
+ function isValidRegex(value) {
666
+ try {
667
+ new RegExp(value);
668
+ return true;
669
+ }
670
+ catch {
671
+ return false;
672
+ }
673
+ }
674
+ function pointerToJsonPath(pointer) {
675
+ const segments = pointer
676
+ .split('/')
677
+ .slice(1)
678
+ .map((segment) => segment.replaceAll('~1', '/').replaceAll('~0', '~'));
679
+ return segments.reduce((result, segment) => (/^\d+$/u.test(segment)
680
+ ? `${result}[${segment}]`
681
+ : result ? `${result}.${segment}` : segment), '');
682
+ }
683
+ function configErrorPath(error) {
684
+ const basePath = pointerToJsonPath(error.instancePath);
685
+ if (error.keyword === 'required') {
686
+ const missingProperty = String(error.params.missingProperty);
687
+ /* v8 ignore next -- the root config has no required properties */
688
+ return basePath ? `${basePath}.${missingProperty}` : missingProperty;
689
+ }
690
+ if (error.keyword === 'additionalProperties') {
691
+ const additionalProperty = String(error.params.additionalProperty);
692
+ return basePath ? `${basePath}.${additionalProperty}` : additionalProperty;
693
+ }
694
+ return basePath || '<root>';
695
+ }
696
+ function configErrorMessage(error) {
697
+ if (error.keyword === 'format' && error.params.format === 'project-path') {
698
+ return 'must be a safe project-relative path';
699
+ }
700
+ if (error.keyword === 'format' && error.params.format === 'regex') {
701
+ return 'must be a valid regular expression';
702
+ }
703
+ if (error.keyword === 'oneOf') {
704
+ return 'must define exactly one of command or builtinGuard';
705
+ }
706
+ /* v8 ignore next -- Ajv always supplies messages for enabled built-in keywords */
707
+ return error.message ?? 'is invalid';
708
+ }
709
+ const schemaValidator = new Ajv2020({
710
+ allErrors: true,
711
+ strict: true,
712
+ });
713
+ schemaValidator.addFormat('project-path', { type: 'string', validate: isSafeProjectPath });
714
+ schemaValidator.addFormat('regex', { type: 'string', validate: isValidRegex });
715
+ const validateSchema = schemaValidator.compile(configSchema);
716
+ export function validateConfig(value) {
717
+ if (validateSchema(value))
718
+ return;
719
+ const errors = Array.from(new Set(validateSchema.errors.map((error) => (`${configErrorPath(error)}: ${configErrorMessage(error)}`)))).sort();
720
+ throw new Error([
721
+ 'Invalid .webtoolkit-cli/config.json:',
722
+ ...errors.map((error) => `- ${error}`),
723
+ ].join('\n'));
724
+ }
725
+ function schemaObject(value) {
726
+ return value;
727
+ }
728
+ function properties() {
729
+ return schemaObject(configSchema.properties);
730
+ }
731
+ function definitions() {
732
+ return schemaObject(configSchema.$defs);
733
+ }
734
+ function resolveReference(schema) {
735
+ const reference = schema.$ref;
736
+ if (typeof reference !== 'string')
737
+ return schema;
738
+ return definitions()[reference.replace('#/$defs/', '')];
739
+ }
740
+ export function configSectionNames() {
741
+ return Object.keys(properties());
742
+ }
743
+ export function getConfigSchema(section) {
744
+ if (!section)
745
+ return configSchema;
746
+ const selected = properties()[section];
747
+ if (!selected)
748
+ throw new Error(`Unknown config section "${section}". Available sections: ${configSectionNames().join(', ')}.`);
749
+ return {
750
+ $schema: configSchema.$schema,
751
+ title: `${section} configuration`,
752
+ type: 'object',
753
+ additionalProperties: false,
754
+ properties: { [section]: selected },
755
+ $defs: configSchema.$defs,
756
+ };
757
+ }
758
+ export function formatConfigHelp(section) {
759
+ if (!section) {
760
+ const lines = [
761
+ 'Usage: webtoolkit config [--help [section] | --json [section]]',
762
+ '',
763
+ 'Configuration file: .webtoolkit-cli/config.json',
764
+ '',
765
+ 'Sections:',
766
+ ];
767
+ for (const name of configSectionNames()) {
768
+ const description = resolveReference(properties()[name]).description;
769
+ lines.push(` ${name.padEnd(18)} ${String(description)}`.trimEnd());
770
+ }
771
+ lines.push('', 'Use `webtoolkit config --help <section>` for fields and examples.');
772
+ lines.push('Use `webtoolkit config --json [section]` for JSON Schema output.');
773
+ return lines.join('\n');
774
+ }
775
+ const selected = properties()[section];
776
+ if (!selected)
777
+ throw new Error(`Unknown config section "${section}". Available sections: ${configSectionNames().join(', ')}.`);
778
+ const resolved = resolveReference(selected);
779
+ const required = new Set(Array.isArray(resolved.required) ? resolved.required : []);
780
+ const sectionProperties = schemaObject(resolved.properties ?? {});
781
+ const lines = [section, String(resolved.description), '', 'Fields:'];
782
+ for (const [name, field] of Object.entries(sectionProperties)) {
783
+ const fieldType = typeof field.type === 'string' ? field.type : 'object';
784
+ const defaultValue = Object.hasOwn(field, 'default') ? `; default=${JSON.stringify(field.default)}` : '';
785
+ lines.push(` ${name} (${fieldType}; ${required.has(name) ? 'required' : 'optional'}${defaultValue})`);
786
+ if (field.description)
787
+ lines.push(` ${String(field.description)}`);
788
+ }
789
+ const examples = Array.isArray(resolved.examples) ? resolved.examples : [];
790
+ if (examples.length > 0) {
791
+ lines.push('', 'Example:', JSON.stringify({ [section]: examples[0] }, null, 2));
792
+ }
793
+ lines.push('', `Machine-readable schema: webtoolkit config --json ${section}`);
794
+ return lines.join('\n');
795
+ }
796
+ export function runConfigReference(args) {
797
+ const json = args.includes('--json');
798
+ const unknownFlags = args.filter((arg) => arg.startsWith('-') && !['--json', '--help', '-h'].includes(arg));
799
+ const sections = args.filter((arg) => !arg.startsWith('-'));
800
+ if (unknownFlags.length > 0 || sections.length > 1) {
801
+ throw new Error(`Usage: webtoolkit config [--help [section] | --json [section]]`);
802
+ }
803
+ const section = sections[0];
804
+ console.info(json ? JSON.stringify(getConfigSchema(section), null, 2) : formatConfigHelp(section));
805
+ }