mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-11 09:51:30 +00:00
@@ -1,7 +1,15 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { dialog, ipcMain } = require('electron');
|
||||
const { normalizeAndResolvePath } = require('../utils/filesystem');
|
||||
const { generateUidBasedOnHash } = require('../utils/common');
|
||||
const { generateYamlContent, getWorkspaceUid } = require('../utils/workspace-config');
|
||||
const {
|
||||
addApiSpecToWorkspace,
|
||||
readWorkspaceConfig,
|
||||
getWorkspaceUid
|
||||
} = require('../utils/workspace-config');
|
||||
|
||||
const DEFAULT_WORKSPACE_NAME = 'My Workspace';
|
||||
|
||||
const normalizeWorkspaceConfig = (config) => {
|
||||
return {
|
||||
@@ -13,6 +21,17 @@ const normalizeWorkspaceConfig = (config) => {
|
||||
};
|
||||
};
|
||||
|
||||
const prepareWorkspaceConfigForClient = (workspaceConfig, isDefault) => {
|
||||
if (isDefault) {
|
||||
return {
|
||||
...workspaceConfig,
|
||||
name: DEFAULT_WORKSPACE_NAME,
|
||||
type: 'default'
|
||||
};
|
||||
}
|
||||
return workspaceConfig;
|
||||
};
|
||||
|
||||
const openApiSpecDialog = async (win, watcher, options = {}) => {
|
||||
const { filePaths } = await dialog.showOpenDialog(win, {
|
||||
properties: ['openFile', 'createFile']
|
||||
@@ -21,7 +40,7 @@ const openApiSpecDialog = async (win, watcher, options = {}) => {
|
||||
if (filePaths && filePaths[0]) {
|
||||
const resolvedPath = normalizeAndResolvePath(filePaths[0]);
|
||||
try {
|
||||
openApiSpec(win, watcher, resolvedPath, options);
|
||||
await openApiSpec(win, watcher, resolvedPath, options);
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Cannot open API spec: "${resolvedPath}"`);
|
||||
}
|
||||
@@ -33,35 +52,16 @@ const openApiSpec = async (win, watcher, apiSpecPath, options = {}) => {
|
||||
const uid = generateUidBasedOnHash(apiSpecPath);
|
||||
|
||||
if (options.workspacePath) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const workspaceFilePath = path.join(options.workspacePath, 'workspace.yml');
|
||||
|
||||
if (fs.existsSync(workspaceFilePath)) {
|
||||
const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8');
|
||||
const workspaceConfig = yaml.load(yamlContent);
|
||||
|
||||
const workspaceConfig = readWorkspaceConfig(options.workspacePath);
|
||||
const specs = workspaceConfig.specs || [];
|
||||
|
||||
let relativePath = apiSpecPath;
|
||||
try {
|
||||
const relPath = path.relative(options.workspacePath, apiSpecPath);
|
||||
if (!relPath.startsWith('..') && !path.isAbsolute(relPath)) {
|
||||
relativePath = relPath;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Using absolute path for API spec:', error.message);
|
||||
}
|
||||
|
||||
const specName = path.basename(apiSpecPath, path.extname(apiSpecPath));
|
||||
const specEntry = {
|
||||
name: specName,
|
||||
path: relativePath
|
||||
};
|
||||
|
||||
const existingSpec = specs.find((a) => {
|
||||
if (!a.path) return false;
|
||||
const existingPath = path.isAbsolute(a.path)
|
||||
? a.path
|
||||
: path.resolve(options.workspacePath, a.path);
|
||||
@@ -69,18 +69,17 @@ const openApiSpec = async (win, watcher, apiSpecPath, options = {}) => {
|
||||
});
|
||||
|
||||
if (!existingSpec) {
|
||||
workspaceConfig.specs = [...specs, specEntry];
|
||||
await addApiSpecToWorkspace(options.workspacePath, {
|
||||
name: specName,
|
||||
path: apiSpecPath
|
||||
});
|
||||
|
||||
const updatedYamlContent = generateYamlContent(workspaceConfig);
|
||||
fs.writeFileSync(workspaceFilePath, updatedYamlContent);
|
||||
|
||||
const normalizedConfig = normalizeWorkspaceConfig(workspaceConfig);
|
||||
const updatedConfig = readWorkspaceConfig(options.workspacePath);
|
||||
const normalizedConfig = normalizeWorkspaceConfig(updatedConfig);
|
||||
const workspaceUid = getWorkspaceUid(options.workspacePath);
|
||||
const isDefault = workspaceUid === 'default';
|
||||
win.webContents.send('main:workspace-config-updated', options.workspacePath, workspaceUid, {
|
||||
...normalizedConfig,
|
||||
type: isDefault ? 'default' : normalizedConfig.type
|
||||
});
|
||||
const configForClient = prepareWorkspaceConfigForClient(normalizedConfig, isDefault);
|
||||
win.webContents.send('main:workspace-config-updated', options.workspacePath, workspaceUid, configForClient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ const { decryptStringSafe } = require('../utils/encryption');
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
|
||||
const DEFAULT_WORKSPACE_NAME = 'My Workspace';
|
||||
|
||||
const envHasSecrets = (environment) => {
|
||||
const secrets = _.filter(environment.variables, (v) => v.secret === true);
|
||||
return secrets && secrets.length > 0;
|
||||
@@ -48,6 +50,7 @@ const handleWorkspaceFileChange = (win, workspacePath) => {
|
||||
|
||||
win.webContents.send('main:workspace-config-updated', workspacePath, workspaceUid, {
|
||||
...workspaceConfig,
|
||||
name: isDefault ? DEFAULT_WORKSPACE_NAME : workspaceConfig.name,
|
||||
type: isDefault ? 'default' : workspaceConfig.type
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -2,10 +2,9 @@ const { ipcMain } = require('electron');
|
||||
const { openApiSpecDialog, openApiSpec } = require('../app/apiSpecs');
|
||||
const { writeFile } = require('../utils/filesystem');
|
||||
const { removeApiSpecUid } = require('../cache/apiSpecUids');
|
||||
const { generateYamlContent } = require('../utils/workspace-config');
|
||||
const { removeApiSpecFromWorkspace } = require('../utils/workspace-config');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedApiSpecs) => {
|
||||
ipcMain.handle('renderer:open-api-spec', (event, workspacePath = null) => {
|
||||
@@ -52,26 +51,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedApiSpecs)
|
||||
const workspaceFilePath = path.join(workspacePath, 'workspace.yml');
|
||||
|
||||
if (fs.existsSync(workspaceFilePath)) {
|
||||
const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8');
|
||||
const workspaceConfig = yaml.load(yamlContent);
|
||||
|
||||
const specs = workspaceConfig.specs || [];
|
||||
|
||||
const filteredSpecs = specs.filter((a) => {
|
||||
const specPathFromYml = a.path;
|
||||
if (!specPathFromYml) return true;
|
||||
|
||||
const absoluteSpecPath = path.isAbsolute(specPathFromYml)
|
||||
? specPathFromYml
|
||||
: path.resolve(workspacePath, specPathFromYml);
|
||||
|
||||
return absoluteSpecPath !== pathname;
|
||||
});
|
||||
|
||||
workspaceConfig.specs = filteredSpecs;
|
||||
|
||||
const updatedYamlContent = generateYamlContent(workspaceConfig);
|
||||
fs.writeFileSync(workspaceFilePath, updatedYamlContent);
|
||||
await removeApiSpecFromWorkspace(workspacePath, pathname);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,19 @@ const {
|
||||
getWorkspaceUid
|
||||
} = require('../utils/workspace-config');
|
||||
|
||||
const DEFAULT_WORKSPACE_NAME = 'My Workspace';
|
||||
|
||||
const prepareWorkspaceConfigForClient = (workspaceConfig, isDefault) => {
|
||||
if (isDefault) {
|
||||
return {
|
||||
...workspaceConfig,
|
||||
name: DEFAULT_WORKSPACE_NAME,
|
||||
type: 'default'
|
||||
};
|
||||
}
|
||||
return workspaceConfig;
|
||||
};
|
||||
|
||||
const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
const lastOpenedWorkspaces = new LastOpenedWorkspaces();
|
||||
|
||||
@@ -58,17 +71,16 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
|
||||
lastOpenedWorkspaces.add(dirPath);
|
||||
|
||||
mainWindow.webContents.send('main:workspace-opened', dirPath, workspaceUid, {
|
||||
...workspaceConfig,
|
||||
type: isDefault ? 'default' : workspaceConfig.type
|
||||
});
|
||||
const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, isDefault);
|
||||
|
||||
mainWindow.webContents.send('main:workspace-opened', dirPath, workspaceUid, configForClient);
|
||||
|
||||
if (workspaceWatcher) {
|
||||
workspaceWatcher.addWatcher(mainWindow, dirPath);
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceConfig: { ...workspaceConfig, type: isDefault ? 'default' : workspaceConfig.type },
|
||||
workspaceConfig: configForClient,
|
||||
workspaceUid,
|
||||
workspacePath: dirPath
|
||||
};
|
||||
@@ -86,18 +98,18 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
|
||||
const workspaceUid = getWorkspaceUid(workspacePath);
|
||||
const isDefault = workspaceUid === 'default';
|
||||
const configWithType = { ...workspaceConfig, type: isDefault ? 'default' : workspaceConfig.type };
|
||||
const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, isDefault);
|
||||
|
||||
lastOpenedWorkspaces.add(workspacePath);
|
||||
|
||||
mainWindow.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configWithType);
|
||||
mainWindow.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configForClient);
|
||||
|
||||
if (workspaceWatcher) {
|
||||
workspaceWatcher.addWatcher(mainWindow, workspacePath);
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceConfig: configWithType,
|
||||
workspaceConfig: configForClient,
|
||||
workspaceUid,
|
||||
workspacePath
|
||||
};
|
||||
@@ -126,18 +138,18 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
|
||||
const workspaceUid = getWorkspaceUid(workspacePath);
|
||||
const isDefault = workspaceUid === 'default';
|
||||
const configWithType = { ...workspaceConfig, type: isDefault ? 'default' : workspaceConfig.type };
|
||||
const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, isDefault);
|
||||
|
||||
lastOpenedWorkspaces.add(workspacePath);
|
||||
|
||||
mainWindow.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configWithType);
|
||||
mainWindow.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configForClient);
|
||||
|
||||
if (workspaceWatcher) {
|
||||
workspaceWatcher.addWatcher(mainWindow, workspacePath);
|
||||
}
|
||||
|
||||
return {
|
||||
workspaceConfig: configWithType,
|
||||
workspaceConfig: configForClient,
|
||||
workspaceUid,
|
||||
workspacePath
|
||||
};
|
||||
@@ -361,11 +373,11 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
|
||||
const workspaceUid = getWorkspaceUid(finalWorkspacePath);
|
||||
const isDefault = workspaceUid === 'default';
|
||||
const configWithType = { ...finalConfig, type: isDefault ? 'default' : finalConfig.type };
|
||||
const configForClient = prepareWorkspaceConfigForClient(finalConfig, isDefault);
|
||||
|
||||
lastOpenedWorkspaces.add(finalWorkspacePath);
|
||||
|
||||
mainWindow.webContents.send('main:workspace-opened', finalWorkspacePath, workspaceUid, configWithType);
|
||||
mainWindow.webContents.send('main:workspace-opened', finalWorkspacePath, workspaceUid, configForClient);
|
||||
|
||||
if (workspaceWatcher) {
|
||||
workspaceWatcher.addWatcher(mainWindow, finalWorkspacePath);
|
||||
@@ -373,7 +385,7 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
|
||||
return {
|
||||
success: true,
|
||||
workspaceConfig: configWithType,
|
||||
workspaceConfig: configForClient,
|
||||
workspaceUid,
|
||||
workspacePath: finalWorkspacePath
|
||||
};
|
||||
@@ -490,10 +502,8 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
const workspaceConfig = readWorkspaceConfig(workspacePath);
|
||||
const workspaceUid = getWorkspaceUid(workspacePath);
|
||||
const isDefault = workspaceUid === 'default';
|
||||
mainWindow.webContents.send('main:workspace-config-updated', workspacePath, workspaceUid, {
|
||||
...workspaceConfig,
|
||||
type: isDefault ? 'default' : workspaceConfig.type
|
||||
});
|
||||
const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, isDefault);
|
||||
mainWindow.webContents.send('main:workspace-config-updated', workspacePath, workspaceUid, configForClient);
|
||||
|
||||
return updatedCollections;
|
||||
} catch (error) {
|
||||
@@ -534,10 +544,8 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
|
||||
const correctWorkspaceUid = getWorkspaceUid(workspacePath);
|
||||
const isDefault = correctWorkspaceUid === 'default';
|
||||
mainWindow.webContents.send('main:workspace-config-updated', workspacePath, correctWorkspaceUid, {
|
||||
...result.updatedConfig,
|
||||
type: isDefault ? 'default' : result.updatedConfig.type
|
||||
});
|
||||
const configForClient = prepareWorkspaceConfigForClient(result.updatedConfig, isDefault);
|
||||
mainWindow.webContents.send('main:workspace-config-updated', workspacePath, correctWorkspaceUid, configForClient);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
@@ -588,12 +596,10 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
|
||||
const { workspacePath, workspaceUid } = result;
|
||||
const workspaceConfig = readWorkspaceConfig(workspacePath);
|
||||
const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, true);
|
||||
|
||||
return {
|
||||
workspaceConfig: {
|
||||
...workspaceConfig,
|
||||
type: 'default'
|
||||
},
|
||||
workspaceConfig: configForClient,
|
||||
workspaceUid,
|
||||
workspacePath
|
||||
};
|
||||
@@ -605,23 +611,19 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
|
||||
ipcMain.on('main:renderer-ready', async (win) => {
|
||||
try {
|
||||
// Load default workspace
|
||||
const defaultResult = await defaultWorkspaceManager.ensureDefaultWorkspaceExists();
|
||||
if (defaultResult) {
|
||||
const { workspacePath, workspaceUid } = defaultResult;
|
||||
const workspaceConfig = readWorkspaceConfig(workspacePath);
|
||||
const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, true);
|
||||
|
||||
win.webContents.send('main:workspace-opened', workspacePath, workspaceUid, {
|
||||
...workspaceConfig,
|
||||
type: 'default'
|
||||
});
|
||||
win.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configForClient);
|
||||
|
||||
if (workspaceWatcher) {
|
||||
workspaceWatcher.addWatcher(win, workspacePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Load other workspaces
|
||||
const workspacePaths = lastOpenedWorkspaces.getAll();
|
||||
const invalidPaths = [];
|
||||
|
||||
@@ -634,11 +636,9 @@ const registerWorkspaceIpc = (mainWindow, workspaceWatcher) => {
|
||||
validateWorkspaceConfig(workspaceConfig);
|
||||
const workspaceUid = getWorkspaceUid(workspacePath);
|
||||
const isDefault = workspaceUid === 'default';
|
||||
const configForClient = prepareWorkspaceConfigForClient(workspaceConfig, isDefault);
|
||||
|
||||
win.webContents.send('main:workspace-opened', workspacePath, workspaceUid, {
|
||||
...workspaceConfig,
|
||||
type: isDefault ? 'default' : workspaceConfig.type
|
||||
});
|
||||
win.webContents.send('main:workspace-opened', workspacePath, workspaceUid, configForClient);
|
||||
|
||||
if (workspaceWatcher) {
|
||||
workspaceWatcher.addWatcher(win, workspacePath);
|
||||
|
||||
@@ -5,11 +5,17 @@ const { generateUidBasedOnHash } = require('../utils/common');
|
||||
const { writeFile } = require('../utils/filesystem');
|
||||
const { getPreferences, savePreferences } = require('./preferences');
|
||||
const { globalEnvironmentsStore } = require('./global-environments');
|
||||
const { generateYamlContent, readWorkspaceConfig, validateWorkspaceConfig } = require('../utils/workspace-config');
|
||||
const {
|
||||
generateYamlContent,
|
||||
readWorkspaceConfig,
|
||||
validateWorkspaceConfig,
|
||||
isValidCollectionEntry
|
||||
} = require('../utils/workspace-config');
|
||||
|
||||
const OPENCOLLECTION_VERSION = '1.0.0';
|
||||
const WORKSPACE_TYPE = 'workspace';
|
||||
const DEFAULT_WORKSPACE_UID = 'default';
|
||||
const MAX_WORKSPACE_CREATION_ATTEMPTS = 20;
|
||||
|
||||
class DefaultWorkspaceManager {
|
||||
constructor() {
|
||||
@@ -106,11 +112,15 @@ class DefaultWorkspaceManager {
|
||||
|
||||
let workspacePath = baseWorkspacePath;
|
||||
let counter = 1;
|
||||
while (fs.existsSync(workspacePath)) {
|
||||
while (fs.existsSync(workspacePath) && counter < MAX_WORKSPACE_CREATION_ATTEMPTS) {
|
||||
workspacePath = `${baseWorkspacePath}-${counter}`;
|
||||
counter++;
|
||||
}
|
||||
|
||||
if (counter >= MAX_WORKSPACE_CREATION_ATTEMPTS) {
|
||||
throw new Error('Unable to create default workspace: too many existing workspace directories');
|
||||
}
|
||||
|
||||
fs.mkdirSync(workspacePath, { recursive: true });
|
||||
fs.mkdirSync(path.join(workspacePath, 'collections'), { recursive: true });
|
||||
fs.mkdirSync(path.join(workspacePath, 'environments'), { recursive: true });
|
||||
@@ -126,8 +136,9 @@ class DefaultWorkspaceManager {
|
||||
docs: ''
|
||||
};
|
||||
|
||||
let migrationCleanupFn = null;
|
||||
if (migrateFromPreferences) {
|
||||
await this.migrateFromPreferences(workspacePath, workspaceConfig);
|
||||
migrationCleanupFn = await this.migrateFromPreferences(workspacePath, workspaceConfig);
|
||||
}
|
||||
|
||||
const yamlContent = generateYamlContent(workspaceConfig);
|
||||
@@ -135,27 +146,38 @@ class DefaultWorkspaceManager {
|
||||
|
||||
await this.setDefaultWorkspacePath(workspacePath);
|
||||
|
||||
if (migrationCleanupFn) {
|
||||
migrationCleanupFn();
|
||||
}
|
||||
|
||||
return workspacePath;
|
||||
}
|
||||
|
||||
async migrateFromPreferences(workspacePath, workspaceConfig) {
|
||||
try {
|
||||
const Store = require('electron-store');
|
||||
const preferencesStore = new Store({ name: 'preferences' });
|
||||
const Store = require('electron-store');
|
||||
const preferencesStore = new Store({ name: 'preferences' });
|
||||
|
||||
let shouldClearGlobalEnvStore = false;
|
||||
let shouldDeleteWorkspaceDocs = false;
|
||||
|
||||
try {
|
||||
const lastOpenedCollections = preferencesStore.get('lastOpenedCollections', []);
|
||||
|
||||
if (lastOpenedCollections && lastOpenedCollections.length > 0) {
|
||||
const collections = lastOpenedCollections.map((collectionPath) => {
|
||||
const absolutePath = path.resolve(collectionPath);
|
||||
const collectionName = path.basename(absolutePath);
|
||||
const collections = lastOpenedCollections
|
||||
.map((collectionPath) => {
|
||||
if (!collectionPath || typeof collectionPath !== 'string') {
|
||||
return null;
|
||||
}
|
||||
const absolutePath = path.resolve(collectionPath);
|
||||
const collectionName = path.basename(absolutePath);
|
||||
|
||||
return {
|
||||
type: 'preference',
|
||||
path: absolutePath,
|
||||
name: collectionName
|
||||
};
|
||||
});
|
||||
return {
|
||||
path: absolutePath,
|
||||
name: collectionName
|
||||
};
|
||||
})
|
||||
.filter((collection) => isValidCollectionEntry(collection));
|
||||
|
||||
workspaceConfig.collections = collections;
|
||||
}
|
||||
@@ -168,6 +190,10 @@ class DefaultWorkspaceManager {
|
||||
const environmentsDir = path.join(workspacePath, 'environments');
|
||||
|
||||
for (const env of globalEnvironments) {
|
||||
if (!env || !env.name || typeof env.name !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const envFilePath = path.join(environmentsDir, `${env.name}.yml`);
|
||||
|
||||
const environment = {
|
||||
@@ -184,18 +210,31 @@ class DefaultWorkspaceManager {
|
||||
}
|
||||
}
|
||||
|
||||
const globalEnvStore = new Store({ name: 'global-environments' });
|
||||
globalEnvStore.clear();
|
||||
shouldClearGlobalEnvStore = true;
|
||||
}
|
||||
|
||||
const defaultWorkspaceDocs = preferencesStore.get('preferences.defaultWorkspaceDocs', '');
|
||||
if (defaultWorkspaceDocs) {
|
||||
workspaceConfig.docs = defaultWorkspaceDocs;
|
||||
preferencesStore.delete('preferences.defaultWorkspaceDocs');
|
||||
shouldDeleteWorkspaceDocs = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to migrate from preferences:', error);
|
||||
}
|
||||
|
||||
return () => {
|
||||
try {
|
||||
if (shouldClearGlobalEnvStore) {
|
||||
const globalEnvStore = new Store({ name: 'global-environments' });
|
||||
globalEnvStore.clear();
|
||||
}
|
||||
if (shouldDeleteWorkspaceDocs) {
|
||||
preferencesStore.delete('preferences.defaultWorkspaceDocs');
|
||||
}
|
||||
} catch (cleanupError) {
|
||||
console.error('Failed to cleanup after migration:', cleanupError);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
needsMigration() {
|
||||
|
||||
@@ -7,7 +7,12 @@ const { writeFile, createDirectory } = require('../utils/filesystem');
|
||||
const { generateUidBasedOnHash, uuid } = require('../utils/common');
|
||||
const { decryptStringSafe } = require('../utils/encryption');
|
||||
const EnvironmentSecretsStore = require('./env-secrets');
|
||||
const { generateYamlContent } = require('../utils/workspace-config');
|
||||
const {
|
||||
readWorkspaceConfig,
|
||||
generateYamlContent,
|
||||
writeWorkspaceFileAtomic
|
||||
} = require('../utils/workspace-config');
|
||||
const { withLock, getWorkspaceLockKey } = require('../utils/workspace-lock');
|
||||
|
||||
const environmentSecretsStore = new EnvironmentSecretsStore();
|
||||
|
||||
@@ -147,29 +152,23 @@ class GlobalEnvironmentsManager {
|
||||
}
|
||||
|
||||
async setActiveGlobalEnvironmentUid(workspacePath, environmentUid) {
|
||||
try {
|
||||
if (!workspacePath) {
|
||||
throw new Error('Workspace path is required');
|
||||
}
|
||||
|
||||
const workspaceFilePath = path.join(workspacePath, 'workspace.yml');
|
||||
|
||||
if (!fs.existsSync(workspaceFilePath)) {
|
||||
throw new Error('Invalid workspace: workspace.yml not found');
|
||||
}
|
||||
|
||||
const yamlContent = fs.readFileSync(workspaceFilePath, 'utf8');
|
||||
const workspaceConfig = yaml.load(yamlContent);
|
||||
|
||||
workspaceConfig.activeEnvironmentUid = environmentUid;
|
||||
|
||||
const yamlOutput = generateYamlContent(workspaceConfig);
|
||||
|
||||
await writeFile(workspaceFilePath, yamlOutput);
|
||||
return true;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
if (!workspacePath) {
|
||||
throw new Error('Workspace path is required');
|
||||
}
|
||||
|
||||
const workspaceFilePath = path.join(workspacePath, 'workspace.yml');
|
||||
|
||||
if (!fs.existsSync(workspaceFilePath)) {
|
||||
throw new Error('Invalid workspace: workspace.yml not found');
|
||||
}
|
||||
|
||||
return withLock(getWorkspaceLockKey(workspacePath), async () => {
|
||||
const workspaceConfig = readWorkspaceConfig(workspacePath);
|
||||
workspaceConfig.activeEnvironmentUid = environmentUid;
|
||||
const yamlOutput = generateYamlContent(workspaceConfig);
|
||||
await writeWorkspaceFileAtomic(workspacePath, yamlOutput);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
async createGlobalEnvironment(workspacePath, { uid, name, variables }) {
|
||||
|
||||
@@ -1,12 +1,124 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const yaml = require('js-yaml');
|
||||
const crypto = require('node:crypto');
|
||||
const { writeFile, validateName } = require('./filesystem');
|
||||
const { generateUidBasedOnHash } = require('./common');
|
||||
const { withLock, getWorkspaceLockKey } = require('./workspace-lock');
|
||||
|
||||
const WORKSPACE_TYPE = 'workspace';
|
||||
const OPENCOLLECTION_VERSION = '1.0.0';
|
||||
|
||||
const quoteYamlValue = (value) => {
|
||||
if (typeof value !== 'string') {
|
||||
return `"${String(value)}"`;
|
||||
}
|
||||
|
||||
if (value === '') {
|
||||
return '""';
|
||||
}
|
||||
|
||||
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
return `"${escaped}"`;
|
||||
};
|
||||
|
||||
const writeWorkspaceFileAtomic = async (workspacePath, content) => {
|
||||
const workspaceFilePath = path.join(workspacePath, 'workspace.yml');
|
||||
const tempFilePath = path.join(os.tmpdir(), `workspace-${Date.now()}-${crypto.randomBytes(16).toString('hex')}.yml`);
|
||||
|
||||
try {
|
||||
await writeFile(tempFilePath, content);
|
||||
|
||||
if (fs.existsSync(workspaceFilePath)) {
|
||||
fs.unlinkSync(workspaceFilePath);
|
||||
}
|
||||
|
||||
fs.renameSync(tempFilePath, workspaceFilePath);
|
||||
} catch (error) {
|
||||
if (fs.existsSync(tempFilePath)) {
|
||||
try {
|
||||
fs.unlinkSync(tempFilePath);
|
||||
} catch (_) {}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const isValidCollectionEntry = (collection) => {
|
||||
if (!collection || typeof collection !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!collection.name || typeof collection.name !== 'string' || collection.name.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!collection.path || typeof collection.path !== 'string' || collection.path.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const isValidSpecEntry = (spec) => {
|
||||
if (!spec || typeof spec !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!spec.name || typeof spec.name !== 'string' || spec.name.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!spec.path || typeof spec.path !== 'string' || spec.path.trim() === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const sanitizeCollections = (collections) => {
|
||||
if (!Array.isArray(collections)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collections.filter((collection) => {
|
||||
if (!isValidCollectionEntry(collection)) {
|
||||
console.error('Skipping invalid collection entry:', collection);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).map((collection) => {
|
||||
const sanitized = {
|
||||
name: collection.name.trim(),
|
||||
path: collection.path.trim()
|
||||
};
|
||||
|
||||
if (collection.remote && typeof collection.remote === 'string') {
|
||||
sanitized.remote = collection.remote.trim();
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
});
|
||||
};
|
||||
|
||||
const sanitizeSpecs = (specs) => {
|
||||
if (!Array.isArray(specs)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return specs.filter((spec) => {
|
||||
if (!isValidSpecEntry(spec)) {
|
||||
console.error('Skipping invalid spec entry:', spec);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).map((spec) => ({
|
||||
name: spec.name.trim(),
|
||||
path: spec.path.trim()
|
||||
}));
|
||||
};
|
||||
|
||||
const makeRelativePath = (workspacePath, absolutePath) => {
|
||||
if (!path.isAbsolute(absolutePath)) {
|
||||
return absolutePath;
|
||||
@@ -102,23 +214,23 @@ const readWorkspaceConfig = (workspacePath) => {
|
||||
|
||||
const generateYamlContent = (config) => {
|
||||
const yamlLines = [];
|
||||
const workspaceName = config.info?.name || config.name || 'Unnamed Workspace';
|
||||
const workspaceType = config.info?.type || config.type || WORKSPACE_TYPE;
|
||||
|
||||
yamlLines.push(`opencollection: ${config.opencollection || OPENCOLLECTION_VERSION}`);
|
||||
yamlLines.push('info:');
|
||||
yamlLines.push(` name: ${config.info?.name || config.name}`);
|
||||
yamlLines.push(` type: ${config.info?.type || config.type || WORKSPACE_TYPE}`);
|
||||
yamlLines.push(` name: ${quoteYamlValue(workspaceName)}`);
|
||||
yamlLines.push(` type: ${workspaceType}`);
|
||||
yamlLines.push('');
|
||||
|
||||
const collections = config.collections || [];
|
||||
const collections = sanitizeCollections(config.collections);
|
||||
if (collections.length > 0) {
|
||||
yamlLines.push('collections:');
|
||||
for (const collection of collections) {
|
||||
yamlLines.push(` - name: ${collection.name}`);
|
||||
yamlLines.push(` path: ${collection.path}`);
|
||||
yamlLines.push(` - name: ${quoteYamlValue(collection.name)}`);
|
||||
yamlLines.push(` path: ${quoteYamlValue(collection.path)}`);
|
||||
if (collection.remote) {
|
||||
yamlLines.push(` remote: ${collection.remote}`);
|
||||
}
|
||||
if (collection.type) {
|
||||
yamlLines.push(` type: ${collection.type}`);
|
||||
yamlLines.push(` remote: ${quoteYamlValue(collection.remote)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -126,12 +238,12 @@ const generateYamlContent = (config) => {
|
||||
}
|
||||
yamlLines.push('');
|
||||
|
||||
const specs = config.specs || [];
|
||||
const specs = sanitizeSpecs(config.specs);
|
||||
if (specs.length > 0) {
|
||||
yamlLines.push('specs:');
|
||||
for (const spec of specs) {
|
||||
yamlLines.push(` - name: ${spec.name}`);
|
||||
yamlLines.push(` path: ${spec.path}`);
|
||||
yamlLines.push(` - name: ${quoteYamlValue(spec.name)}`);
|
||||
yamlLines.push(` path: ${quoteYamlValue(spec.path)}`);
|
||||
}
|
||||
} else {
|
||||
yamlLines.push('specs:');
|
||||
@@ -142,13 +254,13 @@ const generateYamlContent = (config) => {
|
||||
if (docs) {
|
||||
const escapedDocs = docs.includes('\n')
|
||||
? `|-\n ${docs.split('\n').join('\n ')}`
|
||||
: `'${docs.replace(/'/g, '\'\'')}'`;
|
||||
: quoteYamlValue(docs);
|
||||
yamlLines.push(`docs: ${escapedDocs}`);
|
||||
} else {
|
||||
yamlLines.push('docs: \'\'');
|
||||
}
|
||||
|
||||
if (config.activeEnvironmentUid) {
|
||||
if (config.activeEnvironmentUid && typeof config.activeEnvironmentUid === 'string') {
|
||||
yamlLines.push('');
|
||||
yamlLines.push(`activeEnvironmentUid: ${config.activeEnvironmentUid}`);
|
||||
}
|
||||
@@ -159,8 +271,10 @@ const generateYamlContent = (config) => {
|
||||
};
|
||||
|
||||
const writeWorkspaceConfig = async (workspacePath, config) => {
|
||||
const yamlContent = generateYamlContent(config);
|
||||
await writeFile(path.join(workspacePath, 'workspace.yml'), yamlContent);
|
||||
return withLock(getWorkspaceLockKey(workspacePath), async () => {
|
||||
const yamlContent = generateYamlContent(config);
|
||||
await writeWorkspaceFileAtomic(workspacePath, yamlContent);
|
||||
});
|
||||
};
|
||||
|
||||
const validateWorkspaceConfig = (config) => {
|
||||
@@ -182,88 +296,104 @@ const validateWorkspaceConfig = (config) => {
|
||||
};
|
||||
|
||||
const updateWorkspaceName = async (workspacePath, newName) => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
config.name = newName;
|
||||
if (config.info) {
|
||||
config.info.name = newName;
|
||||
}
|
||||
await writeWorkspaceConfig(workspacePath, config);
|
||||
return config;
|
||||
return withLock(getWorkspaceLockKey(workspacePath), async () => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
config.name = newName;
|
||||
if (config.info) {
|
||||
config.info.name = newName;
|
||||
}
|
||||
const yamlContent = generateYamlContent(config);
|
||||
await writeWorkspaceFileAtomic(workspacePath, yamlContent);
|
||||
return config;
|
||||
});
|
||||
};
|
||||
|
||||
const updateWorkspaceDocs = async (workspacePath, docs) => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
config.docs = docs;
|
||||
await writeWorkspaceConfig(workspacePath, config);
|
||||
return docs;
|
||||
return withLock(getWorkspaceLockKey(workspacePath), async () => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
config.docs = docs;
|
||||
const yamlContent = generateYamlContent(config);
|
||||
await writeWorkspaceFileAtomic(workspacePath, yamlContent);
|
||||
return docs;
|
||||
});
|
||||
};
|
||||
|
||||
const addCollectionToWorkspace = async (workspacePath, collection) => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
|
||||
if (!config.collections) {
|
||||
config.collections = [];
|
||||
if (!isValidCollectionEntry(collection)) {
|
||||
throw new Error('Invalid collection: name and path are required');
|
||||
}
|
||||
|
||||
const normalizedCollection = {
|
||||
name: collection.name,
|
||||
path: collection.path
|
||||
};
|
||||
return withLock(getWorkspaceLockKey(workspacePath), async () => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
|
||||
if (collection.remote) {
|
||||
normalizedCollection.remote = collection.remote;
|
||||
}
|
||||
if (!config.collections) {
|
||||
config.collections = [];
|
||||
}
|
||||
|
||||
const existingIndex = config.collections.findIndex((c) => c.path === normalizedCollection.path);
|
||||
const normalizedCollection = {
|
||||
name: collection.name.trim(),
|
||||
path: collection.path.trim()
|
||||
};
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
config.collections[existingIndex] = normalizedCollection;
|
||||
} else {
|
||||
config.collections.push(normalizedCollection);
|
||||
}
|
||||
if (collection.remote && typeof collection.remote === 'string') {
|
||||
normalizedCollection.remote = collection.remote.trim();
|
||||
}
|
||||
|
||||
await writeWorkspaceConfig(workspacePath, config);
|
||||
return config.collections;
|
||||
const existingIndex = config.collections.findIndex((c) => c.path === normalizedCollection.path);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
config.collections[existingIndex] = normalizedCollection;
|
||||
} else {
|
||||
config.collections.push(normalizedCollection);
|
||||
}
|
||||
|
||||
const yamlContent = generateYamlContent(config);
|
||||
await writeWorkspaceFileAtomic(workspacePath, yamlContent);
|
||||
return config.collections;
|
||||
});
|
||||
};
|
||||
|
||||
const removeCollectionFromWorkspace = async (workspacePath, collectionPath) => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
return withLock(getWorkspaceLockKey(workspacePath), async () => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
|
||||
let removedCollection = null;
|
||||
let shouldDeleteFiles = false;
|
||||
let removedCollection = null;
|
||||
let shouldDeleteFiles = false;
|
||||
|
||||
config.collections = (config.collections || []).filter((c) => {
|
||||
const collectionPathFromYml = c.path;
|
||||
config.collections = (config.collections || []).filter((c) => {
|
||||
const collectionPathFromYml = c.path;
|
||||
|
||||
if (!collectionPathFromYml) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const absoluteCollectionPath = path.isAbsolute(collectionPathFromYml)
|
||||
? collectionPathFromYml
|
||||
: path.resolve(workspacePath, collectionPathFromYml);
|
||||
|
||||
if (path.normalize(absoluteCollectionPath) === path.normalize(collectionPath)) {
|
||||
removedCollection = c;
|
||||
|
||||
const hasRemote = c.remote;
|
||||
const isExternalPath = path.isAbsolute(collectionPathFromYml);
|
||||
|
||||
shouldDeleteFiles = !hasRemote && !isExternalPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!collectionPathFromYml) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
const absoluteCollectionPath = path.isAbsolute(collectionPathFromYml)
|
||||
? collectionPathFromYml
|
||||
: path.resolve(workspacePath, collectionPathFromYml);
|
||||
const yamlContent = generateYamlContent(config);
|
||||
await writeWorkspaceFileAtomic(workspacePath, yamlContent);
|
||||
|
||||
if (path.normalize(absoluteCollectionPath) === path.normalize(collectionPath)) {
|
||||
removedCollection = c;
|
||||
|
||||
const hasRemote = c.remote;
|
||||
const isExternalPath = path.isAbsolute(collectionPathFromYml);
|
||||
|
||||
shouldDeleteFiles = !hasRemote && !isExternalPath;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return {
|
||||
removedCollection,
|
||||
shouldDeleteFiles,
|
||||
updatedConfig: config
|
||||
};
|
||||
});
|
||||
|
||||
await writeWorkspaceConfig(workspacePath, config);
|
||||
|
||||
return {
|
||||
removedCollection,
|
||||
shouldDeleteFiles,
|
||||
updatedConfig: config
|
||||
};
|
||||
};
|
||||
|
||||
const getWorkspaceCollections = (workspacePath) => {
|
||||
@@ -297,62 +427,72 @@ const getWorkspaceApiSpecs = (workspacePath) => {
|
||||
};
|
||||
|
||||
const addApiSpecToWorkspace = async (workspacePath, apiSpec) => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
|
||||
if (!config.specs) {
|
||||
config.specs = [];
|
||||
if (!isValidSpecEntry(apiSpec)) {
|
||||
throw new Error('Invalid API spec: name and path are required');
|
||||
}
|
||||
|
||||
const normalizedSpec = {
|
||||
name: apiSpec.name,
|
||||
path: makeRelativePath(workspacePath, apiSpec.path)
|
||||
};
|
||||
return withLock(getWorkspaceLockKey(workspacePath), async () => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
|
||||
const existingIndex = config.specs.findIndex(
|
||||
(a) => a.name === normalizedSpec.name || a.path === normalizedSpec.path
|
||||
);
|
||||
if (!config.specs) {
|
||||
config.specs = [];
|
||||
}
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
config.specs[existingIndex] = normalizedSpec;
|
||||
} else {
|
||||
config.specs.push(normalizedSpec);
|
||||
}
|
||||
const normalizedSpec = {
|
||||
name: apiSpec.name.trim(),
|
||||
path: makeRelativePath(workspacePath, apiSpec.path).trim()
|
||||
};
|
||||
|
||||
await writeWorkspaceConfig(workspacePath, config);
|
||||
return config.specs;
|
||||
const existingIndex = config.specs.findIndex(
|
||||
(a) => a.name === normalizedSpec.name || a.path === normalizedSpec.path
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
config.specs[existingIndex] = normalizedSpec;
|
||||
} else {
|
||||
config.specs.push(normalizedSpec);
|
||||
}
|
||||
|
||||
const yamlContent = generateYamlContent(config);
|
||||
await writeWorkspaceFileAtomic(workspacePath, yamlContent);
|
||||
return config.specs;
|
||||
});
|
||||
};
|
||||
|
||||
const removeApiSpecFromWorkspace = async (workspacePath, apiSpecPath) => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
return withLock(getWorkspaceLockKey(workspacePath), async () => {
|
||||
const config = readWorkspaceConfig(workspacePath);
|
||||
|
||||
if (!config.specs) {
|
||||
return { removedApiSpec: null, updatedConfig: config };
|
||||
}
|
||||
|
||||
let removedApiSpec = null;
|
||||
|
||||
config.specs = config.specs.filter((a) => {
|
||||
const specPathFromYml = a.path;
|
||||
if (!specPathFromYml) return true;
|
||||
|
||||
const absoluteSpecPath = path.isAbsolute(specPathFromYml)
|
||||
? specPathFromYml
|
||||
: path.resolve(workspacePath, specPathFromYml);
|
||||
|
||||
if (path.normalize(absoluteSpecPath) === path.normalize(apiSpecPath)) {
|
||||
removedApiSpec = a;
|
||||
return false;
|
||||
if (!config.specs) {
|
||||
return { removedApiSpec: null, updatedConfig: config };
|
||||
}
|
||||
|
||||
return true;
|
||||
let removedApiSpec = null;
|
||||
|
||||
config.specs = config.specs.filter((a) => {
|
||||
const specPathFromYml = a.path;
|
||||
if (!specPathFromYml) return true;
|
||||
|
||||
const absoluteSpecPath = path.isAbsolute(specPathFromYml)
|
||||
? specPathFromYml
|
||||
: path.resolve(workspacePath, specPathFromYml);
|
||||
|
||||
if (path.normalize(absoluteSpecPath) === path.normalize(apiSpecPath)) {
|
||||
removedApiSpec = a;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const yamlContent = generateYamlContent(config);
|
||||
await writeWorkspaceFileAtomic(workspacePath, yamlContent);
|
||||
|
||||
return {
|
||||
removedApiSpec,
|
||||
updatedConfig: config
|
||||
};
|
||||
});
|
||||
|
||||
await writeWorkspaceConfig(workspacePath, config);
|
||||
|
||||
return {
|
||||
removedApiSpec,
|
||||
updatedConfig: config
|
||||
};
|
||||
};
|
||||
|
||||
const getWorkspaceUid = (workspacePath) => {
|
||||
@@ -382,5 +522,8 @@ module.exports = {
|
||||
addApiSpecToWorkspace,
|
||||
removeApiSpecFromWorkspace,
|
||||
generateYamlContent,
|
||||
getWorkspaceUid
|
||||
getWorkspaceUid,
|
||||
writeWorkspaceFileAtomic,
|
||||
isValidCollectionEntry,
|
||||
isValidSpecEntry
|
||||
};
|
||||
|
||||
43
packages/bruno-electron/src/utils/workspace-lock.js
Normal file
43
packages/bruno-electron/src/utils/workspace-lock.js
Normal file
@@ -0,0 +1,43 @@
|
||||
const locks = new Map();
|
||||
|
||||
const acquireLock = async (key, timeout = 10000) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
while (locks.has(key)) {
|
||||
if (Date.now() - startTime > timeout) {
|
||||
throw new Error(`Lock acquisition timeout for: ${key}`);
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
let releaseFn;
|
||||
const releasePromise = new Promise((resolve) => {
|
||||
releaseFn = resolve;
|
||||
});
|
||||
|
||||
locks.set(key, releasePromise);
|
||||
|
||||
return () => {
|
||||
locks.delete(key);
|
||||
releaseFn();
|
||||
};
|
||||
};
|
||||
|
||||
const withLock = async (key, fn) => {
|
||||
const release = await acquireLock(key);
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
};
|
||||
|
||||
const getWorkspaceLockKey = (workspacePath) => {
|
||||
return `workspace:${workspacePath}`;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
acquireLock,
|
||||
withLock,
|
||||
getWorkspaceLockKey
|
||||
};
|
||||
Reference in New Issue
Block a user