@produck/agent-toolkit 0.6.0 → 0.7.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 +74 -43
- package/bin/agent-toolkit.mjs +26 -33
- package/bin/build-publish-assets.mjs +28 -0
- package/bin/command/enforce-node-baseline/help.txt +12 -10
- package/bin/command/enforce-node-baseline/index.mjs +23 -15
- package/bin/command/main/help.txt +6 -5
- package/bin/command/preflight/help.txt +1 -1
- package/bin/command/preflight/index.mjs +1 -1
- package/bin/command/{sync-coverage-script → sync-coverage}/help.txt +2 -1
- package/bin/command/{sync-coverage-script → sync-coverage}/index.mjs +116 -19
- package/bin/command/sync-editorconfig/index.mjs +10 -153
- package/bin/command/{sync-prettier-config → sync-format}/help.txt +2 -2
- package/bin/command/{sync-prettier-config → sync-format}/index.mjs +63 -9
- package/bin/command/{sync-workspace-config → sync-git}/help.txt +10 -6
- package/bin/command/sync-git/index.mjs +424 -0
- package/bin/command/sync-install/help.txt +14 -0
- package/bin/command/sync-install/index.mjs +106 -0
- package/bin/command/{sync-eslint-config → sync-lint}/help.txt +2 -2
- package/bin/command/{sync-eslint-config → sync-lint}/index.mjs +3 -4
- package/bin/command/sync-publish/help.txt +18 -0
- package/bin/command/sync-publish/index.mjs +157 -0
- package/bin/command/validate-commit-msg/index.mjs +30 -2
- package/package.json +3 -5
- package/publish-assets/gitattributes +5 -0
- package/publish-assets/gitignore +137 -0
- package/publish-assets/instructions/produck/10-produck-node.instructions.md +53 -40
- package/publish-assets/instructions/produck/15-produck-workspace.instructions.md +13 -16
- package/publish-assets/instructions/produck/20-produck-commit.instructions.md +2 -2
- package/publish-assets/instructions/produck/tooling-version-baseline.json +8 -1
- package/bin/command/sync-husky-hooks/help.txt +0 -14
- package/bin/command/sync-husky-hooks/index.mjs +0 -89
- package/bin/command/sync-workspace-config/index.mjs +0 -290
|
@@ -0,0 +1,424 @@
|
|
|
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 GITATTRIBUTES_FILE = '.gitattributes';
|
|
21
|
+
const GITIGNORE_FILE = '.gitignore';
|
|
22
|
+
const HUSKY_DIR = '.husky';
|
|
23
|
+
const PRE_COMMIT_HOOK_FILE = 'pre-commit';
|
|
24
|
+
const COMMIT_MSG_HOOK_FILE = 'commit-msg';
|
|
25
|
+
const REQUIRED_BASELINE_SCRIPT_KEY = 'produck:baseline';
|
|
26
|
+
const REQUIRED_BASELINE_SCRIPT_VALUE =
|
|
27
|
+
'npm exec --package=@produck/agent-toolkit@latest -- agent-toolkit enforce-node-baseline --cwd .';
|
|
28
|
+
const REQUIRED_COMMIT_CHECK_SCRIPT_KEY = 'produck:commit:check';
|
|
29
|
+
const REQUIRED_COMMIT_CHECK_SCRIPT_VALUE = 'npm run produck:format && npm run produck:lint';
|
|
30
|
+
|
|
31
|
+
const GITATTRIBUTES_SOURCE_CANDIDATE_PATHS = [
|
|
32
|
+
path.resolve(REPO_ROOT, '.gitattributes'),
|
|
33
|
+
path.resolve(PACKAGE_ROOT, 'publish-assets/gitattributes'),
|
|
34
|
+
];
|
|
35
|
+
const GITIGNORE_SOURCE_CANDIDATE_PATHS = [
|
|
36
|
+
path.resolve(REPO_ROOT, '.gitignore'),
|
|
37
|
+
path.resolve(PACKAGE_ROOT, 'publish-assets/gitignore'),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const REQUIRED_PRE_COMMIT_HOOK = '#!/usr/bin/env sh\nnpm run produck:commit:check\n';
|
|
41
|
+
const REQUIRED_COMMIT_MSG_HOOK =
|
|
42
|
+
'#!/usr/bin/env sh\nnode ./node_modules/@produck/agent-toolkit/bin/agent-toolkit.mjs validate-commit-msg --file "$1"\n';
|
|
43
|
+
|
|
44
|
+
export function printSyncGitHelp() {
|
|
45
|
+
printTextResource(HELP_FILE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseJsonFile(filePath, label) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
51
|
+
} catch {
|
|
52
|
+
console.error(`${label} is not valid JSON: ${filePath}`);
|
|
53
|
+
process.exit(2);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getRequiredToolkitDevDependency() {
|
|
58
|
+
const overrideVersion = String(process.env.PRODUCK_TOOLKIT_VERSION_OVERRIDE || '').trim();
|
|
59
|
+
if (overrideVersion) {
|
|
60
|
+
return overrideVersion;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
64
|
+
const latestResult = spawnSync(npmCommand, ['view', '@produck/agent-toolkit', 'version'], {
|
|
65
|
+
encoding: 'utf8',
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const latestVersion = String(latestResult.stdout || '').trim();
|
|
69
|
+
if (latestResult.status === 0 && latestVersion) {
|
|
70
|
+
return latestVersion;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const pkg = parseJsonFile(TOOLKIT_PACKAGE_JSON, 'Toolkit package.json');
|
|
74
|
+
const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
|
|
75
|
+
|
|
76
|
+
if (!version) {
|
|
77
|
+
console.error(`Toolkit package version is missing: ${TOOLKIT_PACKAGE_JSON}`);
|
|
78
|
+
process.exit(2);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return version;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadToolingBaseline() {
|
|
85
|
+
const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
|
|
86
|
+
return fs.existsSync(candidatePath);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!toolingBaselinePath) {
|
|
90
|
+
console.error('Tooling baseline file does not exist in expected locations:');
|
|
91
|
+
for (const candidatePath of TOOLING_BASELINE_CANDIDATE_PATHS) {
|
|
92
|
+
console.error(`- ${candidatePath}`);
|
|
93
|
+
}
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
|
|
98
|
+
const huskyVersion = String(baseline?.tools?.husky?.version || '').trim();
|
|
99
|
+
const lernaVersion = String(baseline?.tools?.lerna?.version || '').trim();
|
|
100
|
+
|
|
101
|
+
if (!huskyVersion || !lernaVersion) {
|
|
102
|
+
console.error(
|
|
103
|
+
`Tooling baseline must define fixed tools.husky/lerna.version: ${toolingBaselinePath}`,
|
|
104
|
+
);
|
|
105
|
+
process.exit(2);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
toolingBaselinePath,
|
|
110
|
+
huskyVersion,
|
|
111
|
+
lernaVersion,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readFileIfExists(filePath) {
|
|
116
|
+
if (!fs.existsSync(filePath)) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function parseGitignoreEntries(text) {
|
|
124
|
+
return text
|
|
125
|
+
.split('\n')
|
|
126
|
+
.map((line) => line.trimEnd())
|
|
127
|
+
.filter((line) => line.length > 0 && !line.startsWith('#'));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findMissingGitignoreEntries(currentContent, requiredEntries) {
|
|
131
|
+
if (currentContent === null) {
|
|
132
|
+
return [...requiredEntries];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const existingLines = new Set(currentContent.split('\n').map((line) => line.trimEnd()));
|
|
136
|
+
|
|
137
|
+
return requiredEntries.filter((entry) => !existingLines.has(entry));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function loadGitSourceFiles() {
|
|
141
|
+
const gitattributesSourcePath = GITATTRIBUTES_SOURCE_CANDIDATE_PATHS.find((p) =>
|
|
142
|
+
fs.existsSync(p),
|
|
143
|
+
);
|
|
144
|
+
const gitignoreSourcePath = GITIGNORE_SOURCE_CANDIDATE_PATHS.find((p) => fs.existsSync(p));
|
|
145
|
+
|
|
146
|
+
if (!gitattributesSourcePath) {
|
|
147
|
+
console.error('Org .gitattributes source not found in expected locations:');
|
|
148
|
+
for (const p of GITATTRIBUTES_SOURCE_CANDIDATE_PATHS) {
|
|
149
|
+
console.error(`- ${p}`);
|
|
150
|
+
}
|
|
151
|
+
process.exit(2);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!gitignoreSourcePath) {
|
|
155
|
+
console.error('Org .gitignore source not found in expected locations:');
|
|
156
|
+
for (const p of GITIGNORE_SOURCE_CANDIDATE_PATHS) {
|
|
157
|
+
console.error(`- ${p}`);
|
|
158
|
+
}
|
|
159
|
+
process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const gitattributesContent = fs.readFileSync(gitattributesSourcePath, 'utf8');
|
|
163
|
+
const gitignoreContent = fs.readFileSync(gitignoreSourcePath, 'utf8');
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
gitattributesSourcePath,
|
|
167
|
+
gitignoreSourcePath,
|
|
168
|
+
gitattributesContent,
|
|
169
|
+
gitignoreOrgContent: gitignoreContent,
|
|
170
|
+
gitignoreRequiredEntries: parseGitignoreEntries(gitignoreContent),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function buildScriptState(pkg) {
|
|
175
|
+
const scripts =
|
|
176
|
+
pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
|
|
177
|
+
? { ...pkg.scripts }
|
|
178
|
+
: {};
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
scripts,
|
|
182
|
+
previousBaseline:
|
|
183
|
+
typeof scripts[REQUIRED_BASELINE_SCRIPT_KEY] === 'string'
|
|
184
|
+
? scripts[REQUIRED_BASELINE_SCRIPT_KEY]
|
|
185
|
+
: null,
|
|
186
|
+
previousCommitCheck:
|
|
187
|
+
typeof scripts[REQUIRED_COMMIT_CHECK_SCRIPT_KEY] === 'string'
|
|
188
|
+
? scripts[REQUIRED_COMMIT_CHECK_SCRIPT_KEY]
|
|
189
|
+
: null,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function buildDevDependencyState(pkg) {
|
|
194
|
+
const devDependencies =
|
|
195
|
+
pkg.devDependencies &&
|
|
196
|
+
typeof pkg.devDependencies === 'object' &&
|
|
197
|
+
!Array.isArray(pkg.devDependencies)
|
|
198
|
+
? { ...pkg.devDependencies }
|
|
199
|
+
: {};
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
devDependencies,
|
|
203
|
+
previousManaged: {
|
|
204
|
+
husky: typeof devDependencies.husky === 'string' ? devDependencies.husky : null,
|
|
205
|
+
lerna: typeof devDependencies.lerna === 'string' ? devDependencies.lerna : null,
|
|
206
|
+
'@produck/agent-toolkit':
|
|
207
|
+
typeof devDependencies['@produck/agent-toolkit'] === 'string'
|
|
208
|
+
? devDependencies['@produck/agent-toolkit']
|
|
209
|
+
: null,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function runSyncGit(options) {
|
|
215
|
+
const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
|
|
216
|
+
const check = hasFlag(options, '--check');
|
|
217
|
+
const dryRun = hasFlag(options, '--dry-run') && !check;
|
|
218
|
+
const jsonFile = getSingle(options, '--json', '');
|
|
219
|
+
const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
|
|
220
|
+
|
|
221
|
+
if (!fs.existsSync(cwd)) {
|
|
222
|
+
console.error(`CWD does not exist: ${cwd}`);
|
|
223
|
+
process.exit(2);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const rootPackageJsonPath = path.resolve(cwd, 'package.json');
|
|
227
|
+
if (!fs.existsSync(rootPackageJsonPath)) {
|
|
228
|
+
console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
|
|
229
|
+
process.exit(2);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
|
|
233
|
+
const toolingBaseline = loadToolingBaseline();
|
|
234
|
+
const gitSources = loadGitSourceFiles();
|
|
235
|
+
const requiredGitAttributesContent = gitSources.gitattributesContent;
|
|
236
|
+
const gitignoreRequiredEntries = gitSources.gitignoreRequiredEntries;
|
|
237
|
+
const gitignoreOrgContent = gitSources.gitignoreOrgContent;
|
|
238
|
+
const requiredToolkitDependency = getRequiredToolkitDevDependency();
|
|
239
|
+
const requiredDevDependencies = {
|
|
240
|
+
husky: toolingBaseline.huskyVersion,
|
|
241
|
+
lerna: toolingBaseline.lernaVersion,
|
|
242
|
+
'@produck/agent-toolkit': requiredToolkitDependency,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const scriptState = buildScriptState(pkg);
|
|
246
|
+
const dependencyState = buildDevDependencyState(pkg);
|
|
247
|
+
const scriptValidation = validateRequiredExactEntries(scriptState.scripts, {
|
|
248
|
+
[REQUIRED_BASELINE_SCRIPT_KEY]: REQUIRED_BASELINE_SCRIPT_VALUE,
|
|
249
|
+
[REQUIRED_COMMIT_CHECK_SCRIPT_KEY]: REQUIRED_COMMIT_CHECK_SCRIPT_VALUE,
|
|
250
|
+
});
|
|
251
|
+
const dependencyValidation = validateRequiredExactEntries(
|
|
252
|
+
dependencyState.devDependencies,
|
|
253
|
+
requiredDevDependencies,
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const matchesRequiredBaseline = !(REQUIRED_BASELINE_SCRIPT_KEY in scriptValidation.mismatches);
|
|
257
|
+
const matchesRequiredCommitCheck = !(
|
|
258
|
+
REQUIRED_COMMIT_CHECK_SCRIPT_KEY in scriptValidation.mismatches
|
|
259
|
+
);
|
|
260
|
+
const matchesRequiredManagedDevDependencies = dependencyValidation.ok;
|
|
261
|
+
|
|
262
|
+
const gitAttributesPath = path.resolve(cwd, GITATTRIBUTES_FILE);
|
|
263
|
+
const gitignorePath = path.resolve(cwd, GITIGNORE_FILE);
|
|
264
|
+
const huskyDir = path.resolve(cwd, HUSKY_DIR);
|
|
265
|
+
const preCommitHookPath = path.resolve(huskyDir, PRE_COMMIT_HOOK_FILE);
|
|
266
|
+
const commitMsgHookPath = path.resolve(huskyDir, COMMIT_MSG_HOOK_FILE);
|
|
267
|
+
const currentContent = readFileIfExists(gitAttributesPath);
|
|
268
|
+
const currentGitignoreContent = readFileIfExists(gitignorePath);
|
|
269
|
+
const currentPreCommitHook = readFileIfExists(preCommitHookPath);
|
|
270
|
+
const currentCommitMsgHook = readFileIfExists(commitMsgHookPath);
|
|
271
|
+
const fileExists = currentContent !== null;
|
|
272
|
+
const gitignoreExists = currentGitignoreContent !== null;
|
|
273
|
+
const preCommitHookExists = currentPreCommitHook !== null;
|
|
274
|
+
const commitMsgHookExists = currentCommitMsgHook !== null;
|
|
275
|
+
const matchesRequiredGitAttributes = currentContent === requiredGitAttributesContent;
|
|
276
|
+
const missingGitignoreEntries = findMissingGitignoreEntries(
|
|
277
|
+
currentGitignoreContent,
|
|
278
|
+
gitignoreRequiredEntries,
|
|
279
|
+
);
|
|
280
|
+
const matchesRequiredGitignore = missingGitignoreEntries.length === 0;
|
|
281
|
+
const matchesRequiredPreCommitHook = currentPreCommitHook === REQUIRED_PRE_COMMIT_HOOK;
|
|
282
|
+
const matchesRequiredCommitMsgHook = currentCommitMsgHook === REQUIRED_COMMIT_MSG_HOOK;
|
|
283
|
+
|
|
284
|
+
const mismatches = [];
|
|
285
|
+
if (!matchesRequiredGitAttributes) {
|
|
286
|
+
mismatches.push({
|
|
287
|
+
file: GITATTRIBUTES_FILE,
|
|
288
|
+
expected: 'exact required content',
|
|
289
|
+
actual: fileExists ? 'different content' : 'missing',
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
if (!matchesRequiredGitignore) {
|
|
293
|
+
mismatches.push({
|
|
294
|
+
file: GITIGNORE_FILE,
|
|
295
|
+
expected: 'all required org-baseline entries present',
|
|
296
|
+
actual: gitignoreExists
|
|
297
|
+
? `missing ${missingGitignoreEntries.length} required entries`
|
|
298
|
+
: 'missing',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
if (!matchesRequiredPreCommitHook) {
|
|
302
|
+
mismatches.push({
|
|
303
|
+
file: `${HUSKY_DIR}/${PRE_COMMIT_HOOK_FILE}`,
|
|
304
|
+
expected: 'exact required content',
|
|
305
|
+
actual: preCommitHookExists ? 'different content' : 'missing',
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
if (!matchesRequiredCommitMsgHook) {
|
|
309
|
+
mismatches.push({
|
|
310
|
+
file: `${HUSKY_DIR}/${COMMIT_MSG_HOOK_FILE}`,
|
|
311
|
+
expected: 'exact required content',
|
|
312
|
+
actual: commitMsgHookExists ? 'different content' : 'missing',
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const requiresUpdate =
|
|
317
|
+
mismatches.length > 0 ||
|
|
318
|
+
!matchesRequiredBaseline ||
|
|
319
|
+
!matchesRequiredCommitCheck ||
|
|
320
|
+
!matchesRequiredManagedDevDependencies;
|
|
321
|
+
|
|
322
|
+
if (mode === 'sync' && requiresUpdate) {
|
|
323
|
+
fs.mkdirSync(huskyDir, { recursive: true });
|
|
324
|
+
fs.writeFileSync(gitAttributesPath, requiredGitAttributesContent, 'utf8');
|
|
325
|
+
|
|
326
|
+
if (!matchesRequiredGitignore) {
|
|
327
|
+
if (currentGitignoreContent === null) {
|
|
328
|
+
fs.writeFileSync(gitignorePath, gitignoreOrgContent, 'utf8');
|
|
329
|
+
} else {
|
|
330
|
+
const appendText = `\n# produck:org-baseline\n${missingGitignoreEntries.join('\n')}\n`;
|
|
331
|
+
fs.writeFileSync(gitignorePath, currentGitignoreContent + appendText, 'utf8');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fs.writeFileSync(preCommitHookPath, REQUIRED_PRE_COMMIT_HOOK, 'utf8');
|
|
336
|
+
fs.writeFileSync(commitMsgHookPath, REQUIRED_COMMIT_MSG_HOOK, 'utf8');
|
|
337
|
+
|
|
338
|
+
scriptState.scripts[REQUIRED_BASELINE_SCRIPT_KEY] = REQUIRED_BASELINE_SCRIPT_VALUE;
|
|
339
|
+
scriptState.scripts[REQUIRED_COMMIT_CHECK_SCRIPT_KEY] = REQUIRED_COMMIT_CHECK_SCRIPT_VALUE;
|
|
340
|
+
pkg.scripts = scriptState.scripts;
|
|
341
|
+
|
|
342
|
+
for (const [name, version] of Object.entries(requiredDevDependencies)) {
|
|
343
|
+
dependencyState.devDependencies[name] = version;
|
|
344
|
+
}
|
|
345
|
+
pkg.devDependencies = dependencyState.devDependencies;
|
|
346
|
+
|
|
347
|
+
fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const report = {
|
|
351
|
+
cwd,
|
|
352
|
+
mode,
|
|
353
|
+
ok: true,
|
|
354
|
+
rootPackageJsonPath,
|
|
355
|
+
toolingBaselinePath: toolingBaseline.toolingBaselinePath,
|
|
356
|
+
required: {
|
|
357
|
+
file: GITATTRIBUTES_FILE,
|
|
358
|
+
gitattributesSourcePath: gitSources.gitattributesSourcePath,
|
|
359
|
+
content: requiredGitAttributesContent,
|
|
360
|
+
gitignoreFile: GITIGNORE_FILE,
|
|
361
|
+
gitignoreSourcePath: gitSources.gitignoreSourcePath,
|
|
362
|
+
gitignoreRequiredEntries,
|
|
363
|
+
baselineScriptKey: REQUIRED_BASELINE_SCRIPT_KEY,
|
|
364
|
+
baselineScriptValue: REQUIRED_BASELINE_SCRIPT_VALUE,
|
|
365
|
+
commitCheckScriptKey: REQUIRED_COMMIT_CHECK_SCRIPT_KEY,
|
|
366
|
+
commitCheckScriptValue: REQUIRED_COMMIT_CHECK_SCRIPT_VALUE,
|
|
367
|
+
preCommitHookPath: path.relative(cwd, preCommitHookPath),
|
|
368
|
+
commitMsgHookPath: path.relative(cwd, commitMsgHookPath),
|
|
369
|
+
managedDevDependencies: requiredDevDependencies,
|
|
370
|
+
},
|
|
371
|
+
status: {
|
|
372
|
+
fileExistsBefore: fileExists,
|
|
373
|
+
gitignoreExistsBefore: gitignoreExists,
|
|
374
|
+
preCommitHookExistsBefore: preCommitHookExists,
|
|
375
|
+
commitMsgHookExistsBefore: commitMsgHookExists,
|
|
376
|
+
matchesRequiredGitAttributesBefore: matchesRequiredGitAttributes,
|
|
377
|
+
matchesRequiredGitignoreBefore: matchesRequiredGitignore,
|
|
378
|
+
missingGitignoreEntriesBefore: missingGitignoreEntries,
|
|
379
|
+
matchesRequiredPreCommitHookBefore: matchesRequiredPreCommitHook,
|
|
380
|
+
matchesRequiredCommitMsgHookBefore: matchesRequiredCommitMsgHook,
|
|
381
|
+
matchesRequiredBaselineBefore: matchesRequiredBaseline,
|
|
382
|
+
matchesRequiredCommitCheckBefore: matchesRequiredCommitCheck,
|
|
383
|
+
matchesRequiredManagedDevDependenciesBefore: matchesRequiredManagedDevDependencies,
|
|
384
|
+
mismatchesBefore: mismatches,
|
|
385
|
+
fileExistsAfter: requiresUpdate && mode === 'sync' ? true : fileExists,
|
|
386
|
+
gitignoreExistsAfter: requiresUpdate && mode === 'sync' ? true : gitignoreExists,
|
|
387
|
+
preCommitHookExistsAfter: requiresUpdate && mode === 'sync' ? true : preCommitHookExists,
|
|
388
|
+
commitMsgHookExistsAfter: requiresUpdate && mode === 'sync' ? true : commitMsgHookExists,
|
|
389
|
+
matchesRequiredGitAttributesAfter:
|
|
390
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredGitAttributes,
|
|
391
|
+
matchesRequiredGitignoreAfter:
|
|
392
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredGitignore,
|
|
393
|
+
missingGitignoreEntriesAfter:
|
|
394
|
+
requiresUpdate && mode === 'sync' ? [] : missingGitignoreEntries,
|
|
395
|
+
matchesRequiredPreCommitHookAfter:
|
|
396
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredPreCommitHook,
|
|
397
|
+
matchesRequiredCommitMsgHookAfter:
|
|
398
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredCommitMsgHook,
|
|
399
|
+
matchesRequiredBaselineAfter:
|
|
400
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredBaseline,
|
|
401
|
+
matchesRequiredCommitCheckAfter:
|
|
402
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredCommitCheck,
|
|
403
|
+
matchesRequiredManagedDevDependenciesAfter:
|
|
404
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredManagedDevDependencies,
|
|
405
|
+
mismatchesAfter: requiresUpdate && mode === 'sync' ? [] : mismatches,
|
|
406
|
+
updated: requiresUpdate && mode === 'sync',
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
if (mode === 'check' && requiresUpdate) {
|
|
411
|
+
report.ok = false;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (jsonFile) {
|
|
415
|
+
const outPath = path.resolve(cwd, jsonFile);
|
|
416
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
417
|
+
fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
421
|
+
if (!report.ok) {
|
|
422
|
+
process.exit(2);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Usage:
|
|
2
|
+
agent-toolkit sync-install [--cwd <dir>] [--check] [--dry-run]
|
|
3
|
+
[--json <file>]
|
|
4
|
+
|
|
5
|
+
Behavior:
|
|
6
|
+
- Applies organization-required root install script:
|
|
7
|
+
- scripts.produck:install = npm -v && npm install
|
|
8
|
+
- Removes legacy root install script when present:
|
|
9
|
+
- scripts.deps:install
|
|
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,106 @@
|
|
|
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 LEGACY_INSTALL_SCRIPT_KEY = 'deps:install';
|
|
11
|
+
const REQUIRED_INSTALL_SCRIPT_KEY = 'produck:install';
|
|
12
|
+
const REQUIRED_INSTALL_SCRIPT_VALUE = 'npm -v && npm install';
|
|
13
|
+
|
|
14
|
+
export function printSyncInstallHelp() {
|
|
15
|
+
printTextResource(HELP_FILE);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseJsonFile(filePath, label) {
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
21
|
+
} catch {
|
|
22
|
+
console.error(`${label} is not valid JSON: ${filePath}`);
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function runSyncInstall(options) {
|
|
28
|
+
const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
|
|
29
|
+
const check = hasFlag(options, '--check');
|
|
30
|
+
const dryRun = hasFlag(options, '--dry-run') && !check;
|
|
31
|
+
const jsonFile = getSingle(options, '--json', '');
|
|
32
|
+
const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
|
|
33
|
+
|
|
34
|
+
if (!fs.existsSync(cwd)) {
|
|
35
|
+
console.error(`CWD does not exist: ${cwd}`);
|
|
36
|
+
process.exit(2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const rootPackageJsonPath = path.resolve(cwd, 'package.json');
|
|
40
|
+
if (!fs.existsSync(rootPackageJsonPath)) {
|
|
41
|
+
console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
|
|
42
|
+
process.exit(2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
|
|
46
|
+
const scripts =
|
|
47
|
+
pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
|
|
48
|
+
? { ...pkg.scripts }
|
|
49
|
+
: {};
|
|
50
|
+
|
|
51
|
+
const previousInstall =
|
|
52
|
+
typeof scripts[REQUIRED_INSTALL_SCRIPT_KEY] === 'string'
|
|
53
|
+
? scripts[REQUIRED_INSTALL_SCRIPT_KEY]
|
|
54
|
+
: null;
|
|
55
|
+
const previousLegacyInstall =
|
|
56
|
+
typeof scripts[LEGACY_INSTALL_SCRIPT_KEY] === 'string'
|
|
57
|
+
? scripts[LEGACY_INSTALL_SCRIPT_KEY]
|
|
58
|
+
: null;
|
|
59
|
+
|
|
60
|
+
const matchesRequiredInstall = previousInstall === REQUIRED_INSTALL_SCRIPT_VALUE;
|
|
61
|
+
const legacyInstallScriptPresent = previousLegacyInstall !== null;
|
|
62
|
+
const requiresUpdate = !matchesRequiredInstall || legacyInstallScriptPresent;
|
|
63
|
+
|
|
64
|
+
if (mode === 'sync' && requiresUpdate) {
|
|
65
|
+
delete scripts[LEGACY_INSTALL_SCRIPT_KEY];
|
|
66
|
+
scripts[REQUIRED_INSTALL_SCRIPT_KEY] = REQUIRED_INSTALL_SCRIPT_VALUE;
|
|
67
|
+
pkg.scripts = scripts;
|
|
68
|
+
fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const report = {
|
|
72
|
+
cwd,
|
|
73
|
+
mode,
|
|
74
|
+
ok: true,
|
|
75
|
+
rootPackageJsonPath,
|
|
76
|
+
required: {
|
|
77
|
+
installScriptKey: REQUIRED_INSTALL_SCRIPT_KEY,
|
|
78
|
+
installScriptValue: REQUIRED_INSTALL_SCRIPT_VALUE,
|
|
79
|
+
legacyInstallScriptKey: LEGACY_INSTALL_SCRIPT_KEY,
|
|
80
|
+
},
|
|
81
|
+
status: {
|
|
82
|
+
matchesRequiredInstallBefore: matchesRequiredInstall,
|
|
83
|
+
legacyInstallScriptPresentBefore: legacyInstallScriptPresent,
|
|
84
|
+
matchesRequiredInstallAfter:
|
|
85
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredInstall,
|
|
86
|
+
legacyInstallScriptPresentAfter:
|
|
87
|
+
requiresUpdate && mode === 'sync' ? false : legacyInstallScriptPresent,
|
|
88
|
+
updated: requiresUpdate && mode === 'sync',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (mode === 'check' && requiresUpdate) {
|
|
93
|
+
report.ok = false;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (jsonFile) {
|
|
97
|
+
const outPath = path.resolve(cwd, jsonFile);
|
|
98
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
99
|
+
fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
103
|
+
if (!report.ok) {
|
|
104
|
+
process.exit(2);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Usage:
|
|
2
|
-
agent-toolkit sync-
|
|
2
|
+
agent-toolkit sync-lint [--cwd <dir>] [--check] [--dry-run]
|
|
3
3
|
[--json <file>]
|
|
4
4
|
|
|
5
5
|
Behavior:
|
|
6
6
|
- Applies organization-required root lint script:
|
|
7
|
-
- scripts.produck:lint =
|
|
7
|
+
- scripts.produck:lint = eslint --fix . --max-warnings=0
|
|
8
8
|
- Applies organization-required root ESLint config file:
|
|
9
9
|
- eslint.config.mjs
|
|
10
10
|
- If eslint.config.mjs exists and does not use @produck/eslint-rules,
|
|
@@ -13,8 +13,7 @@ const TOOLKIT_PACKAGE_JSON = path.resolve(PACKAGE_ROOT, 'package.json');
|
|
|
13
13
|
const ESLINT_CONFIG_FILE = 'eslint.config.mjs';
|
|
14
14
|
|
|
15
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';
|
|
16
|
+
const REQUIRED_LINT_SCRIPT_VALUE = 'eslint --fix . --max-warnings=0';
|
|
18
17
|
const REQUIRED_ESLINT_CONFIG = `import globals from 'globals';
|
|
19
18
|
import pluginJs from '@eslint/js';
|
|
20
19
|
import tseslint from 'typescript-eslint';
|
|
@@ -30,7 +29,7 @@ export default [
|
|
|
30
29
|
];
|
|
31
30
|
`;
|
|
32
31
|
|
|
33
|
-
export function
|
|
32
|
+
export function printSyncLintHelp() {
|
|
34
33
|
printTextResource(HELP_FILE);
|
|
35
34
|
}
|
|
36
35
|
|
|
@@ -112,7 +111,7 @@ function patchEslintConfig(existing) {
|
|
|
112
111
|
return { ok: true, patched: true, output };
|
|
113
112
|
}
|
|
114
113
|
|
|
115
|
-
export function
|
|
114
|
+
export function runSyncLint(options) {
|
|
116
115
|
const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
|
|
117
116
|
const check = hasFlag(options, '--check');
|
|
118
117
|
const dryRun = hasFlag(options, '--dry-run') && !check;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Usage:
|
|
2
|
+
agent-toolkit sync-publish [--cwd <dir>] [--check] [--dry-run]
|
|
3
|
+
[--json <file>]
|
|
4
|
+
|
|
5
|
+
Behavior:
|
|
6
|
+
|
|
7
|
+
- Reads lerna.json in <dir> to detect monorepo publish mode
|
|
8
|
+
- If lerna.json is absent, sync mode creates a default lerna.json
|
|
9
|
+
- Sync mode applies organization-required root publish scripts:
|
|
10
|
+
- scripts.produck:publish:check = npm run produck:format && npm run produck:lint && npm run produck:coverage
|
|
11
|
+
- scripts.produck:publish = npm run produck:publish:check && npm run publish --
|
|
12
|
+
when scripts.publish exists; otherwise it falls back to lerna publish
|
|
13
|
+
|
|
14
|
+
Rules:
|
|
15
|
+
|
|
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
|