dsh-remote-tunnel-easy 1.4.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/LICENSE +21 -0
- package/README.md +166 -0
- package/assets/logo.svg +81 -0
- package/cordis.patch.yml +12 -0
- package/index.js +792 -0
- package/lib/client.js +617 -0
- package/lib/cloudflared.js +196 -0
- package/lib/qrcode.js +591 -0
- package/lib/update.js +221 -0
- package/package.json +51 -0
package/lib/update.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-update support for the dsh-remote-tunnel-easy bundle.
|
|
3
|
+
*
|
|
4
|
+
* The Settings card gets an "Update" button that upgrades the installed
|
|
5
|
+
* package to the latest registry version without leaving the UI. This module
|
|
6
|
+
* owns the pure, testable parts — locate the owning profile directory, read
|
|
7
|
+
* its dependency spec, compare versions, and run `pnpm add` inside it — while
|
|
8
|
+
* `index.js` owns the loopback route and in-flight mutex. It deliberately
|
|
9
|
+
* runs the SAME operation the CLI recommends (`dsh plugin --profile <name> add
|
|
10
|
+
* dsh-remote-tunnel-easy@latest`) rather than inventing a second update path.
|
|
11
|
+
*
|
|
12
|
+
* `pnpm add <name>@latest` (not `pnpm update`) is the correct verb: a fixed
|
|
13
|
+
* dependency was recorded with a caret range, and `pnpm update` only moves
|
|
14
|
+
* within that range, so it could never reach a new minor/major. `pnpm add
|
|
15
|
+
* name@latest` rewrites the spec and installs the newest release. The
|
|
16
|
+
* `dsh`-side reconciliation is a no-op for an already-registered bundle, so
|
|
17
|
+
* running pnpm directly is equivalent.
|
|
18
|
+
*
|
|
19
|
+
* A live update does NOT replace the code already loaded into this process:
|
|
20
|
+
* the running tunnel, bridge, and card keep the old version until `dsh web`
|
|
21
|
+
* restarts. The card reports that honestly after a successful install.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readFileSync, readdirSync, realpathSync } from 'node:fs'
|
|
25
|
+
import { homedir } from 'node:os'
|
|
26
|
+
import { join, sep } from 'node:path'
|
|
27
|
+
import { fileURLToPath } from 'node:url'
|
|
28
|
+
import { spawn } from 'node:child_process'
|
|
29
|
+
|
|
30
|
+
/** Own package name — the dependency key in the profile manifest and on the registry. */
|
|
31
|
+
export const PACKAGE_NAME = 'dsh-remote-tunnel-easy'
|
|
32
|
+
|
|
33
|
+
const REGISTRY_BASE = 'https://registry.npmjs.org'
|
|
34
|
+
const DSH_HOME_DIR = '.dsh'
|
|
35
|
+
const PROFILES_DIR = 'profiles'
|
|
36
|
+
|
|
37
|
+
/** Largest chunk of pnpm output kept for an error message, in code units. */
|
|
38
|
+
const MAX_OUTPUT = 4000
|
|
39
|
+
|
|
40
|
+
/** Expand `~`, `~/x`, `~\x` against the OS home; any other value passes through. */
|
|
41
|
+
function expandTilde(path) {
|
|
42
|
+
if (path === '~') return homedir()
|
|
43
|
+
if (path.startsWith('~/') || path.startsWith('~\\')) return join(homedir(), path.slice(2))
|
|
44
|
+
return path
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolve the single-root harness home the same way the boot loader does:
|
|
49
|
+
* `$DSH_HOME` when set and non-blank, otherwise `~/.dsh`. Reimplemented (not
|
|
50
|
+
* imported) so the bundle keeps its zero-dependency install.
|
|
51
|
+
* @returns absolute harness home.
|
|
52
|
+
*/
|
|
53
|
+
export function dshHome() {
|
|
54
|
+
const configured = process.env.DSH_HOME
|
|
55
|
+
return configured !== undefined && configured.trim().length > 0
|
|
56
|
+
? join(expandTilde(configured))
|
|
57
|
+
: join(homedir(), DSH_HOME_DIR)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Canonical on-disk path of this module (symlinks resolved), or undefined. */
|
|
61
|
+
function ownRealPath() {
|
|
62
|
+
try {
|
|
63
|
+
return realpathSync.native(fileURLToPath(import.meta.url))
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Locate the profile directory that owns this install by scanning
|
|
71
|
+
* `$DSH_HOME/profiles/*` for a `node_modules/dsh-remote-tunnel-easy` whose
|
|
72
|
+
* canonical target contains this module's own canonical path. This matches
|
|
73
|
+
* every real install shape — a registry/copy install, pnpm's `.pnpm` virtual
|
|
74
|
+
* store symlink, and a `link:`/`file:` checkout symlink — without depending
|
|
75
|
+
* on `import.meta.url` still carrying the un-resolved node_modules spelling.
|
|
76
|
+
* @param from - own canonical path (defaults to this module's real path).
|
|
77
|
+
* @returns the owning profile directory, or undefined when none matches.
|
|
78
|
+
*/
|
|
79
|
+
export function locateProfileDir(from = ownRealPath()) {
|
|
80
|
+
if (from === undefined) return undefined
|
|
81
|
+
let entries
|
|
82
|
+
try {
|
|
83
|
+
entries = readdirSync(join(dshHome(), PROFILES_DIR), { withFileTypes: true })
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
89
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
90
|
+
const dir = join(dshHome(), PROFILES_DIR, entry.name)
|
|
91
|
+
let target
|
|
92
|
+
try {
|
|
93
|
+
target = realpathSync.native(join(dir, 'node_modules', PACKAGE_NAME))
|
|
94
|
+
} catch {
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
if (from === target || (from.length > target.length && from.startsWith(target + sep))) return dir
|
|
98
|
+
}
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The dependency spec recorded for this package in `profileDir`, or undefined. */
|
|
103
|
+
export function dependencySpec(profileDir) {
|
|
104
|
+
if (profileDir === undefined) return undefined
|
|
105
|
+
try {
|
|
106
|
+
const manifest = JSON.parse(readFileSync(join(profileDir, 'package.json'), 'utf8'))
|
|
107
|
+
const spec = manifest?.dependencies?.[PACKAGE_NAME]
|
|
108
|
+
return typeof spec === 'string' ? spec : undefined
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Classify a dependency spec: `local` for the non-registry forms
|
|
116
|
+
* (`link:`/`file:`/`workspace:`/`portal:`, git URLs, relative paths), and
|
|
117
|
+
* `registry` for a bare name or semver range (which is everything else).
|
|
118
|
+
* @returns 'local' | 'registry' | undefined.
|
|
119
|
+
*/
|
|
120
|
+
export function classifySpec(spec) {
|
|
121
|
+
if (spec === undefined) return undefined
|
|
122
|
+
const value = spec.trim()
|
|
123
|
+
if (/^(?:link|file|workspace|portal):/.test(value)) return 'local'
|
|
124
|
+
if (/^(?:git\+|https?:\/\/|git:|github:|\.{1,2}(?:[/\\]|$))/.test(value)) return 'local'
|
|
125
|
+
return 'registry'
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Read this package's own `version` from package.json, or undefined. */
|
|
129
|
+
export function currentVersion() {
|
|
130
|
+
try {
|
|
131
|
+
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
132
|
+
return typeof manifest.version === 'string' ? manifest.version : undefined
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Parse a `v`-prefixed dotted semver into `[major, minor, patch]`, or undefined. */
|
|
139
|
+
function parseVersion(version) {
|
|
140
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(version).trim())
|
|
141
|
+
return match === null ? undefined : [Number(match[1]), Number(match[2]), Number(match[3])]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Compare two dotted semver strings on major, minor, patch.
|
|
146
|
+
* @returns -1 | 0 | 1, or undefined when either side is unparseable.
|
|
147
|
+
*/
|
|
148
|
+
export function compareVersions(a, b) {
|
|
149
|
+
const left = parseVersion(a)
|
|
150
|
+
const right = parseVersion(b)
|
|
151
|
+
if (left === undefined || right === undefined) return undefined
|
|
152
|
+
for (let i = 0; i < 3; i++) {
|
|
153
|
+
if (left[i] !== right[i]) return left[i] > right[i] ? 1 : -1
|
|
154
|
+
}
|
|
155
|
+
return 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Whether `latest` is strictly newer than `current` (unparseable compares false). */
|
|
159
|
+
export function isNewer(latest, current) {
|
|
160
|
+
const order = compareVersions(latest, current)
|
|
161
|
+
return order !== undefined && order > 0
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Fetch the registry's current `dist-tags.latest` version for this package.
|
|
166
|
+
* Uses the abridged `/<name>/latest` endpoint, which returns only the latest
|
|
167
|
+
* version object. Non-2xx, a missing `version`, or a timeout all throw.
|
|
168
|
+
* @param registryBase - origin (defaults to the public npm registry).
|
|
169
|
+
* @returns the latest published version string.
|
|
170
|
+
*/
|
|
171
|
+
export async function fetchLatestVersion(registryBase = REGISTRY_BASE) {
|
|
172
|
+
const response = await fetch(`${registryBase}/${PACKAGE_NAME}/latest`, {
|
|
173
|
+
headers: { accept: 'application/json' },
|
|
174
|
+
signal: AbortSignal.timeout(10_000),
|
|
175
|
+
})
|
|
176
|
+
if (!response.ok) throw new Error(`registry ${PACKAGE_NAME}/latest returned HTTP ${response.status}`)
|
|
177
|
+
const body = await response.json()
|
|
178
|
+
if (typeof body?.version !== 'string') throw new Error(`registry ${PACKAGE_NAME}/latest has no version field`)
|
|
179
|
+
return body.version
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Run `pnpm add <spec>` inside `profileDir` — the same operation the CLI
|
|
184
|
+
* performs for `dsh plugin --profile <name> add ...`. Streams stdout/stderr
|
|
185
|
+
* into bounded buffers so failures surface the useful tail, and enforces a
|
|
186
|
+
* timeout so a hung install cannot wedge the in-flight mutex forever.
|
|
187
|
+
* @param profileDir - the profile directory pnpm runs in.
|
|
188
|
+
* @param spec - package@range to install (defaults to `name@latest`).
|
|
189
|
+
* @param options.timeoutMs - kill the child after this many ms.
|
|
190
|
+
* @returns {Promise<{ ok: true, output: string }>}; rejects with the tail.
|
|
191
|
+
*/
|
|
192
|
+
export function runPnpmAdd(profileDir, spec = `${PACKAGE_NAME}@latest`, { timeoutMs = 300_000 } = {}) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const child = spawn('pnpm', ['add', spec], {
|
|
195
|
+
cwd: profileDir,
|
|
196
|
+
shell: process.platform === 'win32',
|
|
197
|
+
windowsHide: true,
|
|
198
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
199
|
+
})
|
|
200
|
+
let stdout = ''
|
|
201
|
+
let stderr = ''
|
|
202
|
+
// Bound the retained output regardless of total stream size.
|
|
203
|
+
const keep = (buffer, chunk) => buffer.length < MAX_OUTPUT ? buffer + chunk.toString() : buffer
|
|
204
|
+
child.stdout?.on('data', (chunk) => { stdout = keep(stdout, chunk) })
|
|
205
|
+
child.stderr?.on('data', (chunk) => { stderr = keep(stderr, chunk) })
|
|
206
|
+
const timer = setTimeout(() => child.kill(), timeoutMs)
|
|
207
|
+
child.on('error', (error) => {
|
|
208
|
+
clearTimeout(timer)
|
|
209
|
+
reject(error)
|
|
210
|
+
})
|
|
211
|
+
child.on('close', (code) => {
|
|
212
|
+
clearTimeout(timer)
|
|
213
|
+
if (code === 0) {
|
|
214
|
+
resolve({ ok: true, output: stdout })
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
const detail = (stderr || stdout || `pnpm exited with code ${String(code)}`).trim()
|
|
218
|
+
reject(new Error(detail.slice(0, MAX_OUTPUT)))
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-remote-tunnel-easy",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Remote hassle-free access for DeepSeek Harness: a cloudflared quick tunnel plus a scannable QR code and settings shown in the Settings → Plugins page. Zero runtime dependencies, no database, no Firebase.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./lib/client.js",
|
|
10
|
+
"./cloudflared": "./lib/cloudflared.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"lib/client.js",
|
|
16
|
+
"lib/cloudflared.js",
|
|
17
|
+
"lib/qrcode.js",
|
|
18
|
+
"lib/update.js",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"README.md",
|
|
21
|
+
"assets/logo.svg"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/Raiyan007-gb/dsh-remote-tunnel-easy.git"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"dsh",
|
|
29
|
+
"dsh-bundle",
|
|
30
|
+
"qrcode",
|
|
31
|
+
"cloudflared",
|
|
32
|
+
"remote"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"dependencies": {},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.6"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.6"
|
|
41
|
+
},
|
|
42
|
+
"dsh": {
|
|
43
|
+
"bundle": {
|
|
44
|
+
"patch": "./cordis.patch.yml"
|
|
45
|
+
},
|
|
46
|
+
"client": {
|
|
47
|
+
"platform": "web",
|
|
48
|
+
"inject": []
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|