@rtorcato/repo-tooling 4.7.0 → 5.0.0
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/AGENTS.md +0 -1
- package/dist/base/checks.js +0 -75
- package/dist/base/fixers.js +0 -36
- package/dist/base/secrets.js +126 -0
- package/dist/cli/commands/doctor.js +4 -8
- package/dist/cli/commands/fix-targets.js +0 -7
- package/dist/cli/commands/setup-presets.js +3 -5
- package/dist/cli/commands/setup.js +10 -11
- package/dist/cli/generators/index.js +0 -6
- package/dist/cli/index.js +9 -6
- package/dist/cli/utils/copy-preset.js +0 -20
- package/dist/cli/utils/lockfile.js +24 -2
- package/dist/languages/js/checks.js +0 -62
- package/dist/languages/js/fixers.js +0 -40
- package/package.json +1 -11
- package/tooling/semantic-release/github.mjs +1 -1
- package/dist/cli/generators/brand.js +0 -467
- package/dist/cli/generators/docs-site.js +0 -448
- package/tooling/docusaurus/docs-helpers.mjs +0 -109
- package/tooling/docusaurus/index.d.mts +0 -17
- package/tooling/docusaurus/index.mjs +0 -38
- package/tooling/docusaurus/sync-changelog.mjs +0 -106
- package/tooling/docusaurus/theme-tokens.css +0 -79
- package/tooling/docusaurus/theme.css +0 -390
package/AGENTS.md
CHANGED
|
@@ -84,7 +84,6 @@ npx @rtorcato/repo-tooling doctor --json -d ./existing-repo
|
|
|
84
84
|
# Apply one fixer from the list (run `list --json` for every target)
|
|
85
85
|
npx @rtorcato/repo-tooling fix dependabot --yes --json
|
|
86
86
|
npx @rtorcato/repo-tooling fix engines --yes --json
|
|
87
|
-
npx @rtorcato/repo-tooling fix docs-site --yes --json # scaffold a Docusaurus docs site under apps/docs
|
|
88
87
|
npx @rtorcato/repo-tooling fix bun --yes --json # Bun runtime/test config
|
|
89
88
|
```
|
|
90
89
|
|
package/dist/base/checks.js
CHANGED
|
@@ -100,81 +100,6 @@ export async function checkCommunityHealth(dir) {
|
|
|
100
100
|
hint: 'Run `npx @rtorcato/repo-tooling fix community-health` to scaffold them',
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
-
const BRAND_HINT = 'Run `npx @rtorcato/repo-tooling fix brand` to scaffold brand/ (SVG sources + render.sh) and render the PNGs (needs librsvg)';
|
|
104
|
-
/** The two banners the README consumes — the pair `brand/` exists to keep regenerable. */
|
|
105
|
-
const BANNERS = ['banner', 'banner-mobile'];
|
|
106
|
-
/**
|
|
107
|
-
* Brand assets (#395). A committed PNG with no SVG beside it can't be
|
|
108
|
-
* recoloured, retitled or resized — which was the state of every repo in the
|
|
109
|
-
* family but one. Three signals, all language-agnostic:
|
|
110
|
-
*
|
|
111
|
-
* 1. a rendered banner with no `brand/<name>.svg` source
|
|
112
|
-
* 2. a `brand/` folder with no `render.sh` — sources nobody can render
|
|
113
|
-
* 3. a README still pointing at the pre-amendment root-level `./banner.png`
|
|
114
|
-
*
|
|
115
|
-
* A repo with no brand images at all is `optional-missing`, not a finding:
|
|
116
|
-
* there's nothing broken to fix, and banners are opt-in.
|
|
117
|
-
*/
|
|
118
|
-
export async function checkBrand(dir) {
|
|
119
|
-
const check = 'Brand assets';
|
|
120
|
-
const has = (rel) => fs.pathExists(path.join(dir, rel));
|
|
121
|
-
const problems = [];
|
|
122
|
-
const orphans = [];
|
|
123
|
-
for (const name of BANNERS) {
|
|
124
|
-
const rendered = (await has(`${name}.png`)) || (await has(`brand/${name}.png`));
|
|
125
|
-
if (rendered && !(await has(`brand/${name}.svg`)))
|
|
126
|
-
orphans.push(`${name}.png`);
|
|
127
|
-
}
|
|
128
|
-
if (orphans.length > 0) {
|
|
129
|
-
problems.push(`${orphans.join(' + ')} committed with no brand/*.svg source`);
|
|
130
|
-
}
|
|
131
|
-
const brandDir = await has('brand');
|
|
132
|
-
if (brandDir && !(await has('brand/render.sh'))) {
|
|
133
|
-
problems.push('brand/ has no render.sh — the PNGs cannot be regenerated');
|
|
134
|
-
}
|
|
135
|
-
const readmePath = path.join(dir, 'README.md');
|
|
136
|
-
if (await fs.pathExists(readmePath)) {
|
|
137
|
-
const readme = await fs.readFile(readmePath, 'utf-8');
|
|
138
|
-
if (/(?<!brand\/)banner(?:-mobile)?\.png/.test(readme)) {
|
|
139
|
-
problems.push('README points at root-level banner PNGs (the spec moved them to brand/)');
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
if (problems.length > 0) {
|
|
143
|
-
return { check, status: 'drift', detail: problems.join('; '), hint: BRAND_HINT };
|
|
144
|
-
}
|
|
145
|
-
// Name the pieces still missing (#679). Severity is unchanged: brand is optional.
|
|
146
|
-
const missing = [];
|
|
147
|
-
if (!(await has('brand/favicon.svg')))
|
|
148
|
-
missing.push('favicon (brand/favicon.svg)');
|
|
149
|
-
const unrendered = [];
|
|
150
|
-
for (const n of ['banner', 'banner-mobile', 'social-card', 'favicon-512']) {
|
|
151
|
-
if (!(await has(`brand/${n}.png`)))
|
|
152
|
-
unrendered.push(`${n}.png`);
|
|
153
|
-
}
|
|
154
|
-
if (unrendered.length > 0)
|
|
155
|
-
missing.push(`rendered PNGs (brand/${unrendered.join(', ')})`);
|
|
156
|
-
const readmeFile = path.join(dir, 'README.md');
|
|
157
|
-
const readmeText = (await fs.pathExists(readmeFile)) ? await fs.readFile(readmeFile, 'utf-8') : '';
|
|
158
|
-
if (!/brand\/banner(?:-mobile)?\.png/.test(readmeText))
|
|
159
|
-
missing.push('README banner');
|
|
160
|
-
if (!brandDir) {
|
|
161
|
-
return {
|
|
162
|
-
check,
|
|
163
|
-
status: 'optional-missing',
|
|
164
|
-
detail: `no brand/ folder — missing: ${missing.join('; ')}`,
|
|
165
|
-
hint: BRAND_HINT,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
if (missing.length > 0) {
|
|
169
|
-
return {
|
|
170
|
-
check,
|
|
171
|
-
status: 'ok',
|
|
172
|
-
detail: `brand/ holds the SVG sources and render.sh; missing: ${missing.join('; ')}`,
|
|
173
|
-
hint: BRAND_HINT,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
return { check, status: 'ok', detail: 'brand/ holds the SVG sources and render.sh' };
|
|
177
|
-
}
|
|
178
103
|
/**
|
|
179
104
|
* `org/action@vN`, the only form the generators emit. Same shape as the scan in
|
|
180
105
|
* tests/cli/generators/action-pins.test.ts — major-only, because that's the
|
package/dist/base/fixers.js
CHANGED
|
@@ -15,7 +15,6 @@ import path from 'node:path';
|
|
|
15
15
|
import chalk from 'chalk';
|
|
16
16
|
import fs from 'fs-extra';
|
|
17
17
|
import { installAgentRules, installAiSetup } from '../cli/generators/agent-rules.js';
|
|
18
|
-
import { addReadmeBanner, generateBrand, renderBrand, resolveBrandMeta, syncBrandToDocs, } from '../cli/generators/brand.js';
|
|
19
18
|
import { generateCommunityHealth } from '../cli/generators/community-health.js';
|
|
20
19
|
import { generateCommitlintConfig } from '../cli/generators/git.js';
|
|
21
20
|
import { generateCodeowners, generateEditorConfig } from '../cli/generators/misc.js';
|
|
@@ -247,41 +246,6 @@ export const BASE_FIXERS = [
|
|
|
247
246
|
return { filesWritten };
|
|
248
247
|
},
|
|
249
248
|
},
|
|
250
|
-
{
|
|
251
|
-
target: 'brand',
|
|
252
|
-
selfSafe: true,
|
|
253
|
-
description: 'Scaffold brand/ — favicon, banner, mobile-banner and social-card SVG sources + render.sh — render the PNGs and favicon.ico when rsvg-convert is on PATH, and add the README banner',
|
|
254
|
-
appliesTo: ['Brand assets'],
|
|
255
|
-
outputs: [
|
|
256
|
-
'brand/favicon.svg',
|
|
257
|
-
'brand/banner.svg',
|
|
258
|
-
'brand/banner-mobile.svg',
|
|
259
|
-
'brand/social-card.svg',
|
|
260
|
-
'brand/render.sh',
|
|
261
|
-
'brand/banner.png',
|
|
262
|
-
'brand/banner-mobile.png',
|
|
263
|
-
'brand/social-card.png',
|
|
264
|
-
'brand/favicon-512.png',
|
|
265
|
-
'brand/favicon.ico',
|
|
266
|
-
'README.md',
|
|
267
|
-
],
|
|
268
|
-
// Every SVG is written only when absent, PNGs are re-rendered only when
|
|
269
|
-
// older than their source, and the README edit is a delimited block (or
|
|
270
|
-
// two image paths) — hand-edited art is never clobbered.
|
|
271
|
-
riskLevel: 'safe-merge',
|
|
272
|
-
canFixDrift: true,
|
|
273
|
-
async run({ targetDir, pkg, lock }) {
|
|
274
|
-
const tagline = lock?.rules?.brand?.tagline;
|
|
275
|
-
const filesWritten = await generateBrand(pkg, targetDir, tagline);
|
|
276
|
-
filesWritten.push(...((await renderBrand(targetDir)) ?? []));
|
|
277
|
-
filesWritten.push(...(await syncBrandToDocs(targetDir)));
|
|
278
|
-
const { name } = await resolveBrandMeta(pkg, targetDir, tagline);
|
|
279
|
-
const readme = await addReadmeBanner(targetDir, name);
|
|
280
|
-
if (readme && !filesWritten.includes(readme))
|
|
281
|
-
filesWritten.push(readme);
|
|
282
|
-
return { filesWritten };
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
249
|
{
|
|
286
250
|
target: 'ai',
|
|
287
251
|
description: 'Install all AI agent files at once (AGENTS.md, CLAUDE.md, Cursor, Copilot, Claude skill, MCP example)',
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { jobEnvironment, realGhExec, workflowJobs } from './github-settings.js';
|
|
4
|
+
const WHERE_TO_GET = {
|
|
5
|
+
CODECOV_TOKEN: 'codecov.io -> your repo -> Settings -> Repository Upload Token',
|
|
6
|
+
RELEASE_TOKEN: 'a fine-grained PAT with contents: write (or a GitHub App token) that can push past branch protection',
|
|
7
|
+
};
|
|
8
|
+
const withoutComments = (s) => s
|
|
9
|
+
.split('\n')
|
|
10
|
+
.filter((l) => !l.trimStart().startsWith('#'))
|
|
11
|
+
.join('\n');
|
|
12
|
+
function usesIn(text, file, job, environment) {
|
|
13
|
+
const clean = withoutComments(text);
|
|
14
|
+
const found = new Map();
|
|
15
|
+
for (const m of clean.matchAll(/secrets\.([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
16
|
+
const name = m[1];
|
|
17
|
+
if (name === 'GITHUB_TOKEN' || found.has(name))
|
|
18
|
+
continue;
|
|
19
|
+
const fallback = new RegExp(`secrets\\.${name}\\s*\\|\\|\\s*secrets\\.GITHUB_TOKEN`).test(clean);
|
|
20
|
+
found.set(name, { name, file, job, environment, fallback });
|
|
21
|
+
}
|
|
22
|
+
return [...found.values()];
|
|
23
|
+
}
|
|
24
|
+
/** Every non-GITHUB_TOKEN secret the workflows (and a composite action.yml) reference. */
|
|
25
|
+
export async function collectSecretUses(dir) {
|
|
26
|
+
const uses = [];
|
|
27
|
+
const wfDir = path.join(dir, '.github', 'workflows');
|
|
28
|
+
try {
|
|
29
|
+
const files = (await fs.pathExists(wfDir)) ? (await fs.readdir(wfDir)).sort() : [];
|
|
30
|
+
for (const f of files) {
|
|
31
|
+
if (!/\.ya?ml$/.test(f))
|
|
32
|
+
continue;
|
|
33
|
+
const content = await fs.readFile(path.join(wfDir, f), 'utf-8');
|
|
34
|
+
for (const [job, body] of workflowJobs(content)) {
|
|
35
|
+
uses.push(...usesIn(body, f, job, jobEnvironment(withoutComments(body))));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const a of ['action.yml', 'action.yaml']) {
|
|
39
|
+
const p = path.join(dir, a);
|
|
40
|
+
if (await fs.pathExists(p))
|
|
41
|
+
uses.push(...usesIn(await fs.readFile(p, 'utf-8'), a, '-', null));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
return uses;
|
|
48
|
+
}
|
|
49
|
+
/** One entry per secret name, for setup's Next Steps. */
|
|
50
|
+
export function requiredSecrets(uses) {
|
|
51
|
+
const byName = new Map();
|
|
52
|
+
for (const u of uses) {
|
|
53
|
+
const r = byName.get(u.name) ?? {
|
|
54
|
+
name: u.name,
|
|
55
|
+
usedBy: [],
|
|
56
|
+
whereToGet: WHERE_TO_GET[u.name] ?? null,
|
|
57
|
+
fallback: true,
|
|
58
|
+
};
|
|
59
|
+
if (!r.usedBy.includes(u.file))
|
|
60
|
+
r.usedBy.push(u.file);
|
|
61
|
+
r.fallback &&= u.fallback;
|
|
62
|
+
byName.set(u.name, r);
|
|
63
|
+
}
|
|
64
|
+
return [...byName.values()];
|
|
65
|
+
}
|
|
66
|
+
/** `gh secret set` lines plus where-to-get, for every secret in the list. */
|
|
67
|
+
export function secretHints(secrets) {
|
|
68
|
+
return secrets.map((s) => `gh secret set ${s.name}${s.whereToGet ? ` # ${s.whereToGet}` : ''}`);
|
|
69
|
+
}
|
|
70
|
+
async function listNames(gh, endpoint) {
|
|
71
|
+
const r = await gh(['api', `${endpoint}?per_page=100`]);
|
|
72
|
+
if (!r.ok)
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
const d = JSON.parse(r.stdout);
|
|
76
|
+
return new Set((d.secrets ?? []).flatMap((s) => (s.name ? [s.name] : [])));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const CHECK = 'Repository secrets';
|
|
83
|
+
const skip = (why) => ({
|
|
84
|
+
check: CHECK,
|
|
85
|
+
status: 'ok',
|
|
86
|
+
detail: `not checked (${why})`,
|
|
87
|
+
});
|
|
88
|
+
export async function checkRepositorySecrets(dir, exec) {
|
|
89
|
+
if (!(await fs.pathExists(path.join(dir, '.git'))))
|
|
90
|
+
return skip('not a git repository');
|
|
91
|
+
const uses = await collectSecretUses(dir);
|
|
92
|
+
if (uses.length === 0)
|
|
93
|
+
return { check: CHECK, status: 'ok', detail: 'no secrets referenced' };
|
|
94
|
+
const gh = exec ?? ((args, stdin) => realGhExec(args, stdin, dir));
|
|
95
|
+
const repo = await listNames(gh, 'repos/{owner}/{repo}/actions/secrets');
|
|
96
|
+
if (!repo)
|
|
97
|
+
return skip("can't list secrets — needs gh with admin access");
|
|
98
|
+
// Best effort: an org scope that errors just contributes nothing.
|
|
99
|
+
const org = (await listNames(gh, 'repos/{owner}/{repo}/actions/organization-secrets')) ?? new Set();
|
|
100
|
+
const envs = new Map();
|
|
101
|
+
for (const e of new Set(uses.flatMap((u) => (u.environment ? [u.environment] : [])))) {
|
|
102
|
+
envs.set(e, await listNames(gh, `repos/{owner}/{repo}/environments/${e}/secrets`));
|
|
103
|
+
}
|
|
104
|
+
const missing = uses.filter((u) => {
|
|
105
|
+
if (repo.has(u.name) || org.has(u.name))
|
|
106
|
+
return false;
|
|
107
|
+
if (!u.environment)
|
|
108
|
+
return true;
|
|
109
|
+
const env = envs.get(u.environment);
|
|
110
|
+
// An unreadable environment can't prove absence.
|
|
111
|
+
return env ? !env.has(u.name) : false;
|
|
112
|
+
});
|
|
113
|
+
if (missing.length === 0) {
|
|
114
|
+
return { check: CHECK, status: 'ok', detail: 'all referenced secrets are set' };
|
|
115
|
+
}
|
|
116
|
+
const hard = missing.some((u) => !u.fallback);
|
|
117
|
+
const names = missing
|
|
118
|
+
.map((u) => `${u.name} (${u.file})${u.fallback ? ' [falls back to GITHUB_TOKEN]' : ''}`)
|
|
119
|
+
.join(', ');
|
|
120
|
+
return {
|
|
121
|
+
check: CHECK,
|
|
122
|
+
status: hard ? 'missing' : 'drift',
|
|
123
|
+
detail: `not set: ${names}. Dependabot-triggered runs read Dependabot secrets, not these`,
|
|
124
|
+
hint: secretHints(requiredSecrets(missing)).join('\n'),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -14,14 +14,15 @@ import { SWIFT_GIT_HOOKS, runSwiftChecks } from '../../languages/swift/checks.js
|
|
|
14
14
|
import { readSwiftPackage, renderSwiftWorkflow } from '../../languages/swift/ci.js';
|
|
15
15
|
import { detectAuditLanguage } from '../utils/detect-language.js';
|
|
16
16
|
import { checkGitHubSettings } from '../../base/github-settings.js';
|
|
17
|
+
import { checkRepositorySecrets } from '../../base/secrets.js';
|
|
17
18
|
import { checkMilestones } from '../../base/milestones.js';
|
|
18
19
|
import { checkGitIdentity, checkGitIdentityHistory } from '../../base/git-identity.js';
|
|
19
20
|
import { checkCopiedAssets } from '../utils/copied-assets.js';
|
|
20
21
|
import { LOCKFILE_VERSION, readLockfile } from '../utils/lockfile.js';
|
|
21
22
|
import { compareRulesWithReference } from '../utils/reference-rules.js';
|
|
22
23
|
import { declinedInLock, getFixTargetForCheck } from './fix-targets.js';
|
|
23
|
-
import { checkAiSetup,
|
|
24
|
-
import { allDeps, checkAreTheTypesWrong, checkBiome, checkClaudeWorktreeSettings, checkConfigSchemaVersions,
|
|
24
|
+
import { checkAiSetup, checkCodeowners, checkCodeQL, checkCommunityHealth, checkCoverageUpload, checkDependabot, checkEditorConfig, checkFile, checkGitHooks, checkGitHubActions, checkGitLabCI, checkNestedLanguages, checkPrePushHook, checkReadmeBadges, checkRecommendedMcp, COMMITLINT_FILE_CHECK, } from '../../base/checks.js';
|
|
25
|
+
import { allDeps, checkAreTheTypesWrong, checkBiome, checkClaudeWorktreeSettings, checkConfigSchemaVersions, checkEnginesNode, checkExportsBuildable, checkGitDependencies, checkKnip, checkLintStaged, checkNodeVersionConsistency, checkNodeVersionPin, checkPackageJson, checkPackageManager, checkPeerVersions, checkPublint, checkSemanticRelease, checkSizeLimit, checkTailwind, checkBuildApprovals, checkPnpmWorkspace, checkTreeshakeSetup, checkTurborepo, checkTypedoc, checkVerifyScript, checkVscodeExtensions, evaluateNodeVersion, FILE_CHECKS, jsBadgeAudience, jsGitHooksProfile, readPackageJson, } from '../../languages/js/checks.js';
|
|
25
26
|
export { evaluateNodeVersion };
|
|
26
27
|
const PACKAGE = '@rtorcato/repo-tooling';
|
|
27
28
|
// Detects the broken-release-on-protected-main footgun: a workflow that runs
|
|
@@ -211,12 +212,12 @@ async function runBaseChecks(dir, lock, opts) {
|
|
|
211
212
|
// GitHub repo-settings drift (branch protection, merge settings, workflow
|
|
212
213
|
// permissions). Read-only; self-skips as `ok` outside a live GitHub repo.
|
|
213
214
|
results.push(...(await checkGitHubSettings(dir)));
|
|
215
|
+
results.push(await checkRepositorySecrets(dir));
|
|
214
216
|
// Milestone hygiene (#397) — same seam, same self-skip.
|
|
215
217
|
results.push(await checkMilestones(dir));
|
|
216
218
|
results.push(await checkGitLabCI(dir));
|
|
217
219
|
results.push(await checkCodeowners(dir));
|
|
218
220
|
results.push(await checkCommunityHealth(dir));
|
|
219
|
-
results.push(await checkBrand(dir));
|
|
220
221
|
results.push(await checkAiSetup(dir));
|
|
221
222
|
// #534: advisory. Absent `mcp.recommended` means the repo has nothing to say
|
|
222
223
|
// about MCP, which is not a finding.
|
|
@@ -373,11 +374,6 @@ export async function runDoctor(dir) {
|
|
|
373
374
|
if (await fs.pathExists(path.join(targetDir, 'pnpm-workspace.yaml'))) {
|
|
374
375
|
results.push(await checkTurborepo(targetDir));
|
|
375
376
|
}
|
|
376
|
-
// Docs site is opt-in — only surface the check when a Docusaurus site exists.
|
|
377
|
-
const docsAppDir = await findDocsAppDir(targetDir);
|
|
378
|
-
if (docsAppDir) {
|
|
379
|
-
results.push(await checkDocsSite(targetDir, docsAppDir));
|
|
380
|
-
}
|
|
381
377
|
// Tailwind is opt-in — only surface the check when the repo actually depends on it.
|
|
382
378
|
if ('tailwindcss' in allDeps(pkg)) {
|
|
383
379
|
results.push(await checkTailwind(targetDir, pkg));
|
|
@@ -45,7 +45,6 @@ export const FIX_TARGETS = {
|
|
|
45
45
|
'are-the-types-wrong': 'attw',
|
|
46
46
|
publint: 'publint',
|
|
47
47
|
'README badges': 'badges',
|
|
48
|
-
'Brand assets': 'brand',
|
|
49
48
|
TypeDoc: 'typedoc',
|
|
50
49
|
'AI setup': 'ai',
|
|
51
50
|
'Claude worktree settings': 'ai',
|
|
@@ -177,8 +176,6 @@ export function declinedInLock(lock, checkName) {
|
|
|
177
176
|
return c.turborepo === false;
|
|
178
177
|
case 'Tailwind':
|
|
179
178
|
return c.tailwind === false;
|
|
180
|
-
case 'Brand assets':
|
|
181
|
-
return c.brand === false;
|
|
182
179
|
default:
|
|
183
180
|
return false;
|
|
184
181
|
}
|
|
@@ -248,12 +245,8 @@ export function lockfilePatchForTarget(target, lock) {
|
|
|
248
245
|
return c.nx ? null : { nx: true };
|
|
249
246
|
case 'tailwind':
|
|
250
247
|
return c.tailwind ? null : { tailwind: true };
|
|
251
|
-
case 'docs-site':
|
|
252
|
-
return c.docsSite ? null : { docsSite: true };
|
|
253
248
|
case 'bun':
|
|
254
249
|
return c.bun ? null : { bun: true };
|
|
255
|
-
case 'brand':
|
|
256
|
-
return c.brand ? null : { brand: true };
|
|
257
250
|
default:
|
|
258
251
|
return null;
|
|
259
252
|
}
|
|
@@ -59,7 +59,6 @@ export function buildPresetConfig(name, projectName) {
|
|
|
59
59
|
semanticRelease: true,
|
|
60
60
|
bundler: 'tsup',
|
|
61
61
|
publint: true,
|
|
62
|
-
brand: true,
|
|
63
62
|
};
|
|
64
63
|
case 'web-app':
|
|
65
64
|
return {
|
|
@@ -210,8 +209,9 @@ export const CONFIG_SCHEMA = {
|
|
|
210
209
|
turborepo: { type: 'boolean' },
|
|
211
210
|
nx: { type: 'boolean' },
|
|
212
211
|
tailwind: { type: 'boolean' },
|
|
213
|
-
|
|
214
|
-
|
|
212
|
+
// Deprecated (#718): both scaffolds moved to @rtorcato/shared-docs. Accepted, inert.
|
|
213
|
+
docsSite: { type: 'boolean', deprecated: true },
|
|
214
|
+
brand: { type: 'boolean', deprecated: true },
|
|
215
215
|
bun: { type: 'boolean' },
|
|
216
216
|
},
|
|
217
217
|
};
|
|
@@ -317,8 +317,6 @@ export function computeFileList(config) {
|
|
|
317
317
|
files.push('postcss.config.mjs', 'src/styles/globals.css');
|
|
318
318
|
if (config.bun)
|
|
319
319
|
files.push('bunfig.toml');
|
|
320
|
-
if (config.brand)
|
|
321
|
-
files.push('brand/banner.svg', 'brand/banner-mobile.svg', 'brand/social-card.svg', 'brand/render.sh');
|
|
322
320
|
files.push('README.md');
|
|
323
321
|
return files;
|
|
324
322
|
}
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import fs from 'fs-extra';
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
|
+
import { collectSecretUses, requiredSecrets, secretHints } from '../../base/secrets.js';
|
|
5
6
|
import { formatNpmPublishGuide, npmPublishGuide, } from '../../languages/js/npm-trust.js';
|
|
6
7
|
import { LANGUAGES } from '../../languages/registry.js';
|
|
7
8
|
import { generateConfigs } from '../generators/index.js';
|
|
@@ -23,7 +24,6 @@ const REVIEWABLE_FEATURES = [
|
|
|
23
24
|
{ key: 'securityAutomation', label: 'Security automation (Dependabot + CodeQL)' },
|
|
24
25
|
{ key: 'badges', label: 'README status badges' },
|
|
25
26
|
{ key: 'aiSetup', label: 'AI agent rules (AGENTS.md, CLAUDE.md, Cursor, Copilot)' },
|
|
26
|
-
{ key: 'brand', label: 'Brand assets (brand/ banner + social-card SVGs, render.sh)' },
|
|
27
27
|
];
|
|
28
28
|
/**
|
|
29
29
|
* A preset run may only ask questions when a human is on both ends of the pipe.
|
|
@@ -182,7 +182,7 @@ export async function setupProject(options) {
|
|
|
182
182
|
await formatGeneratedFiles(config, targetDir);
|
|
183
183
|
}
|
|
184
184
|
console.log(chalk.green('\n✅ Setup completed successfully!\n'));
|
|
185
|
-
showNextSteps(config, targetDir);
|
|
185
|
+
await showNextSteps(config, targetDir);
|
|
186
186
|
}
|
|
187
187
|
catch (error) {
|
|
188
188
|
console.error(chalk.red('\n❌ Setup failed:'), error);
|
|
@@ -460,13 +460,6 @@ async function promptForConfig(targetDir, seed) {
|
|
|
460
460
|
message: '🤖 Add AI agent rules (AGENTS.md, CLAUDE.md, Cursor, Copilot, Claude skill)?',
|
|
461
461
|
default: seed?.aiSetup ?? true,
|
|
462
462
|
},
|
|
463
|
-
{
|
|
464
|
-
type: 'confirm',
|
|
465
|
-
name: 'brand',
|
|
466
|
-
message: '🎨 Add brand assets (brand/ banner + social-card SVG sources, render.sh)?',
|
|
467
|
-
// On by default for libraries — the ones that publish a README to npm.
|
|
468
|
-
default: (answers) => seed?.brand ?? answers.projectType === 'library',
|
|
469
|
-
},
|
|
470
463
|
{
|
|
471
464
|
type: 'select',
|
|
472
465
|
name: 'orchestrator',
|
|
@@ -556,7 +549,6 @@ async function promptForConfig(targetDir, seed) {
|
|
|
556
549
|
publint: answers.publint ?? false,
|
|
557
550
|
badges: answers.badges ?? false,
|
|
558
551
|
aiSetup: answers.aiSetup ?? false,
|
|
559
|
-
brand: answers.brand ?? false,
|
|
560
552
|
turborepo: answers.orchestrator === 'turbo',
|
|
561
553
|
nx: answers.orchestrator === 'nx',
|
|
562
554
|
tailwind: answers.tailwind ?? false,
|
|
@@ -580,7 +572,7 @@ export function npmPublishFor(config) {
|
|
|
580
572
|
environment: null,
|
|
581
573
|
});
|
|
582
574
|
}
|
|
583
|
-
function showNextSteps(config,
|
|
575
|
+
async function showNextSteps(config, targetDir) {
|
|
584
576
|
console.log(chalk.bold('\n📋 Next Steps:\n'));
|
|
585
577
|
const steps = [];
|
|
586
578
|
// Every step in the else-branch names a pnpm script. Swift's equivalents are
|
|
@@ -615,6 +607,13 @@ function showNextSteps(config, _targetDir) {
|
|
|
615
607
|
for (const line of formatNpmPublishGuide(npmPublish, true))
|
|
616
608
|
console.log(` ${line}`);
|
|
617
609
|
}
|
|
610
|
+
const secrets = requiredSecrets(await collectSecretUses(targetDir));
|
|
611
|
+
if (secrets.length > 0) {
|
|
612
|
+
console.log(chalk.bold('\n🔐 GitHub setup (secrets the workflows need):\n'));
|
|
613
|
+
for (const line of secretHints(secrets))
|
|
614
|
+
console.log(` ${line}`);
|
|
615
|
+
console.log(' Then: `fix github-settings` (branch protection), `fix release-environment` (release gate), and `doctor` to confirm');
|
|
616
|
+
}
|
|
618
617
|
const skipped = collectSkippedFixSuggestions(config);
|
|
619
618
|
if (skipped.length > 0) {
|
|
620
619
|
console.log(chalk.bold('\n💡 Want to add something you skipped?\n'));
|
|
@@ -19,7 +19,6 @@ import { generateTailwind } from './tailwind.js';
|
|
|
19
19
|
import { generateTurborepo } from './turborepo.js';
|
|
20
20
|
import { generateNx } from './nx.js';
|
|
21
21
|
import { generateBun } from './bun.js';
|
|
22
|
-
import { generateBrand } from './brand.js';
|
|
23
22
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
23
|
const __dirname = path.dirname(__filename);
|
|
25
24
|
export async function generateConfigs(config, targetDir) {
|
|
@@ -123,11 +122,6 @@ export async function generateConfigs(config, targetDir) {
|
|
|
123
122
|
}
|
|
124
123
|
// Generate README
|
|
125
124
|
await generateReadme(config, targetDir);
|
|
126
|
-
// brand/ (#677) — the same generator as `fix brand`. After the README so the
|
|
127
|
-
// banner-path repoint sees it; no tagline yet, as the lockfile isn't written.
|
|
128
|
-
if (config.brand) {
|
|
129
|
-
await generateBrand(await fs.readJson(path.join(targetDir, 'package.json')), targetDir);
|
|
130
|
-
}
|
|
131
125
|
// Copy ts-reset if TypeScript is enabled
|
|
132
126
|
if (config.typescript.enabled) {
|
|
133
127
|
await copyTSReset(targetDir);
|
package/dist/cli/index.js
CHANGED
|
@@ -178,12 +178,6 @@ const TOOL_CATALOG = [
|
|
|
178
178
|
exports: ['@rtorcato/repo-tooling/typedoc'],
|
|
179
179
|
fixTarget: 'typedoc',
|
|
180
180
|
},
|
|
181
|
-
{
|
|
182
|
-
name: 'Docs site',
|
|
183
|
-
description: 'Docusaurus docs site (apps/docs) with shared tokens + GitHub Pages deploy',
|
|
184
|
-
exports: [],
|
|
185
|
-
fixTarget: 'docs-site',
|
|
186
|
-
},
|
|
187
181
|
{
|
|
188
182
|
name: 'esbuild',
|
|
189
183
|
description: 'Fast JavaScript bundler configuration',
|
|
@@ -340,7 +334,16 @@ program
|
|
|
340
334
|
'\n `npx @rtorcato/repo-ai fix claude-skills --force-skills`.');
|
|
341
335
|
process.exitCode = 1;
|
|
342
336
|
});
|
|
337
|
+
// The docs-site and brand scaffolds moved to @rtorcato/shared-docs (#718). The
|
|
338
|
+
// fix targets stay as hidden pointers so old instructions fail with the fix.
|
|
339
|
+
const MOVED_TARGETS = ['docs-site', 'brand'];
|
|
343
340
|
program.hook('preAction', async (_, actionCommand) => {
|
|
341
|
+
const target = actionCommand.args[0];
|
|
342
|
+
if (actionCommand.name() === 'fix' && target && MOVED_TARGETS.includes(target)) {
|
|
343
|
+
console.error(chalk.red(`\`repo-tooling fix ${target}\` moved to @rtorcato/shared-docs.`) +
|
|
344
|
+
`\n Run \`npx @rtorcato/shared-docs …\` (see its README).`);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
344
347
|
const refusal = await selfRepoRefusal(actionCommand.name(), actionCommand.args[0], actionCommand.opts());
|
|
345
348
|
if (refusal) {
|
|
346
349
|
console.log(chalk.yellow('\n⚠️ This command cannot be run inside the @rtorcato/repo-tooling repo itself.\n'));
|
|
@@ -96,26 +96,6 @@ export const PRESETS = {
|
|
|
96
96
|
target: '.mcp.json.example',
|
|
97
97
|
desc: 'Commented MCP server template (copy to .mcp.json to activate)',
|
|
98
98
|
},
|
|
99
|
-
'docusaurus-sync-changelog': {
|
|
100
|
-
source: 'tooling/docusaurus/sync-changelog.mjs',
|
|
101
|
-
target: 'scripts/sync-changelog.mjs',
|
|
102
|
-
desc: 'Canonical CHANGELOG → docs sync script for Docusaurus sites',
|
|
103
|
-
},
|
|
104
|
-
'docusaurus-docs-helpers': {
|
|
105
|
-
source: 'tooling/docusaurus/docs-helpers.mjs',
|
|
106
|
-
target: 'scripts/docs-helpers.mjs',
|
|
107
|
-
desc: 'Docs-generator helpers (markdown-table escaping, export parser, generated-block splice)',
|
|
108
|
-
},
|
|
109
|
-
'docusaurus-theme-tokens': {
|
|
110
|
-
source: 'tooling/docusaurus/theme-tokens.css',
|
|
111
|
-
target: 'apps/docs/src/css/_jt-tokens.css',
|
|
112
|
-
desc: 'Shared Docusaurus design tokens (Geist + navy surfaces; accent per-project)',
|
|
113
|
-
},
|
|
114
|
-
'docusaurus-theme': {
|
|
115
|
-
source: 'tooling/docusaurus/theme.css',
|
|
116
|
-
target: 'apps/docs/src/css/theme.css',
|
|
117
|
-
desc: 'Shared Docusaurus component theme (navbar, cards, sidebar, footer, tables; accent-agnostic — @import after docusaurus-theme-tokens)',
|
|
118
|
-
},
|
|
119
99
|
};
|
|
120
100
|
export function getPackageRoot() {
|
|
121
101
|
const cliFile = new URL(import.meta.url).pathname;
|
|
@@ -163,11 +163,33 @@ export function lockfileSchema() {
|
|
|
163
163
|
brand: {
|
|
164
164
|
type: 'object',
|
|
165
165
|
additionalProperties: false,
|
|
166
|
-
|
|
166
|
+
deprecated: true,
|
|
167
|
+
description: 'Deprecated: the brand scaffold moved to @rtorcato/shared-docs. Still accepted so existing files validate; removed in the next major.',
|
|
167
168
|
properties: {
|
|
168
169
|
tagline: {
|
|
169
170
|
type: 'string',
|
|
170
|
-
description: 'Short line for the banners and social card, used in place of the package.json description. Keep it to two lines of about 42 characters
|
|
171
|
+
description: 'Short line for the banners and social card, used in place of the package.json description. Keep it to two lines of about 42 characters.',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
docs: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
deprecated: true,
|
|
179
|
+
description: 'Deprecated: the docs-site scaffold moved to @rtorcato/shared-docs, which has the sibling list natively. Still accepted so existing files validate; removed in the next major.',
|
|
180
|
+
properties: {
|
|
181
|
+
siblings: {
|
|
182
|
+
type: 'array',
|
|
183
|
+
description: 'Sibling projects to link from the scaffolded docs site (navbar, footer, intro page). Opt-in: when absent the scaffold emits no such links.',
|
|
184
|
+
items: {
|
|
185
|
+
type: 'object',
|
|
186
|
+
additionalProperties: false,
|
|
187
|
+
required: ['label', 'href'],
|
|
188
|
+
properties: {
|
|
189
|
+
label: { type: 'string', minLength: 1 },
|
|
190
|
+
href: { type: 'string', minLength: 1 },
|
|
191
|
+
},
|
|
192
|
+
},
|
|
171
193
|
},
|
|
172
194
|
},
|
|
173
195
|
},
|
|
@@ -1425,68 +1425,6 @@ export async function checkBuildApprovals(dir, pkg) {
|
|
|
1425
1425
|
hint: `Record a decision per package under \`allowBuilds:\` in ${WORKSPACE_FILE} (\`name: true\` to run it, \`false\` to skip). pnpm 11 fails \`pnpm install --frozen-lockfile\` outright while any is undecided, so CI cannot pass until each is answered.`,
|
|
1426
1426
|
};
|
|
1427
1427
|
}
|
|
1428
|
-
/** The docs app dir (apps/docs or apps/doc) if a Docusaurus config lives there. */
|
|
1429
|
-
export async function findDocsAppDir(dir) {
|
|
1430
|
-
for (const app of ['apps/docs', 'apps/doc']) {
|
|
1431
|
-
if (await fs.pathExists(path.join(dir, app, 'docusaurus.config.ts')))
|
|
1432
|
-
return app;
|
|
1433
|
-
}
|
|
1434
|
-
return null;
|
|
1435
|
-
}
|
|
1436
|
-
// Only surfaced when a Docusaurus site exists (see runDoctor). Verifies the
|
|
1437
|
-
// shared-asset wiring #54 standardizes: sync-changelog present + chained into
|
|
1438
|
-
// the docs app's build/start, and the deploy artifact path is `build` (not
|
|
1439
|
-
// `dist`). Opt-in, so a repo without a docs site never sees this.
|
|
1440
|
-
export async function checkDocsSite(dir, docsAppDir) {
|
|
1441
|
-
const check = 'Docs site';
|
|
1442
|
-
const deltas = [];
|
|
1443
|
-
// sync-changelog pumps a root CHANGELOG.md into the site — a semantic-release
|
|
1444
|
-
// shape. Changesets writes per-package changelogs and leaves the (private)
|
|
1445
|
-
// root package without one, so don't require the script there (#425). If a
|
|
1446
|
-
// Changesets repo keeps the script anyway, it still has to be wired.
|
|
1447
|
-
const syncPath = path.join(dir, 'scripts', 'sync-changelog.mjs');
|
|
1448
|
-
if (!(await fs.pathExists(syncPath))) {
|
|
1449
|
-
if (!(await usesChangesets(dir)))
|
|
1450
|
-
deltas.push('scripts/sync-changelog.mjs missing');
|
|
1451
|
-
}
|
|
1452
|
-
else {
|
|
1453
|
-
const pkgPath = path.join(dir, docsAppDir, 'package.json');
|
|
1454
|
-
if (await fs.pathExists(pkgPath)) {
|
|
1455
|
-
try {
|
|
1456
|
-
const docsPkg = (await fs.readJson(pkgPath));
|
|
1457
|
-
const scripts = docsPkg.scripts ?? {};
|
|
1458
|
-
const chained = ['build', 'start'].some((s) => typeof scripts[s] === 'string' && /sync-changelog/.test(scripts[s]));
|
|
1459
|
-
// pnpm 8 doesn't run pre* hooks reliably, so build/start must chain it explicitly.
|
|
1460
|
-
if (!chained)
|
|
1461
|
-
deltas.push(`${docsAppDir} build/start does not chain sync-changelog`);
|
|
1462
|
-
}
|
|
1463
|
-
catch {
|
|
1464
|
-
// Unparseable package.json — a separate check owns that; skip here.
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
// The GitHub Pages deploy must upload `apps/doc*/build` (Docusaurus emits
|
|
1469
|
-
// `build/`, not `dist/`).
|
|
1470
|
-
const workflowsDir = path.join(dir, '.github', 'workflows');
|
|
1471
|
-
if (await fs.pathExists(workflowsDir)) {
|
|
1472
|
-
for (const file of await fs.readdir(workflowsDir)) {
|
|
1473
|
-
if (!/\.ya?ml$/.test(file))
|
|
1474
|
-
continue;
|
|
1475
|
-
const body = await fs.readFile(path.join(workflowsDir, file), 'utf-8').catch(() => '');
|
|
1476
|
-
if (new RegExp(`${docsAppDir}/dist`).test(body)) {
|
|
1477
|
-
deltas.push(`${file} deploys ${docsAppDir}/dist (should be ${docsAppDir}/build)`);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
if (deltas.length)
|
|
1482
|
-
return {
|
|
1483
|
-
check,
|
|
1484
|
-
status: 'drift',
|
|
1485
|
-
detail: deltas.join('; '),
|
|
1486
|
-
hint: 'Align the docs site with the shared standard (sync-changelog wiring, build/ artifact path)',
|
|
1487
|
-
};
|
|
1488
|
-
return { check, status: 'ok', detail: `${docsAppDir} wired per standard` };
|
|
1489
|
-
}
|
|
1490
1428
|
// Only called when `tailwindcss` is a dependency (see runDoctor) — Tailwind is
|
|
1491
1429
|
// opt-in per project, so nudging repos that don't use it would be noise. v4 is
|
|
1492
1430
|
// CSS-first: the wiring is a PostCSS plugin (or the Vite plugin), not a config
|