dsh-crossdesk 0.0.0-stage → 0.1.0

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.md CHANGED
@@ -1,3 +1,23 @@
1
- # Temporary Holding Version
1
+ # dsh-crossdesk
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 把 **DSH 的一个会话**共享到另一台设备,通过你自己部署的 relay 转发。两端的连接都是出站的,所以不需要 VPN、端口转发或同一局域网;执行始终留在会话原来那台机器上。
4
+
5
+ 完整说明见[根目录 README](../../README.md)。这里只放最短路径。
6
+
7
+ ```bash
8
+ npm install -g dsh-crossdesk
9
+ dsh-crossdesk enable # 改 profile、装依赖、写 patch(可重复执行)
10
+ dsh-crossdesk status # 看当前状态
11
+ dsh-crossdesk doctor --relay https://relay.example.com # 连通性诊断
12
+ dsh-crossdesk disable --remove-deps # 彻底还原 profile
13
+ ```
14
+
15
+ `enable` 只做三件事:往 profile 的 `package.json` 加依赖、装依赖、往 `cordis.patch.yml` 插一段带 `# >>> dsh-crossdesk` / `# <<< dsh-crossdesk` 标记的配置。它不修改 DSH 自身、不碰全局配置,`disable` 按标记整体移除。
16
+
17
+ 改完需要**重启 DSH**。之后在会话的分享入口填一次 relay 地址并完成浏览器登录授权(PKCE),凭据存在本机 `$DSH_HOME`,之后不用重复。
18
+
19
+ 要求:DSH `>= 0.2.2-rc.1 < 0.3`,Node `>= 22`(DSH 桌面端自带的 runtime 即可)。
20
+
21
+ ## License
22
+
23
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `dsh-crossdesk` — one command that turns a DSH profile into a publishable host, and checks the
4
+ * relay it will point at.
5
+ *
6
+ * It never patches DSH itself: it edits the profile's own dependency block and user patch file,
7
+ * which is the documented way to mount the shipped Liveshare owner bundle.
8
+ */
9
+ import { existsSync } from 'node:fs'
10
+ import { join } from 'node:path'
11
+ import {
12
+ DEFAULT_LIVESHARE_VERSION,
13
+ LIVESHARE_PACKAGE,
14
+ addDependencies,
15
+ dshHome,
16
+ enablePatch,
17
+ inspectProfile,
18
+ installProfileDependencies,
19
+ machineSubject,
20
+ profileDir,
21
+ readProfileManifest,
22
+ readProfilePatch,
23
+ stripManagedBlock,
24
+ writeProfileManifest,
25
+ writeProfilePatch,
26
+ } from '../src/profile.mjs'
27
+
28
+ const HELP = `dsh-crossdesk — share one DeepSeek Harness session with another device
29
+
30
+ Usage
31
+ dsh-crossdesk enable [--profile desktop] [--relay <url>] [--no-install] [--dry-run]
32
+ dsh-crossdesk disable [--profile desktop] [--remove-deps]
33
+ dsh-crossdesk status [--profile desktop]
34
+ dsh-crossdesk doctor [--profile desktop] [--relay <url>]
35
+ dsh-crossdesk relay [relay flags...] (runs the bundled relay)
36
+
37
+ What enable does
38
+ 1. adds ${LIVESHARE_PACKAGE}@${DEFAULT_LIVESHARE_VERSION} to the profile's dependencies
39
+ 2. installs it in the profile with pnpm (or npm)
40
+ 3. inserts the bundle's two loader entries into the profile's cordis.patch.yml
41
+
42
+ Then restart DeepSeek Harness, open a session's share action, and enter your relay URL once.
43
+ `
44
+
45
+ const args = process.argv.slice(2)
46
+ const command = args[0] ?? 'help'
47
+ const flags = parseFlags(args.slice(1))
48
+
49
+ main().catch((error) => {
50
+ console.error(`dsh-crossdesk: ${error instanceof Error ? error.message : String(error)}`)
51
+ process.exitCode = 1
52
+ })
53
+
54
+ async function main() {
55
+ switch (command) {
56
+ case 'enable':
57
+ await enable()
58
+ return
59
+ case 'disable':
60
+ await disable()
61
+ return
62
+ case 'status':
63
+ await status()
64
+ return
65
+ case 'doctor':
66
+ await doctor()
67
+ return
68
+ case 'relay': {
69
+ process.argv = [process.argv[0], 'dsh-crossdesk-relay', ...args.slice(1)]
70
+ // Installed layout: the relay is a dependency. Repo layout: it sits next door in the workspace.
71
+ try {
72
+ await import('dsh-crossdesk-relay/bin/relay.mjs')
73
+ } catch (error) {
74
+ if (error?.code !== 'ERR_MODULE_NOT_FOUND' && error?.code !== 'MODULE_NOT_FOUND') throw error
75
+ await import('../../relay/bin/relay.mjs')
76
+ }
77
+ return
78
+ }
79
+ case 'help':
80
+ case '--help':
81
+ case '-h':
82
+ process.stdout.write(HELP)
83
+ return
84
+ case 'version':
85
+ case '--version':
86
+ case '-V': {
87
+ const { readFile } = await import('node:fs/promises')
88
+ const raw = await readFile(new URL('../package.json', import.meta.url), 'utf8')
89
+ process.stdout.write(`${JSON.parse(raw).version}\n`)
90
+ return
91
+ }
92
+ default:
93
+ process.stderr.write(`unknown command: ${command}\n\n${HELP}`)
94
+ process.exitCode = 1
95
+ }
96
+ }
97
+
98
+ async function enable() {
99
+ const profile = flags.profile ?? 'desktop'
100
+ const dir = profileDir(profile)
101
+ if (!existsSync(dir)) {
102
+ throw new Error(`profile ${profile} not found at ${dir}; start DeepSeek Harness once so it creates the profile, or pass --profile`)
103
+ }
104
+
105
+ const { manifest } = await readProfileManifest(profile)
106
+ const next = addDependencies(manifest, flags['liveshare-version'] ?? DEFAULT_LIVESHARE_VERSION)
107
+ const depsChanged = JSON.stringify(next.dependencies) !== JSON.stringify(manifest.dependencies)
108
+ if (depsChanged) {
109
+ if (flags['dry-run'] !== true) await writeProfileManifest(profile, next)
110
+ console.log(`dependencies ${depsChanged ? 'updated' : 'already declared'} in ${profile}/package.json`)
111
+ }
112
+
113
+ if (flags['no-install'] !== true) {
114
+ const outcome = await installProfileDependencies(profile, { dryRun: flags['dry-run'] === true })
115
+ console.log(`install ${outcome.ran ? outcome.command : `would run ${outcome.command}`}`)
116
+ } else {
117
+ console.log('install skipped (--no-install)')
118
+ }
119
+
120
+ const { file, text } = await readProfilePatch(profile)
121
+ const patched = enablePatch(text, { subject: flags.subject ?? machineSubject() })
122
+ if (patched.changed && flags['dry-run'] !== true) await writeProfilePatch(profile, patched.after)
123
+ console.log(`patch ${patched.changed ? 'inserted managed block' : 'already present'} in ${file}`)
124
+
125
+ const relay = flags.relay ?? '<your relay URL>'
126
+ console.log(
127
+ [
128
+ '',
129
+ 'Next:',
130
+ ` 1. restart DeepSeek Harness (profile: ${profile})`,
131
+ ` 2. open a session → share action → relay URL: ${relay}`,
132
+ ' 3. sign in once in the popup; the host stores the relay credential',
133
+ ' 4. on the other device, open the share link and sign in to the same relay',
134
+ '',
135
+ 'Docs: https://github.com/vector985/dsh-crossdesk#readme',
136
+ ].join('\n'),
137
+ )
138
+ }
139
+
140
+ async function disable() {
141
+ const profile = flags.profile ?? 'desktop'
142
+ const { file, text } = await readProfilePatch(profile)
143
+ const stripped = stripManagedBlock(text)
144
+ await writeProfilePatch(profile, stripped)
145
+ console.log(`patch managed block removed from ${file}`)
146
+ if (flags['remove-deps'] === true) {
147
+ const { manifest } = await readProfileManifest(profile)
148
+ if (manifest.dependencies !== undefined) {
149
+ delete manifest.dependencies[LIVESHARE_PACKAGE]
150
+ delete manifest.dependencies['@deepseek-ai/dsh-experimental-liveshare-protocol']
151
+ await writeProfileManifest(profile, manifest)
152
+ console.log('dependencies removed')
153
+ }
154
+ }
155
+ console.log('\nRestart DeepSeek Harness to drop the share surface.')
156
+ }
157
+
158
+ async function status() {
159
+ const profile = flags.profile ?? 'desktop'
160
+ const state = await inspectProfile(profile)
161
+ const row = (label, value) => ` ${label.padEnd(14)} ${value}`
162
+ console.log(
163
+ [
164
+ `profile ${profile}`,
165
+ row('home', state.home),
166
+ row('directory', state.dir),
167
+ row('declared', state.declared ? 'yes' : 'no'),
168
+ row('installed', state.installed ? 'yes' : 'no'),
169
+ row('patch entry', state.patchHasEntry ? 'yes' : 'no'),
170
+ row('managed block', state.patchHasBlock ? 'yes' : 'no'),
171
+ '',
172
+ state.declared && state.installed && state.patchHasEntry
173
+ ? 'Ready: restart DeepSeek Harness, then use the session share action.'
174
+ : `Not ready: run dsh-crossdesk enable --profile ${profile}`,
175
+ ].join('\n'),
176
+ )
177
+ }
178
+
179
+ async function doctor() {
180
+ const profile = flags.profile ?? 'desktop'
181
+ const state = await inspectProfile(profile)
182
+ const checks = []
183
+ checks.push(['node >= 22', Number(process.versions.node.split('.')[0]) >= 22, process.versions.node])
184
+ checks.push(['profile exists', existsSync(state.dir), state.dir])
185
+ checks.push(['bundle declared', state.declared, `${LIVESHARE_PACKAGE}`])
186
+ checks.push(['bundle installed', state.installed, state.installed ? 'node_modules present' : 'run enable'])
187
+ checks.push(['patch entry', state.patchHasEntry, state.patchFile])
188
+
189
+ const relay = flags.relay ?? process.env.CROSSDESK_RELAY
190
+ if (relay !== undefined && relay !== '') {
191
+ checks.push(...(await relayChecks(relay)))
192
+ } else {
193
+ checks.push(['relay url', false, 'pass --relay <url> or set CROSSDESK_RELAY'])
194
+ }
195
+
196
+ const width = Math.max(...checks.map(([name]) => name.length))
197
+ for (const [name, ok, detail] of checks) {
198
+ console.log(` ${ok ? 'ok ' : 'FAIL'} ${name.padEnd(width)} ${detail}`)
199
+ }
200
+ const failed = checks.filter(([, ok]) => !ok).length
201
+ console.log(failed === 0 ? '\nAll checks passed.' : `\n${failed} check(s) need attention.`)
202
+ if (failed > 0) process.exitCode = 1
203
+ }
204
+
205
+ async function relayChecks(relay) {
206
+ const base = relay.replace(/\/+$/, '')
207
+ const out = []
208
+ try {
209
+ const health = await fetch(`${base}/__liveshare/health`, { signal: AbortSignal.timeout(8000) })
210
+ const body = await health.json().catch(() => ({}))
211
+ out.push(['relay health', health.ok === true, `${health.status} protocol ${body.protocolVersion ?? '?'}`])
212
+ } catch (error) {
213
+ out.push(['relay health', false, `unreachable: ${error instanceof Error ? error.message : String(error)}`])
214
+ return out
215
+ }
216
+ try {
217
+ const probe = await fetch(`${base}/__liveshare/owner-credential`, { signal: AbortSignal.timeout(8000) })
218
+ const body = await probe.json().catch(() => ({}))
219
+ out.push(['relay credentials', body.supported === true, body.supported === true ? 'supported' : 'relay is too old'])
220
+ } catch (error) {
221
+ out.push(['relay credentials', false, `probe failed: ${error instanceof Error ? error.message : String(error)}`])
222
+ }
223
+ return out
224
+ }
225
+
226
+ function parseFlags(argv) {
227
+ const out = {}
228
+ for (let i = 0; i < argv.length; i += 1) {
229
+ const arg = argv[i]
230
+ if (!arg.startsWith('--')) continue
231
+ const eq = arg.indexOf('=')
232
+ if (eq !== -1) {
233
+ out[arg.slice(2, eq)] = coerce(arg.slice(eq + 1))
234
+ } else if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
235
+ out[arg.slice(2)] = coerce(argv[i + 1])
236
+ i += 1
237
+ } else {
238
+ out[arg.slice(2)] = true
239
+ }
240
+ }
241
+ return out
242
+ }
243
+
244
+ function coerce(value) {
245
+ if (value === 'true') return true
246
+ if (value === 'false') return false
247
+ return value
248
+ }
package/package.json CHANGED
@@ -1,6 +1,25 @@
1
1
  {
2
2
  "name": "dsh-crossdesk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Share one DeepSeek Harness session with another device through your own relay: no VPN, no port forwarding, no source patches.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "src/profile.mjs",
8
+ "bin": {
9
+ "dsh-crossdesk": "bin/cli.mjs"
10
+ },
11
+ "files": [
12
+ "bin",
13
+ "src",
14
+ "README.md"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "dependencies": {
20
+ "dsh-crossdesk-relay": "0.1.0"
21
+ },
22
+ "scripts": {
23
+ "test": "node --test test/*.test.mjs"
24
+ }
6
25
  }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * Profile surgery for `dsh-crossdesk enable`: install the shipped Liveshare owner bundle into a DSH
3
+ * profile and insert its two loader entries into that profile's user patch file.
4
+ *
5
+ * The relay is ours; the host half is the bundle DSH already ships. This module only edits the
6
+ * profile layer, which is exactly what the bundle's README documents, so nothing here can drift
7
+ * from the host's own wiring.
8
+ */
9
+ import { execFile } from 'node:child_process'
10
+ import { existsSync } from 'node:fs'
11
+ import { readFile, writeFile } from 'node:fs/promises'
12
+ import { homedir, hostname, userInfo } from 'node:os'
13
+ import { join } from 'node:path'
14
+ import { promisify } from 'node:util'
15
+ import { createHash } from 'node:crypto'
16
+
17
+ const run = promisify(execFile)
18
+
19
+ export const LIVESHARE_PACKAGE = '@deepseek-ai/dsh-experimental-liveshare'
20
+ export const LIVESHARE_PROTOCOL_PACKAGE = '@deepseek-ai/dsh-experimental-liveshare-protocol'
21
+ export const DEFAULT_LIVESHARE_VERSION = '0.2.2-rc.1'
22
+
23
+ const MARK_START = '# >>> dsh-crossdesk: managed block, edit above or below this pair >>>'
24
+ const MARK_END = '# <<< dsh-crossdesk <<<'
25
+
26
+ export function dshHome() {
27
+ return process.env.DSH_HOME ?? join(homedir(), '.dsh')
28
+ }
29
+
30
+ export function profileDir(name = 'desktop') {
31
+ return join(dshHome(), 'profiles', name)
32
+ }
33
+
34
+ /** A stable, non-secret subject for this machine, so one install keeps one identity. */
35
+ export function machineSubject() {
36
+ const name = safeHostname()
37
+ return `host-${createHash('sha256').update(`${name}:${safeUser()}`).digest('hex').slice(0, 12)}`
38
+ }
39
+
40
+ function safeHostname() {
41
+ try {
42
+ return hostname()
43
+ } catch {
44
+ return 'dsh-host'
45
+ }
46
+ }
47
+
48
+ function safeUser() {
49
+ try {
50
+ return userInfo().username
51
+ } catch {
52
+ return 'user'
53
+ }
54
+ }
55
+
56
+ /** The two loader entries, mirroring the bundle's own patch with literal values. */
57
+ export function managedPatchBlock({ subject = machineSubject(), displayName = safeHostname(), allowedSubjectIds = [] } = {}) {
58
+ return [
59
+ MARK_START,
60
+ '- insert:',
61
+ ' - id: liveshare-viewer-declarations',
62
+ ` name: '${LIVESHARE_PACKAGE}/viewer-declarations'`,
63
+ ' - id: liveshare-share-host',
64
+ ` name: '${LIVESHARE_PACKAGE}'`,
65
+ ' config:',
66
+ ' ownerIdentityProvider: crossdesk',
67
+ ' ownerIdentityTenant: default',
68
+ ` ownerIdentitySubject: '${subject}'`,
69
+ ` ownerIdentityDisplayName: '${displayName}'`,
70
+ ` allowedSubjectIds: [${allowedSubjectIds.map((value) => `'${value}'`).join(', ')}]`,
71
+ MARK_END,
72
+ '',
73
+ ].join('\n')
74
+ }
75
+
76
+ export function stripManagedBlock(text) {
77
+ const lines = text.split('\n')
78
+ const start = lines.findIndex((line) => line.trim() === MARK_START)
79
+ if (start === -1) return text
80
+ const end = lines.findIndex((line, index) => index > start && line.trim() === MARK_END)
81
+ if (end === -1) return lines.slice(0, start).join('\n')
82
+ const kept = [...lines.slice(0, start), ...lines.slice(end + 1)]
83
+ return kept.join('\n').replace(/\n{3,}/gu, '\n\n')
84
+ }
85
+
86
+ /** @returns {{before: string, after: string, changed: boolean}} */
87
+ export function enablePatch(text, options = {}) {
88
+ const original = text ?? ''
89
+ const clean = stripManagedBlock(original).replace(/\s+$/u, '')
90
+ const after = clean === '' ? managedPatchBlock(options) : `${clean}\n\n${managedPatchBlock(options)}`
91
+ return { before: clean, after, changed: after !== original }
92
+ }
93
+
94
+ export async function readProfilePatch(name) {
95
+ const file = join(profileDir(name), 'cordis.patch.yml')
96
+ return { file, text: existsSync(file) ? await readFile(file, 'utf8') : '' }
97
+ }
98
+
99
+ export async function writeProfilePatch(name, text) {
100
+ const file = join(profileDir(name), 'cordis.patch.yml')
101
+ await writeFile(file, text, 'utf8')
102
+ return file
103
+ }
104
+
105
+ /** Add the bundle to the profile's dependency block without reformatting the rest of the file. */
106
+ export function addDependencies(manifest, version = DEFAULT_LIVESHARE_VERSION) {
107
+ const next = structuredClone(manifest)
108
+ next.dependencies = next.dependencies ?? {}
109
+ next.dependencies[LIVESHARE_PACKAGE] = version
110
+ next.dependencies[LIVESHARE_PROTOCOL_PACKAGE] = version
111
+ return next
112
+ }
113
+
114
+ export async function readProfileManifest(name) {
115
+ const file = join(profileDir(name), 'package.json')
116
+ if (!existsSync(file)) {
117
+ return { file, manifest: { name: `dsh-profile-${name}`, private: true, type: 'module', dependencies: {} } }
118
+ }
119
+ return { file, manifest: JSON.parse(await readFile(file, 'utf8')) }
120
+ }
121
+
122
+ export async function writeProfileManifest(name, manifest) {
123
+ const file = join(profileDir(name), 'package.json')
124
+ await writeFile(file, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
125
+ return file
126
+ }
127
+
128
+ /** Run the profile's package manager; returns a description of what ran. */
129
+ export async function installProfileDependencies(name, { dryRun = false } = {}) {
130
+ const dir = profileDir(name)
131
+ if (!existsSync(dir)) throw new Error(`profile directory not found: ${dir}`)
132
+ const manager = await detectPackageManager(dir)
133
+ if (dryRun) return { manager, command: `${manager} install`, ran: false }
134
+ const args = manager === 'pnpm' ? ['install'] : ['install', '--no-audit', '--no-fund']
135
+ await run(manager, args, { cwd: dir, shell: process.platform === 'win32', windowsHide: true, timeout: 10 * 60_000 })
136
+ return { manager, command: `${manager} ${args.join(' ')}`, ran: true }
137
+ }
138
+
139
+ async function detectPackageManager(dir) {
140
+ for (const candidate of ['pnpm', 'npm']) {
141
+ try {
142
+ await run(candidate, ['--version'], { shell: process.platform === 'win32', windowsHide: true, timeout: 30_000 })
143
+ return candidate
144
+ } catch {
145
+ /* try the next one */
146
+ }
147
+ }
148
+ throw new Error(`no package manager found for ${dir}; install ${LIVESHARE_PACKAGE} manually`)
149
+ }
150
+
151
+ /** Everything `status`/`doctor` reports about one profile. */
152
+ export async function inspectProfile(name = 'desktop') {
153
+ const { file: patchFile, text } = await readProfilePatch(name)
154
+ const { file: manifestFile, manifest } = await readProfileManifest(name)
155
+ const installed = existsSync(join(profileDir(name), 'node_modules', LIVESHARE_PACKAGE, 'package.json'))
156
+ return {
157
+ profile: name,
158
+ home: dshHome(),
159
+ dir: profileDir(name),
160
+ patchFile,
161
+ manifestFile,
162
+ patchHasEntry: text.includes('liveshare-share-host'),
163
+ patchHasBlock: text.includes(MARK_START),
164
+ declared: Boolean(manifest.dependencies?.[LIVESHARE_PACKAGE]),
165
+ installed,
166
+ manifest,
167
+ }
168
+ }