mcp-udacity-commit 1.0.1 → 1.2.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.
- package/README.md +13 -0
- package/build/index.js +104 -152
- package/build/lint.js +318 -0
- package/package.json +5 -3
- package/build/index.d.ts +0 -2
- package/build/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -48,8 +48,10 @@ claude mcp add udacity-commit -- node "$(pwd)/build/index.js"
|
|
|
48
48
|
| Primitive | Name | Purpose |
|
|
49
49
|
| --- | --- | --- |
|
|
50
50
|
| Resource | `udacity://commit-styleguide` | The style-guide rules, as markdown |
|
|
51
|
+
| Resource | `udacity://branch-naming` | The companion `type/kebab-case` branch-naming rules, as markdown |
|
|
51
52
|
| Tool | `validate_commit_message` | Checks a message against every rule (type, ≤50-char subject, capitalization, no trailing period, blank line, ≤72-char body wrap) |
|
|
52
53
|
| Tool | `format_commit_message` | Builds a compliant message from `type` + `subject` + optional `body`/`footer` |
|
|
54
|
+
| Tool | `validate_branch_name` | Checks a branch name against the companion `type/kebab-case` convention (e.g. `feat/add-dark-mode`); `release/*` is a typed branch with a version-style description (`release/1.2.0`), and base branches like `main` are exempt |
|
|
53
55
|
|
|
54
56
|
## Example
|
|
55
57
|
|
|
@@ -78,6 +80,17 @@ Resolves: #142
|
|
|
78
80
|
• Subject must not end with a period.
|
|
79
81
|
```
|
|
80
82
|
|
|
83
|
+
`validate_branch_name` enforces the companion `type/kebab-case` convention:
|
|
84
|
+
|
|
85
|
+
```text
|
|
86
|
+
"feat/add-dark-mode" → ✅ Compliant branch name.
|
|
87
|
+
"release/1.2.0" → ✅ Compliant branch name.
|
|
88
|
+
"Feature/Add_Dark_Mode" → ❌ Not compliant.
|
|
89
|
+
• Unknown type "Feature". Use one of: feat, fix, docs, style, refactor, test, chore, release.
|
|
90
|
+
• Description must be lowercase kebab-case. Got: "Add_Dark_Mode".
|
|
91
|
+
"main" → ✅ (base branch — feature-branch rules don't apply)
|
|
92
|
+
```
|
|
93
|
+
|
|
81
94
|
## Develop
|
|
82
95
|
|
|
83
96
|
```bash
|
package/build/index.js
CHANGED
|
@@ -2,158 +2,110 @@
|
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
5
|
+
import { STYLE_GUIDE, BRANCH_GUIDE, validate, formatMessage, validateBranch } from "./lint.js";
|
|
6
|
+
const VERSION = "1.2.0";
|
|
7
|
+
export function createServer() {
|
|
8
|
+
const server = new McpServer({ name: "udacity-commit", version: VERSION });
|
|
9
|
+
server.registerResource("styleguide", "udacity://commit-styleguide", {
|
|
10
|
+
title: "Udacity Git Commit Style Guide",
|
|
11
|
+
description: "The commit-message rules (types, subject, body, footer).",
|
|
12
|
+
mimeType: "text/markdown",
|
|
13
|
+
}, async (uri) => ({ contents: [{ uri: uri.href, text: STYLE_GUIDE }] }));
|
|
14
|
+
server.registerResource("branch-naming", "udacity://branch-naming", {
|
|
15
|
+
title: "Branch Naming (companion convention)",
|
|
16
|
+
description: "type/kebab-case branch-naming rules that pair with the commit style.",
|
|
17
|
+
mimeType: "text/markdown",
|
|
18
|
+
}, async (uri) => ({ contents: [{ uri: uri.href, text: BRANCH_GUIDE }] }));
|
|
19
|
+
server.registerTool("validate_commit_message", {
|
|
20
|
+
title: "Validate a commit message",
|
|
21
|
+
description: "Check a commit message against the Udacity Git Commit Message Style Guide. " +
|
|
22
|
+
"Returns whether it is compliant plus any problems (violations) and warnings (hints).",
|
|
23
|
+
inputSchema: { message: z.string().describe("The full commit message to check") },
|
|
24
|
+
outputSchema: {
|
|
25
|
+
valid: z.boolean(),
|
|
26
|
+
problems: z.array(z.string()),
|
|
27
|
+
warnings: z.array(z.string()),
|
|
28
|
+
},
|
|
29
|
+
}, async ({ message }) => {
|
|
30
|
+
const r = validate(message);
|
|
31
|
+
const text = [
|
|
32
|
+
r.valid ? "✅ Compliant with the Udacity style guide." : "❌ Not compliant.",
|
|
33
|
+
...r.problems.map((p) => ` • ${p}`),
|
|
34
|
+
...r.warnings.map((w) => ` ⚠ ${w}`),
|
|
35
|
+
].join("\n");
|
|
36
|
+
const structuredContent = {
|
|
37
|
+
valid: r.valid,
|
|
38
|
+
problems: r.problems,
|
|
39
|
+
warnings: r.warnings,
|
|
40
|
+
};
|
|
41
|
+
return { content: [{ type: "text", text }], structuredContent };
|
|
42
|
+
});
|
|
43
|
+
server.registerTool("format_commit_message", {
|
|
44
|
+
title: "Format a Udacity-style commit message",
|
|
45
|
+
description: "Compose a compliant commit message from its parts. The subject is capitalized, " +
|
|
46
|
+
"a trailing period is removed, and the body is wrapped at 72 characters.",
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: z.enum(["feat", "fix", "docs", "style", "refactor", "test", "chore"]),
|
|
49
|
+
subject: z.string().describe("Imperative subject; auto-capitalized, trailing period removed"),
|
|
50
|
+
body: z.string().optional().describe("What & why; auto-wrapped at 72 chars"),
|
|
51
|
+
footer: z.string().optional().describe('Issue refs, e.g. "Resolves: #123"'),
|
|
52
|
+
},
|
|
53
|
+
outputSchema: {
|
|
54
|
+
message: z.string(),
|
|
55
|
+
valid: z.boolean(),
|
|
56
|
+
problems: z.array(z.string()),
|
|
57
|
+
warnings: z.array(z.string()),
|
|
58
|
+
},
|
|
59
|
+
}, async (input) => {
|
|
60
|
+
const { message, report } = formatMessage(input);
|
|
61
|
+
const note = !report.valid
|
|
62
|
+
? "❌ " + [...report.problems, ...report.warnings].join("; ")
|
|
63
|
+
: report.warnings.length
|
|
64
|
+
? "✅ compliant (hint: " + report.warnings.join("; ") + ")"
|
|
65
|
+
: "✅ compliant";
|
|
66
|
+
const structuredContent = {
|
|
67
|
+
message,
|
|
68
|
+
valid: report.valid,
|
|
69
|
+
problems: report.problems,
|
|
70
|
+
warnings: report.warnings,
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
content: [{ type: "text", text: `${message}\n\n--- ${note}` }],
|
|
74
|
+
structuredContent,
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
server.registerTool("validate_branch_name", {
|
|
78
|
+
title: "Validate a git branch name",
|
|
79
|
+
description: "Check a git branch name against the companion type/kebab-case convention " +
|
|
80
|
+
'(e.g. "feat/add-dark-mode"). Returns whether it is compliant plus any problems ' +
|
|
81
|
+
"(violations) and warnings (hints). Base branches like main/master are exempt.",
|
|
82
|
+
inputSchema: { name: z.string().describe('The branch name to check, e.g. "feat/add-dark-mode"') },
|
|
83
|
+
outputSchema: {
|
|
84
|
+
valid: z.boolean(),
|
|
85
|
+
problems: z.array(z.string()),
|
|
86
|
+
warnings: z.array(z.string()),
|
|
87
|
+
},
|
|
88
|
+
}, async ({ name }) => {
|
|
89
|
+
const r = validateBranch(name);
|
|
90
|
+
const text = [
|
|
91
|
+
r.valid ? "✅ Compliant branch name." : "❌ Not compliant.",
|
|
92
|
+
...r.problems.map((p) => ` • ${p}`),
|
|
93
|
+
...r.warnings.map((w) => ` ⚠ ${w}`),
|
|
94
|
+
].join("\n");
|
|
95
|
+
const structuredContent = {
|
|
96
|
+
valid: r.valid,
|
|
97
|
+
problems: r.problems,
|
|
98
|
+
warnings: r.warnings,
|
|
99
|
+
};
|
|
100
|
+
return { content: [{ type: "text", text }], structuredContent };
|
|
101
|
+
});
|
|
102
|
+
return server;
|
|
71
103
|
}
|
|
72
|
-
function
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const lines = message.replace(/\s+$/, "").split("\n");
|
|
76
|
-
const subject = lines[0] ?? "";
|
|
77
|
-
const m = subject.match(/^(\w+): (.*)$/);
|
|
78
|
-
if (!m) {
|
|
79
|
-
problems.push(`Subject must follow "type: Subject". Got: "${subject}".`);
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
const [, type, rest] = m;
|
|
83
|
-
if (!TYPES[type]) {
|
|
84
|
-
problems.push(`Unknown type "${type}". Use one of: ${Object.keys(TYPES).join(", ")}.`);
|
|
85
|
-
}
|
|
86
|
-
if (!rest) {
|
|
87
|
-
problems.push("Subject text is empty after the type.");
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
if (!/^[A-Z]/.test(rest)) {
|
|
91
|
-
problems.push(`Subject should begin with a capital letter (got "${rest[0]}").`);
|
|
92
|
-
}
|
|
93
|
-
const first = rest.split(/\s+/)[0];
|
|
94
|
-
if (/(ed|ing)$/i.test(first)) {
|
|
95
|
-
warnings.push(`"${first}" looks past-tense/gerund — use imperative mood ("Add", not "Added").`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
if (/\.$/.test(subject))
|
|
100
|
-
problems.push("Subject must not end with a period.");
|
|
101
|
-
if (subject.length > SUBJECT_MAX) {
|
|
102
|
-
problems.push(`Subject is ${subject.length} chars; max is ${SUBJECT_MAX}.`);
|
|
103
|
-
}
|
|
104
|
-
if (lines.length > 1 && lines[1].trim() !== "") {
|
|
105
|
-
problems.push("Leave a blank line between the subject and the body.");
|
|
106
|
-
}
|
|
107
|
-
for (let i = 2; i < lines.length; i++) {
|
|
108
|
-
if (lines[i].length > BODY_WRAP) {
|
|
109
|
-
problems.push(`Line ${i + 1} is ${lines[i].length} chars; wrap body/footer at ${BODY_WRAP}.`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
return { valid: problems.length === 0, problems, warnings };
|
|
104
|
+
async function main() {
|
|
105
|
+
const server = createServer();
|
|
106
|
+
await server.connect(new StdioServerTransport());
|
|
113
107
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
description: "The commit-message rules (types, subject, body, footer).",
|
|
118
|
-
mimeType: "text/markdown",
|
|
119
|
-
}, async (uri) => ({ contents: [{ uri: uri.href, text: STYLE_GUIDE }] }));
|
|
120
|
-
server.registerTool("validate_commit_message", {
|
|
121
|
-
title: "Validate a commit message",
|
|
122
|
-
description: "Check a commit message against the Udacity Git Commit Message Style Guide.",
|
|
123
|
-
inputSchema: { message: z.string().describe("The full commit message to check") },
|
|
124
|
-
}, async ({ message }) => {
|
|
125
|
-
const r = validate(message);
|
|
126
|
-
const out = [
|
|
127
|
-
r.valid ? "✅ Compliant with the Udacity style guide." : "❌ Not compliant.",
|
|
128
|
-
...r.problems.map((p) => ` • ${p}`),
|
|
129
|
-
...r.warnings.map((w) => ` ⚠ ${w}`),
|
|
130
|
-
].join("\n");
|
|
131
|
-
return { content: [{ type: "text", text: out }] };
|
|
108
|
+
main().catch((err) => {
|
|
109
|
+
console.error(err);
|
|
110
|
+
process.exit(1);
|
|
132
111
|
});
|
|
133
|
-
server.registerTool("format_commit_message", {
|
|
134
|
-
title: "Format a Udacity-style commit message",
|
|
135
|
-
description: "Compose a compliant commit message from its parts.",
|
|
136
|
-
inputSchema: {
|
|
137
|
-
type: z.enum(["feat", "fix", "docs", "style", "refactor", "test", "chore"]),
|
|
138
|
-
subject: z
|
|
139
|
-
.string()
|
|
140
|
-
.describe("Imperative subject; auto-capitalized, trailing period removed"),
|
|
141
|
-
body: z.string().optional().describe("What & why; auto-wrapped at 72 chars"),
|
|
142
|
-
footer: z.string().optional().describe('Issue refs, e.g. "Resolves: #123"'),
|
|
143
|
-
},
|
|
144
|
-
}, async ({ type, subject, body, footer }) => {
|
|
145
|
-
let s = subject.trim().replace(/\.+$/, "");
|
|
146
|
-
s = s.charAt(0).toUpperCase() + s.slice(1);
|
|
147
|
-
const parts = [`${type}: ${s}`];
|
|
148
|
-
if (body?.trim())
|
|
149
|
-
parts.push("", wrap(body.trim(), BODY_WRAP));
|
|
150
|
-
if (footer?.trim())
|
|
151
|
-
parts.push("", footer.trim());
|
|
152
|
-
const msg = parts.join("\n");
|
|
153
|
-
const v = validate(msg);
|
|
154
|
-
const note = v.valid ? "✅ compliant" : "❌ " + v.problems.join("; ");
|
|
155
|
-
return { content: [{ type: "text", text: `${msg}\n\n--- ${note}` }] };
|
|
156
|
-
});
|
|
157
|
-
const transport = new StdioServerTransport();
|
|
158
|
-
await server.connect(transport);
|
|
159
|
-
//# sourceMappingURL=index.js.map
|
package/build/lint.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, side-effect-free linting logic for the Udacity Git Commit Message
|
|
3
|
+
* Style Guide. Kept separate from the MCP server (index.ts) so it can be
|
|
4
|
+
* unit-tested in isolation.
|
|
5
|
+
*
|
|
6
|
+
* Length semantics: all limits are measured in Unicode code points
|
|
7
|
+
* (`width()`), not UTF-16 code units, so emoji / CJK / combining marks are
|
|
8
|
+
* counted the way a human reads them. The 50-char subject limit applies to
|
|
9
|
+
* the WHOLE `type: Subject` line, including the `type: ` prefix.
|
|
10
|
+
*/
|
|
11
|
+
export const SUBJECT_MAX = 50;
|
|
12
|
+
export const BODY_WRAP = 72;
|
|
13
|
+
export const BRANCH_MAX = 50;
|
|
14
|
+
/**
|
|
15
|
+
* Long-lived / protected branches that are exempt from the feature-branch
|
|
16
|
+
* `type/description` rule. `release/*` is NOT here — it is validated as a
|
|
17
|
+
* typed branch with a version-style description (e.g. `release/1.2.0`).
|
|
18
|
+
*/
|
|
19
|
+
export const BASE_BRANCHES = new Set(["main", "master", "dev", "develop", "trunk"]);
|
|
20
|
+
export const TYPES = {
|
|
21
|
+
feat: "A new feature",
|
|
22
|
+
fix: "A bug fix",
|
|
23
|
+
docs: "Changes to documentation",
|
|
24
|
+
style: "Formatting, missing semicolons, etc; no code change",
|
|
25
|
+
refactor: "Refactoring production code",
|
|
26
|
+
test: "Adding tests, refactoring tests; no production code change",
|
|
27
|
+
chore: "Updating build tasks, package configs, etc; no production code change",
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Types allowed as a branch prefix: the commit types plus `release`, which is
|
|
31
|
+
* a branch-only type (release branches are named, not committed). `release`
|
|
32
|
+
* takes a version-style description; every other type takes kebab-case.
|
|
33
|
+
*/
|
|
34
|
+
export const BRANCH_TYPES = [...Object.keys(TYPES), "release"];
|
|
35
|
+
/** Footer keywords recognized for issue-reference validation. */
|
|
36
|
+
export const FOOTER_KEYS = ["Resolves", "Closes", "Fixes", "Fix", "See also", "Refs", "Ref"];
|
|
37
|
+
/**
|
|
38
|
+
* Common non-imperative first words (past tense / gerund). We match against
|
|
39
|
+
* this allow-known-bad list rather than a broad `/(ed|ing)$/` regex so that
|
|
40
|
+
* legitimate imperatives like "Bring", "Embed", "Ring" are never flagged.
|
|
41
|
+
* This favors precision (no false positives) over recall.
|
|
42
|
+
*/
|
|
43
|
+
const NON_IMPERATIVE = new Set([
|
|
44
|
+
// past tense
|
|
45
|
+
"added", "fixed", "updated", "changed", "removed", "deleted", "created",
|
|
46
|
+
"refactored", "implemented", "improved", "renamed", "moved", "merged",
|
|
47
|
+
"reverted", "bumped", "cleaned", "corrected", "adjusted", "enabled",
|
|
48
|
+
"disabled", "introduced", "resolved", "replaced", "converted", "migrated",
|
|
49
|
+
"dropped", "extracted", "wrapped", "tweaked", "optimized", "simplified",
|
|
50
|
+
"formatted", "documented", "tested", "released", "handled", "allowed",
|
|
51
|
+
"prevented", "ensured", "avoided", "unified", "applied", "upgraded",
|
|
52
|
+
"downgraded", "patched", "hardened", "restructured", "reorganized",
|
|
53
|
+
"deprecated", "exposed", "integrated", "validated", "normalized", "cached",
|
|
54
|
+
"supported", "added", "wired", "hooked",
|
|
55
|
+
// gerund
|
|
56
|
+
"adding", "fixing", "updating", "changing", "removing", "deleting",
|
|
57
|
+
"creating", "refactoring", "implementing", "improving", "renaming",
|
|
58
|
+
"moving", "merging", "reverting", "bumping", "cleaning", "correcting",
|
|
59
|
+
"adjusting", "enabling", "disabling", "introducing", "resolving",
|
|
60
|
+
"replacing", "converting", "migrating", "dropping", "extracting",
|
|
61
|
+
"wrapping", "tweaking", "optimizing", "simplifying", "formatting",
|
|
62
|
+
"documenting", "testing", "releasing", "handling", "allowing",
|
|
63
|
+
"preventing", "ensuring", "avoiding", "unifying", "applying", "upgrading",
|
|
64
|
+
"patching", "hardening", "restructuring", "reorganizing", "deprecating",
|
|
65
|
+
"exposing", "integrating", "validating", "normalizing", "caching",
|
|
66
|
+
"supporting",
|
|
67
|
+
]);
|
|
68
|
+
export const STYLE_GUIDE = `# Udacity Git Commit Message Style Guide
|
|
69
|
+
|
|
70
|
+
A commit message has three parts separated by blank lines: **subject**, optional **body**, optional **footer**.
|
|
71
|
+
|
|
72
|
+
type: Subject
|
|
73
|
+
|
|
74
|
+
Body — the what and why, not the how.
|
|
75
|
+
|
|
76
|
+
Resolves: #123
|
|
77
|
+
See also: #456, #789
|
|
78
|
+
|
|
79
|
+
## Types
|
|
80
|
+
${Object.entries(TYPES)
|
|
81
|
+
.map(([t, d]) => `- **${t}**: ${d}`)
|
|
82
|
+
.join("\n")}
|
|
83
|
+
|
|
84
|
+
## Subject
|
|
85
|
+
- \`type: Subject\` format
|
|
86
|
+
- The whole line (including the \`type: \` prefix) is no more than ${SUBJECT_MAX} characters
|
|
87
|
+
- Begins with a capital letter
|
|
88
|
+
- Imperative mood ("Add", not "Added")
|
|
89
|
+
- No trailing period
|
|
90
|
+
- Blank line separates it from the body
|
|
91
|
+
|
|
92
|
+
## Body (optional)
|
|
93
|
+
- Only when the commit needs explanation
|
|
94
|
+
- Explains the **what** and **why**, not the how
|
|
95
|
+
- Wrap each line at ${BODY_WRAP} characters
|
|
96
|
+
|
|
97
|
+
## Footer (optional)
|
|
98
|
+
- References issue-tracker IDs: \`Resolves: #123\`, \`See also: #456, #789\`
|
|
99
|
+
|
|
100
|
+
_Lengths are counted in Unicode code points._
|
|
101
|
+
`;
|
|
102
|
+
export const BRANCH_GUIDE = `# Branch naming (companion convention)
|
|
103
|
+
|
|
104
|
+
Not part of the official Udacity *commit-message* guide, but a natural
|
|
105
|
+
companion: name feature branches after the change they carry, reusing the
|
|
106
|
+
same commit \`type\` set.
|
|
107
|
+
|
|
108
|
+
type/kebab-case-description
|
|
109
|
+
|
|
110
|
+
## Rules
|
|
111
|
+
- \`type/\` prefix — one of: ${BRANCH_TYPES.join(", ")}
|
|
112
|
+
- A single \`/\` separates the type from the description
|
|
113
|
+
- Description is **kebab-case**: lowercase letters and digits joined by single
|
|
114
|
+
hyphens (\`feat/add-dark-mode\`, not \`feat/Add_Dark_Mode\`)
|
|
115
|
+
- \`release/\` branches take a **version-style** description instead: lowercase
|
|
116
|
+
words/digits joined by dots or hyphens (\`release/1.2.0\`, \`release/2024-q1\`)
|
|
117
|
+
- No spaces, underscores, uppercase, or leading/trailing/double hyphens
|
|
118
|
+
- Keep it short — ${BRANCH_MAX} characters or fewer (a hint, not a hard limit)
|
|
119
|
+
|
|
120
|
+
## Examples
|
|
121
|
+
- \`feat/add-dark-mode\`
|
|
122
|
+
- \`fix/duplicate-auth-refresh\`
|
|
123
|
+
- \`chore/bump-deps\`
|
|
124
|
+
- \`release/1.2.0\`
|
|
125
|
+
|
|
126
|
+
Base branches (${[...BASE_BRANCHES].join(", ")}) are exempt.
|
|
127
|
+
`;
|
|
128
|
+
/** Length in Unicode code points (not UTF-16 code units). */
|
|
129
|
+
export function width(s) {
|
|
130
|
+
return [...s].length;
|
|
131
|
+
}
|
|
132
|
+
/** Uppercase the first code point of a string (astral-safe). */
|
|
133
|
+
function capitalizeFirst(s) {
|
|
134
|
+
const chars = [...s];
|
|
135
|
+
if (chars.length === 0)
|
|
136
|
+
return s;
|
|
137
|
+
return chars[0].toUpperCase() + chars.slice(1).join("");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Greedy word-wrap that preserves paragraph breaks AND hard-breaks any single
|
|
141
|
+
* token longer than `max` (URLs, long paths), so no output line ever exceeds
|
|
142
|
+
* the limit. This guarantees `format`'s output always passes `validate`.
|
|
143
|
+
*/
|
|
144
|
+
export function wrap(text, max = BODY_WRAP) {
|
|
145
|
+
return text
|
|
146
|
+
.split("\n")
|
|
147
|
+
.map((para) => {
|
|
148
|
+
const lines = [];
|
|
149
|
+
let line = "";
|
|
150
|
+
const flush = () => {
|
|
151
|
+
if (line) {
|
|
152
|
+
lines.push(line);
|
|
153
|
+
line = "";
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
for (let word of para.split(/\s+/).filter(Boolean)) {
|
|
157
|
+
// hard-break an over-long token across multiple lines
|
|
158
|
+
while (width(word) > max) {
|
|
159
|
+
flush();
|
|
160
|
+
const chars = [...word];
|
|
161
|
+
lines.push(chars.slice(0, max).join(""));
|
|
162
|
+
word = chars.slice(max).join("");
|
|
163
|
+
}
|
|
164
|
+
if (!line)
|
|
165
|
+
line = word;
|
|
166
|
+
else if (width(line) + 1 + width(word) <= max)
|
|
167
|
+
line += " " + word;
|
|
168
|
+
else {
|
|
169
|
+
flush();
|
|
170
|
+
line = word;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
flush();
|
|
174
|
+
return lines.join("\n");
|
|
175
|
+
})
|
|
176
|
+
.join("\n");
|
|
177
|
+
}
|
|
178
|
+
/** Validate a full commit message against the Udacity style guide. */
|
|
179
|
+
export function validate(message) {
|
|
180
|
+
const problems = [];
|
|
181
|
+
const warnings = [];
|
|
182
|
+
// Normalize CRLF / lone CR so line-based checks are reliable.
|
|
183
|
+
const normalized = message.replace(/\r\n?/g, "\n");
|
|
184
|
+
const lines = normalized.split("\n");
|
|
185
|
+
// Drop trailing blank lines (a trailing newline shouldn't count as a body).
|
|
186
|
+
while (lines.length > 1 && lines[lines.length - 1].trim() === "")
|
|
187
|
+
lines.pop();
|
|
188
|
+
const rawSubject = lines[0] ?? "";
|
|
189
|
+
const subject = rawSubject.replace(/\s+$/, "");
|
|
190
|
+
const m = subject.match(/^(\w+): (.*)$/);
|
|
191
|
+
if (!m) {
|
|
192
|
+
problems.push(`Subject must follow "type: Subject". Got: "${subject}".`);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const [, type, rest] = m;
|
|
196
|
+
if (!TYPES[type]) {
|
|
197
|
+
problems.push(`Unknown type "${type}". Use one of: ${Object.keys(TYPES).join(", ")}.`);
|
|
198
|
+
}
|
|
199
|
+
if (!rest) {
|
|
200
|
+
problems.push("Subject text is empty after the type.");
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
if (/^\p{Ll}/u.test(rest)) {
|
|
204
|
+
problems.push(`Subject should begin with a capital letter (got "${[...rest][0]}").`);
|
|
205
|
+
}
|
|
206
|
+
const firstWord = rest.split(/\s+/)[0];
|
|
207
|
+
if (NON_IMPERATIVE.has(firstWord.toLowerCase())) {
|
|
208
|
+
warnings.push(`"${firstWord}" looks past-tense/gerund — use the imperative mood ("Add", not "Added").`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (rawSubject !== subject)
|
|
213
|
+
problems.push("Subject has trailing whitespace.");
|
|
214
|
+
if (/\.$/.test(subject))
|
|
215
|
+
problems.push("Subject must not end with a period.");
|
|
216
|
+
if (width(subject) > SUBJECT_MAX) {
|
|
217
|
+
problems.push(`Subject line is ${width(subject)} chars (incl. the "type: " prefix); max is ${SUBJECT_MAX}.`);
|
|
218
|
+
}
|
|
219
|
+
if (lines.length > 1 && lines[1].trim() !== "") {
|
|
220
|
+
problems.push("Leave a blank line between the subject and the body.");
|
|
221
|
+
}
|
|
222
|
+
for (let i = 2; i < lines.length; i++) {
|
|
223
|
+
const line = lines[i];
|
|
224
|
+
if (width(line) > BODY_WRAP) {
|
|
225
|
+
problems.push(`Line ${i + 1} is ${width(line)} chars; wrap body/footer at ${BODY_WRAP}.`);
|
|
226
|
+
}
|
|
227
|
+
const fm = line.match(/^([A-Za-z][A-Za-z ]*?):\s*(.*)$/);
|
|
228
|
+
if (fm && FOOTER_KEYS.some((k) => k.toLowerCase() === fm[1].toLowerCase())) {
|
|
229
|
+
if (!/#\d+/.test(fm[2])) {
|
|
230
|
+
warnings.push(`Footer "${fm[1]}" should reference an issue, e.g. "${fm[1]}: #123".`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { valid: problems.length === 0, problems, warnings };
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Compose a compliant commit message from parts. Capitalizes the subject,
|
|
238
|
+
* strips a trailing period, and hard-wraps the body so the result always
|
|
239
|
+
* passes `validate`.
|
|
240
|
+
*/
|
|
241
|
+
export function formatMessage(input) {
|
|
242
|
+
const subject = capitalizeFirst(input.subject.trim().replace(/\.+$/, ""));
|
|
243
|
+
const parts = [`${input.type}: ${subject}`];
|
|
244
|
+
if (input.body?.trim())
|
|
245
|
+
parts.push("", wrap(input.body.trim(), BODY_WRAP));
|
|
246
|
+
if (input.footer?.trim())
|
|
247
|
+
parts.push("", input.footer.trim());
|
|
248
|
+
const message = parts.join("\n");
|
|
249
|
+
return { message, report: validate(message) };
|
|
250
|
+
}
|
|
251
|
+
/** Kebab-case: lowercase words (letters/digits) joined by single hyphens. */
|
|
252
|
+
const KEBAB = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
253
|
+
/**
|
|
254
|
+
* Version-style (for `release/` branches): lowercase words/digits joined by
|
|
255
|
+
* dots or hyphens, so `1.2.0`, `2.0.0-rc1`, and `2024-q1` all pass.
|
|
256
|
+
*/
|
|
257
|
+
const RELEASE_DESC = /^[a-z0-9]+([.-][a-z0-9]+)*$/;
|
|
258
|
+
/**
|
|
259
|
+
* Validate a git branch name against the companion `type/kebab-case`
|
|
260
|
+
* convention. Base/long-lived branches (main, master, dev, develop, trunk)
|
|
261
|
+
* are accepted as-is with a note. `release/` is a typed branch and takes a
|
|
262
|
+
* version-style description (e.g. `release/1.2.0`).
|
|
263
|
+
*/
|
|
264
|
+
export function validateBranch(name) {
|
|
265
|
+
const problems = [];
|
|
266
|
+
const warnings = [];
|
|
267
|
+
const branch = name.trim();
|
|
268
|
+
if (!branch) {
|
|
269
|
+
return { valid: false, problems: ["Branch name is empty."], warnings };
|
|
270
|
+
}
|
|
271
|
+
if (branch !== name) {
|
|
272
|
+
problems.push("Branch name has leading/trailing whitespace.");
|
|
273
|
+
}
|
|
274
|
+
// Base / long-lived branches are exempt from the feature-branch rule.
|
|
275
|
+
// (`release/*` is NOT exempt — it is validated as a typed branch below.)
|
|
276
|
+
if (BASE_BRANCHES.has(branch)) {
|
|
277
|
+
warnings.push(`"${branch}" is a base branch — feature-branch naming rules don't apply.`);
|
|
278
|
+
return { valid: problems.length === 0, problems, warnings };
|
|
279
|
+
}
|
|
280
|
+
const slash = branch.indexOf("/");
|
|
281
|
+
if (slash === -1) {
|
|
282
|
+
problems.push(`Branch must follow "type/description". Got: "${branch}".`);
|
|
283
|
+
return { valid: false, problems, warnings };
|
|
284
|
+
}
|
|
285
|
+
const type = branch.slice(0, slash);
|
|
286
|
+
const description = branch.slice(slash + 1);
|
|
287
|
+
if (!BRANCH_TYPES.includes(type)) {
|
|
288
|
+
problems.push(`Unknown type "${type}". Use one of: ${BRANCH_TYPES.join(", ")}.`);
|
|
289
|
+
}
|
|
290
|
+
// `release/` takes a version-style description; every other type is kebab-case.
|
|
291
|
+
const isRelease = type === "release";
|
|
292
|
+
const pattern = isRelease ? RELEASE_DESC : KEBAB;
|
|
293
|
+
if (!description) {
|
|
294
|
+
problems.push("Description after the type is empty.");
|
|
295
|
+
}
|
|
296
|
+
else if (description.includes("/")) {
|
|
297
|
+
problems.push(`Use a single "/" after the type; the description must not contain "/". Got: "${description}".`);
|
|
298
|
+
}
|
|
299
|
+
else if (!pattern.test(description)) {
|
|
300
|
+
// Give the most specific reason we can, else a general shape message.
|
|
301
|
+
if (/[A-Z]/.test(description)) {
|
|
302
|
+
problems.push(`Description must be lowercase ${isRelease ? "version-style (e.g. 1.2.0)" : "kebab-case"}. Got: "${description}".`);
|
|
303
|
+
}
|
|
304
|
+
else if (/[_ ]/.test(description)) {
|
|
305
|
+
problems.push("Use hyphens, not spaces or underscores, to separate words.");
|
|
306
|
+
}
|
|
307
|
+
else if (isRelease) {
|
|
308
|
+
problems.push(`Release description must be version-style: lowercase words/digits joined by dots or hyphens (e.g. "1.2.0", "2024-q1"). Got: "${description}".`);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
problems.push(`Description must be kebab-case: lowercase words joined by single hyphens. Got: "${description}".`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (width(branch) > BRANCH_MAX) {
|
|
315
|
+
warnings.push(`Branch name is ${width(branch)} chars; keep it ${BRANCH_MAX} or fewer.`);
|
|
316
|
+
}
|
|
317
|
+
return { valid: problems.length === 0, problems, warnings };
|
|
318
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-udacity-commit",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "MCP server that validates and formats git commit messages per the Udacity Git Commit Message Style Guide.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,9 +29,11 @@
|
|
|
29
29
|
"node": ">=18"
|
|
30
30
|
},
|
|
31
31
|
"scripts": {
|
|
32
|
-
"build": "tsc && chmod 755 build/index.js",
|
|
32
|
+
"build": "rm -rf build && tsc && chmod 755 build/index.js",
|
|
33
33
|
"start": "node build/index.js",
|
|
34
|
-
"
|
|
34
|
+
"pretest": "npm run build",
|
|
35
|
+
"test": "node --test test/*.test.mjs",
|
|
36
|
+
"prepublishOnly": "npm test",
|
|
35
37
|
"test:client": "node test-client.mjs"
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
package/build/index.d.ts
DELETED
package/build/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|