git-daemon 0.1.1 → 0.1.3

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/config.js ADDED
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.saveConfig = exports.loadConfig = exports.ensureDir = exports.defaultConfig = exports.getTokensPath = exports.getConfigPath = exports.getConfigDir = void 0;
7
+ const env_paths_1 = __importDefault(require("env-paths"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = require("fs");
10
+ const CONFIG_VERSION = 1;
11
+ const CONFIG_FILE = "config.json";
12
+ const TOKENS_FILE = "tokens.json";
13
+ const getConfigDir = () => {
14
+ const override = process.env.GIT_DAEMON_CONFIG_DIR;
15
+ if (override) {
16
+ return override;
17
+ }
18
+ const paths = (0, env_paths_1.default)("Git Daemon", { suffix: "" });
19
+ return paths.config;
20
+ };
21
+ exports.getConfigDir = getConfigDir;
22
+ const getConfigPath = (configDir) => path_1.default.join(configDir, CONFIG_FILE);
23
+ exports.getConfigPath = getConfigPath;
24
+ const getTokensPath = (configDir) => path_1.default.join(configDir, TOKENS_FILE);
25
+ exports.getTokensPath = getTokensPath;
26
+ const defaultConfig = () => ({
27
+ configVersion: CONFIG_VERSION,
28
+ server: {
29
+ host: "127.0.0.1",
30
+ port: 8790,
31
+ },
32
+ originAllowlist: ["https://app.example.com"],
33
+ workspaceRoot: null,
34
+ pairing: {
35
+ tokenTtlDays: 30,
36
+ },
37
+ jobs: {
38
+ maxConcurrent: 1,
39
+ timeoutSeconds: 3600,
40
+ },
41
+ deps: {
42
+ defaultSafer: true,
43
+ },
44
+ logging: {
45
+ directory: "logs",
46
+ maxFiles: 5,
47
+ maxBytes: 5 * 1024 * 1024,
48
+ },
49
+ approvals: {
50
+ entries: [],
51
+ },
52
+ });
53
+ exports.defaultConfig = defaultConfig;
54
+ const ensureDir = async (dir) => {
55
+ await fs_1.promises.mkdir(dir, { recursive: true });
56
+ };
57
+ exports.ensureDir = ensureDir;
58
+ const loadConfig = async (configDir) => {
59
+ await (0, exports.ensureDir)(configDir);
60
+ const configPath = (0, exports.getConfigPath)(configDir);
61
+ try {
62
+ const raw = await fs_1.promises.readFile(configPath, "utf8");
63
+ const data = JSON.parse(raw);
64
+ return {
65
+ ...(0, exports.defaultConfig)(),
66
+ ...data,
67
+ server: {
68
+ ...(0, exports.defaultConfig)().server,
69
+ ...data.server,
70
+ },
71
+ pairing: {
72
+ ...(0, exports.defaultConfig)().pairing,
73
+ ...data.pairing,
74
+ },
75
+ jobs: {
76
+ ...(0, exports.defaultConfig)().jobs,
77
+ ...data.jobs,
78
+ },
79
+ deps: {
80
+ ...(0, exports.defaultConfig)().deps,
81
+ ...data.deps,
82
+ },
83
+ logging: {
84
+ ...(0, exports.defaultConfig)().logging,
85
+ ...data.logging,
86
+ },
87
+ approvals: {
88
+ entries: data.approvals?.entries ?? [],
89
+ },
90
+ };
91
+ }
92
+ catch (err) {
93
+ if (err.code !== "ENOENT") {
94
+ throw err;
95
+ }
96
+ const config = (0, exports.defaultConfig)();
97
+ await (0, exports.saveConfig)(configDir, config);
98
+ return config;
99
+ }
100
+ };
101
+ exports.loadConfig = loadConfig;
102
+ const saveConfig = async (configDir, config) => {
103
+ const configPath = (0, exports.getConfigPath)(configDir);
104
+ await (0, exports.ensureDir)(configDir);
105
+ await fs_1.promises.writeFile(configPath, JSON.stringify(config, null, 2));
106
+ };
107
+ exports.saveConfig = saveConfig;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createContext = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = require("fs");
9
+ const logger_1 = require("./logger");
10
+ const tools_1 = require("./tools");
11
+ const config_1 = require("./config");
12
+ const tokens_1 = require("./tokens");
13
+ const pairing_1 = require("./pairing");
14
+ const jobs_1 = require("./jobs");
15
+ const readPackageVersion = async () => {
16
+ try {
17
+ const pkgPath = path_1.default.resolve(__dirname, "..", "package.json");
18
+ const raw = await fs_1.promises.readFile(pkgPath, "utf8");
19
+ const parsed = JSON.parse(raw);
20
+ return parsed.version ?? "0.0.0";
21
+ }
22
+ catch {
23
+ return "0.0.0";
24
+ }
25
+ };
26
+ const createContext = async () => {
27
+ const configDir = (0, config_1.getConfigDir)();
28
+ const config = await (0, config_1.loadConfig)(configDir);
29
+ if (!config.originAllowlist.length) {
30
+ throw new Error("originAllowlist must contain at least one entry.");
31
+ }
32
+ if (config.server.host !== "127.0.0.1") {
33
+ throw new Error("Server host must be 127.0.0.1 for loopback-only binding.");
34
+ }
35
+ const tokenStore = new tokens_1.TokenStore(configDir);
36
+ await tokenStore.load();
37
+ const logger = await (0, logger_1.createLogger)(configDir, config.logging);
38
+ const capabilities = await (0, tools_1.detectCapabilities)();
39
+ const pairingManager = new pairing_1.PairingManager(tokenStore, config.pairing.tokenTtlDays);
40
+ const jobManager = new jobs_1.JobManager(config.jobs.maxConcurrent, config.jobs.timeoutSeconds);
41
+ const version = await readPackageVersion();
42
+ const build = {
43
+ commit: process.env.GIT_DAEMON_BUILD_COMMIT,
44
+ date: process.env.GIT_DAEMON_BUILD_DATE,
45
+ };
46
+ return {
47
+ config,
48
+ configDir,
49
+ tokenStore,
50
+ pairingManager,
51
+ jobManager,
52
+ capabilities,
53
+ logger,
54
+ version,
55
+ build,
56
+ };
57
+ };
58
+ exports.createContext = createContext;
package/dist/daemon.js ADDED
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const context_1 = require("./context");
4
+ const app_1 = require("./app");
5
+ const start = async () => {
6
+ const ctx = await (0, context_1.createContext)();
7
+ const app = (0, app_1.createApp)(ctx);
8
+ const startupSummary = {
9
+ configDir: ctx.configDir,
10
+ host: ctx.config.server.host,
11
+ port: ctx.config.server.port,
12
+ workspaceRoot: ctx.config.workspaceRoot ?? "not configured",
13
+ originAllowlist: ctx.config.originAllowlist,
14
+ };
15
+ ctx.logger.info(startupSummary, "Git Daemon starting");
16
+ if (process.env.GIT_DAEMON_LOG_STDOUT !== "1") {
17
+ console.log("[Git Daemon] Startup");
18
+ console.log(` config: ${startupSummary.configDir}`);
19
+ console.log(` host: ${startupSummary.host}`);
20
+ console.log(` port: ${startupSummary.port}`);
21
+ console.log(` workspace: ${startupSummary.workspaceRoot}`);
22
+ console.log(` allowlist: ${startupSummary.originAllowlist.join(", ") || "none"}`);
23
+ }
24
+ app.listen(ctx.config.server.port, ctx.config.server.host, () => {
25
+ ctx.logger.info({
26
+ host: ctx.config.server.host,
27
+ port: ctx.config.server.port,
28
+ }, "Git Daemon listening");
29
+ });
30
+ };
31
+ start().catch((err) => {
32
+ console.error("Failed to start Git Daemon", err);
33
+ process.exit(1);
34
+ });
package/dist/deps.js ADDED
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.installDeps = void 0;
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = require("fs");
9
+ const process_1 = require("./process");
10
+ const workspace_1 = require("./workspace");
11
+ const tools_1 = require("./tools");
12
+ const installDeps = async (ctx, workspaceRoot, request) => {
13
+ const repoPath = await (0, workspace_1.resolveInsideWorkspace)(workspaceRoot, request.repoPath);
14
+ await fs_1.promises.access(path_1.default.join(repoPath, "package.json"));
15
+ const manager = await selectManager(repoPath, request.manager);
16
+ const { command, args } = await buildInstallCommand(repoPath, manager, request.mode, request.safer);
17
+ ctx.progress({ kind: "deps", detail: `${command} ${args.join(" ")}` });
18
+ await (0, process_1.runCommand)(ctx, command, args, { cwd: repoPath });
19
+ };
20
+ exports.installDeps = installDeps;
21
+ const selectManager = async (repoPath, requested) => {
22
+ if (requested !== "auto") {
23
+ const installed = await (0, tools_1.isToolInstalled)(requested);
24
+ if (!installed) {
25
+ throw new Error(`${requested} is not installed.`);
26
+ }
27
+ return requested;
28
+ }
29
+ const packageManager = await readPackageManager(repoPath);
30
+ if (packageManager) {
31
+ const name = packageManager.split("@")[0];
32
+ if (name === "pnpm" || name === "yarn" || name === "npm") {
33
+ const installed = await (0, tools_1.isToolInstalled)(name);
34
+ if (installed) {
35
+ return name;
36
+ }
37
+ }
38
+ }
39
+ if (await fileExists(path_1.default.join(repoPath, "pnpm-lock.yaml"))) {
40
+ return "pnpm";
41
+ }
42
+ if (await fileExists(path_1.default.join(repoPath, "yarn.lock"))) {
43
+ return "yarn";
44
+ }
45
+ if (await fileExists(path_1.default.join(repoPath, "package-lock.json"))) {
46
+ return "npm";
47
+ }
48
+ return "npm";
49
+ };
50
+ const readPackageManager = async (repoPath) => {
51
+ try {
52
+ const raw = await fs_1.promises.readFile(path_1.default.join(repoPath, "package.json"), "utf8");
53
+ const parsed = JSON.parse(raw);
54
+ return parsed.packageManager;
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ };
60
+ const buildInstallCommand = async (repoPath, manager, mode, safer) => {
61
+ const lockfileExists = await hasAnyLockfile(repoPath);
62
+ const useCi = mode === "ci" || (mode === "auto" && lockfileExists);
63
+ if (manager === "pnpm") {
64
+ const args = ["install"];
65
+ if (useCi) {
66
+ args.push("--frozen-lockfile");
67
+ }
68
+ if (safer) {
69
+ args.push("--ignore-scripts");
70
+ }
71
+ return { command: "pnpm", args };
72
+ }
73
+ if (manager === "yarn") {
74
+ const args = ["install"];
75
+ const isBerry = await fileExists(path_1.default.join(repoPath, ".yarnrc.yml"));
76
+ if (useCi || isBerry) {
77
+ args.push("--immutable");
78
+ }
79
+ if (safer) {
80
+ args.push("--ignore-scripts");
81
+ }
82
+ return { command: "yarn", args };
83
+ }
84
+ const args = [useCi ? "ci" : "install"];
85
+ if (safer) {
86
+ args.push("--ignore-scripts");
87
+ }
88
+ return { command: "npm", args };
89
+ };
90
+ const hasAnyLockfile = async (repoPath) => (await fileExists(path_1.default.join(repoPath, "pnpm-lock.yaml"))) ||
91
+ (await fileExists(path_1.default.join(repoPath, "yarn.lock"))) ||
92
+ (await fileExists(path_1.default.join(repoPath, "package-lock.json")));
93
+ const fileExists = async (target) => {
94
+ try {
95
+ await fs_1.promises.access(target);
96
+ return true;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ };
package/dist/errors.js ADDED
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.internalError = exports.timeoutError = exports.pathNotFound = exports.repoNotFound = exports.jobNotFound = exports.capabilityNotGranted = exports.invalidRepoUrl = exports.pathOutsideWorkspace = exports.workspaceRequired = exports.rateLimited = exports.originNotAllowed = exports.authInvalid = exports.authRequired = exports.errorBody = exports.ApiError = void 0;
4
+ class ApiError extends Error {
5
+ constructor(status, body) {
6
+ super(body.message);
7
+ this.status = status;
8
+ this.body = body;
9
+ }
10
+ }
11
+ exports.ApiError = ApiError;
12
+ const errorBody = (errorCode, message, details) => ({
13
+ errorCode,
14
+ message,
15
+ ...(details ? { details } : {}),
16
+ });
17
+ exports.errorBody = errorBody;
18
+ const authRequired = () => new ApiError(401, (0, exports.errorBody)("auth_required", "Bearer token required."));
19
+ exports.authRequired = authRequired;
20
+ const authInvalid = () => new ApiError(401, (0, exports.errorBody)("auth_invalid", "Bearer token invalid or expired."));
21
+ exports.authInvalid = authInvalid;
22
+ const originNotAllowed = () => new ApiError(403, (0, exports.errorBody)("origin_not_allowed", "Origin not allowed."));
23
+ exports.originNotAllowed = originNotAllowed;
24
+ const rateLimited = () => new ApiError(429, (0, exports.errorBody)("rate_limited", "Too many requests."));
25
+ exports.rateLimited = rateLimited;
26
+ const workspaceRequired = () => new ApiError(409, (0, exports.errorBody)("workspace_required", "Workspace root not configured."));
27
+ exports.workspaceRequired = workspaceRequired;
28
+ const pathOutsideWorkspace = () => new ApiError(409, (0, exports.errorBody)("path_outside_workspace", "Path is outside the workspace root."));
29
+ exports.pathOutsideWorkspace = pathOutsideWorkspace;
30
+ const invalidRepoUrl = () => new ApiError(422, (0, exports.errorBody)("invalid_repo_url", "Repository URL is invalid."));
31
+ exports.invalidRepoUrl = invalidRepoUrl;
32
+ const capabilityNotGranted = () => new ApiError(409, (0, exports.errorBody)("capability_not_granted", "Capability approval required."));
33
+ exports.capabilityNotGranted = capabilityNotGranted;
34
+ const jobNotFound = () => new ApiError(404, (0, exports.errorBody)("job_not_found", "Job not found."));
35
+ exports.jobNotFound = jobNotFound;
36
+ const repoNotFound = () => new ApiError(404, (0, exports.errorBody)("internal_error", "Repository not found."));
37
+ exports.repoNotFound = repoNotFound;
38
+ const pathNotFound = () => new ApiError(404, (0, exports.errorBody)("internal_error", "Path not found."));
39
+ exports.pathNotFound = pathNotFound;
40
+ const timeoutError = () => new ApiError(500, (0, exports.errorBody)("timeout", "Job timed out."));
41
+ exports.timeoutError = timeoutError;
42
+ const internalError = (message = "Unexpected error.") => new ApiError(500, (0, exports.errorBody)("internal_error", message));
43
+ exports.internalError = internalError;
package/dist/git.js ADDED
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.resolveRepoPath = exports.getRepoStatus = exports.fetchRepo = exports.cloneRepo = exports.RepoNotFoundError = void 0;
40
+ const path_1 = __importDefault(require("path"));
41
+ const fs_1 = require("fs");
42
+ const process_1 = require("./process");
43
+ const workspace_1 = require("./workspace");
44
+ class RepoNotFoundError extends Error {
45
+ }
46
+ exports.RepoNotFoundError = RepoNotFoundError;
47
+ const cloneRepo = async (ctx, workspaceRoot, repoUrl, destRelative, options) => {
48
+ (0, workspace_1.ensureRelative)(destRelative);
49
+ const destPath = await (0, workspace_1.resolveInsideWorkspace)(workspaceRoot, destRelative, true);
50
+ const args = ["clone", repoUrl, destPath];
51
+ if (options?.branch) {
52
+ args.splice(1, 0, "--branch", options.branch);
53
+ }
54
+ if (options?.depth) {
55
+ args.splice(1, 0, "--depth", options.depth.toString());
56
+ }
57
+ await (0, process_1.runCommand)(ctx, "git", args, { cwd: workspaceRoot });
58
+ };
59
+ exports.cloneRepo = cloneRepo;
60
+ const fetchRepo = async (ctx, workspaceRoot, repoPath, remote = "origin", prune = false) => {
61
+ const resolved = await (0, exports.resolveRepoPath)(workspaceRoot, repoPath);
62
+ const args = ["-C", resolved, "fetch", remote];
63
+ if (prune) {
64
+ args.push("--prune");
65
+ }
66
+ await (0, process_1.runCommand)(ctx, "git", args);
67
+ };
68
+ exports.fetchRepo = fetchRepo;
69
+ const getRepoStatus = async (workspaceRoot, repoPath) => {
70
+ const resolved = await (0, exports.resolveRepoPath)(workspaceRoot, repoPath);
71
+ const { execa } = await Promise.resolve().then(() => __importStar(require("execa")));
72
+ const result = await execa("git", [
73
+ "-C",
74
+ resolved,
75
+ "status",
76
+ "--porcelain=2",
77
+ "-b",
78
+ ]);
79
+ return parseStatus(result.stdout);
80
+ };
81
+ exports.getRepoStatus = getRepoStatus;
82
+ const resolveRepoPath = async (workspaceRoot, repoPath) => {
83
+ const resolved = await (0, workspace_1.resolveInsideWorkspace)(workspaceRoot, repoPath);
84
+ await assertRepoExists(resolved);
85
+ return resolved;
86
+ };
87
+ exports.resolveRepoPath = resolveRepoPath;
88
+ const assertRepoExists = async (repoPath) => {
89
+ const stats = await fs_1.promises.stat(repoPath);
90
+ if (!stats.isDirectory()) {
91
+ throw new RepoNotFoundError("Repository path is not a directory.");
92
+ }
93
+ const gitPath = path_1.default.join(repoPath, ".git");
94
+ try {
95
+ await fs_1.promises.access(gitPath);
96
+ }
97
+ catch {
98
+ throw new RepoNotFoundError("Repository .git directory not found.");
99
+ }
100
+ };
101
+ const parseStatus = (output) => {
102
+ let branch = "";
103
+ let ahead = 0;
104
+ let behind = 0;
105
+ let stagedCount = 0;
106
+ let unstagedCount = 0;
107
+ let untrackedCount = 0;
108
+ let conflictsCount = 0;
109
+ const lines = output.split(/\r?\n/);
110
+ for (const line of lines) {
111
+ if (line.startsWith("# branch.head")) {
112
+ branch = line.split(" ").slice(2).join(" ").trim();
113
+ continue;
114
+ }
115
+ if (line.startsWith("# branch.ab")) {
116
+ const parts = line.split(" ");
117
+ const aheadPart = parts.find((part) => part.startsWith("+"));
118
+ const behindPart = parts.find((part) => part.startsWith("-"));
119
+ ahead = aheadPart ? Number(aheadPart.slice(1)) : 0;
120
+ behind = behindPart ? Number(behindPart.slice(1)) : 0;
121
+ continue;
122
+ }
123
+ if (line.startsWith("?")) {
124
+ untrackedCount += 1;
125
+ continue;
126
+ }
127
+ if (line.startsWith("u")) {
128
+ conflictsCount += 1;
129
+ continue;
130
+ }
131
+ if (line.startsWith("1 ") || line.startsWith("2 ")) {
132
+ const x = line[2];
133
+ const y = line[3];
134
+ if (x && x !== ".") {
135
+ stagedCount += 1;
136
+ }
137
+ if (y && y !== ".") {
138
+ unstagedCount += 1;
139
+ }
140
+ }
141
+ }
142
+ const clean = stagedCount === 0 &&
143
+ unstagedCount === 0 &&
144
+ untrackedCount === 0 &&
145
+ conflictsCount === 0;
146
+ return {
147
+ branch,
148
+ ahead,
149
+ behind,
150
+ stagedCount,
151
+ unstagedCount,
152
+ untrackedCount,
153
+ conflictsCount,
154
+ clean,
155
+ };
156
+ };
package/dist/jobs.js ADDED
@@ -0,0 +1,163 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.JobManager = exports.Job = void 0;
7
+ const events_1 = require("events");
8
+ const crypto_1 = __importDefault(require("crypto"));
9
+ const errors_1 = require("./errors");
10
+ const MAX_EVENTS = 2000;
11
+ class Job {
12
+ constructor() {
13
+ this.state = "queued";
14
+ this.createdAt = new Date().toISOString();
15
+ this.events = [];
16
+ this.emitter = new events_1.EventEmitter();
17
+ this.cancelRequested = false;
18
+ this.id = crypto_1.default.randomUUID();
19
+ }
20
+ setCancel(fn) {
21
+ this.cancelFn = fn;
22
+ }
23
+ async cancel() {
24
+ this.cancelRequested = true;
25
+ if (this.cancelFn) {
26
+ await this.cancelFn();
27
+ }
28
+ }
29
+ emit(event) {
30
+ this.events.push(event);
31
+ if (this.events.length > MAX_EVENTS) {
32
+ this.events.shift();
33
+ }
34
+ this.emitter.emit("event", event);
35
+ }
36
+ snapshot() {
37
+ return {
38
+ id: this.id,
39
+ state: this.state,
40
+ createdAt: this.createdAt,
41
+ startedAt: this.startedAt,
42
+ finishedAt: this.finishedAt,
43
+ error: this.error,
44
+ };
45
+ }
46
+ }
47
+ exports.Job = Job;
48
+ class JobManager {
49
+ constructor(maxConcurrent, timeoutSeconds) {
50
+ this.running = 0;
51
+ this.queue = [];
52
+ this.jobs = new Map();
53
+ this.history = [];
54
+ this.maxConcurrent = maxConcurrent;
55
+ this.timeoutMs = timeoutSeconds * 1000;
56
+ }
57
+ enqueue(run) {
58
+ const job = new Job();
59
+ this.jobs.set(job.id, job);
60
+ this.queue.push({ job, run });
61
+ this.track(job);
62
+ this.drain();
63
+ return job;
64
+ }
65
+ get(id) {
66
+ return this.jobs.get(id);
67
+ }
68
+ cancel(id) {
69
+ const queuedIndex = this.queue.findIndex((entry) => entry.job.id === id);
70
+ if (queuedIndex >= 0) {
71
+ const [entry] = this.queue.splice(queuedIndex, 1);
72
+ entry.job.state = "cancelled";
73
+ entry.job.finishedAt = new Date().toISOString();
74
+ entry.job.emit({ type: "state", state: "cancelled" });
75
+ return true;
76
+ }
77
+ const runningJob = this.jobs.get(id);
78
+ if (!runningJob) {
79
+ return false;
80
+ }
81
+ if (runningJob.state !== "running") {
82
+ return false;
83
+ }
84
+ void runningJob.cancel();
85
+ runningJob.state = "cancelled";
86
+ runningJob.finishedAt = new Date().toISOString();
87
+ runningJob.emit({ type: "state", state: "cancelled" });
88
+ return true;
89
+ }
90
+ listRecent() {
91
+ return this.history.map((job) => job.snapshot());
92
+ }
93
+ track(job) {
94
+ this.history.push(job);
95
+ if (this.history.length > 100) {
96
+ this.history.shift();
97
+ }
98
+ }
99
+ drain() {
100
+ while (this.running < this.maxConcurrent && this.queue.length > 0) {
101
+ const entry = this.queue.shift();
102
+ if (!entry) {
103
+ return;
104
+ }
105
+ this.runJob(entry.job, entry.run);
106
+ }
107
+ }
108
+ runJob(job, run) {
109
+ this.running += 1;
110
+ job.state = "running";
111
+ job.startedAt = new Date().toISOString();
112
+ job.emit({ type: "state", state: "running" });
113
+ let timeoutHandle;
114
+ if (this.timeoutMs > 0) {
115
+ timeoutHandle = setTimeout(async () => {
116
+ if (job.state !== "running") {
117
+ return;
118
+ }
119
+ job.error = (0, errors_1.timeoutError)().body;
120
+ await job.cancel();
121
+ job.state = "error";
122
+ job.finishedAt = new Date().toISOString();
123
+ job.emit({ type: "state", state: "error", message: "Timed out" });
124
+ }, this.timeoutMs);
125
+ }
126
+ const ctx = {
127
+ logStdout: (line) => job.emit({ type: "log", stream: "stdout", line }),
128
+ logStderr: (line) => job.emit({ type: "log", stream: "stderr", line }),
129
+ progress: (event) => job.emit({ type: "progress", ...event }),
130
+ setCancel: (fn) => job.setCancel(fn),
131
+ isCancelled: () => job.cancelRequested,
132
+ };
133
+ run(ctx)
134
+ .then(() => {
135
+ if (job.state !== "running") {
136
+ return;
137
+ }
138
+ job.state = "done";
139
+ job.finishedAt = new Date().toISOString();
140
+ job.emit({ type: "state", state: "done" });
141
+ })
142
+ .catch((err) => {
143
+ if (job.state !== "running") {
144
+ return;
145
+ }
146
+ job.state = "error";
147
+ job.finishedAt = new Date().toISOString();
148
+ job.error = {
149
+ errorCode: "internal_error",
150
+ message: err instanceof Error ? err.message : "Job failed.",
151
+ };
152
+ job.emit({ type: "state", state: "error", message: job.error.message });
153
+ })
154
+ .finally(() => {
155
+ if (timeoutHandle) {
156
+ clearTimeout(timeoutHandle);
157
+ }
158
+ this.running -= 1;
159
+ this.drain();
160
+ });
161
+ }
162
+ }
163
+ exports.JobManager = JobManager;