@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/cli.js +71 -101
- package/connection/upgrade.js +45 -97
- package/local-runtime/server/db/session-ui-metadata-db.js +37 -33
- package/local-runtime/server/db/yeaft-project-db.js +15 -4
- package/local-runtime/server/handlers/client-conversation.js +41 -37
- package/local-runtime/server/session-catalog.js +13 -1
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +322 -403
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/upgrade-command.js +62 -39
- package/windows-upgrade-runner.js +196 -0
|
Binary file
|
package/package.json
CHANGED
package/upgrade-command.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { existsSync,
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
54
|
-
export function
|
|
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:
|
|
70
|
-
args: [
|
|
77
|
+
command: nodePath,
|
|
78
|
+
args: [runnerPath, payloadPath],
|
|
71
79
|
options: {
|
|
72
80
|
detached: true,
|
|
73
81
|
stdio: 'ignore',
|
|
74
82
|
windowsHide: true,
|
|
75
|
-
|
|
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
|
|
129
|
-
*
|
|
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
|
-
|
|
157
|
+
nodePath,
|
|
158
|
+
runnerPath,
|
|
159
|
+
payloadPath,
|
|
160
|
+
logPath,
|
|
134
161
|
handoffPath,
|
|
135
162
|
spawnProcess,
|
|
136
163
|
fileExists = existsSync,
|
|
137
|
-
removeFile =
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|