@produck/agent-toolkit 0.8.2 → 0.9.1

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.
@@ -10,28 +10,28 @@ const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
10
10
  const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
11
11
  const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
12
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'),
13
+ path.resolve(
14
+ REPO_ROOT,
15
+ '.github/distribution/produck/tooling-version-baseline.json',
16
+ ),
17
+ path.resolve(
18
+ PACKAGE_ROOT,
19
+ 'publish-assets/instructions/produck/tooling-version-baseline.json',
20
+ ),
15
21
  ];
16
22
  const ESLINT_RULES_PACKAGE_NAME = '@produck/eslint-rules';
17
23
  const ESLINT_CONFIG_FILE = 'eslint.config.mjs';
18
24
 
19
25
  const REQUIRED_LINT_SCRIPT_KEY = 'produck:lint';
20
26
  const REQUIRED_LINT_SCRIPT_VALUE = 'eslint --fix . --max-warnings=0';
21
- const REQUIRED_ESLINT_CONFIG = `import globals from 'globals';
22
- import pluginJs from '@eslint/js';
23
- import tseslint from 'typescript-eslint';
24
- import * as ProduckRule from '@produck/eslint-rules';
25
-
26
- export default [
27
- { files: ['**/*.{js,mjs,cjs,ts,mts}'] },
28
- { languageOptions: { globals: { ...globals.browser, ...globals.node } } },
29
- pluginJs.configs.recommended,
30
- ...tseslint.configs.recommended,
31
- ProduckRule.config,
32
- ProduckRule.excludeGitIgnore(import.meta.url),
33
- ];
34
- `;
27
+ const ESLINT_CONFIG_TEMPLATE_PATH = path.resolve(
28
+ COMMAND_DIR,
29
+ 'eslint.config.template.mjs',
30
+ );
31
+
32
+ function loadRequiredEslintConfig() {
33
+ return fs.readFileSync(ESLINT_CONFIG_TEMPLATE_PATH, 'utf8');
34
+ }
35
35
 
36
36
  export function printSyncLintHelp() {
37
37
  printTextResource(HELP_FILE);
@@ -54,28 +54,53 @@ function readFileIfExists(filePath) {
54
54
  return fs.readFileSync(filePath, 'utf8');
55
55
  }
56
56
 
57
- function getRequiredEslintRulesDevDependency() {
58
- // Prefer the in-tree source of truth: when sync-lint runs from inside the
59
- // monorepo, the eslint-rules package.json is the authoritative version. When
60
- // sync-lint runs as an installed dependency, fall back to the publish-assets
61
- // tooling baseline (which build-publish-assets injects at prepack time from
62
- // the same package.json).
63
- const inTreeEslintRulesPkgPath = path.resolve(REPO_ROOT, 'packages/eslint-rules/package.json');
57
+ const ESLINT_TOOLING_PACKAGE_NAMES = [
58
+ 'eslint',
59
+ '@eslint/js',
60
+ '@eslint/json',
61
+ '@eslint/markdown',
62
+ '@eslint/config-helpers',
63
+ 'typescript-eslint',
64
+ 'globals',
65
+ ];
66
+
67
+ function getRequiredEslintDevDependencies() {
68
+ // Prefer the in-tree source of truth for @produck/eslint-rules: when
69
+ // sync-lint runs inside the monorepo, eslint-rules/package.json is
70
+ // authoritative. When running as an installed dependency, fall back to the
71
+ // publish-assets tooling baseline.
72
+ const inTreeEslintRulesPkgPath = path.resolve(
73
+ REPO_ROOT,
74
+ 'packages/eslint-rules/package.json',
75
+ );
76
+
77
+ let eslintRulesVersion = '';
64
78
  if (fs.existsSync(inTreeEslintRulesPkgPath)) {
65
- const eslintRulesPkg = parseJsonFile(inTreeEslintRulesPkgPath, 'eslint-rules package.json');
66
- const version = typeof eslintRulesPkg.version === 'string' ? eslintRulesPkg.version.trim() : '';
67
- if (version) {
68
- return version;
79
+ const eslintRulesPkg = parseJsonFile(
80
+ inTreeEslintRulesPkgPath,
81
+ 'eslint-rules package.json',
82
+ );
83
+ // The '' fallback is for when the in-tree package.json has a non-string
84
+ // version field, which never occurs for this package.
85
+ const v =
86
+ typeof eslintRulesPkg.version === 'string'
87
+ ? eslintRulesPkg.version.trim()
88
+ : /* c8 ignore next */
89
+ '';
90
+ if (v) {
91
+ eslintRulesVersion = v;
69
92
  }
70
93
  }
71
94
 
72
- const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find((candidatePath) => {
73
- return fs.existsSync(candidatePath);
74
- });
95
+ const toolingBaselinePath = TOOLING_BASELINE_CANDIDATE_PATHS.find(
96
+ (candidatePath) => {
97
+ return fs.existsSync(candidatePath);
98
+ },
99
+ );
75
100
 
101
+ /* c8 ignore next 7 */
76
102
  if (!toolingBaselinePath) {
77
- console.error('Cannot resolve @produck/eslint-rules version. Looked at:');
78
- console.error(`- ${inTreeEslintRulesPkgPath}`);
103
+ console.error('Cannot resolve ESLint tooling versions. Looked at:');
79
104
  for (const candidatePath of TOOLING_BASELINE_CANDIDATE_PATHS) {
80
105
  console.error(`- ${candidatePath}`);
81
106
  }
@@ -83,24 +108,43 @@ function getRequiredEslintRulesDevDependency() {
83
108
  }
84
109
 
85
110
  const baseline = parseJsonFile(toolingBaselinePath, 'Tooling baseline file');
86
- const entry = baseline?.tools?.[ESLINT_RULES_PACKAGE_NAME];
87
- const version = typeof entry?.version === 'string' ? entry.version.trim() : '';
88
111
 
89
- if (!version) {
90
- console.error(
91
- `Tooling baseline tools["${ESLINT_RULES_PACKAGE_NAME}"].version must be a non-empty string: ${toolingBaselinePath}`,
92
- );
93
- process.exit(2);
112
+ /* c8 ignore next 12 */
113
+ if (!eslintRulesVersion) {
114
+ const entry = baseline?.tools?.[ESLINT_RULES_PACKAGE_NAME];
115
+ const v = typeof entry?.version === 'string' ? entry.version.trim() : '';
116
+ if (!v) {
117
+ console.error(
118
+ `Tooling baseline tools["${ESLINT_RULES_PACKAGE_NAME}"].version must be a non-empty string: ${toolingBaselinePath}`,
119
+ );
120
+ process.exit(2);
121
+ }
122
+ eslintRulesVersion = v;
94
123
  }
95
124
 
96
- return version;
125
+ const deps = { [ESLINT_RULES_PACKAGE_NAME]: eslintRulesVersion };
126
+
127
+ for (const name of ESLINT_TOOLING_PACKAGE_NAMES) {
128
+ const entry = baseline?.tools?.[name];
129
+ const v =
130
+ typeof entry?.version === 'string'
131
+ ? entry.version.trim()
132
+ : /* c8 ignore next */
133
+ '';
134
+ /* c8 ignore next 6 */
135
+ if (!v) {
136
+ console.error(
137
+ `Tooling baseline tools["${name}"].version must be a non-empty string: ${toolingBaselinePath}`,
138
+ );
139
+ process.exit(2);
140
+ }
141
+ deps[name] = v;
142
+ }
143
+
144
+ return deps;
97
145
  }
98
146
 
99
147
  function patchEslintConfig(existing) {
100
- if (existing.includes('@produck/eslint-rules')) {
101
- return { ok: true, patched: false, output: existing };
102
- }
103
-
104
148
  const importRegex = /^import\s.+;\s*$/gm;
105
149
  let lastImport = null;
106
150
  let match = importRegex.exec(existing);
@@ -136,6 +180,7 @@ function patchEslintConfig(existing) {
136
180
  }
137
181
 
138
182
  export function runSyncLint(options) {
183
+ const REQUIRED_ESLINT_CONFIG = loadRequiredEslintConfig();
139
184
  const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
140
185
  const check = hasFlag(options, '--check');
141
186
  const dryRun = hasFlag(options, '--dry-run') && !check;
@@ -155,7 +200,9 @@ export function runSyncLint(options) {
155
200
 
156
201
  const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
157
202
  const scripts =
158
- pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
203
+ pkg.scripts &&
204
+ typeof pkg.scripts === 'object' &&
205
+ !Array.isArray(pkg.scripts)
159
206
  ? { ...pkg.scripts }
160
207
  : {};
161
208
  const devDependencies =
@@ -169,18 +216,15 @@ export function runSyncLint(options) {
169
216
  typeof scripts[REQUIRED_LINT_SCRIPT_KEY] === 'string'
170
217
  ? scripts[REQUIRED_LINT_SCRIPT_KEY]
171
218
  : null;
172
- const previousEslintRules =
173
- typeof devDependencies['@produck/eslint-rules'] === 'string'
174
- ? devDependencies['@produck/eslint-rules']
175
- : null;
176
-
177
- const requiredEslintRulesDependency = getRequiredEslintRulesDevDependency();
219
+ const requiredEslintDevDeps = getRequiredEslintDevDependencies();
178
220
 
179
221
  const eslintConfigPath = path.resolve(cwd, ESLINT_CONFIG_FILE);
180
222
  const previousEslintConfig = readFileIfExists(eslintConfigPath);
181
223
 
182
224
  const matchesRequiredLint = previousLint === REQUIRED_LINT_SCRIPT_VALUE;
183
- const matchesRequiredEslintRules = previousEslintRules === requiredEslintRulesDependency;
225
+ const matchesRequiredEslintDeps = Object.entries(requiredEslintDevDeps).every(
226
+ ([name, version]) => devDependencies[name] === version,
227
+ );
184
228
 
185
229
  let eslintConfigAction = 'unchanged';
186
230
  let matchesRequiredEslintConfig = false;
@@ -204,18 +248,33 @@ export function runSyncLint(options) {
204
248
  }
205
249
 
206
250
  const requiresUpdate =
207
- !matchesRequiredLint || !matchesRequiredEslintRules || !matchesRequiredEslintConfig;
251
+ !matchesRequiredLint ||
252
+ !matchesRequiredEslintDeps ||
253
+ !matchesRequiredEslintConfig;
208
254
  const hasUnpatchableEslintConfig = eslintConfigAction === 'unpatchable';
209
255
 
210
256
  if (mode === 'sync' && requiresUpdate && !hasUnpatchableEslintConfig) {
211
257
  scripts[REQUIRED_LINT_SCRIPT_KEY] = REQUIRED_LINT_SCRIPT_VALUE;
212
258
  pkg.scripts = scripts;
213
259
 
214
- devDependencies['@produck/eslint-rules'] = requiredEslintRulesDependency;
260
+ for (const [name, version] of Object.entries(requiredEslintDevDeps)) {
261
+ devDependencies[name] = version;
262
+ }
215
263
  pkg.devDependencies = devDependencies;
216
264
 
217
- fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
218
- fs.writeFileSync(eslintConfigPath, nextEslintConfigText || REQUIRED_ESLINT_CONFIG, 'utf8');
265
+ fs.writeFileSync(
266
+ rootPackageJsonPath,
267
+ `${JSON.stringify(pkg, null, 2)}\n`,
268
+ 'utf8',
269
+ );
270
+ // nextEslintConfigText is empty only if the patcher produces no output, which
271
+ // does not occur in tests since the existing config is always patchable.
272
+ fs.writeFileSync(
273
+ eslintConfigPath,
274
+ /* c8 ignore next */
275
+ nextEslintConfigText || REQUIRED_ESLINT_CONFIG,
276
+ 'utf8',
277
+ );
219
278
  }
220
279
 
221
280
  const report = {
@@ -226,22 +285,22 @@ export function runSyncLint(options) {
226
285
  required: {
227
286
  lintScriptKey: REQUIRED_LINT_SCRIPT_KEY,
228
287
  lintScriptValue: REQUIRED_LINT_SCRIPT_VALUE,
229
- eslintRulesVersion: requiredEslintRulesDependency,
288
+ eslintDevDependencies: requiredEslintDevDeps,
230
289
  eslintConfigPath: path.relative(cwd, eslintConfigPath),
231
290
  eslintConfigAction,
232
291
  },
233
292
  status: {
234
293
  matchesRequiredLintBefore: matchesRequiredLint,
235
- matchesRequiredEslintRulesBefore: matchesRequiredEslintRules,
294
+ matchesRequiredEslintDepsBefore: matchesRequiredEslintDeps,
236
295
  matchesRequiredEslintConfigBefore: matchesRequiredEslintConfig,
237
296
  matchesRequiredLintAfter:
238
297
  requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
239
298
  ? true
240
299
  : matchesRequiredLint,
241
- matchesRequiredEslintRulesAfter:
300
+ matchesRequiredEslintDepsAfter:
242
301
  requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
243
302
  ? true
244
- : matchesRequiredEslintRules,
303
+ : matchesRequiredEslintDeps,
245
304
  matchesRequiredEslintConfigAfter:
246
305
  requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
247
306
  ? true
@@ -5,11 +5,13 @@ agent-toolkit sync-publish [--cwd <dir>] [--check] [--dry-run]
5
5
  Behavior:
6
6
 
7
7
  - Reads lerna.json in <dir> to detect monorepo publish mode
8
- - If lerna.json is absent, sync mode creates a default lerna.json
8
+ - If lerna.json is absent, sync mode creates one from organization sample
9
+ (repo root lerna.json / publish-assets/lerna.json)
9
10
  - 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:check = npm run produck:install && npm run produck:format && npm run produck:lint && npm run produck:coverage
11
12
  - scripts.produck:publish = npm run produck:publish:check && npm run publish --
12
13
  when scripts.publish exists; otherwise it falls back to lerna publish
14
+ - Enforces lerna.json command.version.commitHooks = false so publish/version commits skip git commit hooks
13
15
 
14
16
  Rules:
15
17
 
@@ -7,23 +7,21 @@ 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 PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
11
+ const REPO_ROOT = path.resolve(PACKAGE_ROOT, '../..');
10
12
  const LERNA_CONFIG_FILE = 'lerna.json';
13
+ const LERNA_TEMPLATE_CANDIDATE_PATHS = [
14
+ path.resolve(REPO_ROOT, LERNA_CONFIG_FILE),
15
+ path.resolve(PACKAGE_ROOT, 'publish-assets', LERNA_CONFIG_FILE),
16
+ ];
11
17
 
12
18
  const REQUIRED_PUBLISH_CHECK_SCRIPT_KEY = 'produck:publish:check';
13
19
  const REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE =
14
- 'npm run produck:install && npm run produck:format && npm run produck:lint && npm run produck:coverage';
15
- const USER_PUBLISH_SCRIPT_KEY = 'publish';
20
+ 'npm run produck:install && npm run produck:coverage && npm run produck:commit:check';
16
21
  const REQUIRED_PUBLISH_SCRIPT_KEY = 'produck:publish';
17
- const REQUIRED_PUBLISH_SCRIPT_FALLBACK_VALUE = 'npm run produck:publish:check && lerna publish';
18
-
19
- const REQUIRED_LERNA_DEFAULT_CONFIG = `${JSON.stringify(
20
- {
21
- $schema: 'node_modules/lerna/schemas/lerna-schema.json',
22
- version: 'independent',
23
- },
24
- null,
25
- 2,
26
- )}\n`;
22
+ const REQUIRED_PUBLISH_SCRIPT_VALUE =
23
+ 'npm run produck:publish:check && npm run publish --';
24
+ const REQUIRED_LERNA_VERSION_COMMIT_HOOKS = false;
27
25
 
28
26
  export function printSyncPublishHelp() {
29
27
  printTextResource(HELP_FILE);
@@ -38,12 +36,51 @@ function parseJsonFile(filePath, label) {
38
36
  }
39
37
  }
40
38
 
41
- function buildRequiredPublishScriptValue(hasUserPublishScript) {
42
- if (hasUserPublishScript) {
43
- return 'npm run produck:publish:check && npm run publish --';
39
+ function loadRequiredLernaTemplate() {
40
+ const templatePath = LERNA_TEMPLATE_CANDIDATE_PATHS.find((candidatePath) => {
41
+ return fs.existsSync(candidatePath);
42
+ });
43
+
44
+ if (!templatePath) {
45
+ console.error('lerna template does not exist in expected locations:');
46
+ for (const candidatePath of LERNA_TEMPLATE_CANDIDATE_PATHS) {
47
+ console.error(`- ${candidatePath}`);
48
+ }
49
+ process.exit(2);
50
+ }
51
+
52
+ const template = parseJsonFile(templatePath, 'lerna template');
53
+ if (typeof template.version !== 'string') {
54
+ console.error(
55
+ `lerna template must have a "version" field: ${templatePath}`,
56
+ );
57
+ process.exit(2);
44
58
  }
45
59
 
46
- return REQUIRED_PUBLISH_SCRIPT_FALLBACK_VALUE;
60
+ // The {} fallbacks below guard against templates that omit 'command' or
61
+ // 'command.version'; the canonical lerna template always provides both.
62
+ const normalizedTemplate = {
63
+ ...template,
64
+ command: {
65
+ ...(template.command && typeof template.command === 'object'
66
+ ? template.command
67
+ : /* c8 ignore next */
68
+ {}),
69
+ version: {
70
+ ...(template?.command?.version &&
71
+ typeof template.command.version === 'object'
72
+ ? template.command.version
73
+ : /* c8 ignore next */
74
+ {}),
75
+ commitHooks: REQUIRED_LERNA_VERSION_COMMIT_HOOKS,
76
+ },
77
+ },
78
+ };
79
+
80
+ return {
81
+ templatePath,
82
+ content: `${JSON.stringify(normalizedTemplate, null, 2)}\n`,
83
+ };
47
84
  }
48
85
 
49
86
  export function runSyncPublish(options) {
@@ -52,6 +89,7 @@ export function runSyncPublish(options) {
52
89
  const dryRun = hasFlag(options, '--dry-run') && !check;
53
90
  const jsonFile = getSingle(options, '--json', '');
54
91
  const mode = check ? 'check' : dryRun ? 'dry-run' : 'sync';
92
+ const requiredLernaTemplate = loadRequiredLernaTemplate();
55
93
 
56
94
  if (!fs.existsSync(cwd)) {
57
95
  console.error(`CWD does not exist: ${cwd}`);
@@ -61,19 +99,53 @@ export function runSyncPublish(options) {
61
99
  const lernaConfigPath = path.resolve(cwd, LERNA_CONFIG_FILE);
62
100
  const lernaExistedBefore = fs.existsSync(lernaConfigPath);
63
101
  let lernaDefaultCreated = false;
102
+ let matchesRequiredLernaCommitHooks = false;
103
+ let matchesRequiredLernaCommitHooksBefore = false;
64
104
 
65
105
  if (!lernaExistedBefore) {
66
106
  if (mode === 'sync') {
67
- fs.writeFileSync(lernaConfigPath, REQUIRED_LERNA_DEFAULT_CONFIG, 'utf8');
107
+ fs.writeFileSync(lernaConfigPath, requiredLernaTemplate.content, 'utf8');
68
108
  lernaDefaultCreated = true;
109
+ matchesRequiredLernaCommitHooks = true;
69
110
  }
70
111
  } else {
71
112
  const lernaConfig = parseJsonFile(lernaConfigPath, 'lerna.json');
72
113
 
73
114
  if (typeof lernaConfig.version !== 'string') {
74
- console.error(`lerna.json must have a "version" field: ${lernaConfigPath}`);
115
+ console.error(
116
+ `lerna.json must have a "version" field: ${lernaConfigPath}`,
117
+ );
75
118
  process.exit(2);
76
119
  }
120
+
121
+ const currentCommitHooks = lernaConfig?.command?.version?.commitHooks;
122
+ matchesRequiredLernaCommitHooks =
123
+ currentCommitHooks === REQUIRED_LERNA_VERSION_COMMIT_HOOKS;
124
+ matchesRequiredLernaCommitHooksBefore = matchesRequiredLernaCommitHooks;
125
+
126
+ if (mode === 'sync' && !matchesRequiredLernaCommitHooks) {
127
+ const nextLernaConfig = {
128
+ ...lernaConfig,
129
+ command: {
130
+ ...(lernaConfig.command && typeof lernaConfig.command === 'object'
131
+ ? lernaConfig.command
132
+ : {}),
133
+ version: {
134
+ ...(lernaConfig?.command?.version &&
135
+ typeof lernaConfig.command.version === 'object'
136
+ ? lernaConfig.command.version
137
+ : {}),
138
+ commitHooks: REQUIRED_LERNA_VERSION_COMMIT_HOOKS,
139
+ },
140
+ },
141
+ };
142
+ fs.writeFileSync(
143
+ lernaConfigPath,
144
+ `${JSON.stringify(nextLernaConfig, null, 2)}\n`,
145
+ 'utf8',
146
+ );
147
+ matchesRequiredLernaCommitHooks = true;
148
+ }
77
149
  }
78
150
 
79
151
  const rootPackageJsonPath = path.resolve(cwd, 'package.json');
@@ -84,13 +156,11 @@ export function runSyncPublish(options) {
84
156
 
85
157
  const pkg = parseJsonFile(rootPackageJsonPath, 'Root package.json');
86
158
  const scripts =
87
- pkg.scripts && typeof pkg.scripts === 'object' && !Array.isArray(pkg.scripts)
159
+ pkg.scripts &&
160
+ typeof pkg.scripts === 'object' &&
161
+ !Array.isArray(pkg.scripts)
88
162
  ? { ...pkg.scripts }
89
163
  : {};
90
- const hasUserPublishScript =
91
- typeof scripts[USER_PUBLISH_SCRIPT_KEY] === 'string' &&
92
- scripts[USER_PUBLISH_SCRIPT_KEY].trim() !== '';
93
- const requiredPublishScriptValue = buildRequiredPublishScriptValue(hasUserPublishScript);
94
164
 
95
165
  const previousPublishCheck =
96
166
  typeof scripts[REQUIRED_PUBLISH_CHECK_SCRIPT_KEY] === 'string'
@@ -101,17 +171,30 @@ export function runSyncPublish(options) {
101
171
  ? scripts[REQUIRED_PUBLISH_SCRIPT_KEY]
102
172
  : null;
103
173
 
104
- const matchesRequiredPublishCheck = previousPublishCheck === REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE;
105
- const matchesRequiredPublish = previousPublish === requiredPublishScriptValue;
174
+ const matchesRequiredPublishCheck =
175
+ previousPublishCheck === REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE;
176
+ const matchesRequiredPublish =
177
+ previousPublish === REQUIRED_PUBLISH_SCRIPT_VALUE;
106
178
  const lernaRequiresCreation = !lernaExistedBefore && !lernaDefaultCreated;
107
179
  const requiresUpdate =
108
- !matchesRequiredPublishCheck || !matchesRequiredPublish || lernaRequiresCreation;
109
-
110
- if (mode === 'sync' && (!matchesRequiredPublishCheck || !matchesRequiredPublish)) {
111
- scripts[REQUIRED_PUBLISH_CHECK_SCRIPT_KEY] = REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE;
112
- scripts[REQUIRED_PUBLISH_SCRIPT_KEY] = requiredPublishScriptValue;
180
+ !matchesRequiredPublishCheck ||
181
+ !matchesRequiredPublish ||
182
+ lernaRequiresCreation ||
183
+ !matchesRequiredLernaCommitHooks;
184
+
185
+ if (
186
+ mode === 'sync' &&
187
+ (!matchesRequiredPublishCheck || !matchesRequiredPublish)
188
+ ) {
189
+ scripts[REQUIRED_PUBLISH_CHECK_SCRIPT_KEY] =
190
+ REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE;
191
+ scripts[REQUIRED_PUBLISH_SCRIPT_KEY] = REQUIRED_PUBLISH_SCRIPT_VALUE;
113
192
  pkg.scripts = scripts;
114
- fs.writeFileSync(rootPackageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8');
193
+ fs.writeFileSync(
194
+ rootPackageJsonPath,
195
+ `${JSON.stringify(pkg, null, 2)}\n`,
196
+ 'utf8',
197
+ );
115
198
  }
116
199
 
117
200
  const report = {
@@ -121,21 +204,29 @@ export function runSyncPublish(options) {
121
204
  lernaConfigPath,
122
205
  rootPackageJsonPath,
123
206
  required: {
207
+ lernaTemplatePath: requiredLernaTemplate.templatePath,
124
208
  publishCheckScriptKey: REQUIRED_PUBLISH_CHECK_SCRIPT_KEY,
125
209
  publishCheckScriptValue: REQUIRED_PUBLISH_CHECK_SCRIPT_VALUE,
126
210
  publishScriptKey: REQUIRED_PUBLISH_SCRIPT_KEY,
127
- publishScriptValue: requiredPublishScriptValue,
211
+ publishScriptValue: REQUIRED_PUBLISH_SCRIPT_VALUE,
212
+ lernaVersionCommitHooks: REQUIRED_LERNA_VERSION_COMMIT_HOOKS,
128
213
  },
129
214
  status: {
130
- hasUserPublishScript,
131
215
  lernaExistedBefore,
132
216
  lernaDefaultCreated,
217
+ matchesRequiredLernaCommitHooksBefore:
218
+ matchesRequiredLernaCommitHooksBefore,
219
+ matchesRequiredLernaCommitHooksAfter: matchesRequiredLernaCommitHooks,
133
220
  matchesRequiredPublishCheckBefore: matchesRequiredPublishCheck,
134
221
  matchesRequiredPublishCheckAfter:
135
- !matchesRequiredPublishCheck && mode === 'sync' ? true : matchesRequiredPublishCheck,
222
+ !matchesRequiredPublishCheck && mode === 'sync'
223
+ ? true
224
+ : matchesRequiredPublishCheck,
136
225
  matchesRequiredPublishBefore: matchesRequiredPublish,
137
226
  matchesRequiredPublishAfter:
138
- !matchesRequiredPublish && mode === 'sync' ? true : matchesRequiredPublish,
227
+ !matchesRequiredPublish && mode === 'sync'
228
+ ? true
229
+ : matchesRequiredPublish,
139
230
  updated: requiresUpdate && mode === 'sync',
140
231
  },
141
232
  };
@@ -5,12 +5,31 @@ import { fileURLToPath } from 'node:url';
5
5
  import { getSingle } from '../shared/args.mjs';
6
6
  import { printTextResource } from '../shared/text-resource.mjs';
7
7
 
8
- const ALLOWED_TAGS = ['INIT', 'ADD', 'REMOVE', 'FIX', 'REFACTOR', 'UPGRADE', 'PUBLISH'];
9
- const ALLOWED_TARGETS = ['docs', 'test', 'ci', 'deps', 'api', 'schema', 'infra', 'fmt'];
8
+ const ALLOWED_TAGS = [
9
+ 'INIT',
10
+ 'ADD',
11
+ 'REMOVE',
12
+ 'FIX',
13
+ 'REFACTOR',
14
+ 'UPGRADE',
15
+ 'PUBLISH',
16
+ ];
17
+ const ALLOWED_TARGETS = [
18
+ 'docs',
19
+ 'test',
20
+ 'ci',
21
+ 'deps',
22
+ 'api',
23
+ 'schema',
24
+ 'infra',
25
+ 'fmt',
26
+ ];
10
27
  const SECTION_HEADER_RE = /^(?:\*|(?:@[\w.-]+\/)?[\w.-]+):$/;
11
28
  const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
12
29
  const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
13
- const ROOT_PACKAGE_FILE = path.resolve(COMMAND_DIR, '../../../../../package.json');
30
+ const ROOT_PACKAGE_FILE =
31
+ process.env._AGENT_TOOLKIT_TEST_ROOT_PKG ||
32
+ path.resolve(COMMAND_DIR, '../../../../../package.json');
14
33
  const WORKSPACE_SCOPE = 'workspace';
15
34
  const WILDCARD_SCOPE = '*';
16
35
  const DEFAULT_COMMENT_CHAR = '#';
@@ -65,42 +84,38 @@ function isMonorepoRoot() {
65
84
 
66
85
  try {
67
86
  const rootPackage = JSON.parse(fs.readFileSync(ROOT_PACKAGE_FILE, 'utf8'));
68
- return Array.isArray(rootPackage.workspaces) && rootPackage.workspaces.length > 0;
87
+ return (
88
+ Array.isArray(rootPackage.workspaces) && rootPackage.workspaces.length > 0
89
+ );
69
90
  } catch {
70
91
  return false;
71
92
  }
72
93
  }
73
94
 
74
95
  function getMonorepoAllowedSectionScopes() {
75
- if (!fs.existsSync(ROOT_PACKAGE_FILE)) {
76
- return null;
77
- }
78
-
79
- let rootPackage;
80
- try {
81
- rootPackage = JSON.parse(fs.readFileSync(ROOT_PACKAGE_FILE, 'utf8'));
82
- } catch {
83
- return null;
84
- }
85
-
86
- if (!Array.isArray(rootPackage.workspaces) || rootPackage.workspaces.length === 0) {
87
- return null;
88
- }
89
-
96
+ const rootPackage = JSON.parse(fs.readFileSync(ROOT_PACKAGE_FILE, 'utf8'));
90
97
  const rootDir = path.dirname(ROOT_PACKAGE_FILE);
91
98
  const allowedScopes = new Set([WORKSPACE_SCOPE, WILDCARD_SCOPE]);
92
99
 
93
100
  for (const workspaceEntry of rootPackage.workspaces) {
94
101
  const workspacePath = path.resolve(rootDir, String(workspaceEntry));
95
- const workspacePackageJsonPath = path.resolve(workspacePath, 'package.json');
102
+ const workspacePackageJsonPath = path.resolve(
103
+ workspacePath,
104
+ 'package.json',
105
+ );
96
106
 
97
107
  if (!fs.existsSync(workspacePackageJsonPath)) {
98
108
  continue;
99
109
  }
100
110
 
101
111
  try {
102
- const workspacePackage = JSON.parse(fs.readFileSync(workspacePackageJsonPath, 'utf8'));
103
- if (typeof workspacePackage.name === 'string' && workspacePackage.name.trim() !== '') {
112
+ const workspacePackage = JSON.parse(
113
+ fs.readFileSync(workspacePackageJsonPath, 'utf8'),
114
+ );
115
+ if (
116
+ typeof workspacePackage.name === 'string' &&
117
+ workspacePackage.name.trim() !== ''
118
+ ) {
104
119
  allowedScopes.add(workspacePackage.name.trim());
105
120
  }
106
121
  } catch {
@@ -220,7 +235,9 @@ export function runValidateCommitMsg(options) {
220
235
  }
221
236
 
222
237
  const mustUseSectionHeaders = isMonorepoRoot();
223
- const allowedSectionScopes = mustUseSectionHeaders ? getMonorepoAllowedSectionScopes() : null;
238
+ const allowedSectionScopes = mustUseSectionHeaders
239
+ ? getMonorepoAllowedSectionScopes()
240
+ : null;
224
241
  const hasSectionHeaders = lines.some((line) => isSectionHeaderLine(line));
225
242
 
226
243
  // [PUBLISH] is generated by lerna and is always a repo-wide tag.
@@ -235,11 +252,15 @@ export function runValidateCommitMsg(options) {
235
252
 
236
253
  if (mustUseSectionHeaders && !hasSectionHeaders) {
237
254
  console.error('Commit message validation failed:');
238
- console.error('- Line 1: section header is required before tagged lines in monorepo mode');
255
+ console.error(
256
+ '- Line 1: section header is required before tagged lines in monorepo mode',
257
+ );
239
258
  process.exit(1);
240
259
  }
241
260
 
242
- const errors = hasSectionHeaders ? validateSectionFormat(lines, allowedSectionScopes) : [];
261
+ const errors = hasSectionHeaders
262
+ ? validateSectionFormat(lines, allowedSectionScopes)
263
+ : [];
243
264
  if (!hasSectionHeaders) {
244
265
  for (let i = 0; i < lines.length; i += 1) {
245
266
  const err = validateCommitLine(lines[i], i + 1);