dsh-mobile 0.1.0-alpha.31 → 0.1.0-alpha.33

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.en.md CHANGED
@@ -19,15 +19,19 @@
19
19
 
20
20
  <p align="center"><a href="https://github.com/saya-ch/dsh-mobile/releases"><strong>Download the Android app</strong></a></p>
21
21
 
22
- DSH Mobile is a DeepSeek Harness plugin that lets a mobile browser or the Android app connect over a protected LAN and keep using the same sessions, Workspaces, messages, and tools. It is a mobile entry point only; the DeepSeek Harness source is not modified.
22
+ DSH Mobile is a DeepSeek Harness plugin that lets a mobile browser or the Android app connect over a protected LAN and keep using the same sessions, Workspaces, messages, and tools. It is a mobile entry point only; the DeepSeek Harness source is not modified and no public-Internet tunneling is needed.
23
+
24
+ It also lets you customize the phone from a DSH conversation: `/mobile <what you want>`.
23
25
 
24
26
  ## What it does
25
27
 
26
- - Continue DSH work from a phone: sessions, tools, settings, and live state stay in sync.
27
- - Edit the mobile layout, interactions, and features directly through a DeepSeek Harness conversation; open phone pages usually refresh within a few seconds — customize your mobile client by talking to DSH, not by writing code.
28
- - A dedicated touch layout: session drawer, tool details, settings, and composer are reorganized for touch.
29
- - Auto-discovery on the LAN; Wi-Fi, hotspot, or IP changes normally recover without re-pairing.
30
- - Pair by scanning a QR code, pasting a pairing link, or entering a key — no 43-character key to type.
28
+ - **Continue DSH work from a phone**: the same sessions, Workspaces, messages, and tools, in real time.
29
+ - **Customize the phone UI by talking to DSH**: change the mobile layout, interactions, and features from a conversation; open pages refresh within seconds.
30
+ - **A dedicated touch layout**: session drawer, tool details, settings, and composer reorganized for phones.
31
+ - **Auto-discovery, no re-pairing**: Wi-Fi, hotspot, or IP changes normally recover automatically.
32
+ - **Three pairing options**: scan a QR code, paste a pairing link, or enter a key.
33
+
34
+ A paired device is fully trusted and can operate the DSH on the computer. Use this only on a trusted home or office LAN, or a trusted VPN.
31
35
 
32
36
  ## Quick start
33
37
 
@@ -48,17 +52,25 @@ pnpm dsh plugin --profile web exec dsh-mobile setup
48
52
  pnpm dsh --profile web
49
53
  ```
50
54
 
51
- `setup` automatically selects and remembers the current LAN. Wi-Fi, hotspot, and IP changes normally recover automatically; use `--address 192.168.x.x` only when automatic selection fails.
52
-
53
- After starting DSH, open **Mobile Access** in the lower-left sidebar, enable it, then:
55
+ After starting DSH, open **Mobile Access** in the lower-left sidebar, then:
54
56
 
55
57
  1. Select **Create and copy key** or **Copy pairing link**; the panel shows a pairing QR code.
56
58
  2. In the Android app, tap **Scan QR code** and point the camera at the screen — or tap **Scan**, select the computer, and paste the key or pairing link.
57
59
  3. Pairing establishes persistent device trust; later launches do not ask again.
58
60
 
59
- A paired device is fully trusted and can operate the DSH on the computer. Use this only on a trusted home or office LAN, or a trusted VPN.
61
+ `setup` automatically selects and remembers the current LAN; Wi-Fi, hotspot, and IP changes normally recover without re-pairing. Use `--address 192.168.x.x` only when automatic selection fails. Settings, certificates, devices, and customization files live under `$DSH_HOME/mobile-access/`.
62
+
63
+ ## Extend and customize
60
64
 
61
- The plugin does not modify the DeepSeek Harness source. Settings, certificates, devices, and customization files live under `$DSH_HOME/mobile-access/`.
65
+ Type `/mobile <what you want>` in a DSH conversation, and DSH edits the phone client's files for you; changes apply within a few seconds. For example:
66
+
67
+ ```text
68
+ /mobile turn the phone UI into an old CRT terminal, with messages scrolling like terminal output
69
+ ```
70
+
71
+ Two kinds of changes are supported: the phone UI itself (theme, layout, buttons), and computer capabilities the phone can use (browsing computer files, running programs on the computer). `/mobile` hands the request to the DSH agent, which edits files under the local DSH configuration directory (`$DSH_HOME/mobile-access/`); the phone client applies them automatically. UI changes live in `mobile.css`/`mobile.js`. Computer capabilities come from extensions under `extensions/`, whose `host.mjs` runs with the local user's privileges on the computer. DeepSeek Harness source is not modified.
72
+
73
+ > When using computer-side capabilities, use only content you trust.
62
74
 
63
75
  ## App or mobile browser
64
76
 
@@ -67,42 +79,8 @@ The plugin does not modify the DeepSeek Harness source. Settings, certificates,
67
79
  | Android app | Everyday use | Auto-discovery; private certificate pinning inside the app, no manual browser trust step |
68
80
  | Mobile browser | Temporary or cross-platform | Open the HTTPS origin shown by Mobile Access; trust the certificate manually on first visit |
69
81
 
70
- Discovery uses mDNS/NSD, UDP announcements and queries, plus HTTPS probing. It publishes only the device name, address, port, protocol version, and stable `instanceId` — never keys or device tokens. IP changes do not require another pairing.
71
-
72
- For a browser's first connection, open pairing on the computer, then select **Copy pairing link** and open that link on the phone — the pairing code is prefilled. Alternatively, visit `/mobile-access/pair` on the shown HTTPS origin and enter the 43-character pairing code after the key's final dot. The browser stores a revocable device credential after pairing.
73
-
74
- ## Mobile UI
75
-
76
- Phones use a dedicated layout shell that no longer depends on the desktop three-column DOM, while native components keep a small touch-adaptation layer:
77
-
78
- - A workspace-and-session drawer opens from the top-left.
79
- - Conversations, traces, tool details, and Session logs keep their full capabilities.
80
- - Settings use top-level tabs and a single column.
81
- - The composer keeps command, permission, model, context, image, and send controls.
82
- - **Add Workspace** browses computer folders inside the phone page instead of opening a system picker on the computer.
83
-
84
82
  The Android app is a thin Kotlin WebView shell and contains no frontend copy; mobile browsers load the same page. For compatibility diagnosis, append `?frontend=stock` to the browser URL to temporarily use the previous desktop-page adaptation.
85
83
 
86
- ## Customize from DeepSeek Harness
87
-
88
- Default files:
89
-
90
- ```text
91
- $DSH_HOME/mobile-access/mobile.css
92
- $DSH_HOME/mobile-access/mobile.js
93
- ```
94
-
95
- Ask DeepSeek Harness to edit them, for example:
96
-
97
- ```text
98
- Edit $DSH_HOME/mobile-access/mobile.css and mobile.js to turn the mobile
99
- client into a one-handed development console: add a bottom shortcut bar,
100
- a session status panel, and a press-and-hold voice entry. Narrow screens only;
101
- do not modify the DSH source.
102
- ```
103
-
104
- Changes are applied to open Android and browser pages within a few seconds. Customization is not limited to colors: `mobile.js` can add navigation, shortcuts, dashboards, camera, voice, scanning, and complete interactions with same-origin DeepSeek Harness APIs.
105
-
106
84
  ## How it works
107
85
 
108
86
  ```mermaid
@@ -111,7 +89,7 @@ flowchart LR
111
89
  Gateway -->|"loopback proxy"| DSH["Stock DSH Web and Host"]
112
90
  ```
113
91
 
114
- The Host face owns discovery, pairing, HTTPS, and the proxy. The Client face replaces only the phone layout entry while reusing native feature plugins. Neither the DeepSeek Harness source nor its desktop page on port 3080 is modified.
92
+ Three layers: the Host face for discovery, pairing, HTTPS, loopback proxying, and extension registration; the Client face for the dedicated mobile layout and extension SDK; and the Android app for a narrow native bridge. Neither the DeepSeek Harness source nor its desktop page on port 3080 is modified.
115
93
 
116
94
  ## Security
117
95
 
@@ -125,9 +103,9 @@ See [SECURITY.md](SECURITY.md).
125
103
 
126
104
  | DSH Mobile | Verified DeepSeek Harness releases |
127
105
  | --- | --- |
128
- | `0.1.0-alpha.31` | `0.1.0-rc.5`, `0.1.0-rc.6`, `0.1.0-rc.7` |
106
+ | `0.1.0-alpha.33` | `0.1.0-rc.5`, `0.1.0-rc.6`, `0.1.0-rc.7` |
129
107
 
130
- At startup, the plugin verifies the DSH Host version and the frontend dependencies required by the mobile layout. An unverified release fails with a clear error instead of serving a broken page. CI also tracks the DSH main branch layout slots and mobile semantic markers. If a DSH upgrade reports an incompatibility, update DSH Mobile first.
108
+ At startup, the plugin verifies the DSH Host version and the frontend dependencies required by the mobile layout; an unverified release fails with a clear error instead of serving a broken page. CI also tracks the DSH main branch layout contract. If a DSH upgrade reports an incompatibility, update DSH Mobile first.
131
109
 
132
110
  ## Uninstall
133
111
 
package/README.md CHANGED
@@ -14,9 +14,9 @@
14
14
  </p>
15
15
 
16
16
  <p align="center">
17
+ <a href="#能做什么">能做什么</a> ·
17
18
  <a href="#快速开始">快速开始</a> ·
18
- <a href="#app-与手机浏览器">App 与浏览器</a> ·
19
- <a href="#在-dsh-对话中自定义">自定义</a> ·
19
+ <a href="#扩展与自定义">扩展与自定义</a> ·
20
20
  <a href="README.en.md">English</a>
21
21
  </p>
22
22
 
@@ -26,13 +26,17 @@
26
26
 
27
27
  DSH Mobile 是一个 DeepSeek Harness 插件,让手机浏览器或 Android App 在受保护的局域网内连接电脑,继续使用同一份会话、工作区、消息和工具。它只作为移动入口接入,不修改 DeepSeek Harness 源码。
28
28
 
29
+ 它还能在 DSH 对话里用 `/mobile <需求>` 定制手机端。
30
+
29
31
  ## 能做什么
30
32
 
31
- - 在手机上继续电脑端的 DSH 工作:会话、工具、设置和运行状态实时同步。
32
- - 在 DSH 对话中直接修改手机端布局、交互和功能,打开的手机页面通常会在几秒内刷新——用对话定制自己的移动端。
33
- - 专属移动布局:会话抽屉、工具详情、设置和输入栏都按触屏重新组织。
34
- - 自动发现局域网内的 DSH;切换 Wi-Fi、热点或 IP 后通常自动恢复,无需重新配对。
35
- - 扫码、配对链接或密钥均可配对。
33
+ - **在手机上继续电脑端的工作**:同一份会话、工作区、消息和工具,实时同步。
34
+ - **用对话定制手机端**:直接在 DSH 对话里改手机页面的布局、交互和功能,几秒内刷新。
35
+ - **专属触屏布局**:会话抽屉、工具详情、设置和输入栏都按手机重新组织。
36
+ - **自动发现、无需重新配对**:切换 Wi-Fi、热点或 IP 后通常自动恢复。
37
+ - **三种配对方式**:扫码、配对链接、密钥。
38
+
39
+ 配对设备被视为完全信任,可以操作电脑上的 DSH;建议只在可信的家庭、办公局域网或可信 VPN 中使用。
36
40
 
37
41
  ## 快速开始
38
42
 
@@ -53,17 +57,23 @@ pnpm dsh plugin --profile web exec dsh-mobile setup
53
57
  pnpm dsh --profile web
54
58
  ```
55
59
 
56
- `setup` 会自动选择并记住当前局域网。切换 Wi-Fi、热点或 IP 后通常会自动恢复;仅在自动选择失败时使用 `--address 192.168.x.x`。
57
-
58
- 启动后,在 DeepSeek Harness 左下角打开“移动访问”并确认已开启,然后:
60
+ 启动后,在 DeepSeek Harness 左下角打开“移动访问”,然后:
59
61
 
60
62
  1. 点击“生成并复制密钥”或“复制配对链接”,面板会显示配对二维码。
61
- 2. Android App 点击“扫码配对”,把手机对准电脑屏幕上的二维码即可完成配对;也可以点击“扫描”选择电脑后粘贴密钥或配对链接。
63
+ 2. Android App 点击“扫码配对”,把手机对准电脑屏幕上的二维码即可;也可以点击“扫描”选择电脑后粘贴密钥或配对链接。
62
64
  3. 配对完成后会建立持久设备信任,以后打开 App 无需重复输入。
63
65
 
64
- 配对设备被视为完全信任,可以操作电脑上的 DSH;建议只在可信的家庭、办公局域网或可信 VPN 中使用。
66
+ `setup` 会自动选择并记住当前局域网,切换 Wi-Fi、热点或 IP 后通常自动恢复;仅在自动选择失败时使用 `--address 192.168.x.x`。设置、证书、设备和自定义文件保存在 `$DSH_HOME/mobile-access/`。
67
+
68
+ ## 扩展与自定义
65
69
 
66
- 插件不会修改 DeepSeek Harness 源码。设置、证书、设备和自定义文件保存在 `$DSH_HOME/mobile-access/`。
70
+ 在 DSH 对话里输入 `/mobile <需求>`,DSH 会直接修改手机端的文件,几秒内生效。例如:
71
+
72
+ ```text
73
+ /mobile 把手机端做成老式终端的样子,让消息像终端输出一样逐行滚动
74
+ ```
75
+
76
+ `/mobile` 把需求交给 DSH 对话中的 agent,由它直接修改本机 `$DSH_HOME/mobile-access/` 下的文件,保存后手机端自动生效。改动分两类:界面和交互在 `mobile.css`/`mobile.js`;需要电脑能力时用 `extensions/` 下的扩展,其 `host.mjs` 以本机用户权限在电脑上运行。不修改 DeepSeek Harness 源码。
67
77
 
68
78
  ## App 与手机浏览器
69
79
 
@@ -73,40 +83,7 @@ pnpm dsh --profile web
73
83
  | Android App | 日常使用 | 自动发现;App 内保存私有证书信任,无需在浏览器手动信任证书 |
74
84
  | 手机浏览器 | 临时或跨平台访问 | 打开“移动访问”卡片显示的 HTTPS 地址;首次连接需在浏览器手动信任该证书 |
75
85
 
76
- Android 使用 mDNS/NSD、UDP 公告与主动查询、HTTPS 探测等方式发现 DeepSeek Harness。发现广播设备名、地址、端口、协议版本和稳定 `instanceId`,不会广播密钥或令牌。IP 变化后不需要重新配对。
77
-
78
- 手机浏览器首次连接:在电脑端开启配对后,点击“复制配对链接”并把链接发到手机浏览器打开,配对码会自动填入;也可以打开 HTTPS 地址中的 `/mobile-access/pair`,手动输入密钥最后一段的 43 位配对码。配对完成后,浏览器会保存可撤销的设备凭据。
79
-
80
- ## 移动端界面
81
-
82
- 手机端使用独立的移动布局外壳,不再依赖桌面三栏 DOM;原生的对话、工具、设置等组件保留触屏适配:
83
-
84
- - 左上角打开工作区与会话抽屉。
85
- - 对话、轨迹、工具详情和 Session log 保持原有能力。
86
- - 设置页使用顶部分类和单列内容。
87
- - 输入栏保留命令、权限、模型、上下文、图片和发送控件。
88
- - “添加工作区”在手机上展示电脑目录,不会在电脑上弹系统选择器。
89
-
90
- Android App 只是 Kotlin WebView 薄壳,不内置另一份网页;手机浏览器访问的也是同一页面。需要排查兼容性时,可在浏览器地址后追加 `?frontend=stock`,临时回到旧的桌面页面适配模式。
91
-
92
- ## 在 DSH 对话中自定义
93
-
94
- 默认文件:
95
-
96
- ```text
97
- $DSH_HOME/mobile-access/mobile.css
98
- $DSH_HOME/mobile-access/mobile.js
99
- ```
100
-
101
- 直接在 DeepSeek Harness 对话中提出修改即可,例如:
102
-
103
- ```text
104
- 请编辑 $DSH_HOME/mobile-access/mobile.css 和 mobile.js,
105
- 把移动端改成适合单手操作的开发控制台:增加底部快捷指令、
106
- 会话状态面板和长按语音入口。只影响窄屏,不修改 DSH 源码。
107
- ```
108
-
109
- 保存后,已打开的 App 和浏览器通常会在几秒内应用变化。自定义不限于配色:`mobile.js` 可以添加导航、快捷操作、状态面板、相机、语音、扫码,以及调用同源 DSH API 的完整交互。
86
+ Android App 只是 Kotlin WebView 薄壳,不内置另一份网页;手机浏览器访问的是同一页面。需要排查兼容性时,可在浏览器地址后追加 `?frontend=stock`,临时回到旧的桌面页面适配模式。
110
87
 
111
88
  ## 工作原理
112
89
 
@@ -117,7 +94,7 @@ flowchart LR
117
94
  DSH -->|"同一工作区、会话和事件流"| Phone
118
95
  ```
119
96
 
120
- 插件包含两部分:Host face 提供发现、配对、HTTPS 和回环代理;Client face 替换移动端的布局入口,并复用原生功能插件。DeepSeek Harness 的源码和 3080 桌面页面都不会被修改,安装和卸载完全通过插件机制完成。
97
+ 插件包含三层:Host face 负责发现、配对、HTTPS、回环代理和扩展注册表;Client face 提供独立的移动布局与扩展 SDK;Android App 提供受限的原生 Bridge。DeepSeek Harness 的源码和 3080 桌面页面都不会被修改,安装和卸载完全通过插件机制完成。
121
98
 
122
99
  ## 安全
123
100
 
@@ -132,14 +109,12 @@ flowchart LR
132
109
 
133
110
  | DSH Mobile | 已验证的 DeepSeek Harness |
134
111
  | ------------------ | ------------------------------------------ |
135
- | `0.1.0-alpha.31` | `0.1.0-rc.5`、`0.1.0-rc.6`、`0.1.0-rc.7` |
112
+ | `0.1.0-alpha.33` | `0.1.0-rc.5`、`0.1.0-rc.6`、`0.1.0-rc.7` |
136
113
 
137
- 插件会在启动时检查 DSH Host 版本和移动布局所需的前端依赖;遇到未经验证的版本会直接给出错误,不会带着不兼容页面继续启动。CI 也会持续检查 DSH 主分支的布局插槽和移动端语义标记。升级 DSH 后如遇兼容提示,请先升级 DSH Mobile。
114
+ 插件启动时会检查 DSH Host 版本和移动布局所需的前端依赖,遇到未经验证的版本会直接报错而不是带病启动;CI 也会持续跟踪 DSH 主分支的布局契约。升级 DSH 后如遇兼容提示,请先升级 DSH Mobile。
138
115
 
139
116
  ## 卸载
140
117
 
141
- 保留设备和自定义数据:
142
-
143
118
  ```powershell
144
119
  dsh plugin --profile web remove dsh-mobile
145
120
  ```
package/SECURITY.md CHANGED
@@ -24,6 +24,8 @@ The maintainer will acknowledge a complete report within seven days. Publication
24
24
  - Do not expose the gateway directly to the public Internet.
25
25
  - Treat every paired device as a fully trusted operator. Stock DSH methods reached through the authenticated loopback proxy may read configuration or run tools with the desktop user's authority.
26
26
  - Treat `mobile.js` as application code with the paired page's same-origin authority. Restrict write access to trusted host-side DSH sessions and review generated API calls or browser-permission use.
27
+ - Treat every extension `host.mjs` as a local program with the desktop user's Node.js privileges. It is never sandboxed and is not editable through the mobile gateway; only place code there that you trust.
28
+ - Extension Actions and Routes receive filtered request data, a device identifier, and an abort signal. They cannot set proxy security headers or access the gateway's cookies, device tokens, CSRF tokens, or internal request headers.
27
29
 
28
30
  ## Known alpha limitation
29
31
 
package/lib/cli.js CHANGED
@@ -6,6 +6,8 @@ import { basename, dirname, join, resolve } from "node:path";
6
6
  import { promisify } from "node:util";
7
7
  import { X509Certificate, createPrivateKey, createPublicKey } from "node:crypto";
8
8
  import { generate } from "selfsigned";
9
+ import "@deepseek-ai/cordis";
10
+ import "@deepseek-ai/schemastery";
9
11
  //#region src/managed-setup.ts
10
12
  const execFile$2 = promisify(execFile);
11
13
  const VIRTUAL_INTERFACE_MARKERS = [
@@ -253,6 +255,28 @@ async function refreshManagedServerCertificate(setup, address) {
253
255
  });
254
256
  await Promise.all([atomicWrite(setup.tls.certFile, server.cert), atomicWrite(setup.tls.keyFile, server.private)]);
255
257
  }
258
+ Object.freeze({
259
+ manifest: 65536,
260
+ script: 1048576,
261
+ css: 524288,
262
+ asset: 8388608
263
+ });
264
+ /** A controlled business failure returned by an extension action or route. */
265
+ var MobileExtensionError = class extends Error {
266
+ code;
267
+ status;
268
+ constructor(code, message, status = 400) {
269
+ super(message);
270
+ this.code = code;
271
+ this.status = status;
272
+ this.name = "MobileExtensionError";
273
+ }
274
+ };
275
+ /** Validate a stable extension id. */
276
+ function assertExtensionId(value) {
277
+ if (typeof value !== "string" || !/^[a-z][a-z0-9-]{0,63}$/u.test(value)) throw new MobileExtensionError("invalid_manifest", "extension id is invalid");
278
+ return value;
279
+ }
256
280
  //#endregion
257
281
  //#region src/cli.ts
258
282
  const execFile$1 = promisify(execFile);
@@ -407,6 +431,12 @@ async function setup(args) {
407
431
  ""
408
432
  ].join("\n"), { mode: 384 });
409
433
  }
434
+ const extensions = join(directory, "extensions");
435
+ await mkdir(extensions, {
436
+ recursive: true,
437
+ mode: 448
438
+ });
439
+ await createExtensionScaffold(extensions, "custom", "自定义移动扩展", false);
410
440
  const origin = `https://${network.address}:${String(options.port)}`;
411
441
  await Promise.all([writeFile(join(directory, "setup.json"), `${JSON.stringify({
412
442
  ...managedSetup,
@@ -415,9 +445,69 @@ async function setup(args) {
415
445
  console.log(`DSH Mobile follows ${network.name} and is currently configured for ${origin}`);
416
446
  console.log(`Install this CA certificate on Android once: ${androidCertificate}`);
417
447
  console.log(`Ask DSH to customize the mobile Web UI and features in: ${customCss} and ${customScript}`);
448
+ console.log(`Additional extensions live in: ${extensions}`);
418
449
  console.log("Start DSH with: dsh --profile web");
419
450
  console.log("Then open the Mobile card in the lower-left corner and create a pairing key.");
420
451
  }
452
+ async function createExtensionScaffold(root, id, name, refuseExisting = true) {
453
+ assertExtensionId(id);
454
+ await mkdir(root, {
455
+ recursive: true,
456
+ mode: 448
457
+ });
458
+ const directory = join(root, id);
459
+ try {
460
+ await mkdir(directory, {
461
+ recursive: false,
462
+ mode: 448
463
+ });
464
+ } catch (error) {
465
+ if (error.code === "EEXIST" && !refuseExisting) return;
466
+ if (error.code === "EEXIST") throw new Error(`extension directory already exists: ${id}`);
467
+ throw error;
468
+ }
469
+ const files = {
470
+ "extension.json": `${JSON.stringify({
471
+ schemaVersion: 1,
472
+ id,
473
+ name,
474
+ version: "0.1.0",
475
+ description: "在手机端扩展 DSH"
476
+ }, null, 2)}\n`,
477
+ "host.mjs": `export default async function activate(api) {\n api.action('hello', {\n input: api.schema.object({ name: api.schema.string().max(80) }),\n async run({ signal, deviceId }, input) {\n void signal; void deviceId\n return { message: \`Hello, \${input.name}\` }\n },\n })\n}\n`,
478
+ "mobile.js": `window.dshMobile?.define?.({\n apiVersion: 1,\n id: '${id}',\n activate(api) {\n return api.ui.registerSurface({\n id: '${id}-page', placement: 'page', label: ${JSON.stringify(name)},\n mount(container) {\n container.textContent = ${JSON.stringify(`这是 ${name} 的移动页面。`)}\n return () => container.replaceChildren()\n },\n })\n },\n})\n`,
479
+ "mobile.css": `/* ${name.replaceAll("*/", "* /")} 的移动端样式。保存后通常会在几秒内刷新。 */\n`
480
+ };
481
+ try {
482
+ for (const [file, contents] of Object.entries(files)) await writeFile(join(directory, file), contents, {
483
+ encoding: "utf8",
484
+ flag: "wx",
485
+ mode: 384
486
+ });
487
+ } catch (error) {
488
+ await rm(directory, {
489
+ recursive: true,
490
+ force: true
491
+ });
492
+ throw error;
493
+ }
494
+ console.log(`Created extension: ${directory}`);
495
+ }
496
+ async function extensionCommand(args) {
497
+ const [subcommand, id, ...rest] = args;
498
+ if (subcommand !== "create" || id === void 0) throw new Error("usage: extension create <id> [--name <name>]");
499
+ let name = id;
500
+ for (let index = 0; index < rest.length; index += 1) {
501
+ if (rest[index] === "--name" && rest[index + 1] !== void 0) {
502
+ name = rest[index + 1];
503
+ index += 1;
504
+ continue;
505
+ }
506
+ throw new Error(`unknown extension option: ${rest[index] ?? ""}`);
507
+ }
508
+ if (name.length === 0 || name.length > 120 || /[\u0000-\u001f\u007f]/u.test(name)) throw new Error("--name is invalid");
509
+ await createExtensionScaffold(join(dshHome(), "mobile-access", "extensions"), id, name);
510
+ }
421
511
  async function purge(args) {
422
512
  if (args.length !== 1 || args[0] !== "--yes") throw new Error("purge requires --yes");
423
513
  const home = dshHome();
@@ -431,6 +521,7 @@ async function purge(args) {
431
521
  function help() {
432
522
  console.log([
433
523
  "dsh-mobile setup [--address 192.168.x.x] [--port 3443] [--dsh-port 3080] [--no-firewall]",
524
+ "dsh-mobile extension create <id> [--name <name>]",
434
525
  "dsh-mobile purge --yes",
435
526
  "",
436
527
  "Run through the DSH profile:",
@@ -440,6 +531,7 @@ function help() {
440
531
  async function main() {
441
532
  const [command = "help", ...args] = process.argv.slice(2);
442
533
  if (command === "setup") await setup(args);
534
+ else if (command === "extension") await extensionCommand(args);
443
535
  else if (command === "purge") await purge(args);
444
536
  else if (command === "help" || command === "--help" || command === "-h") help();
445
537
  else throw new Error(`unknown command: ${command}`);