@produck/agent-toolkit 0.6.0 → 0.8.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 +54 -5
- 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 +4 -2
- package/bin/command/sync-format/index.mjs +222 -0
- 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-prettier-config → sync-install}/index.mjs +26 -50
- package/bin/command/sync-instructions/index.mjs +2 -22
- 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/00-produck-base.instructions.md +44 -58
- package/publish-assets/instructions/produck/10-produck-node.instructions.md +59 -82
- package/publish-assets/instructions/produck/12-produck-test.instructions.md +94 -0
- package/publish-assets/instructions/produck/15-produck-workspace.instructions.md +17 -50
- package/publish-assets/instructions/produck/20-produck-commit.instructions.md +2 -2
- package/publish-assets/instructions/produck/tooling-version-baseline.json +14 -2
- package/publish-assets/prettierignore +2 -0
- 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
|
|
@@ -7,27 +7,11 @@ import { printTextResource } from '../shared/text-resource.mjs';
|
|
|
7
7
|
|
|
8
8
|
const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
9
|
const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
|
|
10
|
-
const
|
|
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';
|
|
11
13
|
|
|
12
|
-
|
|
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() {
|
|
14
|
+
export function printSyncInstallHelp() {
|
|
31
15
|
printTextResource(HELP_FILE);
|
|
32
16
|
}
|
|
33
17
|
|
|
@@ -40,15 +24,7 @@ function parseJsonFile(filePath, label) {
|
|
|
40
24
|
}
|
|
41
25
|
}
|
|
42
26
|
|
|
43
|
-
function
|
|
44
|
-
if (!fs.existsSync(filePath)) {
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function runSyncPrettierConfig(options) {
|
|
27
|
+
export function runSyncInstall(options) {
|
|
52
28
|
const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
|
|
53
29
|
const check = hasFlag(options, '--check');
|
|
54
30
|
const dryRun = hasFlag(options, '--dry-run') && !check;
|
|
@@ -72,25 +48,24 @@ export function runSyncPrettierConfig(options) {
|
|
|
72
48
|
? { ...pkg.scripts }
|
|
73
49
|
: {};
|
|
74
50
|
|
|
75
|
-
const
|
|
76
|
-
typeof scripts[
|
|
77
|
-
? scripts[
|
|
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]
|
|
78
58
|
: null;
|
|
79
59
|
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const matchesRequiredFormat = previousFormat === REQUIRED_FORMAT_SCRIPT_VALUE;
|
|
84
|
-
const matchesRequiredPrettierConfig = previousPrettierConfig === REQUIRED_PRETTIER_CONFIG;
|
|
85
|
-
|
|
86
|
-
const requiresUpdate = !matchesRequiredFormat || !matchesRequiredPrettierConfig;
|
|
60
|
+
const matchesRequiredInstall = previousInstall === REQUIRED_INSTALL_SCRIPT_VALUE;
|
|
61
|
+
const legacyInstallScriptPresent = previousLegacyInstall !== null;
|
|
62
|
+
const requiresUpdate = !matchesRequiredInstall || legacyInstallScriptPresent;
|
|
87
63
|
|
|
88
64
|
if (mode === 'sync' && requiresUpdate) {
|
|
89
|
-
scripts[
|
|
65
|
+
delete scripts[LEGACY_INSTALL_SCRIPT_KEY];
|
|
66
|
+
scripts[REQUIRED_INSTALL_SCRIPT_KEY] = REQUIRED_INSTALL_SCRIPT_VALUE;
|
|
90
67
|
pkg.scripts = scripts;
|
|
91
|
-
|
|
92
68
|
fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
|
|
93
|
-
fs.writeFileSync(prettierConfigPath, REQUIRED_PRETTIER_CONFIG, 'utf8');
|
|
94
69
|
}
|
|
95
70
|
|
|
96
71
|
const report = {
|
|
@@ -99,16 +74,17 @@ export function runSyncPrettierConfig(options) {
|
|
|
99
74
|
ok: true,
|
|
100
75
|
rootPackageJsonPath,
|
|
101
76
|
required: {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
77
|
+
installScriptKey: REQUIRED_INSTALL_SCRIPT_KEY,
|
|
78
|
+
installScriptValue: REQUIRED_INSTALL_SCRIPT_VALUE,
|
|
79
|
+
legacyInstallScriptKey: LEGACY_INSTALL_SCRIPT_KEY,
|
|
105
80
|
},
|
|
106
81
|
status: {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
82
|
+
matchesRequiredInstallBefore: matchesRequiredInstall,
|
|
83
|
+
legacyInstallScriptPresentBefore: legacyInstallScriptPresent,
|
|
84
|
+
matchesRequiredInstallAfter:
|
|
85
|
+
requiresUpdate && mode === 'sync' ? true : matchesRequiredInstall,
|
|
86
|
+
legacyInstallScriptPresentAfter:
|
|
87
|
+
requiresUpdate && mode === 'sync' ? false : legacyInstallScriptPresent,
|
|
112
88
|
updated: requiresUpdate && mode === 'sync',
|
|
113
89
|
},
|
|
114
90
|
};
|
|
@@ -88,7 +88,7 @@ export function runSyncInstructions(options) {
|
|
|
88
88
|
const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
|
|
89
89
|
const outArg = getSingle(options, '--out', DEFAULT_NAMESPACE_OUT_DIR);
|
|
90
90
|
const sourceArg = getSingle(options, '--source', '');
|
|
91
|
-
const force =
|
|
91
|
+
const force = true;
|
|
92
92
|
const dryRun = hasFlag(options, '--dry-run');
|
|
93
93
|
const prune = hasFlag(options, '--prune');
|
|
94
94
|
|
|
@@ -146,15 +146,6 @@ export function runSyncInstructions(options) {
|
|
|
146
146
|
if (outLooksLikeFile) {
|
|
147
147
|
const entry = entries[0];
|
|
148
148
|
const exists = fs.existsSync(outPath);
|
|
149
|
-
if (exists && !force) {
|
|
150
|
-
const current = fs.readFileSync(outPath, 'utf8');
|
|
151
|
-
if (current !== entry.content) {
|
|
152
|
-
console.error(`Target already exists: ${outPath}`);
|
|
153
|
-
console.error('Use --force to overwrite.');
|
|
154
|
-
process.exit(2);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
149
|
const report = {
|
|
159
150
|
mode: 'single-file',
|
|
160
151
|
cwd,
|
|
@@ -162,18 +153,16 @@ export function runSyncInstructions(options) {
|
|
|
162
153
|
source: sourceResolved,
|
|
163
154
|
outPath,
|
|
164
155
|
exists,
|
|
165
|
-
overwritten: exists
|
|
156
|
+
overwritten: exists,
|
|
166
157
|
dryRun,
|
|
167
158
|
prune: false,
|
|
168
159
|
initializedUserSpaceEntry: false,
|
|
169
160
|
userSpaceEntryPath: null,
|
|
170
161
|
};
|
|
171
|
-
|
|
172
162
|
if (dryRun) {
|
|
173
163
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
174
164
|
process.exit(0);
|
|
175
165
|
}
|
|
176
|
-
|
|
177
166
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
178
167
|
fs.writeFileSync(outPath, entry.content, 'utf8');
|
|
179
168
|
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
@@ -202,15 +191,6 @@ export function runSyncInstructions(options) {
|
|
|
202
191
|
}
|
|
203
192
|
|
|
204
193
|
const toWrite = planned.filter((item) => !unchanged.includes(item.targetPath));
|
|
205
|
-
const conflicts = toWrite.filter((item) => fs.existsSync(item.targetPath));
|
|
206
|
-
if (conflicts.length > 0 && !force) {
|
|
207
|
-
console.error('Some target files already exist and would change:');
|
|
208
|
-
for (const item of conflicts) {
|
|
209
|
-
console.error(`- ${item.targetPath}`);
|
|
210
|
-
}
|
|
211
|
-
console.error('Use --force to overwrite.');
|
|
212
|
-
process.exit(2);
|
|
213
|
-
}
|
|
214
194
|
|
|
215
195
|
const pruneDeletes = [];
|
|
216
196
|
if (prune && fs.existsSync(outDir)) {
|
|
@@ -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
|