dsh-oh-my-terminal 0.2.3 → 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 CHANGED
@@ -1014,17 +1014,28 @@ function createSettingsBridgeRoutes(settings, namespace, runtimeProfiles) {
1014
1014
  }
1015
1015
 
1016
1016
  // src/settings/index.ts
1017
- function registerSettingsIntegration(ctx, schema, runtimeProfiles) {
1017
+ function registerSettingsNamespace(ctx, schema) {
1018
1018
  ctx.inject(["settings"], (sctx) => {
1019
- sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber));
1020
- const registrar = sctx.settings;
1021
- try {
1022
- registrar.register(SETTINGS_NS, schema);
1023
- } catch (error) {
1024
- const message = error instanceof Error ? error.message : String(error);
1025
- if (!message.includes("already registered")) throw error;
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
+ }
1026
1034
  }
1027
1035
  });
1036
+ }
1037
+ function registerSettingsIntegration(ctx, schema, runtimeProfiles) {
1038
+ registerSettingsNamespace(ctx, schema);
1028
1039
  ctx.inject(["webServer", "settings"], (sctx) => {
1029
1040
  sctx.effect(() => {
1030
1041
  const routes = createSettingsBridgeRoutes(sctx.settings, SETTINGS_NS, runtimeProfiles);
@@ -1146,7 +1157,7 @@ function apply(ctx, config) {
1146
1157
  ctx.inject(["settings"], (sctx) => {
1147
1158
  const settings = sctx.settings;
1148
1159
  if (typeof settings.update !== "function") return;
1149
- void settings.update(PKG_NAME.replace(/^dsh-/, ""), { terminalProfiles: payload }).then(() => {
1160
+ void settings.update(SETTINGS_NS, { terminalProfiles: payload }).then(() => {
1150
1161
  log3.info(`\u7EC8\u7AEF\u914D\u7F6E\u8868\u5DF2\u521D\u59CB\u5316\uFF1A\u63A2\u6D4B\u5230 ${merged.length} \u4E2A\u7EC8\u7AEF`);
1151
1162
  }).catch((error) => {
1152
1163
  const msg = error instanceof Error ? error.message : String(error);
@@ -1156,6 +1167,7 @@ function apply(ctx, config) {
1156
1167
  }
1157
1168
  return merged;
1158
1169
  }
1170
+ registerSettingsNamespace(ctx, Config);
1159
1171
  const cachedProfiles = prepareProfiles();
1160
1172
  const sessions = /* @__PURE__ */ new Map();
1161
1173
  const upgradeDisposers = /* @__PURE__ */ new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-oh-my-terminal",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "DSH Web GUI 底部终端面板插件——node-pty 驱动的多标签交互式终端(Windows ConPTY / POSIX openpty)",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
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
- void settings.update(PKG_NAME.replace(/^dsh-/, ''), { terminalProfiles: payload })
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 -> 会话记录 */
@@ -43,38 +43,74 @@ export { createSettingsBridgeRoutes } from './bridge.js';
43
43
  * @param runtimeProfiles - 运行时终端配置表(探测+合并后的完整数组),传给 bridge
44
44
  * 供 describe 端点覆盖持久化的 terminalProfiles 值
45
45
  */
46
- export function registerSettingsIntegration(
47
- ctx: Context,
48
- schema: unknown,
49
- runtimeProfiles?: unknown[],
50
- ): void {
51
- // 1. 注册 settings 命名空间 + 关闭自动模式——软探测,服务不存在时不激活
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
+ // 注册命名空间 +(可选)关闭自动模式——软探测,服务不存在时不激活
52
60
  ctx.inject(['settings'], (sctx) => {
53
- // settings.configure({ auto: false }, fiber) 关闭自动模式:
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 并中断本回调的后续初始化(包括下面的命名空间注册):
54
76
  // - auto=true:settings 服务自动从 profile patch 读配置并写回插件 Config
55
77
  // - auto=false:插件自行管理配置(本插件的 Config 字段都是 volatile 引用,
56
78
  // 由 cordis loader 注入,无需 settings 服务二次写入)
57
79
  //
58
80
  // fiber 参数绑定配置生命周期到当前 effect——effect 销毁时配置自动清理
59
- sctx.effect(() => sctx.settings.configure({ auto: false }, ctx.fiber));
60
-
61
- // auto=false 同时关闭了命名空间的自动注册——必须在此显式 register,否则
62
- // describe/mutate 恒报「命名空间不存在」,前端配置卡片静默不渲染。重复注册
63
- // (热重载、重复 apply)抛 "already registered",作为幂等情形吞掉;其余注册
64
- // 失败如实上抛,不把失败包装成成功。
65
- /** settings 服务注册面的最小鸭子类型(对齐官方 SettingsProvider.register) */
66
- interface SettingsRegistrar {
67
- /** 注册命名空间 schema;重复注册抛 "already registered" */
68
- register(ns: string, schema: unknown): unknown;
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
+ });
69
88
  }
70
- const registrar = sctx.settings as unknown as SettingsRegistrar;
71
- try {
72
- registrar.register(SETTINGS_NS, schema);
73
- } catch (error) {
74
- const message = error instanceof Error ? error.message : String(error);
75
- if (!message.includes('already registered')) throw error;
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
+ }
76
102
  }
77
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);
78
114
 
79
115
  // 2. 注册 settings bridge 路由(describe + mutate)——软探测,服务不存在时不激活
80
116
  ctx.inject(['webServer', 'settings'], (sctx) => {
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * @file settings 集成注册单元测试
3
3
  * @description 回归锁定 registerSettingsIntegration 的命名空间注册契约:
4
- * auto=false 下 settings 服务不会自动注册命名空间,插件必须显式
5
- * register(SETTINGS_NS, schema),否则 describe/mutate 恒报
6
- * 「命名空间不存在」——前端配置卡片静默不渲染(无报错、无入口)。
4
+ * 注册是 settings 服务注册表的唯一入口,缺注册时 describe/mutate
5
+ * 恒报「命名空间不存在」,前端配置卡片静默不渲染(无报错、无入口)。
6
+ * 同时锁定宿主兼容面:旧宿主(0.1.5 系)的 settings 服务没有
7
+ * configure 方法,集成不得因此抛错中断注册——曾因无守卫调用
8
+ * configure 抛 TypeError,把随后的 register 一并挡掉。
7
9
  * 本测试用假 settings 服务断言:注册确实发生且 schema 原样透传、
8
- * 重复注册幂等吞掉、其余注册失败如实上抛(不把失败包装成成功)。
10
+ * 无 configure 的宿主不炸、有 configure 时才调用、重复注册幂等、
11
+ * 其余注册失败如实上抛(不把失败包装成成功)。
9
12
  */
10
13
 
11
14
  import { describe, it } from 'node:test';
@@ -21,19 +24,27 @@ interface RegisterCall {
21
24
  schema: unknown;
22
25
  }
23
26
 
24
- /** 假 settings 服务:只实现集成代码用到的 configure / register / describe / mutate */
25
- function makeFakeSettings(options: { failRegisterWith?: Error } = {}): {
27
+ /** 假 settings 服务:可选 configure/register 模拟不同宿主版本的暴露面 */
28
+ function makeFakeSettings(options: {
29
+ withConfigure?: boolean;
30
+ withRegister?: boolean;
31
+ failRegisterWith?: Error;
32
+ } = {}): {
26
33
  settings: Record<string, unknown>;
27
34
  registerCalls: RegisterCall[];
28
35
  configureCalls: unknown[];
29
36
  } {
30
37
  const registerCalls: RegisterCall[] = [];
31
38
  const configureCalls: unknown[] = [];
32
- const settings = {
33
- configure(value: unknown): void {
34
- configureCalls.push(value);
35
- },
36
- register(ns: string, schema: unknown): 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 => {
37
48
  if (options.failRegisterWith !== undefined) throw options.failRegisterWith;
38
49
  registerCalls.push({ ns, schema });
39
50
  return {
@@ -42,10 +53,13 @@ function makeFakeSettings(options: { failRegisterWith?: Error } = {}): {
42
53
  update: async (): Promise<void> => undefined,
43
54
  replace: async (): Promise<void> => undefined,
44
55
  };
45
- },
46
- describe: async (): Promise<unknown> => ({ ok: true }),
47
- mutate: async (): Promise<unknown> => ({ ok: true }),
48
- };
56
+ };
57
+ }
58
+ if (options.withConfigure === true) {
59
+ settings.configure = (value: unknown): void => {
60
+ configureCalls.push(value);
61
+ };
62
+ }
49
63
  return { settings, registerCalls, configureCalls };
50
64
  }
51
65
 
@@ -68,14 +82,24 @@ function makeFakeContext(settings: unknown): Context {
68
82
  }
69
83
 
70
84
  describe('settings 集成注册', () => {
71
- it('settings 服务就位时显式注册命名空间并透传 schema', () => {
85
+ it('旧宿主(无 configure)下仍完成命名空间注册,不抛错', () => {
72
86
  const { settings, registerCalls, configureCalls } = makeFakeSettings();
73
87
  const schema = { marker: 'config-schema' };
74
- registerSettingsIntegration(makeFakeContext(settings), schema, []);
88
+ assert.doesNotThrow(() => {
89
+ registerSettingsIntegration(makeFakeContext(settings), schema, []);
90
+ });
75
91
  assert.equal(registerCalls.length, 1);
76
92
  assert.equal(registerCalls[0]?.ns, SETTINGS_NS);
77
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' });
78
100
  assert.deepEqual(configureCalls, [{ auto: false }]);
101
+ assert.equal(registerCalls.length, 1);
102
+ assert.equal(registerCalls[0]?.ns, SETTINGS_NS);
79
103
  });
80
104
 
81
105
  it('重复注册视为幂等情形,吞掉 already registered 不上抛', () => {
@@ -96,4 +120,13 @@ describe('settings 集成注册', () => {
96
120
  /存储段损坏/u,
97
121
  );
98
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
+ });
99
132
  });