@produck/agent-toolkit 0.2.1 → 0.4.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.
@@ -0,0 +1,22 @@
1
+ Usage:
2
+ agent-toolkit sync-coverage-script [--cwd <dir>]
3
+ [--workspace <path>] ... [--check] [--dry-run] [--json <file>]
4
+
5
+ Behavior:
6
+ - Applies organization-required coverage script to workspace package.json files
7
+ - Organization-reserved script key is scripts.produck:coverage
8
+ - Applies organization-required pinned local c8 devDependency to workspace
9
+ package.json files
10
+ - Target script is rendered from organization tooling baseline file
11
+ (lookup order):
12
+ 1) .github/distribution/produck/tooling-version-baseline.json
13
+ 2) publish-assets/instructions/produck/tooling-version-baseline.json
14
+ - Baseline template:
15
+ c8 --reporter=lcov --reporter=html --reporter=text-summary npm test
16
+
17
+ Rules:
18
+ - When --workspace is omitted, root package.json workspaces are used
19
+ - Root workspaces must be explicit paths (no glob tokens)
20
+ - Workspace package.json files must pin devDependencies.c8 to baseline version
21
+ - --check validates without writing and exits non-zero on mismatch
22
+ - --dry-run prints planned changes without writing
@@ -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
+ }
@@ -2,6 +2,7 @@ Usage:
2
2
  agent-toolkit validate-commit-msg --file <message-file>
3
3
 
4
4
  Rules:
5
+ - In monorepo mode, a section header is required before tagged lines
5
6
  - Non-empty lines must be either a section header (for example workspace: or @scope/pkg:) or start with [TAG]
6
7
  - If section headers are used, each section header must be followed by at least one tagged line
7
8
  - No empty lines are allowed
@@ -10,6 +10,7 @@ const ALLOWED_TARGETS = ['docs', 'test', 'ci', 'deps', 'api', 'schema', 'infra',
10
10
  const SECTION_HEADER_RE = /^(?:@[\w.-]+\/)?[\w.-]+:$/;
11
11
  const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
12
12
  const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
13
+ const ROOT_PACKAGE_FILE = path.resolve(COMMAND_DIR, '../../../../../package.json');
13
14
 
14
15
  export function printValidateCommitMsgHelp() {
15
16
  printTextResource(HELP_FILE);
@@ -54,6 +55,19 @@ function isSectionHeaderLine(line) {
54
55
  return SECTION_HEADER_RE.test(line.trim());
55
56
  }
56
57
 
58
+ function isMonorepoRoot() {
59
+ if (!fs.existsSync(ROOT_PACKAGE_FILE)) {
60
+ return false;
61
+ }
62
+
63
+ try {
64
+ const rootPackage = JSON.parse(fs.readFileSync(ROOT_PACKAGE_FILE, 'utf8'));
65
+ return Array.isArray(rootPackage.workspaces) && rootPackage.workspaces.length > 0;
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+
57
71
  function validateSectionFormat(lines) {
58
72
  const errors = [];
59
73
  let currentSection = '';
@@ -128,8 +142,25 @@ export function runValidateCommitMsg(options) {
128
142
  process.exit(2);
129
143
  }
130
144
 
145
+ const mustUseSectionHeaders = isMonorepoRoot();
131
146
  const hasSectionHeaders = lines.some((line) => isSectionHeaderLine(line));
132
147
 
148
+ // [PUBLISH] is generated by lerna and is always a repo-wide tag.
149
+ // In independent mode lerna appends package/version lines after the tag.
150
+ // Neither section headers nor a summary are required for this special tag.
151
+ const isPublishOnlyMessage = /^\[PUBLISH\](\s+.*)?$/.test(lines[0].trim());
152
+
153
+ if (isPublishOnlyMessage) {
154
+ console.log('Commit message validation passed');
155
+ return;
156
+ }
157
+
158
+ if (mustUseSectionHeaders && !hasSectionHeaders) {
159
+ console.error('Commit message validation failed:');
160
+ console.error('- Line 1: section header is required before tagged lines in monorepo mode');
161
+ process.exit(1);
162
+ }
163
+
133
164
  const errors = hasSectionHeaders ? validateSectionFormat(lines) : [];
134
165
  if (!hasSectionHeaders) {
135
166
  for (let i = 0; i < lines.length; i += 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produck/agent-toolkit",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Central CLI toolkit for organization AI execution workflows",
5
5
  "type": "module",
6
6
  "repository": {
@@ -17,7 +17,8 @@
17
17
  "coverage:check": "npm exec --yes -- c8 --check-coverage --lines 100 --functions 100 --branches 100 --statements 100 node --test test/index.mjs",
18
18
  "test": "node --test test/index.mjs",
19
19
  "verify": "node ./bin/agent-toolkit.mjs --help && node ./bin/agent-toolkit.mjs preflight --cwd . --require package.json",
20
- "pack:check": "npm pack --dry-run"
20
+ "pack:check": "npm pack --dry-run",
21
+ "produck:coverage": "c8@11.0.0 --reporter=lcov --reporter=html --reporter=text-summary npm test"
21
22
  },
22
23
  "files": [
23
24
  "bin",
@@ -30,5 +31,8 @@
30
31
  "node": ">=18.0.0"
31
32
  },
32
33
  "license": "MIT",
33
- "gitHead": "555d39b0f0ed95f59d457230f784c9dafe7b84d2"
34
+ "gitHead": "c0aabcec12d70234cfc15cb741fe4ffcf5e4601e",
35
+ "devDependencies": {
36
+ "c8": "11.0.0"
37
+ }
34
38
  }