@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.
- package/README.md +78 -16
- package/bin/agent-toolkit.mjs +32 -0
- package/bin/command/enforce-node-baseline/help.txt +8 -5
- package/bin/command/enforce-node-baseline/index.mjs +32 -0
- package/bin/command/main/help.txt +4 -0
- package/bin/command/preflight/index.mjs +7 -35
- package/bin/command/shared/workspace-validation.mjs +63 -0
- package/bin/command/sync-coverage-script/help.txt +10 -3
- package/bin/command/sync-coverage-script/index.mjs +29 -2
- package/bin/command/sync-editorconfig/editorconfig.template +15 -0
- package/bin/command/sync-editorconfig/help.txt +13 -0
- package/bin/command/sync-editorconfig/index.mjs +233 -0
- package/bin/command/sync-eslint-config/help.txt +18 -0
- package/bin/command/sync-eslint-config/index.mjs +248 -0
- package/bin/command/sync-husky-hooks/help.txt +1 -9
- package/bin/command/sync-husky-hooks/index.mjs +1 -179
- package/bin/command/sync-prettier-config/help.txt +14 -0
- package/bin/command/sync-prettier-config/index.mjs +130 -0
- package/bin/command/sync-workspace-config/help.txt +21 -0
- package/bin/command/sync-workspace-config/index.mjs +290 -0
- package/bin/command/validate-commit-msg/help.txt +2 -1
- package/bin/command/validate-commit-msg/index.mjs +53 -3
- package/package.json +4 -6
- package/publish-assets/instructions/produck/10-produck-node.instructions.md +52 -7
- package/publish-assets/instructions/produck/20-produck-commit.instructions.md +7 -0
|
@@ -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
|
+
}
|