@rtorcato/repo-tooling 3.38.1 → 3.38.2
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/base/checks.js +2 -7
- package/dist/base/fixers.js +22 -6
- package/dist/cli/commands/doctor.js +12 -11
- package/dist/cli/commands/fix.js +49 -11
- package/dist/cli/generators/git.js +20 -7
- package/dist/cli/generators/security.js +37 -8
- package/dist/cli/generators/typedoc.js +7 -4
- package/dist/cli/utils/detect-language.js +14 -0
- package/dist/languages/js/fixers.js +1 -1
- package/dist/languages/registry.js +5 -3
- package/package.json +1 -1
package/dist/base/checks.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'fs-extra';
|
|
|
3
3
|
import { BLOCK_START, hasStaleAgentBlock } from '../cli/generators/agent-rules.js';
|
|
4
4
|
import { BADGE_START, hasPublicOnlyBadges } from '../cli/generators/badges.js';
|
|
5
5
|
import { claudeSkillStatus, SHIPPED_SKILLS, skillDiffCommand, } from '../cli/generators/claude-skills.js';
|
|
6
|
-
import { DEPENDABOT_CONFIG_PATHS, dependabotIgnoreRules } from '../cli/generators/security.js';
|
|
6
|
+
import { DEPENDABOT_CONFIG_PATHS, dependabotConfigDeltas, dependabotEcosystemFor, dependabotIgnoreRules, } from '../cli/generators/security.js';
|
|
7
7
|
import { detectNestedLanguages } from '../cli/utils/detect-language.js';
|
|
8
8
|
/**
|
|
9
9
|
* Root-only detection is the decision (#317), not an oversight — but it used to
|
|
@@ -244,12 +244,7 @@ export async function checkDependabot(dir) {
|
|
|
244
244
|
// depends on it. Flag any config that predates it so `fix dependabot` can
|
|
245
245
|
// bring the pair up to standard.
|
|
246
246
|
const content = await fs.readFile(candidatePath, 'utf8');
|
|
247
|
-
const deltas =
|
|
248
|
-
for (const group of ['production-minor', 'dev-minor', 'major-updates']) {
|
|
249
|
-
if (!new RegExp(`^\\s*${group}:`, 'm').test(content)) {
|
|
250
|
-
deltas.push(`missing \`${group}\` group`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
247
|
+
const deltas = dependabotConfigDeltas(content, await dependabotEcosystemFor(dir));
|
|
253
248
|
const automergePath = path.join(dir, '.github', 'workflows', 'dependabot-automerge.yml');
|
|
254
249
|
if (await fs.pathExists(automergePath)) {
|
|
255
250
|
// Pre-#423 workflows gated on the semver type alone, so production
|
package/dist/base/fixers.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import os from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
|
+
import fs from 'fs-extra';
|
|
17
18
|
import inquirer from 'inquirer';
|
|
18
19
|
import { installAgentRules, installAiSetup } from '../cli/generators/agent-rules.js';
|
|
19
20
|
import { installClaudeSkill, resolveSkillsDir, SHIPPED_SKILLS, skillDiffCommand, } from '../cli/generators/claude-skills.js';
|
|
@@ -21,10 +22,10 @@ import { generateBrand } from '../cli/generators/brand.js';
|
|
|
21
22
|
import { generateCommunityHealth } from '../cli/generators/community-health.js';
|
|
22
23
|
import { generateCommitlintConfig } from '../cli/generators/git.js';
|
|
23
24
|
import { generateCodeowners, generateEditorConfig } from '../cli/generators/misc.js';
|
|
24
|
-
import { findDependabotIgnoreRules, generateCodeQLWorkflow, generateDependabotConfig, generateRenovateConfig, } from '../cli/generators/security.js';
|
|
25
|
+
import { DEPENDABOT_AUTOMERGE_WORKFLOW, dependabotConfigDeltas, dependabotEcosystemFor, findDependabotIgnoreRules, generateCodeQLWorkflow, generateDependabotConfig, generateRenovateConfig, readDependabotConfig, } from '../cli/generators/security.js';
|
|
25
26
|
import { classifyCopiedAssets } from '../cli/utils/copied-assets.js';
|
|
26
27
|
import { copyPreset } from '../cli/utils/copy-preset.js';
|
|
27
|
-
import {
|
|
28
|
+
import { detectAuditLanguage } from '../cli/utils/detect-language.js';
|
|
28
29
|
import { resolveLanguageModule } from '../languages/registry.js';
|
|
29
30
|
import { applyGithubSettings, applyReleaseEnvironment, RELEASE_ENV_CHECK, RELEASE_GATE_CHECK, } from './github-settings.js';
|
|
30
31
|
import { applyLoopLabels } from './labels.js';
|
|
@@ -46,9 +47,16 @@ export class FixerAbort extends Error {
|
|
|
46
47
|
this.name = 'FixerAbort';
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* The repo's language module, resolved from its marker files. No marker → no
|
|
52
|
+
* manifest ecosystem and no CodeQL matrix, rather than npm/JS guessed (#632).
|
|
53
|
+
*/
|
|
50
54
|
async function moduleFor(targetDir) {
|
|
51
|
-
return resolveLanguageModule(await
|
|
55
|
+
return (resolveLanguageModule(await detectAuditLanguage(targetDir)) ?? {
|
|
56
|
+
label: 'unrecognized-language',
|
|
57
|
+
codeqlLanguages: [],
|
|
58
|
+
dependabotEcosystem: null,
|
|
59
|
+
});
|
|
52
60
|
}
|
|
53
61
|
/**
|
|
54
62
|
* Where to install user-global skills, asking only when nothing resolves. Under
|
|
@@ -149,6 +157,15 @@ export const BASE_FIXERS = [
|
|
|
149
157
|
outputs: ['.github/dependabot.yml', '.github/workflows/dependabot-automerge.yml'],
|
|
150
158
|
canFixDrift: true,
|
|
151
159
|
async run({ targetDir }) {
|
|
160
|
+
const ecosystem = await dependabotEcosystemFor(targetDir);
|
|
161
|
+
// The config is already canonical for this repo's ecosystem, so only the
|
|
162
|
+
// workflow half drifts. Leave the config — and its repo-authored comments
|
|
163
|
+
// — alone rather than regenerate a file with nothing to change (#631).
|
|
164
|
+
const existing = await readDependabotConfig(targetDir);
|
|
165
|
+
if (existing && dependabotConfigDeltas(existing.content, ecosystem).length === 0) {
|
|
166
|
+
await fs.outputFile(path.join(targetDir, '.github', 'workflows', 'dependabot-automerge.yml'), DEPENDABOT_AUTOMERGE_WORKFLOW);
|
|
167
|
+
return { filesWritten: ['.github/workflows/dependabot-automerge.yml'] };
|
|
168
|
+
}
|
|
152
169
|
// The template owns the whole file but emits no `ignore:` block, so
|
|
153
170
|
// regenerating deletes any repo-local ignore rule — silently, and with
|
|
154
171
|
// nothing in `doctor` to report the loss afterwards, because from the
|
|
@@ -159,8 +176,7 @@ export const BASE_FIXERS = [
|
|
|
159
176
|
if (ignored) {
|
|
160
177
|
throw new FixerAbort('dependabot-ignore-rules', `refusing to overwrite ${ignored.file} — it has ${ignored.rules.length} \`ignore:\` rule(s) the canonical config does not reproduce: ${ignored.rules.join(', ')}`, `re-add the \`ignore:\` block after regenerating, or delete it from ${ignored.file} to accept the loss — then re-run \`fix dependabot\``);
|
|
161
178
|
}
|
|
162
|
-
|
|
163
|
-
return { filesWritten: await generateDependabotConfig(targetDir, dependabotEcosystem) };
|
|
179
|
+
return { filesWritten: await generateDependabotConfig(targetDir, ecosystem) };
|
|
164
180
|
},
|
|
165
181
|
},
|
|
166
182
|
{
|
|
@@ -11,7 +11,7 @@ import { readPyproject, renderPythonWorkflow } from '../../languages/python/ci.j
|
|
|
11
11
|
import { resolveLanguageModule } from '../../languages/registry.js';
|
|
12
12
|
import { SWIFT_GIT_HOOKS, runSwiftChecks } from '../../languages/swift/checks.js';
|
|
13
13
|
import { readSwiftPackage, renderSwiftWorkflow } from '../../languages/swift/ci.js';
|
|
14
|
-
import {
|
|
14
|
+
import { detectAuditLanguage } from '../utils/detect-language.js';
|
|
15
15
|
import { checkAgentUser } from '../../base/agent-user.js';
|
|
16
16
|
import { checkGitHubSettings } from '../../base/github-settings.js';
|
|
17
17
|
import { checkLoopLabels } from '../../base/labels.js';
|
|
@@ -253,20 +253,21 @@ export async function runDoctor(dir, skillsDir) {
|
|
|
253
253
|
const lock = await readLockfile(targetDir);
|
|
254
254
|
// Per-module dispatch (#285): the base checks (repo hygiene, CI, security,
|
|
255
255
|
// GitHub settings) apply to any repo and run for every language. A supported
|
|
256
|
-
// module layers its own checks on top. 'unknown' (
|
|
257
|
-
//
|
|
258
|
-
const language = await
|
|
256
|
+
// module layers its own checks on top. 'unknown' (no marker) gets the base
|
|
257
|
+
// suite only — JS checks against a C++ or Go repo are all noise (#632).
|
|
258
|
+
const language = await detectAuditLanguage(targetDir);
|
|
259
259
|
const languageModule = resolveLanguageModule(language);
|
|
260
|
-
//
|
|
261
|
-
//
|
|
262
|
-
// with `supported: false` first, and until its module lands its repos get the
|
|
260
|
+
// Also the on-ramp for a new language: it is added to the registry with
|
|
261
|
+
// `supported: false` first, and until its module lands its repos get the
|
|
263
262
|
// full base suite rather than the wholesale skip this replaced.
|
|
264
|
-
if (!languageModule
|
|
263
|
+
if (!languageModule?.supported) {
|
|
265
264
|
const results = [
|
|
266
265
|
{
|
|
267
266
|
check: 'language',
|
|
268
267
|
status: 'ok',
|
|
269
|
-
detail:
|
|
268
|
+
detail: languageModule
|
|
269
|
+
? `detected ${languageModule.label} — running language-agnostic checks; ${languageModule.label}-specific checks land with its module (#139)`
|
|
270
|
+
: 'no language marker (package.json, Package.swift, pyproject.toml, cpanfile, …) — running language-agnostic checks only',
|
|
270
271
|
},
|
|
271
272
|
// hooks: null — guessing a hook convention would nag every repo with a
|
|
272
273
|
// fix target that doesn't exist.
|
|
@@ -275,7 +276,7 @@ export async function runDoctor(dir, skillsDir) {
|
|
|
275
276
|
badges: { audience: 'public', fixTarget: null },
|
|
276
277
|
presetWorkflow: null,
|
|
277
278
|
language,
|
|
278
|
-
codeqlLanguages: languageModule
|
|
279
|
+
codeqlLanguages: languageModule?.codeqlLanguages ?? [],
|
|
279
280
|
skillsDir,
|
|
280
281
|
})),
|
|
281
282
|
];
|
|
@@ -525,7 +526,7 @@ export async function doctorCommand(options = {}) {
|
|
|
525
526
|
const summary = summarize(results);
|
|
526
527
|
console.log();
|
|
527
528
|
console.log(` Summary: ${chalk.green(`${summary.ok} ok`)}, ${chalk.yellow(`${summary.drift} drift`)}, ${chalk.red(`${summary.missing} missing`)}, ${chalk.gray(`${summary.optionalMissing} not configured`)}, ${chalk.blue(`${summary.declared} declared`)}\n`);
|
|
528
|
-
const suggestions = nextStepSuggestions(results, await
|
|
529
|
+
const suggestions = nextStepSuggestions(results, await detectAuditLanguage(dir));
|
|
529
530
|
if (suggestions.length > 0) {
|
|
530
531
|
console.log(chalk.bold(' Next steps:'));
|
|
531
532
|
for (const s of suggestions) {
|
package/dist/cli/commands/fix.js
CHANGED
|
@@ -14,20 +14,21 @@ import { FIXERS, readPackageJson } from '../../languages/js/fixers.js';
|
|
|
14
14
|
import { PERL_FIXERS } from '../../languages/perl/fixers.js';
|
|
15
15
|
import { PYTHON_FIXERS } from '../../languages/python/fixers.js';
|
|
16
16
|
import { SWIFT_FIXERS } from '../../languages/swift/fixers.js';
|
|
17
|
-
import {
|
|
17
|
+
import { detectAuditLanguage } from '../utils/detect-language.js';
|
|
18
18
|
/** The module-specific fixer set per detected language (#286, #289, #290, #303). */
|
|
19
19
|
const LANGUAGE_FIXERS = {
|
|
20
|
+
js: FIXERS,
|
|
20
21
|
swift: SWIFT_FIXERS,
|
|
21
22
|
python: PYTHON_FIXERS,
|
|
22
23
|
perl: PERL_FIXERS,
|
|
23
24
|
};
|
|
24
25
|
/**
|
|
25
26
|
* The fixers that apply to a repo, by detected language: the language-agnostic
|
|
26
|
-
* base set plus the module's own.
|
|
27
|
-
*
|
|
27
|
+
* base set plus the module's own. A repo with no marker gets the base set only —
|
|
28
|
+
* the JS fixers would run against a missing package.json (#632).
|
|
28
29
|
*/
|
|
29
30
|
function fixersForLanguage(language) {
|
|
30
|
-
return [...BASE_FIXERS, ...(LANGUAGE_FIXERS[language] ??
|
|
31
|
+
return [...BASE_FIXERS, ...(LANGUAGE_FIXERS[language] ?? [])];
|
|
31
32
|
}
|
|
32
33
|
/** Every fixer across every language — for `--list` and the unknown-target hint. */
|
|
33
34
|
const ALL_FIXERS = [
|
|
@@ -133,8 +134,16 @@ async function previewFixer(fixer, result, targetDir, pkg, lock) {
|
|
|
133
134
|
},
|
|
134
135
|
});
|
|
135
136
|
// assumeYes: a preview must never prompt.
|
|
137
|
+
let written;
|
|
136
138
|
try {
|
|
137
|
-
|
|
139
|
+
;
|
|
140
|
+
({ filesWritten: written } = await fixer.run({
|
|
141
|
+
targetDir: tmpDir,
|
|
142
|
+
pkg,
|
|
143
|
+
result,
|
|
144
|
+
lock,
|
|
145
|
+
assumeYes: true,
|
|
146
|
+
}));
|
|
138
147
|
}
|
|
139
148
|
catch (err) {
|
|
140
149
|
// A fixer that refuses has nothing to preview. Swallow it here rather than
|
|
@@ -146,14 +155,21 @@ async function previewFixer(fixer, result, targetDir, pkg, lock) {
|
|
|
146
155
|
}
|
|
147
156
|
const previews = [];
|
|
148
157
|
const seen = new Set();
|
|
149
|
-
|
|
158
|
+
// Declared outputs miss globbed writes (apps/docs/**), so also take what the
|
|
159
|
+
// shadow run reported writing.
|
|
160
|
+
const candidates = [
|
|
161
|
+
...fixer.outputs,
|
|
162
|
+
...written.map((f) => (path.isAbsolute(f) ? path.relative(tmpDir, f) : f)),
|
|
163
|
+
];
|
|
164
|
+
for (const output of candidates) {
|
|
150
165
|
const rel = outputToRelativePath(output);
|
|
151
|
-
if (seen.has(rel))
|
|
166
|
+
if (seen.has(rel) || rel.startsWith('..'))
|
|
152
167
|
continue;
|
|
153
168
|
seen.add(rel);
|
|
154
169
|
const tmpPath = path.join(tmpDir, rel);
|
|
155
170
|
const realPath = path.join(targetDir, rel);
|
|
156
|
-
|
|
171
|
+
const stat = await fs.stat(tmpPath).catch(() => null);
|
|
172
|
+
if (!stat?.isFile())
|
|
157
173
|
continue;
|
|
158
174
|
const newContent = await fs.readFile(tmpPath, 'utf-8');
|
|
159
175
|
const existed = await fs.pathExists(realPath);
|
|
@@ -197,12 +213,34 @@ function printPreviews(previews) {
|
|
|
197
213
|
}
|
|
198
214
|
}
|
|
199
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* What a fixer would actually change. Shadow-runs it like `--diff` does, except
|
|
218
|
+
* for safe-add fixers: some of those write remote state or the home dir, so they
|
|
219
|
+
* are never executed — and since they only create missing files, the declared
|
|
220
|
+
* outputs that already exist are dropped instead.
|
|
221
|
+
*/
|
|
222
|
+
async function dryRunFiles(fixer, result, targetDir, pkg, lock) {
|
|
223
|
+
if ((fixer.riskLevel ?? 'destructive') === 'safe-add') {
|
|
224
|
+
const missing = [];
|
|
225
|
+
for (const out of fixer.outputs) {
|
|
226
|
+
if (!(await fs.pathExists(path.join(targetDir, outputToRelativePath(out))))) {
|
|
227
|
+
missing.push(out);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return missing;
|
|
231
|
+
}
|
|
232
|
+
const previews = await previewFixer(fixer, result, targetDir, pkg, lock);
|
|
233
|
+
return previews.filter((p) => p.kind !== 'unchanged').map((p) => p.path);
|
|
234
|
+
}
|
|
200
235
|
async function applyFixer(fixer, result, targetDir, pkg, lock, dryRun, silent, opts) {
|
|
201
236
|
if (dryRun) {
|
|
237
|
+
const files = await dryRunFiles(fixer, result, targetDir, pkg, lock);
|
|
202
238
|
if (!silent) {
|
|
203
|
-
console.log(chalk.cyan(
|
|
239
|
+
console.log(chalk.cyan(files.length > 0
|
|
240
|
+
? ` [dry-run] would write: ${files.join(', ')}`
|
|
241
|
+
: ' [dry-run] nothing to write'));
|
|
204
242
|
}
|
|
205
|
-
return { filesWritten:
|
|
243
|
+
return { filesWritten: files, dryRun: true };
|
|
206
244
|
}
|
|
207
245
|
const { filesWritten } = await fixer.run({ targetDir, pkg, result, lock, ...opts });
|
|
208
246
|
if (!silent && filesWritten.length > 0) {
|
|
@@ -332,7 +370,7 @@ export async function fixCommand(target, options = {}) {
|
|
|
332
370
|
}
|
|
333
371
|
const pkg = await readPackageJson(targetDir);
|
|
334
372
|
const lock = await readLockfile(targetDir);
|
|
335
|
-
const fixers = fixersForLanguage(await
|
|
373
|
+
const fixers = fixersForLanguage(await detectAuditLanguage(targetDir));
|
|
336
374
|
// Same --skills-dir the claude-skills fixer writes to, so the diagnosis fix
|
|
337
375
|
// acts on and the install it performs agree on one directory (#485).
|
|
338
376
|
const results = await runDoctor(targetDir, options.skillsDir);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { hookHasUncommented } from '../../base/checks.js';
|
|
3
4
|
export const PRE_PUSH_HOOK_CONTENT = `echo "🔍 Running pre-push verify..."
|
|
4
5
|
pnpm verify
|
|
5
6
|
STATUS=$?
|
|
@@ -25,10 +26,7 @@ export async function generateHuskyConfig(config, targetDir) {
|
|
|
25
26
|
// Pre-commit hook. husky v10 format: just the command — the v9 shebang +
|
|
26
27
|
// `. "$(dirname ...)/_/husky.sh"` bootstrap is deprecated (warns on every
|
|
27
28
|
// hook run in v9, fails outright in v10).
|
|
28
|
-
|
|
29
|
-
const preCommitContent = 'npx lint-staged\n';
|
|
30
|
-
await fs.writeFile(preCommitPath, preCommitContent);
|
|
31
|
-
await fs.chmod(preCommitPath, 0o755);
|
|
29
|
+
await ensureHookRuns(path.join(huskyDir, 'pre-commit'), /\blint-staged\b/, 'npx lint-staged\n');
|
|
32
30
|
// Pre-push hook — only when the package.json already has a `verify` script.
|
|
33
31
|
// In the setup flow, generatePackageJson runs before this and writes verify
|
|
34
32
|
// when 2+ tools are enabled. In the `fix husky` path, a pre-existing verify
|
|
@@ -63,9 +61,24 @@ export async function generateHuskyConfig(config, targetDir) {
|
|
|
63
61
|
export async function generatePrePushHook(targetDir) {
|
|
64
62
|
const huskyDir = path.join(targetDir, '.husky');
|
|
65
63
|
await fs.ensureDir(huskyDir);
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
await ensureHookRuns(path.join(huskyDir, 'pre-push'), /\bpnpm\s+verify\b/, PRE_PUSH_HOOK_CONTENT, 'pnpm verify\n');
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Write a missing hook; append the command to an existing hook that lacks it.
|
|
68
|
+
* Never replaces a hand-written hook (#629). Husky runs hooks with `sh -e`, so
|
|
69
|
+
* an appended command still fails the hook when it fails.
|
|
70
|
+
*/
|
|
71
|
+
async function ensureHookRuns(hookPath, runs, fresh, line = fresh) {
|
|
72
|
+
if (!(await fs.pathExists(hookPath))) {
|
|
73
|
+
await fs.writeFile(hookPath, fresh);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const existing = await fs.readFile(hookPath, 'utf-8');
|
|
77
|
+
if (hookHasUncommented(existing, runs))
|
|
78
|
+
return;
|
|
79
|
+
await fs.writeFile(hookPath, `${existing.replace(/\n*$/, '\n')}${line}`);
|
|
80
|
+
}
|
|
81
|
+
await fs.chmod(hookPath, 0o755);
|
|
69
82
|
}
|
|
70
83
|
/** Package versions the commit-msg hook needs on disk to run at all. */
|
|
71
84
|
export const COMMITLINT_DEPS = {
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { renderCodeQLWorkflow } from '../../base/ci.js';
|
|
4
4
|
import { LANGUAGES } from '../../languages/registry.js';
|
|
5
|
+
import { detectLanguage } from '../utils/detect-language.js';
|
|
5
6
|
/**
|
|
6
7
|
* The canonical dependency-update standard shared by every @rtorcato repo.
|
|
7
8
|
*
|
|
@@ -73,6 +74,37 @@ export const DEPENDABOT_CONFIG_PATHS = [
|
|
|
73
74
|
'.github/dependabot.yml',
|
|
74
75
|
'.github/dependabot.yaml',
|
|
75
76
|
];
|
|
77
|
+
/**
|
|
78
|
+
* The manifest ecosystem a repo's dependabot.yml should cover, or null when it
|
|
79
|
+
* has none — Perl, or a repo with no recognised manifest at all (#631). Unlike
|
|
80
|
+
* `resolveLanguageModule`, `unknown` does not fall back to npm here: a C++ repo
|
|
81
|
+
* with no package.json has nothing for an npm block to update.
|
|
82
|
+
*/
|
|
83
|
+
export async function dependabotEcosystemFor(dir) {
|
|
84
|
+
const language = await detectLanguage(dir);
|
|
85
|
+
return language === 'unknown' ? null : LANGUAGES[language].dependabotEcosystem;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* How an existing dependabot.yml falls short of `dependabotConfig(ecosystem)`.
|
|
89
|
+
* The canonical groups live on the manifest block, so a repo with no manifest
|
|
90
|
+
* ecosystem is not expected to carry them (#631).
|
|
91
|
+
*/
|
|
92
|
+
export function dependabotConfigDeltas(content, ecosystem) {
|
|
93
|
+
if (ecosystem === null)
|
|
94
|
+
return [];
|
|
95
|
+
return ['production-minor', 'dev-minor', 'major-updates']
|
|
96
|
+
.filter((group) => !new RegExp(`^\\s*${group}:`, 'm').test(content))
|
|
97
|
+
.map((group) => `missing \`${group}\` group`);
|
|
98
|
+
}
|
|
99
|
+
/** The existing dependabot config, in Dependabot's resolution order, or null. */
|
|
100
|
+
export async function readDependabotConfig(dir) {
|
|
101
|
+
for (const file of DEPENDABOT_CONFIG_PATHS) {
|
|
102
|
+
const candidate = path.join(dir, file);
|
|
103
|
+
if (await fs.pathExists(candidate))
|
|
104
|
+
return { file, content: await fs.readFile(candidate, 'utf8') };
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
76
108
|
/**
|
|
77
109
|
* The `ignore:` rules an existing dependabot.yml carries, named by
|
|
78
110
|
* `dependency-name` (#422).
|
|
@@ -236,14 +268,11 @@ export const DEPENDABOT_FILES = [
|
|
|
236
268
|
* they live in — or null when there is nothing to lose (#422).
|
|
237
269
|
*/
|
|
238
270
|
export async function findDependabotIgnoreRules(targetDir) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
return rules.length > 0 ? { file, rules } : null;
|
|
245
|
-
}
|
|
246
|
-
return null;
|
|
271
|
+
const existing = await readDependabotConfig(targetDir);
|
|
272
|
+
if (!existing)
|
|
273
|
+
return null;
|
|
274
|
+
const rules = dependabotIgnoreRules(existing.content);
|
|
275
|
+
return rules.length > 0 ? { file: existing.file, rules } : null;
|
|
247
276
|
}
|
|
248
277
|
/**
|
|
249
278
|
* Scaffold the canonical Dependabot setup: the grouped \`dependabot.yml\` **and**
|
|
@@ -32,9 +32,12 @@ export async function generateTypedocConfig(pkg, targetDir) {
|
|
|
32
32
|
};
|
|
33
33
|
await fs.writeJson(path.join(targetDir, 'typedoc.json'), config, { spaces: 2 });
|
|
34
34
|
}
|
|
35
|
+
/** The path written, or null when a docs.yml already exists — never replaced (#629). */
|
|
35
36
|
export async function generateTypedocWorkflow(targetDir) {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
await fs.
|
|
39
|
-
|
|
37
|
+
const rel = '.github/workflows/docs.yml';
|
|
38
|
+
const workflowPath = path.join(targetDir, rel);
|
|
39
|
+
if (await fs.pathExists(workflowPath))
|
|
40
|
+
return null;
|
|
41
|
+
await fs.outputFile(workflowPath, DOCS_WORKFLOW);
|
|
42
|
+
return rel;
|
|
40
43
|
}
|
|
@@ -32,6 +32,20 @@ export async function detectLanguage(dir) {
|
|
|
32
32
|
}
|
|
33
33
|
return 'unknown';
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The language doctor/fix audit a repo as. Like `detectLanguage`, except a fresh
|
|
37
|
+
* dir — nothing but dotfiles (`.git`, the lockfile) — is audited as JS, the
|
|
38
|
+
* default `setup` scaffolds, so its full suite shows what is still to come. A
|
|
39
|
+
* dir with real content and no marker (C++, Go, a README) stays 'unknown' and
|
|
40
|
+
* gets the base checks only (#632).
|
|
41
|
+
*/
|
|
42
|
+
export async function detectAuditLanguage(dir) {
|
|
43
|
+
const language = await detectLanguage(dir);
|
|
44
|
+
if (language !== 'unknown')
|
|
45
|
+
return language;
|
|
46
|
+
const entries = await fs.readdir(dir).catch(() => []);
|
|
47
|
+
return entries.every((entry) => entry.startsWith('.')) ? 'js' : 'unknown';
|
|
48
|
+
}
|
|
35
49
|
/** Never worth descending into, and expensive when we do. */
|
|
36
50
|
const SKIP_DIRS = new Set(['node_modules', 'dist', 'build', 'target', 'vendor', '.build']);
|
|
37
51
|
/**
|
|
@@ -695,7 +695,7 @@ export const FIXERS = [
|
|
|
695
695
|
await generateTypedocConfig(pkg, targetDir);
|
|
696
696
|
const workflow = await generateTypedocWorkflow(targetDir);
|
|
697
697
|
const pkgPath = path.join(targetDir, 'package.json');
|
|
698
|
-
const filesWritten = ['typedoc.json', workflow];
|
|
698
|
+
const filesWritten = workflow ? ['typedoc.json', workflow] : ['typedoc.json'];
|
|
699
699
|
if (await fs.pathExists(pkgPath)) {
|
|
700
700
|
const pkgData = (await fs.readJson(pkgPath));
|
|
701
701
|
const scripts = pkgData.scripts ?? {};
|
|
@@ -29,9 +29,11 @@ export const LANGUAGES = {
|
|
|
29
29
|
},
|
|
30
30
|
};
|
|
31
31
|
/**
|
|
32
|
-
* Resolve the module for a detected language
|
|
33
|
-
*
|
|
32
|
+
* Resolve the module for a detected language, or null for `unknown` — a repo
|
|
33
|
+
* with no marker (C++, Go, a README) gets the base suite only, never JS (#632).
|
|
34
|
+
* A fresh, empty dir never arrives as `unknown`: `detectAuditLanguage` audits
|
|
35
|
+
* it as JS, and setup's language picker defaults it to JS itself.
|
|
34
36
|
*/
|
|
35
37
|
export function resolveLanguageModule(language) {
|
|
36
|
-
return language === 'unknown' ?
|
|
38
|
+
return language === 'unknown' ? null : LANGUAGES[language];
|
|
37
39
|
}
|
package/package.json
CHANGED