@agent-team-foundation/first-tree-hub 0.9.3 → 0.9.4
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/{bootstrap-CWcBzk6C.mjs → bootstrap-DWifXj9b.mjs} +5 -5
- package/dist/cli/index.mjs +193 -171
- package/dist/{core-Y7M3d2aZ.mjs → core-USyOOh7y.mjs} +641 -2969
- package/dist/index.mjs +4 -4
- package/dist/{logger-core-2yeIU1fc-B-__AsQO.mjs → logger-core-BTmvdflj-DhdipBkV.mjs} +40 -5
- package/dist/{observability-CJzDFY_G-CmvgUuzc.mjs → observability-DV_fQKqV-CuLWzBxQ.mjs} +8 -3
- package/dist/{observability-Xi-sEZI7.mjs → observability-hDEdrmMS.mjs} +2 -2
- package/dist/web/assets/{index-C3GFm5oL.js → index-os2Tqmer.js} +60 -60
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "../
|
|
3
|
-
import { C as
|
|
4
|
-
import "../
|
|
5
|
-
import {
|
|
2
|
+
import "../observability-DV_fQKqV-CuLWzBxQ.mjs";
|
|
3
|
+
import { A as checkServerHealth, C as runMigrations, D as checkDocker, E as checkDatabase, G as SdkError, H as setJsonMode, I as stopPostgres, J as applyClientLoggerConfig, K as SessionRegistry, L as ClientRuntime, M as checkWebSocket, N as printResults, O as checkNodeVersion, R as createOwner, S as uninstallClientService, T as checkClientConfig, V as print, W as FirstTreeHubSDK, Y as configureClientLoggerForService, _ as runHomeMigration, a as showServiceLogs, b as isServiceSupported, c as COMMAND_VERSION, d as promptMissingFields, f as formatCheckReport, g as saveOnboardState, h as onboardCreate, i as parseDuration, j as checkServerReachable, k as checkServerConfig, l as isInteractive, m as onboardCheck, n as declineUpdate, o as validateLevel, p as loadOnboardState, q as cleanWorkspaces, r as promptUpdate, s as startServer, t as createExecuteUpdate, u as promptAddAgent, v as getClientServiceStatus, w as checkAgentConfigs, y as installClientService } from "../core-USyOOh7y.mjs";
|
|
4
|
+
import "../logger-core-BTmvdflj-DhdipBkV.mjs";
|
|
5
|
+
import { C as serverConfigSchema, _ as loadAgents, b as resetConfig, c as saveCredentials, d as DEFAULT_HOME_DIR, f as agentConfigSchema, g as initConfig, h as getConfigValue, i as loadCredentials, l as DEFAULT_CONFIG_DIR, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, r as ensureFreshAdminToken, s as saveAgentConfig, u as DEFAULT_DATA_DIR, w as setConfigValue, x as resetConfigMeta, y as readConfigFile } from "../bootstrap-DWifXj9b.mjs";
|
|
6
6
|
import { n as bindFeishuUser, t as bindFeishuBot } from "../feishu-GlaczcVf.mjs";
|
|
7
|
-
import { Command } from "commander";
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
9
7
|
import { join } from "node:path";
|
|
8
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
9
|
+
import { Command } from "commander";
|
|
10
10
|
import { confirm, input, password, select } from "@inquirer/prompts";
|
|
11
11
|
//#region src/cli/output.ts
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* CLI output re-exports. The underlying implementation lives in
|
|
14
|
+
* `core/output.ts` (the Print layer). Keep these thin wrappers so callers that
|
|
15
|
+
* only depend on `cli/output.ts` keep working during the migration.
|
|
16
|
+
*/
|
|
13
17
|
function success(data) {
|
|
14
|
-
|
|
15
|
-
ok: true,
|
|
16
|
-
data
|
|
17
|
-
})}\n`);
|
|
18
|
+
print.result(data);
|
|
18
19
|
}
|
|
19
|
-
/** Write an error JSON envelope to stderr and exit with the given code. */
|
|
20
20
|
function fail(code, message, exitCode = 1) {
|
|
21
|
-
|
|
22
|
-
ok: false,
|
|
23
|
-
error: {
|
|
24
|
-
code,
|
|
25
|
-
message
|
|
26
|
-
}
|
|
27
|
-
})}\n`);
|
|
28
|
-
process.exit(exitCode);
|
|
21
|
+
return print.fail(code, message, exitCode);
|
|
29
22
|
}
|
|
30
23
|
//#endregion
|
|
31
24
|
//#region src/commands/agent-config.ts
|
|
@@ -318,22 +311,22 @@ function registerAgentCommands(program) {
|
|
|
318
311
|
mode: 448
|
|
319
312
|
});
|
|
320
313
|
setConfigValue(join(agentDir, "agent.yaml"), "agentId", agentId);
|
|
321
|
-
|
|
322
|
-
|
|
314
|
+
print.line(` Agent "${agentName}" added.\n`);
|
|
315
|
+
print.line(` Config: ${join(agentDir, "agent.yaml")}\n`);
|
|
323
316
|
} catch (error) {
|
|
324
317
|
if (error.name === "ExitPromptError") {
|
|
325
|
-
|
|
318
|
+
print.line("\n Cancelled.\n");
|
|
326
319
|
return;
|
|
327
320
|
}
|
|
328
321
|
const msg = error instanceof Error ? error.message : String(error);
|
|
329
|
-
|
|
322
|
+
print.line(` Error: ${msg}\n`);
|
|
330
323
|
process.exit(1);
|
|
331
324
|
}
|
|
332
325
|
});
|
|
333
326
|
agent.command("remove <name>").description("Remove a local agent alias and its runtime data").action((name) => {
|
|
334
327
|
const agentDir = join(DEFAULT_CONFIG_DIR, "agents", name);
|
|
335
328
|
if (!existsSync(agentDir)) {
|
|
336
|
-
|
|
329
|
+
print.line(` Agent "${name}" not found.\n`);
|
|
337
330
|
process.exit(1);
|
|
338
331
|
}
|
|
339
332
|
rmSync(agentDir, {
|
|
@@ -345,7 +338,7 @@ function registerAgentCommands(program) {
|
|
|
345
338
|
force: true
|
|
346
339
|
});
|
|
347
340
|
rmSync(join(DEFAULT_DATA_DIR, "sessions", `${name}.json`), { force: true });
|
|
348
|
-
|
|
341
|
+
print.line(` Agent "${name}" removed.\n`);
|
|
349
342
|
});
|
|
350
343
|
agent.command("list").description("List locally-configured agents").action(() => {
|
|
351
344
|
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
@@ -355,12 +348,12 @@ function registerAgentCommands(program) {
|
|
|
355
348
|
agentsDir
|
|
356
349
|
});
|
|
357
350
|
if (agents.size === 0) {
|
|
358
|
-
|
|
351
|
+
print.line(" No agents configured.\n");
|
|
359
352
|
return;
|
|
360
353
|
}
|
|
361
|
-
for (const [name, config] of agents)
|
|
354
|
+
for (const [name, config] of agents) print.line(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
|
|
362
355
|
} catch {
|
|
363
|
-
|
|
356
|
+
print.line(" No agents configured.\n");
|
|
364
357
|
}
|
|
365
358
|
});
|
|
366
359
|
agent.command("create <name>").description("Create an agent on Hub and bind it locally").requiredOption("--type <type>", "Agent type (human, personal_assistant, autonomous_agent)").requiredOption("--client-id <id>", "Client (machine) that will run this agent — must be owned by you. Run `first-tree-hub client connect` on that machine first.").option("--runtime <runtime>", "Runtime handler (default: claude-code)", "claude-code").option("--display-name <name>", "Display name").option("--server <url>", "Hub server URL").action(async (name, options) => {
|
|
@@ -384,10 +377,10 @@ function registerAgentCommands(program) {
|
|
|
384
377
|
});
|
|
385
378
|
if (!createRes.ok) fail("CREATE_ERROR", (await createRes.json().catch(() => ({}))).error ?? `Failed to create agent (HTTP ${createRes.status})`, 1);
|
|
386
379
|
const created = await createRes.json();
|
|
387
|
-
|
|
380
|
+
print.line(` \u2713 Agent created: ${created.name ?? created.uuid}\n`);
|
|
388
381
|
const agentDir = saveAgentConfig(name, created.uuid, options.runtime);
|
|
389
|
-
|
|
390
|
-
|
|
382
|
+
print.line(` \u2713 Config saved: ${agentDir}/agent.yaml\n`);
|
|
383
|
+
print.line(" ✓ Agent ready — start the client on that machine to bind\n");
|
|
391
384
|
} catch (error) {
|
|
392
385
|
fail("CREATE_ERROR", error instanceof Error ? error.message : String(error));
|
|
393
386
|
}
|
|
@@ -413,7 +406,7 @@ function registerAgentCommands(program) {
|
|
|
413
406
|
signal: AbortSignal.timeout(1e4)
|
|
414
407
|
});
|
|
415
408
|
if (!patchRes.ok) fail("CLAIM_ERROR", (await patchRes.json().catch(() => ({}))).error ?? `Claim failed (HTTP ${patchRes.status})`, 1);
|
|
416
|
-
|
|
409
|
+
print.line(` Claimed "${target.name ?? target.uuid}" — now managed by you.\n`);
|
|
417
410
|
} catch (error) {
|
|
418
411
|
fail("CLAIM_ERROR", error instanceof Error ? error.message : String(error));
|
|
419
412
|
}
|
|
@@ -423,7 +416,7 @@ function registerAgentCommands(program) {
|
|
|
423
416
|
const ttlMs = Number.parseInt(options?.ttl ?? String(defaultDays), 10) * 24 * 60 * 60 * 1e3;
|
|
424
417
|
const workspacesDir = join(DEFAULT_DATA_DIR, "workspaces");
|
|
425
418
|
if (!existsSync(workspacesDir)) {
|
|
426
|
-
|
|
419
|
+
print.line(" No workspaces found.\n");
|
|
427
420
|
return;
|
|
428
421
|
}
|
|
429
422
|
const agentNames = agentName ? [agentName] : readdirSync(workspacesDir);
|
|
@@ -436,9 +429,9 @@ function registerAgentCommands(program) {
|
|
|
436
429
|
for (const [chatId, data] of persisted) if (data.status !== "evicted") activeChatIds.add(chatId);
|
|
437
430
|
const removed = cleanWorkspaces(agentWorkspaceRoot, activeChatIds, ttlMs);
|
|
438
431
|
totalRemoved += removed.length;
|
|
439
|
-
for (const chatId of removed)
|
|
432
|
+
for (const chatId of removed) print.line(` Removed: ${name}/${chatId}\n`);
|
|
440
433
|
}
|
|
441
|
-
|
|
434
|
+
print.line(` ${totalRemoved} workspace(s) cleaned.\n`);
|
|
442
435
|
});
|
|
443
436
|
const bind = agent.command("bind").description("Bind an agent to a client machine or external IM account");
|
|
444
437
|
bind.command("client <agentName>").description("Bind an unbound agent to a client machine (first-time bind only — ID is immutable once set)").requiredOption("--client-id <id>", "Client (machine) ID — must be owned by you. Run `first-tree-hub client connect` on that machine first.").option("--server <url>", "Hub server URL").action(async (agentName, options) => {
|
|
@@ -456,7 +449,7 @@ function registerAgentCommands(program) {
|
|
|
456
449
|
signal: AbortSignal.timeout(1e4)
|
|
457
450
|
});
|
|
458
451
|
if (!patchRes.ok) fail("BIND_CLIENT_ERROR", (await patchRes.json().catch(() => ({}))).error ?? `Bind failed (HTTP ${patchRes.status})`, 1);
|
|
459
|
-
|
|
452
|
+
print.line(` \u2713 Bound "${target.name ?? target.uuid}" to client ${options.clientId}.\n`);
|
|
460
453
|
success({
|
|
461
454
|
agentId: target.uuid,
|
|
462
455
|
clientId: options.clientId
|
|
@@ -471,7 +464,7 @@ function registerAgentCommands(program) {
|
|
|
471
464
|
const serverUrl = resolveServerUrl(options.server);
|
|
472
465
|
const { agentId } = resolveLocalAgent(options.agent);
|
|
473
466
|
await bindFeishuBot(serverUrl, await ensureFreshAccessToken(), agentId, options.appId, options.appSecret);
|
|
474
|
-
|
|
467
|
+
print.line("Feishu bot bound successfully.\n");
|
|
475
468
|
success({
|
|
476
469
|
platform: "feishu",
|
|
477
470
|
bound: true
|
|
@@ -486,7 +479,7 @@ function registerAgentCommands(program) {
|
|
|
486
479
|
const serverUrl = resolveServerUrl(options.server);
|
|
487
480
|
const { agentId } = resolveLocalAgent(options.agent);
|
|
488
481
|
await bindFeishuUser(serverUrl, await ensureFreshAccessToken(), agentId, humanAgentId, options.feishuId);
|
|
489
|
-
|
|
482
|
+
print.line(`Feishu user ${options.feishuId} bound to ${humanAgentId}.\n`);
|
|
490
483
|
success({
|
|
491
484
|
platform: "feishu",
|
|
492
485
|
humanAgentId,
|
|
@@ -560,30 +553,30 @@ function registerAgentCommands(program) {
|
|
|
560
553
|
if (name) {
|
|
561
554
|
const ag = data.agents.find((a) => a.agentId === name);
|
|
562
555
|
if (!ag) {
|
|
563
|
-
|
|
556
|
+
print.line(`\n Agent "${name}" is not running.\n\n`);
|
|
564
557
|
return;
|
|
565
558
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (ag.activeSessions !== null)
|
|
570
|
-
if (ag.clientId)
|
|
571
|
-
|
|
559
|
+
print.line(`\n Agent: ${ag.agentId}\n`);
|
|
560
|
+
print.line(` Runtime: ${ag.runtimeType ?? "—"}\n`);
|
|
561
|
+
print.line(` State: ${ag.runtimeState ?? "—"}\n`);
|
|
562
|
+
if (ag.activeSessions !== null) print.line(` Sessions: ${ag.activeSessions} active / ${ag.totalSessions ?? 0} total\n`);
|
|
563
|
+
if (ag.clientId) print.line(` Client: ${ag.clientId}\n`);
|
|
564
|
+
print.line("\n");
|
|
572
565
|
return;
|
|
573
566
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
567
|
+
print.line(`\n Hub: ${serverUrl}\n\n`);
|
|
568
|
+
print.line(` Clients: ${data.clients} connected\n`);
|
|
569
|
+
print.line(` Agents: ${data.running} running / ${data.total} total\n`);
|
|
570
|
+
print.line(` Errors: ${data.byState.error} | Blocked: ${data.byState.blocked} | Working: ${data.byState.working} | Idle: ${data.byState.idle}\n\n`);
|
|
578
571
|
if (data.agents.length > 0) {
|
|
579
572
|
const header = ` ${"AGENT".padEnd(18)} ${"RUNTIME".padEnd(14)} ${"STATE".padEnd(10)} SESSIONS`;
|
|
580
|
-
|
|
581
|
-
|
|
573
|
+
print.line(`${header}\n`);
|
|
574
|
+
print.line(` ${"─".repeat(header.length - 2)}\n`);
|
|
582
575
|
for (const a of data.agents) {
|
|
583
576
|
const sessions = a.activeSessions !== null ? `${a.activeSessions}/${a.totalSessions ?? 0}` : "—";
|
|
584
|
-
|
|
577
|
+
print.line(` ${(a.agentId ?? "").padEnd(18)} ${(a.runtimeType ?? "—").padEnd(14)} ${(a.runtimeState ?? "—").padEnd(10)} ${sessions}\n`);
|
|
585
578
|
}
|
|
586
|
-
|
|
579
|
+
print.line("\n");
|
|
587
580
|
}
|
|
588
581
|
} catch (error) {
|
|
589
582
|
fail("STATUS_ERROR", error instanceof Error ? error.message : String(error));
|
|
@@ -598,7 +591,7 @@ function registerAgentCommands(program) {
|
|
|
598
591
|
signal: AbortSignal.timeout(1e4)
|
|
599
592
|
});
|
|
600
593
|
if (!response.ok) fail("RESET_ERROR", `Server returned ${response.status}`, 1);
|
|
601
|
-
|
|
594
|
+
print.line(` Agent "${name}" reset to idle.\n`);
|
|
602
595
|
} catch (error) {
|
|
603
596
|
fail("RESET_ERROR", error instanceof Error ? error.message : String(error));
|
|
604
597
|
}
|
|
@@ -616,18 +609,18 @@ function registerAgentCommands(program) {
|
|
|
616
609
|
if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
|
|
617
610
|
const sessions = await response.json();
|
|
618
611
|
if (sessions.length === 0) {
|
|
619
|
-
|
|
612
|
+
print.line(`\n No sessions for "${agentName}".\n\n`);
|
|
620
613
|
return;
|
|
621
614
|
}
|
|
622
|
-
|
|
615
|
+
print.line(`\n Sessions for "${agentName}":\n\n`);
|
|
623
616
|
const header = ` ${"CHAT".padEnd(40)} ${"STATE".padEnd(12)} ${"RUNTIME".padEnd(10)} LAST ACTIVITY`;
|
|
624
|
-
|
|
625
|
-
|
|
617
|
+
print.line(`${header}\n`);
|
|
618
|
+
print.line(` ${"─".repeat(header.length - 2)}\n`);
|
|
626
619
|
for (const s of sessions) {
|
|
627
620
|
const chatShort = s.chatId.length > 38 ? `${s.chatId.slice(0, 35)}...` : s.chatId;
|
|
628
|
-
|
|
621
|
+
print.line(` ${chatShort.padEnd(40)} ${s.state.padEnd(12)} ${(s.runtimeState ?? "—").padEnd(10)} ${s.lastActivityAt}\n`);
|
|
629
622
|
}
|
|
630
|
-
|
|
623
|
+
print.line("\n");
|
|
631
624
|
} catch (error) {
|
|
632
625
|
fail("SESSIONS_ERROR", error instanceof Error ? error.message : String(error));
|
|
633
626
|
}
|
|
@@ -647,7 +640,7 @@ function registerAgentCommands(program) {
|
|
|
647
640
|
const body = await response.text();
|
|
648
641
|
fail("SESSION_CMD_ERROR", `Server returned ${response.status}: ${body}`, 1);
|
|
649
642
|
}
|
|
650
|
-
|
|
643
|
+
print.line(` Session ${cmd}: ${chatId} → sent\n`);
|
|
651
644
|
} catch (error) {
|
|
652
645
|
fail("SESSION_CMD_ERROR", error instanceof Error ? error.message : String(error));
|
|
653
646
|
}
|
|
@@ -671,9 +664,9 @@ function registerAgentCommands(program) {
|
|
|
671
664
|
fail("DM_ERROR", `Failed to create DM: ${dmRes.status} — ${body}`, 1);
|
|
672
665
|
}
|
|
673
666
|
const dm = await dmRes.json();
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
667
|
+
print.line(`\n Chat with ${targetAgent.displayName ?? targetAgent.name ?? targetAgent.uuid}\n`);
|
|
668
|
+
print.line(` Chat ID: ${dm.id}\n`);
|
|
669
|
+
print.line(` Type a message and press Enter. Ctrl+C to exit.\n\n`);
|
|
677
670
|
const rl = (await import("node:readline")).createInterface({
|
|
678
671
|
input: process.stdin,
|
|
679
672
|
output: process.stderr,
|
|
@@ -694,7 +687,7 @@ function registerAgentCommands(program) {
|
|
|
694
687
|
for (const msg of newMessages) {
|
|
695
688
|
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
696
689
|
const preview = content.length > 500 ? `${content.slice(0, 500)}...` : content;
|
|
697
|
-
|
|
690
|
+
print.line(`\r [${targetAgent.displayName ?? targetAgent.name ?? "agent"}] ${preview}\n`);
|
|
698
691
|
}
|
|
699
692
|
if (msgData.items.length > 0 && msgData.items[0]) {
|
|
700
693
|
const newest = msgData.items[0].createdAt;
|
|
@@ -725,16 +718,16 @@ function registerAgentCommands(program) {
|
|
|
725
718
|
});
|
|
726
719
|
if (!sendRes.ok) {
|
|
727
720
|
const body = await sendRes.text();
|
|
728
|
-
|
|
721
|
+
print.line(` [error] Failed to send: ${sendRes.status} — ${body}\n`);
|
|
729
722
|
} else lastSeenAt = (await sendRes.json()).createdAt;
|
|
730
723
|
} catch (err) {
|
|
731
|
-
|
|
724
|
+
print.line(` [error] ${err instanceof Error ? err.message : String(err)}\n`);
|
|
732
725
|
}
|
|
733
726
|
rl.prompt();
|
|
734
727
|
});
|
|
735
728
|
rl.on("close", () => {
|
|
736
729
|
clearInterval(pollTimer);
|
|
737
|
-
|
|
730
|
+
print.line("\n Chat ended.\n");
|
|
738
731
|
process.exit(0);
|
|
739
732
|
});
|
|
740
733
|
} catch (error) {
|
|
@@ -812,14 +805,14 @@ async function promptReplaceOrCancel(newMemberId, newServerUrl) {
|
|
|
812
805
|
const existingOrg = typeof existingPayload?.organizationId === "string" ? existingPayload.organizationId : null;
|
|
813
806
|
const serviceStatus = getClientServiceStatus();
|
|
814
807
|
const serviceLine = serviceStatus.state === "active" ? `running (${serviceStatus.detail ?? "live"})` : serviceStatus.state === "inactive" ? `installed but not running${serviceStatus.detail ? ` — ${serviceStatus.detail}` : ""}` : "not installed";
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
if (existingOrg)
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
808
|
+
print.line("\n");
|
|
809
|
+
print.line(" ⚠️ This computer is already connected to the Hub under another account.\n\n");
|
|
810
|
+
print.line(` Existing account: ${existingMember}\n`);
|
|
811
|
+
if (existingOrg) print.line(` Organization: ${existingOrg.slice(0, 8)}\n`);
|
|
812
|
+
print.line(` Server: ${existing.serverUrl}\n`);
|
|
813
|
+
print.line(` Background service: ${serviceLine}\n\n`);
|
|
814
|
+
print.line(" Replacing only affects THIS computer. Your agents, messages, and\n");
|
|
815
|
+
print.line(" settings on the Hub itself are untouched.\n\n");
|
|
823
816
|
if (await select({
|
|
824
817
|
message: "How would you like to continue?",
|
|
825
818
|
choices: [{
|
|
@@ -836,15 +829,15 @@ async function promptReplaceOrCancel(newMemberId, newServerUrl) {
|
|
|
836
829
|
return "proceed";
|
|
837
830
|
}
|
|
838
831
|
function printIsolationGuide(newServerUrl) {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
832
|
+
print.line("\n Cancelled. The existing account on this computer is untouched.\n\n");
|
|
833
|
+
print.line(" To run this new account alongside it (advanced — no background service):\n\n");
|
|
834
|
+
print.line(" export FIRST_TREE_HUB_HOME=\"$HOME/.first-tree/hub-<label>\"\n");
|
|
835
|
+
print.line(` first-tree-hub client connect ${newServerUrl} --token <token>\n`);
|
|
836
|
+
print.line(" first-tree-hub client start\n\n");
|
|
837
|
+
print.line(" Notes:\n");
|
|
838
|
+
print.line(" - Run the commands in a FRESH terminal (the isolated home must be set first).\n");
|
|
839
|
+
print.line(" - In isolated mode the client stays online only while that terminal runs.\n");
|
|
840
|
+
print.line(" - The main account's background service is not affected.\n\n");
|
|
848
841
|
}
|
|
849
842
|
/**
|
|
850
843
|
* Authenticate via connect token — exchange for full JWT credentials.
|
|
@@ -863,7 +856,7 @@ async function authenticateWithToken(url, token) {
|
|
|
863
856
|
* Authenticate via interactive username/password login.
|
|
864
857
|
*/
|
|
865
858
|
async function authenticateInteractive(url) {
|
|
866
|
-
|
|
859
|
+
print.line("\n Log in to Hub:\n");
|
|
867
860
|
const username = await input({ message: " Username:" });
|
|
868
861
|
const pw = await password({ message: " Password:" });
|
|
869
862
|
const loginRes = await fetch(`${url}/api/v1/auth/login`, {
|
|
@@ -900,30 +893,30 @@ function registerConnectCommand(parent) {
|
|
|
900
893
|
}
|
|
901
894
|
}
|
|
902
895
|
setConfigValue(join(DEFAULT_CONFIG_DIR, "client.yaml"), "server.url", url);
|
|
903
|
-
|
|
896
|
+
print.line(`\n \u2713 Server configured: ${url}\n`);
|
|
904
897
|
saveCredentials({
|
|
905
898
|
...tokens,
|
|
906
899
|
serverUrl: url
|
|
907
900
|
});
|
|
908
|
-
|
|
901
|
+
print.line(" ✓ Authenticated\n");
|
|
909
902
|
resetConfig();
|
|
910
903
|
resetConfigMeta();
|
|
911
904
|
const config = await initConfig({
|
|
912
905
|
schema: clientConfigSchema,
|
|
913
906
|
role: "client"
|
|
914
907
|
});
|
|
915
|
-
|
|
908
|
+
print.line(` \u2713 Connected as this computer (id: ${config.client.id})\n`);
|
|
916
909
|
if (options.service !== false && isServiceSupported()) {
|
|
917
910
|
const info = installClientService();
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
if (info.state === "active" && info.detail)
|
|
922
|
-
|
|
911
|
+
print.line(` \u2713 Installed as a background service (${info.platform}) — you can close this terminal\n\n`);
|
|
912
|
+
print.line(` Unit: ${info.unitPath}\n`);
|
|
913
|
+
print.line(` Logs: ${info.logDir}\n`);
|
|
914
|
+
if (info.state === "active" && info.detail) print.line(` State: running (${info.detail})\n`);
|
|
915
|
+
print.line("\n");
|
|
923
916
|
return;
|
|
924
917
|
}
|
|
925
|
-
if (options.service === false)
|
|
926
|
-
else
|
|
918
|
+
if (options.service === false) print.line(" (--no-service) running inline — Ctrl+C to stop\n");
|
|
919
|
+
else print.line(` Background service not supported on ${process.platform}; running inline.\n`);
|
|
927
920
|
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
928
921
|
const agents = loadAgents({
|
|
929
922
|
schema: agentConfigSchema,
|
|
@@ -941,7 +934,7 @@ function registerConnectCommand(parent) {
|
|
|
941
934
|
await runtime.start();
|
|
942
935
|
runtime.watchAgentsDir(agentsDir);
|
|
943
936
|
const shutdown = async () => {
|
|
944
|
-
|
|
937
|
+
print.line("\n Shutting down...\n");
|
|
945
938
|
runtime.unwatchAgentsDir();
|
|
946
939
|
await runtime.stop();
|
|
947
940
|
process.exit(0);
|
|
@@ -951,11 +944,11 @@ function registerConnectCommand(parent) {
|
|
|
951
944
|
await new Promise(() => {});
|
|
952
945
|
} catch (error) {
|
|
953
946
|
if (error.name === "ExitPromptError") {
|
|
954
|
-
|
|
947
|
+
print.line("\n Cancelled.\n");
|
|
955
948
|
return;
|
|
956
949
|
}
|
|
957
950
|
const msg = error instanceof Error ? error.message : String(error);
|
|
958
|
-
|
|
951
|
+
print.line(` Error: ${msg}\n`);
|
|
959
952
|
process.exit(1);
|
|
960
953
|
} finally {
|
|
961
954
|
resetConfig();
|
|
@@ -980,12 +973,13 @@ function registerClientCommands(program) {
|
|
|
980
973
|
role: "client"
|
|
981
974
|
});
|
|
982
975
|
applyClientLoggerConfig({ level: config.logLevel });
|
|
976
|
+
if (process.env.FIRST_TREE_HUB_SERVICE_MODE === "1") configureClientLoggerForService(join(DEFAULT_HOME_DIR, "logs"));
|
|
983
977
|
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
984
978
|
const agents = loadAgents({
|
|
985
979
|
schema: agentConfigSchema,
|
|
986
980
|
agentsDir
|
|
987
981
|
});
|
|
988
|
-
|
|
982
|
+
print.line(`\n Connecting to ${config.server.url} (client id: ${config.client.id})...\n`);
|
|
989
983
|
const managed = options.interactive === false;
|
|
990
984
|
const runtime = new ClientRuntime(config.server.url, config.client.id, {
|
|
991
985
|
currentVersion: COMMAND_VERSION,
|
|
@@ -999,7 +993,7 @@ function registerClientCommands(program) {
|
|
|
999
993
|
await runtime.start();
|
|
1000
994
|
runtime.watchAgentsDir(agentsDir);
|
|
1001
995
|
const shutdown = async () => {
|
|
1002
|
-
|
|
996
|
+
print.line("\n Shutting down...\n");
|
|
1003
997
|
runtime.unwatchAgentsDir();
|
|
1004
998
|
await runtime.stop();
|
|
1005
999
|
process.exit(0);
|
|
@@ -1009,7 +1003,7 @@ function registerClientCommands(program) {
|
|
|
1009
1003
|
await new Promise(() => {});
|
|
1010
1004
|
} catch (error) {
|
|
1011
1005
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1012
|
-
|
|
1006
|
+
print.line(` Error: ${msg}\n`);
|
|
1013
1007
|
process.exit(1);
|
|
1014
1008
|
} finally {
|
|
1015
1009
|
resetConfig();
|
|
@@ -1017,7 +1011,7 @@ function registerClientCommands(program) {
|
|
|
1017
1011
|
}
|
|
1018
1012
|
});
|
|
1019
1013
|
client.command("doctor").description("Check client environment readiness").action(async () => {
|
|
1020
|
-
|
|
1014
|
+
print.line("\n First Tree Hub Client Doctor\n\n");
|
|
1021
1015
|
printResults([
|
|
1022
1016
|
checkNodeVersion(),
|
|
1023
1017
|
checkClientConfig(),
|
|
@@ -1027,8 +1021,8 @@ function registerClientCommands(program) {
|
|
|
1027
1021
|
]);
|
|
1028
1022
|
});
|
|
1029
1023
|
client.command("stop").description("Stop the client (sends SIGTERM to running process)").action(() => {
|
|
1030
|
-
|
|
1031
|
-
|
|
1024
|
+
print.line(" Client stop: use Ctrl+C or `kill` the running process.\n");
|
|
1025
|
+
print.line(" Daemon mode with PID file is planned for a future release.\n");
|
|
1032
1026
|
});
|
|
1033
1027
|
client.command("status").description("Show client and agent connection status").action(() => {
|
|
1034
1028
|
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
@@ -1038,31 +1032,31 @@ function registerClientCommands(program) {
|
|
|
1038
1032
|
agentsDir
|
|
1039
1033
|
});
|
|
1040
1034
|
if (agents.size === 0) {
|
|
1041
|
-
|
|
1035
|
+
print.line(" No agents configured.\n");
|
|
1042
1036
|
return;
|
|
1043
1037
|
}
|
|
1044
|
-
|
|
1045
|
-
for (const [name, config] of agents)
|
|
1046
|
-
|
|
1038
|
+
print.line("\n Configured agents:\n\n");
|
|
1039
|
+
for (const [name, config] of agents) print.line(` ${name.padEnd(20)} runtime: ${config.runtime.padEnd(14)} agentId: ${config.agentId}\n`);
|
|
1040
|
+
print.line("\n");
|
|
1047
1041
|
} catch {
|
|
1048
|
-
|
|
1042
|
+
print.line(" No agents directory found.\n");
|
|
1049
1043
|
}
|
|
1050
1044
|
});
|
|
1051
1045
|
const service = client.command("service").description("Install/uninstall the background service that keeps this computer online");
|
|
1052
1046
|
service.command("install").description("Install as a background service — auto-starts on login/boot").action(() => {
|
|
1053
1047
|
if (!isServiceSupported()) {
|
|
1054
|
-
|
|
1048
|
+
print.line(` Background service is not supported on ${process.platform}.\n Run \`first-tree-hub client start\` manually to keep the computer online.
|
|
1055
1049
|
`);
|
|
1056
1050
|
process.exit(1);
|
|
1057
1051
|
}
|
|
1058
1052
|
try {
|
|
1059
1053
|
const info = installClientService();
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
if (info.state === "active")
|
|
1064
|
-
else
|
|
1065
|
-
|
|
1054
|
+
print.line(`\n \u2713 Installed as a background service (${info.platform}).\n`);
|
|
1055
|
+
print.line(` Unit: ${info.unitPath}\n`);
|
|
1056
|
+
print.line(` Logs: ${info.logDir}\n`);
|
|
1057
|
+
if (info.state === "active") print.line(` State: running${info.detail ? ` (${info.detail})` : ""}\n`);
|
|
1058
|
+
else print.line(` State: ${info.state}${info.detail ? ` (${info.detail})` : ""}\n`);
|
|
1059
|
+
print.line("\n You can close this terminal — the computer stays online.\n");
|
|
1066
1060
|
} catch (error) {
|
|
1067
1061
|
fail("SERVICE_INSTALL_ERROR", error instanceof Error ? error.message : String(error));
|
|
1068
1062
|
}
|
|
@@ -1070,26 +1064,40 @@ function registerClientCommands(program) {
|
|
|
1070
1064
|
service.command("status").description("Show background service state").action(() => {
|
|
1071
1065
|
const info = getClientServiceStatus();
|
|
1072
1066
|
if (info.platform === "unsupported") {
|
|
1073
|
-
|
|
1067
|
+
print.line(` Not supported on ${process.platform}.\n`);
|
|
1074
1068
|
return;
|
|
1075
1069
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1070
|
+
print.line(`\n ${info.platform}: ${info.label}\n`);
|
|
1071
|
+
print.line(` Unit: ${info.unitPath}\n`);
|
|
1072
|
+
print.line(` Logs: ${info.logDir}\n`);
|
|
1073
|
+
print.line(` State: ${info.state}${info.detail ? ` (${info.detail})` : ""}\n\n`);
|
|
1080
1074
|
});
|
|
1081
1075
|
service.command("uninstall").description("Stop and remove the background service").action(() => {
|
|
1082
1076
|
if (!isServiceSupported()) {
|
|
1083
|
-
|
|
1077
|
+
print.line(` Not supported on ${process.platform}.\n`);
|
|
1084
1078
|
return;
|
|
1085
1079
|
}
|
|
1086
1080
|
try {
|
|
1087
1081
|
const info = uninstallClientService();
|
|
1088
|
-
|
|
1082
|
+
print.line(`\n \u2713 Uninstalled background service (${info.platform}).\n\n`);
|
|
1089
1083
|
} catch (error) {
|
|
1090
1084
|
fail("SERVICE_UNINSTALL_ERROR", error instanceof Error ? error.message : String(error));
|
|
1091
1085
|
}
|
|
1092
1086
|
});
|
|
1087
|
+
service.command("logs").description("Read background-service logs (pretty by default)").option("-f, --tail", "follow new lines as they arrive (Ctrl+C to stop)", false).option("--since <duration>", "only show records newer than duration (e.g. 10s, 5m, 2h, 1d)").option("--level <level>", "minimum level (trace|debug|info|warn|error|fatal)").option("--json", "emit raw NDJSON lines instead of pretty formatting", false).action(async (options) => {
|
|
1088
|
+
try {
|
|
1089
|
+
const level = validateLevel(options.level);
|
|
1090
|
+
const sinceMs = options.since ? parseDuration(options.since) : void 0;
|
|
1091
|
+
await showServiceLogs({
|
|
1092
|
+
tail: options.tail === true,
|
|
1093
|
+
level,
|
|
1094
|
+
sinceMs,
|
|
1095
|
+
json: options.json === true
|
|
1096
|
+
});
|
|
1097
|
+
} catch (error) {
|
|
1098
|
+
fail("SERVICE_LOGS_ERROR", error instanceof Error ? error.message : String(error));
|
|
1099
|
+
}
|
|
1100
|
+
});
|
|
1093
1101
|
client.command("hub-list").description("List clients on the Hub server").option("--server <url>", "Hub server URL").action(async (options) => {
|
|
1094
1102
|
try {
|
|
1095
1103
|
const serverUrl = resolveServerUrl(options.server);
|
|
@@ -1101,18 +1109,18 @@ function registerClientCommands(program) {
|
|
|
1101
1109
|
if (!response.ok) fail("FETCH_ERROR", `Server returned ${response.status}`, 1);
|
|
1102
1110
|
const clients = await response.json();
|
|
1103
1111
|
if (clients.length === 0) {
|
|
1104
|
-
|
|
1112
|
+
print.line(" No clients.\n");
|
|
1105
1113
|
return;
|
|
1106
1114
|
}
|
|
1107
|
-
|
|
1115
|
+
print.line(`\n Clients: ${clients.length}\n\n`);
|
|
1108
1116
|
const header = ` ${"CLIENT".padEnd(20)} ${"HOST".padEnd(25)} ${"AGENTS".padEnd(8)} CONNECTED`;
|
|
1109
|
-
|
|
1110
|
-
|
|
1117
|
+
print.line(`${header}\n`);
|
|
1118
|
+
print.line(` ${"─".repeat(header.length - 2)}\n`);
|
|
1111
1119
|
for (const c of clients) {
|
|
1112
1120
|
const since = c.connectedAt ? timeSince(c.connectedAt) : "—";
|
|
1113
|
-
|
|
1121
|
+
print.line(` ${c.id.padEnd(20)} ${(c.hostname ?? "—").padEnd(25)} ${String(c.agentCount).padEnd(8)} ${since}\n`);
|
|
1114
1122
|
}
|
|
1115
|
-
|
|
1123
|
+
print.line("\n");
|
|
1116
1124
|
} catch (error) {
|
|
1117
1125
|
fail("CLIENT_LIST_ERROR", error instanceof Error ? error.message : String(error));
|
|
1118
1126
|
}
|
|
@@ -1127,7 +1135,7 @@ function registerClientCommands(program) {
|
|
|
1127
1135
|
signal: AbortSignal.timeout(1e4)
|
|
1128
1136
|
});
|
|
1129
1137
|
if (!response.ok) fail("DISCONNECT_ERROR", `Server returned ${response.status}`, 1);
|
|
1130
|
-
|
|
1138
|
+
print.line(` Client "${clientId}" disconnected.\n`);
|
|
1131
1139
|
} catch (error) {
|
|
1132
1140
|
fail("DISCONNECT_ERROR", error instanceof Error ? error.message : String(error));
|
|
1133
1141
|
}
|
|
@@ -1170,10 +1178,10 @@ function registerConfigCommands(program) {
|
|
|
1170
1178
|
schema: flags.client ? clientConfigSchema : serverConfigSchema,
|
|
1171
1179
|
role: flags.client ? "client" : "server"
|
|
1172
1180
|
});
|
|
1173
|
-
|
|
1181
|
+
print.line("\n Configuration saved.\n");
|
|
1174
1182
|
} catch (error) {
|
|
1175
1183
|
if (error.name === "ExitPromptError") {
|
|
1176
|
-
|
|
1184
|
+
print.line("\n Cancelled.\n");
|
|
1177
1185
|
return;
|
|
1178
1186
|
}
|
|
1179
1187
|
throw error;
|
|
@@ -1186,27 +1194,27 @@ function registerConfigCommands(program) {
|
|
|
1186
1194
|
else if (value === "false") parsed = false;
|
|
1187
1195
|
else if (/^\d+$/.test(value)) parsed = Number(value);
|
|
1188
1196
|
setConfigValue(path, key, parsed);
|
|
1189
|
-
|
|
1197
|
+
print.line(` Set ${key} in ${path}\n`);
|
|
1190
1198
|
});
|
|
1191
1199
|
addScopeOptions(config.command("get").description("Get a config value")).argument("<key>", "Config key (dot notation)").option("--show-secrets", "Show secret values in plaintext").action((key, flags) => {
|
|
1192
1200
|
const { path, schema } = resolveConfigPath(flags);
|
|
1193
1201
|
const value = getConfigValue(path, key);
|
|
1194
|
-
if (value === void 0)
|
|
1202
|
+
if (value === void 0) print.line(` ${key}: (not set)\n`);
|
|
1195
1203
|
else {
|
|
1196
1204
|
const display = isSecretField(schema, key) && !flags.showSecrets ? "***" : String(value);
|
|
1197
|
-
|
|
1205
|
+
print.line(` ${key}: ${display}\n`);
|
|
1198
1206
|
}
|
|
1199
1207
|
});
|
|
1200
1208
|
addScopeOptions(config.command("list").description("List all config values")).option("--show-secrets", "Show secret values in plaintext").action((flags) => {
|
|
1201
1209
|
const { path, schema } = resolveConfigPath(flags);
|
|
1202
1210
|
const values = readConfigFile(path);
|
|
1203
1211
|
if (Object.keys(values).length === 0) {
|
|
1204
|
-
|
|
1212
|
+
print.line(` No config found at ${path}\n`);
|
|
1205
1213
|
return;
|
|
1206
1214
|
}
|
|
1207
|
-
|
|
1215
|
+
print.line(`\n Config: ${path}\n\n`);
|
|
1208
1216
|
printFlat(values, schema, "", flags.showSecrets ?? false);
|
|
1209
|
-
|
|
1217
|
+
print.line("\n");
|
|
1210
1218
|
});
|
|
1211
1219
|
}
|
|
1212
1220
|
function printFlat(obj, schema, prefix, showSecrets) {
|
|
@@ -1215,7 +1223,7 @@ function printFlat(obj, schema, prefix, showSecrets) {
|
|
|
1215
1223
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) printFlat(value, schema, fullKey, showSecrets);
|
|
1216
1224
|
else {
|
|
1217
1225
|
const display = isSecretField(schema, fullKey) && !showSecrets ? "***" : String(value);
|
|
1218
|
-
|
|
1226
|
+
print.line(` ${fullKey.padEnd(30)} ${display}\n`);
|
|
1219
1227
|
}
|
|
1220
1228
|
}
|
|
1221
1229
|
}
|
|
@@ -1240,13 +1248,13 @@ function isSecretField(schema, dotPath) {
|
|
|
1240
1248
|
//#region src/commands/onboard.ts
|
|
1241
1249
|
async function promptMissing(args) {
|
|
1242
1250
|
if (!args.server) try {
|
|
1243
|
-
const { resolveServerUrl } = await import("../bootstrap-
|
|
1251
|
+
const { resolveServerUrl } = await import("../bootstrap-DWifXj9b.mjs").then((n) => n.t);
|
|
1244
1252
|
resolveServerUrl();
|
|
1245
1253
|
} catch {
|
|
1246
1254
|
args.server = await input({ message: "Hub server URL:" });
|
|
1247
1255
|
saveOnboardState(args);
|
|
1248
1256
|
}
|
|
1249
|
-
const { loadCredentials } = await import("../bootstrap-
|
|
1257
|
+
const { loadCredentials } = await import("../bootstrap-DWifXj9b.mjs").then((n) => n.t);
|
|
1250
1258
|
if (!loadCredentials()) throw new Error("No saved credentials. Run `first-tree-hub client connect <server-url>` before onboarding.");
|
|
1251
1259
|
if (!args.id) {
|
|
1252
1260
|
args.id = await input({ message: "Agent ID:" });
|
|
@@ -1345,7 +1353,7 @@ function registerOnboardCommand(program) {
|
|
|
1345
1353
|
if (args.check) {
|
|
1346
1354
|
const items = await onboardCheck(args);
|
|
1347
1355
|
const report = formatCheckReport(items);
|
|
1348
|
-
|
|
1356
|
+
print.line(`\nOnboard Check: ${args.id ?? "(no id)"}\n\n${report}\n\n`);
|
|
1349
1357
|
if (items.some((i) => i.status === "missing_required" || i.status === "error")) process.exit(1);
|
|
1350
1358
|
return;
|
|
1351
1359
|
}
|
|
@@ -1353,14 +1361,14 @@ function registerOnboardCommand(program) {
|
|
|
1353
1361
|
const items = await onboardCheck(args);
|
|
1354
1362
|
if (items.some((i) => i.status === "missing_required" || i.status === "error")) {
|
|
1355
1363
|
const report = formatCheckReport(items);
|
|
1356
|
-
|
|
1364
|
+
print.line(`\nOnboard Check: ${args.id ?? "(no id)"}\n\n${report}\n\n`);
|
|
1357
1365
|
fail("MISSING_PARAMS", "Required parameters are missing. See checklist above.");
|
|
1358
1366
|
}
|
|
1359
1367
|
await onboardCreate(args);
|
|
1360
1368
|
} catch (error) {
|
|
1361
1369
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1362
1370
|
if (isInteractive()) {
|
|
1363
|
-
|
|
1371
|
+
print.line(`\n\u274C ${msg}\n\n`);
|
|
1364
1372
|
process.exit(1);
|
|
1365
1373
|
}
|
|
1366
1374
|
fail("ONBOARD_ERROR", msg);
|
|
@@ -1379,22 +1387,22 @@ function registerServerCommands(program) {
|
|
|
1379
1387
|
});
|
|
1380
1388
|
} catch (error) {
|
|
1381
1389
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1382
|
-
|
|
1390
|
+
print.line(`\n Error: ${msg}\n\n`);
|
|
1383
1391
|
process.exit(1);
|
|
1384
1392
|
}
|
|
1385
1393
|
});
|
|
1386
1394
|
server.command("stop").description("Stop the managed PostgreSQL container").action(() => {
|
|
1387
1395
|
try {
|
|
1388
|
-
if (stopPostgres())
|
|
1389
|
-
else
|
|
1396
|
+
if (stopPostgres()) print.line(" PostgreSQL container stopped.\n");
|
|
1397
|
+
else print.line(" No managed PostgreSQL container found.\n");
|
|
1390
1398
|
} catch (error) {
|
|
1391
1399
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1392
|
-
|
|
1400
|
+
print.line(` Error stopping PostgreSQL: ${msg}\n`);
|
|
1393
1401
|
process.exit(1);
|
|
1394
1402
|
}
|
|
1395
1403
|
});
|
|
1396
1404
|
server.command("doctor").description("Check server environment readiness").action(async () => {
|
|
1397
|
-
|
|
1405
|
+
print.line("\n First Tree Hub Server Doctor\n\n");
|
|
1398
1406
|
printResults([
|
|
1399
1407
|
checkNodeVersion(),
|
|
1400
1408
|
checkDocker(),
|
|
@@ -1409,13 +1417,13 @@ function registerServerCommands(program) {
|
|
|
1409
1417
|
const res = await fetch(`${url}/api/v1/health`);
|
|
1410
1418
|
if (res.ok) {
|
|
1411
1419
|
const data = await res.json();
|
|
1412
|
-
|
|
1420
|
+
process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
|
|
1413
1421
|
} else {
|
|
1414
|
-
|
|
1422
|
+
print.line(` Server returned ${res.status}\n`);
|
|
1415
1423
|
process.exit(1);
|
|
1416
1424
|
}
|
|
1417
1425
|
} catch {
|
|
1418
|
-
|
|
1426
|
+
print.line(` Cannot connect to ${url}\n`);
|
|
1419
1427
|
process.exit(1);
|
|
1420
1428
|
}
|
|
1421
1429
|
});
|
|
@@ -1425,10 +1433,10 @@ function registerServerCommands(program) {
|
|
|
1425
1433
|
schema: serverConfigSchema,
|
|
1426
1434
|
role: "server"
|
|
1427
1435
|
})).database.url);
|
|
1428
|
-
|
|
1436
|
+
print.line(` Migrations complete (${tableCount} tables)\n`);
|
|
1429
1437
|
} catch (error) {
|
|
1430
1438
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1431
|
-
|
|
1439
|
+
print.line(` Error: ${msg}\n`);
|
|
1432
1440
|
process.exit(1);
|
|
1433
1441
|
}
|
|
1434
1442
|
});
|
|
@@ -1438,11 +1446,11 @@ function registerServerCommands(program) {
|
|
|
1438
1446
|
schema: serverConfigSchema,
|
|
1439
1447
|
role: "server"
|
|
1440
1448
|
})).database.url, options.username, options.org, options.name, options.password);
|
|
1441
|
-
|
|
1442
|
-
if (!options.password)
|
|
1449
|
+
print.line(` Admin user "${result.username}" created.\n`);
|
|
1450
|
+
if (!options.password) print.line(` Password: ${result.password} (save this — shown only once)\n`);
|
|
1443
1451
|
} catch (error) {
|
|
1444
1452
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1445
|
-
|
|
1453
|
+
print.line(` Error: ${msg}\n`);
|
|
1446
1454
|
process.exit(1);
|
|
1447
1455
|
}
|
|
1448
1456
|
});
|
|
@@ -1451,7 +1459,7 @@ function registerServerCommands(program) {
|
|
|
1451
1459
|
//#region src/commands/status.ts
|
|
1452
1460
|
function registerStatusCommand(program) {
|
|
1453
1461
|
program.command("status").description("Global overview — server health + configured agents").action(async () => {
|
|
1454
|
-
|
|
1462
|
+
print.line("\n");
|
|
1455
1463
|
const serverConfig = readConfigFile(join(DEFAULT_CONFIG_DIR, "server.yaml"));
|
|
1456
1464
|
const serverPort = getNestedValue(serverConfig, "server.port") ?? 8e3;
|
|
1457
1465
|
const serverUrl = `http://${getNestedValue(serverConfig, "server.host") ?? "127.0.0.1"}:${serverPort}`;
|
|
@@ -1460,31 +1468,31 @@ function registerStatusCommand(program) {
|
|
|
1460
1468
|
if (res.ok) {
|
|
1461
1469
|
const data = await res.json();
|
|
1462
1470
|
const uptime = data.uptime_seconds ? formatUptime(data.uptime_seconds) : "unknown";
|
|
1463
|
-
|
|
1464
|
-
} else
|
|
1471
|
+
print.line(` Server: ✓ running (${serverUrl}, uptime: ${uptime})\n`);
|
|
1472
|
+
} else print.line(` Server: ✗ unhealthy (${res.status})\n`);
|
|
1465
1473
|
} catch {
|
|
1466
|
-
|
|
1474
|
+
print.line(` Server: ✗ not running (${serverUrl})\n`);
|
|
1467
1475
|
}
|
|
1468
1476
|
const dbProvider = getNestedValue(serverConfig, "database.provider") ?? "unknown";
|
|
1469
1477
|
const hasDbUrl = getNestedValue(serverConfig, "database.url") !== void 0;
|
|
1470
|
-
|
|
1478
|
+
print.line(` Database: ${hasDbUrl ? "✓ configured" : "✗ not configured"} (${dbProvider})\n`);
|
|
1471
1479
|
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
1472
1480
|
if (existsSync(agentsDir)) try {
|
|
1473
1481
|
const agents = loadAgents({
|
|
1474
1482
|
schema: agentConfigSchema,
|
|
1475
1483
|
agentsDir
|
|
1476
1484
|
});
|
|
1477
|
-
|
|
1485
|
+
print.line(` Agents: ${agents.size} configured\n`);
|
|
1478
1486
|
} catch {
|
|
1479
|
-
|
|
1487
|
+
print.line(" Agents: error reading config\n");
|
|
1480
1488
|
}
|
|
1481
|
-
else
|
|
1489
|
+
else print.line(" Agents: 0 configured\n");
|
|
1482
1490
|
const clientConfigPath = join(DEFAULT_CONFIG_DIR, "client.yaml");
|
|
1483
1491
|
if (existsSync(clientConfigPath)) {
|
|
1484
1492
|
const clientServerUrl = getNestedValue(readConfigFile(clientConfigPath), "server.url");
|
|
1485
|
-
|
|
1486
|
-
} else
|
|
1487
|
-
|
|
1493
|
+
print.line(` Client: configured → ${clientServerUrl}\n`);
|
|
1494
|
+
} else print.line(" Client: not configured\n");
|
|
1495
|
+
print.line("\n");
|
|
1488
1496
|
});
|
|
1489
1497
|
}
|
|
1490
1498
|
function getNestedValue(obj, dotPath) {
|
|
@@ -1508,7 +1516,21 @@ function formatUptime(seconds) {
|
|
|
1508
1516
|
//#region src/cli/index.ts
|
|
1509
1517
|
runHomeMigration();
|
|
1510
1518
|
const program = new Command();
|
|
1511
|
-
program.name("first-tree-hub").description("First Tree Hub — centralized collaboration platform for agent teams").version(COMMAND_VERSION);
|
|
1519
|
+
program.name("first-tree-hub").description("First Tree Hub — centralized collaboration platform for agent teams").version(COMMAND_VERSION).option("--json", "emit only machine-readable JSON on stdout; silence human status lines on stderr").option("--verbose", "raise log level to debug (overrides FIRST_TREE_HUB_LOG_LEVEL)").hook("preAction", (thisCommand) => {
|
|
1520
|
+
const opts = thisCommand.optsWithGlobals();
|
|
1521
|
+
const json = opts.json === true || process.env.FIRST_TREE_HUB_JSON === "1";
|
|
1522
|
+
setJsonMode(json);
|
|
1523
|
+
if (opts.verbose) applyClientLoggerConfig({
|
|
1524
|
+
level: "debug",
|
|
1525
|
+
explicit: true
|
|
1526
|
+
});
|
|
1527
|
+
else if (process.env.FIRST_TREE_HUB_LOG_LEVEL) applyClientLoggerConfig({ explicit: true });
|
|
1528
|
+
else if (json) applyClientLoggerConfig({
|
|
1529
|
+
level: "error",
|
|
1530
|
+
explicit: true
|
|
1531
|
+
});
|
|
1532
|
+
else applyClientLoggerConfig({ level: "warn" });
|
|
1533
|
+
});
|
|
1512
1534
|
registerServerCommands(program);
|
|
1513
1535
|
registerClientCommands(program);
|
|
1514
1536
|
registerAgentCommands(program);
|