@produck/agent-toolkit 0.5.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.
Files changed (34) hide show
  1. package/README.md +74 -43
  2. package/bin/agent-toolkit.mjs +33 -32
  3. package/bin/build-publish-assets.mjs +28 -0
  4. package/bin/command/enforce-node-baseline/help.txt +12 -10
  5. package/bin/command/enforce-node-baseline/index.mjs +31 -15
  6. package/bin/command/main/help.txt +7 -5
  7. package/bin/command/preflight/help.txt +1 -1
  8. package/bin/command/preflight/index.mjs +1 -1
  9. package/bin/command/{sync-coverage-script → sync-coverage}/help.txt +2 -1
  10. package/bin/command/{sync-coverage-script → sync-coverage}/index.mjs +116 -19
  11. package/bin/command/sync-editorconfig/editorconfig.template +15 -0
  12. package/bin/command/sync-editorconfig/help.txt +13 -0
  13. package/bin/command/sync-editorconfig/index.mjs +90 -0
  14. package/bin/command/{sync-prettier-config → sync-format}/help.txt +2 -2
  15. package/bin/command/{sync-prettier-config → sync-format}/index.mjs +63 -9
  16. package/bin/command/{sync-workspace-config → sync-git}/help.txt +10 -6
  17. package/bin/command/sync-git/index.mjs +424 -0
  18. package/bin/command/sync-install/help.txt +14 -0
  19. package/bin/command/sync-install/index.mjs +106 -0
  20. package/bin/command/{sync-eslint-config → sync-lint}/help.txt +2 -2
  21. package/bin/command/{sync-eslint-config → sync-lint}/index.mjs +3 -4
  22. package/bin/command/sync-publish/help.txt +18 -0
  23. package/bin/command/sync-publish/index.mjs +157 -0
  24. package/bin/command/validate-commit-msg/index.mjs +30 -2
  25. package/package.json +3 -5
  26. package/publish-assets/gitattributes +5 -0
  27. package/publish-assets/gitignore +137 -0
  28. package/publish-assets/instructions/produck/10-produck-node.instructions.md +53 -40
  29. package/publish-assets/instructions/produck/15-produck-workspace.instructions.md +13 -16
  30. package/publish-assets/instructions/produck/20-produck-commit.instructions.md +2 -2
  31. package/publish-assets/instructions/produck/tooling-version-baseline.json +8 -1
  32. package/bin/command/sync-husky-hooks/help.txt +0 -14
  33. package/bin/command/sync-husky-hooks/index.mjs +0 -89
  34. 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-eslint-config [--cwd <dir>] [--check] [--dry-run]
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 = npm exec -- eslint --fix . --max-warnings=0 && npm run lint --if-present
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 printSyncEslintConfigHelp() {
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 runSyncEslintConfig(options) {
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