@justlovepl/desktop-bridge 0.0.0-stage → 0.2.7
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 +50 -3
- package/THIRD_PARTY_NOTICES.md +33 -0
- package/bin/desktop-bridge.mjs +118 -0
- package/bin/update.mjs +96 -0
- package/dist/client/Install.cmd +3 -0
- package/dist/client/Install.command +3 -0
- package/dist/client/Install.sh +137 -0
- package/dist/client/Pair-again.cmd +3 -0
- package/dist/client/RePair.cmd +9 -0
- package/dist/client/Start.cmd +4 -0
- package/dist/client/Start.command +3 -0
- package/dist/client/Stop.cmd +4 -0
- package/dist/client/Stop.command +3 -0
- package/dist/client/Uninstall.cmd +3 -0
- package/dist/client/Uninstall.command +3 -0
- package/dist/client/Uninstall.sh +3 -0
- package/dist/client/admin-elevate.mjs +48 -0
- package/dist/client/admin-runtime.mjs +119 -0
- package/dist/client/admin-service.mjs +152 -0
- package/dist/client/agent/agent.mjs +264 -0
- package/dist/client/agent/pair.mjs +31 -0
- package/dist/client/connection.json +1 -0
- package/dist/client/control.mjs +75 -0
- package/dist/client/control.ps1 +12 -0
- package/dist/client/environment-probe.mjs +5 -0
- package/dist/client/environment.mjs +99 -0
- package/dist/client/install-options.mjs +43 -0
- package/dist/client/install.mjs +84 -0
- package/dist/client/install.ps1 +108 -0
- package/dist/client/launch.sh +17 -0
- package/dist/client/lifecycle.mjs +47 -0
- package/dist/client/node-runtime.json +30 -0
- package/dist/client/package-lock.json +8457 -0
- package/dist/client/package.json +23 -0
- package/dist/client/runner.mjs +38 -0
- package/dist/client/start.sh +3 -0
- package/dist/client/startup.mjs +66 -0
- package/dist/client/stop.sh +3 -0
- package/dist/client/uninstall.ps1 +6 -0
- package/dist/client/windows-background.mjs +45 -0
- package/package.json +39 -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,50 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
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
|
+
Version 0.2.7 adds Windows Task Scheduler background management. npm remains the installation, upgrade and control entry point. **0.2.7 is currently a release candidate; public npm publication is pending the publisher account's one-time-password verification.**
|
|
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 # default: background, login startup enabled
|
|
12
|
+
desktop-bridge setup --mode manual # explicitly choose foreground/manual operation
|
|
13
|
+
desktop-bridge status
|
|
14
|
+
desktop-bridge start
|
|
15
|
+
desktop-bridge stop
|
|
16
|
+
desktop-bridge restart
|
|
17
|
+
desktop-bridge pair # explicitly request browser approval
|
|
18
|
+
desktop-bridge mode manual
|
|
19
|
+
desktop-bridge mode background
|
|
20
|
+
desktop-bridge repair # repairs AND re-pairs in the browser
|
|
21
|
+
desktop-bridge uninstall # preserves credentials/results unless --purge
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Installing the npm CLI does not start a service.** Run `desktop-bridge setup` to install/pair the client and select background operation by default. Use `--mode manual` for foreground operation; `--no-start` also selects manual mode. The native installers still ask the user to select a mode explicitly.
|
|
25
|
+
|
|
26
|
+
On Windows, background mode registers a Task Scheduler task for the current user with limited privileges, starts it independently of the terminal/SSH session, and starts it again on user login. The task has no runtime time limit, allows battery operation, and restarts on failures. This mode requires that user to be logged into Windows; running before login is the separate administrator service option. `stop` leaves the task registered but stops the client; `start` resumes it; `mode manual` and `uninstall` remove it. An update migrates the old login shortcut to the managed task while preserving pairing and the saved mode.
|
|
27
|
+
|
|
28
|
+
`--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**.
|
|
29
|
+
|
|
30
|
+
`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.
|
|
31
|
+
|
|
32
|
+
Update the npm CLI and then apply the local client release:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
desktop-bridge update
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
`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. It refuses to install a CLI older than the current version. `repair` also preserves the saved mode, but deliberately requires browser re-pairing approval. A revoked device always requires explicit new pairing.
|
|
39
|
+
|
|
40
|
+
## Packaging / tests
|
|
41
|
+
|
|
42
|
+
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.
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
cd npm-installer
|
|
46
|
+
npm test
|
|
47
|
+
npm pack --dry-run
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
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,118 @@
|
|
|
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
|
+
if(command==='setup'&&!remainder.includes('--mode')&&!remainder.includes('--no-start'))remainder.push('--mode','background');
|
|
55
|
+
return {command,args:remainder,root:customRoot};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function usage(){
|
|
59
|
+
return 'Desktop Bridge '+manifest.version+'\n\n'+
|
|
60
|
+
'Usage: desktop-bridge <command> [options]\n\n'+
|
|
61
|
+
' setup [--mode manual|background] [--administrator] Install and pair (default: background)\n'+
|
|
62
|
+
' start | stop | restart Manage local agent\n'+
|
|
63
|
+
' status Show local connection status\n'+
|
|
64
|
+
' pair Re-pair in your browser\n'+
|
|
65
|
+
' mode manual|background Change startup mode\n'+
|
|
66
|
+
' repair Repair (re-pairs in browser)\n'+
|
|
67
|
+
' update Update npm CLI and installed Agent\n'+
|
|
68
|
+
' uninstall [--purge] Remove local agent\n'+
|
|
69
|
+
' version | help Show CLI info\n\n'+
|
|
70
|
+
'All commands except help/version accept --root <directory>.\n'+
|
|
71
|
+
'No service is installed automatically by npm install.\n'+
|
|
72
|
+
'update upgrades the npm CLI and applies its client release while preserving pairing.\n'+
|
|
73
|
+
'Device unlink/revocation: https://mcp.ilovestudy.de/start.html\n';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function childRun(script,args){
|
|
77
|
+
if(!fs.existsSync(script))throw new Error('Packaged client is missing. Reinstall the npm package.');
|
|
78
|
+
const child=spawn(process.execPath,[script,...args],{stdio:'inherit',windowsHide:false});
|
|
79
|
+
return await new Promise((resolve,reject)=>{
|
|
80
|
+
child.once('error',reject);
|
|
81
|
+
child.once('exit',(code,signal)=>resolve(signal?128:code??1));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function main(argv=process.argv.slice(2)){
|
|
86
|
+
const parsed=parse(argv);
|
|
87
|
+
if(parsed.command==='help'){console.log(usage());return 0;}
|
|
88
|
+
if(parsed.command==='version'){console.log(manifest.version);return 0;}
|
|
89
|
+
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.');
|
|
90
|
+
const root=parsed.root??clientDataRoot();
|
|
91
|
+
const rootArgs=parsed.root?['--root',root]:[];
|
|
92
|
+
if(parsed.command==='setup')return childRun(installer,[...parsed.args,...rootArgs]);
|
|
93
|
+
if(!fs.existsSync(path.join(root,'launch.json')))throw new Error('Client is not installed at '+root+'. Run desktop-bridge setup first.');
|
|
94
|
+
if(parsed.command==='update')return performUpdate({root,packageRoot,packageName:manifest.name});
|
|
95
|
+
if(parsed.command==='repair'){
|
|
96
|
+
console.log('Repair also requires browser re-pairing approval.');
|
|
97
|
+
const {savedMode}=await import('../dist/client/install-options.mjs');
|
|
98
|
+
const mode=savedMode(root);
|
|
99
|
+
if(!mode)throw new Error('Cannot identify saved startup mode. Run setup --mode manual|background.');
|
|
100
|
+
return childRun(installer,['--repair','--mode',mode,...rootArgs]);
|
|
101
|
+
}
|
|
102
|
+
if(parsed.command==='uninstall'){
|
|
103
|
+
const status=await childRun(installer,['--uninstall',...parsed.args,...rootArgs]);
|
|
104
|
+
if(status===0)console.log('To also remove the npm CLI: npm uninstall -g '+manifest.name);
|
|
105
|
+
return status;
|
|
106
|
+
}
|
|
107
|
+
if(parsed.command==='restart'){
|
|
108
|
+
const status=await childRun(controller,['stop',...rootArgs]);
|
|
109
|
+
return status===0?childRun(controller,['start',...rootArgs]):status;
|
|
110
|
+
}
|
|
111
|
+
return childRun(controller,[parsed.command,...parsed.args,...rootArgs]);
|
|
112
|
+
}
|
|
113
|
+
if(process.argv[1]&&fs.realpathSync(path.resolve(process.argv[1]))===fs.realpathSync(fileURLToPath(import.meta.url))){
|
|
114
|
+
main().then(code=>{process.exitCode=code;},error=>{
|
|
115
|
+
console.error('Desktop Bridge:',error.message);
|
|
116
|
+
process.exitCode=1;
|
|
117
|
+
});
|
|
118
|
+
}
|
package/bin/update.mjs
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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 installedPackage=readJson(path.join(packageRoot,'package.json'));
|
|
60
|
+
if(!/^\d+\.\d+\.\d+$/.test(installedPackage.version))throw new Error('Unsupported CLI version; refusing an unsafe update.');
|
|
61
|
+
const npmCli=npmScript??findNpmCli();
|
|
62
|
+
print('Updating npm CLI (device credentials and configuration are kept outside npm)...');
|
|
63
|
+
// A locally tested newer build must not silently downgrade to an older
|
|
64
|
+
// published client. npm resolves the newest matching stable release.
|
|
65
|
+
const npmStatus=await execute(process.execPath,[npmCli,'install','--global','--no-audit','--no-fund',packageName+'@>='+installedPackage.version]);
|
|
66
|
+
if(npmStatus!==0)throw new Error('npm package update failed; local client was not reinstalled.');
|
|
67
|
+
const packageFile=path.join(packageRoot,'package.json');
|
|
68
|
+
const current=readJson(packageFile);
|
|
69
|
+
if(current.name!==packageName)throw new Error('Updated npm package identity is unexpected. Client was not modified.');
|
|
70
|
+
const newEntry=path.join(packageRoot,'bin','desktop-bridge.mjs');
|
|
71
|
+
if(!fs.existsSync(newEntry))throw new Error('Updated CLI entry missing; local client was not modified.');
|
|
72
|
+
print('npm CLI updated to '+current.version+'. Upgrading local Agent in '+previousMode+' mode...');
|
|
73
|
+
// The new package's installer reuses an existing approved device token.
|
|
74
|
+
// --no-browser means routine upgrades must not open an unsolicited login tab.
|
|
75
|
+
const installStatus=await execute(process.execPath,[
|
|
76
|
+
newEntry,'setup','--mode',previousMode,'--no-browser','--root',root
|
|
77
|
+
]);
|
|
78
|
+
if(installStatus!==0)throw new Error('Agent update failed; do not manually erase device data. Check status and retry or repair.');
|
|
79
|
+
const updatedConfig=readJson(configFile);
|
|
80
|
+
for(const [key,value] of Object.entries(previousIdentity)){
|
|
81
|
+
if(updatedConfig[key]!==value)
|
|
82
|
+
throw new Error('Authorization or account binding changed during upgrade ('+key+'). Do not restore revoked tokens; check account and re-pair if needed.');
|
|
83
|
+
}
|
|
84
|
+
const updatedSettings=readJson(settingsFile);
|
|
85
|
+
if(updatedSettings.mode!==previousMode||(updatedSettings.privilege??'standard')!==previousPrivilege)
|
|
86
|
+
throw new Error('Startup mode or privilege changed unexpectedly during upgrade. Inspect settings before restarting.');
|
|
87
|
+
if(oldJournal){
|
|
88
|
+
const updatedJournal=readJson(journalFile);
|
|
89
|
+
for(const [id,entry] of Object.entries(oldJournal)){
|
|
90
|
+
if(entry?.status==='completed'&&(!entry.time||Date.now()-entry.time<30*86400000)&&!Object.hasOwn(updatedJournal,id))
|
|
91
|
+
throw new Error('Recent completed task journal entry missing after upgrade: '+id);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
print('Update complete: npm CLI '+current.version+', device identity and '+previousMode+' startup mode preserved.');
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
@@ -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,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,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
|
+
}
|