@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.
@@ -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 { ok: true, worktreePath: row.path, branch };
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 { ok: true, worktreePath: row.path, branch };
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 `<worktreeRoot>/.git/info/exclude`,
8
- * used for Tier-3 file credentials that must never enter version control and
9
- * must never touch the tracked `.gitignore`.
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 in an error.
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
- /** Append `entry` to `content`, inserting a separating newline only if needed. */
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 separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
22
- return content + separator + entry + "\n";
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
- * Idempotently ensure `relativePath` is listed in the worktree's local-only
45
- * `<worktreeRoot>/.git/info/exclude`. Creates `<worktreeRoot>/.git/info` if it
46
- * does not exist. Matching is exact-line (so `dw.json` is not treated as present
47
- * just because `dw.json.bak` exists). Never edits the tracked `.gitignore`.
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
- export async function ensureGitInfoExcluded(worktreeRoot, relativePath, deps) {
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
- const excludePath = path.join(infoDir, "exclude");
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 .git/info) doesn't exist yet */
110
+ /* exclude file (or info dir) doesn't exist yet */
58
111
  }
59
112
  if (hasExactLine(content, relativePath))
60
113
  return;