imtoagent 0.3.22 → 0.3.24
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/imtoagent-real +555 -3
- package/index.ts +50 -12
- package/modules/cli/setup.ts +185 -54
- package/modules/utils/migrate-workspaces.ts +230 -0
- package/modules/utils/paths.ts +1 -1
- package/modules/utils/workspace-manager.ts +209 -0
- package/package.json +1 -1
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// ================================================================
|
|
2
|
+
// Workspace Migration — 老用户平滑迁移
|
|
3
|
+
// ================================================================
|
|
4
|
+
// 职责:
|
|
5
|
+
// 1. 检测旧目录结构(sessions/ + soul/)
|
|
6
|
+
// 2. 迁移到新 workspace 结构
|
|
7
|
+
// 3. 保留旧目录作为 .backup(不删除,安全优先)
|
|
8
|
+
// 4. 标记迁移完成,避免重复执行
|
|
9
|
+
// ================================================================
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { getDataDir } from './paths';
|
|
14
|
+
|
|
15
|
+
const MIGRATION_MARKER = '.workspace-migrated';
|
|
16
|
+
|
|
17
|
+
interface MigrationResult {
|
|
18
|
+
migrated: boolean;
|
|
19
|
+
botsMigrated: string[];
|
|
20
|
+
errors: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ================================================================
|
|
24
|
+
// 公共入口
|
|
25
|
+
// ================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 检测并执行 workspace 迁移。
|
|
29
|
+
*
|
|
30
|
+
* 启动时调用一次。如果已经迁移过,直接返回。
|
|
31
|
+
* 返回迁移结果摘要,可用于日志输出。
|
|
32
|
+
*/
|
|
33
|
+
export function migrateWorkspaces(): MigrationResult {
|
|
34
|
+
const dataDir = getDataDir();
|
|
35
|
+
const markerPath = path.join(dataDir, MIGRATION_MARKER);
|
|
36
|
+
|
|
37
|
+
// 已经迁移过,跳过
|
|
38
|
+
if (fs.existsSync(markerPath)) {
|
|
39
|
+
return { migrated: false, botsMigrated: [], errors: [] };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result: MigrationResult = { migrated: true, botsMigrated: [], errors: [] };
|
|
43
|
+
|
|
44
|
+
// 加载 bot-ids.json(UUID 映射)
|
|
45
|
+
const botIds = loadBotIds(dataDir);
|
|
46
|
+
|
|
47
|
+
// 旧目录
|
|
48
|
+
const oldSessionsDir = path.join(dataDir, 'sessions');
|
|
49
|
+
const oldSoulDir = path.join(dataDir, 'soul');
|
|
50
|
+
|
|
51
|
+
// 发现需要迁移的 Bot(从旧 sessions 目录扫描)
|
|
52
|
+
const botKeys = discoverBotKeys(oldSessionsDir, oldSoulDir);
|
|
53
|
+
if (botKeys.length === 0) {
|
|
54
|
+
// 没有旧数据,直接标记
|
|
55
|
+
markMigrated(markerPath);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.error(`[Migration] Found ${botKeys.length} bot(s) to migrate: ${botKeys.join(', ')}`);
|
|
60
|
+
|
|
61
|
+
for (const botKey of botKeys) {
|
|
62
|
+
try {
|
|
63
|
+
const botUuid = ensureBotUuid(botKey, botIds, dataDir);
|
|
64
|
+
migrateBotData(botKey, botUuid, dataDir, oldSessionsDir, oldSoulDir);
|
|
65
|
+
result.botsMigrated.push(botKey);
|
|
66
|
+
} catch (e: any) {
|
|
67
|
+
const msg = `[${botKey}] ${e.message}`;
|
|
68
|
+
result.errors.push(msg);
|
|
69
|
+
console.error(`[Migration] ERROR: ${msg}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 迁移完成后创建标记
|
|
74
|
+
markMigrated(markerPath);
|
|
75
|
+
|
|
76
|
+
// 打印摘要
|
|
77
|
+
if (result.botsMigrated.length > 0) {
|
|
78
|
+
console.error(`[Migration] ✅ ${result.botsMigrated.length} bot(s) migrated successfully`);
|
|
79
|
+
}
|
|
80
|
+
if (result.errors.length > 0) {
|
|
81
|
+
console.error(`[Migration] ⚠️ ${result.errors.length} error(s): ${result.errors.join('; ')}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ================================================================
|
|
88
|
+
// 内部函数
|
|
89
|
+
// ================================================================
|
|
90
|
+
|
|
91
|
+
/** 加载 bot-ids.json */
|
|
92
|
+
function loadBotIds(dataDir: string): Record<string, string> {
|
|
93
|
+
const botIdsFile = path.join(dataDir, 'bot-ids.json');
|
|
94
|
+
try {
|
|
95
|
+
if (fs.existsSync(botIdsFile)) {
|
|
96
|
+
return JSON.parse(fs.readFileSync(botIdsFile, 'utf8'));
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// 文件损坏,重新开始
|
|
100
|
+
}
|
|
101
|
+
return {};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** 确保 Bot 有 UUID */
|
|
105
|
+
function ensureBotUuid(
|
|
106
|
+
botKey: string,
|
|
107
|
+
botIds: Record<string, string>,
|
|
108
|
+
dataDir: string,
|
|
109
|
+
): string {
|
|
110
|
+
if (botIds[botKey]) return botIds[botKey];
|
|
111
|
+
|
|
112
|
+
const uuid = crypto.randomUUID();
|
|
113
|
+
botIds[botKey] = uuid;
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
fs.writeFileSync(path.join(dataDir, 'bot-ids.json'), JSON.stringify(botIds, null, 2));
|
|
117
|
+
} catch {
|
|
118
|
+
// 写入失败不影响迁移流程
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return uuid;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 发现需要迁移的 Bot keys */
|
|
125
|
+
function discoverBotKeys(sessionsDir: string, soulDir: string): string[] {
|
|
126
|
+
const botKeys = new Set<string>();
|
|
127
|
+
|
|
128
|
+
// 从 sessions 目录扫描
|
|
129
|
+
if (fs.existsSync(sessionsDir)) {
|
|
130
|
+
try {
|
|
131
|
+
for (const entry of fs.readdirSync(sessionsDir)) {
|
|
132
|
+
if (entry.startsWith('.')) continue; // 跳过 .restore 等隐藏文件
|
|
133
|
+
const entryPath = path.join(sessionsDir, entry);
|
|
134
|
+
if (fs.statSync(entryPath).isDirectory()) {
|
|
135
|
+
botKeys.add(entry);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// 忽略读取失败
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 从 soul 目录扫描(补充)
|
|
144
|
+
if (fs.existsSync(soulDir)) {
|
|
145
|
+
try {
|
|
146
|
+
for (const entry of fs.readdirSync(soulDir)) {
|
|
147
|
+
if (entry.startsWith('.')) continue;
|
|
148
|
+
const entryPath = path.join(soulDir, entry);
|
|
149
|
+
if (fs.statSync(entryPath).isDirectory()) {
|
|
150
|
+
botKeys.add(entry);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} catch {
|
|
154
|
+
// 忽略读取失败
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return Array.from(botKeys);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** 迁移单个 Bot 的数据 */
|
|
162
|
+
function migrateBotData(
|
|
163
|
+
botKey: string,
|
|
164
|
+
botUuid: string,
|
|
165
|
+
dataDir: string,
|
|
166
|
+
oldSessionsDir: string,
|
|
167
|
+
oldSoulDir: string,
|
|
168
|
+
): void {
|
|
169
|
+
const workspacesDir = path.join(dataDir, 'workspaces');
|
|
170
|
+
const newWorkspacePath = path.join(workspacesDir, botUuid);
|
|
171
|
+
const newSessionsPath = path.join(newWorkspacePath, 'sessions');
|
|
172
|
+
const newSoulPath = path.join(newWorkspacePath, 'soul');
|
|
173
|
+
|
|
174
|
+
console.error(`[Migration] Migrating [${botKey}] → workspace/${botUuid}/`);
|
|
175
|
+
|
|
176
|
+
// 确保新目录存在
|
|
177
|
+
fs.mkdirSync(newSessionsPath, { recursive: true });
|
|
178
|
+
fs.mkdirSync(newSoulPath, { recursive: true });
|
|
179
|
+
|
|
180
|
+
// 1. 迁移 session 文件
|
|
181
|
+
const oldBotSessionsDir = path.join(oldSessionsDir, botKey);
|
|
182
|
+
if (fs.existsSync(oldBotSessionsDir)) {
|
|
183
|
+
const sessionFiles = fs.readdirSync(oldBotSessionsDir).filter((f) => f.endsWith('.memory.json'));
|
|
184
|
+
for (const file of sessionFiles) {
|
|
185
|
+
const src = path.join(oldBotSessionsDir, file);
|
|
186
|
+
const dst = path.join(newSessionsPath, file);
|
|
187
|
+
if (!fs.existsSync(dst)) {
|
|
188
|
+
fs.copyFileSync(src, dst);
|
|
189
|
+
console.error(`[Migration] session: ${file}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 2. 迁移 soul 文件
|
|
195
|
+
const oldBotSoulDir = path.join(oldSoulDir, botKey);
|
|
196
|
+
if (fs.existsSync(oldBotSoulDir)) {
|
|
197
|
+
const soulFiles = fs.readdirSync(oldBotSoulDir);
|
|
198
|
+
for (const file of soulFiles) {
|
|
199
|
+
const src = path.join(oldBotSoulDir, file);
|
|
200
|
+
const dst = path.join(newSoulPath, file);
|
|
201
|
+
if (!fs.existsSync(dst) && fs.statSync(src).isFile()) {
|
|
202
|
+
fs.copyFileSync(src, dst);
|
|
203
|
+
console.error(`[Migration] soul: ${file}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 3. 迁移 .restore 文件到数据目录根(全局 marker)
|
|
209
|
+
const oldRestore = path.join(oldSessionsDir, '.restore');
|
|
210
|
+
if (fs.existsSync(oldRestore)) {
|
|
211
|
+
const newRestore = path.join(dataDir, '.restore');
|
|
212
|
+
if (!fs.existsSync(newRestore)) {
|
|
213
|
+
fs.copyFileSync(oldRestore, newRestore);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 标记迁移已完成 */
|
|
219
|
+
function markMigrated(markerPath: string): void {
|
|
220
|
+
const marker = {
|
|
221
|
+
version: 1,
|
|
222
|
+
migratedAt: new Date().toISOString(),
|
|
223
|
+
note: 'Workspace migration completed. Old sessions/ and soul/ directories preserved as backup.',
|
|
224
|
+
};
|
|
225
|
+
try {
|
|
226
|
+
fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2));
|
|
227
|
+
} catch {
|
|
228
|
+
console.error('[Migration] Failed to write migration marker');
|
|
229
|
+
}
|
|
230
|
+
}
|
package/modules/utils/paths.ts
CHANGED
|
@@ -201,7 +201,7 @@ export function getSoulDir(botKey: string): string {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
export function getRestoreMarkerPath(): string {
|
|
204
|
-
return path.join(
|
|
204
|
+
return path.join(getDataDir(), '.restore');
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
export function getTemplatePath(relativePath: string): string {
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// ================================================================
|
|
2
|
+
// WorkspaceManager — 工作空间管理
|
|
3
|
+
// ================================================================
|
|
4
|
+
// 职责:
|
|
5
|
+
// 1. 根据模式(sandbox/global)解析每个 Bot 的工作空间路径
|
|
6
|
+
// 2. 确保工作空间目录存在(含 soul/ 子目录)
|
|
7
|
+
// 3. 路径边界检查(沙盒模式下防止路径穿越)
|
|
8
|
+
// ================================================================
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { getDataDir } from './paths';
|
|
13
|
+
|
|
14
|
+
// ================================================================
|
|
15
|
+
// 类型定义
|
|
16
|
+
// ================================================================
|
|
17
|
+
|
|
18
|
+
export type WorkspaceMode = 'sandbox' | 'global';
|
|
19
|
+
|
|
20
|
+
export interface WorkspaceConfig {
|
|
21
|
+
mode: WorkspaceMode;
|
|
22
|
+
globalPath: string | null;
|
|
23
|
+
botOverrides: Record<string, string>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 从 config.json 中提取 workspace 配置。
|
|
28
|
+
* 老用户无 workspace 配置时,默认 sandbox 模式。
|
|
29
|
+
*/
|
|
30
|
+
export function parseWorkspaceConfig(raw: any): WorkspaceConfig {
|
|
31
|
+
const ws = raw?.workspace || {};
|
|
32
|
+
const mode: WorkspaceMode = ws.mode === 'global' ? 'global' : 'sandbox';
|
|
33
|
+
const globalPath: string | null = ws.globalPath || null;
|
|
34
|
+
const botOverrides: Record<string, string> = {};
|
|
35
|
+
|
|
36
|
+
if (ws.botOverrides && typeof ws.botOverrides === 'object') {
|
|
37
|
+
for (const [k, v] of Object.entries(ws.botOverrides)) {
|
|
38
|
+
if (typeof v === 'string') botOverrides[k] = v;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { mode, globalPath, botOverrides };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ================================================================
|
|
46
|
+
// WorkspaceManager
|
|
47
|
+
// ================================================================
|
|
48
|
+
|
|
49
|
+
export class WorkspaceManager {
|
|
50
|
+
private config: WorkspaceConfig;
|
|
51
|
+
private workspacesDir: string;
|
|
52
|
+
|
|
53
|
+
constructor(config: WorkspaceConfig) {
|
|
54
|
+
this.config = config;
|
|
55
|
+
this.workspacesDir = path.join(getDataDir(), 'workspaces');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 解析 Bot 的工作空间路径。
|
|
60
|
+
*
|
|
61
|
+
* 规则:
|
|
62
|
+
* - 沙盒模式:~/.imtoagent/workspaces/<UUID>/(UUID 保证唯一,可被 botOverrides 覆盖)
|
|
63
|
+
* - 全局模式:直接使用 <globalPath>/(所有 Bot 共享同一入口)
|
|
64
|
+
*/
|
|
65
|
+
getWorkspacePath(botKey: string): string {
|
|
66
|
+
// 优先级:botOverrides > 模式默认路径
|
|
67
|
+
const override = this.config.botOverrides[botKey];
|
|
68
|
+
if (override) return path.resolve(override);
|
|
69
|
+
|
|
70
|
+
// 全局模式:所有 Bot 共享 globalPath,不加 botKey
|
|
71
|
+
if (this.config.mode === 'global' && this.config.globalPath) {
|
|
72
|
+
return path.resolve(this.config.globalPath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 沙盒模式:每个 Bot 独立目录(UUID 保证唯一性,无需日期后缀)
|
|
76
|
+
const botId = this._getBotId(botKey);
|
|
77
|
+
return path.resolve(this.workspacesDir, botId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 生成或获取 Bot 的 UUID。
|
|
82
|
+
* 首次调用时创建 UUID 并持久化到数据目录。
|
|
83
|
+
*/
|
|
84
|
+
private _getBotId(botKey: string): string {
|
|
85
|
+
const botIdsFile = path.join(getDataDir(), 'bot-ids.json');
|
|
86
|
+
let botIds: Record<string, string> = {};
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
if (fs.existsSync(botIdsFile)) {
|
|
90
|
+
botIds = JSON.parse(fs.readFileSync(botIdsFile, 'utf8'));
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// 文件损坏或不可读,重新开始
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!botIds[botKey]) {
|
|
97
|
+
// 生成 UUID v4
|
|
98
|
+
botIds[botKey] = crypto.randomUUID();
|
|
99
|
+
try {
|
|
100
|
+
fs.writeFileSync(botIdsFile, JSON.stringify(botIds, null, 2));
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error(`[Workspace] Failed to persist bot ID for ${botKey}: ${e}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return botIds[botKey];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 确保工作空间目录存在,并初始化 soul/ 子目录。
|
|
111
|
+
*
|
|
112
|
+
* 沙盒模式:创建 <workspace>/soul/
|
|
113
|
+
* 全局模式:创建 <globalPath>/.imtoagent/soul/<botId>/(按 Bot 隔离)
|
|
114
|
+
*/
|
|
115
|
+
ensureWorkspace(botKey: string): void {
|
|
116
|
+
const wsPath = this.getWorkspacePath(botKey);
|
|
117
|
+
const soulPath = this.getSoulPath(botKey);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
if (!fs.existsSync(wsPath)) {
|
|
121
|
+
fs.mkdirSync(wsPath, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
if (!fs.existsSync(soulPath)) {
|
|
124
|
+
fs.mkdirSync(soulPath, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
} catch (e: any) {
|
|
127
|
+
console.error(`[Workspace] Failed to ensure workspace for ${botKey}: ${e.message}`);
|
|
128
|
+
throw e;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* 获取 soul 目录路径。
|
|
134
|
+
* 沙盒模式:<workspacePath>/soul/
|
|
135
|
+
* 全局模式:<globalPath>/.imtoagent/soul/<botId>/
|
|
136
|
+
*/
|
|
137
|
+
getSoulPath(botKey: string): string {
|
|
138
|
+
if (this.config.mode === 'global' && this.config.globalPath) {
|
|
139
|
+
return path.resolve(this.config.globalPath, '.imtoagent', 'soul', botKey);
|
|
140
|
+
}
|
|
141
|
+
// 沙盒模式
|
|
142
|
+
return path.join(this.getWorkspacePath(botKey), 'soul');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 检查路径是否允许该 Bot 访问。
|
|
147
|
+
*
|
|
148
|
+
* 沙盒模式:路径必须在 Bot 的工作空间范围内(或子目录)。
|
|
149
|
+
* 全局模式:不做限制,允许访问任意路径(信任用户配置的全局目录)。
|
|
150
|
+
*
|
|
151
|
+
* 返回 true 表示允许,false 表示拒绝。
|
|
152
|
+
*/
|
|
153
|
+
isPathAllowed(botKey: string, targetPath: string): boolean {
|
|
154
|
+
// 全局模式:不做边界限制
|
|
155
|
+
if (this.config.mode === 'global') {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 沙盒模式:路径必须在工作空间内
|
|
160
|
+
const resolved = path.resolve(targetPath);
|
|
161
|
+
const wsPath = this.getWorkspacePath(botKey);
|
|
162
|
+
const resolvedWs = path.resolve(wsPath);
|
|
163
|
+
|
|
164
|
+
if (resolved === resolvedWs || resolved.startsWith(resolvedWs + path.sep)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 获取工作空间模式。
|
|
173
|
+
*/
|
|
174
|
+
getMode(): WorkspaceMode {
|
|
175
|
+
return this.config.mode;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 规范化路径(解析 '..'、'.' 等),用于 /dir 命令。
|
|
180
|
+
* 如果规范化后的路径超出边界,返回 null。
|
|
181
|
+
*/
|
|
182
|
+
resolveAndValidatePath(botKey: string, inputPath: string, currentCwd: string): string | null {
|
|
183
|
+
// 先相对于当前 cwd 解析
|
|
184
|
+
const resolved = path.resolve(currentCwd, inputPath);
|
|
185
|
+
if (this.isPathAllowed(botKey, resolved)) {
|
|
186
|
+
return resolved;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 获取工作空间配置摘要(用于状态展示)。
|
|
193
|
+
*/
|
|
194
|
+
getConfigSummary(): string {
|
|
195
|
+
if (this.config.mode === 'sandbox') {
|
|
196
|
+
return `sandbox (default: ${this.workspacesDir}/<UUID>/)`;
|
|
197
|
+
}
|
|
198
|
+
return `global (${path.resolve(this.config.globalPath || '.')})`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ================================================================
|
|
203
|
+
// 便捷函数:从原始配置创建 WorkspaceManager
|
|
204
|
+
// ================================================================
|
|
205
|
+
|
|
206
|
+
export function createWorkspaceManager(rawConfig: any): WorkspaceManager {
|
|
207
|
+
const config = parseWorkspaceConfig(rawConfig);
|
|
208
|
+
return new WorkspaceManager(config);
|
|
209
|
+
}
|