@dpesch/mantisbt-mcp-server 1.10.3 → 1.11.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/CHANGELOG.md +41 -0
- package/README.de.md +11 -8
- package/README.md +11 -8
- package/dist/config.js +13 -0
- package/dist/index.js +12 -12
- package/dist/tools/files.js +11 -2
- package/dist/tools/issues.js +47 -15
- package/dist/tools/notes.js +15 -10
- package/docs/cookbook.de.md +67 -0
- package/docs/cookbook.md +67 -0
- package/docs/examples.de.md +8 -0
- package/docs/examples.md +8 -0
- package/package.json +1 -1
- package/server.json +2 -2
- package/.gitea/PULL_REQUEST_TEMPLATE.md +0 -17
- package/.gitea/workflows/ci.yml +0 -95
- package/.github/workflows/ci.yml +0 -32
- package/scripts/hooks/pre-push.mjs +0 -220
- package/scripts/init.mjs +0 -95
- package/scripts/record-fixtures.ts +0 -160
- package/tests/cache.test.ts +0 -265
- package/tests/client.test.ts +0 -372
- package/tests/config.test.ts +0 -263
- package/tests/fixtures/get_current_user.json +0 -40
- package/tests/fixtures/get_issue.json +0 -133
- package/tests/fixtures/get_issue_enums.json +0 -67
- package/tests/fixtures/get_issue_fields_sample.json +0 -76
- package/tests/fixtures/get_project_categories.json +0 -157
- package/tests/fixtures/get_project_versions.json +0 -3
- package/tests/fixtures/get_project_versions_with_data.json +0 -52
- package/tests/fixtures/list_issues.json +0 -95
- package/tests/fixtures/list_projects.json +0 -65
- package/tests/helpers/mock-server.ts +0 -166
- package/tests/helpers/search-mocks.ts +0 -84
- package/tests/prompts/prompts.test.ts +0 -242
- package/tests/resources/resources.test.ts +0 -309
- package/tests/search/embedder.test.ts +0 -81
- package/tests/search/highlight.test.ts +0 -129
- package/tests/search/store.test.ts +0 -193
- package/tests/search/sync.test.ts +0 -249
- package/tests/search/tools.test.ts +0 -661
- package/tests/tools/config.test.ts +0 -212
- package/tests/tools/files.test.ts +0 -343
- package/tests/tools/issues.test.ts +0 -1180
- package/tests/tools/metadata.test.ts +0 -509
- package/tests/tools/monitors.test.ts +0 -101
- package/tests/tools/projects.test.ts +0 -338
- package/tests/tools/relationships.test.ts +0 -177
- package/tests/tools/string-coercion.test.ts +0 -317
- package/tests/tools/users.test.ts +0 -62
- package/tests/utils/date-filter.test.ts +0 -169
- package/tests/version-hint.test.ts +0 -230
- package/tsconfig.build.json +0 -8
- package/vitest.config.ts +0 -8
package/docs/cookbook.md
CHANGED
|
@@ -18,6 +18,8 @@ Tool-oriented recipes for the MantisBT MCP server — each recipe shows exactly
|
|
|
18
18
|
- [Apply a saved filter](#apply-a-saved-filter)
|
|
19
19
|
- [Create an issue](#create-an-issue)
|
|
20
20
|
- [Close an issue (status + resolution)](#close-an-issue-status--resolution)
|
|
21
|
+
- [Resolve an issue with a reason note (single call)](#resolve-an-issue-with-a-reason-note-single-call)
|
|
22
|
+
- [Set custom fields](#set-custom-fields)
|
|
21
23
|
- [Reassign an issue](#reassign-an-issue)
|
|
22
24
|
- [Set fix version](#set-fix-version)
|
|
23
25
|
- [Notes](#notes)
|
|
@@ -194,6 +196,7 @@ Retrieves a single issue by its numeric ID including notes, attachments, tags, a
|
|
|
194
196
|
|
|
195
197
|
**Parameters:**
|
|
196
198
|
- `id` — numeric issue ID
|
|
199
|
+
- `select` — _(optional)_ comma-separated field list (server-side projection); significantly reduces response size for large issues, e.g. `"id,summary,status,notes"`
|
|
197
200
|
|
|
198
201
|
**Request:**
|
|
199
202
|
|
|
@@ -203,6 +206,15 @@ Retrieves a single issue by its numeric ID including notes, attachments, tags, a
|
|
|
203
206
|
}
|
|
204
207
|
```
|
|
205
208
|
|
|
209
|
+
Compact variant (only the fields you need):
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"id": 1042,
|
|
214
|
+
"select": "id,summary,status,handler"
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
206
218
|
**Response:**
|
|
207
219
|
|
|
208
220
|
```json
|
|
@@ -606,6 +618,61 @@ Resolves and closes an issue. Always set **both** `status` and `resolution` —
|
|
|
606
618
|
|
|
607
619
|
---
|
|
608
620
|
|
|
621
|
+
### Resolve an issue with a reason note (single call)
|
|
622
|
+
|
|
623
|
+
Changes the status and appends a note explaining the change — in one `update_issue` call, no separate `add_note` needed.
|
|
624
|
+
|
|
625
|
+
**Tool:** `update_issue`
|
|
626
|
+
|
|
627
|
+
**Parameters:**
|
|
628
|
+
- `id` — numeric issue ID
|
|
629
|
+
- `fields` — fields to change
|
|
630
|
+
- `note` — note text appended after a successful update
|
|
631
|
+
- `note_view_state` — _(optional)_ `"public"` (default) or `"private"`
|
|
632
|
+
|
|
633
|
+
**Request:**
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{
|
|
637
|
+
"id": 1042,
|
|
638
|
+
"fields": {
|
|
639
|
+
"status": { "name": "resolved" },
|
|
640
|
+
"resolution": { "id": 20 }
|
|
641
|
+
},
|
|
642
|
+
"note": "Fixed in commit abc123 — the touch event handler was missing on iOS."
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
**Response:** the updated issue plus a `note` object with its `view_url`.
|
|
647
|
+
|
|
648
|
+
> **Note:** If the update succeeds but the note fails, the response contains `note_error` instead of failing the whole call — retry with `add_note`. For a note without field changes use `add_note` directly.
|
|
649
|
+
|
|
650
|
+
---
|
|
651
|
+
|
|
652
|
+
### Set custom fields
|
|
653
|
+
|
|
654
|
+
Writes custom field values on create or update. The same `custom_fields` format works for both `create_issue` and `update_issue` (inside `fields`).
|
|
655
|
+
|
|
656
|
+
**Tool:** `update_issue`
|
|
657
|
+
|
|
658
|
+
**Request:**
|
|
659
|
+
|
|
660
|
+
```json
|
|
661
|
+
{
|
|
662
|
+
"id": 1042,
|
|
663
|
+
"fields": {
|
|
664
|
+
"custom_fields": [
|
|
665
|
+
{ "field": { "name": "Customer" }, "value": "ACME Corp" },
|
|
666
|
+
{ "field": { "id": 3 }, "value": "2026-07-01" }
|
|
667
|
+
]
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
> **Note:** Only the listed custom fields are changed — others stay untouched. Use `get_issue_fields()` or `get_metadata()` to discover available custom fields per project. Values are always strings.
|
|
673
|
+
|
|
674
|
+
---
|
|
675
|
+
|
|
609
676
|
### Reassign an issue
|
|
610
677
|
|
|
611
678
|
Changes the handler (assignee) of an existing issue.
|
package/docs/examples.de.md
CHANGED
|
@@ -16,6 +16,8 @@ Praktische Beispiele für die Interaktion mit MantisBT über Claude, sobald der
|
|
|
16
16
|
|
|
17
17
|
> »Was ist der Status von Issue #1042?«
|
|
18
18
|
|
|
19
|
+
> »Zeige mir von Issue #1042 nur Titel, Status und Bearbeiter — ohne die vollständige Beschreibung und Notizen.«
|
|
20
|
+
|
|
19
21
|
> »Zeige mir alle Issues, die jsmith diesen Monat gemeldet hat.«
|
|
20
22
|
|
|
21
23
|
> »Welche Issues im Webshop-Projekt blockieren das Release 2.4.0?«
|
|
@@ -30,6 +32,8 @@ Praktische Beispiele für die Interaktion mit MantisBT über Claude, sobald der
|
|
|
30
32
|
|
|
31
33
|
> »Erstelle einen Feature-Request im Frontend-Projekt für einen Dunkelmodus in den Benutzereinstellungen. Niedrige Priorität, kein Fälligkeitsdatum.«
|
|
32
34
|
|
|
35
|
+
> »Erstelle einen Bug im Backend-Projekt, Kategorie API, Titel 'Token-Refresh gibt 500 zurück bei abgelaufenem Token', und setze das Custom Field 'Kunde' auf 'ACME Corp'.«
|
|
36
|
+
|
|
33
37
|
---
|
|
34
38
|
|
|
35
39
|
### Issues aktualisieren
|
|
@@ -40,6 +44,10 @@ Praktische Beispiele für die Interaktion mit MantisBT über Claude, sobald der
|
|
|
40
44
|
|
|
41
45
|
> »Ändere den Schweregrad von #1099 auf 'schwerwiegend' und füge eine Notiz hinzu: auf Produktion reproduziert.«
|
|
42
46
|
|
|
47
|
+
> »Löse Issue #1042 als behoben auf und füge im selben Schritt eine Notiz hinzu: in Release 2.4.1 auf Produktion ausgerollt.«
|
|
48
|
+
|
|
49
|
+
> »Setze das Custom Field 'Ticket-Referenz' an Issue #887 auf 'EXT-4421'.«
|
|
50
|
+
|
|
43
51
|
> »Setze die Fix-Version von Issues #901 und #902 auf 2.4.1.«
|
|
44
52
|
|
|
45
53
|
---
|
package/docs/examples.md
CHANGED
|
@@ -16,6 +16,8 @@ Practical examples of how to interact with MantisBT through Claude once the MCP
|
|
|
16
16
|
|
|
17
17
|
> "What's the status of issue #1042?"
|
|
18
18
|
|
|
19
|
+
> "Show me only the summary, status, and handler of issue #1042 — skip the full description and notes."
|
|
20
|
+
|
|
19
21
|
> "Show me all issues reported by jsmith this month."
|
|
20
22
|
|
|
21
23
|
> "Which issues in the Webshop project are blocking the 2.4.0 release?"
|
|
@@ -30,6 +32,8 @@ Practical examples of how to interact with MantisBT through Claude once the MCP
|
|
|
30
32
|
|
|
31
33
|
> "Create a feature request in the Frontend project for a dark mode in the user settings. Low priority, no due date."
|
|
32
34
|
|
|
35
|
+
> "Create a bug in the Backend project, category API, summary 'Token refresh returns 500 on expired token', and set the custom field 'Customer' to 'ACME Corp'."
|
|
36
|
+
|
|
33
37
|
---
|
|
34
38
|
|
|
35
39
|
### Updating issues
|
|
@@ -40,6 +44,10 @@ Practical examples of how to interact with MantisBT through Claude once the MCP
|
|
|
40
44
|
|
|
41
45
|
> "Change the severity of #1099 to 'major' and add a note: reproduced on production."
|
|
42
46
|
|
|
47
|
+
> "Resolve issue #1042 as fixed and add a note in the same step: deployed to production in release 2.4.1."
|
|
48
|
+
|
|
49
|
+
> "Set the custom field 'Ticket Reference' on issue #887 to 'EXT-4421'."
|
|
50
|
+
|
|
43
51
|
> "Set the fix version of issues #901 and #902 to 2.4.1."
|
|
44
52
|
|
|
45
53
|
---
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
"name": "io.github.dpesch/mantisbt-mcp-server",
|
|
4
4
|
"title": "MantisBT MCP Server",
|
|
5
5
|
"description": "MantisBT MCP server – manage issues, notes, files, tags, and relationships. With semantic search.",
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.11.0",
|
|
7
7
|
"packages": [
|
|
8
8
|
{
|
|
9
9
|
"registryType": "npm",
|
|
10
10
|
"identifier": "@dpesch/mantisbt-mcp-server",
|
|
11
|
-
"version": "1.
|
|
11
|
+
"version": "1.11.0",
|
|
12
12
|
"runtimeHint": "npx",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
## Description
|
|
2
|
-
|
|
3
|
-
<!-- Why is this change needed? What problem does it solve? -->
|
|
4
|
-
|
|
5
|
-
## Changes
|
|
6
|
-
|
|
7
|
-
<!-- Brief summary of what was changed -->
|
|
8
|
-
|
|
9
|
-
## Testing
|
|
10
|
-
|
|
11
|
-
<!-- How did you test this? -->
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
> **Note:** This repository is a public mirror. Your PR will be reviewed and
|
|
16
|
-
> integrated via cherry-pick — it will not be merged directly through Codeberg.
|
|
17
|
-
> You will be credited in the commit message and CHANGELOG.
|
package/.gitea/workflows/ci.yml
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
name: CI / Publish
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
tags:
|
|
7
|
-
- 'v*'
|
|
8
|
-
pull_request:
|
|
9
|
-
branches: [main]
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
ci:
|
|
13
|
-
runs-on: codeberg-small
|
|
14
|
-
if: |
|
|
15
|
-
github.event_name == 'pull_request' ||
|
|
16
|
-
startsWith(github.ref, 'refs/tags/') ||
|
|
17
|
-
!startsWith(github.event.head_commit.message, 'chore: release')
|
|
18
|
-
|
|
19
|
-
steps:
|
|
20
|
-
- uses: actions/checkout@v4
|
|
21
|
-
|
|
22
|
-
- name: Set up Node.js
|
|
23
|
-
uses: actions/setup-node@v4
|
|
24
|
-
with:
|
|
25
|
-
node-version: '20'
|
|
26
|
-
cache: 'npm'
|
|
27
|
-
|
|
28
|
-
- name: Install dependencies
|
|
29
|
-
run: npm ci
|
|
30
|
-
|
|
31
|
-
- name: Typecheck
|
|
32
|
-
run: npm run typecheck
|
|
33
|
-
|
|
34
|
-
- name: Test
|
|
35
|
-
run: npm test
|
|
36
|
-
|
|
37
|
-
- name: Build
|
|
38
|
-
run: npm run build
|
|
39
|
-
|
|
40
|
-
publish:
|
|
41
|
-
runs-on: codeberg-small
|
|
42
|
-
needs: ci
|
|
43
|
-
if: startsWith(github.ref, 'refs/tags/v')
|
|
44
|
-
|
|
45
|
-
steps:
|
|
46
|
-
- uses: actions/checkout@v4
|
|
47
|
-
|
|
48
|
-
- name: Set up Node.js
|
|
49
|
-
uses: actions/setup-node@v4
|
|
50
|
-
with:
|
|
51
|
-
node-version: '20'
|
|
52
|
-
cache: 'npm'
|
|
53
|
-
registry-url: 'https://registry.npmjs.org'
|
|
54
|
-
|
|
55
|
-
- name: Install dependencies
|
|
56
|
-
run: npm ci
|
|
57
|
-
|
|
58
|
-
- name: Build
|
|
59
|
-
run: npm run build
|
|
60
|
-
|
|
61
|
-
- name: Publish to npm
|
|
62
|
-
run: npm publish --access public
|
|
63
|
-
env:
|
|
64
|
-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
65
|
-
|
|
66
|
-
- name: Extract release notes from CHANGELOG
|
|
67
|
-
run: |
|
|
68
|
-
VERSION="${{ github.ref_name }}"
|
|
69
|
-
VERSION_NUM="${VERSION#v}"
|
|
70
|
-
awk "/^## \[${VERSION_NUM}\]/{found=1; next} found && /^## \[/{exit} found{print}" CHANGELOG.md \
|
|
71
|
-
| sed '/^[[:space:]]*$/d' > /tmp/release_notes.md
|
|
72
|
-
echo "Release notes:"
|
|
73
|
-
cat /tmp/release_notes.md
|
|
74
|
-
|
|
75
|
-
- name: Create Codeberg release
|
|
76
|
-
run: |
|
|
77
|
-
curl -s -X POST "https://codeberg.org/api/v1/repos/dpesch/mantisbt-mcp-server/releases" \
|
|
78
|
-
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
|
79
|
-
-H "Content-Type: application/json" \
|
|
80
|
-
-d "$(jq -n \
|
|
81
|
-
--arg tag "${{ github.ref_name }}" \
|
|
82
|
-
--arg name "${{ github.ref_name }}" \
|
|
83
|
-
--rawfile body /tmp/release_notes.md \
|
|
84
|
-
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')"
|
|
85
|
-
|
|
86
|
-
- name: Create GitHub release
|
|
87
|
-
run: |
|
|
88
|
-
curl -s -X POST "https://api.github.com/repos/dpesch/mantisbt-mcp-server/releases" \
|
|
89
|
-
-H "Authorization: Bearer ${{ secrets.GH_RELEASE_TOKEN }}" \
|
|
90
|
-
-H "Content-Type: application/json" \
|
|
91
|
-
-d "$(jq -n \
|
|
92
|
-
--arg tag "${{ github.ref_name }}" \
|
|
93
|
-
--arg name "${{ github.ref_name }}" \
|
|
94
|
-
--rawfile body /tmp/release_notes.md \
|
|
95
|
-
'{tag_name: $tag, name: $name, body: $body, draft: false, prerelease: false}')"
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
|
|
13
|
-
steps:
|
|
14
|
-
- uses: actions/checkout@v4
|
|
15
|
-
|
|
16
|
-
- name: Set up Node.js
|
|
17
|
-
uses: actions/setup-node@v4
|
|
18
|
-
with:
|
|
19
|
-
node-version: '20'
|
|
20
|
-
cache: 'npm'
|
|
21
|
-
|
|
22
|
-
- name: Install dependencies
|
|
23
|
-
run: npm ci
|
|
24
|
-
|
|
25
|
-
- name: Type check
|
|
26
|
-
run: npm run typecheck
|
|
27
|
-
|
|
28
|
-
- name: Build
|
|
29
|
-
run: npm run build
|
|
30
|
-
|
|
31
|
-
- name: Test
|
|
32
|
-
run: npm test
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// pre-push hook — typecheck gate + Codeberg .claude/ filter
|
|
3
|
-
//
|
|
4
|
-
// Typecheck runs for every push (origin and upstream) to catch type errors
|
|
5
|
-
// before they reach CI.
|
|
6
|
-
//
|
|
7
|
-
// Codeberg filter handles multi-commit pushes correctly:
|
|
8
|
-
// 1. Fetches the actual Codeberg tip via ls-remote
|
|
9
|
-
// 2. Finds the local commit whose filtered tree matches that tip (anchor)
|
|
10
|
-
// 3. Filters every commit between anchor and tip in order (oldest first)
|
|
11
|
-
// 4. Builds a proper filtered chain anchored to the actual remote tip
|
|
12
|
-
// 5. Pushes the filtered tip with --force
|
|
13
|
-
// 6. Exits 2 to block git's unfiltered push
|
|
14
|
-
// Exit codes: 0 = no Codeberg refs, 1 = error, 2 = success (filtered push done)
|
|
15
|
-
//
|
|
16
|
-
// Branches are processed before tags so the shaMap is available for tags
|
|
17
|
-
// that point to commits already processed as part of the branch.
|
|
18
|
-
//
|
|
19
|
-
// ⚠ IMPORTANT: upstream (Codeberg) is a filtered mirror — push-only.
|
|
20
|
-
// Never run git pull, git fetch + merge, or git rebase against upstream.
|
|
21
|
-
// Filtered commits have different SHAs; pulling them back creates duplicate
|
|
22
|
-
// history. Use origin (Gitolite) as the authoritative source.
|
|
23
|
-
//
|
|
24
|
-
// --force is always required for Codeberg: filtered SHAs structurally diverge
|
|
25
|
-
// from local SHAs, so git rejects main as non-fast-forward before the hook
|
|
26
|
-
// runs unless --force bypasses that check.
|
|
27
|
-
|
|
28
|
-
import { execSync } from 'node:child_process';
|
|
29
|
-
import { createInterface } from 'node:readline';
|
|
30
|
-
|
|
31
|
-
// Recursion guard — must be first to avoid running typecheck during recursive pushes
|
|
32
|
-
if (process.env._PREPUSH_FILTER_ACTIVE) process.exit(0);
|
|
33
|
-
|
|
34
|
-
// Run typecheck before every push (catches type errors before they hit CI)
|
|
35
|
-
try {
|
|
36
|
-
execSync('npm run typecheck', { stdio: 'inherit' });
|
|
37
|
-
} catch {
|
|
38
|
-
console.error('✗ Typecheck failed — push aborted. Fix type errors first.');
|
|
39
|
-
process.exit(1);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const [,, , remoteUrl] = process.argv;
|
|
43
|
-
|
|
44
|
-
// Only intercept Codeberg pushes for the filter step
|
|
45
|
-
if (!remoteUrl?.includes('codeberg.org')) process.exit(0);
|
|
46
|
-
|
|
47
|
-
console.log('→ Codeberg push detected — filtering .claude/ directory...');
|
|
48
|
-
|
|
49
|
-
const ZERO_SHA = '0'.repeat(40);
|
|
50
|
-
|
|
51
|
-
const git = (cmd, opts = {}) => {
|
|
52
|
-
try {
|
|
53
|
-
return execSync(`git ${cmd}`, { encoding: 'utf8', ...opts }).trim();
|
|
54
|
-
} catch (err) {
|
|
55
|
-
console.error(`✗ git ${cmd}\n${err.stderr || err.message}`);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const gitOptional = (cmd, opts = {}) => {
|
|
61
|
-
try { return execSync(`git ${cmd}`, { encoding: 'utf8', ...opts }).trim(); }
|
|
62
|
-
catch { return ''; }
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
const isSha = s => /^[0-9a-f]{40}$/.test(s);
|
|
66
|
-
|
|
67
|
-
// Remove .claude/ from the root tree of a commit and return the new tree SHA.
|
|
68
|
-
const filterTree = commitSha => {
|
|
69
|
-
const entries = git(`ls-tree "${commitSha}^{tree}"`);
|
|
70
|
-
const filtered = entries.split('\n').filter(e => !e.match(/\t\.claude$/)).join('\n');
|
|
71
|
-
const tree = git('mktree', { input: filtered });
|
|
72
|
-
if (!isSha(tree)) { console.error(`✗ mktree failed for ${commitSha}`); process.exit(1); }
|
|
73
|
-
return tree;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// Create a filtered commit object preserving all original metadata.
|
|
77
|
-
const makeFilteredCommit = (commitSha, filteredTree, mappedParents) => {
|
|
78
|
-
const log = git(`log -1 --format=%an%n%ae%n%aI%n%cn%n%ce%n%cI%n%n%B "${commitSha}"`);
|
|
79
|
-
const [an, ae, aI, cn, ce, cI, , ...msgLines] = log.split('\n');
|
|
80
|
-
const commitMsg = msgLines.join('\n').trimEnd();
|
|
81
|
-
const parentFlags = mappedParents.map(p => `-p ${p}`).join(' ');
|
|
82
|
-
const env = {
|
|
83
|
-
...process.env,
|
|
84
|
-
GIT_AUTHOR_NAME: an, GIT_AUTHOR_EMAIL: ae, GIT_AUTHOR_DATE: aI,
|
|
85
|
-
GIT_COMMITTER_NAME: cn, GIT_COMMITTER_EMAIL: ce, GIT_COMMITTER_DATE: cI,
|
|
86
|
-
};
|
|
87
|
-
const result = git(`commit-tree "${filteredTree}" ${parentFlags}`, { env, input: commitMsg });
|
|
88
|
-
if (!isSha(result)) { console.error(`✗ commit-tree failed for ${commitSha}`); process.exit(1); }
|
|
89
|
-
return result;
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
// Scan ancestors of tipSha (newest first) and return the most recent one whose
|
|
93
|
-
// filtered root tree equals the tree of remoteSha. Returns null if not found.
|
|
94
|
-
const findLocalAnchor = (tipSha, remoteSha, maxDepth = 100) => {
|
|
95
|
-
const remoteTree = gitOptional(`rev-parse "${remoteSha}^{tree}"`);
|
|
96
|
-
if (!remoteTree) return null;
|
|
97
|
-
const candidates = gitOptional(`rev-list --max-count=${maxDepth} "${tipSha}"`);
|
|
98
|
-
if (!candidates) return null;
|
|
99
|
-
for (const sha of candidates.split('\n').filter(Boolean)) {
|
|
100
|
-
if (filterTree(sha) === remoteTree) return sha;
|
|
101
|
-
}
|
|
102
|
-
return null;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
const rl = createInterface({ input: process.stdin });
|
|
106
|
-
const lines = [];
|
|
107
|
-
rl.on('line', line => { if (line.trim()) lines.push(line.trim()); });
|
|
108
|
-
|
|
109
|
-
rl.on('close', () => {
|
|
110
|
-
// Process branches before tags so shaMap is populated when tags are handled.
|
|
111
|
-
const branches = lines.filter(l => l.split(' ')[2]?.startsWith('refs/heads/'));
|
|
112
|
-
const others = lines.filter(l => !l.split(' ')[2]?.startsWith('refs/heads/'));
|
|
113
|
-
|
|
114
|
-
// Batch-fetch all tags from Codeberg (single ls-remote call, ref → sha).
|
|
115
|
-
const remoteTagsRaw = gitOptional(`ls-remote --tags "${remoteUrl}"`);
|
|
116
|
-
const remoteTagMap = new Map(
|
|
117
|
-
remoteTagsRaw
|
|
118
|
-
? remoteTagsRaw.split('\n').filter(Boolean)
|
|
119
|
-
.map(l => { const [sha, ref] = l.split(/\s+/); return [ref, sha]; })
|
|
120
|
-
.filter(([ref]) => ref)
|
|
121
|
-
: []
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
// localSha → filteredSha, accumulated across all refs in this push.
|
|
125
|
-
const shaMap = {};
|
|
126
|
-
let pushed = 0;
|
|
127
|
-
|
|
128
|
-
for (const line of [...branches, ...others]) {
|
|
129
|
-
const parts = line.split(' ');
|
|
130
|
-
if (parts.length < 3) { console.error(`✗ Malformed push input: ${line}`); process.exit(1); }
|
|
131
|
-
const [, localSha, remoteRef] = parts;
|
|
132
|
-
|
|
133
|
-
if (localSha === ZERO_SHA) continue; // deletion — skip
|
|
134
|
-
|
|
135
|
-
const label = remoteRef.replace('refs/heads/', '').replace('refs/tags/', '');
|
|
136
|
-
|
|
137
|
-
if (remoteRef.startsWith('refs/tags/')) {
|
|
138
|
-
if (remoteTagMap.has(remoteRef)) {
|
|
139
|
-
console.log(` ↷ ${label} already on Codeberg — skipping`);
|
|
140
|
-
continue;
|
|
141
|
-
}
|
|
142
|
-
if (!gitOptional(`rev-parse --verify "${localSha}"`)) {
|
|
143
|
-
console.warn(` ⚠ ${label} — commit ${localSha.slice(0, 8)} not found locally, skipping`);
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// If this commit was already filtered as part of a branch push, reuse it.
|
|
149
|
-
if (localSha in shaMap) {
|
|
150
|
-
try {
|
|
151
|
-
execSync(`git push "${remoteUrl}" "${shaMap[localSha]}:${remoteRef}" --force`,
|
|
152
|
-
{ env: { ...process.env, _PREPUSH_FILTER_ACTIVE: '1' }, stdio: 'inherit' });
|
|
153
|
-
} catch {
|
|
154
|
-
console.error(`✗ Push failed for ${label}`); process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
console.log(`✓ ${label} pushed to Codeberg (without .claude/)`);
|
|
157
|
-
pushed++;
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Tags: existing ones were skipped above, so only new tags reach here — no remote anchor.
|
|
162
|
-
// Branches: query individually to find the incremental anchor for filtered chain building.
|
|
163
|
-
const actualRemoteSha = remoteRef.startsWith('refs/tags/')
|
|
164
|
-
? ''
|
|
165
|
-
: (gitOptional(`ls-remote "${remoteUrl}" "${remoteRef}"`)?.split(/\s+/)[0] ?? '');
|
|
166
|
-
|
|
167
|
-
if (!actualRemoteSha) {
|
|
168
|
-
// New ref on Codeberg — filter the tip with no parent.
|
|
169
|
-
shaMap[localSha] = makeFilteredCommit(localSha, filterTree(localSha), []);
|
|
170
|
-
} else {
|
|
171
|
-
// Find the local commit that corresponds to the current Codeberg tip.
|
|
172
|
-
const localAnchor = findLocalAnchor(localSha, actualRemoteSha);
|
|
173
|
-
|
|
174
|
-
if (localAnchor) {
|
|
175
|
-
shaMap[localAnchor] = actualRemoteSha;
|
|
176
|
-
|
|
177
|
-
// Filter all commits between anchor and tip, oldest first.
|
|
178
|
-
const revList = gitOptional(`rev-list --reverse "${localAnchor}..${localSha}"`);
|
|
179
|
-
const commits = revList ? revList.split('\n').filter(Boolean) : [];
|
|
180
|
-
|
|
181
|
-
for (const sha of commits) {
|
|
182
|
-
const filteredTree = filterTree(sha);
|
|
183
|
-
const parents = gitOptional(`log -1 --format=%P "${sha}"`);
|
|
184
|
-
const parentShas = parents ? parents.split(' ').filter(Boolean) : [];
|
|
185
|
-
// Map each parent through shaMap; fall back to actualRemoteSha for
|
|
186
|
-
// parents outside the current range (already on the remote).
|
|
187
|
-
const mappedParents = parentShas
|
|
188
|
-
.map(p => shaMap[p] ?? actualRemoteSha)
|
|
189
|
-
.filter(Boolean);
|
|
190
|
-
shaMap[sha] = makeFilteredCommit(sha, filteredTree, mappedParents);
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
// Fallback: anchor not found — branch only (tags exit early above).
|
|
194
|
-
const remoteExists = !!gitOptional(`rev-parse --verify "${actualRemoteSha}"`);
|
|
195
|
-
console.warn(` ⚠ Could not find local base for ${label}, filtering tip only`);
|
|
196
|
-
shaMap[localSha] = makeFilteredCommit(localSha, filterTree(localSha), remoteExists ? [actualRemoteSha] : []);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const filteredTip = shaMap[localSha];
|
|
201
|
-
if (!filteredTip) {
|
|
202
|
-
console.error(`✗ Could not compute filtered SHA for ${localSha}`);
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
try {
|
|
207
|
-
execSync(`git push "${remoteUrl}" "${filteredTip}:${remoteRef}" --force`,
|
|
208
|
-
{ env: { ...process.env, _PREPUSH_FILTER_ACTIVE: '1' }, stdio: 'inherit' });
|
|
209
|
-
} catch {
|
|
210
|
-
console.error(`✗ Push to Codeberg failed for ${label}`); process.exit(1);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
console.log(`✓ ${label} pushed to Codeberg (without .claude/)`);
|
|
214
|
-
pushed++;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
if (pushed === 0) process.exit(0);
|
|
218
|
-
console.log('→ Filtered push complete. Blocking unfiltered push.');
|
|
219
|
-
process.exit(2); // 2 = success (filtered push done, unfiltered blocked); 1 = error
|
|
220
|
-
});
|
package/scripts/init.mjs
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Project setup script — run via: npm run init
|
|
3
|
-
//
|
|
4
|
-
// Steps:
|
|
5
|
-
// 1. Check Node.js version (requires >=18)
|
|
6
|
-
// 2. Install dependencies (npm install)
|
|
7
|
-
// 3. Install git hooks from scripts/hooks/ into .git/hooks/
|
|
8
|
-
// 4. Run typecheck to verify the setup
|
|
9
|
-
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import { copyFileSync, chmodSync, existsSync, mkdirSync } from 'node:fs';
|
|
12
|
-
import { resolve, dirname, join } from 'node:path';
|
|
13
|
-
import { fileURLToPath } from 'node:url';
|
|
14
|
-
|
|
15
|
-
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
16
|
-
// Auf Windows .cmd-Dateien via cmd.exe aufrufen — kein shell:true nötig, keine Deprecation-Warnung
|
|
17
|
-
const [npmBin, npmBaseArgs] = process.platform === 'win32'
|
|
18
|
-
? ['cmd.exe', ['/c', 'npm']]
|
|
19
|
-
: ['npm', []];
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// 1. Node.js version check
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const [major] = process.versions.node.split('.').map(Number);
|
|
26
|
-
if (major < 18) {
|
|
27
|
-
console.error(`✗ Node.js >= 18 required, found ${process.version}`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
console.log(`✓ Node.js ${process.version}`);
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// 2. Install dependencies
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
console.log('\n→ Installing dependencies...');
|
|
37
|
-
const install = spawnSync(npmBin, [...npmBaseArgs, 'install'], {
|
|
38
|
-
stdio: 'inherit',
|
|
39
|
-
cwd: root,
|
|
40
|
-
});
|
|
41
|
-
if (install.status !== 0) {
|
|
42
|
-
console.error('✗ npm install failed');
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// 3. Install git hooks
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
|
|
50
|
-
console.log('\n→ Installing git hooks...');
|
|
51
|
-
|
|
52
|
-
const hooksSourceDir = resolve(root, 'scripts/hooks');
|
|
53
|
-
const gitHooksDir = resolve(root, '.git/hooks');
|
|
54
|
-
|
|
55
|
-
if (!existsSync(gitHooksDir)) {
|
|
56
|
-
mkdirSync(gitHooksDir, { recursive: true });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const hooks = ['pre-push'];
|
|
60
|
-
for (const hook of hooks) {
|
|
61
|
-
const src = resolve(hooksSourceDir, `${hook}.mjs`);
|
|
62
|
-
const dest = resolve(gitHooksDir, hook);
|
|
63
|
-
|
|
64
|
-
if (!existsSync(src)) {
|
|
65
|
-
console.warn(` ⚠ Hook source not found, skipping: scripts/hooks/${hook}.mjs`);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
copyFileSync(src, dest);
|
|
70
|
-
|
|
71
|
-
// chmod +x (no-op on Windows — git runs hooks directly via shebang)
|
|
72
|
-
try {
|
|
73
|
-
chmodSync(dest, 0o755);
|
|
74
|
-
} catch {
|
|
75
|
-
// Silently ignore on platforms that don't support chmod
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
console.log(` ✓ .git/hooks/${hook} installed`);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
// 4. Typecheck
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
|
|
85
|
-
console.log('\n→ Running typecheck...');
|
|
86
|
-
const check = spawnSync(npmBin, [...npmBaseArgs, 'run', 'typecheck'], {
|
|
87
|
-
stdio: 'inherit',
|
|
88
|
-
cwd: root,
|
|
89
|
-
});
|
|
90
|
-
if (check.status !== 0) {
|
|
91
|
-
console.error('✗ Typecheck failed — check type errors above');
|
|
92
|
-
process.exit(1);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
console.log('\n✓ Setup complete. Happy hacking!');
|