refactor(workspace): profile to workspace model in web lib
Replace profile terminology with workspace; pin to .openclaw-ironclaw state dir.
This commit is contained in:
parent
785dd449f4
commit
796a2fcb34
@ -1,4 +1,4 @@
|
|||||||
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
import { existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync } from "node:fs";
|
||||||
import { execSync, exec } from "node:child_process";
|
import { execSync, exec } from "node:child_process";
|
||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import { join, resolve, normalize, relative } from "node:path";
|
import { join, resolve, normalize, relative } from "node:path";
|
||||||
@ -8,23 +8,19 @@ import { normalizeFilterGroup, type SavedView } from "./object-filters";
|
|||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// UI profile override — allows switching profiles at runtime without env vars.
|
|
||||||
// The active profile is held in-memory for immediate effect and persisted to
|
|
||||||
// ~/.openclaw/.ironclaw-ui-state.json so it survives server restarts.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
const UI_STATE_FILENAME = ".ironclaw-ui-state.json";
|
const UI_STATE_FILENAME = ".ironclaw-ui-state.json";
|
||||||
const LEGACY_STATE_DIRNAME = ".openclaw";
|
const FIXED_STATE_DIRNAME = ".openclaw-ironclaw";
|
||||||
const migratedProfiles = new Set<string>();
|
const WORKSPACE_PREFIX = "workspace-";
|
||||||
|
const ROOT_WORKSPACE_DIRNAME = "workspace";
|
||||||
|
const WORKSPACE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
||||||
|
const DEFAULT_WORKSPACE_NAME = "default";
|
||||||
|
const IRONCLAW_PROFILE = "ironclaw";
|
||||||
|
|
||||||
/** In-memory override; takes precedence over the persisted file. */
|
/** In-memory override; takes precedence over persisted state. */
|
||||||
let _uiActiveProfile: string | null | undefined;
|
let _uiActiveWorkspace: string | null | undefined;
|
||||||
|
|
||||||
type UIState = {
|
type UIState = {
|
||||||
activeProfile?: string | null;
|
activeWorkspace?: string | null;
|
||||||
/** Maps profile names to absolute workspace paths for workspaces outside ~/.openclaw/. */
|
|
||||||
workspaceRegistry?: Record<string, string>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveOpenClawHomeDir(): string {
|
function resolveOpenClawHomeDir(): string {
|
||||||
@ -42,92 +38,53 @@ function expandUserPath(input: string): string {
|
|||||||
return trimmed;
|
return trimmed;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeProfileName(profile: string | null | undefined): string | null {
|
function normalizeWorkspaceName(name: string | null | undefined): string | null {
|
||||||
const normalized = profile?.trim() || null;
|
const normalized = name?.trim() || null;
|
||||||
if (!normalized || normalized.toLowerCase() === "default") {
|
if (!normalized) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!WORKSPACE_NAME_RE.test(normalized)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLegacySharedStateDir(): string {
|
function workspaceDirName(workspaceName: string): string {
|
||||||
const override = process.env.OPENCLAW_STATE_DIR?.trim();
|
return `${WORKSPACE_PREFIX}${workspaceName}`;
|
||||||
if (override) {
|
|
||||||
return expandUserPath(override);
|
|
||||||
}
|
|
||||||
return join(resolveOpenClawHomeDir(), LEGACY_STATE_DIRNAME);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveProfileStateDir(profile: string | null | undefined): string {
|
function workspaceNameFromDirName(dirName: string): string | null {
|
||||||
const override = process.env.OPENCLAW_STATE_DIR?.trim();
|
if (dirName === ROOT_WORKSPACE_DIRNAME) {
|
||||||
if (override) {
|
return DEFAULT_WORKSPACE_NAME;
|
||||||
return expandUserPath(override);
|
|
||||||
}
|
}
|
||||||
const normalizedProfile = normalizeProfileName(profile);
|
if (!dirName.startsWith(WORKSPACE_PREFIX)) {
|
||||||
if (!normalizedProfile) {
|
return null;
|
||||||
return join(resolveOpenClawHomeDir(), LEGACY_STATE_DIRNAME);
|
|
||||||
}
|
}
|
||||||
return join(resolveOpenClawHomeDir(), `.openclaw-${normalizedProfile}`);
|
return normalizeWorkspaceName(dirName.slice(WORKSPACE_PREFIX.length));
|
||||||
}
|
}
|
||||||
|
|
||||||
function moveDirIfMissingTarget(fromDir: string, toDir: string): boolean {
|
function stateDirPath(): string {
|
||||||
if (!existsSync(fromDir) || existsSync(toDir)) {
|
return join(resolveOpenClawHomeDir(), FIXED_STATE_DIRNAME);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const parent = join(toDir, "..");
|
|
||||||
if (!existsSync(parent)) {
|
|
||||||
mkdirSync(parent, { recursive: true });
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
renameSync(fromDir, toDir);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrateLegacyProfileStorage(profile: string | null): void {
|
function resolveWorkspaceDir(workspaceName: string): string {
|
||||||
const normalizedProfile = normalizeProfileName(profile);
|
const stateDir = resolveOpenClawStateDir();
|
||||||
if (!normalizedProfile || process.env.OPENCLAW_STATE_DIR?.trim()) {
|
if (workspaceName === DEFAULT_WORKSPACE_NAME) {
|
||||||
return;
|
const rootWorkspaceDir = join(stateDir, ROOT_WORKSPACE_DIRNAME);
|
||||||
}
|
if (existsSync(rootWorkspaceDir)) {
|
||||||
const key = normalizedProfile.toLowerCase();
|
return rootWorkspaceDir;
|
||||||
if (migratedProfiles.has(key)) {
|
}
|
||||||
return;
|
const prefixedWorkspaceDir = join(stateDir, workspaceDirName(workspaceName));
|
||||||
}
|
if (existsSync(prefixedWorkspaceDir)) {
|
||||||
migratedProfiles.add(key);
|
return prefixedWorkspaceDir;
|
||||||
|
}
|
||||||
const legacyStateDir = resolveLegacySharedStateDir();
|
return rootWorkspaceDir;
|
||||||
const targetStateDir = resolveProfileStateDir(normalizedProfile);
|
|
||||||
const movedWorkspace = moveDirIfMissingTarget(
|
|
||||||
join(legacyStateDir, `workspace-${normalizedProfile}`),
|
|
||||||
join(targetStateDir, "workspace"),
|
|
||||||
);
|
|
||||||
const movedWebChat = moveDirIfMissingTarget(
|
|
||||||
join(legacyStateDir, `web-chat-${normalizedProfile}`),
|
|
||||||
join(targetStateDir, "web-chat"),
|
|
||||||
);
|
|
||||||
if (!movedWorkspace && !movedWebChat) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = readUIState();
|
|
||||||
const existing = state.workspaceRegistry?.[normalizedProfile];
|
|
||||||
if (
|
|
||||||
existing &&
|
|
||||||
resolve(existing) === resolve(join(legacyStateDir, `workspace-${normalizedProfile}`))
|
|
||||||
) {
|
|
||||||
const nextRegistry = { ...state.workspaceRegistry };
|
|
||||||
nextRegistry[normalizedProfile] = join(targetStateDir, "workspace");
|
|
||||||
writeUIState({
|
|
||||||
...state,
|
|
||||||
workspaceRegistry: nextRegistry,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
return join(stateDir, workspaceDirName(workspaceName));
|
||||||
}
|
}
|
||||||
|
|
||||||
function uiStatePath(): string {
|
function uiStatePath(): string {
|
||||||
return join(resolveOpenClawHomeDir(), LEGACY_STATE_DIRNAME, UI_STATE_FILENAME);
|
return join(resolveOpenClawStateDir(), UI_STATE_FILENAME);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readUIState(): UIState {
|
function readUIState(): UIState {
|
||||||
@ -142,60 +99,89 @@ function readUIState(): UIState {
|
|||||||
export function writeUIState(state: UIState): void {
|
export function writeUIState(state: UIState): void {
|
||||||
const p = uiStatePath();
|
const p = uiStatePath();
|
||||||
const dir = join(p, "..");
|
const dir = join(p, "..");
|
||||||
if (!existsSync(dir)) {mkdirSync(dir, { recursive: true });}
|
if (!existsSync(dir)) {
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
}
|
||||||
writeFileSync(p, JSON.stringify(state, null, 2) + "\n");
|
writeFileSync(p, JSON.stringify(state, null, 2) + "\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Get the effective profile: env var > in-memory override > persisted file. */
|
function workspaceNameFromPath(inputPath: string | null | undefined): string | null {
|
||||||
export function getEffectiveProfile(): string | null {
|
if (!inputPath) {
|
||||||
const envProfile = process.env.OPENCLAW_PROFILE?.trim();
|
return null;
|
||||||
if (envProfile) {return envProfile;}
|
}
|
||||||
if (_uiActiveProfile !== undefined) {return _uiActiveProfile;}
|
const resolvedPath = resolve(expandUserPath(inputPath));
|
||||||
const persisted = readUIState().activeProfile;
|
const stateRoot = resolve(resolveOpenClawStateDir());
|
||||||
return persisted?.trim() || null;
|
const rel = relative(stateRoot, resolvedPath);
|
||||||
|
if (!rel || rel.startsWith("..")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const top = rel.split(/[\\/]/)[0];
|
||||||
|
if (!top) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return workspaceNameFromDirName(top);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Set the UI-level profile override (in-memory + persisted). */
|
function scanWorkspaceNames(stateDir: string): string[] {
|
||||||
export function setUIActiveProfile(profile: string | null): void {
|
try {
|
||||||
const normalized = profile?.trim() || null;
|
const names = readdirSync(stateDir, { withFileTypes: true })
|
||||||
_uiActiveProfile = normalized;
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => workspaceNameFromDirName(entry.name))
|
||||||
|
.filter((name): name is string => Boolean(name));
|
||||||
|
return [...new Set(names)].toSorted((a, b) => a.localeCompare(b));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active workspace resolution precedence:
|
||||||
|
* 1) OPENCLAW_WORKSPACE env path (if it points at workspace or workspace-<name>)
|
||||||
|
* 2) in-memory UI override
|
||||||
|
* 3) persisted UI state
|
||||||
|
*/
|
||||||
|
export function getActiveWorkspaceName(): string | null {
|
||||||
|
const stateDir = resolveOpenClawStateDir();
|
||||||
|
const discoveredNames = scanWorkspaceNames(stateDir);
|
||||||
|
const hasDiscoveredWorkspace = (name: string | null | undefined): name is string =>
|
||||||
|
Boolean(name && discoveredNames.includes(name));
|
||||||
|
|
||||||
|
const envWorkspace = process.env.OPENCLAW_WORKSPACE?.trim();
|
||||||
|
const envWorkspaceName = workspaceNameFromPath(envWorkspace);
|
||||||
|
if (hasDiscoveredWorkspace(envWorkspaceName)) {
|
||||||
|
return envWorkspaceName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_uiActiveWorkspace === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (hasDiscoveredWorkspace(_uiActiveWorkspace)) {
|
||||||
|
return _uiActiveWorkspace;
|
||||||
|
}
|
||||||
|
|
||||||
|
const persisted = normalizeWorkspaceName(readUIState().activeWorkspace);
|
||||||
|
if (hasDiscoveredWorkspace(persisted)) {
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
return discoveredNames[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setUIActiveWorkspace(workspaceName: string | null): void {
|
||||||
|
const normalized = normalizeWorkspaceName(workspaceName);
|
||||||
|
_uiActiveWorkspace = normalized;
|
||||||
const existing = readUIState();
|
const existing = readUIState();
|
||||||
writeUIState({ ...existing, activeProfile: normalized });
|
writeUIState({ ...existing, activeWorkspace: normalized });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Reset the in-memory override (re-reads from file on next call). */
|
export function clearUIActiveWorkspaceCache(): void {
|
||||||
export function clearUIActiveProfileCache(): void {
|
_uiActiveWorkspace = undefined;
|
||||||
_uiActiveProfile = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
export function resolveOpenClawStateDir(): string {
|
||||||
// Workspace registry — remembers workspaces created outside ~/.openclaw/.
|
return stateDirPath();
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/** Read the full workspace registry (profile → absolute path). */
|
|
||||||
export function getWorkspaceRegistry(): Record<string, string> {
|
|
||||||
return readUIState().workspaceRegistry ?? {};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Look up a single profile's registered workspace path. */
|
export type DiscoveredWorkspace = {
|
||||||
export function getRegisteredWorkspacePath(profile: string | null): string | null {
|
|
||||||
if (!profile) {return null;}
|
|
||||||
return getWorkspaceRegistry()[profile] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Persist a profile → workspace-path mapping in the registry. */
|
|
||||||
export function registerWorkspacePath(profile: string, absolutePath: string): void {
|
|
||||||
const state = readUIState();
|
|
||||||
const registry = state.workspaceRegistry ?? {};
|
|
||||||
registry[profile] = absolutePath;
|
|
||||||
writeUIState({ ...state, workspaceRegistry: registry });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Profile discovery — scans the filesystem for all profiles/workspaces.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
export type DiscoveredProfile = {
|
|
||||||
name: string;
|
name: string;
|
||||||
stateDir: string;
|
stateDir: string;
|
||||||
workspaceDir: string | null;
|
workspaceDir: string | null;
|
||||||
@ -203,144 +189,107 @@ export type DiscoveredProfile = {
|
|||||||
hasConfig: boolean;
|
hasConfig: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
export function discoverWorkspaces(): DiscoveredWorkspace[] {
|
||||||
* Discover all profiles by scanning profile-scoped state directories
|
const stateDir = resolveOpenClawStateDir();
|
||||||
* (e.g. ~/.openclaw-ironclaw) and merging persisted registry entries.
|
const activeWorkspace = getActiveWorkspaceName();
|
||||||
*/
|
const discovered: DiscoveredWorkspace[] = [];
|
||||||
export function discoverProfiles(): DiscoveredProfile[] {
|
|
||||||
const home = resolveOpenClawHomeDir();
|
|
||||||
const defaultStateDir = resolveProfileStateDir(null);
|
|
||||||
const activeProfile = getEffectiveProfile();
|
|
||||||
const activeNormalized = normalizeProfileName(activeProfile);
|
|
||||||
const profiles: DiscoveredProfile[] = [];
|
|
||||||
const seen = new Set<string>();
|
|
||||||
|
|
||||||
// Default profile
|
for (const workspaceName of scanWorkspaceNames(stateDir)) {
|
||||||
const defaultWs = join(defaultStateDir, "workspace");
|
const workspaceDir = resolveWorkspaceDir(workspaceName);
|
||||||
profiles.push({
|
discovered.push({
|
||||||
name: "default",
|
name: workspaceName,
|
||||||
stateDir: defaultStateDir,
|
|
||||||
workspaceDir: existsSync(defaultWs) ? defaultWs : null,
|
|
||||||
isActive: !activeNormalized,
|
|
||||||
hasConfig: existsSync(join(defaultStateDir, "openclaw.json")),
|
|
||||||
});
|
|
||||||
seen.add("default");
|
|
||||||
|
|
||||||
// Scan for profile-scoped state dirs: ~/.openclaw-<profile>
|
|
||||||
try {
|
|
||||||
const entries = readdirSync(home, { withFileTypes: true });
|
|
||||||
for (const entry of entries) {
|
|
||||||
if (!entry.isDirectory()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const match = entry.name.match(/^\.openclaw-(.+)$/);
|
|
||||||
if (!match || !match[1]) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const profileName = match[1];
|
|
||||||
if (seen.has(profileName)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
migrateLegacyProfileStorage(profileName);
|
|
||||||
const stateDir = resolveProfileStateDir(profileName);
|
|
||||||
const wsDir = join(stateDir, "workspace");
|
|
||||||
profiles.push({
|
|
||||||
name: profileName,
|
|
||||||
stateDir,
|
|
||||||
workspaceDir: existsSync(wsDir) ? wsDir : null,
|
|
||||||
isActive: activeNormalized === profileName,
|
|
||||||
hasConfig: existsSync(join(stateDir, "openclaw.json")),
|
|
||||||
});
|
|
||||||
seen.add(profileName);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// dir unreadable
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge workspaces registered via custom paths (outside profile state dirs).
|
|
||||||
const registry = getWorkspaceRegistry();
|
|
||||||
for (const [rawProfileName, wsPath] of Object.entries(registry)) {
|
|
||||||
const normalized = normalizeProfileName(rawProfileName);
|
|
||||||
const profileName = normalized ?? "default";
|
|
||||||
if (normalized) {
|
|
||||||
migrateLegacyProfileStorage(normalized);
|
|
||||||
}
|
|
||||||
if (seen.has(profileName)) {
|
|
||||||
const existing = profiles.find((p) => p.name === profileName);
|
|
||||||
if (existing && !existing.workspaceDir && existsSync(wsPath)) {
|
|
||||||
existing.workspaceDir = wsPath;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seen.add(profileName);
|
|
||||||
const stateDir = resolveProfileStateDir(normalized);
|
|
||||||
profiles.push({
|
|
||||||
name: profileName,
|
|
||||||
stateDir,
|
stateDir,
|
||||||
workspaceDir: existsSync(wsPath) ? wsPath : null,
|
workspaceDir: existsSync(workspaceDir) ? workspaceDir : null,
|
||||||
isActive: normalized ? activeNormalized === normalized : !activeNormalized,
|
isActive: activeWorkspace === workspaceName,
|
||||||
hasConfig: existsSync(join(stateDir, "openclaw.json")),
|
hasConfig: existsSync(join(stateDir, "openclaw.json")),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return profiles;
|
discovered.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
if (!discovered.some((item) => item.isActive) && discovered.length > 0) {
|
||||||
// State directory & workspace resolution (profile-aware)
|
discovered[0] = {
|
||||||
// ---------------------------------------------------------------------------
|
...discovered[0],
|
||||||
|
isActive: true,
|
||||||
/**
|
};
|
||||||
* Resolve the OpenClaw state directory (base dir for config, sessions, agents, etc.).
|
|
||||||
* Mirrors CLI profile semantics:
|
|
||||||
* - default profile: ~/.openclaw
|
|
||||||
* - named profile: ~/.openclaw-<profile>
|
|
||||||
* - OPENCLAW_STATE_DIR override wins for all profiles
|
|
||||||
*/
|
|
||||||
export function resolveOpenClawStateDir(profileOverride?: string | null): string {
|
|
||||||
const profile = normalizeProfileName(profileOverride ?? getEffectiveProfile());
|
|
||||||
migrateLegacyProfileStorage(profile);
|
|
||||||
return resolveProfileStateDir(profile);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the web-chat sessions directory, scoped to the active profile.
|
|
||||||
* Always stores sessions at <profileStateDir>/web-chat.
|
|
||||||
*/
|
|
||||||
export function resolveWebChatDir(): string {
|
|
||||||
const stateDir = resolveOpenClawStateDir();
|
|
||||||
return join(stateDir, "web-chat");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the workspace directory, checking in order:
|
|
||||||
* 1. OPENCLAW_WORKSPACE env var
|
|
||||||
* 2. Registered profile-specific custom path
|
|
||||||
* 3. <profileStateDir>/workspace
|
|
||||||
* 4. Legacy fallback: ~/.openclaw/workspace-<profile> (non-default only)
|
|
||||||
*/
|
|
||||||
export function resolveWorkspaceRoot(): string | null {
|
|
||||||
const profile = getEffectiveProfile();
|
|
||||||
migrateLegacyProfileStorage(profile);
|
|
||||||
const normalizedProfile = normalizeProfileName(profile);
|
|
||||||
const stateDir = resolveProfileStateDir(profile);
|
|
||||||
const registryPath = getRegisteredWorkspacePath(profile);
|
|
||||||
const legacyWorkspaceFallback =
|
|
||||||
normalizedProfile
|
|
||||||
? join(resolveLegacySharedStateDir(), `workspace-${normalizedProfile}`)
|
|
||||||
: null;
|
|
||||||
const candidates = [
|
|
||||||
process.env.OPENCLAW_WORKSPACE,
|
|
||||||
registryPath,
|
|
||||||
join(stateDir, "workspace"),
|
|
||||||
legacyWorkspaceFallback,
|
|
||||||
].filter(Boolean) as string[];
|
|
||||||
|
|
||||||
for (const dir of candidates) {
|
|
||||||
if (existsSync(dir)) {return dir;}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return discovered;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compatibility shims while callers migrate away from profile semantics.
|
||||||
|
export type DiscoveredProfile = DiscoveredWorkspace;
|
||||||
|
export function discoverProfiles(): DiscoveredProfile[] {
|
||||||
|
return discoverWorkspaces();
|
||||||
|
}
|
||||||
|
export function getEffectiveProfile(): string {
|
||||||
|
return IRONCLAW_PROFILE;
|
||||||
|
}
|
||||||
|
export function setUIActiveProfile(profile: string | null): void {
|
||||||
|
setUIActiveWorkspace(normalizeWorkspaceName(profile));
|
||||||
|
}
|
||||||
|
export function clearUIActiveProfileCache(): void {
|
||||||
|
clearUIActiveWorkspaceCache();
|
||||||
|
}
|
||||||
|
export function getWorkspaceRegistry(): Record<string, string> {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
export function getRegisteredWorkspacePath(_profile: string | null): string | null {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
export function registerWorkspacePath(_profile: string, _absolutePath: string): void {
|
||||||
|
// No-op: workspace paths are discovered from managed dirs:
|
||||||
|
// ~/.openclaw-ironclaw/workspace (default) and ~/.openclaw-ironclaw/workspace-<name>.
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isValidWorkspaceName(name: string): boolean {
|
||||||
|
return normalizeWorkspaceName(name) !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorkspaceDirForName(name: string): string {
|
||||||
|
const normalized = normalizeWorkspaceName(name);
|
||||||
|
if (!normalized) {
|
||||||
|
throw new Error("Invalid workspace name.");
|
||||||
|
}
|
||||||
|
return resolveWorkspaceDir(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWorkspaceRoot(): string | null {
|
||||||
|
const explicitWorkspace = process.env.OPENCLAW_WORKSPACE?.trim();
|
||||||
|
const explicitWorkspaceName = workspaceNameFromPath(explicitWorkspace);
|
||||||
|
if (explicitWorkspaceName) {
|
||||||
|
const managedWorkspaceDir = resolveWorkspaceDir(explicitWorkspaceName);
|
||||||
|
if (existsSync(managedWorkspaceDir)) {
|
||||||
|
return managedWorkspaceDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeWorkspace = getActiveWorkspaceName();
|
||||||
|
if (activeWorkspace) {
|
||||||
|
const activeDir = resolveWorkspaceDir(activeWorkspace);
|
||||||
|
if (existsSync(activeDir)) {
|
||||||
|
return activeDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const discovered = discoverWorkspaces();
|
||||||
|
return discovered.find((workspace) => workspace.isActive)?.workspaceDir ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveWebChatDir(): string {
|
||||||
|
const workspaceRoot = resolveWorkspaceRoot();
|
||||||
|
if (workspaceRoot) {
|
||||||
|
return join(workspaceRoot, ".openclaw", "web-chat");
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeWorkspace = getActiveWorkspaceName();
|
||||||
|
if (activeWorkspace) {
|
||||||
|
return join(resolveWorkspaceDir(activeWorkspace), ".openclaw", "web-chat");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for first-run flows before any workspace is selected/created.
|
||||||
|
return join(resolveWorkspaceDir(DEFAULT_WORKSPACE_NAME), ".openclaw", "web-chat");
|
||||||
|
}
|
||||||
|
|
||||||
/** @deprecated Use `resolveWorkspaceRoot` instead. */
|
/** @deprecated Use `resolveWorkspaceRoot` instead. */
|
||||||
export const resolveDenchRoot = resolveWorkspaceRoot;
|
export const resolveDenchRoot = resolveWorkspaceRoot;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user