@produck/agent-toolkit 0.2.0 → 0.3.3
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 +53 -9
- package/bin/agent-toolkit.mjs +76 -654
- package/bin/build-publish-assets.mjs +49 -0
- package/bin/command/enforce-node-baseline/help.txt +19 -0
- package/bin/command/enforce-node-baseline/index.mjs +155 -0
- package/{templates/help/main.txt → bin/command/main/help.txt} +7 -0
- package/bin/command/main/index.mjs +11 -0
- package/bin/command/preflight/help.txt +9 -0
- package/bin/command/preflight/index.mjs +147 -0
- package/bin/command/run-capture/index.mjs +100 -0
- package/bin/command/shared/args.mjs +45 -0
- package/bin/command/shared/text-resource.mjs +19 -0
- package/bin/command/summarize-log/index.mjs +64 -0
- package/bin/command/sync-coverage-script/help.txt +22 -0
- package/bin/command/sync-coverage-script/index.mjs +275 -0
- package/bin/command/sync-husky-hooks/help.txt +22 -0
- package/bin/command/sync-husky-hooks/index.mjs +267 -0
- package/bin/command/sync-instructions/index.mjs +272 -0
- package/bin/command/validate-commit-msg/help.txt +9 -0
- package/bin/command/validate-commit-msg/index.mjs +183 -0
- package/package.json +5 -3
- package/publish-assets/instructions/produck/00-produck-base.instructions.md +37 -39
- package/publish-assets/instructions/produck/10-produck-node.instructions.md +130 -26
- package/publish-assets/instructions/produck/15-produck-workspace.instructions.md +59 -27
- package/publish-assets/instructions/produck/20-produck-commit.instructions.md +24 -8
- package/publish-assets/instructions/produck/tooling-version-baseline.json +32 -0
- package/templates/help/preflight.txt +0 -3
- package/templates/help/validate-commit-msg.txt +0 -7
- /package/{templates/help/run-capture.txt → bin/command/run-capture/help.txt} +0 -0
- /package/{templates/help/summarize-log.txt → bin/command/summarize-log/help.txt} +0 -0
- /package/{templates/help/sync-instructions.txt → bin/command/sync-instructions/help.txt} +0 -0
- /package/{templates → bin/command/sync-instructions}/user-space-bootstrap.md +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
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 { loadTextResource, printTextResource } from '../shared/text-resource.mjs';
|
|
7
|
+
|
|
8
|
+
const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const PACKAGE_ROOT = path.resolve(COMMAND_DIR, '../../..');
|
|
10
|
+
const PUBLISH_ASSETS_ROOT = path.resolve(PACKAGE_ROOT, 'publish-assets');
|
|
11
|
+
const PUBLISH_INSTRUCTIONS_ROOT = path.resolve(PUBLISH_ASSETS_ROOT, 'instructions');
|
|
12
|
+
const PUBLISH_NAMESPACE_ROOT = path.resolve(PUBLISH_INSTRUCTIONS_ROOT, 'produck');
|
|
13
|
+
const MANAGED_MARKER = '<!-- managed-by: @produck/agent-toolkit -->';
|
|
14
|
+
const DEFAULT_NAMESPACE_OUT_DIR = '.github/instructions/produck';
|
|
15
|
+
const USER_SPACE_ENTRYPOINT = '.github/copilot-instructions.md';
|
|
16
|
+
const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
|
|
17
|
+
const USER_SPACE_BOOTSTRAP_FILE = path.resolve(COMMAND_DIR, 'user-space-bootstrap.md');
|
|
18
|
+
|
|
19
|
+
export function printSyncInstructionsHelp() {
|
|
20
|
+
printTextResource(HELP_FILE);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadDefaultInstructionsTemplate() {
|
|
24
|
+
if (fs.existsSync(PUBLISH_NAMESPACE_ROOT)) {
|
|
25
|
+
const names = fs
|
|
26
|
+
.readdirSync(PUBLISH_NAMESPACE_ROOT)
|
|
27
|
+
.filter((name) => name.endsWith('.instructions.md'))
|
|
28
|
+
.sort((a, b) => a.localeCompare(b));
|
|
29
|
+
const entries = names.map((name) => {
|
|
30
|
+
const abs = path.resolve(PUBLISH_NAMESPACE_ROOT, name);
|
|
31
|
+
let text = fs.readFileSync(abs, 'utf8');
|
|
32
|
+
if (!text.endsWith('\n')) {
|
|
33
|
+
text = `${text}\n`;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
fileName: name,
|
|
37
|
+
content: text,
|
|
38
|
+
sourcePath: abs,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
type: 'dir',
|
|
43
|
+
sourcePath: PUBLISH_NAMESPACE_ROOT,
|
|
44
|
+
entries,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.error('No built-in instruction assets found.');
|
|
49
|
+
console.error('Run prepack/publish to generate publish-assets, or pass --source explicitly.');
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readInstructionEntriesFromDirectory(sourceDir) {
|
|
54
|
+
const names = fs
|
|
55
|
+
.readdirSync(sourceDir)
|
|
56
|
+
.filter((name) => name.endsWith('.instructions.md'))
|
|
57
|
+
.sort((a, b) => a.localeCompare(b));
|
|
58
|
+
return names.map((name) => {
|
|
59
|
+
const sourcePath = path.resolve(sourceDir, name);
|
|
60
|
+
let content = fs.readFileSync(sourcePath, 'utf8');
|
|
61
|
+
if (!content.endsWith('\n')) {
|
|
62
|
+
content = `${content}\n`;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
fileName: name,
|
|
66
|
+
content,
|
|
67
|
+
sourcePath,
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isManagedFile(filePath) {
|
|
73
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
74
|
+
return content.includes(MANAGED_MARKER);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildUserSpaceBootstrapContent(namespaceDirPath, cwd) {
|
|
78
|
+
const namespaceDisplayPath = path.relative(cwd, namespaceDirPath).replace(/\\/g, '/');
|
|
79
|
+
let content = loadTextResource(USER_SPACE_BOOTSTRAP_FILE);
|
|
80
|
+
content = content.replace(/\{\{NAMESPACE_GLOB\}\}/g, `${namespaceDisplayPath}/*.instructions.md`);
|
|
81
|
+
if (!content.endsWith('\n')) {
|
|
82
|
+
content = `${content}\n`;
|
|
83
|
+
}
|
|
84
|
+
return content;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function runSyncInstructions(options) {
|
|
88
|
+
const cwd = path.resolve(getSingle(options, '--cwd', process.cwd()));
|
|
89
|
+
const outArg = getSingle(options, '--out', DEFAULT_NAMESPACE_OUT_DIR);
|
|
90
|
+
const sourceArg = getSingle(options, '--source', '');
|
|
91
|
+
const force = hasFlag(options, '--force');
|
|
92
|
+
const dryRun = hasFlag(options, '--dry-run');
|
|
93
|
+
const prune = hasFlag(options, '--prune');
|
|
94
|
+
|
|
95
|
+
if (!fs.existsSync(cwd)) {
|
|
96
|
+
console.error(`CWD does not exist: ${cwd}`);
|
|
97
|
+
process.exit(2);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const defaults = loadDefaultInstructionsTemplate();
|
|
101
|
+
let sourceType = defaults.type;
|
|
102
|
+
let sourceResolved = defaults.sourcePath;
|
|
103
|
+
let entries = defaults.entries;
|
|
104
|
+
|
|
105
|
+
if (sourceArg) {
|
|
106
|
+
const sourcePath = path.resolve(cwd, sourceArg);
|
|
107
|
+
if (!fs.existsSync(sourcePath)) {
|
|
108
|
+
console.error(`Source path does not exist: ${sourcePath}`);
|
|
109
|
+
process.exit(2);
|
|
110
|
+
}
|
|
111
|
+
const stat = fs.statSync(sourcePath);
|
|
112
|
+
if (stat.isDirectory()) {
|
|
113
|
+
sourceType = 'dir';
|
|
114
|
+
sourceResolved = sourcePath;
|
|
115
|
+
entries = readInstructionEntriesFromDirectory(sourcePath);
|
|
116
|
+
if (entries.length === 0) {
|
|
117
|
+
console.error(`No .instructions.md files in source directory: ${sourcePath}`);
|
|
118
|
+
process.exit(2);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
sourceType = 'file';
|
|
122
|
+
sourceResolved = sourcePath;
|
|
123
|
+
let content = fs.readFileSync(sourcePath, 'utf8');
|
|
124
|
+
if (!content.endsWith('\n')) {
|
|
125
|
+
content = `${content}\n`;
|
|
126
|
+
}
|
|
127
|
+
entries = [
|
|
128
|
+
{
|
|
129
|
+
fileName: path.basename(sourcePath),
|
|
130
|
+
content,
|
|
131
|
+
sourcePath,
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const outPath = path.resolve(cwd, outArg);
|
|
138
|
+
const outLooksLikeFile = outArg.endsWith('.md');
|
|
139
|
+
|
|
140
|
+
if (outLooksLikeFile && entries.length > 1) {
|
|
141
|
+
console.error('Target --out is a file path but source has multiple instruction files.');
|
|
142
|
+
console.error('Use an output directory for multi-file sync.');
|
|
143
|
+
process.exit(2);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (outLooksLikeFile) {
|
|
147
|
+
const entry = entries[0];
|
|
148
|
+
const exists = fs.existsSync(outPath);
|
|
149
|
+
if (exists && !force) {
|
|
150
|
+
const current = fs.readFileSync(outPath, 'utf8');
|
|
151
|
+
if (current !== entry.content) {
|
|
152
|
+
console.error(`Target already exists: ${outPath}`);
|
|
153
|
+
console.error('Use --force to overwrite.');
|
|
154
|
+
process.exit(2);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const report = {
|
|
159
|
+
mode: 'single-file',
|
|
160
|
+
cwd,
|
|
161
|
+
sourceType,
|
|
162
|
+
source: sourceResolved,
|
|
163
|
+
outPath,
|
|
164
|
+
exists,
|
|
165
|
+
overwritten: exists && force,
|
|
166
|
+
dryRun,
|
|
167
|
+
prune: false,
|
|
168
|
+
initializedUserSpaceEntry: false,
|
|
169
|
+
userSpaceEntryPath: null,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
if (dryRun) {
|
|
173
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
174
|
+
process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
178
|
+
fs.writeFileSync(outPath, entry.content, 'utf8');
|
|
179
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const outDir = outPath;
|
|
184
|
+
const planned = entries.map((entry) => {
|
|
185
|
+
return {
|
|
186
|
+
fileName: entry.fileName,
|
|
187
|
+
sourcePath: entry.sourcePath,
|
|
188
|
+
targetPath: path.resolve(outDir, entry.fileName),
|
|
189
|
+
content: entry.content,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const targetSet = new Set(planned.map((item) => item.targetPath));
|
|
194
|
+
const unchanged = [];
|
|
195
|
+
for (const item of planned) {
|
|
196
|
+
if (fs.existsSync(item.targetPath)) {
|
|
197
|
+
const current = fs.readFileSync(item.targetPath, 'utf8');
|
|
198
|
+
if (current === item.content) {
|
|
199
|
+
unchanged.push(item.targetPath);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const toWrite = planned.filter((item) => !unchanged.includes(item.targetPath));
|
|
205
|
+
const conflicts = toWrite.filter((item) => fs.existsSync(item.targetPath));
|
|
206
|
+
if (conflicts.length > 0 && !force) {
|
|
207
|
+
console.error('Some target files already exist and would change:');
|
|
208
|
+
for (const item of conflicts) {
|
|
209
|
+
console.error(`- ${item.targetPath}`);
|
|
210
|
+
}
|
|
211
|
+
console.error('Use --force to overwrite.');
|
|
212
|
+
process.exit(2);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const pruneDeletes = [];
|
|
216
|
+
if (prune && fs.existsSync(outDir)) {
|
|
217
|
+
const existing = fs
|
|
218
|
+
.readdirSync(outDir)
|
|
219
|
+
.filter((name) => name.endsWith('.instructions.md'))
|
|
220
|
+
.map((name) => path.resolve(outDir, name));
|
|
221
|
+
for (const existingPath of existing) {
|
|
222
|
+
if (!targetSet.has(existingPath) && isManagedFile(existingPath)) {
|
|
223
|
+
pruneDeletes.push(existingPath);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const userSpaceEntryPath = path.resolve(cwd, USER_SPACE_ENTRYPOINT);
|
|
229
|
+
const shouldInitUserSpaceEntry = !fs.existsSync(userSpaceEntryPath);
|
|
230
|
+
|
|
231
|
+
const report = {
|
|
232
|
+
mode: 'directory',
|
|
233
|
+
cwd,
|
|
234
|
+
sourceType,
|
|
235
|
+
source: sourceResolved,
|
|
236
|
+
outDir,
|
|
237
|
+
dryRun,
|
|
238
|
+
force,
|
|
239
|
+
prune,
|
|
240
|
+
initializedUserSpaceEntry: shouldInitUserSpaceEntry,
|
|
241
|
+
userSpaceEntryPath,
|
|
242
|
+
files: planned.map((item) => ({
|
|
243
|
+
fileName: item.fileName,
|
|
244
|
+
sourcePath: item.sourcePath,
|
|
245
|
+
targetPath: item.targetPath,
|
|
246
|
+
unchanged: unchanged.includes(item.targetPath),
|
|
247
|
+
})),
|
|
248
|
+
deleteFiles: pruneDeletes,
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
if (dryRun) {
|
|
252
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
257
|
+
for (const item of toWrite) {
|
|
258
|
+
fs.writeFileSync(item.targetPath, item.content, 'utf8');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (shouldInitUserSpaceEntry) {
|
|
262
|
+
fs.mkdirSync(path.dirname(userSpaceEntryPath), { recursive: true });
|
|
263
|
+
const userSpaceBootstrap = buildUserSpaceBootstrapContent(outDir, cwd);
|
|
264
|
+
fs.writeFileSync(userSpaceEntryPath, userSpaceBootstrap, 'utf8');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const filePath of pruneDeletes) {
|
|
268
|
+
fs.unlinkSync(filePath);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
272
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Usage:
|
|
2
|
+
agent-toolkit validate-commit-msg --file <message-file>
|
|
3
|
+
|
|
4
|
+
Rules:
|
|
5
|
+
- In monorepo mode, a section header is required before tagged lines
|
|
6
|
+
- Non-empty lines must be either a section header (for example workspace: or @scope/pkg:) or start with [TAG]
|
|
7
|
+
- If section headers are used, each section header must be followed by at least one tagged line
|
|
8
|
+
- No empty lines are allowed
|
|
9
|
+
- Optional target form: [TAG] <target>: <summary>
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
import { getSingle } from '../shared/args.mjs';
|
|
6
|
+
import { printTextResource } from '../shared/text-resource.mjs';
|
|
7
|
+
|
|
8
|
+
const ALLOWED_TAGS = ['INIT', 'ADD', 'REMOVE', 'FIX', 'REFACTOR', 'UPGRADE', 'PUBLISH'];
|
|
9
|
+
const ALLOWED_TARGETS = ['docs', 'test', 'ci', 'deps', 'api', 'schema', 'infra', 'fmt'];
|
|
10
|
+
const SECTION_HEADER_RE = /^(?:@[\w.-]+\/)?[\w.-]+:$/;
|
|
11
|
+
const COMMAND_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const HELP_FILE = path.resolve(COMMAND_DIR, 'help.txt');
|
|
13
|
+
const ROOT_PACKAGE_FILE = path.resolve(COMMAND_DIR, '../../../../../package.json');
|
|
14
|
+
|
|
15
|
+
export function printValidateCommitMsgHelp() {
|
|
16
|
+
printTextResource(HELP_FILE);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validateCommitLine(line, lineNo) {
|
|
20
|
+
if (line.trim() === '') {
|
|
21
|
+
return `Line ${lineNo}: empty line is not allowed`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const head = line.match(/^\[([A-Z]+)\]\s+/);
|
|
25
|
+
if (!head) {
|
|
26
|
+
return `Line ${lineNo}: must start with [TAG] followed by a space`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tag = head[1];
|
|
30
|
+
if (!ALLOWED_TAGS.includes(tag)) {
|
|
31
|
+
return `Line ${lineNo}: tag [${tag}] is not allowed`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const rest = line.slice(head[0].length);
|
|
35
|
+
if (rest.trim() === '') {
|
|
36
|
+
return `Line ${lineNo}: summary is required after tag`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const targetMatch = rest.match(/^<([^>]+)>:\s+(.+)$/);
|
|
40
|
+
if (targetMatch) {
|
|
41
|
+
const target = targetMatch[1];
|
|
42
|
+
const summary = targetMatch[2];
|
|
43
|
+
if (!ALLOWED_TARGETS.includes(target)) {
|
|
44
|
+
return `Line ${lineNo}: target <${target}> is not allowed`;
|
|
45
|
+
}
|
|
46
|
+
if (summary.trim() === '') {
|
|
47
|
+
return `Line ${lineNo}: summary is required after target`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isSectionHeaderLine(line) {
|
|
55
|
+
return SECTION_HEADER_RE.test(line.trim());
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isMonorepoRoot() {
|
|
59
|
+
if (!fs.existsSync(ROOT_PACKAGE_FILE)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const rootPackage = JSON.parse(fs.readFileSync(ROOT_PACKAGE_FILE, 'utf8'));
|
|
65
|
+
return Array.isArray(rootPackage.workspaces) && rootPackage.workspaces.length > 0;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function validateSectionFormat(lines) {
|
|
72
|
+
const errors = [];
|
|
73
|
+
let currentSection = '';
|
|
74
|
+
let currentSectionLineNo = 0;
|
|
75
|
+
let currentSectionHasTaggedLine = false;
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
78
|
+
const lineNo = i + 1;
|
|
79
|
+
const line = lines[i];
|
|
80
|
+
|
|
81
|
+
if (line.trim() === '') {
|
|
82
|
+
errors.push(`Line ${lineNo}: empty line is not allowed`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (isSectionHeaderLine(line)) {
|
|
87
|
+
if (currentSection && !currentSectionHasTaggedLine) {
|
|
88
|
+
errors.push(
|
|
89
|
+
`Line ${currentSectionLineNo}: section header "${currentSection}" must be followed by at least one tagged line`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
currentSection = line.trim();
|
|
94
|
+
currentSectionLineNo = lineNo;
|
|
95
|
+
currentSectionHasTaggedLine = false;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!currentSection) {
|
|
100
|
+
errors.push(
|
|
101
|
+
`Line ${lineNo}: section header is required before tagged lines when package/workspace sections are used`,
|
|
102
|
+
);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const err = validateCommitLine(line, lineNo);
|
|
107
|
+
if (err) {
|
|
108
|
+
errors.push(err);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
currentSectionHasTaggedLine = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (currentSection && !currentSectionHasTaggedLine) {
|
|
116
|
+
errors.push(
|
|
117
|
+
`Line ${currentSectionLineNo}: section header "${currentSection}" must be followed by at least one tagged line`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return errors;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function runValidateCommitMsg(options) {
|
|
125
|
+
const file = getSingle(options, '--file', '');
|
|
126
|
+
if (!file) {
|
|
127
|
+
printValidateCommitMsgHelp();
|
|
128
|
+
process.exit(2);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const filePath = path.resolve(file);
|
|
132
|
+
if (!fs.existsSync(filePath)) {
|
|
133
|
+
console.error(`Message file not found: ${filePath}`);
|
|
134
|
+
process.exit(2);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const raw = fs.readFileSync(filePath, 'utf8').replace(/\r\n/g, '\n');
|
|
138
|
+
const lines = raw.endsWith('\n') ? raw.slice(0, -1).split('\n') : raw.split('\n');
|
|
139
|
+
|
|
140
|
+
if (lines.length === 0 || (lines.length === 1 && lines[0].trim() === '')) {
|
|
141
|
+
console.error('Commit message is empty');
|
|
142
|
+
process.exit(2);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const mustUseSectionHeaders = isMonorepoRoot();
|
|
146
|
+
const hasSectionHeaders = lines.some((line) => isSectionHeaderLine(line));
|
|
147
|
+
|
|
148
|
+
// [PUBLISH] is generated by lerna and is always a repo-wide tag.
|
|
149
|
+
// In independent mode lerna appends package/version lines after the tag.
|
|
150
|
+
// Neither section headers nor a summary are required for this special tag.
|
|
151
|
+
const isPublishOnlyMessage = /^\[PUBLISH\](\s+.*)?$/.test(lines[0].trim());
|
|
152
|
+
|
|
153
|
+
if (isPublishOnlyMessage) {
|
|
154
|
+
console.log('Commit message validation passed');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (mustUseSectionHeaders && !hasSectionHeaders) {
|
|
159
|
+
console.error('Commit message validation failed:');
|
|
160
|
+
console.error('- Line 1: section header is required before tagged lines in monorepo mode');
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const errors = hasSectionHeaders ? validateSectionFormat(lines) : [];
|
|
165
|
+
if (!hasSectionHeaders) {
|
|
166
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
167
|
+
const err = validateCommitLine(lines[i], i + 1);
|
|
168
|
+
if (err) {
|
|
169
|
+
errors.push(err);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (errors.length > 0) {
|
|
175
|
+
console.error('Commit message validation failed:');
|
|
176
|
+
for (const err of errors) {
|
|
177
|
+
console.error(`- ${err}`);
|
|
178
|
+
}
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log('Commit message validation passed');
|
|
183
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@produck/agent-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Central CLI toolkit for organization AI execution workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"prepack": "node ./bin/build-publish-assets.mjs",
|
|
16
|
+
"coverage": "npm exec --yes -- c8 --reporter=lcov --reporter=html --reporter=text-summary node --test test/index.mjs",
|
|
17
|
+
"coverage:check": "npm exec --yes -- c8 --check-coverage --lines 100 --functions 100 --branches 100 --statements 100 node --test test/index.mjs",
|
|
18
|
+
"test": "node --test test/index.mjs",
|
|
16
19
|
"verify": "node ./bin/agent-toolkit.mjs --help && node ./bin/agent-toolkit.mjs preflight --cwd . --require package.json",
|
|
17
20
|
"pack:check": "npm pack --dry-run"
|
|
18
21
|
},
|
|
19
22
|
"files": [
|
|
20
23
|
"bin",
|
|
21
|
-
"templates",
|
|
22
24
|
"publish-assets"
|
|
23
25
|
],
|
|
24
26
|
"publishConfig": {
|
|
@@ -28,5 +30,5 @@
|
|
|
28
30
|
"node": ">=18.0.0"
|
|
29
31
|
},
|
|
30
32
|
"license": "MIT",
|
|
31
|
-
"gitHead": "
|
|
33
|
+
"gitHead": "45bc825ac6c290ca99b58881ee3b052bd0c2265e"
|
|
32
34
|
}
|
|
@@ -79,11 +79,7 @@ indent_style = space
|
|
|
79
79
|
indent_size = 2
|
|
80
80
|
trim_trailing_whitespace = true
|
|
81
81
|
|
|
82
|
-
[*.yml]
|
|
83
|
-
indent_style = space
|
|
84
|
-
indent_size = 2
|
|
85
|
-
|
|
86
|
-
[*.yaml]
|
|
82
|
+
[*.{yml,yaml}]
|
|
87
83
|
indent_style = space
|
|
88
84
|
indent_size = 2
|
|
89
85
|
|
|
@@ -118,8 +114,11 @@ max_line_length = 80
|
|
|
118
114
|
be present in repository lint configuration.
|
|
119
115
|
- Apply minimal patching only: keep existing repository/framework structure and
|
|
120
116
|
add the smallest necessary changes.
|
|
121
|
-
- Repository-specific overrides are
|
|
122
|
-
|
|
117
|
+
- Repository-specific overrides are optional and should be added only when
|
|
118
|
+
behavior intentionally differs from shared presets.
|
|
119
|
+
- In ESLint flat config, "layer on top" means local override items must appear
|
|
120
|
+
after shared preset items in exported order.
|
|
121
|
+
- No-op overrides that repeat inherited values should be avoided.
|
|
123
122
|
|
|
124
123
|
## Language conventions
|
|
125
124
|
|
|
@@ -142,24 +141,16 @@ max_line_length = 80
|
|
|
142
141
|
- If details are needed, use additional tagged lines.
|
|
143
142
|
- Do not keep summary as an untagged standalone line.
|
|
144
143
|
- Recommended local validation:
|
|
145
|
-
`npm exec --
|
|
146
|
-
|
|
147
|
-
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
-
|
|
152
|
-
<summary>`.
|
|
153
|
-
- Allowed targets are `docs`, `test`, `ci`, `deps`, `api`, `schema`, and
|
|
154
|
-
`infra`.
|
|
155
|
-
- If target syntax is used, target must be wrapped in angle brackets and must be
|
|
156
|
-
from the allowed target list.
|
|
144
|
+
`npm exec -- agent-toolkit validate-commit-msg --file <message-file>`.
|
|
145
|
+
- Commit precheck policy follows
|
|
146
|
+
`.github/distribution/produck/20-produck-commit.instructions.md`.
|
|
147
|
+
- Canonical source for commit tag/target whitelists, legacy mapping, and
|
|
148
|
+
target syntax is
|
|
149
|
+
`.github/distribution/produck/20-produck-commit.instructions.md`.
|
|
150
|
+
- Do not redefine commit tag or target whitelists in other instruction files.
|
|
157
151
|
- For non-monorepo repositories, use `[TAG] summary` directly (no
|
|
158
152
|
package/workspace section headers).
|
|
159
153
|
- Bracketed commit summaries should be in English
|
|
160
|
-
- `[UPGRADE] deps` is allowed for pure dependency upgrades; if IFF artifacts or
|
|
161
|
-
IPC-related artifacts/calls are updated, the summary must name those updates
|
|
162
|
-
explicitly.
|
|
163
154
|
- PR title format is repository-defined; no organization-level title format
|
|
164
155
|
restriction
|
|
165
156
|
- In PR descriptions, summarize what changed, why it changed, how it was
|
|
@@ -189,14 +180,10 @@ Recommended three-step flow:
|
|
|
189
180
|
|
|
190
181
|
Recommended local tools:
|
|
191
182
|
|
|
192
|
-
- `npm exec --
|
|
193
|
-
--
|
|
194
|
-
- `npm exec --
|
|
195
|
-
--
|
|
196
|
-
- `npm exec --package=@produck/agent-toolkit@latest agent-toolkit summarize-log
|
|
197
|
-
--file logs/run.log --last 120`
|
|
198
|
-
- `npm exec --package=@produck/agent-toolkit@latest agent-toolkit summarize-log
|
|
199
|
-
--file logs/run.log --match "FAIL|ERROR"`
|
|
183
|
+
- `npm exec -- agent-toolkit preflight --cwd . --require package.json --ensure-dir logs`
|
|
184
|
+
- `npm exec -- agent-toolkit run-capture --out logs/run.log --cmd "<command>"`
|
|
185
|
+
- `npm exec -- agent-toolkit summarize-log --file logs/run.log --last 120`
|
|
186
|
+
- `npm exec -- agent-toolkit summarize-log --file logs/run.log --match "FAIL|ERROR"`
|
|
200
187
|
|
|
201
188
|
Guardrails:
|
|
202
189
|
|
|
@@ -247,21 +234,31 @@ When CI enforcement is deferred, use manual sync per repository:
|
|
|
247
234
|
`.github/instructions/produck/`.
|
|
248
235
|
2. Keep repository-specific exceptions in `.github/copilot-instructions.md`.
|
|
249
236
|
3. Validate critical policies manually in each update cycle.
|
|
237
|
+
4. After instruction sync, validate duplicated policy sections remain
|
|
238
|
+
consistent across instruction files, especially commit tag and target
|
|
239
|
+
whitelists.
|
|
250
240
|
|
|
251
241
|
Recommended command:
|
|
252
242
|
|
|
253
|
-
- `npm exec --
|
|
243
|
+
- `npm exec -- agent-toolkit sync-instructions --cwd . --source <path-to-org>/.github/distribution/produck --force --prune`
|
|
254
244
|
|
|
255
|
-
If the package
|
|
256
|
-
|
|
245
|
+
If the installed package includes bundled assets, `--source` can be omitted to
|
|
246
|
+
use those built-in assets.
|
|
257
247
|
|
|
258
248
|
This keeps instruction entrypoints aligned without requiring submodule or
|
|
259
249
|
automatic PR rollout.
|
|
260
250
|
|
|
261
251
|
### Central package execution policy
|
|
262
252
|
|
|
263
|
-
When bridge mechanism uses a central npm package,
|
|
264
|
-
|
|
253
|
+
When bridge mechanism uses a central npm package, the package is installed
|
|
254
|
+
locally in downstream repositories at a fixed version managed by the
|
|
255
|
+
organization baseline.
|
|
256
|
+
|
|
257
|
+
- Local install and pinned version are deployed by
|
|
258
|
+
`agent-toolkit sync-husky-hooks`.
|
|
259
|
+
- Invocation uses the locally installed copy:
|
|
260
|
+
`npm exec -- <bin> ...`.
|
|
261
|
+
- Do not use `npm exec --package=<pkg>@latest` for routine invocations.
|
|
265
262
|
|
|
266
263
|
Local implementation reference in this repository:
|
|
267
264
|
|
|
@@ -270,12 +267,12 @@ Local implementation reference in this repository:
|
|
|
270
267
|
- This local path is the implementation source, not an automatic runtime mount
|
|
271
268
|
for other repositories.
|
|
272
269
|
|
|
273
|
-
Required safeguards for
|
|
270
|
+
Required safeguards for central tooling:
|
|
274
271
|
|
|
275
272
|
- Print resolved package version before running high-impact commands.
|
|
276
273
|
- For high-risk operations, run dry-run/preview first, then execute.
|
|
277
274
|
- Keep an emergency fallback path to a pinned version for incident mitigation.
|
|
278
|
-
- Prefer `npm exec --
|
|
275
|
+
- Prefer `npm exec -- <bin> ...` for predictable invocation.
|
|
279
276
|
|
|
280
277
|
Version observability (required before high-impact operations):
|
|
281
278
|
|
|
@@ -307,7 +304,8 @@ Use the following template text in organization AI instructions:
|
|
|
307
304
|
- For large output tasks, use two phases: capture full output, then analyze.
|
|
308
305
|
- Avoid fragile shell pipeline post-processing for long-output commands.
|
|
309
306
|
- Central package policy:
|
|
310
|
-
-
|
|
307
|
+
- Central package is installed locally at a fixed organization-baseline version.
|
|
308
|
+
- Invoke via `npm exec -- <bin> ...` to use the locally installed copy.
|
|
311
309
|
- Print resolved package version before high-impact execution.
|
|
312
310
|
- Use dry-run first for risky operations; keep rollback path to pinned version.
|
|
313
311
|
- Commit message policy:
|
|
@@ -316,7 +314,7 @@ Use the following template text in organization AI instructions:
|
|
|
316
314
|
- Use only allowed tags: `[INIT]`, `[ADD]`, `[REMOVE]`, `[FIX]`,
|
|
317
315
|
`[REFACTOR]`, `[UPGRADE]`.
|
|
318
316
|
- Optional target syntax is `[TAG] <target>: <summary>` with target in:
|
|
319
|
-
`docs`, `test`, `ci`, `deps`, `api`, `schema`, `infra`.
|
|
317
|
+
`docs`, `test`, `ci`, `deps`, `api`, `schema`, `infra`, `fmt`.
|
|
320
318
|
- Do not assume scripts from organization `.github` repository exist in target
|
|
321
319
|
repositories.
|
|
322
320
|
- If a repository provides stricter rules, repository rules override
|