Skip to content

Commit 09c00ad

Browse files
committed
Merge remote-tracking branch 'origin/main' into shared/rewards-center-integration
# Conflicts: # apps/web/src/pages/welcome.tsx
2 parents 77f1e3c + e11cdde commit 09c00ad

5 files changed

Lines changed: 422 additions & 7 deletions

File tree

apps/desktop/main/bootstrap.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import { join, resolve } from "node:path";
33
import { app } from "electron";
44
import { getDesktopNexuHomeDir } from "../shared/desktop-paths";
55
import { resolveRuntimePlatform } from "./platforms/platform-resolver";
6+
import {
7+
getLegacyPackagedNexuHomeDir,
8+
migrateNexuHomeFromUserData,
9+
} from "./services/nexu-home-migration";
610

711
function safeWrite(stream: NodeJS.WriteStream, message: string): void {
812
if (stream.destroyed || !stream.writable) {
@@ -145,12 +149,28 @@ function configurePackagedPaths(): void {
145149
const sessionDataPath = join(effectiveUserDataPath, "session");
146150
const logsPath = join(effectiveUserDataPath, "logs");
147151
const nexuHomePath = getDesktopNexuHomeDir(effectiveUserDataPath);
152+
const legacyPackagedNexuHomePath = getLegacyPackagedNexuHomeDir(
153+
effectiveUserDataPath,
154+
);
148155

149156
mkdirSync(effectiveUserDataPath, { recursive: true });
150157
mkdirSync(sessionDataPath, { recursive: true });
151158
mkdirSync(logsPath, { recursive: true });
152159
mkdirSync(nexuHomePath, { recursive: true });
153160

161+
if (legacyPackagedNexuHomePath !== nexuHomePath) {
162+
migrateNexuHomeFromUserData({
163+
targetNexuHome: nexuHomePath,
164+
sourceNexuHome: legacyPackagedNexuHomePath,
165+
log: (message) => {
166+
safeWrite(
167+
process.stdout,
168+
`[desktop:paths] nexu-home-migration: ${message}\n`,
169+
);
170+
},
171+
});
172+
}
173+
154174
process.env.NEXU_HOME = nexuHomePath;
155175

156176
app.setPath("userData", effectiveUserDataPath);
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import {
2+
cpSync,
3+
existsSync,
4+
mkdirSync,
5+
readFileSync,
6+
writeFileSync,
7+
} from "node:fs";
8+
import { resolve } from "node:path";
9+
10+
const MIGRATION_STAMP = ".desktop-userdata-home-migration-v1";
11+
12+
const COPYABLE_FILES = [
13+
"cloud-profiles.json",
14+
"compiled-openclaw.json",
15+
"skill-ledger.json",
16+
"analytics-state.json",
17+
] as const;
18+
19+
const COPYABLE_DIRS = [
20+
"artifacts",
21+
"skillhub-cache",
22+
"logs",
23+
"runtime",
24+
] as const;
25+
26+
type JsonRecord = Record<string, unknown>;
27+
28+
export interface NexuHomeMigrationOpts {
29+
targetNexuHome: string;
30+
sourceNexuHome: string;
31+
log: (message: string) => void;
32+
}
33+
34+
function safeReadJson(filePath: string): JsonRecord | null {
35+
try {
36+
const parsed = JSON.parse(readFileSync(filePath, "utf8")) as unknown;
37+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
38+
return null;
39+
}
40+
return parsed as JsonRecord;
41+
} catch {
42+
return null;
43+
}
44+
}
45+
46+
function mergeRecords(
47+
target: JsonRecord | undefined,
48+
source: JsonRecord | undefined,
49+
): JsonRecord {
50+
return {
51+
...(target ?? {}),
52+
...(source ?? {}),
53+
};
54+
}
55+
56+
function mergeConfigArrays(
57+
target: unknown,
58+
source: unknown,
59+
): Array<Record<string, unknown>> | undefined {
60+
const targetItems = Array.isArray(target) ? target : [];
61+
const sourceItems = Array.isArray(source) ? source : [];
62+
if (targetItems.length === 0 && sourceItems.length === 0) {
63+
return undefined;
64+
}
65+
66+
const merged = new Map<string, Record<string, unknown>>();
67+
const anon: Array<Record<string, unknown>> = [];
68+
for (const item of targetItems) {
69+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
70+
const record = item as Record<string, unknown>;
71+
const id = typeof record.id === "string" ? record.id : null;
72+
if (id) {
73+
merged.set(id, record);
74+
} else {
75+
anon.push(record);
76+
}
77+
}
78+
79+
for (const item of sourceItems) {
80+
if (!item || typeof item !== "object" || Array.isArray(item)) continue;
81+
const record = item as Record<string, unknown>;
82+
const id = typeof record.id === "string" ? record.id : null;
83+
if (id) {
84+
merged.set(id, {
85+
...(merged.get(id) ?? {}),
86+
...record,
87+
});
88+
} else {
89+
anon.push(record);
90+
}
91+
}
92+
93+
return [...merged.values(), ...anon];
94+
}
95+
96+
function mergeNexuConfig(
97+
target: JsonRecord | null,
98+
source: JsonRecord | null,
99+
): JsonRecord | null {
100+
if (!target && !source) return null;
101+
if (!target) return source;
102+
if (!source) return target;
103+
104+
return {
105+
...target,
106+
...source,
107+
app: mergeRecords(
108+
target.app as JsonRecord | undefined,
109+
source.app as JsonRecord | undefined,
110+
),
111+
runtime: mergeRecords(
112+
target.runtime as JsonRecord | undefined,
113+
source.runtime as JsonRecord | undefined,
114+
),
115+
desktop: mergeRecords(
116+
target.desktop as JsonRecord | undefined,
117+
source.desktop as JsonRecord | undefined,
118+
),
119+
secrets: mergeRecords(
120+
target.secrets as JsonRecord | undefined,
121+
source.secrets as JsonRecord | undefined,
122+
),
123+
templates: mergeRecords(
124+
target.templates as JsonRecord | undefined,
125+
source.templates as JsonRecord | undefined,
126+
),
127+
bots: mergeConfigArrays(target.bots, source.bots) ?? [],
128+
providers: mergeConfigArrays(target.providers, source.providers) ?? [],
129+
integrations:
130+
mergeConfigArrays(target.integrations, source.integrations) ?? [],
131+
channels: mergeConfigArrays(target.channels, source.channels) ?? [],
132+
};
133+
}
134+
135+
function copyDirIfMissing(
136+
sourceDir: string,
137+
targetDir: string,
138+
log: (message: string) => void,
139+
): number {
140+
if (!existsSync(sourceDir) || existsSync(targetDir)) {
141+
return 0;
142+
}
143+
cpSync(sourceDir, targetDir, { recursive: true });
144+
log(`copied dir ${targetDir}`);
145+
return 1;
146+
}
147+
148+
function copyFileIfMissing(
149+
sourceFile: string,
150+
targetFile: string,
151+
log: (message: string) => void,
152+
): number {
153+
if (!existsSync(sourceFile) || existsSync(targetFile)) {
154+
return 0;
155+
}
156+
cpSync(sourceFile, targetFile);
157+
log(`copied file ${targetFile}`);
158+
return 1;
159+
}
160+
161+
function writeStamp(stampPath: string): void {
162+
writeFileSync(stampPath, new Date().toISOString(), "utf8");
163+
}
164+
165+
export function getLegacyPackagedNexuHomeDir(userDataPath: string): string {
166+
return resolve(userDataPath, ".nexu");
167+
}
168+
169+
export function migrateNexuHomeFromUserData(opts: NexuHomeMigrationOpts): void {
170+
const { targetNexuHome, sourceNexuHome, log } = opts;
171+
const stampPath = resolve(targetNexuHome, MIGRATION_STAMP);
172+
173+
if (existsSync(stampPath)) {
174+
log("nexu-home migration already completed, skipping");
175+
return;
176+
}
177+
178+
mkdirSync(targetNexuHome, { recursive: true });
179+
180+
if (!existsSync(sourceNexuHome)) {
181+
log(`legacy nexu-home not found: ${sourceNexuHome}, nothing to migrate`);
182+
writeStamp(stampPath);
183+
return;
184+
}
185+
186+
let migrated = 0;
187+
188+
const sourceConfigPath = resolve(sourceNexuHome, "config.json");
189+
const targetConfigPath = resolve(targetNexuHome, "config.json");
190+
const mergedConfig = mergeNexuConfig(
191+
safeReadJson(targetConfigPath),
192+
safeReadJson(sourceConfigPath),
193+
);
194+
if (mergedConfig) {
195+
writeFileSync(
196+
targetConfigPath,
197+
`${JSON.stringify(mergedConfig, null, 2)}\n`,
198+
"utf8",
199+
);
200+
log(`merged config ${targetConfigPath}`);
201+
migrated++;
202+
}
203+
204+
for (const file of COPYABLE_FILES) {
205+
migrated += copyFileIfMissing(
206+
resolve(sourceNexuHome, file),
207+
resolve(targetNexuHome, file),
208+
log,
209+
);
210+
}
211+
212+
for (const dir of COPYABLE_DIRS) {
213+
migrated += copyDirIfMissing(
214+
resolve(sourceNexuHome, dir),
215+
resolve(targetNexuHome, dir),
216+
log,
217+
);
218+
}
219+
220+
writeStamp(stampPath);
221+
log(`nexu-home migration complete: ${migrated} items migrated`);
222+
}

apps/desktop/shared/desktop-paths.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { homedir } from "node:os";
12
import { resolve } from "node:path";
23

3-
export function getDesktopNexuHomeDir(userDataPath: string): string {
4-
return resolve(userDataPath, ".nexu");
4+
export function getDesktopNexuHomeDir(_userDataPath: string): string {
5+
return resolve(homedir(), ".nexu");
56
}
67

78
export function getOpenclawSkillsDir(userDataPath: string): string {

apps/web/src/pages/welcome.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { LanguageSwitcher } from "../components/language-switcher";
2626
import { ProviderLogo } from "../components/provider-logo";
2727
import { useLocale } from "../hooks/use-locale";
2828
import { usePageTitle } from "../hooks/use-page-title";
29+
import { authClient } from "../lib/auth-client";
2930
import { openExternalUrl } from "../lib/desktop-links";
3031
import { track } from "../lib/tracking";
3132

@@ -73,11 +74,8 @@ export function WelcomePage() {
7374
usePageTitle(t("welcome.pageTitle"));
7475
const navigate = useNavigate();
7576
const queryClient = useQueryClient();
76-
77-
// If already set up, skip welcome
78-
if (isSetupComplete()) {
79-
return <Navigate to="/workspace" replace />;
80-
}
77+
const { data: session, isPending: authPending } = authClient.useSession();
78+
const setupComplete = isSetupComplete();
8179

8280
const [mode, setMode] = useState<Mode>("choose");
8381

@@ -93,6 +91,14 @@ export function WelcomePage() {
9391
const cloudConnected = cloudStatus?.connected ?? false;
9492
const cloudPolling = cloudStatus?.polling ?? false;
9593

94+
if (setupComplete && authPending) {
95+
return <div className="min-h-screen bg-[#0b0b0d]" />;
96+
}
97+
98+
if (setupComplete && session?.user) {
99+
return <Navigate to="/workspace" replace />;
100+
}
101+
96102
useEffect(() => {
97103
if (!cloudConnected) {
98104
return;

0 commit comments

Comments
 (0)