@justlovepl/desktop-bridge 0.2.6 → 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/README.md +10 -6
- package/bin/desktop-bridge.mjs +6 -2
- package/bin/update.mjs +5 -1
- package/dist/client/admin-service.mjs +4 -4
- package/dist/client/agent/agent.mjs +2 -2
- package/dist/client/agent/pair.mjs +1 -1
- package/dist/client/control.mjs +1 -1
- package/dist/client/environment.mjs +1 -1
- package/dist/client/install.mjs +4 -4
- package/dist/client/lifecycle.mjs +5 -0
- package/dist/client/package-lock.json +2 -2
- package/dist/client/package.json +1 -1
- package/dist/client/startup.mjs +14 -8
- package/dist/client/windows-background.mjs +45 -0
- package/package.json +14 -3
package/README.md
CHANGED
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Independent npm distribution for the Desktop Bridge **local device client**. It does not publish the upstream Desktop Commander package or cloud relay.
|
|
4
4
|
|
|
5
|
-
|
|
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
6
|
|
|
7
7
|
Requires Node.js 24 and npm. Native installers for computers without Node remain available at <https://mcp.ilovestudy.de/start.html>.
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
npm install -g @justlovepl/desktop-bridge
|
|
11
|
-
desktop-bridge setup #
|
|
11
|
+
desktop-bridge setup # default: background, login startup enabled
|
|
12
|
+
desktop-bridge setup --mode manual # explicitly choose foreground/manual operation
|
|
12
13
|
desktop-bridge status
|
|
13
14
|
desktop-bridge start
|
|
14
15
|
desktop-bridge stop
|
|
@@ -20,18 +21,21 @@ desktop-bridge repair # repairs AND re-pairs in the browser
|
|
|
20
21
|
desktop-bridge uninstall # preserves credentials/results unless --purge
|
|
21
22
|
```
|
|
22
23
|
|
|
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**.
|
|
24
29
|
|
|
25
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.
|
|
26
31
|
|
|
27
32
|
Update the npm CLI and then apply the local client release:
|
|
28
33
|
|
|
29
34
|
```sh
|
|
30
|
-
|
|
31
|
-
desktop-bridge setup
|
|
35
|
+
desktop-bridge update
|
|
32
36
|
```
|
|
33
37
|
|
|
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
|
|
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.
|
|
35
39
|
|
|
36
40
|
## Packaging / tests
|
|
37
41
|
|
package/bin/desktop-bridge.mjs
CHANGED
|
@@ -51,13 +51,14 @@ export function parse(argv){
|
|
|
51
51
|
}else if(!['--administrator','--no-browser','--no-start'].includes(a))throw new Error('Unknown setup option: '+a);
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
if(command==='setup'&&!remainder.includes('--mode')&&!remainder.includes('--no-start'))remainder.push('--mode','background');
|
|
54
55
|
return {command,args:remainder,root:customRoot};
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
function usage(){
|
|
58
59
|
return 'Desktop Bridge '+manifest.version+'\n\n'+
|
|
59
60
|
'Usage: desktop-bridge <command> [options]\n\n'+
|
|
60
|
-
' setup [--mode manual|background] [--administrator] Install and pair\n'+
|
|
61
|
+
' setup [--mode manual|background] [--administrator] Install and pair (default: background)\n'+
|
|
61
62
|
' start | stop | restart Manage local agent\n'+
|
|
62
63
|
' status Show local connection status\n'+
|
|
63
64
|
' pair Re-pair in your browser\n'+
|
|
@@ -93,7 +94,10 @@ export async function main(argv=process.argv.slice(2)){
|
|
|
93
94
|
if(parsed.command==='update')return performUpdate({root,packageRoot,packageName:manifest.name});
|
|
94
95
|
if(parsed.command==='repair'){
|
|
95
96
|
console.log('Repair also requires browser re-pairing approval.');
|
|
96
|
-
|
|
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]);
|
|
97
101
|
}
|
|
98
102
|
if(parsed.command==='uninstall'){
|
|
99
103
|
const status=await childRun(installer,['--uninstall',...parsed.args,...rootArgs]);
|
package/bin/update.mjs
CHANGED
|
@@ -56,9 +56,13 @@ export async function performUpdate({
|
|
|
56
56
|
const previousPrivilege=settings.privilege??'standard';
|
|
57
57
|
const journalFile=path.join(root,'journal.json');
|
|
58
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.');
|
|
59
61
|
const npmCli=npmScript??findNpmCli();
|
|
60
62
|
print('Updating npm CLI (device credentials and configuration are kept outside npm)...');
|
|
61
|
-
|
|
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]);
|
|
62
66
|
if(npmStatus!==0)throw new Error('npm package update failed; local client was not reinstalled.');
|
|
63
67
|
const packageFile=path.join(packageRoot,'package.json');
|
|
64
68
|
const current=readJson(packageFile);
|
|
@@ -61,7 +61,7 @@ function flag(state) {
|
|
|
61
61
|
fs.writeFileSync(path.join(root,'maintenance.flag'),state,{mode:0o600});
|
|
62
62
|
}
|
|
63
63
|
function settings(mode) {
|
|
64
|
-
writePrivateJson(path.join(root,'client-settings.json'),{mode,privilege:'administrator',version:'0.2.
|
|
64
|
+
writePrivateJson(path.join(root,'client-settings.json'),{mode,privilege:'administrator',version:'0.2.7'});
|
|
65
65
|
}
|
|
66
66
|
function relocate(source,mode) {
|
|
67
67
|
const {base}=validateSource(source);
|
|
@@ -80,7 +80,7 @@ function relocate(source,mode) {
|
|
|
80
80
|
const stage=path.join(parent,'.desktop-bridge-admin-stage-'+process.pid);
|
|
81
81
|
const backup=path.join(parent,'.desktop-bridge-admin-backup-'+process.pid);
|
|
82
82
|
fs.mkdirSync(stage,{mode:0o700});
|
|
83
|
-
const selected=['.desktop-bridge-client','agent-config.json','launch.json','runner.mjs','lifecycle.mjs','control.mjs','startup.mjs','install-options.mjs','admin-runtime.mjs','admin-service.mjs','admin-elevate.mjs','runtime','releases'];
|
|
83
|
+
const selected=['.desktop-bridge-client','agent-config.json','launch.json','runner.mjs','lifecycle.mjs','control.mjs','startup.mjs','windows-background.mjs','install-options.mjs','admin-runtime.mjs','admin-service.mjs','admin-elevate.mjs','runtime','releases'];
|
|
84
84
|
try{
|
|
85
85
|
for(const entry of selected){const file=path.join(base,entry);if(fs.existsSync(file))copyTreeSafe(file,path.join(stage,entry));}
|
|
86
86
|
const oldConfig=fs.existsSync(path.join(root,'agent-config.json'))?JSON.parse(fs.readFileSync(path.join(root,'agent-config.json'),'utf8')):null;
|
|
@@ -93,7 +93,7 @@ function relocate(source,mode) {
|
|
|
93
93
|
const previous=path.join(root,name);if(fs.existsSync(previous))copyTreeSafe(previous,path.join(stage,name));
|
|
94
94
|
else if(fs.existsSync(path.join(base,name)))copyTreeSafe(path.join(base,name),path.join(stage,name));
|
|
95
95
|
}
|
|
96
|
-
writePrivateJson(path.join(stage,'client-settings.json'),{mode,privilege:'administrator',version:'0.2.
|
|
96
|
+
writePrivateJson(path.join(stage,'client-settings.json'),{mode,privilege:'administrator',version:'0.2.7'});
|
|
97
97
|
fs.writeFileSync(path.join(stage,'maintenance.flag'),'installing',{mode:0o600});
|
|
98
98
|
secureTree(stage);
|
|
99
99
|
if(fs.existsSync(root))fs.renameSync(root,backup);
|
|
@@ -146,7 +146,7 @@ if(action==='install'){
|
|
|
146
146
|
if(value&&value!=='--purge')throw new Error('Only --purge is accepted for uninstall');
|
|
147
147
|
if(purge)fs.rmSync(root,{recursive:true,force:true});
|
|
148
148
|
else{
|
|
149
|
-
for(const entry of ['runtime','releases','runner.mjs','lifecycle.mjs','control.mjs','startup.mjs','install-options.mjs','admin-runtime.mjs','admin-service.mjs','admin-elevate.mjs','launch.json'])fs.rmSync(path.join(root,entry),{recursive:true,force:true});
|
|
149
|
+
for(const entry of ['runtime','releases','runner.mjs','lifecycle.mjs','control.mjs','startup.mjs','windows-background.mjs','install-options.mjs','admin-runtime.mjs','admin-service.mjs','admin-elevate.mjs','launch.json'])fs.rmSync(path.join(root,entry),{recursive:true,force:true});
|
|
150
150
|
}
|
|
151
151
|
console.log('Administrator service uninstalled. Device revocation is separate.');
|
|
152
152
|
}
|
|
@@ -106,7 +106,7 @@ const requireRuntime=createRequire(path.join(config.runtimeDir,'package.json'));
|
|
|
106
106
|
const {Client}=await import(pathToFileURL(requireRuntime.resolve('@modelcontextprotocol/sdk/client/index.js')));
|
|
107
107
|
const {StdioClientTransport}=await import(pathToFileURL(requireRuntime.resolve('@modelcontextprotocol/sdk/client/stdio.js')));
|
|
108
108
|
const WebSocket=requireRuntime('ws');
|
|
109
|
-
const client=new Client({name:'personal-desktop-bridge-agent',version:'0.2.
|
|
109
|
+
const client=new Client({name:'personal-desktop-bridge-agent',version:'0.2.7'},{capabilities:{}});
|
|
110
110
|
const transport=new StdioClientTransport({command:process.execPath,args:[config.mcpEntry],cwd:config.workingDirectory??directory,env:{...process.env},stderr:'pipe'});
|
|
111
111
|
let ws,retryTimer,stopping=false,attempt=0,localHealthy=false,checkingHealth=false,healthFailures=0,remoteStopping=false;
|
|
112
112
|
const heartbeatPath=path.join(directory,'heartbeat.json');
|
|
@@ -221,7 +221,7 @@ function connect() {
|
|
|
221
221
|
let lastSeen=Date.now(),welcomed=false;
|
|
222
222
|
const heartbeat=setInterval(()=>{if(Date.now()-lastSeen>65000)socket.terminate();},15000);
|
|
223
223
|
socket.on('ping',()=>lastSeen=Date.now());
|
|
224
|
-
socket.on('open',()=>{send(socket,{type:'hello',protocol:2,deviceId:config.deviceId,tools,capabilities:{shutdown:true},version:'0.2.
|
|
224
|
+
socket.on('open',()=>{send(socket,{type:'hello',protocol:2,deviceId:config.deviceId,tools,capabilities:{shutdown:true},version:'0.2.7',platform:process.platform});});
|
|
225
225
|
socket.on('message',data=>{
|
|
226
226
|
lastSeen=Date.now();
|
|
227
227
|
try {
|
|
@@ -8,7 +8,7 @@ export function saveConfig(filename,config) {
|
|
|
8
8
|
export async function pairDevice(config,filename,{openBrowser=true,intervalMs}={}) {
|
|
9
9
|
const server=new URL(config.serverUrl);
|
|
10
10
|
if(server.protocol!=='https:'&&!['127.0.0.1','localhost'].includes(server.hostname))throw new Error('配对需要 HTTPS');
|
|
11
|
-
const response=await fetch(new URL('/api/pair/start',server),{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:config.deviceName??os.hostname(),platform:process.platform,version:'0.2.
|
|
11
|
+
const response=await fetch(new URL('/api/pair/start',server),{method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({name:config.deviceName??os.hostname(),platform:process.platform,version:'0.2.7'}),signal:AbortSignal.timeout(15000)});
|
|
12
12
|
if(!response.ok)throw new Error('无法开始配对,请检查网络');
|
|
13
13
|
const p=await response.json();
|
|
14
14
|
console.log('请在网页登录并确认这台电脑。配对码:'+p.userCode+'\n'+p.verificationUri);
|
package/dist/client/control.mjs
CHANGED
|
@@ -26,7 +26,7 @@ if(settings.privilege==='administrator'&&process.env.BRIDGE_ADMIN_CONTEXT!=='1')
|
|
|
26
26
|
const request=elevationRequest(action,{mode:value});
|
|
27
27
|
await run(request.command,request.args);
|
|
28
28
|
if(action==='mode'){
|
|
29
|
-
fs.writeFileSync(path.join(root,'client-settings.json'),JSON.stringify({mode:value,privilege:'administrator',version:'0.2.
|
|
29
|
+
fs.writeFileSync(path.join(root,'client-settings.json'),JSON.stringify({mode:value,privilege:'administrator',version:'0.2.7'},null,2),{mode:0o600});
|
|
30
30
|
writeInstructions(root,launch.node,value,'administrator');
|
|
31
31
|
}
|
|
32
32
|
process.exit(0);
|
|
@@ -87,7 +87,7 @@ export async function verifyLocalTools({node,release,workingDirectory}){
|
|
|
87
87
|
export async function verifyLocalToolsInProcess({node,release,workingDirectory}) {
|
|
88
88
|
console.log('正在验证本地工具可以实际执行……');
|
|
89
89
|
const {Client,StdioClientTransport,entry}=await probeModules(release);
|
|
90
|
-
const client=new Client({name:'desktop-bridge-install-check',version:'0.2.
|
|
90
|
+
const client=new Client({name:'desktop-bridge-install-check',version:'0.2.7'},{capabilities:{}});
|
|
91
91
|
const transport=new StdioClientTransport({command:node,args:[entry],cwd:workingDirectory,stderr:'pipe'});
|
|
92
92
|
try{
|
|
93
93
|
const connecting=client.connect(transport,{timeout:30000});transport.stderr?.on('data',()=>{});await connecting;
|
package/dist/client/install.mjs
CHANGED
|
@@ -38,19 +38,19 @@ if(options.stop){console.log('Client stopped. Use your installed Start shortcut
|
|
|
38
38
|
if(options.uninstall){
|
|
39
39
|
await removeStartup(root);
|
|
40
40
|
// Keep credentials and result journal by default so a reinstall can recover them.
|
|
41
|
-
for(const name of ['releases','runtime','runner.mjs','lifecycle.mjs','control.mjs','startup.mjs','install-options.mjs','admin-runtime.mjs','admin-service.mjs','admin-elevate.mjs','launch.json','run-client.cmd','heartbeat.json','START-HERE.txt','Start.cmd','Stop.cmd','RePair.cmd','Start.command','Stop.command','start.sh','stop.sh','launch.sh','control.ps1'])fs.rmSync(path.join(root,name),{recursive:true,force:true});
|
|
41
|
+
for(const name of ['releases','runtime','runner.mjs','lifecycle.mjs','control.mjs','startup.mjs','windows-background.mjs','install-options.mjs','admin-runtime.mjs','admin-service.mjs','admin-elevate.mjs','launch.json','run-client.cmd','heartbeat.json','START-HERE.txt','Start.cmd','Stop.cmd','RePair.cmd','Start.command','Stop.command','start.sh','stop.sh','launch.sh','control.ps1'])fs.rmSync(path.join(root,name),{recursive:true,force:true});
|
|
42
42
|
if(options.purge){releaseLock(installLock);fs.rmSync(root,{recursive:true,force:true});}
|
|
43
43
|
console.log('Client uninstalled. Revoke the device in device management to unlink it.');process.exit(0);
|
|
44
44
|
}
|
|
45
45
|
fs.writeFileSync(path.join(root,'.desktop-bridge-client'),'desktop-bridge-client\n',{mode:0o600});
|
|
46
46
|
if(process.versions.node.split('.')[0]!=='24')throw new Error('Use Install.cmd, Install.command or Install.sh to prepare the supported Node 24 runtime.');
|
|
47
47
|
const {node,npm}=await ensureRuntime(root,source);
|
|
48
|
-
const release=path.join(root,'releases','0.2.
|
|
48
|
+
const release=path.join(root,'releases','0.2.7'),configPath=path.join(root,'agent-config.json');
|
|
49
49
|
fs.mkdirSync(release,{recursive:true,mode:0o700});
|
|
50
50
|
for(const file of ['package.json','package-lock.json'])fs.copyFileSync(path.join(source,file),path.join(release,file));
|
|
51
51
|
const agentSource=fs.existsSync(path.join(source,'agent'))?path.join(source,'agent'):path.join(source,'..','agent');
|
|
52
52
|
fs.cpSync(agentSource,path.join(release,'agent'),{recursive:true,force:true});
|
|
53
|
-
for(const file of ['runner.mjs','lifecycle.mjs','control.mjs','startup.mjs','install-options.mjs','admin-runtime.mjs','admin-service.mjs','admin-elevate.mjs','Start.cmd','Stop.cmd','RePair.cmd','Start.command','Stop.command','start.sh','stop.sh','launch.sh','control.ps1'])fs.copyFileSync(path.join(source,file),path.join(root,file));
|
|
53
|
+
for(const file of ['runner.mjs','lifecycle.mjs','control.mjs','startup.mjs','windows-background.mjs','install-options.mjs','admin-runtime.mjs','admin-service.mjs','admin-elevate.mjs','Start.cmd','Stop.cmd','RePair.cmd','Start.command','Stop.command','start.sh','stop.sh','launch.sh','control.ps1'])fs.copyFileSync(path.join(source,file),path.join(root,file));
|
|
54
54
|
await prepareDependencies({node,npm,release,force:!!options.repair});
|
|
55
55
|
try{await verifyLocalTools({node,release,workingDirectory:root});}
|
|
56
56
|
catch{
|
|
@@ -74,7 +74,7 @@ if(adminRequested){
|
|
|
74
74
|
const helper=path.join(root,'admin-elevate.mjs');
|
|
75
75
|
const request=elevationRequest('install',{source:root,mode});
|
|
76
76
|
await run(request.command,request.args);
|
|
77
|
-
fs.writeFileSync(path.join(root,'client-settings.json'),JSON.stringify({mode,privilege:'administrator',version:'0.2.
|
|
77
|
+
fs.writeFileSync(path.join(root,'client-settings.json'),JSON.stringify({mode,privilege:'administrator',version:'0.2.7'},null,2),{mode:0o600});
|
|
78
78
|
writeInstructions(root,node,mode,'administrator');
|
|
79
79
|
}
|
|
80
80
|
console.log(adminRequested?'管理员运行已授权并配置:'+mode+'。':mode==='manual'?'安装和配对完成:手动模式。使用时运行启动命令,保持窗口,按 Ctrl+C 结束;没有后台实例或登录自启。':'安装和配对完成:后台模式。客户端现在后台运行,登录系统后自动启动。');
|
|
@@ -36,6 +36,11 @@ export function startupFile(root,platform=process.platform,env=process.env,home=
|
|
|
36
36
|
return platform==='win32'?path.join(env.APPDATA??path.join(home,'AppData','Roaming'),'Microsoft','Windows','Start Menu','Programs','Startup','Desktop Bridge.lnk'):platform==='darwin'?path.join(home,'Library','LaunchAgents','de.desktopbridge.agent.plist'):path.join(env.XDG_CONFIG_HOME??path.join(home,'.config'),'autostart','desktop-bridge.desktop');
|
|
37
37
|
}
|
|
38
38
|
export async function removeStartup(root) {
|
|
39
|
+
if(process.platform==='win32'){
|
|
40
|
+
const {windowsBackground}=await import('./windows-background.mjs');
|
|
41
|
+
await windowsBackground('remove',root);
|
|
42
|
+
return; // The Windows helper removes only a shortcut owned by this root.
|
|
43
|
+
}
|
|
39
44
|
const file=startupFile(root);
|
|
40
45
|
if(process.platform==='darwin'&&fs.existsSync(file))try{await run('launchctl',['bootout','gui/'+process.getuid(),file],{stdio:'ignore'});}catch{}
|
|
41
46
|
fs.rmSync(file,{force:true});
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "desktop-bridge-client-runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "desktop-bridge-client-runtime",
|
|
9
|
-
"version": "0.2.
|
|
9
|
+
"version": "0.2.7",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
12
12
|
"@wonderwhy-er/desktop-commander": "0.3.0",
|
package/dist/client/package.json
CHANGED
package/dist/client/startup.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import {spawn} from 'node:child_process';
|
|
4
|
-
import {run,startupFile,removeStartup,stopClient,livePid} from './lifecycle.mjs';
|
|
4
|
+
import {run,startupFile,removeStartup,stopClient,livePid,sleep} from './lifecycle.mjs';
|
|
5
5
|
export const shellQuote=value=>"'"+String(value).replace(/'/g,"'\\''")+"'";
|
|
6
6
|
export const powershellQuote=value=>"'"+String(value).replace(/'/g,"''")+"'";
|
|
7
7
|
const xml=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
@@ -18,11 +18,8 @@ export function startupContent(node,root,platform=process.platform) {
|
|
|
18
18
|
export async function registerStartup(node,root) {
|
|
19
19
|
const file=startupFile(root);fs.mkdirSync(path.dirname(file),{recursive:true});
|
|
20
20
|
if(process.platform==='win32'){
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
const quote=value=>'"'+value.replace(/(\\*)"/g,'$1$1\\"').replace(/(\\+)$/,'$1$1')+'"';
|
|
24
|
-
const argv=[path.join(root,'runner.mjs'),root].map(quote).join(' ');
|
|
25
|
-
await run('powershell.exe',['-NoProfile','-NonInteractive','-Command',`$s=(New-Object -ComObject WScript.Shell).CreateShortcut(${ps(file)});$s.TargetPath=${ps(node)};$s.Arguments=${ps(argv)};$s.WorkingDirectory=${ps(root)};$s.WindowStyle=7;$s.Save()`]);
|
|
21
|
+
const {windowsBackground}=await import('./windows-background.mjs');
|
|
22
|
+
await windowsBackground('register',root,node);
|
|
26
23
|
}else{
|
|
27
24
|
fs.writeFileSync(file,startupContent(node,root));
|
|
28
25
|
if(process.platform==='darwin'){
|
|
@@ -32,7 +29,7 @@ export async function registerStartup(node,root) {
|
|
|
32
29
|
}
|
|
33
30
|
}
|
|
34
31
|
export function writeInstructions(root,node,mode,privilege='standard') {
|
|
35
|
-
const contents=`Desktop Bridge 0.2.
|
|
32
|
+
const contents=`Desktop Bridge 0.2.7 使用说明\n\n权限:${privilege==='administrator'?'管理员(操作将由系统提示授权;后台使用受保护系统服务)':'当前用户'}\n当前模式:${mode==='manual'?'手动 manual':'后台 background'}\n${mode==='manual'?'使用时运行启动命令,保持窗口,按 Ctrl+C 或关闭窗口结束。客户端与本地工具进程会一起停止。未设置登录自启。':'客户端现在后台运行,登录系统后自动启动。这是当前用户登录后自启,不是登录前的系统服务。'}\n\n${process.platform==='win32'?'请将以下命令复制到 PowerShell。':'请将以下命令复制到终端。'}\n\n启动:\n${commandFor(node,root,'start')}\n\n停止:\n${commandFor(node,root,'stop')}\n\n状态:\n${commandFor(node,root,'status')}\n\n切换为手动模式(停止后台实例,移除登录自启):\n${commandFor(node,root,'mode manual')}\n\n切换为后台模式(现在启动,登录系统后自动启动):\n${commandFor(node,root,'mode background')}\n\n本目录中的快捷入口:${process.platform==='win32'?'Start.cmd / Stop.cmd':process.platform==='darwin'?'Start.command / Stop.command':'start.sh / stop.sh'}。\n启动、停止和切换模式使用已安装的私有环境,不会重新安装依赖。\n\n重新配对(旧 Token 失效时):\n${commandFor(node,root,'pair')}\n${process.platform==='win32'?'或双击 RePair.cmd。':'在终端执行上述命令。'}\n配对必须由用户在网页重新确认。\n如果出现 authStatus=re_pair_required,请先重新配对。\n\n请保留本目录,其中保存设备凭据、日志与可恢复的任务结果。停止不会删除这些数据。\n`;
|
|
36
33
|
fs.writeFileSync(path.join(root,'START-HERE.txt'),contents+(privilege==='administrator'?'\n管理员模式下,普通用户不能修改后台程序文件。系统隐私、Google 登录与 ChatGPT 工具确认仍需分别授权。\n':''),{mode:0o600});
|
|
37
34
|
}
|
|
38
35
|
export async function startBackground(root,node) {
|
|
@@ -40,6 +37,15 @@ export async function startBackground(root,node) {
|
|
|
40
37
|
if(auth.authStatus==='re_pair_required')throw new Error('设备需要重新配对。请先执行 RePair.cmd 或 control.mjs pair。');
|
|
41
38
|
if(livePid(path.join(root,'runner-lock','pid'))){console.log('Client is already running.');return;}
|
|
42
39
|
fs.rmSync(path.join(root,'maintenance.flag'),{force:true});
|
|
40
|
+
if(process.platform==='win32'){
|
|
41
|
+
const {windowsBackground}=await import('./windows-background.mjs');
|
|
42
|
+
await windowsBackground('start',root,node);
|
|
43
|
+
for(let i=0;i<60;i++){
|
|
44
|
+
if(livePid(path.join(root,'runner-lock','pid')))return;
|
|
45
|
+
await sleep(250);
|
|
46
|
+
}
|
|
47
|
+
throw new Error('后台任务尚未启动。当前用户需已登录 Windows;请检查任务计划程序后重试。');
|
|
48
|
+
}
|
|
43
49
|
const child=spawn(node,[path.join(root,'runner.mjs'),root],{detached:true,stdio:'ignore'});
|
|
44
50
|
await new Promise((resolve,reject)=>{child.once('spawn',resolve);child.once('error',reject);});child.unref();
|
|
45
51
|
}
|
|
@@ -47,7 +53,7 @@ export async function setMode(root,node,mode) {
|
|
|
47
53
|
if(!['manual','background'].includes(mode))throw new Error('Mode must be manual or background');
|
|
48
54
|
await stopClient(root);
|
|
49
55
|
await removeStartup(root);
|
|
50
|
-
const settings={mode,version:'0.2.
|
|
56
|
+
const settings={mode,version:'0.2.7'};
|
|
51
57
|
fs.writeFileSync(path.join(root,'client-settings.json.tmp'),JSON.stringify(settings,null,2),{mode:0o600});
|
|
52
58
|
fs.renameSync(path.join(root,'client-settings.json.tmp'),path.join(root,'client-settings.json'));
|
|
53
59
|
writeInstructions(root,node,mode);
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {createHash} from 'node:crypto';
|
|
4
|
+
import {execFile} from 'node:child_process';
|
|
5
|
+
import {promisify} from 'node:util';
|
|
6
|
+
|
|
7
|
+
const execute=promisify(execFile);
|
|
8
|
+
const ps=value=>"'"+String(value).replace(/'/g,"''")+"'";
|
|
9
|
+
const quote=value=>'"'+String(value).replace(/(\\*)"/g,'$1$1\\"').replace(/(\\+)$/,'$1$1')+'"';
|
|
10
|
+
export function backgroundTaskName(root){
|
|
11
|
+
return 'DesktopBridge-User-'+createHash('sha256').update(path.win32.resolve(root).toLowerCase()).digest('hex').slice(0,20);
|
|
12
|
+
}
|
|
13
|
+
export function backgroundTaskScript(action,root,node){
|
|
14
|
+
if(!['register','start','remove'].includes(action))throw new Error('Invalid background task action');
|
|
15
|
+
const name=ps(backgroundTaskName(root));
|
|
16
|
+
const args=[path.win32.join(root,'runner.mjs'),root].map(quote).join(' ');
|
|
17
|
+
const migrateShortcut="$legacy=Join-Path $env:APPDATA 'Microsoft\\Windows\\Start Menu\\Programs\\Startup\\Desktop Bridge.lnk';"+
|
|
18
|
+
"if(Test-Path -LiteralPath $legacy){$link=(New-Object -ComObject WScript.Shell).CreateShortcut($legacy);"+
|
|
19
|
+
`if($link.Arguments -eq ${ps(args)} -and $link.WorkingDirectory -eq ${ps(root)}){Remove-Item -LiteralPath $legacy}};`;
|
|
20
|
+
const preamble="$ErrorActionPreference='Stop';$ProgressPreference='SilentlyContinue';"+
|
|
21
|
+
"$identity=[System.Security.Principal.WindowsIdentity]::GetCurrent();$sid=$identity.User.Value;"+
|
|
22
|
+
`$task=Get-ScheduledTask -TaskName ${name} -ErrorAction SilentlyContinue;`+
|
|
23
|
+
// A deterministic name must never let one user replace another user's task.
|
|
24
|
+
"if($task){$owner=$task.Principal.UserId;if($owner -notmatch '^S-1-'){$owner=([System.Security.Principal.NTAccount]$owner).Translate([System.Security.Principal.SecurityIdentifier]).Value};"+
|
|
25
|
+
"if($owner -ne $sid){throw 'Background task belongs to another Windows account'}};";
|
|
26
|
+
if(action==='remove')return preamble+`if($task){Stop-ScheduledTask -TaskName ${name};Unregister-ScheduledTask -TaskName ${name} -Confirm:$false};`+migrateShortcut+'exit 0';
|
|
27
|
+
const register=`$a=New-ScheduledTaskAction -Execute ${ps(node)} -Argument ${ps(args)} -WorkingDirectory ${ps(root)};`+
|
|
28
|
+
"$p=New-ScheduledTaskPrincipal -UserId $sid -LogonType Interactive -RunLevel Limited;"+
|
|
29
|
+
"$t=New-ScheduledTaskTrigger -AtLogOn -User $sid;"+
|
|
30
|
+
"$s=New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit ([TimeSpan]::Zero) -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -StartWhenAvailable -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries;"+
|
|
31
|
+
`Register-ScheduledTask -TaskName ${name} -Action $a -Trigger $t -Principal $p -Settings $s -Force|Out-Null;`;
|
|
32
|
+
// Refresh the action on installation; ordinary start reuses the registered task.
|
|
33
|
+
if(action==='register')return preamble+register+migrateShortcut;
|
|
34
|
+
return preamble+`if(-not $task){${register}}Start-ScheduledTask -TaskName ${name}`;
|
|
35
|
+
}
|
|
36
|
+
export async function windowsBackground(action,root,node){
|
|
37
|
+
const canonical=fs.realpathSync(root);
|
|
38
|
+
const script=backgroundTaskScript(action,canonical,node);
|
|
39
|
+
const encoded=Buffer.from(script,'utf16le').toString('base64');
|
|
40
|
+
try{
|
|
41
|
+
await execute('powershell.exe',['-NoLogo','-NoProfile','-NonInteractive','-EncodedCommand',encoded],{windowsHide:true,timeout:30000,maxBuffer:1024*1024});
|
|
42
|
+
}catch(error){
|
|
43
|
+
throw new Error('Windows background task '+action+' failed: '+String(error.stderr||error.message).slice(0,1500));
|
|
44
|
+
}
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justlovepl/desktop-bridge",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "Cross-platform Desktop Bridge agent installer and lifecycle CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
"bin": {
|
|
14
14
|
"desktop-bridge": "./bin/desktop-bridge.mjs"
|
|
15
15
|
},
|
|
16
|
-
"files": [
|
|
16
|
+
"files": [
|
|
17
|
+
"bin/",
|
|
18
|
+
"dist/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"THIRD_PARTY_NOTICES.md"
|
|
21
|
+
],
|
|
17
22
|
"engines": {
|
|
18
23
|
"node": ">=24 <25"
|
|
19
24
|
},
|
|
@@ -24,5 +29,11 @@
|
|
|
24
29
|
"publishConfig": {
|
|
25
30
|
"access": "public"
|
|
26
31
|
},
|
|
27
|
-
"keywords": [
|
|
32
|
+
"keywords": [
|
|
33
|
+
"desktop-bridge",
|
|
34
|
+
"mcp",
|
|
35
|
+
"remote-desktop",
|
|
36
|
+
"agent",
|
|
37
|
+
"command-line"
|
|
38
|
+
]
|
|
28
39
|
}
|