@justlovepl/desktop-bridge 0.0.0-stage → 0.2.6

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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -3
  3. package/THIRD_PARTY_NOTICES.md +33 -0
  4. package/bin/desktop-bridge.mjs +114 -0
  5. package/bin/update.mjs +92 -0
  6. package/dist/client/Install.cmd +3 -0
  7. package/dist/client/Install.command +3 -0
  8. package/dist/client/Install.sh +137 -0
  9. package/dist/client/Pair-again.cmd +3 -0
  10. package/dist/client/RePair.cmd +9 -0
  11. package/dist/client/Start.cmd +4 -0
  12. package/dist/client/Start.command +3 -0
  13. package/dist/client/Stop.cmd +4 -0
  14. package/dist/client/Stop.command +3 -0
  15. package/dist/client/Uninstall.cmd +3 -0
  16. package/dist/client/Uninstall.command +3 -0
  17. package/dist/client/Uninstall.sh +3 -0
  18. package/dist/client/admin-elevate.mjs +48 -0
  19. package/dist/client/admin-runtime.mjs +119 -0
  20. package/dist/client/admin-service.mjs +152 -0
  21. package/dist/client/agent/agent.mjs +264 -0
  22. package/dist/client/agent/pair.mjs +31 -0
  23. package/dist/client/connection.json +1 -0
  24. package/dist/client/control.mjs +75 -0
  25. package/dist/client/control.ps1 +12 -0
  26. package/dist/client/environment-probe.mjs +5 -0
  27. package/dist/client/environment.mjs +99 -0
  28. package/dist/client/install-options.mjs +43 -0
  29. package/dist/client/install.mjs +84 -0
  30. package/dist/client/install.ps1 +108 -0
  31. package/dist/client/launch.sh +17 -0
  32. package/dist/client/lifecycle.mjs +42 -0
  33. package/dist/client/node-runtime.json +30 -0
  34. package/dist/client/package-lock.json +8457 -0
  35. package/dist/client/package.json +23 -0
  36. package/dist/client/runner.mjs +38 -0
  37. package/dist/client/start.sh +3 -0
  38. package/dist/client/startup.mjs +60 -0
  39. package/dist/client/stop.sh +3 -0
  40. package/dist/client/uninstall.ps1 +6 -0
  41. package/package.json +28 -6
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Desktop Bridge Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
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.
1
+ # Desktop Bridge npm CLI
2
+
3
+ Independent npm distribution for the Desktop Bridge **local device client**. It does not publish the upstream Desktop Commander package or cloud relay.
4
+
5
+ **Status: development branch, not yet published or production validated.**
6
+
7
+ Requires Node.js 24 and npm. Native installers for computers without Node remain available at <https://mcp.ilovestudy.de/start.html>.
8
+
9
+ ```sh
10
+ npm install -g @justlovepl/desktop-bridge
11
+ desktop-bridge setup # must type 1 or 2; blank Enter prompts again
12
+ desktop-bridge status
13
+ desktop-bridge start
14
+ desktop-bridge stop
15
+ desktop-bridge restart
16
+ desktop-bridge pair # explicitly request browser approval
17
+ desktop-bridge mode manual
18
+ desktop-bridge mode background
19
+ desktop-bridge repair # repairs AND re-pairs in the browser
20
+ desktop-bridge uninstall # preserves credentials/results unless --purge
21
+ ```
22
+
23
+ **The CLI installation itself never prompts or starts a service.** Each interactive `desktop-bridge setup` or reinstall must explicitly select `1` (manual) or `2` (background); pressing Enter without a choice repeats the prompt. For non-interactive setup, a startup mode is mandatory. No existing credential, Agent, or login startup configuration may change before the choice is validated.\n\nUse `desktop-bridge setup --mode manual` or `--mode background` in automated shells. `--administrator` requires explicit OS authorization and respects the existing protected service restrictions. `--root <directory>` selects a dedicated custom data directory. **There is no install/start action in npm postinstall**.
24
+
25
+ `desktop-bridge uninstall --purge` permanently deletes local credentials and result journals. Uninstalling locally does not revoke the server token; use device management at <https://mcp.ilovestudy.de/start.html> to revoke device authorization. Existing administrator service installations require the protected service's own approved uninstall flow.
26
+
27
+ Update the npm CLI and then apply the local client release:
28
+
29
+ ```sh
30
+ npm update -g @justlovepl/desktop-bridge
31
+ desktop-bridge setup
32
+ ```
33
+
34
+ `npm update` alone does not replace an already running agent. `desktop-bridge update` upgrades the CLI and Agent while explicitly passing the existing saved mode, with credential, mode, and task journal protection; routine update is not an interactive re-install. A new pairing still requires an explicit browser login and approval if the existing device authorization was revoked.
35
+
36
+ ## Packaging / tests
37
+
38
+ The npm package lives in `npm-installer/`, distinct from the root upstream `package.json` and `bridge/client/package.json`. Packing stages files from an explicit allowlist in `bridge/client/` and `bridge/agent/`; no cloud server, installed user data or secret credentials enter the package.
39
+
40
+ ```sh
41
+ cd npm-installer
42
+ npm test
43
+ npm pack --dry-run
44
+ ```
45
+
46
+ Review the resulting npm tarball and run real setup/stop/re-pair/update/uninstall tests on Windows, macOS and Linux before first publication. **Never run npm publish from the repository root or bridge/client directory.**
@@ -0,0 +1,33 @@
1
+ # Third-party components
2
+
3
+ Desktop Bridge uses Desktop Commander MCP as its local tool engine. The currently deployed engine is `@wonderwhy-er/desktop-commander` version `0.3.0`, already pinned on p111. The original Remote client and official hosted backend are not used by this bridge's connection.
4
+
5
+ Upstream: https://github.com/wonderwhy-er/DesktopCommanderMCP
6
+
7
+ Desktop Commander MCP is MIT licensed:
8
+
9
+ Copyright (c) 2024-2025 Eduard Ruzga and Desktop Commander Contributors
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ The bridge also depends on the official MCP TypeScript SDK, Express, express-rate-limit, and ws. Their licenses are included in their installed npm packages and remain applicable. `package-lock.json` pins the relay dependencies.
30
+
31
+ ## NES.css
32
+
33
+ The administration page uses [NES.css](https://github.com/nostalgic-css/NES.css), version 2.3.0, for 8-bit UI components. NES.css is distributed under the MIT License. The unmodified stylesheet and complete license text are stored at `public/vendor/nes.css/nes.min.css` and `public/vendor/nes.css/LICENSE`; there is no third-party CSS or font request at runtime.
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ /** CLI does not start a service during npm install; explicit setup is required. */
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import {spawn} from 'node:child_process';
7
+ import {fileURLToPath} from 'node:url';
8
+ import {performUpdate} from './update.mjs';
9
+
10
+ const packageRoot=fileURLToPath(new URL('../',import.meta.url));
11
+ const clientRoot=path.join(packageRoot,'dist','client');
12
+ const installer=path.join(clientRoot,'install.mjs');
13
+ const controller=path.join(clientRoot,'control.mjs');
14
+ const manifest=JSON.parse(fs.readFileSync(path.join(packageRoot,'package.json'),'utf8'));
15
+ const commands=new Set(['setup','start','stop','restart','status','pair','mode','repair','uninstall','update']);
16
+
17
+ export function clientDataRoot(platform=process.platform,env=process.env,home=os.homedir()){
18
+ return platform==='win32'?path.win32.join(env.LOCALAPPDATA??path.win32.join(home,'AppData','Local'),'DesktopBridgeClient')
19
+ :platform==='darwin'?path.posix.join(home,'Library','Application Support','DesktopBridge')
20
+ :path.posix.join(env.XDG_DATA_HOME??path.posix.join(home,'.local','share'),'desktop-bridge');
21
+ }
22
+
23
+ export function parse(argv){
24
+ const [command='help',...args]=argv;
25
+ if(['help','--help','-h'].includes(command))return {command:'help'};
26
+ if(['version','--version','-v'].includes(command))return {command:'version'};
27
+ if(!commands.has(command))throw new Error('Unknown command: '+command);
28
+ let customRoot;
29
+ const remainder=[];
30
+ for(let i=0;i<args.length;i++){
31
+ if(args[i]==='--root'){
32
+ const value=args[++i];
33
+ if(!value||value.startsWith('--')||customRoot)throw new Error('--root requires exactly one directory');
34
+ customRoot=path.resolve(value);
35
+ }else remainder.push(args[i]);
36
+ }
37
+ const control=['start','stop','status','pair','mode','restart'].includes(command);
38
+ if(control){
39
+ if(command==='mode'){
40
+ if(remainder.length!==1||!['manual','background'].includes(remainder[0]))throw new Error('Usage: desktop-bridge mode manual|background');
41
+ }else if(remainder.length)throw new Error(command+' does not accept extra arguments');
42
+ }else if(command==='uninstall'){
43
+ if(remainder.some(x=>x!=='--purge')||remainder.filter(x=>x==='--purge').length>1)throw new Error('Usage: desktop-bridge uninstall [--purge]');
44
+ }else if(command==='repair'||command==='update'){
45
+ if(remainder.length)throw new Error('Usage: desktop-bridge '+command+' [--root directory]');
46
+ }else{
47
+ for(let i=0;i<remainder.length;i++){
48
+ const a=remainder[i];
49
+ if(a==='--mode'){
50
+ if(!['manual','background'].includes(remainder[++i]))throw new Error('Use --mode manual or --mode background');
51
+ }else if(!['--administrator','--no-browser','--no-start'].includes(a))throw new Error('Unknown setup option: '+a);
52
+ }
53
+ }
54
+ return {command,args:remainder,root:customRoot};
55
+ }
56
+
57
+ function usage(){
58
+ return 'Desktop Bridge '+manifest.version+'\n\n'+
59
+ 'Usage: desktop-bridge <command> [options]\n\n'+
60
+ ' setup [--mode manual|background] [--administrator] Install and pair\n'+
61
+ ' start | stop | restart Manage local agent\n'+
62
+ ' status Show local connection status\n'+
63
+ ' pair Re-pair in your browser\n'+
64
+ ' mode manual|background Change startup mode\n'+
65
+ ' repair Repair (re-pairs in browser)\n'+
66
+ ' update Update npm CLI and installed Agent\n'+
67
+ ' uninstall [--purge] Remove local agent\n'+
68
+ ' version | help Show CLI info\n\n'+
69
+ 'All commands except help/version accept --root <directory>.\n'+
70
+ 'No service is installed automatically by npm install.\n'+
71
+ 'update upgrades the npm CLI and applies its client release while preserving pairing.\n'+
72
+ 'Device unlink/revocation: https://mcp.ilovestudy.de/start.html\n';
73
+ }
74
+
75
+ async function childRun(script,args){
76
+ if(!fs.existsSync(script))throw new Error('Packaged client is missing. Reinstall the npm package.');
77
+ const child=spawn(process.execPath,[script,...args],{stdio:'inherit',windowsHide:false});
78
+ return await new Promise((resolve,reject)=>{
79
+ child.once('error',reject);
80
+ child.once('exit',(code,signal)=>resolve(signal?128:code??1));
81
+ });
82
+ }
83
+
84
+ export async function main(argv=process.argv.slice(2)){
85
+ const parsed=parse(argv);
86
+ if(parsed.command==='help'){console.log(usage());return 0;}
87
+ if(parsed.command==='version'){console.log(manifest.version);return 0;}
88
+ if(Number(process.versions.node.split('.')[0])!==24)throw new Error('Desktop Bridge needs Node.js 24. Install Node.js 24 before using this CLI.');
89
+ const root=parsed.root??clientDataRoot();
90
+ const rootArgs=parsed.root?['--root',root]:[];
91
+ if(parsed.command==='setup')return childRun(installer,[...parsed.args,...rootArgs]);
92
+ if(!fs.existsSync(path.join(root,'launch.json')))throw new Error('Client is not installed at '+root+'. Run desktop-bridge setup first.');
93
+ if(parsed.command==='update')return performUpdate({root,packageRoot,packageName:manifest.name});
94
+ if(parsed.command==='repair'){
95
+ console.log('Repair also requires browser re-pairing approval.');
96
+ return childRun(installer,['--repair',...rootArgs]);
97
+ }
98
+ if(parsed.command==='uninstall'){
99
+ const status=await childRun(installer,['--uninstall',...parsed.args,...rootArgs]);
100
+ if(status===0)console.log('To also remove the npm CLI: npm uninstall -g '+manifest.name);
101
+ return status;
102
+ }
103
+ if(parsed.command==='restart'){
104
+ const status=await childRun(controller,['stop',...rootArgs]);
105
+ return status===0?childRun(controller,['start',...rootArgs]):status;
106
+ }
107
+ return childRun(controller,[parsed.command,...parsed.args,...rootArgs]);
108
+ }
109
+ if(process.argv[1]&&fs.realpathSync(path.resolve(process.argv[1]))===fs.realpathSync(fileURLToPath(import.meta.url))){
110
+ main().then(code=>{process.exitCode=code;},error=>{
111
+ console.error('Desktop Bridge:',error.message);
112
+ process.exitCode=1;
113
+ });
114
+ }
package/bin/update.mjs ADDED
@@ -0,0 +1,92 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import {spawn} from 'node:child_process';
4
+
5
+ /**
6
+ * Upgrade npm CLI first, then let the NEW package's installer update the
7
+ * separate per-user client runtime. Credentials stay in the user data root.
8
+ */
9
+ export function findNpmCli({node=process.execPath,env=process.env}={}){
10
+ const bin=path.dirname(node);
11
+ const candidates=[
12
+ env.npm_execpath,
13
+ path.join(bin,'node_modules','npm','bin','npm-cli.js'),
14
+ path.resolve(bin,'../lib/node_modules/npm/bin/npm-cli.js')
15
+ ];
16
+ const found=candidates.find(p=>p&&path.basename(p)==='npm-cli.js'&&fs.existsSync(p));
17
+ if(!found)throw new Error('Cannot locate npm-cli.js. Install npm alongside Node.js 24 or run npm update -g manually.');
18
+ return found;
19
+ }
20
+
21
+ export async function runProcess(executable,args){
22
+ const child=spawn(executable,args,{stdio:'inherit',windowsHide:false});
23
+ return await new Promise((resolve,reject)=>{
24
+ child.once('error',reject);
25
+ child.once('exit',(code,signal)=>resolve(signal?128:code??1));
26
+ });
27
+ }
28
+
29
+ function readJson(file){
30
+ try{return JSON.parse(fs.readFileSync(file,'utf8'));}
31
+ catch{throw new Error('Cannot read valid JSON from '+path.basename(file));}
32
+ }
33
+ const identityFields=['deviceToken','deviceId','serverUrl','userId','accountId','ownerId','tenantId','deviceName','accountHint'];
34
+ function getIdentity(config){
35
+ return Object.fromEntries(identityFields.filter(name=>Object.hasOwn(config,name)).map(name=>[name,config[name]]));
36
+ }
37
+
38
+ export async function performUpdate({
39
+ root,packageRoot,packageName='@justlovepl/desktop-bridge',
40
+ npmScript,execute=runProcess,print=message=>console.log(message)
41
+ }){
42
+ const installed=path.join(root,'launch.json');
43
+ if(!fs.existsSync(installed))throw new Error('Local client not installed. Run desktop-bridge setup before updating.');
44
+ if(path.resolve(root)===path.resolve(packageRoot)||path.resolve(packageRoot).startsWith(path.resolve(root)+path.sep))
45
+ throw new Error('Client data directory must not contain the npm package.');
46
+ const configFile=path.join(root,'agent-config.json');
47
+ const settingsFile=path.join(root,'client-settings.json');
48
+ const config=readJson(configFile),settings=readJson(settingsFile);
49
+ if(!config.deviceId||!config.deviceToken)throw new Error('Device is not paired. Run desktop-bridge pair before updating.');
50
+ if(!['manual','background'].includes(settings.mode))
51
+ throw new Error('Cannot identify saved startup mode; refusing an update that might change it.');
52
+ if(fs.existsSync(path.join(root,'auth-status.json'))&&readJson(path.join(root,'auth-status.json')).authStatus==='re_pair_required')
53
+ throw new Error('Device authorization is revoked. Re-pair it before updating.');
54
+ const previousIdentity=getIdentity(config);
55
+ const previousMode=settings.mode;
56
+ const previousPrivilege=settings.privilege??'standard';
57
+ const journalFile=path.join(root,'journal.json');
58
+ const oldJournal=fs.existsSync(journalFile)?readJson(journalFile):null;
59
+ const npmCli=npmScript??findNpmCli();
60
+ print('Updating npm CLI (device credentials and configuration are kept outside npm)...');
61
+ const npmStatus=await execute(process.execPath,[npmCli,'install','--global','--no-audit','--no-fund',packageName+'@latest']);
62
+ if(npmStatus!==0)throw new Error('npm package update failed; local client was not reinstalled.');
63
+ const packageFile=path.join(packageRoot,'package.json');
64
+ const current=readJson(packageFile);
65
+ if(current.name!==packageName)throw new Error('Updated npm package identity is unexpected. Client was not modified.');
66
+ const newEntry=path.join(packageRoot,'bin','desktop-bridge.mjs');
67
+ if(!fs.existsSync(newEntry))throw new Error('Updated CLI entry missing; local client was not modified.');
68
+ print('npm CLI updated to '+current.version+'. Upgrading local Agent in '+previousMode+' mode...');
69
+ // The new package's installer reuses an existing approved device token.
70
+ // --no-browser means routine upgrades must not open an unsolicited login tab.
71
+ const installStatus=await execute(process.execPath,[
72
+ newEntry,'setup','--mode',previousMode,'--no-browser','--root',root
73
+ ]);
74
+ if(installStatus!==0)throw new Error('Agent update failed; do not manually erase device data. Check status and retry or repair.');
75
+ const updatedConfig=readJson(configFile);
76
+ for(const [key,value] of Object.entries(previousIdentity)){
77
+ if(updatedConfig[key]!==value)
78
+ throw new Error('Authorization or account binding changed during upgrade ('+key+'). Do not restore revoked tokens; check account and re-pair if needed.');
79
+ }
80
+ const updatedSettings=readJson(settingsFile);
81
+ if(updatedSettings.mode!==previousMode||(updatedSettings.privilege??'standard')!==previousPrivilege)
82
+ throw new Error('Startup mode or privilege changed unexpectedly during upgrade. Inspect settings before restarting.');
83
+ if(oldJournal){
84
+ const updatedJournal=readJson(journalFile);
85
+ for(const [id,entry] of Object.entries(oldJournal)){
86
+ if(entry?.status==='completed'&&(!entry.time||Date.now()-entry.time<30*86400000)&&!Object.hasOwn(updatedJournal,id))
87
+ throw new Error('Recent completed task journal entry missing after upgrade: '+id);
88
+ }
89
+ }
90
+ print('Update complete: npm CLI '+current.version+', device identity and '+previousMode+' startup mode preserved.');
91
+ return 0;
92
+ }
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0install.ps1" %*
3
+ if errorlevel 1 pause
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ SOURCE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
3
+ exec /bin/sh "$SOURCE/Install.sh" "$@"
@@ -0,0 +1,137 @@
1
+ #!/bin/sh
2
+ set -eu
3
+ umask 077
4
+ fail() { echo "$*" >&2; exit 1; }
5
+ # Validate portable options before downloading or changing the local installation.
6
+ NEXT=''; DATA_ROOT=''; MODE=''; NO_START=0; STOP=0; UNINSTALL=0; PURGE=0
7
+ for ARG in "$@"; do
8
+ if [ -n "$NEXT" ]; then
9
+ case "$ARG" in ''|--*) fail "$NEXT 缺少参数值。";; esac
10
+ if [ "$NEXT" = --root ]; then DATA_ROOT="$ARG"
11
+ else
12
+ case "$ARG" in manual|background) ;; *) fail '--mode 只能是 manual 或 background。';; esac
13
+ [ -z "$MODE" ] || [ "$MODE" = "$ARG" ] || fail '不能同时指定不同的 --mode。'
14
+ MODE="$ARG"
15
+ fi
16
+ NEXT=''
17
+ else
18
+ case "$ARG" in
19
+ --root|--mode) NEXT="$ARG";;
20
+ --no-start) NO_START=1;; --stop) STOP=1;; --uninstall) UNINSTALL=1;; --purge) PURGE=1;;
21
+ --repair|--no-browser) ;; *) fail "未知参数:$ARG";;
22
+ esac
23
+ fi
24
+ done
25
+ [ -z "$NEXT" ] || fail "$NEXT 缺少参数值。"
26
+ [ "$NO_START:$MODE" != 1:background ] || fail '--no-start 不能与 --mode background 同时使用。'
27
+ [ "$STOP:$UNINSTALL" != 1:1 ] || fail '--stop 不能与 --uninstall 同时使用。'
28
+ [ "$PURGE:$UNINSTALL" != 1:0 ] || fail '--purge 必须与 --uninstall 一起使用。'
29
+ for TOOL in uname dirname; do command -v "$TOOL" >/dev/null 2>&1 || fail "缺少基础工具 $TOOL,无法识别安装环境。"; done
30
+ SOURCE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
31
+ case "$(uname -s)" in Darwin) PLATFORM=darwin;; Linux) PLATFORM=linux;; *) fail '支持的系统:macOS 和 Linux。';; esac
32
+ case "$(uname -m)" in x86_64|amd64) ARCH=x64;; arm64|aarch64) ARCH=arm64;; *) fail '支持的处理器:x64 和 ARM64。';; esac
33
+ VERSION=24.19.0
34
+ if [ -z "$DATA_ROOT" ]; then
35
+ if [ "$PLATFORM" = darwin ]; then DATA_ROOT="$HOME/Library/Application Support/DesktopBridge"; else DATA_ROOT="${XDG_DATA_HOME:-$HOME/.local/share}/desktop-bridge"; fi
36
+ fi
37
+ LOCAL_RUNTIME="$DATA_ROOT/runtime/node-v$VERSION-$PLATFORM-$ARCH"
38
+ LOCAL_NODE="$LOCAL_RUNTIME/bin/node"
39
+ LOCAL_NPM="$LOCAL_RUNTIME/lib/node_modules/npm/bin/npm-cli.js"
40
+ if [ -x "$LOCAL_NODE" ] && [ -f "$LOCAL_NPM" ]; then
41
+ if NODE_VERSION=$("$LOCAL_NODE" --version 2>/dev/null) && [ "$NODE_VERSION" = "v$VERSION" ] && NPM_VERSION=$("$LOCAL_NODE" "$LOCAL_NPM" --version 2>/dev/null) && [ -n "$NPM_VERSION" ]; then
42
+ exec "$LOCAL_NODE" "$SOURCE/install.mjs" "$@"
43
+ fi
44
+ fi
45
+ [ ! -e "$LOCAL_RUNTIME" ] || echo '私有 Node 或 npm 无法正常运行,将下载校验后的官方环境进行修复。' >&2
46
+ # Discover every required fallback before beginning a network request.
47
+ for TOOL in mktemp rm chmod; do command -v "$TOOL" >/dev/null 2>&1 || fail "缺少基础工具 $TOOL,无法创建和清理私有临时目录。"; done
48
+ PYTHON=''
49
+ if command -v python3 >/dev/null 2>&1 && python3 -c 'import sys; assert sys.version_info >= (3, 8); import urllib.request, ssl, hashlib, tarfile' >/dev/null 2>&1; then PYTHON=python3; fi
50
+ if command -v curl >/dev/null 2>&1; then DOWNLOADER=curl
51
+ elif command -v wget >/dev/null 2>&1; then DOWNLOADER=wget
52
+ elif [ -n "$PYTHON" ]; then DOWNLOADER=python
53
+ else fail '缺少可用下载环境:需要 curl、wget 或 Python 3(含 HTTPS 标准库)。'; fi
54
+ if command -v sha256sum >/dev/null 2>&1; then HASHER=sha256sum
55
+ elif command -v shasum >/dev/null 2>&1; then HASHER=shasum
56
+ elif [ -n "$PYTHON" ]; then HASHER=python
57
+ else fail '缺少 SHA-256 校验环境:需要 sha256sum、shasum 或 Python 3。'; fi
58
+ if command -v tar >/dev/null 2>&1 && { [ "$PLATFORM" = darwin ] || command -v gzip >/dev/null 2>&1; }; then EXTRACTOR=tar
59
+ elif [ -n "$PYTHON" ]; then EXTRACTOR=python
60
+ else fail '缺少解压环境:需要 tar(Linux 同时需要 gzip)或 Python 3;尚未开始下载。'; fi
61
+ case "$PLATFORM-$ARCH" in
62
+ darwin-x64) HASH=d1b5e999db158c62fe8f7267a4476b035d8bd93b1a605bac24a3f0dd166e3316;;
63
+ darwin-arm64) HASH=8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d;;
64
+ linux-x64) HASH=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4;;
65
+ linux-arm64) HASH=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f;;
66
+ esac
67
+ # Bootstrap only in a private directory. install.mjs owns the durable installation.
68
+ TEMP=$(mktemp -d "${TMPDIR:-/tmp}/desktop-bridge.XXXXXXXX") || fail '无法创建私有临时目录,请检查临时目录的写入权限。'
69
+ trap 'rm -rf "$TEMP"' 0
70
+ trap 'exit 129' HUP
71
+ trap 'exit 130' INT
72
+ trap 'exit 143' TERM
73
+ chmod 700 "$TEMP" || fail '无法保护临时目录权限。'
74
+ FILE="node-v$VERSION-$PLATFORM-$ARCH.tar.gz"
75
+ URL="https://nodejs.org/dist/v$VERSION/$FILE"
76
+ echo '正在准备经过 SHA-256 校验的官方 Node 24.19.0 环境…' >&2
77
+ case "$DOWNLOADER" in
78
+ curl) curl --fail --location --retry 2 --proto '=https' --proto-redir '=https' --tlsv1.2 "$URL" -o "$TEMP/runtime.tar.gz" || fail '官方环境下载失败,请检查网络、HTTPS 证书或代理设置。';;
79
+ wget) wget --https-only "$URL" -O "$TEMP/runtime.tar.gz" || fail '官方环境下载失败,请检查网络、HTTPS 证书或代理设置。';;
80
+ python) python3 - "$URL" "$TEMP/runtime.tar.gz" <<'PY' || fail '官方环境下载失败,请检查网络、HTTPS 证书或代理设置。'
81
+ import shutil, sys, urllib.parse, urllib.request
82
+ class HTTPSOnly(urllib.request.HTTPRedirectHandler):
83
+ def redirect_request(self, req, fp, code, msg, headers, newurl):
84
+ if urllib.parse.urlparse(newurl).scheme != 'https':
85
+ raise ValueError('Refusing a non-HTTPS redirect')
86
+ return super().redirect_request(req, fp, code, msg, headers, newurl)
87
+ with urllib.request.build_opener(HTTPSOnly()).open(sys.argv[1], timeout=30) as response, open(sys.argv[2], 'wb') as output:
88
+ shutil.copyfileobj(response, output)
89
+ PY
90
+ ;;
91
+ esac
92
+ case "$HASHER" in
93
+ sha256sum) HASH_OUTPUT=$(sha256sum "$TEMP/runtime.tar.gz") || fail 'SHA-256 校验工具执行失败。'; ACTUAL=${HASH_OUTPUT%% *};;
94
+ shasum) HASH_OUTPUT=$(shasum -a 256 "$TEMP/runtime.tar.gz") || fail 'SHA-256 校验工具执行失败。'; ACTUAL=${HASH_OUTPUT%% *};;
95
+ python) ACTUAL=$(python3 - "$TEMP/runtime.tar.gz" <<'PY'
96
+ import hashlib, sys
97
+ value=hashlib.sha256()
98
+ with open(sys.argv[1], 'rb') as source:
99
+ for block in iter(lambda: source.read(1024*1024), b''):
100
+ value.update(block)
101
+ print(value.hexdigest())
102
+ PY
103
+ ) || fail 'Python SHA-256 校验失败。';;
104
+ esac
105
+ [ "$ACTUAL" = "$HASH" ] || fail '官方 Node 环境校验失败(checksum mismatch),已拒绝执行。'
106
+ if [ "$EXTRACTOR" = tar ]; then tar -xzf "$TEMP/runtime.tar.gz" -C "$TEMP" || fail '官方环境解压失败,请检查磁盘空间。'
107
+ else python3 - "$TEMP/runtime.tar.gz" "$TEMP" <<'PY' || fail 'Python 安全解压失败,请检查压缩包和磁盘空间。'
108
+ import os, pathlib, sys, tarfile
109
+ root=os.path.realpath(sys.argv[2])
110
+ def inside(value):
111
+ return os.path.commonpath((root, os.path.realpath(value))) == root
112
+ with tarfile.open(sys.argv[1], 'r:gz') as archive:
113
+ for member in archive:
114
+ parts=pathlib.PurePosixPath(member.name)
115
+ target=os.path.join(root, member.name)
116
+ if parts.is_absolute() or '..' in parts.parts or not inside(target):
117
+ raise ValueError('Unsafe archive path')
118
+ if member.issym() or member.islnk():
119
+ link=os.path.join(os.path.dirname(target) if member.issym() else root, member.linkname)
120
+ if os.path.isabs(member.linkname) or not inside(link):
121
+ raise ValueError('Unsafe archive link')
122
+ elif not (member.isfile() or member.isdir()):
123
+ raise ValueError('Unsupported archive entry')
124
+ archive.extract(member, root)
125
+ # Recheck links after extracting the complete graph; never execute an escaping link.
126
+ for directory, folders, files in os.walk(root, followlinks=False):
127
+ for name in folders + files:
128
+ if not inside(os.path.join(directory, name)):
129
+ raise ValueError('Unsafe archive link graph')
130
+ PY
131
+ fi
132
+ BOOT_NODE="$TEMP/node-v$VERSION-$PLATFORM-$ARCH/bin/node"
133
+ BOOT_NPM="$TEMP/node-v$VERSION-$PLATFORM-$ARCH/lib/node_modules/npm/bin/npm-cli.js"
134
+ [ -x "$BOOT_NODE" ] && [ -f "$BOOT_NPM" ] || fail '官方环境不完整:缺少 Node 或 npm。'
135
+ NODE_VERSION=$("$BOOT_NODE" --version 2>/dev/null) && [ "$NODE_VERSION" = "v$VERSION" ] || fail '下载的 Node 无法运行或版本不匹配,请检查系统架构和执行权限。'
136
+ NPM_VERSION=$("$BOOT_NODE" "$BOOT_NPM" --version 2>/dev/null) && [ -n "$NPM_VERSION" ] || fail '下载环境中的 npm 无法运行,请重新下载。'
137
+ "$BOOT_NODE" "$SOURCE/install.mjs" "$@"
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0install.ps1" -Repair %*
3
+ if errorlevel 1 pause
@@ -0,0 +1,9 @@
1
+ @echo off
2
+ setlocal DisableDelayedExpansion
3
+ rem Manual re-authorization. The browser must confirm this device again.
4
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0control.ps1" -Action pair %*
5
+ set "RESULT=%ERRORLEVEL%"
6
+ if not "%RESULT%"=="0" echo Pairing did not finish. Check the error above and retry when ready.
7
+ echo.
8
+ pause
9
+ exit /b %RESULT%
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ setlocal DisableDelayedExpansion
3
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0control.ps1" -Action start %*
4
+ if errorlevel 1 pause
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ SOURCE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
3
+ exec /bin/sh "$SOURCE/start.sh" "$@"
@@ -0,0 +1,4 @@
1
+ @echo off
2
+ setlocal DisableDelayedExpansion
3
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0control.ps1" -Action stop %*
4
+ if errorlevel 1 pause
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ SOURCE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
3
+ exec /bin/sh "$SOURCE/stop.sh" "$@"
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0install.ps1" -Uninstall %*
3
+ if errorlevel 1 pause
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ SOURCE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
3
+ exec /bin/sh "$SOURCE/Uninstall.sh" "$@"
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+ SOURCE=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
3
+ exec /bin/sh "$SOURCE/Install.sh" --uninstall "$@"
@@ -0,0 +1,48 @@
1
+ // The only user-facing elevation helper. Never elevates without a requested action.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import {spawn} from 'node:child_process';
5
+ import {fileURLToPath} from 'node:url';
6
+ import {protectedRoot} from './admin-runtime.mjs';
7
+ const folder=path.dirname(fileURLToPath(import.meta.url));
8
+ export function psQuote(s){return "'"+String(s).replace(/'/g,"''")+"'";}
9
+ export function elevationRequest(action,{source,mode,purge=false,root=protectedRoot(),platform=process.platform}={}) {
10
+ if(!['install','start','stop','status','mode','uninstall'].includes(action))throw new Error('Unknown administrator action.');
11
+ if(action==='install'&&(!source||!['manual','background'].includes(mode)))throw new Error('Installation needs a paired source and an explicit mode.');
12
+ if(action==='mode'&&!['manual','background'].includes(mode))throw new Error('Mode must be manual or background.');
13
+ if(purge&&action!=='uninstall')throw new Error('--purge requires uninstall.');
14
+ const installer=action==='install'?path.join(source,'admin-service.mjs'):path.join(root,'admin-service.mjs');
15
+ // The protected root is intentionally unreadable to ordinary users.
16
+ // Derive the known runtime path without inspecting privileged files.
17
+ const safeNode=action==='install'?process.execPath:path.join(root,'runtime',`node-v24.19.0-${platform}-${process.arch}`,platform==='win32'?'node.exe':'bin/node');
18
+ const args=[installer,action];
19
+ if(action==='install')args.push(source,mode);
20
+ if(action==='mode')args.push(mode);
21
+ if(purge)args.push('--purge');
22
+ if(platform==='win32') {
23
+ // Windows UAC is shown through the OS; exit code is propagated to the caller.
24
+ const windowsArg=x=>'"'+String(x).replace(/(\\*)"/g,'$1$1\\"').replace(/(\\+)$/,'$1$1')+'"';
25
+ const list=psQuote(args.map(windowsArg).join(' '));
26
+ const script='$ErrorActionPreference="Stop"; $p=Start-Process -FilePath '+psQuote(safeNode)+
27
+ ' -ArgumentList '+list+' -Verb RunAs -Wait -PassThru; exit $p.ExitCode';
28
+ return {command:'powershell.exe',args:['-NoProfile','-Command',script]};
29
+ }
30
+ if(!['darwin','linux'].includes(platform))throw new Error('Unsupported platform.');
31
+ return {command:'sudo',args:['--',safeNode,...args]};
32
+ }
33
+ function run(command,args) {
34
+ return new Promise((resolve,reject)=>{
35
+ const child=spawn(command,args,{stdio:'inherit'});
36
+ child.once('error',reject);
37
+ child.once('exit',(code,signal)=>code===0?resolve():reject(new Error('Administrator authorization or operation failed: '+(signal??code))));
38
+ });
39
+ }
40
+ const argv=process.argv.slice(2);
41
+ if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){
42
+ const [action,...args]=argv;
43
+ const opts=action==='install'?{source:args[0],mode:args[1]}:action==='mode'?{mode:args[0]}:action==='uninstall'?{purge:args[0]==='--purge'}:{};
44
+ if((action==='install'&&args.length!==2)||(action==='mode'&&args.length!==1)||(action==='uninstall'&&args.length>1)||(action!=='install'&&action!=='mode'&&action!=='uninstall'&&args.length))throw new Error('Unexpected administrative arguments.');
45
+ const request=elevationRequest(action,opts);
46
+ console.log('Administrator authorization will be requested by the operating system. This does not grant Google, ChatGPT or macOS privacy permissions.');
47
+ await run(request.command,request.args);
48
+ }