dsh-code-server-app 0.1.30 → 0.1.32

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
@@ -1,11 +1,19 @@
1
1
  /**
2
- * dsh-code-server — host 半部:code-server 进程的启动/停止/状态管理 + /code-server JSON API。
2
+ * dsh-code-server — host 半部:code-server 进程的启动/停止/状态管理 + /api/code-server JSON API。
3
3
  *
4
4
  * 零外部依赖:只用 Node 内置模块(child_process / http / fs / path / os)。
5
5
  * 静态 profile 插件,与 dsh-webproxy-router-plugin 同形态:
6
6
  * - exports.name = 插件名(与 cordis.patch.yml 行 id 一致)
7
- * - exports.inject = ['webServer'](硬依赖:等待 webserver 服务就绪)
8
- * - apply(ctx, config) 注册 3 条 exact JSON 路由:status / start / stop
7
+ * - exports.inject = ['connection', 'settings'](connection 承载 client↔host 通道)
8
+ * - apply(ctx, config) 在共享 /api 通道注册 5 条 exact Fetch 路由:
9
+ * status / start / stop / setup / open-file
10
+ *
11
+ * 通道选择(重要):**不依赖 webServer**。路由经 ctx.connection.fetch.register
12
+ * 挂在 Connection 服务的共享 /api 通道上:
13
+ * - web profile:Connection 自己把 /api 前缀挂到 webServer(带 Host/Origin + 浏览器鉴权);
14
+ * - desktop profile:apps/desktop-host 把 /api/* 交给同一个 createSharedFetchHandler('/api')
15
+ * (IPC 帧管道,无 HTTP 服务器)→ 插件在 desktop 下同样可用。
16
+ * 客户端因此只需同源 fetch('/api/code-server/<op>')(见 src/factory.js 的 api())。
9
17
  *
10
18
  * 设计要点:
11
19
  * - 进程生命周期归本插件:启动写 pid.json,停止用树级终止(taskkill /T 或
@@ -14,13 +22,13 @@
14
22
  * running(不重复启动);否则清理 pid.json 视为 stopped。绝不误杀别的进程。
15
23
  * - 就绪探测轮询 /healthz;失败时 status 携带启动日志尾部与错误信息。
16
24
  * - auth=none 仅允许回环 host;非回环强制 password(未配置 token 则拒绝启动)。
17
- * - 挂载于 ctx.effect:插件销毁(host 关闭/卸载)时回收自己启动的进程。
25
+ * - 挂载于 ctx.effect:插件销毁(host 关闭/卸载)时回收自己启动的进程与路由。
18
26
  *
19
- * API(同源 fetch,与 webproxy-plugin webServer JSON API 同机制):
20
- * GET /code-server/status → { ok, running, status, port, pid, cwd, url,
21
- * version, error, logTail[, adopted] }
22
- * POST /code-server/start { cwd? } → 幂等启动(换 cwd 则先停后启) → status
23
- * POST /code-server/stop → 停止 → status
27
+ * API(同源 fetch;web desktop 同一套路径):
28
+ * GET /api/code-server/status → { ok, running, status, port, pid, cwd, url,
29
+ * version, error, logTail[, adopted] }
30
+ * POST /api/code-server/start { cwd? } → 幂等启动(换 cwd 则先停后启) → status
31
+ * POST /api/code-server/stop → 停止 → status
24
32
  */
25
33
 
26
34
  import { spawn, execFile, execFileSync } from 'node:child_process';
@@ -53,7 +61,9 @@ if (z === null || z === undefined) {
53
61
  }
54
62
 
55
63
  export const name = 'code-server';
56
- export const inject = ['webServer', 'settings'];
64
+ // connection:client↔host 通道(web webServer 的 /api,desktop 走 IPC 帧管道);
65
+ // settings:设置卡片的数据域。两者在 web 与 desktop profile 下都存在。
66
+ export const inject = ['connection', 'settings'];
57
67
 
58
68
  /** 设置命名空间:卡片经官方 settings 域读写,持久化到官方 settings 文档。 */
59
69
  export const SETTINGS_NS = 'code-server';
@@ -659,132 +669,113 @@ export async function apply(ctx, config) {
659
669
  });
660
670
  }
661
671
 
662
- async function handleApi(req, res) {
663
- const method = (req.method || 'GET').toUpperCase();
664
- const payload = { ok: false, error: 'unknown route' };
672
+ // ---- /api/code-server 路由(Connection 共享通道;web 与 desktop 通用) ----
673
+ const API_BASE = '/api/code-server';
674
+
675
+ /** JSON 响应(与旧 webServer JSON API 相同的载荷形状)。 */
676
+ function jsonResponse(value, status = 200) {
677
+ return new Response(JSON.stringify(value), {
678
+ status,
679
+ headers: { 'content-type': 'application/json; charset=utf-8' },
680
+ });
681
+ }
682
+
683
+ /** 读取 JSON 请求体;空体 / 非法 JSON 回退为 {}(旧实现是逐块 best-effort 合并)。 */
684
+ async function readJsonBody(request) {
665
685
  try {
666
- if (req.url === '/code-server/status' && method === 'GET') {
667
- Object.assign(payload, snapshot());
668
- } else if (req.url === '/code-server/start' && method === 'POST') {
669
- let body = {};
670
- for await (const chunk of req) {
671
- const text = chunk.toString('utf8');
672
- if (text) {
673
- try {
674
- body = { ...body, ...JSON.parse(text) };
675
- } catch {
676
- // ignore malformed fragments; keep best-effort
677
- }
678
- }
679
- }
680
- const cwd = body && typeof body.cwd === 'string' ? body.cwd : undefined;
681
- Object.assign(payload, await start(cwd));
682
- } else if (req.url === '/code-server/stop' && method === 'POST') {
683
- await stop('user');
684
- Object.assign(payload, snapshot());
685
- } else if (req.url === '/code-server/setup' && method === 'POST') {
686
- // 环境安装:后台执行 setup 脚本(npm 自装 code-server + native + vscode 内部依赖)
687
- if (state.setup.running) {
688
- // 已在安装中:同样视为"已发起"(客户端会轮询 setup 状态直至结束)
689
- payload.ok = true;
690
- payload.message = '环境安装已在进行中,请等待完成';
691
- payload.error = null;
692
- } else {
693
- startSetup();
694
- Object.assign(payload, snapshot());
695
- payload.ok = true; // 安装任务已启动;服务态 error(如未安装)不掩盖安装态
696
- }
697
- } else if (req.url === '/code-server/open-file' && method === 'POST') {
698
- // 打开文件:host 写信号文件,内置扩展(dshcs-open-file)在 VS Code 中打开它
699
- let body = {};
700
- for await (const chunk of req) {
701
- const text = chunk.toString('utf8');
702
- if (text) {
703
- try {
704
- body = { ...body, ...JSON.parse(text) };
705
- } catch {
706
- // ignore malformed fragments; keep best-effort
707
- }
708
- }
709
- }
710
- const file = body && typeof body.file === 'string' ? body.file : null;
711
- if (file === null || file === '') {
712
- payload.error = '需要 file 字段(要打开的绝对路径)';
713
- payload.status = 400;
714
- res.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
715
- res.end(JSON.stringify(payload));
716
- return;
717
- }
718
- const root = dataRoot(cfg);
719
- const userDataDir = cfg.userDataDir || path.join(root, 'user-data');
720
- const signal = openFileSignalPath(userDataDir);
721
- try {
722
- fs.mkdirSync(path.dirname(signal), { recursive: true });
723
- fs.writeFileSync(signal, JSON.stringify({ file, ts: Date.now() }), 'utf8');
724
- // 同时确保 code-server 运行(信号由扩展消费)
725
- payload.ok = true;
726
- payload.signal = signal;
727
- } catch (err) {
728
- payload.error = err && err.message ? err.message : String(err);
729
- }
730
- } else {
731
- payload.error = 'unknown route';
732
- payload.status = 404;
733
- res.writeHead(404, { 'content-type': 'application/json; charset=utf-8' });
734
- res.end(JSON.stringify(payload));
735
- return;
686
+ const value = await request.json();
687
+ return value !== null && typeof value === 'object' && !Array.isArray(value) ? value : {};
688
+ } catch {
689
+ return {};
690
+ }
691
+ }
692
+
693
+ /** 统一的失败载荷(HTTP 200 + ok:false,客户端按 ok 判定)。 */
694
+ function failureResponse(err) {
695
+ return jsonResponse({
696
+ ok: false,
697
+ error: err && err.message ? err.message : String(err),
698
+ runner: 'code-server',
699
+ });
700
+ }
701
+
702
+ async function handleStatus() {
703
+ return jsonResponse(snapshot());
704
+ }
705
+
706
+ async function handleStart(request) {
707
+ try {
708
+ const body = await readJsonBody(request);
709
+ const cwd = typeof body.cwd === 'string' ? body.cwd : undefined;
710
+ return jsonResponse(await start(cwd));
711
+ } catch (err) {
712
+ return failureResponse(err);
713
+ }
714
+ }
715
+
716
+ async function handleStop() {
717
+ try {
718
+ await stop('user');
719
+ return jsonResponse(snapshot());
720
+ } catch (err) {
721
+ return failureResponse(err);
722
+ }
723
+ }
724
+
725
+ async function handleSetup() {
726
+ try {
727
+ // 环境安装:后台执行 setup 脚本(npm 自装 code-server + native + vscode 内部依赖)
728
+ if (state.setup.running) {
729
+ // 已在安装中:同样视为"已发起"(客户端会轮询 setup 状态直至结束)
730
+ return jsonResponse({ ...snapshot(), ok: true, message: '环境安装已在进行中,请等待完成', error: null });
736
731
  }
737
- res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
738
- res.end(JSON.stringify(payload));
732
+ startSetup();
733
+ // 安装任务已启动;服务态 error(如未安装)不掩盖安装态
734
+ return jsonResponse({ ...snapshot(), ok: true });
739
735
  } catch (err) {
740
- payload.ok = false;
741
- payload.error = err && err.message ? err.message : String(err);
742
- payload.runner = 'code-server';
743
- res.writeHead(200, { 'content-type': 'application/json; charset=utf-8' });
744
- res.end(JSON.stringify(payload));
736
+ return failureResponse(err);
745
737
  }
746
738
  }
747
739
 
748
- /** 固化图标服务:返回插件携带的 code-server 官方图标(不依赖运行时安装)
749
- * - /code-server/icon.ico → assets/favicon.ico(标签页/桌面图标)
750
- * - /code-server/icon.svg → assets/favicon.svg(PWA/深色模式) */
751
- function handleIcon(req, res) {
752
- const isSvg = req.url === '/code-server/icon.svg';
753
- const file = isSvg ? 'favicon.svg' : 'favicon.ico';
754
- const p = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'assets', file);
740
+ async function handleOpenFile(request) {
755
741
  try {
756
- if (!fs.existsSync(p)) {
757
- res.writeHead(404, { 'content-type': 'text/plain; charset=utf-8' });
758
- res.end('icon not found');
759
- return;
742
+ // 打开文件:host 写信号文件,内置扩展(dshcs-open-file) VS Code 中打开它
743
+ const body = await readJsonBody(request);
744
+ const file = typeof body.file === 'string' ? body.file : null;
745
+ if (file === null || file === '') {
746
+ return jsonResponse({ ok: false, error: '需要 file 字段(要打开的绝对路径)' }, 400);
760
747
  }
761
- const data = fs.readFileSync(p);
762
- res.writeHead(200, {
763
- 'content-type': isSvg ? 'image/svg+xml' : 'image/x-icon',
764
- 'cache-control': 'public, max-age=86400',
765
- });
766
- res.end(data);
748
+ const root = dataRoot(cfg);
749
+ const userDataDir = cfg.userDataDir || path.join(root, 'user-data');
750
+ const signal = openFileSignalPath(userDataDir);
751
+ fs.mkdirSync(path.dirname(signal), { recursive: true });
752
+ fs.writeFileSync(signal, JSON.stringify({ file, ts: Date.now() }), 'utf8');
753
+ return jsonResponse({ ok: true, signal });
767
754
  } catch (err) {
768
- res.writeHead(500, { 'content-type': 'application/json; charset=utf-8' });
769
- res.end(JSON.stringify({ ok: false, error: err && err.message ? err.message : String(err) }));
755
+ return failureResponse(err);
770
756
  }
771
757
  }
772
758
 
773
- const webServer = ctx.get('webServer');
774
- if (webServer === undefined) {
775
- console.error('[code-server] webServer service unavailable; plugin registered but idle');
759
+ const connection = ctx.get('connection');
760
+ if (connection === undefined || connection.fetch === undefined) {
761
+ console.error('[code-server] connection service unavailable; plugin registered but idle');
776
762
  return;
777
763
  }
778
764
 
765
+ // 每条操作一条 exact Fetch 路由。desktop 的 assetHandler 只把 /api/* 交给
766
+ // createSharedFetchHandler('/api'),所以路径必须落在 /api 下。
779
767
  const disposers = [
780
- webServer.register({ kind: 'exact', path: '/code-server/status', handler: handleApi }),
781
- webServer.register({ kind: 'exact', path: '/code-server/start', handler: handleApi }),
782
- webServer.register({ kind: 'exact', path: '/code-server/stop', handler: handleApi }),
783
- webServer.register({ kind: 'exact', path: '/code-server/setup', handler: handleApi }),
784
- webServer.register({ kind: 'exact', path: '/code-server/open-file', handler: handleApi }),
785
- webServer.register({ kind: 'exact', path: '/code-server/icon.ico', handler: handleIcon }),
786
- webServer.register({ kind: 'exact', path: '/code-server/icon.svg', handler: handleIcon }),
787
- ];
768
+ { path: `${API_BASE}/status`, methods: ['GET'], fetch: handleStatus },
769
+ { path: `${API_BASE}/start`, methods: ['POST'], fetch: handleStart },
770
+ { path: `${API_BASE}/stop`, methods: ['POST'], fetch: handleStop },
771
+ { path: `${API_BASE}/setup`, methods: ['POST'], fetch: handleSetup },
772
+ { path: `${API_BASE}/open-file`, methods: ['POST'], fetch: handleOpenFile },
773
+ ].map(route => connection.fetch.register({
774
+ path: route.path,
775
+ methods: route.methods,
776
+ requestBody: 'buffered',
777
+ fetch: route.fetch,
778
+ }));
788
779
 
789
780
  ctx.effect(() => {
790
781
  return () => {
@@ -792,7 +783,9 @@ export async function apply(ctx, config) {
792
783
  stopPolling();
793
784
  for (const d of disposers) {
794
785
  try {
795
- d();
786
+ // connection.fetch.register 的 disposer 是异步的(返回 Promise);
787
+ // 注册本身也挂在当前 fiber 上,这里显式回收只是双保险。
788
+ Promise.resolve(d()).catch(() => {});
796
789
  } catch {
797
790
  // ignore
798
791
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-code-server-app",
3
- "version": "0.1.30",
4
- "description": "Integrate code-server (VS Code in the browser) into DSH Web: floating ball entry + fullscreen overlay, host-side process lifecycle over a /code-server JSON API (motion-based spring window animations).",
3
+ "version": "0.1.32",
4
+ "description": "Integrate code-server (VS Code in the browser) into DSH Web and Desktop: right-sidebar tab (DSH >= 0.1.5-alpha.1) with a floating-ball fallback for older hosts, over the Connection /api channel (ctx.connection.fetch; no webServer dependency) with host-side process lifecycle.",
5
5
  "homepage": "https://github.com/jinsiyu/dsh-code-server-app",
6
6
  "repository": {
7
7
  "type": "git",
@@ -23,11 +23,12 @@
23
23
  "inject": [
24
24
  "@deepseek-ai/dsh-client-connection",
25
25
  "@deepseek-ai/dsh-client-runtime",
26
- "@deepseek-ai/dsh-client-ui-settings"
26
+ "@deepseek-ai/dsh-client-ui-settings",
27
+ "@deepseek-ai/dsh-client-ui-sidebar-right"
27
28
  ]
28
29
  },
29
30
  "env": {
30
- "minVersion": "0.1.2-rc.1"
31
+ "minVersion": "0.1.5-alpha.1"
31
32
  }
32
33
  },
33
34
  "engines": {