@zgeoff/chex 0.0.1 → 0.0.3

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 (54) hide show
  1. package/README.md +96 -3
  2. package/lefthook.yml +123 -0
  3. package/package.json +31 -3
  4. package/src/build-init-package.ts +57 -0
  5. package/src/build-init-plan.ts +96 -0
  6. package/src/build-lefthook-extends.ts +117 -0
  7. package/src/check-names.ts +18 -0
  8. package/src/cli.ts +49 -0
  9. package/src/collect-command-names.ts +297 -0
  10. package/src/collect-expression-references.ts +17 -0
  11. package/src/dependency-gates/collect-stale-age-gate-excludes.ts +83 -0
  12. package/src/dependency-gates/collect-unneeded-audit-ignores.ts +41 -0
  13. package/src/dependency-gates/collect-unneeded-overrides.ts +70 -0
  14. package/src/dependency-gates/first-party-scopes.ts +1 -0
  15. package/src/dependency-gates/is-record.ts +4 -0
  16. package/src/dependency-gates/parse-audit-advisories.ts +33 -0
  17. package/src/dependency-gates/parse-audit-ignore-ids.ts +15 -0
  18. package/src/dependency-gates/parse-fixable-packages.ts +29 -0
  19. package/src/dependency-gates/parse-lockfile-dependents.ts +77 -0
  20. package/src/dependency-gates/parse-root-manifest.ts +68 -0
  21. package/src/dependency-gates/read-dependency-gate-findings.ts +91 -0
  22. package/src/dependency-gates/run-dependency-gates-check.ts +54 -0
  23. package/src/dependency-gates/types.ts +19 -0
  24. package/src/docs/check-length.sh +117 -0
  25. package/src/docs/check-links.sh +245 -0
  26. package/src/docs/check-prose.sh +657 -0
  27. package/src/docs/check-readmes.sh +102 -0
  28. package/src/docs/check-skill.sh +413 -0
  29. package/src/docs/check-vision.sh +34 -0
  30. package/src/find-install-cache-path.ts +16 -0
  31. package/src/format-commit-lines.ts +3 -0
  32. package/src/make-check-runner.ts +40 -0
  33. package/src/parse-added-lines.ts +112 -0
  34. package/src/parse-chex-args.ts +76 -0
  35. package/src/parse-lane-limits.ts +59 -0
  36. package/src/parse-pushed-refs.ts +29 -0
  37. package/src/parse-shell-words.ts +520 -0
  38. package/src/pick-merge-blobs-command.ts +152 -0
  39. package/src/read-branch-base.ts +31 -0
  40. package/src/read-branch-files.ts +59 -0
  41. package/src/read-checks-profile.ts +57 -0
  42. package/src/read-pushed-ranges.ts +47 -0
  43. package/src/read-pushed-refs-input.ts +18 -0
  44. package/src/read-range-commits.ts +30 -0
  45. package/src/run-chex.ts +62 -0
  46. package/src/run-commits-check.ts +178 -0
  47. package/src/run-init.ts +129 -0
  48. package/src/run-local-paths-check.ts +185 -0
  49. package/src/run-program.ts +38 -0
  50. package/src/run-test-timeouts-check.ts +498 -0
  51. package/src/run-uncommitted-check.ts +82 -0
  52. package/src/run-workflows-check.ts +613 -0
  53. package/src/shell-escape-parsers.ts +197 -0
  54. package/src/write-text-file.ts +7 -0
@@ -0,0 +1,185 @@
1
+ import { hostname } from 'node:os';
2
+ import { formatCommitLines } from './format-commit-lines.ts';
3
+ import { parseAddedLines } from './parse-added-lines.ts';
4
+ import type { MergeBlobsReadings } from './pick-merge-blobs-command.ts';
5
+ import { pickMergeBlobsCommand } from './pick-merge-blobs-command.ts';
6
+ import { readPushedRanges } from './read-pushed-ranges.ts';
7
+ import { readPushedRefsInput } from './read-pushed-refs-input.ts';
8
+ import type { CommitRange } from './read-range-commits.ts';
9
+ import { readRangeCommits } from './read-range-commits.ts';
10
+ import type { RunProgram } from './run-program.ts';
11
+ import { runProgram } from './run-program.ts';
12
+
13
+ export interface LocalPathsCheckOptions {
14
+ readonly cwd: string;
15
+
16
+ readonly ranges: readonly CommitRange[];
17
+
18
+ readonly env: Readonly<Record<string, string | undefined>>;
19
+
20
+ readonly host: string;
21
+
22
+ readonly runProgram: RunProgram;
23
+ }
24
+
25
+ export interface LocalPathFinding {
26
+ // Relative to the repo root.
27
+ readonly path: string;
28
+
29
+ // The line's number in the file as the commit that adds it leaves it.
30
+ readonly line: number;
31
+
32
+ readonly message: string;
33
+ }
34
+
35
+ // Every commit is read, because a later commit that removes the line still pushes the one that adds
36
+ // it.
37
+ export function runLocalPathsCheck(options: Readonly<LocalPathsCheckOptions>): LocalPathFinding[] {
38
+ const home = findHomePrefix(options.env['HOME']);
39
+ const host = findHostPattern(options.host);
40
+
41
+ if (home === null && host === null) {
42
+ return [];
43
+ }
44
+
45
+ const commits = readRangeCommits(options, options.ranges);
46
+
47
+ // git log reads HEAD when stdin names no commit.
48
+ if (commits.length === 0) {
49
+ return [];
50
+ }
51
+
52
+ runMergeBlobsFetch(options, commits);
53
+
54
+ const result = options.runProgram(
55
+ [
56
+ 'git',
57
+ '-c',
58
+ 'core.quotePath=false',
59
+ 'log',
60
+ '-p',
61
+ '-U0',
62
+ '-M',
63
+ '--no-color',
64
+ '--no-ext-diff',
65
+ '--no-textconv',
66
+
67
+ // A merge shows the lines its author adds beyond git's own merge, such as a conflict's
68
+ // resolution; the lines it brings from either parent come from commits the log reads anyway.
69
+ '--diff-merges=remerge',
70
+ '--format=%x00%h',
71
+ '--no-walk=unsorted',
72
+ '--stdin',
73
+ ],
74
+ { cwd: options.cwd, env: options.env, stdin: formatCommitLines(commits) },
75
+ );
76
+
77
+ if (result.exitCode !== 0) {
78
+ throw new Error(`git log failed: ${result.stderr.trim()}`);
79
+ }
80
+
81
+ return parseAddedLines(result.stdout).flatMap((added) => {
82
+ const findings: LocalPathFinding[] = [];
83
+
84
+ if (home !== null && added.text.includes(home)) {
85
+ findings.push({
86
+ path: added.path,
87
+ line: added.line,
88
+ message: `commit ${added.hash} adds the home directory ${home}; remove it before you push`,
89
+ });
90
+ }
91
+
92
+ if (host !== null && host.pattern.test(added.text)) {
93
+ findings.push({
94
+ path: added.path,
95
+ line: added.line,
96
+ message: `commit ${added.hash} adds the host name ${host.name}; remove it before you push`,
97
+ });
98
+ }
99
+
100
+ return findings;
101
+ });
102
+ }
103
+
104
+ // A trailing slash keeps `/home/ada` from matching `/home/adam`, and a home of `/` would match
105
+ // every URL.
106
+ function findHomePrefix(home: string | undefined): string | null {
107
+ const trimmed = home?.replace(/\/+$/u, '') ?? '';
108
+
109
+ return trimmed === '' ? null : `${trimmed}/`;
110
+ }
111
+
112
+ // A whole word, as `grep -w` reads one. `localhost` is in every repo's configs.
113
+ function findHostPattern(name: string): { name: string; pattern: RegExp } | null {
114
+ if (name === '' || name === 'localhost') {
115
+ return null;
116
+ }
117
+
118
+ const escaped = name.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
119
+
120
+ return { name, pattern: new RegExp(`(?<![A-Za-z0-9_])${escaped}(?![A-Za-z0-9_])`, 'u') };
121
+ }
122
+
123
+ function runMergeBlobsFetch(
124
+ options: Readonly<LocalPathsCheckOptions>,
125
+ commits: readonly string[],
126
+ ): void {
127
+ let readings: MergeBlobsReadings = { commits, bases: [] };
128
+
129
+ for (
130
+ let command = pickMergeBlobsCommand(readings);
131
+ command !== null;
132
+ command = pickMergeBlobsCommand(readings)
133
+ ) {
134
+ const result = options.runProgram(command.argv, {
135
+ cwd: options.cwd,
136
+ env: options.env,
137
+ ...(command.stdin === undefined ? {} : { stdin: command.stdin }),
138
+ });
139
+
140
+ if (command.exitCodes !== 'any' && !command.exitCodes.includes(result.exitCode)) {
141
+ throw new Error(`${command.argv.join(' ')} failed: ${result.stderr.trim()}`);
142
+ }
143
+
144
+ readings =
145
+ command.reading === 'bases'
146
+ ? { ...readings, bases: [...readings.bases, result.stdout] }
147
+ : { ...readings, [command.reading]: result.stdout };
148
+ }
149
+ }
150
+
151
+ // Returns the exit code: 0 when clean, 1 with a finding, and 2 when the check cannot run.
152
+ export async function runLocalPathsCommand(args: readonly string[]): Promise<number> {
153
+ try {
154
+ const ranges = readPushedRanges({
155
+ cwd: process.cwd(),
156
+ env: process.env,
157
+ pushedRefs: await readPushedRefsInput(args, () => Bun.stdin.text()),
158
+ runProgram,
159
+ });
160
+
161
+ const findings = runLocalPathsCheck({
162
+ cwd: process.cwd(),
163
+ ranges,
164
+ env: process.env,
165
+ host: hostname(),
166
+ runProgram,
167
+ });
168
+
169
+ for (const finding of findings) {
170
+ process.stdout.write(`${finding.path}:${finding.line}: ${finding.message}\n`);
171
+ }
172
+
173
+ return findings.length === 0 ? 0 : 1;
174
+ } catch (error) {
175
+ process.stderr.write(
176
+ `local-paths: ${error instanceof Error ? error.message : String(error)}\n`,
177
+ );
178
+
179
+ return 2;
180
+ }
181
+ }
182
+
183
+ if (import.meta.main) {
184
+ process.exitCode = await runLocalPathsCommand(process.argv.slice(2));
185
+ }
@@ -0,0 +1,38 @@
1
+ export interface ProgramOptions {
2
+ readonly cwd: string;
3
+
4
+ // The whole environment of the program; it inherits nothing from this process.
5
+ readonly env: Readonly<Record<string, string | undefined>>;
6
+
7
+ readonly stdin?: string;
8
+ }
9
+
10
+ export interface ProgramResult {
11
+ readonly exitCode: number;
12
+ readonly stdout: string;
13
+ readonly stderr: string;
14
+ }
15
+
16
+ export type RunProgram = (
17
+ argv: readonly string[],
18
+ options: Readonly<ProgramOptions>,
19
+ ) => ProgramResult;
20
+
21
+ export function runProgram(
22
+ argv: readonly string[],
23
+ options: Readonly<ProgramOptions>,
24
+ ): ProgramResult {
25
+ const result = Bun.spawnSync([...argv], {
26
+ cwd: options.cwd,
27
+ env: { ...options.env },
28
+ stdin: options.stdin === undefined ? 'ignore' : Buffer.from(options.stdin),
29
+ stdout: 'pipe',
30
+ stderr: 'pipe',
31
+ });
32
+
33
+ return {
34
+ exitCode: result.exitCode,
35
+ stdout: result.stdout.toString(),
36
+ stderr: result.stderr.toString(),
37
+ };
38
+ }