Files
ForgeFlow/src/renderer/mock-bridge.js
T
NuklearRabbit 4ad698c4eb Release ForgeFlow 0.8.1
Add advanced Git and deployment workflows, secure backups and auditing, live Gitea integration, desktop notifications, connection validation, and the premium responsive UX refresh.
2026-07-26 00:42:17 +02:00

1540 lines
50 KiB
JavaScript

(() => {
if (window.forgeflow) return;
const wait = (ms = 180) => new Promise((resolve) => setTimeout(resolve, ms));
const clone = (value) => JSON.parse(JSON.stringify(value));
const iso = (offset = 0) => new Date(Date.now() + offset).toISOString();
const storage = {
get(key) {
try {
return localStorage.getItem(key);
} catch {
return null;
}
},
set(key, value) {
try {
localStorage.setItem(key, value);
} catch {}
},
};
const repositoryListeners = new Set();
const operationListeners = new Set();
const updateListeners = new Set();
const emitRepositories = () =>
repositoryListeners.forEach((listener) =>
listener({ reason: "demo-change" }),
);
const emitOperations = (operations) =>
operationListeners.forEach((listener) =>
listener({ operations: clone(operations) }),
);
const randomSha = () =>
`${Math.random().toString(16).slice(2)}${Date.now().toString(16)}`
.padEnd(40, "a")
.slice(0, 40);
const makeStatus = ({
head,
branch = "main",
ahead = 0,
behind = 0,
upstream = `origin/${branch}`,
files = [],
}) => ({
branch: { oid: head, head: branch, upstream, ahead, behind },
files,
counts: {
changed: files.length,
staged: files.filter((item) => item.staged).length,
unstaged: files.filter((item) => item.unstaged).length,
conflicts: files.filter((item) => item.conflict).length,
untracked: files.filter((item) => item.untracked).length,
},
clean: files.length === 0,
root: "",
remoteUrl: "",
head,
shortHead: head.slice(0, 7),
fingerprint: `${head}:${branch}:${ahead}:${behind}:${files.map((item) => `${item.path}:${item.indexCode}${item.worktreeCode}`).join("|")}`,
});
const makeFile = (path, status = "modified", options = {}) => ({
path,
originalPath: options.originalPath || null,
indexCode: options.staged
? status === "added"
? "A"
: status === "deleted"
? "D"
: "M"
: ".",
worktreeCode: options.staged
? "."
: status === "untracked"
? "?"
: status === "deleted"
? "D"
: status === "conflict"
? "U"
: "M",
staged: Boolean(options.staged),
unstaged: !options.staged,
untracked: status === "untracked",
conflict: status === "conflict",
status,
});
const profile = (id, name, environment, options = {}) => ({
id,
name,
environment,
provider: "gitea-actions",
branch: options.branch || "main",
workflowFile: options.workflowFile || "deploy.yml",
rollbackWorkflowFile: options.rollbackWorkflowFile ?? "rollback.yml",
healthcheckUrl:
options.healthcheckUrl || `https://${environment}.internal/health`,
statusUrl:
options.statusUrl ||
`https://${environment}.internal/.well-known/forgeflow`,
confirmationRequired: options.confirmationRequired !== false,
inputs: {},
state: {
liveSha: options.liveSha || null,
previousSha: options.previousSha || null,
healthy: options.healthy ?? null,
healthConfigured: true,
statusConfigured: true,
healthStatus: options.healthy === false ? 503 : 200,
healthLatencyMs: 42,
checkedAt: options.checkedAt || iso(-120000),
},
});
const now = iso();
const defaultPreferences = {
autoRefresh: true,
repositoryPollSeconds: 4,
operationPollSeconds: 5,
fetchIntervalMinutes: 10,
preferredCloneProtocol: "https",
diagnosticsEnabled: true,
diagnosticLevel: "info",
logRetentionDays: 14,
maxLogFileMb: 8,
};
let state = {
schemaVersion: 8,
setupComplete: storage.get("forgeflow-demo-setup") !== "false",
appearance: storage.get("forgeflow-theme") || "dark",
gitea: {
baseUrl: "https://gitea.internal",
user: { login: "jens", full_name: "Jens" },
hasToken: true,
},
workspaceRoots: ["C:\\Development"],
repositoryMappings: {},
deploymentProfiles: {},
deploymentStates: {},
favorites: [
"jens/microsoft-cloud-operations-platform",
"jens/unraid-appops-gateway",
],
updates: {
owner: "Jens",
repo: "ForgeFlow",
branch: "main",
autoCheck: true,
lastCheckedAt: null,
},
servers: [
{
id: "server-unraid",
name: "Unraid",
host: "192.168.1.10",
port: 22,
username: "root",
authType: "privateKey",
basePath: "/mnt/user/appdata",
privateKeyPath: "C:\\Users\\Jens\\.ssh\\id_ed25519",
hostFingerprint: "SHA256:demo",
hasPassword: false,
hasPassphrase: false,
},
],
preferences: { ...defaultPreferences },
operations: [
{
id: "op-success",
type: "deployment",
action: "deploy",
status: "success",
repository: "jens/microsoft-cloud-operations-platform",
profileId: "profile-mcop-prod",
profileName: "Production",
environment: "production",
workflowFile: "deploy.yml",
branch: "main",
sha: "b82f91ab0173cd4346ca0f0f7dcc3e8182cc8fd0",
shortSha: "b82f91a",
createdAt: now,
updatedAt: now,
stages: [
{ id: "requested", label: "Requested", status: "complete" },
{ id: "verified", label: "Verified", status: "complete" },
{ id: "queued", label: "Workflow queued", status: "complete" },
{ id: "runner", label: "Runner execution", status: "complete" },
{ id: "healthcheck", label: "Healthcheck", status: "complete" },
{ id: "complete", label: "Complete", status: "complete" },
],
logs: [
"[info] Exact commit verified.",
"[job] deploy: success",
"[ok] Server reports b82f91a and healthcheck returned 200.",
],
run: {
id: 48,
runNumber: 48,
status: "completed",
conclusion: "success",
name: "ForgeFlow deployment",
},
runUrl:
"https://gitea.internal/jens/microsoft-cloud-operations-platform/actions/runs/48",
},
{
id: "op-failed",
type: "deployment",
action: "deploy",
status: "failed",
repository: "jens/portfolio",
profileId: "profile-portfolio",
profileName: "Production",
environment: "production",
workflowFile: "deploy.yml",
branch: "main",
sha: "a7f2e1c1bb6147fc8b6633d2b08500c93402a719",
shortSha: "a7f2e1c",
createdAt: iso(-86400000),
updatedAt: iso(-86300000),
failure: { stage: "healthcheck", message: "Healthcheck returned 502." },
stages: [
{ id: "requested", label: "Requested", status: "complete" },
{ id: "verified", label: "Verified", status: "complete" },
{ id: "queued", label: "Workflow queued", status: "complete" },
{ id: "runner", label: "Runner execution", status: "complete" },
{ id: "healthcheck", label: "Healthcheck", status: "failed" },
{ id: "complete", label: "Complete", status: "failed" },
],
logs: ["[job] deploy: success", "[error] Healthcheck returned 502."],
},
],
};
let repositories = [
{
id: 1,
name: "microsoft-cloud-operations-platform",
fullName: "jens/microsoft-cloud-operations-platform",
owner: { login: "jens" },
description: "Tenant-aware Microsoft cloud operations console.",
private: true,
defaultBranch: "main",
htmlUrl:
"https://gitea.internal/jens/microsoft-cloud-operations-platform",
cloneUrl:
"https://gitea.internal/jens/microsoft-cloud-operations-platform.git",
sshUrl: "git@gitea.internal:jens/microsoft-cloud-operations-platform.git",
updatedAt: now,
localPath: "C:\\Development\\Microsoft-Cloud-Operations-Platform",
localStatus: makeStatus({
head: "b82f91ab0173cd4346ca0f0f7dcc3e8182cc8fd0",
}),
linkState: "linked",
deploymentProfiles: [
profile("profile-mcop-prod", "Production", "production", {
liveSha: "72bd10eb0173cd4346ca0f0f7dcc3e8182cc8fd0",
previousSha: "6ac991ab0173cd4346ca0f0f7dcc3e8182cc8fd0",
healthy: true,
}),
profile("profile-mcop-stage", "Staging", "staging", {
liveSha: "b82f91ab0173cd4346ca0f0f7dcc3e8182cc8fd0",
previousSha: "72bd10eb0173cd4346ca0f0f7dcc3e8182cc8fd0",
healthy: true,
confirmationRequired: false,
}),
],
},
{
id: 2,
name: "vacancyradar",
fullName: "jens/vacancyradar",
owner: { login: "jens" },
description: "Local-first vacancy intelligence cockpit.",
private: true,
defaultBranch: "main",
htmlUrl: "https://gitea.internal/jens/vacancyradar",
cloneUrl: "https://gitea.internal/jens/vacancyradar.git",
sshUrl: "git@gitea.internal:jens/vacancyradar.git",
updatedAt: now,
localPath: "C:\\Development\\VacancyRadar",
localStatus: makeStatus({
head: "c9182d0d28318c8cf0af109edc054732426aadf1",
branch: "feature/deployment-api",
files: [
makeFile("src/api/deploy.ts", "added", { staged: true }),
makeFile("src/main.tsx"),
makeFile("src/components/Sidebar.tsx"),
],
}),
linkState: "linked",
deploymentProfiles: [
profile("profile-vr", "Production", "production", {
liveSha: "c117ab9d28318c8cf0af109edc054732426aadf1",
previousSha: "b1f57aad28318c8cf0af109edc054732426aadf1",
healthy: true,
}),
],
},
{
id: 3,
name: "unraid-appops-gateway",
fullName: "jens/unraid-appops-gateway",
owner: { login: "jens" },
description: "Safe operations gateway for Unraid and Portainer.",
private: true,
defaultBranch: "main",
htmlUrl: "https://gitea.internal/jens/unraid-appops-gateway",
cloneUrl: "https://gitea.internal/jens/unraid-appops-gateway.git",
sshUrl: "git@gitea.internal:jens/unraid-appops-gateway.git",
updatedAt: now,
localPath: "C:\\Development\\Unraid-AppOps-Gateway",
localStatus: makeStatus({
head: "f2d1e0a1bb6147fc8b6633d2b08500c93402a719",
ahead: 2,
}),
linkState: "linked",
deploymentProfiles: [
profile("profile-appops", "Production", "production", {
liveSha: "8ac731b1bb6147fc8b6633d2b08500c93402a719",
previousSha: "7bc198a1bb6147fc8b6633d2b08500c93402a719",
healthy: true,
}),
],
},
{
id: 4,
name: "support-bundle-collector",
fullName: "jens/support-bundle-collector",
owner: { login: "jens" },
description: "Privacy-aware Windows support bundle collector.",
private: true,
defaultBranch: "main",
htmlUrl: "https://gitea.internal/jens/support-bundle-collector",
cloneUrl: "https://gitea.internal/jens/support-bundle-collector.git",
sshUrl: "git@gitea.internal:jens/support-bundle-collector.git",
updatedAt: now,
localPath: null,
localStatus: null,
linkState: "remote-only",
deploymentProfiles: [],
},
{
id: 5,
name: "portfolio",
fullName: "jens/portfolio",
owner: { login: "jens" },
description: "Professional infrastructure and automation portfolio.",
private: false,
defaultBranch: "main",
htmlUrl: "https://gitea.internal/jens/portfolio",
cloneUrl: "https://gitea.internal/jens/portfolio.git",
sshUrl: "git@gitea.internal:jens/portfolio.git",
updatedAt: now,
localPath: "C:\\Development\\portfolio",
localStatus: makeStatus({
head: "a7f2e1c1bb6147fc8b6633d2b08500c93402a719",
behind: 1,
}),
linkState: "linked",
deploymentProfiles: [
profile("profile-portfolio", "Production", "production", {
liveSha: "4c20dd11bb6147fc8b6633d2b08500c93402a719",
previousSha: "31adfe11bb6147fc8b6633d2b08500c93402a719",
healthy: false,
}),
],
},
];
const diffs = {
"src/api/deploy.ts": `diff --git a/src/api/deploy.ts b/src/api/deploy.ts\nnew file mode 100644\n--- /dev/null\n+++ b/src/api/deploy.ts\n@@ -0,0 +1,18 @@\n+export interface DeploymentRequest {\n+ environment: 'staging' | 'production';\n+ commitSha: string;\n+}\n+\n+export async function deploy(request: DeploymentRequest) {\n+ return api.post('/deployments', request);\n+}`,
"src/main.tsx": `diff --git a/src/main.tsx b/src/main.tsx\nindex 45ad1a2..939fc17 100644\n--- a/src/main.tsx\n+++ b/src/main.tsx\n@@ -24,8 +24,9 @@ import { Router } from './routes';\n-const API_ENDPOINT = 'http://localhost:3000';\n+const API_ENDPOINT = process.env.VITE_API_URL || '/api';\n+const DEPLOY_VERSION = '1.0.4-rc1';`,
"src/components/Sidebar.tsx": `diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx\nindex a7bbd82..bf21e90 100644\n--- a/src/components/Sidebar.tsx\n+++ b/src/components/Sidebar.tsx\n@@ -31,6 +31,7 @@ export function Sidebar() {\n+ <NavItem to="/deployments">Deployments</NavItem>`,
};
const findRepo = (localPath) =>
repositories.find((item) => item.localPath === localPath);
const findProfileRepo = (profileId) =>
repositories.find((item) =>
item.deploymentProfiles.some((entry) => entry.id === profileId),
);
const syncState = () => {
state.deploymentProfiles = {};
state.deploymentStates = {};
state.repositoryMappings = {};
for (const repository of repositories) {
if (repository.localPath)
state.repositoryMappings[repository.fullName.toLowerCase()] =
repository.localPath;
state.deploymentProfiles[repository.fullName.toLowerCase()] =
repository.deploymentProfiles.map(
({ state: profileState, ...entry }) => entry,
);
for (const entry of repository.deploymentProfiles)
if (entry.state) state.deploymentStates[entry.id] = clone(entry.state);
}
};
const recompute = (repository) => {
const status = repository.localStatus;
if (status) {
status.counts = {
changed: status.files.length,
staged: status.files.filter((item) => item.staged).length,
unstaged: status.files.filter((item) => item.unstaged).length,
conflicts: status.files.filter((item) => item.conflict).length,
untracked: status.files.filter((item) => item.untracked).length,
};
status.clean = status.files.length === 0;
status.shortHead = status.head.slice(0, 7);
status.branch.oid = status.head;
}
repository.favorite = state.favorites.includes(
repository.fullName.toLowerCase(),
);
repository.readyToDeploy = Boolean(
repository.localPath &&
status?.clean &&
status.branch.upstream &&
status.branch.ahead === 0 &&
status.branch.behind === 0 &&
repository.deploymentProfiles.some(
(entry) => entry.branch === status.branch.head,
),
);
repository.attention =
!repository.localPath ||
Boolean(
status?.counts.conflicts ||
status?.branch.behind ||
status?.branch.ahead ||
status?.counts.changed,
);
repository.attentionReason = !repository.localPath
? "No local folder linked"
: status?.counts.conflicts
? `${status.counts.conflicts} conflict(s)`
: status?.counts.changed
? `${status.counts.changed} local change(s)`
: status?.branch.behind
? `${status.branch.behind} commit(s) behind remote`
: status?.branch.ahead
? `${status.branch.ahead} unpushed commit(s)`
: null;
repository.preferredCloneUrl =
state.preferences.preferredCloneProtocol === "ssh"
? repository.sshUrl
: repository.cloneUrl;
};
const snapshot = () => {
repositories.forEach(recompute);
syncState();
return clone(repositories);
};
syncState();
const commitHistory = [
{
sha: "c9182d0d28318c8cf0af109edc054732426aadf1",
shortSha: "c9182d0",
author: "Jens",
date: now,
subject: "feat: add deployment provider contract",
},
{
sha: "1fa7399d28318c8cf0af109edc054732426aadf1",
shortSha: "1fa7399",
author: "Jens",
date: iso(-86400000),
subject: "refactor: consolidate repository state",
},
{
sha: "a251a11d28318c8cf0af109edc054732426aadf1",
shortSha: "a251a11",
author: "Jens",
date: iso(-172800000),
subject: "docs: define deployment safety gates",
},
];
const branchesByRepo = new Map();
const stashesByRepo = new Map();
function updateOperation(operation) {
state.operations = [
clone(operation),
...state.operations.filter((item) => item.id !== operation.id),
].slice(0, 250);
emitOperations([operation]);
return clone(operation);
}
function advanceOperation(operation) {
if (
!operation ||
["success", "failed", "cancelled", "rolled-back"].includes(
operation.status,
)
)
return operation;
operation.demoPolls = (operation.demoPolls || 0) + 1;
if (operation.demoPolls === 1) {
operation.status = "running";
operation.run = {
id: 81,
runNumber: 81,
status: "running",
conclusion: null,
name:
operation.action === "rollback"
? "ForgeFlow rollback"
: "ForgeFlow deployment",
};
operation.runUrl = `https://gitea.internal/${operation.repository}/actions/runs/81`;
operation.stages.find((item) => item.id === "queued").status = "complete";
operation.stages.find((item) => item.id === "runner").status = "active";
operation.jobs = [
{
id: 201,
name: operation.action === "rollback" ? "rollback" : "deploy",
status: "running",
conclusion: null,
},
];
operation.logs.push(`[job] ${operation.jobs[0].name}: running`);
} else if (operation.demoPolls >= 2) {
operation.status =
operation.action === "rollback" ? "rolled-back" : "success";
operation.stages.forEach((item) => {
item.status = "complete";
});
operation.jobs = [
{
id: 201,
name: operation.action === "rollback" ? "rollback" : "deploy",
status: "completed",
conclusion: "success",
},
];
operation.logs.push(
"[ok] Runner completed successfully.",
`[ok] Server status endpoint confirms ${operation.shortSha}.`,
);
const repository = repositories.find(
(item) => item.fullName === operation.repository,
);
const targetProfile = repository?.deploymentProfiles.find(
(item) => item.id === operation.profileId,
);
if (targetProfile) {
const oldLive = targetProfile.state.liveSha;
targetProfile.state.previousSha = oldLive;
targetProfile.state.liveSha = operation.sha;
targetProfile.state.healthy = true;
targetProfile.state.checkedAt = iso();
}
}
operation.updatedAt = iso();
return operation;
}
window.forgeflow = Object.freeze({
async bootstrap() {
await wait(80);
snapshot();
return {
appVersion: "0.8.1-demo",
platform: "win32",
state: clone(state),
git: { available: true, version: "git version 2.47.3" },
diagnostics: {
enabled: true,
level: state.preferences.diagnosticLevel,
retentionDays: state.preferences.logRetentionDays,
maxFileMb: state.preferences.maxLogFileMb,
directory: "<HOME>/AppData/Roaming/ForgeFlow/diagnostics",
fileCount: 2,
totalBytes: 18432,
totalSize: "18.0 KB",
latestAt: iso(-2000),
lastWriteError: null,
},
};
},
async selectDirectory() {
await wait();
return "C:\\Development";
},
async selectKeyFile() {
await wait();
return "C:\\Users\\Jens\\.ssh\\id_ed25519";
},
async setupPreflight({ baseUrl, token, roots = [] }) {
await wait(240);
const checks = [
{
id: "git.available",
label: "Git command line",
status: "pass",
detail: "git version 2.47.3",
required: true,
},
{
id: "git.identity",
label: "Git author identity",
status: "pass",
detail: "Jens <jens@example.invalid>",
required: false,
},
{
id: "storage.userdata",
label: "Application data storage",
status: "pass",
detail: "ForgeFlow can write its local configuration.",
required: true,
},
{
id: "storage.diagnostics",
label: "Diagnostic log storage",
status: "pass",
detail: "The diagnostic directory is writable.",
required: true,
},
{
id: "storage.credentials",
label: "Protected credential storage",
status: "pass",
detail: "The operating system can encrypt the Gitea token at rest.",
required: false,
},
{
id: "workspace.roots",
label: "Development folders",
status: roots.length ? "pass" : "warning",
detail: roots.length
? `${roots.length} folder(s) selected.`
: "No development folder selected yet.",
required: false,
},
{
id: "gitea.connection",
label: "Gitea connection",
status: baseUrl && token ? "pass" : "warning",
detail:
baseUrl && token
? "Connection parameters are ready for validation."
: "Enter the Gitea URL and token.",
required: false,
},
];
return {
kind: "system",
startedAt: iso(-100),
completedAt: iso(),
checks,
summary: {
counts: {
pass: checks.filter((i) => i.status === "pass").length,
warning: checks.filter((i) => i.status === "warning").length,
fail: 0,
skipped: 0,
},
blocking: [],
ready: true,
},
};
},
async validateGitea({ baseUrl, token }) {
await wait(320);
if (!baseUrl || !token)
throw new Error("Enter an instance URL and access token.");
return {
baseUrl: baseUrl.replace(/\/$/, ""),
user: { login: "jens", full_name: "Jens" },
repositoryCount: repositories.length,
version: "1.26.0",
};
},
async completeSetup(payload) {
await wait(300);
state.setupComplete = true;
state.gitea = {
baseUrl: payload.baseUrl,
user: payload.user,
hasToken: true,
};
state.workspaceRoots = payload.workspaceRoots;
storage.set("forgeflow-demo-setup", "true");
return { state: clone(state), tokenState: { persistent: true } };
},
async updateGitea(payload) {
const validation = await this.validateGitea({
...payload,
token: payload.token || "preserved-demo-token",
});
state.gitea = {
baseUrl: validation.baseUrl,
user: validation.user,
hasToken: true,
};
return {
validation,
tokenState: { persistent: true, preserved: !payload.token },
state: clone(state),
};
},
async setWorkspaceRoots(roots) {
state.workspaceRoots = [...new Set(roots)];
return clone(state);
},
async setAppearance(appearance) {
state.appearance = appearance;
storage.set("forgeflow-theme", appearance);
return clone(state);
},
async setPreferences(preferences) {
state.preferences = { ...state.preferences, ...preferences };
snapshot();
return clone(state);
},
async setUpdatePreferences(updates) {
state.updates = { ...state.updates, ...updates };
return clone(state);
},
async checkForUpdates() {
await wait(300);
return {
checkedAt: iso(),
owner: state.updates.owner,
repo: state.updates.repo,
branch: state.updates.branch,
currentVersion: "0.5.4",
remoteVersion: "0.6.0",
remoteSha: "a".repeat(40),
shortSha: "aaaaaaa",
available: true,
mode: "source",
};
},
async downloadUpdate() {
await wait(500);
return {
...(await this.checkForUpdates()),
downloaded: true,
archivePath: "C:\\Temp\\ForgeFlow-0.4.1.zip",
sha256: "b".repeat(64),
};
},
async applyUpdate() {
await wait(200);
return { launched: true, confirmed: true, version: "0.6.0" };
},
async saveServer(server) {
const saved = {
...server,
id: server.id || `server-${Date.now()}`,
hasPassword: server.authType === "password",
hasPassphrase: false,
};
state.servers = [
saved,
...state.servers.filter((item) => item.id !== saved.id),
];
return { server: clone(saved), state: clone(state) };
},
async deleteServer(serverId) {
state.servers = state.servers.filter((item) => item.id !== serverId);
return clone(state);
},
async testServer(serverId) {
const server = state.servers.find((item) => item.id === serverId);
server.hostFingerprint = server.hostFingerprint || "SHA256:demo";
return {
connected: true,
fingerprint: server.hostFingerprint,
server: clone(server),
output: "Linux\n/usr/bin/git\nDocker Compose version v2",
state: clone(state),
};
},
async inspectServerProject() {
return {
exists: true,
rootGit: true,
head: "d42d4a7".padEnd(40, "0"),
branch: "main",
trackedChanges: [],
composeFiles: ["docker-compose.yml"],
nestedGit: ["source"],
dockerfile: true,
};
},
async refreshRepositories() {
await wait(260);
return snapshot();
},
async discoverRepositories() {
await wait(360);
return snapshot()
.filter((repo) => repo.localPath)
.map((repo) => ({
localPath: repo.localPath,
remoteUrl: repo.cloneUrl,
status: repo.localStatus,
}));
},
async favoriteRepository(fullName, favorite) {
const key = fullName.toLowerCase();
state.favorites = favorite
? [...new Set([...state.favorites, key])]
: state.favorites.filter((item) => item !== key);
snapshot();
return clone(state);
},
async linkRepository(fullName, localPath) {
const repo = repositories.find((item) => item.fullName === fullName);
repo.localPath = localPath;
repo.linkState = "linked";
repo.localStatus = makeStatus({ head: randomSha() });
emitRepositories();
return snapshot();
},
async unlinkRepository(fullName) {
const repo = repositories.find((item) => item.fullName === fullName);
repo.localPath = null;
repo.localStatus = null;
repo.linkState = "remote-only";
emitRepositories();
return snapshot();
},
async repositoryStatus(localPath) {
return clone(findRepo(localPath)?.localStatus);
},
async repositoryDiff(localPath, filePath) {
await wait(80);
return (
diffs[filePath] ||
`diff --git a/${filePath} b/${filePath}\n--- a/${filePath}\n+++ b/${filePath}\n@@ -1 +1 @@\n-old\n+new`
);
},
async repositoryDiffHunks(localPath, filePath) {
const diff = await this.repositoryDiff(localPath, filePath);
return {
filePath,
partialSupported: true,
hunks: [
{
index: 0,
heading: "@@ -1 +1 @@",
additions: 1,
deletions: 1,
lines: diff.split("\n").slice(-4),
},
],
};
},
async stageHunks(localPath, filePath) {
return this.stageFiles(localPath, [filePath]);
},
async conflictState(localPath) {
const repo = findRepo(localPath);
const files = repo.localStatus.files
.filter((item) => item.conflict)
.map((item) => item.path);
return {
operation: files.length ? "merge" : null,
files,
canContinue: false,
status: clone(repo.localStatus),
};
},
async resolveConflict(localPath, filePath) {
const repo = findRepo(localPath);
const file = repo.localStatus.files.find(
(item) => item.path === filePath,
);
if (file) {
file.conflict = false;
file.staged = true;
file.unstaged = false;
}
recompute(repo);
return this.conflictState(localPath);
},
async continueGitOperation(localPath) {
return this.conflictState(localPath);
},
async abortGitOperation(localPath) {
return this.conflictState(localPath);
},
async stageFiles(localPath, files) {
const repo = findRepo(localPath);
repo.localStatus.files.forEach((item) => {
if (!files?.length || files.includes(item.path)) {
item.staged = true;
item.unstaged = false;
item.indexCode = item.untracked ? "A" : "M";
item.worktreeCode = ".";
}
});
recompute(repo);
emitRepositories();
return clone(repo.localStatus);
},
async unstageFiles(localPath, files) {
const repo = findRepo(localPath);
repo.localStatus.files.forEach((item) => {
if (!files?.length || files.includes(item.path)) {
item.staged = false;
item.unstaged = true;
item.indexCode = ".";
item.worktreeCode = item.untracked ? "?" : "M";
}
});
recompute(repo);
emitRepositories();
return clone(repo.localStatus);
},
async commit(localPath, message, files) {
await wait(520);
if (!message?.trim()) throw new Error("Enter a commit message.");
const repo = findRepo(localPath);
repo.localStatus.files = repo.localStatus.files.filter(
(item) => !files?.includes(item.path),
);
repo.localStatus.head = randomSha();
repo.localStatus.branch.ahead += 1;
recompute(repo);
emitRepositories();
return {
commitOutput: `[${repo.localStatus.branch.head} ${repo.localStatus.shortHead}] ${message}`,
commitSha: repo.localStatus.head,
status: clone(repo.localStatus),
};
},
async commitAndPush(localPath, message, files) {
const result = await this.commit(localPath, message, files);
const repo = findRepo(localPath);
await wait(240);
repo.localStatus.branch.ahead = 0;
recompute(repo);
emitRepositories();
return {
...result,
pushOutput: "Push completed.",
status: clone(repo.localStatus),
};
},
async commitStaged(localPath, message) {
const repo = findRepo(localPath);
return this.commit(
localPath,
message,
repo.localStatus.files
.filter((item) => item.staged)
.map((item) => item.path),
);
},
async commitStagedAndPush(localPath, message) {
const repo = findRepo(localPath);
return this.commitAndPush(
localPath,
message,
repo.localStatus.files
.filter((item) => item.staged)
.map((item) => item.path),
);
},
async push(localPath) {
await wait(360);
const repo = findRepo(localPath);
repo.localStatus.branch.ahead = 0;
recompute(repo);
emitRepositories();
return { output: "Push completed.", status: clone(repo.localStatus) };
},
async fetch() {
await wait(260);
return { output: "Fetch completed." };
},
async pull(localPath) {
await wait(380);
const repo = findRepo(localPath);
repo.localStatus.branch.behind = 0;
recompute(repo);
emitRepositories();
return { output: "Fast-forwarded.", status: clone(repo.localStatus) };
},
async history() {
await wait(100);
return clone(commitHistory);
},
async branchProtection(fullName, branch) {
return {
branch,
protected: branch === "main",
requiredApprovals: branch === "main" ? 1 : 0,
requireSignedCommits: false,
};
},
async pullRequests() {
return [
{
number: 42,
title: "Harden deployment preflight",
html_url: "https://gitea.internal/jens/vacancyradar/pulls/42",
created_at: iso(-7_200_000),
updated_at: iso(-900_000),
head: { ref: "feature/deployment-api" },
base: { ref: "main" },
},
];
},
async createPullRequest(fullName, title, body, base) {
return {
number: 42,
title,
body,
base,
html_url: `https://gitea.internal/${fullName}/pulls/42`,
};
},
async branches(localPath) {
const repo = findRepo(localPath);
if (!branchesByRepo.has(localPath))
branchesByRepo.set(localPath, [
{
name: repo.localStatus.branch.head,
current: true,
sha: repo.localStatus.head,
shortSha: repo.localStatus.shortHead,
upstream: repo.localStatus.branch.upstream,
},
{
name: "main",
current: repo.localStatus.branch.head === "main",
sha: repo.localStatus.head,
shortSha: repo.localStatus.shortHead,
upstream: "origin/main",
},
]);
return clone(branchesByRepo.get(localPath));
},
async checkoutBranch(localPath, branch) {
const repo = findRepo(localPath);
if (!repo.localStatus.clean)
throw new Error(
"Commit or stash local changes before switching branches.",
);
const list = await this.branches(localPath);
list.forEach((item) => {
item.current = item.name === branch;
});
branchesByRepo.set(localPath, list);
repo.localStatus.branch.head = branch;
repo.localStatus.branch.upstream = `origin/${branch}`;
recompute(repo);
emitRepositories();
return { status: clone(repo.localStatus), branches: clone(list) };
},
async createBranch(localPath, branch) {
const repo = findRepo(localPath);
const list = await this.branches(localPath);
list.forEach((item) => {
item.current = false;
});
list.unshift({
name: branch,
current: true,
sha: repo.localStatus.head,
shortSha: repo.localStatus.shortHead,
upstream: null,
});
branchesByRepo.set(localPath, list);
repo.localStatus.branch.head = branch;
repo.localStatus.branch.upstream = null;
recompute(repo);
emitRepositories();
return { status: clone(repo.localStatus), branches: clone(list) };
},
async stash(localPath, message) {
const repo = findRepo(localPath);
const list = stashesByRepo.get(localPath) || [];
list.unshift({
ref: `stash@{${list.length}}`,
subject: message || "ForgeFlow stash",
date: iso(),
});
stashesByRepo.set(localPath, list);
repo.localStatus.files = [];
recompute(repo);
emitRepositories();
return {
output: "Saved working directory and index state.",
status: clone(repo.localStatus),
stashes: clone(list),
};
},
async stashList(localPath) {
return clone(stashesByRepo.get(localPath) || []);
},
async popStash(localPath, ref) {
const repo = findRepo(localPath);
const list = stashesByRepo.get(localPath) || [];
const index = list.findIndex((item) => item.ref === ref);
if (index < 0) throw new Error("Stash not found.");
list.splice(index, 1);
stashesByRepo.set(localPath, list);
repo.localStatus.files = [makeFile("src/restored-from-stash.ts")];
recompute(repo);
emitRepositories();
return {
output: "Stash applied.",
status: clone(repo.localStatus),
stashes: clone(list),
};
},
async indexLockInfo() {
return { exists: false, ageMs: 0 };
},
async repairIndexLock() {
return { removed: true };
},
async setOrigin(localPath, remoteUrl) {
const repo = findRepo(localPath);
repo.localStatus.remoteUrl = remoteUrl;
repo.sshUrl = remoteUrl;
emitRepositories();
return clone(repo.localStatus);
},
async normalizeOrigins() {
const changes = [];
repositories
.filter((repo) => repo.localPath && repo.sshUrl)
.forEach((repo) => {
if (repo.localStatus.remoteUrl !== repo.sshUrl) {
changes.push({
fullName: repo.fullName,
previous: repo.localStatus.remoteUrl,
next: repo.sshUrl,
});
repo.localStatus.remoteUrl = repo.sshUrl;
}
});
emitRepositories();
return { changes, repositories: snapshot() };
},
async cloneRepository(fullName, mode = "default") {
await wait(620);
const repository = repositories.find(
(item) => item.fullName === fullName,
);
if (!repository) throw new Error("Repository not found.");
if (repository.localPath)
throw new Error("This repository already has a linked local folder.");
const root =
mode === "custom" ? "D:\\OtherProjects" : state.workspaceRoots[0];
if (!root) return { cancelled: true };
const target = `${root.replace(/[\\/]+$/, "")}\\${repository.name}`;
const head = randomSha();
repository.localPath = target;
repository.localStatus = makeStatus({
head,
branch: repository.defaultBranch || "main",
});
repository.localStatus.root = target;
repository.localStatus.remoteUrl =
repository.preferredCloneUrl || repository.cloneUrl;
repository.linkState = "linked";
recompute(repository);
const current = snapshot();
emitRepositories();
return {
target,
status: clone(repository.localStatus),
reused: false,
repositories: current,
state: clone(state),
};
},
async openPath() {
return true;
},
async openEditor() {
return { launched: true, executable: "code" };
},
async openTerminal() {
return { launched: true, executable: "wt.exe" };
},
async openExternal() {
return true;
},
async saveDeploymentProfile(fullName, input) {
const repo = repositories.find((item) => item.fullName === fullName);
const existing = repo.deploymentProfiles.find(
(item) => item.id === input.id,
);
const saved = {
...(existing ||
profile(
input.id || `profile-${Date.now()}`,
input.name || input.environment,
input.environment || "production",
)),
...input,
id: input.id || `profile-${Date.now()}`,
provider: "gitea-actions",
inputs: existing?.inputs || {},
state: existing?.state || {
liveSha: null,
previousSha: null,
healthy: null,
healthConfigured: Boolean(input.healthcheckUrl),
statusConfigured: Boolean(input.statusUrl),
checkedAt: null,
},
};
repo.deploymentProfiles = [
...repo.deploymentProfiles.filter((item) => item.id !== saved.id),
saved,
];
snapshot();
return { profile: clone(saved), state: clone(state) };
},
async deleteDeploymentProfile(fullName, profileId) {
const repo = repositories.find((item) => item.fullName === fullName);
repo.deploymentProfiles = repo.deploymentProfiles.filter(
(item) => item.id !== profileId,
);
snapshot();
return { profiles: clone(repo.deploymentProfiles), state: clone(state) };
},
async deploymentPreflight(repository, profileId) {
await wait(280);
const profile = repository.deploymentProfiles.find(
(item) => item.id === profileId,
);
const status = repository.localStatus;
const checks = [
{
id: "repository.linked",
label: "Local repository link",
status: repository.localPath ? "pass" : "fail",
detail: repository.localPath || "No local folder linked.",
required: true,
},
{
id: "git.branch",
label: "Allowed branch",
status: status?.branch.head === profile?.branch ? "pass" : "fail",
detail: `Current: ${status?.branch.head || "unknown"}; required: ${profile?.branch || "unknown"}.`,
required: true,
},
{
id: "git.clean",
label: "Clean working tree",
status: status?.clean ? "pass" : "fail",
detail: status?.clean
? "No uncommitted changes."
: `${status?.counts.changed || 0} changed file(s).`,
required: true,
},
{
id: "git.sync",
label: "Local and Gitea synchronized",
status:
!status?.branch.ahead && !status?.branch.behind ? "pass" : "fail",
detail: `${status?.branch.ahead || 0} ahead, ${status?.branch.behind || 0} behind.`,
required: true,
},
{
id: "workflow.deploy.remote",
label: "Deploy workflow on Gitea branch",
status: "pass",
detail: `${profile?.workflowFile || "deploy.yml"} exists on ${profile?.branch || "main"}.`,
required: true,
},
{
id: "gitea.actions",
label: "Gitea Actions API",
status: "pass",
detail: "The Actions runs endpoint is accessible.",
required: true,
},
{
id: "server.status",
label: "Server version endpoint",
status: profile?.statusUrl ? "pass" : "warning",
detail: profile?.statusUrl
? `Endpoint reachable; live ${profile.state?.liveSha?.slice(0, 7) || "unknown"}.`
: "No status URL configured.",
required: false,
},
{
id: "server.health",
label: "Application healthcheck",
status: profile?.healthcheckUrl ? "pass" : "warning",
detail: profile?.healthcheckUrl
? "HTTP 200 in 42 ms."
: "No healthcheck URL configured.",
required: false,
},
];
const blocking = checks
.filter((i) => i.required && i.status === "fail")
.map((i) => i.id);
return {
kind: "deployment",
repository: repository.fullName,
profileId,
startedAt: iso(-100),
completedAt: iso(),
checks,
summary: {
counts: {
pass: checks.filter((i) => i.status === "pass").length,
warning: checks.filter((i) => i.status === "warning").length,
fail: checks.filter((i) => i.status === "fail").length,
skipped: 0,
},
blocking,
ready: blocking.length === 0,
},
head: status?.head || null,
};
},
async deploy(repository, profileId, sha) {
await wait(320);
const selected = repository.deploymentProfiles.find(
(item) => item.id === profileId,
);
const operation = {
id: `deploy-${Date.now()}`,
type: "deployment",
action: "deploy",
status: "queued",
repository: repository.fullName,
profileId,
profileName: selected.name,
environment: selected.environment,
workflowFile: selected.workflowFile,
branch: selected.branch,
sha,
shortSha: sha.slice(0, 7),
dispatchedAt: iso(),
createdAt: iso(),
updatedAt: iso(),
demoPolls: 0,
stages: [
{ id: "requested", label: "Requested", status: "complete" },
{ id: "verified", label: "Verified", status: "complete" },
{ id: "queued", label: "Workflow queued", status: "active" },
{ id: "runner", label: "Runner execution", status: "pending" },
{ id: "healthcheck", label: "Healthcheck", status: "pending" },
{ id: "complete", label: "Complete", status: "pending" },
],
logs: [
`[info] Verified clean ${selected.branch} at ${sha}`,
`[ok] Gitea accepted ${selected.workflowFile}.`,
],
};
return updateOperation(operation);
},
async rollback(repository, profileId, targetSha) {
await wait(320);
const selected = repository.deploymentProfiles.find(
(item) => item.id === profileId,
);
const operation = {
id: `rollback-${Date.now()}`,
type: "deployment",
action: "rollback",
status: "queued",
repository: repository.fullName,
profileId,
profileName: selected.name,
environment: selected.environment,
workflowFile: selected.rollbackWorkflowFile,
branch: selected.branch,
sha: targetSha,
shortSha: targetSha.slice(0, 7),
dispatchedAt: iso(),
createdAt: iso(),
updatedAt: iso(),
demoPolls: 0,
stages: [
{ id: "requested", label: "Requested", status: "complete" },
{ id: "verified", label: "Verified", status: "complete" },
{ id: "queued", label: "Workflow queued", status: "active" },
{ id: "runner", label: "Runner execution", status: "pending" },
{ id: "healthcheck", label: "Healthcheck", status: "pending" },
{ id: "complete", label: "Complete", status: "pending" },
],
logs: [
`[warning] Rollback target verified: ${targetSha}`,
`[ok] Gitea accepted ${selected.rollbackWorkflowFile}.`,
],
};
return updateOperation(operation);
},
async healthcheck() {
await wait(160);
return { configured: true, healthy: true, status: 200, latencyMs: 42 };
},
async refreshProfileState(fullName, profileId) {
await wait(240);
const repo =
repositories.find((item) => item.fullName === fullName) ||
findProfileRepo(profileId);
const target = repo?.deploymentProfiles.find(
(item) => item.id === profileId,
);
if (!target) throw new Error("Deployment profile not found.");
target.state = {
...target.state,
checkedAt: iso(),
healthy: target.state.healthy !== false,
healthConfigured: Boolean(target.healthcheckUrl),
statusConfigured: Boolean(target.statusUrl),
};
syncState();
return clone(target.state);
},
async refreshOperations(operationId = null) {
await wait(300);
if (operationId) {
const operation = state.operations.find(
(item) => item.id === operationId,
);
if (!operation) throw new Error("Operation not found.");
return updateOperation(advanceOperation(operation));
}
const active = state.operations
.filter(
(item) =>
!["success", "failed", "cancelled", "rolled-back"].includes(
item.status,
),
)
.map(advanceOperation);
if (active.length) emitOperations(active);
state.operations = state.operations.map(
(item) => active.find((entry) => entry.id === item.id) || item,
);
return clone(active);
},
async getOperation(operationId) {
return clone(
state.operations.find((item) => item.id === operationId) || null,
);
},
async diagnosticsStatus() {
return {
enabled: state.preferences.diagnosticsEnabled !== false,
level: state.preferences.diagnosticLevel,
retentionDays: state.preferences.logRetentionDays,
maxFileMb: state.preferences.maxLogFileMb,
directory: "<HOME>/AppData/Roaming/ForgeFlow/diagnostics",
fileCount: 2,
totalBytes: 18432,
totalSize: "18.0 KB",
latestAt: iso(-2000),
lastWriteError: null,
};
},
async exportConfigurationBackup() {
return {
filePath: "C:\\Downloads\\ForgeFlow-Configuration-demo.ffbackup",
};
},
async importConfigurationBackup() {
return { state: clone(state), exportedAt: iso(-86400000) };
},
async listAuditEvents() {
return [
{
id: "audit-1",
timestamp: iso(-60000),
event: "deployment.completed",
details: { repository: "Jens/ForgeFlow", result: "success" },
},
];
},
async exportAuditLog() {
return { filePath: "C:\\Downloads\\ForgeFlow-Audit-demo.json", count: 1 };
},
async clearDiagnostics() {
return {
enabled: true,
level: state.preferences.diagnosticLevel,
retentionDays: state.preferences.logRetentionDays,
maxFileMb: state.preferences.maxLogFileMb,
directory: "<HOME>/AppData/Roaming/ForgeFlow/diagnostics",
fileCount: 1,
totalBytes: 256,
totalSize: "256 B",
latestAt: iso(),
lastWriteError: null,
};
},
async openDiagnosticsFolder() {
return true;
},
async exportDiagnostics(privacyMode = "standard") {
await wait(500);
return {
path: `C:\Users\Jens\Downloads\ForgeFlow-Diagnostics-demo.zip`,
bytes: 38221,
size: "37.3 KB",
sha256: "b".repeat(64),
privacyMode,
generatedAt: iso(),
};
},
async showDiagnosticBundle() {
return true;
},
async reportRendererEvent() {
return true;
},
onRepositoriesChanged(listener) {
repositoryListeners.add(listener);
return () => repositoryListeners.delete(listener);
},
onOperationsChanged(listener) {
operationListeners.add(listener);
return () => operationListeners.delete(listener);
},
onUpdatesChanged(listener) {
updateListeners.add(listener);
return () => updateListeners.delete(listener);
},
async reset() {
state.setupComplete = false;
storage.set("forgeflow-demo-setup", "false");
return clone(state);
},
});
})();