@rtorcato/repo-tooling 3.31.1 → 3.31.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.
|
@@ -11,6 +11,7 @@ import fs from 'fs-extra';
|
|
|
11
11
|
import { realGitExec } from '../../base/git-identity.js';
|
|
12
12
|
import { getPackageRoot } from '../utils/copy-preset.js';
|
|
13
13
|
import { shellQuote } from '../utils/shell.js';
|
|
14
|
+
import { isNewerVersion, resolveShippedVersion } from '../utils/version.js';
|
|
14
15
|
/**
|
|
15
16
|
* Skills this package owns the content of and keeps up to date. The loop first —
|
|
16
17
|
* it is the pipeline; the other three are its drivers (burst, on-ramp, status).
|
|
@@ -115,47 +116,6 @@ export function classifySkillContent(installed, shipped) {
|
|
|
115
116
|
return 'unknown';
|
|
116
117
|
return recorded === hashSkillContent(installed) ? 'pristine' : 'modified';
|
|
117
118
|
}
|
|
118
|
-
function versionParts(version) {
|
|
119
|
-
return version.split('.').map((n) => Number.parseInt(n, 10) || 0);
|
|
120
|
-
}
|
|
121
|
-
/** True when `a` is a strictly higher version than `b`. Prerelease tags are ignored. */
|
|
122
|
-
export function isNewerVersion(a, b) {
|
|
123
|
-
const [left, right] = [versionParts(a), versionParts(b)];
|
|
124
|
-
for (let i = 0; i < 3; i++) {
|
|
125
|
-
const [x, y] = [left[i] ?? 0, right[i] ?? 0];
|
|
126
|
-
if (x !== y)
|
|
127
|
-
return x > y;
|
|
128
|
-
}
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* The version to stamp, given what `package.json` claims.
|
|
133
|
-
*
|
|
134
|
-
* In a published tarball that field is authoritative — `@semantic-release/npm`
|
|
135
|
-
* rewrites it before packing. In a *git checkout* it is not: this repo runs
|
|
136
|
-
* semantic-release without `@semantic-release/git` (#417), so nothing ever
|
|
137
|
-
* writes the released version back and the field sits at whatever it was last
|
|
138
|
-
* hand-set to. Observed 2026-08-22: `package.json` 3.11.0 against npm 3.21.1,
|
|
139
|
-
* ten minor versions of drift.
|
|
140
|
-
*
|
|
141
|
-
* That mattered because the stamp feeds the downgrade guard below: a skill
|
|
142
|
-
* installed from npm could not be updated from a local checkout, and the
|
|
143
|
-
* refusal claimed the installed copy was "newer" when only its *label* was.
|
|
144
|
-
*
|
|
145
|
-
* So when the package root is a git checkout, take the nearest tag as well and
|
|
146
|
-
* keep whichever is higher. Monotonic on purpose — this can only ever raise the
|
|
147
|
-
* answer, so a tagless, shallow, or git-less environment keeps today's
|
|
148
|
-
* behaviour rather than silently stamping something lower.
|
|
149
|
-
*/
|
|
150
|
-
export async function resolveShippedVersion(root, pkgVersion, git = realGitExec) {
|
|
151
|
-
if (!(await fs.pathExists(path.join(root, '.git'))))
|
|
152
|
-
return pkgVersion;
|
|
153
|
-
const described = await git(['describe', '--tags', '--abbrev=0'], root);
|
|
154
|
-
const tag = described?.trim().replace(/^v/, '');
|
|
155
|
-
if (!tag || !/^\d+\.\d+/.test(tag))
|
|
156
|
-
return pkgVersion;
|
|
157
|
-
return isNewerVersion(tag, pkgVersion) ? tag : pkgVersion;
|
|
158
|
-
}
|
|
159
119
|
/** The skill source and the package version that will be stamped into it. */
|
|
160
120
|
export async function readShippedSkill(name = SHIPPED_SKILL, git = realGitExec) {
|
|
161
121
|
const root = getPackageRoot();
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import fs from 'fs-extra';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { PNPM_FALLBACK_VERSION, detectPnpmVersion } from './misc.js';
|
|
@@ -8,6 +9,8 @@ export async function generatePackageJson(config, targetDir) {
|
|
|
8
9
|
existingPackageJson = await fs.readJson(packageJsonPath);
|
|
9
10
|
}
|
|
10
11
|
const includeTreeshake = Boolean(config.treeshakeCheck && config.projectType === 'library');
|
|
12
|
+
const generatedScripts = getScripts(config, { includeTreeshake });
|
|
13
|
+
const existingScripts = existingPackageJson?.scripts ?? {};
|
|
11
14
|
const packageJson = {
|
|
12
15
|
name: config.projectName,
|
|
13
16
|
version: '0.1.0',
|
|
@@ -17,10 +20,7 @@ export async function generatePackageJson(config, targetDir) {
|
|
|
17
20
|
// generated workflows carry no `version:` input (#364).
|
|
18
21
|
packageManager: `pnpm@${detectPnpmVersion() ?? PNPM_FALLBACK_VERSION}`,
|
|
19
22
|
...existingPackageJson,
|
|
20
|
-
scripts:
|
|
21
|
-
...getScripts(config, { includeTreeshake }),
|
|
22
|
-
...existingPackageJson?.scripts,
|
|
23
|
-
},
|
|
23
|
+
scripts: resolveScripts(config, generatedScripts, existingScripts),
|
|
24
24
|
dependencies: {
|
|
25
25
|
...existingPackageJson?.dependencies,
|
|
26
26
|
},
|
|
@@ -66,6 +66,30 @@ export async function generatePackageJson(config, targetDir) {
|
|
|
66
66
|
// (see ensureBuildApprovals in build.ts).
|
|
67
67
|
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
68
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Merge the generated scripts with what the package.json already had. Existing
|
|
71
|
+
* values win — that merge-don't-clobber policy is what makes re-running `setup`
|
|
72
|
+
* on a live repo safe.
|
|
73
|
+
*
|
|
74
|
+
* `build` on a library is the one exception. The library branch of
|
|
75
|
+
* generatePackageJson overwrites the whole publish contract
|
|
76
|
+
* (main/module/types/exports) unconditionally, and that contract names files
|
|
77
|
+
* only the preset's bundler emits: a preserved `build: tsc` produces
|
|
78
|
+
* dist/index.js + dist/index.d.ts and never the ./dist/index.cjs that `main`
|
|
79
|
+
* and every `require` condition point at, so `pnpm build && npm publish` ships
|
|
80
|
+
* a package whose CJS entry points 404 (#570). Owning the contract means owning
|
|
81
|
+
* its producer; the replacement is announced rather than silent.
|
|
82
|
+
*/
|
|
83
|
+
function resolveScripts(config, generated, existing) {
|
|
84
|
+
const scripts = { ...generated, ...existing };
|
|
85
|
+
if (config.projectType !== 'library' || !generated.build)
|
|
86
|
+
return scripts;
|
|
87
|
+
if (existing.build && existing.build !== generated.build) {
|
|
88
|
+
console.warn(chalk.yellow(`⚠️ Replaced "build": "${existing.build}" with "${generated.build}" — the library exports map names files only ${config.bundler} emits.`));
|
|
89
|
+
}
|
|
90
|
+
scripts.build = generated.build;
|
|
91
|
+
return scripts;
|
|
92
|
+
}
|
|
69
93
|
function getScripts(config, opts = {}) {
|
|
70
94
|
const scripts = {};
|
|
71
95
|
// TypeScript scripts
|
package/dist/cli/index.js
CHANGED
|
@@ -3,12 +3,12 @@ import path from 'node:path';
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import fs from 'fs-extra';
|
|
6
|
-
import packageJson from '../../package.json' with { type: 'json' };
|
|
7
6
|
import { doctorCommand } from './commands/doctor.js';
|
|
8
7
|
import { fixCommand } from './commands/fix.js';
|
|
9
8
|
import { loopGuardCommand } from './commands/loop-guard.js';
|
|
10
9
|
import { setupProject } from './commands/setup.js';
|
|
11
10
|
import { copyPreset, PRESETS } from './utils/copy-preset.js';
|
|
11
|
+
import { getToolVersion } from './utils/version.js';
|
|
12
12
|
async function isSelfRepo(dir) {
|
|
13
13
|
try {
|
|
14
14
|
const pkg = await fs.readJson(path.join(dir, 'package.json'));
|
|
@@ -22,7 +22,7 @@ const program = new Command();
|
|
|
22
22
|
program
|
|
23
23
|
.name('@rtorcato/repo-tooling')
|
|
24
24
|
.description('🛠️ JavaScript and TypeScript tooling setup for modern projects')
|
|
25
|
-
.version(
|
|
25
|
+
.version(await getToolVersion());
|
|
26
26
|
program
|
|
27
27
|
.command('setup')
|
|
28
28
|
.alias('init')
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
|
-
import packageJson from '../../../package.json' with { type: 'json' };
|
|
4
3
|
import { CONFIG_SCHEMA, validateProjectConfig } from '../commands/setup-presets.js';
|
|
5
4
|
import { SHIPPED_SKILLS } from '../generators/claude-skills.js';
|
|
5
|
+
import { getToolVersion } from './version.js';
|
|
6
6
|
export const LOCKFILE_NAME = '.repo-tooling.json';
|
|
7
7
|
// Package and bin name used before the js-tooling→repo-tooling rename (#272).
|
|
8
8
|
// The bin no longer exists and the package is 404 on the registry, so any
|
|
@@ -243,7 +243,7 @@ export async function writeLockfile(dir, config, assets) {
|
|
|
243
243
|
record: {
|
|
244
244
|
config,
|
|
245
245
|
...(carried && Object.keys(carried).length > 0 ? { assets: carried } : {}),
|
|
246
|
-
writtenBy: `@rtorcato/repo-tooling@${
|
|
246
|
+
writtenBy: `@rtorcato/repo-tooling@${await getToolVersion()}`,
|
|
247
247
|
writtenAt: new Date().toISOString(),
|
|
248
248
|
},
|
|
249
249
|
...(existing?.rules ? { rules: existing.rules } : {}),
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one answer to "what version of this package is running" (#572).
|
|
3
|
+
*
|
|
4
|
+
* There were two. `--version` and the lockfile's `writtenBy` read the imported
|
|
5
|
+
* `package.json` directly; the Claude-skills stamp went through
|
|
6
|
+
* `resolveShippedVersion`, which corrects for the drift documented below. A
|
|
7
|
+
* single `doctor` run could report both — the skills check saying 3.31.0 while
|
|
8
|
+
* `writtenBy` in the same run said 3.11.0. Everything now calls
|
|
9
|
+
* `getToolVersion()`.
|
|
10
|
+
*/
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import fs from 'fs-extra';
|
|
13
|
+
import packageJson from '../../../package.json' with { type: 'json' };
|
|
14
|
+
import { realGitExec } from '../../base/git-identity.js';
|
|
15
|
+
import { getPackageRoot } from './copy-preset.js';
|
|
16
|
+
function versionParts(version) {
|
|
17
|
+
return version.split('.').map((n) => Number.parseInt(n, 10) || 0);
|
|
18
|
+
}
|
|
19
|
+
/** True when `a` is a strictly higher version than `b`. Prerelease tags are ignored. */
|
|
20
|
+
export function isNewerVersion(a, b) {
|
|
21
|
+
const [left, right] = [versionParts(a), versionParts(b)];
|
|
22
|
+
for (let i = 0; i < 3; i++) {
|
|
23
|
+
const [x, y] = [left[i] ?? 0, right[i] ?? 0];
|
|
24
|
+
if (x !== y)
|
|
25
|
+
return x > y;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The version to report, given what `package.json` claims.
|
|
31
|
+
*
|
|
32
|
+
* In a published tarball that field is authoritative — `@semantic-release/npm`
|
|
33
|
+
* rewrites it before packing. In a *git checkout* it is not: this repo runs
|
|
34
|
+
* semantic-release without `@semantic-release/git` (#417), so nothing ever
|
|
35
|
+
* writes the released version back and the field sits at whatever it was last
|
|
36
|
+
* hand-set to. Observed 2026-08-22: `package.json` 3.11.0 against npm 3.21.1,
|
|
37
|
+
* ten minor versions of drift.
|
|
38
|
+
*
|
|
39
|
+
* That mattered first because the stamp feeds the skills downgrade guard: a
|
|
40
|
+
* skill installed from npm could not be updated from a local checkout, and the
|
|
41
|
+
* refusal claimed the installed copy was "newer" when only its *label* was.
|
|
42
|
+
*
|
|
43
|
+
* So when the package root is a git checkout, take the nearest tag as well and
|
|
44
|
+
* keep whichever is higher. Monotonic on purpose — this can only ever raise the
|
|
45
|
+
* answer, so a tagless, shallow, or git-less environment keeps today's
|
|
46
|
+
* behaviour rather than silently reporting something lower.
|
|
47
|
+
*/
|
|
48
|
+
export async function resolveShippedVersion(root, pkgVersion, git = realGitExec) {
|
|
49
|
+
if (!(await fs.pathExists(path.join(root, '.git'))))
|
|
50
|
+
return pkgVersion;
|
|
51
|
+
const described = await git(['describe', '--tags', '--abbrev=0'], root);
|
|
52
|
+
const tag = described?.trim().replace(/^v/, '');
|
|
53
|
+
if (!tag || !/^\d+\.\d+/.test(tag))
|
|
54
|
+
return pkgVersion;
|
|
55
|
+
return isNewerVersion(tag, pkgVersion) ? tag : pkgVersion;
|
|
56
|
+
}
|
|
57
|
+
let cached;
|
|
58
|
+
/**
|
|
59
|
+
* This package's version, for `--version`, `writtenBy`, and the skill stamps.
|
|
60
|
+
*
|
|
61
|
+
* Memoized: in a checkout `resolveShippedVersion` spawns `git describe`, and
|
|
62
|
+
* nothing about the answer changes mid-process. An npm install has no `.git`
|
|
63
|
+
* under the package root, so that path costs one `pathExists` and no subprocess.
|
|
64
|
+
*/
|
|
65
|
+
export function getToolVersion() {
|
|
66
|
+
cached ??= resolveShippedVersion(getPackageRoot(), String(packageJson.version));
|
|
67
|
+
return cached;
|
|
68
|
+
}
|
|
@@ -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, renderSwiftReleaseWorkflow, renderSwiftWorkflow } from './ci.js';
|
|
21
|
+
import { parsePackageSwift, 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
|
/**
|
|
@@ -196,6 +196,10 @@ git tag 1.0.0 && git push origin 1.0.0
|
|
|
196
196
|
/**
|
|
197
197
|
* Relative paths `setup --dry-run` reports for a Swift config, in write order.
|
|
198
198
|
* Kept beside the writer so the two can't drift.
|
|
199
|
+
*
|
|
200
|
+
* This is the greenfield list. Against a directory that already has a
|
|
201
|
+
* Package.swift the writer preserves it and skips the target scaffolding, so the
|
|
202
|
+
* preview over-reports there — the safe direction for a dry run.
|
|
199
203
|
*/
|
|
200
204
|
export function swiftFileList(config) {
|
|
201
205
|
const module = swiftModuleName(config.projectName);
|
|
@@ -225,12 +229,29 @@ export function swiftFileList(config) {
|
|
|
225
229
|
files.push('README.md');
|
|
226
230
|
return files;
|
|
227
231
|
}
|
|
228
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* Scaffold a SwiftPM library. The Swift arm of `generateConfigs`.
|
|
234
|
+
*
|
|
235
|
+
* Package.swift is source, not config (#574): rewriting one that already exists
|
|
236
|
+
* renames the package after the *directory* and leaves the real `Sources/<Target>/`
|
|
237
|
+
* declared by no target — and `swift build` still exits 0, because SwiftPM just
|
|
238
|
+
* ignores the orphaned directory. So an existing manifest is left byte-identical
|
|
239
|
+
* and the module name is read back off it rather than derived from the directory,
|
|
240
|
+
* the same way the JS presets merge into a package.json instead of replacing it.
|
|
241
|
+
*/
|
|
229
242
|
export async function generateSwiftProject(config, targetDir) {
|
|
230
|
-
const
|
|
231
|
-
await fs.
|
|
232
|
-
|
|
233
|
-
|
|
243
|
+
const manifestPath = path.join(targetDir, 'Package.swift');
|
|
244
|
+
const existingManifest = (await fs.pathExists(manifestPath))
|
|
245
|
+
? parsePackageSwift(await fs.readFile(manifestPath, 'utf-8'))
|
|
246
|
+
: null;
|
|
247
|
+
const module = existingManifest?.products[0] ?? swiftModuleName(config.projectName);
|
|
248
|
+
// Nothing is scaffolded under Sources/ or Tests/ either: those would be
|
|
249
|
+
// directories for a target the existing manifest doesn't declare.
|
|
250
|
+
if (!existingManifest) {
|
|
251
|
+
await fs.writeFile(manifestPath, packageSwift(module));
|
|
252
|
+
await fs.outputFile(path.join(targetDir, 'Sources', module, `${module}.swift`), sourceFile(module));
|
|
253
|
+
await fs.outputFile(path.join(targetDir, 'Tests', `${module}Tests`, `${module}Tests.swift`), testFile(module));
|
|
254
|
+
}
|
|
234
255
|
await copyPreset('swiftlint', targetDir);
|
|
235
256
|
await copyPreset('periphery', targetDir);
|
|
236
257
|
await ensureSwiftGitignore(targetDir);
|
package/package.json
CHANGED