@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.
Files changed (54) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.de.md +11 -8
  3. package/README.md +11 -8
  4. package/dist/config.js +13 -0
  5. package/dist/index.js +12 -12
  6. package/dist/tools/files.js +11 -2
  7. package/dist/tools/issues.js +47 -15
  8. package/dist/tools/notes.js +15 -10
  9. package/docs/cookbook.de.md +67 -0
  10. package/docs/cookbook.md +67 -0
  11. package/docs/examples.de.md +8 -0
  12. package/docs/examples.md +8 -0
  13. package/package.json +1 -1
  14. package/server.json +2 -2
  15. package/.gitea/PULL_REQUEST_TEMPLATE.md +0 -17
  16. package/.gitea/workflows/ci.yml +0 -95
  17. package/.github/workflows/ci.yml +0 -32
  18. package/scripts/hooks/pre-push.mjs +0 -220
  19. package/scripts/init.mjs +0 -95
  20. package/scripts/record-fixtures.ts +0 -160
  21. package/tests/cache.test.ts +0 -265
  22. package/tests/client.test.ts +0 -372
  23. package/tests/config.test.ts +0 -263
  24. package/tests/fixtures/get_current_user.json +0 -40
  25. package/tests/fixtures/get_issue.json +0 -133
  26. package/tests/fixtures/get_issue_enums.json +0 -67
  27. package/tests/fixtures/get_issue_fields_sample.json +0 -76
  28. package/tests/fixtures/get_project_categories.json +0 -157
  29. package/tests/fixtures/get_project_versions.json +0 -3
  30. package/tests/fixtures/get_project_versions_with_data.json +0 -52
  31. package/tests/fixtures/list_issues.json +0 -95
  32. package/tests/fixtures/list_projects.json +0 -65
  33. package/tests/helpers/mock-server.ts +0 -166
  34. package/tests/helpers/search-mocks.ts +0 -84
  35. package/tests/prompts/prompts.test.ts +0 -242
  36. package/tests/resources/resources.test.ts +0 -309
  37. package/tests/search/embedder.test.ts +0 -81
  38. package/tests/search/highlight.test.ts +0 -129
  39. package/tests/search/store.test.ts +0 -193
  40. package/tests/search/sync.test.ts +0 -249
  41. package/tests/search/tools.test.ts +0 -661
  42. package/tests/tools/config.test.ts +0 -212
  43. package/tests/tools/files.test.ts +0 -343
  44. package/tests/tools/issues.test.ts +0 -1180
  45. package/tests/tools/metadata.test.ts +0 -509
  46. package/tests/tools/monitors.test.ts +0 -101
  47. package/tests/tools/projects.test.ts +0 -338
  48. package/tests/tools/relationships.test.ts +0 -177
  49. package/tests/tools/string-coercion.test.ts +0 -317
  50. package/tests/tools/users.test.ts +0 -62
  51. package/tests/utils/date-filter.test.ts +0 -169
  52. package/tests/version-hint.test.ts +0 -230
  53. package/tsconfig.build.json +0 -8
  54. 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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dpesch/mantisbt-mcp-server",
3
- "version": "1.10.3",
3
+ "version": "1.11.0",
4
4
  "mcpName": "io.github.dpesch/mantisbt-mcp-server",
5
5
  "description": "MCP server for MantisBT REST API – read and manage bug tracker issues",
6
6
  "author": "Dominik Pesch",
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.10.3",
6
+ "version": "1.11.0",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@dpesch/mantisbt-mcp-server",
11
- "version": "1.10.3",
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.
@@ -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}')"
@@ -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!');