@getforgeai/cli 0.1.0-beta.1
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/LICENSE +201 -0
- package/README.md +80 -0
- package/dist/bin/forgeai.d.ts +3 -0
- package/dist/bin/forgeai.js +8 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +180 -0
- package/dist/commands/auth-login.d.ts +4 -0
- package/dist/commands/auth-login.js +202 -0
- package/dist/commands/auth-logout.d.ts +2 -0
- package/dist/commands/auth-logout.js +41 -0
- package/dist/commands/auth-status.d.ts +4 -0
- package/dist/commands/auth-status.js +81 -0
- package/dist/commands/cli-list.d.ts +4 -0
- package/dist/commands/cli-list.js +40 -0
- package/dist/commands/cli-unlink.d.ts +4 -0
- package/dist/commands/cli-unlink.js +45 -0
- package/dist/commands/config-set.d.ts +5 -0
- package/dist/commands/config-set.js +63 -0
- package/dist/commands/connect.d.ts +9 -0
- package/dist/commands/connect.js +165 -0
- package/dist/commands/disconnect.d.ts +5 -0
- package/dist/commands/disconnect.js +126 -0
- package/dist/commands/journal-clear.d.ts +2 -0
- package/dist/commands/journal-clear.js +16 -0
- package/dist/commands/journal-list.d.ts +2 -0
- package/dist/commands/journal-list.js +20 -0
- package/dist/commands/journal-status.d.ts +2 -0
- package/dist/commands/journal-status.js +18 -0
- package/dist/commands/mcp-serve.d.ts +13 -0
- package/dist/commands/mcp-serve.js +74 -0
- package/dist/commands/org-list.d.ts +4 -0
- package/dist/commands/org-list.js +56 -0
- package/dist/commands/org-unlink.d.ts +4 -0
- package/dist/commands/org-unlink.js +63 -0
- package/dist/commands/reconnect.d.ts +4 -0
- package/dist/commands/reconnect.js +46 -0
- package/dist/commands/release-open.d.ts +2 -0
- package/dist/commands/release-open.js +43 -0
- package/dist/commands/session-respond.d.ts +2 -0
- package/dist/commands/session-respond.js +29 -0
- package/dist/commands/task-open.d.ts +2 -0
- package/dist/commands/task-open.js +42 -0
- package/dist/commands/vm.d.ts +21 -0
- package/dist/commands/vm.js +122 -0
- package/dist/commands/workflow-open.d.ts +2 -0
- package/dist/commands/workflow-open.js +43 -0
- package/dist/config/config-manager.d.ts +31 -0
- package/dist/config/config-manager.js +70 -0
- package/dist/config/token-manager.d.ts +11 -0
- package/dist/config/token-manager.js +87 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +22 -0
- package/dist/lib/auth-client.d.ts +1670 -0
- package/dist/lib/auth-client.js +10 -0
- package/dist/lib/branch-manager.d.ts +79 -0
- package/dist/lib/branch-manager.js +620 -0
- package/dist/lib/clone-and-checkout.d.ts +6 -0
- package/dist/lib/clone-and-checkout.js +41 -0
- package/dist/lib/connection-manager.d.ts +58 -0
- package/dist/lib/connection-manager.js +436 -0
- package/dist/lib/daemon-entry.d.ts +2 -0
- package/dist/lib/daemon-entry.js +210 -0
- package/dist/lib/daemon.d.ts +38 -0
- package/dist/lib/daemon.js +148 -0
- package/dist/lib/event-journal.d.ts +24 -0
- package/dist/lib/event-journal.js +76 -0
- package/dist/lib/ipc-client.d.ts +10 -0
- package/dist/lib/ipc-client.js +70 -0
- package/dist/lib/ipc-server.d.ts +36 -0
- package/dist/lib/ipc-server.js +70 -0
- package/dist/lib/node-version-guard.d.ts +8 -0
- package/dist/lib/node-version-guard.js +60 -0
- package/dist/lib/open-directory.d.ts +7 -0
- package/dist/lib/open-directory.js +41 -0
- package/dist/lib/repo-manager.d.ts +29 -0
- package/dist/lib/repo-manager.js +103 -0
- package/dist/lib/resilient-emitter.d.ts +53 -0
- package/dist/lib/resilient-emitter.js +160 -0
- package/dist/lib/rest-client.d.ts +164 -0
- package/dist/lib/rest-client.js +139 -0
- package/dist/lib/skill-manager.d.ts +23 -0
- package/dist/lib/skill-manager.js +167 -0
- package/dist/lib/socket-client.d.ts +68 -0
- package/dist/lib/socket-client.js +160 -0
- package/dist/mcp/mcp-probe.d.ts +11 -0
- package/dist/mcp/mcp-probe.js +106 -0
- package/dist/mcp/mcp-response.d.ts +29 -0
- package/dist/mcp/mcp-response.js +67 -0
- package/dist/mcp/mcp-server.d.ts +19 -0
- package/dist/mcp/mcp-server.js +76 -0
- package/dist/mcp/resources/project-conventions.d.ts +4 -0
- package/dist/mcp/resources/project-conventions.js +50 -0
- package/dist/mcp/resources/task-current.d.ts +4 -0
- package/dist/mcp/resources/task-current.js +51 -0
- package/dist/mcp/tools/forge-add-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-group-dependency.js +20 -0
- package/dist/mcp/tools/forge-add-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-add-task-dependency.js +20 -0
- package/dist/mcp/tools/forge-ask-human.d.ts +4 -0
- package/dist/mcp/tools/forge-ask-human.js +80 -0
- package/dist/mcp/tools/forge-column-done.d.ts +4 -0
- package/dist/mcp/tools/forge-column-done.js +46 -0
- package/dist/mcp/tools/forge-complete-step.d.ts +4 -0
- package/dist/mcp/tools/forge-complete-step.js +139 -0
- package/dist/mcp/tools/forge-create-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task-group.js +22 -0
- package/dist/mcp/tools/forge-create-task.d.ts +4 -0
- package/dist/mcp/tools/forge-create-task.js +28 -0
- package/dist/mcp/tools/forge-delete-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task-group.js +17 -0
- package/dist/mcp/tools/forge-delete-task.d.ts +4 -0
- package/dist/mcp/tools/forge-delete-task.js +17 -0
- package/dist/mcp/tools/forge-get-task.d.ts +4 -0
- package/dist/mcp/tools/forge-get-task.js +17 -0
- package/dist/mcp/tools/forge-merge-back.d.ts +4 -0
- package/dist/mcp/tools/forge-merge-back.js +179 -0
- package/dist/mcp/tools/forge-release-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-release-merge.js +159 -0
- package/dist/mcp/tools/forge-release-notes.d.ts +4 -0
- package/dist/mcp/tools/forge-release-notes.js +51 -0
- package/dist/mcp/tools/forge-release-tag.d.ts +4 -0
- package/dist/mcp/tools/forge-release-tag.js +125 -0
- package/dist/mcp/tools/forge-remove-group-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-group-dependency.js +19 -0
- package/dist/mcp/tools/forge-remove-task-dependency.d.ts +4 -0
- package/dist/mcp/tools/forge-remove-task-dependency.js +19 -0
- package/dist/mcp/tools/forge-requeue.d.ts +4 -0
- package/dist/mcp/tools/forge-requeue.js +39 -0
- package/dist/mcp/tools/forge-submit-pr.d.ts +4 -0
- package/dist/mcp/tools/forge-submit-pr.js +68 -0
- package/dist/mcp/tools/forge-update-status.d.ts +4 -0
- package/dist/mcp/tools/forge-update-status.js +77 -0
- package/dist/mcp/tools/forge-update-task-group.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task-group.js +21 -0
- package/dist/mcp/tools/forge-update-task.d.ts +4 -0
- package/dist/mcp/tools/forge-update-task.js +25 -0
- package/dist/mcp/tools/forge-validate-merge.d.ts +4 -0
- package/dist/mcp/tools/forge-validate-merge.js +194 -0
- package/dist/mcp/types.d.ts +39 -0
- package/dist/mcp/types.js +8 -0
- package/dist/methodology/artifact-manager.d.ts +14 -0
- package/dist/methodology/artifact-manager.js +55 -0
- package/dist/orchestrator/agent-registry.d.ts +53 -0
- package/dist/orchestrator/agent-registry.js +96 -0
- package/dist/orchestrator/agent-spawner.d.ts +112 -0
- package/dist/orchestrator/agent-spawner.js +1518 -0
- package/dist/orchestrator/connectors/claude-code-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/claude-code-connector.js +60 -0
- package/dist/orchestrator/connectors/connector.d.ts +95 -0
- package/dist/orchestrator/connectors/connector.js +25 -0
- package/dist/orchestrator/connectors/docker-connector.d.ts +14 -0
- package/dist/orchestrator/connectors/docker-connector.js +337 -0
- package/dist/orchestrator/connectors/native-vm-connector.d.ts +11 -0
- package/dist/orchestrator/connectors/native-vm-connector.js +162 -0
- package/dist/orchestrator/connectors/opencode-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/opencode-connector.js +39 -0
- package/dist/orchestrator/connectors/vm-connector.d.ts +3 -0
- package/dist/orchestrator/connectors/vm-connector.js +139 -0
- package/dist/orchestrator/dispatch-queue.d.ts +43 -0
- package/dist/orchestrator/dispatch-queue.js +93 -0
- package/dist/orchestrator/resume-handler.d.ts +34 -0
- package/dist/orchestrator/resume-handler.js +159 -0
- package/dist/orchestrator/stream-batcher.d.ts +16 -0
- package/dist/orchestrator/stream-batcher.js +50 -0
- package/dist/orchestrator/stream-json-extractor.d.ts +97 -0
- package/dist/orchestrator/stream-json-extractor.js +579 -0
- package/dist/vm/claude-token-manager.d.ts +36 -0
- package/dist/vm/claude-token-manager.js +223 -0
- package/dist/vm/docker-executor.d.ts +131 -0
- package/dist/vm/docker-executor.js +360 -0
- package/dist/vm/docker-process-proxy.d.ts +31 -0
- package/dist/vm/docker-process-proxy.js +105 -0
- package/dist/vm/forge-vm-agent.d.ts +11 -0
- package/dist/vm/forge-vm-agent.js +159 -0
- package/dist/vm/git-credentials.d.ts +43 -0
- package/dist/vm/git-credentials.js +53 -0
- package/dist/vm/image-manager.d.ts +7 -0
- package/dist/vm/image-manager.js +61 -0
- package/dist/vm/log.d.ts +9 -0
- package/dist/vm/log.js +18 -0
- package/dist/vm/mcp-bridge.d.ts +63 -0
- package/dist/vm/mcp-bridge.js +243 -0
- package/dist/vm/mcp-stub.d.ts +15 -0
- package/dist/vm/mcp-stub.js +67 -0
- package/dist/vm/native-addon.d.ts +69 -0
- package/dist/vm/native-addon.js +68 -0
- package/dist/vm/provider-resolver.d.ts +7 -0
- package/dist/vm/provider-resolver.js +27 -0
- package/dist/vm/providers/lima.d.ts +17 -0
- package/dist/vm/providers/lima.js +191 -0
- package/dist/vm/providers/qemu.d.ts +28 -0
- package/dist/vm/providers/qemu.js +260 -0
- package/dist/vm/providers/vfkit.d.ts +40 -0
- package/dist/vm/providers/vfkit.js +371 -0
- package/dist/vm/providers/wsl2.d.ts +27 -0
- package/dist/vm/providers/wsl2.js +226 -0
- package/dist/vm/session-manager.d.ts +130 -0
- package/dist/vm/session-manager.js +494 -0
- package/dist/vm/session-snapshot.d.ts +31 -0
- package/dist/vm/session-snapshot.js +180 -0
- package/dist/vm/types.d.ts +73 -0
- package/dist/vm/types.js +2 -0
- package/dist/vm/vm-helper-client.d.ts +81 -0
- package/dist/vm/vm-helper-client.js +335 -0
- package/dist/vm/vm-helper.d.ts +11 -0
- package/dist/vm/vm-helper.js +156 -0
- package/dist/vm/vm-ipc-protocol.d.ts +84 -0
- package/dist/vm/vm-ipc-protocol.js +9 -0
- package/dist/vm/vm-manager-facade.d.ts +23 -0
- package/dist/vm/vm-manager-facade.js +43 -0
- package/dist/vm/vm-manager.d.ts +74 -0
- package/dist/vm/vm-manager.js +515 -0
- package/dist/vm/vm-process-proxy.d.ts +34 -0
- package/dist/vm/vm-process-proxy.js +64 -0
- package/dist/vm/vsock-bridge.d.ts +44 -0
- package/dist/vm/vsock-bridge.js +140 -0
- package/dist/vm/vsock-protocol.d.ts +98 -0
- package/dist/vm/vsock-protocol.js +57 -0
- package/package.json +65 -0
- package/rootfs/Dockerfile +50 -0
- package/rootfs/forge-mcp-relay.mjs +158 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
/**
|
|
3
|
+
* Fail fast, and legibly, on an unsupported Node version.
|
|
4
|
+
*
|
|
5
|
+
* Several code paths rely on `import.meta.dirname` (Node 20.11+): the daemon
|
|
6
|
+
* spawn in lib/daemon.ts, the relay staging in vm/session-manager.ts, and the
|
|
7
|
+
* agent image build in vm/image-manager.ts. On older Node it is `undefined`,
|
|
8
|
+
* so `join(undefined, ...)` throws `The "path" argument must be of type
|
|
9
|
+
* string. Received undefined` — a message that tells the user nothing.
|
|
10
|
+
*
|
|
11
|
+
* npm only *warns* on an `engines` mismatch (engine-strict is off by default),
|
|
12
|
+
* so the install itself does not protect users. This check does.
|
|
13
|
+
*/
|
|
14
|
+
const FALLBACK_MINIMUM = "20.11.0";
|
|
15
|
+
function parse(version) {
|
|
16
|
+
const [major = 0, minor = 0, patch = 0] = version
|
|
17
|
+
.replace(/^v/, "")
|
|
18
|
+
.split(".")
|
|
19
|
+
.map((part) => Number.parseInt(part, 10) || 0);
|
|
20
|
+
return [major, minor, patch];
|
|
21
|
+
}
|
|
22
|
+
/** Read the minimum from package.json `engines.node` (e.g. ">=20.11"). */
|
|
23
|
+
function readRequiredVersion() {
|
|
24
|
+
try {
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const pkg = require("../../package.json");
|
|
27
|
+
const range = pkg.engines?.node;
|
|
28
|
+
const match = range ? /(\d+(?:\.\d+){0,2})/.exec(range) : null;
|
|
29
|
+
return match ? match[1] : FALLBACK_MINIMUM;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return FALLBACK_MINIMUM;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** True when `current` is at least `required`. Exported for tests. */
|
|
36
|
+
export function isSupportedNodeVersion(current, required) {
|
|
37
|
+
const [cMajor, cMinor, cPatch] = parse(current);
|
|
38
|
+
const [rMajor, rMinor, rPatch] = parse(required);
|
|
39
|
+
if (cMajor !== rMajor)
|
|
40
|
+
return cMajor > rMajor;
|
|
41
|
+
if (cMinor !== rMinor)
|
|
42
|
+
return cMinor > rMinor;
|
|
43
|
+
return cPatch >= rPatch;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Print an actionable message and exit(1) when Node is too old.
|
|
47
|
+
* Called once from the CLI entry point, before anything else runs.
|
|
48
|
+
*/
|
|
49
|
+
export function assertSupportedNodeVersion(current = process.versions.node, required = readRequiredVersion()) {
|
|
50
|
+
if (isSupportedNodeVersion(current, required))
|
|
51
|
+
return;
|
|
52
|
+
process.stderr.write(`\nForgeAI CLI requires Node.js ${required} or newer — you are running v${current}.\n\n` +
|
|
53
|
+
`Several commands (forge connect, forge vm init) rely on Node APIs added in ${required}\n` +
|
|
54
|
+
`and fail in confusing ways on older versions.\n\n` +
|
|
55
|
+
`Upgrade Node, for example:\n` +
|
|
56
|
+
` nvm install --lts && nvm use --lts\n` +
|
|
57
|
+
` (or download from https://nodejs.org)\n\n`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=node-version-guard.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type Opener = "terminal" | "vscode" | "finder";
|
|
2
|
+
/**
|
|
3
|
+
* Open a directory in the configured application.
|
|
4
|
+
* Cross-platform: macOS, Linux, Windows.
|
|
5
|
+
*/
|
|
6
|
+
export declare function openDirectory(dirPath: string, opener?: Opener): void;
|
|
7
|
+
//# sourceMappingURL=open-directory.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { platform } from "node:os";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
/**
|
|
5
|
+
* Open a directory in the configured application.
|
|
6
|
+
* Cross-platform: macOS, Linux, Windows.
|
|
7
|
+
*/
|
|
8
|
+
export function openDirectory(dirPath, opener = "terminal") {
|
|
9
|
+
const os = platform();
|
|
10
|
+
const { cmd, args } = resolveCommand(os, opener, dirPath);
|
|
11
|
+
console.log(chalk.dim(`[open] Launching: ${cmd} ${args.join(" ")}`));
|
|
12
|
+
const child = spawn(cmd, args, {
|
|
13
|
+
detached: true,
|
|
14
|
+
stdio: "ignore",
|
|
15
|
+
});
|
|
16
|
+
child.unref();
|
|
17
|
+
}
|
|
18
|
+
function resolveCommand(os, opener, dirPath) {
|
|
19
|
+
switch (opener) {
|
|
20
|
+
case "vscode":
|
|
21
|
+
return { cmd: "code", args: [dirPath] };
|
|
22
|
+
case "finder":
|
|
23
|
+
if (os === "darwin")
|
|
24
|
+
return { cmd: "open", args: [dirPath] };
|
|
25
|
+
if (os === "win32")
|
|
26
|
+
return { cmd: "explorer", args: [dirPath] };
|
|
27
|
+
return { cmd: "xdg-open", args: [dirPath] };
|
|
28
|
+
case "terminal":
|
|
29
|
+
default:
|
|
30
|
+
if (os === "darwin")
|
|
31
|
+
return { cmd: "open", args: ["-a", "Terminal", dirPath] };
|
|
32
|
+
if (os === "win32")
|
|
33
|
+
return {
|
|
34
|
+
cmd: "cmd",
|
|
35
|
+
args: ["/c", "start", "cmd", "/K", `cd /d ${dirPath}`],
|
|
36
|
+
};
|
|
37
|
+
// Linux: try common terminal emulators
|
|
38
|
+
return { cmd: "xterm", args: ["-e", `cd '${dirPath}' && $SHELL`] };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=open-directory.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the repo path for a project.
|
|
3
|
+
* Convention: ~/.forgeai/repos/<projectSlug>/
|
|
4
|
+
*/
|
|
5
|
+
export declare function getRepoPath(projectSlug: string): string;
|
|
6
|
+
/**
|
|
7
|
+
* Check if a directory exists and is a git repo (has .git/ directory).
|
|
8
|
+
* Distinguishes real repos from worktrees (which have a .git *file*, not a directory).
|
|
9
|
+
*/
|
|
10
|
+
export declare function repoExists(targetDir: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Clone a git repository to the target directory.
|
|
13
|
+
* Validates URL format before cloning to prevent command injection.
|
|
14
|
+
*/
|
|
15
|
+
export declare function cloneRepo(repoUrl: string, targetDir: string): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Fetch all remote refs for a repository.
|
|
18
|
+
* Non-fatal on failure — worktree creation may still succeed with stale refs.
|
|
19
|
+
*/
|
|
20
|
+
export declare function fetchRepo(repoDir: string): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve the local repo path for a project.
|
|
23
|
+
* - If repoUrl is provided: clone if missing, fetch if existing
|
|
24
|
+
* - If repoUrl is missing: fall back to process.cwd() with deprecation warning
|
|
25
|
+
*
|
|
26
|
+
* Uses per-project mutex to serialize clone/fetch operations.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveRepoPath(projectSlug: string, repoUrl?: string): Promise<string>;
|
|
29
|
+
//# sourceMappingURL=repo-manager.d.ts.map
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { Mutex } from "async-mutex";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
// Per-project mutex to serialize clone/fetch operations
|
|
10
|
+
const repoMutexes = new Map();
|
|
11
|
+
function getRepoMutex(projectSlug) {
|
|
12
|
+
let mutex = repoMutexes.get(projectSlug);
|
|
13
|
+
if (!mutex) {
|
|
14
|
+
mutex = new Mutex();
|
|
15
|
+
repoMutexes.set(projectSlug, mutex);
|
|
16
|
+
}
|
|
17
|
+
return mutex;
|
|
18
|
+
}
|
|
19
|
+
// Require a real host after protocol (not just "https://" or "git@.:")
|
|
20
|
+
const VALID_GIT_URL = /^(https?:\/\/[\w.-]+\w(\/|$)|git@\w[\w.-]*:)/;
|
|
21
|
+
/**
|
|
22
|
+
* Build the repo path for a project.
|
|
23
|
+
* Convention: ~/.forgeai/repos/<projectSlug>/
|
|
24
|
+
*/
|
|
25
|
+
export function getRepoPath(projectSlug) {
|
|
26
|
+
return join(homedir(), ".forgeai", "repos", projectSlug);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check if a directory exists and is a git repo (has .git/ directory).
|
|
30
|
+
* Distinguishes real repos from worktrees (which have a .git *file*, not a directory).
|
|
31
|
+
*/
|
|
32
|
+
export function repoExists(targetDir) {
|
|
33
|
+
const gitPath = join(targetDir, ".git");
|
|
34
|
+
if (!existsSync(gitPath))
|
|
35
|
+
return false;
|
|
36
|
+
try {
|
|
37
|
+
return statSync(gitPath).isDirectory();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Clone a git repository to the target directory.
|
|
45
|
+
* Validates URL format before cloning to prevent command injection.
|
|
46
|
+
*/
|
|
47
|
+
export async function cloneRepo(repoUrl, targetDir) {
|
|
48
|
+
if (!VALID_GIT_URL.test(repoUrl)) {
|
|
49
|
+
throw new Error(`Invalid git URL format: ${repoUrl}`);
|
|
50
|
+
}
|
|
51
|
+
console.log(chalk.cyan(`[repo-manager] Cloning ${repoUrl} to ${targetDir}`));
|
|
52
|
+
const { stderr } = await execFileAsync("git", ["clone", repoUrl, targetDir], {
|
|
53
|
+
timeout: 120_000, // 2 min timeout for large repos
|
|
54
|
+
});
|
|
55
|
+
if (stderr) {
|
|
56
|
+
// git clone outputs progress to stderr even on success
|
|
57
|
+
console.log(chalk.dim(`[repo-manager] ${stderr.trim()}`));
|
|
58
|
+
}
|
|
59
|
+
console.log(chalk.green(`[repo-manager] Clone complete: ${targetDir}`));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Fetch all remote refs for a repository.
|
|
63
|
+
* Non-fatal on failure — worktree creation may still succeed with stale refs.
|
|
64
|
+
*/
|
|
65
|
+
export async function fetchRepo(repoDir) {
|
|
66
|
+
try {
|
|
67
|
+
await execFileAsync("git", ["fetch", "--all"], {
|
|
68
|
+
cwd: repoDir,
|
|
69
|
+
timeout: 60_000, // 1 min timeout
|
|
70
|
+
});
|
|
71
|
+
console.log(chalk.green(`[repo-manager] Fetched all refs for ${repoDir}`));
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
console.warn(chalk.yellow(`[repo-manager] git fetch failed for ${repoDir}: ${message}`));
|
|
76
|
+
// Non-fatal — worktree creation may still succeed with stale refs
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Resolve the local repo path for a project.
|
|
81
|
+
* - If repoUrl is provided: clone if missing, fetch if existing
|
|
82
|
+
* - If repoUrl is missing: fall back to process.cwd() with deprecation warning
|
|
83
|
+
*
|
|
84
|
+
* Uses per-project mutex to serialize clone/fetch operations.
|
|
85
|
+
*/
|
|
86
|
+
export async function resolveRepoPath(projectSlug, repoUrl) {
|
|
87
|
+
if (!repoUrl) {
|
|
88
|
+
console.warn(chalk.yellow("[repo-manager] No repoUrl provided, falling back to process.cwd() (deprecated)"));
|
|
89
|
+
return process.cwd();
|
|
90
|
+
}
|
|
91
|
+
const targetDir = getRepoPath(projectSlug);
|
|
92
|
+
const mutex = getRepoMutex(projectSlug);
|
|
93
|
+
return mutex.runExclusive(async () => {
|
|
94
|
+
if (repoExists(targetDir)) {
|
|
95
|
+
await fetchRepo(targetDir);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
await cloneRepo(repoUrl, targetDir);
|
|
99
|
+
}
|
|
100
|
+
return targetDir;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=repo-manager.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AgentStatusPayload, TaskUpdatePayload, CliHeartbeatPayload, SyncRequest, SyncResponse } from "@getforgeai/protocol";
|
|
2
|
+
import type { EventJournal } from "./event-journal.js";
|
|
3
|
+
type SocketClient = {
|
|
4
|
+
socket: {
|
|
5
|
+
connected: boolean;
|
|
6
|
+
on(event: string, handler: (...args: never[]) => void): unknown;
|
|
7
|
+
};
|
|
8
|
+
emitAgentStatus: (payload: Omit<AgentStatusPayload, "orgId" | "timestamp">) => unknown;
|
|
9
|
+
emitTaskUpdate: (payload: Omit<TaskUpdatePayload, "orgId" | "timestamp">) => unknown;
|
|
10
|
+
emitCliHeartbeat: (payload: Omit<CliHeartbeatPayload, "orgId" | "timestamp">) => unknown;
|
|
11
|
+
};
|
|
12
|
+
type RestClient = {
|
|
13
|
+
syncEvents: (data: SyncRequest) => Promise<SyncResponse>;
|
|
14
|
+
};
|
|
15
|
+
type ResilientEmitterOptions = {
|
|
16
|
+
socketClient: SocketClient;
|
|
17
|
+
restClient: RestClient;
|
|
18
|
+
journal: EventJournal;
|
|
19
|
+
cliId: string;
|
|
20
|
+
orgId: string;
|
|
21
|
+
projectId?: string;
|
|
22
|
+
initialCursor?: string;
|
|
23
|
+
onSyncStart?: () => void;
|
|
24
|
+
onSyncComplete?: (count: number) => void;
|
|
25
|
+
onSyncError?: (error: Error) => void;
|
|
26
|
+
};
|
|
27
|
+
export declare class ResilientEmitter {
|
|
28
|
+
private readonly socketClient;
|
|
29
|
+
private readonly restClient;
|
|
30
|
+
private readonly journal;
|
|
31
|
+
private readonly cliId;
|
|
32
|
+
private readonly orgId;
|
|
33
|
+
private readonly projectId;
|
|
34
|
+
private connected;
|
|
35
|
+
private syncing;
|
|
36
|
+
private lastSyncCursor;
|
|
37
|
+
readonly onSyncStart?: () => void;
|
|
38
|
+
readonly onSyncComplete?: (count: number) => void;
|
|
39
|
+
readonly onSyncError?: (error: Error) => void;
|
|
40
|
+
constructor(options: ResilientEmitterOptions);
|
|
41
|
+
private setupConnectionListeners;
|
|
42
|
+
isConnected(): boolean;
|
|
43
|
+
emitAgentStatus(payload: Omit<AgentStatusPayload, "orgId" | "timestamp">): void;
|
|
44
|
+
emitTaskUpdate(payload: Omit<TaskUpdatePayload, "orgId" | "timestamp">): void;
|
|
45
|
+
emitCliHeartbeat(payload: Omit<CliHeartbeatPayload, "orgId" | "timestamp">): void;
|
|
46
|
+
private appendToJournal;
|
|
47
|
+
syncPendingEvents(): Promise<number>;
|
|
48
|
+
private syncWithRetry;
|
|
49
|
+
getPendingCount(): number;
|
|
50
|
+
getLastSyncCursor(): string;
|
|
51
|
+
}
|
|
52
|
+
export {};
|
|
53
|
+
//# sourceMappingURL=resilient-emitter.d.ts.map
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const BATCH_SIZE = 100;
|
|
2
|
+
const MAX_RETRIES = 5;
|
|
3
|
+
const BACKOFF_BASE_MS = 1_000;
|
|
4
|
+
const BACKOFF_MAX_MS = 30_000;
|
|
5
|
+
export class ResilientEmitter {
|
|
6
|
+
socketClient;
|
|
7
|
+
restClient;
|
|
8
|
+
journal;
|
|
9
|
+
cliId;
|
|
10
|
+
orgId;
|
|
11
|
+
projectId;
|
|
12
|
+
connected;
|
|
13
|
+
syncing = false;
|
|
14
|
+
lastSyncCursor;
|
|
15
|
+
onSyncStart;
|
|
16
|
+
onSyncComplete;
|
|
17
|
+
onSyncError;
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.socketClient = options.socketClient;
|
|
20
|
+
this.restClient = options.restClient;
|
|
21
|
+
this.journal = options.journal;
|
|
22
|
+
this.cliId = options.cliId;
|
|
23
|
+
this.orgId = options.orgId;
|
|
24
|
+
this.projectId = options.projectId ?? null;
|
|
25
|
+
this.connected = options.socketClient.socket.connected;
|
|
26
|
+
this.lastSyncCursor = options.initialCursor ?? "";
|
|
27
|
+
this.onSyncStart = options.onSyncStart;
|
|
28
|
+
this.onSyncComplete = options.onSyncComplete;
|
|
29
|
+
this.onSyncError = options.onSyncError;
|
|
30
|
+
this.setupConnectionListeners();
|
|
31
|
+
}
|
|
32
|
+
setupConnectionListeners() {
|
|
33
|
+
this.socketClient.socket.on("connect", () => {
|
|
34
|
+
this.connected = true;
|
|
35
|
+
void this.syncPendingEvents();
|
|
36
|
+
});
|
|
37
|
+
this.socketClient.socket.on("disconnect", () => {
|
|
38
|
+
this.connected = false;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
isConnected() {
|
|
42
|
+
return this.connected;
|
|
43
|
+
}
|
|
44
|
+
emitAgentStatus(payload) {
|
|
45
|
+
const timestamp = new Date().toISOString();
|
|
46
|
+
// Journal meaningful events (not heartbeats)
|
|
47
|
+
this.appendToJournal({
|
|
48
|
+
type: "agent:status",
|
|
49
|
+
timestamp,
|
|
50
|
+
payload: payload,
|
|
51
|
+
});
|
|
52
|
+
if (this.connected) {
|
|
53
|
+
this.socketClient.emitAgentStatus(payload);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
emitTaskUpdate(payload) {
|
|
57
|
+
const timestamp = new Date().toISOString();
|
|
58
|
+
this.appendToJournal({
|
|
59
|
+
type: "task:update",
|
|
60
|
+
timestamp,
|
|
61
|
+
payload: payload,
|
|
62
|
+
});
|
|
63
|
+
if (this.connected) {
|
|
64
|
+
this.socketClient.emitTaskUpdate(payload);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
emitCliHeartbeat(payload) {
|
|
68
|
+
// Heartbeats are NOT journaled (ephemeral, high-frequency)
|
|
69
|
+
// Only emit via socket when connected
|
|
70
|
+
if (this.connected) {
|
|
71
|
+
this.socketClient.emitCliHeartbeat(payload);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
appendToJournal(event) {
|
|
75
|
+
this.journal.append({
|
|
76
|
+
...event,
|
|
77
|
+
cliId: this.cliId,
|
|
78
|
+
orgId: this.orgId,
|
|
79
|
+
projectId: this.projectId,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async syncPendingEvents() {
|
|
83
|
+
if (this.syncing)
|
|
84
|
+
return 0;
|
|
85
|
+
this.syncing = true;
|
|
86
|
+
this.onSyncStart?.();
|
|
87
|
+
let totalSynced = 0;
|
|
88
|
+
try {
|
|
89
|
+
while (true) {
|
|
90
|
+
const pending = this.journal.readSince(this.lastSyncCursor);
|
|
91
|
+
if (pending.length === 0)
|
|
92
|
+
break;
|
|
93
|
+
// Batch into chunks
|
|
94
|
+
const batch = pending.slice(0, BATCH_SIZE);
|
|
95
|
+
const syncRequest = {
|
|
96
|
+
cliId: this.cliId,
|
|
97
|
+
projectId: this.projectId ?? undefined,
|
|
98
|
+
events: batch.map((e) => ({
|
|
99
|
+
eventId: e.eventId,
|
|
100
|
+
type: e.type,
|
|
101
|
+
timestamp: e.timestamp,
|
|
102
|
+
cliId: e.cliId,
|
|
103
|
+
orgId: e.orgId,
|
|
104
|
+
projectId: e.projectId,
|
|
105
|
+
payload: e.payload,
|
|
106
|
+
})),
|
|
107
|
+
cursor: this.lastSyncCursor || undefined,
|
|
108
|
+
};
|
|
109
|
+
const response = await this.syncWithRetry(syncRequest);
|
|
110
|
+
totalSynced += response.accepted;
|
|
111
|
+
// Only advance cursor and clear journal when events were actually accepted
|
|
112
|
+
// (prevents data loss on rate-limited or rejected responses)
|
|
113
|
+
if (response.accepted > 0) {
|
|
114
|
+
const lastEvent = batch.at(-1);
|
|
115
|
+
this.lastSyncCursor = lastEvent.eventId;
|
|
116
|
+
this.journal.clear(lastEvent.eventId);
|
|
117
|
+
}
|
|
118
|
+
// If we processed less than a full batch or none were accepted, we're done
|
|
119
|
+
if (batch.length < BATCH_SIZE || response.accepted === 0)
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
if (totalSynced > 0) {
|
|
123
|
+
this.onSyncComplete?.(totalSynced);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
this.onSyncError?.(error instanceof Error ? error : new Error(String(error)));
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
this.syncing = false;
|
|
131
|
+
}
|
|
132
|
+
return totalSynced;
|
|
133
|
+
}
|
|
134
|
+
async syncWithRetry(request) {
|
|
135
|
+
let lastError;
|
|
136
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
137
|
+
if (attempt > 0) {
|
|
138
|
+
const delay = Math.min(BACKOFF_BASE_MS * Math.pow(2, attempt - 1), BACKOFF_MAX_MS);
|
|
139
|
+
await sleep(delay);
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
return await this.restClient.syncEvents(request);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw lastError ?? new Error("Sync failed after retries");
|
|
149
|
+
}
|
|
150
|
+
getPendingCount() {
|
|
151
|
+
return this.journal.pendingCount();
|
|
152
|
+
}
|
|
153
|
+
getLastSyncCursor() {
|
|
154
|
+
return this.lastSyncCursor;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function sleep(ms) {
|
|
158
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
159
|
+
}
|
|
160
|
+
//# sourceMappingURL=resilient-emitter.js.map
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import type { HeartbeatRequest, HeartbeatResponse, SyncRequest, SyncResponse, TaskPostBody, TaskResponse } from "@getforgeai/protocol";
|
|
2
|
+
type RestClientOptions = {
|
|
3
|
+
timeoutMs?: number;
|
|
4
|
+
maxRetries?: number;
|
|
5
|
+
retryDelayMs?: number;
|
|
6
|
+
};
|
|
7
|
+
type RegisterCliRequest = {
|
|
8
|
+
cliId: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
agentType?: string;
|
|
11
|
+
agentSlots?: number;
|
|
12
|
+
};
|
|
13
|
+
type RegisterCliResponse = {
|
|
14
|
+
id: string;
|
|
15
|
+
cliId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
memberId: string;
|
|
18
|
+
agentType: string;
|
|
19
|
+
agentSlots: number;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
};
|
|
22
|
+
type CliDeviceItem = {
|
|
23
|
+
id: string;
|
|
24
|
+
cliId: string;
|
|
25
|
+
name: string;
|
|
26
|
+
memberId: string;
|
|
27
|
+
memberName: string;
|
|
28
|
+
memberEmail: string;
|
|
29
|
+
agentType: string;
|
|
30
|
+
agentSlots: number;
|
|
31
|
+
lastSeenAt: string | null;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
};
|
|
34
|
+
type ListCliDevicesResponse = {
|
|
35
|
+
devices: CliDeviceItem[];
|
|
36
|
+
};
|
|
37
|
+
type ProjectState = {
|
|
38
|
+
tasks: unknown[];
|
|
39
|
+
tasksAvailable: boolean;
|
|
40
|
+
agents: unknown[];
|
|
41
|
+
agentsAvailable: boolean;
|
|
42
|
+
clis: {
|
|
43
|
+
cliId: string;
|
|
44
|
+
name: string;
|
|
45
|
+
status: string;
|
|
46
|
+
agentSlots: number;
|
|
47
|
+
lastSeenAt: string | null;
|
|
48
|
+
}[];
|
|
49
|
+
lastSyncTimestamp: string;
|
|
50
|
+
projectId: string | null;
|
|
51
|
+
};
|
|
52
|
+
type UpdateCliConfigRequest = {
|
|
53
|
+
cliId: string;
|
|
54
|
+
name?: string;
|
|
55
|
+
agentType?: string;
|
|
56
|
+
agentSlots?: number;
|
|
57
|
+
};
|
|
58
|
+
type TaskDetail = {
|
|
59
|
+
id: string;
|
|
60
|
+
title: string;
|
|
61
|
+
description: string;
|
|
62
|
+
completed: boolean;
|
|
63
|
+
kanbanSubStatus: string | null;
|
|
64
|
+
kanbanColumn: string | null;
|
|
65
|
+
assigneeId?: string | null;
|
|
66
|
+
cliDeviceId?: string | null;
|
|
67
|
+
taskBranch?: string | null;
|
|
68
|
+
project?: {
|
|
69
|
+
slug: string;
|
|
70
|
+
repoUrl: string | null;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
type WorkflowDetail = {
|
|
74
|
+
id: string;
|
|
75
|
+
name: string;
|
|
76
|
+
slug: string;
|
|
77
|
+
status: string;
|
|
78
|
+
baseBranch: string | null;
|
|
79
|
+
sourceBranch: string | null;
|
|
80
|
+
project: {
|
|
81
|
+
slug: string;
|
|
82
|
+
repoUrl: string | null;
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
type ReleaseDetail = {
|
|
86
|
+
id: string;
|
|
87
|
+
version: string;
|
|
88
|
+
releaseBranch: string | null;
|
|
89
|
+
targetBranch: string;
|
|
90
|
+
project: {
|
|
91
|
+
slug: string;
|
|
92
|
+
repoUrl: string | null;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
type TaskRequeueResult = {
|
|
96
|
+
allowed: boolean;
|
|
97
|
+
count: number;
|
|
98
|
+
max: number;
|
|
99
|
+
reason?: string;
|
|
100
|
+
};
|
|
101
|
+
type TaskAdvanceResult = {
|
|
102
|
+
advanced: boolean;
|
|
103
|
+
previousColumn?: string;
|
|
104
|
+
newColumn?: string;
|
|
105
|
+
newSubStatus?: string;
|
|
106
|
+
terminal?: boolean;
|
|
107
|
+
released?: boolean;
|
|
108
|
+
column?: string;
|
|
109
|
+
};
|
|
110
|
+
type RestClient = {
|
|
111
|
+
heartbeat: (data: HeartbeatRequest) => Promise<HeartbeatResponse>;
|
|
112
|
+
getTasks: () => Promise<TaskResponse>;
|
|
113
|
+
getTask: (taskId: string) => Promise<TaskDetail>;
|
|
114
|
+
/** @deprecated Use updateTaskSubStatus instead */
|
|
115
|
+
updateTaskStatus: (data: TaskPostBody) => Promise<{
|
|
116
|
+
status: string;
|
|
117
|
+
taskId: string;
|
|
118
|
+
}>;
|
|
119
|
+
updateTaskSubStatus: (data: {
|
|
120
|
+
taskId: string;
|
|
121
|
+
kanbanSubStatus: string;
|
|
122
|
+
agentQuestion?: string;
|
|
123
|
+
}) => Promise<{
|
|
124
|
+
status: string;
|
|
125
|
+
taskId: string;
|
|
126
|
+
}>;
|
|
127
|
+
requeueTask: (taskId: string, reason?: string, errorReason?: string, resetAt?: string) => Promise<TaskRequeueResult>;
|
|
128
|
+
advanceTask: (taskId: string, options?: {
|
|
129
|
+
fallback?: boolean;
|
|
130
|
+
}) => Promise<TaskAdvanceResult>;
|
|
131
|
+
respondToAgent: (taskId: string, response: string) => Promise<{
|
|
132
|
+
success: boolean;
|
|
133
|
+
}>;
|
|
134
|
+
syncEvents: (data: SyncRequest) => Promise<SyncResponse>;
|
|
135
|
+
registerCli: (data: RegisterCliRequest) => Promise<RegisterCliResponse>;
|
|
136
|
+
updateCliConfig: (data: UpdateCliConfigRequest) => Promise<RegisterCliResponse>;
|
|
137
|
+
listCliDevices: () => Promise<ListCliDevicesResponse>;
|
|
138
|
+
unlinkCli: (data: {
|
|
139
|
+
cliId: string;
|
|
140
|
+
}) => Promise<{
|
|
141
|
+
status: string;
|
|
142
|
+
}>;
|
|
143
|
+
disconnectCli: (data: {
|
|
144
|
+
cliId: string;
|
|
145
|
+
}) => Promise<{
|
|
146
|
+
status: string;
|
|
147
|
+
}>;
|
|
148
|
+
getProjectState: (projectId?: string) => Promise<ProjectState>;
|
|
149
|
+
getSkill: (skillName: string) => Promise<unknown>;
|
|
150
|
+
getWorkflow: (workflowId: string) => Promise<WorkflowDetail>;
|
|
151
|
+
getRelease: (releaseId: string) => Promise<ReleaseDetail>;
|
|
152
|
+
};
|
|
153
|
+
export declare function createRestClient(serverUrl: string, token: string, orgId: string, options?: RestClientOptions): RestClient;
|
|
154
|
+
type OrgSelectionResponse = {
|
|
155
|
+
orgIds: string[];
|
|
156
|
+
orgSlugs: string[];
|
|
157
|
+
};
|
|
158
|
+
export declare function getOrgSelection(serverUrl: string, token: string, userCode: string): Promise<OrgSelectionResponse>;
|
|
159
|
+
export declare class RestClientError extends Error {
|
|
160
|
+
status: number;
|
|
161
|
+
constructor(message: string, status: number);
|
|
162
|
+
}
|
|
163
|
+
export {};
|
|
164
|
+
//# sourceMappingURL=rest-client.d.ts.map
|