@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.
- package/README.md +22 -22
- package/bin/agent-toolkit.mjs +33 -8
- package/bin/build-publish-assets.mjs +132 -24
- package/bin/command/enforce-node-baseline/index.mjs +18 -5
- package/bin/command/preflight/index.mjs +20 -4
- package/bin/command/run-capture/index.mjs +7 -1
- package/bin/command/shared/workspace-validation.mjs +9 -3
- package/bin/command/sync-coverage/index.mjs +103 -48
- package/bin/command/sync-coverage/required-c8-config.json +15 -0
- package/bin/command/sync-editorconfig/index.mjs +2 -1
- package/bin/command/sync-format/index.mjs +92 -37
- package/bin/command/sync-git/index.mjs +97 -35
- package/bin/command/sync-install/index.mjs +10 -3
- package/bin/command/sync-instructions/index.mjs +35 -10
- package/bin/command/sync-lint/eslint.config.template.mjs +32 -0
- package/bin/command/sync-lint/index.mjs +119 -60
- package/bin/command/sync-publish/help.txt +4 -2
- package/bin/command/sync-publish/index.mjs +126 -35
- package/bin/command/validate-commit-msg/index.mjs +46 -25
- package/package.json +5 -3
- package/publish-assets/eslint.config.template.mjs +32 -0
- package/publish-assets/gitignore +3 -0
- package/publish-assets/instructions/produck/20-produck-commit.instructions.md +3 -3
- package/publish-assets/instructions/produck/stale.instructions.md +1 -0
- package/publish-assets/instructions/produck/tooling-version-baseline.json +36 -1
- package/publish-assets/lerna.json +14 -0
- package/publish-assets/prettierrc +11 -0
|
@@ -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(
|
|
14
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
73
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
);
|
|
93
|
-
|
|
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
|
-
|
|
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 &&
|
|
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
|
|
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
|
|
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 ||
|
|
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
|
-
|
|
260
|
+
for (const [name, version] of Object.entries(requiredEslintDevDeps)) {
|
|
261
|
+
devDependencies[name] = version;
|
|
262
|
+
}
|
|
215
263
|
pkg.devDependencies = devDependencies;
|
|
216
264
|
|
|
217
|
-
fs.writeFileSync(
|
|
218
|
-
|
|
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
|
-
|
|
288
|
+
eslintDevDependencies: requiredEslintDevDeps,
|
|
230
289
|
eslintConfigPath: path.relative(cwd, eslintConfigPath),
|
|
231
290
|
eslintConfigAction,
|
|
232
291
|
},
|
|
233
292
|
status: {
|
|
234
293
|
matchesRequiredLintBefore: matchesRequiredLint,
|
|
235
|
-
|
|
294
|
+
matchesRequiredEslintDepsBefore: matchesRequiredEslintDeps,
|
|
236
295
|
matchesRequiredEslintConfigBefore: matchesRequiredEslintConfig,
|
|
237
296
|
matchesRequiredLintAfter:
|
|
238
297
|
requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
|
|
239
298
|
? true
|
|
240
299
|
: matchesRequiredLint,
|
|
241
|
-
|
|
300
|
+
matchesRequiredEslintDepsAfter:
|
|
242
301
|
requiresUpdate && mode === 'sync' && !hasUnpatchableEslintConfig
|
|
243
302
|
? true
|
|
244
|
-
:
|
|
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
|
|
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:
|
|
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
|
|
18
|
-
|
|
19
|
-
const
|
|
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
|
|
42
|
-
|
|
43
|
-
return
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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 &&
|
|
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 =
|
|
105
|
-
|
|
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 ||
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
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:
|
|
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'
|
|
222
|
+
!matchesRequiredPublishCheck && mode === 'sync'
|
|
223
|
+
? true
|
|
224
|
+
: matchesRequiredPublishCheck,
|
|
136
225
|
matchesRequiredPublishBefore: matchesRequiredPublish,
|
|
137
226
|
matchesRequiredPublishAfter:
|
|
138
|
-
!matchesRequiredPublish && mode === 'sync'
|
|
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 = [
|
|
9
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
103
|
-
|
|
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
|
|
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(
|
|
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
|
|
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);
|