@produck/agent-toolkit 0.2.0 → 0.3.3
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 +53 -9
- package/bin/agent-toolkit.mjs +76 -654
- package/bin/build-publish-assets.mjs +49 -0
- package/bin/command/enforce-node-baseline/help.txt +19 -0
- package/bin/command/enforce-node-baseline/index.mjs +155 -0
- package/{templates/help/main.txt → bin/command/main/help.txt} +7 -0
- package/bin/command/main/index.mjs +11 -0
- package/bin/command/preflight/help.txt +9 -0
- package/bin/command/preflight/index.mjs +147 -0
- package/bin/command/run-capture/index.mjs +100 -0
- package/bin/command/shared/args.mjs +45 -0
- package/bin/command/shared/text-resource.mjs +19 -0
- package/bin/command/summarize-log/index.mjs +64 -0
- package/bin/command/sync-coverage-script/help.txt +22 -0
- package/bin/command/sync-coverage-script/index.mjs +275 -0
- package/bin/command/sync-husky-hooks/help.txt +22 -0
- package/bin/command/sync-husky-hooks/index.mjs +267 -0
- package/bin/command/sync-instructions/index.mjs +272 -0
- package/bin/command/validate-commit-msg/help.txt +9 -0
- package/bin/command/validate-commit-msg/index.mjs +183 -0
- package/package.json +5 -3
- package/publish-assets/instructions/produck/00-produck-base.instructions.md +37 -39
- package/publish-assets/instructions/produck/10-produck-node.instructions.md +130 -26
- package/publish-assets/instructions/produck/15-produck-workspace.instructions.md +59 -27
- package/publish-assets/instructions/produck/20-produck-commit.instructions.md +24 -8
- package/publish-assets/instructions/produck/tooling-version-baseline.json +32 -0
- package/templates/help/preflight.txt +0 -3
- package/templates/help/validate-commit-msg.txt +0 -7
- /package/{templates/help/run-capture.txt → bin/command/run-capture/help.txt} +0 -0
- /package/{templates/help/summarize-log.txt → bin/command/summarize-log/help.txt} +0 -0
- /package/{templates/help/sync-instructions.txt → bin/command/sync-instructions/help.txt} +0 -0
- /package/{templates → bin/command/sync-instructions}/user-space-bootstrap.md +0 -0
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { getMulti, getSingle, hasFlag } from '../shared/args.mjs';
|
|
6
|
+
import { printTextResource } from '../shared/text-resource.mjs';
|
|
7
|
+
|
|
8
|
+
const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
|
|
10
|
+
const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
|
|
11
|
+
const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
|
|
12
|
+
const TOOLING_BASELINE_CANDIDATE_PATHS = [
|
|
13
|
+
path.resolve(REPO_ROOT, '.github/distribution/produck/tooling-version-baseline.json'),
|
|
14
|
+
path.resolve(PACKAGE_ROOT, 'publish-assets/instructions/produck/tooling-version-baseline.json'),
|
|
15
|
+
];
|
|
16
|
+
const GLOB_TOKEN_PATTERN = /[*?{}[\]]/;
|
|
17
|
+
const REQUIRED_COVERAGE_SCRIPT_KEY = 'produck:coverage';
|
|
18
|
+
|
|
19
|
+
export function printSyncCoverageScriptHelp() {
|
|
20
|
+
printTextResource(HELP_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadToolingBaseline() {
|
|
24
|
+
const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
|
|
25
|
+
return fs.existsSync(candidatePath);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!toolingBaselinePath) {
|
|
29
|
+
console.error('Tooling baseline file does not exist in expected locations:');
|
|
30
|
+
for (const candidatePath of TOOLING_BASELINE_CANDIDATE_PATHS) {
|
|
31
|
+
console.error(`- ${candidatePath}`);
|
|
32
|
+
}
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
|
|
37
|
+
const c8Version = baseline?.tools?.c8?.version;
|
|
38
|
+
const coverageTemplate = baseline?.coverage?.scriptTemplate;
|
|
39
|
+
|
|
40
|
+
if (typeof baseline.schemaVersion !== 'number') {
|
|
41
|
+
console.error(`Tooling baseline schemaVersion must be a number: ${toolingBaselinePath}`);
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (typeof c8Version !== 'string' || c8Version.trim() === '') {
|
|
46
|
+
console.error(
|
|
47
|
+
`Tooling baseline tools.c8.version must be a non-empty string: ${toolingBaselinePath}`,
|
|
48
|
+
);
|
|
49
|
+
process.exit(2);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (typeof coverageTemplate !== 'string' || coverageTemplate.trim() === '') {
|
|
53
|
+
console.error(
|
|
54
|
+
`Tooling baseline coverage.scriptTemplate must be a non-empty string: ${toolingBaselinePath}`,
|
|
55
|
+
);
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
baseline,
|
|
61
|
+
toolingBaselinePath,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function buildRequiredCoverageScript(baseline) {
|
|
66
|
+
const c8Version = String(baseline.tools.c8.version);
|
|
67
|
+
const coverageTemplate = String(baseline.coverage.scriptTemplate);
|
|
68
|
+
return coverageTemplate.replace(/\{c8\.version\}/g, c8Version);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildRequiredC8DevDependency(baseline) {
|
|
72
|
+
return String(baseline.tools.c8.version);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseJsonFile(filePath, label) {
|
|
76
|
+
try {
|
|
77
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
78
|
+
} catch {
|
|
79
|
+
console.error(`${label} is not valid JSON: ${filePath}`);
|
|
80
|
+
process.exit(2);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveWorkspacePaths(cwd, options) {
|
|
85
|
+
const manual = getMulti(options, '--workspace');
|
|
86
|
+
if (manual.length > 0) {
|
|
87
|
+
return manual;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const rootPackageJsonPath = path.resolve(cwd, 'package.json');
|
|
91
|
+
if (!fs.existsSync(rootPackageJsonPath)) {
|
|
92
|
+
console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
|
|
93
|
+
process.exit(2);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const rootPackageJson = parseJsonFile(rootPackageJsonPath, 'Root package.json');
|
|
97
|
+
if (!Array.isArray(rootPackageJson.workspaces)) {
|
|
98
|
+
console.error('Root package.json `workspaces` must be an explicit array');
|
|
99
|
+
process.exit(2);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const workspaces = rootPackageJson.workspaces.map((entry) => String(entry));
|
|
103
|
+
if (workspaces.length === 0) {
|
|
104
|
+
console.error('Root package.json `workspaces` must not be empty');
|
|
105
|
+
process.exit(2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hasGlob = workspaces.some((entry) => GLOB_TOKEN_PATTERN.test(entry));
|
|
109
|
+
if (hasGlob) {
|
|
110
|
+
console.error('Root package.json `workspaces` must use explicit paths without glob tokens');
|
|
111
|
+
process.exit(2);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return workspaces;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function reconcileCoverageScript(
|
|
118
|
+
cwd,
|
|
119
|
+
workspacePath,
|
|
120
|
+
mode,
|
|
121
|
+
requiredCoverageScript,
|
|
122
|
+
requiredC8Version,
|
|
123
|
+
) {
|
|
124
|
+
const packageDir = path.resolve(cwd, workspacePath);
|
|
125
|
+
const packageJsonPath = path.resolve(packageDir, 'package.json');
|
|
126
|
+
|
|
127
|
+
const result = {
|
|
128
|
+
workspacePath,
|
|
129
|
+
packageDir,
|
|
130
|
+
packageJsonPath,
|
|
131
|
+
exists: false,
|
|
132
|
+
validJson: false,
|
|
133
|
+
previousCoverage: null,
|
|
134
|
+
coverageScript: null,
|
|
135
|
+
previousC8DevDependency: null,
|
|
136
|
+
c8DevDependency: null,
|
|
137
|
+
matchesRequiredCoverageBefore: false,
|
|
138
|
+
matchesRequiredCoverageAfter: false,
|
|
139
|
+
matchesRequiredC8DevDependencyBefore: false,
|
|
140
|
+
matchesRequiredC8DevDependencyAfter: false,
|
|
141
|
+
updated: false,
|
|
142
|
+
error: '',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
146
|
+
result.error = `Workspace package.json does not exist: ${workspacePath}`;
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
result.exists = true;
|
|
150
|
+
|
|
151
|
+
let pkg;
|
|
152
|
+
try {
|
|
153
|
+
pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
154
|
+
result.validJson = true;
|
|
155
|
+
} catch {
|
|
156
|
+
result.error = `Workspace package.json is not valid JSON: ${workspacePath}`;
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const scripts =
|
|
161
|
+
pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
|
|
162
|
+
? { ...pkg.scripts }
|
|
163
|
+
: {};
|
|
164
|
+
const devDependencies =
|
|
165
|
+
pkg.devDependencies &&
|
|
166
|
+
typeof pkg.devDependencies === 'object' &&
|
|
167
|
+
!Array.isArray(pkg.devDependencies)
|
|
168
|
+
? { ...pkg.devDependencies }
|
|
169
|
+
: {};
|
|
170
|
+
|
|
171
|
+
const previousCoverage =
|
|
172
|
+
typeof scripts[REQUIRED_COVERAGE_SCRIPT_KEY] === 'string'
|
|
173
|
+
? scripts[REQUIRED_COVERAGE_SCRIPT_KEY]
|
|
174
|
+
: null;
|
|
175
|
+
const previousC8DevDependency =
|
|
176
|
+
typeof devDependencies.c8 === 'string' ? devDependencies.c8 : null;
|
|
177
|
+
result.previousCoverage = previousCoverage;
|
|
178
|
+
result.previousC8DevDependency = previousC8DevDependency;
|
|
179
|
+
result.matchesRequiredCoverageBefore = previousCoverage === requiredCoverageScript;
|
|
180
|
+
result.matchesRequiredC8DevDependencyBefore = previousC8DevDependency === requiredC8Version;
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
(!result.matchesRequiredCoverageBefore || !result.matchesRequiredC8DevDependencyBefore) &&
|
|
184
|
+
mode === 'sync'
|
|
185
|
+
) {
|
|
186
|
+
scripts[REQUIRED_COVERAGE_SCRIPT_KEY] = requiredCoverageScript;
|
|
187
|
+
devDependencies.c8 = requiredC8Version;
|
|
188
|
+
pkg.scripts = scripts;
|
|
189
|
+
pkg.devDependencies = devDependencies;
|
|
190
|
+
fs.writeFileSync(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
191
|
+
result.updated = true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
result.coverageScript =
|
|
195
|
+
mode === 'sync' && !result.matchesRequiredCoverageBefore
|
|
196
|
+
? requiredCoverageScript
|
|
197
|
+
: previousCoverage;
|
|
198
|
+
result.c8DevDependency =
|
|
199
|
+
mode === 'sync' && !result.matchesRequiredC8DevDependencyBefore
|
|
200
|
+
? requiredC8Version
|
|
201
|
+
: previousC8DevDependency;
|
|
202
|
+
|
|
203
|
+
result.matchesRequiredCoverageAfter = result.updated || result.matchesRequiredCoverageBefore;
|
|
204
|
+
result.matchesRequiredC8DevDependencyAfter =
|
|
205
|
+
result.updated || result.matchesRequiredC8DevDependencyBefore;
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function runSyncCoverageScript(options) {
|
|
210
|
+
const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
|
|
211
|
+
const check = hasFlag(options, '--check');
|
|
212
|
+
const dryRun = hasFlag(options, '--dry-run');
|
|
213
|
+
const jsonFile = getSingle(options, '--json', '');
|
|
214
|
+
const { baseline: toolingBaseline, toolingBaselinePath } = loadToolingBaseline();
|
|
215
|
+
const requiredCoverageScript = buildRequiredCoverageScript(toolingBaseline);
|
|
216
|
+
const requiredC8Version = buildRequiredC8DevDependency(toolingBaseline);
|
|
217
|
+
|
|
218
|
+
if (!fs.existsSync(cwd)) {
|
|
219
|
+
console.error(`CWD does not exist: ${cwd}`);
|
|
220
|
+
process.exit(2);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const workspacePaths = resolveWorkspacePaths(cwd, options);
|
|
224
|
+
const mode = dryRun ? 'dry-run' : check ? 'check' : 'sync';
|
|
225
|
+
|
|
226
|
+
const report = {
|
|
227
|
+
cwd,
|
|
228
|
+
mode,
|
|
229
|
+
toolingBaselinePath,
|
|
230
|
+
toolingBaseline: {
|
|
231
|
+
schemaVersion: toolingBaseline.schemaVersion,
|
|
232
|
+
c8Version: toolingBaseline.tools.c8.version,
|
|
233
|
+
},
|
|
234
|
+
requiredCoverageScript,
|
|
235
|
+
requiredC8DevDependency: requiredC8Version,
|
|
236
|
+
workspaces: workspacePaths,
|
|
237
|
+
results: [],
|
|
238
|
+
ok: true,
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
for (const workspacePath of workspacePaths) {
|
|
242
|
+
const effectiveMode = mode === 'sync' ? 'sync' : 'check';
|
|
243
|
+
const item = reconcileCoverageScript(
|
|
244
|
+
cwd,
|
|
245
|
+
workspacePath,
|
|
246
|
+
effectiveMode,
|
|
247
|
+
requiredCoverageScript,
|
|
248
|
+
requiredC8Version,
|
|
249
|
+
);
|
|
250
|
+
report.results.push(item);
|
|
251
|
+
|
|
252
|
+
if (item.error) {
|
|
253
|
+
report.ok = false;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (
|
|
258
|
+
mode === 'check' &&
|
|
259
|
+
(!item.matchesRequiredCoverageAfter || !item.matchesRequiredC8DevDependencyAfter)
|
|
260
|
+
) {
|
|
261
|
+
report.ok = false;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (jsonFile) {
|
|
266
|
+
const outPath = path.resolve(cwd, jsonFile);
|
|
267
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
268
|
+
fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
272
|
+
if (!report.ok) {
|
|
273
|
+
process.exit(2);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Usage:
|
|
2
|
+
agent-toolkit sync-husky-hooks [--cwd <dir>] [--check] [--dry-run]
|
|
3
|
+
[--json <file>]
|
|
4
|
+
|
|
5
|
+
Behavior:
|
|
6
|
+
- Applies organization-required root scripts for local hook gates:
|
|
7
|
+
- scripts.prepare = husky
|
|
8
|
+
- scripts.produck:precommit-check = npm run format:check && npm run lint
|
|
9
|
+
- Applies organization-required root managed devDependencies:
|
|
10
|
+
- devDependencies.c8 = <fixed-version-from-baseline>
|
|
11
|
+
- devDependencies.husky = <fixed-version-from-baseline>
|
|
12
|
+
- devDependencies.lerna = <fixed-version-from-baseline>
|
|
13
|
+
- devDependencies.@produck/agent-toolkit = <latest-version-resolved-at-runtime>
|
|
14
|
+
- Applies organization-required hook files:
|
|
15
|
+
- .husky/pre-commit
|
|
16
|
+
- .husky/commit-msg
|
|
17
|
+
- commit-msg hook validates message via local node_modules toolkit path
|
|
18
|
+
|
|
19
|
+
Rules:
|
|
20
|
+
- --check validates without writing and exits non-zero on mismatch
|
|
21
|
+
- --dry-run prints planned changes without writing
|
|
22
|
+
- --check takes precedence over --dry-run
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
import { getSingle, hasFlag } from '../shared/args.mjs';
|
|
7
|
+
import { printTextResource } from '../shared/text-resource.mjs';
|
|
8
|
+
|
|
9
|
+
const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
|
|
11
|
+
const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
|
|
12
|
+
const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
|
|
13
|
+
const TOOLKIT_PACKAGE_JSON = path.resolve(PACKAGE_ROOT, 'package.json');
|
|
14
|
+
const TOOLING_BASELINE_CANDIDATE_PATHS = [
|
|
15
|
+
path.resolve(REPO_ROOT, '.github/distribution/produck/tooling-version-baseline.json'),
|
|
16
|
+
path.resolve(PACKAGE_ROOT, 'publish-assets/instructions/produck/tooling-version-baseline.json'),
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const REQUIRED_PREPARE_SCRIPT = 'husky';
|
|
20
|
+
const REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY = 'produck:precommit-check';
|
|
21
|
+
const REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE = 'npm run format:check && npm run lint';
|
|
22
|
+
|
|
23
|
+
const REQUIRED_PRE_COMMIT_HOOK = '#!/usr/bin/env sh\nnpm run produck:precommit-check\n';
|
|
24
|
+
const REQUIRED_COMMIT_MSG_HOOK =
|
|
25
|
+
'#!/usr/bin/env sh\nnode ./node_modules/@produck/agent-toolkit/bin/agent-toolkit.mjs validate-commit-msg --file "$1"\n';
|
|
26
|
+
|
|
27
|
+
export function printSyncHuskyHooksHelp() {
|
|
28
|
+
printTextResource(HELP_FILE);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseJsonFile(filePath, label) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
34
|
+
} catch {
|
|
35
|
+
console.error(`${label} is not valid JSON: ${filePath}`);
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getRequiredToolkitDevDependency() {
|
|
41
|
+
const overrideVersion = String(process.env.PRODUCK_TOOLKIT_VERSION_OVERRIDE || '').trim();
|
|
42
|
+
if (overrideVersion) {
|
|
43
|
+
return overrideVersion;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
47
|
+
const latestResult = spawnSync(npmCommand, ['view', '@produck/agent-toolkit', 'version'], {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const latestVersion = String(latestResult.stdout || '').trim();
|
|
52
|
+
if (latestResult.status === 0 && latestVersion) {
|
|
53
|
+
return latestVersion;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const pkg = parseJsonFile(TOOLKIT_PACKAGE_JSON, 'Toolkit package.json');
|
|
57
|
+
const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
|
|
58
|
+
|
|
59
|
+
if (!version) {
|
|
60
|
+
console.error(`Toolkit package version is missing: ${TOOLKIT_PACKAGE_JSON}`);
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return version;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function loadToolingBaseline() {
|
|
68
|
+
const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
|
|
69
|
+
return fs.existsSync(candidatePath);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!toolingBaselinePath) {
|
|
73
|
+
console.error('Tooling baseline file does not exist in expected locations:');
|
|
74
|
+
for (const candidatePath of TOOLING_BASELINE_CANDIDATE_PATHS) {
|
|
75
|
+
console.error(`- ${candidatePath}`);
|
|
76
|
+
}
|
|
77
|
+
process.exit(2);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
|
|
81
|
+
const c8Version = String(baseline?.tools?.c8?.version || '').trim();
|
|
82
|
+
const huskyVersion = String(baseline?.tools?.husky?.version || '').trim();
|
|
83
|
+
const lernaVersion = String(baseline?.tools?.lerna?.version || '').trim();
|
|
84
|
+
|
|
85
|
+
if (!c8Version || !huskyVersion || !lernaVersion) {
|
|
86
|
+
console.error(
|
|
87
|
+
`Tooling baseline must define fixed tools.c8/husky/lerna.version: ${toolingBaselinePath}`,
|
|
88
|
+
);
|
|
89
|
+
process.exit(2);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
toolingBaselinePath,
|
|
94
|
+
c8Version,
|
|
95
|
+
huskyVersion,
|
|
96
|
+
lernaVersion,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function readFileIfExists(filePath) {
|
|
101
|
+
if (!fs.existsSync(filePath)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildScriptState(pkg) {
|
|
109
|
+
const scripts =
|
|
110
|
+
pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
|
|
111
|
+
? { ...pkg.scripts }
|
|
112
|
+
: {};
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
scripts,
|
|
116
|
+
previousPrepare: typeof scripts.prepare === 'string' ? scripts.prepare : null,
|
|
117
|
+
previousPrecommitCheck:
|
|
118
|
+
typeof scripts[REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY] === 'string'
|
|
119
|
+
? scripts[REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY]
|
|
120
|
+
: null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function buildDevDependencyState(pkg) {
|
|
125
|
+
const devDependencies =
|
|
126
|
+
pkg.devDependencies &&
|
|
127
|
+
typeof pkg.devDependencies === 'object' &&
|
|
128
|
+
!Array.isArray(pkg.devDependencies)
|
|
129
|
+
? { ...pkg.devDependencies }
|
|
130
|
+
: {};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
devDependencies,
|
|
134
|
+
previousManaged: {
|
|
135
|
+
c8: typeof devDependencies.c8 === 'string' ? devDependencies.c8 : null,
|
|
136
|
+
husky: typeof devDependencies.husky === 'string' ? devDependencies.husky : null,
|
|
137
|
+
lerna: typeof devDependencies.lerna === 'string' ? devDependencies.lerna : null,
|
|
138
|
+
'@produck/agent-toolkit':
|
|
139
|
+
typeof devDependencies['@produck/agent-toolkit'] === 'string'
|
|
140
|
+
? devDependencies['@produck/agent-toolkit']
|
|
141
|
+
: null,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function runSyncHuskyHooks(options) {
|
|
147
|
+
const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
|
|
148
|
+
const check = hasFlag(options, '--check');
|
|
149
|
+
const dryRun = hasFlag(options, '--dry-run') && !check;
|
|
150
|
+
const jsonFile = getSingle(options, '--json', '');
|
|
151
|
+
const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
|
|
152
|
+
|
|
153
|
+
if (!fs.existsSync(cwd)) {
|
|
154
|
+
console.error(`CWD does not exist: ${cwd}`);
|
|
155
|
+
process.exit(2);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const rootPackageJsonPath = path.resolve(cwd, 'package.json');
|
|
159
|
+
if (!fs.existsSync(rootPackageJsonPath)) {
|
|
160
|
+
console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
|
|
161
|
+
process.exit(2);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
|
|
165
|
+
const toolingBaseline = loadToolingBaseline();
|
|
166
|
+
const requiredToolkitDependency = getRequiredToolkitDevDependency();
|
|
167
|
+
const requiredDevDependencies = {
|
|
168
|
+
c8: toolingBaseline.c8Version,
|
|
169
|
+
husky: toolingBaseline.huskyVersion,
|
|
170
|
+
lerna: toolingBaseline.lernaVersion,
|
|
171
|
+
'@produck/agent-toolkit': requiredToolkitDependency,
|
|
172
|
+
};
|
|
173
|
+
const scriptState = buildScriptState(pkg);
|
|
174
|
+
const dependencyState = buildDevDependencyState(pkg);
|
|
175
|
+
|
|
176
|
+
const huskyDir = path.resolve(cwd, '.husky');
|
|
177
|
+
const preCommitHookPath = path.resolve(huskyDir, 'pre-commit');
|
|
178
|
+
const commitMsgHookPath = path.resolve(huskyDir, 'commit-msg');
|
|
179
|
+
|
|
180
|
+
const previousPreCommitHook = readFileIfExists(preCommitHookPath);
|
|
181
|
+
const previousCommitMsgHook = readFileIfExists(commitMsgHookPath);
|
|
182
|
+
|
|
183
|
+
const matchesRequiredPrepare = scriptState.previousPrepare === REQUIRED_PREPARE_SCRIPT;
|
|
184
|
+
const matchesRequiredPrecommitCheck =
|
|
185
|
+
scriptState.previousPrecommitCheck === REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE;
|
|
186
|
+
const matchesRequiredManagedDevDependencies = Object.entries(requiredDevDependencies).every(
|
|
187
|
+
([name, version]) => {
|
|
188
|
+
return dependencyState.previousManaged[name] === version;
|
|
189
|
+
},
|
|
190
|
+
);
|
|
191
|
+
const matchesRequiredPreCommitHook = previousPreCommitHook === REQUIRED_PRE_COMMIT_HOOK;
|
|
192
|
+
const matchesRequiredCommitMsgHook = previousCommitMsgHook === REQUIRED_COMMIT_MSG_HOOK;
|
|
193
|
+
|
|
194
|
+
const requiresUpdate =
|
|
195
|
+
!matchesRequiredPrepare ||
|
|
196
|
+
!matchesRequiredPrecommitCheck ||
|
|
197
|
+
!matchesRequiredManagedDevDependencies ||
|
|
198
|
+
!matchesRequiredPreCommitHook ||
|
|
199
|
+
!matchesRequiredCommitMsgHook;
|
|
200
|
+
|
|
201
|
+
if (mode === 'sync' && requiresUpdate) {
|
|
202
|
+
scriptState.scripts.prepare = REQUIRED_PREPARE_SCRIPT;
|
|
203
|
+
scriptState.scripts[REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY] =
|
|
204
|
+
REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE;
|
|
205
|
+
pkg.scripts = scriptState.scripts;
|
|
206
|
+
|
|
207
|
+
for (const [name, version] of Object.entries(requiredDevDependencies)) {
|
|
208
|
+
dependencyState.devDependencies[name] = version;
|
|
209
|
+
}
|
|
210
|
+
pkg.devDependencies = dependencyState.devDependencies;
|
|
211
|
+
|
|
212
|
+
fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
213
|
+
|
|
214
|
+
fs.mkdirSync(huskyDir, { recursive: true });
|
|
215
|
+
fs.writeFileSync(preCommitHookPath, REQUIRED_PRE_COMMIT_HOOK, 'utf8');
|
|
216
|
+
fs.writeFileSync(commitMsgHookPath, REQUIRED_COMMIT_MSG_HOOK, 'utf8');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const report = {
|
|
220
|
+
cwd,
|
|
221
|
+
mode,
|
|
222
|
+
ok: true,
|
|
223
|
+
rootPackageJsonPath,
|
|
224
|
+
toolingBaselinePath: toolingBaseline.toolingBaselinePath,
|
|
225
|
+
required: {
|
|
226
|
+
prepareScript: REQUIRED_PREPARE_SCRIPT,
|
|
227
|
+
precommitCheckScriptKey: REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY,
|
|
228
|
+
precommitCheckScriptValue: REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE,
|
|
229
|
+
managedDevDependencies: requiredDevDependencies,
|
|
230
|
+
preCommitHookPath: path.relative(cwd, preCommitHookPath),
|
|
231
|
+
commitMsgHookPath: path.relative(cwd, commitMsgHookPath),
|
|
232
|
+
},
|
|
233
|
+
status: {
|
|
234
|
+
matchesRequiredPrepareBefore: matchesRequiredPrepare,
|
|
235
|
+
matchesRequiredPrecommitCheckBefore: matchesRequiredPrecommitCheck,
|
|
236
|
+
matchesRequiredManagedDevDependenciesBefore: matchesRequiredManagedDevDependencies,
|
|
237
|
+
matchesRequiredPreCommitHookBefore: matchesRequiredPreCommitHook,
|
|
238
|
+
matchesRequiredCommitMsgHookBefore: matchesRequiredCommitMsgHook,
|
|
239
|
+
matchesRequiredPrepareAfter:
|
|
240
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredPrepare,
|
|
241
|
+
matchesRequiredPrecommitCheckAfter:
|
|
242
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredPrecommitCheck,
|
|
243
|
+
matchesRequiredManagedDevDependenciesAfter:
|
|
244
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredManagedDevDependencies,
|
|
245
|
+
matchesRequiredPreCommitHookAfter:
|
|
246
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredPreCommitHook,
|
|
247
|
+
matchesRequiredCommitMsgHookAfter:
|
|
248
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredCommitMsgHook,
|
|
249
|
+
updated: requiresUpdate && mode === 'sync',
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
if (mode === 'check' && requiresUpdate) {
|
|
254
|
+
report.ok = false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (jsonFile) {
|
|
258
|
+
const outPath = path.resolve(cwd, jsonFile);
|
|
259
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
260
|
+
fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
264
|
+
if (!report.ok) {
|
|
265
|
+
process.exit(2);
|
|
266
|
+
}
|
|
267
|
+
}
|