@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,456 @@
1
+ import { existsSync, readdirSync, writeFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createInterface } from 'node:readline/promises';
4
+ import { stdin as input, stdout as output } from 'node:process';
5
+ import { Node, Project } from 'ts-morph';
6
+ const colors = {
7
+ reset: '\x1b[0m',
8
+ bright: '\x1b[1m',
9
+ red: '\x1b[31m',
10
+ green: '\x1b[32m',
11
+ yellow: '\x1b[33m',
12
+ cyan: '\x1b[36m',
13
+ gray: '\x1b[90m',
14
+ };
15
+ const defaultExcludePatterns = [
16
+ 'node_modules',
17
+ 'dist',
18
+ 'build',
19
+ '\\.next',
20
+ 'coverage',
21
+ '\\.test\\.',
22
+ '\\.spec\\.',
23
+ '\\.config\\.',
24
+ '\\.setup\\.',
25
+ ];
26
+ function colorize(value, color) {
27
+ return `${color}${value}${colors.reset}`;
28
+ }
29
+ function shouldProcessFile(filePath, excludePatterns) {
30
+ return /\.(ts|tsx)$/u.test(filePath) && !excludePatterns.some((pattern) => pattern.test(filePath));
31
+ }
32
+ function collectFiles(rootDir, includePaths, excludePatterns) {
33
+ const files = [];
34
+ function walkDir(dir) {
35
+ if (!existsSync(dir))
36
+ return;
37
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
38
+ const fullPath = path.join(dir, entry.name);
39
+ if (entry.isDirectory()) {
40
+ if (!excludePatterns.some((pattern) => pattern.test(fullPath))) {
41
+ walkDir(fullPath);
42
+ }
43
+ }
44
+ else if (entry.isFile() && shouldProcessFile(fullPath, excludePatterns)) {
45
+ files.push(fullPath);
46
+ }
47
+ }
48
+ }
49
+ for (const includePath of includePaths) {
50
+ walkDir(path.join(rootDir, includePath));
51
+ }
52
+ return files;
53
+ }
54
+ function isFunctionLike(node) {
55
+ return Node.isFunctionDeclaration(node) ||
56
+ Node.isMethodDeclaration(node) ||
57
+ Node.isArrowFunction(node) ||
58
+ Node.isFunctionExpression(node) ||
59
+ Node.isConstructorDeclaration(node);
60
+ }
61
+ function normalizeTypeText(typeText) {
62
+ return typeText
63
+ .replace(/import\([^)]+\)\./gu, '')
64
+ .replace(/\bReact\.JSX\.Element\b/gu, 'JSX.Element')
65
+ .replace(/\bReact\.ReactElement\b/gu, 'JSX.Element')
66
+ .replace(/\bReactElement\b/gu, 'JSX.Element')
67
+ .replace(/\bBoolean\b/gu, 'boolean')
68
+ .replace(/\bString\b/gu, 'string')
69
+ .replace(/\bNumber\b/gu, 'number')
70
+ .replace(/\bobject\b/gu, 'Object')
71
+ .replace(/\bfunction\b/gu, 'Function')
72
+ .replace(/\s+/gu, '')
73
+ .replace(/;(?=[}\]])/gu, '');
74
+ }
75
+ function isTypeComparable(typeText) {
76
+ return !(typeText.includes('import(') ||
77
+ typeText.includes('{') ||
78
+ typeText.includes('}') ||
79
+ typeText.includes('=>') ||
80
+ typeText.includes('\n') ||
81
+ typeText.includes(' is ') ||
82
+ typeText.includes('typeof '));
83
+ }
84
+ function extractTagType(tagText) {
85
+ return tagText.match(/@\w+\s+\{([^}]+)\}/u)?.[1]?.trim() ?? null;
86
+ }
87
+ function parseParamTag(tagText) {
88
+ const paramNameMatch = tagText.match(/@param\s+(?:\{[^}]+\}\s+)?(\[[^\]]+\]|\.{3}[\w$.]+|[\w$.]+)/u);
89
+ if (!paramNameMatch)
90
+ return null;
91
+ let rawName = paramNameMatch[1].trim();
92
+ let optional = false;
93
+ let optionalSyntaxExplicit = false;
94
+ if (rawName.startsWith('[') && rawName.endsWith(']')) {
95
+ optional = true;
96
+ optionalSyntaxExplicit = true;
97
+ rawName = rawName.slice(1, -1);
98
+ }
99
+ if (rawName.startsWith('...'))
100
+ rawName = rawName.slice(3);
101
+ const assignmentIndex = rawName.indexOf('=');
102
+ if (assignmentIndex >= 0) {
103
+ optional = true;
104
+ optionalSyntaxExplicit = true;
105
+ rawName = rawName.slice(0, assignmentIndex);
106
+ }
107
+ const name = rawName.trim();
108
+ if (!name)
109
+ return null;
110
+ return {
111
+ name,
112
+ type: extractTagType(tagText),
113
+ optional,
114
+ optionalSyntaxExplicit,
115
+ hasSeparator: tagText.includes(' - '),
116
+ };
117
+ }
118
+ function getLongJSDocLineIssues(jsDocs, maxLineLength) {
119
+ const issues = [];
120
+ for (const jsDoc of jsDocs) {
121
+ const blockStartLine = jsDoc.getStartLineNumber();
122
+ const blockLines = jsDoc.getText().split(/\r?\n/u);
123
+ for (let lineOffset = 0; lineOffset < blockLines.length; lineOffset += 1) {
124
+ const lineLength = blockLines[lineOffset].replace(/\t/gu, ' ').length;
125
+ if (lineLength > maxLineLength) {
126
+ issues.push(`JSDoc line too long (line ${blockStartLine + lineOffset}: ${lineLength} > ${maxLineLength})`);
127
+ }
128
+ }
129
+ }
130
+ return issues;
131
+ }
132
+ function validateJSDoc(node, jsDocs, maxLineLength) {
133
+ const issues = [];
134
+ if (jsDocs.length === 0)
135
+ return ['Missing JSDoc'];
136
+ const descriptions = jsDocs.map((jsDoc) => jsDoc.getDescription().trim()).filter(Boolean);
137
+ const allTags = jsDocs.flatMap((jsDoc) => jsDoc.getTags());
138
+ const hasDescriptionTag = allTags.some((tag) => tag.getTagName() === 'description');
139
+ if (descriptions.length === 0 && !hasDescriptionTag) {
140
+ issues.push('Missing description');
141
+ }
142
+ issues.push(...getLongJSDocLineIssues(jsDocs, maxLineLength));
143
+ if (!isFunctionLike(node))
144
+ return issues;
145
+ const params = Node.isFunctionDeclaration(node) ||
146
+ Node.isMethodDeclaration(node) ||
147
+ Node.isConstructorDeclaration(node) ||
148
+ Node.isArrowFunction(node) ||
149
+ Node.isFunctionExpression(node)
150
+ ? node.getParameters()
151
+ : [];
152
+ const hasDestructuredSignatureParam = params.some((param) => {
153
+ const name = param.getName();
154
+ return name.startsWith('{') || name.startsWith('[');
155
+ });
156
+ const signatureParams = params
157
+ .map((param) => {
158
+ const name = param.getName();
159
+ if (name.startsWith('{') || name.startsWith('['))
160
+ return null;
161
+ return {
162
+ name,
163
+ explicitTypeText: param.getTypeNode()?.getText() ?? null,
164
+ optional: param.isOptional() || param.hasInitializer(),
165
+ optionalSyntaxExplicit: param.hasQuestionToken() || param.hasInitializer(),
166
+ };
167
+ })
168
+ .filter((param) => param !== null);
169
+ const paramTags = allTags
170
+ .filter((tag) => tag.getTagName() === 'param')
171
+ .map((tag) => ({ parsed: parseParamTag(tag.getText()), rawText: tag.getText() }))
172
+ .filter((entry) => entry.parsed !== null);
173
+ const paramTagsByName = new Map();
174
+ for (const paramTag of paramTags) {
175
+ const list = paramTagsByName.get(paramTag.parsed.name) ?? [];
176
+ list.push(paramTag);
177
+ paramTagsByName.set(paramTag.parsed.name, list);
178
+ }
179
+ for (const param of signatureParams) {
180
+ const matchingTags = paramTagsByName.get(param.name) ?? [];
181
+ const paramTag = matchingTags[0];
182
+ if (!paramTag) {
183
+ issues.push(`Missing @param ${param.name}`);
184
+ continue;
185
+ }
186
+ if (matchingTags.length > 1) {
187
+ issues.push(`Duplicated @param ${param.name} (${matchingTags.length}x)`);
188
+ }
189
+ if (!paramTag.parsed.type) {
190
+ issues.push(`Missing type in @param ${param.name}`);
191
+ }
192
+ else if (param.explicitTypeText && isTypeComparable(param.explicitTypeText) && isTypeComparable(paramTag.parsed.type)) {
193
+ const signatureType = normalizeTypeText(param.explicitTypeText);
194
+ const jsDocType = normalizeTypeText(paramTag.parsed.type);
195
+ if (signatureType !== jsDocType) {
196
+ issues.push(`@param ${param.name} type mismatch (JSDoc: {${paramTag.parsed.type}} | signature: ${param.explicitTypeText})`);
197
+ }
198
+ }
199
+ if (!paramTag.parsed.hasSeparator) {
200
+ issues.push(`Missing separator " - " in @param ${param.name}`);
201
+ }
202
+ if (param.optionalSyntaxExplicit && paramTag.parsed.optionalSyntaxExplicit && param.optional !== paramTag.parsed.optional) {
203
+ issues.push(`@param optionality mismatch for ${param.name}`);
204
+ }
205
+ }
206
+ const signatureParamNames = new Set(signatureParams.map((param) => param.name));
207
+ for (const [paramName] of paramTagsByName.entries()) {
208
+ if (!signatureParamNames.has(paramName) && !paramName.includes('.') && !hasDestructuredSignatureParam) {
209
+ issues.push(`@param ${paramName} is not present in signature`);
210
+ }
211
+ }
212
+ if (Node.isFunctionDeclaration(node) || Node.isMethodDeclaration(node) || Node.isArrowFunction(node) || Node.isFunctionExpression(node)) {
213
+ const returnTypeText = node.getReturnType().getText();
214
+ const isVoid = returnTypeText === 'void' || returnTypeText === 'Promise<void>' || returnTypeText === 'undefined';
215
+ const returnTag = allTags.find((tag) => tag.getTagName() === 'returns' || tag.getTagName() === 'return');
216
+ if (!isVoid && !returnTag) {
217
+ issues.push('Missing @returns');
218
+ }
219
+ else if (returnTag) {
220
+ const returnTagType = extractTagType(returnTag.getText());
221
+ const explicitReturnTypeText = node.getReturnTypeNode()?.getText() ?? null;
222
+ if (!returnTagType) {
223
+ issues.push('Missing type in @returns');
224
+ }
225
+ else if (explicitReturnTypeText && isTypeComparable(explicitReturnTypeText) && isTypeComparable(returnTagType)) {
226
+ const signatureReturnType = normalizeTypeText(explicitReturnTypeText);
227
+ const jsDocReturnType = normalizeTypeText(returnTagType);
228
+ if (signatureReturnType !== jsDocReturnType) {
229
+ issues.push(`@returns type mismatch (JSDoc: {${returnTagType}} | signature: ${explicitReturnTypeText})`);
230
+ }
231
+ }
232
+ }
233
+ }
234
+ return issues;
235
+ }
236
+ function analyzeFile(sourceFile, rootDir, maxLineLength) {
237
+ const symbolIssues = [];
238
+ let totalSymbols = 0;
239
+ for (const func of sourceFile.getFunctions()) {
240
+ totalSymbols += 1;
241
+ const issues = validateJSDoc(func, func.getJsDocs(), maxLineLength);
242
+ if (issues.length > 0) {
243
+ symbolIssues.push({ name: func.getName() || '<anonymous>', line: func.getStartLineNumber(), type: 'function', issues });
244
+ }
245
+ }
246
+ for (const cls of sourceFile.getClasses()) {
247
+ totalSymbols += 1;
248
+ const className = cls.getName() || '<anonymous>';
249
+ const classIssues = validateJSDoc(cls, cls.getJsDocs(), maxLineLength);
250
+ if (classIssues.length > 0) {
251
+ symbolIssues.push({ name: className, line: cls.getStartLineNumber(), type: 'class', issues: classIssues });
252
+ }
253
+ for (const method of cls.getMethods()) {
254
+ totalSymbols += 1;
255
+ const issues = validateJSDoc(method, method.getJsDocs(), maxLineLength);
256
+ if (issues.length > 0) {
257
+ symbolIssues.push({ name: `${className}.${method.getName()}`, line: method.getStartLineNumber(), type: 'method', issues });
258
+ }
259
+ }
260
+ for (const prop of cls.getProperties()) {
261
+ totalSymbols += 1;
262
+ if (prop.getJsDocs().length === 0) {
263
+ symbolIssues.push({ name: `${className}.${prop.getName()}`, line: prop.getStartLineNumber(), type: 'property', issues: ['Missing JSDoc'] });
264
+ }
265
+ }
266
+ }
267
+ for (const iface of sourceFile.getInterfaces()) {
268
+ totalSymbols += 1;
269
+ const issues = validateJSDoc(iface, iface.getJsDocs(), maxLineLength);
270
+ if (issues.length > 0) {
271
+ symbolIssues.push({ name: iface.getName(), line: iface.getStartLineNumber(), type: 'interface', issues });
272
+ }
273
+ }
274
+ for (const typeAlias of sourceFile.getTypeAliases()) {
275
+ totalSymbols += 1;
276
+ const issues = validateJSDoc(typeAlias, typeAlias.getJsDocs(), maxLineLength);
277
+ if (issues.length > 0) {
278
+ symbolIssues.push({ name: typeAlias.getName(), line: typeAlias.getStartLineNumber(), type: 'type', issues });
279
+ }
280
+ }
281
+ for (const variableStatement of sourceFile.getVariableStatements()) {
282
+ for (const declaration of variableStatement.getDeclarations()) {
283
+ const initializer = declaration.getInitializer();
284
+ if (initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer))) {
285
+ totalSymbols += 1;
286
+ const issues = validateJSDoc(initializer, variableStatement.getJsDocs(), maxLineLength);
287
+ if (issues.length > 0) {
288
+ symbolIssues.push({ name: declaration.getName(), line: declaration.getStartLineNumber(), type: 'function', issues });
289
+ }
290
+ }
291
+ }
292
+ }
293
+ if (totalSymbols === 0 || symbolIssues.length === 0)
294
+ return null;
295
+ const documentedSymbols = totalSymbols - symbolIssues.length;
296
+ return {
297
+ filePath: path.relative(rootDir, sourceFile.getFilePath()),
298
+ totalSymbols,
299
+ documentedSymbols,
300
+ coverage: (documentedSymbols / totalSymbols) * 100,
301
+ symbolIssues,
302
+ };
303
+ }
304
+ function printReport(reports) {
305
+ console.info('');
306
+ console.info(colorize('JSDoc coverage report', `${colors.bright}${colors.cyan}`));
307
+ console.info('');
308
+ if (reports.length === 0) {
309
+ console.info(colorize('All analyzed symbols are documented.', colors.green));
310
+ return;
311
+ }
312
+ const sortedReports = [...reports].sort((left, right) => left.coverage - right.coverage);
313
+ const totalSymbols = reports.reduce((sum, report) => sum + report.totalSymbols, 0);
314
+ const totalDocumented = reports.reduce((sum, report) => sum + report.documentedSymbols, 0);
315
+ const overallCoverage = (totalDocumented / totalSymbols) * 100;
316
+ console.info(colorize('Review rules:', colors.bright));
317
+ console.info('- Change only JSDoc blocks unless an implementation change is explicitly requested.');
318
+ console.info('- Keep signatures, imports, exports, dependency choices, and runtime behavior unchanged.');
319
+ console.info('- Prefer English JSDoc and explicit types in @param and @returns.');
320
+ console.info('- Use @param {type} name - description syntax.');
321
+ console.info('');
322
+ console.info(colorize('Summary', colors.bright));
323
+ console.info(`Files with issues: ${colorize(String(reports.length), colors.yellow)}`);
324
+ console.info(`Total symbols: ${colorize(String(totalSymbols), colors.cyan)}`);
325
+ console.info(`Documented symbols: ${colorize(String(totalDocumented), colors.green)}`);
326
+ console.info(`Missing/invalid JSDoc: ${colorize(String(totalSymbols - totalDocumented), colors.red)}`);
327
+ console.info(`Overall coverage: ${colorize(`${overallCoverage.toFixed(1)}%`, overallCoverage >= 80 ? colors.green : overallCoverage >= 50 ? colors.yellow : colors.red)}`);
328
+ console.info('');
329
+ console.info(colorize('Top 10 files with issues', colors.bright));
330
+ for (const [index, report] of sortedReports.slice(0, 10).entries()) {
331
+ console.info('');
332
+ console.info(`${index + 1}. ${colorize(report.filePath, colors.bright)}`);
333
+ console.info(` Coverage: ${report.coverage.toFixed(1)}% (${report.documentedSymbols}/${report.totalSymbols})`);
334
+ for (const symbol of report.symbolIssues) {
335
+ console.info(` - ${symbol.type} ${symbol.name} (line ${symbol.line})`);
336
+ for (const issue of symbol.issues) {
337
+ console.info(` * ${issue}`);
338
+ }
339
+ }
340
+ }
341
+ if (sortedReports.length > 10) {
342
+ console.info('');
343
+ console.info(colorize(`...and ${sortedReports.length - 10} more files with issues.`, colors.gray));
344
+ }
345
+ }
346
+ function generateMarkdownReport(reports) {
347
+ const sortedReports = [...reports].sort((left, right) => left.coverage - right.coverage);
348
+ const totalSymbols = reports.reduce((sum, report) => sum + report.totalSymbols, 0);
349
+ const totalDocumented = reports.reduce((sum, report) => sum + report.documentedSymbols, 0);
350
+ const overallCoverage = totalSymbols === 0 ? 100 : (totalDocumented / totalSymbols) * 100;
351
+ const lines = [
352
+ '# JSDoc Coverage Report',
353
+ '',
354
+ `Generated at: ${new Date().toISOString()}`,
355
+ '',
356
+ '## Review Rules',
357
+ '',
358
+ '- Change only JSDoc blocks unless an implementation change is explicitly requested.',
359
+ '- Keep signatures, imports, exports, dependency choices, and runtime behavior unchanged.',
360
+ '- Prefer English JSDoc and explicit types in `@param` and `@returns`.',
361
+ '- Use `@param {type} name - description` syntax.',
362
+ '',
363
+ '## Summary',
364
+ '',
365
+ `- Files with issues: ${reports.length}`,
366
+ `- Total symbols: ${totalSymbols}`,
367
+ `- Documented symbols: ${totalDocumented}`,
368
+ `- Missing/invalid JSDoc: ${totalSymbols - totalDocumented}`,
369
+ `- Overall coverage: ${overallCoverage.toFixed(1)}%`,
370
+ '',
371
+ '## Files With Issues',
372
+ '',
373
+ ];
374
+ if (sortedReports.length === 0) {
375
+ lines.push('All analyzed symbols are documented.', '');
376
+ return lines.join('\n');
377
+ }
378
+ for (const [index, report] of sortedReports.entries()) {
379
+ lines.push(`### ${index + 1}. ${report.filePath}`);
380
+ lines.push('');
381
+ lines.push(`Coverage: ${report.coverage.toFixed(1)}% (${report.documentedSymbols}/${report.totalSymbols})`);
382
+ lines.push('');
383
+ for (const symbol of report.symbolIssues) {
384
+ lines.push(`- \`${symbol.name}\` (${symbol.type}, line ${symbol.line})`);
385
+ for (const issue of symbol.issues) {
386
+ lines.push(` - ${issue}`);
387
+ }
388
+ }
389
+ lines.push('');
390
+ }
391
+ return lines.join('\n');
392
+ }
393
+ async function shouldWriteMarkdown(promptForReport, rawArgs) {
394
+ if (rawArgs.includes('--write') || rawArgs.includes('--report'))
395
+ return true;
396
+ if (rawArgs.includes('--no-report'))
397
+ return false;
398
+ if (!promptForReport)
399
+ return false;
400
+ if (!input.isTTY || !output.isTTY)
401
+ return false;
402
+ const rl = createInterface({ input, output });
403
+ try {
404
+ const answer = await rl.question(`${colorize('Generate full Markdown report? (s/N): ', colors.cyan)}`);
405
+ return ['s', 'sim', 'y', 'yes'].includes(answer.trim().toLowerCase());
406
+ }
407
+ finally {
408
+ rl.close();
409
+ }
410
+ }
411
+ export async function runJSDocReport(runtime, rawArgs) {
412
+ const config = runtime.config.jsdocReport;
413
+ if (!config?.includePaths?.length) {
414
+ throw new Error('jsdocReport.includePaths is not configured.');
415
+ }
416
+ const maxLineLengthFromEnv = Number(process.env.JSDOC_MAX_LINE_LENGTH);
417
+ const maxLineLength = Number.isFinite(maxLineLengthFromEnv) && maxLineLengthFromEnv > 0
418
+ ? maxLineLengthFromEnv
419
+ : config.maxLineLength ?? 250;
420
+ const excludePatterns = (config.excludePatterns ?? defaultExcludePatterns).map((pattern) => new RegExp(pattern, 'u'));
421
+ const fileArgs = rawArgs.filter((arg) => !arg.startsWith('-'));
422
+ const files = fileArgs.length > 0
423
+ ? fileArgs.map((arg) => path.resolve(process.cwd(), arg)).filter(existsSync)
424
+ : collectFiles(runtime.cwd, config.includePaths, excludePatterns);
425
+ console.info(colorize(`Analyzing ${files.length} TypeScript file(s)...`, colors.cyan));
426
+ const project = new Project({
427
+ tsConfigFilePath: path.join(runtime.cwd, 'tsconfig.json'),
428
+ skipAddingFilesFromTsConfig: true,
429
+ });
430
+ const reports = [];
431
+ for (const [index, filePath] of files.entries()) {
432
+ if (files.length > 0) {
433
+ process.stdout.write(`\rJSDoc analysis ${index + 1}/${files.length}`);
434
+ }
435
+ try {
436
+ const sourceFile = project.addSourceFileAtPath(filePath);
437
+ const report = analyzeFile(sourceFile, runtime.cwd, maxLineLength);
438
+ if (report)
439
+ reports.push(report);
440
+ project.removeSourceFile(sourceFile);
441
+ }
442
+ catch (error) {
443
+ console.error(`\nFailed to analyze ${filePath}`);
444
+ console.error(error);
445
+ }
446
+ }
447
+ if (files.length > 0)
448
+ console.info('\n');
449
+ printReport(reports);
450
+ if (await shouldWriteMarkdown(config.promptForReport ?? false, rawArgs)) {
451
+ const outputPath = path.join(runtime.cwd, config.reportFile ?? 'temp_jsdocs_check.md');
452
+ writeFileSync(outputPath, generateMarkdownReport(reports), 'utf8');
453
+ console.info('');
454
+ console.info(colorize(`Markdown report generated: ${path.relative(runtime.cwd, outputPath)}`, colors.green));
455
+ }
456
+ }
@@ -0,0 +1,20 @@
1
+ export type CommandResult = {
2
+ code: number;
3
+ output: string;
4
+ };
5
+ export type CommandSpec = {
6
+ command: string;
7
+ args?: string[];
8
+ cwd?: string;
9
+ env?: Record<string, string>;
10
+ };
11
+ export declare function resolveCwd(rootDir: string, cwd?: string): string;
12
+ export declare function formatCommand(command: string, args?: string[]): string;
13
+ export declare function buildPackageManagerCommand(packageManager: string, args: string[]): CommandSpec;
14
+ export declare function buildFreshPackageManagerCommand(packageManager: string, args: string[]): CommandSpec;
15
+ export declare function resolveSpawnSpec(command: string, args?: string[]): {
16
+ command: string;
17
+ args: string[];
18
+ };
19
+ export declare function runCommandBuffered(spec: CommandSpec, rootDir: string): Promise<CommandResult>;
20
+ export declare function runCommandInherited(spec: CommandSpec, rootDir: string): number;
@@ -0,0 +1,86 @@
1
+ import { spawn, spawnSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ export function resolveCwd(rootDir, cwd) {
4
+ if (!cwd)
5
+ return rootDir;
6
+ return path.isAbsolute(cwd) ? cwd : path.join(rootDir, cwd);
7
+ }
8
+ export function formatCommand(command, args = []) {
9
+ return [command, ...args].map((arg) => (/[\s"]/u.test(arg) ? JSON.stringify(arg) : arg)).join(' ');
10
+ }
11
+ export function buildPackageManagerCommand(packageManager, args) {
12
+ const npmExecPath = process.env.npm_execpath;
13
+ if (npmExecPath && packageManager === 'pnpm') {
14
+ return { command: process.execPath, args: [npmExecPath, ...args] };
15
+ }
16
+ return {
17
+ command: process.platform === 'win32' && packageManager === 'pnpm' ? 'pnpm.cmd' : packageManager,
18
+ args,
19
+ };
20
+ }
21
+ export function buildFreshPackageManagerCommand(packageManager, args) {
22
+ return {
23
+ command: process.platform === 'win32' && packageManager === 'pnpm' ? 'pnpm.cmd' : packageManager,
24
+ args,
25
+ };
26
+ }
27
+ const WINDOWS_CMD_WRAPPED_COMMANDS = new Set([
28
+ 'npm',
29
+ 'npm.cmd',
30
+ 'pnpm',
31
+ 'pnpm.cmd',
32
+ 'webtoolkit',
33
+ 'webtoolkit.cmd',
34
+ 'yarn',
35
+ 'yarn.cmd',
36
+ ]);
37
+ export function resolveSpawnSpec(command, args = []) {
38
+ if (process.platform !== 'win32')
39
+ return { command, args };
40
+ if (!WINDOWS_CMD_WRAPPED_COMMANDS.has(command))
41
+ return { command, args };
42
+ return {
43
+ command: process.env.ComSpec ?? 'cmd.exe',
44
+ args: ['/d', '/s', '/c', command.replace(/\.cmd$/iu, ''), ...args],
45
+ };
46
+ }
47
+ export function runCommandBuffered(spec, rootDir) {
48
+ const resolved = resolveSpawnSpec(spec.command, spec.args ?? []);
49
+ const child = spawn(resolved.command, resolved.args, {
50
+ cwd: resolveCwd(rootDir, spec.cwd),
51
+ env: {
52
+ ...process.env,
53
+ FORCE_COLOR: '1',
54
+ ...(spec.env ?? {}),
55
+ },
56
+ shell: false,
57
+ stdio: ['ignore', 'pipe', 'pipe'],
58
+ });
59
+ let output = '';
60
+ child.stdout?.on('data', (chunk) => {
61
+ output += chunk.toString();
62
+ });
63
+ child.stderr?.on('data', (chunk) => {
64
+ output += chunk.toString();
65
+ });
66
+ return new Promise((resolve, reject) => {
67
+ child.on('error', reject);
68
+ child.on('close', (code) => resolve({ code: code ?? 1, output }));
69
+ });
70
+ }
71
+ export function runCommandInherited(spec, rootDir) {
72
+ const resolved = resolveSpawnSpec(spec.command, spec.args ?? []);
73
+ const result = spawnSync(resolved.command, resolved.args, {
74
+ cwd: resolveCwd(rootDir, spec.cwd),
75
+ env: {
76
+ ...process.env,
77
+ FORCE_COLOR: '1',
78
+ ...(spec.env ?? {}),
79
+ },
80
+ stdio: 'inherit',
81
+ });
82
+ if (result.error) {
83
+ throw result.error;
84
+ }
85
+ return result.status ?? 1;
86
+ }
@@ -0,0 +1,7 @@
1
+ import type { WebToolkitCliConfig } from './config.js';
2
+ type Runtime = {
3
+ cwd: string;
4
+ config: WebToolkitCliConfig;
5
+ };
6
+ export declare function runReadyService(_runtime: Runtime, rawArgs: string[]): Promise<void>;
7
+ export {};