@produck/agent-toolkit 0.4.0 → 0.6.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.
@@ -1,6 +1,5 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { spawnSync } from 'node:child_process';
4
3
  import { fileURLToPath } from 'node:url';
5
4
 
6
5
  import { getSingle, hasFlag } from '../shared/args.mjs';
@@ -8,17 +7,6 @@ import { printTextResource } from '../shared/text-resource.mjs';
8
7
 
9
8
  const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
10
9
  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
10
 
23
11
  const REQUIRED_PRE_COMMIT_HOOK = '#!/usr/bin/env sh\nnpm run produck:precommit-check\n';
24
12
  const REQUIRED_COMMIT_MSG_HOOK =
@@ -28,75 +16,6 @@ export function printSyncHuskyHooksHelp() {
28
16
  printTextResource(HELP_FILE);
29
17
  }
30
18
 
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
19
  function readFileIfExists(filePath) {
101
20
  if (!fs.existsSync(filePath)) {
102
21
  return null;
@@ -105,44 +24,6 @@ function readFileIfExists(filePath) {
105
24
  return fs.readFileSync(filePath, 'utf8');
106
25
  }
107
26
 
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
27
  export function runSyncHuskyHooks(options) {
147
28
  const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
148
29
  const check = hasFlag(options, '--check');
@@ -155,24 +36,6 @@ export function runSyncHuskyHooks(options) {
155
36
  process.exit(2);
156
37
  }
157
38
 
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
39
  const huskyDir = path.resolve(cwd, '.husky');
177
40
  const preCommitHookPath = path.resolve(huskyDir, 'pre-commit');
178
41
  const commitMsgHookPath = path.resolve(huskyDir, 'commit-msg');
@@ -180,37 +43,11 @@ export function runSyncHuskyHooks(options) {
180
43
  const previousPreCommitHook = readFileIfExists(preCommitHookPath);
181
44
  const previousCommitMsgHook = readFileIfExists(commitMsgHookPath);
182
45
 
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
46
  const matchesRequiredPreCommitHook = previousPreCommitHook === REQUIRED_PRE_COMMIT_HOOK;
192
47
  const matchesRequiredCommitMsgHook = previousCommitMsgHook === REQUIRED_COMMIT_MSG_HOOK;
193
-
194
- const requiresUpdate =
195
- !matchesRequiredPrepare ||
196
- !matchesRequiredPrecommitCheck ||
197
- !matchesRequiredManagedDevDependencies ||
198
- !matchesRequiredPreCommitHook ||
199
- !matchesRequiredCommitMsgHook;
48
+ const requiresUpdate = !matchesRequiredPreCommitHook || !matchesRequiredCommitMsgHook;
200
49
 
201
50
  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
51
  fs.mkdirSync(huskyDir, { recursive: true });
215
52
  fs.writeFileSync(preCommitHookPath, REQUIRED_PRE_COMMIT_HOOK, 'utf8');
216
53
  fs.writeFileSync(commitMsgHookPath, REQUIRED_COMMIT_MSG_HOOK, 'utf8');
@@ -220,28 +57,13 @@ export function runSyncHuskyHooks(options) {
220
57
  cwd,
221
58
  mode,
222
59
  ok: true,
223
- rootPackageJsonPath,
224
- toolingBaselinePath: toolingBaseline.toolingBaselinePath,
225
60
  required: {
226
- prepareScript: REQUIRED_PREPARE_SCRIPT,
227
- precommitCheckScriptKey: REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY,
228
- precommitCheckScriptValue: REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE,
229
- managedDevDependencies: requiredDevDependencies,
230
61
  preCommitHookPath: path.relative(cwd, preCommitHookPath),
231
62
  commitMsgHookPath: path.relative(cwd, commitMsgHookPath),
232
63
  },
233
64
  status: {
234
- matchesRequiredPrepareBefore: matchesRequiredPrepare,
235
- matchesRequiredPrecommitCheckBefore: matchesRequiredPrecommitCheck,
236
- matchesRequiredManagedDevDependenciesBefore: matchesRequiredManagedDevDependencies,
237
65
  matchesRequiredPreCommitHookBefore: matchesRequiredPreCommitHook,
238
66
  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
67
  matchesRequiredPreCommitHookAfter:
246
68
  requiresUpdate && mode === 'sync' ? true : matchesRequiredPreCommitHook,
247
69
  matchesRequiredCommitMsgHookAfter:
@@ -0,0 +1,14 @@
1
+ Usage:
2
+ agent-toolkit sync-prettier-config [--cwd <dir>] [--check] [--dry-run]
3
+ [--json <file>]
4
+
5
+ Behavior:
6
+ - Applies organization-required root format script:
7
+ - scripts.produck:format = npm exec -- prettier --check . && npm run format --if-present
8
+ - Applies organization-required root Prettier config file:
9
+ - .prettierrc
10
+
11
+ Rules:
12
+ - --check validates without writing and exits non-zero on mismatch
13
+ - --dry-run prints planned changes without writing
14
+ - --check takes precedence over --dry-run
@@ -0,0 +1,130 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ import { 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 PRETTIER_CONFIG_FILE = '.prettierrc';
11
+
12
+ const REQUIRED_FORMAT_SCRIPT_KEY = 'produck:format';
13
+ const REQUIRED_FORMAT_SCRIPT_VALUE =
14
+ 'npm exec -- prettier --check . && npm run format --if-present';
15
+ const REQUIRED_PRETTIER_CONFIG = `${JSON.stringify(
16
+ {
17
+ semi: true,
18
+ singleQuote: true,
19
+ tabWidth: 2,
20
+ useTabs: false,
21
+ trailingComma: 'all',
22
+ bracketSpacing: true,
23
+ arrowParens: 'always',
24
+ printWidth: 100,
25
+ },
26
+ null,
27
+ 2,
28
+ )}\n`;
29
+
30
+ export function printSyncPrettierConfigHelp() {
31
+ printTextResource(HELP_FILE);
32
+ }
33
+
34
+ function parseJsonFile(filePath, label) {
35
+ try {
36
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
37
+ } catch {
38
+ console.error(`${label} is not valid JSON: ${filePath}`);
39
+ process.exit(2);
40
+ }
41
+ }
42
+
43
+ function readFileIfExists(filePath) {
44
+ if (!fs.existsSync(filePath)) {
45
+ return null;
46
+ }
47
+
48
+ return fs.readFileSync(filePath, 'utf8');
49
+ }
50
+
51
+ export function runSyncPrettierConfig(options) {
52
+ const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
53
+ const check = hasFlag(options, '--check');
54
+ const dryRun = hasFlag(options, '--dry-run') && !check;
55
+ const jsonFile = getSingle(options, '--json', '');
56
+ const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
57
+
58
+ if (!fs.existsSync(cwd)) {
59
+ console.error(`CWD does not exist: ${cwd}`);
60
+ process.exit(2);
61
+ }
62
+
63
+ const rootPackageJsonPath = path.resolve(cwd, 'package.json');
64
+ if (!fs.existsSync(rootPackageJsonPath)) {
65
+ console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
66
+ process.exit(2);
67
+ }
68
+
69
+ const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
70
+ const scripts =
71
+ pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
72
+ ? { ...pkg.scripts }
73
+ : {};
74
+
75
+ const previousFormat =
76
+ typeof scripts[REQUIRED_FORMAT_SCRIPT_KEY] === 'string'
77
+ ? scripts[REQUIRED_FORMAT_SCRIPT_KEY]
78
+ : null;
79
+
80
+ const prettierConfigPath = path.resolve(cwd, PRETTIER_CONFIG_FILE);
81
+ const previousPrettierConfig = readFileIfExists(prettierConfigPath);
82
+
83
+ const matchesRequiredFormat = previousFormat === REQUIRED_FORMAT_SCRIPT_VALUE;
84
+ const matchesRequiredPrettierConfig = previousPrettierConfig === REQUIRED_PRETTIER_CONFIG;
85
+
86
+ const requiresUpdate = !matchesRequiredFormat || !matchesRequiredPrettierConfig;
87
+
88
+ if (mode === 'sync' && requiresUpdate) {
89
+ scripts[REQUIRED_FORMAT_SCRIPT_KEY] = REQUIRED_FORMAT_SCRIPT_VALUE;
90
+ pkg.scripts = scripts;
91
+
92
+ fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
93
+ fs.writeFileSync(prettierConfigPath, REQUIRED_PRETTIER_CONFIG, 'utf8');
94
+ }
95
+
96
+ const report = {
97
+ cwd,
98
+ mode,
99
+ ok: true,
100
+ rootPackageJsonPath,
101
+ required: {
102
+ formatScriptKey: REQUIRED_FORMAT_SCRIPT_KEY,
103
+ formatScriptValue: REQUIRED_FORMAT_SCRIPT_VALUE,
104
+ prettierConfigPath: path.relative(cwd, prettierConfigPath),
105
+ },
106
+ status: {
107
+ matchesRequiredFormatBefore: matchesRequiredFormat,
108
+ matchesRequiredPrettierConfigBefore: matchesRequiredPrettierConfig,
109
+ matchesRequiredFormatAfter: requiresUpdate && mode === 'sync' ? true : matchesRequiredFormat,
110
+ matchesRequiredPrettierConfigAfter:
111
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredPrettierConfig,
112
+ updated: requiresUpdate && mode === 'sync',
113
+ },
114
+ };
115
+
116
+ if (mode === 'check' && requiresUpdate) {
117
+ report.ok = false;
118
+ }
119
+
120
+ if (jsonFile) {
121
+ const outPath = path.resolve(cwd, jsonFile);
122
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
123
+ fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
124
+ }
125
+
126
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
127
+ if (!report.ok) {
128
+ process.exit(2);
129
+ }
130
+ }
@@ -0,0 +1,21 @@
1
+ Usage:
2
+ agent-toolkit sync-workspace-config [--cwd <dir>] [--check] [--dry-run]
3
+ [--json <file>]
4
+
5
+ Behavior:
6
+ - Applies organization-required root shared scripts:
7
+ - scripts.produck:baseline = npm exec --package=@produck/agent-toolkit@latest -- agent-toolkit enforce-node-baseline --cwd .
8
+ - scripts.produck:coverage = c8 --config .c8rc.json npm run test --workspaces --if-present
9
+ - scripts.produck:precommit-check = npm run produck:format && npm run produck:lint
10
+ - Applies organization-required root c8 config file:
11
+ - .c8rc.json
12
+ - Applies organization-required root shared managed devDependencies:
13
+ - devDependencies.c8 = <fixed-version-from-baseline>
14
+ - devDependencies.husky = <fixed-version-from-baseline>
15
+ - devDependencies.lerna = <fixed-version-from-baseline>
16
+ - devDependencies.@produck/agent-toolkit = <latest-version-resolved-at-runtime>
17
+
18
+ Rules:
19
+ - --check validates without writing and exits non-zero on mismatch
20
+ - --dry-run prints planned changes without writing
21
+ - --check takes precedence over --dry-run
@@ -0,0 +1,290 @@
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
+ import { validateRequiredExactEntries } from '../shared/workspace-validation.mjs';
9
+
10
+ const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
11
+ const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
12
+ const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
13
+ const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
14
+ const TOOLKIT_PACKAGE_JSON = path.resolve(PACKAGE_ROOT, 'package.json');
15
+ const TOOLING_BASELINE_CANDIDATE_PATHS = [
16
+ path.resolve(REPO_ROOT, '.github/distribution/produck/tooling-version-baseline.json'),
17
+ path.resolve(PACKAGE_ROOT, 'publish-assets/instructions/produck/tooling-version-baseline.json'),
18
+ ];
19
+
20
+ const REQUIRED_BASELINE_SCRIPT_KEY = 'produck:baseline';
21
+ const REQUIRED_BASELINE_SCRIPT_VALUE =
22
+ 'npm exec --package=@produck/agent-toolkit@latest -- agent-toolkit enforce-node-baseline --cwd .';
23
+ const REQUIRED_WORKSPACE_COVERAGE_SCRIPT_KEY = 'produck:coverage';
24
+ const REQUIRED_WORKSPACE_COVERAGE_SCRIPT_VALUE =
25
+ 'c8 --config .c8rc.json npm run test --workspaces --if-present';
26
+ const REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY = 'produck:precommit-check';
27
+ const REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE = 'npm run produck:format && npm run produck:lint';
28
+ const REQUIRED_C8_CONFIG_FILE = '.c8rc.json';
29
+ const REQUIRED_C8_CONFIG_CONTENT = `${JSON.stringify(
30
+ {
31
+ reporter: ['lcov', 'html', 'text-summary'],
32
+ },
33
+ null,
34
+ 2,
35
+ )}\n`;
36
+
37
+ export function printSyncWorkspaceConfigHelp() {
38
+ printTextResource(HELP_FILE);
39
+ }
40
+
41
+ function parseJsonFile(filePath, label) {
42
+ try {
43
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
44
+ } catch {
45
+ console.error(`${label} is not valid JSON: ${filePath}`);
46
+ process.exit(2);
47
+ }
48
+ }
49
+
50
+ function getRequiredToolkitDevDependency() {
51
+ const overrideVersion = String(process.env.PRODUCK_TOOLKIT_VERSION_OVERRIDE || '').trim();
52
+ if (overrideVersion) {
53
+ return overrideVersion;
54
+ }
55
+
56
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
57
+ const latestResult = spawnSync(npmCommand, ['view', '@produck/agent-toolkit', 'version'], {
58
+ encoding: 'utf8',
59
+ });
60
+
61
+ const latestVersion = String(latestResult.stdout || '').trim();
62
+ if (latestResult.status === 0 && latestVersion) {
63
+ return latestVersion;
64
+ }
65
+
66
+ const pkg = parseJsonFile(TOOLKIT_PACKAGE_JSON, 'Toolkit package.json');
67
+ const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
68
+
69
+ if (!version) {
70
+ console.error(`Toolkit package version is missing: ${TOOLKIT_PACKAGE_JSON}`);
71
+ process.exit(2);
72
+ }
73
+
74
+ return version;
75
+ }
76
+
77
+ function loadToolingBaseline() {
78
+ const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
79
+ return fs.existsSync(candidatePath);
80
+ });
81
+
82
+ if (!toolingBaselinePath) {
83
+ console.error('Tooling baseline file does not exist in expected locations:');
84
+ for (const candidatePath of TOOLING_BASELINE_CANDIDATE_PATHS) {
85
+ console.error(`- ${candidatePath}`);
86
+ }
87
+ process.exit(2);
88
+ }
89
+
90
+ const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
91
+ const c8Version = String(baseline?.tools?.c8?.version || '').trim();
92
+ const huskyVersion = String(baseline?.tools?.husky?.version || '').trim();
93
+ const lernaVersion = String(baseline?.tools?.lerna?.version || '').trim();
94
+
95
+ if (!c8Version || !huskyVersion || !lernaVersion) {
96
+ console.error(
97
+ `Tooling baseline must define fixed tools.c8/husky/lerna.version: ${toolingBaselinePath}`,
98
+ );
99
+ process.exit(2);
100
+ }
101
+
102
+ return {
103
+ toolingBaselinePath,
104
+ c8Version,
105
+ huskyVersion,
106
+ lernaVersion,
107
+ };
108
+ }
109
+
110
+ function buildScriptState(pkg) {
111
+ const scripts =
112
+ pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
113
+ ? { ...pkg.scripts }
114
+ : {};
115
+
116
+ return {
117
+ scripts,
118
+ previousBaseline:
119
+ typeof scripts[REQUIRED_BASELINE_SCRIPT_KEY] === 'string'
120
+ ? scripts[REQUIRED_BASELINE_SCRIPT_KEY]
121
+ : null,
122
+ previousCoverage:
123
+ typeof scripts[REQUIRED_WORKSPACE_COVERAGE_SCRIPT_KEY] === 'string'
124
+ ? scripts[REQUIRED_WORKSPACE_COVERAGE_SCRIPT_KEY]
125
+ : null,
126
+ previousPrecommitCheck:
127
+ typeof scripts[REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY] === 'string'
128
+ ? scripts[REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY]
129
+ : null,
130
+ };
131
+ }
132
+
133
+ function readFileIfExists(filePath) {
134
+ if (!fs.existsSync(filePath)) {
135
+ return null;
136
+ }
137
+
138
+ return fs.readFileSync(filePath, 'utf8');
139
+ }
140
+
141
+ function buildDevDependencyState(pkg) {
142
+ const devDependencies =
143
+ pkg.devDependencies &&
144
+ typeof pkg.devDependencies === 'object' &&
145
+ !Array.isArray(pkg.devDependencies)
146
+ ? { ...pkg.devDependencies }
147
+ : {};
148
+
149
+ return {
150
+ devDependencies,
151
+ previousManaged: {
152
+ c8: typeof devDependencies.c8 === 'string' ? devDependencies.c8 : null,
153
+ husky: typeof devDependencies.husky === 'string' ? devDependencies.husky : null,
154
+ lerna: typeof devDependencies.lerna === 'string' ? devDependencies.lerna : null,
155
+ '@produck/agent-toolkit':
156
+ typeof devDependencies['@produck/agent-toolkit'] === 'string'
157
+ ? devDependencies['@produck/agent-toolkit']
158
+ : null,
159
+ },
160
+ };
161
+ }
162
+
163
+ export function runSyncWorkspaceConfig(options) {
164
+ const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
165
+ const check = hasFlag(options, '--check');
166
+ const dryRun = hasFlag(options, '--dry-run') && !check;
167
+ const jsonFile = getSingle(options, '--json', '');
168
+ const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
169
+
170
+ if (!fs.existsSync(cwd)) {
171
+ console.error(`CWD does not exist: ${cwd}`);
172
+ process.exit(2);
173
+ }
174
+
175
+ const rootPackageJsonPath = path.resolve(cwd, 'package.json');
176
+ if (!fs.existsSync(rootPackageJsonPath)) {
177
+ console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
178
+ process.exit(2);
179
+ }
180
+
181
+ const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
182
+ const toolingBaseline = loadToolingBaseline();
183
+ const requiredToolkitDependency = getRequiredToolkitDevDependency();
184
+ const requiredDevDependencies = {
185
+ c8: toolingBaseline.c8Version,
186
+ husky: toolingBaseline.huskyVersion,
187
+ lerna: toolingBaseline.lernaVersion,
188
+ '@produck/agent-toolkit': requiredToolkitDependency,
189
+ };
190
+
191
+ const scriptState = buildScriptState(pkg);
192
+ const dependencyState = buildDevDependencyState(pkg);
193
+ const c8ConfigPath = path.resolve(cwd, REQUIRED_C8_CONFIG_FILE);
194
+ const currentC8ConfigContent = readFileIfExists(c8ConfigPath);
195
+
196
+ const scriptValidation = validateRequiredExactEntries(scriptState.scripts, {
197
+ [REQUIRED_BASELINE_SCRIPT_KEY]: REQUIRED_BASELINE_SCRIPT_VALUE,
198
+ [REQUIRED_WORKSPACE_COVERAGE_SCRIPT_KEY]: REQUIRED_WORKSPACE_COVERAGE_SCRIPT_VALUE,
199
+ [REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY]: REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE,
200
+ });
201
+ const dependencyValidation = validateRequiredExactEntries(
202
+ dependencyState.devDependencies,
203
+ requiredDevDependencies,
204
+ );
205
+
206
+ const matchesRequiredBaseline = !(REQUIRED_BASELINE_SCRIPT_KEY in scriptValidation.mismatches);
207
+ const matchesRequiredWorkspaceCoverage = !(
208
+ REQUIRED_WORKSPACE_COVERAGE_SCRIPT_KEY in scriptValidation.mismatches
209
+ );
210
+ const matchesRequiredPrecommitCheck = !(
211
+ REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY in scriptValidation.mismatches
212
+ );
213
+ const matchesRequiredManagedDevDependencies = dependencyValidation.ok;
214
+ const matchesRequiredC8Config = currentC8ConfigContent === REQUIRED_C8_CONFIG_CONTENT;
215
+
216
+ const requiresUpdate =
217
+ !matchesRequiredBaseline ||
218
+ !matchesRequiredWorkspaceCoverage ||
219
+ !matchesRequiredPrecommitCheck ||
220
+ !matchesRequiredManagedDevDependencies ||
221
+ !matchesRequiredC8Config;
222
+
223
+ if (mode === 'sync' && requiresUpdate) {
224
+ scriptState.scripts[REQUIRED_BASELINE_SCRIPT_KEY] = REQUIRED_BASELINE_SCRIPT_VALUE;
225
+ scriptState.scripts[REQUIRED_WORKSPACE_COVERAGE_SCRIPT_KEY] =
226
+ REQUIRED_WORKSPACE_COVERAGE_SCRIPT_VALUE;
227
+ scriptState.scripts[REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY] =
228
+ REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE;
229
+ pkg.scripts = scriptState.scripts;
230
+
231
+ for (const [name, version] of Object.entries(requiredDevDependencies)) {
232
+ dependencyState.devDependencies[name] = version;
233
+ }
234
+ pkg.devDependencies = dependencyState.devDependencies;
235
+
236
+ fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
237
+ fs.writeFileSync(c8ConfigPath, REQUIRED_C8_CONFIG_CONTENT, 'utf8');
238
+ }
239
+
240
+ const report = {
241
+ cwd,
242
+ mode,
243
+ ok: true,
244
+ rootPackageJsonPath,
245
+ toolingBaselinePath: toolingBaseline.toolingBaselinePath,
246
+ required: {
247
+ baselineScriptKey: REQUIRED_BASELINE_SCRIPT_KEY,
248
+ baselineScriptValue: REQUIRED_BASELINE_SCRIPT_VALUE,
249
+ workspaceCoverageScriptKey: REQUIRED_WORKSPACE_COVERAGE_SCRIPT_KEY,
250
+ workspaceCoverageScriptValue: REQUIRED_WORKSPACE_COVERAGE_SCRIPT_VALUE,
251
+ precommitCheckScriptKey: REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY,
252
+ precommitCheckScriptValue: REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE,
253
+ c8ConfigFile: REQUIRED_C8_CONFIG_FILE,
254
+ managedDevDependencies: requiredDevDependencies,
255
+ },
256
+ status: {
257
+ matchesRequiredBaselineBefore: matchesRequiredBaseline,
258
+ matchesRequiredWorkspaceCoverageBefore: matchesRequiredWorkspaceCoverage,
259
+ matchesRequiredPrecommitCheckBefore: matchesRequiredPrecommitCheck,
260
+ matchesRequiredManagedDevDependenciesBefore: matchesRequiredManagedDevDependencies,
261
+ matchesRequiredC8ConfigBefore: matchesRequiredC8Config,
262
+ matchesRequiredBaselineAfter:
263
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredBaseline,
264
+ matchesRequiredWorkspaceCoverageAfter:
265
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredWorkspaceCoverage,
266
+ matchesRequiredPrecommitCheckAfter:
267
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredPrecommitCheck,
268
+ matchesRequiredManagedDevDependenciesAfter:
269
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredManagedDevDependencies,
270
+ matchesRequiredC8ConfigAfter:
271
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredC8Config,
272
+ updated: requiresUpdate && mode === 'sync',
273
+ },
274
+ };
275
+
276
+ if (mode === 'check' && requiresUpdate) {
277
+ report.ok = false;
278
+ }
279
+
280
+ if (jsonFile) {
281
+ const outPath = path.resolve(cwd, jsonFile);
282
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
283
+ fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
284
+ }
285
+
286
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
287
+ if (!report.ok) {
288
+ process.exit(2);
289
+ }
290
+ }