lantern-harness 0.1.1 → 0.1.3
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/CHANGELOG.md +74 -0
- package/README.md +10 -10
- package/builtin/session/lh-spaces.mjs +105 -0
- package/cli/acp-server.mjs +15 -1
- package/cli/data-root.mjs +80 -3
- package/cli/lantern.mjs +27 -10
- package/cli/server.mjs +4 -0
- package/cli/spaces.mjs +278 -38
- package/config/app.defaults.json +4 -2
- package/dist/500.html +9 -5
- package/dist/_astro/Base.ByB0D4ZA.css +1 -0
- package/dist/_astro/{Base.astro_astro_type_script_index_0_lang.lxEwQ1R4.js → Base.astro_astro_type_script_index_0_lang.Cg_8YLVA.js} +1 -1
- package/dist/_astro/CaseGenerateApp.BlZyjWEr.js +9 -0
- package/dist/_astro/CasesLibrary.C76La3Xj.js +1 -0
- package/dist/_astro/ChatApp.gfsPLvan.js +269 -0
- package/dist/_astro/ColumnResize.Bs6GL3DJ.js +1 -0
- package/dist/_astro/DatePicker.BdwFRioL.js +1 -0
- package/dist/_astro/{DocsFileBrowser.DkE26UTR.js → DocsFileBrowser.BxAyrkkw.js} +1 -1
- package/dist/_astro/KnowledgeLibrary.Cv488Hot.js +1 -0
- package/dist/_astro/NavLibraries.ALZ9e12z.js +1 -0
- package/dist/_astro/PluginsPage.B2uxCiHL.js +1 -0
- package/dist/_astro/{PluginsPage.BNzpPg2H.js → PluginsPage.DEkyOfQE.js} +2 -2
- package/dist/_astro/PromptsLibrary.BUC1OiCH.js +1 -0
- package/dist/_astro/RequirementsLibrary.D3olQI63.js +1 -0
- package/dist/_astro/{SearchableSelect.DMS2XmXc.js → SearchableSelect.DP74JRWW.js} +1 -1
- package/dist/_astro/SelectDropdown.D2i7-lQC.js +1 -0
- package/dist/_astro/{SettingsDialog.DD-XOFEX.js → SettingsDialog.CrMDjSLJ.js} +1 -1
- package/dist/_astro/SlideOverDrawer.C7UVeCX1.js +1 -0
- package/dist/_astro/SpaceSwitcher.Wf8EPYUx.js +7 -0
- package/dist/_astro/TasksBoard.2j7rGT9O.js +1 -0
- package/dist/_astro/TemplatesLibrary.J0x7yIcY.js +1 -0
- package/dist/_astro/TodosBoard.BugeB7_7.js +1 -0
- package/dist/_astro/app-config.vqDkEMS3.js +1 -0
- package/dist/_astro/doc-roots.DAJP9Svr.js +1 -0
- package/dist/_astro/i18n.9qZsrs3u.js +7 -0
- package/dist/_astro/{open-docs-in-session.D3JCwMWB.js → open-docs-in-session.fLwnA5qh.js} +1 -1
- package/dist/_astro/plugins.DSUPSizB.js +1 -0
- package/dist/_astro/project-docs.Cd2gKPKh.js +1 -0
- package/dist/_astro/{settings.BoQ0e0m1.js → settings.CkbipdPI.js} +2 -2
- package/dist/_astro/{useListAnim.CnbC_9MC.js → useListAnim.Y3N2budK.js} +1 -1
- package/dist/cases/generate/index.html +10 -6
- package/dist/cases/library/index.html +10 -6
- package/dist/docs/changelog/index.html +80 -6
- package/dist/docs/install/index.html +26 -12
- package/dist/docs/manual/index.html +74 -11
- package/dist/index.html +10 -6
- package/dist/knowledge/index.html +10 -6
- package/dist/knowledge//345/212/237/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/347/274/226/345/206/231/350/247/204/350/214/203/index.html +9 -5
- package/dist/plugins/index.html +10 -6
- package/dist/prompts/index.html +10 -6
- package/dist/requirements/index.html +10 -6
- package/dist/tasks/index.html +253 -0
- package/dist/templates/index.html +10 -6
- package/dist/templates//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//345/212/237/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212//346/200/247/350/203/275/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213//346/200/247/350/203/275/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212//346/216/245/345/217/243/346/265/213/350/257/225/346/212/245/345/221/212/346/250/241/346/235/277/index.html +9 -5
- package/dist/templates//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213//350/207/252/345/212/250/345/214/226/346/265/213/350/257/225/347/224/250/344/276/213/346/250/241/346/235/277/index.html +9 -5
- package/dist/todos/index.html +253 -0
- package/docs//345/205/250/345/261/200/346/217/222/344/273/266.md +2 -2
- package/docs//345/256/211/350/243/205/346/211/213/345/206/214.md +14 -12
- package/docs//346/223/215/344/275/234/346/211/213/345/206/214.md +73 -23
- package/lh-plugins/lh-project-docs.mjs +2 -0
- package/lh-plugins/lh-tasks.mjs +223 -0
- package/lh-plugins/lh-todos.mjs +297 -0
- package/lh-plugins/storage-domain.mjs +1 -0
- package/package.json +1 -1
- package/dist/_astro/Base.B9f_iFAZ.css +0 -1
- package/dist/_astro/CaseGenerateApp.Br4uKjun.js +0 -9
- package/dist/_astro/CasesLibrary.CpPoVQ4P.js +0 -1
- package/dist/_astro/ChatApp.CYSPlSNP.js +0 -258
- package/dist/_astro/KnowledgeLibrary.DZinlAWn.js +0 -1
- package/dist/_astro/NavLibraries.EayBsg5g.js +0 -1
- package/dist/_astro/PluginsPage.CY61glLU.js +0 -1
- package/dist/_astro/PromptsLibrary.DfrrCjs7.js +0 -1
- package/dist/_astro/RequirementsLibrary.Dt0Gj6Nr.js +0 -1
- package/dist/_astro/SelectDropdown.Dc7QaPbH.js +0 -1
- package/dist/_astro/SpaceSwitcher.Yy2w8MDF.js +0 -7
- package/dist/_astro/TemplatesLibrary.uK-SjK1z.js +0 -1
- package/dist/_astro/app-config.CpjKWnzM.js +0 -1
- package/dist/_astro/i18n.BVvhz-vw.js +0 -7
- package/dist/_astro/plugins.CrpY8UI9.js +0 -1
package/cli/spaces.mjs
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* 文件位置:`~/.config/lantern-harness/spaces.json`(与 CLI 的 config.json 同目录)
|
|
5
5
|
*
|
|
6
|
+
* 一份文件里按「池根」分区:每个空间条目记着自己属于哪个数据目录(`root`),
|
|
7
|
+
* 激活项也按池根分别记录(`activeByRoot`)。`lantern init` 换了数据空间之后,
|
|
8
|
+
* 列表只看新池根下的空间,不会把上一个数据目录里的空间一并列出来。
|
|
9
|
+
* 旧版全局 `activeId` / 无 `root` 的条目在读取时按路径推断归属,向后兼容。
|
|
10
|
+
*
|
|
6
11
|
* 「公共空间」是内建虚拟项,不入库:用户数据目录下直接是 `公共空间/`,源码仓库则在 `空间池/公共空间/`。
|
|
7
12
|
* 新建空间会自动归进同一层(与公共空间同级)。界面路径隐去「空间池」。
|
|
8
13
|
* 与自定义空间同构,因此可以整体拷贝 / 备份。
|
|
@@ -81,6 +86,46 @@ export function getSpacesPath() {
|
|
|
81
86
|
return path.join(getUserConfigDir(), 'spaces.json');
|
|
82
87
|
}
|
|
83
88
|
|
|
89
|
+
/** 当前池根:由 CLI / 服务端在知道数据目录后告知,供无 baseRoot 参数的调用(激活项)使用 */
|
|
90
|
+
let registryRoot = '';
|
|
91
|
+
|
|
92
|
+
/** @param {string} [root] */
|
|
93
|
+
export function setRegistryRoot(root) {
|
|
94
|
+
if (root) registryRoot = path.resolve(root);
|
|
95
|
+
return registryRoot;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 条目所属池根:优先读登记值,其次按路径推断(`<根>/空间池/<名称>` 或 `<根>/<名称>`) */
|
|
99
|
+
function inferEntryRoot(p) {
|
|
100
|
+
const abs = path.resolve(String(p || ''));
|
|
101
|
+
const marker = `${path.sep}${SPACE_POOL_DIR}${path.sep}`;
|
|
102
|
+
const idx = abs.lastIndexOf(marker);
|
|
103
|
+
return idx > 0 ? abs.slice(0, idx) : '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function sameRoot(a, b) {
|
|
107
|
+
if (!a || !b) return false;
|
|
108
|
+
return path.resolve(a) === path.resolve(b);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** abs 是否落在 dir 内(含 dir 本身) */
|
|
112
|
+
function isInsideDir(abs, dir) {
|
|
113
|
+
if (!abs || !dir) return false;
|
|
114
|
+
const rel = path.relative(path.resolve(dir), path.resolve(abs));
|
|
115
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function rootKey(root) {
|
|
119
|
+
const abs = root || registryRoot;
|
|
120
|
+
return abs ? path.resolve(abs) : '';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function activeIdForRoot(reg, root) {
|
|
124
|
+
const key = rootKey(root);
|
|
125
|
+
const id = key ? reg.activeByRoot?.[key] : '';
|
|
126
|
+
return String(id || PUBLIC_SPACE_ID);
|
|
127
|
+
}
|
|
128
|
+
|
|
84
129
|
/**
|
|
85
130
|
* 新建空间的默认父目录:与公共空间同级(用户数据目录 = 选中的文件夹;源码仓库 = 空间池)。
|
|
86
131
|
* @param {string} [baseRoot] 池根;缺省用 cwd
|
|
@@ -89,6 +134,13 @@ export function defaultManagedParent(baseRoot) {
|
|
|
89
134
|
return poolPath(baseRoot || process.cwd());
|
|
90
135
|
}
|
|
91
136
|
|
|
137
|
+
/** 登记条目时推导它属于哪个池根:优先用调用方给的池根,其次从父目录反推 */
|
|
138
|
+
function resolveEntryRoot(baseRoot, parent) {
|
|
139
|
+
return path.resolve(
|
|
140
|
+
baseRoot || inferEntryRoot(parent) || parent || registryRoot || process.cwd(),
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
92
144
|
/**
|
|
93
145
|
* 注册表里的绝对路径失效时(整个仓库/数据目录被改名或搬移)按清单身份在新池根下重定位。
|
|
94
146
|
* 只认清单 id / 名称都对得上的目录,避免把同名但无关的文件夹当成原空间。
|
|
@@ -120,16 +172,36 @@ function rebaseMissingSpacePath(abs, root, pool, entry) {
|
|
|
120
172
|
|
|
121
173
|
/**
|
|
122
174
|
* 按当前池布局改写注册表路径:用户目录把 `空间池/X` 抬到 `X`;源码仓库把误摊的 `X` 收回 `空间池/X`。
|
|
175
|
+
*
|
|
176
|
+
* 只改写「属于本池根」的条目。其它池根的条目原样保留 —— 除非它登记的池根已经不存在
|
|
177
|
+
* (整个数据目录被改名 / 搬移),这时按清单身份把它接到当前池根下,继续可用。
|
|
123
178
|
* @param {string} [baseRoot]
|
|
124
179
|
*/
|
|
125
180
|
export function relocateSpaceRegistryPaths(baseRoot) {
|
|
126
|
-
const root = path.resolve(baseRoot || process.cwd());
|
|
181
|
+
const root = path.resolve(baseRoot || registryRoot || process.cwd());
|
|
127
182
|
const pool = poolPath(root);
|
|
128
183
|
const nestedPrefix = path.join(root, SPACE_POOL_DIR);
|
|
129
|
-
const reg = loadSpacesRegistry();
|
|
184
|
+
const reg = loadSpacesRegistry(root);
|
|
130
185
|
let dirty = false;
|
|
131
186
|
for (const s of reg.spaces) {
|
|
132
187
|
const abs = path.resolve(s.path);
|
|
188
|
+
const own = s.root ? path.resolve(s.root) : root;
|
|
189
|
+
if (!sameRoot(own, root)) {
|
|
190
|
+
// 别的数据空间:池根还在就不要动,否则换空间时会串台
|
|
191
|
+
if (isDir(own)) continue;
|
|
192
|
+
// 池根整个被改名 / 搬走:按清单身份接到当前池根
|
|
193
|
+
let next = isDir(abs) ? null : rebaseMissingSpacePath(abs, root, pool, s);
|
|
194
|
+
if (!next && s.external) {
|
|
195
|
+
const byName = path.resolve(pool, s.name);
|
|
196
|
+
if (byName !== abs && isDir(byName)) next = byName;
|
|
197
|
+
}
|
|
198
|
+
if (next) {
|
|
199
|
+
s.path = next;
|
|
200
|
+
s.root = root;
|
|
201
|
+
dirty = true;
|
|
202
|
+
}
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
133
205
|
let next = null;
|
|
134
206
|
if (!isDir(abs)) {
|
|
135
207
|
// 旧根被改名 / 搬走:先按清单身份找回来
|
|
@@ -176,34 +248,63 @@ function newSpaceId() {
|
|
|
176
248
|
|
|
177
249
|
function emptyRegistry() {
|
|
178
250
|
// managedParent 不再落进注册表:它由池根推导,存下来只会在换机器后变成死路径
|
|
179
|
-
return { activeId: PUBLIC_SPACE_ID, spaces: [] };
|
|
251
|
+
return { activeId: PUBLIC_SPACE_ID, activeByRoot: {}, spaces: [] };
|
|
180
252
|
}
|
|
181
253
|
|
|
182
254
|
/**
|
|
183
255
|
* 读取注册表;任何损坏都退回空注册表(宁可重置元数据,也不能让服务起不来)。
|
|
184
256
|
*
|
|
185
257
|
* 不再读取/推断 `managedParent`:空间位置一律由池根推导(见 `defaultManagedParent`)。
|
|
186
|
-
*
|
|
258
|
+
* 条目缺 `root` 时按路径推断;推断不出来的(老版本的外部纳入)归到当前池根,保持旧行为。
|
|
259
|
+
* @param {string} [root] 当前池根;缺省用 `setRegistryRoot` 记下的值
|
|
260
|
+
* @returns {{ activeId: string, activeByRoot: Record<string, string>, spaces: Array<{id:string,name:string,path:string,createdAt:number,root:string}> }}
|
|
187
261
|
*/
|
|
188
|
-
export function loadSpacesRegistry() {
|
|
262
|
+
export function loadSpacesRegistry(root) {
|
|
189
263
|
const fp = getSpacesPath();
|
|
190
264
|
if (!fs.existsSync(fp)) return emptyRegistry();
|
|
265
|
+
const cur = rootKey(root);
|
|
191
266
|
try {
|
|
192
267
|
const raw = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
193
268
|
if (!raw || typeof raw !== 'object') return emptyRegistry();
|
|
194
269
|
const spaces = Array.isArray(raw.spaces)
|
|
195
270
|
? raw.spaces
|
|
196
|
-
.map((it) =>
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
271
|
+
.map((it) => {
|
|
272
|
+
const abs = path.resolve(String(it?.path || ''));
|
|
273
|
+
// 归属推断:登记值 → 路径里的「空间池」段 → 当前池根内(老版本的外部纳入)
|
|
274
|
+
// 都不成立时留空:不属于任何池根,不会被任何数据目录列出来
|
|
275
|
+
const own =
|
|
276
|
+
String(it?.root || '').trim() ||
|
|
277
|
+
inferEntryRoot(abs) ||
|
|
278
|
+
(cur && isInsideDir(abs, cur) ? cur : '');
|
|
279
|
+
return {
|
|
280
|
+
id: String(it?.id || '').trim(),
|
|
281
|
+
name: String(it?.name || '').trim(),
|
|
282
|
+
path: abs,
|
|
283
|
+
createdAt: Number(it?.createdAt) || 0,
|
|
284
|
+
// 用户手工确认纳入的文件夹:不写 space.json,保持目录原样
|
|
285
|
+
...(it?.external === true ? { external: true } : {}),
|
|
286
|
+
...(own ? { root: path.resolve(own) } : {}),
|
|
287
|
+
};
|
|
288
|
+
})
|
|
204
289
|
.filter((it) => it.id && it.name && it.path && it.path !== path.sep)
|
|
205
290
|
: [];
|
|
206
|
-
|
|
291
|
+
const activeByRoot = {};
|
|
292
|
+
const rawActive = raw.activeByRoot;
|
|
293
|
+
if (rawActive && typeof rawActive === 'object' && !Array.isArray(rawActive)) {
|
|
294
|
+
for (const [k, v] of Object.entries(rawActive)) {
|
|
295
|
+
const key = String(k || '').trim();
|
|
296
|
+
const id = String(v || '').trim();
|
|
297
|
+
if (!key || !id) continue;
|
|
298
|
+
activeByRoot[path.resolve(key)] = id;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// 旧版只有一个全局 activeId:归到它所属空间的那个池根,别让它跨数据目录生效
|
|
302
|
+
const legacyActive = String(raw.activeId || PUBLIC_SPACE_ID);
|
|
303
|
+
if (legacyActive !== PUBLIC_SPACE_ID) {
|
|
304
|
+
const owner = spaces.find((s) => s.id === legacyActive && s.root);
|
|
305
|
+
if (owner?.root && !activeByRoot[owner.root]) activeByRoot[owner.root] = legacyActive;
|
|
306
|
+
}
|
|
307
|
+
return { activeId: legacyActive, activeByRoot, spaces };
|
|
207
308
|
} catch {
|
|
208
309
|
return emptyRegistry();
|
|
209
310
|
}
|
|
@@ -212,9 +313,17 @@ export function loadSpacesRegistry() {
|
|
|
212
313
|
export function saveSpacesRegistry(reg) {
|
|
213
314
|
const dir = getUserConfigDir();
|
|
214
315
|
fs.mkdirSync(dir, { recursive: true });
|
|
316
|
+
const spaces = Array.isArray(reg.spaces) ? reg.spaces : [];
|
|
317
|
+
const activeByRoot = { ...(reg.activeByRoot || {}) };
|
|
318
|
+
for (const [k, v] of Object.entries(activeByRoot)) {
|
|
319
|
+
if (!k || !v) delete activeByRoot[k];
|
|
320
|
+
}
|
|
321
|
+
// activeId 只作旧版本兼容:写当前池根的激活项,读代码以 activeByRoot 为准
|
|
322
|
+
const cur = rootKey();
|
|
215
323
|
const next = {
|
|
216
|
-
activeId: String(reg.activeId || PUBLIC_SPACE_ID),
|
|
217
|
-
|
|
324
|
+
activeId: String((cur && activeByRoot[cur]) || reg.activeId || PUBLIC_SPACE_ID),
|
|
325
|
+
activeByRoot,
|
|
326
|
+
spaces,
|
|
218
327
|
};
|
|
219
328
|
fs.writeFileSync(getSpacesPath(), `${JSON.stringify(next, null, 2)}\n`, 'utf8');
|
|
220
329
|
return next;
|
|
@@ -376,8 +485,9 @@ function hasKnownContentDir(abs) {
|
|
|
376
485
|
* @param {{ managedParent?: string, baseRoot?: string, adopt?: string[] }} [opts]
|
|
377
486
|
*/
|
|
378
487
|
export function discoverSpaces(opts = {}) {
|
|
379
|
-
const reg = loadSpacesRegistry();
|
|
488
|
+
const reg = loadSpacesRegistry(opts.baseRoot);
|
|
380
489
|
const parent = path.resolve(opts.managedParent || defaultManagedParent(opts.baseRoot));
|
|
490
|
+
const ownerRoot = resolveEntryRoot(opts.baseRoot, parent);
|
|
381
491
|
const added = [];
|
|
382
492
|
const backfilled = [];
|
|
383
493
|
const skipped = [];
|
|
@@ -428,6 +538,7 @@ export function discoverSpaces(opts = {}) {
|
|
|
428
538
|
name: clean.name,
|
|
429
539
|
path: abs,
|
|
430
540
|
createdAt: Date.now(),
|
|
541
|
+
root: ownerRoot,
|
|
431
542
|
external: true,
|
|
432
543
|
};
|
|
433
544
|
reg.spaces.push(entry);
|
|
@@ -500,6 +611,7 @@ export function discoverSpaces(opts = {}) {
|
|
|
500
611
|
name: clean.name,
|
|
501
612
|
path: abs,
|
|
502
613
|
createdAt: (read.ok ? read.manifest.createdAt : 0) || Date.now(),
|
|
614
|
+
root: ownerRoot,
|
|
503
615
|
};
|
|
504
616
|
reg.spaces.push(entry);
|
|
505
617
|
usedIds.add(id);
|
|
@@ -524,10 +636,14 @@ export function discoverSpaces(opts = {}) {
|
|
|
524
636
|
* @param {string} baseRoot 池根
|
|
525
637
|
*/
|
|
526
638
|
export function listSpaces(baseRoot) {
|
|
527
|
-
|
|
528
|
-
|
|
639
|
+
const root = path.resolve(baseRoot);
|
|
640
|
+
setRegistryRoot(root);
|
|
641
|
+
relocateSpaceRegistryPaths(root);
|
|
642
|
+
const reg = loadSpacesRegistry(root);
|
|
643
|
+
// 只列本池根下的空间:换数据目录后不会串到上一个数据目录
|
|
644
|
+
const mine = reg.spaces.filter((s) => sameRoot(s.root, root));
|
|
529
645
|
// 公共空间也是选中文件夹里的一个目录,与自定义空间同构
|
|
530
|
-
const publicRoot = path.resolve(publicSpacePath(
|
|
646
|
+
const publicRoot = path.resolve(publicSpacePath(root));
|
|
531
647
|
const all = [
|
|
532
648
|
{
|
|
533
649
|
id: PUBLIC_SPACE_ID,
|
|
@@ -536,16 +652,17 @@ export function listSpaces(baseRoot) {
|
|
|
536
652
|
createdAt: 0,
|
|
537
653
|
isPublic: true,
|
|
538
654
|
},
|
|
539
|
-
...
|
|
655
|
+
...mine.map((s) => ({ ...s, isPublic: false })),
|
|
540
656
|
];
|
|
657
|
+
const activeId = activeIdForRoot(reg, root);
|
|
541
658
|
return {
|
|
542
|
-
baseRoot:
|
|
543
|
-
managedParent: defaultManagedParent(
|
|
544
|
-
activeId
|
|
659
|
+
baseRoot: root,
|
|
660
|
+
managedParent: defaultManagedParent(root),
|
|
661
|
+
activeId,
|
|
545
662
|
spaces: all.map((s) => ({
|
|
546
663
|
...s,
|
|
547
664
|
exists: isDir(s.path),
|
|
548
|
-
isActive: s.id ===
|
|
665
|
+
isActive: s.id === activeId,
|
|
549
666
|
})),
|
|
550
667
|
};
|
|
551
668
|
}
|
|
@@ -573,10 +690,11 @@ export function createSpace({ name, managedParent, baseRoot }) {
|
|
|
573
690
|
const clean = sanitizeSpaceName(name);
|
|
574
691
|
if (!clean.ok) return { ok: false, error: clean.error };
|
|
575
692
|
|
|
693
|
+
const parent = path.resolve(managedParent || defaultManagedParent(baseRoot));
|
|
694
|
+
setRegistryRoot(baseRoot || inferEntryRoot(parent));
|
|
576
695
|
const reg = loadSpacesRegistry();
|
|
577
696
|
if (nameTaken(reg, clean.name)) return { ok: false, error: `已存在同名空间「${clean.name}」` };
|
|
578
697
|
|
|
579
|
-
const parent = path.resolve(managedParent || defaultManagedParent(baseRoot));
|
|
580
698
|
const dir = path.join(parent, clean.name);
|
|
581
699
|
|
|
582
700
|
// 目录可能已存在(例如用户手工建过):不能贸然复用,否则会把无关内容当成数据空间
|
|
@@ -588,7 +706,13 @@ export function createSpace({ name, managedParent, baseRoot }) {
|
|
|
588
706
|
return { ok: false, error: `创建目录失败:${e instanceof Error ? e.message : String(e)}` };
|
|
589
707
|
}
|
|
590
708
|
|
|
591
|
-
const entry = {
|
|
709
|
+
const entry = {
|
|
710
|
+
id: newSpaceId(),
|
|
711
|
+
name: clean.name,
|
|
712
|
+
path: dir,
|
|
713
|
+
createdAt: Date.now(),
|
|
714
|
+
root: resolveEntryRoot(baseRoot, parent),
|
|
715
|
+
};
|
|
592
716
|
try {
|
|
593
717
|
writeSpaceManifest(dir, entry);
|
|
594
718
|
} catch (e) {
|
|
@@ -667,7 +791,17 @@ export function copySpace(id, nextName, baseRoot) {
|
|
|
667
791
|
return { ok: false, error: `复制失败:${e instanceof Error ? e.message : String(e)}` };
|
|
668
792
|
}
|
|
669
793
|
|
|
670
|
-
|
|
794
|
+
// 复制品里的工作区注册表还指着源空间的绝对路径,必须改指新目录,
|
|
795
|
+
// 否则新空间打开时「工作区」停在被复制的那个空间上。
|
|
796
|
+
retargetCopiedWorkspaces(dir, resolved.space.path);
|
|
797
|
+
|
|
798
|
+
const entry = {
|
|
799
|
+
id: newSpaceId(),
|
|
800
|
+
name: clean.name,
|
|
801
|
+
path: dir,
|
|
802
|
+
createdAt: Date.now(),
|
|
803
|
+
root: resolveEntryRoot(baseRoot, parent),
|
|
804
|
+
};
|
|
671
805
|
try {
|
|
672
806
|
// 拷贝继承的是源空间的清单(含源 id),必须改写成新身份
|
|
673
807
|
writeSpaceManifest(dir, entry);
|
|
@@ -679,6 +813,69 @@ export function copySpace(id, nextName, baseRoot) {
|
|
|
679
813
|
return { ok: true, space: entry };
|
|
680
814
|
}
|
|
681
815
|
|
|
816
|
+
/**
|
|
817
|
+
* 把复制品里的工作区注册表改指新目录。
|
|
818
|
+
*
|
|
819
|
+
* 源空间的 `workspaces.json` 记的是源目录的绝对路径(`active` 与 `ws_project` 条目);
|
|
820
|
+
* 直接拷过来后,源目录仍真实存在,`ensureWorkspaceRegistry` 的「父目录 / 不存在」清理都不会命中,
|
|
821
|
+
* 于是新空间的工作区停在源空间上。这里把指向源目录的条目改名到新目录,其余条目保持不动。
|
|
822
|
+
*/
|
|
823
|
+
function retargetCopiedWorkspaces(newDir, sourceDir) {
|
|
824
|
+
const fp = path.join(newDir, LOCAL_REL, 'workspaces.json');
|
|
825
|
+
const target = path.resolve(newDir);
|
|
826
|
+
const source = path.resolve(sourceDir);
|
|
827
|
+
const folderName = (abs) => path.basename(abs) || abs;
|
|
828
|
+
let raw;
|
|
829
|
+
try {
|
|
830
|
+
raw = JSON.parse(fs.readFileSync(fp, 'utf8'));
|
|
831
|
+
} catch {
|
|
832
|
+
// 源空间没有工作区注册表(或已损坏):不补,首次打开会按空间根自建
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (!raw || typeof raw !== 'object') return;
|
|
836
|
+
|
|
837
|
+
const items = (Array.isArray(raw.items) ? raw.items : []).map((it) => {
|
|
838
|
+
const p = String(it?.path ?? '');
|
|
839
|
+
if (!p || path.resolve(p) !== source) return it;
|
|
840
|
+
return {
|
|
841
|
+
...it,
|
|
842
|
+
id: 'ws_project',
|
|
843
|
+
path: target,
|
|
844
|
+
name: folderName(target),
|
|
845
|
+
addedAt: Number(it?.addedAt) || 0,
|
|
846
|
+
};
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
// 复制品必须有自己的根工作区(源注册表里没有源目录条目时补一条)
|
|
850
|
+
if (!items.some((it) => path.resolve(String(it?.path ?? '')) === target)) {
|
|
851
|
+
items.unshift({ id: 'ws_project', path: target, name: folderName(target), addedAt: 0 });
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const activeRaw = String(raw.active ?? '');
|
|
855
|
+
const active =
|
|
856
|
+
!activeRaw || path.resolve(activeRaw) === source ? target : path.resolve(activeRaw);
|
|
857
|
+
|
|
858
|
+
try {
|
|
859
|
+
fs.writeFileSync(
|
|
860
|
+
fp,
|
|
861
|
+
`${JSON.stringify(
|
|
862
|
+
{
|
|
863
|
+
active,
|
|
864
|
+
// 插件开发借用包根是临时态,不跟着复制
|
|
865
|
+
activeHarness: false,
|
|
866
|
+
items,
|
|
867
|
+
updatedAt: Date.now(),
|
|
868
|
+
},
|
|
869
|
+
null,
|
|
870
|
+
2,
|
|
871
|
+
)}\n`,
|
|
872
|
+
'utf8',
|
|
873
|
+
);
|
|
874
|
+
} catch {
|
|
875
|
+
// 写不进去不影响空间可用性,下次打开会按空间根自愈
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
682
879
|
/**
|
|
683
880
|
* 从列表移除空间。
|
|
684
881
|
* @param {{ id: string, purge?: boolean, baseRoot: string }} opts purge=true 时连磁盘数据一起删
|
|
@@ -691,14 +888,20 @@ export function removeSpace({ id, purge = false, baseRoot }) {
|
|
|
691
888
|
|
|
692
889
|
const reg = loadSpacesRegistry();
|
|
693
890
|
reg.spaces = reg.spaces.filter((s) => s.id !== space.id);
|
|
891
|
+
for (const [key, val] of Object.entries(reg.activeByRoot || {})) {
|
|
892
|
+
if (val === space.id) delete reg.activeByRoot[key];
|
|
893
|
+
}
|
|
694
894
|
if (reg.activeId === space.id) reg.activeId = PUBLIC_SPACE_ID;
|
|
695
895
|
saveSpacesRegistry(reg);
|
|
696
896
|
|
|
697
897
|
if (!purge) return { ok: true, purged: false, space };
|
|
698
898
|
|
|
699
899
|
// 用户手工纳入的外部文件夹:没有空间元数据就不是我们铺的目录,
|
|
700
|
-
//
|
|
701
|
-
|
|
900
|
+
// 只移出列表,绝不代删(那可能是他自己的资料文件夹)。
|
|
901
|
+
// 例外:目录里已经有 `.lantern` 元数据(会话 / 配置 / 记忆),说明确实被本应用
|
|
902
|
+
// 当成数据空间用过——用户点「删除数据」后还留着空壳只会让人以为没删掉。
|
|
903
|
+
const hasLanternMeta = fs.existsSync(spaceMetaDir(space.path));
|
|
904
|
+
if (space.external && !resolveManifestPath(space.path) && !hasLanternMeta) {
|
|
702
905
|
return {
|
|
703
906
|
ok: true,
|
|
704
907
|
purged: false,
|
|
@@ -744,16 +947,35 @@ export function assertLooksLikeSpace(dir, managedParent) {
|
|
|
744
947
|
return { ok: true };
|
|
745
948
|
}
|
|
746
949
|
|
|
747
|
-
/**
|
|
950
|
+
/** 设置当前空间(按池根分别持久化,重启后沿用) */
|
|
748
951
|
export function setActiveSpace(id) {
|
|
749
952
|
const reg = loadSpacesRegistry();
|
|
750
|
-
|
|
953
|
+
const wanted = String(id || PUBLIC_SPACE_ID);
|
|
954
|
+
const owner = reg.spaces.find((s) => s.id === wanted);
|
|
955
|
+
const key = rootKey() || owner?.root || '';
|
|
956
|
+
if (key) {
|
|
957
|
+
if (!reg.activeByRoot) reg.activeByRoot = {};
|
|
958
|
+
reg.activeByRoot[path.resolve(key)] = wanted;
|
|
959
|
+
}
|
|
960
|
+
reg.activeId = wanted;
|
|
751
961
|
saveSpacesRegistry(reg);
|
|
752
|
-
return
|
|
962
|
+
return wanted;
|
|
753
963
|
}
|
|
754
964
|
|
|
755
965
|
export function getActiveSpaceId() {
|
|
756
|
-
|
|
966
|
+
const reg = loadSpacesRegistry();
|
|
967
|
+
const key = rootKey();
|
|
968
|
+
if (!key) return String(reg.activeId || PUBLIC_SPACE_ID);
|
|
969
|
+
return String(reg.activeByRoot?.[key] || PUBLIC_SPACE_ID);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** 忘掉某个池根的全部登记(`lantern wipe` 删掉数据目录后调用,避免留下死路径) */
|
|
973
|
+
export function forgetRegistryRoot(root) {
|
|
974
|
+
const key = path.resolve(root);
|
|
975
|
+
const reg = loadSpacesRegistry(key);
|
|
976
|
+
reg.spaces = reg.spaces.filter((s) => !sameRoot(s.root, key));
|
|
977
|
+
if (reg.activeByRoot) delete reg.activeByRoot[key];
|
|
978
|
+
saveSpacesRegistry(reg);
|
|
757
979
|
}
|
|
758
980
|
|
|
759
981
|
// ── 文件系统工具 ──────────────────────────────────────────────
|
|
@@ -765,8 +987,21 @@ export function getActiveSpaceId() {
|
|
|
765
987
|
*/
|
|
766
988
|
const COPY_SKIP_DIRS = new Set(['node_modules', '.git', '.astro', 'dist']);
|
|
767
989
|
|
|
768
|
-
/**
|
|
769
|
-
|
|
990
|
+
/**
|
|
991
|
+
* 复制空间时额外跳过的「派生缓存」(相对空间根,posix 路径)。
|
|
992
|
+
*
|
|
993
|
+
* 这些都能从源数据重建,且复制时若正好被服务进程写着,拷过来只可能是半份 / 不一致的:
|
|
994
|
+
* 会话检索索引用 sqlite(可能带 WAL / shm),spill 只是工具结果的溢出缓存。
|
|
995
|
+
*/
|
|
996
|
+
const COPY_SKIP_LOCAL_FILES = new Set([
|
|
997
|
+
`${LOCAL_REL}/session-query.sqlite`,
|
|
998
|
+
`${LOCAL_REL}/session-query.sqlite-wal`,
|
|
999
|
+
`${LOCAL_REL}/session-query.sqlite-shm`,
|
|
1000
|
+
]);
|
|
1001
|
+
const COPY_SKIP_LOCAL_DIRS = new Set([`${LOCAL_REL}/spill`]);
|
|
1002
|
+
|
|
1003
|
+
/** 递归复制目录(跳过 .DS_Store、构建/版本控制目录与派生缓存,保留符号链接本身而非解引用) */
|
|
1004
|
+
export function copyDirSync(from, to, rel = '') {
|
|
770
1005
|
const src = path.resolve(from);
|
|
771
1006
|
const dest = path.resolve(to);
|
|
772
1007
|
|
|
@@ -779,14 +1014,17 @@ export function copyDirSync(from, to) {
|
|
|
779
1014
|
for (const ent of fs.readdirSync(src, { withFileTypes: true })) {
|
|
780
1015
|
if (ent.name === '.DS_Store') continue;
|
|
781
1016
|
if (ent.isDirectory() && COPY_SKIP_DIRS.has(ent.name)) continue;
|
|
1017
|
+
const nextRel = rel ? `${rel}/${ent.name}` : ent.name;
|
|
782
1018
|
const s = path.join(src, ent.name);
|
|
783
1019
|
const d = path.join(dest, ent.name);
|
|
784
1020
|
if (ent.isSymbolicLink()) {
|
|
785
1021
|
const link = fs.readlinkSync(s);
|
|
786
1022
|
fs.symlinkSync(link, d);
|
|
787
1023
|
} else if (ent.isDirectory()) {
|
|
788
|
-
|
|
1024
|
+
if (COPY_SKIP_LOCAL_DIRS.has(nextRel)) continue;
|
|
1025
|
+
copyDirSync(s, d, nextRel);
|
|
789
1026
|
} else {
|
|
1027
|
+
if (COPY_SKIP_LOCAL_FILES.has(nextRel)) continue;
|
|
790
1028
|
fs.copyFileSync(s, d);
|
|
791
1029
|
}
|
|
792
1030
|
}
|
|
@@ -925,10 +1163,11 @@ export async function importSpaceFromZip({ zipFile, name, managedParent, baseRoo
|
|
|
925
1163
|
const clean = sanitizeSpaceName(name ?? manifest?.name ?? fallbackName);
|
|
926
1164
|
if (!clean.ok) return { ok: false, error: clean.error };
|
|
927
1165
|
|
|
1166
|
+
const parent = path.resolve(managedParent || defaultManagedParent(baseRoot));
|
|
1167
|
+
setRegistryRoot(baseRoot || inferEntryRoot(parent));
|
|
928
1168
|
const reg = loadSpacesRegistry();
|
|
929
1169
|
if (nameTaken(reg, clean.name)) return { ok: false, error: `已存在同名空间「${clean.name}」` };
|
|
930
1170
|
|
|
931
|
-
const parent = path.resolve(managedParent || defaultManagedParent(baseRoot));
|
|
932
1171
|
const dir = path.join(parent, clean.name);
|
|
933
1172
|
if (fs.existsSync(dir)) return { ok: false, error: `目录已存在:${dir}` };
|
|
934
1173
|
|
|
@@ -952,6 +1191,7 @@ export async function importSpaceFromZip({ zipFile, name, managedParent, baseRoo
|
|
|
952
1191
|
name: clean.name,
|
|
953
1192
|
path: dir,
|
|
954
1193
|
createdAt: Date.now(),
|
|
1194
|
+
root: resolveEntryRoot(baseRoot, parent),
|
|
955
1195
|
};
|
|
956
1196
|
writeSpaceManifest(dir, entry);
|
|
957
1197
|
|
package/config/app.defaults.json
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"plugins": {
|
|
17
17
|
"cases": false,
|
|
18
18
|
"caseGen": false,
|
|
19
|
-
"prompts":
|
|
19
|
+
"prompts": true,
|
|
20
20
|
"promptGen": false,
|
|
21
21
|
"knowledgeLib": false,
|
|
22
22
|
"knowledgeGen": false,
|
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
"templates": false,
|
|
25
25
|
"knowledge": false,
|
|
26
26
|
"memory": false,
|
|
27
|
-
"pluginDev": false
|
|
27
|
+
"pluginDev": false,
|
|
28
|
+
"todos": false,
|
|
29
|
+
"tasks": false
|
|
28
30
|
}
|
|
29
31
|
}
|