@task-handoff/server 0.0.3-alpha.3
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 +3 -0
- package/bin/task-handoff-install-server +52 -0
- package/bin/task-handoff-install-server-services +250 -0
- package/bin/task-handoff-server +225 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
|
|
7
|
+
function resolvePackageRoot(packageName) {
|
|
8
|
+
const manifest = require.resolve(`${packageName}/package.json`, { paths: [__dirname] });
|
|
9
|
+
return path.dirname(manifest);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function packageBin(packageName, binName) {
|
|
13
|
+
const packageRoot = resolvePackageRoot(packageName);
|
|
14
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
15
|
+
const relativeBin = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[binName];
|
|
16
|
+
if (!relativeBin) {
|
|
17
|
+
throw new Error(`${packageName} does not provide the ${binName} executable.`);
|
|
18
|
+
}
|
|
19
|
+
return path.join(packageRoot, relativeBin);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let controlPlane;
|
|
23
|
+
let nodeAgent;
|
|
24
|
+
let controlledInstance;
|
|
25
|
+
try {
|
|
26
|
+
controlPlane = packageBin("@task-handoff/control-plane", "task-handoff-control-plane");
|
|
27
|
+
nodeAgent = packageBin("@task-handoff/node-agent", "task-handoff-node-agent");
|
|
28
|
+
controlledInstance = packageBin("@task-handoff/controlled-instance", "task-handoff-controlled-instance");
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(`TaskHandoff server package is incomplete: ${error.message}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const result = spawnSync(
|
|
35
|
+
path.join(__dirname, "task-handoff-install-server-services"),
|
|
36
|
+
[
|
|
37
|
+
"--control-plane-bin",
|
|
38
|
+
controlPlane,
|
|
39
|
+
"--node-agent-bin",
|
|
40
|
+
nodeAgent,
|
|
41
|
+
"--controlled-instance-bin",
|
|
42
|
+
controlledInstance,
|
|
43
|
+
...process.argv.slice(2),
|
|
44
|
+
],
|
|
45
|
+
{ stdio: "inherit" },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (result.error) {
|
|
49
|
+
console.error(`Failed to start the TaskHandoff service installer: ${result.error.message}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
process.exit(result.status ?? 1);
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -eu
|
|
3
|
+
|
|
4
|
+
INSTALLER_BIN_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
|
5
|
+
REPO_DIR="$(CDPATH= cd -- "$INSTALLER_BIN_DIR/.." && pwd)"
|
|
6
|
+
TASK_HANDOFF_BIN=""
|
|
7
|
+
TASK_HANDOFF_COMMAND=""
|
|
8
|
+
CONTROL_PLANE_BIN=""
|
|
9
|
+
NODE_AGENT_BIN=""
|
|
10
|
+
CONTROLLED_INSTANCE_BIN=""
|
|
11
|
+
CONTROL_PLANE_COMMAND=""
|
|
12
|
+
NODE_AGENT_COMMAND=""
|
|
13
|
+
CONTROLLED_INSTANCE_COMMAND=""
|
|
14
|
+
SERVICE_USER="root"
|
|
15
|
+
ENV_DIR="/etc/task-handoff"
|
|
16
|
+
CONTROL_PLANE_DATA_DIR="/var/lib/task-handoff/control-plane"
|
|
17
|
+
NODE_AGENT_DATA_DIR="/var/lib/task-handoff/node-agent"
|
|
18
|
+
CONTROL_PLANE_HOST="0.0.0.0"
|
|
19
|
+
CONTROL_PLANE_PORT="8081"
|
|
20
|
+
NODE_AGENT_HOST="127.0.0.1"
|
|
21
|
+
NODE_AGENT_PORT="8091"
|
|
22
|
+
NODE_AGENT_IPC_PATH="/run/task-handoff/node-agent.sock"
|
|
23
|
+
AUTH_MODE="password"
|
|
24
|
+
STATIC_DIR=""
|
|
25
|
+
|
|
26
|
+
usage() {
|
|
27
|
+
cat <<'USAGE'
|
|
28
|
+
Usage: scripts/install-server-services.sh [options]
|
|
29
|
+
|
|
30
|
+
Installs server-side TaskHandoff services on a systemd host:
|
|
31
|
+
task-handoff-node-agent.service
|
|
32
|
+
task-handoff-control-plane.service
|
|
33
|
+
|
|
34
|
+
Options:
|
|
35
|
+
--repo-dir <path> Repository or unpacked release directory
|
|
36
|
+
--task-handoff-bin <path> Legacy combined task-handoff executable
|
|
37
|
+
--control-plane-bin <path> Control-plane executable
|
|
38
|
+
--node-agent-bin <path> Node-agent executable
|
|
39
|
+
--controlled-instance-bin <path> Controlled-instance executable for local runtimes
|
|
40
|
+
--service-user <user> systemd service user, default root
|
|
41
|
+
--control-plane-data-dir <path> Control-plane data directory
|
|
42
|
+
--node-agent-data-dir <path> Node-agent data directory
|
|
43
|
+
--control-plane-host <host> Control-plane bind host, default 0.0.0.0
|
|
44
|
+
--control-plane-port <port> Control-plane port, default 8081
|
|
45
|
+
--node-agent-host <host> Local node-agent bind host, default 127.0.0.1
|
|
46
|
+
--node-agent-port <port> Local node-agent port, default 8091
|
|
47
|
+
--node-agent-ipc-path <path> Local control socket, default /run/task-handoff/node-agent.sock
|
|
48
|
+
--auth-mode <mode> Control-plane auth mode: password or disabled
|
|
49
|
+
--static-dir <path> Built control-plane UI directory
|
|
50
|
+
USAGE
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
need_root() {
|
|
54
|
+
if [ "$(id -u)" != "0" ]; then
|
|
55
|
+
echo "This installer must run as root. Re-run with sudo." >&2
|
|
56
|
+
exit 1
|
|
57
|
+
fi
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
while [ "$#" -gt 0 ]; do
|
|
61
|
+
case "$1" in
|
|
62
|
+
--repo-dir) REPO_DIR="${2:-}"; shift 2 ;;
|
|
63
|
+
--task-handoff-bin) TASK_HANDOFF_BIN="${2:-}"; shift 2 ;;
|
|
64
|
+
--control-plane-bin) CONTROL_PLANE_BIN="${2:-}"; shift 2 ;;
|
|
65
|
+
--node-agent-bin) NODE_AGENT_BIN="${2:-}"; shift 2 ;;
|
|
66
|
+
--controlled-instance-bin) CONTROLLED_INSTANCE_BIN="${2:-}"; shift 2 ;;
|
|
67
|
+
--service-user) SERVICE_USER="${2:-}"; shift 2 ;;
|
|
68
|
+
--control-plane-data-dir) CONTROL_PLANE_DATA_DIR="${2:-}"; shift 2 ;;
|
|
69
|
+
--node-agent-data-dir) NODE_AGENT_DATA_DIR="${2:-}"; shift 2 ;;
|
|
70
|
+
--control-plane-host) CONTROL_PLANE_HOST="${2:-}"; shift 2 ;;
|
|
71
|
+
--control-plane-port) CONTROL_PLANE_PORT="${2:-}"; shift 2 ;;
|
|
72
|
+
--node-agent-host) NODE_AGENT_HOST="${2:-}"; shift 2 ;;
|
|
73
|
+
--node-agent-port) NODE_AGENT_PORT="${2:-}"; shift 2 ;;
|
|
74
|
+
--node-agent-ipc-path) NODE_AGENT_IPC_PATH="${2:-}"; shift 2 ;;
|
|
75
|
+
--auth-mode) AUTH_MODE="${2:-}"; shift 2 ;;
|
|
76
|
+
--static-dir) STATIC_DIR="${2:-}"; shift 2 ;;
|
|
77
|
+
-h|--help) usage; exit 0 ;;
|
|
78
|
+
*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
|
79
|
+
esac
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
need_root
|
|
83
|
+
|
|
84
|
+
if [ "$AUTH_MODE" != "password" ] && [ "$AUTH_MODE" != "disabled" ]; then
|
|
85
|
+
echo "--auth-mode must be password or disabled." >&2
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
if ! command -v systemctl >/dev/null 2>&1; then
|
|
90
|
+
echo "systemd is required for this installer." >&2
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
94
|
+
echo "node is required for this installer." >&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
resolve_command() {
|
|
99
|
+
candidate="$1"
|
|
100
|
+
if [ -f "$candidate" ]; then
|
|
101
|
+
if [ -x "$candidate" ]; then
|
|
102
|
+
printf '%s\n' "$candidate"
|
|
103
|
+
else
|
|
104
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
105
|
+
echo "node is required to run $candidate." >&2
|
|
106
|
+
exit 1
|
|
107
|
+
fi
|
|
108
|
+
printf '%s %s\n' "$(command -v node)" "$candidate"
|
|
109
|
+
fi
|
|
110
|
+
elif command -v "$candidate" >/dev/null 2>&1; then
|
|
111
|
+
command -v "$candidate"
|
|
112
|
+
else
|
|
113
|
+
echo "TaskHandoff executable was not found: $candidate" >&2
|
|
114
|
+
exit 1
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if [ -n "$TASK_HANDOFF_BIN" ]; then
|
|
119
|
+
TASK_HANDOFF_COMMAND="$(resolve_command "$TASK_HANDOFF_BIN")"
|
|
120
|
+
elif [ -z "$CONTROL_PLANE_BIN" ] && [ -z "$NODE_AGENT_BIN" ] && [ -z "$CONTROLLED_INSTANCE_BIN" ] && [ -f "$REPO_DIR/bin/task-handoff.js" ]; then
|
|
121
|
+
TASK_HANDOFF_COMMAND="$(resolve_command "$REPO_DIR/bin/task-handoff.js")"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
if [ -n "$TASK_HANDOFF_COMMAND" ]; then
|
|
125
|
+
CONTROL_PLANE_COMMAND="$TASK_HANDOFF_COMMAND control-plane"
|
|
126
|
+
NODE_AGENT_COMMAND="$TASK_HANDOFF_COMMAND node-agent"
|
|
127
|
+
CONTROLLED_INSTANCE_COMMAND="$TASK_HANDOFF_COMMAND web"
|
|
128
|
+
if [ -z "$STATIC_DIR" ]; then
|
|
129
|
+
STATIC_DIR="$REPO_DIR/packages/control-plane-ui/dist"
|
|
130
|
+
fi
|
|
131
|
+
else
|
|
132
|
+
if [ -z "$CONTROL_PLANE_BIN" ] && [ -f "$INSTALLER_BIN_DIR/task-handoff-control-plane" ]; then
|
|
133
|
+
CONTROL_PLANE_BIN="$INSTALLER_BIN_DIR/task-handoff-control-plane"
|
|
134
|
+
fi
|
|
135
|
+
if [ -z "$NODE_AGENT_BIN" ] && [ -f "$INSTALLER_BIN_DIR/task-handoff-node-agent" ]; then
|
|
136
|
+
NODE_AGENT_BIN="$INSTALLER_BIN_DIR/task-handoff-node-agent"
|
|
137
|
+
fi
|
|
138
|
+
if [ -z "$CONTROLLED_INSTANCE_BIN" ] && [ -f "$INSTALLER_BIN_DIR/task-handoff-controlled-instance" ]; then
|
|
139
|
+
CONTROLLED_INSTANCE_BIN="$INSTALLER_BIN_DIR/task-handoff-controlled-instance"
|
|
140
|
+
fi
|
|
141
|
+
CONTROL_PLANE_COMMAND="$(resolve_command "${CONTROL_PLANE_BIN:-task-handoff-control-plane}")"
|
|
142
|
+
NODE_AGENT_COMMAND="$(resolve_command "${NODE_AGENT_BIN:-task-handoff-node-agent}")"
|
|
143
|
+
CONTROLLED_INSTANCE_COMMAND="$(resolve_command "${CONTROLLED_INSTANCE_BIN:-task-handoff-controlled-instance}") web"
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
CONTROL_PLANE_STATIC_OPTION=""
|
|
147
|
+
if [ -n "$STATIC_DIR" ]; then
|
|
148
|
+
CONTROL_PLANE_STATIC_OPTION="--static-dir $STATIC_DIR"
|
|
149
|
+
fi
|
|
150
|
+
|
|
151
|
+
NODE_AGENT_IPC_ENDPOINT="$(node -e 'process.stdout.write(`ipc://${encodeURIComponent(process.argv[1])}`)' "$NODE_AGENT_IPC_PATH")"
|
|
152
|
+
|
|
153
|
+
if [ "$SERVICE_USER" != "root" ] && ! id "$SERVICE_USER" >/dev/null 2>&1; then
|
|
154
|
+
useradd --system --create-home --home-dir /var/lib/task-handoff --shell /usr/sbin/nologin "$SERVICE_USER"
|
|
155
|
+
fi
|
|
156
|
+
if [ "$SERVICE_USER" != "root" ] && getent group docker >/dev/null 2>&1; then
|
|
157
|
+
usermod -aG docker "$SERVICE_USER" || true
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
assert_service_command_accessible() {
|
|
161
|
+
command_value="$1"
|
|
162
|
+
command_label="$2"
|
|
163
|
+
executable="${command_value%% *}"
|
|
164
|
+
if [ "$SERVICE_USER" != "root" ] && command -v runuser >/dev/null 2>&1 && ! runuser -u "$SERVICE_USER" -- test -x "$executable"; then
|
|
165
|
+
echo "$command_label is not executable by service user $SERVICE_USER: $executable" >&2
|
|
166
|
+
echo "Install Node.js and TaskHandoff under a system-wide prefix such as /usr/local, not a root-only NVM directory." >&2
|
|
167
|
+
exit 1
|
|
168
|
+
fi
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
assert_service_command_accessible "$CONTROL_PLANE_COMMAND" "Control-plane command"
|
|
172
|
+
assert_service_command_accessible "$NODE_AGENT_COMMAND" "Node-agent command"
|
|
173
|
+
assert_service_command_accessible "$CONTROLLED_INSTANCE_COMMAND" "Controlled-instance command"
|
|
174
|
+
|
|
175
|
+
mkdir -p "$ENV_DIR" "$CONTROL_PLANE_DATA_DIR" "$NODE_AGENT_DATA_DIR"
|
|
176
|
+
chown -R "$SERVICE_USER":"$SERVICE_USER" "$CONTROL_PLANE_DATA_DIR" "$NODE_AGENT_DATA_DIR" 2>/dev/null || true
|
|
177
|
+
|
|
178
|
+
cat > "$ENV_DIR/node-agent.env" <<EOF
|
|
179
|
+
TASK_HANDOFF_NODE_AGENT_HOST=$NODE_AGENT_HOST
|
|
180
|
+
TASK_HANDOFF_NODE_AGENT_PORT=$NODE_AGENT_PORT
|
|
181
|
+
TASK_HANDOFF_NODE_AGENT_DATA_DIR=$NODE_AGENT_DATA_DIR
|
|
182
|
+
TASK_HANDOFF_NODE_AGENT_CONNECTION_MODE=local-ipc
|
|
183
|
+
TASK_HANDOFF_NODE_AGENT_IPC_PATH=$NODE_AGENT_IPC_PATH
|
|
184
|
+
TASK_HANDOFF_NODE_AGENT_CONTAINER_URL=http://host.docker.internal:$NODE_AGENT_PORT
|
|
185
|
+
TASK_HANDOFF_LOCAL_CONTROLLED_COMMAND=$CONTROLLED_INSTANCE_COMMAND
|
|
186
|
+
EOF
|
|
187
|
+
chmod 0640 "$ENV_DIR/node-agent.env"
|
|
188
|
+
|
|
189
|
+
cat > "$ENV_DIR/control-plane.env" <<EOF
|
|
190
|
+
TASK_HANDOFF_CONTROL_PLANE_HOST=$CONTROL_PLANE_HOST
|
|
191
|
+
TASK_HANDOFF_CONTROL_PLANE_PORT=$CONTROL_PLANE_PORT
|
|
192
|
+
TASK_HANDOFF_CONTROL_PLANE_DATA_DIR=$CONTROL_PLANE_DATA_DIR
|
|
193
|
+
TASK_HANDOFF_CONTROL_PLANE_STATIC_DIR=$STATIC_DIR
|
|
194
|
+
TASK_HANDOFF_CONTROL_PLANE_AUTH_MODE=$AUTH_MODE
|
|
195
|
+
TASK_HANDOFF_NODE_AGENT_ENDPOINT=http://$NODE_AGENT_HOST:$NODE_AGENT_PORT
|
|
196
|
+
TASK_HANDOFF_NODE_AGENT_CONTROL_ENDPOINT=$NODE_AGENT_IPC_ENDPOINT
|
|
197
|
+
TASK_HANDOFF_NODE_AGENT_CONTAINER_URL=http://host.docker.internal:$NODE_AGENT_PORT
|
|
198
|
+
EOF
|
|
199
|
+
chmod 0640 "$ENV_DIR/control-plane.env"
|
|
200
|
+
|
|
201
|
+
cat > /etc/systemd/system/task-handoff-node-agent.service <<EOF
|
|
202
|
+
[Unit]
|
|
203
|
+
Description=TaskHandoff Local Node Agent
|
|
204
|
+
After=network-online.target docker.service
|
|
205
|
+
Wants=network-online.target
|
|
206
|
+
|
|
207
|
+
[Service]
|
|
208
|
+
Type=simple
|
|
209
|
+
User=$SERVICE_USER
|
|
210
|
+
WorkingDirectory=$NODE_AGENT_DATA_DIR
|
|
211
|
+
RuntimeDirectory=task-handoff
|
|
212
|
+
RuntimeDirectoryMode=0700
|
|
213
|
+
EnvironmentFile=-$ENV_DIR/node-agent.env
|
|
214
|
+
ExecStart=$NODE_AGENT_COMMAND --host $NODE_AGENT_HOST --port $NODE_AGENT_PORT --data-dir $NODE_AGENT_DATA_DIR --connection-mode local-ipc --ipc-path $NODE_AGENT_IPC_PATH
|
|
215
|
+
Restart=always
|
|
216
|
+
RestartSec=3
|
|
217
|
+
KillSignal=SIGTERM
|
|
218
|
+
|
|
219
|
+
[Install]
|
|
220
|
+
WantedBy=multi-user.target
|
|
221
|
+
EOF
|
|
222
|
+
|
|
223
|
+
cat > /etc/systemd/system/task-handoff-control-plane.service <<EOF
|
|
224
|
+
[Unit]
|
|
225
|
+
Description=TaskHandoff Control Plane
|
|
226
|
+
After=network-online.target task-handoff-node-agent.service
|
|
227
|
+
Wants=network-online.target task-handoff-node-agent.service
|
|
228
|
+
|
|
229
|
+
[Service]
|
|
230
|
+
Type=simple
|
|
231
|
+
User=$SERVICE_USER
|
|
232
|
+
WorkingDirectory=$CONTROL_PLANE_DATA_DIR
|
|
233
|
+
EnvironmentFile=-$ENV_DIR/control-plane.env
|
|
234
|
+
ExecStartPre=/bin/sh -c 'for i in \$(seq 1 30); do [ -S "$NODE_AGENT_IPC_PATH" ] && exit 0; sleep 1; done; exit 1'
|
|
235
|
+
ExecStart=$CONTROL_PLANE_COMMAND --host $CONTROL_PLANE_HOST --port $CONTROL_PLANE_PORT --data-dir $CONTROL_PLANE_DATA_DIR $CONTROL_PLANE_STATIC_OPTION --auth-mode $AUTH_MODE
|
|
236
|
+
Restart=always
|
|
237
|
+
RestartSec=3
|
|
238
|
+
KillSignal=SIGTERM
|
|
239
|
+
|
|
240
|
+
[Install]
|
|
241
|
+
WantedBy=multi-user.target
|
|
242
|
+
EOF
|
|
243
|
+
|
|
244
|
+
systemctl daemon-reload
|
|
245
|
+
systemctl enable --now task-handoff-node-agent.service
|
|
246
|
+
systemctl enable --now task-handoff-control-plane.service
|
|
247
|
+
|
|
248
|
+
echo "TaskHandoff server services are installed."
|
|
249
|
+
echo "Control plane: task-handoff-control-plane.service on $CONTROL_PLANE_HOST:$CONTROL_PLANE_PORT"
|
|
250
|
+
echo "Local node-agent: task-handoff-node-agent.service on $NODE_AGENT_HOST:$NODE_AGENT_PORT"
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const http = require("node:http");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { spawnSync } = require("node:child_process");
|
|
7
|
+
|
|
8
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
9
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
console.log(`Usage: task-handoff-server <command> [options]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
status Show installed version and service state
|
|
16
|
+
check [options] Check npm for an available version
|
|
17
|
+
update [options] Update from npm and restart services safely
|
|
18
|
+
|
|
19
|
+
Update options:
|
|
20
|
+
--channel <channel> npm channel: stable, beta, or alpha (default: stable)
|
|
21
|
+
--to <version> Install an exact version instead of a channel
|
|
22
|
+
--registry <url> Use a specific npm registry
|
|
23
|
+
--force Reinstall even when the resolved version is unchanged
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function run(command, args, options = {}) {
|
|
28
|
+
const result = spawnSync(command, args, { encoding: "utf8", ...options });
|
|
29
|
+
if (result.status !== 0) {
|
|
30
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
31
|
+
throw new Error(`${command} ${args.join(" ")} failed${output ? `:\n${output}` : ""}`);
|
|
32
|
+
}
|
|
33
|
+
return String(result.stdout || "").trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function findGlobalPrefix() {
|
|
37
|
+
let current = packageRoot;
|
|
38
|
+
while (current !== path.dirname(current)) {
|
|
39
|
+
if (path.basename(current) === "node_modules" && path.basename(path.dirname(current)) === "lib") {
|
|
40
|
+
return path.dirname(path.dirname(current));
|
|
41
|
+
}
|
|
42
|
+
current = path.dirname(current);
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`Cannot determine the npm global prefix from ${packageRoot}.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseEnvFile(file) {
|
|
48
|
+
const values = {};
|
|
49
|
+
if (!fs.existsSync(file)) return values;
|
|
50
|
+
for (const line of fs.readFileSync(file, "utf8").split(/\r?\n/)) {
|
|
51
|
+
if (!line || line.startsWith("#")) continue;
|
|
52
|
+
const separator = line.indexOf("=");
|
|
53
|
+
if (separator <= 0) continue;
|
|
54
|
+
values[line.slice(0, separator)] = line.slice(separator + 1);
|
|
55
|
+
}
|
|
56
|
+
return values;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function serviceUser() {
|
|
60
|
+
const unit = "/etc/systemd/system/task-handoff-node-agent.service";
|
|
61
|
+
if (!fs.existsSync(unit)) return "root";
|
|
62
|
+
const match = fs.readFileSync(unit, "utf8").match(/^User=(.+)$/m);
|
|
63
|
+
return match?.[1] || "root";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function currentInstallOptions() {
|
|
67
|
+
const controlPlane = parseEnvFile("/etc/task-handoff/control-plane.env");
|
|
68
|
+
const nodeAgent = parseEnvFile("/etc/task-handoff/node-agent.env");
|
|
69
|
+
return [
|
|
70
|
+
"--service-user", serviceUser(),
|
|
71
|
+
"--control-plane-data-dir", controlPlane.TASK_HANDOFF_CONTROL_PLANE_DATA_DIR || "/var/lib/task-handoff/control-plane",
|
|
72
|
+
"--node-agent-data-dir", nodeAgent.TASK_HANDOFF_NODE_AGENT_DATA_DIR || "/var/lib/task-handoff/node-agent",
|
|
73
|
+
"--control-plane-host", controlPlane.TASK_HANDOFF_CONTROL_PLANE_HOST || "0.0.0.0",
|
|
74
|
+
"--control-plane-port", controlPlane.TASK_HANDOFF_CONTROL_PLANE_PORT || "8081",
|
|
75
|
+
"--node-agent-host", nodeAgent.TASK_HANDOFF_NODE_AGENT_HOST || "127.0.0.1",
|
|
76
|
+
"--node-agent-port", nodeAgent.TASK_HANDOFF_NODE_AGENT_PORT || "8091",
|
|
77
|
+
"--node-agent-ipc-path", nodeAgent.TASK_HANDOFF_NODE_AGENT_IPC_PATH || "/run/task-handoff/node-agent.sock",
|
|
78
|
+
"--auth-mode", controlPlane.TASK_HANDOFF_CONTROL_PLANE_AUTH_MODE || "password",
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function systemctlState(service) {
|
|
83
|
+
const result = spawnSync("systemctl", ["is-active", service], { encoding: "utf8" });
|
|
84
|
+
return String(result.stdout || "unknown").trim() || "unknown";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function npmVersion(target, registry) {
|
|
88
|
+
const args = ["view", `@task-handoff/server@${target}`, "version", "--json"];
|
|
89
|
+
if (registry) args.push("--registry", registry);
|
|
90
|
+
const value = JSON.parse(run("npm", args));
|
|
91
|
+
if (typeof value !== "string") throw new Error(`npm target ${target} did not resolve to one version.`);
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function npmTagForChannel(channel) {
|
|
96
|
+
return channel === "stable" ? "latest" : channel;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function waitForSocket(socketPath, timeoutMs = 30000) {
|
|
100
|
+
const deadline = Date.now() + timeoutMs;
|
|
101
|
+
while (Date.now() < deadline) {
|
|
102
|
+
try {
|
|
103
|
+
if (fs.statSync(socketPath).isSocket()) return;
|
|
104
|
+
} catch {}
|
|
105
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250);
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`Node agent socket was not ready after ${timeoutMs}ms: ${socketPath}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function waitForHttp(port, timeoutMs = 30000) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
const deadline = Date.now() + timeoutMs;
|
|
113
|
+
const attempt = () => {
|
|
114
|
+
const request = http.get({ host: "127.0.0.1", port, path: "/api/health", timeout: 1000 }, (response) => {
|
|
115
|
+
response.resume();
|
|
116
|
+
if (response.statusCode === 200) return resolve();
|
|
117
|
+
retry();
|
|
118
|
+
});
|
|
119
|
+
request.on("timeout", () => request.destroy());
|
|
120
|
+
request.on("error", retry);
|
|
121
|
+
};
|
|
122
|
+
const retry = () => {
|
|
123
|
+
if (Date.now() >= deadline) return reject(new Error(`Control plane was not healthy on port ${port} after ${timeoutMs}ms.`));
|
|
124
|
+
setTimeout(attempt, 250);
|
|
125
|
+
};
|
|
126
|
+
attempt();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function acquireLock() {
|
|
131
|
+
const lock = "/run/task-handoff-server-update.lock";
|
|
132
|
+
try {
|
|
133
|
+
fs.mkdirSync(lock);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error.code === "EEXIST") throw new Error("Another TaskHandoff server update is already running.");
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
return () => fs.rmSync(lock, { recursive: true, force: true });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseOptions(args, allowExactVersion) {
|
|
142
|
+
let channel = "stable";
|
|
143
|
+
let channelExplicit = false;
|
|
144
|
+
let exactVersion = "";
|
|
145
|
+
let registry = "";
|
|
146
|
+
let force = false;
|
|
147
|
+
while (args.length) {
|
|
148
|
+
const option = args.shift();
|
|
149
|
+
if (option === "--channel") {
|
|
150
|
+
channel = args.shift() || "";
|
|
151
|
+
channelExplicit = true;
|
|
152
|
+
}
|
|
153
|
+
else if (option === "--to" && allowExactVersion) exactVersion = args.shift() || "";
|
|
154
|
+
else if (option === "--registry") registry = args.shift() || "";
|
|
155
|
+
else if (option === "--force" && allowExactVersion) force = true;
|
|
156
|
+
else throw new Error(`Unknown option: ${option}`);
|
|
157
|
+
}
|
|
158
|
+
if (!new Set(["stable", "beta", "alpha"]).has(channel)) {
|
|
159
|
+
throw new Error("--channel must be stable, beta, or alpha.");
|
|
160
|
+
}
|
|
161
|
+
if (channelExplicit && exactVersion) throw new Error("Use either --channel or --to, not both.");
|
|
162
|
+
if (allowExactVersion && exactVersion && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(exactVersion)) {
|
|
163
|
+
throw new Error("--to must be an exact semantic version.");
|
|
164
|
+
}
|
|
165
|
+
return { target: exactVersion || npmTagForChannel(channel), channel, registry, force };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function main() {
|
|
169
|
+
const command = process.argv[2];
|
|
170
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
171
|
+
usage();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (command === "status") {
|
|
175
|
+
console.log(`Package: @task-handoff/server ${manifest.version}`);
|
|
176
|
+
console.log(`Node agent: ${systemctlState("task-handoff-node-agent.service")}`);
|
|
177
|
+
console.log(`Control plane: ${systemctlState("task-handoff-control-plane.service")}`);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (command === "check") {
|
|
181
|
+
const { target, channel, registry } = parseOptions(process.argv.slice(3), false);
|
|
182
|
+
const available = npmVersion(target, registry);
|
|
183
|
+
console.log(`Installed: ${manifest.version}`);
|
|
184
|
+
console.log(`Available (${channel} channel, npm ${target}): ${available}`);
|
|
185
|
+
console.log(available === manifest.version ? "Up to date." : `Update available: task-handoff-server update --to ${available}`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (command !== "update") throw new Error(`Unknown command: ${command}`);
|
|
189
|
+
if (typeof process.getuid === "function" && process.getuid() !== 0) {
|
|
190
|
+
throw new Error("Run updates as root so npm and systemd can be updated.");
|
|
191
|
+
}
|
|
192
|
+
const { target, registry, force } = parseOptions(process.argv.slice(3), true);
|
|
193
|
+
const targetVersion = target === manifest.version ? manifest.version : npmVersion(target, registry);
|
|
194
|
+
if (targetVersion === manifest.version && !force) {
|
|
195
|
+
console.log(`@task-handoff/server ${manifest.version} is already installed.`);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const releaseLock = acquireLock();
|
|
199
|
+
try {
|
|
200
|
+
const prefix = findGlobalPrefix();
|
|
201
|
+
const installOptions = currentInstallOptions();
|
|
202
|
+
const nodeSocket = installOptions[installOptions.indexOf("--node-agent-ipc-path") + 1];
|
|
203
|
+
const controlPlanePort = Number(installOptions[installOptions.indexOf("--control-plane-port") + 1]);
|
|
204
|
+
console.log(`Updating @task-handoff/server ${manifest.version} -> ${targetVersion}`);
|
|
205
|
+
const npmArgs = ["install", "--global", "--prefix", prefix, `@task-handoff/server@${targetVersion}`];
|
|
206
|
+
if (registry) npmArgs.push("--registry", registry);
|
|
207
|
+
run("npm", npmArgs, { stdio: "inherit" });
|
|
208
|
+
run(path.join(prefix, "bin", "task-handoff-install-server"), installOptions, { stdio: "inherit" });
|
|
209
|
+
run("systemctl", ["restart", "task-handoff-node-agent.service"]);
|
|
210
|
+
waitForSocket(nodeSocket);
|
|
211
|
+
run("systemctl", ["restart", "task-handoff-control-plane.service"]);
|
|
212
|
+
await waitForHttp(controlPlanePort);
|
|
213
|
+
console.log(`Updated TaskHandoff server to ${targetVersion}.`);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error(`Update failed. To reinstall the previous version, run: npm install -g @task-handoff/server@${manifest.version}`);
|
|
216
|
+
throw error;
|
|
217
|
+
} finally {
|
|
218
|
+
releaseLock();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main().catch((error) => {
|
|
223
|
+
console.error(`Error: ${error.message}`);
|
|
224
|
+
process.exit(1);
|
|
225
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@task-handoff/server",
|
|
3
|
+
"version": "0.0.3-alpha.3",
|
|
4
|
+
"description": "Complete TaskHandoff server package.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"task-handoff-install-server": "bin/task-handoff-install-server",
|
|
9
|
+
"task-handoff-server-install": "bin/task-handoff-install-server",
|
|
10
|
+
"task-handoff-server": "bin/task-handoff-server"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=22.22.2"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@task-handoff/control-plane": "0.0.3-alpha.3",
|
|
21
|
+
"@task-handoff/node-agent": "0.0.3-alpha.3",
|
|
22
|
+
"@task-handoff/controlled-instance": "0.0.3-alpha.3"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
}
|
|
27
|
+
}
|