feat: Enhance Git tools installation process with progress tracking and Windows path management. Added UI updates for installation steps, including status and progress indicators, to improve user experience during setup.
This commit is contained in:
parent
1573df675b
commit
c78159d442
|
|
@ -0,0 +1 @@
|
|||
nodeLinker: node-modules
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "yarn build:main && concurrently -k \"vite\" \"wait-on http://localhost:5173 && cross-env NODE_OPTIONS= VITE_DEV_SERVER_URL=http://localhost:5173 VITE_DEV_UI=1 electron .\"",
|
||||
"dev:ui": "yarn build:main && concurrently -k \"vite\" \"wait-on http://localhost:5173 && cross-env NODE_OPTIONS= VITE_DEV_SERVER_URL=http://localhost:5173 VITE_DEV_UI=1 electron .\"",
|
||||
"build:renderer": "vite build",
|
||||
"build:main": "tsc -p tsconfig.main.json",
|
||||
"build": "yarn build:renderer && yarn build:main",
|
||||
|
|
|
|||
|
|
@ -104,6 +104,8 @@ type InstallStep = {
|
|||
ok: boolean;
|
||||
message?: string;
|
||||
result?: CommandRunResult;
|
||||
status?: "running" | "done" | "error";
|
||||
progress?: { current: number; total: number; percent: number };
|
||||
};
|
||||
|
||||
type InstallGitToolsResult = {
|
||||
|
|
@ -194,7 +196,35 @@ const checkCommand = async (
|
|||
});
|
||||
};
|
||||
|
||||
const refreshWindowsPath = async () => {
|
||||
if (process.platform !== "win32") {
|
||||
return;
|
||||
}
|
||||
const result = await runCommand(
|
||||
"powershell",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
"[Environment]::GetEnvironmentVariable('Path','Machine');[Environment]::GetEnvironmentVariable('Path','User')",
|
||||
],
|
||||
COMMAND_TIMEOUT_MS,
|
||||
);
|
||||
if (!result.ok || !result.output) {
|
||||
return;
|
||||
}
|
||||
const merged = result.output
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.join(";");
|
||||
const combined = [merged, process.env.PATH].filter(Boolean).join(";");
|
||||
const unique = Array.from(new Set(combined.split(";").filter(Boolean))).join(";");
|
||||
process.env.PATH = unique;
|
||||
process.env.Path = unique;
|
||||
};
|
||||
|
||||
const checkGitTools = async () => {
|
||||
await refreshWindowsPath();
|
||||
const git = await checkCommand("git", ["--version"]);
|
||||
const lfs = await checkCommand("git", ["lfs", "version"]);
|
||||
|
||||
|
|
@ -306,6 +336,7 @@ const runCommand = async (
|
|||
};
|
||||
|
||||
const getGitPaths = async () => {
|
||||
await refreshWindowsPath();
|
||||
const git = await resolveCommandPath("git");
|
||||
const lfs = await resolveCommandPath("git-lfs");
|
||||
|
||||
|
|
@ -985,6 +1016,24 @@ const installGitTools = async (
|
|||
}
|
||||
|
||||
const steps: InstallStep[] = [];
|
||||
const totalSteps = 3;
|
||||
const emitProgress = (
|
||||
name: string,
|
||||
current: number,
|
||||
status: InstallStep["status"],
|
||||
message?: string,
|
||||
result?: CommandRunResult,
|
||||
) => {
|
||||
const percent = Math.round((current / totalSteps) * 100);
|
||||
onProgress?.({
|
||||
name,
|
||||
ok: status !== "error",
|
||||
message,
|
||||
status,
|
||||
progress: { current, total: totalSteps, percent },
|
||||
result,
|
||||
});
|
||||
};
|
||||
|
||||
const wingetPath = await resolveCommandPath("winget");
|
||||
if (!wingetPath.ok) {
|
||||
|
|
@ -998,6 +1047,8 @@ const installGitTools = async (
|
|||
args: ["--version"],
|
||||
error: wingetPath.error,
|
||||
},
|
||||
status: "error" as const,
|
||||
progress: { current: 1, total: totalSteps, percent: 33 },
|
||||
};
|
||||
steps.push(errorStep);
|
||||
onProgress?.(errorStep);
|
||||
|
|
@ -1020,14 +1071,18 @@ const installGitTools = async (
|
|||
// skipping separate success step for winget to match previous behavior purely?
|
||||
// User asked for "same template", so seeing "Winget Check... OK" is good.
|
||||
// I will add it for clarity.
|
||||
emitProgress("winget 확인", 1, "running");
|
||||
const wingetStep = {
|
||||
name: "winget 확인",
|
||||
ok: true,
|
||||
result: { ok: true, command: "winget", args: ["--version"] }
|
||||
result: { ok: true, command: "winget", args: ["--version"] },
|
||||
status: "done" as const,
|
||||
progress: { current: 1, total: totalSteps, percent: 33 },
|
||||
};
|
||||
steps.push(wingetStep);
|
||||
onProgress?.(wingetStep);
|
||||
|
||||
emitProgress("Git 설치", 2, "running");
|
||||
const gitInstall = await runCommand(
|
||||
"winget",
|
||||
[
|
||||
|
|
@ -1044,18 +1099,18 @@ const installGitTools = async (
|
|||
],
|
||||
INSTALL_TIMEOUT_MS,
|
||||
);
|
||||
steps.push({
|
||||
const gitInstallStep: InstallStep = {
|
||||
name: "Git 설치",
|
||||
ok: gitInstall.ok,
|
||||
message: gitInstall.output || gitInstall.error, // Prioritize output
|
||||
result: gitInstall
|
||||
});
|
||||
onProgress?.({
|
||||
name: "Git 설치",
|
||||
ok: gitInstall.ok,
|
||||
message: gitInstall.output || gitInstall.error // Prioritize output
|
||||
});
|
||||
message: gitInstall.ok ? undefined : (gitInstall.error ?? gitInstall.output),
|
||||
result: gitInstall,
|
||||
status: gitInstall.ok ? "done" : "error",
|
||||
progress: { current: 2, total: totalSteps, percent: 67 },
|
||||
};
|
||||
steps.push(gitInstallStep);
|
||||
onProgress?.(gitInstallStep);
|
||||
|
||||
emitProgress("Git LFS 설치", 3, "running");
|
||||
const lfsInstall = await runCommand(
|
||||
"winget",
|
||||
[
|
||||
|
|
@ -1072,19 +1127,19 @@ const installGitTools = async (
|
|||
],
|
||||
INSTALL_TIMEOUT_MS,
|
||||
);
|
||||
steps.push({
|
||||
const lfsInstallStep: InstallStep = {
|
||||
name: "Git LFS 설치",
|
||||
ok: lfsInstall.ok,
|
||||
message: lfsInstall.output || lfsInstall.error, // Prioritize output
|
||||
result: lfsInstall
|
||||
});
|
||||
onProgress?.({
|
||||
name: "Git LFS 설치",
|
||||
ok: lfsInstall.ok,
|
||||
message: lfsInstall.output || lfsInstall.error // Prioritize output
|
||||
});
|
||||
message: lfsInstall.ok ? undefined : (lfsInstall.error ?? lfsInstall.output),
|
||||
result: lfsInstall,
|
||||
status: lfsInstall.ok ? "done" : "error",
|
||||
progress: { current: 3, total: totalSteps, percent: 100 },
|
||||
};
|
||||
steps.push(lfsInstallStep);
|
||||
onProgress?.(lfsInstallStep);
|
||||
|
||||
const ok = steps.every((step) => step.ok);
|
||||
await refreshWindowsPath();
|
||||
const check = await checkGitTools();
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ const App = () => {
|
|||
const [skipToolsCheck, setSkipToolsCheck] = useState(false);
|
||||
const [devProceedReady, setDevProceedReady] = useState(false);
|
||||
const [simulateToolsMissing, setSimulateToolsMissing] = useState(false);
|
||||
const [simulateInstallProgress, setSimulateInstallProgress] = useState(false);
|
||||
const [gitCheckInProgress, setGitCheckInProgress] = useState(false);
|
||||
const [gitCheckedAt, setGitCheckedAt] = useState<number | undefined>();
|
||||
const [gitCheckResult, setGitCheckResult] = useState<GitToolsCheck | undefined>();
|
||||
|
|
@ -100,8 +101,17 @@ const App = () => {
|
|||
const sessionResumeAttemptedRef = useRef(false);
|
||||
const transitionTimeoutRef = useRef<number | undefined>();
|
||||
const simulateToolsMissingRef = useRef(false);
|
||||
const devInstallTimerRef = useRef<number | undefined>();
|
||||
const [modalTitle, setModalTitle] = useState("모드 적용 중...");
|
||||
const [syncSteps, setSyncSteps] = useState<Array<{ name: string; ok: boolean; message?: string }>>([]);
|
||||
const [syncSteps, setSyncSteps] = useState<
|
||||
Array<{
|
||||
name: string;
|
||||
ok: boolean;
|
||||
message?: string;
|
||||
status?: "running" | "done" | "error";
|
||||
progress?: { current: number; total: number; percent: number };
|
||||
}>
|
||||
>([]);
|
||||
const [syncResult, setSyncResult] = useState<{ ok: boolean; error?: string } | undefined>();
|
||||
|
||||
const getStepDisplayInfo = (rawName: string) => {
|
||||
|
|
@ -134,6 +144,27 @@ const App = () => {
|
|||
return { title: rawName, command: "" };
|
||||
};
|
||||
|
||||
const upsertSyncStep = useCallback(
|
||||
(nextStep: {
|
||||
name: string;
|
||||
ok: boolean;
|
||||
message?: string;
|
||||
status?: "running" | "done" | "error";
|
||||
progress?: { current: number; total: number; percent: number };
|
||||
}) => {
|
||||
setSyncSteps((prev) => {
|
||||
const index = prev.findIndex((step) => step.name === nextStep.name);
|
||||
if (index < 0) {
|
||||
return [...prev, nextStep];
|
||||
}
|
||||
const updated = [...prev];
|
||||
updated[index] = { ...updated[index], ...nextStep };
|
||||
return updated;
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const asObject = (value: unknown) =>
|
||||
value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
||||
const pickString = (...values: unknown[]) =>
|
||||
|
|
@ -512,6 +543,14 @@ const App = () => {
|
|||
simulateToolsMissingRef.current = simulateToolsMissing;
|
||||
}, [simulateToolsMissing]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (devInstallTimerRef.current) {
|
||||
window.clearTimeout(devInstallTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => clearTransitionTimeout();
|
||||
}, [clearTransitionTimeout]);
|
||||
|
|
@ -578,6 +617,15 @@ const App = () => {
|
|||
return Array.from(tags);
|
||||
}, [modVersionStatus]);
|
||||
|
||||
const syncProgress = useMemo(() => {
|
||||
for (let i = syncSteps.length - 1; i >= 0; i -= 1) {
|
||||
if (syncSteps[i].progress) {
|
||||
return { progress: syncSteps[i].progress, stepName: syncSteps[i].name };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}, [syncSteps]);
|
||||
|
||||
const startReady = Boolean(sptInstallInfo?.ok && modVersionStatus?.ok && !syncInProgress);
|
||||
const startStatusLabel = syncInProgress
|
||||
? "동기화 중"
|
||||
|
|
@ -655,7 +703,14 @@ const App = () => {
|
|||
setSyncInProgress(true);
|
||||
|
||||
const cleanup = window.sptLauncher.onGitInstallProgress((_e, step: any) => {
|
||||
setSyncSteps((prev) => [...prev, step]);
|
||||
const normalized = {
|
||||
name: step.name,
|
||||
ok: Boolean(step.ok),
|
||||
message: step.ok ? undefined : step.message,
|
||||
status: step.status,
|
||||
progress: step.progress
|
||||
};
|
||||
upsertSyncStep(normalized);
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
@ -682,6 +737,60 @@ const App = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const runDevInstallSimulation = useCallback(() => {
|
||||
if (!DEV_UI_ENABLED) {
|
||||
return;
|
||||
}
|
||||
if (devInstallTimerRef.current) {
|
||||
window.clearTimeout(devInstallTimerRef.current);
|
||||
}
|
||||
setSimulateInstallProgress(true);
|
||||
setInstallInProgress(true);
|
||||
setInstallMessage(undefined);
|
||||
setSyncSteps([]);
|
||||
setSyncResult(undefined);
|
||||
setModalTitle("Git 도구 설치 중...");
|
||||
setSyncInProgress(true);
|
||||
|
||||
const steps = [
|
||||
{ name: "winget 확인", percent: 33 },
|
||||
{ name: "Git 설치", percent: 67 },
|
||||
{ name: "Git LFS 설치", percent: 100 }
|
||||
];
|
||||
|
||||
let index = 0;
|
||||
const tick = () => {
|
||||
const current = steps[index];
|
||||
upsertSyncStep({
|
||||
name: current.name,
|
||||
ok: true,
|
||||
status: "running",
|
||||
progress: { current: index + 1, total: steps.length, percent: current.percent }
|
||||
});
|
||||
|
||||
devInstallTimerRef.current = window.setTimeout(() => {
|
||||
upsertSyncStep({
|
||||
name: current.name,
|
||||
ok: true,
|
||||
status: "done",
|
||||
progress: { current: index + 1, total: steps.length, percent: current.percent }
|
||||
});
|
||||
index += 1;
|
||||
if (index < steps.length) {
|
||||
tick();
|
||||
return;
|
||||
}
|
||||
setInstallMessage("자동 설치가 완료되었습니다.");
|
||||
setSyncResult({ ok: true });
|
||||
setInstallInProgress(false);
|
||||
setSyncInProgress(false);
|
||||
setSimulateInstallProgress(false);
|
||||
}, 800);
|
||||
};
|
||||
|
||||
tick();
|
||||
}, [upsertSyncStep]);
|
||||
|
||||
const handleFetchGitPaths = async () => {
|
||||
if (!window.sptLauncher?.getGitPaths) {
|
||||
window.alert("경로 확인 기능이 준비되지 않았습니다.");
|
||||
|
|
@ -930,19 +1039,13 @@ const App = () => {
|
|||
? window.sptLauncher.onModSyncProgress((_event, step) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const s = step as any;
|
||||
setSyncSteps((prev) => {
|
||||
// Update existing step if name matches, or add new
|
||||
// Ideally steps come in order. Let's just append or update last?
|
||||
// The event sends "completed" steps usually?
|
||||
// My main implementation sends: { name, result: { ok... } }
|
||||
// I'll map it to simplified UI step
|
||||
const uiStep = {
|
||||
name: s.name,
|
||||
ok: s.result?.ok ?? false,
|
||||
message: s.result?.message ?? s.result?.error
|
||||
message: s.result?.ok ? undefined : s.result?.message ?? s.result?.error,
|
||||
status: s.result?.ok ? "done" : "error"
|
||||
};
|
||||
return [...prev, uiStep];
|
||||
});
|
||||
upsertSyncStep(uiStep);
|
||||
})
|
||||
: () => {};
|
||||
|
||||
|
|
@ -959,11 +1062,14 @@ const App = () => {
|
|||
// If steps are returned in result (history), use them?
|
||||
// The real-time listener should have captured them, but let's ensure we have full list
|
||||
if (result.steps) {
|
||||
setSyncSteps(result.steps.map(s => ({
|
||||
setSyncSteps(
|
||||
result.steps.map((s) => ({
|
||||
name: s.name,
|
||||
ok: s.ok,
|
||||
message: s.message
|
||||
})));
|
||||
message: s.ok ? undefined : s.message,
|
||||
status: s.ok ? "done" : "error"
|
||||
}))
|
||||
);
|
||||
}
|
||||
setModCheckMessage(result.error ?? "모드 동기화에 실패했습니다.");
|
||||
return false;
|
||||
|
|
@ -972,11 +1078,14 @@ const App = () => {
|
|||
setSyncResult({ ok: true });
|
||||
// Ensure steps from result are shown (more reliable final state)
|
||||
if (result.steps) {
|
||||
setSyncSteps(result.steps.map(s => ({
|
||||
setSyncSteps(
|
||||
result.steps.map((s) => ({
|
||||
name: s.name,
|
||||
ok: s.ok,
|
||||
message: s.message
|
||||
})));
|
||||
message: s.ok ? undefined : s.message,
|
||||
status: s.ok ? "done" : "error"
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
await applyModSyncResult(result.tag ?? targetTag);
|
||||
|
|
@ -1142,6 +1251,16 @@ const App = () => {
|
|||
{simulateToolsMissing ? "Git/LFS 시뮬레이션 해제" : "Git/LFS 미설치 시뮬레이션"}
|
||||
</button>
|
||||
)}
|
||||
{DEV_UI_ENABLED && (
|
||||
<button
|
||||
type="button"
|
||||
className="ghost"
|
||||
onClick={runDevInstallSimulation}
|
||||
disabled={simulateInstallProgress || syncInProgress || installInProgress}
|
||||
>
|
||||
{simulateInstallProgress ? "설치 진행 시뮬레이션 중..." : "설치 진행 시뮬레이션"}
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
|
@ -1478,18 +1597,43 @@ const App = () => {
|
|||
)}
|
||||
</header>
|
||||
<div className="modal-body">
|
||||
{syncProgress?.progress && (
|
||||
<div className="sync-progress">
|
||||
<div className="sync-progress-header">
|
||||
<span>진행률(Progress)</span>
|
||||
<span>{syncProgress.progress.percent}%</span>
|
||||
</div>
|
||||
<div className="sync-progress-bar">
|
||||
<div
|
||||
className="sync-progress-fill"
|
||||
style={{ width: `${syncProgress.progress.percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
{syncProgress.stepName && (
|
||||
<div className="sync-progress-step">
|
||||
현재 단계(Current Step): {getStepDisplayInfo(syncProgress.stepName).title}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="sync-steps">
|
||||
{syncSteps.map((step, idx) => {
|
||||
const info = getStepDisplayInfo(step.name);
|
||||
const stepClass =
|
||||
step.status === "running" ? "running" : step.ok ? "ok" : "fail";
|
||||
const stepIcon =
|
||||
step.status === "running" ? "⏳" : step.ok ? "✅" : "❌";
|
||||
return (
|
||||
<div key={idx} className={`sync-step ${step.ok ? "ok" : "fail"}`}>
|
||||
<div key={idx} className={`sync-step ${stepClass}`}>
|
||||
<span className="step-number">{idx + 1}.</span>
|
||||
<div className="step-content">
|
||||
<div className="step-title">{info.title}</div>
|
||||
<div className="step-command">{info.command}</div>
|
||||
{step.message && <div className="step-msg">Error: {step.message}</div>}
|
||||
{!step.ok && step.message && (
|
||||
<div className="step-msg">오류(Error): {step.message}</div>
|
||||
)}
|
||||
</div>
|
||||
<span className="step-icon">{step.ok ? "✅" : "❌"}</span>
|
||||
<span className="step-icon">{stepIcon}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -710,6 +710,11 @@ button.ghost:hover:not(:disabled) {
|
|||
background: rgba(16, 185, 129, 0.05);
|
||||
}
|
||||
|
||||
.sync-step.running {
|
||||
border-left: 3px solid var(--status-warn);
|
||||
background: rgba(245, 158, 11, 0.05);
|
||||
}
|
||||
|
||||
.sync-step.fail {
|
||||
border-left: 3px solid var(--status-error);
|
||||
background: rgba(239, 68, 68, 0.05);
|
||||
|
|
@ -769,6 +774,42 @@ button.ghost:hover:not(:disabled) {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.sync-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.sync-progress-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.sync-progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sync-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary), var(--primary-hover));
|
||||
transition: width 0.2s ease-out;
|
||||
}
|
||||
|
||||
.sync-progress-step {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
|
|
|
|||
Loading…
Reference in New Issue