@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.
Files changed (36) hide show
  1. package/README.md +74 -43
  2. package/bin/agent-toolkit.mjs +26 -33
  3. package/bin/build-publish-assets.mjs +54 -5
  4. package/bin/command/enforce-node-baseline/help.txt +12 -10
  5. package/bin/command/enforce-node-baseline/index.mjs +23 -15
  6. package/bin/command/main/help.txt +6 -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/index.mjs +10 -153
  12. package/bin/command/{sync-prettier-config → sync-format}/help.txt +4 -2
  13. package/bin/command/sync-format/index.mjs +222 -0
  14. package/bin/command/{sync-workspace-config → sync-git}/help.txt +10 -6
  15. package/bin/command/sync-git/index.mjs +424 -0
  16. package/bin/command/sync-install/help.txt +14 -0
  17. package/bin/command/{sync-prettier-config → sync-install}/index.mjs +26 -50
  18. package/bin/command/sync-instructions/index.mjs +2 -22
  19. package/bin/command/{sync-eslint-config → sync-lint}/help.txt +2 -2
  20. package/bin/command/{sync-eslint-config → sync-lint}/index.mjs +3 -4
  21. package/bin/command/sync-publish/help.txt +18 -0
  22. package/bin/command/sync-publish/index.mjs +157 -0
  23. package/bin/command/validate-commit-msg/index.mjs +30 -2
  24. package/package.json +3 -5
  25. package/publish-assets/gitattributes +5 -0
  26. package/publish-assets/gitignore +137 -0
  27. package/publish-assets/instructions/produck/00-produck-base.instructions.md +44 -58
  28. package/publish-assets/instructions/produck/10-produck-node.instructions.md +59 -82
  29. package/publish-assets/instructions/produck/12-produck-test.instructions.md +94 -0
  30. package/publish-assets/instructions/produck/15-produck-workspace.instructions.md +17 -50
  31. package/publish-assets/instructions/produck/20-produck-commit.instructions.md +2 -2
  32. package/publish-assets/instructions/produck/tooling-version-baseline.json +14 -2
  33. package/publish-assets/prettierignore +2 -0
  34. package/bin/command/sync-husky-hooks/help.txt +0 -14
  35. package/bin/command/sync-husky-hooks/index.mjs +0 -89
  36. 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 PRETTIER_CONFIG_FILE = '.prettierrc';
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
- const REQUIRED_FORMAT_SCRIPT_KEY = 'produck:format';
13
- const REQUIRED_FORMAT_SCRIPT_VALUE =
14
- 'npm exec -- prettier --check . && npm run format --if-present';
15
- const REQUIRED_PRETTIER_CONFIG = `${JSON.stringify(
16
- {
17
- semi: true,
18
- singleQuote: true,
19
- tabWidth: 2,
20
- useTabs: false,
21
- trailingComma: 'all',
22
- bracketSpacing: true,
23
- arrowParens: 'always',
24
- printWidth: 100,
25
- },
26
- null,
27
- 2,
28
- )}\n`;
29
-
30
- export function printSyncPrettierConfigHelp() {
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 readFileIfExists(filePath) {
44
- if (!fs.existsSync(filePath)) {
45
- return null;
46
- }
47
-
48
- return fs.readFileSync(filePath, 'utf8');
49
- }
50
-
51
- export function runSyncPrettierConfig(options) {
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 previousFormat =
76
- typeof scripts[REQUIRED_FORMAT_SCRIPT_KEY] === 'string'
77
- ? scripts[REQUIRED_FORMAT_SCRIPT_KEY]
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 prettierConfigPath = path.resolve(cwd, PRETTIER_CONFIG_FILE);
81
- const previousPrettierConfig = readFileIfExists(prettierConfigPath);
82
-
83
- const matchesRequiredFormat = previousFormat === REQUIRED_FORMAT_SCRIPT_VALUE;
84
- const matchesRequiredPrettierConfig = previousPrettierConfig === REQUIRED_PRETTIER_CONFIG;
85
-
86
- const requiresUpdate = !matchesRequiredFormat || !matchesRequiredPrettierConfig;
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[REQUIRED_FORMAT_SCRIPT_KEY] = REQUIRED_FORMAT_SCRIPT_VALUE;
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
- formatScriptKey: REQUIRED_FORMAT_SCRIPT_KEY,
103
- formatScriptValue: REQUIRED_FORMAT_SCRIPT_VALUE,
104
- prettierConfigPath: path.relative(cwd, prettierConfigPath),
77
+ installScriptKey: REQUIRED_INSTALL_SCRIPT_KEY,
78
+ installScriptValue: REQUIRED_INSTALL_SCRIPT_VALUE,
79
+ legacyInstallScriptKey: LEGACY_INSTALL_SCRIPT_KEY,
105
80
  },
106
81
  status: {
107
- matchesRequiredFormatBefore: matchesRequiredFormat,
108
- matchesRequiredPrettierConfigBefore: matchesRequiredPrettierConfig,
109
- matchesRequiredFormatAfter: requiresUpdate && mode === 'sync' ? true : matchesRequiredFormat,
110
- matchesRequiredPrettierConfigAfter:
111
- requiresUpdate && mode === 'sync' ? true : matchesRequiredPrettierConfig,
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 = hasFlag(options, '--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 && force,
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-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