@lzear/repo-lint 0.0.1

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.
package/dist/bin.js ADDED
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+ #!/usr/bin/env node
3
+
4
+ // src/bin.ts
5
+ import { mkdirSync } from "fs";
6
+ import { parseArgs } from "util";
7
+
8
+ // src/index.ts
9
+ import { execSync } from "child_process";
10
+ import { existsSync as existsSync2 } from "fs";
11
+ import { tmpdir } from "os";
12
+ import path2 from "path";
13
+
14
+ // src/checks.ts
15
+ import { spawnSync } from "child_process";
16
+ import { existsSync, readFileSync, readdirSync } from "fs";
17
+ import path from "path";
18
+ import { fileURLToPath } from "url";
19
+ import { run as ncuRun } from "npm-check-updates";
20
+ import { publint } from "publint";
21
+ var _dirname = path.dirname(fileURLToPath(import.meta.url));
22
+ var readPkg = (dir) => {
23
+ const f = path.join(dir, "package.json");
24
+ if (!existsSync(f)) return null;
25
+ try {
26
+ return JSON.parse(readFileSync(f, "utf8"));
27
+ } catch {
28
+ return null;
29
+ }
30
+ };
31
+ var getWorkspacePatterns = (pkg) => {
32
+ const ws = pkg.workspaces;
33
+ if (Array.isArray(ws)) return ws;
34
+ const wsPkg = ws;
35
+ if (Array.isArray(wsPkg?.packages)) return wsPkg.packages;
36
+ return [];
37
+ };
38
+ var patternToBase = (rootDir, pattern) => {
39
+ const parts = pattern.split("/");
40
+ if (parts.length === 2 && parts[1] === "*")
41
+ return path.join(rootDir, parts[0] ?? "");
42
+ if (parts.length === 1 && parts[0] === "*") return rootDir;
43
+ return null;
44
+ };
45
+ var getWorkspaceDirs = (rootDir) => {
46
+ const pkg = readPkg(rootDir);
47
+ if (!pkg) return [];
48
+ const dirs = [];
49
+ for (const pattern of getWorkspacePatterns(pkg)) {
50
+ const base = patternToBase(rootDir, pattern);
51
+ if (!base || !existsSync(base)) continue;
52
+ for (const entry of readdirSync(base, { withFileTypes: true })) {
53
+ if (entry.isDirectory()) dirs.push(path.join(base, entry.name));
54
+ }
55
+ }
56
+ return dirs;
57
+ };
58
+ var findBin = (name, startDir) => {
59
+ let dir = startDir;
60
+ while (true) {
61
+ const bin = path.join(dir, "node_modules", ".bin", name);
62
+ if (existsSync(bin)) return bin;
63
+ const parent = path.join(dir, "..");
64
+ if (parent === dir) return null;
65
+ dir = parent;
66
+ }
67
+ };
68
+ var eachPublishedPkg = async (dir, fn) => {
69
+ const pkg = readPkg(dir);
70
+ if (!pkg) return { pass: true };
71
+ if (pkg.private === true) {
72
+ const wsDirs = getWorkspaceDirs(dir);
73
+ if (wsDirs.length === 0) return { pass: true };
74
+ const results = await Promise.all(
75
+ wsDirs.map((d) => eachPublishedPkg(d, fn))
76
+ );
77
+ const failures = results.filter((r) => !r.pass);
78
+ if (failures.length === 0) return { pass: true };
79
+ return {
80
+ pass: false,
81
+ detail: failures.flatMap((r) => r.detail ? [r.detail] : []).join("\n") || void 0
82
+ };
83
+ }
84
+ return fn(dir);
85
+ };
86
+ var readmeIncludes = (dir, needle) => {
87
+ const f = path.join(dir, "README.md");
88
+ return existsSync(f) && readFileSync(f, "utf8").includes(needle);
89
+ };
90
+ var LOCAL_CHECKS = [
91
+ {
92
+ id: "readme-exists",
93
+ desc: "README.md exists",
94
+ type: "local",
95
+ check: (dir) => existsSync(path.join(dir, "README.md"))
96
+ },
97
+ {
98
+ id: "readme-ci-badge",
99
+ desc: "README has CI badge",
100
+ type: "local",
101
+ check: (dir) => readmeIncludes(dir, "workflows/ci.yml/badge.svg")
102
+ },
103
+ {
104
+ id: "readme-codacy-grade-badge",
105
+ desc: "README has Codacy grade badge",
106
+ type: "local",
107
+ check: (dir) => readmeIncludes(dir, "project/badge/Grade/")
108
+ },
109
+ {
110
+ id: "readme-codacy-coverage-badge",
111
+ desc: "README has Codacy coverage badge",
112
+ type: "local",
113
+ check: (dir) => readmeIncludes(dir, "project/badge/Coverage/")
114
+ },
115
+ {
116
+ id: "readme-npm-badge",
117
+ desc: "README has npm badge",
118
+ type: "local",
119
+ check: (dir) => readmeIncludes(dir, "shields.io/npm/v/")
120
+ },
121
+ {
122
+ id: "editorconfig",
123
+ desc: ".editorconfig exists",
124
+ type: "local",
125
+ check: (dir) => existsSync(path.join(dir, ".editorconfig"))
126
+ },
127
+ {
128
+ id: "codacy-config",
129
+ desc: ".codacy.yml exists",
130
+ type: "local",
131
+ check: (dir) => existsSync(path.join(dir, ".codacy.yml"))
132
+ },
133
+ {
134
+ id: "license",
135
+ desc: "LICENSE exists",
136
+ type: "local",
137
+ check: (dir) => existsSync(path.join(dir, "LICENSE"))
138
+ },
139
+ {
140
+ id: "ci-workflow",
141
+ desc: "CI workflow exists",
142
+ type: "local",
143
+ check: (dir) => existsSync(path.join(dir, ".github/workflows/ci.yml"))
144
+ },
145
+ {
146
+ id: "renovate",
147
+ desc: "renovate.json exists",
148
+ type: "local",
149
+ check: (dir) => existsSync(path.join(dir, "renovate.json"))
150
+ },
151
+ {
152
+ id: "pkg-publint",
153
+ desc: "publint (all published packages)",
154
+ type: "local",
155
+ check: (dir) => eachPublishedPkg(dir, async (pkgDir) => {
156
+ const { messages } = await publint({ pkgDir });
157
+ const failures = messages.filter(
158
+ (m) => m.type === "error" || m.type === "warning"
159
+ );
160
+ if (failures.length === 0) return { pass: true };
161
+ return {
162
+ pass: false,
163
+ detail: failures.map((m) => `[${m.type}] ${m.code}`).join("\n")
164
+ };
165
+ })
166
+ },
167
+ {
168
+ id: "pkg-attw",
169
+ desc: "attw (all published packages)",
170
+ type: "local",
171
+ check: (dir) => {
172
+ const bin = findBin("attw", _dirname);
173
+ if (!bin) return { pass: false, detail: "attw not available" };
174
+ return eachPublishedPkg(dir, (pkgDir) => {
175
+ const r = spawnSync(
176
+ process.execPath,
177
+ [bin, "--pack", "--profile", "esm-only"],
178
+ {
179
+ cwd: pkgDir,
180
+ encoding: "utf8",
181
+ stdio: ["ignore", "pipe", "pipe"]
182
+ }
183
+ );
184
+ if (r.status === 0) return { pass: true };
185
+ return { pass: false, detail: (r.stdout + r.stderr).trim() };
186
+ });
187
+ }
188
+ },
189
+ {
190
+ id: "pkg-knip",
191
+ desc: "knip (no unused exports/deps)",
192
+ type: "local",
193
+ check: (dir) => {
194
+ const bin = findBin("knip", _dirname);
195
+ if (!bin) return { pass: false, detail: "knip not available" };
196
+ const r = spawnSync(process.execPath, [bin], {
197
+ cwd: dir,
198
+ encoding: "utf8",
199
+ stdio: ["ignore", "pipe", "pipe"],
200
+ env: { ...process.env, NO_COLOR: "1" }
201
+ });
202
+ if (r.status === 0) return { pass: true };
203
+ const detail = (r.stdout + r.stderr).trim();
204
+ return { pass: false, detail: `${detail}
205
+
206
+ Run: npx knip` };
207
+ }
208
+ },
209
+ {
210
+ id: "deps-fresh",
211
+ desc: "dependencies up to date (ncu)",
212
+ type: "local",
213
+ check: async (dir) => {
214
+ const pkg = readPkg(dir);
215
+ const hasWorkspaces = Array.isArray(pkg?.workspaces) && pkg.workspaces.length > 0;
216
+ try {
217
+ const result = await ncuRun({
218
+ packageFile: path.join(dir, "package.json"),
219
+ ...hasWorkspaces ? { workspaces: true } : {},
220
+ silent: true
221
+ });
222
+ const entries = hasWorkspaces ? Object.values(
223
+ result
224
+ ).flatMap((x) => Object.entries(x)) : Object.entries(result);
225
+ if (entries.length === 0) return true;
226
+ const lines = entries.map(([k, v]) => `${k} \u2192 ${v}`).join("\n");
227
+ const cmd = hasWorkspaces ? "yarn dlx npm-check-updates --dep dev,optional,peer,prod,packageManager -u --workspaces && yarn install" : "yarn dlx npm-check-updates --dep dev,optional,peer,prod,packageManager -u && yarn install";
228
+ return { pass: false, detail: `${lines}
229
+
230
+ Run: ${cmd}` };
231
+ } catch (error) {
232
+ return { pass: false, detail: String(error) };
233
+ }
234
+ }
235
+ }
236
+ ];
237
+ var listSecrets = (repo) => {
238
+ try {
239
+ const result = spawnSync(
240
+ "gh",
241
+ ["secret", "list", "--repo", repo, "--json", "name"],
242
+ {
243
+ encoding: "utf8",
244
+ stdio: ["ignore", "pipe", "ignore"]
245
+ }
246
+ );
247
+ if (result.status !== 0) return null;
248
+ return JSON.parse(result.stdout).map((s) => s.name);
249
+ } catch {
250
+ return null;
251
+ }
252
+ };
253
+ var REMOTE_CHECKS = [
254
+ {
255
+ id: "secret-npm-token",
256
+ desc: "Secret NPM_TOKEN set",
257
+ type: "remote",
258
+ check: (repo) => {
259
+ const secrets = listSecrets(repo);
260
+ return secrets?.includes("NPM_TOKEN") ?? false;
261
+ }
262
+ },
263
+ {
264
+ id: "secret-codacy-token",
265
+ desc: "Secret CODACY_PROJECT_TOKEN set",
266
+ type: "remote",
267
+ check: (repo) => {
268
+ const secrets = listSecrets(repo);
269
+ return secrets?.includes("CODACY_PROJECT_TOKEN") ?? false;
270
+ }
271
+ }
272
+ ];
273
+ var CHECKS = [...LOCAL_CHECKS, ...REMOTE_CHECKS];
274
+
275
+ // src/index.ts
276
+ var checkLocal = async (options = {}) => {
277
+ const { dir = process.cwd(), skipRemote = false } = options;
278
+ const repo = options.repo ?? (() => {
279
+ try {
280
+ const remote = execSync("git remote get-url origin", {
281
+ encoding: "utf8",
282
+ stdio: ["ignore", "pipe", "ignore"]
283
+ }).trim();
284
+ const match = /github\.com[:/](.+?)(?:\.git)?$/.exec(remote);
285
+ return match?.[1] ?? path2.basename(dir);
286
+ } catch {
287
+ return dir;
288
+ }
289
+ })();
290
+ const localResults = await Promise.all(
291
+ LOCAL_CHECKS.map(async (c) => {
292
+ const raw = await c.check(dir);
293
+ return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
294
+ })
295
+ );
296
+ const remoteResults = skipRemote ? [] : REMOTE_CHECKS.map((c) => ({
297
+ id: c.id,
298
+ desc: c.desc,
299
+ pass: c.check(repo)
300
+ }));
301
+ return { repo, results: [...localResults, ...remoteResults] };
302
+ };
303
+ var checkRepo = async (repo, options = {}) => {
304
+ const {
305
+ token,
306
+ baseDir = path2.join(tmpdir(), "repo-lint"),
307
+ skipRemote = false
308
+ } = options;
309
+ const dir = path2.join(baseDir, repo.replace("/", "__"));
310
+ if (existsSync2(dir)) {
311
+ execSync(`git -C ${dir} pull --quiet`, { stdio: "ignore" });
312
+ } else {
313
+ const url = token ? `https://${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
314
+ execSync(`git clone --depth 1 --quiet ${url} ${dir}`, { stdio: "ignore" });
315
+ }
316
+ const localResults = await Promise.all(
317
+ LOCAL_CHECKS.map(async (c) => {
318
+ const raw = await c.check(dir);
319
+ return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
320
+ })
321
+ );
322
+ const remoteResults = skipRemote ? [] : REMOTE_CHECKS.map((c) => ({
323
+ id: c.id,
324
+ desc: c.desc,
325
+ pass: c.check(repo)
326
+ }));
327
+ return { repo, results: [...localResults, ...remoteResults] };
328
+ };
329
+
330
+ // src/bin.ts
331
+ var { values } = parseArgs({
332
+ options: {
333
+ local: { type: "boolean", default: false },
334
+ repos: { type: "string" },
335
+ token: { type: "string" },
336
+ dir: { type: "string", default: `${process.env.HOME ?? "/tmp"}/repo-lint` },
337
+ "skip-remote": { type: "boolean", default: false }
338
+ }
339
+ });
340
+ var printReport = (result) => {
341
+ let anyFail = false;
342
+ const pass = result.results.filter((r) => r.pass).length;
343
+ const total = result.results.length;
344
+ for (const r of result.results) {
345
+ console.log(` ${r.pass ? "\u2713" : "\u2717"} ${r.desc}`);
346
+ if (!r.pass) anyFail = true;
347
+ }
348
+ console.log(`
349
+ ${pass}/${total} checks passed`);
350
+ return anyFail;
351
+ };
352
+ var main = async () => {
353
+ let anyFail = false;
354
+ if (values.local) {
355
+ console.log(`
356
+ \u2500\u2500 ${process.cwd()}`);
357
+ const result = await checkLocal({ skipRemote: values["skip-remote"] });
358
+ anyFail = printReport(result);
359
+ } else {
360
+ if (!values.repos) {
361
+ console.error(
362
+ "Usage:\n repo-lint --local # check current repo\n repo-lint --repos lzear/r1,lzear/r2 # check remote repos\nOptions:\n --token ghp_xxx GitHub token for private repos\n --skip-remote skip secret checks (no gh auth needed)"
363
+ );
364
+ process.exit(1);
365
+ }
366
+ mkdirSync(values.dir, { recursive: true });
367
+ for (const repo of values.repos.split(",").map((r) => r.trim())) {
368
+ console.log(`
369
+ \u2500\u2500 ${repo}`);
370
+ try {
371
+ const result = await checkRepo(repo, {
372
+ token: values.token,
373
+ baseDir: values.dir,
374
+ skipRemote: values["skip-remote"]
375
+ });
376
+ if (printReport(result)) anyFail = true;
377
+ } catch {
378
+ console.log(" \u2717 could not clone repo");
379
+ anyFail = true;
380
+ }
381
+ }
382
+ }
383
+ process.exit(anyFail ? 1 : 0);
384
+ };
385
+ void main();
@@ -0,0 +1,46 @@
1
+ interface CheckDetail {
2
+ pass: boolean;
3
+ detail?: string;
4
+ }
5
+ type CheckResult$1 = boolean | CheckDetail | Promise<boolean | CheckDetail>;
6
+ interface LocalCheck {
7
+ id: string;
8
+ desc: string;
9
+ type: 'local';
10
+ check: (dir: string) => CheckResult$1;
11
+ }
12
+ interface RemoteCheck {
13
+ id: string;
14
+ desc: string;
15
+ type: 'remote';
16
+ check: (repo: string) => boolean;
17
+ }
18
+ type Check = LocalCheck | RemoteCheck;
19
+ declare const LOCAL_CHECKS: LocalCheck[];
20
+ declare const REMOTE_CHECKS: RemoteCheck[];
21
+ declare const CHECKS: Check[];
22
+
23
+ interface CheckResult {
24
+ id: string;
25
+ desc: string;
26
+ pass: boolean;
27
+ detail?: string;
28
+ }
29
+ interface RepoReport {
30
+ repo: string;
31
+ results: CheckResult[];
32
+ }
33
+ interface CheckRepoOptions {
34
+ token?: string;
35
+ baseDir?: string;
36
+ skipRemote?: boolean;
37
+ }
38
+ interface CheckLocalOptions {
39
+ dir?: string;
40
+ skipRemote?: boolean;
41
+ repo?: string;
42
+ }
43
+ declare const checkLocal: (options?: CheckLocalOptions) => Promise<RepoReport>;
44
+ declare const checkRepo: (repo: string, options?: CheckRepoOptions) => Promise<RepoReport>;
45
+
46
+ export { CHECKS, type Check, type CheckDetail, type CheckLocalOptions, type CheckRepoOptions, type CheckResult, LOCAL_CHECKS, type LocalCheck, REMOTE_CHECKS, type RemoteCheck, type RepoReport, checkLocal, checkRepo };
package/dist/index.js ADDED
@@ -0,0 +1,328 @@
1
+ // src/index.ts
2
+ import { execSync } from "child_process";
3
+ import { existsSync as existsSync2 } from "fs";
4
+ import { tmpdir } from "os";
5
+ import path2 from "path";
6
+
7
+ // src/checks.ts
8
+ import { spawnSync } from "child_process";
9
+ import { existsSync, readFileSync, readdirSync } from "fs";
10
+ import path from "path";
11
+ import { fileURLToPath } from "url";
12
+ import { run as ncuRun } from "npm-check-updates";
13
+ import { publint } from "publint";
14
+ var _dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ var readPkg = (dir) => {
16
+ const f = path.join(dir, "package.json");
17
+ if (!existsSync(f)) return null;
18
+ try {
19
+ return JSON.parse(readFileSync(f, "utf8"));
20
+ } catch {
21
+ return null;
22
+ }
23
+ };
24
+ var getWorkspacePatterns = (pkg) => {
25
+ const ws = pkg.workspaces;
26
+ if (Array.isArray(ws)) return ws;
27
+ const wsPkg = ws;
28
+ if (Array.isArray(wsPkg?.packages)) return wsPkg.packages;
29
+ return [];
30
+ };
31
+ var patternToBase = (rootDir, pattern) => {
32
+ const parts = pattern.split("/");
33
+ if (parts.length === 2 && parts[1] === "*")
34
+ return path.join(rootDir, parts[0] ?? "");
35
+ if (parts.length === 1 && parts[0] === "*") return rootDir;
36
+ return null;
37
+ };
38
+ var getWorkspaceDirs = (rootDir) => {
39
+ const pkg = readPkg(rootDir);
40
+ if (!pkg) return [];
41
+ const dirs = [];
42
+ for (const pattern of getWorkspacePatterns(pkg)) {
43
+ const base = patternToBase(rootDir, pattern);
44
+ if (!base || !existsSync(base)) continue;
45
+ for (const entry of readdirSync(base, { withFileTypes: true })) {
46
+ if (entry.isDirectory()) dirs.push(path.join(base, entry.name));
47
+ }
48
+ }
49
+ return dirs;
50
+ };
51
+ var findBin = (name, startDir) => {
52
+ let dir = startDir;
53
+ while (true) {
54
+ const bin = path.join(dir, "node_modules", ".bin", name);
55
+ if (existsSync(bin)) return bin;
56
+ const parent = path.join(dir, "..");
57
+ if (parent === dir) return null;
58
+ dir = parent;
59
+ }
60
+ };
61
+ var eachPublishedPkg = async (dir, fn) => {
62
+ const pkg = readPkg(dir);
63
+ if (!pkg) return { pass: true };
64
+ if (pkg.private === true) {
65
+ const wsDirs = getWorkspaceDirs(dir);
66
+ if (wsDirs.length === 0) return { pass: true };
67
+ const results = await Promise.all(
68
+ wsDirs.map((d) => eachPublishedPkg(d, fn))
69
+ );
70
+ const failures = results.filter((r) => !r.pass);
71
+ if (failures.length === 0) return { pass: true };
72
+ return {
73
+ pass: false,
74
+ detail: failures.flatMap((r) => r.detail ? [r.detail] : []).join("\n") || void 0
75
+ };
76
+ }
77
+ return fn(dir);
78
+ };
79
+ var readmeIncludes = (dir, needle) => {
80
+ const f = path.join(dir, "README.md");
81
+ return existsSync(f) && readFileSync(f, "utf8").includes(needle);
82
+ };
83
+ var LOCAL_CHECKS = [
84
+ {
85
+ id: "readme-exists",
86
+ desc: "README.md exists",
87
+ type: "local",
88
+ check: (dir) => existsSync(path.join(dir, "README.md"))
89
+ },
90
+ {
91
+ id: "readme-ci-badge",
92
+ desc: "README has CI badge",
93
+ type: "local",
94
+ check: (dir) => readmeIncludes(dir, "workflows/ci.yml/badge.svg")
95
+ },
96
+ {
97
+ id: "readme-codacy-grade-badge",
98
+ desc: "README has Codacy grade badge",
99
+ type: "local",
100
+ check: (dir) => readmeIncludes(dir, "project/badge/Grade/")
101
+ },
102
+ {
103
+ id: "readme-codacy-coverage-badge",
104
+ desc: "README has Codacy coverage badge",
105
+ type: "local",
106
+ check: (dir) => readmeIncludes(dir, "project/badge/Coverage/")
107
+ },
108
+ {
109
+ id: "readme-npm-badge",
110
+ desc: "README has npm badge",
111
+ type: "local",
112
+ check: (dir) => readmeIncludes(dir, "shields.io/npm/v/")
113
+ },
114
+ {
115
+ id: "editorconfig",
116
+ desc: ".editorconfig exists",
117
+ type: "local",
118
+ check: (dir) => existsSync(path.join(dir, ".editorconfig"))
119
+ },
120
+ {
121
+ id: "codacy-config",
122
+ desc: ".codacy.yml exists",
123
+ type: "local",
124
+ check: (dir) => existsSync(path.join(dir, ".codacy.yml"))
125
+ },
126
+ {
127
+ id: "license",
128
+ desc: "LICENSE exists",
129
+ type: "local",
130
+ check: (dir) => existsSync(path.join(dir, "LICENSE"))
131
+ },
132
+ {
133
+ id: "ci-workflow",
134
+ desc: "CI workflow exists",
135
+ type: "local",
136
+ check: (dir) => existsSync(path.join(dir, ".github/workflows/ci.yml"))
137
+ },
138
+ {
139
+ id: "renovate",
140
+ desc: "renovate.json exists",
141
+ type: "local",
142
+ check: (dir) => existsSync(path.join(dir, "renovate.json"))
143
+ },
144
+ {
145
+ id: "pkg-publint",
146
+ desc: "publint (all published packages)",
147
+ type: "local",
148
+ check: (dir) => eachPublishedPkg(dir, async (pkgDir) => {
149
+ const { messages } = await publint({ pkgDir });
150
+ const failures = messages.filter(
151
+ (m) => m.type === "error" || m.type === "warning"
152
+ );
153
+ if (failures.length === 0) return { pass: true };
154
+ return {
155
+ pass: false,
156
+ detail: failures.map((m) => `[${m.type}] ${m.code}`).join("\n")
157
+ };
158
+ })
159
+ },
160
+ {
161
+ id: "pkg-attw",
162
+ desc: "attw (all published packages)",
163
+ type: "local",
164
+ check: (dir) => {
165
+ const bin = findBin("attw", _dirname);
166
+ if (!bin) return { pass: false, detail: "attw not available" };
167
+ return eachPublishedPkg(dir, (pkgDir) => {
168
+ const r = spawnSync(
169
+ process.execPath,
170
+ [bin, "--pack", "--profile", "esm-only"],
171
+ {
172
+ cwd: pkgDir,
173
+ encoding: "utf8",
174
+ stdio: ["ignore", "pipe", "pipe"]
175
+ }
176
+ );
177
+ if (r.status === 0) return { pass: true };
178
+ return { pass: false, detail: (r.stdout + r.stderr).trim() };
179
+ });
180
+ }
181
+ },
182
+ {
183
+ id: "pkg-knip",
184
+ desc: "knip (no unused exports/deps)",
185
+ type: "local",
186
+ check: (dir) => {
187
+ const bin = findBin("knip", _dirname);
188
+ if (!bin) return { pass: false, detail: "knip not available" };
189
+ const r = spawnSync(process.execPath, [bin], {
190
+ cwd: dir,
191
+ encoding: "utf8",
192
+ stdio: ["ignore", "pipe", "pipe"],
193
+ env: { ...process.env, NO_COLOR: "1" }
194
+ });
195
+ if (r.status === 0) return { pass: true };
196
+ const detail = (r.stdout + r.stderr).trim();
197
+ return { pass: false, detail: `${detail}
198
+
199
+ Run: npx knip` };
200
+ }
201
+ },
202
+ {
203
+ id: "deps-fresh",
204
+ desc: "dependencies up to date (ncu)",
205
+ type: "local",
206
+ check: async (dir) => {
207
+ const pkg = readPkg(dir);
208
+ const hasWorkspaces = Array.isArray(pkg?.workspaces) && pkg.workspaces.length > 0;
209
+ try {
210
+ const result = await ncuRun({
211
+ packageFile: path.join(dir, "package.json"),
212
+ ...hasWorkspaces ? { workspaces: true } : {},
213
+ silent: true
214
+ });
215
+ const entries = hasWorkspaces ? Object.values(
216
+ result
217
+ ).flatMap((x) => Object.entries(x)) : Object.entries(result);
218
+ if (entries.length === 0) return true;
219
+ const lines = entries.map(([k, v]) => `${k} \u2192 ${v}`).join("\n");
220
+ const cmd = hasWorkspaces ? "yarn dlx npm-check-updates --dep dev,optional,peer,prod,packageManager -u --workspaces && yarn install" : "yarn dlx npm-check-updates --dep dev,optional,peer,prod,packageManager -u && yarn install";
221
+ return { pass: false, detail: `${lines}
222
+
223
+ Run: ${cmd}` };
224
+ } catch (error) {
225
+ return { pass: false, detail: String(error) };
226
+ }
227
+ }
228
+ }
229
+ ];
230
+ var listSecrets = (repo) => {
231
+ try {
232
+ const result = spawnSync(
233
+ "gh",
234
+ ["secret", "list", "--repo", repo, "--json", "name"],
235
+ {
236
+ encoding: "utf8",
237
+ stdio: ["ignore", "pipe", "ignore"]
238
+ }
239
+ );
240
+ if (result.status !== 0) return null;
241
+ return JSON.parse(result.stdout).map((s) => s.name);
242
+ } catch {
243
+ return null;
244
+ }
245
+ };
246
+ var REMOTE_CHECKS = [
247
+ {
248
+ id: "secret-npm-token",
249
+ desc: "Secret NPM_TOKEN set",
250
+ type: "remote",
251
+ check: (repo) => {
252
+ const secrets = listSecrets(repo);
253
+ return secrets?.includes("NPM_TOKEN") ?? false;
254
+ }
255
+ },
256
+ {
257
+ id: "secret-codacy-token",
258
+ desc: "Secret CODACY_PROJECT_TOKEN set",
259
+ type: "remote",
260
+ check: (repo) => {
261
+ const secrets = listSecrets(repo);
262
+ return secrets?.includes("CODACY_PROJECT_TOKEN") ?? false;
263
+ }
264
+ }
265
+ ];
266
+ var CHECKS = [...LOCAL_CHECKS, ...REMOTE_CHECKS];
267
+
268
+ // src/index.ts
269
+ var checkLocal = async (options = {}) => {
270
+ const { dir = process.cwd(), skipRemote = false } = options;
271
+ const repo = options.repo ?? (() => {
272
+ try {
273
+ const remote = execSync("git remote get-url origin", {
274
+ encoding: "utf8",
275
+ stdio: ["ignore", "pipe", "ignore"]
276
+ }).trim();
277
+ const match = /github\.com[:/](.+?)(?:\.git)?$/.exec(remote);
278
+ return match?.[1] ?? path2.basename(dir);
279
+ } catch {
280
+ return dir;
281
+ }
282
+ })();
283
+ const localResults = await Promise.all(
284
+ LOCAL_CHECKS.map(async (c) => {
285
+ const raw = await c.check(dir);
286
+ return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
287
+ })
288
+ );
289
+ const remoteResults = skipRemote ? [] : REMOTE_CHECKS.map((c) => ({
290
+ id: c.id,
291
+ desc: c.desc,
292
+ pass: c.check(repo)
293
+ }));
294
+ return { repo, results: [...localResults, ...remoteResults] };
295
+ };
296
+ var checkRepo = async (repo, options = {}) => {
297
+ const {
298
+ token,
299
+ baseDir = path2.join(tmpdir(), "repo-lint"),
300
+ skipRemote = false
301
+ } = options;
302
+ const dir = path2.join(baseDir, repo.replace("/", "__"));
303
+ if (existsSync2(dir)) {
304
+ execSync(`git -C ${dir} pull --quiet`, { stdio: "ignore" });
305
+ } else {
306
+ const url = token ? `https://${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
307
+ execSync(`git clone --depth 1 --quiet ${url} ${dir}`, { stdio: "ignore" });
308
+ }
309
+ const localResults = await Promise.all(
310
+ LOCAL_CHECKS.map(async (c) => {
311
+ const raw = await c.check(dir);
312
+ return typeof raw === "boolean" ? { id: c.id, desc: c.desc, pass: raw } : { id: c.id, desc: c.desc, ...raw };
313
+ })
314
+ );
315
+ const remoteResults = skipRemote ? [] : REMOTE_CHECKS.map((c) => ({
316
+ id: c.id,
317
+ desc: c.desc,
318
+ pass: c.check(repo)
319
+ }));
320
+ return { repo, results: [...localResults, ...remoteResults] };
321
+ };
322
+ export {
323
+ CHECKS,
324
+ LOCAL_CHECKS,
325
+ REMOTE_CHECKS,
326
+ checkLocal,
327
+ checkRepo
328
+ };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@lzear/repo-lint",
3
+ "version": "0.0.1",
4
+ "description": "Checks lzear repos against forge standards",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/lzear/forge.git",
8
+ "directory": "packages/repo-lint"
9
+ },
10
+ "author": "lzear",
11
+ "license": "MIT",
12
+ "sideEffects": false,
13
+ "type": "module",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
20
+ "bin": "./dist/bin.js",
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "test": "vitest run",
28
+ "test:coverage": "vitest run --coverage",
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "dependencies": {
32
+ "@arethetypeswrong/cli": "^0.18",
33
+ "knip": "^6",
34
+ "npm-check-updates": "^22",
35
+ "publint": "^0.3"
36
+ },
37
+ "devDependencies": {
38
+ "@lzear/configs": "workspace:*",
39
+ "@types/node": "^25",
40
+ "@vitest/coverage-v8": "^4",
41
+ "tsup": "^8",
42
+ "typescript": "^6",
43
+ "vitest": "^4"
44
+ },
45
+ "engines": {
46
+ "node": ">=20"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }