@rtorcato/repo-tooling 3.2.5 → 3.3.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/README.md CHANGED
@@ -72,6 +72,14 @@ See the [Getting Started guide](https://rtorcato.github.io/repo-tooling/guides/g
72
72
  | `doctor` | Diagnose an existing project for missing or drifted tooling. | `npx @rtorcato/repo-tooling doctor` |
73
73
  | `fix [target]` | Apply scaffolders for what `doctor` flagged (`--yes`, `--dry-run`, `--diff`). | `npx @rtorcato/repo-tooling fix` |
74
74
 
75
+ Prefer to run the audit in CI? `doctor` also ships as a GitHub Action:
76
+
77
+ ```yaml
78
+ - uses: rtorcato/repo-tooling@v3.2.5
79
+ ```
80
+
81
+ See the [GitHub Actions reference](https://rtorcato.github.io/repo-tooling/reference/github-actions/#run-doctor-as-a-github-action) for its inputs and outputs.
82
+
75
83
  Every command takes `-d, --directory <path>`; run any with `--help` for its full flags. Run `list` (or `list --json`) for the full set of `fix` targets — it's the source of truth. Notable ones include `fix docs-site` (scaffold a [Docusaurus docs site](https://rtorcato.github.io/repo-tooling/guides/docs-site/)) and `fix bun` (Bun runtime config).
76
84
 
77
85
  ## The `.repo-tooling.json` lockfile
@@ -71,7 +71,27 @@ export async function checkCommunityHealth(dir) {
71
71
  hint: 'Run `npx @rtorcato/repo-tooling fix community-health` to scaffold them',
72
72
  };
73
73
  }
74
- export async function checkGitHubActions(dir) {
74
+ /**
75
+ * `org/action@vN`, the only form the generators emit. Same shape as the scan in
76
+ * tests/cli/generators/action-pins.test.ts — major-only, because that's the
77
+ * granularity every emitted pin uses.
78
+ */
79
+ const ACTION_PIN = /\b([a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*)@v(\d+)/g;
80
+ function actionPins(yaml) {
81
+ const pins = new Map();
82
+ for (const [, action, major] of yaml.matchAll(ACTION_PIN)) {
83
+ if (action && major)
84
+ pins.set(action, major);
85
+ }
86
+ return pins;
87
+ }
88
+ /**
89
+ * @param preset the ci.yml this repo's generator would render right now, or null
90
+ * for a language whose CI generator isn't wired up. Supplied by the caller
91
+ * rather than rendered here: the JS preset needs a ProjectConfig and the Swift
92
+ * one needs Package.swift, neither of which belongs in a base check.
93
+ */
94
+ export async function checkGitHubActions(dir, preset = null) {
75
95
  const workflowsDir = path.join(dir, '.github', 'workflows');
76
96
  if (!(await fs.pathExists(workflowsDir))) {
77
97
  return {
@@ -92,6 +112,33 @@ export async function checkGitHubActions(dir) {
92
112
  hint: 'Add a workflow file (e.g. ci.yml) under .github/workflows/',
93
113
  };
94
114
  }
115
+ // "A workflow exists" said nothing about whether it still matches the preset,
116
+ // so doctor reported ok on a ci.yml that had drifted arbitrarily far — the
117
+ // blind half of #349/#340. Compare action pins rather than the whole file: a
118
+ // consuming repo is *expected* to add jobs and steps, so a byte diff is noise
119
+ // on every customized repo, while a pin major that disagrees with the preset
120
+ // is exactly the drift that loops (a bump here, a sync there, forever).
121
+ const ciPath = path.join(workflowsDir, 'ci.yml');
122
+ if (preset && (await fs.pathExists(ciPath))) {
123
+ const ours = actionPins(preset);
124
+ const deltas = [];
125
+ for (const [action, major] of actionPins(await fs.readFile(ciPath, 'utf-8'))) {
126
+ const expected = ours.get(action);
127
+ // Only the intersection is comparable — an action the repo runs but the
128
+ // preset never emits is the consumer's own business.
129
+ if (expected && expected !== major) {
130
+ deltas.push(`${action}@v${major} (preset emits @v${expected})`);
131
+ }
132
+ }
133
+ if (deltas.length > 0) {
134
+ return {
135
+ check: 'GitHub Actions',
136
+ status: 'drift',
137
+ detail: `ci.yml action pins disagree with the preset: ${deltas.join('; ')}`,
138
+ hint: 'Run `npx @rtorcato/repo-tooling fix github-actions --diff` to see the delta before overwriting — a pin *ahead* of the preset means regenerating would downgrade it',
139
+ };
140
+ }
141
+ }
95
142
  return {
96
143
  check: 'GitHub Actions',
97
144
  status: 'ok',
@@ -1,8 +1,12 @@
1
1
  import path from 'node:path';
2
2
  import chalk from 'chalk';
3
3
  import fs from 'fs-extra';
4
+ import { renderGitHubWorkflow } from '../../base/ci.js';
5
+ import { githubJobs } from '../../languages/js/ci.js';
6
+ import { inferProjectConfig } from '../../languages/js/fixers.js';
4
7
  import { resolveLanguageModule } from '../../languages/registry.js';
5
8
  import { SWIFT_GIT_HOOKS, runSwiftChecks } from '../../languages/swift/checks.js';
9
+ import { readSwiftPackage, renderSwiftWorkflow } from '../../languages/swift/ci.js';
6
10
  import { detectLanguage } from '../utils/detect-language.js';
7
11
  import { checkGitHubSettings } from '../../base/github-settings.js';
8
12
  import { checkGitIdentity } from '../../base/git-identity.js';
@@ -148,7 +152,7 @@ async function runBaseChecks(dir, lock, opts) {
148
152
  results.push(await checkGitHooks(dir, opts.hooks));
149
153
  results.push(await checkPrePushHook(dir, opts.hooks));
150
154
  }
151
- results.push(await checkGitHubActions(dir));
155
+ results.push(await checkGitHubActions(dir, opts.presetWorkflow));
152
156
  results.push(await checkDependabot(dir));
153
157
  results.push(await checkCodeQL(dir));
154
158
  // GitHub repo-settings drift (branch protection, merge settings, workflow
@@ -186,6 +190,7 @@ export async function runDoctor(dir) {
186
190
  ...(await runBaseChecks(targetDir, lock, {
187
191
  hooks: null,
188
192
  badges: { audience: 'public', fixTarget: null },
193
+ presetWorkflow: null,
189
194
  })),
190
195
  ];
191
196
  return demoteDeclined(results, lock);
@@ -205,6 +210,7 @@ export async function runDoctor(dir) {
205
210
  // badges always apply. No fixer: `fix badges` derives the block from
206
211
  // package.json name/repository, which a Swift repo hasn't got.
207
212
  badges: { audience: 'public', fixTarget: null },
213
+ presetWorkflow: renderSwiftWorkflow(await readSwiftPackage(targetDir)),
208
214
  })),
209
215
  ...(await runSwiftChecks(targetDir)),
210
216
  ];
@@ -254,6 +260,7 @@ export async function runDoctor(dir) {
254
260
  results.push(...(await runBaseChecks(targetDir, lock, {
255
261
  hooks: jsGitHooksProfile(pkg),
256
262
  badges: { audience: jsBadgeAudience(pkg), fixTarget: 'badges' },
263
+ presetWorkflow: renderGitHubWorkflow(githubJobs(inferProjectConfig(pkg))),
257
264
  })));
258
265
  return demoteDeclined(results, lock);
259
266
  }
@@ -57,6 +57,7 @@ const SWIFT_FIX_TARGETS = {
57
57
  SwiftLint: 'swiftlint',
58
58
  Periphery: 'periphery',
59
59
  'Swift .gitignore': 'swift-gitignore',
60
+ 'Release automation': 'swift-release',
60
61
  };
61
62
  export function getFixTargetForCheck(checkName, language) {
62
63
  if (language === 'swift' && SWIFT_FIX_TARGETS[checkName]) {
@@ -97,6 +98,10 @@ export function declinedInLock(lock, checkName) {
97
98
  c.linting?.tool === 'none' &&
98
99
  c.testing?.framework === 'none');
99
100
  case 'semantic-release':
101
+ // The Swift shape of the same recorded choice (#310): `semanticRelease`
102
+ // is the config's release-automation flag, and on a SwiftPM repo that
103
+ // means a tag-triggered workflow rather than an npm publish.
104
+ case 'Release automation':
100
105
  return c.semanticRelease === false;
101
106
  case 'Dependabot':
102
107
  case 'CodeQL':
@@ -160,6 +165,7 @@ export function lockfilePatchForTarget(target, lock) {
160
165
  case 'swift-git-hooks':
161
166
  return c.gitHooks ? null : { gitHooks: true };
162
167
  case 'semantic-release':
168
+ case 'swift-release':
163
169
  return c.semanticRelease ? null : { semanticRelease: true };
164
170
  case 'dependabot':
165
171
  case 'renovate':
@@ -89,8 +89,10 @@ export function buildPresetConfig(name, projectName) {
89
89
  // commitlint *is* an npm package and needs node on PATH to run, so
90
90
  // the Swift hooks deliberately wire no commit-msg hook.
91
91
  commitLint: false,
92
- // semantic-release publishes to npm. SwiftPM consumes git tags.
93
- semanticRelease: false,
92
+ // The release-automation flag, not the npm tool: semantic-release
93
+ // publishes to npm, and SwiftPM consumes git tags, so on this path it
94
+ // means a tag-triggered release workflow instead (#310).
95
+ semanticRelease: true,
94
96
  securityAutomation: true,
95
97
  bundler: 'none',
96
98
  // Every badge URL is derived from package.json (name + repository),
@@ -16,7 +16,19 @@ const CODECOV_YML = `coverage:
16
16
  target: auto
17
17
  threshold: 1%
18
18
  `;
19
- export async function generateGitHubActions(config, targetDir) {
19
+ /** Matches the fixer's declared output, so `fix` reports the same path it lists. */
20
+ export const CI_WORKFLOW = '.github/workflows/ci.yml';
21
+ /**
22
+ * @param overwrite Replace a ci.yml that no longer matches the preset. Off by
23
+ * default: this used to write unconditionally, so a consuming repo's customized
24
+ * workflow — an extra job, a Dependabot-bumped action pin — was reverted on
25
+ * every sync with no diff, no prompt and no backup (#349, the mechanism behind
26
+ * #340). Same self-enforced safe-add as github-workflows.ts, widened to "or is
27
+ * byte-identical anyway" so a no-op regeneration still reports honestly. Only a
28
+ * caller that has told the user this workflow itself is drifting passes true.
29
+ * @returns the files actually written, relative to targetDir.
30
+ */
31
+ export async function generateGitHubActions(config, targetDir, { overwrite = false } = {}) {
20
32
  const workflowsDir = path.join(targetDir, '.github', 'workflows');
21
33
  await fs.ensureDir(workflowsDir);
22
34
  // This is the JS path specifically. Swift (#287) renders its own workflow
@@ -26,10 +38,18 @@ export async function generateGitHubActions(config, targetDir) {
26
38
  // paths meet at renderGitHubWorkflow() in src/base/ci.ts, which is the seam
27
39
  // that actually matters.
28
40
  const workflow = renderGitHubWorkflow(githubJobs(config));
29
- await fs.writeFile(path.join(workflowsDir, 'ci.yml'), workflow);
41
+ const ciPath = path.join(workflowsDir, 'ci.yml');
42
+ const existing = (await fs.pathExists(ciPath)) ? await fs.readFile(ciPath, 'utf-8') : null;
43
+ const filesWritten = [];
44
+ if (overwrite || existing === null || existing === workflow) {
45
+ await fs.writeFile(ciPath, workflow);
46
+ filesWritten.push(CI_WORKFLOW);
47
+ }
30
48
  // codecov.yml is the CI's coverage-upload companion — emit it alongside ci.yml
31
49
  // whenever the workflow uploads coverage, so the codecov badge isn't red.
32
50
  if (usesCoverage(config)) {
33
51
  await fs.writeFile(path.join(targetDir, 'codecov.yml'), CODECOV_YML);
52
+ filesWritten.push('codecov.yml');
34
53
  }
54
+ return filesWritten;
35
55
  }
@@ -4,7 +4,7 @@ import fs from 'fs-extra';
4
4
  import { buildBadgeBlock, parseRepository, upsertBadges } from '../../cli/generators/badges.js';
5
5
  import { generateReleasePleaseConfig, generateRolldownConfig, generateRollupConfig, generateSemanticReleaseConfig, } from '../../cli/generators/build.js';
6
6
  import { generateHuskyConfig, generatePrePushHook } from '../../cli/generators/git.js';
7
- import { generateGitHubActions } from '../../cli/generators/github-actions.js';
7
+ import { CI_WORKFLOW, generateGitHubActions } from '../../cli/generators/github-actions.js';
8
8
  import { generateGitLabCI } from '../../cli/generators/gitlab-ci.js';
9
9
  import { GH_WORKFLOWS, generateGhWorkflow } from '../../cli/generators/github-workflows.js';
10
10
  import { generateESLintConfig, generatePrettierConfig } from '../../cli/generators/linting.js';
@@ -22,7 +22,8 @@ import { generateDocsSite } from '../../cli/generators/docs-site.js';
22
22
  import { generateTypedocConfig, generateTypedocWorkflow } from '../../cli/generators/typedoc.js';
23
23
  import { copyPreset } from '../../cli/utils/copy-preset.js';
24
24
  import { LOCKFILE_NAME, writeLockfile } from '../../cli/utils/lockfile.js';
25
- function inferProjectConfig(pkg) {
25
+ /** Exported so doctor can render the preset ci.yml it compares against (#349). */
26
+ export function inferProjectConfig(pkg) {
26
27
  const deps = {
27
28
  ...(pkg?.dependencies ?? {}),
28
29
  ...(pkg?.devDependencies ?? {}),
@@ -303,13 +304,19 @@ export const FIXERS = [
303
304
  target: 'github-actions',
304
305
  description: 'Scaffold .github/workflows/ci.yml (+ codecov.yml when tests run)',
305
306
  appliesTo: ['GitHub Actions', 'Coverage upload', 'npm OIDC publish'],
306
- outputs: ['.github/workflows/ci.yml', 'codecov.yml'],
307
+ outputs: [CI_WORKFLOW, 'codecov.yml'],
307
308
  canFixDrift: true,
308
- async run({ targetDir, pkg }) {
309
- await generateGitHubActions(inferProjectConfig(pkg), targetDir);
310
- const filesWritten = ['.github/workflows/ci.yml'];
311
- if (await fs.pathExists(path.join(targetDir, 'codecov.yml')))
312
- filesWritten.push('codecov.yml');
309
+ async run({ targetDir, pkg, result }) {
310
+ // Only a `GitHub Actions` finding means the user was shown that the
311
+ // workflow itself is wrong (and got the destructive-overwrite prompt).
312
+ // A sibling finding — Coverage upload, npm OIDC publish — must not take a
313
+ // customized ci.yml down with it (#349).
314
+ const filesWritten = await generateGitHubActions(inferProjectConfig(pkg), targetDir, {
315
+ overwrite: result.check === 'GitHub Actions',
316
+ });
317
+ if (!filesWritten.includes(CI_WORKFLOW)) {
318
+ console.log(chalk.yellow(` ${CI_WORKFLOW} differs from the preset — left as-is; run \`fix github-actions --diff\` to see the delta`));
319
+ }
313
320
  return { filesWritten };
314
321
  },
315
322
  },
@@ -105,6 +105,42 @@ export async function checkPackageSwift(dir) {
105
105
  detail: `Package.swift declares tools ${toolsVersion} and explicit platforms`,
106
106
  };
107
107
  }
108
+ /**
109
+ * Release automation for a SwiftPM package (#310). There is no publish step to
110
+ * look for — a release *is* a semver git tag consumers resolve with
111
+ * `.package(url:from:)` — so "configured" means a workflow that fires on a tag
112
+ * push. semantic-release is deliberately not accepted as evidence: its pipeline
113
+ * is npm end to end, and a Swift repo running it is publishing the wrong thing.
114
+ *
115
+ * Only the trigger section is searched (everything above `jobs:`), because
116
+ * `tags:` also appears inside job steps — docker/metadata-action emits one — and
117
+ * a step that mentions tags is not a release trigger.
118
+ */
119
+ export async function checkSwiftRelease(dir) {
120
+ const check = 'Release automation';
121
+ const hint = 'Run `npx @rtorcato/repo-tooling fix swift-release` to scaffold a tag-triggered release workflow';
122
+ const workflowsDir = path.join(dir, '.github', 'workflows');
123
+ if (await fs.pathExists(workflowsDir)) {
124
+ const files = (await fs.readdir(workflowsDir)).filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
125
+ for (const file of files) {
126
+ const contents = await fs.readFile(path.join(workflowsDir, file), 'utf-8');
127
+ const triggers = contents.split(/^jobs:/m)[0] ?? '';
128
+ if (/^\s+tags:/m.test(triggers)) {
129
+ return {
130
+ check,
131
+ status: 'ok',
132
+ detail: `.github/workflows/${file} releases on a tag push`,
133
+ };
134
+ }
135
+ }
136
+ }
137
+ return {
138
+ check,
139
+ status: 'optional-missing',
140
+ detail: 'no workflow triggered by a version tag',
141
+ hint,
142
+ };
143
+ }
108
144
  /**
109
145
  * The Swift shape of the base `Git hooks` / `Pre-push hook` checks (#309).
110
146
  * `install` is null because the wiring — `git config core.hooksPath` — is
@@ -123,5 +159,6 @@ export async function runSwiftChecks(dir) {
123
159
  await checkPackageSwift(dir),
124
160
  ...(await Promise.all(SWIFT_FILE_CHECKS.map((spec) => checkFile(dir, spec)))),
125
161
  await checkSwiftGitignore(dir),
162
+ await checkSwiftRelease(dir),
126
163
  ];
127
164
  }
@@ -140,6 +140,49 @@ ${pkg.platforms.map((p) => ` - ${p}`).join('\n')}`,
140
140
  export function renderSwiftWorkflow(pkg) {
141
141
  return renderGitHubWorkflow(swiftGithubJobs(pkg));
142
142
  }
143
+ /**
144
+ * The release pipeline (#310). SwiftPM has no registry publish step — a release
145
+ * *is* a semver git tag that consumers resolve with `.package(url:from:)` — so
146
+ * the workflow fires on the tag rather than on a merge, and its only output is
147
+ * a GitHub Release with generated notes.
148
+ *
149
+ * The build/test gate runs before the release is cut because a tag is
150
+ * effectively permanent: SwiftPM caches resolved tags, so re-pointing a bad one
151
+ * doesn't reliably reach consumers who already resolved it.
152
+ *
153
+ * `gh` (preinstalled on the runner) rather than a release action: one fewer
154
+ * third-party pin to track, and `--verify-tag` refuses to invent a tag that
155
+ * isn't actually pushed.
156
+ */
157
+ export function renderSwiftReleaseWorkflow() {
158
+ return `name: 🏷️ Release
159
+
160
+ on:
161
+ push:
162
+ tags:
163
+ - '[0-9]+.[0-9]+.[0-9]+'
164
+ - 'v[0-9]+.[0-9]+.[0-9]+'
165
+
166
+ jobs:
167
+ release:
168
+ runs-on: ${MACOS}
169
+ permissions:
170
+ contents: write
171
+ steps:
172
+ ${XCODE_SETUP}
173
+
174
+ - name: 🏗️ swift build
175
+ run: swift build
176
+
177
+ - name: 🧪 swift test
178
+ run: swift test
179
+
180
+ - name: 🏷️ Publish GitHub Release
181
+ env:
182
+ GH_TOKEN: \${{ github.token }}
183
+ run: gh release create "\${{ github.ref_name }}" --generate-notes --verify-tag
184
+ `;
185
+ }
143
186
  /**
144
187
  * GitLab runs Swift in the official Linux image. That rules out Xcode — so no
145
188
  * platform matrix and no SwiftLint (a Homebrew/macOS tool in practice); the
@@ -7,7 +7,7 @@ import fs from 'fs-extra';
7
7
  import { buildPresetConfig } from '../../cli/commands/setup-presets.js';
8
8
  import { copyPreset } from '../../cli/utils/copy-preset.js';
9
9
  import { LOCKFILE_NAME, writeLockfile } from '../../cli/utils/lockfile.js';
10
- import { readSwiftPackage, renderSwiftGitLabCI, renderSwiftWorkflow } from './ci.js';
10
+ import { readSwiftPackage, renderSwiftGitLabCI, renderSwiftReleaseWorkflow, renderSwiftWorkflow, } from './ci.js';
11
11
  import { SWIFT_HOOKS_DIR, installSwiftGitHooks } from './git-hooks.js';
12
12
  import { ensureSwiftGitignore } from './gitignore.js';
13
13
  export const SWIFT_FIXERS = [
@@ -73,6 +73,19 @@ export const SWIFT_FIXERS = [
73
73
  return { filesWritten: ['.github/workflows/ci.yml'] };
74
74
  },
75
75
  },
76
+ {
77
+ target: 'swift-release',
78
+ description: 'Scaffold .github/workflows/release.yml (build + test on a version tag, then publish a GitHub Release)',
79
+ appliesTo: ['Release automation'],
80
+ outputs: ['.github/workflows/release.yml'],
81
+ canFixDrift: true,
82
+ async run({ targetDir }) {
83
+ const workflowsDir = path.join(targetDir, '.github', 'workflows');
84
+ await fs.ensureDir(workflowsDir);
85
+ await fs.writeFile(path.join(workflowsDir, 'release.yml'), renderSwiftReleaseWorkflow());
86
+ return { filesWritten: ['.github/workflows/release.yml'] };
87
+ },
88
+ },
76
89
  {
77
90
  target: 'swift-gitlab-ci',
78
91
  description: 'Scaffold .gitlab-ci.yml for Swift (swift build + test on the Linux Swift image)',
@@ -18,7 +18,7 @@ import { generateEditorConfig } from '../../cli/generators/misc.js';
18
18
  import { generateCodeQLWorkflow, generateDependabotConfig } from '../../cli/generators/security.js';
19
19
  import { copyPreset } from '../../cli/utils/copy-preset.js';
20
20
  import { LANGUAGES } from '../registry.js';
21
- import { readSwiftPackage, renderSwiftWorkflow } from './ci.js';
21
+ import { readSwiftPackage, renderSwiftReleaseWorkflow, renderSwiftWorkflow } from './ci.js';
22
22
  import { SWIFT_HOOKS_DIR, installSwiftGitHooks } from './git-hooks.js';
23
23
  import { ensureSwiftGitignore } from './gitignore.js';
24
24
  /**
@@ -168,7 +168,18 @@ ${config.projectName}/
168
168
  - **GitHub Actions** — \`swift build\`/\`swift test\` on macOS, plus \`xcodebuild\` per declared platform
169
169
 
170
170
  Run \`npx @rtorcato/repo-tooling doctor\` any time to audit this repo against the standard.
171
+ ${config.semanticRelease
172
+ ? `
173
+ ## Releasing
171
174
 
175
+ SwiftPM has no registry: a release is a semver git tag. Pushing one runs the
176
+ build/test gate and publishes a GitHub Release from it.
177
+
178
+ \`\`\`bash
179
+ git tag 1.0.0 && git push origin 1.0.0
180
+ \`\`\`
181
+ `
182
+ : ''}
172
183
  ## Contributing
173
184
 
174
185
  1. Fork the repository
@@ -199,6 +210,9 @@ export function swiftFileList(config) {
199
210
  '.editorconfig',
200
211
  '.github/workflows/ci.yml',
201
212
  ];
213
+ if (config.semanticRelease) {
214
+ files.push('.github/workflows/release.yml');
215
+ }
202
216
  if (config.gitHooks) {
203
217
  files.push(`${SWIFT_HOOKS_DIR}/pre-commit`, `${SWIFT_HOOKS_DIR}/pre-push`);
204
218
  }
@@ -227,6 +241,10 @@ export async function generateSwiftProject(config, targetDir) {
227
241
  const workflowsDir = path.join(targetDir, '.github', 'workflows');
228
242
  await fs.ensureDir(workflowsDir);
229
243
  await fs.writeFile(path.join(workflowsDir, 'ci.yml'), renderSwiftWorkflow(await readSwiftPackage(targetDir)));
244
+ // A tag-triggered release, not an npm publish (#310) — see the preset.
245
+ if (config.semanticRelease) {
246
+ await fs.writeFile(path.join(workflowsDir, 'release.yml'), renderSwiftReleaseWorkflow());
247
+ }
230
248
  // `core.hooksPath` won't stick here — `setup` scaffolds into a directory that
231
249
  // isn't a git repo yet — so the hooks land as files and the README tells you
232
250
  // the one-line config to run after `git init`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rtorcato/repo-tooling",
3
- "version": "3.2.5",
3
+ "version": "3.3.0",
4
4
  "description": "One CLI to scaffold, audit and fix your repo's whole toolchain — linting, tests, commits, releases & CI.",
5
5
  "type": "module",
6
6
  "keywords": [