Files
ForgeFlow/src/renderer/actions/shell.js
T
NuklearRabbit 58d361bbab
ForgeFlow quality gate / quality (push) Canceled after 0s
fix: align deployment links across repository views
2026-08-01 17:05:03 +02:00

518 lines
19 KiB
JavaScript

async function handleShellActions(event, target, action, repository) {
if (action === "navigate") {
ui.currentView = target.dataset.view;
ui.modal = null;
render();
if (ui.currentView === "deployments" && (ui.boot?.state?.servers || []).length && !(ui.serverDiscovery || []).length) {
setLoading(true, "Reading Docker, Compose and DockerMan inventory from Unraid…");
await refreshDeploymentTruth(true);
setLoading(false);
}
} else if (action === "select-repo") selectRepository(target.dataset.id);
else if (action === "open-deployment-link") {
if (!repository) return true;
selectRepository(repository.id, false);
ui.selectedProfileId = target.dataset.profileId || selectedProfile(repository)?.id || null;
ui.repositoryTab = "deployments";
ui.currentView = "repository";
render();
} else if (action === "select-deployment-profile") {
ui.selectedProfileId = target.dataset.profileId || null;
ui.repositoryTab = "deployments";
render();
}
else if (action === "refresh") {
await refreshRepositories(true);
await refreshActiveOperations(false);
await refreshDeploymentTruth(false);
} else if (action === "refresh-operations") {
setLoading(true, "Refreshing deployment operations and live server state…");
await refreshActiveOperations();
await refreshDeploymentTruth(true);
setLoading(false);
} else if (action === "toggle-theme") {
const appearance =
document.documentElement.dataset.theme === "dark" ? "light" : "dark";
applyTheme(appearance);
ui.boot.state = await window.forgeflow.setAppearance(appearance);
render();
} else if (action === "open-palette") {
ui.paletteQuery = "";
ui.modal = { type: "command-palette" };
render();
} else if (action === "repo-tab") {
ui.repositoryTab = target.dataset.tab;
if (ui.repositoryTab === "gittools" && !ui.branches.length)
await loadGitTools(repository);
else if (ui.repositoryTab === "validator" && !ui.gitValidation) {
setLoading(true, "Validating Git and Gitea best practices…");
try {
ui.gitValidation = await window.forgeflow.gitValidatorScan(
repository.fullName,
);
} catch (error) {
showToast("Git Validator failed", error.message, "error");
} finally {
setLoading(false);
}
render();
} else if (ui.repositoryTab === "settings") {
try {
ui.pullRequests = await window.forgeflow.pullRequests(
repository.fullName,
"open",
);
} catch (error) {
ui.pullRequests = [];
showToast("Could not load pull requests", error.message, "error");
}
render();
} else render();
} else if (action === "git-validator-scan") {
setLoading(true, "Validating Git and Gitea best practices…");
try {
ui.gitValidation = await window.forgeflow.gitValidatorScan(
repository.fullName,
);
showToast(
"Git validation complete",
`${ui.gitValidation.score}/100 · ${ui.gitValidation.grade}`,
ui.gitValidation.summary.errors ? "error" : "success",
);
} catch (error) {
showToast("Git Validator failed", error.message, "error");
} finally {
setLoading(false);
}
} else if (action === "git-validator-repair") {
const check = ui.gitValidation?.checks?.[Number(target.dataset.checkIndex)];
if (!check?.fixAction) return;
let preview;
try {
preview = await window.forgeflow.gitValidatorPreviewRepair(repository.fullName, check);
} catch (error) {
showToast("Preview failed", error.message, "error");
return;
}
if (!confirm(`${check.confirmation || `Apply ${check.title}?`}\n\nReviewable change:\n${preview.diff}\n\nNothing will be committed or pushed.`)) return;
setLoading(true, `Repairing ${check.title}…`);
try {
await window.forgeflow.gitValidatorRepair(repository.fullName, check);
await refreshRepositories(false, true);
ui.gitValidation = await window.forgeflow.gitValidatorScan(
repository.fullName,
);
showToast("Git best practice repaired", check.title, "success");
} catch (error) {
showToast("Repair failed", error.message, "error");
} finally {
setLoading(false);
}
} else if (action === "git-validator-policy") {
setLoading(true, "Applying assurance policy…");
try {
await window.forgeflow.gitValidatorSetPolicy(repository.fullName, { id: target.value });
ui.gitValidation = await window.forgeflow.gitValidatorScan(repository.fullName);
showToast("Policy updated", ui.gitValidation.policy.label, "success");
} catch (error) {
showToast("Policy update failed", error.message, "error");
} finally {
setLoading(false);
}
} else if (action === "git-validator-export") {
try {
const exported = await window.forgeflow.gitValidatorExport(repository.fullName, target.dataset.format || "markdown");
const url = URL.createObjectURL(new Blob([exported.content], { type: exported.mimeType }));
const link = document.createElement("a");
link.href = url;
link.download = `${repository.name || "repository"}-git-assurance.${exported.extension}`;
link.click();
URL.revokeObjectURL(url);
showToast("Report exported", link.download, "success");
} catch (error) {
showToast("Export failed", error.message, "error");
}
} else if (action === "git-validator-suppress") {
const check = ui.gitValidation?.checks?.[Number(target.dataset.checkIndex)];
if (!check) return;
const reason = prompt("Reason for this temporary exception (minimum 10 characters):", "Accepted temporarily while remediation is tracked.");
if (!reason) return;
const author = prompt("Exception owner:", ui.boot?.state?.gitea?.user?.login || "");
if (!author) return;
const ticket = prompt("Ticket reference (optional):", "") || "";
const expiresAt = new Date(Date.now() + 7 * 86_400_000).toISOString();
try {
await window.forgeflow.gitValidatorSuppress(repository.fullName, { checkId: check.id, reason, author, ticket, expiresAt, scope: "repository", evidence: `${ui.gitValidation.commitSha || "unknown"}:${check.id}:${check.status}` });
ui.gitValidation = await window.forgeflow.gitValidatorScan(repository.fullName);
showToast("Exception documented", `Expires ${formatDate(expiresAt)}.`, "success");
} catch (error) {
showToast("Exception rejected", error.message, "error");
}
} else if (action === "toggle-favorite") {
ui.boot.state = await window.forgeflow.favoriteRepository(
repository.fullName,
!repository.favorite,
);
repository.favorite = !repository.favorite;
render();
} else if (action === "select-file") {
if (event.target.matches("input[type=checkbox]")) return;
ui.selectedFile = target.dataset.path;
await loadDiff(repository, ui.selectedFile);
} else if (action === "toggle-all-files") {
const files = repository.localStatus?.files || [];
ui.selectedFiles =
ui.selectedFiles.size === files.length
? new Set()
: new Set(files.map((file) => file.path));
render();
} else if (action === "copy-diff") {
await navigator.clipboard.writeText(ui.diff || "");
showToast("Copied", "Diff copied to clipboard.", "success");
} else if (action === "open-hunk-staging") {
if (ui.diffHunks?.partialSupported) {
ui.modal = { type: "hunk-staging" };
render();
}
} else if (action === "stage-chosen-hunks") {
const indexes = [
...document.querySelectorAll("[data-hunk-index]:checked"),
].map((input) => Number(input.dataset.hunkIndex));
if (!indexes.length) return;
const result = await runOperation(
"Staging selected hunks…",
() =>
window.forgeflow.stageHunks(
repository.localPath,
ui.selectedFile,
indexes,
),
"Selected hunks staged.",
);
if (result) {
ui.modal = null;
await loadDiff(selectedRepository(), ui.selectedFile);
}
} else if (action === "open-file-editor") {
await window.forgeflow
.openEditor(repository.localPath, ui.selectedFile)
.catch((error) =>
showToast("Could not open editor", error.message, "error"),
);
} else if (action === "open-editor") {
await window.forgeflow
.openEditor(repository.localPath)
.catch((error) =>
showToast("Could not open editor", error.message, "error"),
);
} else if (action === "open-terminal") {
await window.forgeflow
.openTerminal(repository.localPath)
.catch((error) =>
showToast("Could not open terminal", error.message, "error"),
);
} else if (action === "load-conflicts") {
ui.conflictState = await window.forgeflow.conflictState(
repository.localPath,
);
ui.modal = { type: "conflict-guide" };
render();
} else if (action === "resolve-conflict") {
if (
!ui.selectedFile ||
!confirm(`Apply “${target.dataset.resolution}” to ${ui.selectedFile}?`)
)
return;
ui.conflictState = await window.forgeflow.resolveConflict(
repository.localPath,
ui.selectedFile,
target.dataset.resolution,
);
await refreshRepositories(false);
ui.modal = { type: "conflict-guide" };
render();
} else if (action === "open-conflict-file") {
await window.forgeflow.openEditor(
repository.localPath,
target.dataset.path,
);
} else if (action === "continue-git-operation") {
ui.conflictState = await window.forgeflow.continueGitOperation(
repository.localPath,
);
ui.modal = null;
await refreshRepositories(false);
showToast(
"Git operation continued",
"The repository operation completed.",
"success",
);
} else if (action === "abort-git-operation") {
if (
!confirm(
"Abort the active Git operation? Conflict-resolution work may be discarded.",
)
)
return;
await window.forgeflow.abortGitOperation(repository.localPath);
ui.modal = null;
await refreshRepositories(false);
} else if (action === "check-branch-protection") {
ui.branchProtection = await window.forgeflow.branchProtection(
repository.fullName,
repository.localStatus.branch.head,
);
showToast(
ui.branchProtection.protected
? "Protected branch"
: "Branch is not protected",
ui.branchProtection.protected
? `${ui.branchProtection.requiredApprovals} approval(s) required.`
: "Direct pushes are permitted by the reported branch rule.",
ui.branchProtection.protected ? "info" : "success",
);
render();
} else if (action === "open-pull-request") {
const subject =
ui.history[0]?.subject || repository.localStatus.branch.head;
ui.modal = {
type: "pull-request",
title: subject,
body: `## Summary\n\nChanges from ${repository.localStatus.branch.head}.`,
};
render();
} else if (action === "load-pull-requests") {
try {
ui.pullRequests = await window.forgeflow.pullRequests(
repository.fullName,
"open",
);
render();
} catch (error) {
showToast("Could not load pull requests", error.message, "error");
}
} else if (action === "open-pull-request-url") {
if (target.dataset.url)
await window.forgeflow.openExternal(target.dataset.url);
} else if (action === "create-pull-request") {
setLoading(true, "Creating pull request…");
try {
const pull = await window.forgeflow.createPullRequest(
repository.fullName,
document.querySelector("#pr-title").value,
document.querySelector("#pr-body").value,
document.querySelector("#pr-base").value,
);
ui.modal = null;
ui.pullRequests = await window.forgeflow
.pullRequests(repository.fullName, "open")
.catch(() => ui.pullRequests);
showToast("Pull request created", `#${pull.number}`, "success");
if (pull.html_url) await window.forgeflow.openExternal(pull.html_url);
} catch (error) {
showToast("Could not create pull request", error.message, "error");
}
setLoading(false);
} else if (action === "copy-logs") {
const text = (ui.activeDeployment?.logs || []).join("\n");
await navigator.clipboard.writeText(text);
showToast(
"Copied",
"Safe operation output copied. Open Gitea for raw runner logs.",
"success",
);
} else if (action === "stage-selected") {
if (!repository?.localPath || !ui.selectedFiles.size) return;
await runOperation(
"Staging selected files…",
() =>
window.forgeflow.stageFiles(repository.localPath, [
...ui.selectedFiles,
]),
"Files staged.",
);
} else if (action === "unstage-selected") {
if (!repository?.localPath || !ui.selectedFiles.size) return;
await runOperation(
"Unstaging selected files…",
() =>
window.forgeflow.unstageFiles(repository.localPath, [
...ui.selectedFiles,
]),
"Files unstaged.",
);
} else if (action === "commit-push" || action === "commit-only") {
if (
!repository?.localPath ||
!ui.commitMessage.trim() ||
(!ui.selectedFiles.size && !repository.localStatus?.counts?.staged)
)
return;
const selected = [...ui.selectedFiles];
const result = await runOperation(
action === "commit-push"
? "Committing and pushing…"
: "Creating local commit…",
() =>
action === "commit-push"
? selected.length
? window.forgeflow.commitAndPush(
repository.localPath,
ui.commitMessage,
selected,
)
: window.forgeflow.commitStagedAndPush(
repository.localPath,
ui.commitMessage,
)
: selected.length
? window.forgeflow.commit(
repository.localPath,
ui.commitMessage,
selected,
)
: window.forgeflow.commitStaged(
repository.localPath,
ui.commitMessage,
),
action === "commit-push"
? "Changes committed and pushed to Gitea."
: "Local commit created.",
);
if (result) {
ui.commitMessage = "";
ui.selectedFiles.clear();
ui.selectedFile = null;
ui.diff = "";
}
} else if (action === "push")
await runOperation(
"Pushing local commits…",
() => window.forgeflow.push(repository.localPath),
"Push completed.",
);
else if (action === "fetch")
await runOperation(
"Fetching from Gitea…",
() => window.forgeflow.fetch(repository.localPath),
"Remote state refreshed.",
);
else if (action === "pull")
await runOperation(
"Synchronizing from Gitea…",
() => window.forgeflow.pull(repository.localPath),
"Local branch fast-forwarded.",
);
else if (action === "load-history") {
setLoading(true, "Loading commit history…");
try {
ui.history = await window.forgeflow.history(repository.localPath, 50);
} catch (error) {
showToast("History unavailable", error.message, "error");
}
setLoading(false);
} else if (action === "load-git-tools") await loadGitTools(repository);
else if (action === "create-branch") {
const branch = document.querySelector("#new-branch-name")?.value.trim();
if (branch)
await runOperation(
`Creating ${branch}…`,
() => window.forgeflow.createBranch(repository.localPath, branch),
`Switched to ${branch}.`,
);
await loadGitTools(selectedRepository());
} else if (action === "checkout-branch") {
await runOperation(
`Switching to ${target.dataset.branch}…`,
() =>
window.forgeflow.checkoutBranch(
repository.localPath,
target.dataset.branch,
),
`Switched to ${target.dataset.branch}.`,
);
await loadGitTools(selectedRepository());
} else if (action === "stash-changes") {
const result = await runOperation(
"Stashing local changes…",
() =>
window.forgeflow.stash(
repository.localPath,
`ForgeFlow ${new Date().toLocaleString()}`,
),
"Local changes stashed.",
);
if (result) ui.stashes = result.stashes;
} else if (action === "pop-stash") {
const result = await runOperation(
`Applying ${target.dataset.stashRef}…`,
() =>
window.forgeflow.popStash(
repository.localPath,
target.dataset.stashRef,
),
"Stash applied.",
);
if (result) ui.stashes = result.stashes;
} else if (action === "open-path")
await window.forgeflow
.openPath(repository.localPath)
.catch((error) =>
showToast("Could not open folder", error.message, "error"),
);
else if (action === "open-gitea")
await window.forgeflow
.openExternal(repository.htmlUrl)
.catch((error) =>
showToast("Could not open Gitea", error.message, "error"),
);
else if (action === "link-repo") {
const localPath = await window.forgeflow.selectDirectory({
title: `Link local folder for ${repository.name}`,
});
if (localPath) {
ui.repositories =
(await runOperation(
"Linking local repository…",
() => window.forgeflow.linkRepository(repository.fullName, localPath),
"Local folder linked.",
{ refresh: false },
)) || ui.repositories;
selectRepository(repository.id);
}
} else if (action === "unlink-repo") {
ui.repositories =
(await runOperation(
"Removing local link…",
() => window.forgeflow.unlinkRepository(repository.fullName),
"Repository link removed.",
{ refresh: false },
)) || ui.repositories;
selectRepository(repository.id);
} else if (action === "clone-repo" || action === "clone-repo-custom") {
const mode = action === "clone-repo-custom" ? "custom" : "default";
const clone = await runOperation(
mode === "custom"
? "Choosing location and cloning repository…"
: `Cloning ${repository.name} into the default project root…`,
() => window.forgeflow.cloneRepository(repository.fullName, mode),
null,
{ refresh: false },
);
if (clone?.cancelled) return;
if (clone?.target) {
if (clone.state) ui.boot.state = clone.state;
ui.repositories =
clone.repositories || (await window.forgeflow.refreshRepositories());
selectRepository(repository.id);
showToast(
clone.reused ? "Existing repository linked" : "Repository cloned",
clone.target,
"success",
);
}
}
else return false;
return true;
}