@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,359 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import readline from 'node:readline/promises';
5
+ function toPosix(value) {
6
+ return value.replace(/\\/g, '/').split(path.sep).join('/');
7
+ }
8
+ function compilePatterns(patterns) {
9
+ return patterns.map((pattern) => new RegExp(pattern, 'i'));
10
+ }
11
+ async function isDirectory(target) {
12
+ try {
13
+ const stat = await fs.stat(target);
14
+ return stat.isDirectory();
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ async function readDirSafe(target) {
21
+ try {
22
+ return await fs.readdir(target);
23
+ }
24
+ catch {
25
+ return [];
26
+ }
27
+ }
28
+ function isWorkspaceRoot(relPath, workspaceRootNames) {
29
+ const normalized = toPosix(relPath);
30
+ if (normalized === '')
31
+ return true;
32
+ const parts = normalized.split('/').filter(Boolean);
33
+ if (parts.length !== 2)
34
+ return false;
35
+ return workspaceRootNames.has(parts[0]) && parts[1].length > 0;
36
+ }
37
+ function isProtectedRoot(relFromRoot, protectedRootNames) {
38
+ const parts = toPosix(relFromRoot).split('/').filter(Boolean);
39
+ return parts.length === 1 && protectedRootNames.has(parts[0]);
40
+ }
41
+ function shouldRemoveFile(fileName, relPath, config) {
42
+ const normalizedRelPath = toPosix(relPath);
43
+ const patterns = compilePatterns(config.removableFilePatterns);
44
+ if (config.removableSpecificFiles.map(toPosix).includes(normalizedRelPath))
45
+ return true;
46
+ if (config.removableFileNames.includes(fileName))
47
+ return true;
48
+ if (config.removableFileSuffixes.some((suffix) => fileName.endsWith(suffix)))
49
+ return true;
50
+ if (config.removableFilePrefixes.some((prefix) => fileName.startsWith(prefix)))
51
+ return true;
52
+ return patterns.some((pattern) => pattern.test(fileName));
53
+ }
54
+ async function removeTarget(targetPath, options) {
55
+ if (options.dryRun)
56
+ return;
57
+ await fs.rm(targetPath, { recursive: true, force: true });
58
+ }
59
+ async function collectArtifactRemovals(dirPath, options, levelConfig, runtime, relPath = '', removals = []) {
60
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
61
+ const workspaceRootNames = new Set(runtime.config.cleaner.workspaceRootNames);
62
+ const skipArtifactDirNames = new Set(runtime.config.cleaner.skipArtifactDirNames);
63
+ const currentIsWorkspaceRoot = isWorkspaceRoot(relPath, workspaceRootNames);
64
+ for (const entry of entries) {
65
+ const entryPath = path.join(dirPath, entry.name);
66
+ const entryRelPath = relPath ? path.join(relPath, entry.name) : entry.name;
67
+ if (entry.isDirectory()) {
68
+ if (skipArtifactDirNames.has(entry.name))
69
+ continue;
70
+ if (currentIsWorkspaceRoot && levelConfig.removableDirNames.includes(entry.name)) {
71
+ removals.push({ kind: 'dir', relPath: entryRelPath });
72
+ await removeTarget(entryPath, options);
73
+ continue;
74
+ }
75
+ await collectArtifactRemovals(entryPath, options, levelConfig, runtime, entryRelPath, removals);
76
+ continue;
77
+ }
78
+ if (entry.isFile() && shouldRemoveFile(entry.name, entryRelPath, levelConfig)) {
79
+ removals.push({ kind: 'file', relPath: entryRelPath });
80
+ await removeTarget(entryPath, options);
81
+ }
82
+ }
83
+ return removals;
84
+ }
85
+ async function cleanupEmptyDirectories(dir, relFromRoot, options, runtime, removals) {
86
+ const dirName = path.basename(dir);
87
+ const skipEmptyDirNames = new Set(runtime.config.cleaner.skipEmptyDirNames);
88
+ const protectedRootNames = new Set(runtime.config.cleaner.protectedRootNames);
89
+ if (skipEmptyDirNames.has(dirName))
90
+ return false;
91
+ const protectedRoot = isProtectedRoot(relFromRoot, protectedRootNames);
92
+ const entries = await readDirSafe(dir);
93
+ if (entries.length === 0) {
94
+ if (protectedRoot)
95
+ return false;
96
+ removals.push({ kind: 'empty-dir', relPath: relFromRoot || path.basename(dir) });
97
+ if (!options.dryRun)
98
+ await fs.rmdir(dir);
99
+ return true;
100
+ }
101
+ for (const entry of entries) {
102
+ const entryPath = path.join(dir, entry);
103
+ const entryRelPath = path.join(relFromRoot, entry);
104
+ if (await isDirectory(entryPath)) {
105
+ await cleanupEmptyDirectories(entryPath, entryRelPath, options, runtime, removals);
106
+ }
107
+ }
108
+ const remaining = await readDirSafe(dir);
109
+ if (remaining.length === 0) {
110
+ if (protectedRoot)
111
+ return false;
112
+ removals.push({ kind: 'empty-dir', relPath: relFromRoot || path.basename(dir) });
113
+ if (!options.dryRun)
114
+ await fs.rmdir(dir);
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+ export function parseLevel(value) {
120
+ const normalized = value.trim().toLowerCase();
121
+ if (normalized === 'empty')
122
+ return 'empty';
123
+ if (normalized === 'cache')
124
+ return 'cache';
125
+ if (normalized === 'deep')
126
+ return 'deep';
127
+ if (normalized === 'nuclear')
128
+ return 'nuclear';
129
+ return null;
130
+ }
131
+ function parseReinstallPolicy(value) {
132
+ const normalized = value.trim().toLowerCase();
133
+ if (normalized === 'ask')
134
+ return 'ask';
135
+ if (normalized === 'always')
136
+ return 'always';
137
+ if (normalized === 'never')
138
+ return 'never';
139
+ return null;
140
+ }
141
+ export function printCleanHelp() {
142
+ console.info('Usage: webtoolkit clean [options]');
143
+ console.info('');
144
+ console.info('Options:');
145
+ console.info(' --level <empty|cache|deep|nuclear> Set cleanup level directly.');
146
+ console.info(' --interactive Force interactive level prompt.');
147
+ console.info(' --dry-run Print removals without deleting.');
148
+ console.info(' --no-store-prune Skip package-manager store prune in nuclear mode.');
149
+ console.info(' --reinstall <ask|always|never> Control dependency reinstall in nuclear mode.');
150
+ console.info(' --help Show this help message.');
151
+ }
152
+ export function parseCleanArgs(argv) {
153
+ const options = {
154
+ level: undefined,
155
+ dryRun: false,
156
+ noStorePrune: false,
157
+ interactive: false,
158
+ reinstall: 'ask',
159
+ };
160
+ for (let index = 0; index < argv.length; index += 1) {
161
+ const arg = argv[index];
162
+ if (arg === '--') {
163
+ continue;
164
+ }
165
+ if (arg === '--help' || arg === '-h') {
166
+ printCleanHelp();
167
+ process.exit(0);
168
+ }
169
+ if (arg === '--dry-run') {
170
+ options.dryRun = true;
171
+ continue;
172
+ }
173
+ if (arg === '--no-store-prune') {
174
+ options.noStorePrune = true;
175
+ continue;
176
+ }
177
+ if (arg === '--interactive') {
178
+ options.interactive = true;
179
+ continue;
180
+ }
181
+ if (arg === '--level') {
182
+ const next = argv[index + 1];
183
+ if (!next)
184
+ throw new Error('Missing value for `--level`.');
185
+ const level = parseLevel(next);
186
+ if (!level)
187
+ throw new Error(`Invalid level: ${next}`);
188
+ options.level = level;
189
+ index += 1;
190
+ continue;
191
+ }
192
+ if (arg.startsWith('--level=')) {
193
+ const level = parseLevel(arg.slice('--level='.length));
194
+ if (!level)
195
+ throw new Error(`Invalid level: ${arg}`);
196
+ options.level = level;
197
+ continue;
198
+ }
199
+ if (arg === '--reinstall') {
200
+ const next = argv[index + 1];
201
+ if (!next)
202
+ throw new Error('Missing value for `--reinstall`.');
203
+ const policy = parseReinstallPolicy(next);
204
+ if (!policy)
205
+ throw new Error(`Invalid reinstall policy: ${next}`);
206
+ options.reinstall = policy;
207
+ index += 1;
208
+ continue;
209
+ }
210
+ if (arg.startsWith('--reinstall=')) {
211
+ const policy = parseReinstallPolicy(arg.slice('--reinstall='.length));
212
+ if (!policy)
213
+ throw new Error(`Invalid reinstall policy: ${arg}`);
214
+ options.reinstall = policy;
215
+ continue;
216
+ }
217
+ throw new Error(`Unknown option: ${arg}`);
218
+ }
219
+ return options;
220
+ }
221
+ async function chooseLevelInteractively() {
222
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
223
+ throw new Error('Interactive mode requires a TTY shell.');
224
+ }
225
+ const rl = readline.createInterface({
226
+ input: process.stdin,
227
+ output: process.stdout,
228
+ });
229
+ try {
230
+ console.info('Choose cleanup level:');
231
+ console.info(' 1) empty - Remove empty directories only');
232
+ console.info(' 2) cache - Remove cache/temp artifacts');
233
+ console.info(' 3) deep - Cache + build artifacts (without node_modules)');
234
+ console.info(' 4) nuclear - Deep + node_modules + package-manager store prune');
235
+ const answer = (await rl.question('Level [1-4, default 2]: ')).trim().toLowerCase();
236
+ if (answer === '' || answer === '2' || answer === 'cache')
237
+ return 'cache';
238
+ if (answer === '1' || answer === 'empty')
239
+ return 'empty';
240
+ if (answer === '3' || answer === 'deep')
241
+ return 'deep';
242
+ if (answer === '4' || answer === 'nuclear')
243
+ return 'nuclear';
244
+ throw new Error(`Invalid level choice: ${answer}`);
245
+ }
246
+ finally {
247
+ rl.close();
248
+ }
249
+ }
250
+ export function resolvePackageManagerCommand(packageManager, commandArgs) {
251
+ if (process.platform === 'win32') {
252
+ return {
253
+ command: 'cmd.exe',
254
+ args: ['/d', '/s', '/c', packageManager, ...commandArgs],
255
+ };
256
+ }
257
+ return {
258
+ command: packageManager,
259
+ args: commandArgs,
260
+ };
261
+ }
262
+ function runPackageManagerCommand(runtime, commandArgs, label) {
263
+ const command = resolvePackageManagerCommand(runtime.config.packageManager, commandArgs);
264
+ const result = spawnSync(command.command, command.args, {
265
+ cwd: runtime.cwd,
266
+ stdio: 'inherit',
267
+ shell: false,
268
+ });
269
+ if (result.error) {
270
+ throw new Error(`Failed to run ${label}: ${result.error.message}`);
271
+ }
272
+ if (result.status && result.status !== 0) {
273
+ throw new Error(`${label} failed with exit code ${result.status}.`);
274
+ }
275
+ }
276
+ async function shouldReinstallDependencies(options) {
277
+ if (options.reinstall === 'always')
278
+ return true;
279
+ if (options.reinstall === 'never')
280
+ return false;
281
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
282
+ console.info('Skipping reinstall prompt because this shell is not interactive.');
283
+ return false;
284
+ }
285
+ const rl = readline.createInterface({
286
+ input: process.stdin,
287
+ output: process.stdout,
288
+ });
289
+ try {
290
+ const answer = (await rl.question('Reinstall all dependencies now with package manager install --force? [y/N] ')).trim().toLowerCase();
291
+ return answer === 'y' || answer === 'yes' || answer === 's' || answer === 'sim';
292
+ }
293
+ finally {
294
+ rl.close();
295
+ }
296
+ }
297
+ function printSummary(level, options, levelConfig, removals) {
298
+ const normalized = removals.map((item) => ({ ...item, relPath: toPosix(item.relPath) }));
299
+ const dirCount = normalized.filter((item) => item.kind === 'dir').length;
300
+ const emptyDirCount = normalized.filter((item) => item.kind === 'empty-dir').length;
301
+ const fileCount = normalized.filter((item) => item.kind === 'file').length;
302
+ console.info(`${options.dryRun ? '[dry-run] ' : ''}${levelConfig.label}: removed ${dirCount} artifact director${dirCount === 1 ? 'y' : 'ies'}, ${emptyDirCount} empty director${emptyDirCount === 1 ? 'y' : 'ies'} and ${fileCount} file${fileCount === 1 ? '' : 's'}.`);
303
+ if (normalized.length === 0) {
304
+ console.info('No removable targets found for this level.');
305
+ return;
306
+ }
307
+ for (const removal of normalized.sort((a, b) => a.relPath.localeCompare(b.relPath))) {
308
+ console.info(` - [${removal.kind}] ${removal.relPath}`);
309
+ }
310
+ if (level === 'nuclear' && !options.dryRun) {
311
+ console.info('Nuclear cleanup completed.');
312
+ }
313
+ }
314
+ export async function runCleaner(options, runtime) {
315
+ const interactiveShell = process.stdin.isTTY && process.stdout.isTTY;
316
+ let level = options.level;
317
+ if (!level && (options.interactive || interactiveShell)) {
318
+ level = await chooseLevelInteractively();
319
+ }
320
+ if (!level) {
321
+ level = 'cache';
322
+ }
323
+ const levelConfig = runtime.config.cleaner.levels[level];
324
+ const removals = [];
325
+ const hasArtifactTargets = levelConfig.removableDirNames.length > 0 ||
326
+ levelConfig.removableFileNames.length > 0 ||
327
+ levelConfig.removableSpecificFiles.length > 0 ||
328
+ levelConfig.removableFileSuffixes.length > 0 ||
329
+ levelConfig.removableFilePrefixes.length > 0 ||
330
+ levelConfig.removableFilePatterns.length > 0;
331
+ if (hasArtifactTargets) {
332
+ await collectArtifactRemovals(runtime.cwd, options, levelConfig, runtime, '', removals);
333
+ }
334
+ if (levelConfig.removeEmptyDirs) {
335
+ const topEntries = await readDirSafe(runtime.cwd);
336
+ for (const entry of topEntries) {
337
+ const target = path.join(runtime.cwd, entry);
338
+ if (await isDirectory(target)) {
339
+ await cleanupEmptyDirectories(target, entry, options, runtime, removals);
340
+ }
341
+ }
342
+ }
343
+ printSummary(level, options, levelConfig, removals);
344
+ if (level !== 'nuclear' || options.dryRun)
345
+ return removals;
346
+ if (options.noStorePrune) {
347
+ console.info(`Skipped \`${runtime.config.packageManager} store prune\`.`);
348
+ }
349
+ else {
350
+ runPackageManagerCommand(runtime, ['store', 'prune'], `${runtime.config.packageManager} store prune`);
351
+ }
352
+ if (await shouldReinstallDependencies(options)) {
353
+ console.info('Reinstalling dependencies...');
354
+ runPackageManagerCommand(runtime, ['install', '--force'], `${runtime.config.packageManager} install --force`);
355
+ return removals;
356
+ }
357
+ console.info('Skipped dependency reinstall.');
358
+ return removals;
359
+ }
@@ -0,0 +1,8 @@
1
+ type JsonSchema = Record<string, unknown>;
2
+ export declare const configSchema: JsonSchema;
3
+ export declare function validateConfig(value: unknown): void;
4
+ export declare function configSectionNames(): string[];
5
+ export declare function getConfigSchema(section?: string): JsonSchema;
6
+ export declare function formatConfigHelp(section?: string): string;
7
+ export declare function runConfigReference(args: string[]): void;
8
+ export {};