@whyvix/remote 0.3.13 → 0.3.15

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
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@whyvix/remote",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "files": [
7
7
  "packages/agent/src",
8
8
  "packages/cli/index.js",
9
9
  "packages/cli/browser.js",
10
+ "packages/cli/presenter.js",
10
11
  "packages/protocol/src",
11
12
  "native/gui-helper-manifest.json",
12
13
  "native/win32-x64/whyvix-gui-helper.exe",
@@ -24,7 +25,7 @@
24
25
  "test:r3-r5": "node scripts/r3-r5-test.js",
25
26
  "test:r10-gui-window-focus": "node scripts/r10-gui-window-action-contract-test.js && node scripts/r10-gui-window-action-source-test.js && node scripts/r10-gui-window-action-agent-capability-test.js && node scripts/r10-gui-window-action-authorization-test.js && node scripts/r10-gui-window-action-loader-test.js && node scripts/r10-gui-window-action-mcp-schema-test.js && node scripts/r10-gui-window-action-cli-wiring-test.js && node scripts/r10-deployment-certify-authorization-test.js && node scripts/r10-deployment-certify-protocol-test.js && node scripts/r10-deployment-activation-response-test.js",
26
27
  "test:r10-documents": "node scripts/r10-precision-document-inspect-test.js && node scripts/r10-precision-document-compare-test.js && node scripts/r10-precision-docx-mutation-test.js && node scripts/r10-precision-xlsx-mutation-test.js && node scripts/r10-precision-pptx-mutation-test.js",
27
- "test:r10-observability": "node scripts/r10-write-preview-test.js"
28
+ "test:r10-observability": "node scripts/r10-write-preview-test.js && node scripts/r10-cli-presentation-test.js && node scripts/r10-usage-metering-test.js"
28
29
  },
29
30
  "dependencies": {
30
31
  "@modelcontextprotocol/ext-apps": "^2.0.0",
@@ -3,6 +3,13 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
 
5
5
  const DEFAULT_EXECUTABLES = null;
6
+ const WINDOWS_TOOLCHAIN_PATTERNS = [
7
+ "C:/wamp64/bin/mysql/*/bin",
8
+ "C:/wamp/bin/mysql/*/bin",
9
+ "C:/xampp/mysql/bin",
10
+ "C:/Program Files/MySQL/*/bin",
11
+ "C:/Program Files/MariaDB */bin"
12
+ ];
6
13
  const BLOCKED_SHELL_EXECUTABLES = new Set([
7
14
  "cmd", "command", "powershell", "pwsh", "sh", "bash", "dash", "zsh", "fish", "wsl"
8
15
  ]);
@@ -23,6 +30,24 @@ function normalizeRegistry(value) {
23
30
  return new Map(Object.entries(value || {}).map(([name, command]) => [String(name).toLowerCase().replace(/\.exe$/, ""), String(command)]));
24
31
  }
25
32
 
33
+ function expandWindowsRoots(patterns) {
34
+ const roots=[];
35
+ for(const pattern of patterns){
36
+ if(!pattern.includes("*")){roots.push(pattern);continue;}
37
+ const before=pattern.slice(0,pattern.indexOf("*")),after=pattern.slice(pattern.indexOf("*")+1),base=before.replace(/[\\/]$/,"");
38
+ try{for(const entry of fs.readdirSync(base,{withFileTypes:true}))if(entry.isDirectory())roots.push(path.join(base,entry.name,after));}catch{}
39
+ }
40
+ return roots;
41
+ }
42
+ function executableMatches(executable,{platform,pathValue,pathExtValue,toolchainPatterns=WINDOWS_TOOLCHAIN_PATTERNS}={}){
43
+ const dirs=String(pathValue??process.env.PATH??"").split(path.delimiter).filter(Boolean);
44
+ if(platform==="win32")dirs.push(...expandWindowsRoots(toolchainPatterns));
45
+ const extensions=platform==="win32"?(path.extname(executable.command)?[""]:String(pathExtValue??process.env.PATHEXT??".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)):[""];
46
+ const matches=[];
47
+ for(const dir of dirs)for(const ext of extensions){const candidate=path.resolve(dir,executable.command+ext);try{const stat=fs.statSync(candidate);if(stat.isFile()&&!matches.some(x=>x.toLowerCase()===candidate.toLowerCase()))matches.push(candidate);}catch{}}
48
+ return matches.slice(0,16);
49
+ }
50
+
26
51
  function resolveExecutable(registry, requested) {
27
52
  const raw = String(requested || "").trim();
28
53
  if (!raw) fail("INVALID_ARGUMENT", "command is required");
@@ -40,8 +65,8 @@ function resolveExecutable(registry, requested) {
40
65
  }
41
66
 
42
67
  export class ProcessManager {
43
- constructor({ audit=()=>{}, spawnFn=spawn, allowedExecutables=DEFAULT_EXECUTABLES, platform=process.platform, nodeExecPath=process.execPath, maxOutputBytes=262144, maxRetainedJobs=100, maxRunningJobs=16, jobRetentionMs=300000, now=()=>Date.now() }={}) {
44
- this.audit=audit; this.spawnFn=spawnFn; this.executableRegistry=normalizeRegistry(allowedExecutables); this.platform=platform; this.nodeExecPath=nodeExecPath;
68
+ constructor({ audit=()=>{}, spawnFn=spawn, allowedExecutables=DEFAULT_EXECUTABLES, platform=process.platform, nodeExecPath=process.execPath, toolchainPatterns=WINDOWS_TOOLCHAIN_PATTERNS, maxOutputBytes=262144, maxRetainedJobs=100, maxRunningJobs=16, jobRetentionMs=300000, now=()=>Date.now() }={}) {
69
+ this.audit=audit; this.spawnFn=spawnFn; this.executableRegistry=normalizeRegistry(allowedExecutables); this.platform=platform; this.nodeExecPath=nodeExecPath; this.toolchainPatterns=toolchainPatterns;
45
70
  this.maxOutputBytes=Math.max(1024,Math.min(Number(maxOutputBytes)||262144,1048576));
46
71
  this.maxRetainedJobs=Math.max(1,Math.min(Number(maxRetainedJobs)||100,1000));
47
72
  this.maxRunningJobs=Math.max(1,Math.min(Number(maxRunningJobs)||16,128));
@@ -58,14 +83,11 @@ export class ProcessManager {
58
83
 
59
84
  discover({command,pathValue=process.env.PATH||"",pathExtValue=process.env.PATHEXT||".COM;.EXE;.BAT;.CMD"}={}) {
60
85
  const executable=resolveExecutable(this.executableRegistry,command);
61
- const dirs=String(pathValue).split(path.delimiter).filter(Boolean);
62
- const extensions=this.platform==="win32"?(path.extname(executable.command)?[""]:String(pathExtValue).split(";").filter(Boolean)):[""];
63
- const matches=[];
64
- for(const dir of dirs)for(const ext of extensions){const candidate=path.resolve(dir,executable.command+ext);try{const stat=fs.statSync(candidate);if(stat.isFile()&&!matches.includes(candidate))matches.push(candidate);}catch{}}
65
- return{command:executable.name,available:matches.length>0,matches:matches.slice(0,16)};
86
+ const matches=executableMatches(executable,{platform:this.platform,pathValue,pathExtValue,toolchainPatterns:this.toolchainPatterns});
87
+ return{command:executable.name,available:matches.length>0,matches};
66
88
  }
67
89
 
68
- start({command,args=[],cwd,timeoutMs=30000,ownerId}={}) {
90
+ start({command,args=[],cwd,timeoutMs=30000,ownerId,executableMatch}={}) {
69
91
  this.#cleanup();
70
92
  const executable=resolveExecutable(this.executableRegistry,command);
71
93
  if(!Array.isArray(args)||args.some(x=>typeof x!=="string"))fail("INVALID_ARGUMENT","args must be an array of strings");
@@ -74,6 +96,13 @@ export class ProcessManager {
74
96
  args=["--no-install",...args];
75
97
  }
76
98
  let spawnCommand=executable.command;
99
+ if(executableMatch!=null){
100
+ const requested=path.resolve(String(executableMatch));
101
+ const matches=executableMatches(executable,{platform:this.platform,toolchainPatterns:this.toolchainPatterns});
102
+ const selected=matches.find(x=>path.resolve(x).toLowerCase()===requested.toLowerCase());
103
+ if(!selected)fail("LOCAL_EXEC_NOT_ALLOWED","Selected executable is not an approved discovery match");
104
+ spawnCommand=selected;
105
+ }
77
106
  if(this.platform==="win32"&&(executable.name==="npm"||executable.name==="npx")) {
78
107
  const cli=path.join(path.dirname(this.nodeExecPath),"node_modules","npm","bin",executable.name==="npm"?"npm-cli.js":"npx-cli.js");
79
108
  spawnCommand=this.nodeExecPath; args=[cli,...args];
@@ -11,7 +11,8 @@ import { loadGuiBackend } from "../agent/src/gui-backend-loader.js";
11
11
  import { runWindowsGuiHelper } from "../agent/src/windows-gui-helper-runner.js";
12
12
  import { loadGuiWindowActionBackend } from "../agent/src/gui-window-action-backend-loader.js";
13
13
  import { runWindowsGuiWindowAction } from "../agent/src/windows-gui-window-action-runner.js";
14
- import { openBrowser } from "./browser.js";
14
+ import { openBrowser } from "./browser.js";
15
+ import { createCliPresenter } from "./presenter.js";
15
16
 
16
17
  const packageRoot=join(dirname(fileURLToPath(import.meta.url)),"..","..");
17
18
  const packageMeta=JSON.parse(await readFile(join(packageRoot,"package.json"),"utf8"));
@@ -62,8 +63,8 @@ if(!credential){
62
63
  credential=await pair();
63
64
  console.log("Device authorized.");
64
65
  }
65
- console.log("WHYVIX Remote");
66
- console.log("");
66
+ const presenter=createCliPresenter();
67
+ presenter.header({version:packageMeta.version,device:os.hostname(),gateway:gatewayUrl,workspaces});
67
68
  const agent=createAgent({
68
69
  gatewayUrl,
69
70
  deviceId,
@@ -85,22 +86,9 @@ const agent=createAgent({
85
86
  else if(event?.event==="remote.exit") console.log("REMOTE EXIT",event.code,event.errorCode||"OK");
86
87
  else if(event?.event==="deploy.prepare_local"||event?.event==="deploy.stage") console.log("DEPLOY",event.event,event.profile,event.commit);
87
88
  },
88
- log:(event,tool,meta)=>{
89
- const quiet=tool==="process.output"||tool==="remote.output";
90
- if(event==="[tool:request]"&&!quiet) console.log("←",tool,meta?.path||meta?.cwd||"");
91
- else if(event==="[tool:success]"&&!quiet) console.log("✓",tool);
92
- },
93
- errorLog:(event,tool,code,message)=>{
94
- if(event==="[tool:rejected]") console.error("✗",tool,"·",code,"·",message);
95
- },
96
- onRegistered:({authorizedAccount})=>{
97
- console.log("Device :",os.hostname());
98
- console.log("Status : Connected");
99
- console.log("Authorized :",authorizedAccount||"-");
100
- console.log("");
101
- console.log("Keep this terminal open while using WHYVIX Remote.");
102
- console.log("Press Ctrl+C to disconnect.");
103
- }
89
+ log:(event,tool,meta)=>presenter.log(event,tool,meta),
90
+ errorLog:(event,tool,code,message)=>presenter.error(event,tool,code,message),
91
+ onRegistered:({authorizedAccount})=>presenter.registered(authorizedAccount)
104
92
  });
105
93
  let shuttingDown=false;
106
94
  async function shutdown(){
@@ -0,0 +1,16 @@
1
+ export function createCliPresenter({write=console.log,error=console.error}={}){
2
+ const line="─".repeat(64);
3
+ const toolLabel=(tool="")=>String(tool).replace(/^fs\./,"FILE ").replace(/^document\./,"OFFICE ").replace(/^process\./,"PROCESS ").replace(/^remote\./,"REMOTE ").replace(/^git\./,"GIT ").replace(/_/g," ").toUpperCase();
4
+ const preview=(value)=>{
5
+ const rows=String(value).split(/\r?\n/),head=rows.shift()||"[write-preview]";
6
+ write(""); write("PREVIEW :",head.replace(/^\[write-preview\]\s*/,""));
7
+ for(const row of rows)write(" "+row);
8
+ };
9
+ return{
10
+ line,toolLabel,
11
+ header({version,device,gateway,workspaces}){write("");write("WHYVIX Remote v"+String(version||"unknown"));write(line);write("Device :",device);write("Gateway :",gateway);write("Workspace :",workspaces.join(" ; "));write("Status : Connecting...");write(line);},
12
+ log(event,tool,meta={}){const quiet=tool==="process.output"||tool==="remote.output";if(String(event).startsWith("[write-preview]"))preview(event);else if(event==="[agent:connecting]")write("Status : Connecting...");else if(event==="[agent:connected]")write("Status : Gateway connected");else if(event==="[agent:disconnected]")write("Status : Disconnected");else if(event==="[agent:reconnect-scheduled]")write("Status : Reconnecting...");else if(event==="[tool:request]"&&!quiet){write("");write("REQUEST :",toolLabel(tool),meta.path||meta.cwd||"");}else if(event==="[tool:success]"&&!quiet)write("RESULT : OK ·",toolLabel(tool));},
13
+ error(event,tool,code,message){if(event==="[tool:rejected]")error("RESULT : FAILED ·",toolLabel(tool),"·",code,"·",message);else if(event==="[agent:authorization-required]")error("Status : Authorization required");else if(event==="[agent:socket-error]")error("Status : Connection error ·",tool||"");},
14
+ registered(authorizedAccount){write("Status : Connected");write("Authorized :",authorizedAccount||"-");write(line);write("Ready. Keep this terminal open while using WHYVIX Remote.");write("Press Ctrl+C to disconnect.");}
15
+ };
16
+ }