@toon-protocol/townhouse 0.1.0-rc5
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/LICENSE +190 -0
- package/README.md +386 -0
- package/dist/chunk-IB6TNCUQ.js +8274 -0
- package/dist/chunk-IB6TNCUQ.js.map +1 -0
- package/dist/chunk-UTFWPLTB.js +59 -0
- package/dist/chunk-UTFWPLTB.js.map +1 -0
- package/dist/cli.d.ts +38 -0
- package/dist/cli.js +684 -0
- package/dist/cli.js.map +1 -0
- package/dist/compose/townhouse-dev.yml +406 -0
- package/dist/compose/townhouse-hs.yml +276 -0
- package/dist/demo-MJR47QHZ.js +117 -0
- package/dist/demo-MJR47QHZ.js.map +1 -0
- package/dist/image-manifest.json +32 -0
- package/dist/index.d.ts +1410 -0
- package/dist/index.js +180 -0
- package/dist/index.js.map +1 -0
- package/package.json +72 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire } from 'module'; const require = createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
ConnectorAdminClient,
|
|
5
|
+
DEFAULT_ATOR_PROXY,
|
|
6
|
+
DockerOrchestrator,
|
|
7
|
+
TransportProbe,
|
|
8
|
+
WalletManager,
|
|
9
|
+
createApiServer,
|
|
10
|
+
createWizardApiServer,
|
|
11
|
+
decryptWallet,
|
|
12
|
+
encryptWallet,
|
|
13
|
+
getDefaultConfig,
|
|
14
|
+
loadConfig,
|
|
15
|
+
loadWallet,
|
|
16
|
+
saveWallet
|
|
17
|
+
} from "./chunk-IB6TNCUQ.js";
|
|
18
|
+
import "./chunk-UTFWPLTB.js";
|
|
19
|
+
|
|
20
|
+
// src/cli.ts
|
|
21
|
+
import { parseArgs } from "util";
|
|
22
|
+
import { mkdirSync, writeFileSync, existsSync } from "fs";
|
|
23
|
+
import { join, resolve } from "path";
|
|
24
|
+
import { homedir } from "os";
|
|
25
|
+
import { pathToFileURL } from "url";
|
|
26
|
+
import { stringify } from "yaml";
|
|
27
|
+
import Docker from "dockerode";
|
|
28
|
+
|
|
29
|
+
// src/cli/browser-opener.ts
|
|
30
|
+
import { spawn } from "child_process";
|
|
31
|
+
var RealBrowserOpener = class {
|
|
32
|
+
async open(url) {
|
|
33
|
+
let cmd;
|
|
34
|
+
let args;
|
|
35
|
+
switch (process.platform) {
|
|
36
|
+
case "darwin":
|
|
37
|
+
cmd = "open";
|
|
38
|
+
args = [url];
|
|
39
|
+
break;
|
|
40
|
+
case "win32":
|
|
41
|
+
cmd = "cmd";
|
|
42
|
+
args = ["/c", "start", "", url];
|
|
43
|
+
break;
|
|
44
|
+
default:
|
|
45
|
+
cmd = "xdg-open";
|
|
46
|
+
args = [url];
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
return new Promise((resolve2) => {
|
|
50
|
+
let settled = false;
|
|
51
|
+
const settle = () => {
|
|
52
|
+
if (settled) return;
|
|
53
|
+
settled = true;
|
|
54
|
+
resolve2();
|
|
55
|
+
};
|
|
56
|
+
try {
|
|
57
|
+
const child = spawn(cmd, args, {
|
|
58
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
59
|
+
detached: true
|
|
60
|
+
});
|
|
61
|
+
child.once("error", (err) => {
|
|
62
|
+
console.warn(
|
|
63
|
+
`[Townhouse] Could not open browser via ${cmd}: ${err.message}`
|
|
64
|
+
);
|
|
65
|
+
settle();
|
|
66
|
+
});
|
|
67
|
+
child.once("spawn", () => {
|
|
68
|
+
child.unref();
|
|
69
|
+
settle();
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
73
|
+
console.warn(`[Townhouse] Could not open browser: ${msg}`);
|
|
74
|
+
settle();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// src/cli.ts
|
|
81
|
+
var CliHelpRequested = class extends Error {
|
|
82
|
+
constructor() {
|
|
83
|
+
super(HELP_TEXT);
|
|
84
|
+
this.name = "CliHelpRequested";
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var HELP_TEXT = `townhouse \u2014 TOON node orchestrator
|
|
88
|
+
|
|
89
|
+
Usage:
|
|
90
|
+
townhouse setup [--no-browser] [--port <n>] [--config-dir <dir>] Run the first-run setup wizard
|
|
91
|
+
townhouse init [--force] [--config-dir <dir>] [--password <pw>] [--preset <name>] [--yes] Initialize config + wallet
|
|
92
|
+
townhouse up [--town] [--mill] [--dvm] [-c <path>] [--password <pw>] Start nodes
|
|
93
|
+
townhouse down [-c <path>] Stop all nodes
|
|
94
|
+
townhouse status [-c <path>] Show node status
|
|
95
|
+
townhouse metrics [-c <path>] Show connector metrics
|
|
96
|
+
townhouse wallet show [-c <path>] [--password <pw>] Show derived addresses
|
|
97
|
+
townhouse --help Show this help
|
|
98
|
+
|
|
99
|
+
Flags:
|
|
100
|
+
--town Start Town (Nostr relay) node
|
|
101
|
+
--mill Start Mill (swap) node
|
|
102
|
+
--dvm Start DVM (compute) node
|
|
103
|
+
--password Wallet password (non-interactive mode)
|
|
104
|
+
--no-browser Skip opening the browser automatically (setup command)
|
|
105
|
+
--port Override the API port (setup command, default 9400)
|
|
106
|
+
--preset Init from a named preset (init only). Supported: demo
|
|
107
|
+
--yes Non-interactive (init only); with --preset=demo uses demo password if --password absent
|
|
108
|
+
If no flags given, starts all enabled nodes from config.`;
|
|
109
|
+
var DEFAULT_CONFIG_DIR = join(homedir(), ".townhouse");
|
|
110
|
+
var DEFAULT_CONFIG_PATH = join(DEFAULT_CONFIG_DIR, "config.yaml");
|
|
111
|
+
async function handleInit(force, configDir, password, preset, yes) {
|
|
112
|
+
const dir = resolve(configDir ?? DEFAULT_CONFIG_DIR);
|
|
113
|
+
const configPath = join(dir, "config.yaml");
|
|
114
|
+
if (existsSync(configPath) && !force) {
|
|
115
|
+
console.error(
|
|
116
|
+
`Config already exists at ${configPath}. Use --force to overwrite.`
|
|
117
|
+
);
|
|
118
|
+
process.exitCode = 1;
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
122
|
+
let configToWrite;
|
|
123
|
+
if (preset === "demo") {
|
|
124
|
+
const { buildDemoConfig, DEMO_DETERMINISTIC_PASSWORD } = await import("./demo-MJR47QHZ.js");
|
|
125
|
+
configToWrite = buildDemoConfig({ walletPath: join(dir, "wallet.enc") });
|
|
126
|
+
if (yes && !password) {
|
|
127
|
+
password = DEMO_DETERMINISTIC_PASSWORD;
|
|
128
|
+
console.log(
|
|
129
|
+
"[demo preset] Using deterministic demo password (insecure \u2014 demo only)."
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
configToWrite = getDefaultConfig();
|
|
134
|
+
configToWrite.wallet.encrypted_path = join(dir, "wallet.enc");
|
|
135
|
+
}
|
|
136
|
+
const yamlContent = stringify(configToWrite);
|
|
137
|
+
writeFileSync(configPath, yamlContent, {
|
|
138
|
+
encoding: "utf-8",
|
|
139
|
+
mode: 384
|
|
140
|
+
});
|
|
141
|
+
console.log(`Config created at ${configPath}`);
|
|
142
|
+
const walletPath = join(dir, "wallet.enc");
|
|
143
|
+
if (existsSync(walletPath) && !force) {
|
|
144
|
+
console.log(
|
|
145
|
+
`Wallet already exists at ${walletPath}. Skipping wallet generation.`
|
|
146
|
+
);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const walletPassword = password ?? process.env["TOWNHOUSE_WALLET_PASSWORD"];
|
|
150
|
+
if (!walletPassword) {
|
|
151
|
+
console.error(
|
|
152
|
+
"Wallet password required. Use --password flag or TOWNHOUSE_WALLET_PASSWORD env var."
|
|
153
|
+
);
|
|
154
|
+
process.exitCode = 1;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const walletManager = new WalletManager({ encryptedPath: walletPath });
|
|
158
|
+
const { mnemonic } = await walletManager.generate();
|
|
159
|
+
console.log("");
|
|
160
|
+
console.log("=== IMPORTANT: Back up your seed phrase ===");
|
|
161
|
+
console.log("");
|
|
162
|
+
console.log(` ${mnemonic}`);
|
|
163
|
+
console.log("");
|
|
164
|
+
console.log("This is the ONLY time your seed phrase will be shown.");
|
|
165
|
+
console.log("Store it safely. You will need it to recover your node keys.");
|
|
166
|
+
console.log("============================================");
|
|
167
|
+
console.log("");
|
|
168
|
+
const encrypted = encryptWallet(mnemonic, walletPassword);
|
|
169
|
+
await saveWallet(walletPath, encrypted);
|
|
170
|
+
console.log(`Wallet saved to ${walletPath}`);
|
|
171
|
+
console.log("");
|
|
172
|
+
console.log("Derived Node Addresses:");
|
|
173
|
+
console.log("-----------------------");
|
|
174
|
+
const allKeys = walletManager.getAllKeys();
|
|
175
|
+
for (const info of allKeys) {
|
|
176
|
+
console.log(` ${info.nodeType.padEnd(6)} Nostr: ${info.nostrPubkey}`);
|
|
177
|
+
console.log(` ${"".padEnd(6)} EVM: ${info.evmAddress}`);
|
|
178
|
+
}
|
|
179
|
+
walletManager.lock();
|
|
180
|
+
}
|
|
181
|
+
async function handleSetup(configDir, port, noBrowser, dockerInstance, browserOpener) {
|
|
182
|
+
const dir = resolve(configDir ?? DEFAULT_CONFIG_DIR);
|
|
183
|
+
const configPath = join(dir, "config.yaml");
|
|
184
|
+
const walletPath = join(dir, "wallet.enc");
|
|
185
|
+
if (existsSync(configPath) && existsSync(walletPath)) {
|
|
186
|
+
console.log("Already initialized \u2014 run `townhouse up` to start your nodes");
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (existsSync(configPath) && !existsSync(walletPath)) {
|
|
190
|
+
console.error(
|
|
191
|
+
`Found ${configPath} but no wallet at ${walletPath}.
|
|
192
|
+
Delete the orphan config and re-run \`townhouse setup\`, or restore the wallet from backup.`
|
|
193
|
+
);
|
|
194
|
+
process.exitCode = 1;
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const docker = dockerInstance ?? new Docker();
|
|
198
|
+
const opener = browserOpener ?? new RealBrowserOpener();
|
|
199
|
+
const wizardServer = await createWizardApiServer({
|
|
200
|
+
configDir: dir,
|
|
201
|
+
configPath,
|
|
202
|
+
walletPath,
|
|
203
|
+
port,
|
|
204
|
+
docker
|
|
205
|
+
});
|
|
206
|
+
const url = `http://127.0.0.1:${port}/wizard`;
|
|
207
|
+
try {
|
|
208
|
+
await wizardServer.app.listen({ host: "127.0.0.1", port });
|
|
209
|
+
} catch (err) {
|
|
210
|
+
const e = err;
|
|
211
|
+
if (e.code === "EADDRINUSE") {
|
|
212
|
+
console.error(
|
|
213
|
+
`Port ${port} is already in use. Pass \`--port <n>\` to choose a different port.`
|
|
214
|
+
);
|
|
215
|
+
process.exitCode = 1;
|
|
216
|
+
try {
|
|
217
|
+
await wizardServer.close();
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
throw err;
|
|
223
|
+
}
|
|
224
|
+
console.log(`Wizard ready at ${url}`);
|
|
225
|
+
if (!noBrowser) {
|
|
226
|
+
await opener.open(url);
|
|
227
|
+
}
|
|
228
|
+
let shuttingDown = false;
|
|
229
|
+
const shutdown = async (sig) => {
|
|
230
|
+
if (shuttingDown) return;
|
|
231
|
+
shuttingDown = true;
|
|
232
|
+
console.log(`
|
|
233
|
+
Received ${sig}, shutting down...`);
|
|
234
|
+
try {
|
|
235
|
+
await wizardServer.close();
|
|
236
|
+
} catch {
|
|
237
|
+
}
|
|
238
|
+
process.exit(0);
|
|
239
|
+
};
|
|
240
|
+
process.once("SIGINT", () => {
|
|
241
|
+
void shutdown("SIGINT");
|
|
242
|
+
});
|
|
243
|
+
process.once("SIGTERM", () => {
|
|
244
|
+
void shutdown("SIGTERM");
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
async function handleWalletShow(config, password) {
|
|
248
|
+
const walletPath = config.wallet.encrypted_path;
|
|
249
|
+
const result = await loadWallet(walletPath);
|
|
250
|
+
if (!result) {
|
|
251
|
+
console.error("No wallet found. Run `townhouse init` first.");
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (result.permissionsWarning) {
|
|
256
|
+
console.error(result.permissionsWarning);
|
|
257
|
+
}
|
|
258
|
+
const walletPassword = password ?? process.env["TOWNHOUSE_WALLET_PASSWORD"];
|
|
259
|
+
if (!walletPassword) {
|
|
260
|
+
console.error(
|
|
261
|
+
"Wallet password required. Use --password flag or TOWNHOUSE_WALLET_PASSWORD env var."
|
|
262
|
+
);
|
|
263
|
+
process.exitCode = 1;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const walletManager = new WalletManager({ encryptedPath: walletPath });
|
|
267
|
+
try {
|
|
268
|
+
await walletManager.fromMnemonic(
|
|
269
|
+
decryptWallet(result.wallet, walletPassword)
|
|
270
|
+
);
|
|
271
|
+
} catch (err) {
|
|
272
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
273
|
+
console.error(`Failed to decrypt wallet: ${msg}`);
|
|
274
|
+
process.exitCode = 1;
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
console.log(
|
|
278
|
+
"Node Type | Nostr Pubkey | EVM Address | Derivation Path"
|
|
279
|
+
);
|
|
280
|
+
console.log(
|
|
281
|
+
"-----------|------------------------------------------------------------------|--------------------------------------------|--------------------------"
|
|
282
|
+
);
|
|
283
|
+
const allKeys = walletManager.getAllKeys();
|
|
284
|
+
for (const info of allKeys) {
|
|
285
|
+
console.log(
|
|
286
|
+
`${info.nodeType.padEnd(10)} | ${info.nostrPubkey} | ${info.evmAddress} | ${info.nostrDerivationPath}`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
walletManager.lock();
|
|
290
|
+
}
|
|
291
|
+
async function handleStatus(docker, config) {
|
|
292
|
+
const orchestrator = new DockerOrchestrator(docker, config);
|
|
293
|
+
const statuses = await orchestrator.status();
|
|
294
|
+
console.log("Node Status:");
|
|
295
|
+
console.log("------------");
|
|
296
|
+
for (const s of statuses) {
|
|
297
|
+
const health = s.health ? ` (${s.health})` : "";
|
|
298
|
+
console.log(` ${s.name.padEnd(12)} ${s.state}${health}`);
|
|
299
|
+
}
|
|
300
|
+
const connectorHs = config.transport.hiddenService;
|
|
301
|
+
const relayHs = config.transport.relayHiddenService;
|
|
302
|
+
if (config.transport.mode === "ator" || connectorHs?.externalUrl || relayHs?.externalUrl || config.transport.externalUrl) {
|
|
303
|
+
console.log("");
|
|
304
|
+
console.log("Hidden Services:");
|
|
305
|
+
console.log("----------------");
|
|
306
|
+
const connectorUrl = connectorHs?.externalUrl ?? config.transport.externalUrl;
|
|
307
|
+
if (connectorUrl) {
|
|
308
|
+
console.log(` Connector (BTP): ${connectorUrl}`);
|
|
309
|
+
}
|
|
310
|
+
if (relayHs?.externalUrl) {
|
|
311
|
+
console.log(` Relay (Nostr): ${relayHs.externalUrl}`);
|
|
312
|
+
}
|
|
313
|
+
if (!connectorUrl && !relayHs?.externalUrl) {
|
|
314
|
+
console.log(" (ator mode set but no externalUrl configured)");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
try {
|
|
318
|
+
const adminClient = new ConnectorAdminClient(
|
|
319
|
+
`http://127.0.0.1:${config.connector.adminPort}`
|
|
320
|
+
);
|
|
321
|
+
const metrics = await adminClient.getMetrics();
|
|
322
|
+
const peers = await adminClient.getPeers();
|
|
323
|
+
const activePeers = peers.filter((p) => p.connected).length;
|
|
324
|
+
console.log("");
|
|
325
|
+
console.log("Connector Metrics:");
|
|
326
|
+
console.log("------------------");
|
|
327
|
+
console.log(` Packets forwarded: ${metrics.aggregate.packetsForwarded}`);
|
|
328
|
+
console.log(` Active peers: ${activePeers}/${peers.length}`);
|
|
329
|
+
} catch {
|
|
330
|
+
console.log("");
|
|
331
|
+
console.log("Connector Metrics: unavailable");
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function handleMetrics(config) {
|
|
335
|
+
const adminClient = new ConnectorAdminClient(
|
|
336
|
+
`http://127.0.0.1:${config.connector.adminPort}`
|
|
337
|
+
);
|
|
338
|
+
try {
|
|
339
|
+
const metrics = await adminClient.getMetrics();
|
|
340
|
+
const peers = await adminClient.getPeers();
|
|
341
|
+
const peerMetrics = new Map(metrics.peers.map((p) => [p.peerId, p]));
|
|
342
|
+
console.log("Connector Metrics:");
|
|
343
|
+
console.log("------------------");
|
|
344
|
+
console.log(` Packets forwarded: ${metrics.aggregate.packetsForwarded}`);
|
|
345
|
+
console.log(` Packets rejected: ${metrics.aggregate.packetsRejected}`);
|
|
346
|
+
console.log(` Bytes sent: ${metrics.aggregate.bytesSent}`);
|
|
347
|
+
console.log("");
|
|
348
|
+
console.log("Peers:");
|
|
349
|
+
console.log("------");
|
|
350
|
+
if (peers.length === 0) {
|
|
351
|
+
console.log(" No peers connected");
|
|
352
|
+
} else {
|
|
353
|
+
for (const peer of peers) {
|
|
354
|
+
const status = peer.connected ? "connected" : "disconnected";
|
|
355
|
+
const packets = peerMetrics.get(peer.id)?.packetsForwarded ?? 0;
|
|
356
|
+
console.log(` ${peer.id.padEnd(12)} ${status} (${packets} packets)`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch (error) {
|
|
360
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
361
|
+
console.error(`Failed to fetch connector metrics: ${msg}`);
|
|
362
|
+
process.exitCode = 1;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function resolveProfiles(values, config) {
|
|
366
|
+
const explicitFlags = [];
|
|
367
|
+
if (values["town"]) explicitFlags.push("town");
|
|
368
|
+
if (values["mill"]) explicitFlags.push("mill");
|
|
369
|
+
if (values["dvm"]) explicitFlags.push("dvm");
|
|
370
|
+
if (explicitFlags.length > 0) {
|
|
371
|
+
return explicitFlags;
|
|
372
|
+
}
|
|
373
|
+
const enabled = [];
|
|
374
|
+
if (config.nodes.town.enabled) enabled.push("town");
|
|
375
|
+
if (config.nodes.mill.enabled) enabled.push("mill");
|
|
376
|
+
if (config.nodes.dvm.enabled) enabled.push("dvm");
|
|
377
|
+
return enabled;
|
|
378
|
+
}
|
|
379
|
+
async function handleUp(configPath, config, profiles, docker, password, dryRun = false) {
|
|
380
|
+
if (profiles.length === 0) {
|
|
381
|
+
console.log(
|
|
382
|
+
"No nodes enabled in config. Enable nodes in config.yaml first."
|
|
383
|
+
);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
const walletPath = config.wallet.encrypted_path;
|
|
387
|
+
let walletManager;
|
|
388
|
+
if (!existsSync(walletPath)) {
|
|
389
|
+
console.error(
|
|
390
|
+
`Wallet not found at ${walletPath}. Run \`townhouse setup\` first (or restore your wallet backup).`
|
|
391
|
+
);
|
|
392
|
+
process.exitCode = 1;
|
|
393
|
+
return;
|
|
394
|
+
} else {
|
|
395
|
+
const walletPassword = password ?? process.env["TOWNHOUSE_WALLET_PASSWORD"];
|
|
396
|
+
if (!walletPassword) {
|
|
397
|
+
throw new Error(
|
|
398
|
+
"Wallet password required to start the API. Use --password flag or TOWNHOUSE_WALLET_PASSWORD env var."
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const loaded = await loadWallet(walletPath);
|
|
402
|
+
if (!loaded) {
|
|
403
|
+
throw new Error(`Wallet at ${walletPath} could not be read.`);
|
|
404
|
+
}
|
|
405
|
+
if (loaded.permissionsWarning) {
|
|
406
|
+
console.error(loaded.permissionsWarning);
|
|
407
|
+
}
|
|
408
|
+
walletManager = new WalletManager({ encryptedPath: walletPath });
|
|
409
|
+
try {
|
|
410
|
+
await walletManager.fromMnemonic(
|
|
411
|
+
decryptWallet(loaded.wallet, walletPassword)
|
|
412
|
+
);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
415
|
+
throw new Error(`Failed to decrypt wallet: ${msg}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const orchestrator = new DockerOrchestrator(docker, config, walletManager);
|
|
419
|
+
orchestrator.on(
|
|
420
|
+
"containerState",
|
|
421
|
+
(event) => {
|
|
422
|
+
console.log(` ${event.name}: ${event.state}`);
|
|
423
|
+
}
|
|
424
|
+
);
|
|
425
|
+
orchestrator.on(
|
|
426
|
+
"pullProgress",
|
|
427
|
+
(event) => {
|
|
428
|
+
const progress = event.progress ? ` ${event.progress}` : "";
|
|
429
|
+
console.log(` [pull] ${event.image}: ${event.status}${progress}`);
|
|
430
|
+
}
|
|
431
|
+
);
|
|
432
|
+
let apiServer;
|
|
433
|
+
const sigintHandler = async () => {
|
|
434
|
+
console.log("\nReceived SIGINT, shutting down gracefully...");
|
|
435
|
+
if (apiServer) {
|
|
436
|
+
try {
|
|
437
|
+
await apiServer.close();
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
await orchestrator.down();
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
process.exit(0);
|
|
446
|
+
};
|
|
447
|
+
process.on("SIGINT", sigintHandler);
|
|
448
|
+
const sigtermHandler = async () => {
|
|
449
|
+
console.log("\nReceived SIGTERM, shutting down gracefully...");
|
|
450
|
+
if (apiServer) {
|
|
451
|
+
try {
|
|
452
|
+
await apiServer.close();
|
|
453
|
+
} catch {
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
await orchestrator.down();
|
|
458
|
+
} catch {
|
|
459
|
+
}
|
|
460
|
+
process.exit(0);
|
|
461
|
+
};
|
|
462
|
+
process.on("SIGTERM", sigtermHandler);
|
|
463
|
+
let serverStarted = false;
|
|
464
|
+
if (profiles.includes("dvm") && config.nodes.dvm.enabled && !process.env["TURBO_TOKEN"]) {
|
|
465
|
+
console.warn(
|
|
466
|
+
"[townhouse] WARN: TURBO_TOKEN is not set \u2014 Arweave DVM (kind:5094) uploads will fail at first job."
|
|
467
|
+
);
|
|
468
|
+
console.warn(
|
|
469
|
+
"[townhouse] Export TURBO_TOKEN=<arweave-jwk-json> before `townhouse up` to enable uploads."
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
try {
|
|
473
|
+
console.log(`Starting nodes: ${profiles.join(", ")}...`);
|
|
474
|
+
if (!dryRun) {
|
|
475
|
+
await orchestrator.up(profiles);
|
|
476
|
+
console.log("All nodes started successfully.");
|
|
477
|
+
} else {
|
|
478
|
+
console.log("[dry-run] Skipped orchestrator.up()");
|
|
479
|
+
}
|
|
480
|
+
if (walletManager) {
|
|
481
|
+
const connectorAdmin = new ConnectorAdminClient(
|
|
482
|
+
`http://127.0.0.1:${config.connector.adminPort}`
|
|
483
|
+
);
|
|
484
|
+
const transportProbe = new TransportProbe({
|
|
485
|
+
proxyUrl: config.transport.mode === "ator" ? config.transport.socksProxy ?? DEFAULT_ATOR_PROXY : ""
|
|
486
|
+
});
|
|
487
|
+
if (config.transport.mode === "ator") {
|
|
488
|
+
transportProbe.start();
|
|
489
|
+
}
|
|
490
|
+
const apiDeps = {
|
|
491
|
+
configPath,
|
|
492
|
+
config,
|
|
493
|
+
orchestrator,
|
|
494
|
+
wallet: walletManager,
|
|
495
|
+
connectorAdmin,
|
|
496
|
+
transportProbe
|
|
497
|
+
};
|
|
498
|
+
apiServer = await createApiServer(apiDeps);
|
|
499
|
+
const { host, port } = config.api;
|
|
500
|
+
if (!dryRun) {
|
|
501
|
+
await apiServer.app.listen({
|
|
502
|
+
host: host ?? "127.0.0.1",
|
|
503
|
+
port: port ?? 9400
|
|
504
|
+
});
|
|
505
|
+
serverStarted = true;
|
|
506
|
+
console.log(
|
|
507
|
+
`
|
|
508
|
+
[Townhouse API] listening on http://${host ?? "127.0.0.1"}:${port ?? 9400}`
|
|
509
|
+
);
|
|
510
|
+
console.log(
|
|
511
|
+
" GET /nodes, GET /nodes/:type, PATCH /nodes/:type/config, GET /wallet, WS /metrics"
|
|
512
|
+
);
|
|
513
|
+
} else {
|
|
514
|
+
console.log(
|
|
515
|
+
`[dry-run] API factory invoked: configPath=${configPath} host=${host ?? "127.0.0.1"} port=${port ?? 9400} connectorAdmin=http://127.0.0.1:${config.connector.adminPort} wallet=WalletManager`
|
|
516
|
+
);
|
|
517
|
+
await apiServer.close();
|
|
518
|
+
apiServer = void 0;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
} catch (error) {
|
|
522
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
523
|
+
if (msg.includes("Docker is not running") || msg.includes("ENOENT") || msg.includes("ECONNREFUSED") || msg.includes("socket")) {
|
|
524
|
+
throw new Error(
|
|
525
|
+
`Docker is not available. Please ensure Docker is running and try again. (${msg})`
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
} finally {
|
|
530
|
+
if (!serverStarted) {
|
|
531
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
532
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async function handleDown(config, docker) {
|
|
537
|
+
const orchestrator = new DockerOrchestrator(docker, config);
|
|
538
|
+
orchestrator.on(
|
|
539
|
+
"containerState",
|
|
540
|
+
(event) => {
|
|
541
|
+
console.log(` ${event.name}: ${event.state}`);
|
|
542
|
+
}
|
|
543
|
+
);
|
|
544
|
+
console.log("Stopping nodes...");
|
|
545
|
+
await orchestrator.down();
|
|
546
|
+
console.log("All nodes stopped.");
|
|
547
|
+
}
|
|
548
|
+
async function main(argv, dockerInstance, browserOpener) {
|
|
549
|
+
const { values, positionals } = parseArgs({
|
|
550
|
+
args: argv,
|
|
551
|
+
options: {
|
|
552
|
+
help: { type: "boolean" },
|
|
553
|
+
force: { type: "boolean" },
|
|
554
|
+
config: { type: "string", short: "c" },
|
|
555
|
+
"config-dir": { type: "string" },
|
|
556
|
+
town: { type: "boolean" },
|
|
557
|
+
mill: { type: "boolean" },
|
|
558
|
+
dvm: { type: "boolean" },
|
|
559
|
+
password: { type: "string" },
|
|
560
|
+
"dry-run": { type: "boolean" },
|
|
561
|
+
"no-browser": { type: "boolean" },
|
|
562
|
+
port: { type: "string" },
|
|
563
|
+
preset: { type: "string" },
|
|
564
|
+
yes: { type: "boolean" }
|
|
565
|
+
},
|
|
566
|
+
strict: false,
|
|
567
|
+
allowPositionals: true
|
|
568
|
+
});
|
|
569
|
+
if (values.help) {
|
|
570
|
+
console.log(HELP_TEXT);
|
|
571
|
+
throw new CliHelpRequested();
|
|
572
|
+
}
|
|
573
|
+
const command = positionals[0];
|
|
574
|
+
if (!command) {
|
|
575
|
+
console.log(HELP_TEXT);
|
|
576
|
+
throw new CliHelpRequested();
|
|
577
|
+
}
|
|
578
|
+
switch (command) {
|
|
579
|
+
case "setup": {
|
|
580
|
+
const portStr = values["port"];
|
|
581
|
+
const port = portStr ? Number(portStr) : 9400;
|
|
582
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
583
|
+
console.error("--port must be an integer between 1 and 65535");
|
|
584
|
+
process.exitCode = 1;
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
await handleSetup(
|
|
588
|
+
values["config-dir"],
|
|
589
|
+
port,
|
|
590
|
+
values["no-browser"] === true,
|
|
591
|
+
dockerInstance,
|
|
592
|
+
browserOpener
|
|
593
|
+
);
|
|
594
|
+
break;
|
|
595
|
+
}
|
|
596
|
+
case "init": {
|
|
597
|
+
const presetVal = values.preset;
|
|
598
|
+
if (presetVal !== void 0 && presetVal !== "demo") {
|
|
599
|
+
console.error(`Unknown preset: ${presetVal}. Supported: demo`);
|
|
600
|
+
process.exitCode = 1;
|
|
601
|
+
break;
|
|
602
|
+
}
|
|
603
|
+
await handleInit(
|
|
604
|
+
values.force === true,
|
|
605
|
+
values["config-dir"],
|
|
606
|
+
values.password,
|
|
607
|
+
presetVal,
|
|
608
|
+
values.yes === true
|
|
609
|
+
);
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
case "wallet": {
|
|
613
|
+
const subCommand = positionals[1];
|
|
614
|
+
if (subCommand === "show") {
|
|
615
|
+
const configPath = values.config ?? DEFAULT_CONFIG_PATH;
|
|
616
|
+
const config = loadConfig(configPath);
|
|
617
|
+
await handleWalletShow(config, values.password);
|
|
618
|
+
} else {
|
|
619
|
+
console.error(
|
|
620
|
+
"Usage: townhouse wallet show [-c <path>] [--password <pw>]"
|
|
621
|
+
);
|
|
622
|
+
process.exitCode = 1;
|
|
623
|
+
}
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
case "status": {
|
|
627
|
+
const configPath = values.config ?? DEFAULT_CONFIG_PATH;
|
|
628
|
+
const config = loadConfig(configPath);
|
|
629
|
+
const docker = dockerInstance ?? new Docker();
|
|
630
|
+
await handleStatus(docker, config);
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case "up": {
|
|
634
|
+
const configPath = values.config ?? DEFAULT_CONFIG_PATH;
|
|
635
|
+
const config = loadConfig(configPath);
|
|
636
|
+
const docker = dockerInstance ?? new Docker();
|
|
637
|
+
const profiles = resolveProfiles(values, config);
|
|
638
|
+
await handleUp(
|
|
639
|
+
configPath,
|
|
640
|
+
config,
|
|
641
|
+
profiles,
|
|
642
|
+
docker,
|
|
643
|
+
values.password,
|
|
644
|
+
values["dry-run"] === true
|
|
645
|
+
);
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
case "down": {
|
|
649
|
+
const configPath = values.config ?? DEFAULT_CONFIG_PATH;
|
|
650
|
+
const config = loadConfig(configPath);
|
|
651
|
+
const docker = dockerInstance ?? new Docker();
|
|
652
|
+
await handleDown(config, docker);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
case "metrics": {
|
|
656
|
+
const configPath = values.config ?? DEFAULT_CONFIG_PATH;
|
|
657
|
+
const config = loadConfig(configPath);
|
|
658
|
+
await handleMetrics(config);
|
|
659
|
+
break;
|
|
660
|
+
}
|
|
661
|
+
default: {
|
|
662
|
+
const sanitized = command.replace(/[\x00-\x1f\x7f]/g, "");
|
|
663
|
+
console.error(`Unknown command: ${sanitized}`);
|
|
664
|
+
console.log(HELP_TEXT);
|
|
665
|
+
process.exitCode = 1;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
var invokedFile = process.argv[1];
|
|
670
|
+
var invokedDirectly = typeof invokedFile === "string" && import.meta.url === pathToFileURL(invokedFile).href;
|
|
671
|
+
if (invokedDirectly) {
|
|
672
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
673
|
+
if (error instanceof CliHelpRequested) {
|
|
674
|
+
process.exit(0);
|
|
675
|
+
}
|
|
676
|
+
console.error("[Townhouse] Error:", error);
|
|
677
|
+
process.exit(1);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
export {
|
|
681
|
+
CliHelpRequested,
|
|
682
|
+
main
|
|
683
|
+
};
|
|
684
|
+
//# sourceMappingURL=cli.js.map
|