remote-codex 0.11.43 → 0.11.45
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/README.md +9 -0
- package/apps/relay-server/dist/index.js +17 -1
- package/apps/supervisor-api/dist/index.js +2189 -1798
- package/apps/supervisor-web/dist/assets/index-BO9S3vTX.css +1 -0
- package/apps/supervisor-web/dist/assets/index-GqVDOqbI.js +22 -0
- package/apps/supervisor-web/dist/assets/{thread-ui-Dmrigdek.js → thread-ui-BWC_ljvN.js} +11 -11
- package/apps/supervisor-web/dist/index.html +3 -3
- package/bin/remote-codex.mjs +426 -35
- package/docs/windows.md +81 -0
- package/package.json +14 -3
- package/packages/claude/src/runtimeAdapter.test.ts +32 -4
- package/packages/claude/src/runtimeAdapter.ts +21 -22
- package/packages/codex/src/appServerManager.test.ts +47 -0
- package/packages/codex/src/appServerManager.ts +6 -2
- package/packages/codex/src/runtimeAdapter.test.ts +9 -2
- package/packages/opencode/src/historyItems.ts +7 -6
- package/packages/opencode/src/runtimeAdapter.ts +7 -11
- package/packages/process-runtime/src/index.test.ts +132 -0
- package/packages/process-runtime/src/index.ts +253 -0
- package/packages/shared/src/index.ts +12 -0
- package/scripts/service-manager.mjs +112 -4
- package/scripts/verify-relay-supervisor-smoke.mjs +262 -0
- package/scripts/windows/install-relay-supervisor-task.ps1 +44 -0
- package/scripts/windows/relay-smoke.ps1 +16 -0
- package/scripts/windows/uninstall-relay-supervisor-task.ps1 +27 -0
- package/scripts/windows/validate-real-codex.mjs +680 -0
- package/apps/supervisor-web/dist/assets/index-BT0SM9C-.js +0 -21
- package/apps/supervisor-web/dist/assets/index-BcCLYWAf.css +0 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import net from 'node:net';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
import crossSpawn from 'cross-spawn';
|
|
9
|
+
|
|
10
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const packageRoot = path.resolve(scriptDir, '..');
|
|
12
|
+
const temporaryRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'Remote Codex Relay Smoke '));
|
|
13
|
+
const workspaceRoot = path.join(temporaryRoot, 'workspace root 开发');
|
|
14
|
+
const relayPort = await reservePort();
|
|
15
|
+
const supervisorPort = await reservePort();
|
|
16
|
+
const relayBaseUrl = `http://127.0.0.1:${relayPort}`;
|
|
17
|
+
const processes = [];
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await fsp.mkdir(workspaceRoot, { recursive: true });
|
|
21
|
+
const relayEnvironment = {
|
|
22
|
+
REMOTE_CODEX_ADMIN_USERNAME: 'smoke-admin',
|
|
23
|
+
REMOTE_CODEX_ADMIN_PASSWORD: 'smoke-admin-password',
|
|
24
|
+
REMOTE_CODEX_RELAY_SESSION_SECRET: 'smoke-session-secret-32-characters',
|
|
25
|
+
REMOTE_CODEX_RELAY_REGISTRATION_ENABLED: 'true',
|
|
26
|
+
REMOTE_CODEX_RELAY_DATA_DIR: path.join(temporaryRoot, 'relay data'),
|
|
27
|
+
REMOTE_CODEX_RELAY_HOST: '127.0.0.1',
|
|
28
|
+
REMOTE_CODEX_RELAY_PORT: String(relayPort),
|
|
29
|
+
};
|
|
30
|
+
const relayEntry = path.join(packageRoot, 'apps', 'relay-server', 'dist', 'index.js');
|
|
31
|
+
const relay = startProcess(relayEntry, relayEnvironment);
|
|
32
|
+
processes.push(relay);
|
|
33
|
+
await waitForHttp(`${relayBaseUrl}/healthz`, relay, 20_000);
|
|
34
|
+
|
|
35
|
+
const username = `relay-smoke-${crypto.randomBytes(4).toString('hex')}`;
|
|
36
|
+
const registration = await jsonRequest(`${relayBaseUrl}/relay/auth/register`, {
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: { email: `${username}@example.test`, username, password: 'relay-smoke-password' },
|
|
39
|
+
});
|
|
40
|
+
const userToken = requiredString(registration.token, 'registration token');
|
|
41
|
+
const deviceRegistration = await jsonRequest(`${relayBaseUrl}/relay/devices`, {
|
|
42
|
+
method: 'POST', token: userToken, body: { name: `Windows smoke ${process.platform}` },
|
|
43
|
+
});
|
|
44
|
+
const deviceId = requiredString(deviceRegistration.device?.id, 'device id');
|
|
45
|
+
const deviceToken = requiredString(deviceRegistration.token, 'device token');
|
|
46
|
+
|
|
47
|
+
const instanceId = crypto.randomUUID();
|
|
48
|
+
const controlToken = crypto.randomBytes(32).toString('base64url');
|
|
49
|
+
const controlEndpoint = process.platform === 'win32'
|
|
50
|
+
? `\\\\.\\pipe\\remote-codex-relay-smoke-${instanceId}`
|
|
51
|
+
: path.join('/tmp', `rc-relay-${instanceId.slice(0, 12)}.sock`);
|
|
52
|
+
const supervisor = startProcess(path.join(packageRoot, 'apps', 'supervisor-api', 'dist', 'index.js'), {
|
|
53
|
+
HOST: '127.0.0.1',
|
|
54
|
+
PORT: String(supervisorPort),
|
|
55
|
+
DATABASE_URL: path.join(temporaryRoot, 'supervisor.sqlite'),
|
|
56
|
+
WORKSPACE_ROOT: workspaceRoot,
|
|
57
|
+
CODEX_HOME: path.join(temporaryRoot, '.codex'),
|
|
58
|
+
REMOTE_CODEX_PACKAGE_ROOT: packageRoot,
|
|
59
|
+
REMOTE_CODEX_MODE: 'relay',
|
|
60
|
+
REMOTE_CODEX_ADMIN_USERNAME: 'supervisor-admin',
|
|
61
|
+
REMOTE_CODEX_ADMIN_PASSWORD: 'supervisor-admin-password',
|
|
62
|
+
REMOTE_CODEX_SESSION_SECRET: 'supervisor-session-secret-32-characters',
|
|
63
|
+
REMOTE_CODEX_RELAY_SERVER_URL: `ws://127.0.0.1:${relayPort}`,
|
|
64
|
+
REMOTE_CODEX_RELAY_AGENT_TOKEN: deviceToken,
|
|
65
|
+
REMOTE_CODEX_ENABLED_AGENT_PROVIDERS: 'relay-smoke-fake-only',
|
|
66
|
+
REMOTE_CODEX_E2E_FAKE_RUNTIME: 'true',
|
|
67
|
+
REMOTE_CODEX_LIFECYCLE_CONTROL_ENDPOINT: controlEndpoint,
|
|
68
|
+
REMOTE_CODEX_LIFECYCLE_CONTROL_TOKEN: controlToken,
|
|
69
|
+
REMOTE_CODEX_LIFECYCLE_INSTANCE_ID: instanceId,
|
|
70
|
+
});
|
|
71
|
+
processes.push(supervisor);
|
|
72
|
+
|
|
73
|
+
const deviceApi = `${relayBaseUrl}/relay/devices/${deviceId}`;
|
|
74
|
+
await waitForHttp(`${deviceApi}/healthz`, supervisor, 30_000, userToken);
|
|
75
|
+
const workspace = await jsonRequest(`${deviceApi}/api/workspaces`, {
|
|
76
|
+
method: 'POST', token: userToken,
|
|
77
|
+
body: { absPath: path.join(workspaceRoot, 'project with spaces 项目') },
|
|
78
|
+
});
|
|
79
|
+
const workspaceId = requiredString(workspace.id, 'workspace id');
|
|
80
|
+
const thread = await jsonRequest(`${deviceApi}/api/threads/start`, {
|
|
81
|
+
method: 'POST', token: userToken,
|
|
82
|
+
body: {
|
|
83
|
+
workspaceId, provider: 'claude', model: 'ios-e2e-stream',
|
|
84
|
+
approvalMode: 'yolo', title: 'Windows relay smoke',
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
const threadId = requiredString(thread.id, 'thread id');
|
|
88
|
+
await jsonRequest(`${deviceApi}/api/threads/${threadId}/prompt`, {
|
|
89
|
+
method: 'POST', token: userToken, body: { prompt: 'WINDOWS_RELAY_SMOKE' },
|
|
90
|
+
});
|
|
91
|
+
await waitForJson(
|
|
92
|
+
`${deviceApi}/api/threads/${threadId}`, userToken,
|
|
93
|
+
(value) => {
|
|
94
|
+
const serialized = JSON.stringify(value);
|
|
95
|
+
return serialized.includes('IOS_STREAM_DELTA_READY') &&
|
|
96
|
+
!serialized.includes('IOS_STREAM_COMPLETED');
|
|
97
|
+
}, 5_000,
|
|
98
|
+
);
|
|
99
|
+
const detail = await waitForJson(
|
|
100
|
+
`${deviceApi}/api/threads/${threadId}`, userToken,
|
|
101
|
+
(value) => JSON.stringify(value).includes('IOS_STREAM_COMPLETED'), 35_000,
|
|
102
|
+
);
|
|
103
|
+
const lastTurn = detail.turns?.at?.(-1);
|
|
104
|
+
if (lastTurn?.status !== 'completed') {
|
|
105
|
+
throw new Error(`Fake runtime turn did not complete: ${JSON.stringify(lastTurn)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await jsonRequest(`${deviceApi}/api/threads/${threadId}/prompt`, {
|
|
109
|
+
method: 'POST', token: userToken, body: { prompt: 'WINDOWS_RELAY_FOLLOWUP' },
|
|
110
|
+
});
|
|
111
|
+
await waitForJson(
|
|
112
|
+
`${deviceApi}/api/threads/${threadId}`, userToken,
|
|
113
|
+
(value) => value.turns?.length === 2 && value.turns.at(-1)?.status === 'completed', 35_000,
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
await stopProcess(relay);
|
|
117
|
+
const restartedRelay = startProcess(relayEntry, relayEnvironment);
|
|
118
|
+
processes.push(restartedRelay);
|
|
119
|
+
await waitForHttp(`${relayBaseUrl}/healthz`, restartedRelay, 20_000);
|
|
120
|
+
await waitForHttp(`${deviceApi}/healthz`, supervisor, 30_000, userToken);
|
|
121
|
+
const reloadedAfterReconnect = await jsonRequest(`${deviceApi}/api/threads/${threadId}`, {
|
|
122
|
+
token: userToken,
|
|
123
|
+
});
|
|
124
|
+
if (reloadedAfterReconnect.turns?.length !== 2) {
|
|
125
|
+
throw new Error('Transcript did not survive Relay disconnect and reconnect.');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const status = await requestControl({ controlEndpoint, controlToken, instanceId }, 'status');
|
|
129
|
+
if (status.ok !== true || status.instanceId !== instanceId) {
|
|
130
|
+
throw new Error(`Unexpected lifecycle status: ${JSON.stringify(status)}`);
|
|
131
|
+
}
|
|
132
|
+
await requestControl({ controlEndpoint, controlToken, instanceId }, 'shutdown');
|
|
133
|
+
await waitForExit(supervisor, 10_000);
|
|
134
|
+
console.log(`Relay supervisor smoke passed on ${process.platform}: device ${deviceId}, thread ${threadId}.`);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const diagnostics = processes
|
|
137
|
+
.map((child) => `${path.basename(child.entry)} output:\n${child.output()}`).join('\n');
|
|
138
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}\n${diagnostics}`);
|
|
139
|
+
} finally {
|
|
140
|
+
await Promise.allSettled(processes.map((child) => stopProcess(child)));
|
|
141
|
+
await fsp.rm(temporaryRoot, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function startProcess(entry, additionalEnv) {
|
|
145
|
+
const child = crossSpawn(process.execPath, [entry], {
|
|
146
|
+
cwd: packageRoot,
|
|
147
|
+
windowsHide: true,
|
|
148
|
+
env: { ...process.env, NODE_ENV: 'production', LOG_LEVEL: 'warn', ...additionalEnv },
|
|
149
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
150
|
+
});
|
|
151
|
+
let output = '';
|
|
152
|
+
child.stdout?.on('data', (chunk) => { output += String(chunk); });
|
|
153
|
+
child.stderr?.on('data', (chunk) => { output += String(chunk); });
|
|
154
|
+
return Object.assign(child, { entry, output: () => output });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function jsonRequest(url, options = {}) {
|
|
158
|
+
const response = await fetch(url, {
|
|
159
|
+
method: options.method ?? 'GET',
|
|
160
|
+
headers: {
|
|
161
|
+
...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
|
|
162
|
+
...(options.body ? { 'content-type': 'application/json' } : {}),
|
|
163
|
+
},
|
|
164
|
+
...(options.body ? { body: JSON.stringify(options.body) } : {}),
|
|
165
|
+
signal: AbortSignal.timeout(5_000),
|
|
166
|
+
});
|
|
167
|
+
const text = await response.text();
|
|
168
|
+
let value;
|
|
169
|
+
try { value = text ? JSON.parse(text) : null; } catch { value = text; }
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
throw new Error(`${options.method ?? 'GET'} ${url} returned ${response.status}: ${text}`);
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function waitForHttp(url, child, timeoutMs, token) {
|
|
177
|
+
const deadline = Date.now() + timeoutMs;
|
|
178
|
+
while (Date.now() < deadline) {
|
|
179
|
+
if (child.exitCode !== null) throw new Error(`${path.basename(child.entry)} exited with ${child.exitCode}.`);
|
|
180
|
+
try { await jsonRequest(url, { token }); return; } catch { await delay(200); }
|
|
181
|
+
}
|
|
182
|
+
throw new Error(`Timed out waiting for ${url}.`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function waitForJson(url, token, predicate, timeoutMs) {
|
|
186
|
+
const deadline = Date.now() + timeoutMs;
|
|
187
|
+
let latest = null;
|
|
188
|
+
while (Date.now() < deadline) {
|
|
189
|
+
latest = await jsonRequest(url, { token });
|
|
190
|
+
if (predicate(latest)) return latest;
|
|
191
|
+
await delay(100);
|
|
192
|
+
}
|
|
193
|
+
throw new Error(`Timed out waiting for expected response from ${url}: ${JSON.stringify(latest)}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function requiredString(value, label) {
|
|
197
|
+
if (typeof value !== 'string' || !value) throw new Error(`Missing ${label}: ${JSON.stringify(value)}`);
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function reservePort() {
|
|
202
|
+
return new Promise((resolve, reject) => {
|
|
203
|
+
const server = net.createServer();
|
|
204
|
+
server.once('error', reject);
|
|
205
|
+
server.listen(0, '127.0.0.1', () => {
|
|
206
|
+
const address = server.address();
|
|
207
|
+
const port = typeof address === 'object' && address ? address.port : null;
|
|
208
|
+
server.close(() => port ? resolve(port) : reject(new Error('Unable to reserve a port.')));
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function requestControl(state, action) {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const socket = net.createConnection(state.controlEndpoint);
|
|
216
|
+
const timer = setTimeout(() => { socket.destroy(); reject(new Error('Lifecycle control timed out.')); }, 2_000);
|
|
217
|
+
let output = '';
|
|
218
|
+
socket.setEncoding('utf8');
|
|
219
|
+
socket.once('connect', () => socket.write(`${JSON.stringify({
|
|
220
|
+
action, token: state.controlToken, instanceId: state.instanceId,
|
|
221
|
+
})}\n`));
|
|
222
|
+
socket.on('data', (chunk) => {
|
|
223
|
+
output += chunk;
|
|
224
|
+
const newline = output.indexOf('\n');
|
|
225
|
+
if (newline < 0) return;
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
socket.end();
|
|
228
|
+
resolve(JSON.parse(output.slice(0, newline)));
|
|
229
|
+
});
|
|
230
|
+
socket.once('error', (error) => { clearTimeout(timer); reject(error); });
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function waitForExit(child, timeoutMs) {
|
|
235
|
+
if (child.exitCode !== null) return Promise.resolve();
|
|
236
|
+
return new Promise((resolve, reject) => {
|
|
237
|
+
const timer = setTimeout(() => reject(new Error(`${path.basename(child.entry)} did not exit cleanly.`)), timeoutMs);
|
|
238
|
+
child.once('exit', () => { clearTimeout(timer); resolve(); });
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function stopProcess(child) {
|
|
243
|
+
if (child.exitCode !== null) return;
|
|
244
|
+
child.kill('SIGTERM');
|
|
245
|
+
try {
|
|
246
|
+
await waitForExit(child, 3_000);
|
|
247
|
+
} catch {
|
|
248
|
+
if (process.platform === 'win32' && child.pid) {
|
|
249
|
+
await new Promise((resolve) => {
|
|
250
|
+
const killer = crossSpawn('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
|
|
251
|
+
killer.once('close', resolve);
|
|
252
|
+
killer.once('error', resolve);
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
child.kill('SIGKILL');
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function delay(milliseconds) {
|
|
261
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
262
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[string]$TaskName = 'Remote Codex Relay Supervisor',
|
|
4
|
+
[string]$PackageRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
$node = (Get-Command node.exe -ErrorAction Stop).Source
|
|
9
|
+
$entry = Join-Path $PackageRoot 'bin\remote-codex.mjs'
|
|
10
|
+
if (-not (Test-Path -LiteralPath $entry -PathType Leaf)) {
|
|
11
|
+
throw "Remote Codex entrypoint was not found: $entry"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
|
15
|
+
$action = New-ScheduledTaskAction `
|
|
16
|
+
-Execute $node `
|
|
17
|
+
-Argument ('"{0}" relay-supervisor start' -f $entry) `
|
|
18
|
+
-WorkingDirectory $PackageRoot
|
|
19
|
+
$trigger = New-ScheduledTaskTrigger -AtLogOn -User $identity
|
|
20
|
+
$principal = New-ScheduledTaskPrincipal `
|
|
21
|
+
-UserId $identity `
|
|
22
|
+
-LogonType Interactive `
|
|
23
|
+
-RunLevel Limited
|
|
24
|
+
$settings = New-ScheduledTaskSettingsSet `
|
|
25
|
+
-AllowStartIfOnBatteries `
|
|
26
|
+
-DontStopIfGoingOnBatteries `
|
|
27
|
+
-RestartCount 5 `
|
|
28
|
+
-RestartInterval (New-TimeSpan -Minutes 1) `
|
|
29
|
+
-ExecutionTimeLimit ([TimeSpan]::Zero) `
|
|
30
|
+
-MultipleInstances IgnoreNew
|
|
31
|
+
|
|
32
|
+
Register-ScheduledTask `
|
|
33
|
+
-TaskName $TaskName `
|
|
34
|
+
-Action $action `
|
|
35
|
+
-Trigger $trigger `
|
|
36
|
+
-Principal $principal `
|
|
37
|
+
-Settings $settings `
|
|
38
|
+
-Description 'Starts the current user Remote Codex Relay Supervisor after logon.' `
|
|
39
|
+
-Force | Out-Null
|
|
40
|
+
|
|
41
|
+
Write-Host "Installed scheduled task: $TaskName"
|
|
42
|
+
Write-Host 'Starting the relay supervisor now...'
|
|
43
|
+
& $node $entry relay-supervisor start
|
|
44
|
+
exit $LASTEXITCODE
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
$ErrorActionPreference = 'Stop'
|
|
2
|
+
|
|
3
|
+
$PackageRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..')
|
|
4
|
+
$SmokeScript = Join-Path $PackageRoot 'scripts\verify-relay-supervisor-smoke.mjs'
|
|
5
|
+
$Node = (Get-Command node.exe -ErrorAction Stop).Source
|
|
6
|
+
|
|
7
|
+
Push-Location $PackageRoot
|
|
8
|
+
try {
|
|
9
|
+
& $Node $SmokeScript
|
|
10
|
+
if ($LASTEXITCODE -ne 0) {
|
|
11
|
+
throw "Relay smoke test exited with code $LASTEXITCODE."
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
finally {
|
|
15
|
+
Pop-Location
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[string]$TaskName = 'Remote Codex Relay Supervisor',
|
|
4
|
+
[switch]$PurgeData,
|
|
5
|
+
[string]$PackageRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
$ErrorActionPreference = 'Stop'
|
|
9
|
+
$node = (Get-Command node.exe -ErrorAction Stop).Source
|
|
10
|
+
$entry = Join-Path $PackageRoot 'bin\remote-codex.mjs'
|
|
11
|
+
|
|
12
|
+
if (Test-Path -LiteralPath $entry -PathType Leaf) {
|
|
13
|
+
& $node $entry relay-supervisor stop
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
|
17
|
+
Write-Host "Removed scheduled task: $TaskName"
|
|
18
|
+
|
|
19
|
+
if ($PurgeData) {
|
|
20
|
+
$dataDirectory = Join-Path $env:USERPROFILE '.remote-codex'
|
|
21
|
+
if (Test-Path -LiteralPath $dataDirectory -PathType Container) {
|
|
22
|
+
Remove-Item -LiteralPath $dataDirectory -Recurse -Force
|
|
23
|
+
Write-Host "Removed data directory: $dataDirectory"
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
Write-Host 'Configuration, logs, and databases were preserved under %USERPROFILE%\.remote-codex.'
|
|
27
|
+
}
|