@loucompanion/forge-bridge 0.1.1-dev.242bb53ef13f

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 (76) hide show
  1. package/README.md +63 -0
  2. package/dist/bridge/bridge.base.d.ts +17 -0
  3. package/dist/bridge/bridge.base.js +1 -0
  4. package/dist/bridge/bridge.service.d.ts +45 -0
  5. package/dist/bridge/bridge.service.js +340 -0
  6. package/dist/bridge/bridge.types.d.ts +76 -0
  7. package/dist/bridge/bridge.types.js +1 -0
  8. package/dist/bridge/index.d.ts +2 -0
  9. package/dist/bridge/index.js +1 -0
  10. package/dist/bridge/internals.d.ts +3 -0
  11. package/dist/bridge/internals.js +1 -0
  12. package/dist/cleanup/cleanup.base.d.ts +4 -0
  13. package/dist/cleanup/cleanup.base.js +1 -0
  14. package/dist/cleanup/cleanup.service.d.ts +5 -0
  15. package/dist/cleanup/cleanup.service.js +5 -0
  16. package/dist/cleanup/cleanup.types.d.ts +6 -0
  17. package/dist/cleanup/cleanup.types.js +1 -0
  18. package/dist/cleanup/index.d.ts +2 -0
  19. package/dist/cleanup/index.js +1 -0
  20. package/dist/cleanup/internals.d.ts +3 -0
  21. package/dist/cleanup/internals.js +1 -0
  22. package/dist/cli/bin.d.ts +2 -0
  23. package/dist/cli/bin.js +4 -0
  24. package/dist/cli/cli.base.d.ts +7 -0
  25. package/dist/cli/cli.base.js +1 -0
  26. package/dist/cli/cli.service.d.ts +45 -0
  27. package/dist/cli/cli.service.js +400 -0
  28. package/dist/cli/index.d.ts +2 -0
  29. package/dist/cli/index.js +1 -0
  30. package/dist/cli/internals.d.ts +1 -0
  31. package/dist/cli/internals.js +1 -0
  32. package/dist/cli/local-config.d.ts +31 -0
  33. package/dist/cli/local-config.js +146 -0
  34. package/dist/config.d.ts +5 -0
  35. package/dist/config.js +8 -0
  36. package/dist/index.d.ts +7 -0
  37. package/dist/index.js +7 -0
  38. package/dist/session/index.d.ts +2 -0
  39. package/dist/session/index.js +1 -0
  40. package/dist/session/internals.d.ts +7 -0
  41. package/dist/session/internals.js +2 -0
  42. package/dist/session/provider-cli/index.d.ts +3 -0
  43. package/dist/session/provider-cli/index.js +1 -0
  44. package/dist/session/provider-cli/provider-cli.base.d.ts +6 -0
  45. package/dist/session/provider-cli/provider-cli.base.js +1 -0
  46. package/dist/session/provider-cli/provider-cli.service.d.ts +16 -0
  47. package/dist/session/provider-cli/provider-cli.service.js +111 -0
  48. package/dist/session/provider-cli/provider-cli.types.d.ts +12 -0
  49. package/dist/session/provider-cli/provider-cli.types.js +1 -0
  50. package/dist/session/session.base.d.ts +7 -0
  51. package/dist/session/session.base.js +1 -0
  52. package/dist/session/session.service.d.ts +10 -0
  53. package/dist/session/session.service.js +92 -0
  54. package/dist/session/session.types.d.ts +107 -0
  55. package/dist/session/session.types.js +151 -0
  56. package/dist/shared/git/git.d.ts +6 -0
  57. package/dist/shared/git/git.js +18 -0
  58. package/dist/shared/path/path.d.ts +4 -0
  59. package/dist/shared/path/path.js +29 -0
  60. package/dist/shared/process/process.d.ts +16 -0
  61. package/dist/shared/process/process.js +32 -0
  62. package/dist/shared/redaction/redaction.d.ts +2 -0
  63. package/dist/shared/redaction/redaction.js +30 -0
  64. package/dist/version.d.ts +1 -0
  65. package/dist/version.js +1 -0
  66. package/dist/worktree/index.d.ts +2 -0
  67. package/dist/worktree/index.js +1 -0
  68. package/dist/worktree/internals.d.ts +3 -0
  69. package/dist/worktree/internals.js +1 -0
  70. package/dist/worktree/worktree.base.d.ts +5 -0
  71. package/dist/worktree/worktree.base.js +1 -0
  72. package/dist/worktree/worktree.service.d.ts +12 -0
  73. package/dist/worktree/worktree.service.js +139 -0
  74. package/dist/worktree/worktree.types.d.ts +15 -0
  75. package/dist/worktree/worktree.types.js +1 -0
  76. package/package.json +52 -0
@@ -0,0 +1,2 @@
1
+ export declare function redactSecrets<T>(value: T, exactSecrets?: readonly string[]): T;
2
+ export declare function redactText(value: string, exactSecrets?: readonly string[]): string;
@@ -0,0 +1,30 @@
1
+ const secretKeyPattern = /(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)/i;
2
+ export function redactSecrets(value, exactSecrets = []) {
3
+ return redactValue(value, exactSecrets);
4
+ }
5
+ export function redactText(value, exactSecrets = []) {
6
+ let redacted = value;
7
+ for (const secret of exactSecrets) {
8
+ if (secret.length > 0) {
9
+ redacted = redacted.split(secret).join("[REDACTED]");
10
+ }
11
+ }
12
+ return redacted
13
+ .replace(/(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)(["':=\s]+)([^"',\s]+)/gi, "$1$2[REDACTED]")
14
+ .replace(/(sk-[a-zA-Z0-9_-]{12,})/g, "[REDACTED]");
15
+ }
16
+ function redactValue(value, exactSecrets) {
17
+ if (typeof value === "string") {
18
+ return redactText(value, exactSecrets);
19
+ }
20
+ if (Array.isArray(value)) {
21
+ return value.map((item) => redactValue(item, exactSecrets));
22
+ }
23
+ if (!value || typeof value !== "object") {
24
+ return value;
25
+ }
26
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [
27
+ key,
28
+ secretKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
29
+ ]));
30
+ }
@@ -0,0 +1 @@
1
+ export declare const forgeBridgeVersion = "0.1.0";
@@ -0,0 +1 @@
1
+ export const forgeBridgeVersion = "0.1.0";
@@ -0,0 +1,2 @@
1
+ export { GitWorktreeService } from "./worktree.service.js";
2
+ export type { WorktreeService } from "./worktree.base.js";
@@ -0,0 +1 @@
1
+ export { GitWorktreeService } from "./worktree.service.js";
@@ -0,0 +1,3 @@
1
+ export { normalizeBranchName } from "./worktree.service.js";
2
+ export type { WorktreeService } from "./worktree.base.js";
3
+ export type { WorktreeHandle, WorktreePrepareRequest } from "./worktree.types.js";
@@ -0,0 +1 @@
1
+ export { normalizeBranchName } from "./worktree.service.js";
@@ -0,0 +1,5 @@
1
+ import type { WorktreeHandle, WorktreePrepareRequest } from "./worktree.types.js";
2
+ export interface WorktreeService {
3
+ prepare(request: WorktreePrepareRequest): Promise<WorktreeHandle>;
4
+ release(handle: WorktreeHandle): Promise<void>;
5
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import { GitClient } from "../shared/git/git.js";
2
+ import type { WorktreeService } from "./worktree.base.js";
3
+ import type { WorktreeHandle, WorktreePrepareRequest } from "./worktree.types.js";
4
+ export declare class GitWorktreeService implements WorktreeService {
5
+ private readonly git;
6
+ constructor(git?: GitClient);
7
+ prepare(request: WorktreePrepareRequest): Promise<WorktreeHandle>;
8
+ release(handle: WorktreeHandle): Promise<void>;
9
+ private resolveRepoPath;
10
+ private ensureGitIdentity;
11
+ }
12
+ export declare function normalizeBranchName(value: string | null | undefined, fallback: string): string;
@@ -0,0 +1,139 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { GitClient } from "../shared/git/git.js";
4
+ import { ensureDirectory, isSubPath, resolveExistingDirectoryPath, safeSegment } from "../shared/path/path.js";
5
+ export class GitWorktreeService {
6
+ git;
7
+ constructor(git = new GitClient()) {
8
+ this.git = git;
9
+ }
10
+ async prepare(request) {
11
+ const allowedRoot = await ensureDirectory(request.allowedRepoRoot);
12
+ const repoPath = await this.resolveRepoPath(request.repo, allowedRoot);
13
+ const baseBranch = normalizeBranchName(request.baseBranch, "develop");
14
+ await this.ensureGitIdentity(repoPath);
15
+ const shortId = request.workItem.id.replace(/-/g, "").slice(0, 12);
16
+ const kind = safeSegment(request.workItem.kind, "work-item");
17
+ const branchName = normalizeBranchName(`${kind}/${shortId}`, "work-item/unknown");
18
+ const worktreeRoot = path.join(path.dirname(repoPath), ".forge-worktrees", path.basename(repoPath));
19
+ const worktreePath = path.join(worktreeRoot, kind, shortId);
20
+ const resolvedWorktreeParent = await ensureDirectory(path.dirname(worktreePath));
21
+ if (!isSubPath(allowedRoot, resolvedWorktreeParent)) {
22
+ throw new Error(`Worktree path must stay under ${allowedRoot}.`);
23
+ }
24
+ const exists = await directoryExists(worktreePath);
25
+ let resolvedWorktreePath;
26
+ if (!exists) {
27
+ await this.git.run(repoPath, ["worktree", "add", "-B", branchName, worktreePath, baseBranch]);
28
+ resolvedWorktreePath = await resolveExistingDirectoryPath(worktreePath);
29
+ }
30
+ else {
31
+ resolvedWorktreePath = await resolveExistingDirectoryPath(worktreePath);
32
+ if (!isSubPath(allowedRoot, resolvedWorktreePath)) {
33
+ throw new Error(`Worktree path must stay under ${allowedRoot}.`);
34
+ }
35
+ }
36
+ await this.ensureGitIdentity(resolvedWorktreePath);
37
+ return {
38
+ repoPath,
39
+ worktreePath: resolvedWorktreePath,
40
+ branchName,
41
+ baseBranch,
42
+ cleanupWorktrees: request.cleanupWorktrees
43
+ };
44
+ }
45
+ async release(handle) {
46
+ if (!handle.cleanupWorktrees || !(await directoryExists(handle.worktreePath))) {
47
+ return;
48
+ }
49
+ try {
50
+ await this.git.run(handle.repoPath, ["worktree", "remove", "--force", handle.worktreePath]);
51
+ await this.git.run(handle.repoPath, ["worktree", "prune"]);
52
+ }
53
+ catch {
54
+ await fs.rm(handle.worktreePath, { recursive: true, force: true });
55
+ }
56
+ }
57
+ async resolveRepoPath(repo, allowedRoot) {
58
+ if (repo.serverLocalPath && repo.serverLocalPath.trim().length > 0) {
59
+ const requestedPath = await resolveExistingDirectoryPath(repo.serverLocalPath);
60
+ if (!isSubPath(allowedRoot, requestedPath) || !(await gitMetadataExists(requestedPath, allowedRoot))) {
61
+ throw new Error(`Repo server-local path must be an existing Git repository under ${allowedRoot}.`);
62
+ }
63
+ return requestedPath;
64
+ }
65
+ if (!repo.cloneOnDemand) {
66
+ throw new Error("Repo has no server-local path and cloneOnDemand is disabled.");
67
+ }
68
+ const repoPath = path.join(allowedRoot, `${safeSegment(repo.name, "repo")}-${repo.id.replace(/-/g, "")}`);
69
+ if (!(await gitMetadataExists(repoPath, allowedRoot))) {
70
+ await fs.mkdir(repoPath, { recursive: true });
71
+ await this.git.run(repoPath, ["init", "-b", normalizeBranchName(repo.defaultBranch, "develop")]);
72
+ await this.ensureGitIdentity(repoPath);
73
+ await fs.writeFile(path.join(repoPath, "README.md"), `# ${repo.name}\n`, "utf8");
74
+ await this.git.run(repoPath, ["add", "README.md"]);
75
+ await this.git.run(repoPath, ["commit", "-m", "seed forge bridge repo"]);
76
+ }
77
+ const resolved = await resolveExistingDirectoryPath(repoPath);
78
+ if (!isSubPath(allowedRoot, resolved)) {
79
+ throw new Error(`Repo path must stay under ${allowedRoot}.`);
80
+ }
81
+ return resolved;
82
+ }
83
+ async ensureGitIdentity(repoPath) {
84
+ await this.git.run(repoPath, ["config", "user.email", "forge-bridge@forge.local"]);
85
+ await this.git.run(repoPath, ["config", "user.name", "Forge Bridge"]);
86
+ }
87
+ }
88
+ export function normalizeBranchName(value, fallback) {
89
+ const branch = (value?.trim() || fallback).trim();
90
+ if (branch.length === 0 ||
91
+ branch.startsWith("-") ||
92
+ branch === "@" ||
93
+ branch.includes("..") ||
94
+ branch.includes("//") ||
95
+ branch.includes("@{") ||
96
+ branch.endsWith("/") ||
97
+ branch.endsWith(".") ||
98
+ branch.toLowerCase().endsWith(".lock") ||
99
+ /\s/.test(branch) ||
100
+ /[\u0000-\u001f~^:?*[\\]/.test(branch) ||
101
+ branch.split("/").some((part) => part.length === 0 || part.startsWith(".") || part.toLowerCase().endsWith(".lock"))) {
102
+ throw new Error("Git branch name must be valid, without whitespace, and cannot start with '-'.");
103
+ }
104
+ return branch;
105
+ }
106
+ async function gitMetadataExists(repoPath, allowedRoot) {
107
+ const gitPath = path.join(repoPath, ".git");
108
+ const stat = await fs.lstat(gitPath).catch(() => null);
109
+ if (!stat) {
110
+ return false;
111
+ }
112
+ if (stat.isSymbolicLink()) {
113
+ const resolvedGitPath = await resolveExistingDirectoryPath(gitPath);
114
+ return isSubPath(allowedRoot, resolvedGitPath);
115
+ }
116
+ if (stat.isDirectory()) {
117
+ const resolvedGitPath = await resolveExistingDirectoryPath(gitPath);
118
+ return isSubPath(allowedRoot, resolvedGitPath);
119
+ }
120
+ if (!stat.isFile()) {
121
+ return false;
122
+ }
123
+ const content = (await fs.readFile(gitPath, "utf8")).trim();
124
+ const prefix = "gitdir:";
125
+ if (!content.toLowerCase().startsWith(prefix)) {
126
+ return false;
127
+ }
128
+ const gitDir = content.slice(prefix.length).trim();
129
+ if (gitDir.length === 0) {
130
+ return false;
131
+ }
132
+ const gitDirPath = path.isAbsolute(gitDir) ? gitDir : path.join(repoPath, gitDir);
133
+ const resolvedGitDir = await resolveExistingDirectoryPath(gitDirPath);
134
+ return isSubPath(allowedRoot, resolvedGitDir);
135
+ }
136
+ async function directoryExists(inputPath) {
137
+ const stat = await fs.stat(inputPath).catch(() => null);
138
+ return stat?.isDirectory() ?? false;
139
+ }
@@ -0,0 +1,15 @@
1
+ import type { DispatchRepo, DispatchWorkItem } from "../session/session.types.js";
2
+ export interface WorktreePrepareRequest {
3
+ readonly repo: DispatchRepo;
4
+ readonly workItem: DispatchWorkItem;
5
+ readonly baseBranch: string;
6
+ readonly allowedRepoRoot: string;
7
+ readonly cleanupWorktrees: boolean;
8
+ }
9
+ export interface WorktreeHandle {
10
+ readonly repoPath: string;
11
+ readonly worktreePath: string;
12
+ readonly branchName: string;
13
+ readonly baseBranch: string;
14
+ readonly cleanupWorktrees: boolean;
15
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@loucompanion/forge-bridge",
3
+ "version": "0.1.1-dev.242bb53ef13f",
4
+ "description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "bin": {
8
+ "forge-bridge": "./dist/cli/bin.js"
9
+ },
10
+ "main": "./dist/index.js",
11
+ "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "import": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+ssh://git@github.com/coleman399/Forge.git",
26
+ "directory": "tools/forge-bridge"
27
+ },
28
+ "homepage": "https://github.com/coleman399/Forge/tree/develop/tools/forge-bridge#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/coleman399/Forge/issues"
31
+ },
32
+ "engines": {
33
+ "node": ">=22.0.0"
34
+ },
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json",
40
+ "test": "vitest run",
41
+ "test:watch": "vitest",
42
+ "smoke:package": "npm run build && node scripts/smoke-package.mjs"
43
+ },
44
+ "dependencies": {
45
+ "@microsoft/signalr": "^8.0.17"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^22.15.32",
49
+ "typescript": "^5.8.3",
50
+ "vitest": "^3.2.3"
51
+ }
52
+ }