verify-public-repository-links.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. /** Reject tracked files that reference an unavailable legacy repository. */
  2. import { execFileSync } from 'node:child_process'
  3. import { existsSync, lstatSync, readFileSync, readlinkSync } from 'node:fs'
  4. import { resolve } from 'node:path'
  5. import { pathToFileURL } from 'node:url'
  6. const root = resolve(import.meta.dirname, '..')
  7. const unavailableOwner = ['deepseek', 'ai'].join('-')
  8. const unavailableRepositoryName = ['deepseek', 'harness', 'sdk'].join('-')
  9. const unavailableRepository = `${unavailableOwner}/${unavailableRepositoryName}`
  10. const archivedAgentNotePrefix = '.agents/notes/archived/'
  11. const namedReferenceCharacters: Readonly<Record<string, string>> = {
  12. hyphen: '-',
  13. num: '#',
  14. period: '.',
  15. colon: ':',
  16. sol: '/',
  17. }
  18. /**
  19. * Normalize escaped repository references for source-text policy checks.
  20. * @param source - Source text containing literal, encoded, or compatibility characters.
  21. * @returns Lowercase text with URL, JavaScript, and HTML character escapes decoded.
  22. */
  23. export function canonicalReferenceText(source: string): string {
  24. return source
  25. .normalize('NFKC')
  26. .replaceAll('\\/', '/')
  27. .replace(/\\(?:u([\da-f]{4})|x([\da-f]{2}))/gi, (_match, unicode: string | undefined, byte: string | undefined) =>
  28. String.fromCodePoint(Number.parseInt(unicode ?? byte ?? '', 16)))
  29. .replace(/%([\da-f]{2})/gi, (_match, code: string) => String.fromCodePoint(Number.parseInt(code, 16)))
  30. .replace(/&#(?:(\d+)|x([\da-f]+));/gi, (entity, decimal: string | undefined, hexadecimal: string | undefined) => {
  31. const code = Number.parseInt(decimal ?? hexadecimal ?? '', decimal === undefined ? 16 : 10)
  32. return code <= 0x10ffff ? String.fromCodePoint(code) : entity
  33. })
  34. .replace(/&(hyphen|num|period|colon|sol);/gi, (entity, name: string) => namedReferenceCharacters[name.toLowerCase()] ?? entity)
  35. .normalize('NFKC')
  36. .toLowerCase()
  37. }
  38. /** One tracked reference to the unavailable repository. */
  39. export interface UnavailableRepositoryReference {
  40. /** Repository-relative file path. */
  41. file: string
  42. /** One-based source line. */
  43. line: number
  44. }
  45. /**
  46. * Locate unavailable-repository references in one active text file.
  47. * @param file - Repository-relative path used in diagnostics.
  48. * @param source - Text to inspect.
  49. * @returns every matching source line, excluding frozen archived Agent Notes.
  50. */
  51. export function findUnavailableRepositoryReferences(file: string, source: string): UnavailableRepositoryReference[] {
  52. if (file.startsWith(archivedAgentNotePrefix)) return []
  53. const references: UnavailableRepositoryReference[] = []
  54. for (const [index, line] of source.split('\n').entries()) {
  55. const canonicalLine = canonicalReferenceText(line)
  56. if (canonicalLine.includes(unavailableRepository)) references.push({ file, line: index + 1 })
  57. }
  58. return references
  59. }
  60. function trackedFiles(repoRoot: string): string[] {
  61. return execFileSync('git', ['ls-files', '-z'], { cwd: repoRoot, encoding: 'utf8' })
  62. .split('\0')
  63. .filter(file => file !== '')
  64. }
  65. function scanRepository(repoRoot: string): UnavailableRepositoryReference[] {
  66. const references: UnavailableRepositoryReference[] = []
  67. for (const file of trackedFiles(repoRoot)) {
  68. const path = resolve(repoRoot, file)
  69. if (!existsSync(path)) continue
  70. const stat = lstatSync(path)
  71. if (!stat.isFile() && !stat.isSymbolicLink()) continue
  72. const source = stat.isSymbolicLink() ? readlinkSync(path) : readFileSync(path, 'utf8')
  73. if (source.includes('\0')) continue
  74. references.push(...findUnavailableRepositoryReferences(file, source))
  75. }
  76. return references
  77. }
  78. const invokedPath = process.argv[1]
  79. const isMain = invokedPath !== undefined && import.meta.url === pathToFileURL(resolve(invokedPath)).href
  80. if (isMain) {
  81. const references = scanRepository(root)
  82. if (references.length === 0) {
  83. console.log('verify-public-repository-links: tracked files reference no unavailable repository.')
  84. } else {
  85. console.error('verify-public-repository-links: unavailable repository references found:')
  86. for (const reference of references) console.error(` ${reference.file}:${String(reference.line)}`)
  87. process.exitCode = 1
  88. }
  89. }