refactor: split renderer ipc and unraid domains
This commit is contained in:
+14
-744
@@ -1,5 +1,4 @@
|
||||
"use strict";
|
||||
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs/promises");
|
||||
const { fileURLToPath } = require("node:url");
|
||||
@@ -9,12 +8,14 @@ const {
|
||||
cloneDirectoryName,
|
||||
resolveCloneTarget,
|
||||
} = require("../shared/clone-target.cjs");
|
||||
const { registerRepositoryIpc } = require("./ipc/repository-handlers.cjs");
|
||||
const { registerDeploymentIpc } = require("./ipc/deployment-handlers.cjs");
|
||||
const { registerOperationsIpc } = require("./ipc/operations-handlers.cjs");
|
||||
const {
|
||||
createEncryptedBackup,
|
||||
readEncryptedBackup,
|
||||
} = require("./configuration-backup.cjs");
|
||||
const { evaluateDeploymentPolicy } = require("../shared/deployment-policy.cjs");
|
||||
|
||||
let diagnosticsService = null;
|
||||
const TRUSTED_RENDERER_PATH = path.resolve(
|
||||
__dirname,
|
||||
@@ -76,7 +77,6 @@ function register(channel, handler) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerIpc({
|
||||
store,
|
||||
git,
|
||||
@@ -502,383 +502,11 @@ function registerIpc({
|
||||
}),
|
||||
);
|
||||
|
||||
register("repositories:refresh", async () => {
|
||||
const result = await repositories.refresh();
|
||||
monitor?.setPaths(repositories.getWatchPaths());
|
||||
return result;
|
||||
});
|
||||
|
||||
register("repositories:discover", async ({ roots }) => {
|
||||
const paths = await repositories.discoverAll(
|
||||
roots || store.data.workspaceRoots,
|
||||
);
|
||||
return repositories.getLocalDescriptors(paths);
|
||||
});
|
||||
|
||||
register("repository:favorite", async ({ fullName, favorite }) =>
|
||||
store.setFavorite(fullName, favorite),
|
||||
);
|
||||
|
||||
register("repository:link", async ({ fullName, localPath }) => {
|
||||
await git.ensureRepository(localPath);
|
||||
const remoteUrl = await git.getRemoteUrl(localPath).catch(() => "");
|
||||
if (
|
||||
!remoteUrl ||
|
||||
!matchRemoteToRepository(remoteUrl, [{ full_name: fullName }])
|
||||
) {
|
||||
throw new Error(
|
||||
`The selected folder's origin does not match ${fullName}.`,
|
||||
);
|
||||
}
|
||||
await store.saveMapping(fullName, localPath);
|
||||
await diagnostics.info("repository.linked", { fullName, localPath });
|
||||
const result = await repositories.refresh();
|
||||
monitor?.setPaths(repositories.getWatchPaths());
|
||||
return result;
|
||||
});
|
||||
|
||||
register("repository:unlink", async ({ fullName }) => {
|
||||
await store.removeMapping(fullName);
|
||||
await diagnostics.info("repository.unlinked", { fullName });
|
||||
const result = await repositories.refresh();
|
||||
monitor?.setPaths(repositories.getWatchPaths());
|
||||
return result;
|
||||
});
|
||||
|
||||
register("repository:status", async ({ localPath }) =>
|
||||
git.status(await assertKnownRepositoryPath(localPath)),
|
||||
);
|
||||
register("repository:diff", async ({ localPath, filePath, staged }) =>
|
||||
git.diff(await assertKnownRepositoryPath(localPath), filePath, staged),
|
||||
);
|
||||
register("repository:diff-hunks", async ({ localPath, filePath }) =>
|
||||
git.diffHunks(await assertKnownRepositoryPath(localPath), filePath),
|
||||
);
|
||||
register(
|
||||
"repository:stage-hunks",
|
||||
async ({ localPath, filePath, hunkIndexes }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.stageHunks(safePath, filePath, hunkIndexes),
|
||||
);
|
||||
},
|
||||
);
|
||||
register("repository:conflicts", async ({ localPath }) =>
|
||||
git.conflictState(await assertKnownRepositoryPath(localPath)),
|
||||
);
|
||||
register(
|
||||
"repository:resolve-conflict",
|
||||
async ({ localPath, filePath, resolution }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
const result = await withRepositoryMutation(safePath, () =>
|
||||
git.resolveConflict(safePath, filePath, resolution),
|
||||
);
|
||||
await audit.append("git.conflict.resolved", {
|
||||
localPath: safePath,
|
||||
filePath,
|
||||
resolution,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
);
|
||||
register("repository:continue-operation", async ({ localPath }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
const result = await withRepositoryMutation(safePath, () =>
|
||||
git.continueInterruptedOperation(safePath),
|
||||
);
|
||||
await audit.append("git.operation.continued", { localPath: safePath });
|
||||
return result;
|
||||
});
|
||||
register("repository:abort-operation", async ({ localPath }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
const result = await withRepositoryMutation(safePath, () =>
|
||||
git.abortInterruptedOperation(safePath),
|
||||
);
|
||||
await audit.append("git.operation.aborted", {
|
||||
localPath: safePath,
|
||||
operation: result.aborted,
|
||||
});
|
||||
return result;
|
||||
});
|
||||
register("repository:stage", async ({ localPath, files }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () => git.stage(safePath, files));
|
||||
});
|
||||
register("repository:unstage", async ({ localPath, files }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () => git.unstage(safePath, files));
|
||||
});
|
||||
register("repository:commit", async ({ localPath, message, files }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.commit(safePath, message, files),
|
||||
);
|
||||
});
|
||||
register("repository:commit-staged", async ({ localPath, message }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.commitStaged(safePath, message),
|
||||
);
|
||||
});
|
||||
register("repository:commit-staged-push", async ({ localPath, message }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.commitStagedAndPush(safePath, message),
|
||||
);
|
||||
});
|
||||
register("repository:commit-push", async ({ localPath, message, files }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.commitAndPush(safePath, message, files),
|
||||
);
|
||||
});
|
||||
register("repository:push", async ({ localPath }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () => git.push(safePath));
|
||||
});
|
||||
register("repository:fetch", async ({ localPath }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () => git.fetch(safePath));
|
||||
});
|
||||
register("repository:pull", async ({ localPath }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.pullFastForward(safePath),
|
||||
);
|
||||
});
|
||||
register("repository:history", async ({ localPath, limit }) =>
|
||||
git.history(await assertKnownRepositoryPath(localPath), limit),
|
||||
);
|
||||
register("repository:branch-protection", async ({ fullName, branch }) => {
|
||||
const repository = await resolveRepository({ fullName });
|
||||
return gitea.getBranchProtection(
|
||||
repository.owner.login,
|
||||
repository.name,
|
||||
branch ||
|
||||
repository.localStatus?.branch?.head ||
|
||||
repository.defaultBranch,
|
||||
);
|
||||
});
|
||||
register("repository:pull-requests", async ({ fullName, state = "open" }) => {
|
||||
const repository = await resolveRepository({ fullName });
|
||||
return gitea.listPullRequests({
|
||||
owner: repository.owner.login,
|
||||
repo: repository.name,
|
||||
state,
|
||||
});
|
||||
});
|
||||
register(
|
||||
"repository:create-pull-request",
|
||||
async ({ fullName, title, body, base }) => {
|
||||
const repository = await resolveRepository({ fullName });
|
||||
if (!repository.localPath || !repository.localStatus?.clean)
|
||||
throw new Error(
|
||||
"A clean linked repository is required before creating a pull request.",
|
||||
);
|
||||
const head = repository.localStatus.branch?.head;
|
||||
if (!head || !repository.localStatus.branch?.upstream)
|
||||
throw new Error(
|
||||
"Publish the current branch before creating a pull request.",
|
||||
);
|
||||
if (repository.localStatus.branch.ahead > 0)
|
||||
throw new Error(
|
||||
"Push all local commits before creating a pull request.",
|
||||
);
|
||||
const pullRequest = await gitea.createPullRequest({
|
||||
owner: repository.owner.login,
|
||||
repo: repository.name,
|
||||
head,
|
||||
base: base || repository.defaultBranch,
|
||||
title,
|
||||
body,
|
||||
});
|
||||
await audit.append("pull-request.created", {
|
||||
repository: repository.fullName,
|
||||
number: pullRequest.number,
|
||||
head,
|
||||
base: base || repository.defaultBranch,
|
||||
url: pullRequest.html_url,
|
||||
});
|
||||
return pullRequest;
|
||||
},
|
||||
);
|
||||
register("repository:branches", async ({ localPath }) =>
|
||||
git.branches(await assertKnownRepositoryPath(localPath)),
|
||||
);
|
||||
register("repository:checkout-branch", async ({ localPath, branch }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.checkoutBranch(safePath, branch),
|
||||
);
|
||||
});
|
||||
register("repository:create-branch", async ({ localPath, branch }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.createBranch(safePath, branch),
|
||||
);
|
||||
});
|
||||
register("repository:stash", async ({ localPath, message }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () => git.stash(safePath, message));
|
||||
});
|
||||
register("repository:stash-list", async ({ localPath }) =>
|
||||
git.stashList(await assertKnownRepositoryPath(localPath)),
|
||||
);
|
||||
register("repository:stash-pop", async ({ localPath, ref }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () => git.popStash(safePath, ref));
|
||||
});
|
||||
register("repository:index-lock", async ({ localPath }) =>
|
||||
git.getIndexLockInfo(await assertKnownRepositoryPath(localPath)),
|
||||
);
|
||||
register("repository:git-recovery-status", async ({ localPath }) =>
|
||||
git.reconcile(await assertKnownRepositoryPath(localPath)),
|
||||
);
|
||||
register("repository:repair-index-lock", async ({ localPath }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.removeStaleIndexLock(safePath),
|
||||
);
|
||||
});
|
||||
register(
|
||||
"repository:repair-git-locks",
|
||||
async ({ localPath, force = false }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.repairStaleGitLocks(safePath, {
|
||||
minimumAgeMs: force ? 0 : 10_000,
|
||||
allowWithoutProcessProbe: force === true,
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
register("repository:reconcile", async ({ localPath }) =>
|
||||
git.reconcile(await assertKnownRepositoryPath(localPath)),
|
||||
);
|
||||
register("repository:repair-sync", async ({ localPath, strategy }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.repairSync(safePath, strategy),
|
||||
);
|
||||
});
|
||||
register("repository:set-origin", async ({ localPath, remoteUrl }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
return withRepositoryMutation(safePath, () =>
|
||||
git.setRemoteUrl(safePath, remoteUrl),
|
||||
);
|
||||
});
|
||||
|
||||
register("repositories:normalize-origins", async () => {
|
||||
const current = await repositories.refresh();
|
||||
const changes = [];
|
||||
for (const repository of current) {
|
||||
if (!repository.localPath || !repository.sshUrl) continue;
|
||||
const actual = await git
|
||||
.getRemoteUrl(repository.localPath)
|
||||
.catch(() => "");
|
||||
if (actual === repository.sshUrl) continue;
|
||||
await withRepositoryMutation(repository.localPath, () =>
|
||||
git.setRemoteUrl(repository.localPath, repository.sshUrl),
|
||||
);
|
||||
changes.push({
|
||||
fullName: repository.fullName,
|
||||
previous: actual,
|
||||
next: repository.sshUrl,
|
||||
});
|
||||
}
|
||||
const refreshed = await repositories.refresh();
|
||||
monitor?.setPaths(repositories.getWatchPaths());
|
||||
await diagnostics.info("repositories.origins.normalized", {
|
||||
count: changes.length,
|
||||
changes,
|
||||
});
|
||||
return { changes, repositories: refreshed };
|
||||
});
|
||||
|
||||
register("repository:clone", async ({ fullName, mode = "default" }) => {
|
||||
if (!["default", "custom"].includes(mode))
|
||||
throw new Error("Unsupported clone location mode.");
|
||||
|
||||
let projectRoot = store.data.workspaceRoots[0] || null;
|
||||
if (mode === "custom" || !projectRoot) {
|
||||
const result = await dialog.showOpenDialog({
|
||||
title: `Choose a project root for ${String(fullName || "repository")}`,
|
||||
defaultPath: projectRoot || undefined,
|
||||
buttonLabel: "Use this project root",
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
});
|
||||
if (result.canceled || !result.filePaths[0]) return { cancelled: true };
|
||||
projectRoot = result.filePaths[0];
|
||||
}
|
||||
|
||||
return cloneRepositoryInto(fullName, projectRoot);
|
||||
});
|
||||
|
||||
register("repository:open-path", async ({ localPath }) => {
|
||||
const safePath = await assertKnownRepositoryPath(localPath);
|
||||
const error = await shell.openPath(safePath);
|
||||
if (error) throw new Error(error);
|
||||
return true;
|
||||
});
|
||||
register(
|
||||
"repository:open-editor",
|
||||
async ({ localPath, filePath = "", line = 1 }) =>
|
||||
externalTools.launch(
|
||||
"editor",
|
||||
await assertKnownRepositoryPath(localPath),
|
||||
filePath,
|
||||
line,
|
||||
),
|
||||
);
|
||||
register("repository:open-terminal", async ({ localPath }) =>
|
||||
externalTools.launch(
|
||||
"terminal",
|
||||
await assertKnownRepositoryPath(localPath),
|
||||
),
|
||||
);
|
||||
|
||||
register("external:open", async ({ url }) => {
|
||||
const parsed = new URL(url);
|
||||
if (!["http:", "https:"].includes(parsed.protocol))
|
||||
throw new Error("Only HTTP and HTTPS links can be opened.");
|
||||
await shell.openExternal(parsed.toString());
|
||||
return true;
|
||||
});
|
||||
|
||||
register("git-validator:scan", async ({ fullName }) => {
|
||||
const repository = await resolveRepository({ fullName });
|
||||
const report = await gitValidator.scan(repository);
|
||||
await diagnostics.info("git-validator.scan.completed", {
|
||||
repository: repository.fullName,
|
||||
score: report.score,
|
||||
summary: report.summary,
|
||||
});
|
||||
return report;
|
||||
});
|
||||
register("git-validator:repair", async ({ fullName, check }) => {
|
||||
const repository = await resolveRepository({ fullName });
|
||||
const allowed = new Set([
|
||||
"align-origin",
|
||||
"configure-local-safety",
|
||||
"add-gitignore",
|
||||
"add-gitattributes",
|
||||
"add-editorconfig",
|
||||
"protect-default-branch",
|
||||
]);
|
||||
if (!allowed.has(check?.fixAction))
|
||||
throw new Error("Unsupported Git Validator repair request.");
|
||||
const result = await gitValidator.repair(repository, check);
|
||||
await audit.append("git-validator.repair", {
|
||||
repository: repository.fullName,
|
||||
checkId: check.id,
|
||||
action: check.fixAction,
|
||||
});
|
||||
await diagnostics.info("git-validator.repair.completed", {
|
||||
repository: repository.fullName,
|
||||
checkId: check.id,
|
||||
action: check.fixAction,
|
||||
});
|
||||
return result;
|
||||
registerRepositoryIpc({
|
||||
register, repositories, store, git, gitea, monitor, diagnostics, audit,
|
||||
externalTools, gitValidator, withRepositoryMutation, assertKnownRepositoryPath,
|
||||
resolveRepository, cloneRepositoryInto, cloneDirectoryName,
|
||||
matchRemoteToRepository, shell, dialog,
|
||||
});
|
||||
|
||||
register("troubleshooter:scan", async ({ fullName = null }) => {
|
||||
@@ -1102,371 +730,13 @@ function registerIpc({
|
||||
return results;
|
||||
});
|
||||
|
||||
register("deployment:save-profile", async ({ fullName, profile }) => {
|
||||
const saved = await store.saveDeploymentProfile(fullName, profile);
|
||||
await diagnostics.info("deployment.profile.saved", {
|
||||
repository: fullName,
|
||||
profile: saved,
|
||||
});
|
||||
return { profile: saved, state: store.getPublicState() };
|
||||
registerDeploymentIpc({
|
||||
register, store, resolveRepository, unraid, deployments, evaluateDeploymentPolicy,
|
||||
audit, deployKeys, repositories, inventoryReviews, diagnostics, git, gitea, ssh,
|
||||
});
|
||||
register("deployment:delete-profile", async ({ fullName, profileId }) => {
|
||||
const profiles = await store.deleteDeploymentProfile(fullName, profileId);
|
||||
await diagnostics.info("deployment.profile.deleted", {
|
||||
repository: fullName,
|
||||
profileId,
|
||||
});
|
||||
return { profiles, state: store.getPublicState() };
|
||||
});
|
||||
register("deployment:preflight", async ({ repository, profileId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const profile = store.getDeploymentProfile(current.fullName, profileId);
|
||||
if (profile?.provider === "ssh-unraid")
|
||||
return unraid.preflight({ repository: current, profileId });
|
||||
return preflight.runDeployment({ repository: current, profileId });
|
||||
});
|
||||
register("deployment:repair-write-access", async ({ repository, profileId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const profile = store.getDeploymentProfile(current.fullName, profileId);
|
||||
if (profile?.provider !== "ssh-unraid")
|
||||
throw new Error("Write-access repair is available only for SSH / Unraid deployment profiles.");
|
||||
const result = await unraid.repairWriteAccess({ repository: current, profileId });
|
||||
await audit.append("deployment.write-access.repaired", {
|
||||
repository: current.fullName,
|
||||
profileId,
|
||||
changed: result.changed,
|
||||
remotePath: result.after?.remotePath || result.before?.remotePath || null,
|
||||
});
|
||||
return result;
|
||||
});
|
||||
register(
|
||||
"deployment:dispatch",
|
||||
async ({
|
||||
repository,
|
||||
profileId,
|
||||
sha,
|
||||
note = "",
|
||||
override = false,
|
||||
overrideReason = "",
|
||||
}) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const profile = store.getDeploymentProfile(current.fullName, profileId);
|
||||
const policy = evaluateDeploymentPolicy(profile, {
|
||||
note,
|
||||
override,
|
||||
reason: overrideReason,
|
||||
});
|
||||
await audit.append("deployment.requested", {
|
||||
repository: current.fullName,
|
||||
profileId,
|
||||
sha,
|
||||
note: policy.note,
|
||||
overridden: policy.overridden,
|
||||
overrideReason: policy.reason,
|
||||
});
|
||||
const operation =
|
||||
profile?.provider === "ssh-unraid"
|
||||
? await unraid.deploy({ repository: current, profileId, sha })
|
||||
: await deployments.deploy({ repository: current, profileId, sha });
|
||||
if (operation?.id)
|
||||
await store.addOperation({
|
||||
...operation,
|
||||
releaseNote: policy.note,
|
||||
policyOverride: policy.overridden
|
||||
? { reason: policy.reason, violations: policy.violations }
|
||||
: null,
|
||||
});
|
||||
return operation;
|
||||
},
|
||||
);
|
||||
register(
|
||||
"deployment:rollback",
|
||||
async ({ repository, profileId, targetSha }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const profile = store.getDeploymentProfile(current.fullName, profileId);
|
||||
if (profile?.provider === "ssh-unraid")
|
||||
return unraid.rollback({ repository: current, profileId, targetSha });
|
||||
return deployments.rollback({
|
||||
repository: current,
|
||||
profileId,
|
||||
targetSha,
|
||||
});
|
||||
},
|
||||
);
|
||||
register("deployment:health", ({ url }) => deployments.checkHealth(url));
|
||||
register("deployment:link-server-workload", async ({ repository, serverId, workloadId, deploymentMode = "server-git", remoteFolder = "" }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const result = await unraid.linkServerWorkload({
|
||||
repository: current,
|
||||
serverId,
|
||||
workloadId,
|
||||
deploymentMode,
|
||||
remoteFolder,
|
||||
});
|
||||
return { ...result, state: store.getPublicState() };
|
||||
});
|
||||
register("deployment:configure-server-git-access", async ({ repository, profileId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const result = await unraid.configureServerGitAccess({ repository: current, profileId });
|
||||
await audit.append("deployment.server-git-access-configured", {
|
||||
repository: current.fullName,
|
||||
profileId,
|
||||
keyFingerprint: result.keyFingerprint,
|
||||
hostFingerprint: result.hostFingerprint,
|
||||
});
|
||||
return { ...result, state: store.getPublicState() };
|
||||
});
|
||||
register("deployment:verify-server-git-profile", async ({ repository, profileId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const result = await unraid.verifyServerGitProfile({ repository: current, profileId });
|
||||
await audit.append("deployment.server-git-access-verified", {
|
||||
repository: current.fullName,
|
||||
profileId,
|
||||
readiness: result.readiness,
|
||||
ready: result.ready,
|
||||
checkedAt: result.checkedAt,
|
||||
});
|
||||
return result;
|
||||
});
|
||||
register("deployment:deploy-key-inventory", async ({ repository, profileId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
return deployKeys.inventory({ repository: current, profileId });
|
||||
});
|
||||
register("deployment:plan-deploy-key-rotation", async ({ repository, profileId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
return deployKeys.planRotation({ repository: current, profileId });
|
||||
});
|
||||
register("deployment:apply-deploy-key-rotation", async ({ repository, profileId, planId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const result = await deployKeys.rotate({ repository: current, profileId, expectedPlanId: planId });
|
||||
return { ...result, state: store.getPublicState() };
|
||||
});
|
||||
register("deployment:plan-deploy-key-revocation", async ({ repository, profileId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
return deployKeys.planRevocation({ repository: current, profileId });
|
||||
});
|
||||
register("deployment:apply-deploy-key-revocation", async ({ repository, profileId, planId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const result = await deployKeys.revoke({ repository: current, profileId, expectedPlanId: planId });
|
||||
return { ...result, state: store.getPublicState() };
|
||||
});
|
||||
register("deployment:restore-deploy-key", async ({ repository, profileId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
const result = await deployKeys.restore({ repository: current, profileId });
|
||||
return { ...result, state: store.getPublicState() };
|
||||
});
|
||||
register("deployment:discover-server-workloads", async () => {
|
||||
const repositoryList = await repositories.refresh();
|
||||
const remoteRepositories = repositoryList.filter(
|
||||
(repository) => repository.owner?.login !== "local",
|
||||
);
|
||||
const results = [];
|
||||
for (const server of store.data.servers || []) {
|
||||
try {
|
||||
results.push(
|
||||
await unraid.discoverServerWorkloads(server.id, remoteRepositories),
|
||||
);
|
||||
} catch (error) {
|
||||
results.push({
|
||||
serverId: server.id,
|
||||
serverName: server.name,
|
||||
detected: 0,
|
||||
adopted: 0,
|
||||
verified: 0,
|
||||
linked: 0,
|
||||
unmatched: 0,
|
||||
needsReview: 0,
|
||||
capabilities: {},
|
||||
warnings: [],
|
||||
workloads: [],
|
||||
error: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
});
|
||||
register("deployment:plan-server-reconciliation", async ({ serverId }) => {
|
||||
const repositoryList = await repositories.refresh();
|
||||
const remoteRepositories = repositoryList.filter((repository) => repository.owner?.login !== "local");
|
||||
const result = await unraid.planServerInventoryReconciliation(serverId, remoteRepositories, { autoLink: true });
|
||||
await audit.append("deployment.server-reconciliation-planned", {
|
||||
serverId,
|
||||
planId: result.plan.id,
|
||||
summary: result.plan.summary,
|
||||
});
|
||||
return result;
|
||||
});
|
||||
register("deployment:apply-server-reconciliation", async ({ serverId, planId }) => {
|
||||
const repositoryList = await repositories.refresh();
|
||||
const remoteRepositories = repositoryList.filter((repository) => repository.owner?.login !== "local");
|
||||
const result = await unraid.reconcileServerInventory(serverId, remoteRepositories, { autoLink: true, expectedPlanId: planId });
|
||||
await audit.append("deployment.server-reconciliation-applied", {
|
||||
serverId,
|
||||
planId,
|
||||
adopted: result.adopted,
|
||||
refreshed: result.refreshed,
|
||||
retired: result.retired,
|
||||
recoverySnapshot: result.recoverySnapshot?.filePath || null,
|
||||
});
|
||||
return { ...result, state: store.getPublicState() };
|
||||
});
|
||||
register("deployment:plan-inventory-review", async ({ serverId, workloadId, action, reason = "", repositoryFullName = null }) => {
|
||||
const repositoryList = await repositories.refresh();
|
||||
const inventory = await unraid.scanServerInventory(serverId, repositoryList.filter((item) => item.owner?.login !== "local"));
|
||||
const workload = inventory.workloads.find((item) => item.workloadId === workloadId);
|
||||
if (!workload) throw Object.assign(new Error("The workload changed or disappeared. Rescan before reviewing it."), { code: "INVENTORY_REVIEW_WORKLOAD_STALE" });
|
||||
return inventoryReviews.preview({ serverId, workload, action, reason, repositoryFullName });
|
||||
});
|
||||
register("deployment:apply-inventory-review", async ({ serverId, workloadId, action, reason = "", repositoryFullName = null, planId }) => {
|
||||
const repositoryList = await repositories.refresh();
|
||||
const inventory = await unraid.scanServerInventory(serverId, repositoryList.filter((item) => item.owner?.login !== "local"));
|
||||
const workload = inventory.workloads.find((item) => item.workloadId === workloadId);
|
||||
if (!workload) throw Object.assign(new Error("The workload changed or disappeared. Rescan before applying the review."), { code: "INVENTORY_REVIEW_WORKLOAD_STALE" });
|
||||
const plan = inventoryReviews.preview({ serverId, workload, action, reason, repositoryFullName });
|
||||
const result = await inventoryReviews.apply({ plan, expectedPlanId: planId });
|
||||
return { ...result, inventory: await unraid.scanServerInventory(serverId, repositoryList.filter((item) => item.owner?.login !== "local")), state: store.getPublicState() };
|
||||
});
|
||||
register("deployment:profile-state", async ({ fullName, profileId }) => {
|
||||
const profile = store.getDeploymentProfile(fullName, profileId);
|
||||
if (profile?.provider === "ssh-unraid") {
|
||||
let giteaSha = null;
|
||||
try {
|
||||
const [owner, repo] = String(fullName || "").split("/");
|
||||
const branch = await gitea.getBranch(owner, repo, profile.branch);
|
||||
giteaSha = branch?.commit?.id || branch?.commit?.sha || null;
|
||||
} catch {}
|
||||
return unraid.refreshProfileState(fullName, profileId, giteaSha);
|
||||
}
|
||||
return deployments.refreshProfileState(fullName, profileId);
|
||||
});
|
||||
register(
|
||||
"deployment:apply-dockerman-metadata",
|
||||
async ({ repository, profileId }) => {
|
||||
const current = await resolveRepository(repository);
|
||||
return unraid.applyDockerManMetadata({ repository: current, profileId });
|
||||
},
|
||||
);
|
||||
register("deployment:reconcile", async ({ fullName, profileId }) => {
|
||||
const profile = store.getDeploymentProfile(fullName, profileId);
|
||||
if (profile?.provider !== "ssh-unraid")
|
||||
return deployments.refreshProfileState(fullName, profileId);
|
||||
const [owner, repo] = String(fullName || "").split("/");
|
||||
const branch = await gitea.getBranch(owner, repo, profile.branch);
|
||||
const giteaSha =
|
||||
branch?.commit?.id ||
|
||||
branch?.commit?.sha ||
|
||||
branch?.commit?.commit?.id ||
|
||||
null;
|
||||
const state = await unraid.refreshProfileState(
|
||||
fullName,
|
||||
profileId,
|
||||
giteaSha,
|
||||
);
|
||||
const operations = store.data.operations.filter(
|
||||
(item) =>
|
||||
item.profileId === profileId &&
|
||||
item.provider === "ssh-unraid" &&
|
||||
!["success", "failed", "cancelled", "rolled-back"].includes(
|
||||
item.status,
|
||||
),
|
||||
);
|
||||
for (const operation of operations)
|
||||
await unraid.refreshOperation(operation.id);
|
||||
return {
|
||||
state,
|
||||
operations: await unraid.reconcileRecordedOperations(profileId, state),
|
||||
};
|
||||
});
|
||||
register("operations:refresh", async ({ operationId }) => {
|
||||
if (operationId) {
|
||||
const operation = store.getOperation(operationId);
|
||||
if (operation?.provider === "ssh-unraid")
|
||||
return unraid.refreshOperation(operationId);
|
||||
return deployments.refreshOperation(operationId);
|
||||
}
|
||||
const [actions, sshOperations] = await Promise.all([
|
||||
deployments.refreshActiveOperations(),
|
||||
unraid.refreshActiveOperations(),
|
||||
]);
|
||||
return [...actions, ...sshOperations];
|
||||
});
|
||||
register("operations:get", ({ operationId }) =>
|
||||
store.getOperation(operationId),
|
||||
);
|
||||
|
||||
register("diagnostics:status", () => diagnostics.getStatus());
|
||||
register("diagnostics:clear", () => diagnostics.clear());
|
||||
register("diagnostics:open-folder", async () => {
|
||||
const error = await shell.openPath(diagnostics.logDirectory);
|
||||
if (error) throw new Error(error);
|
||||
return true;
|
||||
});
|
||||
register("diagnostics:export", async ({ privacyMode = "standard" }) => {
|
||||
if (!["standard", "strict"].includes(privacyMode))
|
||||
throw new Error("Unsupported diagnostic privacy mode.");
|
||||
const result = await dialog.showSaveDialog({
|
||||
title: "Export ForgeFlow diagnostic bundle",
|
||||
defaultPath: path.join(
|
||||
app.getPath("downloads"),
|
||||
`ForgeFlow-Diagnostics-${new Date().toISOString().replace(/[:.]/g, "-")}.zip`,
|
||||
),
|
||||
filters: [{ name: "ZIP archive", extensions: ["zip"] }],
|
||||
});
|
||||
if (result.canceled || !result.filePath) return null;
|
||||
const repositoryState = await repositories.refresh().catch((error) => {
|
||||
diagnostics.warning("diagnostics.repository-snapshot.failed", error);
|
||||
return [];
|
||||
});
|
||||
const systemPreflight = await preflight
|
||||
.runSystem()
|
||||
.catch((error) => ({ error: error.message }));
|
||||
const destinationPath =
|
||||
path.extname(result.filePath).toLowerCase() === ".zip"
|
||||
? result.filePath
|
||||
: `${result.filePath}.zip`;
|
||||
return diagnostics.exportSupportBundle({
|
||||
destinationPath,
|
||||
publicState: store.getPublicState(),
|
||||
repositories: repositoryState,
|
||||
operations: store.data.operations,
|
||||
preflight: systemPreflight,
|
||||
privacyMode,
|
||||
extra: {
|
||||
appVersion: app.getVersion(),
|
||||
setupComplete: store.data.setupComplete,
|
||||
},
|
||||
});
|
||||
});
|
||||
register("diagnostics:show-bundle", async ({ filePath }) => {
|
||||
if (!diagnostics.isKnownBundlePath(filePath))
|
||||
throw new Error(
|
||||
"Only the most recently generated support bundle can be revealed.",
|
||||
);
|
||||
shell.showItemInFolder(filePath);
|
||||
return true;
|
||||
});
|
||||
register(
|
||||
"renderer:report",
|
||||
async ({ level = "info", event = "renderer.event", details = {} }) => {
|
||||
const method = ["debug", "info", "warning", "error"].includes(level)
|
||||
? level
|
||||
: "info";
|
||||
await diagnostics[method](
|
||||
`renderer.${String(event || "event").slice(0, 120)}`,
|
||||
details,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
register("app:reset", async () => {
|
||||
await diagnostics.info("app.reset.requested", {});
|
||||
store.data = store.migrate({});
|
||||
store.sessionToken = null;
|
||||
await store.save();
|
||||
monitor?.setPaths([]);
|
||||
monitor?.restart();
|
||||
return store.getPublicState();
|
||||
registerOperationsIpc({
|
||||
register, store, unraid, deployments, diagnostics, shell, dialog, path, app,
|
||||
repositories, preflight, monitor,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user