Harden SQLite migration and apply post-merge fixes
Migration hardening: - Back up JSON to $PASEO_HOME/backup/pre-migration/ before import - Deduplicate projects/workspaces by path before insert - Log per-batch progress during agent snapshot import - Clear error messages for corrupt JSON files - 5 new test cases for backup, dedup, progress, and error clarity Post-merge fixes: - Add worktreeRoot to session checkout result (type error fix) - Port reload agent tab action from main - Remove deleted useDelayedHistoryRefreshToast hook usage
This commit is contained in:
@@ -20,7 +20,6 @@ import {
|
|||||||
type AgentScreenMissingState,
|
type AgentScreenMissingState,
|
||||||
} from "@/hooks/use-agent-screen-state-machine";
|
} from "@/hooks/use-agent-screen-state-machine";
|
||||||
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
import { useArchiveAgent } from "@/hooks/use-archive-agent";
|
||||||
import { useDelayedHistoryRefreshToast } from "@/hooks/use-delayed-history-refresh-toast";
|
|
||||||
import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
|
import { useAgentInputDraft } from "@/hooks/use-agent-input-draft";
|
||||||
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
||||||
import { useStableEvent } from "@/hooks/use-stable-event";
|
import { useStableEvent } from "@/hooks/use-stable-event";
|
||||||
@@ -808,28 +807,6 @@ function ChatAgentContent({
|
|||||||
setMissingAgentState({ kind: "idle" });
|
setMissingAgentState({ kind: "idle" });
|
||||||
}, [agentId, serverId]);
|
}, [agentId, serverId]);
|
||||||
|
|
||||||
const isHistoryRefreshCatchingUp =
|
|
||||||
viewState.tag === "ready" &&
|
|
||||||
viewState.sync.status === "catching_up" &&
|
|
||||||
viewState.sync.ui === "toast";
|
|
||||||
const shouldEmitSyncErrorToast =
|
|
||||||
viewState.tag === "ready" &&
|
|
||||||
viewState.sync.status === "sync_error" &&
|
|
||||||
viewState.sync.shouldEmitSyncErrorToast;
|
|
||||||
|
|
||||||
useDelayedHistoryRefreshToast({
|
|
||||||
isCatchingUp: isHistoryRefreshCatchingUp,
|
|
||||||
indicatorColor: theme.colors.primary,
|
|
||||||
showToast: panelToast.api.show,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!shouldEmitSyncErrorToast) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
panelToast.api.error("Failed to refresh agent. Retrying in background.");
|
|
||||||
}, [panelToast.api, shouldEmitSyncErrorToast]);
|
|
||||||
|
|
||||||
if (viewState.tag === "not_found") {
|
if (viewState.tag === "not_found") {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container} testID="agent-not-found">
|
<View style={styles.container} testID="agent-not-found">
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ type WorkspaceDesktopTabsRowProps = {
|
|||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
||||||
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
||||||
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
||||||
@@ -332,6 +333,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onReloadAgent,
|
||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
onCloseTabsToRight,
|
onCloseTabsToRight,
|
||||||
onCloseOtherTabs,
|
onCloseOtherTabs,
|
||||||
@@ -453,6 +455,7 @@ export function WorkspaceDesktopTabsRow({
|
|||||||
normalizedWorkspaceId={normalizedWorkspaceId}
|
normalizedWorkspaceId={normalizedWorkspaceId}
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
|
onReloadAgent={onReloadAgent}
|
||||||
onCloseTabsToLeft={onCloseTabsToLeft}
|
onCloseTabsToLeft={onCloseTabsToLeft}
|
||||||
onCloseTabsToRight={onCloseTabsToRight}
|
onCloseTabsToRight={onCloseTabsToRight}
|
||||||
onCloseOtherTabs={onCloseOtherTabs}
|
onCloseOtherTabs={onCloseOtherTabs}
|
||||||
@@ -583,6 +586,7 @@ function ResolvedDesktopTabChip({
|
|||||||
normalizedWorkspaceId,
|
normalizedWorkspaceId,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onReloadAgent,
|
||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
onCloseTabsToRight,
|
onCloseTabsToRight,
|
||||||
onCloseOtherTabs,
|
onCloseOtherTabs,
|
||||||
@@ -606,6 +610,7 @@ function ResolvedDesktopTabChip({
|
|||||||
normalizedWorkspaceId: string;
|
normalizedWorkspaceId: string;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
onCloseTabsToLeft: (tabId: string) => Promise<void> | void;
|
||||||
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
onCloseTabsToRight: (tabId: string) => Promise<void> | void;
|
||||||
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
onCloseOtherTabs: (tabId: string) => Promise<void> | void;
|
||||||
@@ -628,6 +633,7 @@ function ResolvedDesktopTabChip({
|
|||||||
tabCount,
|
tabCount,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onReloadAgent,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCloseTabsToLeft,
|
onCloseTabsToLeft,
|
||||||
onCloseTabsToRight,
|
onCloseTabsToRight,
|
||||||
@@ -642,6 +648,7 @@ function ResolvedDesktopTabChip({
|
|||||||
onCloseTabsToRight,
|
onCloseTabsToRight,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
|
onReloadAgent,
|
||||||
tabCount,
|
tabCount,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
Ellipsis,
|
Ellipsis,
|
||||||
EllipsisVertical,
|
EllipsisVertical,
|
||||||
PanelRight,
|
PanelRight,
|
||||||
|
RotateCw,
|
||||||
SquarePen,
|
SquarePen,
|
||||||
SquareTerminal,
|
SquareTerminal,
|
||||||
X,
|
X,
|
||||||
@@ -186,6 +187,7 @@ type MobileWorkspaceTabSwitcherProps = {
|
|||||||
onSelectSwitcherTab: (key: string) => void;
|
onSelectSwitcherTab: (key: string) => void;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
onCloseTabsAbove: (tabId: string) => Promise<void> | void;
|
onCloseTabsAbove: (tabId: string) => Promise<void> | void;
|
||||||
onCloseTabsBelow: (tabId: string) => Promise<void> | void;
|
onCloseTabsBelow: (tabId: string) => Promise<void> | void;
|
||||||
@@ -273,6 +275,7 @@ function MobileWorkspaceTabOption({
|
|||||||
onPress,
|
onPress,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onReloadAgent,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCloseTabsAbove,
|
onCloseTabsAbove,
|
||||||
onCloseTabsBelow,
|
onCloseTabsBelow,
|
||||||
@@ -288,6 +291,7 @@ function MobileWorkspaceTabOption({
|
|||||||
onPress: () => void;
|
onPress: () => void;
|
||||||
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
onCopyResumeCommand: (agentId: string) => Promise<void> | void;
|
||||||
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
onCopyAgentId: (agentId: string) => Promise<void> | void;
|
||||||
|
onReloadAgent: (agentId: string) => Promise<void> | void;
|
||||||
onCloseTab: (tabId: string) => Promise<void> | void;
|
onCloseTab: (tabId: string) => Promise<void> | void;
|
||||||
onCloseTabsAbove: (tabId: string) => Promise<void> | void;
|
onCloseTabsAbove: (tabId: string) => Promise<void> | void;
|
||||||
onCloseTabsBelow: (tabId: string) => Promise<void> | void;
|
onCloseTabsBelow: (tabId: string) => Promise<void> | void;
|
||||||
@@ -303,6 +307,7 @@ function MobileWorkspaceTabOption({
|
|||||||
menuTestIDBase,
|
menuTestIDBase,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onReloadAgent,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCloseTabsBefore: onCloseTabsAbove,
|
onCloseTabsBefore: onCloseTabsAbove,
|
||||||
onCloseTabsAfter: onCloseTabsBelow,
|
onCloseTabsAfter: onCloseTabsBelow,
|
||||||
@@ -346,11 +351,14 @@ function MobileWorkspaceTabOption({
|
|||||||
disabled={entry.disabled}
|
disabled={entry.disabled}
|
||||||
destructive={entry.destructive}
|
destructive={entry.destructive}
|
||||||
onSelect={entry.onSelect}
|
onSelect={entry.onSelect}
|
||||||
|
tooltip={entry.tooltip}
|
||||||
leading={(() => {
|
leading={(() => {
|
||||||
const iconColor = theme.colors.foregroundMuted;
|
const iconColor = theme.colors.foregroundMuted;
|
||||||
switch (entry.icon) {
|
switch (entry.icon) {
|
||||||
case "copy":
|
case "copy":
|
||||||
return <Copy size={16} color={iconColor} />;
|
return <Copy size={16} color={iconColor} />;
|
||||||
|
case "rotate-cw":
|
||||||
|
return <RotateCw size={16} color={iconColor} />;
|
||||||
case "arrow-left-to-line":
|
case "arrow-left-to-line":
|
||||||
return <ArrowLeftToLine size={16} color={iconColor} />;
|
return <ArrowLeftToLine size={16} color={iconColor} />;
|
||||||
case "arrow-right-to-line":
|
case "arrow-right-to-line":
|
||||||
@@ -393,6 +401,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
|||||||
onSelectSwitcherTab,
|
onSelectSwitcherTab,
|
||||||
onCopyResumeCommand,
|
onCopyResumeCommand,
|
||||||
onCopyAgentId,
|
onCopyAgentId,
|
||||||
|
onReloadAgent,
|
||||||
onCloseTab,
|
onCloseTab,
|
||||||
onCloseTabsAbove,
|
onCloseTabsAbove,
|
||||||
onCloseTabsBelow,
|
onCloseTabsBelow,
|
||||||
@@ -464,6 +473,7 @@ const MobileWorkspaceTabSwitcher = memo(function MobileWorkspaceTabSwitcher({
|
|||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onCopyResumeCommand={onCopyResumeCommand}
|
onCopyResumeCommand={onCopyResumeCommand}
|
||||||
onCopyAgentId={onCopyAgentId}
|
onCopyAgentId={onCopyAgentId}
|
||||||
|
onReloadAgent={onReloadAgent}
|
||||||
onCloseTab={onCloseTab}
|
onCloseTab={onCloseTab}
|
||||||
onCloseTabsAbove={onCloseTabsAbove}
|
onCloseTabsAbove={onCloseTabsAbove}
|
||||||
onCloseTabsBelow={onCloseTabsBelow}
|
onCloseTabsBelow={onCloseTabsBelow}
|
||||||
@@ -1308,6 +1318,23 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
[normalizedServerId, toast],
|
[normalizedServerId, toast],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleReloadAgent = useCallback(
|
||||||
|
async (agentId: string) => {
|
||||||
|
if (!client || !isConnected) {
|
||||||
|
toast.error("Host is not connected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.refreshAgent(agentId);
|
||||||
|
toast.show("Reloaded agent", { variant: "success" });
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(error instanceof Error ? error.message : "Failed to reload agent");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[client, isConnected, toast],
|
||||||
|
);
|
||||||
|
|
||||||
const handleCopyWorkspacePath = useCallback(async () => {
|
const handleCopyWorkspacePath = useCallback(async () => {
|
||||||
if (!workspaceDirectory) {
|
if (!workspaceDirectory) {
|
||||||
toast.error("Workspace path not available");
|
toast.error("Workspace path not available");
|
||||||
@@ -2135,6 +2162,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
onSelectSwitcherTab={handleSelectSwitcherTab}
|
onSelectSwitcherTab={handleSelectSwitcherTab}
|
||||||
onCopyResumeCommand={handleCopyResumeCommand}
|
onCopyResumeCommand={handleCopyResumeCommand}
|
||||||
onCopyAgentId={handleCopyAgentId}
|
onCopyAgentId={handleCopyAgentId}
|
||||||
|
onReloadAgent={handleReloadAgent}
|
||||||
onCloseTab={handleCloseTabById}
|
onCloseTab={handleCloseTabById}
|
||||||
onCloseTabsAbove={handleCloseTabsToLeft}
|
onCloseTabsAbove={handleCloseTabsToLeft}
|
||||||
onCloseTabsBelow={handleCloseTabsToRight}
|
onCloseTabsBelow={handleCloseTabsToRight}
|
||||||
@@ -2155,6 +2183,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
onCloseTab={handleCloseTabById}
|
onCloseTab={handleCloseTabById}
|
||||||
onCopyResumeCommand={handleCopyResumeCommand}
|
onCopyResumeCommand={handleCopyResumeCommand}
|
||||||
onCopyAgentId={handleCopyAgentId}
|
onCopyAgentId={handleCopyAgentId}
|
||||||
|
onReloadAgent={handleReloadAgent}
|
||||||
onCloseTabsToLeft={handleCloseTabsToLeft}
|
onCloseTabsToLeft={handleCloseTabsToLeft}
|
||||||
onCloseTabsToRight={handleCloseTabsToRight}
|
onCloseTabsToRight={handleCloseTabsToRight}
|
||||||
onCloseOtherTabs={handleCloseOtherTabs}
|
onCloseOtherTabs={handleCloseOtherTabs}
|
||||||
@@ -2190,6 +2219,7 @@ function WorkspaceScreenContent({ serverId, workspaceId }: WorkspaceScreenProps)
|
|||||||
onCloseTab={handleCloseTabById}
|
onCloseTab={handleCloseTabById}
|
||||||
onCopyResumeCommand={handleCopyResumeCommand}
|
onCopyResumeCommand={handleCopyResumeCommand}
|
||||||
onCopyAgentId={handleCopyAgentId}
|
onCopyAgentId={handleCopyAgentId}
|
||||||
|
onReloadAgent={handleReloadAgent}
|
||||||
onCloseTabsToLeft={handleCloseTabsToLeftInPane}
|
onCloseTabsToLeft={handleCloseTabsToLeftInPane}
|
||||||
onCloseTabsToRight={handleCloseTabsToRightInPane}
|
onCloseTabsToRight={handleCloseTabsToRightInPane}
|
||||||
onCloseOtherTabs={handleCloseOtherTabsInPane}
|
onCloseOtherTabs={handleCloseOtherTabsInPane}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
|
|
||||||
import { createTestLogger } from "../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../test-utils/test-logger.js";
|
||||||
import { openPaseoDatabase, type PaseoDatabaseHandle } from "./sqlite-database.js";
|
import { openPaseoDatabase, type PaseoDatabaseHandle } from "./sqlite-database.js";
|
||||||
@@ -192,6 +192,78 @@ describe("importLegacyAgentSnapshots", () => {
|
|||||||
expect(rows).toHaveLength(150);
|
expect(rows).toHaveLength(150);
|
||||||
expect(rows.map((row) => row.agentId)).toContain("agent-149");
|
expect(rows.map((row) => row.agentId)).toContain("agent-149");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("creates backup of agent directory before import", async () => {
|
||||||
|
await seedWorkspace("/tmp/project");
|
||||||
|
writeLegacyAgentJson({
|
||||||
|
paseoHome,
|
||||||
|
relativePath: "agents/project-a/agent-1.json",
|
||||||
|
payload: createLegacyAgentJson(),
|
||||||
|
});
|
||||||
|
|
||||||
|
await importLegacyAgentSnapshots({
|
||||||
|
db: database.db,
|
||||||
|
paseoHome,
|
||||||
|
logger: createTestLogger(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const backupPath = path.join(
|
||||||
|
paseoHome,
|
||||||
|
"backup",
|
||||||
|
"pre-migration",
|
||||||
|
"agents",
|
||||||
|
"project-a",
|
||||||
|
"agent-1.json",
|
||||||
|
);
|
||||||
|
expect(existsSync(backupPath)).toBe(true);
|
||||||
|
expect(JSON.parse(readFileSync(backupPath, "utf8"))).toMatchObject({
|
||||||
|
id: "agent-1",
|
||||||
|
cwd: "/tmp/project",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("logs batch progress for large imports", async () => {
|
||||||
|
await seedWorkspace("/tmp/large-project");
|
||||||
|
const logger = createTestLogger();
|
||||||
|
const infoSpy = vi.spyOn(logger, "info");
|
||||||
|
|
||||||
|
for (let index = 0; index < 150; index += 1) {
|
||||||
|
writeLegacyAgentJson({
|
||||||
|
paseoHome,
|
||||||
|
relativePath: `agents/large-project/agent-${index}.json`,
|
||||||
|
payload: createLegacyAgentJson({
|
||||||
|
id: `agent-${index}`,
|
||||||
|
cwd: "/tmp/large-project",
|
||||||
|
runtimeInfo: {
|
||||||
|
provider: "codex",
|
||||||
|
sessionId: `session-${index}`,
|
||||||
|
model: "gpt-5.1-codex-mini",
|
||||||
|
modeId: "plan",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await importLegacyAgentSnapshots({
|
||||||
|
db: database.db,
|
||||||
|
paseoHome,
|
||||||
|
logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const batchLogs = infoSpy.mock.calls.filter(
|
||||||
|
([context, message]) => message === "Importing agent snapshot batch" && typeof context === "object",
|
||||||
|
);
|
||||||
|
expect(batchLogs.length).toBeGreaterThan(1);
|
||||||
|
expect(batchLogs[0]?.[0]).toMatchObject({
|
||||||
|
batch: 1,
|
||||||
|
totalBatches: batchLogs.length,
|
||||||
|
});
|
||||||
|
expect(batchLogs.at(-1)?.[0]).toMatchObject({
|
||||||
|
batch: batchLogs.length,
|
||||||
|
totalBatches: batchLogs.length,
|
||||||
|
rowsProcessed: 150,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createLegacyAgentJson(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
function createLegacyAgentJson(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||||
|
|||||||
@@ -40,7 +40,25 @@ export async function importLegacyAgentSnapshots(options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const records = await readLegacyAgentRecords(path.join(options.paseoHome, "agents"), options.logger);
|
const agentsDir = path.join(options.paseoHome, "agents");
|
||||||
|
if (!(await pathExists(agentsDir))) {
|
||||||
|
options.logger.info("Skipping legacy agent snapshot import because no legacy files exist");
|
||||||
|
return {
|
||||||
|
status: "skipped",
|
||||||
|
reason: "no-legacy-files",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await backupLegacyAgentDirectory({
|
||||||
|
sourceDir: agentsDir,
|
||||||
|
paseoHome: options.paseoHome,
|
||||||
|
logger: options.logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { records, skippedCount } = await readLegacyAgentRecords(agentsDir, options.logger);
|
||||||
|
if (skippedCount > 0) {
|
||||||
|
options.logger.warn({ skippedCount }, "Skipped invalid agent JSON files during migration");
|
||||||
|
}
|
||||||
if (records.length === 0) {
|
if (records.length === 0) {
|
||||||
options.logger.info("Skipping legacy agent snapshot import because no legacy files exist");
|
options.logger.info("Skipping legacy agent snapshot import because no legacy files exist");
|
||||||
return {
|
return {
|
||||||
@@ -109,8 +127,15 @@ export async function importLegacyAgentSnapshots(options: {
|
|||||||
const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(record.cwd));
|
const workspaceId = workspaceIdsByDirectory.get(normalizeWorkspaceId(record.cwd));
|
||||||
return workspaceId === undefined ? [] : [toAgentSnapshotRowValues({ record, workspaceId })];
|
return workspaceId === undefined ? [] : [toAgentSnapshotRowValues({ record, workspaceId })];
|
||||||
});
|
});
|
||||||
|
const totalBatches = Math.ceil(rows.length / MAX_AGENT_SNAPSHOT_ROWS_PER_INSERT);
|
||||||
for (let startIndex = 0; startIndex < rows.length; startIndex += MAX_AGENT_SNAPSHOT_ROWS_PER_INSERT) {
|
for (let startIndex = 0; startIndex < rows.length; startIndex += MAX_AGENT_SNAPSHOT_ROWS_PER_INSERT) {
|
||||||
const batch = rows.slice(startIndex, startIndex + MAX_AGENT_SNAPSHOT_ROWS_PER_INSERT);
|
const batch = rows.slice(startIndex, startIndex + MAX_AGENT_SNAPSHOT_ROWS_PER_INSERT);
|
||||||
|
const batchNum = Math.floor(startIndex / MAX_AGENT_SNAPSHOT_ROWS_PER_INSERT) + 1;
|
||||||
|
const rowsProcessed = startIndex + batch.length;
|
||||||
|
options.logger.info(
|
||||||
|
{ batch: batchNum, totalBatches, rowsProcessed },
|
||||||
|
"Importing agent snapshot batch",
|
||||||
|
);
|
||||||
tx.insert(agentSnapshots).values(batch).run();
|
tx.insert(agentSnapshots).values(batch).run();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -126,23 +151,29 @@ export async function importLegacyAgentSnapshots(options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readLegacyAgentRecords(baseDir: string, logger: Logger): Promise<StoredAgentRecord[]> {
|
async function readLegacyAgentRecords(baseDir: string, logger: Logger): Promise<{
|
||||||
|
records: StoredAgentRecord[];
|
||||||
|
skippedCount: number;
|
||||||
|
}> {
|
||||||
let entries: Array<import("node:fs").Dirent> = [];
|
let entries: Array<import("node:fs").Dirent> = [];
|
||||||
try {
|
try {
|
||||||
entries = await fs.readdir(baseDir, { withFileTypes: true });
|
entries = await fs.readdir(baseDir, { withFileTypes: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
return [];
|
return { records: [], skippedCount: 0 };
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordsById = new Map<string, StoredAgentRecord>();
|
const recordsById = new Map<string, StoredAgentRecord>();
|
||||||
|
let skippedCount = 0;
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isFile() && entry.name.endsWith(".json")) {
|
if (entry.isFile() && entry.name.endsWith(".json")) {
|
||||||
const record = await readRecordFile(path.join(baseDir, entry.name), logger);
|
const record = await readRecordFile(path.join(baseDir, entry.name), logger);
|
||||||
if (record) {
|
if (record) {
|
||||||
recordsById.set(record.id, record);
|
recordsById.set(record.id, record);
|
||||||
|
} else {
|
||||||
|
skippedCount += 1;
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -165,11 +196,16 @@ async function readLegacyAgentRecords(baseDir: string, logger: Logger): Promise<
|
|||||||
const record = await readRecordFile(path.join(baseDir, entry.name, childEntry.name), logger);
|
const record = await readRecordFile(path.join(baseDir, entry.name, childEntry.name), logger);
|
||||||
if (record) {
|
if (record) {
|
||||||
recordsById.set(record.id, record);
|
recordsById.set(record.id, record);
|
||||||
|
} else {
|
||||||
|
skippedCount += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array.from(recordsById.values());
|
return {
|
||||||
|
records: Array.from(recordsById.values()),
|
||||||
|
skippedCount,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readRecordFile(filePath: string, logger: Logger): Promise<StoredAgentRecord | null> {
|
async function readRecordFile(filePath: string, logger: Logger): Promise<StoredAgentRecord | null> {
|
||||||
@@ -186,3 +222,26 @@ async function hasAnyAgentSnapshotRows(db: PaseoDatabaseHandle["db"]): Promise<b
|
|||||||
const rows = await db.select({ count: count() }).from(agentSnapshots);
|
const rows = await db.select({ count: count() }).from(agentSnapshots);
|
||||||
return (rows[0]?.count ?? 0) > 0;
|
return (rows[0]?.count ?? 0) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function backupLegacyAgentDirectory(options: {
|
||||||
|
sourceDir: string;
|
||||||
|
paseoHome: string;
|
||||||
|
logger: Logger;
|
||||||
|
}): Promise<void> {
|
||||||
|
const backupPath = path.join(options.paseoHome, "backup", "pre-migration", "agents");
|
||||||
|
await fs.mkdir(path.dirname(backupPath), { recursive: true });
|
||||||
|
await fs.cp(options.sourceDir, backupPath, { recursive: true });
|
||||||
|
options.logger.info({ backupPath }, "Backed up legacy agent snapshots before migration");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pathExists(targetPath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(targetPath);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||||
|
|
||||||
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
||||||
|
|
||||||
@@ -173,6 +173,136 @@ describe("importLegacyProjectWorkspaceJson", () => {
|
|||||||
expect(await database.db.select().from(projects)).toEqual([]);
|
expect(await database.db.select().from(projects)).toEqual([]);
|
||||||
expect(await database.db.select().from(workspaces)).toEqual([]);
|
expect(await database.db.select().from(workspaces)).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("deduplicates projects with the same rootPath", async () => {
|
||||||
|
writeLegacyJson({
|
||||||
|
paseoHome,
|
||||||
|
projectsJson: [
|
||||||
|
{
|
||||||
|
projectId: "project-1",
|
||||||
|
rootPath: "/tmp/project-1",
|
||||||
|
kind: "git",
|
||||||
|
displayName: "First Project",
|
||||||
|
createdAt: "2026-03-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-01T00:00:00.000Z",
|
||||||
|
archivedAt: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
projectId: "project-2",
|
||||||
|
rootPath: "/tmp/project-1",
|
||||||
|
kind: "git",
|
||||||
|
displayName: "Replacement Project",
|
||||||
|
createdAt: "2026-03-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||||
|
archivedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workspacesJson: [
|
||||||
|
{
|
||||||
|
workspaceId: "workspace-1",
|
||||||
|
projectId: "project-2",
|
||||||
|
cwd: "/tmp/project-1",
|
||||||
|
kind: "local_checkout",
|
||||||
|
displayName: "main",
|
||||||
|
createdAt: "2026-03-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-03T00:00:00.000Z",
|
||||||
|
archivedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await importLegacyProjectWorkspaceJson({
|
||||||
|
db: database.db,
|
||||||
|
paseoHome,
|
||||||
|
logger: createTestLogger(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
status: "imported",
|
||||||
|
importedProjects: 1,
|
||||||
|
importedWorkspaces: 1,
|
||||||
|
});
|
||||||
|
const projectRows = await database.db.select().from(projects);
|
||||||
|
expect(projectRows).toHaveLength(1);
|
||||||
|
expect(projectRows[0]).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
directory: "/tmp/project-1",
|
||||||
|
displayName: "Replacement Project",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("creates backup of JSON files before import", async () => {
|
||||||
|
writeLegacyJson({
|
||||||
|
paseoHome,
|
||||||
|
projectsJson: [
|
||||||
|
{
|
||||||
|
projectId: "project-1",
|
||||||
|
rootPath: "/tmp/project-1",
|
||||||
|
kind: "git",
|
||||||
|
displayName: "Project One",
|
||||||
|
createdAt: "2026-03-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-02T00:00:00.000Z",
|
||||||
|
archivedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workspacesJson: [
|
||||||
|
{
|
||||||
|
workspaceId: "workspace-1",
|
||||||
|
projectId: "project-1",
|
||||||
|
cwd: "/tmp/project-1",
|
||||||
|
kind: "local_checkout",
|
||||||
|
displayName: "main",
|
||||||
|
createdAt: "2026-03-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-02T00:00:00.000Z",
|
||||||
|
archivedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
await importLegacyProjectWorkspaceJson({
|
||||||
|
db: database.db,
|
||||||
|
paseoHome,
|
||||||
|
logger: createTestLogger(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const backupDir = path.join(paseoHome, "backup", "pre-migration");
|
||||||
|
const projectsBackupPath = path.join(backupDir, "projects.json");
|
||||||
|
const workspacesBackupPath = path.join(backupDir, "workspaces.json");
|
||||||
|
expect(existsSync(projectsBackupPath)).toBe(true);
|
||||||
|
expect(existsSync(workspacesBackupPath)).toBe(true);
|
||||||
|
expect(JSON.parse(readFileSync(projectsBackupPath, "utf8"))).toHaveLength(1);
|
||||||
|
expect(JSON.parse(readFileSync(workspacesBackupPath, "utf8"))).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("produces clear error message for corrupt project JSON", async () => {
|
||||||
|
writeLegacyJson({
|
||||||
|
paseoHome,
|
||||||
|
projectsJson: [
|
||||||
|
{
|
||||||
|
projectId: "project-1",
|
||||||
|
rootPath: 123,
|
||||||
|
kind: "git",
|
||||||
|
displayName: "Project One",
|
||||||
|
createdAt: "2026-03-01T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-03-02T00:00:00.000Z",
|
||||||
|
archivedAt: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
workspacesJson: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
importLegacyProjectWorkspaceJson({
|
||||||
|
db: database.db,
|
||||||
|
paseoHome,
|
||||||
|
logger: createTestLogger(),
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(
|
||||||
|
`Failed to parse ${path.join(paseoHome, "projects", "projects.json")}. ` +
|
||||||
|
"The file may be corrupted.",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function writeLegacyJson(input: {
|
function writeLegacyJson(input: {
|
||||||
|
|||||||
@@ -49,11 +49,7 @@ export async function importLegacyProjectWorkspaceJson(options: {
|
|||||||
}): Promise<LegacyProjectWorkspaceImportResult> {
|
}): Promise<LegacyProjectWorkspaceImportResult> {
|
||||||
const projectsPath = path.join(options.paseoHome, "projects", "projects.json");
|
const projectsPath = path.join(options.paseoHome, "projects", "projects.json");
|
||||||
const workspacesPath = path.join(options.paseoHome, "projects", "workspaces.json");
|
const workspacesPath = path.join(options.paseoHome, "projects", "workspaces.json");
|
||||||
const [projectRows, workspaceRows, databaseHasRows] = await Promise.all([
|
const databaseHasRows = await hasAnyProjectWorkspaceRows(options.db);
|
||||||
readLegacyProjects(projectsPath),
|
|
||||||
readLegacyWorkspaces(workspacesPath),
|
|
||||||
hasAnyProjectWorkspaceRows(options.db),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (databaseHasRows) {
|
if (databaseHasRows) {
|
||||||
options.logger.info("Skipping legacy project/workspace JSON import because the DB is not empty");
|
options.logger.info("Skipping legacy project/workspace JSON import because the DB is not empty");
|
||||||
@@ -63,6 +59,30 @@ export async function importLegacyProjectWorkspaceJson(options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [projectsExists, workspacesExists] = await Promise.all([
|
||||||
|
pathExists(projectsPath),
|
||||||
|
pathExists(workspacesPath),
|
||||||
|
]);
|
||||||
|
if (!projectsExists && !workspacesExists) {
|
||||||
|
options.logger.info("Skipping legacy project/workspace JSON import because no legacy files exist");
|
||||||
|
return {
|
||||||
|
status: "skipped",
|
||||||
|
reason: "no-legacy-files",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await backupLegacyProjectWorkspaceJson({
|
||||||
|
projectsPath,
|
||||||
|
workspacesPath,
|
||||||
|
paseoHome: options.paseoHome,
|
||||||
|
logger: options.logger,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [projectRows, workspaceRows] = await Promise.all([
|
||||||
|
readLegacyProjects(projectsPath),
|
||||||
|
readLegacyWorkspaces(workspacesPath),
|
||||||
|
]);
|
||||||
|
|
||||||
if (projectRows.length === 0 && workspaceRows.length === 0) {
|
if (projectRows.length === 0 && workspaceRows.length === 0) {
|
||||||
options.logger.info("Skipping legacy project/workspace JSON import because no legacy files exist");
|
options.logger.info("Skipping legacy project/workspace JSON import because no legacy files exist");
|
||||||
return {
|
return {
|
||||||
@@ -71,10 +91,18 @@ export async function importLegacyProjectWorkspaceJson(options: {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const dedupedProjects = [...new Map(projectRows.map((project) => [project.rootPath, project])).values()];
|
||||||
|
const dedupedWorkspaces = [...new Map(workspaceRows.map((workspace) => [workspace.cwd, workspace])).values()];
|
||||||
|
|
||||||
|
options.logger.info(
|
||||||
|
{ projects: dedupedProjects.length, workspaces: dedupedWorkspaces.length },
|
||||||
|
"Starting legacy project/workspace import",
|
||||||
|
);
|
||||||
|
|
||||||
options.db.transaction((tx) => {
|
options.db.transaction((tx) => {
|
||||||
// Insert projects, mapping old format to new schema
|
// Insert projects, mapping old format to new schema
|
||||||
const projectDirectoryToId = new Map<string, number>();
|
const projectDirectoryToId = new Map<string, number>();
|
||||||
for (const legacy of projectRows) {
|
for (const legacy of dedupedProjects) {
|
||||||
const row = tx
|
const row = tx
|
||||||
.insert(projects)
|
.insert(projects)
|
||||||
.values({
|
.values({
|
||||||
@@ -100,7 +128,7 @@ export async function importLegacyProjectWorkspaceJson(options: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Insert workspaces, resolving project FK
|
// Insert workspaces, resolving project FK
|
||||||
for (const legacy of workspaceRows) {
|
for (const legacy of dedupedWorkspaces) {
|
||||||
const projectId = legacyProjectIdToNewId.get(legacy.projectId);
|
const projectId = legacyProjectIdToNewId.get(legacy.projectId);
|
||||||
if (projectId === undefined) {
|
if (projectId === undefined) {
|
||||||
throw new Error(`Legacy workspace ${legacy.workspaceId} references unknown project ${legacy.projectId}`);
|
throw new Error(`Legacy workspace ${legacy.workspaceId} references unknown project ${legacy.projectId}`);
|
||||||
@@ -125,27 +153,49 @@ export async function importLegacyProjectWorkspaceJson(options: {
|
|||||||
|
|
||||||
options.logger.info(
|
options.logger.info(
|
||||||
{
|
{
|
||||||
importedProjects: projectRows.length,
|
importedProjects: dedupedProjects.length,
|
||||||
importedWorkspaces: workspaceRows.length,
|
importedWorkspaces: dedupedWorkspaces.length,
|
||||||
},
|
},
|
||||||
"Imported legacy project/workspace JSON into the database",
|
"Imported legacy project/workspace JSON into the database",
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "imported",
|
status: "imported",
|
||||||
importedProjects: projectRows.length,
|
importedProjects: dedupedProjects.length,
|
||||||
importedWorkspaces: workspaceRows.length,
|
importedWorkspaces: dedupedWorkspaces.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readLegacyProjects(filePath: string) {
|
async function readLegacyProjects(filePath: string) {
|
||||||
const raw = await readOptionalJsonFile(filePath);
|
const raw = await readOptionalJsonFile(filePath);
|
||||||
return raw ? z.array(LegacyProjectSchema).parse(raw) : [];
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return z.array(LegacyProjectSchema).parse(raw);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse ${filePath}. The file may be corrupted. ` +
|
||||||
|
`Check the file and fix or remove invalid entries. ` +
|
||||||
|
`Original error: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readLegacyWorkspaces(filePath: string) {
|
async function readLegacyWorkspaces(filePath: string) {
|
||||||
const raw = await readOptionalJsonFile(filePath);
|
const raw = await readOptionalJsonFile(filePath);
|
||||||
return raw ? z.array(LegacyWorkspaceSchema).parse(raw) : [];
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return z.array(LegacyWorkspaceSchema).parse(raw);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse ${filePath}. The file may be corrupted. ` +
|
||||||
|
`Check the file and fix or remove invalid entries. ` +
|
||||||
|
`Original error: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readOptionalJsonFile(filePath: string): Promise<unknown | null> {
|
async function readOptionalJsonFile(filePath: string): Promise<unknown | null> {
|
||||||
@@ -170,3 +220,34 @@ async function hasAnyProjectWorkspaceRows(db: PaseoDatabaseHandle["db"]): Promis
|
|||||||
const workspaceCount = workspaceCountRows[0]?.count ?? 0;
|
const workspaceCount = workspaceCountRows[0]?.count ?? 0;
|
||||||
return projectCount > 0 || workspaceCount > 0;
|
return projectCount > 0 || workspaceCount > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function backupLegacyProjectWorkspaceJson(options: {
|
||||||
|
projectsPath: string;
|
||||||
|
workspacesPath: string;
|
||||||
|
paseoHome: string;
|
||||||
|
logger: Logger;
|
||||||
|
}): Promise<void> {
|
||||||
|
const backupDir = path.join(options.paseoHome, "backup", "pre-migration");
|
||||||
|
await fs.mkdir(backupDir, { recursive: true });
|
||||||
|
|
||||||
|
if (await pathExists(options.projectsPath)) {
|
||||||
|
await fs.copyFile(options.projectsPath, path.join(backupDir, "projects.json"));
|
||||||
|
}
|
||||||
|
if (await pathExists(options.workspacesPath)) {
|
||||||
|
await fs.copyFile(options.workspacesPath, path.join(backupDir, "workspaces.json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
options.logger.info({ backupPath: backupDir }, "Backed up legacy project/workspace JSON before migration");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pathExists(targetPath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await fs.access(targetPath);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1290,6 +1290,7 @@ export class Session {
|
|||||||
isGit: false as const,
|
isGit: false as const,
|
||||||
currentBranch: null,
|
currentBranch: null,
|
||||||
remoteUrl: null,
|
remoteUrl: null,
|
||||||
|
worktreeRoot: null,
|
||||||
isPaseoOwnedWorktree: false as const,
|
isPaseoOwnedWorktree: false as const,
|
||||||
mainRepoRoot: null,
|
mainRepoRoot: null,
|
||||||
}
|
}
|
||||||
@@ -1299,6 +1300,7 @@ export class Session {
|
|||||||
isGit: true as const,
|
isGit: true as const,
|
||||||
currentBranch: workspace.displayName,
|
currentBranch: workspace.displayName,
|
||||||
remoteUrl: project.gitRemote,
|
remoteUrl: project.gitRemote,
|
||||||
|
worktreeRoot: workspace.directory,
|
||||||
isPaseoOwnedWorktree: true as const,
|
isPaseoOwnedWorktree: true as const,
|
||||||
mainRepoRoot: project.directory,
|
mainRepoRoot: project.directory,
|
||||||
}
|
}
|
||||||
@@ -1307,6 +1309,7 @@ export class Session {
|
|||||||
isGit: true as const,
|
isGit: true as const,
|
||||||
currentBranch: workspace.displayName,
|
currentBranch: workspace.displayName,
|
||||||
remoteUrl: project.gitRemote,
|
remoteUrl: project.gitRemote,
|
||||||
|
worktreeRoot: workspace.directory,
|
||||||
isPaseoOwnedWorktree: false as const,
|
isPaseoOwnedWorktree: false as const,
|
||||||
mainRepoRoot: null,
|
mainRepoRoot: null,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user