518 lines
19 KiB
JavaScript
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;
|
|
}
|