@langify-org/dsh-code-server 0.2.0 → 0.2.2

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/README.ja.md CHANGED
@@ -44,7 +44,7 @@ DSH の Web 版や Desktop 版では **Plugins** ページから追加できま
44
44
  - ブラウザーでは、DSH が自分のオリジンの `/code-server` でエディターを配信します。すべてのリクエストと WebSocket は、先に DSH の認証とオリジンの検査を通ります。そのためエディターには、DSH に届く場所(Tailscale 経由など)からしか届きません。
45
45
  - DSH Desktop は DSH を `dsh-app://app` で表示します。code-server はここでは動きません。サーバーの宛先を自分のページのアドレスから作り、このスキームでは webview に必要な Service Worker も使えないためです。また Desktop は、自分以外のオリジンから Host への WebSocket を遮断します。そこで Desktop では、プラグイン自身が `127.0.0.1:<desktopPort>` でエディターを配信します。DSH とは別の、ふつうの http のオリジンです。待ち受けは、Desktop が初めて Code ページを開いたときに始めます。エディターのアドレスのパスには、プラグインの起動ごとに作るランダムなトークンが入ります。code-server はすべてをページからの相対パスで指すので、どのリクエストにもトークンが付きます。トークンを受け取れるのは、DSH が認証したページだけです。さらに、別のホスト名を名乗るリクエストは拒否し、DNS rebinding を防ぎます。
46
46
  - 起動のたびに、同梱の VS Code 拡張 **DSH Code Bridge**(`langify-org.dsh-code-bridge`)を code-server に入れ、古いものを置き換えます。
47
- - Code ページごとに専用の接続(binding)を作ります。ページは binding をエディターの URL の断片(サーバーには送られない部分)に入れ、そのエディター画面の拡張がそれを読み取って、別の非公開 socket でプラグインに接続します。同じフォルダーを複数のページで開いていても、ファイルはその画面にだけ開きます。
47
+ - Code ページごとに専用の接続(binding)を作ります。ページは binding をエディターの URL の断片(サーバーには送られない部分)に入れ、そのエディター画面の拡張がそれを読み取って、別の非公開 socket でプラグインに接続します。同じフォルダーを複数のページで開いていても、ファイルはその画面にだけ開きます。エディターの画面が再読み込みしたとき(フォルダーを信頼したときなど)は、その画面の拡張が同じ binding につなぎ直します。ページはそれを最大1分待ちます。
48
48
 
49
49
  ## セキュリティ
50
50
 
@@ -52,14 +52,15 @@ code-server とそのターミナル・拡張は、あなたの権限で、DSH
52
52
 
53
53
  ## 手動確認
54
54
 
55
- ブラウザーと実際の code-server が必要なため、`pnpm check` には含めていません。リリース前に `pnpm dev` で確認します。`pnpm dev` では [dev.patch.yml](dev.patch.yml) により、code-server の設定と拡張は `.dsh/code-server/` に置かれ、普段の code-server とは分かれます。Host とページの変更は `pnpm dev` の実行中に反映されますが、`src/extension/` の変更は `pnpm dev` を起動し直すまで反映されません。code-server が拡張を入れるのは起動時だけだからです。
55
+ ブラウザーと実際の code-server が必要なため、`pnpm check` には含めていません。リリース前に `pnpm dev` で確認します。`pnpm dev` では [dev.patch.yml](dev.patch.yml) により、code-server の設定と拡張は `.dsh/code-server/` に置かれ、普段の code-server とは分かれます。Host とページの変更は `pnpm dev` の実行中に反映されますが、`src/extension/` の変更は、プラグインが次に開くエディターの画面から反映されるので、Code タブで**再接続**を押してください。
56
56
 
57
57
  1. **Code** にはエディターだけが表示され、上に何も出ない。エディターのステータスバーの **DSH** が接続済みになる(Restricted Mode でも)。接続が終わった後もエディターは残り、上の行に**再接続**が出る。
58
58
  2. **Workspace files** で選んだテキストファイルが Code ページの該当行で開く。画像は DSH のビューアーのまま開く。
59
59
  3. サインインしていないと `/code-server/` と `POST /api/langify-code-server/create` が 401 を返し、別オリジンからの WebSocket の upgrade は 403 を返す。
60
60
  4. セッションの作業フォルダーが変わると、エディターが開き直すかを尋ねる。未保存のファイルがあれば先に保存を求め、そのあとページが新しいフォルダーでエディターを開き直す。
61
61
  5. DSH Desktop では、エディターが `http://127.0.0.1:41820/…` から読み込まれ、テーマとファイルアイコンが出て、Markdown のプレビューが開く。トークンが無いとポートは 404 を返す。
62
- 6. Code タブを閉じると binding が閉じる。DSH を止めると code-server も止まり、socket が消える。
62
+ 6. エディターで **Developer: Reload Window** を実行したあとも、DSH で選んだファイルが**再接続**なしでその画面に開く。
63
+ 7. Code タブを閉じると binding が閉じる。DSH を止めると code-server も止まり、socket が消える。
63
64
 
64
65
  ## 既知の制約
65
66
 
package/README.md CHANGED
@@ -44,7 +44,7 @@ With the defaults, the editor shares settings and extensions with your own code-
44
44
  - In a browser, DSH serves the editor under `/code-server` on its own origin. Every request and WebSocket passes DSH's authentication and origin check first, so the editor is reachable wherever DSH is, for example over Tailscale, and nowhere else.
45
45
  - DSH Desktop shows DSH at `dsh-app://app`, where code-server cannot run: it builds its server's addresses from its own page's, and that scheme allows no service workers, which webviews need. Desktop also refuses WebSockets to its Host from any other origin. So in Desktop the plugin serves the editor itself, on `127.0.0.1:<desktopPort>`, a plain http origin apart from DSH's. It starts listening when Desktop first opens the Code page. The editor's address carries in its path a random token, made anew whenever the plugin starts; code-server addresses everything relative to its page, so every request carries it. Only pages DSH has authenticated receive the token. The port also refuses requests that name another host, which stops DNS rebinding.
46
46
  - Before each start, the plugin installs its bundled VS Code extension, **DSH Code Bridge** (`langify-org.dsh-code-bridge`), into code-server, replacing any older copy.
47
- - Each Code page gets its own connection ("binding"). The page puts the binding in the editor's URL fragment, which never reaches a server; the extension in that editor window reads it back and connects to the plugin over another private socket. Files open in that window only, even when several pages show the same folder.
47
+ - Each Code page gets its own connection ("binding"). The page puts the binding in the editor's URL fragment, which never reaches a server; the extension in that editor window reads it back and connects to the plugin over another private socket. Files open in that window only, even when several pages show the same folder. When the window reloads, for example after you trust its folder, its extension attaches to the binding again; the page waits up to a minute for that.
48
48
 
49
49
  ## Security
50
50
 
@@ -52,14 +52,15 @@ code-server runs as you, and so do its terminal and extensions, outside DSH's sa
52
52
 
53
53
  ## Manual checks
54
54
 
55
- These need a browser and a real code-server, so they are not part of `pnpm check`. Run them with `pnpm dev` before a release. Under `pnpm dev`, [dev.patch.yml](dev.patch.yml) keeps code-server's settings and extensions in `.dsh/code-server/`, apart from your own code-server. Changes to the host and the page reload while `pnpm dev` runs; a change under `src/extension/` takes effect only after restarting `pnpm dev`, because code-server installs the extension when it starts.
55
+ These need a browser and a real code-server, so they are not part of `pnpm check`. Run them with `pnpm dev` before a release. Under `pnpm dev`, [dev.patch.yml](dev.patch.yml) keeps code-server's settings and extensions in `.dsh/code-server/`, apart from your own code-server. Changes to the host and the page reload while `pnpm dev` runs; a change under `src/extension/` reaches the next editor window the plugin opens, so press **Reconnect** in the Code tab.
56
56
 
57
57
  1. **Code** shows the editor with nothing above it, and the editor's status bar shows **DSH** as connected, also in Restricted Mode. After the connection ends, the editor stays and a line above it offers **Reconnect**.
58
58
  2. A text file chosen in **Workspace files** opens in the Code page at its line; an image keeps DSH's viewer.
59
59
  3. Without signing in, `/code-server/` and `POST /api/langify-code-server/create` answer 401, and a WebSocket upgrade from another origin answers 403.
60
60
  4. After the session's working folder changes, the editor offers to reopen; with an unsaved file it asks to save first, then the page reloads the editor on the new folder.
61
61
  5. In DSH Desktop, the editor loads from `http://127.0.0.1:41820/…` with your theme and file icons, and a Markdown preview opens. The port answers 404 without the token.
62
- 6. Closing the Code tab closes its binding; stopping DSH stops code-server and removes its sockets.
62
+ 6. After **Developer: Reload Window** in the editor, a file chosen in DSH still opens there, without **Reconnect**.
63
+ 7. Closing the Code tab closes its binding; stopping DSH stops code-server and removes its sockets.
63
64
 
64
65
  ## Known limitations
65
66
 
package/README.zh.md CHANGED
@@ -44,7 +44,7 @@ dsh plugin --profile <profile> add @langify-org/dsh-code-server
44
44
  - 在浏览器中,DSH 在自己的源下通过 `/code-server` 提供编辑器。每个请求和 WebSocket 都会先经过 DSH 的认证和来源检查,因此只有能访问 DSH 的地方(例如通过 Tailscale)才能访问编辑器。
45
45
  - DSH Desktop 在 `dsh-app://app` 显示 DSH,code-server 无法在那里运行:它根据自身页面的地址构造服务器地址,而且该协议不支持 webview 所需的 Service Worker。Desktop 还会拒绝来自其他源、发往其 Host 的 WebSocket。因此在 Desktop 中,插件自己在 `127.0.0.1:<desktopPort>` 提供编辑器,这是一个独立于 DSH 的普通 http 源。Desktop 第一次打开 Code 页面时才开始监听。编辑器地址的路径中带有一个随机令牌,插件每次启动时重新生成;code-server 的所有地址都相对于其页面,因此每个请求都会带上它。只有经过 DSH 认证的页面才能拿到该令牌。该端口还会拒绝声明为其他主机名的请求,以防 DNS rebinding。
46
46
  - 每次启动前,插件都会把内置的 VS Code 扩展 **DSH Code Bridge**(`langify-org.dsh-code-bridge`)安装到 code-server 中,替换旧版本。
47
- - 每个 Code 页面都有自己的连接(binding)。页面把 binding 放在编辑器 URL 的片段中(这一部分不会发送到服务器),该编辑器窗口中的扩展读取它,并通过另一个私有套接字连接到插件。即使多个页面打开了同一个文件夹,文件也只会在对应的窗口中打开。
47
+ - 每个 Code 页面都有自己的连接(binding)。页面把 binding 放在编辑器 URL 的片段中(这一部分不会发送到服务器),该编辑器窗口中的扩展读取它,并通过另一个私有套接字连接到插件。即使多个页面打开了同一个文件夹,文件也只会在对应的窗口中打开。编辑器窗口重新加载时(例如在你信任其文件夹之后),该窗口中的扩展会重新连接到同一个 binding;页面最多等待一分钟。
48
48
 
49
49
  ## 安全
50
50
 
@@ -52,14 +52,15 @@ code-server 及其终端和扩展以你的身份运行,不受 DSH 沙箱和审
52
52
 
53
53
  ## 手动检查
54
54
 
55
- 这些检查需要浏览器和真实的 code-server,因此不包含在 `pnpm check` 中。发布前请用 `pnpm dev` 进行检查。在 `pnpm dev` 下,[dev.patch.yml](dev.patch.yml) 会把 code-server 的设置和扩展放在 `.dsh/code-server/` 中,与你平时使用的 code-server 分开。Host 和页面的修改会在 `pnpm dev` 运行期间重新加载;而 `src/extension/` 下的修改要重启 `pnpm dev` 后才会生效,因为 code-server 只在启动时安装扩展。
55
+ 这些检查需要浏览器和真实的 code-server,因此不包含在 `pnpm check` 中。发布前请用 `pnpm dev` 进行检查。在 `pnpm dev` 下,[dev.patch.yml](dev.patch.yml) 会把 code-server 的设置和扩展放在 `.dsh/code-server/` 中,与你平时使用的 code-server 分开。Host 和页面的修改会在 `pnpm dev` 运行期间重新加载;而 `src/extension/` 下的修改会在插件打开的下一个编辑器窗口中生效,因此请在 Code 标签页中点击**重新连接**。
56
56
 
57
57
  1. **Code** 只显示编辑器,上方没有其他内容;编辑器状态栏中的 **DSH** 显示为已连接(受限模式下也是如此)。连接结束后编辑器仍会保留,上方的一行会提供**重新连接**。
58
58
  2. 在 **Workspace files** 中选择的文本文件会在 Code 页面的对应行打开;图片仍使用 DSH 的查看器。
59
59
  3. 未登录时,`/code-server/` 和 `POST /api/langify-code-server/create` 返回 401,来自其他源的 WebSocket upgrade 返回 403。
60
60
  4. 会话的工作文件夹变化后,编辑器会询问是否重新打开;如有未保存的文件,会先要求保存,然后页面在新文件夹中重新打开编辑器。
61
61
  5. 在 DSH Desktop 中,编辑器从 `http://127.0.0.1:41820/…` 加载,显示你的主题和文件图标,Markdown 预览可以打开;没有令牌时该端口返回 404。
62
- 6. 关闭 Code 标签页会关闭其 binding;停止 DSH 会停止 code-server 并删除其套接字。
62
+ 6. 在编辑器中执行 **Developer: Reload Window** 之后,在 DSH 中选择的文件仍会在该窗口打开,无需**重新连接**。
63
+ 7. 关闭 Code 标签页会关闭其 binding;停止 DSH 会停止 code-server 并删除其套接字。
63
64
 
64
65
  ## 已知限制
65
66
 
package/dist/client.cjs CHANGED
@@ -38,7 +38,6 @@ window.__ModuleLoader__.load({
38
38
  "error.desktop_port_in_use": "The editor's port for DSH Desktop is taken: {message}. Choose another one with the plugin's desktopPort setting.",
39
39
  "error.unknown_binding": "This editor connection is gone. Reconnect.",
40
40
  "error.not_ready": "The editor is still starting.",
41
- "error.binding_disconnected": "The editor window disconnected. Reconnect.",
42
41
  "error.binding_expired": "The editor connection expired. Reconnect.",
43
42
  "error.binding_closed": "The editor connection was closed. Reconnect.",
44
43
  "error.internal_error": "The plugin failed: {message}",
@@ -83,7 +82,6 @@ window.__ModuleLoader__.load({
83
82
  "error.desktop_port_in_use": "供 DSH Desktop 使用的编辑器端口已被占用:{message}。请在插件设置 desktopPort 中改用其他端口。",
84
83
  "error.unknown_binding": "与此编辑器的连接已不存在。请重新连接。",
85
84
  "error.not_ready": "编辑器仍在启动中。",
86
- "error.binding_disconnected": "编辑器窗口已断开。请重新连接。",
87
85
  "error.binding_expired": "与编辑器的连接已过期。请重新连接。",
88
86
  "error.binding_closed": "与编辑器的连接已关闭。请重新连接。",
89
87
  "error.internal_error": "插件出错:{message}",
@@ -128,7 +126,6 @@ window.__ModuleLoader__.load({
128
126
  "error.desktop_port_in_use": "DSH Desktop 用のエディターのポートが使われています: {message}。プラグインの設定 desktopPort で別の番号にしてください。",
129
127
  "error.unknown_binding": "このエディターとの接続はもうありません。再接続してください。",
130
128
  "error.not_ready": "エディターはまだ起動中です。",
131
- "error.binding_disconnected": "エディターの画面が切断されました。再接続してください。",
132
129
  "error.binding_expired": "エディターとの接続が期限切れになりました。再接続してください。",
133
130
  "error.binding_closed": "エディターとの接続が閉じられました。再接続してください。",
134
131
  "error.internal_error": "プラグインでエラーが起きました: {message}",
@@ -185,7 +182,6 @@ window.__ModuleLoader__.load({
185
182
  state: zod.z.enum([
186
183
  "pending",
187
184
  "ready",
188
- "disconnected",
189
185
  "expired",
190
186
  "closed"
191
187
  ]),
@@ -201,7 +197,6 @@ window.__ModuleLoader__.load({
201
197
  // src/client/session-bus.ts
202
198
  /** After these the page must reconnect; files are never sent to another window. */
203
199
  const FINAL_PHASES = /* @__PURE__ */ new Set([
204
- "disconnected",
205
200
  "expired",
206
201
  "closed",
207
202
  "error"
@@ -329,7 +324,6 @@ window.__ModuleLoader__.load({
329
324
  /** After these, the binding is gone and the page must reconnect. */
330
325
  const FINAL_ERRORS = /* @__PURE__ */ new Set([
331
326
  "unknown_binding",
332
- "binding_disconnected",
333
327
  "binding_expired",
334
328
  "binding_closed"
335
329
  ]);
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { chmod, mkdtemp, realpath, rm } from "node:fs/promises";
1
+ import { chmod, mkdtemp, realpath, rm, stat } from "node:fs/promises";
2
2
  import { homedir, tmpdir } from "node:os";
3
3
  import { isAbsolute, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -46,7 +46,6 @@ const HOST_ERROR_CODES = [
46
46
  "desktop_port_in_use",
47
47
  "unknown_binding",
48
48
  "not_ready",
49
- "binding_disconnected",
50
49
  "binding_expired",
51
50
  "binding_closed",
52
51
  "internal_error"
@@ -82,7 +81,6 @@ z.strictObject({
82
81
  state: z.enum([
83
82
  "pending",
84
83
  "ready",
85
- "disconnected",
86
84
  "expired",
87
85
  "closed"
88
86
  ]),
@@ -196,7 +194,7 @@ var CodeServerError = class extends Error {
196
194
  }
197
195
  };
198
196
  // src/host/bridge.ts
199
- /** An unclaimed binding expires this long after creation. */
197
+ /** A binding without a window expires this long after it was created or lost its window. */
200
198
  const CONNECT_TIMEOUT_MS = 6e4;
201
199
  /** A binding expires this long after the page last watched it. */
202
200
  const LEASE_MS = 12e4;
@@ -206,7 +204,6 @@ const OPEN_TIMEOUT_MS = 15e3;
206
204
  const WATCH_MS = 2e4;
207
205
  const HELLO_TIMEOUT_MS = 5e3;
208
206
  const finalError = {
209
- disconnected: "binding_disconnected",
210
207
  expired: "binding_expired",
211
208
  closed: "binding_closed"
212
209
  };
@@ -252,6 +249,7 @@ var Bridge = class Bridge {
252
249
  reconnect: false,
253
250
  announcedCwd: cwd,
254
251
  leaseUntil: now + LEASE_MS,
252
+ peer: void 0,
255
253
  opens: /* @__PURE__ */ new Map(),
256
254
  watchers: /* @__PURE__ */ new Set()
257
255
  };
@@ -375,16 +373,29 @@ var Bridge = class Bridge {
375
373
  const deadline = binding.state === "pending" ? Math.min(binding.connectBy, binding.leaseUntil) : binding.leaseUntil;
376
374
  binding.timer = setTimeout(() => this.#finish(binding, "expired"), deadline - Date.now());
377
375
  }
378
- /** Enter a final state once; the record stays for a lease so watches can read it. */
379
- #finish(binding, state) {
380
- if (binding.state !== "pending" && binding.state !== "ready") return;
381
- binding.state = state;
376
+ /** Let go of the window: its unanswered requests have an unknown outcome. */
377
+ #detach(binding, reason) {
382
378
  binding.peer?.destroy();
379
+ binding.peer = void 0;
383
380
  for (const open of binding.opens.values()) {
384
381
  clearTimeout(open.timer);
385
- open.reject(new CodeServerError("outcome_unknown", `The binding was ${state} before the editor answered`));
382
+ open.reject(new CodeServerError("outcome_unknown", `${reason} before the editor answered`));
386
383
  }
387
384
  binding.opens.clear();
385
+ }
386
+ /** Wait for the window again, as for a new binding. */
387
+ #lose(binding) {
388
+ this.#detach(binding, "The editor window went away");
389
+ binding.state = "pending";
390
+ binding.connectBy = Date.now() + CONNECT_TIMEOUT_MS;
391
+ this.#schedule(binding);
392
+ this.#changed(binding);
393
+ }
394
+ /** Enter a final state once; the record stays for a lease so watches can read it. */
395
+ #finish(binding, state) {
396
+ if (binding.state !== "pending" && binding.state !== "ready") return;
397
+ binding.state = state;
398
+ this.#detach(binding, `The binding was ${state}`);
388
399
  clearTimeout(binding.timer);
389
400
  binding.timer = setTimeout(() => this.#bindings.delete(binding.id), LEASE_MS);
390
401
  this.#changed(binding);
@@ -406,7 +417,7 @@ var Bridge = class Bridge {
406
417
  socket.on("close", () => {
407
418
  this.#sockets.delete(socket);
408
419
  clearTimeout(helloTimer);
409
- if (binding?.peer === socket) this.#finish(binding, "disconnected");
420
+ if (binding?.peer === socket) this.#lose(binding);
410
421
  });
411
422
  readFrames(socket, (value) => {
412
423
  if (refused) return;
@@ -415,8 +426,9 @@ var Bridge = class Bridge {
415
426
  const hello = helloSchema.safeParse(value);
416
427
  if (!hello.success) return reject("invalid_hello", "Expected a hello frame");
417
428
  const candidate = this.#bindings.get(hello.data.bindingId);
418
- if (candidate?.state !== "pending") return reject("binding_unavailable", "The binding cannot be claimed");
429
+ if (candidate?.state !== "pending" && candidate?.state !== "ready") return reject("binding_unavailable", "The binding has ended");
419
430
  if (candidate.cwd !== hello.data.cwd) return reject("cwd_mismatch", "The window opened another folder");
431
+ this.#detach(candidate, "Another window took over");
420
432
  binding = candidate;
421
433
  binding.peer = socket;
422
434
  binding.state = "ready";
@@ -460,6 +472,11 @@ var CodeServer = class {
460
472
  #starting;
461
473
  #child;
462
474
  #disposed = false;
475
+ /** The CLI with its directories, once resolved, to install the extension with. */
476
+ #cli;
477
+ /** The installed VSIX's modification time and size. */
478
+ #installed = "";
479
+ #installing = Promise.resolve();
463
480
  /**
464
481
  * @param extension - VSIX installed before every start.
465
482
  * @param onStarted - called once per process; its disposer runs when the process exits.
@@ -479,6 +496,18 @@ var CodeServer = class {
479
496
  });
480
497
  return this.#starting;
481
498
  }
499
+ /**
500
+ * Install the bundled extension again if its VSIX changed since it was
501
+ * installed, so that windows opened from now on run it. Only a linked
502
+ * development copy of the plugin changes while it runs.
503
+ */
504
+ syncExtension() {
505
+ const installing = this.#installing.then(async () => {
506
+ if (this.#cli !== void 0) await this.#installExtension(this.#cli);
507
+ });
508
+ this.#installing = installing.catch(() => {});
509
+ return installing;
510
+ }
482
511
  async dispose() {
483
512
  this.#disposed = true;
484
513
  const child = this.#child;
@@ -495,16 +524,12 @@ var CodeServer = class {
495
524
  throw new CodeServerError("code_server_not_found", `Cannot find ${this.#options.command}`, { cause: error });
496
525
  }
497
526
  const directories = [...this.#options.userDataDir === void 0 ? [] : ["--user-data-dir", this.#options.userDataDir], ...this.#options.extensionsDir === void 0 ? [] : ["--extensions-dir", this.#options.extensionsDir]];
527
+ this.#cli = [command, ...directories];
498
528
  const version = await this.#run([command, "--version"]);
499
529
  const commit = /^\S+ ([0-9a-f]{40}) /.exec(version)?.[1];
500
530
  if (commit === void 0) throw new CodeServerError("code_server_failed", `Unrecognized code-server version: ${version}`);
501
- await this.#run([
502
- command,
503
- ...directories,
504
- "--install-extension",
505
- this.#extension,
506
- "--force"
507
- ]);
531
+ this.#installed = "";
532
+ await this.syncExtension();
508
533
  await rm(this.#socketPath, { force: true });
509
534
  if (this.#disposed) throw new CodeServerError("code_server_failed", "The plugin has stopped");
510
535
  const child = this.#subprocess.spawn({
@@ -546,6 +571,18 @@ var CodeServer = class {
546
571
  await waitUntilListening(this.#socketPath, child);
547
572
  return running;
548
573
  }
574
+ async #installExtension(cli) {
575
+ const { mtimeMs, size } = await stat(this.#extension);
576
+ const stamp = `${mtimeMs}:${size}`;
577
+ if (stamp === this.#installed) return;
578
+ await this.#run([
579
+ ...cli,
580
+ "--install-extension",
581
+ this.#extension,
582
+ "--force"
583
+ ]);
584
+ this.#installed = stamp;
585
+ }
549
586
  /** Run a short code-server CLI command and return its stdout. */
550
587
  async #run(argv) {
551
588
  const child = this.#subprocess.spawn({
@@ -603,6 +640,7 @@ function rpcRoutes(services, bridge, codeServer, editors) {
603
640
  const { cwd } = await sessionFolders(services, sessionId, signal);
604
641
  const folder = await localPath(services, cwd, cwd, "directory", signal);
605
642
  await codeServer.ensure();
643
+ await codeServer.syncExtension();
606
644
  const base = await editors[ingress]();
607
645
  signal.throwIfAborted();
608
646
  const bindingId = bridge.create(sessionId, ownerId, folder);
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langify-org/dsh-code-server",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Edit the session's working folder in code-server inside DeepSeek Harness, served through DSH's authenticated web server",
5
5
  "keywords": [
6
6
  "code-server",
@@ -83,7 +83,7 @@
83
83
  }
84
84
  },
85
85
  "scripts": {
86
- "build": "tsdown && cd extension && vsce package --no-dependencies --skip-license --out dsh-code-bridge.vsix",
86
+ "build": "tsdown",
87
87
  "typecheck": "tsc -p src/client"
88
88
  }
89
89
  }