@whyvix/remote 0.3.1 → 0.3.2
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 +1 -1
- package/packages/agent/src/agent.js +6 -4
- package/packages/agent/src/deployment-manager.js +15 -0
- package/packages/agent/src/deployment-profile-loader.js +22 -0
- package/packages/agent/src/process-manager.js +9 -3
- package/packages/agent/src/tools.js +13 -7
- package/packages/cli/index.js +24 -7
package/package.json
CHANGED
|
@@ -5,14 +5,16 @@ import { envelope, parseMessage, TYPES } from "../../protocol/src/index.js";
|
|
|
5
5
|
import { createSecurity } from "./security.js";
|
|
6
6
|
import { ProcessManager } from "./process-manager.js";
|
|
7
7
|
import { RemoteExecutionManager } from "./remote-execution-manager.js";
|
|
8
|
+
import { DeploymentManager } from "./deployment-manager.js";
|
|
8
9
|
import { createToolExecutor } from "./tools.js";
|
|
9
10
|
|
|
10
|
-
export function createAgent({gatewayUrl="ws://127.0.0.1:8787",deviceId=randomUUID(),deviceName=os.hostname(),token="",workspaces=[process.cwd()],remoteTargets=[],reconnectMs=1000,reconnect=true,audit=()=>{},log=console.log,errorLog=console.error,onRegistered=()=>{}}={}) {
|
|
11
|
+
export function createAgent({gatewayUrl="ws://127.0.0.1:8787",deviceId=randomUUID(),deviceName=os.hostname(),token="",workspaces=[process.cwd()],remoteTargets=[],deploymentProfiles=[],agentVersion="unknown",capabilities=[],reconnectMs=1000,reconnect=true,audit=()=>{},log=console.log,errorLog=console.error,onRegistered=()=>{}}={}) {
|
|
11
12
|
let ws=null,reconnectTimer=null,stopped=false,generation=0;
|
|
12
13
|
const security=createSecurity({token,workspaces,audit});
|
|
13
14
|
const processManager=new ProcessManager({audit});
|
|
14
15
|
const remoteExecutionManager=new RemoteExecutionManager({targets:remoteTargets,audit});
|
|
15
|
-
const
|
|
16
|
+
const deploymentManager=new DeploymentManager({profiles:deploymentProfiles,security,audit});
|
|
17
|
+
const executeTool=createToolExecutor({security,processManager,remoteExecutionManager,deploymentManager});
|
|
16
18
|
function scheduleReconnect(reason){
|
|
17
19
|
if(stopped||!reconnect||reconnectTimer)return;
|
|
18
20
|
log("[agent:reconnect-scheduled]",reason,reconnectMs);
|
|
@@ -24,8 +26,8 @@ export function createAgent({gatewayUrl="ws://127.0.0.1:8787",deviceId=randomUUI
|
|
|
24
26
|
const current=++generation;
|
|
25
27
|
const socket=new WebSocket(gatewayUrl); ws=socket;
|
|
26
28
|
log("[agent:connecting]",gatewayUrl,"generation="+current);
|
|
27
|
-
socket.on("open",()=>{if(ws!==socket)return;log("[agent:connected]",gatewayUrl);socket.send(JSON.stringify(envelope(TYPES.REGISTER,{auth:{token},device:{id:deviceId,name:deviceName,platform:process.platform,agentVersion:
|
|
28
|
-
socket.on("message",async(raw)=>{if(ws!==socket)return;try{const m=parseMessage(raw);if(m.type===TYPES.REGISTERED){log("[agent:registered]",m.deviceId);onRegistered({deviceId:m.deviceId,authorizedAccount:m.authorizedAccount||""});return;}if(m.type!==TYPES.REQUEST)return;log("[tool:request]",m.tool);try{const result=await executeTool(m.tool,m.arguments);if(m.tool==="device.ping")Object.assign(result,{deviceId,deviceName});log("[tool:success]",m.tool);if(socket.readyState===WebSocket.OPEN)socket.send(JSON.stringify(envelope(TYPES.RESPONSE,{requestId:m.requestId,result})));}catch(error){errorLog("[tool:rejected]",m.tool,error.code||"TOOL_ERROR",error.message);if(socket.readyState===WebSocket.OPEN)socket.send(JSON.stringify(envelope(TYPES.ERROR,{requestId:m.requestId,error:{code:error.code||"TOOL_ERROR",message:error.message}})));}}catch(error){errorLog("[agent:protocol-error]",error.message);}});
|
|
29
|
+
socket.on("open",()=>{if(ws!==socket)return;log("[agent:connected]",gatewayUrl);socket.send(JSON.stringify(envelope(TYPES.REGISTER,{auth:{token},device:{id:deviceId,name:deviceName,platform:process.platform,agentVersion,capabilities:[...new Set(capabilities.map(String))].sort()}})));});
|
|
30
|
+
socket.on("message",async(raw)=>{if(ws!==socket)return;try{const m=parseMessage(raw);if(m.type===TYPES.REGISTERED){log("[agent:registered]",m.deviceId);onRegistered({deviceId:m.deviceId,authorizedAccount:m.authorizedAccount||""});return;}if(m.type!==TYPES.REQUEST)return;log("[tool:request]",m.tool,{path:m.arguments?.path,cwd:m.arguments?.cwd});try{const result=await executeTool(m.tool,m.arguments);if(m.tool==="device.ping")Object.assign(result,{deviceId,deviceName});log("[tool:success]",m.tool);if(socket.readyState===WebSocket.OPEN)socket.send(JSON.stringify(envelope(TYPES.RESPONSE,{requestId:m.requestId,result})));}catch(error){errorLog("[tool:rejected]",m.tool,error.code||"TOOL_ERROR",error.message);if(socket.readyState===WebSocket.OPEN)socket.send(JSON.stringify(envelope(TYPES.ERROR,{requestId:m.requestId,error:{code:error.code||"TOOL_ERROR",message:error.message}})));}}catch(error){errorLog("[agent:protocol-error]",error.message);}});
|
|
29
31
|
socket.on("close",(code)=>{log("[agent:disconnected]","generation="+current,"code="+code);if(ws===socket)ws=null;if(code===1008){errorLog("[agent:authorization-required] device credential rejected");return;}if(code===4001){errorLog("[agent:replaced] newer WHYVIX Remote connection is active");return;}scheduleReconnect("close");});
|
|
30
32
|
socket.on("error",(e)=>{errorLog("[agent:socket-error]",e.message);scheduleReconnect("error");});
|
|
31
33
|
}
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { execFile } from "node:child_process";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { buildDeploymentOperation } from "./deployment-protocol.js";
|
|
6
7
|
|
|
7
8
|
const execFileAsync=promisify(execFile);
|
|
8
9
|
const PROFILE_RE=/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
@@ -48,4 +49,18 @@ export class DeploymentManager{
|
|
|
48
49
|
this.audit({event:"deploy.prepare_local",profile:profile.alias,commit,dependencyChanged});
|
|
49
50
|
return{profile:profile.alias,remoteAlias:profile.remoteAlias,commit,shortSha,artifact,artifactSha256,dependencyChanged};
|
|
50
51
|
}
|
|
52
|
+
async stageArtifact({profile:profileAlias,commit,artifact,artifactSha256}){
|
|
53
|
+
const profile=this.#profile(profileAlias);
|
|
54
|
+
const operation=buildDeploymentOperation(profile,{operation:"stage",commit,artifactSha256});
|
|
55
|
+
const localArtifact=this.security.resolveAllowed(artifact);
|
|
56
|
+
const stat=await fs.stat(localArtifact);
|
|
57
|
+
if(!stat.isFile())fail("DEPLOY_TRANSFER_FAILED","Deployment artifact must be a regular file");
|
|
58
|
+
const actualSha256=await sha256(localArtifact);
|
|
59
|
+
if(actualSha256!==artifactSha256)fail("DEPLOY_CHECKSUM_MISMATCH","Local deployment artifact checksum changed");
|
|
60
|
+
try{
|
|
61
|
+
await this.execFileFn("scp",["--",localArtifact,`${operation.remoteAlias}:${operation.artifactPath}`],{windowsHide:true,maxBuffer:1024*1024});
|
|
62
|
+
}catch{fail("DEPLOY_TRANSFER_FAILED","Deployment artifact transfer failed");}
|
|
63
|
+
this.audit({event:"deploy.stage",profile:profile.alias,commit,artifactSha256});
|
|
64
|
+
return{profile:profile.alias,remoteAlias:operation.remoteAlias,commit,releaseId:operation.releaseId,artifactPath:operation.artifactPath,artifactSha256};
|
|
65
|
+
}
|
|
51
66
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
function fail(code,message){throw Object.assign(new Error(message),{code});}
|
|
5
|
+
|
|
6
|
+
export async function loadDeploymentProfiles(filePath){
|
|
7
|
+
const requested=String(filePath||"").trim();
|
|
8
|
+
if(!requested)return[];
|
|
9
|
+
const resolved=path.resolve(requested);
|
|
10
|
+
let parsed;
|
|
11
|
+
try{parsed=JSON.parse(await fs.readFile(resolved,"utf8"));}catch(error){
|
|
12
|
+
if(error instanceof SyntaxError)fail("DEPLOY_PROFILE_DENIED","Deployment profile file is not valid JSON");
|
|
13
|
+
fail("DEPLOY_PROFILE_DENIED","Deployment profile file cannot be read");
|
|
14
|
+
}
|
|
15
|
+
if(!Array.isArray(parsed))fail("DEPLOY_PROFILE_DENIED","Deployment profile file must contain an array");
|
|
16
|
+
return parsed.map((profile)=>{
|
|
17
|
+
if(!profile||typeof profile!=="object"||Array.isArray(profile))fail("DEPLOY_PROFILE_DENIED","Invalid deployment profile");
|
|
18
|
+
const allowed=new Set(["alias","label","remoteAlias","repository","remoteReleaseRoot","remoteStagingRoot","serviceName"]);
|
|
19
|
+
for(const key of Object.keys(profile))if(!allowed.has(key))fail("DEPLOY_PROFILE_DENIED","Unsupported deployment profile field");
|
|
20
|
+
return{alias:profile.alias,label:profile.label||"",remoteAlias:profile.remoteAlias,repository:profile.repository,...(profile.remoteReleaseRoot!==undefined?{remoteReleaseRoot:profile.remoteReleaseRoot}:{}),...(profile.remoteStagingRoot!==undefined?{remoteStagingRoot:profile.remoteStagingRoot}:{}),...(profile.serviceName!==undefined?{serviceName:profile.serviceName}:{})};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
2
3
|
|
|
3
4
|
const DEFAULT_EXECUTABLES = Object.freeze({
|
|
4
5
|
node: process.execPath,
|
|
@@ -35,8 +36,8 @@ function resolveExecutable(registry, requested) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export class ProcessManager {
|
|
38
|
-
constructor({ audit=()=>{}, spawnFn=spawn, allowedExecutables=DEFAULT_EXECUTABLES, maxOutputBytes=262144, maxRetainedJobs=100, maxRunningJobs=16, jobRetentionMs=300000, now=()=>Date.now() }={}) {
|
|
39
|
-
this.audit=audit; this.spawnFn=spawnFn; this.executableRegistry=normalizeRegistry(allowedExecutables);
|
|
39
|
+
constructor({ audit=()=>{}, spawnFn=spawn, allowedExecutables=DEFAULT_EXECUTABLES, platform=process.platform, nodeExecPath=process.execPath, maxOutputBytes=262144, maxRetainedJobs=100, maxRunningJobs=16, jobRetentionMs=300000, now=()=>Date.now() }={}) {
|
|
40
|
+
this.audit=audit; this.spawnFn=spawnFn; this.executableRegistry=normalizeRegistry(allowedExecutables); this.platform=platform; this.nodeExecPath=nodeExecPath;
|
|
40
41
|
this.maxOutputBytes=Math.max(1024,Math.min(Number(maxOutputBytes)||262144,1048576));
|
|
41
42
|
this.maxRetainedJobs=Math.max(1,Math.min(Number(maxRetainedJobs)||100,1000));
|
|
42
43
|
this.maxRunningJobs=Math.max(1,Math.min(Number(maxRunningJobs)||16,128));
|
|
@@ -59,9 +60,14 @@ export class ProcessManager {
|
|
|
59
60
|
if(args.some(x=>/^--(?:yes|package|call|shell|install)/i.test(x))) fail("LOCAL_EXEC_NOT_ALLOWED","npx install or shell options are not allowed");
|
|
60
61
|
args=["--no-install",...args];
|
|
61
62
|
}
|
|
63
|
+
let spawnCommand=executable.command;
|
|
64
|
+
if(this.platform==="win32"&&(executable.name==="npm"||executable.name==="npx")) {
|
|
65
|
+
const cli=path.join(path.dirname(this.nodeExecPath),"node_modules","npm","bin",executable.name==="npm"?"npm-cli.js":"npx-cli.js");
|
|
66
|
+
spawnCommand=this.nodeExecPath; args=[cli,...args];
|
|
67
|
+
}
|
|
62
68
|
if([...this.processes.values()].filter(x=>x.running).length>=this.maxRunningJobs)fail("LOCAL_JOB_LIMIT","Local running job limit reached");
|
|
63
69
|
const id=String(this.nextId++);let child;
|
|
64
|
-
try{child=this.spawnFn(
|
|
70
|
+
try{child=this.spawnFn(spawnCommand,args,{cwd,windowsHide:true,shell:false});}catch{fail("LOCAL_EXEC_FAILED","Local process failed to start");}
|
|
65
71
|
const state={id,ownerId:ownerId==null?null:String(ownerId),command:executable.name,cwd,stdout:"",stderr:"",truncated:false,exitCode:null,errorCode:null,timedOut:false,running:true,completedAt:null,child};
|
|
66
72
|
this.processes.set(id,state);this.audit({event:"process.start",id,command:executable.name,cwd});
|
|
67
73
|
child.stdout?.on("data",x=>appendBounded(state,"stdout",x,this.maxOutputBytes));child.stderr?.on("data",x=>appendBounded(state,"stderr",x,this.maxOutputBytes));
|
|
@@ -5,7 +5,7 @@ import { execFile } from "node:child_process";
|
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
const execFileAsync=promisify(execFile);
|
|
7
7
|
|
|
8
|
-
export function createToolExecutor({ security, processManager, remoteExecutionManager }) {
|
|
8
|
+
export function createToolExecutor({ security, processManager, remoteExecutionManager, deploymentManager }) {
|
|
9
9
|
return async function executeTool(tool,args={}) {
|
|
10
10
|
security.audit({event:"tool.request",tool});
|
|
11
11
|
if(tool==="device.ping") return {pong:true,timestamp:new Date().toISOString()};
|
|
@@ -70,19 +70,20 @@ export function createToolExecutor({ security, processManager, remoteExecutionMa
|
|
|
70
70
|
await fs.writeFile(target,updated,{encoding:"utf8",flag:"w"});
|
|
71
71
|
return{path:target,bytes,replacements:1};
|
|
72
72
|
}
|
|
73
|
+
if(tool==="deploy.prepare_local"){if(!deploymentManager)throw Object.assign(new Error("Deployment is not configured"),{code:"DEPLOY_PROFILE_DENIED"});return deploymentManager.prepareLocal({profile:args.profile,commit:args.commit,baseCommit:args.baseCommit??null});}
|
|
73
74
|
if(tool==="remote.targets"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});return{targets:remoteExecutionManager.listTargets()};}
|
|
74
75
|
if(tool==="remote.start"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});return remoteExecutionManager.start(args);}
|
|
75
76
|
if(tool==="remote.deploy_status"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_TARGET_DENIED"});return remoteExecutionManager.startDeployHelper({alias:args.alias,input:{operation:"status",commit:args.commit},timeoutMs:args.timeoutMs,ownerId:args.ownerId});}
|
|
76
77
|
if(tool==="remote.output"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_JOB_NOT_FOUND"});return remoteExecutionManager.output(args);}
|
|
77
78
|
if(tool==="remote.stop"){if(!remoteExecutionManager)throw Object.assign(new Error("Remote execution is not configured"),{code:"REMOTE_JOB_NOT_FOUND"});return remoteExecutionManager.stop(args);}
|
|
78
|
-
if(tool==="process.start"){const cwd=security.
|
|
79
|
+
if(tool==="process.start"){const cwd=security.resolveFilesystemPath(args.cwd);return processManager.start({...args,cwd});}
|
|
79
80
|
if(tool==="process.output")return processManager.output(args);
|
|
80
81
|
if(tool==="process.stop")return processManager.stop(args);
|
|
81
|
-
if(tool==="git.status"||tool==="git.diff"||tool==="git.log"){const cwd=security.
|
|
82
|
-
if(tool==="git.stage_paths"||tool==="git.commit"){
|
|
83
|
-
const cwd=security.
|
|
82
|
+
if(tool==="git.status"||tool==="git.diff"||tool==="git.log"){const cwd=security.resolveFilesystemPath(args.cwd);const gitArgs=tool==="git.status"?["status","--porcelain=v1","--branch"]:tool==="git.diff"?["diff","--",...(args.paths||[])]:["log","--oneline","-n",String(Math.min(Number(args.limit||10),50))];const {stdout,stderr}=await execFileAsync("git",gitArgs,{cwd,windowsHide:true,maxBuffer:1024*1024});return{stdout,stderr};}
|
|
83
|
+
if(tool==="git.stage_paths"||tool==="git.commit"||tool==="git.push"){
|
|
84
|
+
const cwd=security.resolveFilesystemPath(args.cwd);
|
|
84
85
|
const {stdout:rootRaw}=await execFileAsync("git",["rev-parse","--show-toplevel"],{cwd,windowsHide:true,maxBuffer:1024*1024});
|
|
85
|
-
const repoRoot=security.
|
|
86
|
+
const repoRoot=security.resolveFilesystemPath(rootRaw.trim());
|
|
86
87
|
if(tool==="git.stage_paths"){
|
|
87
88
|
const {stdout:alreadyStaged}=await execFileAsync("git",["diff","--cached","--name-only"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
|
|
88
89
|
if(alreadyStaged.trim())throw Object.assign(new Error("Repository already has staged changes"),{code:"INDEX_NOT_EMPTY"});
|
|
@@ -90,7 +91,7 @@ export function createToolExecutor({ security, processManager, remoteExecutionMa
|
|
|
90
91
|
if(paths.length<1||paths.length>64)throw Object.assign(new Error("paths must contain 1 to 64 explicit paths"),{code:"INVALID_ARGUMENT"});
|
|
91
92
|
const relativePaths=paths.map((item)=>{
|
|
92
93
|
if(!item||item==="."||item==="*"||item.startsWith("-"))throw Object.assign(new Error("Only explicit repository paths may be staged"),{code:"INVALID_ARGUMENT"});
|
|
93
|
-
const absolute=security.
|
|
94
|
+
const absolute=security.resolveFilesystemPath(path.resolve(repoRoot,item));
|
|
94
95
|
const relative=path.relative(repoRoot,absolute);
|
|
95
96
|
if(!relative||relative.startsWith(".."+path.sep)||path.isAbsolute(relative))throw Object.assign(new Error("Path outside repository"),{code:"PATH_DENIED"});
|
|
96
97
|
return relative.split(path.sep).join("/");
|
|
@@ -99,6 +100,11 @@ export function createToolExecutor({ security, processManager, remoteExecutionMa
|
|
|
99
100
|
const {stdout}=await execFileAsync("git",["diff","--cached","--name-only","--",...relativePaths],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
|
|
100
101
|
return{staged:stdout.split(/\r?\n/).filter(Boolean)};
|
|
101
102
|
}
|
|
103
|
+
if(tool==="git.push"){
|
|
104
|
+
const {stdout,stderr}=await execFileAsync("git",["push"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
|
|
105
|
+
const {stdout:sha}=await execFileAsync("git",["rev-parse","HEAD"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
|
|
106
|
+
return{sha:sha.trim(),stdout,stderr};
|
|
107
|
+
}
|
|
102
108
|
const message=String(args.message||"");
|
|
103
109
|
if(!message.trim()||message.length>500||/[\r\n]/.test(message))throw Object.assign(new Error("Commit message must be 1 to 500 characters on one line"),{code:"INVALID_ARGUMENT"});
|
|
104
110
|
const {stdout:before}=await execFileAsync("git",["diff","--cached","--name-only"],{cwd:repoRoot,windowsHide:true,maxBuffer:1024*1024});
|
package/packages/cli/index.js
CHANGED
|
@@ -2,16 +2,22 @@
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import {
|
|
5
|
+
import { join, dirname } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { createAgent } from "../agent/src/agent.js";
|
|
8
|
+
import { loadDeploymentProfiles } from "../agent/src/deployment-profile-loader.js";
|
|
7
9
|
import { openBrowser } from "./browser.js";
|
|
8
10
|
|
|
11
|
+
const packageRoot=join(dirname(fileURLToPath(import.meta.url)),"..","..");
|
|
12
|
+
const packageMeta=JSON.parse(await readFile(join(packageRoot,"package.json"),"utf8"));
|
|
9
13
|
const command=process.argv[2]||"remote";
|
|
10
14
|
if(command!=="remote"){console.error("Usage: whyvix-remote remote");process.exit(2);}
|
|
11
15
|
const gatewayUrl=process.env.WHYVIX_GATEWAY_URL||"wss://remote.whyvix.com";
|
|
12
16
|
const authUrl=(process.env.WHYVIX_AUTH_URL||"https://auth.whyvix.com").replace(/\/$/,"");
|
|
13
17
|
const workspaces=(process.env.WHYVIX_WORKSPACES||process.cwd()).split(";").filter(Boolean);
|
|
14
|
-
const remoteTargets=(process.env.WHYVIX_REMOTE_TARGETS||"").split(";").map(alias=>alias.trim()).filter(Boolean).map(alias=>({alias}));
|
|
18
|
+
const remoteTargets=(process.env.WHYVIX_REMOTE_TARGETS||"").split(";").map(alias=>alias.trim()).filter(Boolean).map(alias=>({alias}));
|
|
19
|
+
const deploymentProfiles=await loadDeploymentProfiles(process.env.WHYVIX_DEPLOY_PROFILES_FILE||"");
|
|
20
|
+
const capabilities=["filesystem.crud","process.bounded","git.read","git.mutate",...(remoteTargets.length?["remote.bounded"]:[]),...(deploymentProfiles.length?["deployment.profiled"]:[])];
|
|
15
21
|
const deviceId=process.env.WHYVIX_DEVICE_ID||crypto.createHash("sha256").update(os.hostname()+"|"+os.userInfo().username).digest("hex").slice(0,24);
|
|
16
22
|
const credentialDir=join(os.homedir(),".whyvix-remote");
|
|
17
23
|
const credentialPath=join(credentialDir,"device.json");
|
|
@@ -56,10 +62,21 @@ const agent=createAgent({
|
|
|
56
62
|
token:credential,
|
|
57
63
|
workspaces,
|
|
58
64
|
remoteTargets,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
deploymentProfiles,
|
|
66
|
+
agentVersion:String(packageMeta.version||"unknown"),
|
|
67
|
+
capabilities,
|
|
68
|
+
audit:(event)=>{
|
|
69
|
+
if(event?.event==="process.start") console.log("RUN",event.command,event.cwd);
|
|
70
|
+
else if(event?.event==="process.exit") console.log("EXIT",event.code,event.errorCode||"OK");
|
|
71
|
+
else if(event?.event==="process.error") console.error("PROCESS ERROR",event.code);
|
|
72
|
+
else if(event?.event==="remote.start") console.log("REMOTE",event.alias,event.executable);
|
|
73
|
+
else if(event?.event==="remote.exit") console.log("REMOTE EXIT",event.code,event.errorCode||"OK");
|
|
74
|
+
else if(event?.event==="deploy.prepare_local"||event?.event==="deploy.stage") console.log("DEPLOY",event.event,event.profile,event.commit);
|
|
75
|
+
},
|
|
76
|
+
log:(event,tool,meta)=>{
|
|
77
|
+
const quiet=tool==="process.output"||tool==="remote.output";
|
|
78
|
+
if(event==="[tool:request]"&&!quiet) console.log("←",tool,meta?.path||meta?.cwd||"");
|
|
79
|
+
else if(event==="[tool:success]"&&!quiet) console.log("✓",tool);
|
|
63
80
|
},
|
|
64
81
|
errorLog:(event,tool,code,message)=>{
|
|
65
82
|
if(event==="[tool:rejected]") console.error("✗",tool,"·",code,"·",message);
|