webhelm 1.0.5 → 1.0.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.
package/package.json
CHANGED
package/runtime/cli.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { ensureDaemon, request, handle, SOCKET } from './node-daemon.mjs';
|
|
|
8
8
|
import { PLATFORMS, listPlatforms, platformProfile, actionPath, validateConfig } from './platforms.mjs';
|
|
9
9
|
import { ACTIONS_DIR, ensureActions, syncActions, publishActions } from './actions-repo.mjs';
|
|
10
10
|
import { migrateConfig, migrateChromeProfile } from './migrate.mjs';
|
|
11
|
-
import { installLoginGuard } from './login-guard.mjs';
|
|
11
|
+
import { installLoginGuard, stageRuntimeForLoginGuard } from './login-guard.mjs';
|
|
12
12
|
|
|
13
13
|
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const emit = value => console.log(JSON.stringify(value));
|
|
@@ -25,6 +25,7 @@ Usage:
|
|
|
25
25
|
webhelm migrate # after stopping Chrome and the old WebHelm daemon
|
|
26
26
|
webhelm setup
|
|
27
27
|
webhelm status
|
|
28
|
+
webhelm ensure # verify/recover managed Chrome and Harness, not just daemon ping
|
|
28
29
|
webhelm capacity [--workspace NAME]
|
|
29
30
|
webhelm session start [--url URL]
|
|
30
31
|
webhelm session list
|
|
@@ -109,9 +110,11 @@ async function main(args = process.argv.slice(2)) {
|
|
|
109
110
|
if (group === 'setup') {
|
|
110
111
|
const repo=await ensureActions();
|
|
111
112
|
const chrome=await ensureDaemon();
|
|
112
|
-
const
|
|
113
|
+
const persisted=stageRuntimeForLoginGuard();
|
|
114
|
+
const login_guard=installLoginGuard({cliPath:path.join(persisted.path,'bin/webhelm.mjs'),nodePath:process.execPath});
|
|
113
115
|
return emit({status:'ready',...repo,daemon_pid:chrome.pid,managed_chrome:chrome.managed_chrome,login_guard});
|
|
114
116
|
}
|
|
117
|
+
if (group === 'ensure') return emit(await request({op:'guard-check'}));
|
|
115
118
|
if (group === 'status') {
|
|
116
119
|
const daemon = await ensureDaemon();
|
|
117
120
|
return emit({version:daemon.version, platform_dir:PLATFORMS, root:ROOT,
|
package/runtime/config-paths.mjs
CHANGED
|
@@ -2,9 +2,9 @@ import os from 'node:os';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
|
|
5
|
-
export const CONFIG_ROOT = path.join(os.homedir(), '.config', 'webhelm');
|
|
6
|
-
export const LEGACY_ROOT = path.join(os.homedir(), '.config', 'browser-workspace');
|
|
7
|
-
export const LOCAL_ROOT = path.join(os.homedir(), '.local', 'webhelm');
|
|
5
|
+
export const CONFIG_ROOT = path.resolve(process.env.WEBHELM_CONFIG_ROOT || path.join(os.homedir(), '.config', 'webhelm'));
|
|
6
|
+
export const LEGACY_ROOT = path.resolve(process.env.WEBHELM_LEGACY_ROOT || (process.env.WEBHELM_CONFIG_ROOT ? path.join(CONFIG_ROOT,'legacy-browser-workspace') : path.join(os.homedir(), '.config', 'browser-workspace')));
|
|
7
|
+
export const LOCAL_ROOT = path.resolve(process.env.WEBHELM_LOCAL_ROOT || path.join(os.homedir(), '.local', 'webhelm'));
|
|
8
8
|
export const OLD_PROFILE_DIR = path.join(CONFIG_ROOT, 'chrome');
|
|
9
9
|
export const PROFILE_DIR = path.join(LOCAL_ROOT, 'chrome');
|
|
10
10
|
export const RUNTIME_DIR = path.join(CONFIG_ROOT, 'runtime');
|
|
@@ -3,9 +3,10 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
5
|
import {fileURLToPath} from 'node:url';
|
|
6
|
+
import {LOCAL_ROOT} from './config-paths.mjs';
|
|
6
7
|
|
|
7
8
|
export const EXTENSION_ID='kkkpgmhpoeceojalkpfdidffdfnlgdog';
|
|
8
|
-
export const EXTENSION_HOME=path.resolve(process.env.WEBHELM_EXTENSION_HOME || path.join(
|
|
9
|
+
export const EXTENSION_HOME=path.resolve(process.env.WEBHELM_EXTENSION_HOME || path.join(LOCAL_ROOT,'extension'));
|
|
9
10
|
const SOURCE=fileURLToPath(new URL('./extension/',import.meta.url));
|
|
10
11
|
const FILES=['manifest.json','service-worker.mjs','workspace-manager.mjs','workspace.html','workspace-page.mjs'];
|
|
11
12
|
|
package/runtime/login-guard.mjs
CHANGED
|
@@ -2,23 +2,57 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import {execFileSync} from 'node:child_process';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
5
6
|
|
|
6
7
|
const LABEL='com.lalalic.webhelm.keepalive';
|
|
8
|
+
// launchd must never depend on npm's removable npx cache, or on the Skill
|
|
9
|
+
// launcher being present. Stage an immutable, package-backed runtime locally.
|
|
10
|
+
export function stageRuntimeForLoginGuard({home=os.homedir(),source=fileURLToPath(new URL('../',import.meta.url)),yamlSource=null}={}){
|
|
11
|
+
const manifest=JSON.parse(fs.readFileSync(path.join(source,'package.json'),'utf8'));
|
|
12
|
+
if(manifest.name!=='webhelm'||!/^\d+\.\d+\.\d+$/.test(manifest.version))throw Error('Invalid WebHelm runtime package');
|
|
13
|
+
const parent=path.join(home,'.local/share/webhelm');
|
|
14
|
+
const target=path.join(parent,`runtime-${manifest.version}`);
|
|
15
|
+
const keyfiles=['package.json','bin/webhelm.mjs','runtime/cli.mjs','runtime/node-daemon.mjs','runtime/login-guard.mjs'];
|
|
16
|
+
const equal=()=>keyfiles.every(name=>fs.existsSync(path.join(target,name))&&fs.readFileSync(path.join(target,name)).equals(fs.readFileSync(path.join(source,name))))&&fs.existsSync(path.join(target,'node_modules/yaml/package.json'));
|
|
17
|
+
if(equal())return {path:target,version:manifest.version,updated:false};
|
|
18
|
+
const yamlDir=yamlSource||path.resolve(fileURLToPath(import.meta.resolve('yaml')),'../..');
|
|
19
|
+
if(!fs.existsSync(path.join(yamlDir,'package.json')))throw Error('Bundled Runtime yaml dependency missing');
|
|
20
|
+
fs.mkdirSync(parent,{recursive:true,mode:0o700});
|
|
21
|
+
const tmp=fs.mkdtempSync(path.join(parent,'.webhelm-runtime-stage-'));
|
|
22
|
+
try{
|
|
23
|
+
for(const dirname of ['bin','runtime'])fs.cpSync(path.join(source,dirname),path.join(tmp,dirname),{recursive:true});
|
|
24
|
+
fs.copyFileSync(path.join(source,'package.json'),path.join(tmp,'package.json'));
|
|
25
|
+
fs.mkdirSync(path.join(tmp,'node_modules'),{recursive:true});
|
|
26
|
+
fs.cpSync(yamlDir,path.join(tmp,'node_modules/yaml'),{recursive:true});
|
|
27
|
+
let old=null;
|
|
28
|
+
if(fs.existsSync(target)){
|
|
29
|
+
old=path.join(parent,`.webhelm-runtime-old-${process.pid}-${Date.now()}`);
|
|
30
|
+
fs.renameSync(target,old);
|
|
31
|
+
}
|
|
32
|
+
try{fs.renameSync(tmp,target);}catch(error){if(old)fs.renameSync(old,target);throw error;}
|
|
33
|
+
if(old)fs.rmSync(old,{recursive:true,force:true});
|
|
34
|
+
return {path:target,version:manifest.version,updated:true};
|
|
35
|
+
}finally{if(fs.existsSync(tmp))fs.rmSync(tmp,{recursive:true,force:true});}
|
|
36
|
+
}
|
|
37
|
+
|
|
7
38
|
const escapeXml=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
8
|
-
export function loginGuardDefinition(home=os.homedir()) {
|
|
9
|
-
const cli=
|
|
39
|
+
export function loginGuardDefinition(home=os.homedir(),{cliPath=path.join(home,'.local/bin/webhelm'),nodePath=null}={}) {
|
|
40
|
+
const cli=cliPath;
|
|
41
|
+
const program=nodePath||cli;
|
|
42
|
+
const args=nodePath?`<string>${escapeXml(cli)}</string><string>ensure</string>`:'<string>ensure</string>';
|
|
10
43
|
const environment=[path.join(home,'.local/bin'),path.join(home,'.local/opt/node22/bin'),
|
|
11
44
|
'/opt/homebrew/bin','/usr/local/bin','/usr/bin','/bin'].join(':');
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
45
|
+
// Use the cached, tested npm Runtime directly to avoid registry/network
|
|
46
|
+
// dependency at login or when Chrome has just auto-updated. This must call
|
|
47
|
+
// 'ensure', not 'status': a live daemon does not imply a live Chrome.
|
|
48
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>\n<key>Label</key><string>${LABEL}</string>\n<key>ProgramArguments</key><array><string>${escapeXml(program)}</string>${args}</array>\n<key>EnvironmentVariables</key><dict><key>PATH</key><string>${escapeXml(environment)}</string><key>HOME</key><string>${escapeXml(home)}</string></dict>\n<key>RunAtLoad</key><true/>\n<key>StartInterval</key><integer>30</integer>\n<key>StandardOutPath</key><string>/dev/null</string>\n<key>StandardErrorPath</key><string>/dev/null</string>\n</dict></plist>\n`;
|
|
15
49
|
}
|
|
16
|
-
export function installLoginGuard({home=os.homedir(), platform=process.platform, apply=true}={}){
|
|
50
|
+
export function installLoginGuard({home=os.homedir(), platform=process.platform, apply=true,cliPath,nodePath}={}){
|
|
17
51
|
if(platform!=='darwin')return {status:'unsupported',label:LABEL};
|
|
18
|
-
const cli=path.join(home,'.local/bin/webhelm');
|
|
19
|
-
if(!fs.existsSync(cli))return {status:'skipped',reason:'
|
|
52
|
+
const cli=cliPath||path.join(home,'.local/bin/webhelm');
|
|
53
|
+
if(!fs.existsSync(cli))return {status:'skipped',reason:'WebHelm runtime launcher missing',label:LABEL};
|
|
20
54
|
const dest=path.join(home,'Library/LaunchAgents',`${LABEL}.plist`);
|
|
21
|
-
const content=loginGuardDefinition(home);
|
|
55
|
+
const content=loginGuardDefinition(home,{cliPath,nodePath});
|
|
22
56
|
fs.mkdirSync(path.dirname(dest),{recursive:true});
|
|
23
57
|
if(fs.existsSync(dest)){
|
|
24
58
|
const old=fs.readFileSync(dest,'utf8');
|
|
@@ -2,15 +2,18 @@
|
|
|
2
2
|
// close a user's Chrome windows to recover the managed browser.
|
|
3
3
|
export function createManagedChromeGuard(check, {intervalMs=15000, onError=()=>{}}={}) {
|
|
4
4
|
if(typeof check!=='function')throw TypeError('health check must be a function');
|
|
5
|
-
let timer=null,
|
|
5
|
+
let timer=null, inFlight=null, stopped=false;
|
|
6
6
|
let last={status:'not_checked'};
|
|
7
|
-
const tick=
|
|
8
|
-
if(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
const tick=()=>{
|
|
8
|
+
if(stopped)return Promise.resolve(last);
|
|
9
|
+
if(inFlight)return inFlight;
|
|
10
|
+
inFlight=(async()=>{
|
|
11
|
+
try{last={status:'healthy',...(await check())};}
|
|
12
|
+
catch(error){last={status:'degraded',error:String(error?.message||error)};onError(error);}
|
|
13
|
+
finally{inFlight=null;}
|
|
14
|
+
return last;
|
|
15
|
+
})();
|
|
16
|
+
return inFlight;
|
|
14
17
|
};
|
|
15
18
|
return {
|
|
16
19
|
tick,
|
package/runtime/node-daemon.mjs
CHANGED
|
@@ -575,7 +575,7 @@ async function runPlatform(req) {
|
|
|
575
575
|
self_heal_capable:!auto,result};
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
-
export async function handle(req){switch(req.op){case'ping':return {ok:true,pid:process.pid,version:PACKAGE_VERSION,queue_paused:process.env.WEBHELM_CHATGPT_QUEUE_PAUSED==='1',session_count:sessions.size,managed_chrome:managedChrome||{profile_dir:PROFILE_DIR, endpoint:process.env.WEBHELM_CDP_URL||'http://127.0.0.1:9222',reused:true},chrome_guard:managedChromeGuard.status()};case'heartbeat':return heartbeatSession(req.session_id);case'list':return {session_count:sessions.size,sessions:[...sessions.values()].map(s=>({session_id:s.session_id,workspace:s.workspace,target_id:s.targetId,last_used_at:s.last_used_at}))};case'start':return createSession(req);case'exec':return execute(req);case'stop':return stopSession(req.session_id);case'stop-all':{const stopped=[];for(const id of [...sessions.keys()])stopped.push(await stopSession(id));return {stopped_count:stopped.length,error_count:0,stopped,errors:[]}}case'capacity':return workspaceCapacity(req);case'workspace-create':case'workspace-delete':return workspaceOp(req);case'chatgpt-new-turn-enqueue':return getChatGPTQueue().enqueue(req.config);case'platform-run':return runPlatform(req);default:throw Error(`unknown op: ${req.op}`)}}
|
|
578
|
+
export async function handle(req){switch(req.op){case'ping':return {ok:true,pid:process.pid,version:PACKAGE_VERSION,queue_paused:process.env.WEBHELM_CHATGPT_QUEUE_PAUSED==='1',session_count:sessions.size,managed_chrome:managedChrome||{profile_dir:PROFILE_DIR, endpoint:process.env.WEBHELM_CDP_URL||'http://127.0.0.1:9222',reused:true},chrome_guard:managedChromeGuard.status()};case'guard-check':{let status=await managedChromeGuard.tick();for(let retry=0;retry<6 && status.status!=='healthy' && !status.skipped;retry++){await delay(800);status=await managedChromeGuard.tick();}if(status.status!=='healthy' && !status.skipped)throw Error(`WebHelm Chrome health degraded: ${status.error||status.status}`);return {ok:true,chrome_guard:status};}case'heartbeat':return heartbeatSession(req.session_id);case'list':return {session_count:sessions.size,sessions:[...sessions.values()].map(s=>({session_id:s.session_id,workspace:s.workspace,target_id:s.targetId,last_used_at:s.last_used_at}))};case'start':return createSession(req);case'exec':return execute(req);case'stop':return stopSession(req.session_id);case'stop-all':{const stopped=[];for(const id of [...sessions.keys()])stopped.push(await stopSession(id));return {stopped_count:stopped.length,error_count:0,stopped,errors:[]}}case'capacity':return workspaceCapacity(req);case'workspace-create':case'workspace-delete':return workspaceOp(req);case'chatgpt-new-turn-enqueue':return getChatGPTQueue().enqueue(req.config);case'platform-run':return runPlatform(req);default:throw Error(`unknown op: ${req.op}`)}}
|
|
579
579
|
function serve(conn) {
|
|
580
580
|
let buffer = '';
|
|
581
581
|
conn.setEncoding('utf8');
|