@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
@@ -14,14 +14,42 @@ const TOOLING_BASELINE_CANDIDATE_PATHS = [
14
14
  path.resolve(PACKAGE_ROOT, 'publish-assets/instructions/produck/tooling-version-baseline.json'),
15
15
  ];
16
16
  const GLOB_TOKEN_PATTERN = /[*?{}[\]]/;
17
+ const REQUIRED_ROOT_COVERAGE_SCRIPT_KEY = 'produck:coverage';
18
+ const REQUIRED_ROOT_COVERAGE_SCRIPT_VALUE =
19
+ 'c8 --config .c8rc.json npm run test --workspaces --if-present';
17
20
  const REQUIRED_COVERAGE_SCRIPT_KEY = 'produck:coverage';
18
21
  const REQUIRED_TEST_SCRIPT_KEY = 'test';
19
22
  const DEFAULT_TEST_SCRIPT_VALUE = 'node -e "console.log(\'No tests configured\')"';
20
-
21
- export function printSyncCoverageScriptHelp() {
23
+ const REQUIRED_C8_CONFIG_FILE = '.c8rc.json';
24
+ const REQUIRED_C8_CONFIG_CONTENT = `${JSON.stringify(
25
+ {
26
+ 'check-coverage': true,
27
+ all: true,
28
+ branches: 99.5,
29
+ exclude: ['**/node_modules/**', '**/coverage/**', '**/dist/**', '**/build/**', '**/out/**'],
30
+ functions: 99.5,
31
+ include: ['src/**', 'extension/**'],
32
+ reporter: ['lcov', 'html', 'text-summary'],
33
+ statements: 99.5,
34
+ lines: 99.5,
35
+ },
36
+ null,
37
+ 2,
38
+ )}\n`;
39
+
40
+ export function printSyncCoverageHelp() {
22
41
  printTextResource(HELP_FILE);
23
42
  }
24
43
 
44
+ function parseJsonFile(filePath, label) {
45
+ try {
46
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
47
+ } catch {
48
+ console.error(`${label} is not valid JSON: ${filePath}`);
49
+ process.exit(2);
50
+ }
51
+ }
52
+
25
53
  function loadToolingBaseline() {
26
54
  const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
27
55
  return fs.existsSync(candidatePath);
@@ -36,14 +64,12 @@ function loadToolingBaseline() {
36
64
  }
37
65
 
38
66
  const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
39
- const c8Version = baseline?.tools?.c8?.version;
40
- const coverageTemplate = baseline?.coverage?.scriptTemplate;
41
-
42
67
  if (typeof baseline.schemaVersion !== 'number') {
43
68
  console.error(`Tooling baseline schemaVersion must be a number: ${toolingBaselinePath}`);
44
69
  process.exit(2);
45
70
  }
46
71
 
72
+ const c8Version = baseline?.tools?.c8?.version;
47
73
  if (typeof c8Version !== 'string' || c8Version.trim() === '') {
48
74
  console.error(
49
75
  `Tooling baseline tools.c8.version must be a non-empty string: ${toolingBaselinePath}`,
@@ -51,6 +77,7 @@ function loadToolingBaseline() {
51
77
  process.exit(2);
52
78
  }
53
79
 
80
+ const coverageTemplate = baseline?.coverage?.scriptTemplate;
54
81
  if (typeof coverageTemplate !== 'string' || coverageTemplate.trim() === '') {
55
82
  console.error(
56
83
  `Tooling baseline coverage.scriptTemplate must be a non-empty string: ${toolingBaselinePath}`,
@@ -74,13 +101,12 @@ function buildRequiredC8DevDependency(baseline) {
74
101
  return String(baseline.tools.c8.version);
75
102
  }
76
103
 
77
- function parseJsonFile(filePath, label) {
78
- try {
79
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
80
- } catch {
81
- console.error(`${label} is not valid JSON: ${filePath}`);
82
- process.exit(2);
104
+ function readFileIfExists(filePath) {
105
+ if (!fs.existsSync(filePath)) {
106
+ return null;
83
107
  }
108
+
109
+ return fs.readFileSync(filePath, 'utf8');
84
110
  }
85
111
 
86
112
  function resolveWorkspacePaths(cwd, options) {
@@ -91,20 +117,17 @@ function resolveWorkspacePaths(cwd, options) {
91
117
 
92
118
  const rootPackageJsonPath = path.resolve(cwd, 'package.json');
93
119
  if (!fs.existsSync(rootPackageJsonPath)) {
94
- console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
95
- process.exit(2);
120
+ return [];
96
121
  }
97
122
 
98
123
  const rootPackageJson = parseJsonFile(rootPackageJsonPath, 'Root package.json');
99
124
  if (!Array.isArray(rootPackageJson.workspaces)) {
100
- console.error('Root package.json `workspaces` must be an explicit array');
101
- process.exit(2);
125
+ return [];
102
126
  }
103
127
 
104
128
  const workspaces = rootPackageJson.workspaces.map((entry) => String(entry));
105
129
  if (workspaces.length === 0) {
106
- console.error('Root package.json `workspaces` must not be empty');
107
- process.exit(2);
130
+ return [];
108
131
  }
109
132
 
110
133
  const hasGlob = workspaces.some((entry) => GLOB_TOKEN_PATTERN.test(entry));
@@ -116,6 +139,69 @@ function resolveWorkspacePaths(cwd, options) {
116
139
  return workspaces;
117
140
  }
118
141
 
142
+ function syncRootCoverage(cwd, mode, requiredC8Version) {
143
+ const rootPackageJsonPath = path.resolve(cwd, 'package.json');
144
+ const c8ConfigPath = path.resolve(cwd, REQUIRED_C8_CONFIG_FILE);
145
+ const currentC8ConfigContent = readFileIfExists(c8ConfigPath);
146
+ const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
147
+ const scripts =
148
+ pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
149
+ ? { ...pkg.scripts }
150
+ : {};
151
+ const devDependencies =
152
+ pkg.devDependencies &&
153
+ typeof pkg.devDependencies === 'object' &&
154
+ !Array.isArray(pkg.devDependencies)
155
+ ? { ...pkg.devDependencies }
156
+ : {};
157
+ const previousRootCoverageScript =
158
+ typeof scripts[REQUIRED_ROOT_COVERAGE_SCRIPT_KEY] === 'string'
159
+ ? scripts[REQUIRED_ROOT_COVERAGE_SCRIPT_KEY]
160
+ : null;
161
+ const previousC8DevDependency =
162
+ typeof devDependencies.c8 === 'string' ? devDependencies.c8 : null;
163
+ const matchesRequiredRootCoverageBefore =
164
+ previousRootCoverageScript === REQUIRED_ROOT_COVERAGE_SCRIPT_VALUE;
165
+ const matchesRequiredC8DevDependencyBefore = previousC8DevDependency === requiredC8Version;
166
+ const matchesRequiredC8ConfigBefore = currentC8ConfigContent === REQUIRED_C8_CONFIG_CONTENT;
167
+ const requiresUpdate =
168
+ !matchesRequiredRootCoverageBefore ||
169
+ !matchesRequiredC8DevDependencyBefore ||
170
+ !matchesRequiredC8ConfigBefore;
171
+
172
+ if (mode === 'sync' && requiresUpdate) {
173
+ scripts[REQUIRED_ROOT_COVERAGE_SCRIPT_KEY] = REQUIRED_ROOT_COVERAGE_SCRIPT_VALUE;
174
+ pkg.scripts = scripts;
175
+ devDependencies.c8 = requiredC8Version;
176
+ pkg.devDependencies = devDependencies;
177
+ fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
178
+ fs.writeFileSync(c8ConfigPath, REQUIRED_C8_CONFIG_CONTENT, 'utf8');
179
+ }
180
+
181
+ return {
182
+ rootPackageJsonPath,
183
+ required: {
184
+ rootCoverageScriptKey: REQUIRED_ROOT_COVERAGE_SCRIPT_KEY,
185
+ rootCoverageScriptValue: REQUIRED_ROOT_COVERAGE_SCRIPT_VALUE,
186
+ c8ConfigFile: REQUIRED_C8_CONFIG_FILE,
187
+ c8ConfigContent: REQUIRED_C8_CONFIG_CONTENT,
188
+ c8DevDependency: requiredC8Version,
189
+ },
190
+ status: {
191
+ matchesRequiredRootCoverageBefore,
192
+ matchesRequiredC8DevDependencyBefore,
193
+ matchesRequiredC8ConfigBefore,
194
+ matchesRequiredRootCoverageAfter:
195
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredRootCoverageBefore,
196
+ matchesRequiredC8DevDependencyAfter:
197
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredC8DevDependencyBefore,
198
+ matchesRequiredC8ConfigAfter:
199
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredC8ConfigBefore,
200
+ updated: requiresUpdate && mode === 'sync',
201
+ },
202
+ };
203
+ }
204
+
119
205
  function reconcileCoverageScript(
120
206
  cwd,
121
207
  workspacePath,
@@ -230,7 +316,7 @@ function reconcileCoverageScript(
230
316
  return result;
231
317
  }
232
318
 
233
- export function runSyncCoverageScript(options) {
319
+ export function runSyncCoverage(options) {
234
320
  const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
235
321
  const check = hasFlag(options, '--check');
236
322
  const dryRun = hasFlag(options, '--dry-run');
@@ -244,8 +330,9 @@ export function runSyncCoverageScript(options) {
244
330
  process.exit(2);
245
331
  }
246
332
 
247
- const workspacePaths = resolveWorkspacePaths(cwd, options);
248
333
  const mode = dryRun ? 'dry-run' : check ? 'check' : 'sync';
334
+ const root = syncRootCoverage(cwd, mode, requiredC8Version);
335
+ const workspacePaths = resolveWorkspacePaths(cwd, options);
249
336
 
250
337
  const report = {
251
338
  cwd,
@@ -258,11 +345,21 @@ export function runSyncCoverageScript(options) {
258
345
  requiredCoverageScript,
259
346
  requiredTestScript: DEFAULT_TEST_SCRIPT_VALUE,
260
347
  requiredC8DevDependency: requiredC8Version,
348
+ root,
261
349
  workspaces: workspacePaths,
262
350
  results: [],
263
351
  ok: true,
264
352
  };
265
353
 
354
+ if (
355
+ mode === 'check' &&
356
+ (!root.status.matchesRequiredRootCoverageAfter ||
357
+ !root.status.matchesRequiredC8DevDependencyAfter ||
358
+ !root.status.matchesRequiredC8ConfigAfter)
359
+ ) {
360
+ report.ok = false;
361
+ }
362
+
266
363
  for (const workspacePath of workspacePaths) {
267
364
  const effectiveMode = mode === 'sync' ? 'sync' : 'check';
268
365
  const item = reconcileCoverageScript(
@@ -12,154 +12,10 @@ const TEMPLATE_FILE = path.resolve(COMMAND_DIR, 'editorconfig.template');
12
12
 
13
13
  const REQUIRED_EDITORCONFIG_CONTENT = fs.readFileSync(TEMPLATE_FILE, 'utf8');
14
14
 
15
- // Required key-value pairs for validation
16
- const REQUIRED_SECTIONS = {
17
- root: {
18
- line: 'root = true',
19
- },
20
- '*': {
21
- keys: {
22
- charset: 'utf-8',
23
- indent_style: 'space',
24
- indent_size: '2',
25
- trim_trailing_whitespace: 'true',
26
- },
27
- },
28
- '*.{yml,yaml}': {
29
- keys: {
30
- indent_style: 'space',
31
- indent_size: '2',
32
- },
33
- },
34
- '*.md': {
35
- keys: {
36
- trim_trailing_whitespace: 'false',
37
- max_line_length: '80',
38
- },
39
- },
40
- };
41
-
42
15
  export function printSyncEditorconfigHelp() {
43
16
  printTextResource(HELP_FILE);
44
17
  }
45
18
 
46
- function parseEditorconfig(content) {
47
- const sections = {};
48
- let currentSection = null;
49
-
50
- for (const line of content.split('\n')) {
51
- const trimmed = line.trim();
52
-
53
- // Skip empty lines and comments
54
- if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith(';')) {
55
- continue;
56
- }
57
-
58
- // Check for section header
59
- const sectionMatch = trimmed.match(/^\[(.+)\]$/);
60
- if (sectionMatch) {
61
- currentSection = sectionMatch[1];
62
- sections[currentSection] = {};
63
- continue;
64
- }
65
-
66
- // Check for root = true
67
- const rootMatch = trimmed.match(/^root\s*=\s*(.+)$/i);
68
- if (rootMatch && !currentSection) {
69
- sections._root = rootMatch[1].trim().toLowerCase();
70
- continue;
71
- }
72
-
73
- // Parse key-value pair
74
- if (currentSection) {
75
- const kvMatch = trimmed.match(/^([^=]+)\s*=\s*(.+)$/);
76
- if (kvMatch) {
77
- sections[currentSection][kvMatch[1].trim().toLowerCase()] = kvMatch[2].trim().toLowerCase();
78
- }
79
- }
80
- }
81
-
82
- return sections;
83
- }
84
-
85
- function validateEditorconfig(sections) {
86
- const mismatches = [];
87
-
88
- // Check root
89
- if (sections._root !== 'true') {
90
- mismatches.push({ section: '_root', expected: 'true', actual: sections._root || 'missing' });
91
- }
92
-
93
- // Check each required section
94
- for (const [sectionName, config] of Object.entries(REQUIRED_SECTIONS)) {
95
- if (sectionName === 'root') continue;
96
-
97
- if (!sections[sectionName]) {
98
- mismatches.push({ section: `[${sectionName}]`, expected: 'present', actual: 'missing' });
99
- continue;
100
- }
101
-
102
- if (config.keys) {
103
- for (const [key, expectedValue] of Object.entries(config.keys)) {
104
- const actualValue = sections[sectionName][key];
105
- if (actualValue !== expectedValue) {
106
- mismatches.push({
107
- section: `[${sectionName}]`,
108
- key,
109
- expected: expectedValue,
110
- actual: actualValue || 'missing',
111
- });
112
- }
113
- }
114
- }
115
- }
116
-
117
- return mismatches;
118
- }
119
-
120
- function buildUpdatedContent(existingContent) {
121
- const existingSections = parseEditorconfig(existingContent);
122
- const lines = [];
123
-
124
- // Add root if missing
125
- if (existingSections._root !== 'true') {
126
- lines.push('root = true');
127
- }
128
-
129
- // Process each required section
130
- for (const [sectionName, config] of Object.entries(REQUIRED_SECTIONS)) {
131
- if (sectionName === 'root') continue;
132
-
133
- const existingSection = existingSections[sectionName] || {};
134
- const missingKeys = [];
135
-
136
- if (config.keys) {
137
- for (const [key, expectedValue] of Object.entries(config.keys)) {
138
- if (existingSection[key] !== expectedValue) {
139
- missingKeys.push({ key, value: expectedValue });
140
- }
141
- }
142
- }
143
-
144
- if (missingKeys.length > 0 || !existingSections[sectionName]) {
145
- lines.push('');
146
- lines.push(`[${sectionName}]`);
147
- for (const { key, value } of missingKeys) {
148
- lines.push(`${key} = ${value}`);
149
- }
150
- }
151
- }
152
-
153
- // If no updates needed, return original
154
- // c8 ignore next 3
155
- if (lines.length === 0) {
156
- return existingContent;
157
- }
158
-
159
- // Append missing entries to existing content
160
- return existingContent.trimEnd() + lines.join('\n') + '\n';
161
- }
162
-
163
19
  function readFileIfExists(filePath) {
164
20
  if (!fs.existsSync(filePath)) {
165
21
  return null;
@@ -183,17 +39,18 @@ export function runSyncEditorconfig(options) {
183
39
  const editorconfigPath = path.resolve(cwd, EDITORCONFIG_FILE);
184
40
  const currentContent = readFileIfExists(editorconfigPath);
185
41
  const fileExists = currentContent !== null;
42
+ const upToDate = fileExists && currentContent === REQUIRED_EDITORCONFIG_CONTENT;
186
43
 
187
- const sections = currentContent ? parseEditorconfig(currentContent) : {};
188
- const mismatches = validateEditorconfig(sections);
189
- const requiresUpdate = mismatches.length > 0 || !fileExists;
190
-
191
- let plannedContent = null;
192
- if (requiresUpdate) {
193
- plannedContent = fileExists
194
- ? buildUpdatedContent(currentContent)
195
- : REQUIRED_EDITORCONFIG_CONTENT;
44
+ const mismatches = [];
45
+ if (!upToDate) {
46
+ mismatches.push({
47
+ file: EDITORCONFIG_FILE,
48
+ expected: 'exact required content',
49
+ actual: fileExists ? 'different content' : 'missing',
50
+ });
196
51
  }
52
+ const requiresUpdate = mismatches.length > 0;
53
+ const plannedContent = requiresUpdate ? REQUIRED_EDITORCONFIG_CONTENT : null;
197
54
 
198
55
  if (mode === 'sync' && requiresUpdate && plannedContent) {
199
56
  fs.writeFileSync(editorconfigPath, plannedContent, 'utf8');
@@ -1,12 +1,14 @@
1
1
  Usage:
2
- agent-toolkit sync-prettier-config [--cwd <dir>] [--check] [--dry-run]
2
+ agent-toolkit sync-format [--cwd <dir>] [--check] [--dry-run]
3
3
  [--json <file>]
4
4
 
5
5
  Behavior:
6
6
  - Applies organization-required root format script:
7
- - scripts.produck:format = npm exec -- prettier --check . && npm run format --if-present
7
+ - scripts.produck:format = prettier --write . --ignore-path .prettierignore --ignore-path .gitignore
8
8
  - Applies organization-required root Prettier config file:
9
9
  - .prettierrc
10
+ - Applies organization-required root Prettier ignore file:
11
+ - .prettierignore (prettier-specific patterns; .gitignore is also used via --ignore-path)
10
12
 
11
13
  Rules:
12
14
  - --check validates without writing and exits non-zero on mismatch
@@ -0,0 +1,222 @@
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 PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
11
+ const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
12
+ const TOOLING_BASELINE_CANDIDATE_PATHS = [
13
+ path.resolve(REPO_ROOT, '.github/distribution/produck/tooling-version-baseline.json'),
14
+ path.resolve(PACKAGE_ROOT, 'publish-assets/instructions/produck/tooling-version-baseline.json'),
15
+ ];
16
+ const PRETTIER_CONFIG_FILE = '.prettierrc';
17
+ const PRETTIER_IGNORE_FILE = '.prettierignore';
18
+ const REQUIRED_PRETTIER_DEV_DEPENDENCY_KEY = 'prettier';
19
+
20
+ const PRETTIER_IGNORE_SOURCE_CANDIDATE_PATHS = [
21
+ path.resolve(REPO_ROOT, '.prettierignore'),
22
+ path.resolve(PACKAGE_ROOT, 'publish-assets/prettierignore'),
23
+ ];
24
+
25
+ const REQUIRED_FORMAT_SCRIPT_KEY = 'produck:format';
26
+ const REQUIRED_FORMAT_SCRIPT_VALUE =
27
+ 'prettier --write . --ignore-path .prettierignore --ignore-path .gitignore';
28
+ const REQUIRED_PRETTIER_CONFIG = `${JSON.stringify(
29
+ {
30
+ semi: true,
31
+ singleQuote: true,
32
+ tabWidth: 2,
33
+ useTabs: false,
34
+ trailingComma: 'all',
35
+ bracketSpacing: true,
36
+ arrowParens: 'always',
37
+ printWidth: 100,
38
+ },
39
+ null,
40
+ 2,
41
+ )}\n`;
42
+
43
+ function loadPrettierIgnoreContent() {
44
+ const sourcePath = PRETTIER_IGNORE_SOURCE_CANDIDATE_PATHS.find((p) => fs.existsSync(p));
45
+
46
+ if (!sourcePath) {
47
+ console.error('Org .prettierignore source not found in expected locations:');
48
+ for (const p of PRETTIER_IGNORE_SOURCE_CANDIDATE_PATHS) {
49
+ console.error(`- ${p}`);
50
+ }
51
+ process.exit(2);
52
+ }
53
+
54
+ return {
55
+ sourcePath,
56
+ content: fs.readFileSync(sourcePath, 'utf8'),
57
+ };
58
+ }
59
+
60
+ export function printSyncFormatHelp() {
61
+ printTextResource(HELP_FILE);
62
+ }
63
+
64
+ function readFileIfExists(filePath) {
65
+ if (!fs.existsSync(filePath)) {
66
+ return null;
67
+ }
68
+
69
+ return fs.readFileSync(filePath, 'utf8');
70
+ }
71
+
72
+ function parseJsonFile(filePath, label) {
73
+ try {
74
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
75
+ } catch {
76
+ console.error(`${label} is not valid JSON: ${filePath}`);
77
+ process.exit(2);
78
+ }
79
+ }
80
+
81
+ function loadToolingBaseline() {
82
+ const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
83
+ return fs.existsSync(candidatePath);
84
+ });
85
+
86
+ if (!toolingBaselinePath) {
87
+ console.error('Tooling baseline file does not exist in expected locations:');
88
+ for (const candidatePath of TOOLING_BASELINE_CANDIDATE_PATHS) {
89
+ console.error(`- ${candidatePath}`);
90
+ }
91
+ process.exit(2);
92
+ }
93
+
94
+ const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
95
+ const prettierVersion = String(baseline?.tools?.prettier?.version || '').trim();
96
+
97
+ if (!prettierVersion) {
98
+ console.error(
99
+ `Tooling baseline must define fixed tools.prettier.version: ${toolingBaselinePath}`,
100
+ );
101
+ process.exit(2);
102
+ }
103
+
104
+ return { toolingBaselinePath, prettierVersion };
105
+ }
106
+
107
+ export function runSyncFormat(options) {
108
+ const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
109
+ const check = hasFlag(options, '--check');
110
+ const dryRun = hasFlag(options, '--dry-run') && !check;
111
+ const jsonFile = getSingle(options, '--json', '');
112
+ const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
113
+
114
+ if (!fs.existsSync(cwd)) {
115
+ console.error(`CWD does not exist: ${cwd}`);
116
+ process.exit(2);
117
+ }
118
+
119
+ const rootPackageJsonPath = path.resolve(cwd, 'package.json');
120
+ if (!fs.existsSync(rootPackageJsonPath)) {
121
+ console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
122
+ process.exit(2);
123
+ }
124
+
125
+ const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
126
+ const toolingBaseline = loadToolingBaseline();
127
+ const requiredPrettierVersion = toolingBaseline.prettierVersion;
128
+ const { sourcePath: prettierIgnoreSourcePath, content: REQUIRED_PRETTIER_IGNORE_CONTENT } =
129
+ loadPrettierIgnoreContent();
130
+ const scripts =
131
+ pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
132
+ ? { ...pkg.scripts }
133
+ : {};
134
+ const devDependencies =
135
+ pkg.devDependencies &&
136
+ typeof pkg.devDependencies === 'object' &&
137
+ !Array.isArray(pkg.devDependencies)
138
+ ? { ...pkg.devDependencies }
139
+ : {};
140
+
141
+ const previousFormat =
142
+ typeof scripts[REQUIRED_FORMAT_SCRIPT_KEY] === 'string'
143
+ ? scripts[REQUIRED_FORMAT_SCRIPT_KEY]
144
+ : null;
145
+ const previousPrettierDep =
146
+ typeof devDependencies[REQUIRED_PRETTIER_DEV_DEPENDENCY_KEY] === 'string'
147
+ ? devDependencies[REQUIRED_PRETTIER_DEV_DEPENDENCY_KEY]
148
+ : null;
149
+
150
+ const prettierConfigPath = path.resolve(cwd, PRETTIER_CONFIG_FILE);
151
+ const prettierIgnorePath = path.resolve(cwd, PRETTIER_IGNORE_FILE);
152
+ const previousPrettierConfig = readFileIfExists(prettierConfigPath);
153
+ const previousPrettierIgnore = readFileIfExists(prettierIgnorePath);
154
+
155
+ const matchesRequiredFormat = previousFormat === REQUIRED_FORMAT_SCRIPT_VALUE;
156
+ const matchesRequiredPrettierConfig = previousPrettierConfig === REQUIRED_PRETTIER_CONFIG;
157
+ const matchesRequiredPrettierDep = previousPrettierDep === requiredPrettierVersion;
158
+ const matchesRequiredPrettierIgnore = previousPrettierIgnore === REQUIRED_PRETTIER_IGNORE_CONTENT;
159
+
160
+ const requiresUpdate =
161
+ !matchesRequiredFormat ||
162
+ !matchesRequiredPrettierConfig ||
163
+ !matchesRequiredPrettierDep ||
164
+ !matchesRequiredPrettierIgnore;
165
+
166
+ if (mode === 'sync' && requiresUpdate) {
167
+ scripts[REQUIRED_FORMAT_SCRIPT_KEY] = REQUIRED_FORMAT_SCRIPT_VALUE;
168
+ pkg.scripts = scripts;
169
+
170
+ devDependencies[REQUIRED_PRETTIER_DEV_DEPENDENCY_KEY] = requiredPrettierVersion;
171
+ pkg.devDependencies = devDependencies;
172
+
173
+ fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
174
+ fs.writeFileSync(prettierConfigPath, REQUIRED_PRETTIER_CONFIG, 'utf8');
175
+ fs.writeFileSync(prettierIgnorePath, REQUIRED_PRETTIER_IGNORE_CONTENT, 'utf8');
176
+ }
177
+
178
+ const report = {
179
+ cwd,
180
+ mode,
181
+ ok: true,
182
+ rootPackageJsonPath,
183
+ toolingBaselinePath: toolingBaseline.toolingBaselinePath,
184
+ required: {
185
+ formatScriptKey: REQUIRED_FORMAT_SCRIPT_KEY,
186
+ formatScriptValue: REQUIRED_FORMAT_SCRIPT_VALUE,
187
+ prettierConfigPath: path.relative(cwd, prettierConfigPath),
188
+ prettierIgnorePath: path.relative(cwd, prettierIgnorePath),
189
+ prettierIgnoreSourcePath,
190
+ managedDevDependencies: { [REQUIRED_PRETTIER_DEV_DEPENDENCY_KEY]: requiredPrettierVersion },
191
+ },
192
+ status: {
193
+ matchesRequiredFormatBefore: matchesRequiredFormat,
194
+ matchesRequiredPrettierConfigBefore: matchesRequiredPrettierConfig,
195
+ matchesRequiredPrettierDepBefore: matchesRequiredPrettierDep,
196
+ matchesRequiredPrettierIgnoreBefore: matchesRequiredPrettierIgnore,
197
+ matchesRequiredFormatAfter: requiresUpdate && mode === 'sync' ? true : matchesRequiredFormat,
198
+ matchesRequiredPrettierConfigAfter:
199
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredPrettierConfig,
200
+ matchesRequiredPrettierDepAfter:
201
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredPrettierDep,
202
+ matchesRequiredPrettierIgnoreAfter:
203
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredPrettierIgnore,
204
+ updated: requiresUpdate && mode === 'sync',
205
+ },
206
+ };
207
+
208
+ if (mode === 'check' && requiresUpdate) {
209
+ report.ok = false;
210
+ }
211
+
212
+ if (jsonFile) {
213
+ const outPath = path.resolve(cwd, jsonFile);
214
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
215
+ fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
216
+ }
217
+
218
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
219
+ if (!report.ok) {
220
+ process.exit(2);
221
+ }
222
+ }
@@ -1,19 +1,23 @@
1
1
  Usage:
2
- agent-toolkit sync-workspace-config [--cwd <dir>] [--check] [--dry-run]
2
+ agent-toolkit sync-git [--cwd <dir>] [--check] [--dry-run]
3
3
  [--json <file>]
4
4
 
5
5
  Behavior:
6
6
  - Applies organization-required root shared scripts:
7
7
  - scripts.produck:baseline = npm exec --package=@produck/agent-toolkit@latest -- agent-toolkit enforce-node-baseline --cwd .
8
- - scripts.produck:coverage = c8 --config .c8rc.json npm run test --workspaces --if-present
9
- - scripts.produck:precommit-check = npm run produck:format && npm run produck:lint
10
- - Applies organization-required root c8 config file:
11
- - .c8rc.json
8
+ - scripts.produck:commit:check = npm run produck:format && npm run produck:lint
12
9
  - Applies organization-required root shared managed devDependencies:
13
- - devDependencies.c8 = <fixed-version-from-baseline>
14
10
  - devDependencies.husky = <fixed-version-from-baseline>
15
11
  - devDependencies.lerna = <fixed-version-from-baseline>
16
12
  - devDependencies.@produck/agent-toolkit = <latest-version-resolved-at-runtime>
13
+ - Applies organization-required root .gitattributes file (exact replacement)
14
+ - Normalizes line endings for text files
15
+ - Keeps Windows script entrypoints on CRLF
16
+ - Ensures root .gitignore contains all org-baseline required entries
17
+ (downstream can extend; missing entries are appended)
18
+ - Applies organization-required hook files:
19
+ - .husky/pre-commit
20
+ - .husky/commit-msg
17
21
 
18
22
  Rules:
19
23
  - --check validates without writing and exits non-zero on mismatch