@loucompanion/forge-bridge 0.1.3-dev.b6be5e7f9668 → 0.1.3-dev.ba7d36726eda
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 +78 -0
- package/dist/bridge/bridge.base.d.ts +5 -1
- package/dist/bridge/bridge.service.d.ts +52 -3
- package/dist/bridge/bridge.service.js +796 -22
- package/dist/bridge/bridge.types.d.ts +135 -1
- package/dist/cleanup/cleanup.service.d.ts +13 -2
- package/dist/cleanup/cleanup.service.js +435 -3
- package/dist/cleanup/cleanup.types.d.ts +10 -0
- package/dist/cleanup/index.d.ts +1 -1
- package/dist/cleanup/index.js +1 -1
- package/dist/cleanup/internals.d.ts +2 -2
- package/dist/cleanup/internals.js +1 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +217 -0
- package/dist/cli/cli.service.d.ts +42 -2
- package/dist/cli/cli.service.js +356 -205
- package/dist/cli/local-config.d.ts +6 -0
- package/dist/cli/local-config.js +45 -2
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +15 -0
- package/dist/runtime/attempt-node-runtime-launcher.js +50 -0
- package/dist/runtime/attempt-process-supervisor-registry.d.ts +20 -0
- package/dist/runtime/attempt-process-supervisor-registry.js +64 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +74 -0
- package/dist/runtime/attempt-process-supervisor.js +290 -0
- package/dist/runtime/host-listener-inspector.d.ts +28 -0
- package/dist/runtime/host-listener-inspector.js +185 -0
- package/dist/runtime/host-port-broker.d.ts +76 -0
- package/dist/runtime/host-port-broker.js +333 -0
- package/dist/runtime/host-runtime-contract.d.ts +39 -0
- package/dist/runtime/host-runtime-contract.js +295 -0
- package/dist/runtime/host-runtime-lease.d.ts +122 -0
- package/dist/runtime/host-runtime-lease.js +1241 -0
- package/dist/runtime/host-runtime-promotion.d.ts +21 -0
- package/dist/runtime/host-runtime-promotion.js +128 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +44 -0
- package/dist/runtime/node-host-runtime-adapter.js +205 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +20 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +136 -0
- package/dist/runtime/runtime-isolation-lease.d.ts +67 -0
- package/dist/runtime/runtime-isolation-lease.js +158 -0
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +35 -0
- package/dist/sandbox/image-policy.js +247 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +45 -0
- package/dist/sandbox/preflight.js +300 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +209 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +119 -0
- package/dist/sandbox/runtime.d.ts +297 -0
- package/dist/sandbox/runtime.js +2338 -0
- package/dist/sandbox/sandbox.types.d.ts +86 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/smoke.d.ts +76 -0
- package/dist/sandbox/smoke.js +380 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +225 -0
- package/dist/sandbox/workspace.service.d.ts +13 -0
- package/dist/sandbox/workspace.service.js +67 -0
- package/dist/session/phase-completion.artifacts.d.ts +10 -0
- package/dist/session/phase-completion.artifacts.js +620 -0
- package/dist/session/phase-completion.d.ts +32 -44
- package/dist/session/phase-completion.helpers.d.ts +112 -0
- package/dist/session/phase-completion.helpers.js +665 -0
- package/dist/session/phase-completion.js +28 -3485
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +220 -0
- package/dist/session/phase-completion.runtime.d.ts +36 -0
- package/dist/session/phase-completion.runtime.js +611 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +707 -0
- package/dist/session/phase-completion.validation.d.ts +34 -0
- package/dist/session/phase-completion.validation.js +724 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +778 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +329 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +34 -0
- package/dist/session/provider-cli/provider-cli.process.js +316 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +47 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +573 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +65 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +41 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +9 -53
- package/dist/session/provider-cli/provider-cli.service.js +182 -2536
- package/dist/session/provider-cli/provider-cli.types.d.ts +3 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +20 -0
- package/dist/session/provider-cli/provider-output-state.js +445 -0
- package/dist/session/session-redaction.d.ts +3 -0
- package/dist/session/session-redaction.js +23 -0
- package/dist/session/session.base.d.ts +4 -0
- package/dist/session/session.service.d.ts +6 -1
- package/dist/session/session.service.js +202 -53
- package/dist/session/session.types.d.ts +123 -1
- package/dist/session/session.types.js +301 -3
- package/dist/shared/path/path.js +3 -1
- package/dist/shared/process/host-process-recovery.d.ts +12 -0
- package/dist/shared/process/host-process-recovery.js +78 -0
- package/dist/shared/process/host-process-supervisor.d.ts +40 -0
- package/dist/shared/process/host-process-supervisor.js +195 -0
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +10 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +39 -6
- package/dist/worktree/dependency-warmer.d.ts +11 -0
- package/dist/worktree/dependency-warmer.js +45 -1
- package/dist/worktree/index.d.ts +1 -0
- package/dist/worktree/index.js +1 -0
- package/dist/worktree/prior-phase-artifact-hydrator.d.ts +2 -0
- package/dist/worktree/prior-phase-artifact-hydrator.js +136 -0
- package/dist/worktree/worktree-paths.d.ts +4 -0
- package/dist/worktree/worktree-paths.js +30 -0
- package/dist/worktree/worktree.base.d.ts +3 -0
- package/dist/worktree/worktree.service.d.ts +8 -3
- package/dist/worktree/worktree.service.js +89 -52
- package/dist/worktree/worktree.types.d.ts +3 -1
- package/package.json +6 -3
- package/runtime/windows-job-supervisor.ps1 +287 -0
- package/runtime/windows-listener-owner.ps1 +33 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const windowsJobSupervisorPath = fileURLToPath(new URL("../../../runtime/windows-job-supervisor.ps1", import.meta.url));
|
|
7
|
+
export function resolveHostProcessSupervisedLaunch(request, resolvedCommand, resolvedArgs, platform = process.platform) {
|
|
8
|
+
const env = request.env;
|
|
9
|
+
if (env?.FORGE_RUNTIME_MODE !== "host")
|
|
10
|
+
return null;
|
|
11
|
+
const supervisorId = requiredEnvironment(env, "FORGE_PROCESS_SUPERVISOR_ID");
|
|
12
|
+
const metadataPath = requiredEnvironment(env, "FORGE_PROCESS_SUPERVISOR_METADATA_PATH");
|
|
13
|
+
const logDirectory = requiredEnvironment(env, "FORGE_LOG_DIR");
|
|
14
|
+
assertMetadataPath(logDirectory, metadataPath);
|
|
15
|
+
const commandDigest = createHash("sha256")
|
|
16
|
+
.update(JSON.stringify([resolvedCommand, ...resolvedArgs]))
|
|
17
|
+
.digest("hex");
|
|
18
|
+
if (platform === "win32") {
|
|
19
|
+
if (!fs.existsSync(windowsJobSupervisorPath)) {
|
|
20
|
+
throw new Error(`host_process_supervisor_missing: ${windowsJobSupervisorPath}`);
|
|
21
|
+
}
|
|
22
|
+
const payload = Buffer.from(JSON.stringify({
|
|
23
|
+
command: resolvedCommand,
|
|
24
|
+
args: resolvedArgs,
|
|
25
|
+
cwd: request.cwd,
|
|
26
|
+
parentPid: process.pid,
|
|
27
|
+
supervisorId,
|
|
28
|
+
}), "utf8").toString("base64");
|
|
29
|
+
return {
|
|
30
|
+
command: windowsPowerShellPath(),
|
|
31
|
+
args: [
|
|
32
|
+
"-NoLogo",
|
|
33
|
+
"-NoProfile",
|
|
34
|
+
"-NonInteractive",
|
|
35
|
+
"-ExecutionPolicy",
|
|
36
|
+
"Bypass",
|
|
37
|
+
"-File",
|
|
38
|
+
windowsJobSupervisorPath,
|
|
39
|
+
"-Mode",
|
|
40
|
+
"Outer",
|
|
41
|
+
"-Payload",
|
|
42
|
+
payload,
|
|
43
|
+
],
|
|
44
|
+
detached: false,
|
|
45
|
+
supervisorId,
|
|
46
|
+
strategy: "windows_job_object",
|
|
47
|
+
metadataPath,
|
|
48
|
+
commandDigest,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
command: resolvedCommand,
|
|
53
|
+
args: resolvedArgs,
|
|
54
|
+
detached: true,
|
|
55
|
+
supervisorId,
|
|
56
|
+
strategy: "unix_process_group",
|
|
57
|
+
metadataPath,
|
|
58
|
+
commandDigest,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function persistHostProcessSupervisorMetadata(launch, request, rootPid, rootCreatedAt) {
|
|
62
|
+
if (!Number.isSafeInteger(rootPid) || rootPid <= 0) {
|
|
63
|
+
throw new Error("host_process_supervisor_invalid: supervised root PID was not assigned.");
|
|
64
|
+
}
|
|
65
|
+
const env = request.env ?? {};
|
|
66
|
+
const metadata = {
|
|
67
|
+
schemaVersion: 1,
|
|
68
|
+
supervisorId: launch.supervisorId,
|
|
69
|
+
strategy: launch.strategy,
|
|
70
|
+
rootPid,
|
|
71
|
+
rootCreatedAt,
|
|
72
|
+
rootIdentity: captureHostProcessIdentity(rootPid),
|
|
73
|
+
commandDigest: launch.commandDigest,
|
|
74
|
+
worktreePath: request.cwd,
|
|
75
|
+
runtimeLeaseId: requiredEnvironment(env, "FORGE_RESOURCE_LEASE_ID"),
|
|
76
|
+
dispatchAttemptId: requiredEnvironment(env, "FORGE_DISPATCH_ATTEMPT_ID"),
|
|
77
|
+
fencingToken: requiredEnvironment(env, "FORGE_FENCING_TOKEN"),
|
|
78
|
+
bridgePid: process.pid,
|
|
79
|
+
};
|
|
80
|
+
fs.writeFileSync(launch.metadataPath, `${JSON.stringify(metadata)}\n`, {
|
|
81
|
+
encoding: "utf8",
|
|
82
|
+
flag: "wx",
|
|
83
|
+
mode: 0o600,
|
|
84
|
+
});
|
|
85
|
+
return metadata;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Captures an OS-issued process birth marker. Recovery compares this marker
|
|
89
|
+
* before signalling a persisted PID so PID reuse can never redirect cleanup
|
|
90
|
+
* at an unrelated process.
|
|
91
|
+
*/
|
|
92
|
+
export function captureHostProcessIdentity(pid, platform = process.platform) {
|
|
93
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
94
|
+
return null;
|
|
95
|
+
try {
|
|
96
|
+
if (platform === "linux") {
|
|
97
|
+
const bootId = fs.readFileSync("/proc/sys/kernel/random/boot_id", "utf8").trim();
|
|
98
|
+
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
|
99
|
+
const close = stat.lastIndexOf(")");
|
|
100
|
+
const fields = close >= 0 ? stat.slice(close + 2).trim().split(/\s+/u) : [];
|
|
101
|
+
const startTicks = fields[19];
|
|
102
|
+
return bootId && startTicks && /^\d+$/u.test(startTicks)
|
|
103
|
+
? `linux:${bootId}:${startTicks}`
|
|
104
|
+
: null;
|
|
105
|
+
}
|
|
106
|
+
if (platform === "win32") {
|
|
107
|
+
const output = execFileSync(windowsPowerShellPath(), [
|
|
108
|
+
"-NoLogo",
|
|
109
|
+
"-NoProfile",
|
|
110
|
+
"-NonInteractive",
|
|
111
|
+
"-Command",
|
|
112
|
+
`(Get-Process -Id ${pid} -ErrorAction Stop).StartTime.ToUniversalTime().Ticks`,
|
|
113
|
+
], {
|
|
114
|
+
encoding: "utf8",
|
|
115
|
+
windowsHide: true,
|
|
116
|
+
timeout: 2_000,
|
|
117
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
118
|
+
}).trim();
|
|
119
|
+
return /^\d+$/u.test(output) ? `win32:${output}` : null;
|
|
120
|
+
}
|
|
121
|
+
// Other Unix variants do not expose a sufficiently strong birth marker
|
|
122
|
+
// through Node. Fail closed rather than trust a second-resolution ps time.
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export async function terminateHostProcessTree(child, launch, graceMs = 2_000) {
|
|
130
|
+
if (!launch) {
|
|
131
|
+
try {
|
|
132
|
+
child.kill("SIGTERM");
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// The process already settled.
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (launch.strategy === "windows_job_object") {
|
|
140
|
+
try {
|
|
141
|
+
child.kill("SIGTERM");
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Closing the launcher is best effort when the Job Object already settled.
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const processGroupId = child.pid;
|
|
149
|
+
if (!processGroupId || !signalUnixProcessGroup(processGroupId, "SIGTERM"))
|
|
150
|
+
return;
|
|
151
|
+
await new Promise(resolve => setTimeout(resolve, graceMs));
|
|
152
|
+
if (unixProcessGroupExists(processGroupId)) {
|
|
153
|
+
signalUnixProcessGroup(processGroupId, "SIGKILL");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function requiredEnvironment(env, name) {
|
|
157
|
+
const value = env[name]?.trim();
|
|
158
|
+
if (!value)
|
|
159
|
+
throw new Error(`host_process_supervisor_invalid: ${name} is required.`);
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
function assertMetadataPath(logDirectory, metadataPath) {
|
|
163
|
+
const root = path.resolve(logDirectory);
|
|
164
|
+
const target = path.resolve(metadataPath);
|
|
165
|
+
if (target !== root && !target.startsWith(`${root}${path.sep}`)) {
|
|
166
|
+
throw new Error("host_process_supervisor_invalid: metadata path must be inside FORGE_LOG_DIR.");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function windowsPowerShellPath() {
|
|
170
|
+
const systemRoot = process.env.SystemRoot?.trim() || "C:\\Windows";
|
|
171
|
+
const candidate = path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
172
|
+
return fs.existsSync(candidate) ? candidate : "powershell.exe";
|
|
173
|
+
}
|
|
174
|
+
function signalUnixProcessGroup(processGroupId, signal) {
|
|
175
|
+
try {
|
|
176
|
+
process.kill(-processGroupId, signal);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
if (error.code === "ESRCH")
|
|
181
|
+
return false;
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function unixProcessGroupExists(processGroupId) {
|
|
186
|
+
try {
|
|
187
|
+
process.kill(-processGroupId, 0);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error.code === "ESRCH")
|
|
192
|
+
return false;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface LaunchCommand {
|
|
2
|
+
readonly command: string;
|
|
3
|
+
readonly args: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function buildWindowsCmdLaunch(command: string, args: readonly string[], comSpec?: string | undefined): LaunchCommand;
|
|
6
|
+
export declare function buildWindowsLaunchCommand(commandPath: string, args: readonly string[]): LaunchCommand;
|
|
7
|
+
export declare function windowsCommandLine(command: string, args: readonly string[]): string;
|
|
8
|
+
export declare function quoteWindowsCommandLineArg(value: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
export function buildWindowsCmdLaunch(command, args, comSpec = process.env.ComSpec) {
|
|
3
|
+
const resolvedComSpec = comSpec?.trim();
|
|
4
|
+
const commandLine = windowsCommandLine(command, args);
|
|
5
|
+
const cmdPayload = commandLine.startsWith('"') ? `"${commandLine}"` : commandLine;
|
|
6
|
+
return {
|
|
7
|
+
command: resolvedComSpec || "cmd.exe",
|
|
8
|
+
args: ["/d", "/s", "/v:off", "/c", cmdPayload]
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function buildWindowsLaunchCommand(commandPath, args) {
|
|
12
|
+
const extension = path.extname(commandPath).toLowerCase();
|
|
13
|
+
if (extension === ".ps1") {
|
|
14
|
+
return {
|
|
15
|
+
command: powershellExecutable(),
|
|
16
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", commandPath, ...args]
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (extension === ".cmd" || extension === ".bat") {
|
|
20
|
+
return buildWindowsCmdLaunch(commandPath, args);
|
|
21
|
+
}
|
|
22
|
+
return { command: commandPath, args };
|
|
23
|
+
}
|
|
24
|
+
export function windowsCommandLine(command, args) {
|
|
25
|
+
return [command, ...args].map(quoteWindowsCommandLineArg).join(" ");
|
|
26
|
+
}
|
|
27
|
+
export function quoteWindowsCommandLineArg(value) {
|
|
28
|
+
const escaped = value
|
|
29
|
+
.replace(/\^/g, "^^")
|
|
30
|
+
.replace(/%/g, "^%");
|
|
31
|
+
if (escaped.length > 0 && !/[\s"&|<>()^%!]/.test(escaped)) {
|
|
32
|
+
return escaped;
|
|
33
|
+
}
|
|
34
|
+
return `"${escaped.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1")}"`;
|
|
35
|
+
}
|
|
36
|
+
function powershellExecutable() {
|
|
37
|
+
const systemRoot = process.env.SystemRoot;
|
|
38
|
+
return systemRoot
|
|
39
|
+
? path.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe")
|
|
40
|
+
: "powershell.exe";
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function supportsDraftImageAttachmentsFor(cliProviders: readonly string[]): boolean;
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
const secretKeyPattern = /(api[_-]?key|authorization|bearer|client[_-]?secret|password|secret|token)/i;
|
|
2
|
+
const attachmentContentKeyPattern = /^(textContent|contentBase64)$/i;
|
|
3
|
+
const serializedAttachmentContentPattern = /("(?:textContent|contentBase64)"\s*:\s*")(?:(?:\\.)|[^"\\])*(")/gi;
|
|
4
|
+
const authorizationCredentialPattern = /(\bauthorization\s*[:=]\s*(?:bearer|basic)?\s*)([^"',\s]+)/gi;
|
|
5
|
+
const bearerCredentialPattern = /(\bbearer\s+)([^"',\s]+)/gi;
|
|
6
|
+
const secretAssignmentPattern = /(api[_-]?key|client[_-]?secret|password|secret|token)(\s*["']?\s*[:=]\s*["']?\s*)([^"',\s]+)/gi;
|
|
2
7
|
export function redactSecrets(value, exactSecrets = []) {
|
|
3
8
|
return redactValue(value, exactSecrets);
|
|
4
9
|
}
|
|
5
10
|
export function redactText(value, exactSecrets = []) {
|
|
6
|
-
let redacted = value;
|
|
11
|
+
let redacted = value.replace(serializedAttachmentContentPattern, "$1[REDACTED]$2");
|
|
7
12
|
for (const secret of exactSecrets) {
|
|
8
13
|
if (secret.length > 0) {
|
|
9
14
|
redacted = redacted.split(secret).join("[REDACTED]");
|
|
10
15
|
}
|
|
11
16
|
}
|
|
12
17
|
return redacted
|
|
13
|
-
.replace(
|
|
18
|
+
.replace(authorizationCredentialPattern, "$1[REDACTED]")
|
|
19
|
+
.replace(bearerCredentialPattern, "$1[REDACTED]")
|
|
20
|
+
.replace(secretAssignmentPattern, "$1$2[REDACTED]")
|
|
14
21
|
.replace(/(sk-[a-zA-Z0-9_-]{12,})/g, "[REDACTED]");
|
|
15
22
|
}
|
|
16
23
|
function redactValue(value, exactSecrets) {
|
|
@@ -25,6 +32,6 @@ function redactValue(value, exactSecrets) {
|
|
|
25
32
|
}
|
|
26
33
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
27
34
|
key,
|
|
28
|
-
secretKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
|
|
35
|
+
secretKeyPattern.test(key) || attachmentContentKeyPattern.test(key) ? "[REDACTED]" : redactValue(item, exactSecrets)
|
|
29
36
|
]));
|
|
30
37
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export declare const forgeBridgeVersion = "0.1.3-dev.ba7d36726eda";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.3-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.3-dev.ba7d36726eda";
|
|
@@ -36,19 +36,29 @@ const DRAFT_OPERATIONAL_SKILLS = new Set([
|
|
|
36
36
|
"draft-repo-context",
|
|
37
37
|
"draft-graph-context",
|
|
38
38
|
"draft-product-scope",
|
|
39
|
-
"draft-work-order-plan",
|
|
40
39
|
"draft-critic",
|
|
41
40
|
"draft-proposal"
|
|
42
41
|
]);
|
|
42
|
+
const DRAFT_WORK_ITEM_OPERATIONAL_SKILLS = new Set([
|
|
43
|
+
"draft-work-item-plan"
|
|
44
|
+
]);
|
|
45
|
+
const DRAFT_WORK_ORDER_OPERATIONAL_SKILLS = new Set([
|
|
46
|
+
"draft-work-order-plan"
|
|
47
|
+
]);
|
|
43
48
|
const DRAFT_OPERATIONAL_HOOKS = new Set([
|
|
44
49
|
"draft-intent",
|
|
45
50
|
"draft-repo-context",
|
|
46
51
|
"draft-graph-context",
|
|
47
52
|
"draft-product-scope",
|
|
48
|
-
"draft-work-order-plan",
|
|
49
53
|
"draft-critic",
|
|
50
54
|
"draft-proposal"
|
|
51
55
|
]);
|
|
56
|
+
const DRAFT_WORK_ITEM_OPERATIONAL_HOOKS = new Set([
|
|
57
|
+
"draft-work-item-plan"
|
|
58
|
+
]);
|
|
59
|
+
const DRAFT_WORK_ORDER_OPERATIONAL_HOOKS = new Set([
|
|
60
|
+
"draft-work-order-plan"
|
|
61
|
+
]);
|
|
52
62
|
export async function injectWorktreeArtifacts(worktreePath, artifacts) {
|
|
53
63
|
for (const artifact of artifacts) {
|
|
54
64
|
await writeArtifact(worktreePath, artifact);
|
|
@@ -64,7 +74,7 @@ export function filterArtifactsForLane(artifacts, lane, phase) {
|
|
|
64
74
|
return artifacts.filter((artifact) => shouldInjectArchitectArtifact(artifact, commandSlug));
|
|
65
75
|
}
|
|
66
76
|
if (normalizedLane === "draft") {
|
|
67
|
-
return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug));
|
|
77
|
+
return artifacts.filter((artifact) => shouldInjectDraftArtifact(artifact, commandSlug, phase));
|
|
68
78
|
}
|
|
69
79
|
return artifacts;
|
|
70
80
|
}
|
|
@@ -142,7 +152,7 @@ function shouldInjectArchitectArtifact(artifact, commandSlug) {
|
|
|
142
152
|
return false;
|
|
143
153
|
}
|
|
144
154
|
}
|
|
145
|
-
function shouldInjectDraftArtifact(artifact, commandSlug) {
|
|
155
|
+
function shouldInjectDraftArtifact(artifact, commandSlug, phase) {
|
|
146
156
|
if (artifact.scope.trim().toLowerCase() !== "operational") {
|
|
147
157
|
return false;
|
|
148
158
|
}
|
|
@@ -151,13 +161,36 @@ function shouldInjectDraftArtifact(artifact, commandSlug) {
|
|
|
151
161
|
case "command":
|
|
152
162
|
return slug === commandSlug;
|
|
153
163
|
case "skill":
|
|
154
|
-
return DRAFT_OPERATIONAL_SKILLS.has(slug);
|
|
164
|
+
return DRAFT_OPERATIONAL_SKILLS.has(slug) || draftPhaseSkills(phase).has(slug);
|
|
155
165
|
case "hook":
|
|
156
|
-
return DRAFT_OPERATIONAL_HOOKS.has(slug);
|
|
166
|
+
return DRAFT_OPERATIONAL_HOOKS.has(slug) || draftPhaseHooks(phase).has(slug);
|
|
157
167
|
default:
|
|
158
168
|
return false;
|
|
159
169
|
}
|
|
160
170
|
}
|
|
171
|
+
function draftPhaseSkills(phase) {
|
|
172
|
+
switch (normalizeDraftPhase(phase)) {
|
|
173
|
+
case "work_order":
|
|
174
|
+
return DRAFT_WORK_ORDER_OPERATIONAL_SKILLS;
|
|
175
|
+
case "story":
|
|
176
|
+
return DRAFT_WORK_ITEM_OPERATIONAL_SKILLS;
|
|
177
|
+
default:
|
|
178
|
+
return DRAFT_WORK_ITEM_OPERATIONAL_SKILLS;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function draftPhaseHooks(phase) {
|
|
182
|
+
switch (normalizeDraftPhase(phase)) {
|
|
183
|
+
case "work_order":
|
|
184
|
+
return DRAFT_WORK_ORDER_OPERATIONAL_HOOKS;
|
|
185
|
+
case "story":
|
|
186
|
+
return DRAFT_WORK_ITEM_OPERATIONAL_HOOKS;
|
|
187
|
+
default:
|
|
188
|
+
return DRAFT_WORK_ITEM_OPERATIONAL_HOOKS;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function normalizeDraftPhase(phase) {
|
|
192
|
+
return phase.trim().toLowerCase().replaceAll("-", "_");
|
|
193
|
+
}
|
|
161
194
|
export function buildSpecBody(laneCommand, workItem, phase = "implementation") {
|
|
162
195
|
const prompt = laneCommand.body.replaceAll("{workItemId}", workItem.id);
|
|
163
196
|
return `---
|
|
@@ -30,3 +30,14 @@ export declare function createDirectoryLink(sourcePath: string, targetPath: stri
|
|
|
30
30
|
export declare function lockfilesDiffer(baseCheckoutPath: string, worktreePath: string, packageRootRelative: string): Promise<boolean>;
|
|
31
31
|
export declare function requiresInstallFallback(entry: WarmPathEntry, baseCheckoutPath: string, worktreePath: string, sourceExists: boolean): Promise<string | null>;
|
|
32
32
|
export declare function declaresNativeModules(packageRootPath: string): Promise<boolean>;
|
|
33
|
+
export interface NpmLaunchOptions {
|
|
34
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
35
|
+
readonly execPath?: string;
|
|
36
|
+
readonly platform?: NodeJS.Platform;
|
|
37
|
+
readonly homeDir?: string;
|
|
38
|
+
readonly candidates?: readonly string[];
|
|
39
|
+
}
|
|
40
|
+
export declare function resolveNpmLaunch(args: readonly string[], options?: NpmLaunchOptions): {
|
|
41
|
+
command: string;
|
|
42
|
+
args: readonly string[];
|
|
43
|
+
};
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
2
4
|
import path from "node:path";
|
|
3
5
|
import { NodeProcessRunner } from "../shared/process/process.js";
|
|
6
|
+
import { buildWindowsCmdLaunch } from "../shared/process/windows-launch.js";
|
|
4
7
|
import { isSubPath } from "../shared/path/path.js";
|
|
5
8
|
const SKIP_DIR_NAMES = new Set([
|
|
6
9
|
".git",
|
|
@@ -184,7 +187,8 @@ async function runInstallFallback(entry, worktreePath, reason, executor) {
|
|
|
184
187
|
if (entry.ecosystem === "node") {
|
|
185
188
|
const lockPath = path.join(cwd, "package-lock.json");
|
|
186
189
|
const args = (await pathExists(lockPath)) ? ["ci"] : ["install"];
|
|
187
|
-
|
|
190
|
+
const launch = resolveNpmLaunch(args);
|
|
191
|
+
await executor.run(cwd, launch.command, launch.args);
|
|
188
192
|
return { ecosystem: entry.ecosystem, packageRoot: entry.packageRoot, reason, command: `npm ${args.join(" ")}` };
|
|
189
193
|
}
|
|
190
194
|
if (entry.ecosystem === "python") {
|
|
@@ -286,6 +290,46 @@ async function sha256File(filePath) {
|
|
|
286
290
|
const content = await fs.readFile(filePath);
|
|
287
291
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
288
292
|
}
|
|
293
|
+
export function resolveNpmLaunch(args, options = {}) {
|
|
294
|
+
const env = options.env ?? process.env;
|
|
295
|
+
const execPath = options.execPath ?? process.execPath;
|
|
296
|
+
const npmExecPath = env.npm_execpath;
|
|
297
|
+
if (npmExecPath) {
|
|
298
|
+
return { command: execPath, args: [npmExecPath, ...args] };
|
|
299
|
+
}
|
|
300
|
+
const platform = options.platform ?? process.platform;
|
|
301
|
+
if (platform === "win32") {
|
|
302
|
+
const comSpec = options.env ? env.ComSpec ?? "cmd.exe" : env.ComSpec;
|
|
303
|
+
return buildWindowsCmdLaunch("npm.cmd", args, comSpec);
|
|
304
|
+
}
|
|
305
|
+
for (const candidate of options.candidates ?? defaultNpmCliCandidates(execPath, options.homeDir ?? os.homedir())) {
|
|
306
|
+
const resolvedCandidate = tryResolveNpmCandidate(candidate);
|
|
307
|
+
if (resolvedCandidate) {
|
|
308
|
+
return { command: execPath, args: [resolvedCandidate, ...args] };
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return { command: "npm", args };
|
|
312
|
+
}
|
|
313
|
+
function tryResolveNpmCandidate(candidate) {
|
|
314
|
+
try {
|
|
315
|
+
if (!existsSync(candidate)) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
return realpathSync(candidate);
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function defaultNpmCliCandidates(execPath, homeDir) {
|
|
325
|
+
const candidates = [];
|
|
326
|
+
const execDir = path.dirname(execPath);
|
|
327
|
+
if (path.isAbsolute(execDir)) {
|
|
328
|
+
candidates.push(path.join(execDir, "npm"), path.join(execDir, "npm-cli.js"), path.join(execDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
|
|
329
|
+
}
|
|
330
|
+
candidates.push("/usr/bin/npm", "/usr/local/bin/npm", path.join(homeDir, ".npm-global", "bin", "npm"), path.join(homeDir, ".npm-global", "lib", "node_modules", "npm", "bin", "npm-cli.js"));
|
|
331
|
+
return candidates;
|
|
332
|
+
}
|
|
289
333
|
const defaultCommandExecutor = {
|
|
290
334
|
async run(cwd, command, args) {
|
|
291
335
|
const result = await defaultProcessRunner.run({ command, cwd, args });
|
package/dist/worktree/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { GitWorktreeService } from "./worktree.service.js";
|
|
2
|
+
export { managedRepoRoot, resolveManagedWorktreeRoot, resolveManagedWorktreeRootForRepo, } from "./worktree-paths.js";
|
|
2
3
|
export { detectWarmPaths, warmWorktreeDependencies, createDirectoryLink, lockfilesDiffer, requiresInstallFallback } from "./dependency-warmer.js";
|
|
3
4
|
export type { WarmDependenciesResult, WarmPathEntry, InstallFallbackResult } from "./dependency-warmer.js";
|
|
4
5
|
export type { WorktreeService } from "./worktree.base.js";
|
package/dist/worktree/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { GitWorktreeService } from "./worktree.service.js";
|
|
2
|
+
export { managedRepoRoot, resolveManagedWorktreeRoot, resolveManagedWorktreeRootForRepo, } from "./worktree-paths.js";
|
|
2
3
|
export { detectWarmPaths, warmWorktreeDependencies, createDirectoryLink, lockfilesDiffer, requiresInstallFallback } from "./dependency-warmer.js";
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isSubPath } from "../shared/path/path.js";
|
|
5
|
+
const MAX_ARTIFACT_COUNT = 128;
|
|
6
|
+
const MAX_CONTENT_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
const MAX_TOTAL_BYTES = 12 * 1024 * 1024;
|
|
8
|
+
const phaseFiles = {
|
|
9
|
+
implementation: {
|
|
10
|
+
verdict: "implementation.completeness.json",
|
|
11
|
+
report: "implementation.report.md"
|
|
12
|
+
},
|
|
13
|
+
self_review: {
|
|
14
|
+
verdict: "self-review.report.json",
|
|
15
|
+
report: "self-review.summary.md"
|
|
16
|
+
},
|
|
17
|
+
validation: {
|
|
18
|
+
verdict: "validation.verdict.json",
|
|
19
|
+
report: "validation.report.md"
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
export async function hydratePriorPhaseArtifacts(worktreePath, artifacts = []) {
|
|
23
|
+
if (artifacts.length === 0)
|
|
24
|
+
return 0;
|
|
25
|
+
if (artifacts.length > MAX_ARTIFACT_COUNT) {
|
|
26
|
+
throw new Error(`Prior phase artifact handoff exceeds ${MAX_ARTIFACT_COUNT} artifacts.`);
|
|
27
|
+
}
|
|
28
|
+
const targets = new Set();
|
|
29
|
+
let totalBytes = 0;
|
|
30
|
+
for (const artifact of artifacts) {
|
|
31
|
+
const workItemId = safeArtifactSegment(artifact.workItemId, "work item id");
|
|
32
|
+
const files = phaseFiles[artifact.phase];
|
|
33
|
+
if (!files) {
|
|
34
|
+
throw new Error(`Unsupported prior phase artifact phase '${artifact.phase}'.`);
|
|
35
|
+
}
|
|
36
|
+
const verdictBytes = Buffer.byteLength(artifact.verdictJson, "utf8");
|
|
37
|
+
const reportBytes = Buffer.byteLength(artifact.report, "utf8");
|
|
38
|
+
if (verdictBytes > MAX_CONTENT_BYTES || reportBytes > MAX_CONTENT_BYTES) {
|
|
39
|
+
throw new Error(`Prior phase artifact '${artifact.artifactId}' exceeds the per-file hydration limit.`);
|
|
40
|
+
}
|
|
41
|
+
totalBytes += verdictBytes + reportBytes;
|
|
42
|
+
if (totalBytes > MAX_TOTAL_BYTES) {
|
|
43
|
+
throw new Error("Prior phase artifact handoff exceeds the total hydration limit.");
|
|
44
|
+
}
|
|
45
|
+
verifyContentHash(artifact.verdictJson, artifact.verdictContentHash, artifact.artifactId, "verdict");
|
|
46
|
+
verifyContentHash(artifact.report, artifact.reportContentHash, artifact.artifactId, "report");
|
|
47
|
+
validateVerdict(artifact);
|
|
48
|
+
const artifactRoot = path.resolve(worktreePath, ".forge", "work-items", workItemId, "artifacts");
|
|
49
|
+
if (!isSubPath(worktreePath, artifactRoot)) {
|
|
50
|
+
throw new Error("Prior phase artifact target must stay under the worktree.");
|
|
51
|
+
}
|
|
52
|
+
await ensureNoSymlinkPathSegments(worktreePath, artifactRoot);
|
|
53
|
+
await fs.mkdir(artifactRoot, { recursive: true });
|
|
54
|
+
await ensureNoSymlinkPathSegments(worktreePath, artifactRoot);
|
|
55
|
+
const verdictPath = path.join(artifactRoot, files.verdict);
|
|
56
|
+
const reportPath = path.join(artifactRoot, files.report);
|
|
57
|
+
const targetKey = verdictPath.toLowerCase();
|
|
58
|
+
if (targets.has(targetKey)) {
|
|
59
|
+
throw new Error(`Duplicate prior phase artifact target for work item '${workItemId}' phase '${artifact.phase}'.`);
|
|
60
|
+
}
|
|
61
|
+
targets.add(targetKey);
|
|
62
|
+
await writeAtomic(verdictPath, artifact.verdictJson);
|
|
63
|
+
await writeAtomic(reportPath, artifact.report);
|
|
64
|
+
}
|
|
65
|
+
return artifacts.length;
|
|
66
|
+
}
|
|
67
|
+
function validateVerdict(artifact) {
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(artifact.verdictJson);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new Error(`Prior phase artifact '${artifact.artifactId}' verdict is not valid JSON.`);
|
|
74
|
+
}
|
|
75
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
76
|
+
throw new Error(`Prior phase artifact '${artifact.artifactId}' verdict must be a JSON object.`);
|
|
77
|
+
}
|
|
78
|
+
const recordedPhase = parsed["phase"];
|
|
79
|
+
const normalizedPhase = typeof recordedPhase === "string"
|
|
80
|
+
? recordedPhase.trim().toLowerCase().replace(/-/g, "_")
|
|
81
|
+
: "";
|
|
82
|
+
if (normalizedPhase !== artifact.phase) {
|
|
83
|
+
throw new Error(`Prior phase artifact '${artifact.artifactId}' phase does not match its verdict.`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function verifyContentHash(content, expected, artifactId, label) {
|
|
87
|
+
const normalizedExpected = expected.trim().toLowerCase().replace(/^sha256:/, "");
|
|
88
|
+
if (!/^[0-9a-f]{64}$/.test(normalizedExpected)) {
|
|
89
|
+
throw new Error(`Prior phase artifact '${artifactId}' ${label} hash is invalid.`);
|
|
90
|
+
}
|
|
91
|
+
const actual = createHash("sha256").update(content, "utf8").digest("hex");
|
|
92
|
+
if (actual !== normalizedExpected) {
|
|
93
|
+
throw new Error(`Prior phase artifact '${artifactId}' ${label} content_hash mismatch.`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function safeArtifactSegment(value, label) {
|
|
97
|
+
const trimmed = value.trim();
|
|
98
|
+
if (!/^[a-zA-Z0-9_-]{1,128}$/.test(trimmed)) {
|
|
99
|
+
throw new Error(`Prior phase artifact ${label} is not a safe path segment.`);
|
|
100
|
+
}
|
|
101
|
+
return trimmed;
|
|
102
|
+
}
|
|
103
|
+
async function writeAtomic(targetPath, content) {
|
|
104
|
+
const temporaryPath = `${targetPath}.tmp-${randomUUID()}`;
|
|
105
|
+
try {
|
|
106
|
+
await fs.writeFile(temporaryPath, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
107
|
+
await fs.rename(temporaryPath, targetPath);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
await fs.rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function ensureNoSymlinkPathSegments(root, targetPath) {
|
|
114
|
+
const resolvedRoot = path.resolve(root);
|
|
115
|
+
const resolvedTarget = path.resolve(targetPath);
|
|
116
|
+
const relative = path.relative(resolvedRoot, resolvedTarget);
|
|
117
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
118
|
+
throw new Error("Prior phase artifact path must stay under the worktree.");
|
|
119
|
+
}
|
|
120
|
+
let current = resolvedRoot;
|
|
121
|
+
for (const segment of relative.split(path.sep)) {
|
|
122
|
+
if (!segment)
|
|
123
|
+
continue;
|
|
124
|
+
current = path.join(current, segment);
|
|
125
|
+
const stat = await fs.lstat(current).catch((error) => {
|
|
126
|
+
if (error.code === "ENOENT")
|
|
127
|
+
return null;
|
|
128
|
+
throw error;
|
|
129
|
+
});
|
|
130
|
+
if (!stat)
|
|
131
|
+
return;
|
|
132
|
+
if (stat.isSymbolicLink()) {
|
|
133
|
+
throw new Error("Prior phase artifact path must not traverse symlinked path segments.");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function managedRepoRoot(allowedRoot: string): string;
|
|
2
|
+
export declare function resolveManagedWorktreeRoot(allowedRoot: string, repoPath: string): string;
|
|
3
|
+
export declare function resolveManagedWorktreeRootForRepo(repoPath: string, allowedRoot?: string): string;
|
|
4
|
+
export declare function defaultWorktreeCoordinatorStateRoot(): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isSubPath } from "../shared/path/path.js";
|
|
4
|
+
export function managedRepoRoot(allowedRoot) {
|
|
5
|
+
return path.join(path.resolve(allowedRoot), ".forge-worktrees", "_repos");
|
|
6
|
+
}
|
|
7
|
+
export function resolveManagedWorktreeRoot(allowedRoot, repoPath) {
|
|
8
|
+
const resolvedAllowedRoot = path.resolve(allowedRoot);
|
|
9
|
+
const resolvedRepoPath = path.resolve(repoPath);
|
|
10
|
+
const baseName = path.basename(resolvedRepoPath);
|
|
11
|
+
return isSubPath(managedRepoRoot(resolvedAllowedRoot), resolvedRepoPath)
|
|
12
|
+
? path.join(resolvedAllowedRoot, ".forge-worktrees", baseName)
|
|
13
|
+
: path.join(path.dirname(resolvedRepoPath), ".forge-worktrees", baseName);
|
|
14
|
+
}
|
|
15
|
+
export function resolveManagedWorktreeRootForRepo(repoPath, allowedRoot) {
|
|
16
|
+
const resolvedRepoPath = path.resolve(repoPath);
|
|
17
|
+
if (allowedRoot && isSubPath(path.resolve(allowedRoot), resolvedRepoPath)) {
|
|
18
|
+
return resolveManagedWorktreeRoot(allowedRoot, resolvedRepoPath);
|
|
19
|
+
}
|
|
20
|
+
const repoParent = path.dirname(resolvedRepoPath);
|
|
21
|
+
const managedRoot = path.dirname(repoParent);
|
|
22
|
+
if (path.basename(repoParent) === "_repos" && path.basename(managedRoot) === ".forge-worktrees") {
|
|
23
|
+
return path.join(managedRoot, path.basename(resolvedRepoPath));
|
|
24
|
+
}
|
|
25
|
+
return path.join(repoParent, ".forge-worktrees", path.basename(resolvedRepoPath));
|
|
26
|
+
}
|
|
27
|
+
export function defaultWorktreeCoordinatorStateRoot() {
|
|
28
|
+
return process.env.FORGE_SANDBOX_STATE_ROOT
|
|
29
|
+
?? path.join(os.homedir(), ".forge", "bridge", "sandboxes");
|
|
30
|
+
}
|
|
@@ -3,3 +3,6 @@ export interface WorktreeService {
|
|
|
3
3
|
prepare(request: WorktreePrepareRequest): Promise<WorktreeHandle>;
|
|
4
4
|
release(handle: WorktreeHandle): Promise<void>;
|
|
5
5
|
}
|
|
6
|
+
export interface WorktreeLifecycleCoordinator {
|
|
7
|
+
withLock<T>(repoPath: string, action: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
8
|
+
}
|