dsh-oh-my-terminal 0.2.2 → 0.2.4
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/lib/index.js +23 -4
- package/package.json +1 -1
- package/src/index.ts +13 -5
- package/src/settings/index.ts +64 -7
- package/tests/unit/settings-integration.test.ts +132 -0
package/lib/index.js
CHANGED
|
@@ -1014,10 +1014,28 @@ function createSettingsBridgeRoutes(settings, namespace, runtimeProfiles) {
|
|
|
1014
1014
|
}
|
|
1015
1015
|
|
|
1016
1016
|
// src/settings/index.ts
|
|
1017
|
-
function
|
|
1017
|
+
function registerSettingsNamespace(ctx, schema) {
|
|
1018
1018
|
ctx.inject(["settings"], (sctx) => {
|
|
1019
|
-
|
|
1019
|
+
const facade = sctx.settings;
|
|
1020
|
+
if (typeof facade.configure === "function") {
|
|
1021
|
+
const configure = facade.configure;
|
|
1022
|
+
sctx.effect(() => {
|
|
1023
|
+
const cleanup = configure.call(facade, { auto: false }, ctx.fiber);
|
|
1024
|
+
return typeof cleanup === "function" ? cleanup : () => void 0;
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
if (typeof facade.register === "function") {
|
|
1028
|
+
try {
|
|
1029
|
+
facade.register(SETTINGS_NS, schema);
|
|
1030
|
+
} catch (error) {
|
|
1031
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1032
|
+
if (!message.includes("already registered")) throw error;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1020
1035
|
});
|
|
1036
|
+
}
|
|
1037
|
+
function registerSettingsIntegration(ctx, schema, runtimeProfiles) {
|
|
1038
|
+
registerSettingsNamespace(ctx, schema);
|
|
1021
1039
|
ctx.inject(["webServer", "settings"], (sctx) => {
|
|
1022
1040
|
sctx.effect(() => {
|
|
1023
1041
|
const routes = createSettingsBridgeRoutes(sctx.settings, SETTINGS_NS, runtimeProfiles);
|
|
@@ -1139,7 +1157,7 @@ function apply(ctx, config) {
|
|
|
1139
1157
|
ctx.inject(["settings"], (sctx) => {
|
|
1140
1158
|
const settings = sctx.settings;
|
|
1141
1159
|
if (typeof settings.update !== "function") return;
|
|
1142
|
-
void settings.update(
|
|
1160
|
+
void settings.update(SETTINGS_NS, { terminalProfiles: payload }).then(() => {
|
|
1143
1161
|
log3.info(`\u7EC8\u7AEF\u914D\u7F6E\u8868\u5DF2\u521D\u59CB\u5316\uFF1A\u63A2\u6D4B\u5230 ${merged.length} \u4E2A\u7EC8\u7AEF`);
|
|
1144
1162
|
}).catch((error) => {
|
|
1145
1163
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -1149,6 +1167,7 @@ function apply(ctx, config) {
|
|
|
1149
1167
|
}
|
|
1150
1168
|
return merged;
|
|
1151
1169
|
}
|
|
1170
|
+
registerSettingsNamespace(ctx, Config);
|
|
1152
1171
|
const cachedProfiles = prepareProfiles();
|
|
1153
1172
|
const sessions = /* @__PURE__ */ new Map();
|
|
1154
1173
|
const upgradeDisposers = /* @__PURE__ */ new Map();
|
|
@@ -1277,7 +1296,7 @@ function apply(ctx, config) {
|
|
|
1277
1296
|
upgradeDisposers
|
|
1278
1297
|
})
|
|
1279
1298
|
});
|
|
1280
|
-
registerSettingsIntegration(ctx, cachedProfiles);
|
|
1299
|
+
registerSettingsIntegration(ctx, Config, cachedProfiles);
|
|
1281
1300
|
ctx.effect(() => {
|
|
1282
1301
|
return () => {
|
|
1283
1302
|
disposeRoute();
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -61,7 +61,7 @@ import { createWsHandlers } from './ws-handler.js';
|
|
|
61
61
|
import { createRouteHandler } from './routes.js';
|
|
62
62
|
import type { CreateSessionOptions } from './routes.js';
|
|
63
63
|
import { makeId, getSessionCounter, resolveSpawn, resolveSessionCwd } from './routes.js';
|
|
64
|
-
import { registerSettingsIntegration } from './settings/index.js';
|
|
64
|
+
import { registerSettingsIntegration, registerSettingsNamespace, SETTINGS_NS } from './settings/index.js';
|
|
65
65
|
import type { TerminalProfile } from './terminal/index.js';
|
|
66
66
|
import { detectTerminalProfiles, mergeProfiles } from './terminal/index.js';
|
|
67
67
|
|
|
@@ -317,7 +317,10 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
317
317
|
ctx.inject(['settings'], (sctx) => {
|
|
318
318
|
const settings = sctx.settings as { update?: (ns: string, patch: object) => Promise<void> };
|
|
319
319
|
if (typeof settings.update !== 'function') return;
|
|
320
|
-
|
|
320
|
+
// 命名空间必须与 settings 集成注册的 SETTINGS_NS 逐字一致——
|
|
321
|
+
// 此前误用包名派生串(PKG_NAME 去前缀 = 'oh-my-terminal'),与注册的
|
|
322
|
+
// 'terminal-panel' 对不上,首次落盘恒报「namespace not registered」
|
|
323
|
+
void settings.update(SETTINGS_NS, { terminalProfiles: payload })
|
|
321
324
|
.then(() => { log.info(`终端配置表已初始化:探测到 ${merged.length} 个终端`); })
|
|
322
325
|
.catch((error: unknown) => {
|
|
323
326
|
const msg = error instanceof Error ? error.message : String(error);
|
|
@@ -329,6 +332,10 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
329
332
|
}
|
|
330
333
|
|
|
331
334
|
/** 启动时求值的终端配置表(探测 + 合并结果) */
|
|
335
|
+
// 命名空间注册必须先于 prepareProfiles 的首跑写回(时序竞态:写回早于注册会
|
|
336
|
+
// 报「namespace is not registered」)——故在此提前注册,settings/index.ts
|
|
337
|
+
// 内部保留幂等兜底
|
|
338
|
+
registerSettingsNamespace(ctx, Config);
|
|
332
339
|
const cachedProfiles: TerminalProfile[] = prepareProfiles();
|
|
333
340
|
|
|
334
341
|
/** id -> 会话记录 */
|
|
@@ -521,9 +528,10 @@ export function apply(ctx: Context, config: Config): void {
|
|
|
521
528
|
});
|
|
522
529
|
|
|
523
530
|
// —— settings 集成(可选,软探测)——
|
|
524
|
-
// 传入
|
|
525
|
-
//
|
|
526
|
-
|
|
531
|
+
// 传入 Config schema:auto=false 下命名空间需显式注册(见 settings/index.ts),
|
|
532
|
+
// 缺注册则设置卡片静默不渲染;cachedProfiles 让 describe 端点返回运行时合并
|
|
533
|
+
// 探测结果后的完整配置表,而非配置系统持久化值(可能是空串)
|
|
534
|
+
registerSettingsIntegration(ctx, Config, cachedProfiles);
|
|
527
535
|
|
|
528
536
|
// —— 插件销毁清理 ——
|
|
529
537
|
ctx.effect(() => {
|
package/src/settings/index.ts
CHANGED
|
@@ -37,23 +37,80 @@ export { createSettingsBridgeRoutes } from './bridge.js';
|
|
|
37
37
|
* 逐一调用清理。
|
|
38
38
|
*
|
|
39
39
|
* @param ctx - cordis 上下文(插件主入口的 apply 参数)
|
|
40
|
+
* @param schema - 插件 Config 的 schemastery schema,用于注册 settings 命名空间。
|
|
41
|
+
* auto=false 下服务不会自动注册,缺了这一步 describe/mutate 恒报「命名空间
|
|
42
|
+
* 不存在」,前端配置卡片静默不渲染(无报错、无入口)
|
|
40
43
|
* @param runtimeProfiles - 运行时终端配置表(探测+合并后的完整数组),传给 bridge
|
|
41
44
|
* 供 describe 端点覆盖持久化的 terminalProfiles 值
|
|
42
45
|
*/
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
/**
|
|
47
|
+
* 注册 settings 命名空间(幂等,须先于任何 settings 读写)。
|
|
48
|
+
*
|
|
49
|
+
* 命名空间注册是 settings 服务注册表的唯一入口;首次配置落盘(prepareProfiles
|
|
50
|
+
* 的首跑写回)等读写都依赖它——必须在 apply 早期调用本函数,晚于首次写回会
|
|
51
|
+
* 报「namespace is not registered」(时序竞态)。重复注册(热重载、重复 apply、
|
|
52
|
+
* 以及 registerSettingsIntegration 的兜底调用)抛 "already registered",作为幂等
|
|
53
|
+
* 情形吞掉;其余注册失败(如存储段损坏)如实上抛,不把失败包装成成功。
|
|
54
|
+
*
|
|
55
|
+
* @param ctx - cordis 上下文
|
|
56
|
+
* @param schema - 插件 Config 的 schemastery schema
|
|
57
|
+
*/
|
|
58
|
+
export function registerSettingsNamespace(ctx: Context, schema: unknown): void {
|
|
59
|
+
// 注册命名空间 +(可选)关闭自动模式——软探测,服务不存在时不激活
|
|
48
60
|
ctx.inject(['settings'], (sctx) => {
|
|
49
|
-
|
|
61
|
+
/** settings 服务能力面(不同宿主版本暴露面不同,逐能力探测) */
|
|
62
|
+
interface SettingsFacade {
|
|
63
|
+
/** 新宿主可选能力:关闭自动写回;旧宿主(0.1.5 系)无此方法 */
|
|
64
|
+
configure?: (options: { auto: boolean }, fiber?: unknown) => void;
|
|
65
|
+
/**
|
|
66
|
+
* 注册命名空间 schema;重复注册抛 "already registered"。
|
|
67
|
+
* 可选——宿主 SettingsForms 靠 cordis loader entries 发现命名空间,
|
|
68
|
+
* 不一定提供此方法(如 0.1.7-rc.1 的 SettingsForms 无 register)。
|
|
69
|
+
*/
|
|
70
|
+
register?: (ns: string, schema: unknown) => unknown;
|
|
71
|
+
}
|
|
72
|
+
const facade = sctx.settings as unknown as SettingsFacade;
|
|
73
|
+
|
|
74
|
+
// configure 仅在宿主提供时调用——0.1.5 系宿主的 settings 服务没有该方法,
|
|
75
|
+
// 直接调用会 TypeError 并中断本回调的后续初始化(包括下面的命名空间注册):
|
|
50
76
|
// - auto=true:settings 服务自动从 profile patch 读配置并写回插件 Config
|
|
51
77
|
// - auto=false:插件自行管理配置(本插件的 Config 字段都是 volatile 引用,
|
|
52
78
|
// 由 cordis loader 注入,无需 settings 服务二次写入)
|
|
53
79
|
//
|
|
54
80
|
// fiber 参数绑定配置生命周期到当前 effect——effect 销毁时配置自动清理
|
|
55
|
-
|
|
81
|
+
if (typeof facade.configure === 'function') {
|
|
82
|
+
const configure = facade.configure;
|
|
83
|
+
sctx.effect(() => {
|
|
84
|
+
const cleanup = configure.call(facade, { auto: false }, ctx.fiber);
|
|
85
|
+
// effect 要求回调返回清理函数;configure 的返回值即其注册的清理效应
|
|
86
|
+
return typeof cleanup === 'function' ? cleanup : (): void => undefined;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// register 同样逐能力探测——宿主 SettingsForms 靠 cordis loader entries
|
|
91
|
+
// (configEditor.configuration())发现命名空间,不一定提供 register 方法
|
|
92
|
+
// (0.1.7-rc.1 的 SettingsForms 无此方法)。方法不存在时跳过,不抛 TypeError;
|
|
93
|
+
// 方法存在时重复注册(热重载、重复 apply)抛 "already registered",作为幂等
|
|
94
|
+
// 情形吞掉;其余注册失败(如存储段损坏)如实上抛,不把失败包装成成功。
|
|
95
|
+
if (typeof facade.register === 'function') {
|
|
96
|
+
try {
|
|
97
|
+
facade.register(SETTINGS_NS, schema);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
100
|
+
if (!message.includes('already registered')) throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
56
103
|
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function registerSettingsIntegration(
|
|
107
|
+
ctx: Context,
|
|
108
|
+
schema: unknown,
|
|
109
|
+
runtimeProfiles?: unknown[],
|
|
110
|
+
): void {
|
|
111
|
+
// 0. 命名空间注册兜底(幂等)——正常由调用方在 apply 早期先行调用
|
|
112
|
+
// registerSettingsNamespace,此处重复注册被吞
|
|
113
|
+
registerSettingsNamespace(ctx, schema);
|
|
57
114
|
|
|
58
115
|
// 2. 注册 settings bridge 路由(describe + mutate)——软探测,服务不存在时不激活
|
|
59
116
|
ctx.inject(['webServer', 'settings'], (sctx) => {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file settings 集成注册单元测试
|
|
3
|
+
* @description 回归锁定 registerSettingsIntegration 的命名空间注册契约:
|
|
4
|
+
* 注册是 settings 服务注册表的唯一入口,缺注册时 describe/mutate
|
|
5
|
+
* 恒报「命名空间不存在」,前端配置卡片静默不渲染(无报错、无入口)。
|
|
6
|
+
* 同时锁定宿主兼容面:旧宿主(0.1.5 系)的 settings 服务没有
|
|
7
|
+
* configure 方法,集成不得因此抛错中断注册——曾因无守卫调用
|
|
8
|
+
* configure 抛 TypeError,把随后的 register 一并挡掉。
|
|
9
|
+
* 本测试用假 settings 服务断言:注册确实发生且 schema 原样透传、
|
|
10
|
+
* 无 configure 的宿主不炸、有 configure 时才调用、重复注册幂等、
|
|
11
|
+
* 其余注册失败如实上抛(不把失败包装成成功)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
17
|
+
import { registerSettingsIntegration, SETTINGS_NS } from '../../src/settings/index.js';
|
|
18
|
+
|
|
19
|
+
/** register 调用记录 */
|
|
20
|
+
interface RegisterCall {
|
|
21
|
+
/** 被注册的命名空间 */
|
|
22
|
+
ns: string;
|
|
23
|
+
/** 传入的 schema 引用 */
|
|
24
|
+
schema: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 假 settings 服务:可选 configure/register 模拟不同宿主版本的暴露面 */
|
|
28
|
+
function makeFakeSettings(options: {
|
|
29
|
+
withConfigure?: boolean;
|
|
30
|
+
withRegister?: boolean;
|
|
31
|
+
failRegisterWith?: Error;
|
|
32
|
+
} = {}): {
|
|
33
|
+
settings: Record<string, unknown>;
|
|
34
|
+
registerCalls: RegisterCall[];
|
|
35
|
+
configureCalls: unknown[];
|
|
36
|
+
} {
|
|
37
|
+
const registerCalls: RegisterCall[] = [];
|
|
38
|
+
const configureCalls: unknown[] = [];
|
|
39
|
+
const settings: Record<string, unknown> = {
|
|
40
|
+
describe: async (): Promise<unknown> => [],
|
|
41
|
+
mutate: async (): Promise<unknown> => undefined,
|
|
42
|
+
writable: true,
|
|
43
|
+
};
|
|
44
|
+
// register 可选——0.1.7-rc.1 的 SettingsForms 靠 cordis loader entries 发现
|
|
45
|
+
// 命名空间,不提供 register 方法
|
|
46
|
+
if (options.withRegister !== false) {
|
|
47
|
+
settings.register = (ns: string, schema: unknown): unknown => {
|
|
48
|
+
if (options.failRegisterWith !== undefined) throw options.failRegisterWith;
|
|
49
|
+
registerCalls.push({ ns, schema });
|
|
50
|
+
return {
|
|
51
|
+
get: () => ({}),
|
|
52
|
+
watch: () => (): void => undefined,
|
|
53
|
+
update: async (): Promise<void> => undefined,
|
|
54
|
+
replace: async (): Promise<void> => undefined,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (options.withConfigure === true) {
|
|
59
|
+
settings.configure = (value: unknown): void => {
|
|
60
|
+
configureCalls.push(value);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { settings, registerCalls, configureCalls };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** 假 cordis 上下文:inject 直接回调,effect 立即执行并回收 cleanup */
|
|
67
|
+
function makeFakeContext(settings: unknown): Context {
|
|
68
|
+
const ctx = {
|
|
69
|
+
fiber: {},
|
|
70
|
+
inject(_deps: string[], callback: (sctx: Record<string, unknown>) => void): void {
|
|
71
|
+
callback({
|
|
72
|
+
settings,
|
|
73
|
+
webServer: { register: () => (): void => undefined },
|
|
74
|
+
effect: (fn: () => unknown): void => {
|
|
75
|
+
const returned = fn();
|
|
76
|
+
if (typeof returned === 'function') returned();
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
return ctx as unknown as Context;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('settings 集成注册', () => {
|
|
85
|
+
it('旧宿主(无 configure)下仍完成命名空间注册,不抛错', () => {
|
|
86
|
+
const { settings, registerCalls, configureCalls } = makeFakeSettings();
|
|
87
|
+
const schema = { marker: 'config-schema' };
|
|
88
|
+
assert.doesNotThrow(() => {
|
|
89
|
+
registerSettingsIntegration(makeFakeContext(settings), schema, []);
|
|
90
|
+
});
|
|
91
|
+
assert.equal(registerCalls.length, 1);
|
|
92
|
+
assert.equal(registerCalls[0]?.ns, SETTINGS_NS);
|
|
93
|
+
assert.equal(registerCalls[0]?.schema, schema);
|
|
94
|
+
assert.deepEqual(configureCalls, []);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('新宿主(有 configure)时调用 auto:false 并照常注册', () => {
|
|
98
|
+
const { settings, registerCalls, configureCalls } = makeFakeSettings({ withConfigure: true });
|
|
99
|
+
registerSettingsIntegration(makeFakeContext(settings), { marker: 'x' });
|
|
100
|
+
assert.deepEqual(configureCalls, [{ auto: false }]);
|
|
101
|
+
assert.equal(registerCalls.length, 1);
|
|
102
|
+
assert.equal(registerCalls[0]?.ns, SETTINGS_NS);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('重复注册视为幂等情形,吞掉 already registered 不上抛', () => {
|
|
106
|
+
const { settings } = makeFakeSettings({
|
|
107
|
+
failRegisterWith: new Error('settings namespace "terminal-panel" is already registered'),
|
|
108
|
+
});
|
|
109
|
+
assert.doesNotThrow(() => {
|
|
110
|
+
registerSettingsIntegration(makeFakeContext(settings), { marker: 'x' });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('其他注册失败如实上抛,不把失败包装成成功', () => {
|
|
115
|
+
const { settings } = makeFakeSettings({
|
|
116
|
+
failRegisterWith: new Error('settings namespace "terminal-panel" 存储段损坏'),
|
|
117
|
+
});
|
|
118
|
+
assert.throws(
|
|
119
|
+
() => registerSettingsIntegration(makeFakeContext(settings), { marker: 'x' }),
|
|
120
|
+
/存储段损坏/u,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('宿主无 register 方法时不抛 TypeError(靠 cordis loader entries 发现命名空间)', () => {
|
|
125
|
+
const { settings, registerCalls } = makeFakeSettings({ withRegister: false });
|
|
126
|
+
assert.doesNotThrow(() => {
|
|
127
|
+
registerSettingsIntegration(makeFakeContext(settings), { marker: 'x' });
|
|
128
|
+
});
|
|
129
|
+
// 宿主无 register——registerCalls 为空,但不抛错
|
|
130
|
+
assert.equal(registerCalls.length, 0);
|
|
131
|
+
});
|
|
132
|
+
});
|