@produck/agent-toolkit 0.4.0 → 0.5.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 +24 -0
- package/bin/command/enforce-node-baseline/help.txt +8 -5
- package/bin/command/enforce-node-baseline/index.mjs +24 -0
- package/bin/command/main/help.txt +3 -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-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
|
@@ -0,0 +1,248 @@
|
|
|
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 TOOLKIT_PACKAGE_JSON = path.resolve(PACKAGE_ROOT, 'package.json');
|
|
13
|
+
const ESLINT_CONFIG_FILE = 'eslint.config.mjs';
|
|
14
|
+
|
|
15
|
+
const REQUIRED_LINT_SCRIPT_KEY = 'produck:lint';
|
|
16
|
+
const REQUIRED_LINT_SCRIPT_VALUE =
|
|
17
|
+
'npm exec -- eslint --fix . --max-warnings=0 && npm run lint --if-present';
|
|
18
|
+
const REQUIRED_ESLINT_CONFIG = `import globals from 'globals';
|
|
19
|
+
import pluginJs from '@eslint/js';
|
|
20
|
+
import tseslint from 'typescript-eslint';
|
|
21
|
+
import * as ProduckRule from '@produck/eslint-rules';
|
|
22
|
+
|
|
23
|
+
export default [
|
|
24
|
+
{ files: ['**/*.{js,mjs,cjs,ts,mts}'] },
|
|
25
|
+
{ languageOptions: { globals: { ...globals.browser, ...globals.node } } },
|
|
26
|
+
pluginJs.configs.recommended,
|
|
27
|
+
...tseslint.configs.recommended,
|
|
28
|
+
ProduckRule.config,
|
|
29
|
+
ProduckRule.excludeGitIgnore(import.meta.url),
|
|
30
|
+
];
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
export function printSyncEslintConfigHelp() {
|
|
34
|
+
printTextResource(HELP_FILE);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseJsonFile(filePath, label) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
40
|
+
} catch {
|
|
41
|
+
console.error(`${label} is not valid JSON: ${filePath}`);
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readFileIfExists(filePath) {
|
|
47
|
+
if (!fs.existsSync(filePath)) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getRequiredEslintRulesDevDependency() {
|
|
55
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
56
|
+
const latestResult = spawnSync(npmCommand, ['view', '@produck/eslint-rules', 'version'], {
|
|
57
|
+
encoding: 'utf8',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const latestVersion = String(latestResult.stdout || '').trim();
|
|
61
|
+
if (latestResult.status === 0 && latestVersion) {
|
|
62
|
+
return latestVersion;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const pkg = parseJsonFile(TOOLKIT_PACKAGE_JSON, 'Toolkit package.json');
|
|
66
|
+
const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
|
|
67
|
+
|
|
68
|
+
if (!version) {
|
|
69
|
+
console.error(`Toolkit package version is missing: ${TOOLKIT_PACKAGE_JSON}`);
|
|
70
|
+
process.exit(2);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return version;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function patchEslintConfig(existing) {
|
|
77
|
+
if (existing.includes('@produck/eslint-rules')) {
|
|
78
|
+
return { ok: true, patched: false, output: existing };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const importRegex = /^import\s.+;\s*$/gm;
|
|
82
|
+
let lastImport = null;
|
|
83
|
+
let match = importRegex.exec(existing);
|
|
84
|
+
while (match) {
|
|
85
|
+
lastImport = match;
|
|
86
|
+
match = importRegex.exec(existing);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!lastImport) {
|
|
90
|
+
return { ok: false, patched: false, output: existing };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const importInsertAt = lastImport.index + lastImport[0].length;
|
|
94
|
+
let output =
|
|
95
|
+
`${existing.slice(0, importInsertAt)}\nimport * as ProduckRule from '@produck/eslint-rules';` +
|
|
96
|
+
existing.slice(importInsertAt);
|
|
97
|
+
|
|
98
|
+
const exportStart = output.indexOf('export default [');
|
|
99
|
+
const exportEnd = output.lastIndexOf('];');
|
|
100
|
+
if (exportStart === -1 || exportEnd === -1 || exportEnd < exportStart) {
|
|
101
|
+
return { ok: false, patched: false, output: existing };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
output =
|
|
105
|
+
`${output.slice(0, exportEnd)} ProduckRule.config,\n ProduckRule.excludeGitIgnore(import.meta.url),\n` +
|
|
106
|
+
output.slice(exportEnd);
|
|
107
|
+
|
|
108
|
+
if (!output.endsWith('\n')) {
|
|
109
|
+
output = `${output}\n`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { ok: true, patched: true, output };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function runSyncEslintConfig(options) {
|
|
116
|
+
const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
|
|
117
|
+
const check = hasFlag(options, '--check');
|
|
118
|
+
const dryRun = hasFlag(options, '--dry-run') && !check;
|
|
119
|
+
const jsonFile = getSingle(options, '--json', '');
|
|
120
|
+
const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
|
|
121
|
+
|
|
122
|
+
if (!fs.existsSync(cwd)) {
|
|
123
|
+
console.error(`CWD does not exist: ${cwd}`);
|
|
124
|
+
process.exit(2);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const rootPackageJsonPath = path.resolve(cwd, 'package.json');
|
|
128
|
+
if (!fs.existsSync(rootPackageJsonPath)) {
|
|
129
|
+
console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
|
|
130
|
+
process.exit(2);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
|
|
134
|
+
const scripts =
|
|
135
|
+
pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
|
|
136
|
+
? { ...pkg.scripts }
|
|
137
|
+
: {};
|
|
138
|
+
const devDependencies =
|
|
139
|
+
pkg.devDependencies &&
|
|
140
|
+
typeof pkg.devDependencies === 'object' &&
|
|
141
|
+
!Array.isArray(pkg.devDependencies)
|
|
142
|
+
? { ...pkg.devDependencies }
|
|
143
|
+
: {};
|
|
144
|
+
|
|
145
|
+
const previousLint =
|
|
146
|
+
typeof scripts[REQUIRED_LINT_SCRIPT_KEY] === 'string'
|
|
147
|
+
? scripts[REQUIRED_LINT_SCRIPT_KEY]
|
|
148
|
+
: null;
|
|
149
|
+
const previousEslintRules =
|
|
150
|
+
typeof devDependencies['@produck/eslint-rules'] === 'string'
|
|
151
|
+
? devDependencies['@produck/eslint-rules']
|
|
152
|
+
: null;
|
|
153
|
+
|
|
154
|
+
const requiredEslintRulesDependency = getRequiredEslintRulesDevDependency();
|
|
155
|
+
|
|
156
|
+
const eslintConfigPath = path.resolve(cwd, ESLINT_CONFIG_FILE);
|
|
157
|
+
const previousEslintConfig = readFileIfExists(eslintConfigPath);
|
|
158
|
+
|
|
159
|
+
const matchesRequiredLint = previousLint === REQUIRED_LINT_SCRIPT_VALUE;
|
|
160
|
+
const matchesRequiredEslintRules = previousEslintRules === requiredEslintRulesDependency;
|
|
161
|
+
|
|
162
|
+
let eslintConfigAction = 'unchanged';
|
|
163
|
+
let matchesRequiredEslintConfig = false;
|
|
164
|
+
let nextEslintConfigText = previousEslintConfig;
|
|
165
|
+
|
|
166
|
+
if (previousEslintConfig === null) {
|
|
167
|
+
eslintConfigAction = 'initialized';
|
|
168
|
+
nextEslintConfigText = REQUIRED_ESLINT_CONFIG;
|
|
169
|
+
} else if (previousEslintConfig === REQUIRED_ESLINT_CONFIG) {
|
|
170
|
+
matchesRequiredEslintConfig = true;
|
|
171
|
+
} else if (previousEslintConfig.includes('@produck/eslint-rules')) {
|
|
172
|
+
matchesRequiredEslintConfig = true;
|
|
173
|
+
} else {
|
|
174
|
+
const patched = patchEslintConfig(previousEslintConfig);
|
|
175
|
+
if (patched.ok) {
|
|
176
|
+
eslintConfigAction = 'patched';
|
|
177
|
+
nextEslintConfigText = patched.output;
|
|
178
|
+
} else {
|
|
179
|
+
eslintConfigAction = 'unpatchable';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const requiresUpdate =
|
|
184
|
+
!matchesRequiredLint || !matchesRequiredEslintRules || !matchesRequiredEslintConfig;
|
|
185
|
+
const hasUnpatchableEslintConfig = eslintConfigAction === 'unpatchable';
|
|
186
|
+
|
|
187
|
+
if (mode === 'sync' && requiresUpdate && !hasUnpatchableEslintConfig) {
|
|
188
|
+
scripts[REQUIRED_LINT_SCRIPT_KEY] = REQUIRED_LINT_SCRIPT_VALUE;
|
|
189
|
+
pkg.scripts = scripts;
|
|
190
|
+
|
|
191
|
+
devDependencies['@produck/eslint-rules'] = requiredEslintRulesDependency;
|
|
192
|
+
pkg.devDependencies = devDependencies;
|
|
193
|
+
|
|
194
|
+
fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
195
|
+
fs.writeFileSync(eslintConfigPath, nextEslintConfigText || REQUIRED_ESLINT_CONFIG, 'utf8');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const report = {
|
|
199
|
+
cwd,
|
|
200
|
+
mode,
|
|
201
|
+
ok: true,
|
|
202
|
+
rootPackageJsonPath,
|
|
203
|
+
required: {
|
|
204
|
+
lintScriptKey: REQUIRED_LINT_SCRIPT_KEY,
|
|
205
|
+
lintScriptValue: REQUIRED_LINT_SCRIPT_VALUE,
|
|
206
|
+
eslintRulesVersion: requiredEslintRulesDependency,
|
|
207
|
+
eslintConfigPath: path.relative(cwd, eslintConfigPath),
|
|
208
|
+
eslintConfigAction,
|
|
209
|
+
},
|
|
210
|
+
status: {
|
|
211
|
+
matchesRequiredLintBefore: matchesRequiredLint,
|
|
212
|
+
matchesRequiredEslintRulesBefore: matchesRequiredEslintRules,
|
|
213
|
+
matchesRequiredEslintConfigBefore: matchesRequiredEslintConfig,
|
|
214
|
+
matchesRequiredLintAfter:
|
|
215
|
+
requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
|
|
216
|
+
? true
|
|
217
|
+
: matchesRequiredLint,
|
|
218
|
+
matchesRequiredEslintRulesAfter:
|
|
219
|
+
requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
|
|
220
|
+
? true
|
|
221
|
+
: matchesRequiredEslintRules,
|
|
222
|
+
matchesRequiredEslintConfigAfter:
|
|
223
|
+
requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
|
|
224
|
+
? true
|
|
225
|
+
: matchesRequiredEslintConfig,
|
|
226
|
+
updated: requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig,
|
|
227
|
+
hasUnpatchableEslintConfig,
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (mode === 'check' && (requiresUpdate || hasUnpatchableEslintConfig)) {
|
|
232
|
+
report.ok = false;
|
|
233
|
+
}
|
|
234
|
+
if ((mode === 'sync' || mode === 'dry-run') && hasUnpatchableEslintConfig) {
|
|
235
|
+
report.ok = false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (jsonFile) {
|
|
239
|
+
const outPath = path.resolve(cwd, jsonFile);
|
|
240
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
241
|
+
fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
245
|
+
if (!report.ok) {
|
|
246
|
+
process.exit(2);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -3,15 +3,7 @@ Usage:
|
|
|
3
3
|
[--json <file>]
|
|
4
4
|
|
|
5
5
|
Behavior:
|
|
6
|
-
- Applies organization-required
|
|
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:
|
|
6
|
+
- Applies organization-required hook files only:
|
|
15
7
|
- .husky/pre-commit
|
|
16
8
|
- .husky/commit-msg
|
|
17
9
|
- commit-msg hook validates message via local node_modules toolkit path
|
|
@@ -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
|