refactor: split renderer ipc and unraid domains
This commit is contained in:
@@ -0,0 +1,635 @@
|
||||
function createMockRepositoryBridge(context) {
|
||||
const { wait, clone, iso, storage, repositoryListeners, operationListeners, updateListeners, emitRepositories, emitOperations, randomSha, now, profile, state, repositories, recompute, snapshot, commitHistory, advanceOperation, syncState, diffs, findRepo, findProfileRepo, branchesByRepo, stashesByRepo, updateOperation } = context;
|
||||
return {
|
||||
async bootstrap() {
|
||||
await wait(80);
|
||||
snapshot();
|
||||
return {
|
||||
appVersion: "0.10.0-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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user