@tpsdev-ai/cli 0.1.0 → 0.2.0
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/dist/bin/tps.js +154 -7
- package/dist/bin/tps.js.map +1 -1
- package/dist/src/commands/backup.d.ts +19 -0
- package/dist/src/commands/backup.d.ts.map +1 -0
- package/dist/src/commands/backup.js +595 -0
- package/dist/src/commands/backup.js.map +1 -0
- package/dist/src/commands/bootstrap.d.ts +7 -0
- package/dist/src/commands/bootstrap.d.ts.map +1 -0
- package/dist/src/commands/bootstrap.js +255 -0
- package/dist/src/commands/bootstrap.js.map +1 -0
- package/dist/src/commands/git.d.ts +8 -0
- package/dist/src/commands/git.d.ts.map +1 -0
- package/dist/src/commands/git.js +53 -0
- package/dist/src/commands/git.js.map +1 -0
- package/dist/src/commands/identity.d.ts +1 -1
- package/dist/src/commands/identity.d.ts.map +1 -1
- package/dist/src/commands/identity.js +6 -6
- package/dist/src/commands/identity.js.map +1 -1
- package/dist/src/commands/mail.d.ts +1 -1
- package/dist/src/commands/mail.d.ts.map +1 -1
- package/dist/src/commands/mail.js +33 -7
- package/dist/src/commands/mail.js.map +1 -1
- package/dist/src/commands/office-manager.d.ts +147 -0
- package/dist/src/commands/office-manager.d.ts.map +1 -0
- package/dist/src/commands/office-manager.js +171 -0
- package/dist/src/commands/office-manager.js.map +1 -0
- package/dist/src/commands/office.d.ts +4 -11
- package/dist/src/commands/office.d.ts.map +1 -1
- package/dist/src/commands/office.js +266 -384
- package/dist/src/commands/office.js.map +1 -1
- package/dist/src/commands/secrets.d.ts +9 -0
- package/dist/src/commands/secrets.d.ts.map +1 -0
- package/dist/src/commands/secrets.js +54 -0
- package/dist/src/commands/secrets.js.map +1 -0
- package/dist/src/commands/status.d.ts +33 -0
- package/dist/src/commands/status.d.ts.map +1 -0
- package/dist/src/commands/status.js +407 -0
- package/dist/src/commands/status.js.map +1 -0
- package/dist/src/generators/brief.d.ts +6 -0
- package/dist/src/generators/brief.d.ts.map +1 -0
- package/dist/src/generators/brief.js +33 -0
- package/dist/src/generators/brief.js.map +1 -0
- package/dist/src/generators/claude-code.d.ts +1 -0
- package/dist/src/generators/claude-code.d.ts.map +1 -1
- package/dist/src/generators/claude-code.js +5 -0
- package/dist/src/generators/claude-code.js.map +1 -1
- package/dist/src/generators/codex.d.ts +1 -0
- package/dist/src/generators/codex.d.ts.map +1 -1
- package/dist/src/generators/codex.js +5 -0
- package/dist/src/generators/codex.js.map +1 -1
- package/dist/src/generators/openclaw.d.ts.map +1 -1
- package/dist/src/generators/openclaw.js +4 -0
- package/dist/src/generators/openclaw.js.map +1 -1
- package/dist/src/schema/manifest.d.ts +191 -44
- package/dist/src/schema/manifest.d.ts.map +1 -1
- package/dist/src/schema/manifest.js +58 -55
- package/dist/src/schema/manifest.js.map +1 -1
- package/dist/src/schema/sanitizer.d.ts.map +1 -1
- package/dist/src/schema/sanitizer.js +3 -1
- package/dist/src/schema/sanitizer.js.map +1 -1
- package/dist/src/utils/agent-info.js +1 -1
- package/dist/src/utils/agent-info.js.map +1 -1
- package/dist/src/utils/archive.d.ts +2 -9
- package/dist/src/utils/archive.d.ts.map +1 -1
- package/dist/src/utils/archive.js +90 -62
- package/dist/src/utils/archive.js.map +1 -1
- package/dist/src/utils/identity.d.ts +13 -3
- package/dist/src/utils/identity.d.ts.map +1 -1
- package/dist/src/utils/identity.js +109 -16
- package/dist/src/utils/identity.js.map +1 -1
- package/dist/src/utils/mail-handler.js +2 -2
- package/dist/src/utils/mail-handler.js.map +1 -1
- package/dist/src/utils/manifest.d.ts.map +1 -1
- package/dist/src/utils/manifest.js +17 -14
- package/dist/src/utils/manifest.js.map +1 -1
- package/dist/src/utils/noise-ik-transport.js +1 -1
- package/dist/src/utils/noise-ik-transport.js.map +1 -1
- package/dist/src/utils/nono.d.ts +1 -1
- package/dist/src/utils/nono.d.ts.map +1 -1
- package/dist/src/utils/nono.js.map +1 -1
- package/dist/src/utils/plain-tcp-transport.js +1 -1
- package/dist/src/utils/plain-tcp-transport.js.map +1 -1
- package/dist/src/utils/provision.d.ts.map +1 -1
- package/dist/src/utils/provision.js +8 -1
- package/dist/src/utils/provision.js.map +1 -1
- package/dist/src/utils/relay.js +4 -4
- package/dist/src/utils/relay.js.map +1 -1
- package/dist/src/utils/vault.d.ts +21 -0
- package/dist/src/utils/vault.d.ts.map +1 -0
- package/dist/src/utils/vault.js +67 -0
- package/dist/src/utils/vault.js.map +1 -0
- package/dist/src/utils/workspace.d.ts +14 -0
- package/dist/src/utils/workspace.d.ts.map +1 -0
- package/dist/src/utils/workspace.js +53 -0
- package/dist/src/utils/workspace.js.map +1 -0
- package/dist/src/utils/ws-noise-transport.js +2 -2
- package/dist/src/utils/ws-noise-transport.js.map +1 -1
- package/nono-profiles/tps-backup.toml +18 -0
- package/nono-profiles/tps-bootstrap.toml +20 -0
- package/nono-profiles/tps-office-manager.toml +21 -0
- package/nono-profiles/tps-restore.toml +18 -0
- package/nono-profiles/tps-status.toml +19 -0
- package/package.json +7 -27
- package/LICENSE +0 -201
- package/README.md +0 -79
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { sanitizeIdentifier } from "../schema/sanitizer.js";
|
|
6
|
-
import { connectAndKeepAlive, startRelay, syncRemoteBranch } from "../utils/relay.js";
|
|
7
7
|
import { connectionAlive, listHostStates } from "../utils/connection-state.js";
|
|
8
|
-
import {
|
|
9
|
-
import { provisionTeam } from "../utils/provision.js";
|
|
10
|
-
import { fileURLToPath } from "node:url";
|
|
11
|
-
import { fingerprint, loadHostIdentity, lookupBranch, registerBranch, revokeBranch } from "../utils/identity.js";
|
|
8
|
+
import { fingerprint, loadHostIdentity, registerBranch, revokeBranch } from "../utils/identity.js";
|
|
12
9
|
import { NoiseIkTransport } from "../utils/noise-ik-transport.js";
|
|
10
|
+
import { provisionTeam } from "../utils/provision.js";
|
|
11
|
+
import { connectAndKeepAlive, startRelay } from "../utils/relay.js";
|
|
12
|
+
import { isSandboxReady, loadImageIntoSandbox, sandboxExec, sandboxSocketPath, waitForSandbox } from "../utils/sandbox.js";
|
|
13
|
+
import { MSG_JOIN_COMPLETE, MSG_MAIL_DELIVER } from "../utils/wire-mail.js";
|
|
13
14
|
import { WsNoiseTransport } from "../utils/ws-noise-transport.js";
|
|
14
|
-
import {
|
|
15
|
+
import { branchRoot as sharedBranchRoot, resolveTeamId, workspacePath as sharedWorkspacePath } from "../utils/workspace.js";
|
|
16
|
+
import { runOfficeManager, loadWorkspaceManifest } from "./office-manager.js";
|
|
15
17
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
18
|
const BOOTSTRAP_TEMPLATE = `#!/bin/bash
|
|
17
19
|
set -e
|
|
@@ -51,88 +53,75 @@ nohup openclaw gateway run --config "$CONFIG" > __WORKSPACE__/gateway.log 2>&1 &
|
|
|
51
53
|
echo "Branch office agent ready (gateway pid $!)"
|
|
52
54
|
`;
|
|
53
55
|
function branchRoot() {
|
|
54
|
-
return
|
|
56
|
+
return sharedBranchRoot();
|
|
57
|
+
}
|
|
58
|
+
function workspacePath(agentId) {
|
|
59
|
+
return sharedWorkspacePath(agentId);
|
|
60
|
+
}
|
|
61
|
+
function sandboxName(agentId) {
|
|
62
|
+
const teamId = resolveTeamId(agentId);
|
|
63
|
+
return `tps-${teamId}`;
|
|
64
|
+
}
|
|
65
|
+
function relayPidFile(agentId) {
|
|
66
|
+
return join(workspacePath(agentId), "relay.pid");
|
|
67
|
+
}
|
|
68
|
+
function outboxCounts(agentId) {
|
|
69
|
+
const ws = workspacePath(agentId);
|
|
70
|
+
const root = join(ws, "mail", "outbox");
|
|
71
|
+
const countDir = (dir) => existsSync(dir) ? readdirSync(dir).filter(f => f.endsWith(".json")).length : 0;
|
|
72
|
+
return {
|
|
73
|
+
newCount: countDir(join(root, "new")),
|
|
74
|
+
curCount: countDir(join(root, "cur")),
|
|
75
|
+
failedCount: countDir(join(root, "failed")),
|
|
76
|
+
};
|
|
55
77
|
}
|
|
56
78
|
function validateAgent(agent) {
|
|
57
79
|
if (!agent) {
|
|
58
|
-
console.error("Agent is required.");
|
|
80
|
+
console.error("Agent name is required.");
|
|
59
81
|
process.exit(1);
|
|
60
82
|
}
|
|
61
83
|
const safe = sanitizeIdentifier(agent);
|
|
62
84
|
if (safe !== agent) {
|
|
63
|
-
console.error(`Invalid agent
|
|
85
|
+
console.error(`Invalid agent identifier: ${agent}`);
|
|
64
86
|
process.exit(1);
|
|
65
87
|
}
|
|
66
88
|
return agent;
|
|
67
89
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
throw new Error("Invalid join token URL");
|
|
75
|
-
}
|
|
76
|
-
if (u.protocol !== "tps:")
|
|
77
|
-
throw new Error("Join token must use tps:// scheme");
|
|
78
|
-
const host = u.searchParams.get("host") || "";
|
|
79
|
-
const portRaw = u.searchParams.get("port") || "";
|
|
80
|
-
const transportRaw = u.searchParams.get("transport") || "ws";
|
|
81
|
-
const pubkeyRaw = u.searchParams.get("pubkey") || "";
|
|
82
|
-
const sigRaw = u.searchParams.get("sigpubkey") || "";
|
|
83
|
-
const fp = u.searchParams.get("fp") || "";
|
|
84
|
-
if (!host)
|
|
85
|
-
throw new Error("Join token missing host");
|
|
86
|
-
const transport = transportRaw === "tcp" ? "tcp" : "ws";
|
|
87
|
-
const port = Number(portRaw);
|
|
88
|
-
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
|
89
|
-
throw new Error("Join token has invalid port");
|
|
90
|
-
}
|
|
91
|
-
if (!pubkeyRaw)
|
|
92
|
-
throw new Error("Join token missing pubkey");
|
|
93
|
-
if (!sigRaw)
|
|
94
|
-
throw new Error("Join token missing sigpubkey");
|
|
95
|
-
if (!fp)
|
|
96
|
-
throw new Error("Join token missing fingerprint");
|
|
97
|
-
let encryptionPubkey;
|
|
98
|
-
let signingPubkey;
|
|
90
|
+
function resolveSandboxId(agentId) {
|
|
91
|
+
const result = spawnSync("nono", ["list", "--json"], { encoding: "utf8" });
|
|
92
|
+
if (result.status !== 0)
|
|
93
|
+
return null;
|
|
99
94
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
95
|
+
const states = JSON.parse(result.stdout);
|
|
96
|
+
const target = sandboxName(agentId);
|
|
97
|
+
const found = states.find((s) => s.name === target);
|
|
98
|
+
return found ? found.id : null;
|
|
102
99
|
}
|
|
103
100
|
catch {
|
|
104
|
-
|
|
105
|
-
}
|
|
106
|
-
if (encryptionPubkey.length !== 32)
|
|
107
|
-
throw new Error("Join token encryption pubkey must be 32 bytes");
|
|
108
|
-
if (signingPubkey.length !== 32)
|
|
109
|
-
throw new Error("Join token signing pubkey must be 32 bytes");
|
|
110
|
-
const normalizedFp = fp.startsWith("sha256:") ? fp : `sha256:${fp}`;
|
|
111
|
-
const expected = `sha256:${fingerprint(signingPubkey)}`;
|
|
112
|
-
if (expected !== normalizedFp) {
|
|
113
|
-
throw new Error("Join token fingerprint does not match signing key");
|
|
101
|
+
return null;
|
|
114
102
|
}
|
|
115
|
-
return { host, port, transport, encryptionPubkey, signingPubkey, fingerprint: normalizedFp };
|
|
116
|
-
}
|
|
117
|
-
function workspacePath(agent) {
|
|
118
|
-
return join(branchRoot(), agent);
|
|
119
103
|
}
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const raw = process.env.TPS_SANDBOX_AGENT || "claude";
|
|
125
|
-
const safe = sanitizeIdentifier(raw);
|
|
126
|
-
if (safe !== raw) {
|
|
127
|
-
throw new Error(`Invalid sandbox agent: ${raw}`);
|
|
104
|
+
export function parseJoinToken(tokenUrl) {
|
|
105
|
+
const url = new URL(tokenUrl);
|
|
106
|
+
if (url.protocol !== "tps:" || url.host !== "join") {
|
|
107
|
+
throw new Error("Invalid join token protocol. Must be tps://join?...");
|
|
128
108
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
109
|
+
const host = url.searchParams.get("host");
|
|
110
|
+
const port = Number(url.searchParams.get("port"));
|
|
111
|
+
const pubkey = url.searchParams.get("pubkey");
|
|
112
|
+
const sigpubkey = url.searchParams.get("sigpubkey");
|
|
113
|
+
const fp = url.searchParams.get("fp");
|
|
114
|
+
if (!host || isNaN(port) || !pubkey || !sigpubkey || !fp) {
|
|
115
|
+
throw new Error("Invalid join token: missing required parameters");
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
host,
|
|
119
|
+
port,
|
|
120
|
+
transport: url.searchParams.get("transport") || "ws",
|
|
121
|
+
encryptionPubkey: new Uint8Array(Buffer.from(pubkey, "base64url")),
|
|
122
|
+
signingPubkey: new Uint8Array(Buffer.from(sigpubkey, "base64url")),
|
|
123
|
+
fingerprint: fp,
|
|
124
|
+
};
|
|
136
125
|
}
|
|
137
126
|
function setupWorkspace(agent) {
|
|
138
127
|
const ws = workspacePath(agent);
|
|
@@ -143,11 +132,11 @@ function setupWorkspace(agent) {
|
|
|
143
132
|
mkdirSync(join(ws, "mail", "outbox", "failed"), { recursive: true });
|
|
144
133
|
mkdirSync(join(ws, "mail", "outbox", "paused"), { recursive: true });
|
|
145
134
|
const bootstrap = join(ws, "bootstrap.sh");
|
|
146
|
-
// Security: never source bootstrap from process.cwd().
|
|
147
|
-
// Use the trusted in-code template to avoid cwd injection.
|
|
148
|
-
// ops-15.5: Don't overwrite existing bootstrap.sh if user customized it.
|
|
149
135
|
if (!existsSync(bootstrap)) {
|
|
150
|
-
const
|
|
136
|
+
const teamRoot = join(branchRoot(), agent);
|
|
137
|
+
const template = BOOTSTRAP_TEMPLATE
|
|
138
|
+
.replaceAll("__WORKSPACE__", ws)
|
|
139
|
+
.replaceAll("__TEAM_ROOT__", teamRoot);
|
|
151
140
|
writeFileSync(bootstrap, template, { mode: 0o755 });
|
|
152
141
|
}
|
|
153
142
|
else {
|
|
@@ -155,336 +144,108 @@ function setupWorkspace(agent) {
|
|
|
155
144
|
}
|
|
156
145
|
return ws;
|
|
157
146
|
}
|
|
158
|
-
function resolveSandboxId(agent) {
|
|
159
|
-
const sid = idFile(agent);
|
|
160
|
-
if (existsSync(sid)) {
|
|
161
|
-
return readFileSync(sid, "utf-8").trim();
|
|
162
|
-
}
|
|
163
|
-
const listed = spawnSync("docker", ["sandbox", "ls", "--json"], { encoding: "utf-8" });
|
|
164
|
-
if (listed.status !== 0)
|
|
165
|
-
return null;
|
|
166
|
-
try {
|
|
167
|
-
const parsed = JSON.parse(listed.stdout || "{}");
|
|
168
|
-
const rows = parsed.vms || [];
|
|
169
|
-
const expected = sandboxName(agent).toLowerCase();
|
|
170
|
-
const match = rows.find((r) => (r.name || "").toLowerCase() === expected);
|
|
171
|
-
if (!match)
|
|
172
|
-
return null;
|
|
173
|
-
return (match.name || match.id || match.sandboxId || null);
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
function relayLogFile(agent) {
|
|
180
|
-
return join(workspacePath(agent), "relay.log");
|
|
181
|
-
}
|
|
182
|
-
function startRelayProcess(agent) {
|
|
183
|
-
const pidFile = relayPidFile(agent);
|
|
184
|
-
if (existsSync(pidFile)) {
|
|
185
|
-
const pid = Number(readFileSync(pidFile, "utf-8").trim());
|
|
186
|
-
try {
|
|
187
|
-
// Check if process exists (signal 0)
|
|
188
|
-
process.kill(pid, 0);
|
|
189
|
-
console.log(`Relay already running (pid ${pid})`);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
catch {
|
|
193
|
-
// Process dead, proceed to spawn new one
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
const logPath = relayLogFile(agent);
|
|
197
|
-
const logFd = openSync(logPath, "a");
|
|
198
|
-
const child = spawn(process.execPath, [process.argv[1], "office", "relay", agent], {
|
|
199
|
-
detached: true,
|
|
200
|
-
stdio: ["ignore", logFd, logFd],
|
|
201
|
-
env: { ...process.env },
|
|
202
|
-
});
|
|
203
|
-
child.unref();
|
|
204
|
-
// Close the fd in the parent so we don't hold it open (child has it now)
|
|
205
|
-
// Wait, spawn with detached doesn't automatically close fds in parent?
|
|
206
|
-
// Actually, passing fd to stdio options duplicates it to child. Parent can close.
|
|
207
|
-
// But we need to be careful not to close it before spawn uses it? Spawn is synchronous in setup.
|
|
208
|
-
// Node docs say: "The file descriptor is duplicated in the child process."
|
|
209
|
-
// Safe to close in parent after spawn returns.
|
|
210
|
-
try {
|
|
211
|
-
closeSync(logFd);
|
|
212
|
-
}
|
|
213
|
-
catch { }
|
|
214
|
-
writeFileSync(relayPidFile(agent), `${child.pid}\n`, "utf-8");
|
|
215
|
-
}
|
|
216
|
-
function stopRelayProcess(agent) {
|
|
217
|
-
const pf = relayPidFile(agent);
|
|
218
|
-
if (!existsSync(pf))
|
|
219
|
-
return;
|
|
220
|
-
const pid = Number(readFileSync(pf, "utf-8").trim());
|
|
221
|
-
if (pid > 0) {
|
|
222
|
-
try {
|
|
223
|
-
process.kill(pid, "SIGTERM");
|
|
224
|
-
}
|
|
225
|
-
catch {
|
|
226
|
-
// already stopped
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
function outboxCounts(agent) {
|
|
231
|
-
const ws = workspacePath(agent);
|
|
232
|
-
const outNew = join(ws, "mail", "outbox", "new");
|
|
233
|
-
const outCur = join(ws, "mail", "outbox", "cur");
|
|
234
|
-
const outFailed = join(ws, "mail", "outbox", "failed");
|
|
235
|
-
const newCount = existsSync(outNew) ? readdirSync(outNew).filter((f) => f.endsWith(".json")).length : 0;
|
|
236
|
-
const curCount = existsSync(outCur) ? readdirSync(outCur).filter((f) => f.endsWith(".json")).length : 0;
|
|
237
|
-
const failedCount = existsSync(outFailed) ? readdirSync(outFailed).filter((f) => f.endsWith(".json")).length : 0;
|
|
238
|
-
return { newCount, curCount, failedCount };
|
|
239
|
-
}
|
|
240
147
|
export async function runOffice(args) {
|
|
241
148
|
switch (args.action) {
|
|
242
|
-
case "join": {
|
|
243
|
-
const agent = validateAgent(args.agent);
|
|
244
|
-
if (!args.joinToken) {
|
|
245
|
-
console.error("Usage: tps office join <name> <join-token-url>");
|
|
246
|
-
process.exit(1);
|
|
247
|
-
}
|
|
248
|
-
const token = parseJoinToken(args.joinToken);
|
|
249
|
-
const existing = lookupBranch(agent);
|
|
250
|
-
if (existing) {
|
|
251
|
-
console.error(`Branch '${agent}' is already registered. Revoke first or use a different name.`);
|
|
252
|
-
process.exit(1);
|
|
253
|
-
}
|
|
254
|
-
registerBranch(agent, token.signingPubkey, {
|
|
255
|
-
fingerprint: `sha256:${fingerprint(token.signingPubkey)}`,
|
|
256
|
-
trust: "standard",
|
|
257
|
-
}, token.encryptionPubkey);
|
|
258
|
-
console.log(`Connecting to ${token.host}:${token.port}...`);
|
|
259
|
-
const hostKp = loadHostIdentity();
|
|
260
|
-
const wire = token.transport === "ws" ? new WsNoiseTransport(hostKp) : new NoiseIkTransport(hostKp);
|
|
261
|
-
const channel = await wire.connect({
|
|
262
|
-
host: token.host,
|
|
263
|
-
port: token.port,
|
|
264
|
-
branchId: agent,
|
|
265
|
-
hostPublicKey: token.encryptionPubkey,
|
|
266
|
-
});
|
|
267
|
-
console.log(`Noise_IK handshake OK — branch fingerprint verified: ${token.fingerprint}`);
|
|
268
|
-
await channel.send({
|
|
269
|
-
type: MSG_JOIN_COMPLETE,
|
|
270
|
-
seq: 0,
|
|
271
|
-
ts: new Date().toISOString(),
|
|
272
|
-
body: {
|
|
273
|
-
hostPubkey: Buffer.from(hostKp.encryption.publicKey).toString("base64url"),
|
|
274
|
-
hostFingerprint: fingerprint(hostKp.encryption.publicKey),
|
|
275
|
-
hostId: process.env.TPS_HOST_ID || "host",
|
|
276
|
-
},
|
|
277
|
-
});
|
|
278
|
-
const ws = workspacePath(agent);
|
|
279
|
-
mkdirSync(ws, { recursive: true });
|
|
280
|
-
const remoteRecord = {
|
|
281
|
-
host: token.host,
|
|
282
|
-
port: token.port,
|
|
283
|
-
branchId: agent,
|
|
284
|
-
fingerprint: token.fingerprint,
|
|
285
|
-
pubkey: Buffer.from(token.encryptionPubkey).toString("base64url"),
|
|
286
|
-
joinedAt: new Date().toISOString(),
|
|
287
|
-
transport: token.transport,
|
|
288
|
-
};
|
|
289
|
-
writeFileSync(join(ws, "remote.json"), JSON.stringify(remoteRecord, null, 2), "utf-8");
|
|
290
|
-
await channel.close();
|
|
291
|
-
console.log(`Branch '${agent}' registered.`);
|
|
292
|
-
console.log("Host pubkey sent to branch.");
|
|
293
|
-
console.log("Remote branch office ready.");
|
|
294
|
-
return;
|
|
295
|
-
}
|
|
296
149
|
case "start": {
|
|
297
|
-
const agent = validateAgent(args.agent);
|
|
298
|
-
// ops-17: Check for manifest mode (team provisioning)
|
|
299
150
|
if (args.manifest) {
|
|
300
151
|
try {
|
|
301
152
|
provisionTeam(args.manifest, branchRoot());
|
|
302
|
-
|
|
153
|
+
if (args.soundstage && args.agent) {
|
|
154
|
+
const team = validateAgent(args.agent);
|
|
155
|
+
const teamRoot = join(branchRoot(), team);
|
|
156
|
+
const ws = join(teamRoot, "workspace");
|
|
157
|
+
const marker = join(teamRoot, "soundstage.json");
|
|
158
|
+
mkdirSync(teamRoot, { recursive: true });
|
|
159
|
+
writeFileSync(marker, JSON.stringify({ enabled: true, startedAt: new Date().toISOString() }));
|
|
160
|
+
const teamBootstrap = join(teamRoot, "bootstrap.sh");
|
|
161
|
+
let bs = BOOTSTRAP_TEMPLATE
|
|
162
|
+
.replaceAll("__WORKSPACE__", ws)
|
|
163
|
+
.replaceAll("__TEAM_ROOT__", teamRoot);
|
|
164
|
+
const workspaceBootstrap = join(ws, "bootstrap.sh");
|
|
165
|
+
if (existsSync(workspaceBootstrap)) {
|
|
166
|
+
bs = readFileSync(workspaceBootstrap, "utf-8").replaceAll("__TEAM_ROOT__", teamRoot);
|
|
167
|
+
}
|
|
168
|
+
writeFileSync(teamBootstrap, bs, { mode: 0o755 });
|
|
169
|
+
}
|
|
170
|
+
console.log(`Team provisioned from manifest.`);
|
|
303
171
|
}
|
|
304
172
|
catch (e) {
|
|
305
173
|
console.error(`Failed to provision team: ${e.message}`);
|
|
306
174
|
process.exit(1);
|
|
307
175
|
}
|
|
176
|
+
return;
|
|
308
177
|
}
|
|
178
|
+
const agent = validateAgent(args.agent);
|
|
179
|
+
const sName = sandboxName(agent);
|
|
309
180
|
const ws = setupWorkspace(agent);
|
|
310
181
|
if (args.soundstage) {
|
|
311
|
-
|
|
182
|
+
console.log("🎬 Soundstage mode enabled (Mock LLM, local isolation)");
|
|
312
183
|
const teamRoot = join(branchRoot(), agent);
|
|
313
|
-
const marker =
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// Use teamRoot instead of ws to place outside workspace
|
|
321
|
-
writeFileSync(join(teamRoot, "soundstage.json"), JSON.stringify(marker, null, 2), "utf-8");
|
|
322
|
-
// Rewrite openclaw.json to point at mock LLM
|
|
323
|
-
const configPath = join(ws, ".openclaw", "openclaw.json");
|
|
324
|
-
if (existsSync(configPath)) {
|
|
325
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
326
|
-
if (config.agents?.defaults?.model) {
|
|
327
|
-
config.agents.defaults.model.primary = "openai-compatible/mock-soundstage";
|
|
328
|
-
config.agents.defaults.model.fallbacks = [];
|
|
329
|
-
}
|
|
330
|
-
// Add base URL for the mock
|
|
331
|
-
config.agents = config.agents || {};
|
|
332
|
-
config.agents.defaults = config.agents.defaults || {};
|
|
333
|
-
config.agents.defaults.baseUrl = "http://127.0.0.1:11434/v1";
|
|
334
|
-
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
335
|
-
}
|
|
336
|
-
// Copy mock LLM server into team root (NOT workspace — agents can't modify it)
|
|
337
|
-
const mockSrc = join(__dirname, "..", "soundstage", "mock-llm.js");
|
|
338
|
-
const mockDst = join(teamRoot, "mock-llm.js");
|
|
339
|
-
if (!existsSync(mockSrc)) {
|
|
340
|
-
throw new Error("Soundstage mock LLM not found. Run `bun run build` first.");
|
|
341
|
-
}
|
|
342
|
-
copyFileSync(mockSrc, mockDst);
|
|
343
|
-
// Substitute __TEAM_ROOT__ in bootstrap (setupWorkspace only handles __WORKSPACE__)
|
|
344
|
-
const bootstrapPath = join(ws, "bootstrap.sh");
|
|
345
|
-
if (existsSync(bootstrapPath)) {
|
|
346
|
-
const bs = readFileSync(bootstrapPath, "utf-8").replaceAll("__TEAM_ROOT__", teamRoot);
|
|
347
|
-
writeFileSync(bootstrapPath, bs, { mode: 0o755 });
|
|
184
|
+
const marker = join(teamRoot, "soundstage.json");
|
|
185
|
+
mkdirSync(teamRoot, { recursive: true });
|
|
186
|
+
writeFileSync(marker, JSON.stringify({ enabled: true, startedAt: new Date().toISOString() }));
|
|
187
|
+
const soundstageImage = join(__dirname, "..", "..", "images", "tps-soundstage.tar");
|
|
188
|
+
if (existsSync(soundstageImage)) {
|
|
189
|
+
console.log(`Loading soundstage image: ${soundstageImage}`);
|
|
190
|
+
loadImageIntoSandbox(sName, soundstageImage);
|
|
348
191
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
// Idempotency: check if sandbox already exists
|
|
354
|
-
const existingId = resolveSandboxId(agent);
|
|
355
|
-
if (!existingId) {
|
|
356
|
-
const sAgent = sandboxAgent();
|
|
357
|
-
console.log(`Creating sandbox ${sName}...`);
|
|
358
|
-
// docker sandbox create hangs indefinitely (v0.11.0 bug) even after
|
|
359
|
-
// the VM is ready. Spawn it in the background and wait for the socket.
|
|
360
|
-
const createLog = join(ws, "create.log");
|
|
361
|
-
const createLogFd = openSync(createLog, "a");
|
|
362
|
-
const createArgs = ["sandbox", "create", "--name", sName];
|
|
363
|
-
// Soundstage uses a pre-built image with openclaw already installed
|
|
364
|
-
if (args.soundstage) {
|
|
365
|
-
createArgs.push("--template", "tps-soundstage:latest");
|
|
366
|
-
}
|
|
367
|
-
createArgs.push(sAgent, ws);
|
|
368
|
-
const createChild = spawn("docker", createArgs, {
|
|
369
|
-
detached: true,
|
|
370
|
-
stdio: ["ignore", createLogFd, createLogFd],
|
|
371
|
-
});
|
|
372
|
-
createChild.unref();
|
|
373
|
-
try {
|
|
374
|
-
closeSync(createLogFd);
|
|
375
|
-
}
|
|
376
|
-
catch { }
|
|
377
|
-
// Wait for VM socket to appear (the create command keeps running but VM is ready)
|
|
378
|
-
console.log("Waiting for sandbox VM to boot...");
|
|
379
|
-
if (!waitForSandbox(sName, 90000)) {
|
|
380
|
-
// Check if sandbox appeared in ls even if socket isn't ready
|
|
381
|
-
const check = spawnSync("docker", ["sandbox", "ls", "--json"], { encoding: "utf-8" });
|
|
382
|
-
const vms = JSON.parse(check.stdout || "{}").vms || [];
|
|
383
|
-
const found = vms.find((v) => v.name === sName);
|
|
384
|
-
if (!found) {
|
|
385
|
-
console.error(`Sandbox creation failed. Check ${createLog}`);
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
388
|
-
// VM exists but socket not ready — wait longer
|
|
389
|
-
console.log("VM created, waiting for daemon...");
|
|
390
|
-
if (!waitForSandbox(sName, 30000)) {
|
|
391
|
-
console.error("Sandbox VM daemon did not become ready.");
|
|
392
|
-
process.exit(1);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
console.log("Sandbox VM ready.");
|
|
192
|
+
const bootstrap = join(ws, "bootstrap.sh");
|
|
193
|
+
if (existsSync(bootstrap)) {
|
|
194
|
+
const bs = readFileSync(bootstrap, "utf-8").replaceAll("__TEAM_ROOT__", teamRoot);
|
|
195
|
+
writeFileSync(bootstrap, bs, { mode: 0o755 });
|
|
396
196
|
}
|
|
397
197
|
}
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// Ensure VM is ready (may already be if we just created it above)
|
|
403
|
-
if (!isSandboxReady(id)) {
|
|
404
|
-
console.log("Waiting for sandbox VM...");
|
|
405
|
-
if (!waitForSandbox(id, 60000)) {
|
|
406
|
-
console.error("Sandbox VM did not become ready in 60s.");
|
|
407
|
-
process.exit(1);
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
// Load base image for bootstrap execution
|
|
411
|
-
console.log("Loading base image into sandbox...");
|
|
412
|
-
if (!loadImageIntoSandbox(id, "node:22-alpine")) {
|
|
413
|
-
if (!loadImageIntoSandbox(id, "alpine:latest")) {
|
|
414
|
-
console.error("Failed to load base image into sandbox VM.");
|
|
415
|
-
process.exit(1);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
// Execute bootstrap via direct socket (workaround for docker sandbox exec bug)
|
|
419
|
-
console.log("Running bootstrap...");
|
|
420
|
-
const exec = sandboxExec(id, ["sh", join(ws, "bootstrap.sh")], {
|
|
421
|
-
workspace: ws,
|
|
422
|
-
image: "node:22-alpine",
|
|
423
|
-
});
|
|
424
|
-
if (exec.status !== 0) {
|
|
425
|
-
const fallback = sandboxExec(id, ["sh", join(ws, "bootstrap.sh")], {
|
|
426
|
-
workspace: ws,
|
|
427
|
-
image: "alpine:latest",
|
|
428
|
-
});
|
|
429
|
-
if (fallback.status !== 0) {
|
|
430
|
-
console.error(fallback.stderr || fallback.stdout || "Bootstrap failed.");
|
|
431
|
-
process.exit(1);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
198
|
+
const teamId = resolveTeamId(agent);
|
|
199
|
+
const isTeam = teamId !== agent;
|
|
200
|
+
if (isTeam) {
|
|
201
|
+
console.log(`(Shared team sandbox: ${teamId})`);
|
|
434
202
|
}
|
|
435
|
-
if (process.env.
|
|
436
|
-
|
|
203
|
+
if (process.env.TPS_OFFICE_SKIP_VM === "1") {
|
|
204
|
+
console.log(`✓ Sandbox ready for ${agent} (SKIPPED).`);
|
|
205
|
+
return;
|
|
437
206
|
}
|
|
438
|
-
console.log(`
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
case "stop": {
|
|
445
|
-
const agent = validateAgent(args.agent);
|
|
446
|
-
stopRelayProcess(agent);
|
|
447
|
-
const id = resolveSandboxId(agent);
|
|
448
|
-
if (!id) {
|
|
449
|
-
console.error(`No sandbox found for agent: ${agent}`);
|
|
207
|
+
console.log(`Starting sandbox VM for ${agent}...`);
|
|
208
|
+
spawnSync("nono", ["start", sName, "--mount", `${ws}:/workspace`, "--image", "node:22-alpine"], { stdio: "inherit" });
|
|
209
|
+
if (!waitForSandbox(sName)) {
|
|
210
|
+
console.error("Timed out waiting for sandbox to be ready.");
|
|
450
211
|
process.exit(1);
|
|
451
212
|
}
|
|
452
|
-
|
|
453
|
-
if
|
|
454
|
-
|
|
455
|
-
|
|
213
|
+
console.log(`✓ Sandbox ready for ${agent}.`);
|
|
214
|
+
// Auto-run Office Manager if a workspace manifest exists
|
|
215
|
+
if (loadWorkspaceManifest(ws)) {
|
|
216
|
+
console.log("Workspace manifest found — running Office Manager...");
|
|
217
|
+
await runOfficeManager(ws, { dryRun: false });
|
|
456
218
|
}
|
|
457
|
-
console.log(`Stopped sandbox ${id} (${agent})`);
|
|
458
219
|
return;
|
|
459
220
|
}
|
|
460
|
-
case "
|
|
221
|
+
case "setup": {
|
|
461
222
|
const agent = validateAgent(args.agent);
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
223
|
+
const ws = workspacePath(agent);
|
|
224
|
+
const ok = await runOfficeManager(ws, { dryRun: args.dryRun ?? false });
|
|
225
|
+
if (!ok)
|
|
465
226
|
process.exit(1);
|
|
466
|
-
}
|
|
467
|
-
revokeBranch(agent, "manual revocation");
|
|
468
|
-
console.log(`Branch '${agent}' revoked. Run 'tps branch init' on the remote to re-join.`);
|
|
469
|
-
return;
|
|
470
|
-
}
|
|
471
|
-
case "sync": {
|
|
472
|
-
const agent = validateAgent(args.agent);
|
|
473
|
-
const result = await syncRemoteBranch(agent);
|
|
474
|
-
console.log(`Sync complete. Received ${result.received} message(s) from ${agent}.`);
|
|
475
227
|
return;
|
|
476
228
|
}
|
|
477
|
-
case "
|
|
229
|
+
case "stop": {
|
|
478
230
|
const agent = validateAgent(args.agent);
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
231
|
+
const sName = sandboxName(agent);
|
|
232
|
+
const soundstageMarker = join(branchRoot(), agent, "soundstage.json");
|
|
233
|
+
if (existsSync(soundstageMarker)) {
|
|
234
|
+
try {
|
|
235
|
+
unlinkSync(soundstageMarker);
|
|
236
|
+
}
|
|
237
|
+
catch { }
|
|
238
|
+
}
|
|
239
|
+
const sid = resolveSandboxId(agent);
|
|
240
|
+
if (!sid) {
|
|
241
|
+
process.stderr.write("No sandbox found for agent\n");
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
console.log(`Stopping sandbox VM for ${agent}...`);
|
|
245
|
+
const stopResult = spawnSync("nono", ["stop", sName], { stdio: "inherit" });
|
|
246
|
+
if (stopResult.status !== 0) {
|
|
247
|
+
process.exit(stopResult.status ?? 1);
|
|
248
|
+
}
|
|
488
249
|
return;
|
|
489
250
|
}
|
|
490
251
|
case "list": {
|
|
@@ -535,16 +296,12 @@ export async function runOffice(args) {
|
|
|
535
296
|
console.log(`Workspace: ${ws}`);
|
|
536
297
|
if (isSoundstage) {
|
|
537
298
|
console.log(`Mode: 🎬 soundstage (mock LLM, real sandbox)`);
|
|
538
|
-
console.log(`Sandbox: ${sid || "not running"}${vmReady ? " (VM ready)" : ""}`);
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
console.log(`Sandbox: ${sid || "not running"}${vmReady ? " (VM ready)" : ""}`);
|
|
542
299
|
}
|
|
300
|
+
console.log(`Sandbox: ${sid || "not running"}${vmReady ? " (VM ready)" : ""}`);
|
|
543
301
|
if (sid)
|
|
544
302
|
console.log(`Socket: ${sandboxSocketPath(sName2)}`);
|
|
545
303
|
console.log(`Relay: ${relayRunning ? "running" : "stopped"}`);
|
|
546
304
|
console.log(`Outbox pending: ${counts.newCount} (cur=${counts.curCount}, failed=${counts.failedCount})`);
|
|
547
|
-
// Check for paused messages (loop detection)
|
|
548
305
|
const pausedDir = join(ws, "mail", "outbox", "paused");
|
|
549
306
|
if (existsSync(pausedDir)) {
|
|
550
307
|
const pausedCount = readdirSync(pausedDir).filter((f) => f.endsWith(".json")).length;
|
|
@@ -566,7 +323,6 @@ export async function runOffice(args) {
|
|
|
566
323
|
stop();
|
|
567
324
|
process.exit(0);
|
|
568
325
|
});
|
|
569
|
-
// keep process alive
|
|
570
326
|
setInterval(() => { }, 60_000);
|
|
571
327
|
return;
|
|
572
328
|
}
|
|
@@ -592,6 +348,132 @@ export async function runOffice(args) {
|
|
|
592
348
|
if (result.stderr)
|
|
593
349
|
process.stderr.write(result.stderr);
|
|
594
350
|
process.exit(result.status ?? 1);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
case "join": {
|
|
354
|
+
if (!args.agent || !args.joinToken) {
|
|
355
|
+
console.error("Usage: tps office join <name> <join-token>");
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
const agent = validateAgent(args.agent);
|
|
359
|
+
const token = parseJoinToken(args.joinToken);
|
|
360
|
+
registerBranch(agent, token.signingPubkey, {
|
|
361
|
+
fingerprint: `sha256:${fingerprint(token.signingPubkey)}`,
|
|
362
|
+
trust: "standard",
|
|
363
|
+
}, token.encryptionPubkey);
|
|
364
|
+
console.log(`Connecting to ${token.host}:${token.port}...`);
|
|
365
|
+
const hostKp = await loadHostIdentity();
|
|
366
|
+
const wire = token.transport === "ws" ? new WsNoiseTransport(hostKp) : new NoiseIkTransport(hostKp);
|
|
367
|
+
const channel = await wire.connect({
|
|
368
|
+
host: token.host,
|
|
369
|
+
port: token.port,
|
|
370
|
+
branchId: agent,
|
|
371
|
+
hostPublicKey: token.encryptionPubkey,
|
|
372
|
+
});
|
|
373
|
+
console.log(`Noise_IK handshake OK — branch fingerprint verified: ${token.fingerprint}`);
|
|
374
|
+
await channel.send({
|
|
375
|
+
type: MSG_JOIN_COMPLETE,
|
|
376
|
+
seq: 0,
|
|
377
|
+
ts: new Date().toISOString(),
|
|
378
|
+
body: {
|
|
379
|
+
hostPubkey: Buffer.from(hostKp.encryption.publicKey).toString("base64url"),
|
|
380
|
+
hostFingerprint: fingerprint(hostKp.encryption.publicKey),
|
|
381
|
+
hostId: process.env.TPS_HOST_ID || "host",
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
const ws = workspacePath(agent);
|
|
385
|
+
mkdirSync(ws, { recursive: true });
|
|
386
|
+
const remoteRecord = {
|
|
387
|
+
host: token.host,
|
|
388
|
+
port: token.port,
|
|
389
|
+
branchId: agent,
|
|
390
|
+
fingerprint: token.fingerprint,
|
|
391
|
+
pubkey: Buffer.from(token.encryptionPubkey).toString("base64url"),
|
|
392
|
+
joinedAt: new Date().toISOString(),
|
|
393
|
+
transport: token.transport,
|
|
394
|
+
};
|
|
395
|
+
writeFileSync(join(ws, "remote.json"), JSON.stringify(remoteRecord, null, 2), "utf-8");
|
|
396
|
+
await channel.close();
|
|
397
|
+
console.log(`Branch '${agent}' registered.`);
|
|
398
|
+
console.log("Host pubkey sent to branch.");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
case "revoke": {
|
|
402
|
+
const agent = validateAgent(args.agent);
|
|
403
|
+
revokeBranch(agent, "manual revocation");
|
|
404
|
+
const ws = workspacePath(agent);
|
|
405
|
+
const rPath = join(ws, "remote.json");
|
|
406
|
+
if (existsSync(rPath)) {
|
|
407
|
+
try {
|
|
408
|
+
unlinkSync(rPath);
|
|
409
|
+
}
|
|
410
|
+
catch { }
|
|
411
|
+
}
|
|
412
|
+
console.log(`Branch '${agent}' revoked.`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
case "sync": {
|
|
416
|
+
if (!args.agent) {
|
|
417
|
+
console.error("Usage: tps office sync <name>");
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
const { syncRemoteBranch: syncRemote } = await import("../utils/relay.js");
|
|
421
|
+
const { received } = await syncRemote(args.agent);
|
|
422
|
+
console.log(`Sync complete. Received ${received} message(s).`);
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
case "connect": {
|
|
426
|
+
if (!args.agent) {
|
|
427
|
+
console.error("Usage: tps office connect <name>");
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
const hostKp = await loadHostIdentity();
|
|
431
|
+
const stop = await connectAndKeepAlive(args.agent, {
|
|
432
|
+
onMessage: (msg) => {
|
|
433
|
+
if (msg.type === MSG_MAIL_DELIVER) {
|
|
434
|
+
console.log(`\n[${new Date().toLocaleTimeString()}] ✉️ Mail received`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
console.log(`Persistent connection active for '${args.agent}'. Press Ctrl+C to disconnect.`);
|
|
439
|
+
process.on("SIGINT", async () => {
|
|
440
|
+
await stop();
|
|
441
|
+
process.exit(0);
|
|
442
|
+
});
|
|
443
|
+
setInterval(() => { }, 60_000);
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
case "kill": {
|
|
447
|
+
let killed = 0;
|
|
448
|
+
const { listHostStates, clearHostState } = await import("../utils/connection-state.js");
|
|
449
|
+
const states = listHostStates();
|
|
450
|
+
for (const s of states) {
|
|
451
|
+
try {
|
|
452
|
+
process.kill(s.pid, "SIGTERM");
|
|
453
|
+
clearHostState(s.branch);
|
|
454
|
+
killed++;
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
clearHostState(s.branch);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const pidPath = join(process.env.HOME || homedir(), ".tps", "branch", "branch.pid");
|
|
461
|
+
if (existsSync(pidPath)) {
|
|
462
|
+
try {
|
|
463
|
+
const pid = Number(readFileSync(pidPath, "utf-8").trim());
|
|
464
|
+
if (pid) {
|
|
465
|
+
process.kill(pid, "SIGTERM");
|
|
466
|
+
killed++;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
catch { }
|
|
470
|
+
try {
|
|
471
|
+
rmSync(pidPath, { force: true });
|
|
472
|
+
}
|
|
473
|
+
catch { }
|
|
474
|
+
}
|
|
475
|
+
console.log(`Kill switch engaged. Terminated ${killed} TPS process(es).`);
|
|
476
|
+
break;
|
|
595
477
|
}
|
|
596
478
|
}
|
|
597
479
|
}
|