@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
|
@@ -0,0 +1,233 @@
|
|
|
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 EDITORCONFIG_FILE = '.editorconfig';
|
|
11
|
+
const TEMPLATE_FILE = path.resolve(COMMAND_DIR, 'editorconfig.template');
|
|
12
|
+
|
|
13
|
+
const REQUIRED_EDITORCONFIG_CONTENT = fs.readFileSync(TEMPLATE_FILE, 'utf8');
|
|
14
|
+
|
|
15
|
+
// Required key-value pairs for validation
|
|
16
|
+
const REQUIRED_SECTIONS = {
|
|
17
|
+
root: {
|
|
18
|
+
line: 'root = true',
|
|
19
|
+
},
|
|
20
|
+
'*': {
|
|
21
|
+
keys: {
|
|
22
|
+
charset: 'utf-8',
|
|
23
|
+
indent_style: 'space',
|
|
24
|
+
indent_size: '2',
|
|
25
|
+
trim_trailing_whitespace: 'true',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
'*.{yml,yaml}': {
|
|
29
|
+
keys: {
|
|
30
|
+
indent_style: 'space',
|
|
31
|
+
indent_size: '2',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
'*.md': {
|
|
35
|
+
keys: {
|
|
36
|
+
trim_trailing_whitespace: 'false',
|
|
37
|
+
max_line_length: '80',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function printSyncEditorconfigHelp() {
|
|
43
|
+
printTextResource(HELP_FILE);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseEditorconfig(content) {
|
|
47
|
+
const sections = {};
|
|
48
|
+
let currentSection = null;
|
|
49
|
+
|
|
50
|
+
for (const line of content.split('\n')) {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
|
|
53
|
+
// Skip empty lines and comments
|
|
54
|
+
if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith(';')) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check for section header
|
|
59
|
+
const sectionMatch = trimmed.match(/^\[(.+)\]$/);
|
|
60
|
+
if (sectionMatch) {
|
|
61
|
+
currentSection = sectionMatch[1];
|
|
62
|
+
sections[currentSection] = {};
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for root = true
|
|
67
|
+
const rootMatch = trimmed.match(/^root\s*=\s*(.+)$/i);
|
|
68
|
+
if (rootMatch && !currentSection) {
|
|
69
|
+
sections._root = rootMatch[1].trim().toLowerCase();
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Parse key-value pair
|
|
74
|
+
if (currentSection) {
|
|
75
|
+
const kvMatch = trimmed.match(/^([^=]+)\s*=\s*(.+)$/);
|
|
76
|
+
if (kvMatch) {
|
|
77
|
+
sections[currentSection][kvMatch[1].trim().toLowerCase()] = kvMatch[2].trim().toLowerCase();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return sections;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function validateEditorconfig(sections) {
|
|
86
|
+
const mismatches = [];
|
|
87
|
+
|
|
88
|
+
// Check root
|
|
89
|
+
if (sections._root !== 'true') {
|
|
90
|
+
mismatches.push({ section: '_root', expected: 'true', actual: sections._root || 'missing' });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check each required section
|
|
94
|
+
for (const [sectionName, config] of Object.entries(REQUIRED_SECTIONS)) {
|
|
95
|
+
if (sectionName === 'root') continue;
|
|
96
|
+
|
|
97
|
+
if (!sections[sectionName]) {
|
|
98
|
+
mismatches.push({ section: `[${sectionName}]`, expected: 'present', actual: 'missing' });
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (config.keys) {
|
|
103
|
+
for (const [key, expectedValue] of Object.entries(config.keys)) {
|
|
104
|
+
const actualValue = sections[sectionName][key];
|
|
105
|
+
if (actualValue !== expectedValue) {
|
|
106
|
+
mismatches.push({
|
|
107
|
+
section: `[${sectionName}]`,
|
|
108
|
+
key,
|
|
109
|
+
expected: expectedValue,
|
|
110
|
+
actual: actualValue || 'missing',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return mismatches;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildUpdatedContent(existingContent) {
|
|
121
|
+
const existingSections = parseEditorconfig(existingContent);
|
|
122
|
+
const lines = [];
|
|
123
|
+
|
|
124
|
+
// Add root if missing
|
|
125
|
+
if (existingSections._root !== 'true') {
|
|
126
|
+
lines.push('root = true');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Process each required section
|
|
130
|
+
for (const [sectionName, config] of Object.entries(REQUIRED_SECTIONS)) {
|
|
131
|
+
if (sectionName === 'root') continue;
|
|
132
|
+
|
|
133
|
+
const existingSection = existingSections[sectionName] || {};
|
|
134
|
+
const missingKeys = [];
|
|
135
|
+
|
|
136
|
+
if (config.keys) {
|
|
137
|
+
for (const [key, expectedValue] of Object.entries(config.keys)) {
|
|
138
|
+
if (existingSection[key] !== expectedValue) {
|
|
139
|
+
missingKeys.push({ key, value: expectedValue });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (missingKeys.length > 0 || !existingSections[sectionName]) {
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push(`[${sectionName}]`);
|
|
147
|
+
for (const { key, value } of missingKeys) {
|
|
148
|
+
lines.push(`${key} = ${value}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// If no updates needed, return original
|
|
154
|
+
// c8 ignore next 3
|
|
155
|
+
if (lines.length === 0) {
|
|
156
|
+
return existingContent;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Append missing entries to existing content
|
|
160
|
+
return existingContent.trimEnd() + lines.join('\n') + '\n';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readFileIfExists(filePath) {
|
|
164
|
+
if (!fs.existsSync(filePath)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function runSyncEditorconfig(options) {
|
|
172
|
+
const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
|
|
173
|
+
const check = hasFlag(options, '--check');
|
|
174
|
+
const dryRun = hasFlag(options, '--dry-run') && !check;
|
|
175
|
+
const jsonFile = getSingle(options, '--json', '');
|
|
176
|
+
const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
|
|
177
|
+
|
|
178
|
+
if (!fs.existsSync(cwd)) {
|
|
179
|
+
console.error(`CWD does not exist: ${cwd}`);
|
|
180
|
+
process.exit(2);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const editorconfigPath = path.resolve(cwd, EDITORCONFIG_FILE);
|
|
184
|
+
const currentContent = readFileIfExists(editorconfigPath);
|
|
185
|
+
const fileExists = currentContent !== null;
|
|
186
|
+
|
|
187
|
+
const sections = currentContent ? parseEditorconfig(currentContent) : {};
|
|
188
|
+
const mismatches = validateEditorconfig(sections);
|
|
189
|
+
const requiresUpdate = mismatches.length > 0 || !fileExists;
|
|
190
|
+
|
|
191
|
+
let plannedContent = null;
|
|
192
|
+
if (requiresUpdate) {
|
|
193
|
+
plannedContent = fileExists
|
|
194
|
+
? buildUpdatedContent(currentContent)
|
|
195
|
+
: REQUIRED_EDITORCONFIG_CONTENT;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (mode === 'sync' && requiresUpdate && plannedContent) {
|
|
199
|
+
fs.writeFileSync(editorconfigPath, plannedContent, 'utf8');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const report = {
|
|
203
|
+
cwd,
|
|
204
|
+
mode,
|
|
205
|
+
ok: true,
|
|
206
|
+
editorconfigPath,
|
|
207
|
+
required: {
|
|
208
|
+
file: EDITORCONFIG_FILE,
|
|
209
|
+
},
|
|
210
|
+
status: {
|
|
211
|
+
fileExistsBefore: fileExists,
|
|
212
|
+
mismatchesBefore: mismatches,
|
|
213
|
+
fileExistsAfter: requiresUpdate && mode === 'sync' ? true : fileExists,
|
|
214
|
+
mismatchesAfter: requiresUpdate && mode === 'sync' ? [] : mismatches,
|
|
215
|
+
updated: requiresUpdate && mode === 'sync',
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (mode === 'check' && requiresUpdate) {
|
|
220
|
+
report.ok = false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (jsonFile) {
|
|
224
|
+
const outPath = path.resolve(cwd, jsonFile);
|
|
225
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
226
|
+
fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
230
|
+
if (!report.ok) {
|
|
231
|
+
process.exit(2);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Usage:
|
|
2
|
+
agent-toolkit sync-eslint-config [--cwd <dir>] [--check] [--dry-run]
|
|
3
|
+
[--json <file>]
|
|
4
|
+
|
|
5
|
+
Behavior:
|
|
6
|
+
- Applies organization-required root lint script:
|
|
7
|
+
- scripts.produck:lint = npm exec -- eslint --fix . --max-warnings=0 && npm run lint --if-present
|
|
8
|
+
- Applies organization-required root ESLint config file:
|
|
9
|
+
- eslint.config.mjs
|
|
10
|
+
- If eslint.config.mjs exists and does not use @produck/eslint-rules,
|
|
11
|
+
append Produck integration at the tail of export default array
|
|
12
|
+
- Applies organization-required root managed devDependency:
|
|
13
|
+
- devDependencies.@produck/eslint-rules = <latest-version-resolved-at-runtime>
|
|
14
|
+
|
|
15
|
+
Rules:
|
|
16
|
+
- --check validates without writing and exits non-zero on mismatch
|
|
17
|
+
- --dry-run prints planned changes without writing
|
|
18
|
+
- --check takes precedence over --dry-run
|
|
@@ -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
|