@bridge_gpt/mcp-server 0.2.30 → 0.2.32
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 -21
- package/build/command-assets-doctor.js +72 -0
- package/build/command-provisioning.js +157 -0
- package/build/credential-store.js +302 -18
- package/build/doctor.js +80 -35
- package/build/executor/test-clock.js +3 -1
- package/build/executor/worktree.js +31 -2
- package/build/git-ignore-utils.js +67 -14
- package/build/index.js +2874 -2873
- package/build/install-bridge.js +1513 -453
- package/build/install-doctor.js +37 -4
- package/build/readme.generated.js +1 -1
- package/build/start-tickets-prereqs.js +45 -0
- package/build/start-tickets.js +44 -3
- package/build/version.generated.js +1 -1
- package/package.json +4 -3
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { fetchAndResolveBaseSha } from "../base-ref.js";
|
|
10
10
|
import { commandSucceeded } from "../start-tickets-prereqs.js";
|
|
11
11
|
import { createWorktreeForTicket } from "../worktree-core.js";
|
|
12
|
+
import { provisionCommandsForWorktree } from "../command-provisioning.js";
|
|
12
13
|
/**
|
|
13
14
|
* Resolve the branch for a real spawn job: `expected_branch` wins; else
|
|
14
15
|
* `feature/<ticket_key>`; else a structured contract failure.
|
|
@@ -35,6 +36,34 @@ function toWorktreeCoreDeps(deps) {
|
|
|
35
36
|
cwd: deps.cwd,
|
|
36
37
|
};
|
|
37
38
|
}
|
|
39
|
+
/** Build the command-provisioning boundary from the executor deps (BAPI-664). */
|
|
40
|
+
function toCommandProvisioningDeps(deps) {
|
|
41
|
+
return {
|
|
42
|
+
readFile: deps.readFile,
|
|
43
|
+
writeFile: deps.writeFile,
|
|
44
|
+
mkdir: deps.mkdir,
|
|
45
|
+
runCommand: deps.runCommand,
|
|
46
|
+
platform: deps.platform,
|
|
47
|
+
cwd: deps.cwd,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Finalize a resolved executor worktree by bootstrapping the packaged Claude
|
|
52
|
+
* command assets into it (BAPI-664) BEFORE the worker process is handed the
|
|
53
|
+
* worktree path. A provisioning failure is mapped into the executor's existing
|
|
54
|
+
* bounded non-success worktree result so the affected worker is not launched with
|
|
55
|
+
* a missing slash command, while the executor process stays available for later
|
|
56
|
+
* jobs. On success the existing successful ensure-worktree result shape is
|
|
57
|
+
* preserved unchanged. Errors stay secret-free (no raw filesystem exception, Git
|
|
58
|
+
* output, or command contents).
|
|
59
|
+
*/
|
|
60
|
+
async function finalizeExecutorWorktree(worktreePath, branch, deps) {
|
|
61
|
+
const provisioned = await provisionCommandsForWorktree(worktreePath, toCommandProvisioningDeps(deps));
|
|
62
|
+
if (!provisioned.ok) {
|
|
63
|
+
return { ok: false, error: provisioned.error };
|
|
64
|
+
}
|
|
65
|
+
return { ok: true, worktreePath, branch };
|
|
66
|
+
}
|
|
38
67
|
/**
|
|
39
68
|
* Ensure the worktree for a job exists, delegating to the shared Worktrunk
|
|
40
69
|
* primitive. A create failure is returned as a structured failure that the job
|
|
@@ -92,7 +121,7 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
92
121
|
// (= `origin/<branch>`) inside the primitive, so both land at origin.
|
|
93
122
|
{ freshenFromOrigin: originRef });
|
|
94
123
|
if (row.status === "created" && typeof row.path === "string") {
|
|
95
|
-
return
|
|
124
|
+
return finalizeExecutorWorktree(row.path, branch, deps);
|
|
96
125
|
}
|
|
97
126
|
return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
|
|
98
127
|
}
|
|
@@ -117,7 +146,7 @@ export async function ensureExecutorWorktree(job, options, deps, policy = {}) {
|
|
|
117
146
|
const baseSha = resolvedBase.base_sha;
|
|
118
147
|
const row = await createWorktreeForTicket(toWorktreeCoreDeps(deps), key, { [key]: branch }, options.worktrunkBinary, baseSha, guardStaleWorktree, { alignExistingBranchTo: baseSha, verifyHeadMatches: baseSha });
|
|
119
148
|
if (row.status === "created" && typeof row.path === "string") {
|
|
120
|
-
return
|
|
149
|
+
return finalizeExecutorWorktree(row.path, branch, deps);
|
|
121
150
|
}
|
|
122
151
|
return { ok: false, error: row.error ?? `worktree creation failed for branch '${branch}'` };
|
|
123
152
|
}
|
|
@@ -4,22 +4,49 @@
|
|
|
4
4
|
* Two idempotent, exact-line appenders:
|
|
5
5
|
* - `ensureGitignored` writes the tracked `<cwd>/.gitignore` (shared with the
|
|
6
6
|
* existing `--init` scaffolding behavior).
|
|
7
|
-
* - `ensureGitInfoExcluded` writes the UNtracked
|
|
8
|
-
* used for Tier-3 file credentials
|
|
9
|
-
* must never
|
|
7
|
+
* - `ensureGitInfoExcluded` writes the UNtracked, repository-common
|
|
8
|
+
* `info/exclude`, used for Tier-3 file credentials and (BAPI-664) bootstrapped
|
|
9
|
+
* Claude command assets that must never enter version control and must never
|
|
10
|
+
* touch the tracked `.gitignore`.
|
|
11
|
+
*
|
|
12
|
+
* Linked-worktree note (BAPI-664): a worktree created by `git worktree add` (as
|
|
13
|
+
* Worktrunk does) does NOT contain a `.git` directory — it contains a `.git`
|
|
14
|
+
* *file* that points at the common Git directory. So the historical
|
|
15
|
+
* `<worktreeRoot>/.git/info/exclude` join is WRONG for a linked worktree. When a
|
|
16
|
+
* list-based `runCommand` and `platform` are injected, this helper asks Git for
|
|
17
|
+
* the authoritative path with `git rev-parse --git-path info/exclude` (relative
|
|
18
|
+
* to the worktree root); `info/exclude` lives in the common Git directory shared
|
|
19
|
+
* by all sibling worktrees, and an ignore rule never hides an already-tracked
|
|
20
|
+
* file, so the shared scope is safe. When `runCommand`/`platform` are absent the
|
|
21
|
+
* helper falls back to the legacy `<worktreeRoot>/.git/info/exclude` join, which
|
|
22
|
+
* remains correct for a main repository whose `.git` is a real directory (the
|
|
23
|
+
* SFCC dw.json caller relies on this).
|
|
10
24
|
*
|
|
11
25
|
* Both are dependency-injected so they are unit-testable without real I/O, and
|
|
12
|
-
* neither ever includes credential file contents
|
|
26
|
+
* neither ever includes credential file contents — or raw Git/command output —
|
|
27
|
+
* in an error.
|
|
13
28
|
*/
|
|
14
29
|
import path from "path";
|
|
30
|
+
/** The `path` API for the target platform (win32 vs posix). */
|
|
31
|
+
function pathApiForPlatform(platform) {
|
|
32
|
+
return platform === "win32" ? path.win32 : path.posix;
|
|
33
|
+
}
|
|
15
34
|
/** True when `content` already contains `entry` as its own exact (trimmed) line. */
|
|
16
35
|
function hasExactLine(content, entry) {
|
|
17
36
|
return content.split("\n").some((line) => line.trim() === entry);
|
|
18
37
|
}
|
|
19
|
-
/**
|
|
38
|
+
/** Detect the newline convention of existing content (CRLF wins if present). */
|
|
39
|
+
function detectNewline(content) {
|
|
40
|
+
return /\r\n/.test(content) ? "\r\n" : "\n";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Append `entry` to `content`, inserting a separating newline only if needed and
|
|
44
|
+
* using the file's own newline convention (CRLF preserved when already present).
|
|
45
|
+
*/
|
|
20
46
|
function appendLine(content, entry) {
|
|
21
|
-
const
|
|
22
|
-
|
|
47
|
+
const newline = detectNewline(content);
|
|
48
|
+
const separator = content.length > 0 && !content.endsWith("\n") ? newline : "";
|
|
49
|
+
return content + separator + entry + newline;
|
|
23
50
|
}
|
|
24
51
|
/**
|
|
25
52
|
* Idempotently ensure `filePath` is listed in `<cwd>/.gitignore`. An absolute
|
|
@@ -41,20 +68,46 @@ export async function ensureGitignored(cwd, filePath, deps) {
|
|
|
41
68
|
await deps.writeFile(gitignorePath, appendLine(content, entry));
|
|
42
69
|
}
|
|
43
70
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
71
|
+
* Resolve the worktree's `info/exclude` path. With an injected `runCommand` +
|
|
72
|
+
* `platform`, ask Git (`git rev-parse --git-path info/exclude`) so linked
|
|
73
|
+
* worktrees resolve to the common Git directory; a Git failure or empty path
|
|
74
|
+
* throws a bounded, output-free error. Without them, fall back to the legacy
|
|
75
|
+
* `<worktreeRoot>/.git/info/exclude` join (correct for a main repository).
|
|
48
76
|
*/
|
|
49
|
-
|
|
77
|
+
async function resolveInfoExcludeLocation(worktreeRoot, deps) {
|
|
78
|
+
if (deps.runCommand && deps.platform) {
|
|
79
|
+
const api = pathApiForPlatform(deps.platform);
|
|
80
|
+
const result = await deps.runCommand("git", ["rev-parse", "--git-path", "info/exclude"], { cwd: worktreeRoot });
|
|
81
|
+
if (result.exitCode !== 0) {
|
|
82
|
+
// Never surface raw Git stdout/stderr (it can echo paths / tokens).
|
|
83
|
+
throw new Error("Failed to resolve the worktree info/exclude path via 'git rev-parse --git-path'.");
|
|
84
|
+
}
|
|
85
|
+
const raw = result.stdout.trim();
|
|
86
|
+
if (raw.length === 0) {
|
|
87
|
+
throw new Error("'git rev-parse --git-path info/exclude' returned an empty path.");
|
|
88
|
+
}
|
|
89
|
+
const excludePath = api.isAbsolute(raw) ? api.normalize(raw) : api.resolve(worktreeRoot, raw);
|
|
90
|
+
return { excludePath, infoDir: api.dirname(excludePath) };
|
|
91
|
+
}
|
|
50
92
|
const infoDir = path.join(worktreeRoot, ".git", "info");
|
|
51
|
-
|
|
93
|
+
return { excludePath: path.join(infoDir, "exclude"), infoDir };
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Idempotently ensure `relativePath` is listed in the worktree's local-only,
|
|
97
|
+
* repository-common `info/exclude`. Resolves the exclude path via Git for linked
|
|
98
|
+
* worktrees (see module docstring) and creates its parent directory if missing.
|
|
99
|
+
* Matching is exact-line (so `dw.json` is not treated as present just because
|
|
100
|
+
* `dw.json.bak` exists) and the append respects the file's newline convention.
|
|
101
|
+
* Never edits the tracked `.gitignore`.
|
|
102
|
+
*/
|
|
103
|
+
export async function ensureGitInfoExcluded(worktreeRoot, relativePath, deps) {
|
|
104
|
+
const { excludePath, infoDir } = await resolveInfoExcludeLocation(worktreeRoot, deps);
|
|
52
105
|
let content = "";
|
|
53
106
|
try {
|
|
54
107
|
content = await deps.readFile(excludePath);
|
|
55
108
|
}
|
|
56
109
|
catch {
|
|
57
|
-
/* exclude file (or
|
|
110
|
+
/* exclude file (or info dir) doesn't exist yet */
|
|
58
111
|
}
|
|
59
112
|
if (hasExactLine(content, relativePath))
|
|
60
113
|
return;
|