diff --git a/src/main/cli/cli.ts b/src/main/cli/cli.ts index 2166d258..2c94dc5f 100644 --- a/src/main/cli/cli.ts +++ b/src/main/cli/cli.ts @@ -33,6 +33,7 @@ export const APPLET_DEV_TMP_FOLDER_PREFIX = 'moss-applet-dev'; export interface CliOpts { profile?: string; + fork?: string; devConfig?: string | undefined; devDataDir?: string | undefined; agentIdx?: number | undefined; @@ -54,6 +55,7 @@ export interface CliOpts { export interface RunOptions { profile: string | undefined; + fork: string | undefined; appstoreNetworkSeed: string; devInfo: WeAppletDevInfo | undefined; bootstrapUrl: string | undefined; @@ -70,6 +72,8 @@ export interface RunOptions { } export function validateArgs(args: CliOpts): RunOptions { + const fork = args.fork; + // validate --profile argument const allowedProfilePattern = /^[0-9a-zA-Z-]+$/; if (args.profile && !allowedProfilePattern.test(args.profile)) { @@ -126,6 +130,9 @@ export function validateArgs(args: CliOpts): RunOptions { if (args.holochainPath && typeof args.holochainPath !== 'string') { throw new Error('The --holochain-path argument must be of type string.'); } + if (fork && typeof fork !== 'string') { + throw new Error('The --fork argument must be of type string.'); + } if (args.holochainRustLog && typeof args.holochainRustLog !== 'string') { throw new Error('The --holochain-rust-log argument must be of type string.'); } @@ -176,6 +183,7 @@ export function validateArgs(args: CliOpts): RunOptions { return { profile, + fork: fork ? fork : undefined, appstoreNetworkSeed, devInfo, bootstrapUrl: args.bootstrapUrl, @@ -319,7 +327,7 @@ function readAndValidateDevConfig( const allGroupNetworkSeeds = groups.map((group) => group.networkSeed); const uniqueGroupNetworkSeeds = new Set(allGroupNetworkSeeds); if (uniqueGroupNetworkSeeds.size !== allGroupNetworkSeeds.length) { - throw new Error(`Invalid We dev config: Group network seeds must all be unique.`); + throw new Error(`Invalid We dev config: Group network seeds must all be unique.`); } // validate applets applets.forEach((applet) => { @@ -389,7 +397,7 @@ function readAndValidateDevConfig( }); if (configObject && !configObject.toolCurations) { - configObject.toolCurations = []; + configObject.toolCurations = []; } const allAppletNames = applets.map((applet) => applet.name); diff --git a/src/main/index.ts b/src/main/index.ts index 1c2d8aa5..8255a033 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -177,6 +177,10 @@ program '-p, --profile ', 'Runs Moss with a custom profile with its own dedicated data store.', ) + .option( + '--fork ', + 'When importing existing data, appends this suffix to all imported network seeds.', + ) .option( '-n, --network-seed ', 'Installs any default apps with the provided network seed in case there are any and have not yet been installed.', @@ -416,6 +420,7 @@ if (!RUNNING_WITH_COMMAND) { let SYSTRAY: Tray | undefined = undefined; let isAppQuitting = false; let LOCAL_SERVICES_HANDLE: childProcess.ChildProcessWithoutNullStreams | undefined; + const skipLegacyProfileImportPrompt = !!RUN_OPTIONS.profile && !RUN_OPTIONS.fork; const WAL_WINDOWS: Record< string, { @@ -1059,21 +1064,21 @@ if (!RUNNING_WITH_COMMAND) { app.quit(); } }), - ipcMain.handle('get-network-overrides', () => { - const overrides = WE_FILE_SYSTEM.getNetworkOverrides(); - const networkUrls = getNetworkUrls(); - return { - overrides, - defaults: { - bootstrapUrl: PRODUCTION_BOOTSTRAP_URLS[0], - relayUrl: PRODUCTION_RELAY_URLS[0], - }, - current: { - bootstrapUrl: networkUrls.bootstrap_urls[0], - relayUrl: networkUrls.relay_urls[0], - }, - }; - }); + ipcMain.handle('get-network-overrides', () => { + const overrides = WE_FILE_SYSTEM.getNetworkOverrides(); + const networkUrls = getNetworkUrls(); + return { + overrides, + defaults: { + bootstrapUrl: PRODUCTION_BOOTSTRAP_URLS[0], + relayUrl: PRODUCTION_RELAY_URLS[0], + }, + current: { + bootstrapUrl: networkUrls.bootstrap_urls[0], + relayUrl: networkUrls.relay_urls[0], + }, + }; + }); ipcMain.handle('set-network-overrides', async (_e, overrides: { bootstrapUrl?: string; relayUrl?: string }) => { WE_FILE_SYSTEM.setNetworkOverrides(overrides); // Relaunch Moss @@ -1110,7 +1115,7 @@ if (!RUNNING_WITH_COMMAND) { app.relaunch(options); app.quit(); }); - ipcMain.handle('is-main-window-focused', (): boolean | undefined => MAIN_WINDOW?.isFocused()); + ipcMain.handle('is-main-window-focused', (): boolean | undefined => MAIN_WINDOW?.isFocused()); ipcMain.handle( 'notification', ( @@ -1486,7 +1491,7 @@ if (!RUNNING_WITH_COMMAND) { let network_info: NetworkInfo = { bootstrap_urls: [], signal_urls: [], relay_urls: [] }; try { network_info = getNetworkUrls(); - } catch (e) {console.error('Failed to get network urls', e)} + } catch (e) { console.error('Failed to get network urls', e) } /** */ return HOLOCHAIN_MANAGER ? { @@ -1527,6 +1532,11 @@ if (!RUNNING_WITH_COMMAND) { return !WE_FILE_SYSTEM.keystoreInitialized(); }); ipcMain.handle('find-legacy-profiles', (): LegacyProfileInfo[] => { + // Skip the legacy import choice for explicit custom profiles, but still let + // the renderer continue into the normal first-launch setup flow. + if (skipLegacyProfileImportPrompt) { + return []; + } return findLegacyProfiles(app); }); ipcMain.handle('get-lair-binary-version', (): string => { @@ -1800,6 +1810,19 @@ if (!RUNNING_WITH_COMMAND) { // Execute a groups import from a parsed array. Used by both dialog-based and // auto-import (pending) flows. const runGroupsImport = async (groups: GroupExportEntry[]): Promise => { + const forkImportedSeed = (seed: string | undefined): string | undefined => { + if (!seed) { + console.warn('No seed provided for group. Skipping seed forking.'); + return undefined; + } else if (!RUN_OPTIONS.fork) { + console.warn('Fork option is enabled but no seed fork string provided. Skipping seed forking.'); + return seed; + } else { + console.log(`Forking imported seed "${seed}" with fork "${RUN_OPTIONS.fork}"`); + return `${seed}${RUN_OPTIONS.fork}`; + } + }; + const allApps = await HOLOCHAIN_MANAGER!.adminWebsocket.listApps({}); let myPubKey = globalPubKeyFromListAppsResponse(allApps); if (!myPubKey) myPubKey = await getOrCreateAgentPubKey(); @@ -1814,7 +1837,9 @@ if (!RUNNING_WITH_COMMAND) { for (let gi = 0; gi < groups.length; gi++) { const group = groups[gi]; const current = gi + 1; - const { networkSeed, progenitor, groupProfile, agentProfile, description } = group; + const { progenitor, groupProfile, agentProfile, description } = group; + const networkSeed = forkImportedSeed(group.networkSeed); + console.log(`Importing group ${current}/${total}: "${groupProfile?.name || 'Unnamed'}" with network seed "${networkSeed}" and progenitor "${progenitor}"`); if (!networkSeed) { results.push({ groupName: groupProfile?.name, status: 'error', error: 'Missing network seed' }); @@ -1858,7 +1883,7 @@ if (!RUNNING_WITH_COMMAND) { }); let importGroupStatus: ImportStatus; - if (amProgenitor) { + if (amProgenitor || !progenitor) { if (groupProfile) { emitProgress(current, groupProfile?.name, 'setting-profile'); await appWs.callZome({ @@ -1878,54 +1903,19 @@ if (!RUNNING_WITH_COMMAND) { } importGroupStatus = 'created'; } else { - let profileSynced = false; - const deadline = Date.now() + 20000; - while (Date.now() < deadline) { - const secondsLeft = Math.ceil((deadline - Date.now()) / 1000); - emitProgress(current, groupProfile?.name, 'waiting-for-sync', { secondsLeft }); - await new Promise((r) => setTimeout(r, 5000)); - try { - const profileRecord = await appWs.callZome({ - role_name: 'group', - zome_name: 'group', - fn_name: 'get_group_profile', - payload: { input: null, local: false }, - }); - if (profileRecord) { profileSynced = true; break; } - } catch (_pollErr) { - // continue polling - } - } - - if (profileSynced) { - importGroupStatus = 'joined'; - } else if (!progenitor) { - if (groupProfile) { - emitProgress(current, groupProfile?.name, 'setting-profile'); - await appWs.callZome({ - role_name: 'group', - zome_name: 'group', - fn_name: 'set_group_profile', - payload: { name: groupProfile.name, icon_src: groupProfile.icon_src }, - }); - } - if (description) { - await appWs.callZome({ - role_name: 'group', - zome_name: 'group', - fn_name: 'set_group_meta_data', - payload: { name: 'description', data: description, permission_hash: null }, - }); - } - importGroupStatus = 'created'; - } else { - importGroupStatus = 'joined-no-profile'; - } + importGroupStatus = 'joined-no-profile'; } // Set the agent's own profile in this group if we have one from the export. if (agentProfile) { try { + const normalizedAgentFields = + agentProfile.fields && + typeof agentProfile.fields === 'object' && + !Array.isArray(agentProfile.fields) + ? agentProfile.fields + : {}; + emitProgress(current, groupProfile?.name, 'setting-profile'); await appWs.callZome({ role_name: 'group', @@ -1933,7 +1923,7 @@ if (!RUNNING_WITH_COMMAND) { fn_name: 'create_profile', payload: { nickname: agentProfile.nickname, - fields: agentProfile.fields, + fields: normalizedAgentFields, }, }); } catch (profileErr) { @@ -1942,8 +1932,17 @@ if (!RUNNING_WITH_COMMAND) { } if (group.tools && group.tools.length > 0) { + if (progenitor && !amProgenitor) { + console.log( + `Skipping tool import for group ${appId} because importing agent is not the listed progenitor.` + ); + } + } + + if (group.tools && group.tools.length > 0 && (amProgenitor || !progenitor)) { for (let ti = 0; ti < group.tools.length; ti++) { const tool = group.tools[ti]; + const toolNetworkSeed = forkImportedSeed(tool.network_seed); emitProgress(current, groupProfile?.name, 'installing-tool', { toolName: tool.toolName || tool.custom_name, toolIndex: ti + 1, @@ -1995,7 +1994,7 @@ if (!RUNNING_WITH_COMMAND) { sha256_ui: sha256Ui, sha256_webhapp: sha256Webhapp, distribution_info: JSON.stringify(newDistributionInfo), - network_seed: tool.network_seed, + network_seed: toolNetworkSeed, properties: {}, }; @@ -2057,7 +2056,7 @@ if (!RUNNING_WITH_COMMAND) { fs.writeFileSync(webHappPath, new Uint8Array(buffer)); const { happPath } = await rustUtils.saveHappOrWebhapp(webHappPath, happsDir, uisDir); happToBeInstalledPath = happPath; - try { fs.rmSync(tmpImportDir, { recursive: true }); } catch (_) {} + try { fs.rmSync(tmpImportDir, { recursive: true }); } catch (_) { } } const appAssetsInfo: AppAssetsInfo = deriveAppAssetsInfo( @@ -2073,7 +2072,7 @@ if (!RUNNING_WITH_COMMAND) { source: { type: 'path', value: happToBeInstalledPath }, installed_app_id: appletAppId, agent_key: myPubKey, - network_seed: tool.network_seed, + network_seed: toolNetworkSeed, }); await HOLOCHAIN_MANAGER!.adminWebsocket.enableApp({ installed_app_id: appletAppId });