@agent-team-foundation/first-tree-hub 0.9.10 → 0.9.11
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-hh_PkTu6.mjs → bootstrap-CtVqQA8a.mjs} +1 -1
- package/dist/cli/index.mjs +9 -64
- package/dist/{core-BWaSYpXv.mjs → core-CuSIXoof.mjs} +563 -660
- package/dist/{feishu-BJaN64iR.mjs → feishu-B2sjp6Z6.mjs} +26 -3
- package/dist/index.mjs +5 -5
- package/dist/{logger-core-BTmvdflj-DhdipBkV.mjs → logger-core-BTmvdflj-DjW8FM4T.mjs} +1 -1
- package/dist/{observability-hDEdrmMS.mjs → observability-DDkJwSKv.mjs} +2 -2
- package/dist/{observability-DV_fQKqV-CuLWzBxQ.mjs → observability-DV_fQKqV-oxfXX6Z2.mjs} +1 -1
- package/dist/web/assets/index-DStPeqrX.css +1 -0
- package/dist/web/assets/index-Dwp1u5SF.js +371 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-Cyvhyw0R.js +0 -361
- package/dist/web/assets/index-DEwlT6PE.css +0 -1
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { m as __toESM } from "./esm-CYu4tXXn.mjs";
|
|
2
|
-
import { _ as withSpan, a as endWsConnectionSpan, b as require_pino, c as messageAttrs, d as rootLogger$1, g as startWsConnectionSpan, i as currentTraceId, n as applyLoggerConfig, o as getFastifyOtelPlugin, p as setWsConnectionAttrs, r as createLogger$1, t as adapterAttrs, u as observabilityPlugin, v as withWsMessageSpan, y as FIRST_TREE_HUB_ATTR } from "./observability-DV_fQKqV-
|
|
3
|
-
import {
|
|
4
|
-
import { C as
|
|
5
|
-
import { $ as sessionStateMessageSchema, A as createMemberSchema, B as loginSchema, C as agentTypeSchema$1, D as createAdapterMappingSchema, E as createAdapterConfigSchema, F as extractMentions, G as runtimeStateMessageSchema, H as notificationQuerySchema, I as imageInlineContentSchema, J as sendToAgentSchema, K as selfServiceFeishuBotSchema, L as inboxPollQuerySchema, M as createTaskSchema, N as delegateFeishuUserSchema, O as createAgentSchema, P as dryRunAgentRuntimeConfigSchema, Q as sessionReconcileRequestSchema, R as isRedactedEnvValue, S as agentRuntimeConfigPayloadSchema$1, T as connectTokenExchangeSchema, U as paginationQuerySchema, V as messageSourceSchema$1, W as refreshTokenSchema, X as sessionEventMessageSchema, Y as sessionCompletionMessageSchema, Z as sessionEventSchema$1, _ as addParticipantSchema, a as AGENT_SELECTOR_HEADER$1, at as updateMemberSchema, b as agentBindRequestSchema, c as AGENT_TYPES, ct as updateTaskStatusSchema, d as SYSTEM_CONFIG_DEFAULTS, et as taskListQuerySchema, f as TASK_CREATOR_TYPES, g as WS_AUTH_FRAME_TIMEOUT_MS, h as TASK_TERMINAL_STATUSES, i as AGENT_BIND_REJECT_REASONS, it as updateChatSchema, j as createOrganizationSchema, k as createChatSchema, l as AGENT_VISIBILITY, lt as wsAuthFrameSchema, m as TASK_STATUSES, nt as updateAgentRuntimeConfigSchema, o as AGENT_SOURCES, ot as updateOrganizationSchema, p as TASK_HEALTH_SIGNALS, q as sendMessageSchema, rt as updateAgentSchema, s as AGENT_STATUSES, st as updateSystemConfigSchema, tt as updateAdapterConfigSchema, u as DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD, v as adminCreateTaskSchema, w as clientRegisterSchema, x as agentPinnedMessageSchema$1, y as adminUpdateTaskSchema, z as linkTaskChatSchema } from "./feishu-BJaN64iR.mjs";
|
|
2
|
+
import { _ as withSpan, a as endWsConnectionSpan, b as require_pino, c as messageAttrs, d as rootLogger$1, g as startWsConnectionSpan, i as currentTraceId, n as applyLoggerConfig, o as getFastifyOtelPlugin, p as setWsConnectionAttrs, r as createLogger$1, t as adapterAttrs, u as observabilityPlugin, v as withWsMessageSpan, y as FIRST_TREE_HUB_ATTR } from "./observability-DV_fQKqV-oxfXX6Z2.mjs";
|
|
3
|
+
import { C as serverConfigSchema, S as resolveConfigReadonly, _ as loadAgents, d as DEFAULT_HOME_DIR$1, f as agentConfigSchema, g as initConfig, i as loadCredentials, l as DEFAULT_CONFIG_DIR, m as collectMissingPrompts, n as ensureFreshAccessToken, o as resolveServerUrl, p as clientConfigSchema, s as saveAgentConfig, u as DEFAULT_DATA_DIR$1, v as migrateLegacyHome, w as setConfigValue } from "./bootstrap-CtVqQA8a.mjs";
|
|
4
|
+
import { $ as sessionEventSchema$1, A as createChatSchema, B as isReservedAgentName$1, C as agentRuntimeConfigPayloadSchema$1, D as createAdapterConfigSchema, E as connectTokenExchangeSchema, F as dryRunAgentRuntimeConfigSchema, G as paginationQuerySchema, H as loginSchema, I as extractMentions, J as selfServiceFeishuBotSchema, K as refreshTokenSchema, L as imageInlineContentSchema, M as createOrganizationSchema, N as createTaskSchema, O as createAdapterMappingSchema, P as delegateFeishuUserSchema, Q as sessionEventMessageSchema, R as inboxPollQuerySchema, S as agentPinnedMessageSchema$1, T as clientRegisterSchema, U as messageSourceSchema$1, V as linkTaskChatSchema, W as notificationQuerySchema, X as sendToAgentSchema, Y as sendMessageSchema, Z as sessionCompletionMessageSchema, _ as WS_AUTH_FRAME_TIMEOUT_MS, a as AGENT_NAME_REGEX$1, at as updateAgentSchema, b as adminUpdateTaskSchema, c as AGENT_STATUSES, ct as updateOrganizationSchema, d as DEFAULT_AGENT_RUNTIME_CONFIG_PAYLOAD, dt as wsAuthFrameSchema, et as sessionReconcileRequestSchema, f as SYSTEM_CONFIG_DEFAULTS, g as TASK_TERMINAL_STATUSES, h as TASK_STATUSES, i as AGENT_BIND_REJECT_REASONS, it as updateAgentRuntimeConfigSchema, j as createMemberSchema, k as createAgentSchema, l as AGENT_TYPES, lt as updateSystemConfigSchema, m as TASK_HEALTH_SIGNALS, nt as taskListQuerySchema, o as AGENT_SELECTOR_HEADER$1, ot as updateChatSchema, p as TASK_CREATOR_TYPES, q as runtimeStateMessageSchema, rt as updateAdapterConfigSchema, s as AGENT_SOURCES, st as updateMemberSchema, tt as sessionStateMessageSchema, u as AGENT_VISIBILITY, ut as updateTaskStatusSchema, v as addParticipantSchema, w as agentTypeSchema$1, x as agentBindRequestSchema, y as adminCreateTaskSchema, z as isRedactedEnvValue } from "./feishu-B2sjp6Z6.mjs";
|
|
6
5
|
import { createRequire } from "node:module";
|
|
7
6
|
import { ZodError, z } from "zod";
|
|
8
7
|
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
9
8
|
import { Writable } from "node:stream";
|
|
10
9
|
import { homedir, hostname, platform, tmpdir, userInfo } from "node:os";
|
|
11
10
|
import { EventEmitter } from "node:events";
|
|
12
|
-
import { closeSync, copyFileSync,
|
|
11
|
+
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
13
12
|
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
14
13
|
import WebSocket from "ws";
|
|
15
14
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
@@ -32,7 +31,6 @@ import Fastify from "fastify";
|
|
|
32
31
|
import { bigserial, boolean, index, integer, jsonb, pgTable, primaryKey, serial, text, timestamp, unique, uniqueIndex } from "drizzle-orm/pg-core";
|
|
33
32
|
import { SignJWT, jwtVerify } from "jose";
|
|
34
33
|
import { Client, EventDispatcher, LoggerLevel, WSClient } from "@larksuiteoapi/node-sdk";
|
|
35
|
-
import { createInterface } from "node:readline";
|
|
36
34
|
//#region ../client/dist/observability-B4kO005X.mjs
|
|
37
35
|
var import_pino = /* @__PURE__ */ __toESM(require_pino(), 1);
|
|
38
36
|
/**
|
|
@@ -430,8 +428,31 @@ const agentTypeSchema = z.enum([
|
|
|
430
428
|
const agentVisibilitySchema = z.enum(["private", "organization"]);
|
|
431
429
|
const agentSourceSchema = z.enum(["admin-api", "portal"]);
|
|
432
430
|
z.enum(["active", "suspended"]);
|
|
431
|
+
/**
|
|
432
|
+
* Agent-name rules (see docs/agent-naming-design.md §3.1):
|
|
433
|
+
* - Lowercase ASCII slug, hyphens + underscores allowed.
|
|
434
|
+
* - Must start with alphanumeric: `-` / `_` as first char collide with
|
|
435
|
+
* CLI flag parsing and markdown list syntax.
|
|
436
|
+
* - 1–64 chars — aligned with `MENTION_REGEX` so any valid name can be
|
|
437
|
+
* @-mentioned in chat. Older rows created under the previous 1–100
|
|
438
|
+
* regex are grandfathered; the tight rule only gates new creates.
|
|
439
|
+
*/
|
|
440
|
+
const AGENT_NAME_REGEX = /^[a-z0-9][a-z0-9_-]{0,63}$/;
|
|
441
|
+
const RESERVED_AGENT_NAMES_SET = new Set([
|
|
442
|
+
"admin",
|
|
443
|
+
"agent",
|
|
444
|
+
"first-tree",
|
|
445
|
+
"hub",
|
|
446
|
+
"me",
|
|
447
|
+
"null",
|
|
448
|
+
"system",
|
|
449
|
+
"undefined"
|
|
450
|
+
]);
|
|
451
|
+
function isReservedAgentName(name) {
|
|
452
|
+
return RESERVED_AGENT_NAMES_SET.has(name);
|
|
453
|
+
}
|
|
433
454
|
z.object({
|
|
434
|
-
name: z.string().min(1).max(
|
|
455
|
+
name: z.string().min(1).max(64).regex(AGENT_NAME_REGEX, "Must start with a letter or digit and contain only lowercase letters, digits, hyphens (-), and underscores (_). Max 64 chars.").refine((n) => !isReservedAgentName(n), { message: "That agent name is reserved — pick a different one." }).optional(),
|
|
435
456
|
type: agentTypeSchema,
|
|
436
457
|
displayName: z.string().max(200).optional(),
|
|
437
458
|
delegateMention: z.string().max(100).optional(),
|
|
@@ -5373,386 +5394,84 @@ function getContainerPassword() {
|
|
|
5373
5394
|
throw new Error("Cannot determine PostgreSQL password from container");
|
|
5374
5395
|
}
|
|
5375
5396
|
//#endregion
|
|
5376
|
-
//#region src/core/
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5385
|
-
|
|
5386
|
-
|
|
5397
|
+
//#region src/core/service-install.ts
|
|
5398
|
+
/**
|
|
5399
|
+
* Run a subprocess capturing stderr so failures surface a meaningful error
|
|
5400
|
+
* instead of Node's opaque "Command failed". Used for launchctl/systemctl —
|
|
5401
|
+
* anywhere the stderr message is diagnostically crucial.
|
|
5402
|
+
*/
|
|
5403
|
+
function runCapture(program, args, timeoutMs) {
|
|
5404
|
+
const res = spawnSync(program, args, {
|
|
5405
|
+
encoding: "utf-8",
|
|
5406
|
+
timeout: timeoutMs,
|
|
5407
|
+
stdio: [
|
|
5408
|
+
"ignore",
|
|
5409
|
+
"pipe",
|
|
5410
|
+
"pipe"
|
|
5411
|
+
]
|
|
5387
5412
|
});
|
|
5388
|
-
}
|
|
5389
|
-
function get(obj, dotPath) {
|
|
5390
|
-
const parts = dotPath.split(".");
|
|
5391
|
-
let current = obj;
|
|
5392
|
-
for (const part of parts) {
|
|
5393
|
-
if (current === null || current === void 0 || typeof current !== "object") return void 0;
|
|
5394
|
-
current = current[part];
|
|
5395
|
-
}
|
|
5396
|
-
return current;
|
|
5397
|
-
}
|
|
5398
|
-
function checkNodeVersion() {
|
|
5399
|
-
const version = process.versions.node;
|
|
5400
|
-
const [major] = version.split(".").map(Number);
|
|
5401
|
-
const ok = major !== void 0 && major >= 22;
|
|
5402
|
-
return {
|
|
5403
|
-
label: "Node.js",
|
|
5404
|
-
ok,
|
|
5405
|
-
detail: ok ? `v${version}` : `v${version} (requires >= 22.16)`
|
|
5406
|
-
};
|
|
5407
|
-
}
|
|
5408
|
-
function checkDocker() {
|
|
5409
|
-
try {
|
|
5410
|
-
return {
|
|
5411
|
-
label: "Docker",
|
|
5412
|
-
ok: true,
|
|
5413
|
-
detail: execFileSync("docker", ["--version"], {
|
|
5414
|
-
encoding: "utf-8",
|
|
5415
|
-
timeout: 5e3
|
|
5416
|
-
}).trim().replace("Docker version ", "v").split(",")[0] ?? ""
|
|
5417
|
-
};
|
|
5418
|
-
} catch {
|
|
5419
|
-
return {
|
|
5420
|
-
label: "Docker",
|
|
5421
|
-
ok: false,
|
|
5422
|
-
detail: "not found (optional — needed for auto PG provisioning)"
|
|
5423
|
-
};
|
|
5424
|
-
}
|
|
5425
|
-
}
|
|
5426
|
-
function checkServerConfig() {
|
|
5427
|
-
const hasFile = existsSync(join(DEFAULT_CONFIG_DIR, "server.yaml"));
|
|
5428
|
-
const hasEnv = !!process.env.FIRST_TREE_HUB_DATABASE_URL;
|
|
5429
|
-
if (hasFile && hasEnv) return {
|
|
5430
|
-
label: "Config",
|
|
5431
|
-
ok: true,
|
|
5432
|
-
detail: "config file + env vars"
|
|
5433
|
-
};
|
|
5434
|
-
if (hasFile) return {
|
|
5435
|
-
label: "Config",
|
|
5436
|
-
ok: true,
|
|
5437
|
-
detail: join(DEFAULT_CONFIG_DIR, "server.yaml")
|
|
5438
|
-
};
|
|
5439
|
-
if (hasEnv) return {
|
|
5440
|
-
label: "Config",
|
|
5441
|
-
ok: true,
|
|
5442
|
-
detail: "via environment variables"
|
|
5443
|
-
};
|
|
5413
|
+
if (res.status === 0) return { ok: true };
|
|
5444
5414
|
return {
|
|
5445
|
-
label: "Config",
|
|
5446
5415
|
ok: false,
|
|
5447
|
-
|
|
5416
|
+
stderr: (res.stderr ?? "").trim(),
|
|
5417
|
+
code: res.status
|
|
5448
5418
|
};
|
|
5449
5419
|
}
|
|
5450
|
-
|
|
5451
|
-
const
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5420
|
+
function sleepSync(ms) {
|
|
5421
|
+
const shared = new Int32Array(new SharedArrayBuffer(4));
|
|
5422
|
+
Atomics.wait(shared, 0, 0, ms);
|
|
5423
|
+
}
|
|
5424
|
+
const LAUNCHD_LABEL = "dev.first-tree-hub.client";
|
|
5425
|
+
const SYSTEMD_UNIT = "first-tree-hub-client.service";
|
|
5426
|
+
const LOG_DIR = join(DEFAULT_HOME_DIR$1, "logs");
|
|
5427
|
+
function whichBin(name) {
|
|
5457
5428
|
try {
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
await sql.unsafe("SELECT 1");
|
|
5465
|
-
await sql.end();
|
|
5466
|
-
return {
|
|
5467
|
-
label: "Database",
|
|
5468
|
-
ok: true,
|
|
5469
|
-
detail: "connected"
|
|
5470
|
-
};
|
|
5471
|
-
} catch (err) {
|
|
5472
|
-
return {
|
|
5473
|
-
label: "Database",
|
|
5474
|
-
ok: false,
|
|
5475
|
-
detail: `unreachable — ${(err instanceof Error ? err.message : String(err)).slice(0, 80)}`
|
|
5476
|
-
};
|
|
5429
|
+
return execFileSync(process.platform === "win32" ? "where" : "which", [name], {
|
|
5430
|
+
encoding: "utf-8",
|
|
5431
|
+
timeout: 3e3
|
|
5432
|
+
}).split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0] ?? null;
|
|
5433
|
+
} catch {
|
|
5434
|
+
return null;
|
|
5477
5435
|
}
|
|
5478
5436
|
}
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
};
|
|
5437
|
+
/**
|
|
5438
|
+
* Resolve how the service should launch the CLI.
|
|
5439
|
+
*
|
|
5440
|
+
* Prefers the installed `first-tree-hub` bin on PATH (usually a shim under
|
|
5441
|
+
* /usr/local/bin or ~/.npm-global/bin). Falls back to invoking the current
|
|
5442
|
+
* Node interpreter against the running script (handles `pnpm dev`, tsx, and
|
|
5443
|
+
* dev-only global installs).
|
|
5444
|
+
*/
|
|
5445
|
+
function resolveCliInvocation() {
|
|
5446
|
+
const bin = whichBin("first-tree-hub");
|
|
5447
|
+
if (bin && isAbsolute(bin)) try {
|
|
5491
5448
|
return {
|
|
5492
|
-
|
|
5493
|
-
|
|
5494
|
-
detail: `unhealthy (HTTP ${res.status}) at ${host}:${port}`
|
|
5449
|
+
kind: "bin",
|
|
5450
|
+
program: realpathSync(bin)
|
|
5495
5451
|
};
|
|
5496
5452
|
} catch {
|
|
5497
5453
|
return {
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
detail: `not running at ${host}:${port}`
|
|
5454
|
+
kind: "bin",
|
|
5455
|
+
program: bin
|
|
5501
5456
|
};
|
|
5502
5457
|
}
|
|
5503
|
-
|
|
5504
|
-
|
|
5505
|
-
const
|
|
5506
|
-
const hasEnv = !!process.env.FIRST_TREE_HUB_SERVER_URL;
|
|
5507
|
-
if (hasFile && hasEnv) return {
|
|
5508
|
-
label: "Config",
|
|
5509
|
-
ok: true,
|
|
5510
|
-
detail: "config file + env vars"
|
|
5511
|
-
};
|
|
5512
|
-
if (hasFile) return {
|
|
5513
|
-
label: "Config",
|
|
5514
|
-
ok: true,
|
|
5515
|
-
detail: join(DEFAULT_CONFIG_DIR, "client.yaml")
|
|
5516
|
-
};
|
|
5517
|
-
if (hasEnv) return {
|
|
5518
|
-
label: "Config",
|
|
5519
|
-
ok: true,
|
|
5520
|
-
detail: "via environment variables"
|
|
5521
|
-
};
|
|
5458
|
+
const script = process.argv[1];
|
|
5459
|
+
if (!script) throw new Error("Cannot resolve CLI entry point (process.argv[1] is empty).");
|
|
5460
|
+
const scriptAbs = isAbsolute(script) ? script : join(process.cwd(), script);
|
|
5522
5461
|
return {
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5462
|
+
kind: "node",
|
|
5463
|
+
program: process.execPath,
|
|
5464
|
+
args: [scriptAbs]
|
|
5526
5465
|
};
|
|
5527
5466
|
}
|
|
5528
|
-
|
|
5529
|
-
|
|
5530
|
-
|
|
5531
|
-
|
|
5532
|
-
|
|
5533
|
-
detail: "not configured (FIRST_TREE_HUB_SERVER_URL or config file)"
|
|
5534
|
-
};
|
|
5535
|
-
try {
|
|
5536
|
-
const res = await fetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(5e3) });
|
|
5537
|
-
if (res.ok) return {
|
|
5538
|
-
label: "Server URL",
|
|
5539
|
-
ok: true,
|
|
5540
|
-
detail: serverUrl
|
|
5541
|
-
};
|
|
5542
|
-
return {
|
|
5543
|
-
label: "Server URL",
|
|
5544
|
-
ok: false,
|
|
5545
|
-
detail: `unhealthy (HTTP ${res.status}) at ${serverUrl}`
|
|
5546
|
-
};
|
|
5547
|
-
} catch {
|
|
5548
|
-
return {
|
|
5549
|
-
label: "Server URL",
|
|
5550
|
-
ok: false,
|
|
5551
|
-
detail: `unreachable at ${serverUrl}`
|
|
5552
|
-
};
|
|
5553
|
-
}
|
|
5467
|
+
function ensureLogDir() {
|
|
5468
|
+
mkdirSync(LOG_DIR, {
|
|
5469
|
+
recursive: true,
|
|
5470
|
+
mode: 448
|
|
5471
|
+
});
|
|
5554
5472
|
}
|
|
5555
|
-
function
|
|
5556
|
-
|
|
5557
|
-
if (!existsSync(agentsDir)) return {
|
|
5558
|
-
label: "Agents",
|
|
5559
|
-
ok: false,
|
|
5560
|
-
detail: "no agents configured"
|
|
5561
|
-
};
|
|
5562
|
-
try {
|
|
5563
|
-
const agents = loadAgents({
|
|
5564
|
-
schema: agentConfigSchema,
|
|
5565
|
-
agentsDir
|
|
5566
|
-
});
|
|
5567
|
-
if (agents.size === 0) return {
|
|
5568
|
-
label: "Agents",
|
|
5569
|
-
ok: false,
|
|
5570
|
-
detail: "no agents configured"
|
|
5571
|
-
};
|
|
5572
|
-
const names = [...agents.keys()].join(", ");
|
|
5573
|
-
return {
|
|
5574
|
-
label: "Agents",
|
|
5575
|
-
ok: true,
|
|
5576
|
-
detail: `${agents.size} configured (${names})`
|
|
5577
|
-
};
|
|
5578
|
-
} catch {
|
|
5579
|
-
return {
|
|
5580
|
-
label: "Agents",
|
|
5581
|
-
ok: false,
|
|
5582
|
-
detail: "error reading agent configs"
|
|
5583
|
-
};
|
|
5584
|
-
}
|
|
5585
|
-
}
|
|
5586
|
-
async function checkWebSocket() {
|
|
5587
|
-
const serverUrl = get(getClientConfig(), "server.url");
|
|
5588
|
-
if (typeof serverUrl !== "string" || !serverUrl) return {
|
|
5589
|
-
label: "WebSocket",
|
|
5590
|
-
ok: false,
|
|
5591
|
-
detail: "cannot check (no server URL)"
|
|
5592
|
-
};
|
|
5593
|
-
const wsUrl = serverUrl.replace(/^http/, "ws");
|
|
5594
|
-
try {
|
|
5595
|
-
if ((await fetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(3e3) })).ok) return {
|
|
5596
|
-
label: "WebSocket",
|
|
5597
|
-
ok: true,
|
|
5598
|
-
detail: `${wsUrl} (server reachable)`
|
|
5599
|
-
};
|
|
5600
|
-
return {
|
|
5601
|
-
label: "WebSocket",
|
|
5602
|
-
ok: false,
|
|
5603
|
-
detail: "server not healthy"
|
|
5604
|
-
};
|
|
5605
|
-
} catch {
|
|
5606
|
-
return {
|
|
5607
|
-
label: "WebSocket",
|
|
5608
|
-
ok: false,
|
|
5609
|
-
detail: `server unreachable at ${serverUrl}`
|
|
5610
|
-
};
|
|
5611
|
-
}
|
|
5612
|
-
}
|
|
5613
|
-
function printResults(results) {
|
|
5614
|
-
for (const r of results) {
|
|
5615
|
-
const icon = r.ok ? "✓" : "✗";
|
|
5616
|
-
print.line(` ${icon} ${r.label.padEnd(22)} ${r.detail}\n`);
|
|
5617
|
-
}
|
|
5618
|
-
blank();
|
|
5619
|
-
const failures = results.filter((r) => !r.ok);
|
|
5620
|
-
if (failures.length === 0) print.line(" All checks passed.\n");
|
|
5621
|
-
else print.line(` ${failures.length} issue(s) found.\n`);
|
|
5622
|
-
blank();
|
|
5623
|
-
}
|
|
5624
|
-
//#endregion
|
|
5625
|
-
//#region src/core/migrate.ts
|
|
5626
|
-
/**
|
|
5627
|
-
* Resolve the drizzle migrations directory.
|
|
5628
|
-
* 1. npm install: embedded at dist/drizzle/ (relative to the built CLI)
|
|
5629
|
-
* 2. Monorepo dev: resolved from @first-tree-hub/server package
|
|
5630
|
-
*/
|
|
5631
|
-
function resolveMigrationsFolder() {
|
|
5632
|
-
const embeddedPath = join(dirname(fileURLToPath(import.meta.url)), "..", "drizzle");
|
|
5633
|
-
if (existsSync(embeddedPath)) return embeddedPath;
|
|
5634
|
-
return join(dirname(fileURLToPath(import.meta.resolve("@first-tree-hub/server/package.json"))), "drizzle");
|
|
5635
|
-
}
|
|
5636
|
-
/**
|
|
5637
|
-
* Validate that migration journal timestamps are strictly increasing.
|
|
5638
|
-
* Drizzle silently skips migrations whose `when` is <= the last applied
|
|
5639
|
-
* timestamp, which causes missing columns/tables with no error.
|
|
5640
|
-
*/
|
|
5641
|
-
function validateJournalOrder(migrationsFolder) {
|
|
5642
|
-
const journalPath = join(migrationsFolder, "meta", "_journal.json");
|
|
5643
|
-
if (!existsSync(journalPath)) return;
|
|
5644
|
-
const journal = JSON.parse(readFileSync(journalPath, "utf-8"));
|
|
5645
|
-
let prevWhen = 0;
|
|
5646
|
-
let prevTag = "";
|
|
5647
|
-
for (const entry of journal.entries) {
|
|
5648
|
-
if (entry.when <= prevWhen) throw new Error(`Migration journal timestamps are not monotonically increasing:\n "${prevTag}" (when: ${prevWhen}) >= "${entry.tag}" (when: ${entry.when})\n Drizzle will silently skip "${entry.tag}". Fix the 'when' values in:\n ${journalPath}`);
|
|
5649
|
-
prevWhen = entry.when;
|
|
5650
|
-
prevTag = entry.tag;
|
|
5651
|
-
}
|
|
5652
|
-
}
|
|
5653
|
-
/**
|
|
5654
|
-
* Run Drizzle database migrations.
|
|
5655
|
-
*/
|
|
5656
|
-
async function runMigrations(databaseUrl) {
|
|
5657
|
-
const migrationsFolder = resolveMigrationsFolder();
|
|
5658
|
-
validateJournalOrder(migrationsFolder);
|
|
5659
|
-
const client = postgres(databaseUrl, { max: 1 });
|
|
5660
|
-
const db = drizzle(client);
|
|
5661
|
-
try {
|
|
5662
|
-
await migrate(db, { migrationsFolder });
|
|
5663
|
-
} finally {
|
|
5664
|
-
await client.end();
|
|
5665
|
-
}
|
|
5666
|
-
const countClient = postgres(databaseUrl, { max: 1 });
|
|
5667
|
-
try {
|
|
5668
|
-
return (await countClient`
|
|
5669
|
-
SELECT count(*)::int AS count
|
|
5670
|
-
FROM information_schema.tables
|
|
5671
|
-
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
5672
|
-
`)[0].count;
|
|
5673
|
-
} finally {
|
|
5674
|
-
await countClient.end();
|
|
5675
|
-
}
|
|
5676
|
-
}
|
|
5677
|
-
//#endregion
|
|
5678
|
-
//#region src/core/service-install.ts
|
|
5679
|
-
/**
|
|
5680
|
-
* Run a subprocess capturing stderr so failures surface a meaningful error
|
|
5681
|
-
* instead of Node's opaque "Command failed". Used for launchctl/systemctl —
|
|
5682
|
-
* anywhere the stderr message is diagnostically crucial.
|
|
5683
|
-
*/
|
|
5684
|
-
function runCapture(program, args, timeoutMs) {
|
|
5685
|
-
const res = spawnSync(program, args, {
|
|
5686
|
-
encoding: "utf-8",
|
|
5687
|
-
timeout: timeoutMs,
|
|
5688
|
-
stdio: [
|
|
5689
|
-
"ignore",
|
|
5690
|
-
"pipe",
|
|
5691
|
-
"pipe"
|
|
5692
|
-
]
|
|
5693
|
-
});
|
|
5694
|
-
if (res.status === 0) return { ok: true };
|
|
5695
|
-
return {
|
|
5696
|
-
ok: false,
|
|
5697
|
-
stderr: (res.stderr ?? "").trim(),
|
|
5698
|
-
code: res.status
|
|
5699
|
-
};
|
|
5700
|
-
}
|
|
5701
|
-
function sleepSync(ms) {
|
|
5702
|
-
const shared = new Int32Array(new SharedArrayBuffer(4));
|
|
5703
|
-
Atomics.wait(shared, 0, 0, ms);
|
|
5704
|
-
}
|
|
5705
|
-
const LAUNCHD_LABEL = "dev.first-tree-hub.client";
|
|
5706
|
-
const SYSTEMD_UNIT = "first-tree-hub-client.service";
|
|
5707
|
-
const LOG_DIR$1 = join(DEFAULT_HOME_DIR$1, "logs");
|
|
5708
|
-
function whichBin(name) {
|
|
5709
|
-
try {
|
|
5710
|
-
return execFileSync(process.platform === "win32" ? "where" : "which", [name], {
|
|
5711
|
-
encoding: "utf-8",
|
|
5712
|
-
timeout: 3e3
|
|
5713
|
-
}).split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0] ?? null;
|
|
5714
|
-
} catch {
|
|
5715
|
-
return null;
|
|
5716
|
-
}
|
|
5717
|
-
}
|
|
5718
|
-
/**
|
|
5719
|
-
* Resolve how the service should launch the CLI.
|
|
5720
|
-
*
|
|
5721
|
-
* Prefers the installed `first-tree-hub` bin on PATH (usually a shim under
|
|
5722
|
-
* /usr/local/bin or ~/.npm-global/bin). Falls back to invoking the current
|
|
5723
|
-
* Node interpreter against the running script (handles `pnpm dev`, tsx, and
|
|
5724
|
-
* dev-only global installs).
|
|
5725
|
-
*/
|
|
5726
|
-
function resolveCliInvocation() {
|
|
5727
|
-
const bin = whichBin("first-tree-hub");
|
|
5728
|
-
if (bin && isAbsolute(bin)) try {
|
|
5729
|
-
return {
|
|
5730
|
-
kind: "bin",
|
|
5731
|
-
program: realpathSync(bin)
|
|
5732
|
-
};
|
|
5733
|
-
} catch {
|
|
5734
|
-
return {
|
|
5735
|
-
kind: "bin",
|
|
5736
|
-
program: bin
|
|
5737
|
-
};
|
|
5738
|
-
}
|
|
5739
|
-
const script = process.argv[1];
|
|
5740
|
-
if (!script) throw new Error("Cannot resolve CLI entry point (process.argv[1] is empty).");
|
|
5741
|
-
const scriptAbs = isAbsolute(script) ? script : join(process.cwd(), script);
|
|
5742
|
-
return {
|
|
5743
|
-
kind: "node",
|
|
5744
|
-
program: process.execPath,
|
|
5745
|
-
args: [scriptAbs]
|
|
5746
|
-
};
|
|
5747
|
-
}
|
|
5748
|
-
function ensureLogDir() {
|
|
5749
|
-
mkdirSync(LOG_DIR$1, {
|
|
5750
|
-
recursive: true,
|
|
5751
|
-
mode: 448
|
|
5752
|
-
});
|
|
5753
|
-
}
|
|
5754
|
-
function launchdPlistPath() {
|
|
5755
|
-
return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
5473
|
+
function launchdPlistPath() {
|
|
5474
|
+
return join(homedir(), "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
5756
5475
|
}
|
|
5757
5476
|
function renderPlist(invocation) {
|
|
5758
5477
|
const argsXml = (invocation.kind === "bin" ? [
|
|
@@ -5767,8 +5486,8 @@ function renderPlist(invocation) {
|
|
|
5767
5486
|
"start",
|
|
5768
5487
|
"--no-interactive"
|
|
5769
5488
|
]).map((a) => ` <string>${escapeXml(a)}</string>`).join("\n");
|
|
5770
|
-
const stdoutFallback = join(LOG_DIR
|
|
5771
|
-
const stderrFallback = join(LOG_DIR
|
|
5489
|
+
const stdoutFallback = join(LOG_DIR, "client.stdout.log");
|
|
5490
|
+
const stderrFallback = join(LOG_DIR, "client.stderr.log");
|
|
5772
5491
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
5773
5492
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTD/PropertyList-1.0.dtd">
|
|
5774
5493
|
<plist version="1.0">
|
|
@@ -5888,7 +5607,7 @@ function installLaunchd() {
|
|
|
5888
5607
|
lastBootstrapErr = res;
|
|
5889
5608
|
if (attempt < 2) sleepSync(1e3);
|
|
5890
5609
|
}
|
|
5891
|
-
if (lastBootstrapErr) throw new Error(`launchctl bootstrap failed: ${lastBootstrapErr.stderr || `exit ${lastBootstrapErr.code ?? "unknown"}`}\n Command: launchctl bootstrap ${target} ${plistPath}\n Recovery: \`launchctl bootout ${target}/${LAUNCHD_LABEL}\` then \`first-tree-hub
|
|
5610
|
+
if (lastBootstrapErr) throw new Error(`launchctl bootstrap failed: ${lastBootstrapErr.stderr || `exit ${lastBootstrapErr.code ?? "unknown"}`}\n Command: launchctl bootstrap ${target} ${plistPath}\n Recovery: \`launchctl bootout ${target}/${LAUNCHD_LABEL}\` then \`first-tree-hub client connect <server-url>\`.`);
|
|
5892
5611
|
const enableRes = runCapture("launchctl", ["enable", `${target}/${LAUNCHD_LABEL}`], 5e3);
|
|
5893
5612
|
if (!enableRes.ok) print.line(` warning: launchctl enable: ${enableRes.stderr || `exit ${enableRes.code ?? "unknown"}`}\n`);
|
|
5894
5613
|
const { state, detail } = launchdState();
|
|
@@ -5896,7 +5615,7 @@ function installLaunchd() {
|
|
|
5896
5615
|
platform: "launchd",
|
|
5897
5616
|
label: LAUNCHD_LABEL,
|
|
5898
5617
|
unitPath: plistPath,
|
|
5899
|
-
logDir: LOG_DIR
|
|
5618
|
+
logDir: LOG_DIR,
|
|
5900
5619
|
state,
|
|
5901
5620
|
detail
|
|
5902
5621
|
};
|
|
@@ -5910,7 +5629,7 @@ function uninstallLaunchd() {
|
|
|
5910
5629
|
platform: "launchd",
|
|
5911
5630
|
label: LAUNCHD_LABEL,
|
|
5912
5631
|
unitPath: plistPath,
|
|
5913
|
-
logDir: LOG_DIR
|
|
5632
|
+
logDir: LOG_DIR,
|
|
5914
5633
|
state: "not-installed"
|
|
5915
5634
|
};
|
|
5916
5635
|
}
|
|
@@ -5928,8 +5647,8 @@ Type=simple
|
|
|
5928
5647
|
ExecStart=${invocation.kind === "bin" ? `${shellQuote(invocation.program)} client start --no-interactive` : `${shellQuote(invocation.program)} ${invocation.args.map(shellQuote).join(" ")} client start --no-interactive`}
|
|
5929
5648
|
Restart=always
|
|
5930
5649
|
RestartSec=10
|
|
5931
|
-
StandardOutput=append:${join(LOG_DIR
|
|
5932
|
-
StandardError=append:${join(LOG_DIR
|
|
5650
|
+
StandardOutput=append:${join(LOG_DIR, "client.stdout.log")}
|
|
5651
|
+
StandardError=append:${join(LOG_DIR, "client.stderr.log")}
|
|
5933
5652
|
Environment=PATH=/usr/local/bin:/usr/bin:/bin
|
|
5934
5653
|
Environment=FIRST_TREE_HUB_SERVICE_MODE=1
|
|
5935
5654
|
|
|
@@ -5941,128 +5660,453 @@ function shellQuote(value) {
|
|
|
5941
5660
|
if (/^[A-Za-z0-9_\-./:=]+$/.test(value)) return value;
|
|
5942
5661
|
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
5943
5662
|
}
|
|
5944
|
-
function systemdState() {
|
|
5945
|
-
if (!existsSync(systemdUnitPath())) return { state: "not-installed" };
|
|
5946
|
-
const res = spawnSync("systemctl", [
|
|
5947
|
-
"--user",
|
|
5948
|
-
"is-active",
|
|
5949
|
-
SYSTEMD_UNIT
|
|
5950
|
-
], {
|
|
5951
|
-
encoding: "utf-8",
|
|
5952
|
-
timeout: 5e3,
|
|
5953
|
-
stdio: [
|
|
5954
|
-
"ignore",
|
|
5955
|
-
"pipe",
|
|
5956
|
-
"pipe"
|
|
5957
|
-
]
|
|
5958
|
-
});
|
|
5959
|
-
const out = (res.stdout ?? "").trim();
|
|
5960
|
-
if (res.status === 0 && out === "active") return {
|
|
5961
|
-
state: "active",
|
|
5962
|
-
detail: "running"
|
|
5663
|
+
function systemdState() {
|
|
5664
|
+
if (!existsSync(systemdUnitPath())) return { state: "not-installed" };
|
|
5665
|
+
const res = spawnSync("systemctl", [
|
|
5666
|
+
"--user",
|
|
5667
|
+
"is-active",
|
|
5668
|
+
SYSTEMD_UNIT
|
|
5669
|
+
], {
|
|
5670
|
+
encoding: "utf-8",
|
|
5671
|
+
timeout: 5e3,
|
|
5672
|
+
stdio: [
|
|
5673
|
+
"ignore",
|
|
5674
|
+
"pipe",
|
|
5675
|
+
"pipe"
|
|
5676
|
+
]
|
|
5677
|
+
});
|
|
5678
|
+
const out = (res.stdout ?? "").trim();
|
|
5679
|
+
if (res.status === 0 && out === "active") return {
|
|
5680
|
+
state: "active",
|
|
5681
|
+
detail: "running"
|
|
5682
|
+
};
|
|
5683
|
+
return {
|
|
5684
|
+
state: "inactive",
|
|
5685
|
+
detail: out || "unit present but not active"
|
|
5686
|
+
};
|
|
5687
|
+
}
|
|
5688
|
+
function installSystemd() {
|
|
5689
|
+
const invocation = resolveCliInvocation();
|
|
5690
|
+
ensureLogDir();
|
|
5691
|
+
const unitPath = systemdUnitPath();
|
|
5692
|
+
mkdirSync(dirname(unitPath), { recursive: true });
|
|
5693
|
+
writeFileSync(unitPath, renderSystemdUnit(invocation), { mode: 420 });
|
|
5694
|
+
const reloadRes = runCapture("systemctl", ["--user", "daemon-reload"], 5e3);
|
|
5695
|
+
if (!reloadRes.ok) throw new Error(`systemctl --user daemon-reload failed: ${reloadRes.stderr || `exit ${reloadRes.code ?? "unknown"}`}`);
|
|
5696
|
+
const enableRes = runCapture("systemctl", [
|
|
5697
|
+
"--user",
|
|
5698
|
+
"enable",
|
|
5699
|
+
"--now",
|
|
5700
|
+
SYSTEMD_UNIT
|
|
5701
|
+
], 1e4);
|
|
5702
|
+
if (!enableRes.ok) throw new Error(`systemctl --user enable --now ${SYSTEMD_UNIT} failed: ${enableRes.stderr || `exit ${enableRes.code ?? "unknown"}`}\n Recovery: \`systemctl --user stop ${SYSTEMD_UNIT}\` then \`first-tree-hub client connect <server-url>\`.`);
|
|
5703
|
+
const { state, detail } = systemdState();
|
|
5704
|
+
return {
|
|
5705
|
+
platform: "systemd",
|
|
5706
|
+
label: SYSTEMD_UNIT,
|
|
5707
|
+
unitPath,
|
|
5708
|
+
logDir: LOG_DIR,
|
|
5709
|
+
state,
|
|
5710
|
+
detail
|
|
5711
|
+
};
|
|
5712
|
+
}
|
|
5713
|
+
function uninstallSystemd() {
|
|
5714
|
+
const unitPath = systemdUnitPath();
|
|
5715
|
+
const disableRes = runCapture("systemctl", [
|
|
5716
|
+
"--user",
|
|
5717
|
+
"disable",
|
|
5718
|
+
"--now",
|
|
5719
|
+
SYSTEMD_UNIT
|
|
5720
|
+
], 1e4);
|
|
5721
|
+
if (!disableRes.ok && !/not found|no such|not loaded/i.test(disableRes.stderr)) print.line(` warning: systemctl disable during uninstall: ${disableRes.stderr || `exit ${disableRes.code ?? "unknown"}`}\n`);
|
|
5722
|
+
if (existsSync(unitPath)) rmSync(unitPath);
|
|
5723
|
+
const reloadRes = runCapture("systemctl", ["--user", "daemon-reload"], 5e3);
|
|
5724
|
+
if (!reloadRes.ok) print.line(` warning: systemctl daemon-reload during uninstall: ${reloadRes.stderr || `exit ${reloadRes.code ?? "unknown"}`}\n`);
|
|
5725
|
+
return {
|
|
5726
|
+
platform: "systemd",
|
|
5727
|
+
label: SYSTEMD_UNIT,
|
|
5728
|
+
unitPath,
|
|
5729
|
+
logDir: LOG_DIR,
|
|
5730
|
+
state: "not-installed"
|
|
5731
|
+
};
|
|
5732
|
+
}
|
|
5733
|
+
/** Is background-service install supported on the current platform? */
|
|
5734
|
+
function isServiceSupported() {
|
|
5735
|
+
return process.platform === "darwin" || process.platform === "linux";
|
|
5736
|
+
}
|
|
5737
|
+
/**
|
|
5738
|
+
* Install the background service for the current platform.
|
|
5739
|
+
*
|
|
5740
|
+
* @throws {Error} if the platform is not supported or the service manager fails.
|
|
5741
|
+
*/
|
|
5742
|
+
function installClientService() {
|
|
5743
|
+
if (process.platform === "darwin") return installLaunchd();
|
|
5744
|
+
if (process.platform === "linux") return installSystemd();
|
|
5745
|
+
throw new Error(`Background service install is not supported on ${process.platform}. Run \`first-tree-hub client start\` manually to keep the computer online.`);
|
|
5746
|
+
}
|
|
5747
|
+
/** Report the current service state without modifying anything. */
|
|
5748
|
+
function getClientServiceStatus() {
|
|
5749
|
+
if (process.platform === "darwin") {
|
|
5750
|
+
const { state, detail } = launchdState();
|
|
5751
|
+
return {
|
|
5752
|
+
platform: "launchd",
|
|
5753
|
+
label: LAUNCHD_LABEL,
|
|
5754
|
+
unitPath: launchdPlistPath(),
|
|
5755
|
+
logDir: LOG_DIR,
|
|
5756
|
+
state,
|
|
5757
|
+
detail
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
5760
|
+
if (process.platform === "linux") {
|
|
5761
|
+
const { state, detail } = systemdState();
|
|
5762
|
+
return {
|
|
5763
|
+
platform: "systemd",
|
|
5764
|
+
label: SYSTEMD_UNIT,
|
|
5765
|
+
unitPath: systemdUnitPath(),
|
|
5766
|
+
logDir: LOG_DIR,
|
|
5767
|
+
state,
|
|
5768
|
+
detail
|
|
5769
|
+
};
|
|
5770
|
+
}
|
|
5771
|
+
return {
|
|
5772
|
+
platform: "unsupported",
|
|
5773
|
+
label: "",
|
|
5774
|
+
unitPath: "",
|
|
5775
|
+
logDir: LOG_DIR,
|
|
5776
|
+
state: "not-installed",
|
|
5777
|
+
detail: `platform ${process.platform} not supported`
|
|
5778
|
+
};
|
|
5779
|
+
}
|
|
5780
|
+
/** Uninstall the background service. No-op if not installed. */
|
|
5781
|
+
function uninstallClientService() {
|
|
5782
|
+
if (process.platform === "darwin") return uninstallLaunchd();
|
|
5783
|
+
if (process.platform === "linux") return uninstallSystemd();
|
|
5784
|
+
return getClientServiceStatus();
|
|
5785
|
+
}
|
|
5786
|
+
//#endregion
|
|
5787
|
+
//#region src/core/doctor.ts
|
|
5788
|
+
function getServerConfig() {
|
|
5789
|
+
return resolveConfigReadonly({
|
|
5790
|
+
schema: serverConfigSchema,
|
|
5791
|
+
role: "server"
|
|
5792
|
+
});
|
|
5793
|
+
}
|
|
5794
|
+
function getClientConfig() {
|
|
5795
|
+
return resolveConfigReadonly({
|
|
5796
|
+
schema: clientConfigSchema,
|
|
5797
|
+
role: "client"
|
|
5798
|
+
});
|
|
5799
|
+
}
|
|
5800
|
+
function get(obj, dotPath) {
|
|
5801
|
+
const parts = dotPath.split(".");
|
|
5802
|
+
let current = obj;
|
|
5803
|
+
for (const part of parts) {
|
|
5804
|
+
if (current === null || current === void 0 || typeof current !== "object") return void 0;
|
|
5805
|
+
current = current[part];
|
|
5806
|
+
}
|
|
5807
|
+
return current;
|
|
5808
|
+
}
|
|
5809
|
+
function checkNodeVersion() {
|
|
5810
|
+
const version = process.versions.node;
|
|
5811
|
+
const [major] = version.split(".").map(Number);
|
|
5812
|
+
const ok = major !== void 0 && major >= 22;
|
|
5813
|
+
return {
|
|
5814
|
+
label: "Node.js",
|
|
5815
|
+
ok,
|
|
5816
|
+
detail: ok ? `v${version}` : `v${version} (requires >= 22.16)`
|
|
5817
|
+
};
|
|
5818
|
+
}
|
|
5819
|
+
function checkDocker() {
|
|
5820
|
+
try {
|
|
5821
|
+
return {
|
|
5822
|
+
label: "Docker",
|
|
5823
|
+
ok: true,
|
|
5824
|
+
detail: execFileSync("docker", ["--version"], {
|
|
5825
|
+
encoding: "utf-8",
|
|
5826
|
+
timeout: 5e3
|
|
5827
|
+
}).trim().replace("Docker version ", "v").split(",")[0] ?? ""
|
|
5828
|
+
};
|
|
5829
|
+
} catch {
|
|
5830
|
+
return {
|
|
5831
|
+
label: "Docker",
|
|
5832
|
+
ok: false,
|
|
5833
|
+
detail: "not found (optional — needed for auto PG provisioning)"
|
|
5834
|
+
};
|
|
5835
|
+
}
|
|
5836
|
+
}
|
|
5837
|
+
function checkServerConfig() {
|
|
5838
|
+
const hasFile = existsSync(join(DEFAULT_CONFIG_DIR, "server.yaml"));
|
|
5839
|
+
const hasEnv = !!process.env.FIRST_TREE_HUB_DATABASE_URL;
|
|
5840
|
+
if (hasFile && hasEnv) return {
|
|
5841
|
+
label: "Config",
|
|
5842
|
+
ok: true,
|
|
5843
|
+
detail: "config file + env vars"
|
|
5844
|
+
};
|
|
5845
|
+
if (hasFile) return {
|
|
5846
|
+
label: "Config",
|
|
5847
|
+
ok: true,
|
|
5848
|
+
detail: join(DEFAULT_CONFIG_DIR, "server.yaml")
|
|
5849
|
+
};
|
|
5850
|
+
if (hasEnv) return {
|
|
5851
|
+
label: "Config",
|
|
5852
|
+
ok: true,
|
|
5853
|
+
detail: "via environment variables"
|
|
5854
|
+
};
|
|
5855
|
+
return {
|
|
5856
|
+
label: "Config",
|
|
5857
|
+
ok: false,
|
|
5858
|
+
detail: "no config file or env vars found"
|
|
5859
|
+
};
|
|
5860
|
+
}
|
|
5861
|
+
async function checkDatabase() {
|
|
5862
|
+
const dbUrl = get(getServerConfig(), "database.url");
|
|
5863
|
+
if (typeof dbUrl !== "string" || !dbUrl) return {
|
|
5864
|
+
label: "Database",
|
|
5865
|
+
ok: false,
|
|
5866
|
+
detail: "not configured (FIRST_TREE_HUB_DATABASE_URL or config file)"
|
|
5867
|
+
};
|
|
5868
|
+
try {
|
|
5869
|
+
const { default: pg } = await import("postgres");
|
|
5870
|
+
const sql = pg(dbUrl, {
|
|
5871
|
+
max: 1,
|
|
5872
|
+
connect_timeout: 5,
|
|
5873
|
+
idle_timeout: 1
|
|
5874
|
+
});
|
|
5875
|
+
await sql.unsafe("SELECT 1");
|
|
5876
|
+
await sql.end();
|
|
5877
|
+
return {
|
|
5878
|
+
label: "Database",
|
|
5879
|
+
ok: true,
|
|
5880
|
+
detail: "connected"
|
|
5881
|
+
};
|
|
5882
|
+
} catch (err) {
|
|
5883
|
+
return {
|
|
5884
|
+
label: "Database",
|
|
5885
|
+
ok: false,
|
|
5886
|
+
detail: `unreachable — ${(err instanceof Error ? err.message : String(err)).slice(0, 80)}`
|
|
5887
|
+
};
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
async function checkServerHealth() {
|
|
5891
|
+
const config = getServerConfig();
|
|
5892
|
+
const host = get(config, "server.host") ?? "127.0.0.1";
|
|
5893
|
+
const port = get(config, "server.port") ?? 8e3;
|
|
5894
|
+
const url = `http://${host}:${port}/healthz`;
|
|
5895
|
+
try {
|
|
5896
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(3e3) });
|
|
5897
|
+
if (res.ok) return {
|
|
5898
|
+
label: "Server Health",
|
|
5899
|
+
ok: true,
|
|
5900
|
+
detail: `running at ${host}:${port}`
|
|
5901
|
+
};
|
|
5902
|
+
return {
|
|
5903
|
+
label: "Server Health",
|
|
5904
|
+
ok: false,
|
|
5905
|
+
detail: `unhealthy (HTTP ${res.status}) at ${host}:${port}`
|
|
5906
|
+
};
|
|
5907
|
+
} catch {
|
|
5908
|
+
return {
|
|
5909
|
+
label: "Server Health",
|
|
5910
|
+
ok: false,
|
|
5911
|
+
detail: `not running at ${host}:${port}`
|
|
5912
|
+
};
|
|
5913
|
+
}
|
|
5914
|
+
}
|
|
5915
|
+
function checkClientConfig() {
|
|
5916
|
+
const hasFile = existsSync(join(DEFAULT_CONFIG_DIR, "client.yaml"));
|
|
5917
|
+
const hasEnv = !!process.env.FIRST_TREE_HUB_SERVER_URL;
|
|
5918
|
+
if (hasFile && hasEnv) return {
|
|
5919
|
+
label: "Config",
|
|
5920
|
+
ok: true,
|
|
5921
|
+
detail: "config file + env vars"
|
|
5922
|
+
};
|
|
5923
|
+
if (hasFile) return {
|
|
5924
|
+
label: "Config",
|
|
5925
|
+
ok: true,
|
|
5926
|
+
detail: join(DEFAULT_CONFIG_DIR, "client.yaml")
|
|
5927
|
+
};
|
|
5928
|
+
if (hasEnv) return {
|
|
5929
|
+
label: "Config",
|
|
5930
|
+
ok: true,
|
|
5931
|
+
detail: "via environment variables"
|
|
5932
|
+
};
|
|
5933
|
+
return {
|
|
5934
|
+
label: "Config",
|
|
5935
|
+
ok: false,
|
|
5936
|
+
detail: "no config file or env vars found"
|
|
5937
|
+
};
|
|
5938
|
+
}
|
|
5939
|
+
async function checkServerReachable() {
|
|
5940
|
+
const serverUrl = get(getClientConfig(), "server.url");
|
|
5941
|
+
if (typeof serverUrl !== "string" || !serverUrl) return {
|
|
5942
|
+
label: "Server URL",
|
|
5943
|
+
ok: false,
|
|
5944
|
+
detail: "not configured (FIRST_TREE_HUB_SERVER_URL or config file)"
|
|
5945
|
+
};
|
|
5946
|
+
try {
|
|
5947
|
+
const res = await fetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(5e3) });
|
|
5948
|
+
if (res.ok) return {
|
|
5949
|
+
label: "Server URL",
|
|
5950
|
+
ok: true,
|
|
5951
|
+
detail: serverUrl
|
|
5952
|
+
};
|
|
5953
|
+
return {
|
|
5954
|
+
label: "Server URL",
|
|
5955
|
+
ok: false,
|
|
5956
|
+
detail: `unhealthy (HTTP ${res.status}) at ${serverUrl}`
|
|
5957
|
+
};
|
|
5958
|
+
} catch {
|
|
5959
|
+
return {
|
|
5960
|
+
label: "Server URL",
|
|
5961
|
+
ok: false,
|
|
5962
|
+
detail: `unreachable at ${serverUrl}`
|
|
5963
|
+
};
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
function checkAgentConfigs() {
|
|
5967
|
+
const agentsDir = join(DEFAULT_CONFIG_DIR, "agents");
|
|
5968
|
+
if (!existsSync(agentsDir)) return {
|
|
5969
|
+
label: "Agents",
|
|
5970
|
+
ok: false,
|
|
5971
|
+
detail: "no agents configured"
|
|
5972
|
+
};
|
|
5973
|
+
try {
|
|
5974
|
+
const agents = loadAgents({
|
|
5975
|
+
schema: agentConfigSchema,
|
|
5976
|
+
agentsDir
|
|
5977
|
+
});
|
|
5978
|
+
if (agents.size === 0) return {
|
|
5979
|
+
label: "Agents",
|
|
5980
|
+
ok: false,
|
|
5981
|
+
detail: "no agents configured"
|
|
5982
|
+
};
|
|
5983
|
+
const names = [...agents.keys()].join(", ");
|
|
5984
|
+
return {
|
|
5985
|
+
label: "Agents",
|
|
5986
|
+
ok: true,
|
|
5987
|
+
detail: `${agents.size} configured (${names})`
|
|
5988
|
+
};
|
|
5989
|
+
} catch {
|
|
5990
|
+
return {
|
|
5991
|
+
label: "Agents",
|
|
5992
|
+
ok: false,
|
|
5993
|
+
detail: "error reading agent configs"
|
|
5994
|
+
};
|
|
5995
|
+
}
|
|
5996
|
+
}
|
|
5997
|
+
function checkBackgroundService() {
|
|
5998
|
+
const info = getClientServiceStatus();
|
|
5999
|
+
if (info.platform === "unsupported") return {
|
|
6000
|
+
label: "Background service",
|
|
6001
|
+
ok: true,
|
|
6002
|
+
detail: `not supported on ${process.platform} — runs inline`
|
|
5963
6003
|
};
|
|
5964
|
-
return {
|
|
5965
|
-
|
|
5966
|
-
|
|
6004
|
+
if (info.state === "active") return {
|
|
6005
|
+
label: "Background service",
|
|
6006
|
+
ok: true,
|
|
6007
|
+
detail: `running (${info.platform}${info.detail ? `, ${info.detail}` : ""}); logs at ${info.logDir}`
|
|
6008
|
+
};
|
|
6009
|
+
if (info.state === "inactive") return {
|
|
6010
|
+
label: "Background service",
|
|
6011
|
+
ok: false,
|
|
6012
|
+
detail: `installed but not running${info.detail ? ` — ${info.detail}` : ""}; unit at ${info.unitPath}`
|
|
5967
6013
|
};
|
|
5968
|
-
}
|
|
5969
|
-
function installSystemd() {
|
|
5970
|
-
const invocation = resolveCliInvocation();
|
|
5971
|
-
ensureLogDir();
|
|
5972
|
-
const unitPath = systemdUnitPath();
|
|
5973
|
-
mkdirSync(dirname(unitPath), { recursive: true });
|
|
5974
|
-
writeFileSync(unitPath, renderSystemdUnit(invocation), { mode: 420 });
|
|
5975
|
-
const reloadRes = runCapture("systemctl", ["--user", "daemon-reload"], 5e3);
|
|
5976
|
-
if (!reloadRes.ok) throw new Error(`systemctl --user daemon-reload failed: ${reloadRes.stderr || `exit ${reloadRes.code ?? "unknown"}`}`);
|
|
5977
|
-
const enableRes = runCapture("systemctl", [
|
|
5978
|
-
"--user",
|
|
5979
|
-
"enable",
|
|
5980
|
-
"--now",
|
|
5981
|
-
SYSTEMD_UNIT
|
|
5982
|
-
], 1e4);
|
|
5983
|
-
if (!enableRes.ok) throw new Error(`systemctl --user enable --now ${SYSTEMD_UNIT} failed: ${enableRes.stderr || `exit ${enableRes.code ?? "unknown"}`}\n Recovery: \`systemctl --user stop ${SYSTEMD_UNIT}\` then \`first-tree-hub service install\`.`);
|
|
5984
|
-
const { state, detail } = systemdState();
|
|
5985
6014
|
return {
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
logDir: LOG_DIR$1,
|
|
5990
|
-
state,
|
|
5991
|
-
detail
|
|
6015
|
+
label: "Background service",
|
|
6016
|
+
ok: false,
|
|
6017
|
+
detail: "not installed — re-run `first-tree-hub client connect <url>` to install"
|
|
5992
6018
|
};
|
|
5993
6019
|
}
|
|
5994
|
-
function
|
|
5995
|
-
const
|
|
5996
|
-
|
|
5997
|
-
"
|
|
5998
|
-
|
|
5999
|
-
"
|
|
6000
|
-
SYSTEMD_UNIT
|
|
6001
|
-
], 1e4);
|
|
6002
|
-
if (!disableRes.ok && !/not found|no such|not loaded/i.test(disableRes.stderr)) print.line(` warning: systemctl disable during uninstall: ${disableRes.stderr || `exit ${disableRes.code ?? "unknown"}`}\n`);
|
|
6003
|
-
if (existsSync(unitPath)) rmSync(unitPath);
|
|
6004
|
-
const reloadRes = runCapture("systemctl", ["--user", "daemon-reload"], 5e3);
|
|
6005
|
-
if (!reloadRes.ok) print.line(` warning: systemctl daemon-reload during uninstall: ${reloadRes.stderr || `exit ${reloadRes.code ?? "unknown"}`}\n`);
|
|
6006
|
-
return {
|
|
6007
|
-
platform: "systemd",
|
|
6008
|
-
label: SYSTEMD_UNIT,
|
|
6009
|
-
unitPath,
|
|
6010
|
-
logDir: LOG_DIR$1,
|
|
6011
|
-
state: "not-installed"
|
|
6020
|
+
async function checkWebSocket() {
|
|
6021
|
+
const serverUrl = get(getClientConfig(), "server.url");
|
|
6022
|
+
if (typeof serverUrl !== "string" || !serverUrl) return {
|
|
6023
|
+
label: "WebSocket",
|
|
6024
|
+
ok: false,
|
|
6025
|
+
detail: "cannot check (no server URL)"
|
|
6012
6026
|
};
|
|
6027
|
+
const wsUrl = serverUrl.replace(/^http/, "ws");
|
|
6028
|
+
try {
|
|
6029
|
+
if ((await fetch(`${serverUrl}/healthz`, { signal: AbortSignal.timeout(3e3) })).ok) return {
|
|
6030
|
+
label: "WebSocket",
|
|
6031
|
+
ok: true,
|
|
6032
|
+
detail: `${wsUrl} (server reachable)`
|
|
6033
|
+
};
|
|
6034
|
+
return {
|
|
6035
|
+
label: "WebSocket",
|
|
6036
|
+
ok: false,
|
|
6037
|
+
detail: "server not healthy"
|
|
6038
|
+
};
|
|
6039
|
+
} catch {
|
|
6040
|
+
return {
|
|
6041
|
+
label: "WebSocket",
|
|
6042
|
+
ok: false,
|
|
6043
|
+
detail: `server unreachable at ${serverUrl}`
|
|
6044
|
+
};
|
|
6045
|
+
}
|
|
6013
6046
|
}
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6047
|
+
function printResults(results) {
|
|
6048
|
+
for (const r of results) {
|
|
6049
|
+
const icon = r.ok ? "✓" : "✗";
|
|
6050
|
+
print.line(` ${icon} ${r.label.padEnd(22)} ${r.detail}\n`);
|
|
6051
|
+
}
|
|
6052
|
+
blank();
|
|
6053
|
+
const failures = results.filter((r) => !r.ok);
|
|
6054
|
+
if (failures.length === 0) print.line(" All checks passed.\n");
|
|
6055
|
+
else print.line(` ${failures.length} issue(s) found.\n`);
|
|
6056
|
+
blank();
|
|
6057
|
+
}
|
|
6058
|
+
//#endregion
|
|
6059
|
+
//#region src/core/migrate.ts
|
|
6060
|
+
/**
|
|
6061
|
+
* Resolve the drizzle migrations directory.
|
|
6062
|
+
* 1. npm install: embedded at dist/drizzle/ (relative to the built CLI)
|
|
6063
|
+
* 2. Monorepo dev: resolved from @first-tree-hub/server package
|
|
6064
|
+
*/
|
|
6065
|
+
function resolveMigrationsFolder() {
|
|
6066
|
+
const embeddedPath = join(dirname(fileURLToPath(import.meta.url)), "..", "drizzle");
|
|
6067
|
+
if (existsSync(embeddedPath)) return embeddedPath;
|
|
6068
|
+
return join(dirname(fileURLToPath(import.meta.resolve("@first-tree-hub/server/package.json"))), "drizzle");
|
|
6017
6069
|
}
|
|
6018
6070
|
/**
|
|
6019
|
-
*
|
|
6020
|
-
*
|
|
6021
|
-
*
|
|
6071
|
+
* Validate that migration journal timestamps are strictly increasing.
|
|
6072
|
+
* Drizzle silently skips migrations whose `when` is <= the last applied
|
|
6073
|
+
* timestamp, which causes missing columns/tables with no error.
|
|
6022
6074
|
*/
|
|
6023
|
-
function
|
|
6024
|
-
|
|
6025
|
-
if (
|
|
6026
|
-
|
|
6075
|
+
function validateJournalOrder(migrationsFolder) {
|
|
6076
|
+
const journalPath = join(migrationsFolder, "meta", "_journal.json");
|
|
6077
|
+
if (!existsSync(journalPath)) return;
|
|
6078
|
+
const journal = JSON.parse(readFileSync(journalPath, "utf-8"));
|
|
6079
|
+
let prevWhen = 0;
|
|
6080
|
+
let prevTag = "";
|
|
6081
|
+
for (const entry of journal.entries) {
|
|
6082
|
+
if (entry.when <= prevWhen) throw new Error(`Migration journal timestamps are not monotonically increasing:\n "${prevTag}" (when: ${prevWhen}) >= "${entry.tag}" (when: ${entry.when})\n Drizzle will silently skip "${entry.tag}". Fix the 'when' values in:\n ${journalPath}`);
|
|
6083
|
+
prevWhen = entry.when;
|
|
6084
|
+
prevTag = entry.tag;
|
|
6085
|
+
}
|
|
6027
6086
|
}
|
|
6028
|
-
/**
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6087
|
+
/**
|
|
6088
|
+
* Run Drizzle database migrations.
|
|
6089
|
+
*/
|
|
6090
|
+
async function runMigrations(databaseUrl) {
|
|
6091
|
+
const migrationsFolder = resolveMigrationsFolder();
|
|
6092
|
+
validateJournalOrder(migrationsFolder);
|
|
6093
|
+
const client = postgres(databaseUrl, { max: 1 });
|
|
6094
|
+
const db = drizzle(client);
|
|
6095
|
+
try {
|
|
6096
|
+
await migrate(db, { migrationsFolder });
|
|
6097
|
+
} finally {
|
|
6098
|
+
await client.end();
|
|
6040
6099
|
}
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
return
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
};
|
|
6100
|
+
const countClient = postgres(databaseUrl, { max: 1 });
|
|
6101
|
+
try {
|
|
6102
|
+
return (await countClient`
|
|
6103
|
+
SELECT count(*)::int AS count
|
|
6104
|
+
FROM information_schema.tables
|
|
6105
|
+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE'
|
|
6106
|
+
`)[0].count;
|
|
6107
|
+
} finally {
|
|
6108
|
+
await countClient.end();
|
|
6051
6109
|
}
|
|
6052
|
-
return {
|
|
6053
|
-
platform: "unsupported",
|
|
6054
|
-
label: "",
|
|
6055
|
-
unitPath: "",
|
|
6056
|
-
logDir: LOG_DIR$1,
|
|
6057
|
-
state: "not-installed",
|
|
6058
|
-
detail: `platform ${process.platform} not supported`
|
|
6059
|
-
};
|
|
6060
|
-
}
|
|
6061
|
-
/** Uninstall the background service. No-op if not installed. */
|
|
6062
|
-
function uninstallClientService() {
|
|
6063
|
-
if (process.platform === "darwin") return uninstallLaunchd();
|
|
6064
|
-
if (process.platform === "linux") return uninstallSystemd();
|
|
6065
|
-
return getClientServiceStatus();
|
|
6066
6110
|
}
|
|
6067
6111
|
//#endregion
|
|
6068
6112
|
//#region src/core/migrate-home.ts
|
|
@@ -6098,7 +6142,7 @@ function runHomeMigration() {
|
|
|
6098
6142
|
}
|
|
6099
6143
|
print.line(`[first-tree-hub] Copied client home to new layout: ${result.from} → ${result.to}\n (Legacy directory preserved as a backup — delete it manually once you've verified the new location works.)\n`);
|
|
6100
6144
|
if (process.argv.includes("--no-interactive")) {
|
|
6101
|
-
print.line("[first-tree-hub] Note: running as background service — skipped auto re-register to avoid self-termination.\n
|
|
6145
|
+
print.line("[first-tree-hub] Note: running as background service — skipped auto re-register to avoid self-termination.\n Service paths will refresh on the next `first-tree-hub client connect <url>`.\n");
|
|
6102
6146
|
return;
|
|
6103
6147
|
}
|
|
6104
6148
|
const status = getClientServiceStatus();
|
|
@@ -6108,7 +6152,7 @@ function runHomeMigration() {
|
|
|
6108
6152
|
print.line(`[first-tree-hub] Re-registered background service with new home paths.\n`);
|
|
6109
6153
|
} catch (err) {
|
|
6110
6154
|
const msg = err instanceof Error ? err.message : String(err);
|
|
6111
|
-
print.line(`[first-tree-hub] WARNING: home migration succeeded but re-registering the background service failed: ${msg}\n
|
|
6155
|
+
print.line(`[first-tree-hub] WARNING: home migration succeeded but re-registering the background service failed: ${msg}\n Re-run \`first-tree-hub client connect <url>\` to refresh service paths.\n`);
|
|
6112
6156
|
}
|
|
6113
6157
|
}
|
|
6114
6158
|
//#endregion
|
|
@@ -6279,7 +6323,7 @@ async function onboardCreate(args) {
|
|
|
6279
6323
|
}
|
|
6280
6324
|
const runtimeAgent = args.type === "human" ? args.assistant : args.id;
|
|
6281
6325
|
if (args.feishuBotAppId && args.feishuBotAppSecret) {
|
|
6282
|
-
const { bindFeishuBot } = await import("./feishu-
|
|
6326
|
+
const { bindFeishuBot } = await import("./feishu-B2sjp6Z6.mjs").then((n) => n.r);
|
|
6283
6327
|
const targetAgentUuid = args.type === "human" ? assistantUuid : primary.uuid;
|
|
6284
6328
|
if (!targetAgentUuid) print.line(`Warning: Cannot bind Feishu bot — no runtime agent available for "${args.id}".\n`);
|
|
6285
6329
|
else {
|
|
@@ -7177,7 +7221,7 @@ function createFeedbackHandler(config) {
|
|
|
7177
7221
|
return { handle };
|
|
7178
7222
|
}
|
|
7179
7223
|
//#endregion
|
|
7180
|
-
//#region ../server/dist/app-
|
|
7224
|
+
//#region ../server/dist/app-UkoRS4YL.mjs
|
|
7181
7225
|
var __defProp = Object.defineProperty;
|
|
7182
7226
|
var __exportAll = (all, no_symbols) => {
|
|
7183
7227
|
let target = {};
|
|
@@ -8207,6 +8251,7 @@ async function createAgent(db, data) {
|
|
|
8207
8251
|
const uuid = uuidv7();
|
|
8208
8252
|
const name = data.name ?? null;
|
|
8209
8253
|
if (name?.startsWith(RESERVED_AGENT_NAME_PREFIX)) throw new BadRequestError(`Agent name "${name}" is reserved — names starting with "${RESERVED_AGENT_NAME_PREFIX}" are Hub-internal`);
|
|
8254
|
+
if (name && isReservedAgentName$1(name)) throw new BadRequestError(`Agent name "${name}" is reserved — pick a different one.`);
|
|
8210
8255
|
const inboxId = `inbox_${uuid}`;
|
|
8211
8256
|
let orgId;
|
|
8212
8257
|
let managerId;
|
|
@@ -8262,6 +8307,21 @@ async function createAgent(db, data) {
|
|
|
8262
8307
|
throw err;
|
|
8263
8308
|
}
|
|
8264
8309
|
}
|
|
8310
|
+
async function checkAgentNameAvailability(db, orgId, name) {
|
|
8311
|
+
if (!AGENT_NAME_REGEX$1.test(name)) return {
|
|
8312
|
+
available: false,
|
|
8313
|
+
reason: "invalid"
|
|
8314
|
+
};
|
|
8315
|
+
if (isReservedAgentName$1(name) || name.startsWith(RESERVED_AGENT_NAME_PREFIX)) return {
|
|
8316
|
+
available: false,
|
|
8317
|
+
reason: "reserved"
|
|
8318
|
+
};
|
|
8319
|
+
const [existing] = await db.select({ uuid: agents.uuid }).from(agents).where(and(eq(agents.organizationId, orgId), eq(agents.name, name), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
|
|
8320
|
+
return existing ? {
|
|
8321
|
+
available: false,
|
|
8322
|
+
reason: "taken"
|
|
8323
|
+
} : { available: true };
|
|
8324
|
+
}
|
|
8265
8325
|
async function getAgent(db, uuid) {
|
|
8266
8326
|
const [agent] = await db.select().from(agents).where(and(eq(agents.uuid, uuid), ne(agents.status, AGENT_STATUSES.DELETED))).limit(1);
|
|
8267
8327
|
if (!agent) throw new NotFoundError(`Agent "${uuid}" not found`);
|
|
@@ -9516,6 +9576,18 @@ async function adminAgentRoutes(app) {
|
|
|
9516
9576
|
nextCursor: result.nextCursor
|
|
9517
9577
|
};
|
|
9518
9578
|
});
|
|
9579
|
+
/**
|
|
9580
|
+
* Pre-create availability probe for the web creation form. The caller types
|
|
9581
|
+
* an agent name; we answer whether the POST would succeed (`available: true`)
|
|
9582
|
+
* or why it would fail (`invalid` / `reserved` / `taken`) without actually
|
|
9583
|
+
* inserting a row. The regular POST still validates authoritatively — this is
|
|
9584
|
+
* a pure UX convenience. Scoped to the caller's org, so two orgs can each
|
|
9585
|
+
* have a `coder` without one blocking the other.
|
|
9586
|
+
*/
|
|
9587
|
+
app.get("/names/:name/availability", async (request) => {
|
|
9588
|
+
const scope = memberScope(request);
|
|
9589
|
+
return await checkAgentNameAvailability(app.db, scope.organizationId, request.params.name);
|
|
9590
|
+
});
|
|
9519
9591
|
app.post("/", async (request, reply) => {
|
|
9520
9592
|
const scope = memberScope(request);
|
|
9521
9593
|
const body = createAgentSchema.parse(request.body);
|
|
@@ -15031,7 +15103,7 @@ async function startServer(options) {
|
|
|
15031
15103
|
instanceId: `srv_${randomUUID().slice(0, 8)}`,
|
|
15032
15104
|
commandVersion: COMMAND_VERSION
|
|
15033
15105
|
};
|
|
15034
|
-
const { initTelemetry, shutdownTelemetry } = await import("./observability-
|
|
15106
|
+
const { initTelemetry, shutdownTelemetry } = await import("./observability-DDkJwSKv.mjs");
|
|
15035
15107
|
await initTelemetry(serverConfig.observability.tracing, config.instanceId);
|
|
15036
15108
|
const app = await buildApp(config);
|
|
15037
15109
|
const shutdown = async () => {
|
|
@@ -15081,175 +15153,6 @@ function resolveWebDist() {
|
|
|
15081
15153
|
} catch {}
|
|
15082
15154
|
}
|
|
15083
15155
|
//#endregion
|
|
15084
|
-
//#region src/core/service-logs.ts
|
|
15085
|
-
const LOG_DIR = join(DEFAULT_HOME_DIR$1, "logs");
|
|
15086
|
-
const PRIMARY_LOG = join(LOG_DIR, "client.log");
|
|
15087
|
-
const FALLBACK_STDOUT = join(LOG_DIR, "client.stdout.log");
|
|
15088
|
-
const FALLBACK_STDERR = join(LOG_DIR, "client.stderr.log");
|
|
15089
|
-
/**
|
|
15090
|
-
* Duration string → milliseconds. Accepts `10s`, `5m`, `2h`, `1d`; rejects
|
|
15091
|
-
* everything else. Keeps the parser tiny rather than pulling in a library —
|
|
15092
|
-
* the `--since` flag is the only consumer.
|
|
15093
|
-
*/
|
|
15094
|
-
function parseDuration(input) {
|
|
15095
|
-
const match = /^(\d+)\s*(s|m|h|d)$/.exec(input.trim());
|
|
15096
|
-
if (!match) throw new Error(`invalid duration "${input}" (expected e.g. 30s, 5m, 2h, 1d)`);
|
|
15097
|
-
return Number(match[1]) * ({
|
|
15098
|
-
s: 1e3,
|
|
15099
|
-
m: 6e4,
|
|
15100
|
-
h: 36e5,
|
|
15101
|
-
d: 864e5
|
|
15102
|
-
}[match[2]] ?? 0);
|
|
15103
|
-
}
|
|
15104
|
-
const LEVEL_RANK = {
|
|
15105
|
-
trace: 10,
|
|
15106
|
-
debug: 20,
|
|
15107
|
-
info: 30,
|
|
15108
|
-
warn: 40,
|
|
15109
|
-
error: 50,
|
|
15110
|
-
fatal: 60
|
|
15111
|
-
};
|
|
15112
|
-
/** Rotated log files, newest-first. Missing files are silently skipped. */
|
|
15113
|
-
function listLogFilesNewestFirst() {
|
|
15114
|
-
const files = [];
|
|
15115
|
-
if (existsSync(PRIMARY_LOG)) files.push(PRIMARY_LOG);
|
|
15116
|
-
for (let i = 1;; i++) {
|
|
15117
|
-
const p = `${PRIMARY_LOG}.${i}`;
|
|
15118
|
-
if (!existsSync(p)) break;
|
|
15119
|
-
files.push(p);
|
|
15120
|
-
}
|
|
15121
|
-
return files;
|
|
15122
|
-
}
|
|
15123
|
-
/** Supervisor fallback files (raw stdout/stderr, not NDJSON). Missing files skipped. */
|
|
15124
|
-
function listFallbackFiles() {
|
|
15125
|
-
const files = [];
|
|
15126
|
-
if (existsSync(FALLBACK_STDERR)) files.push(FALLBACK_STDERR);
|
|
15127
|
-
if (existsSync(FALLBACK_STDOUT)) files.push(FALLBACK_STDOUT);
|
|
15128
|
-
return files;
|
|
15129
|
-
}
|
|
15130
|
-
function matchesFilters(obj, minLevel, cutoffMs) {
|
|
15131
|
-
if (minLevel !== void 0) {
|
|
15132
|
-
const lvl = typeof obj.level === "number" ? obj.level : NaN;
|
|
15133
|
-
if (!Number.isFinite(lvl) || lvl < minLevel) return false;
|
|
15134
|
-
}
|
|
15135
|
-
if (cutoffMs !== void 0) {
|
|
15136
|
-
const t = parseLogTime(obj.time);
|
|
15137
|
-
if (t === null || t < cutoffMs) return false;
|
|
15138
|
-
}
|
|
15139
|
-
return true;
|
|
15140
|
-
}
|
|
15141
|
-
/** Logger writes `time` as a local-ish string (`YYYY-MM-DD HH:mm:ss`). */
|
|
15142
|
-
function parseLogTime(value) {
|
|
15143
|
-
if (typeof value === "number") return value;
|
|
15144
|
-
if (typeof value !== "string") return null;
|
|
15145
|
-
const iso = value.replace(" ", "T");
|
|
15146
|
-
const ms = Date.parse(iso);
|
|
15147
|
-
return Number.isFinite(ms) ? ms : null;
|
|
15148
|
-
}
|
|
15149
|
-
function renderLine(line, json) {
|
|
15150
|
-
if (!line.trim()) return null;
|
|
15151
|
-
if (json) return `${line}\n`;
|
|
15152
|
-
try {
|
|
15153
|
-
return formatPrettyEntry$1(line);
|
|
15154
|
-
} catch {
|
|
15155
|
-
return `${line}\n`;
|
|
15156
|
-
}
|
|
15157
|
-
}
|
|
15158
|
-
function processLogLine(line, minLevel, cutoffMs, json) {
|
|
15159
|
-
let obj;
|
|
15160
|
-
try {
|
|
15161
|
-
obj = JSON.parse(line);
|
|
15162
|
-
} catch {
|
|
15163
|
-
return json ? null : `${line}\n`;
|
|
15164
|
-
}
|
|
15165
|
-
if (!matchesFilters(obj, minLevel, cutoffMs)) return null;
|
|
15166
|
-
return renderLine(line, json);
|
|
15167
|
-
}
|
|
15168
|
-
async function readFileLines(path, minLevel, cutoffMs, json) {
|
|
15169
|
-
const rl = createInterface({ input: createReadStream(path, { encoding: "utf8" }) });
|
|
15170
|
-
for await (const line of rl) {
|
|
15171
|
-
const rendered = processLogLine(line, minLevel, cutoffMs, json);
|
|
15172
|
-
if (rendered) process.stdout.write(rendered);
|
|
15173
|
-
}
|
|
15174
|
-
}
|
|
15175
|
-
/**
|
|
15176
|
-
* Read a supervisor fallback file (launchd / systemd stdout/stderr capture).
|
|
15177
|
-
* These are plain text, not NDJSON: level and time filters don't apply, so we
|
|
15178
|
-
* honour `--since` by dropping the whole file when its mtime predates the
|
|
15179
|
-
* cutoff and otherwise pass every line through. In pretty mode each line is
|
|
15180
|
-
* tagged with the source so operators can tell it apart from pino output; in
|
|
15181
|
-
* `--json` mode we emit a synthetic record so NDJSON consumers keep one
|
|
15182
|
-
* object per line.
|
|
15183
|
-
*/
|
|
15184
|
-
async function readFallbackFile(path, cutoffMs, json) {
|
|
15185
|
-
try {
|
|
15186
|
-
const mtime = statSync(path).mtimeMs;
|
|
15187
|
-
if (cutoffMs !== void 0 && mtime < cutoffMs) return;
|
|
15188
|
-
} catch {
|
|
15189
|
-
return;
|
|
15190
|
-
}
|
|
15191
|
-
const source = path.endsWith(".stderr.log") ? "supervisor:stderr" : "supervisor:stdout";
|
|
15192
|
-
const rl = createInterface({ input: createReadStream(path, { encoding: "utf8" }) });
|
|
15193
|
-
for await (const line of rl) {
|
|
15194
|
-
if (!line.trim()) continue;
|
|
15195
|
-
if (json) process.stdout.write(`${JSON.stringify({
|
|
15196
|
-
source,
|
|
15197
|
-
line
|
|
15198
|
-
})}\n`);
|
|
15199
|
-
else process.stdout.write(`[${source}] ${line}\n`);
|
|
15200
|
-
}
|
|
15201
|
-
}
|
|
15202
|
-
/**
|
|
15203
|
-
* Print existing log history, applying filters. `--tail` then switches to
|
|
15204
|
-
* follow mode and keeps printing new lines as the active file grows; rotation
|
|
15205
|
-
* is not handled during the tail (a follow-up rotation will simply stop
|
|
15206
|
-
* emitting new lines — operator can re-run the command).
|
|
15207
|
-
*/
|
|
15208
|
-
async function showServiceLogs(options) {
|
|
15209
|
-
if (!existsSync(LOG_DIR)) {
|
|
15210
|
-
print.status("logs", `directory not found: ${LOG_DIR}`);
|
|
15211
|
-
return;
|
|
15212
|
-
}
|
|
15213
|
-
const minLevel = options.level ? LEVEL_RANK[options.level] : void 0;
|
|
15214
|
-
const cutoffMs = options.sinceMs !== void 0 ? Date.now() - options.sinceMs : void 0;
|
|
15215
|
-
for (const f of listFallbackFiles()) await readFallbackFile(f, cutoffMs, options.json);
|
|
15216
|
-
const files = listLogFilesNewestFirst().reverse();
|
|
15217
|
-
for (const f of files) await readFileLines(f, minLevel, cutoffMs, options.json);
|
|
15218
|
-
if (!options.tail) return;
|
|
15219
|
-
if (!existsSync(PRIMARY_LOG)) print.status("tail", "waiting for client.log to appear...");
|
|
15220
|
-
await new Promise((resolve) => {
|
|
15221
|
-
let position = existsSync(PRIMARY_LOG) ? statSync(PRIMARY_LOG).size : 0;
|
|
15222
|
-
const onChange = () => {
|
|
15223
|
-
if (!existsSync(PRIMARY_LOG)) return;
|
|
15224
|
-
const current = statSync(PRIMARY_LOG).size;
|
|
15225
|
-
if (current < position) position = 0;
|
|
15226
|
-
if (current <= position) return;
|
|
15227
|
-
const stream = createReadStream(PRIMARY_LOG, {
|
|
15228
|
-
start: position,
|
|
15229
|
-
end: current - 1,
|
|
15230
|
-
encoding: "utf8"
|
|
15231
|
-
});
|
|
15232
|
-
position = current;
|
|
15233
|
-
createInterface({ input: stream }).on("line", (line) => {
|
|
15234
|
-
const rendered = processLogLine(line, minLevel, cutoffMs, options.json);
|
|
15235
|
-
if (rendered) process.stdout.write(rendered);
|
|
15236
|
-
});
|
|
15237
|
-
};
|
|
15238
|
-
watchFile(PRIMARY_LOG, { interval: 500 }, onChange);
|
|
15239
|
-
process.once("SIGINT", () => {
|
|
15240
|
-
unwatchFile(PRIMARY_LOG, onChange);
|
|
15241
|
-
resolve();
|
|
15242
|
-
});
|
|
15243
|
-
});
|
|
15244
|
-
}
|
|
15245
|
-
/** Validated flag parsers the CLI layer can reuse without re-doing the work. */
|
|
15246
|
-
function validateLevel(value) {
|
|
15247
|
-
if (value === void 0) return void 0;
|
|
15248
|
-
const parsed = parseLogLevel$1(value);
|
|
15249
|
-
if (parsed.fellBack) throw new Error(`invalid --level "${value}" (expected one of ${LOG_LEVELS$1.join(", ")})`);
|
|
15250
|
-
return parsed.level;
|
|
15251
|
-
}
|
|
15252
|
-
//#endregion
|
|
15253
15156
|
//#region src/core/update.ts
|
|
15254
15157
|
const PACKAGE_NAME = "@agent-team-foundation/first-tree-hub";
|
|
15255
15158
|
/**
|
|
@@ -15426,4 +15329,4 @@ function createExecuteUpdate({ managed }) {
|
|
|
15426
15329
|
};
|
|
15427
15330
|
}
|
|
15428
15331
|
//#endregion
|
|
15429
|
-
export {
|
|
15332
|
+
export { resolveCliInvocation as A, resolveReplyToFromEnv as B, checkServerHealth as C, getClientServiceStatus as D, printResults as E, ClientRuntime as F, ClientOrgMismatchError$1 as G, print as H, handleClientOrgMismatch as I, SessionRegistry as J, FirstTreeHubSDK as K, rotateClientIdWithBackup as L, ensurePostgres as M, isDockerAvailable as N, installClientService as O, stopPostgres as P, createOwner as R, checkServerConfig as S, checkWebSocket as T, setJsonMode as U, blank as V, status as W, applyClientLoggerConfig as X, cleanWorkspaces as Y, configureClientLoggerForService as Z, checkBackgroundService as _, COMMAND_VERSION as a, checkDocker as b, promptMissingFields as c, onboardCheck as d, onboardCreate as f, checkAgentConfigs as g, runMigrations as h, startServer as i, uninstallClientService as j, isServiceSupported as k, formatCheckReport as l, runHomeMigration as m, declineUpdate as n, isInteractive as o, saveOnboardState as p, SdkError as q, promptUpdate as r, promptAddAgent as s, createExecuteUpdate as t, loadOnboardState as u, checkClientConfig as v, checkServerReachable as w, checkNodeVersion as x, checkDatabase as y, hasUser as z };
|