@produck/agent-toolkit 0.3.3 → 0.5.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.
@@ -0,0 +1,248 @@
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
+
9
+ const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
10
+ const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
11
+ const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
12
+ const TOOLKIT_PACKAGE_JSON = path.resolve(PACKAGE_ROOT, 'package.json');
13
+ const ESLINT_CONFIG_FILE = 'eslint.config.mjs';
14
+
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';
18
+ const REQUIRED_ESLINT_CONFIG = `import globals from 'globals';
19
+ import pluginJs from '@eslint/js';
20
+ import tseslint from 'typescript-eslint';
21
+ import * as ProduckRule from '@produck/eslint-rules';
22
+
23
+ export default [
24
+ { files: ['**/*.{js,mjs,cjs,ts,mts}'] },
25
+ { languageOptions: { globals: { ...globals.browser, ...globals.node } } },
26
+ pluginJs.configs.recommended,
27
+ ...tseslint.configs.recommended,
28
+ ProduckRule.config,
29
+ ProduckRule.excludeGitIgnore(import.meta.url),
30
+ ];
31
+ `;
32
+
33
+ export function printSyncEslintConfigHelp() {
34
+ printTextResource(HELP_FILE);
35
+ }
36
+
37
+ function parseJsonFile(filePath, label) {
38
+ try {
39
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
40
+ } catch {
41
+ console.error(`${label} is not valid JSON: ${filePath}`);
42
+ process.exit(2);
43
+ }
44
+ }
45
+
46
+ function readFileIfExists(filePath) {
47
+ if (!fs.existsSync(filePath)) {
48
+ return null;
49
+ }
50
+
51
+ return fs.readFileSync(filePath, 'utf8');
52
+ }
53
+
54
+ function getRequiredEslintRulesDevDependency() {
55
+ const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
56
+ const latestResult = spawnSync(npmCommand, ['view', '@produck/eslint-rules', 'version'], {
57
+ encoding: 'utf8',
58
+ });
59
+
60
+ const latestVersion = String(latestResult.stdout || '').trim();
61
+ if (latestResult.status === 0 && latestVersion) {
62
+ return latestVersion;
63
+ }
64
+
65
+ const pkg = parseJsonFile(TOOLKIT_PACKAGE_JSON, 'Toolkit package.json');
66
+ const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
67
+
68
+ if (!version) {
69
+ console.error(`Toolkit package version is missing: ${TOOLKIT_PACKAGE_JSON}`);
70
+ process.exit(2);
71
+ }
72
+
73
+ return version;
74
+ }
75
+
76
+ function patchEslintConfig(existing) {
77
+ if (existing.includes('@produck/eslint-rules')) {
78
+ return { ok: true, patched: false, output: existing };
79
+ }
80
+
81
+ const importRegex = /^import\s.+;\s*$/gm;
82
+ let lastImport = null;
83
+ let match = importRegex.exec(existing);
84
+ while (match) {
85
+ lastImport = match;
86
+ match = importRegex.exec(existing);
87
+ }
88
+
89
+ if (!lastImport) {
90
+ return { ok: false, patched: false, output: existing };
91
+ }
92
+
93
+ const importInsertAt = lastImport.index + lastImport[0].length;
94
+ let output =
95
+ `${existing.slice(0, importInsertAt)}\nimport * as ProduckRule from '@produck/eslint-rules';` +
96
+ existing.slice(importInsertAt);
97
+
98
+ const exportStart = output.indexOf('export default [');
99
+ const exportEnd = output.lastIndexOf('];');
100
+ if (exportStart === -1 || exportEnd === -1 || exportEnd < exportStart) {
101
+ return { ok: false, patched: false, output: existing };
102
+ }
103
+
104
+ output =
105
+ `${output.slice(0, exportEnd)} ProduckRule.config,\n ProduckRule.excludeGitIgnore(import.meta.url),\n` +
106
+ output.slice(exportEnd);
107
+
108
+ if (!output.endsWith('\n')) {
109
+ output = `${output}\n`;
110
+ }
111
+
112
+ return { ok: true, patched: true, output };
113
+ }
114
+
115
+ export function runSyncEslintConfig(options) {
116
+ const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
117
+ const check = hasFlag(options, '--check');
118
+ const dryRun = hasFlag(options, '--dry-run') && !check;
119
+ const jsonFile = getSingle(options, '--json', '');
120
+ const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
121
+
122
+ if (!fs.existsSync(cwd)) {
123
+ console.error(`CWD does not exist: ${cwd}`);
124
+ process.exit(2);
125
+ }
126
+
127
+ const rootPackageJsonPath = path.resolve(cwd, 'package.json');
128
+ if (!fs.existsSync(rootPackageJsonPath)) {
129
+ console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
130
+ process.exit(2);
131
+ }
132
+
133
+ const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
134
+ const scripts =
135
+ pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
136
+ ? { ...pkg.scripts }
137
+ : {};
138
+ const devDependencies =
139
+ pkg.devDependencies &&
140
+ typeof pkg.devDependencies === 'object' &&
141
+ !Array.isArray(pkg.devDependencies)
142
+ ? { ...pkg.devDependencies }
143
+ : {};
144
+
145
+ const previousLint =
146
+ typeof scripts[REQUIRED_LINT_SCRIPT_KEY] === 'string'
147
+ ? scripts[REQUIRED_LINT_SCRIPT_KEY]
148
+ : null;
149
+ const previousEslintRules =
150
+ typeof devDependencies['@produck/eslint-rules'] === 'string'
151
+ ? devDependencies['@produck/eslint-rules']
152
+ : null;
153
+
154
+ const requiredEslintRulesDependency = getRequiredEslintRulesDevDependency();
155
+
156
+ const eslintConfigPath = path.resolve(cwd, ESLINT_CONFIG_FILE);
157
+ const previousEslintConfig = readFileIfExists(eslintConfigPath);
158
+
159
+ const matchesRequiredLint = previousLint === REQUIRED_LINT_SCRIPT_VALUE;
160
+ const matchesRequiredEslintRules = previousEslintRules === requiredEslintRulesDependency;
161
+
162
+ let eslintConfigAction = 'unchanged';
163
+ let matchesRequiredEslintConfig = false;
164
+ let nextEslintConfigText = previousEslintConfig;
165
+
166
+ if (previousEslintConfig === null) {
167
+ eslintConfigAction = 'initialized';
168
+ nextEslintConfigText = REQUIRED_ESLINT_CONFIG;
169
+ } else if (previousEslintConfig === REQUIRED_ESLINT_CONFIG) {
170
+ matchesRequiredEslintConfig = true;
171
+ } else if (previousEslintConfig.includes('@produck/eslint-rules')) {
172
+ matchesRequiredEslintConfig = true;
173
+ } else {
174
+ const patched = patchEslintConfig(previousEslintConfig);
175
+ if (patched.ok) {
176
+ eslintConfigAction = 'patched';
177
+ nextEslintConfigText = patched.output;
178
+ } else {
179
+ eslintConfigAction = 'unpatchable';
180
+ }
181
+ }
182
+
183
+ const requiresUpdate =
184
+ !matchesRequiredLint || !matchesRequiredEslintRules || !matchesRequiredEslintConfig;
185
+ const hasUnpatchableEslintConfig = eslintConfigAction === 'unpatchable';
186
+
187
+ if (mode === 'sync' && requiresUpdate && !hasUnpatchableEslintConfig) {
188
+ scripts[REQUIRED_LINT_SCRIPT_KEY] = REQUIRED_LINT_SCRIPT_VALUE;
189
+ pkg.scripts = scripts;
190
+
191
+ devDependencies['@produck/eslint-rules'] = requiredEslintRulesDependency;
192
+ pkg.devDependencies = devDependencies;
193
+
194
+ fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
195
+ fs.writeFileSync(eslintConfigPath, nextEslintConfigText || REQUIRED_ESLINT_CONFIG, 'utf8');
196
+ }
197
+
198
+ const report = {
199
+ cwd,
200
+ mode,
201
+ ok: true,
202
+ rootPackageJsonPath,
203
+ required: {
204
+ lintScriptKey: REQUIRED_LINT_SCRIPT_KEY,
205
+ lintScriptValue: REQUIRED_LINT_SCRIPT_VALUE,
206
+ eslintRulesVersion: requiredEslintRulesDependency,
207
+ eslintConfigPath: path.relative(cwd, eslintConfigPath),
208
+ eslintConfigAction,
209
+ },
210
+ status: {
211
+ matchesRequiredLintBefore: matchesRequiredLint,
212
+ matchesRequiredEslintRulesBefore: matchesRequiredEslintRules,
213
+ matchesRequiredEslintConfigBefore: matchesRequiredEslintConfig,
214
+ matchesRequiredLintAfter:
215
+ requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
216
+ ? true
217
+ : matchesRequiredLint,
218
+ matchesRequiredEslintRulesAfter:
219
+ requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
220
+ ? true
221
+ : matchesRequiredEslintRules,
222
+ matchesRequiredEslintConfigAfter:
223
+ requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
224
+ ? true
225
+ : matchesRequiredEslintConfig,
226
+ updated: requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig,
227
+ hasUnpatchableEslintConfig,
228
+ },
229
+ };
230
+
231
+ if (mode === 'check' && (requiresUpdate || hasUnpatchableEslintConfig)) {
232
+ report.ok = false;
233
+ }
234
+ if ((mode === 'sync' || mode === 'dry-run') && hasUnpatchableEslintConfig) {
235
+ report.ok = false;
236
+ }
237
+
238
+ if (jsonFile) {
239
+ const outPath = path.resolve(cwd, jsonFile);
240
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
241
+ fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
242
+ }
243
+
244
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
245
+ if (!report.ok) {
246
+ process.exit(2);
247
+ }
248
+ }
@@ -3,15 +3,7 @@ Usage:
3
3
  [--json <file>]
4
4
 
5
5
  Behavior:
6
- - Applies organization-required root scripts for local hook gates:
7
- - scripts.prepare = husky
8
- - scripts.produck:precommit-check = npm run format:check && npm run lint
9
- - Applies organization-required root managed devDependencies:
10
- - devDependencies.c8 = <fixed-version-from-baseline>
11
- - devDependencies.husky = <fixed-version-from-baseline>
12
- - devDependencies.lerna = <fixed-version-from-baseline>
13
- - devDependencies.@produck/agent-toolkit = <latest-version-resolved-at-runtime>
14
- - Applies organization-required hook files:
6
+ - Applies organization-required hook files only:
15
7
  - .husky/pre-commit
16
8
  - .husky/commit-msg
17
9
  - commit-msg hook validates message via local node_modules toolkit path
@@ -1,6 +1,5 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { spawnSync } from 'node:child_process';
4
3
  import { fileURLToPath } from 'node:url';
5
4
 
6
5
  import { getSingle, hasFlag } from '../shared/args.mjs';
@@ -8,17 +7,6 @@ import { printTextResource } from '../shared/text-resource.mjs';
8
7
 
9
8
  const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
10
9
  const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
11
- const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
12
- const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
13
- const TOOLKIT_PACKAGE_JSON = path.resolve(PACKAGE_ROOT, 'package.json');
14
- const TOOLING_BASELINE_CANDIDATE_PATHS = [
15
- path.resolve(REPO_ROOT, '.github/distribution/produck/tooling-version-baseline.json'),
16
- path.resolve(PACKAGE_ROOT, 'publish-assets/instructions/produck/tooling-version-baseline.json'),
17
- ];
18
-
19
- const REQUIRED_PREPARE_SCRIPT = 'husky';
20
- const REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY = 'produck:precommit-check';
21
- const REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE = 'npm run format:check && npm run lint';
22
10
 
23
11
  const REQUIRED_PRE_COMMIT_HOOK = '#!/usr/bin/env sh\nnpm run produck:precommit-check\n';
24
12
  const REQUIRED_COMMIT_MSG_HOOK =
@@ -28,75 +16,6 @@ export function printSyncHuskyHooksHelp() {
28
16
  printTextResource(HELP_FILE);
29
17
  }
30
18
 
31
- function parseJsonFile(filePath, label) {
32
- try {
33
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
34
- } catch {
35
- console.error(`${label} is not valid JSON: ${filePath}`);
36
- process.exit(2);
37
- }
38
- }
39
-
40
- function getRequiredToolkitDevDependency() {
41
- const overrideVersion = String(process.env.PRODUCK_TOOLKIT_VERSION_OVERRIDE || '').trim();
42
- if (overrideVersion) {
43
- return overrideVersion;
44
- }
45
-
46
- const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
47
- const latestResult = spawnSync(npmCommand, ['view', '@produck/agent-toolkit', 'version'], {
48
- encoding: 'utf8',
49
- });
50
-
51
- const latestVersion = String(latestResult.stdout || '').trim();
52
- if (latestResult.status === 0 && latestVersion) {
53
- return latestVersion;
54
- }
55
-
56
- const pkg = parseJsonFile(TOOLKIT_PACKAGE_JSON, 'Toolkit package.json');
57
- const version = typeof pkg.version === 'string' ? pkg.version.trim() : '';
58
-
59
- if (!version) {
60
- console.error(`Toolkit package version is missing: ${TOOLKIT_PACKAGE_JSON}`);
61
- process.exit(2);
62
- }
63
-
64
- return version;
65
- }
66
-
67
- function loadToolingBaseline() {
68
- const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
69
- return fs.existsSync(candidatePath);
70
- });
71
-
72
- if (!toolingBaselinePath) {
73
- console.error('Tooling baseline file does not exist in expected locations:');
74
- for (const candidatePath of TOOLING_BASELINE_CANDIDATE_PATHS) {
75
- console.error(`- ${candidatePath}`);
76
- }
77
- process.exit(2);
78
- }
79
-
80
- const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
81
- const c8Version = String(baseline?.tools?.c8?.version || '').trim();
82
- const huskyVersion = String(baseline?.tools?.husky?.version || '').trim();
83
- const lernaVersion = String(baseline?.tools?.lerna?.version || '').trim();
84
-
85
- if (!c8Version || !huskyVersion || !lernaVersion) {
86
- console.error(
87
- `Tooling baseline must define fixed tools.c8/husky/lerna.version: ${toolingBaselinePath}`,
88
- );
89
- process.exit(2);
90
- }
91
-
92
- return {
93
- toolingBaselinePath,
94
- c8Version,
95
- huskyVersion,
96
- lernaVersion,
97
- };
98
- }
99
-
100
19
  function readFileIfExists(filePath) {
101
20
  if (!fs.existsSync(filePath)) {
102
21
  return null;
@@ -105,44 +24,6 @@ function readFileIfExists(filePath) {
105
24
  return fs.readFileSync(filePath, 'utf8');
106
25
  }
107
26
 
108
- function buildScriptState(pkg) {
109
- const scripts =
110
- pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
111
- ? { ...pkg.scripts }
112
- : {};
113
-
114
- return {
115
- scripts,
116
- previousPrepare: typeof scripts.prepare === 'string' ? scripts.prepare : null,
117
- previousPrecommitCheck:
118
- typeof scripts[REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY] === 'string'
119
- ? scripts[REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY]
120
- : null,
121
- };
122
- }
123
-
124
- function buildDevDependencyState(pkg) {
125
- const devDependencies =
126
- pkg.devDependencies &&
127
- typeof pkg.devDependencies === 'object' &&
128
- !Array.isArray(pkg.devDependencies)
129
- ? { ...pkg.devDependencies }
130
- : {};
131
-
132
- return {
133
- devDependencies,
134
- previousManaged: {
135
- c8: typeof devDependencies.c8 === 'string' ? devDependencies.c8 : null,
136
- husky: typeof devDependencies.husky === 'string' ? devDependencies.husky : null,
137
- lerna: typeof devDependencies.lerna === 'string' ? devDependencies.lerna : null,
138
- '@produck/agent-toolkit':
139
- typeof devDependencies['@produck/agent-toolkit'] === 'string'
140
- ? devDependencies['@produck/agent-toolkit']
141
- : null,
142
- },
143
- };
144
- }
145
-
146
27
  export function runSyncHuskyHooks(options) {
147
28
  const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
148
29
  const check = hasFlag(options, '--check');
@@ -155,24 +36,6 @@ export function runSyncHuskyHooks(options) {
155
36
  process.exit(2);
156
37
  }
157
38
 
158
- const rootPackageJsonPath = path.resolve(cwd, 'package.json');
159
- if (!fs.existsSync(rootPackageJsonPath)) {
160
- console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
161
- process.exit(2);
162
- }
163
-
164
- const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
165
- const toolingBaseline = loadToolingBaseline();
166
- const requiredToolkitDependency = getRequiredToolkitDevDependency();
167
- const requiredDevDependencies = {
168
- c8: toolingBaseline.c8Version,
169
- husky: toolingBaseline.huskyVersion,
170
- lerna: toolingBaseline.lernaVersion,
171
- '@produck/agent-toolkit': requiredToolkitDependency,
172
- };
173
- const scriptState = buildScriptState(pkg);
174
- const dependencyState = buildDevDependencyState(pkg);
175
-
176
39
  const huskyDir = path.resolve(cwd, '.husky');
177
40
  const preCommitHookPath = path.resolve(huskyDir, 'pre-commit');
178
41
  const commitMsgHookPath = path.resolve(huskyDir, 'commit-msg');
@@ -180,37 +43,11 @@ export function runSyncHuskyHooks(options) {
180
43
  const previousPreCommitHook = readFileIfExists(preCommitHookPath);
181
44
  const previousCommitMsgHook = readFileIfExists(commitMsgHookPath);
182
45
 
183
- const matchesRequiredPrepare = scriptState.previousPrepare === REQUIRED_PREPARE_SCRIPT;
184
- const matchesRequiredPrecommitCheck =
185
- scriptState.previousPrecommitCheck === REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE;
186
- const matchesRequiredManagedDevDependencies = Object.entries(requiredDevDependencies).every(
187
- ([name, version]) => {
188
- return dependencyState.previousManaged[name] === version;
189
- },
190
- );
191
46
  const matchesRequiredPreCommitHook = previousPreCommitHook === REQUIRED_PRE_COMMIT_HOOK;
192
47
  const matchesRequiredCommitMsgHook = previousCommitMsgHook === REQUIRED_COMMIT_MSG_HOOK;
193
-
194
- const requiresUpdate =
195
- !matchesRequiredPrepare ||
196
- !matchesRequiredPrecommitCheck ||
197
- !matchesRequiredManagedDevDependencies ||
198
- !matchesRequiredPreCommitHook ||
199
- !matchesRequiredCommitMsgHook;
48
+ const requiresUpdate = !matchesRequiredPreCommitHook || !matchesRequiredCommitMsgHook;
200
49
 
201
50
  if (mode === 'sync' && requiresUpdate) {
202
- scriptState.scripts.prepare = REQUIRED_PREPARE_SCRIPT;
203
- scriptState.scripts[REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY] =
204
- REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE;
205
- pkg.scripts = scriptState.scripts;
206
-
207
- for (const [name, version] of Object.entries(requiredDevDependencies)) {
208
- dependencyState.devDependencies[name] = version;
209
- }
210
- pkg.devDependencies = dependencyState.devDependencies;
211
-
212
- fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
213
-
214
51
  fs.mkdirSync(huskyDir, { recursive: true });
215
52
  fs.writeFileSync(preCommitHookPath, REQUIRED_PRE_COMMIT_HOOK, 'utf8');
216
53
  fs.writeFileSync(commitMsgHookPath, REQUIRED_COMMIT_MSG_HOOK, 'utf8');
@@ -220,28 +57,13 @@ export function runSyncHuskyHooks(options) {
220
57
  cwd,
221
58
  mode,
222
59
  ok: true,
223
- rootPackageJsonPath,
224
- toolingBaselinePath: toolingBaseline.toolingBaselinePath,
225
60
  required: {
226
- prepareScript: REQUIRED_PREPARE_SCRIPT,
227
- precommitCheckScriptKey: REQUIRED_PRECOMMIT_CHECK_SCRIPT_KEY,
228
- precommitCheckScriptValue: REQUIRED_PRECOMMIT_CHECK_SCRIPT_VALUE,
229
- managedDevDependencies: requiredDevDependencies,
230
61
  preCommitHookPath: path.relative(cwd, preCommitHookPath),
231
62
  commitMsgHookPath: path.relative(cwd, commitMsgHookPath),
232
63
  },
233
64
  status: {
234
- matchesRequiredPrepareBefore: matchesRequiredPrepare,
235
- matchesRequiredPrecommitCheckBefore: matchesRequiredPrecommitCheck,
236
- matchesRequiredManagedDevDependenciesBefore: matchesRequiredManagedDevDependencies,
237
65
  matchesRequiredPreCommitHookBefore: matchesRequiredPreCommitHook,
238
66
  matchesRequiredCommitMsgHookBefore: matchesRequiredCommitMsgHook,
239
- matchesRequiredPrepareAfter:
240
- requiresUpdate && mode === 'sync' ? true : matchesRequiredPrepare,
241
- matchesRequiredPrecommitCheckAfter:
242
- requiresUpdate && mode === 'sync' ? true : matchesRequiredPrecommitCheck,
243
- matchesRequiredManagedDevDependenciesAfter:
244
- requiresUpdate && mode === 'sync' ? true : matchesRequiredManagedDevDependencies,
245
67
  matchesRequiredPreCommitHookAfter:
246
68
  requiresUpdate && mode === 'sync' ? true : matchesRequiredPreCommitHook,
247
69
  matchesRequiredCommitMsgHookAfter:
@@ -0,0 +1,14 @@
1
+ Usage:
2
+ agent-toolkit sync-prettier-config [--cwd <dir>] [--check] [--dry-run]
3
+ [--json <file>]
4
+
5
+ Behavior:
6
+ - Applies organization-required root format script:
7
+ - scripts.produck:format = npm exec -- prettier --check . && npm run format --if-present
8
+ - Applies organization-required root Prettier config file:
9
+ - .prettierrc
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,130 @@
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 PRETTIER_CONFIG_FILE = '.prettierrc';
11
+
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() {
31
+ printTextResource(HELP_FILE);
32
+ }
33
+
34
+ function parseJsonFile(filePath, label) {
35
+ try {
36
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
37
+ } catch {
38
+ console.error(`${label} is not valid JSON: ${filePath}`);
39
+ process.exit(2);
40
+ }
41
+ }
42
+
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) {
52
+ const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
53
+ const check = hasFlag(options, '--check');
54
+ const dryRun = hasFlag(options, '--dry-run') && !check;
55
+ const jsonFile = getSingle(options, '--json', '');
56
+ const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
57
+
58
+ if (!fs.existsSync(cwd)) {
59
+ console.error(`CWD does not exist: ${cwd}`);
60
+ process.exit(2);
61
+ }
62
+
63
+ const rootPackageJsonPath = path.resolve(cwd, 'package.json');
64
+ if (!fs.existsSync(rootPackageJsonPath)) {
65
+ console.error(`Root package.json does not exist: ${rootPackageJsonPath}`);
66
+ process.exit(2);
67
+ }
68
+
69
+ const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
70
+ const scripts =
71
+ pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
72
+ ? { ...pkg.scripts }
73
+ : {};
74
+
75
+ const previousFormat =
76
+ typeof scripts[REQUIRED_FORMAT_SCRIPT_KEY] === 'string'
77
+ ? scripts[REQUIRED_FORMAT_SCRIPT_KEY]
78
+ : null;
79
+
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;
87
+
88
+ if (mode === 'sync' && requiresUpdate) {
89
+ scripts[REQUIRED_FORMAT_SCRIPT_KEY] = REQUIRED_FORMAT_SCRIPT_VALUE;
90
+ pkg.scripts = scripts;
91
+
92
+ fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
93
+ fs.writeFileSync(prettierConfigPath, REQUIRED_PRETTIER_CONFIG, 'utf8');
94
+ }
95
+
96
+ const report = {
97
+ cwd,
98
+ mode,
99
+ ok: true,
100
+ rootPackageJsonPath,
101
+ required: {
102
+ formatScriptKey: REQUIRED_FORMAT_SCRIPT_KEY,
103
+ formatScriptValue: REQUIRED_FORMAT_SCRIPT_VALUE,
104
+ prettierConfigPath: path.relative(cwd, prettierConfigPath),
105
+ },
106
+ status: {
107
+ matchesRequiredFormatBefore: matchesRequiredFormat,
108
+ matchesRequiredPrettierConfigBefore: matchesRequiredPrettierConfig,
109
+ matchesRequiredFormatAfter: requiresUpdate && mode === 'sync' ? true : matchesRequiredFormat,
110
+ matchesRequiredPrettierConfigAfter:
111
+ requiresUpdate && mode === 'sync' ? true : matchesRequiredPrettierConfig,
112
+ updated: requiresUpdate && mode === 'sync',
113
+ },
114
+ };
115
+
116
+ if (mode === 'check' && requiresUpdate) {
117
+ report.ok = false;
118
+ }
119
+
120
+ if (jsonFile) {
121
+ const outPath = path.resolve(cwd, jsonFile);
122
+ fs.mkdirSync(path.dirname(outPath), { recursive: true });
123
+ fs.writeFileSync(outPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
124
+ }
125
+
126
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
127
+ if (!report.ok) {
128
+ process.exit(2);
129
+ }
130
+ }
@@ -0,0 +1,21 @@
1
+ Usage:
2
+ agent-toolkit sync-workspace-config [--cwd <dir>] [--check] [--dry-run]
3
+ [--json <file>]
4
+
5
+ Behavior:
6
+ - Applies organization-required root shared scripts:
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
12
+ - Applies organization-required root shared managed devDependencies:
13
+ - devDependencies.c8 = <fixed-version-from-baseline>
14
+ - devDependencies.husky = <fixed-version-from-baseline>
15
+ - devDependencies.lerna = <fixed-version-from-baseline>
16
+ - devDependencies.@produck/agent-toolkit = <latest-version-resolved-at-runtime>
17
+
18
+ Rules:
19
+ - --check validates without writing and exits non-zero on mismatch
20
+ - --dry-run prints planned changes without writing
21
+ - --check takes precedence over --dry-run