publint-all.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import { execFile } from 'node:child_process'
  2. import { existsSync, readdirSync } from 'node:fs'
  3. import { availableParallelism } from 'node:os'
  4. import { resolve } from 'node:path'
  5. import { promisify } from 'node:util'
  6. const execFileAsync = promisify(execFile)
  7. const CONCURRENCY_ENV = 'DSH_PUBLINT_CONCURRENCY'
  8. // publint every harness package. Packages live at packages/<group>/<pkg>
  9. // (the group dirs — core/llm/bash/… — are pure containers); vendor/ is private
  10. // upstream code and examples/ are not packages, both out of scope. Derived
  11. // from the hierarchy so a new package needs no edit here.
  12. const root = resolve(import.meta.dirname, '..')
  13. const packagesRoot = resolve(root, 'packages')
  14. type PublintResult =
  15. | { path: string; status: 'passed'; stdout: string; stderr: string }
  16. | { path: string; status: 'failed'; stdout: string; stderr: string; message: string }
  17. function workspacePackages(): string[] {
  18. return readdirSync(packagesRoot, { withFileTypes: true })
  19. .filter(group => group.isDirectory())
  20. .flatMap(group =>
  21. readdirSync(resolve(packagesRoot, group.name), { withFileTypes: true })
  22. .filter(pkg => pkg.isDirectory())
  23. .filter(pkg => existsSync(resolve(packagesRoot, group.name, pkg.name, 'package.json')))
  24. .map(pkg => `packages/${group.name}/${pkg.name}`),
  25. )
  26. }
  27. function publintConcurrency(total: number): number {
  28. if (total === 0) return 0
  29. const raw = process.env[CONCURRENCY_ENV]
  30. if (raw !== undefined) {
  31. const parsed = Number.parseInt(raw, 10)
  32. if (!Number.isSafeInteger(parsed) || parsed < 1) {
  33. throw new Error(`publint-all: ${CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`)
  34. }
  35. return Math.min(total, parsed)
  36. }
  37. return Math.min(total, availableParallelism())
  38. }
  39. function outputText(value: unknown): string {
  40. if (typeof value === 'string') return value
  41. if (Buffer.isBuffer(value)) return value.toString()
  42. return ''
  43. }
  44. async function runPublint(path: string): Promise<PublintResult> {
  45. try {
  46. const { stdout, stderr } = await execFileAsync('node_modules/.bin/publint', [path], {
  47. cwd: root,
  48. encoding: 'utf8',
  49. maxBuffer: 10 * 1024 * 1024,
  50. })
  51. return { path, status: 'passed', stdout, stderr }
  52. } catch (error: unknown) {
  53. const failed = error as { stdout?: unknown; stderr?: unknown; message?: string }
  54. return {
  55. path,
  56. status: 'failed',
  57. stdout: outputText(failed.stdout),
  58. stderr: outputText(failed.stderr),
  59. message: failed.message ?? 'publint failed',
  60. }
  61. }
  62. }
  63. async function runAll(paths: string[], concurrency: number): Promise<PublintResult[]> {
  64. let next = 0
  65. const results: Array<PublintResult | undefined> = []
  66. await Promise.all(Array.from({ length: concurrency }, async () => {
  67. for (;;) {
  68. const index = next
  69. next += 1
  70. const path = paths[index]
  71. if (path === undefined) return
  72. results[index] = await runPublint(path)
  73. }
  74. }))
  75. return paths.map((path, index) => {
  76. const result = results[index]
  77. if (result === undefined) throw new Error(`publint-all: missing result for ${path}.`)
  78. return result
  79. })
  80. }
  81. function printResult(result: PublintResult): void {
  82. console.log(`Running publint for ${result.path}...`)
  83. process.stdout.write(result.stdout)
  84. process.stderr.write(result.stderr)
  85. if (result.status === 'failed') console.error(result.message)
  86. }
  87. const packages = workspacePackages()
  88. const concurrency = publintConcurrency(packages.length)
  89. console.log(`publint-all: linting ${packages.length} package(s) with ${concurrency} worker(s).`)
  90. const results = await runAll(packages, concurrency)
  91. for (const result of results) printResult(result)
  92. if (results.some(result => result.status === 'failed')) process.exit(1)