@loucompanion/forge-bridge 0.1.1-dev.8dcbc6245e51 → 0.1.1-dev.af36c7fa102b

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/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const forgeBridgeVersion = "0.1.1-dev.8dcbc6245e51";
1
+ export const forgeBridgeVersion = "0.1.1-dev.af36c7fa102b";
@@ -8,4 +8,4 @@ export declare function buildSpecBody(laneCommand: DispatchResolvedArtifact, wor
8
8
  readonly title: string;
9
9
  readonly description: string;
10
10
  readonly acceptanceCriteria: string;
11
- }): string;
11
+ }, phase?: string): string;
@@ -15,11 +15,13 @@ export async function writeArtifact(worktreePath, artifact) {
15
15
  const scope = safeArtifactSegment("scope", artifact.scope);
16
16
  const slug = safeArtifactSegment("slug", artifact.slug);
17
17
  const forgeRoot = path.resolve(worktreePath, ".forge");
18
- const targetDir = path.resolve(forgeRoot, kindFolder, scope);
19
- assertInside(forgeRoot, targetDir, "Artifact directory");
20
- await fs.mkdir(targetDir, { recursive: true });
21
- const filePath = path.resolve(targetDir, `${slug}.md`);
18
+ const scopeDir = path.resolve(forgeRoot, kindFolder, scope);
19
+ assertInside(forgeRoot, scopeDir, "Artifact directory");
20
+ const filePath = normalizedArtifactKind(artifact.kind) === "skill"
21
+ ? path.resolve(scopeDir, slug, "SKILL.md")
22
+ : path.resolve(scopeDir, `${slug}.md`);
22
23
  assertInside(forgeRoot, filePath, "Artifact file");
24
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
23
25
  await fs.writeFile(filePath, artifact.body, "utf8");
24
26
  }
25
27
  export function selectLaneCommand(artifacts, lane, phase) {
@@ -43,12 +45,13 @@ export function resolveCommandSlug(lane, phase) {
43
45
  return "dev-prompt";
44
46
  }
45
47
  }
46
- export function buildSpecBody(laneCommand, workItem) {
48
+ export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
47
49
  const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
48
50
  return `---
49
51
  work_item_id: ${workItem.id}
50
52
  status: approved
51
53
  lane: ${laneCommand.lane ?? "develop"}
54
+ phase: ${phase}
52
55
  command_slug: ${laneCommand.slug}
53
56
  ---
54
57
 
@@ -7,6 +7,8 @@ export declare class GitWorktreeService implements WorktreeService {
7
7
  prepare(request: WorktreePrepareRequest): Promise<WorktreeHandle>;
8
8
  release(handle: WorktreeHandle): Promise<void>;
9
9
  private resolveRepoPath;
10
+ private findExistingRepoPath;
11
+ private resolveMatchingRepo;
10
12
  private ensureBaseBranch;
11
13
  private gitRefExists;
12
14
  private ensureGitIdentity;
@@ -18,7 +18,7 @@ export class GitWorktreeService {
18
18
  const shortId = request.workItem.id.replace(/-/g, "").slice(0, 12);
19
19
  const kind = safeSegment(request.workItem.kind, "work-item");
20
20
  const branchName = normalizeBranchName(`${kind}/${shortId}`, "work-item/unknown");
21
- const worktreeRoot = path.join(path.dirname(repoPath), ".forge-worktrees", path.basename(repoPath));
21
+ const worktreeRoot = resolveWorktreeRoot(allowedRoot, repoPath);
22
22
  const worktreePath = path.join(worktreeRoot, kind, shortId);
23
23
  const resolvedWorktreeParent = await ensureDirectory(path.dirname(worktreePath));
24
24
  if (!isSubPath(allowedRoot, resolvedWorktreeParent)) {
@@ -78,7 +78,11 @@ export class GitWorktreeService {
78
78
  if (!repo.cloneOnDemand) {
79
79
  throw new Error("Repo has no server-local path and cloneOnDemand is disabled.");
80
80
  }
81
- const repoPath = path.join(allowedRoot, `${safeSegment(repo.name, "repo")}-${repo.id.replace(/-/g, "")}`);
81
+ const discovered = await this.findExistingRepoPath(repo, allowedRoot);
82
+ if (discovered) {
83
+ return discovered;
84
+ }
85
+ const repoPath = path.join(managedRepoRoot(allowedRoot), `${safeSegment(repo.name, "repo")}-${repo.id.replace(/-/g, "")}`);
82
86
  if (!(await gitMetadataExists(repoPath, allowedRoot))) {
83
87
  const repoPathExists = await pathExists(repoPath);
84
88
  if (repoPathExists) {
@@ -109,6 +113,46 @@ export class GitWorktreeService {
109
113
  }
110
114
  return resolved;
111
115
  }
116
+ async findExistingRepoPath(repo, allowedRoot) {
117
+ const expectedRemote = normalizeRemote(repo.remoteUrl);
118
+ const rawName = repo.name.trim();
119
+ const nameCandidates = uniqueStrings([
120
+ isSinglePathSegment(rawName) ? path.join(allowedRoot, rawName) : "",
121
+ path.join(allowedRoot, safeSegment(repo.name, "repo"))
122
+ ]);
123
+ for (const candidate of nameCandidates) {
124
+ const resolved = await this.resolveMatchingRepo(candidate, allowedRoot, expectedRemote);
125
+ if (resolved) {
126
+ return resolved;
127
+ }
128
+ }
129
+ if (expectedRemote.length > 0) {
130
+ const entries = await fs.readdir(allowedRoot, { withFileTypes: true }).catch(() => []);
131
+ for (const entry of entries) {
132
+ if (!entry.isDirectory() || entry.name === ".forge-worktrees") {
133
+ continue;
134
+ }
135
+ const resolved = await this.resolveMatchingRepo(path.join(allowedRoot, entry.name), allowedRoot, expectedRemote);
136
+ if (resolved) {
137
+ return resolved;
138
+ }
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ async resolveMatchingRepo(candidate, allowedRoot, expectedRemote) {
144
+ if (!(await gitMetadataExists(candidate, allowedRoot))) {
145
+ return null;
146
+ }
147
+ if (expectedRemote.length > 0) {
148
+ const origin = await this.git.run(candidate, ["remote", "get-url", "origin"]).catch(() => "");
149
+ if (normalizeRemote(origin) !== expectedRemote) {
150
+ return null;
151
+ }
152
+ }
153
+ const resolved = await resolveExistingDirectoryPath(candidate);
154
+ return isSubPath(allowedRoot, resolved) ? resolved : null;
155
+ }
112
156
  async ensureBaseBranch(repoPath, baseBranch) {
113
157
  if (await this.gitRefExists(repoPath, `refs/heads/${baseBranch}`)) {
114
158
  return;
@@ -132,6 +176,15 @@ export class GitWorktreeService {
132
176
  await this.git.run(repoPath, ["config", "user.name", "Forge Bridge"]);
133
177
  }
134
178
  }
179
+ function managedRepoRoot(allowedRoot) {
180
+ return path.join(allowedRoot, ".forge-worktrees", "_repos");
181
+ }
182
+ function resolveWorktreeRoot(allowedRoot, repoPath) {
183
+ const baseName = path.basename(repoPath);
184
+ return isSubPath(managedRepoRoot(allowedRoot), repoPath)
185
+ ? path.join(allowedRoot, ".forge-worktrees", baseName)
186
+ : path.join(path.dirname(repoPath), ".forge-worktrees", baseName);
187
+ }
135
188
  function isCloneableRemote(remoteUrl) {
136
189
  const trimmed = remoteUrl?.trim() ?? "";
137
190
  if (trimmed.length === 0 || trimmed.startsWith("local://")) {
@@ -194,3 +247,55 @@ async function directoryExists(inputPath) {
194
247
  const stat = await fs.stat(inputPath).catch(() => null);
195
248
  return stat?.isDirectory() ?? false;
196
249
  }
250
+ function uniqueStrings(values) {
251
+ const seen = new Set();
252
+ const result = [];
253
+ for (const value of values) {
254
+ if (!value || seen.has(value)) {
255
+ continue;
256
+ }
257
+ seen.add(value);
258
+ result.push(value);
259
+ }
260
+ return result;
261
+ }
262
+ function isSinglePathSegment(value) {
263
+ return value.length > 0 && value !== "." && value !== ".." && !value.includes("/") && !value.includes("\\");
264
+ }
265
+ function normalizeRemote(remoteUrl) {
266
+ const trimmed = remoteUrl?.trim() ?? "";
267
+ if (trimmed.length === 0) {
268
+ return "";
269
+ }
270
+ const gitHubSsh = /^git@github\.com:(.+)$/i.exec(trimmed);
271
+ if (gitHubSsh) {
272
+ return stripGitSuffix(`https://github.com/${gitHubSsh[1]}`).toLowerCase();
273
+ }
274
+ if (path.isAbsolute(trimmed)) {
275
+ return normalizeLocalPath(trimmed);
276
+ }
277
+ try {
278
+ const url = new URL(trimmed);
279
+ if (url.protocol === "file:") {
280
+ return normalizeLocalPath(normalizeFileUrlPath(url));
281
+ }
282
+ url.username = "";
283
+ url.password = "";
284
+ url.hash = "";
285
+ return stripGitSuffix(url.toString().replace(/\/$/, "")).toLowerCase();
286
+ }
287
+ catch {
288
+ return stripGitSuffix(trimmed.replace(/\\/g, "/")).toLowerCase();
289
+ }
290
+ }
291
+ function stripGitSuffix(value) {
292
+ return value.replace(/\.git$/i, "").replace(/\/$/, "");
293
+ }
294
+ function normalizeFileUrlPath(url) {
295
+ const pathname = decodeURIComponent(url.pathname);
296
+ return /^\/[A-Za-z]:[\\/]/.test(pathname) ? pathname.slice(1) : pathname;
297
+ }
298
+ function normalizeLocalPath(value) {
299
+ const normalized = path.resolve(value).replace(/\\/g, "/");
300
+ return process.platform === "win32" ? normalized.toLowerCase() : normalized;
301
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loucompanion/forge-bridge",
3
- "version": "0.1.1-dev.8dcbc6245e51",
3
+ "version": "0.1.1-dev.af36c7fa102b",
4
4
  "description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",