@yeaft/webchat-agent 1.0.331 → 1.0.333

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.331",
3
+ "version": "1.0.333",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,4 +1,5 @@
1
- import { existsSync, unlinkSync } from 'node:fs';
1
+ import { copyFileSync, existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { delimiter, dirname, join, win32 } from 'node:path';
2
3
  import { setTimeout as delay } from 'node:timers/promises';
3
4
 
4
5
  export const DEFAULT_UPGRADE_REGISTRY = 'https://pkg.yeaft.com/';
@@ -21,23 +22,43 @@ export function buildUpgradeMetadataArgs(packageSpec, field) {
21
22
  }
22
23
 
23
24
  /** Build argv for an npm install against the Yeaft registry. */
24
- export function buildUpgradeInstallArgs(packageSpec, { global = true } = {}) {
25
+ export function buildUpgradeInstallArgs(packageSpec, { global = true, quiet = false } = {}) {
25
26
  return [
26
27
  'install',
27
28
  ...(global ? ['-g'] : []),
28
29
  packageSpec,
29
30
  `--registry=${DEFAULT_UPGRADE_REGISTRY}`,
31
+ ...(quiet ? ['--no-audit', '--no-fund', '--loglevel=error'] : []),
30
32
  ];
31
33
  }
32
34
 
33
- /** Build argv for updating an installed package through the Yeaft registry. */
34
- export function buildUpgradeUpdateArgs(packageName, { global = true } = {}) {
35
- return [
36
- 'update',
37
- ...(global ? ['-g'] : []),
38
- packageName,
39
- `--registry=${DEFAULT_UPGRADE_REGISTRY}`,
40
- ];
35
+ function resolveNodeCliPath(nodePath, packageName, relativeCliPaths, fileExists = existsSync, pathValue = process.env.PATH || '') {
36
+ const pathApi = /^[A-Za-z]:[\\/]/.test(String(nodePath)) ? win32 : { dirname, join };
37
+ const nodeDir = pathApi.dirname(String(nodePath));
38
+ const roots = [nodeDir, pathApi.dirname(nodeDir)];
39
+ const pathDelimiter = pathApi === win32 ? ';' : delimiter;
40
+ for (const entry of String(pathValue).split(pathDelimiter)) {
41
+ const trimmed = entry.trim().replace(/^"|"$/g, '');
42
+ if (trimmed) roots.push(trimmed);
43
+ }
44
+
45
+ for (const root of [...new Set(roots)]) {
46
+ for (const cliPath of relativeCliPaths) {
47
+ const candidate = pathApi.join(root, 'node_modules', packageName, ...cliPath);
48
+ if (fileExists(candidate)) return candidate;
49
+ }
50
+ }
51
+ return null;
52
+ }
53
+
54
+ /** Resolve npm's JavaScript entry point without a `.cmd` wrapper. */
55
+ export function resolveWindowsNpmCliPath(nodePath, fileExists = existsSync, pathValue) {
56
+ return resolveNodeCliPath(nodePath, 'npm', [['bin', 'npm-cli.js']], fileExists, pathValue);
57
+ }
58
+
59
+ /** Resolve PM2's JavaScript entry point without a PowerShell/cmd wrapper. */
60
+ export function resolveWindowsPm2CliPath(nodePath, fileExists = existsSync, pathValue) {
61
+ return resolveNodeCliPath(nodePath, 'pm2', [['bin', 'pm2'], ['bin', 'pm2.js']], fileExists, pathValue);
41
62
  }
42
63
 
43
64
  /** Build the npm metadata command used by `yeaft-agent upgrade`. */
@@ -50,33 +71,37 @@ export function buildUpgradeInstallCommand(packageSpec, options) {
50
71
  return ['npm', ...buildUpgradeInstallArgs(packageSpec, options)].join(' ');
51
72
  }
52
73
 
53
- /** Build the npm update command used after a Windows Agent has exited. */
54
- export function buildUpgradeUpdateCommand(packageName) {
55
- return ['npm', ...buildUpgradeUpdateArgs(packageName)].join(' ');
56
- }
57
-
58
- function quoteCmdPath(path) {
59
- return `"${String(path).replace(/"/g, '""')}"`;
60
- }
61
-
62
- /**
63
- * Build the exact CreateProcess contract for a batch file path. cmd.exe needs
64
- * the full command string quoted once; windowsVerbatimArguments prevents Node
65
- * from escaping those quotes a second time when the path contains spaces.
66
- */
67
- export function buildWindowsUpgradeInvocation(batPath) {
74
+ /** Build a shell-free detached Node invocation for the Windows updater. */
75
+ export function buildWindowsUpgradeInvocation({ nodePath, runnerPath, payloadPath, logPath }) {
68
76
  return {
69
- command: 'cmd.exe',
70
- args: ['/d', '/s', '/c', quoteCmdPath(batPath)],
77
+ command: nodePath,
78
+ args: [runnerPath, payloadPath],
71
79
  options: {
72
80
  detached: true,
73
81
  stdio: 'ignore',
74
82
  windowsHide: true,
75
- windowsVerbatimArguments: true,
83
+ env: { ...process.env, YEAFT_UPGRADE_LOG: logPath },
76
84
  },
77
85
  };
78
86
  }
79
87
 
88
+ /**
89
+ * Copy the updater out of the npm package before npm replaces that package.
90
+ * The runner imports only this helper module, so copy both files together.
91
+ */
92
+ export function prepareWindowsUpgradeRunner({ sourceRunnerPath, sourceCommandPath, runnerPath, commandPath, payloadPath, payload }) {
93
+ const runtimeDir = dirname(runnerPath);
94
+ const moduleManifestPath = join(runtimeDir, 'package.json');
95
+ mkdirSync(runtimeDir, { recursive: true });
96
+ for (const path of [runnerPath, commandPath, moduleManifestPath, payloadPath, payload.handoffPath]) {
97
+ try { rmSync(path, { force: true }); } catch {}
98
+ }
99
+ copyFileSync(sourceRunnerPath, runnerPath);
100
+ copyFileSync(sourceCommandPath, commandPath);
101
+ writeFileSync(moduleManifestPath, JSON.stringify({ type: 'module' }));
102
+ writeFileSync(payloadPath, JSON.stringify(payload));
103
+ }
104
+
80
105
  function waitForSpawn(child) {
81
106
  return new Promise((resolve, reject) => {
82
107
  const onError = err => {
@@ -125,16 +150,18 @@ async function waitForUpgradeHandoff({
125
150
  }
126
151
 
127
152
  /**
128
- * Launch the detached Windows updater and wait for the batch script itself to
129
- * confirm execution before the caller stops PM2 or exits. `spawn` only proves
130
- * that cmd.exe was created; the handoff file proves the updater took control.
153
+ * Launch the detached Windows updater and confirm that its handoff marker
154
+ * remains present before the caller stops PM2 or exits.
131
155
  */
132
156
  export async function launchWindowsUpgradeScript({
133
- batPath,
157
+ nodePath,
158
+ runnerPath,
159
+ payloadPath,
160
+ logPath,
134
161
  handoffPath,
135
162
  spawnProcess,
136
163
  fileExists = existsSync,
137
- removeFile = unlinkSync,
164
+ removeFile = path => rmSync(path, { force: true }),
138
165
  sleep = delay,
139
166
  timeoutMs = 5000,
140
167
  pollIntervalMs = 50,
@@ -143,11 +170,7 @@ export async function launchWindowsUpgradeScript({
143
170
  if (typeof spawnProcess !== 'function') throw new TypeError('spawnProcess is required');
144
171
  if (!handoffPath) throw new TypeError('handoffPath is required');
145
172
 
146
- try { removeFile(handoffPath); } catch (err) {
147
- if (err?.code !== 'ENOENT') throw err;
148
- }
149
-
150
- const invocation = buildWindowsUpgradeInvocation(batPath);
173
+ const invocation = buildWindowsUpgradeInvocation({ nodePath, runnerPath, payloadPath, logPath });
151
174
  let child;
152
175
  try {
153
176
  child = spawnProcess(invocation.command, invocation.args, invocation.options);
@@ -182,7 +205,7 @@ export async function launchWindowsUpgradeScript({
182
205
  }
183
206
 
184
207
  child.unref();
185
- return 'cmd.exe';
208
+ return nodePath;
186
209
  }
187
210
 
188
211
  /** Build the URL used by the startup-only update notification. */
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { appendFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { spawn } from 'node:child_process';
5
+ import { dirname } from 'node:path';
6
+ import { pathToFileURL } from 'node:url';
7
+ import { setTimeout as delay } from 'node:timers/promises';
8
+ import {
9
+ buildUpgradeInstallArgs,
10
+ resolveWindowsNpmCliPath,
11
+ } from './upgrade-command.js';
12
+
13
+ const PID_POLL_INTERVAL_MS = 100;
14
+ const PID_WAIT_TIMEOUT_MS = 30_000;
15
+ const FILE_LOCK_RETRY_MS = [0, 250, 750, 1_500];
16
+
17
+ function appendLog(logPath, message) {
18
+ try {
19
+ appendFileSync(logPath, `[Upgrade] ${message}\n`);
20
+ } catch {}
21
+ }
22
+
23
+ export function isProcessRunning(pid, probe = process.kill) {
24
+ try {
25
+ probe(pid, 0);
26
+ return true;
27
+ } catch (err) {
28
+ return err?.code === 'EPERM';
29
+ }
30
+ }
31
+
32
+ export async function waitForProcessExit(pid, {
33
+ timeoutMs = PID_WAIT_TIMEOUT_MS,
34
+ pollIntervalMs = PID_POLL_INTERVAL_MS,
35
+ processRunning = isProcessRunning,
36
+ sleep = delay,
37
+ now = Date.now,
38
+ } = {}) {
39
+ const deadline = now() + timeoutMs;
40
+ while (processRunning(pid)) {
41
+ if (now() >= deadline) return false;
42
+ await sleep(pollIntervalMs);
43
+ }
44
+ return true;
45
+ }
46
+
47
+ function runProcess(command, args, options = {}) {
48
+ return new Promise((resolve, reject) => {
49
+ const { onStderr, ...spawnOptions } = options;
50
+ const child = spawn(command, args, spawnOptions);
51
+ if (typeof onStderr === 'function') child.stderr?.on('data', onStderr);
52
+ child.once('error', reject);
53
+ child.once('exit', (code, signal) => {
54
+ if (signal) reject(new Error(`${command} exited with signal ${signal}`));
55
+ else resolve(code ?? 1);
56
+ });
57
+ });
58
+ }
59
+
60
+ function isRetryableWindowsInstallFailure(code, stderr) {
61
+ if (code === 0) return false;
62
+ return /\b(EBUSY|EPERM|EACCES)\b|resource busy|operation not permitted|permission denied/iu.test(stderr);
63
+ }
64
+
65
+ export async function installWindowsUpgrade({
66
+ nodePath,
67
+ npmCliPath,
68
+ packageSpec,
69
+ globalInstall,
70
+ installDir,
71
+ logPath,
72
+ run = runProcess,
73
+ sleep = delay,
74
+ fileExists,
75
+ }) {
76
+ const resolvedNpmCliPath = npmCliPath || resolveWindowsNpmCliPath(nodePath, fileExists);
77
+ if (!resolvedNpmCliPath) throw new Error('npm JavaScript CLI entry point could not be resolved');
78
+ const command = nodePath;
79
+ const args = [
80
+ resolvedNpmCliPath,
81
+ ...buildUpgradeInstallArgs(packageSpec, { global: globalInstall, quiet: true }),
82
+ ];
83
+ const cwd = globalInstall ? process.env.TEMP : installDir;
84
+
85
+ for (let attempt = 0; attempt < FILE_LOCK_RETRY_MS.length; attempt++) {
86
+ const retryDelayMs = FILE_LOCK_RETRY_MS[attempt];
87
+ if (retryDelayMs) {
88
+ appendLog(logPath, `Retrying npm install after ${retryDelayMs}ms file-lock delay`);
89
+ await sleep(retryDelayMs);
90
+ }
91
+
92
+ let stderr = '';
93
+ const exitCode = await run(command, args, {
94
+ cwd,
95
+ env: process.env,
96
+ windowsHide: true,
97
+ stdio: ['ignore', 'ignore', 'pipe'],
98
+ onStderr: chunk => { stderr += String(chunk); },
99
+ });
100
+ if (exitCode === 0) return { exitCode, attempts: attempt + 1, command, args };
101
+ if (!isRetryableWindowsInstallFailure(exitCode, stderr) || attempt === FILE_LOCK_RETRY_MS.length - 1) {
102
+ if (stderr.trim()) appendLog(logPath, `npm stderr: ${stderr.trim()}`);
103
+ return { exitCode, attempts: attempt + 1, command, args };
104
+ }
105
+ }
106
+
107
+ return { exitCode: 1, attempts: FILE_LOCK_RETRY_MS.length, command, args };
108
+ }
109
+
110
+ export async function startPm2Service({ nodePath, pm2CliPath, ecosystemPath, logPath, run = runProcess }) {
111
+ if (!pm2CliPath || !ecosystemPath) return true;
112
+ appendLog(logPath, 'Re-registering Agent via PM2');
113
+ const startCode = await run(nodePath, [pm2CliPath, 'start', ecosystemPath], {
114
+ env: process.env,
115
+ windowsHide: true,
116
+ stdio: 'ignore',
117
+ });
118
+ if (startCode !== 0) return false;
119
+ const saveCode = await run(nodePath, [pm2CliPath, 'save'], {
120
+ env: process.env,
121
+ windowsHide: true,
122
+ stdio: 'ignore',
123
+ });
124
+ return saveCode === 0;
125
+ }
126
+
127
+ /**
128
+ * Run after the original Agent exits. No shell pipeline is involved: Windows
129
+ * PID polling is handled by Node and npm/PM2 run through their JS entry points.
130
+ */
131
+ export async function runWindowsUpgrade(options, dependencies = {}) {
132
+ const {
133
+ parentPid,
134
+ packageSpec,
135
+ globalInstall,
136
+ installDir,
137
+ logPath,
138
+ handoffPath,
139
+ runnerPath,
140
+ commandPath,
141
+ payloadPath,
142
+ nodePath,
143
+ npmCliPath,
144
+ pm2CliPath,
145
+ ecosystemPath,
146
+ } = options;
147
+ const wait = dependencies.waitForProcessExit || waitForProcessExit;
148
+ const install = dependencies.installWindowsUpgrade || installWindowsUpgrade;
149
+ const startService = dependencies.startPm2Service || startPm2Service;
150
+
151
+ mkdirSync(dirname(handoffPath), { recursive: true });
152
+ writeFileSync(handoffPath, 'started');
153
+ appendLog(logPath, `Started at ${new Date().toISOString()}`);
154
+ appendLog(logPath, `Waiting for PID ${parentPid} to exit`);
155
+ const exited = await wait(parentPid);
156
+ if (!exited) appendLog(logPath, `Timed out waiting for PID ${parentPid}; continuing with bounded npm retries`);
157
+ else appendLog(logPath, 'Original process exited');
158
+
159
+ let result;
160
+ try {
161
+ result = await install({ nodePath, npmCliPath, packageSpec, globalInstall, installDir, logPath });
162
+ } catch (err) {
163
+ appendLog(logPath, `npm install failed to start: ${err?.message || err}`);
164
+ result = { exitCode: 1, attempts: 0 };
165
+ }
166
+ appendLog(logPath, `npm install ${result.exitCode === 0 ? 'succeeded' : `failed with exit code ${result.exitCode}`} after ${result.attempts} attempt(s)`);
167
+
168
+ let restarted = false;
169
+ try {
170
+ restarted = await startService({ nodePath, pm2CliPath, ecosystemPath, logPath });
171
+ } catch (err) {
172
+ appendLog(logPath, `WARNING: PM2 service restart failed: ${err?.message || err}`);
173
+ }
174
+ if (!restarted) appendLog(logPath, 'WARNING: PM2 service was not restarted');
175
+ appendLog(logPath, `Finished at ${new Date().toISOString()}`);
176
+
177
+ // Do not delete the running script. Windows keeps it executable until exit,
178
+ // but deferred cleanup on the next upgrade is more reliable than self-delete.
179
+ for (const path of [handoffPath, payloadPath]) {
180
+ try { rmSync(path, { force: true }); } catch {}
181
+ }
182
+ return { exitCode: result.exitCode, restarted, cleanupPaths: [runnerPath, commandPath] };
183
+ }
184
+
185
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
186
+ let payload;
187
+ try {
188
+ payload = JSON.parse(readFileSync(process.argv[2], 'utf8'));
189
+ const result = await runWindowsUpgrade(payload);
190
+ process.exitCode = result.exitCode;
191
+ } catch (err) {
192
+ const logPath = payload?.logPath || process.env.YEAFT_UPGRADE_LOG;
193
+ if (logPath) appendLog(logPath, `Runner failed: ${err?.stack || err}`);
194
+ process.exitCode = 1;
195
+ }
196
+ }