@kitecd/cli 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/index.js +12 -0
  2. package/dist/server/index.js +2827 -137
  3. package/dist/upload.js +3 -1
  4. package/dist/web/assets/AuditLog-BO9BJdsk.js +1 -0
  5. package/dist/web/assets/ConfirmDialog-CikXU818.js +1 -0
  6. package/dist/web/assets/Dashboard-ho157_Gr.js +1 -0
  7. package/dist/web/assets/DefaultLayout-Y_852d55.js +1 -0
  8. package/dist/web/assets/DefaultLayout-lj5NNciV.css +1 -0
  9. package/dist/web/assets/FileExplorer-oxT6ZdHt.js +1 -0
  10. package/dist/web/assets/FolderPickerDialog-CDO-yrvE.css +1 -0
  11. package/dist/web/assets/FolderPickerDialog-CKohwBIP.js +1 -0
  12. package/dist/web/assets/LogBoard-DkvHTXcb.js +6 -0
  13. package/dist/web/assets/LogBoard-Dx8yNofc.css +1 -0
  14. package/dist/web/assets/LogTail-CuaBDDKf.js +9 -0
  15. package/dist/web/assets/Login-kcvq2T9U.js +1 -0
  16. package/dist/web/assets/Migration-C_M_Exzf.js +1 -0
  17. package/dist/web/assets/ProjectDetail-BTOfo71A.js +1 -0
  18. package/dist/web/assets/ProjectList-D_PoTDT9.js +1 -0
  19. package/dist/web/assets/ProjectList-YJlvRRNh.css +1 -0
  20. package/dist/web/assets/ProjectTagsEditor-zXN_5rwP.js +1 -0
  21. package/dist/web/assets/Settings-BL4hNZpU.js +1 -0
  22. package/dist/web/assets/Storage-B-pZjj08.js +1 -0
  23. package/dist/web/assets/Storage-CFbfZtA5.css +1 -0
  24. package/dist/web/assets/Terminal-Bh7bM6Bb.css +1 -0
  25. package/dist/web/assets/Terminal-fTZlKf2Y.js +7 -0
  26. package/dist/web/assets/{activity-Ba-YPfmn.js → activity-DZCOq6kQ.js} +1 -1
  27. package/dist/web/assets/{archive-CJ5gDBKY.js → archive-MB1JcYug.js} +1 -1
  28. package/dist/web/assets/arrow-left-CsZYc3XK.js +1 -0
  29. package/dist/web/assets/{circle-alert-DQzM-U4P.js → circle-alert-kmcvMchB.js} +1 -1
  30. package/dist/web/assets/clock-DIBzfDoY.js +1 -0
  31. package/dist/web/assets/constants-m1eFfRMw.js +1 -0
  32. package/dist/web/assets/{copy-C1rADgcQ.js → copy-CNXWZJbC.js} +1 -1
  33. package/dist/web/assets/createLucideIcon-BmsTm7z-.js +1 -0
  34. package/dist/web/assets/{database-CI6acTSY.js → database-D6rC_24l.js} +1 -1
  35. package/dist/web/assets/{eye-off-4PD31MPV.js → eye-off-BNUfzp8P.js} +1 -1
  36. package/dist/web/assets/{eye-Cas8HsmK.js → eye-vRDqRzR9.js} +1 -1
  37. package/dist/web/assets/file-text-BxWt6is5.js +1 -0
  38. package/dist/web/assets/{folder-D9pvA6oI.js → folder-CYPJgLMr.js} +1 -1
  39. package/dist/web/assets/{folder-open-CGeIri0U.js → folder-open-DQz0XviI.js} +1 -1
  40. package/dist/web/assets/{hard-drive-CGWwV4Ei.js → hard-drive-DBiQxkNS.js} +1 -1
  41. package/dist/web/assets/history-dkZbN_TB.js +1 -0
  42. package/dist/web/assets/{house-8ZvvlaEh.js → house-D9JIOKIs.js} +1 -1
  43. package/dist/web/assets/index-BZU6i5nw.js +2 -0
  44. package/dist/web/assets/index-BrlC5Hdt.css +1 -0
  45. package/dist/web/assets/loader-circle-OwtRa1dp.js +1 -0
  46. package/dist/web/assets/pencil-BTFuj0gA.js +1 -0
  47. package/dist/web/assets/plus-CcefsCv_.js +1 -0
  48. package/dist/web/assets/{refresh-cw-0yb8DZDc.js → refresh-cw-OqaxwQhF.js} +1 -1
  49. package/dist/web/assets/{rotate-ccw-YevaWXw9.js → rotate-ccw-D8C0iiAw.js} +1 -1
  50. package/dist/web/assets/{save-BuzCP3T1.js → save-D0NTjP8Q.js} +1 -1
  51. package/dist/web/assets/{scroll-text-BIYMFNN5.js → scroll-text-6UwJwqap.js} +1 -1
  52. package/dist/web/assets/{server-B31hj0g7.js → server-DFHpqwVn.js} +1 -1
  53. package/dist/web/assets/square-terminal-C1oJyHEG.js +1 -0
  54. package/dist/web/assets/{sun-BaEbKNDV.js → sun-Cg6PdQms.js} +1 -1
  55. package/dist/web/assets/terminal-CUz5b_ol.js +1 -0
  56. package/dist/web/assets/{trash-2-BWqtqkeW.js → trash-2-20ikd6fk.js} +1 -1
  57. package/dist/web/assets/useIntervalRaf-CXWU2lqg.js +1 -0
  58. package/dist/web/index.html +3 -3
  59. package/package.json +10 -6
  60. package/scripts/postinstall.js +53 -0
  61. package/dist/web/assets/AuditLog-BFmJfgzL.js +0 -1
  62. package/dist/web/assets/ConfirmDialog-CJ8lJeUc.js +0 -1
  63. package/dist/web/assets/Dashboard-BTliTkq1.js +0 -1
  64. package/dist/web/assets/DefaultLayout-BG_y85yG.js +0 -1
  65. package/dist/web/assets/DefaultLayout-CZENO67n.css +0 -1
  66. package/dist/web/assets/FileExplorer-Bf32_MMS.js +0 -1
  67. package/dist/web/assets/LogBoard-0so-XiW3.css +0 -1
  68. package/dist/web/assets/LogBoard-CDz4n0DY.js +0 -6
  69. package/dist/web/assets/Login-C3zfObzP.js +0 -1
  70. package/dist/web/assets/Migration-BaKlcCkB.js +0 -1
  71. package/dist/web/assets/ProjectDetail-BL_D0OJY.js +0 -1
  72. package/dist/web/assets/ProjectList-7AnhOS7h.css +0 -1
  73. package/dist/web/assets/ProjectList-C4KuZEq4.js +0 -1
  74. package/dist/web/assets/Settings-CzWG9312.js +0 -1
  75. package/dist/web/assets/Storage-BCao_e7Y.js +0 -1
  76. package/dist/web/assets/Storage-DNadqpUy.css +0 -1
  77. package/dist/web/assets/arrow-left-WEOMLXIi.js +0 -1
  78. package/dist/web/assets/chevron-right-DLiDJVJl.js +0 -1
  79. package/dist/web/assets/clock-IwxBKgP4.js +0 -1
  80. package/dist/web/assets/constants-Ch47JPs3.js +0 -1
  81. package/dist/web/assets/createLucideIcon-CE-ry2oA.js +0 -1
  82. package/dist/web/assets/file-text-C6nzo1us.js +0 -1
  83. package/dist/web/assets/index-BFE6PEIL.js +0 -2
  84. package/dist/web/assets/index-XrzJwjrk.css +0 -1
  85. package/dist/web/assets/loader-circle-uiC7GaCG.js +0 -1
  86. package/dist/web/assets/plus-CfMi1Jv1.js +0 -1
  87. package/dist/web/assets/square-terminal-DT6aoXm0.js +0 -1
@@ -32076,7 +32076,7 @@ var init_node2 = __esm(() => {
32076
32076
  NodeRequestURL = class extends FastURL {
32077
32077
  #req;
32078
32078
  constructor({ req }) {
32079
- const path13 = req.url || "/";
32079
+ const path17 = req.url || "/";
32080
32080
  let host = req.headers.host || req.headers[":authority"];
32081
32081
  if (host && !HOST_RE.test(host))
32082
32082
  host = "_invalid_";
@@ -32086,15 +32086,15 @@ var init_node2 = __esm(() => {
32086
32086
  else
32087
32087
  host = "localhost";
32088
32088
  const protocol = req.socket?.encrypted || req.headers["x-forwarded-proto"] === "https" || req.headers[":scheme"] === "https" ? "https:" : "http:";
32089
- if (path13[0] === "/") {
32090
- const qIndex = path13.indexOf("?");
32089
+ if (path17[0] === "/") {
32090
+ const qIndex = path17.indexOf("?");
32091
32091
  super({
32092
32092
  protocol,
32093
32093
  host,
32094
- pathname: qIndex === -1 ? path13 : path13.slice(0, qIndex) || "/",
32095
- search: qIndex === -1 ? "" : path13.slice(qIndex) || ""
32094
+ pathname: qIndex === -1 ? path17 : path17.slice(0, qIndex) || "/",
32095
+ search: qIndex === -1 ? "" : path17.slice(qIndex) || ""
32096
32096
  });
32097
- } else if (path13 === "*")
32097
+ } else if (path17 === "*")
32098
32098
  super({
32099
32099
  protocol,
32100
32100
  host,
@@ -32102,7 +32102,7 @@ var init_node2 = __esm(() => {
32102
32102
  search: ""
32103
32103
  });
32104
32104
  else
32105
- super(path13);
32105
+ super(path17);
32106
32106
  this.#req = req;
32107
32107
  }
32108
32108
  get pathname() {
@@ -32643,7 +32643,7 @@ __export(exports_dist, {
32643
32643
  });
32644
32644
  function createWebSocketAdapter() {
32645
32645
  const store = {};
32646
- function handler(app, path13, options) {
32646
+ function handler(app, path17, options) {
32647
32647
  const { parse: parse5, body, response, ...rest3 } = options;
32648
32648
  const validateMessage = getSchemaValidator(body, {
32649
32649
  modules: app.definitions.typebox,
@@ -32668,7 +32668,7 @@ function createWebSocketAdapter() {
32668
32668
  };
32669
32669
  return ws;
32670
32670
  };
32671
- app.route("WS", path13, async (context) => {
32671
+ app.route("WS", path17, async (context) => {
32672
32672
  const { set: set22, path: path22, qi, headers, query, params } = context;
32673
32673
  const id = context.request.wsId;
32674
32674
  context.validator = validateResponse;
@@ -32885,8 +32885,8 @@ var handleFile2 = (response, set22) => {
32885
32885
  }, handleElysiaFile2 = (file2, set22 = {
32886
32886
  headers: {}
32887
32887
  }) => {
32888
- const path13 = file2.path;
32889
- const contentType = mime[path13.slice(path13.lastIndexOf(".") + 1)];
32888
+ const path17 = file2.path;
32889
+ const contentType = mime[path17.slice(path17.lastIndexOf(".") + 1)];
32890
32890
  if (contentType)
32891
32891
  set22.headers["content-type"] = contentType;
32892
32892
  if (file2.stats && set22 && set22.status !== 206 && set22.status !== 304 && set22.status !== 412 && set22.status !== 416)
@@ -33411,8 +33411,11 @@ import { drizzle } from "drizzle-orm/libsql";
33411
33411
  // src/db/schema.ts
33412
33412
  var exports_schema = {};
33413
33413
  __export(exports_schema, {
33414
+ tags: () => tags,
33414
33415
  settings: () => settings,
33415
33416
  projects: () => projects,
33417
+ projectTags: () => projectTags,
33418
+ projectLogSources: () => projectLogSources,
33416
33419
  deployments: () => deployments,
33417
33420
  categories: () => categories,
33418
33421
  auditLogs: () => auditLogs
@@ -33426,11 +33429,13 @@ var projects = sqliteTable("projects", {
33426
33429
  token: text("token").notNull().unique(),
33427
33430
  preDeployScript: text("pre_deploy_script"),
33428
33431
  postDeployScript: text("post_deploy_script"),
33432
+ postDeployAsync: integer3("post_deploy_async", { mode: "boolean" }).default(false),
33429
33433
  env: text("env"),
33430
33434
  status: text("status").default("idle"),
33431
33435
  cleanMode: text("clean_mode"),
33432
33436
  protectPaths: text("protect_paths"),
33433
33437
  categoryId: text("category_id"),
33438
+ pm2AppName: text("pm2_app_name"),
33434
33439
  createdAt: text("created_at").notNull(),
33435
33440
  updatedAt: text("updated_at").notNull()
33436
33441
  });
@@ -33460,6 +33465,29 @@ var deployments = sqliteTable("deployments", {
33460
33465
  artifactSize: integer3("artifact_size"),
33461
33466
  rollbackOf: text("rollback_of")
33462
33467
  });
33468
+ var projectLogSources = sqliteTable("project_log_sources", {
33469
+ id: text("id").primaryKey(),
33470
+ projectId: text("project_id").references(() => projects.id).notNull(),
33471
+ label: text("label").notNull(),
33472
+ filePath: text("file_path").notNull(),
33473
+ kind: text("kind").default("plain"),
33474
+ sortOrder: integer3("sort_order").default(0),
33475
+ createdAt: text("created_at").notNull(),
33476
+ updatedAt: text("updated_at").notNull()
33477
+ });
33478
+ var tags = sqliteTable("tags", {
33479
+ id: text("id").primaryKey(),
33480
+ name: text("name").notNull().unique(),
33481
+ color: text("color"),
33482
+ sortOrder: integer3("sort_order").default(0),
33483
+ createdAt: text("created_at").notNull(),
33484
+ updatedAt: text("updated_at").notNull()
33485
+ });
33486
+ var projectTags = sqliteTable("project_tags", {
33487
+ projectId: text("project_id").references(() => projects.id).notNull(),
33488
+ tagId: text("tag_id").references(() => tags.id).notNull(),
33489
+ createdAt: text("created_at").notNull()
33490
+ });
33463
33491
  var auditLogs = sqliteTable("audit_logs", {
33464
33492
  id: text("id").primaryKey(),
33465
33493
  createdAt: text("created_at").notNull(),
@@ -33492,6 +33520,7 @@ var initDb = async () => {
33492
33520
  token TEXT NOT NULL UNIQUE,
33493
33521
  pre_deploy_script TEXT,
33494
33522
  post_deploy_script TEXT,
33523
+ post_deploy_async INTEGER DEFAULT 0,
33495
33524
  env TEXT,
33496
33525
  status TEXT DEFAULT 'idle',
33497
33526
  created_at TEXT NOT NULL,
@@ -33511,6 +33540,12 @@ var initDb = async () => {
33511
33540
  await client.execute(`ALTER TABLE projects ADD COLUMN category_id TEXT`);
33512
33541
  } catch {}
33513
33542
  await client.execute(`CREATE INDEX IF NOT EXISTS idx_projects_category_id ON projects(category_id);`);
33543
+ try {
33544
+ await client.execute(`ALTER TABLE projects ADD COLUMN post_deploy_async INTEGER DEFAULT 0`);
33545
+ } catch {}
33546
+ try {
33547
+ await client.execute(`ALTER TABLE projects ADD COLUMN pm2_app_name TEXT`);
33548
+ } catch {}
33514
33549
  await client.execute(`
33515
33550
  CREATE TABLE IF NOT EXISTS categories (
33516
33551
  id TEXT PRIMARY KEY,
@@ -33522,6 +33557,27 @@ var initDb = async () => {
33522
33557
  );
33523
33558
  `);
33524
33559
  await client.execute(`CREATE INDEX IF NOT EXISTS idx_categories_sort_order ON categories(sort_order);`);
33560
+ await client.execute(`
33561
+ CREATE TABLE IF NOT EXISTS tags (
33562
+ id TEXT PRIMARY KEY,
33563
+ name TEXT NOT NULL UNIQUE,
33564
+ color TEXT,
33565
+ sort_order INTEGER DEFAULT 0,
33566
+ created_at TEXT NOT NULL,
33567
+ updated_at TEXT NOT NULL
33568
+ );
33569
+ `);
33570
+ await client.execute(`CREATE INDEX IF NOT EXISTS idx_tags_sort_order ON tags(sort_order);`);
33571
+ await client.execute(`
33572
+ CREATE TABLE IF NOT EXISTS project_tags (
33573
+ project_id TEXT NOT NULL REFERENCES projects(id),
33574
+ tag_id TEXT NOT NULL REFERENCES tags(id),
33575
+ created_at TEXT NOT NULL,
33576
+ PRIMARY KEY (project_id, tag_id)
33577
+ );
33578
+ `);
33579
+ await client.execute(`CREATE INDEX IF NOT EXISTS idx_project_tags_project_id ON project_tags(project_id);`);
33580
+ await client.execute(`CREATE INDEX IF NOT EXISTS idx_project_tags_tag_id ON project_tags(tag_id);`);
33525
33581
  await client.execute(`
33526
33582
  CREATE TABLE IF NOT EXISTS settings (
33527
33583
  key TEXT PRIMARY KEY,
@@ -33552,6 +33608,36 @@ var initDb = async () => {
33552
33608
  sql: `INSERT OR IGNORE INTO settings (key, value) VALUES (?, ?)`,
33553
33609
  args: ["artifact_keep_n", "10"]
33554
33610
  });
33611
+ const seededRow = await client.execute({
33612
+ sql: "SELECT value FROM settings WHERE key = ?",
33613
+ args: ["tags.seeded"]
33614
+ });
33615
+ if (!seededRow.rows[0]) {
33616
+ const now = new Date().toISOString();
33617
+ const seedTags = [
33618
+ { name: "前端", color: "blue" },
33619
+ { name: "后端", color: "green" },
33620
+ { name: "Node", color: "green" },
33621
+ { name: "Java", color: "yellow" },
33622
+ { name: "Go", color: "cyan" },
33623
+ { name: "Python", color: "purple" },
33624
+ { name: "PM2", color: "pink" },
33625
+ { name: "Docker", color: "cyan" },
33626
+ { name: "SSR", color: "purple" },
33627
+ { name: "静态站点", color: "gray" }
33628
+ ];
33629
+ for (let i = 0;i < seedTags.length; i++) {
33630
+ const t2 = seedTags[i];
33631
+ await client.execute({
33632
+ sql: `INSERT OR IGNORE INTO tags (id, name, color, sort_order, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
33633
+ args: ["tag_" + randomUUID().replace(/-/g, "").substring(0, 12), t2.name, t2.color, i, now, now]
33634
+ });
33635
+ }
33636
+ await client.execute({
33637
+ sql: `INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)`,
33638
+ args: ["tags.seeded", "1"]
33639
+ });
33640
+ }
33555
33641
  await client.execute(`
33556
33642
  CREATE TABLE IF NOT EXISTS deployments (
33557
33643
  id TEXT PRIMARY KEY,
@@ -33597,6 +33683,20 @@ var initDb = async () => {
33597
33683
  await client.execute(`CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at);`);
33598
33684
  await client.execute(`CREATE INDEX IF NOT EXISTS idx_audit_logs_action ON audit_logs(action);`);
33599
33685
  await client.execute(`CREATE INDEX IF NOT EXISTS idx_audit_logs_target_id ON audit_logs(target_id);`);
33686
+ await client.execute(`
33687
+ CREATE TABLE IF NOT EXISTS project_log_sources (
33688
+ id TEXT PRIMARY KEY,
33689
+ project_id TEXT NOT NULL REFERENCES projects(id),
33690
+ label TEXT NOT NULL,
33691
+ file_path TEXT NOT NULL,
33692
+ kind TEXT DEFAULT 'plain',
33693
+ sort_order INTEGER DEFAULT 0,
33694
+ created_at TEXT NOT NULL,
33695
+ updated_at TEXT NOT NULL
33696
+ );
33697
+ `);
33698
+ await client.execute(`CREATE INDEX IF NOT EXISTS idx_project_log_sources_project_id ON project_log_sources(project_id);`);
33699
+ await client.execute(`CREATE INDEX IF NOT EXISTS idx_project_log_sources_sort_order ON project_log_sources(sort_order);`);
33600
33700
  if (process.env.KITE_SEED_DEMO_PROJECT !== "false") {
33601
33701
  const existing = await client.execute("SELECT COUNT(*) as count FROM projects");
33602
33702
  const count = existing.rows[0]?.count ?? 0;
@@ -33717,10 +33817,59 @@ var db = {
33717
33817
  if (!project)
33718
33818
  return false;
33719
33819
  await ormDb.delete(deployments).where(eq(deployments.projectId, id));
33820
+ await ormDb.delete(projectLogSources).where(eq(projectLogSources.projectId, id));
33821
+ await ormDb.delete(projectTags).where(eq(projectTags.projectId, id));
33720
33822
  await ormDb.delete(projects).where(eq(projects.id, id));
33721
33823
  return true;
33722
33824
  }
33723
33825
  },
33826
+ logSources: {
33827
+ async findByProject(projectId) {
33828
+ await ensureDbReady();
33829
+ return await ormDb.select().from(projectLogSources).where(eq(projectLogSources.projectId, projectId)).orderBy(asc(projectLogSources.sortOrder), asc(projectLogSources.label));
33830
+ },
33831
+ async findById(id) {
33832
+ await ensureDbReady();
33833
+ const result = await ormDb.select().from(projectLogSources).where(eq(projectLogSources.id, id)).limit(1);
33834
+ return result[0] || null;
33835
+ },
33836
+ async create(data) {
33837
+ await ensureDbReady();
33838
+ const now = new Date().toISOString();
33839
+ const row = {
33840
+ id: "lsrc_" + randomUUID().replace(/-/g, "").substring(0, 12),
33841
+ projectId: data.projectId,
33842
+ label: data.label,
33843
+ filePath: data.filePath,
33844
+ kind: data.kind ?? "plain",
33845
+ sortOrder: data.sortOrder ?? 0,
33846
+ createdAt: now,
33847
+ updatedAt: now
33848
+ };
33849
+ await ormDb.insert(projectLogSources).values(row);
33850
+ return row;
33851
+ },
33852
+ async update(id, data) {
33853
+ await ensureDbReady();
33854
+ const patch = { updatedAt: new Date().toISOString() };
33855
+ if (data.label !== undefined)
33856
+ patch.label = data.label;
33857
+ if (data.kind !== undefined)
33858
+ patch.kind = data.kind;
33859
+ if (data.sortOrder !== undefined)
33860
+ patch.sortOrder = data.sortOrder;
33861
+ await ormDb.update(projectLogSources).set(patch).where(eq(projectLogSources.id, id));
33862
+ return this.findById(id);
33863
+ },
33864
+ async remove(id) {
33865
+ await ensureDbReady();
33866
+ const existing = await this.findById(id);
33867
+ if (!existing)
33868
+ return false;
33869
+ await ormDb.delete(projectLogSources).where(eq(projectLogSources.id, id));
33870
+ return true;
33871
+ }
33872
+ },
33724
33873
  categories: {
33725
33874
  async findAll() {
33726
33875
  await ensureDbReady();
@@ -33780,6 +33929,107 @@ var db = {
33780
33929
  return Number(result.rows[0]?.count ?? 0);
33781
33930
  }
33782
33931
  },
33932
+ tags: {
33933
+ async findAll() {
33934
+ await ensureDbReady();
33935
+ return await ormDb.select().from(tags).orderBy(asc(tags.sortOrder), asc(tags.name));
33936
+ },
33937
+ async findById(id) {
33938
+ await ensureDbReady();
33939
+ const result = await ormDb.select().from(tags).where(eq(tags.id, id)).limit(1);
33940
+ return result[0] || null;
33941
+ },
33942
+ async findByName(name) {
33943
+ await ensureDbReady();
33944
+ const result = await ormDb.select().from(tags).where(eq(tags.name, name)).limit(1);
33945
+ return result[0] || null;
33946
+ },
33947
+ async create(data) {
33948
+ await ensureDbReady();
33949
+ const now = new Date().toISOString();
33950
+ const row = {
33951
+ id: data.id || "tag_" + randomUUID().replace(/-/g, "").substring(0, 12),
33952
+ name: data.name,
33953
+ color: data.color ?? null,
33954
+ sortOrder: data.sortOrder ?? 0,
33955
+ createdAt: now,
33956
+ updatedAt: now
33957
+ };
33958
+ await ormDb.insert(tags).values(row);
33959
+ return row;
33960
+ },
33961
+ async update(id, data) {
33962
+ await ensureDbReady();
33963
+ const patch = { updatedAt: new Date().toISOString() };
33964
+ if (data.name !== undefined)
33965
+ patch.name = data.name;
33966
+ if (data.color !== undefined)
33967
+ patch.color = data.color;
33968
+ if (data.sortOrder !== undefined)
33969
+ patch.sortOrder = data.sortOrder;
33970
+ await ormDb.update(tags).set(patch).where(eq(tags.id, id));
33971
+ return this.findById(id);
33972
+ },
33973
+ async remove(id) {
33974
+ await ensureDbReady();
33975
+ const tag = await this.findById(id);
33976
+ if (!tag)
33977
+ return false;
33978
+ await ormDb.delete(projectTags).where(eq(projectTags.tagId, id));
33979
+ await ormDb.delete(tags).where(eq(tags.id, id));
33980
+ return true;
33981
+ },
33982
+ async countProjects(id) {
33983
+ await ensureDbReady();
33984
+ const result = await client.execute({
33985
+ sql: "SELECT COUNT(*) as count FROM project_tags WHERE tag_id = ?",
33986
+ args: [id]
33987
+ });
33988
+ return Number(result.rows[0]?.count ?? 0);
33989
+ }
33990
+ },
33991
+ projectTags: {
33992
+ async listByProject(projectId) {
33993
+ await ensureDbReady();
33994
+ const rows = await ormDb.select().from(projectTags).where(eq(projectTags.projectId, projectId));
33995
+ return rows.map((r) => r.tagId);
33996
+ },
33997
+ async listAllPairs() {
33998
+ await ensureDbReady();
33999
+ const rows = await ormDb.select().from(projectTags);
34000
+ return rows.map((r) => ({ projectId: r.projectId, tagId: r.tagId }));
34001
+ },
34002
+ async setForProject(projectId, tagIds) {
34003
+ await ensureDbReady();
34004
+ const now = new Date().toISOString();
34005
+ await ormDb.delete(projectTags).where(eq(projectTags.projectId, projectId));
34006
+ const unique = Array.from(new Set(tagIds.filter(Boolean)));
34007
+ for (const tagId of unique) {
34008
+ try {
34009
+ await ormDb.insert(projectTags).values({ projectId, tagId, createdAt: now });
34010
+ } catch {}
34011
+ }
34012
+ },
34013
+ async projectIdsHavingAll(tagIds) {
34014
+ await ensureDbReady();
34015
+ const ids = Array.from(new Set(tagIds.filter(Boolean)));
34016
+ if (ids.length === 0)
34017
+ return [];
34018
+ const placeholders = ids.map(() => "?").join(",");
34019
+ const res = await client.execute({
34020
+ sql: `SELECT project_id AS pid FROM project_tags
34021
+ WHERE tag_id IN (${placeholders})
34022
+ GROUP BY project_id
34023
+ HAVING COUNT(DISTINCT tag_id) = ?`,
34024
+ args: [...ids, ids.length]
34025
+ });
34026
+ return res.rows.map((r) => String(r.pid));
34027
+ },
34028
+ async clearProject(projectId) {
34029
+ await ensureDbReady();
34030
+ await ormDb.delete(projectTags).where(eq(projectTags.projectId, projectId));
34031
+ }
34032
+ },
33783
34033
  deployments: {
33784
34034
  async insert(data) {
33785
34035
  await ensureDbReady();
@@ -36373,7 +36623,7 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
36373
36623
  if (guard4.locked) {
36374
36624
  const retrySec = Math.ceil(guard4.retryAfterMs / 1000);
36375
36625
  set3.status = 429;
36376
- set3.headers = { ...set3.headers || {}, "Retry-After": String(retrySec) };
36626
+ set3.headers["Retry-After"] = String(retrySec);
36377
36627
  await writeAudit({ headers }, {
36378
36628
  action: "auth.login_failed",
36379
36629
  targetType: "auth",
@@ -36400,12 +36650,28 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
36400
36650
  return { success: false, message: "Invalid token" };
36401
36651
  }, {
36402
36652
  body: t.Object({ token: t.String() })
36403
- }).get("/api/projects", async ({ headers, set: set3 }) => {
36653
+ }).get("/api/projects", async ({ headers, query, set: set3 }) => {
36404
36654
  if (!verifyAdminToken(headers)) {
36405
36655
  set3.status = 401;
36406
36656
  return { error: "Unauthorized" };
36407
36657
  }
36408
- return await db.projects.findAllWithMeta();
36658
+ const items = await db.projects.findAllWithMeta();
36659
+ const pairs = await db.projectTags.listAllPairs();
36660
+ const tagMap = new Map;
36661
+ for (const p of pairs) {
36662
+ if (!tagMap.has(p.projectId))
36663
+ tagMap.set(p.projectId, []);
36664
+ tagMap.get(p.projectId).push(p.tagId);
36665
+ }
36666
+ const withTags = items.map((p) => ({ ...p, tagIds: tagMap.get(p.id) ?? [] }));
36667
+ const tagIdsParam = typeof query.tagIds === "string" ? query.tagIds.trim() : "";
36668
+ if (!tagIdsParam)
36669
+ return withTags;
36670
+ const wantedIds = tagIdsParam.split(",").map((s) => s.trim()).filter(Boolean);
36671
+ if (wantedIds.length === 0)
36672
+ return withTags;
36673
+ const matchIds = new Set(await db.projectTags.projectIdsHavingAll(wantedIds));
36674
+ return withTags.filter((p) => matchIds.has(p.id));
36409
36675
  }).post("/api/projects", async ({ headers, body, set: set3 }) => {
36410
36676
  if (!verifyAdminToken(headers)) {
36411
36677
  set3.status = 401;
@@ -36426,29 +36692,53 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
36426
36692
  }
36427
36693
  categoryId = cat.id;
36428
36694
  }
36695
+ const pm2AppName = typeof body.pm2AppName === "string" && body.pm2AppName.trim() !== "" ? body.pm2AppName.trim() : null;
36696
+ const tagIds = [];
36697
+ if (Array.isArray(body.tagIds)) {
36698
+ for (const tid of body.tagIds) {
36699
+ if (typeof tid !== "string" || !tid)
36700
+ continue;
36701
+ const tag = await db.tags.findById(tid);
36702
+ if (tag)
36703
+ tagIds.push(tag.id);
36704
+ }
36705
+ }
36706
+ if (pm2AppName) {
36707
+ const pm2Tag = await db.tags.findByName("PM2");
36708
+ if (pm2Tag && !tagIds.includes(pm2Tag.id)) {
36709
+ tagIds.push(pm2Tag.id);
36710
+ }
36711
+ }
36712
+ const { tagIds: _omitTagIds, ...rest3 } = body;
36429
36713
  const project = await db.projects.create({
36430
- ...body,
36714
+ ...rest3,
36431
36715
  categoryId,
36716
+ pm2AppName,
36432
36717
  id: "proj_" + randomUUID2().replace(/-/g, "").substring(0, 12),
36433
36718
  token: "kt_" + randomUUID2().replace(/-/g, "")
36434
36719
  });
36720
+ if (tagIds.length > 0) {
36721
+ await db.projectTags.setForProject(project.id, tagIds);
36722
+ }
36435
36723
  await writeAudit({ headers }, {
36436
36724
  action: "project.create",
36437
36725
  targetType: "project",
36438
36726
  targetId: project.id,
36439
36727
  targetName: project.name,
36440
36728
  before: null,
36441
- after: sanitize2(project),
36729
+ after: { ...sanitize2(project), tagIds },
36442
36730
  summary: `创建项目 ${project.name}`
36443
36731
  });
36444
- return { success: true, project };
36732
+ return { success: true, project: { ...project, tagIds } };
36445
36733
  }, {
36446
36734
  body: t.Object({
36447
36735
  name: t.String(),
36448
36736
  description: t.Optional(t.String()),
36449
36737
  deployPath: t.String(),
36450
36738
  env: t.Optional(t.String()),
36451
- categoryId: t.Optional(t.Union([t.String(), t.Null()]))
36739
+ categoryId: t.Optional(t.Union([t.String(), t.Null()])),
36740
+ pm2AppName: t.Optional(t.Union([t.String(), t.Null()])),
36741
+ tagIds: t.Optional(t.Array(t.String()))
36452
36742
  })
36453
36743
  }).get("/api/projects/:id", async ({ headers, params, set: set3 }) => {
36454
36744
  if (!verifyAdminToken(headers)) {
@@ -36460,7 +36750,8 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
36460
36750
  set3.status = 404;
36461
36751
  return { error: "Project not found" };
36462
36752
  }
36463
- return project;
36753
+ const tagIds = await db.projectTags.listByProject(params.id);
36754
+ return { ...project, tagIds };
36464
36755
  }).put("/api/projects/:id", async ({ headers, params, body, set: set3 }) => {
36465
36756
  if (!verifyAdminToken(headers)) {
36466
36757
  set3.status = 401;
@@ -36526,12 +36817,62 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
36526
36817
  return { error: "categoryId must be string or null" };
36527
36818
  }
36528
36819
  }
36820
+ if (typeof patch.pm2AppName !== "undefined") {
36821
+ if (patch.pm2AppName === null) {} else if (typeof patch.pm2AppName === "string") {
36822
+ const v = patch.pm2AppName.trim();
36823
+ patch.pm2AppName = v === "" ? null : v;
36824
+ } else {
36825
+ set3.status = 400;
36826
+ return { error: "pm2AppName must be string or null" };
36827
+ }
36828
+ }
36829
+ let nextTagIds;
36830
+ if (typeof patch.tagIds !== "undefined") {
36831
+ if (!Array.isArray(patch.tagIds)) {
36832
+ set3.status = 400;
36833
+ return { error: "tagIds must be string[]" };
36834
+ }
36835
+ const validated = [];
36836
+ for (const tid of patch.tagIds) {
36837
+ if (typeof tid !== "string" || !tid)
36838
+ continue;
36839
+ const tag = await db.tags.findById(tid);
36840
+ if (tag)
36841
+ validated.push(tag.id);
36842
+ }
36843
+ nextTagIds = validated;
36844
+ delete patch.tagIds;
36845
+ }
36529
36846
  const after = await db.projects.update(params.id, patch);
36530
36847
  if (!after) {
36531
36848
  set3.status = 404;
36532
36849
  return { error: "Project not found" };
36533
36850
  }
36851
+ const beforeTagIds = await db.projectTags.listByProject(params.id);
36852
+ const beforePm2 = (before.pm2AppName || "").trim();
36853
+ const afterPm2 = (after.pm2AppName || "").trim();
36854
+ if (afterPm2 && afterPm2 !== beforePm2) {
36855
+ const pm2Tag = await db.tags.findByName("PM2");
36856
+ if (pm2Tag) {
36857
+ const base = nextTagIds !== undefined ? nextTagIds : [...beforeTagIds];
36858
+ if (!base.includes(pm2Tag.id)) {
36859
+ base.push(pm2Tag.id);
36860
+ nextTagIds = base;
36861
+ }
36862
+ }
36863
+ }
36864
+ if (nextTagIds !== undefined) {
36865
+ await db.projectTags.setForProject(params.id, nextTagIds);
36866
+ }
36534
36867
  const diff = diffFields(before, after, Object.keys(patch));
36868
+ if (nextTagIds !== undefined) {
36869
+ const sortedBefore = [...beforeTagIds].sort();
36870
+ const sortedAfter = [...nextTagIds].sort();
36871
+ if (sortedBefore.join(",") !== sortedAfter.join(",")) {
36872
+ diff.before.tagIds = sortedBefore;
36873
+ diff.after.tagIds = sortedAfter;
36874
+ }
36875
+ }
36535
36876
  if (Object.keys(diff.after).length > 0) {
36536
36877
  await writeAudit({ headers }, {
36537
36878
  action: "project.update",
@@ -36543,18 +36884,22 @@ var deployRoutes = new Elysia().post("/api/auth/login", async ({ body, headers,
36543
36884
  summary: `更新项目配置:${Object.keys(diff.after).join(", ")}`
36544
36885
  });
36545
36886
  }
36546
- return { success: true, project: after };
36887
+ const respTagIds = nextTagIds !== undefined ? nextTagIds : beforeTagIds;
36888
+ return { success: true, project: { ...after, tagIds: respTagIds } };
36547
36889
  }, {
36548
36890
  body: t.Object({
36549
36891
  name: t.Optional(t.String()),
36550
36892
  preDeployScript: t.Optional(t.String()),
36551
36893
  postDeployScript: t.Optional(t.String()),
36894
+ postDeployAsync: t.Optional(t.Boolean()),
36552
36895
  deployPath: t.Optional(t.String()),
36553
36896
  description: t.Optional(t.String()),
36554
36897
  env: t.Optional(t.String()),
36555
36898
  cleanMode: t.Optional(t.Union([t.Literal("merge"), t.Literal("clean"), t.Literal("clean-all"), t.Null()])),
36556
36899
  protectPaths: t.Optional(t.Union([t.Array(t.String()), t.Null()])),
36557
- categoryId: t.Optional(t.Union([t.String(), t.Null()]))
36900
+ categoryId: t.Optional(t.Union([t.String(), t.Null()])),
36901
+ pm2AppName: t.Optional(t.Union([t.String(), t.Null()])),
36902
+ tagIds: t.Optional(t.Array(t.String()))
36558
36903
  })
36559
36904
  }).delete("/api/projects/:id", async ({ headers, params, set: set3 }) => {
36560
36905
  if (!verifyAdminToken(headers)) {
@@ -36690,6 +37035,19 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
36690
37035
  }
36691
37036
  const preDeployCmd = body.preDeploy || project.preDeployScript;
36692
37037
  const postDeployCmd = body.postDeploy || project.postDeployScript;
37038
+ const parseBool = (v) => {
37039
+ if (typeof v === "boolean")
37040
+ return v;
37041
+ if (typeof v === "string") {
37042
+ if (v === "true" || v === "1")
37043
+ return true;
37044
+ if (v === "false" || v === "0" || v === "")
37045
+ return false;
37046
+ }
37047
+ return;
37048
+ };
37049
+ const overrideAsync = parseBool(body.postDeployAsync);
37050
+ const postDeployAsync = overrideAsync !== undefined ? overrideAsync : Boolean(project.postDeployAsync);
36693
37051
  let deployEnv;
36694
37052
  if (body.env) {
36695
37053
  try {
@@ -36796,22 +37154,56 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
36796
37154
  await appendLog(`[Kite Deploy] Extracting files...`);
36797
37155
  new import_adm_zip.default(tempZipPath).extractAllTo(destPath, true);
36798
37156
  if (postDeployCmd) {
36799
- sendEvent(controller, "log", { data: `[Kite Deploy] Running Post-deploy: ${postDeployCmd}` });
36800
- await appendLog(`[Kite Deploy] Running Post-deploy: ${postDeployCmd}`);
36801
- let failed = false;
36802
- for await (const line of runShellCommand(postDeployCmd, destPath, deployEnv)) {
36803
- if (line.startsWith("\x00EXIT:")) {
36804
- const exitCode = parseInt(line.slice(6));
36805
- if (exitCode !== 0) {
36806
- failed = true;
37157
+ if (postDeployAsync) {
37158
+ const dispatchMsg = `[Kite Deploy] Dispatching Post-deploy asynchronously (not waiting): ${postDeployCmd}`;
37159
+ sendEvent(controller, "log", { data: dispatchMsg });
37160
+ await appendLog(dispatchMsg);
37161
+ (async () => {
37162
+ try {
37163
+ for await (const line of runShellCommand(postDeployCmd, destPath, deployEnv)) {
37164
+ if (line.startsWith("\x00EXIT:")) {
37165
+ const exitCode = parseInt(line.slice(6));
37166
+ const exitMsg = exitCode === 0 ? `[Kite Deploy] (async) Post-deploy exited with code 0` : `[Kite Deploy] (async) Post-deploy exited with code ${exitCode}`;
37167
+ await appendLog(exitMsg);
37168
+ if (exitCode !== 0) {
37169
+ try {
37170
+ await writeAudit({ headers }, {
37171
+ action: "deploy.post_deploy_failed",
37172
+ targetType: "project",
37173
+ targetId: project.id,
37174
+ targetName: project.name,
37175
+ summary: `异步 postDeploy 退出码 ${exitCode}(部署 ${deploymentRow.id})`,
37176
+ status: "failed",
37177
+ errorMessage: `Post-deploy exited with code ${exitCode}`
37178
+ });
37179
+ } catch {}
37180
+ }
37181
+ } else {
37182
+ await appendLog(line);
37183
+ }
37184
+ }
37185
+ } catch (asyncErr) {
37186
+ await appendLog(`[Kite Deploy] (async) Post-deploy error: ${asyncErr?.message || asyncErr}`);
37187
+ }
37188
+ })();
37189
+ } else {
37190
+ sendEvent(controller, "log", { data: `[Kite Deploy] Running Post-deploy: ${postDeployCmd}` });
37191
+ await appendLog(`[Kite Deploy] Running Post-deploy: ${postDeployCmd}`);
37192
+ let failed = false;
37193
+ for await (const line of runShellCommand(postDeployCmd, destPath, deployEnv)) {
37194
+ if (line.startsWith("\x00EXIT:")) {
37195
+ const exitCode = parseInt(line.slice(6));
37196
+ if (exitCode !== 0) {
37197
+ failed = true;
37198
+ }
37199
+ } else {
37200
+ sendEvent(controller, "log", { data: line });
37201
+ await appendLog(line);
36807
37202
  }
36808
- } else {
36809
- sendEvent(controller, "log", { data: line });
36810
- await appendLog(line);
36811
37203
  }
37204
+ if (failed)
37205
+ throw new Error("Post-deploy failed");
36812
37206
  }
36813
- if (failed)
36814
- throw new Error("Post-deploy failed");
36815
37207
  }
36816
37208
  await fs3.unlink(tempZipPath);
36817
37209
  const durationStr = ((Date.now() - startTime) / 1000).toFixed(1) + "s";
@@ -36866,6 +37258,7 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
36866
37258
  projectId: t.String(),
36867
37259
  preDeploy: t.Optional(t.String()),
36868
37260
  postDeploy: t.Optional(t.String()),
37261
+ postDeployAsync: t.Optional(t.Union([t.Boolean(), t.String()])),
36869
37262
  env: t.Optional(t.Any()),
36870
37263
  startedAt: t.Optional(t.String())
36871
37264
  })
@@ -36888,7 +37281,7 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
36888
37281
  }
36889
37282
  try {
36890
37283
  const entries = await fs3.readdir(targetPath, { withFileTypes: true });
36891
- const items = await Promise.all(entries.filter((e) => !e.name.startsWith(".")).map(async (entry) => {
37284
+ const items = await Promise.all(entries.map(async (entry) => {
36892
37285
  const fullPath = path5.join(targetPath, entry.name);
36893
37286
  const relativePath = subPath ? `${subPath}/${entry.name}` : entry.name;
36894
37287
  const stat2 = await fs3.stat(fullPath);
@@ -36896,6 +37289,7 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
36896
37289
  name: entry.name,
36897
37290
  path: relativePath,
36898
37291
  isDir: entry.isDirectory(),
37292
+ isHidden: entry.name.startsWith("."),
36899
37293
  size: stat2.size,
36900
37294
  mtime: stat2.mtime.toISOString()
36901
37295
  };
@@ -36903,6 +37297,8 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
36903
37297
  items.sort((a, b) => {
36904
37298
  if (a.isDir !== b.isDir)
36905
37299
  return a.isDir ? -1 : 1;
37300
+ if (a.isHidden !== b.isHidden)
37301
+ return a.isHidden ? 1 : -1;
36906
37302
  return a.name.localeCompare(b.name);
36907
37303
  });
36908
37304
  return items;
@@ -37051,6 +37447,67 @@ data: ${JSON.stringify({ status: log3.status, duration: log3.duration })}
37051
37447
  cleanMode: t.Optional(t.Union([t.Literal("merge"), t.Literal("clean"), t.Literal("clean-all"), t.Null()])),
37052
37448
  protectPaths: t.Optional(t.Array(t.String()))
37053
37449
  })
37450
+ }).patch("/api/deployments/:id/status", async ({ headers, params, body, set: set3 }) => {
37451
+ if (!verifyAdminToken(headers)) {
37452
+ set3.status = 401;
37453
+ return { error: "Unauthorized" };
37454
+ }
37455
+ const nextStatus = body.status;
37456
+ if (nextStatus !== "success" && nextStatus !== "failed") {
37457
+ set3.status = 400;
37458
+ return { error: 'status must be "success" or "failed"' };
37459
+ }
37460
+ const deployment = await db.deployments.findById(params.id);
37461
+ if (!deployment) {
37462
+ set3.status = 404;
37463
+ return { error: "Deployment not found" };
37464
+ }
37465
+ if (deployment.status !== "running") {
37466
+ set3.status = 409;
37467
+ return { error: `Deployment is already ${deployment.status}, cannot mark`, code: "NOT_RUNNING" };
37468
+ }
37469
+ const endTimeIso = new Date().toISOString();
37470
+ const startMs = new Date(deployment.startTime).getTime();
37471
+ const endMs = new Date(endTimeIso).getTime();
37472
+ const durationStr = Number.isFinite(startMs) && Number.isFinite(endMs) && endMs >= startMs ? ((endMs - startMs) / 1000).toFixed(1) + "s" : deployment.duration || "0s";
37473
+ const markLine = `[Kite] Manually marked as ${nextStatus} by admin at ${endTimeIso}`;
37474
+ const nextOutput = deployment.output ? `${deployment.output}
37475
+ ${markLine}` : markLine;
37476
+ await db.deployments.update(deployment.id, {
37477
+ status: nextStatus,
37478
+ endTime: endTimeIso,
37479
+ duration: durationStr,
37480
+ output: nextOutput
37481
+ });
37482
+ const project = await db.projects.findById(deployment.projectId);
37483
+ if (project && project.status === "running") {
37484
+ await db.projects.update(project.id, { status: nextStatus });
37485
+ }
37486
+ broadcastToSubscribers(deployment.id, "log", markLine);
37487
+ broadcastToSubscribers(deployment.id, "status", JSON.stringify({ status: nextStatus, duration: durationStr }));
37488
+ await writeAudit({ headers }, {
37489
+ action: "deployment.mark_status",
37490
+ targetType: "deployment",
37491
+ targetId: deployment.id,
37492
+ targetName: deployment.projectName,
37493
+ before: { status: "running", endTime: deployment.endTime ?? null, duration: deployment.duration ?? null },
37494
+ after: { status: nextStatus, endTime: endTimeIso, duration: durationStr },
37495
+ summary: `手动将部署 ${deployment.id.slice(0, 8)} 标记为 ${nextStatus}`
37496
+ });
37497
+ return {
37498
+ success: true,
37499
+ deployment: {
37500
+ ...deployment,
37501
+ status: nextStatus,
37502
+ endTime: endTimeIso,
37503
+ duration: durationStr,
37504
+ output: nextOutput
37505
+ }
37506
+ };
37507
+ }, {
37508
+ body: t.Object({
37509
+ status: t.Union([t.Literal("success"), t.Literal("failed")])
37510
+ })
37054
37511
  }).post("/api/deployments/:id/rollback", async ({ headers, params, set: set3 }) => {
37055
37512
  if (!verifyAdminToken(headers)) {
37056
37513
  set3.status = 401;
@@ -37235,7 +37692,7 @@ var settingsRoutes = new Elysia().get("/api/settings", async ({ headers, set: se
37235
37692
  set3.status = 401;
37236
37693
  return { error: "Unauthorized" };
37237
37694
  }
37238
- const allowed = ["webhook_url", "webhook_events", "default_deploy_path", "max_upload_size", "global_deploy_token", "artifact_keep_n"];
37695
+ const allowed = ["webhook_url", "webhook_events", "default_deploy_path", "max_upload_size", "global_deploy_token", "artifact_keep_n", "deployment_stuck_threshold_min"];
37239
37696
  const beforeAll = await db.settings.getAll();
37240
37697
  const entries = {};
37241
37698
  for (const [key, value2] of Object.entries(body)) {
@@ -37250,6 +37707,14 @@ var settingsRoutes = new Elysia().get("/api/settings", async ({ headers, set: se
37250
37707
  return { error: `global_deploy_token 强度不足:${result.reason}` };
37251
37708
  }
37252
37709
  }
37710
+ if (Object.prototype.hasOwnProperty.call(entries, "deployment_stuck_threshold_min")) {
37711
+ const n = Number(entries.deployment_stuck_threshold_min);
37712
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1 || n > 1440) {
37713
+ set3.status = 400;
37714
+ return { error: "deployment_stuck_threshold_min 必须为 1~1440 之间的整数(分钟)" };
37715
+ }
37716
+ entries.deployment_stuck_threshold_min = String(n);
37717
+ }
37253
37718
  await db.settings.setMany(entries);
37254
37719
  const afterAll = await db.settings.getAll();
37255
37720
  const changedKeys = Object.keys(entries);
@@ -37271,7 +37736,8 @@ var settingsRoutes = new Elysia().get("/api/settings", async ({ headers, set: se
37271
37736
  default_deploy_path: t.Optional(t.String()),
37272
37737
  max_upload_size: t.Optional(t.String()),
37273
37738
  global_deploy_token: t.Optional(t.String()),
37274
- artifact_keep_n: t.Optional(t.String())
37739
+ artifact_keep_n: t.Optional(t.String()),
37740
+ deployment_stuck_threshold_min: t.Optional(t.String())
37275
37741
  })
37276
37742
  }).post("/api/settings/token", async ({ headers, body, set: set3 }) => {
37277
37743
  if (!verifyAdminToken(headers)) {
@@ -37283,7 +37749,9 @@ var settingsRoutes = new Elysia().get("/api/settings", async ({ headers, set: se
37283
37749
  if (guard4.locked) {
37284
37750
  const retrySec = Math.ceil(guard4.retryAfterMs / 1000);
37285
37751
  set3.status = 429;
37286
- set3.headers = { ...set3.headers || {}, "Retry-After": String(retrySec) };
37752
+ if (!set3.headers)
37753
+ set3.headers = {};
37754
+ set3.headers["Retry-After"] = String(retrySec);
37287
37755
  return { error: `Too many attempts, please retry after ${retrySec}s` };
37288
37756
  }
37289
37757
  const { oldToken, newToken } = body;
@@ -38641,33 +39109,47 @@ var fsRoutes = new Elysia().get("/api/fs/home", ({ headers, set: set3 }) => {
38641
39109
  set3.status = 500;
38642
39110
  return { error: err?.message || "readdir failed", path: normalized };
38643
39111
  }
38644
- const dirEntries = raw.filter((e) => e.isDirectory() || e.isSymbolicLink());
38645
- const truncated = dirEntries.length > MAX_ENTRIES;
38646
- const sliced = truncated ? dirEntries.slice(0, MAX_ENTRIES) : dirEntries;
39112
+ const includeRaw = typeof query.include === "string" ? query.include : "dirs";
39113
+ const include = includeRaw === "files" || includeRaw === "both" ? includeRaw : "dirs";
39114
+ const wantedEntries = raw.filter((e) => e.isDirectory() || e.isFile() || e.isSymbolicLink());
39115
+ const truncated = wantedEntries.length > MAX_ENTRIES;
39116
+ const sliced = truncated ? wantedEntries.slice(0, MAX_ENTRIES) : wantedEntries;
38647
39117
  const entries = await Promise.all(sliced.map(async (e) => {
38648
39118
  const full = path11.join(normalized, e.name);
38649
39119
  let isDir = e.isDirectory();
39120
+ let isFile = e.isFile();
38650
39121
  const isSymlink = e.isSymbolicLink();
38651
- if (!isDir && isSymlink) {
39122
+ if (isSymlink && !isDir && !isFile) {
38652
39123
  try {
38653
39124
  const s = await fs9.stat(full);
38654
39125
  isDir = s.isDirectory();
39126
+ isFile = s.isFile();
38655
39127
  } catch {
38656
39128
  isDir = false;
39129
+ isFile = false;
38657
39130
  }
38658
39131
  }
38659
39132
  return {
38660
39133
  name: e.name,
38661
39134
  path: full,
38662
39135
  isDir,
39136
+ isFile,
38663
39137
  isHidden: e.name.startsWith("."),
38664
39138
  isSymlink
38665
39139
  };
38666
39140
  }));
38667
- const dirOnly = entries.filter((e) => e.isDir);
38668
- dirOnly.sort((a, b) => {
39141
+ const filtered = entries.filter((e) => {
39142
+ if (include === "dirs")
39143
+ return e.isDir;
39144
+ if (include === "files")
39145
+ return e.isFile;
39146
+ return e.isDir || e.isFile;
39147
+ });
39148
+ filtered.sort((a, b) => {
38669
39149
  if (a.isHidden !== b.isHidden)
38670
39150
  return a.isHidden ? 1 : -1;
39151
+ if (a.isDir !== b.isDir)
39152
+ return a.isDir ? -1 : 1;
38671
39153
  return a.name.localeCompare(b.name);
38672
39154
  });
38673
39155
  return {
@@ -38676,7 +39158,7 @@ var fsRoutes = new Elysia().get("/api/fs/home", ({ headers, set: set3 }) => {
38676
39158
  exists: true,
38677
39159
  isDir: true,
38678
39160
  truncated,
38679
- entries: dirOnly
39161
+ entries: filtered
38680
39162
  };
38681
39163
  });
38682
39164
 
@@ -38842,58 +39324,2197 @@ var categoryRoutes = new Elysia().get("/api/categories", async ({ headers, set:
38842
39324
  return { success: true, detachedProjects: projectCount };
38843
39325
  });
38844
39326
 
38845
- // src/static.ts
39327
+ // src/routes/log-sources.ts
38846
39328
  init_dist3();
39329
+ import path13 from "node:path";
39330
+
39331
+ // src/lib/log-tail.ts
39332
+ import fs10 from "node:fs";
39333
+ import fsp from "node:fs/promises";
38847
39334
  import path12 from "node:path";
38848
- var MIME_TYPES = {
38849
- ".html": "text/html; charset=utf-8",
38850
- ".css": "text/css; charset=utf-8",
38851
- ".js": "application/javascript; charset=utf-8",
38852
- ".json": "application/json; charset=utf-8",
38853
- ".svg": "image/svg+xml",
38854
- ".png": "image/png",
38855
- ".jpg": "image/jpeg",
38856
- ".jpeg": "image/jpeg",
38857
- ".gif": "image/gif",
38858
- ".ico": "image/x-icon",
38859
- ".woff": "font/woff",
38860
- ".woff2": "font/woff2",
38861
- ".ttf": "font/ttf",
38862
- ".eot": "application/vnd.ms-fontobject",
38863
- ".map": "application/json",
38864
- ".txt": "text/plain; charset=utf-8",
38865
- ".xml": "application/xml"
38866
- };
38867
- function getMimeType(filePath) {
38868
- const ext2 = path12.extname(filePath).toLowerCase();
38869
- return MIME_TYPES[ext2] || "application/octet-stream";
39335
+ import os5 from "node:os";
39336
+ import readline from "node:readline";
39337
+ var log6 = moduleLogger("log-tail");
39338
+ var MAX_RANGE_SIZE = 1024 * 1024;
39339
+ var DEFAULT_RANGE_SIZE = 64 * 1024;
39340
+ var MAX_TAIL_LINES = 5000;
39341
+ var DEFAULT_TAIL_LINES = 200;
39342
+ var MAX_SEARCH_HITS = 5000;
39343
+ var DEFAULT_SEARCH_HITS = 500;
39344
+ var DEFAULT_SEARCH_CONTEXT = 2;
39345
+ var STREAM_HEARTBEAT_MS = 15000;
39346
+ var STREAM_POLL_MS = 1500;
39347
+ var STREAM_FLUSH_MS = 500;
39348
+ function getBlacklist() {
39349
+ const home = process.env.KITE_HOME || path12.join(os5.homedir(), ".kite");
39350
+ return [
39351
+ path12.join(home, "config.json"),
39352
+ path12.join(home, "kite.db"),
39353
+ "/etc/shadow",
39354
+ "/etc/passwd",
39355
+ "/etc/master.passwd"
39356
+ ];
38870
39357
  }
38871
- var staticPlugin = new Elysia().get("/*", async ({ request, set: set3 }) => {
38872
- const webDir = process.env.KITE_WEB_DIR;
38873
- const url = new URL(request.url);
38874
- let pathname = url.pathname;
38875
- if (!webDir) {
38876
- if (pathname === "/")
38877
- return "Deploy Server is running!";
38878
- set3.status = 404;
38879
- return { error: "Web console not available" };
39358
+ async function pathGuard(rawPath, opts = {}) {
39359
+ if (typeof rawPath !== "string" || !rawPath) {
39360
+ return { ok: false, status: 400, error: "path is required" };
38880
39361
  }
38881
- const filePath = path12.resolve(webDir, "." + pathname);
38882
- if (!filePath.startsWith(path12.resolve(webDir))) {
38883
- set3.status = 403;
38884
- return { error: "Access denied" };
39362
+ if (!path12.isAbsolute(rawPath)) {
39363
+ return { ok: false, status: 400, error: "path must be absolute" };
38885
39364
  }
38886
- const { stat: stat2, access } = await import("node:fs/promises");
39365
+ let resolved = path12.resolve(rawPath);
38887
39366
  try {
38888
- const fileStat = await stat2(filePath);
38889
- if (fileStat.isFile()) {
38890
- const buffer = await readFileBuffer(filePath);
38891
- set3.headers["Content-Type"] = getMimeType(filePath);
38892
- set3.headers["Cache-Control"] = pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache";
38893
- return new Response(buffer);
38894
- }
39367
+ resolved = await fsp.realpath(resolved);
39368
+ } catch (err) {
39369
+ if (err?.code === "ENOENT") {
39370
+ if (opts.allowMissing)
39371
+ return { ok: true, resolved };
39372
+ return { ok: false, status: 404, error: "path not found", resolved };
39373
+ }
39374
+ if (err?.code === "EACCES" || err?.code === "EPERM") {
39375
+ return { ok: false, status: 403, error: "permission denied", resolved };
39376
+ }
39377
+ return { ok: false, status: 500, error: err?.message || "realpath failed", resolved };
39378
+ }
39379
+ const blacklist = getBlacklist();
39380
+ for (const blocked of blacklist) {
39381
+ if (resolved === blocked) {
39382
+ return { ok: false, status: 403, error: "path is blacklisted", resolved };
39383
+ }
39384
+ }
39385
+ let stat2;
39386
+ try {
39387
+ stat2 = await fsp.stat(resolved);
39388
+ } catch (err) {
39389
+ if (err?.code === "ENOENT")
39390
+ return { ok: false, status: 404, error: "path not found", resolved };
39391
+ if (err?.code === "EACCES" || err?.code === "EPERM") {
39392
+ return { ok: false, status: 403, error: "permission denied", resolved };
39393
+ }
39394
+ return { ok: false, status: 500, error: err?.message || "stat failed", resolved };
39395
+ }
39396
+ if (!stat2.isFile()) {
39397
+ return { ok: false, status: 400, error: "path is not a regular file", resolved };
39398
+ }
39399
+ return { ok: true, resolved, size: stat2.size };
39400
+ }
39401
+ function isBinarySample(buf) {
39402
+ if (buf.length === 0)
39403
+ return false;
39404
+ let nul = 0;
39405
+ let ctrl = 0;
39406
+ const sample = buf.subarray(0, Math.min(buf.length, 4096));
39407
+ for (let i = 0;i < sample.length; i++) {
39408
+ const b = sample[i];
39409
+ if (b === 0)
39410
+ nul++;
39411
+ else if (b < 9 || b > 13 && b < 32)
39412
+ ctrl++;
39413
+ }
39414
+ if (nul / sample.length > 0.01)
39415
+ return true;
39416
+ if (ctrl / sample.length > 0.3)
39417
+ return true;
39418
+ return false;
39419
+ }
39420
+ async function readTail(filePath, maxLines) {
39421
+ const fh = await fsp.open(filePath, "r");
39422
+ try {
39423
+ const stat2 = await fh.stat();
39424
+ const size = stat2.size;
39425
+ if (size === 0)
39426
+ return { lines: [], binary: false, size, startOffset: 0 };
39427
+ const chunkSize = 64 * 1024;
39428
+ let position = size;
39429
+ const chunks = [];
39430
+ let lineCount = 0;
39431
+ let binary = false;
39432
+ let sampled = false;
39433
+ while (position > 0 && lineCount <= maxLines) {
39434
+ const readSize = Math.min(chunkSize, position);
39435
+ position -= readSize;
39436
+ const buf = Buffer.alloc(readSize);
39437
+ await fh.read(buf, 0, readSize, position);
39438
+ if (!sampled) {
39439
+ binary = isBinarySample(buf);
39440
+ sampled = true;
39441
+ if (binary)
39442
+ return { lines: [], binary: true, size, startOffset: size };
39443
+ }
39444
+ chunks.unshift(buf);
39445
+ for (let i = 0;i < buf.length; i++) {
39446
+ if (buf[i] === 10)
39447
+ lineCount++;
39448
+ }
39449
+ }
39450
+ const all = Buffer.concat(chunks).toString("utf8");
39451
+ const allLines = all.split(`
39452
+ `);
39453
+ if (allLines.length > 0 && allLines[allLines.length - 1] === "")
39454
+ allLines.pop();
39455
+ const sliced = allLines.length > maxLines ? allLines.slice(allLines.length - maxLines) : allLines;
39456
+ return { lines: sliced, binary: false, size, startOffset: position };
39457
+ } finally {
39458
+ await fh.close();
39459
+ }
39460
+ }
39461
+ async function readRange(filePath, opts = {}) {
39462
+ const fh = await fsp.open(filePath, "r");
39463
+ try {
39464
+ const stat2 = await fh.stat();
39465
+ const fileSize = stat2.size;
39466
+ const winSize = Math.max(1024, Math.min(opts.size ?? DEFAULT_RANGE_SIZE, MAX_RANGE_SIZE));
39467
+ const direction = opts.direction ?? (opts.offset === undefined ? "tail" : "forward");
39468
+ if (fileSize === 0) {
39469
+ return { startOffset: 0, endOffset: 0, fileSize, lines: [], truncatedHead: false, truncatedTail: false, binary: false };
39470
+ }
39471
+ let start;
39472
+ if (direction === "tail") {
39473
+ start = Math.max(0, fileSize - winSize);
39474
+ } else if (direction === "backward") {
39475
+ const o = opts.offset ?? fileSize;
39476
+ start = Math.max(0, o - winSize);
39477
+ } else {
39478
+ start = Math.max(0, Math.min(opts.offset ?? 0, fileSize));
39479
+ }
39480
+ let end = Math.min(start + winSize, fileSize);
39481
+ const len = end - start;
39482
+ if (len <= 0) {
39483
+ return { startOffset: start, endOffset: end, fileSize, lines: [], truncatedHead: false, truncatedTail: false, binary: false };
39484
+ }
39485
+ const buf = Buffer.alloc(len);
39486
+ await fh.read(buf, 0, len, start);
39487
+ if (isBinarySample(buf)) {
39488
+ return { startOffset: start, endOffset: end, fileSize, lines: [], truncatedHead: false, truncatedTail: false, binary: true };
39489
+ }
39490
+ let truncatedHead = false;
39491
+ let truncatedTail = false;
39492
+ let viewStart = 0;
39493
+ let viewEnd = buf.length;
39494
+ if (start > 0) {
39495
+ const nl = buf.indexOf(10);
39496
+ if (nl >= 0) {
39497
+ viewStart = nl + 1;
39498
+ truncatedHead = true;
39499
+ } else {
39500
+ return {
39501
+ startOffset: start,
39502
+ endOffset: end,
39503
+ fileSize,
39504
+ lines: [],
39505
+ truncatedHead: true,
39506
+ truncatedTail: end < fileSize,
39507
+ binary: false
39508
+ };
39509
+ }
39510
+ }
39511
+ if (end < fileSize) {
39512
+ const nl = buf.lastIndexOf(10);
39513
+ if (nl >= viewStart) {
39514
+ viewEnd = nl + 1;
39515
+ truncatedTail = true;
39516
+ } else {
39517
+ return {
39518
+ startOffset: start,
39519
+ endOffset: end,
39520
+ fileSize,
39521
+ lines: [],
39522
+ truncatedHead,
39523
+ truncatedTail: true,
39524
+ binary: false
39525
+ };
39526
+ }
39527
+ }
39528
+ const text2 = buf.subarray(viewStart, viewEnd).toString("utf8");
39529
+ const lines = text2.split(`
39530
+ `);
39531
+ if (lines.length > 0 && lines[lines.length - 1] === "")
39532
+ lines.pop();
39533
+ return {
39534
+ startOffset: start + viewStart,
39535
+ endOffset: start + viewEnd,
39536
+ fileSize,
39537
+ lines,
39538
+ truncatedHead,
39539
+ truncatedTail,
39540
+ binary: false
39541
+ };
39542
+ } finally {
39543
+ await fh.close();
39544
+ }
39545
+ }
39546
+ function watchTail(filePath, initialOffset, cb) {
39547
+ let lastSize = initialOffset;
39548
+ let closed = false;
39549
+ let reading = false;
39550
+ let pending = false;
39551
+ const handleChange = async () => {
39552
+ if (closed)
39553
+ return;
39554
+ if (reading) {
39555
+ pending = true;
39556
+ return;
39557
+ }
39558
+ reading = true;
39559
+ try {
39560
+ let stat2;
39561
+ try {
39562
+ stat2 = await fsp.stat(filePath);
39563
+ } catch (err) {
39564
+ if (err?.code === "ENOENT") {
39565
+ if (lastSize !== 0) {
39566
+ lastSize = 0;
39567
+ cb.onRotate();
39568
+ }
39569
+ return;
39570
+ }
39571
+ throw err;
39572
+ }
39573
+ const newSize = stat2.size;
39574
+ if (newSize < lastSize) {
39575
+ lastSize = 0;
39576
+ cb.onRotate();
39577
+ return;
39578
+ }
39579
+ if (newSize === lastSize)
39580
+ return;
39581
+ const fh = await fsp.open(filePath, "r");
39582
+ try {
39583
+ const len = newSize - lastSize;
39584
+ const buf = Buffer.alloc(len);
39585
+ await fh.read(buf, 0, len, lastSize);
39586
+ lastSize = newSize;
39587
+ cb.onAppend(buf.toString("utf8"), newSize);
39588
+ } finally {
39589
+ await fh.close();
39590
+ }
39591
+ } catch (err) {
39592
+ cb.onError?.(err);
39593
+ } finally {
39594
+ reading = false;
39595
+ if (pending) {
39596
+ pending = false;
39597
+ setImmediate(handleChange);
39598
+ }
39599
+ }
39600
+ };
39601
+ let watcher = null;
39602
+ try {
39603
+ watcher = fs10.watch(filePath, { persistent: false }, () => {
39604
+ handleChange();
39605
+ });
39606
+ watcher.on("error", (err) => {
39607
+ cb.onError?.(err);
39608
+ });
39609
+ } catch (err) {
39610
+ log6.warn({ filePath, err: err.message }, "fs.watch failed; relying on polling");
39611
+ }
39612
+ const timer = setInterval(handleChange, STREAM_POLL_MS);
39613
+ if (typeof timer.unref === "function")
39614
+ timer.unref();
39615
+ return {
39616
+ close() {
39617
+ if (closed)
39618
+ return;
39619
+ closed = true;
39620
+ try {
39621
+ watcher?.close();
39622
+ } catch {}
39623
+ clearInterval(timer);
39624
+ }
39625
+ };
39626
+ }
39627
+ function grepStream(filePath, opts, cb) {
39628
+ const maxHits = Math.max(1, Math.min(opts.maxHits ?? DEFAULT_SEARCH_HITS, MAX_SEARCH_HITS));
39629
+ const context = Math.max(0, Math.min(opts.context ?? DEFAULT_SEARCH_CONTEXT, 20));
39630
+ let matcher;
39631
+ if (opts.regex) {
39632
+ let re;
39633
+ try {
39634
+ re = new RegExp(opts.q, opts.caseInsensitive ? "i" : "");
39635
+ } catch (err) {
39636
+ cb.onError(new Error(`invalid regex: ${err?.message || err}`));
39637
+ return { abort() {} };
39638
+ }
39639
+ matcher = (line) => re.test(line);
39640
+ } else if (opts.caseInsensitive) {
39641
+ const needle = opts.q.toLowerCase();
39642
+ matcher = (line) => line.toLowerCase().includes(needle);
39643
+ } else {
39644
+ const needle = opts.q;
39645
+ matcher = (line) => line.includes(needle);
39646
+ }
39647
+ const start = Math.max(0, opts.fromOffset ?? 0);
39648
+ const end = opts.toOffset !== undefined ? Math.max(start, opts.toOffset) : undefined;
39649
+ const readStream = fs10.createReadStream(filePath, end === undefined ? { start } : { start, end });
39650
+ const rl = readline.createInterface({ input: readStream, crlfDelay: Infinity });
39651
+ let bytesSeen = start;
39652
+ let hits = 0;
39653
+ let aborted = false;
39654
+ const beforeBuf = [];
39655
+ const pendingAfter = [];
39656
+ function flushPending(currentLine) {
39657
+ if (!currentLine) {
39658
+ for (const p of pendingAfter)
39659
+ cb.onHit(p.hit);
39660
+ pendingAfter.length = 0;
39661
+ return;
39662
+ }
39663
+ const stillPending = [];
39664
+ for (const p of pendingAfter) {
39665
+ p.hit.after.push(currentLine.text);
39666
+ p.need -= 1;
39667
+ if (p.need <= 0)
39668
+ cb.onHit(p.hit);
39669
+ else
39670
+ stillPending.push(p);
39671
+ }
39672
+ pendingAfter.length = 0;
39673
+ pendingAfter.push(...stillPending);
39674
+ }
39675
+ function pushBefore(entry) {
39676
+ beforeBuf.push(entry);
39677
+ if (beforeBuf.length > context)
39678
+ beforeBuf.shift();
39679
+ }
39680
+ rl.on("line", (line) => {
39681
+ if (aborted)
39682
+ return;
39683
+ if (opts.signal?.aborted) {
39684
+ abort();
39685
+ return;
39686
+ }
39687
+ const lineOffset = bytesSeen;
39688
+ const byteLen = Buffer.byteLength(line, "utf8") + 1;
39689
+ bytesSeen += byteLen;
39690
+ flushPending({ offset: lineOffset, text: line });
39691
+ if (matcher(line)) {
39692
+ const hit = {
39693
+ offset: lineOffset,
39694
+ text: line,
39695
+ before: beforeBuf.map((b) => b.text),
39696
+ after: []
39697
+ };
39698
+ if (context > 0) {
39699
+ pendingAfter.push({ hit, need: context });
39700
+ } else {
39701
+ cb.onHit(hit);
39702
+ }
39703
+ hits += 1;
39704
+ if (hits >= maxHits) {
39705
+ cb.onTruncated();
39706
+ abort();
39707
+ return;
39708
+ }
39709
+ }
39710
+ pushBefore({ offset: lineOffset, text: line });
39711
+ });
39712
+ rl.on("close", () => {
39713
+ if (aborted)
39714
+ return;
39715
+ flushPending(null);
39716
+ cb.onDone(bytesSeen - start);
39717
+ });
39718
+ readStream.on("error", (err) => {
39719
+ if (aborted)
39720
+ return;
39721
+ aborted = true;
39722
+ cb.onError(err);
39723
+ });
39724
+ function abort() {
39725
+ if (aborted)
39726
+ return;
39727
+ aborted = true;
39728
+ try {
39729
+ rl.close();
39730
+ } catch {}
39731
+ try {
39732
+ readStream.destroy();
39733
+ } catch {}
39734
+ flushPending(null);
39735
+ cb.onDone(bytesSeen - start);
39736
+ }
39737
+ if (opts.signal) {
39738
+ if (opts.signal.aborted)
39739
+ abort();
39740
+ else
39741
+ opts.signal.addEventListener("abort", abort, { once: true });
39742
+ }
39743
+ return { abort };
39744
+ }
39745
+
39746
+ // src/routes/log-sources.ts
39747
+ var log7 = moduleLogger("log-sources");
39748
+ var VALID_KINDS = new Set(["pm2", "nginx", "plain"]);
39749
+ function normalizeKind(input) {
39750
+ if (typeof input !== "string")
39751
+ return "plain";
39752
+ return VALID_KINDS.has(input) ? input : "plain";
39753
+ }
39754
+ function basename(p) {
39755
+ return path13.basename(p) || p;
39756
+ }
39757
+ function toInt(v, fallback) {
39758
+ if (typeof v === "number" && Number.isFinite(v))
39759
+ return Math.trunc(v);
39760
+ if (typeof v === "string" && v.trim() !== "") {
39761
+ const n = Number(v);
39762
+ if (Number.isFinite(n))
39763
+ return Math.trunc(n);
39764
+ }
39765
+ return fallback;
39766
+ }
39767
+ function sseLine(event, data) {
39768
+ return new TextEncoder().encode(`event: ${event}
39769
+ data: ${JSON.stringify(data)}
39770
+
39771
+ `);
39772
+ }
39773
+ function sseComment(text2) {
39774
+ return new TextEncoder().encode(`: ${text2}
39775
+
39776
+ `);
39777
+ }
39778
+ var logSourceRoutes = new Elysia().get("/api/projects/:id/log-sources", async ({ headers, params, set: set3 }) => {
39779
+ if (!verifyAdminToken(headers)) {
39780
+ set3.status = 401;
39781
+ return { error: "Unauthorized" };
39782
+ }
39783
+ const project = await db.projects.findById(params.id);
39784
+ if (!project) {
39785
+ set3.status = 404;
39786
+ return { error: "Project not found" };
39787
+ }
39788
+ const sources = await db.logSources.findByProject(params.id);
39789
+ return { items: sources };
39790
+ }).post("/api/projects/:id/log-sources", async ({ headers, params, body, set: set3 }) => {
39791
+ if (!verifyAdminToken(headers)) {
39792
+ set3.status = 401;
39793
+ return { error: "Unauthorized" };
39794
+ }
39795
+ const project = await db.projects.findById(params.id);
39796
+ if (!project) {
39797
+ set3.status = 404;
39798
+ return { error: "Project not found" };
39799
+ }
39800
+ const rawItems = Array.isArray(body?.items) ? body.items : null;
39801
+ if (!rawItems || rawItems.length === 0) {
39802
+ set3.status = 400;
39803
+ return { error: "items is required (array of {label, filePath, kind?})" };
39804
+ }
39805
+ if (rawItems.length > 50) {
39806
+ set3.status = 400;
39807
+ return { error: "too many items in one request (max 50)" };
39808
+ }
39809
+ const created = [];
39810
+ const errors2 = [];
39811
+ for (let i = 0;i < rawItems.length; i++) {
39812
+ const item = rawItems[i] || {};
39813
+ const filePath = typeof item.filePath === "string" ? item.filePath : "";
39814
+ if (!filePath) {
39815
+ errors2.push({ index: i, error: "filePath is required" });
39816
+ continue;
39817
+ }
39818
+ const guard4 = await pathGuard(filePath, { allowMissing: true });
39819
+ if (!guard4.ok) {
39820
+ errors2.push({ index: i, error: guard4.error || "invalid path" });
39821
+ continue;
39822
+ }
39823
+ const label = typeof item.label === "string" && item.label.trim() ? item.label.trim() : basename(filePath);
39824
+ const kind = normalizeKind(item.kind);
39825
+ const row = await db.logSources.create({
39826
+ projectId: params.id,
39827
+ label,
39828
+ filePath: guard4.resolved || filePath,
39829
+ kind
39830
+ });
39831
+ created.push(row);
39832
+ }
39833
+ if (created.length === 0) {
39834
+ set3.status = 400;
39835
+ return { error: "no item created", details: errors2 };
39836
+ }
39837
+ return { created, errors: errors2 };
39838
+ }).patch("/api/log-sources/:sourceId", async ({ headers, params, body, set: set3 }) => {
39839
+ if (!verifyAdminToken(headers)) {
39840
+ set3.status = 401;
39841
+ return { error: "Unauthorized" };
39842
+ }
39843
+ const existing = await db.logSources.findById(params.sourceId);
39844
+ if (!existing) {
39845
+ set3.status = 404;
39846
+ return { error: "Log source not found" };
39847
+ }
39848
+ const patch = {};
39849
+ if (typeof body?.label === "string") {
39850
+ const trimmed = body.label.trim();
39851
+ if (!trimmed) {
39852
+ set3.status = 400;
39853
+ return { error: "label cannot be empty" };
39854
+ }
39855
+ patch.label = trimmed;
39856
+ }
39857
+ if (body?.kind !== undefined) {
39858
+ patch.kind = normalizeKind(body.kind);
39859
+ }
39860
+ if (body?.sortOrder !== undefined) {
39861
+ patch.sortOrder = toInt(body.sortOrder, existing.sortOrder ?? 0);
39862
+ }
39863
+ const updated = await db.logSources.update(params.sourceId, patch);
39864
+ return updated;
39865
+ }).delete("/api/log-sources/:sourceId", async ({ headers, params, set: set3 }) => {
39866
+ if (!verifyAdminToken(headers)) {
39867
+ set3.status = 401;
39868
+ return { error: "Unauthorized" };
39869
+ }
39870
+ const ok = await db.logSources.remove(params.sourceId);
39871
+ if (!ok) {
39872
+ set3.status = 404;
39873
+ return { error: "Log source not found" };
39874
+ }
39875
+ return { ok: true };
39876
+ }).get("/api/log-sources/:sourceId/meta", async ({ headers, params, set: set3 }) => {
39877
+ if (!verifyAdminToken(headers)) {
39878
+ set3.status = 401;
39879
+ return { error: "Unauthorized" };
39880
+ }
39881
+ const src = await db.logSources.findById(params.sourceId);
39882
+ if (!src) {
39883
+ set3.status = 404;
39884
+ return { error: "Log source not found" };
39885
+ }
39886
+ const guard4 = await pathGuard(src.filePath);
39887
+ if (!guard4.ok) {
39888
+ set3.status = guard4.status || 500;
39889
+ return { error: guard4.error || "path error" };
39890
+ }
39891
+ return {
39892
+ id: src.id,
39893
+ label: src.label,
39894
+ filePath: src.filePath,
39895
+ resolvedPath: guard4.resolved,
39896
+ kind: src.kind ?? "plain",
39897
+ size: guard4.size ?? 0
39898
+ };
39899
+ }).get("/api/log-sources/:sourceId/range", async ({ headers, params, query, set: set3 }) => {
39900
+ if (!verifyAdminToken(headers)) {
39901
+ set3.status = 401;
39902
+ return { error: "Unauthorized" };
39903
+ }
39904
+ const src = await db.logSources.findById(params.sourceId);
39905
+ if (!src) {
39906
+ set3.status = 404;
39907
+ return { error: "Log source not found" };
39908
+ }
39909
+ const guard4 = await pathGuard(src.filePath);
39910
+ if (!guard4.ok) {
39911
+ set3.status = guard4.status || 500;
39912
+ return { error: guard4.error || "path error" };
39913
+ }
39914
+ const rawOffset = query.offset;
39915
+ const rawDirection = query.direction;
39916
+ const size = Math.max(1024, Math.min(toInt(query.size, DEFAULT_RANGE_SIZE), MAX_RANGE_SIZE));
39917
+ const direction = rawDirection === "backward" || rawDirection === "forward" || rawDirection === "tail" ? rawDirection : rawOffset === undefined ? "tail" : "forward";
39918
+ const offset = rawOffset === undefined ? undefined : Math.max(0, toInt(rawOffset, 0));
39919
+ try {
39920
+ const result = await readRange(guard4.resolved, { offset, size, direction });
39921
+ return result;
39922
+ } catch (err) {
39923
+ log7.warn({ err: err?.message, sourceId: src.id }, "readRange failed");
39924
+ set3.status = 500;
39925
+ return { error: err?.message || "read failed" };
39926
+ }
39927
+ }).get("/api/log-sources/:sourceId/stream", async ({ headers, params, query, set: set3, request }) => {
39928
+ if (!verifyAdminToken(headers)) {
39929
+ set3.status = 401;
39930
+ return new Response("Unauthorized", { status: 401 });
39931
+ }
39932
+ const src = await db.logSources.findById(params.sourceId);
39933
+ if (!src) {
39934
+ set3.status = 404;
39935
+ return new Response("Not found", { status: 404 });
39936
+ }
39937
+ const guard4 = await pathGuard(src.filePath);
39938
+ if (!guard4.ok) {
39939
+ set3.status = guard4.status || 500;
39940
+ return new Response(guard4.error || "path error", { status: guard4.status || 500 });
39941
+ }
39942
+ const tailLines = Math.max(1, Math.min(toInt(query.tailLines, DEFAULT_TAIL_LINES), MAX_TAIL_LINES));
39943
+ const filePath = guard4.resolved;
39944
+ let watcher = null;
39945
+ let heartbeat = null;
39946
+ let flushTimer = null;
39947
+ let pendingLines = [];
39948
+ let closed = false;
39949
+ let controllerRef = null;
39950
+ const safeEnqueue = (chunk) => {
39951
+ if (closed || !controllerRef)
39952
+ return;
39953
+ try {
39954
+ controllerRef.enqueue(chunk);
39955
+ } catch {}
39956
+ };
39957
+ const flush = () => {
39958
+ if (flushTimer) {
39959
+ clearTimeout(flushTimer);
39960
+ flushTimer = null;
39961
+ }
39962
+ if (pendingLines.length === 0)
39963
+ return;
39964
+ const batch = pendingLines;
39965
+ pendingLines = [];
39966
+ safeEnqueue(sseLine("lines", { lines: batch }));
39967
+ };
39968
+ const scheduleFlush = () => {
39969
+ if (flushTimer)
39970
+ return;
39971
+ flushTimer = setTimeout(flush, STREAM_FLUSH_MS);
39972
+ };
39973
+ const cleanup = () => {
39974
+ if (closed)
39975
+ return;
39976
+ closed = true;
39977
+ if (heartbeat)
39978
+ clearInterval(heartbeat);
39979
+ if (flushTimer)
39980
+ clearTimeout(flushTimer);
39981
+ if (watcher) {
39982
+ try {
39983
+ watcher.close();
39984
+ } catch {}
39985
+ }
39986
+ try {
39987
+ controllerRef?.close();
39988
+ } catch {}
39989
+ };
39990
+ const stream = new ReadableStream({
39991
+ start(controller) {
39992
+ controllerRef = controller;
39993
+ (async () => {
39994
+ try {
39995
+ const initial = await readTail(filePath, tailLines);
39996
+ safeEnqueue(sseLine("snapshot", {
39997
+ lines: initial.lines,
39998
+ size: initial.size,
39999
+ binary: initial.binary
40000
+ }));
40001
+ if (initial.binary) {
40002
+ cleanup();
40003
+ return;
40004
+ }
40005
+ watcher = watchTail(filePath, initial.size, {
40006
+ onAppend: (chunk) => {
40007
+ if (closed)
40008
+ return;
40009
+ const parts = chunk.split(`
40010
+ `);
40011
+ if (parts.length > 0 && parts[parts.length - 1] === "")
40012
+ parts.pop();
40013
+ if (parts.length === 0)
40014
+ return;
40015
+ if (pendingLines.length + parts.length > 2000) {
40016
+ pendingLines = pendingLines.concat(parts).slice(-2000);
40017
+ } else {
40018
+ pendingLines.push(...parts);
40019
+ }
40020
+ scheduleFlush();
40021
+ },
40022
+ onRotate: () => {
40023
+ if (closed)
40024
+ return;
40025
+ flush();
40026
+ safeEnqueue(sseLine("rotated", { at: new Date().toISOString() }));
40027
+ readTail(filePath, tailLines).then((again) => {
40028
+ if (closed)
40029
+ return;
40030
+ safeEnqueue(sseLine("snapshot", {
40031
+ lines: again.lines,
40032
+ size: again.size,
40033
+ binary: again.binary
40034
+ }));
40035
+ }).catch((err) => {
40036
+ safeEnqueue(sseLine("error", { message: err?.message || "rotate read failed" }));
40037
+ });
40038
+ },
40039
+ onError: (err) => {
40040
+ if (closed)
40041
+ return;
40042
+ safeEnqueue(sseLine("error", { message: err?.message || "watch error" }));
40043
+ }
40044
+ });
40045
+ heartbeat = setInterval(() => safeEnqueue(sseComment("keep-alive")), STREAM_HEARTBEAT_MS);
40046
+ } catch (err) {
40047
+ safeEnqueue(sseLine("error", { message: err?.message || "init failed" }));
40048
+ cleanup();
40049
+ }
40050
+ })();
40051
+ },
40052
+ cancel() {
40053
+ cleanup();
40054
+ }
40055
+ });
40056
+ request.signal?.addEventListener("abort", cleanup, { once: true });
40057
+ return new Response(stream, {
40058
+ headers: {
40059
+ "Content-Type": "text/event-stream",
40060
+ "Cache-Control": "no-cache",
40061
+ Connection: "keep-alive",
40062
+ "X-Accel-Buffering": "no"
40063
+ }
40064
+ });
40065
+ }).get("/api/log-sources/:sourceId/search", async ({ headers, params, query, set: set3, request }) => {
40066
+ if (!verifyAdminToken(headers)) {
40067
+ set3.status = 401;
40068
+ return new Response("Unauthorized", { status: 401 });
40069
+ }
40070
+ const src = await db.logSources.findById(params.sourceId);
40071
+ if (!src) {
40072
+ set3.status = 404;
40073
+ return new Response("Not found", { status: 404 });
40074
+ }
40075
+ const guard4 = await pathGuard(src.filePath);
40076
+ if (!guard4.ok) {
40077
+ set3.status = guard4.status || 500;
40078
+ return new Response(guard4.error || "path error", { status: guard4.status || 500 });
40079
+ }
40080
+ const q = typeof query.q === "string" ? query.q : "";
40081
+ if (!q) {
40082
+ set3.status = 400;
40083
+ return new Response("q is required", { status: 400 });
40084
+ }
40085
+ const regex2 = query.regex === "true" || query.regex === "1";
40086
+ const caseInsensitive = query.caseInsensitive === "true" || query.caseInsensitive === "1";
40087
+ const maxHits = Math.max(1, Math.min(toInt(query.maxHits, DEFAULT_SEARCH_HITS), MAX_SEARCH_HITS));
40088
+ const context = Math.max(0, Math.min(toInt(query.context, DEFAULT_SEARCH_CONTEXT), 20));
40089
+ const fromOffset = query.fromOffset !== undefined ? Math.max(0, toInt(query.fromOffset, 0)) : undefined;
40090
+ const toOffset = query.toOffset !== undefined ? Math.max(0, toInt(query.toOffset, 0)) : undefined;
40091
+ let controllerRef = null;
40092
+ let heartbeat = null;
40093
+ let stopper = null;
40094
+ let closed = false;
40095
+ const safeEnqueue = (chunk) => {
40096
+ if (closed || !controllerRef)
40097
+ return;
40098
+ try {
40099
+ controllerRef.enqueue(chunk);
40100
+ } catch {}
40101
+ };
40102
+ const cleanup = () => {
40103
+ if (closed)
40104
+ return;
40105
+ closed = true;
40106
+ if (heartbeat)
40107
+ clearInterval(heartbeat);
40108
+ if (stopper) {
40109
+ try {
40110
+ stopper.abort();
40111
+ } catch {}
40112
+ }
40113
+ try {
40114
+ controllerRef?.close();
40115
+ } catch {}
40116
+ };
40117
+ const stream = new ReadableStream({
40118
+ start(controller) {
40119
+ controllerRef = controller;
40120
+ heartbeat = setInterval(() => safeEnqueue(sseComment("keep-alive")), STREAM_HEARTBEAT_MS);
40121
+ stopper = grepStream(guard4.resolved, {
40122
+ q,
40123
+ regex: regex2,
40124
+ caseInsensitive,
40125
+ maxHits,
40126
+ context,
40127
+ fromOffset,
40128
+ toOffset,
40129
+ signal: request.signal
40130
+ }, {
40131
+ onHit: (hit) => safeEnqueue(sseLine("hit", hit)),
40132
+ onTruncated: () => safeEnqueue(sseLine("truncated", { maxHits })),
40133
+ onDone: (scannedBytes) => {
40134
+ safeEnqueue(sseLine("done", { scannedBytes }));
40135
+ cleanup();
40136
+ },
40137
+ onError: (err) => {
40138
+ safeEnqueue(sseLine("error", { message: err?.message || "search error" }));
40139
+ cleanup();
40140
+ }
40141
+ });
40142
+ },
40143
+ cancel() {
40144
+ cleanup();
40145
+ }
40146
+ });
40147
+ request.signal?.addEventListener("abort", cleanup, { once: true });
40148
+ return new Response(stream, {
40149
+ headers: {
40150
+ "Content-Type": "text/event-stream",
40151
+ "Cache-Control": "no-cache",
40152
+ Connection: "keep-alive",
40153
+ "X-Accel-Buffering": "no"
40154
+ }
40155
+ });
40156
+ });
40157
+
40158
+ // src/routes/system.ts
40159
+ init_dist3();
40160
+
40161
+ // src/lib/system-metrics.ts
40162
+ import os6 from "node:os";
40163
+ import fs11 from "node:fs/promises";
40164
+ function readCpuSnapshot() {
40165
+ const cpus = os6.cpus();
40166
+ let idle = 0;
40167
+ let total = 0;
40168
+ for (const c of cpus) {
40169
+ const t2 = c.times;
40170
+ idle += t2.idle;
40171
+ total += t2.user + t2.nice + t2.sys + t2.idle + t2.irq;
40172
+ }
40173
+ return { idle, total };
40174
+ }
40175
+ var lastSnapshot = null;
40176
+ var lastCpuPercent = null;
40177
+ function sampleCpuPercent() {
40178
+ const cur = readCpuSnapshot();
40179
+ if (!lastSnapshot) {
40180
+ lastSnapshot = cur;
40181
+ return lastCpuPercent;
40182
+ }
40183
+ const idleDiff = cur.idle - lastSnapshot.idle;
40184
+ const totalDiff = cur.total - lastSnapshot.total;
40185
+ lastSnapshot = cur;
40186
+ if (totalDiff <= 0)
40187
+ return lastCpuPercent;
40188
+ const pct = Math.max(0, Math.min(100, Math.round((1 - idleDiff / totalDiff) * 1e4) / 100));
40189
+ lastCpuPercent = pct;
40190
+ return pct;
40191
+ }
40192
+ readCpuSnapshot();
40193
+ lastSnapshot = readCpuSnapshot();
40194
+ async function readAvailableMemoryBytes(totalBytes) {
40195
+ try {
40196
+ if (process.platform === "darwin") {
40197
+ const { stdout, code } = await runCmd("vm_stat", []);
40198
+ if (code !== 0)
40199
+ return os6.freemem();
40200
+ const pageSizeMatch = stdout.match(/page size of (\d+) bytes/);
40201
+ const pageSize = pageSizeMatch ? Number(pageSizeMatch[1]) : 4096;
40202
+ const pick3 = (key) => {
40203
+ const m = stdout.match(new RegExp(`Pages ${key}:\\s+(\\d+)`));
40204
+ return m ? Number(m[1]) : 0;
40205
+ };
40206
+ const free = pick3("free");
40207
+ const inactive = pick3("inactive");
40208
+ const speculative = pick3("speculative");
40209
+ const purgeable = pick3("purgeable");
40210
+ const available = (free + inactive + speculative + purgeable) * pageSize;
40211
+ return available > 0 ? Math.min(available, totalBytes) : os6.freemem();
40212
+ }
40213
+ if (process.platform === "linux") {
40214
+ const content = await fs11.readFile("/proc/meminfo", "utf8");
40215
+ const m = content.match(/^MemAvailable:\s+(\d+)\s*kB/m);
40216
+ if (m) {
40217
+ const bytes = Number(m[1]) * 1024;
40218
+ return Number.isFinite(bytes) ? bytes : os6.freemem();
40219
+ }
40220
+ return os6.freemem();
40221
+ }
40222
+ } catch {
40223
+ return os6.freemem();
40224
+ }
40225
+ return os6.freemem();
40226
+ }
40227
+ var lastProcCpuUsage = process.cpuUsage();
40228
+ var lastProcSampleAt = Date.now();
40229
+ var lastProcCpuPercent = null;
40230
+ function sampleProcessCpuPercent() {
40231
+ const usage = process.cpuUsage(lastProcCpuUsage);
40232
+ const now = Date.now();
40233
+ const elapsedMs = now - lastProcSampleAt;
40234
+ lastProcCpuUsage = process.cpuUsage();
40235
+ lastProcSampleAt = now;
40236
+ if (elapsedMs <= 0)
40237
+ return lastProcCpuPercent;
40238
+ const cpuMs = (usage.user + usage.system) / 1000;
40239
+ const cores = Math.max(1, os6.cpus().length);
40240
+ const pct = Math.max(0, Math.min(100, Math.round(cpuMs / (elapsedMs * cores) * 1e4) / 100));
40241
+ lastProcCpuPercent = pct;
40242
+ return pct;
40243
+ }
40244
+ async function collectSystemResources() {
40245
+ const cpus = os6.cpus();
40246
+ const cpuPercent = sampleCpuPercent();
40247
+ const totalMem = os6.totalmem();
40248
+ const freeMem = os6.freemem();
40249
+ const [availMem, disk] = await Promise.all([
40250
+ readAvailableMemoryBytes(totalMem),
40251
+ fsFree()
40252
+ ]);
40253
+ const usedMem = Math.max(0, totalMem - availMem);
40254
+ const mem = process.memoryUsage();
40255
+ const runtimeName = typeof globalThis.Bun !== "undefined" ? "bun" : "node";
40256
+ const runtimeVersion = runtimeName === "bun" ? `v${globalThis.Bun.version}` : process.version;
40257
+ return {
40258
+ collectedAt: new Date().toISOString(),
40259
+ host: {
40260
+ hostname: os6.hostname(),
40261
+ platform: process.platform,
40262
+ arch: process.arch,
40263
+ cpuModel: cpus[0]?.model ?? null,
40264
+ cpuCount: cpus.length,
40265
+ loadAvg: os6.loadavg(),
40266
+ uptimeSec: Math.floor(os6.uptime())
40267
+ },
40268
+ cpu: { percent: cpuPercent },
40269
+ memory: {
40270
+ totalBytes: totalMem,
40271
+ freeBytes: freeMem,
40272
+ availableBytes: availMem,
40273
+ usedBytes: usedMem,
40274
+ percentUsed: totalMem > 0 ? Math.round(usedMem / totalMem * 1e4) / 100 : 0
40275
+ },
40276
+ disk,
40277
+ process: {
40278
+ pid: process.pid,
40279
+ runtime: runtimeName,
40280
+ runtimeVersion,
40281
+ uptimeSec: Math.floor(process.uptime()),
40282
+ cpuPercent: sampleProcessCpuPercent(),
40283
+ memoryRssBytes: mem.rss,
40284
+ memoryHeapUsedBytes: mem.heapUsed
40285
+ }
40286
+ };
40287
+ }
40288
+
40289
+ // src/routes/system.ts
40290
+ var systemRoutes = new Elysia().get("/api/system/resources", async ({ headers, set: set3 }) => {
40291
+ if (!verifyAdminToken(headers)) {
40292
+ set3.status = 401;
40293
+ return { error: "Unauthorized" };
40294
+ }
40295
+ return await collectSystemResources();
40296
+ });
40297
+
40298
+ // src/routes/pm2.ts
40299
+ init_dist3();
40300
+
40301
+ // src/lib/pm2.ts
40302
+ import os7 from "node:os";
40303
+ import path14 from "node:path";
40304
+ var cachedPm2Path = undefined;
40305
+ async function resolvePm2Path() {
40306
+ if (cachedPm2Path !== undefined)
40307
+ return cachedPm2Path;
40308
+ const candidates = ["pm2"];
40309
+ const home = os7.homedir();
40310
+ candidates.push(path14.join(home, ".local/bin/pm2"), "/usr/local/bin/pm2", "/opt/homebrew/bin/pm2");
40311
+ for (const c of candidates) {
40312
+ const probe = await runCmd(c, ["--version"], 1500);
40313
+ if (probe.code === 0) {
40314
+ cachedPm2Path = c;
40315
+ return c;
40316
+ }
40317
+ }
40318
+ cachedPm2Path = null;
40319
+ return null;
40320
+ }
40321
+ async function isPm2Available() {
40322
+ return await resolvePm2Path() !== null;
40323
+ }
40324
+ var cache3 = null;
40325
+ var CACHE_TTL_MS3 = 1500;
40326
+ async function pm2Jlist() {
40327
+ if (cache3 && Date.now() - cache3.at < CACHE_TTL_MS3)
40328
+ return cache3.data;
40329
+ const bin = await resolvePm2Path();
40330
+ if (!bin)
40331
+ return [];
40332
+ const { stdout, code } = await runCmd(bin, ["jlist"], 4000);
40333
+ if (code !== 0)
40334
+ return [];
40335
+ try {
40336
+ const start = stdout.indexOf("[");
40337
+ if (start < 0)
40338
+ return [];
40339
+ const arr = JSON.parse(stdout.slice(start));
40340
+ if (!Array.isArray(arr))
40341
+ return [];
40342
+ cache3 = { at: Date.now(), data: arr };
40343
+ return arr;
40344
+ } catch {
40345
+ return [];
40346
+ }
40347
+ }
40348
+ async function listPm2Apps() {
40349
+ const list = await pm2Jlist();
40350
+ return list.map((item) => ({
40351
+ name: String(item?.name ?? ""),
40352
+ pmId: Number(item?.pm_id ?? -1),
40353
+ status: String(item?.pm2_env?.status ?? "unknown")
40354
+ })).filter((x) => x.name);
40355
+ }
40356
+ async function getPm2AppStatus(name) {
40357
+ const available = await isPm2Available();
40358
+ if (!available) {
40359
+ return { found: false, name, message: "pm2 binary not found in PATH" };
40360
+ }
40361
+ const list = await pm2Jlist();
40362
+ const matched = list.filter((item) => String(item?.name) === name);
40363
+ if (matched.length === 0) {
40364
+ return { found: false, name, message: `no pm2 app named "${name}"` };
40365
+ }
40366
+ const first = matched[0];
40367
+ let cpuSum = 0;
40368
+ let memSum = 0;
40369
+ let restarts = 0;
40370
+ let unstable = 0;
40371
+ let uptimeMax = 0;
40372
+ let status2 = "unknown";
40373
+ for (const m of matched) {
40374
+ const monit = m?.monit || {};
40375
+ cpuSum += Number(monit.cpu ?? 0);
40376
+ memSum += Number(monit.memory ?? 0);
40377
+ const env4 = m?.pm2_env || {};
40378
+ restarts += Number(env4.restart_time ?? 0);
40379
+ unstable += Number(env4.unstable_restarts ?? 0);
40380
+ if (typeof env4.pm_uptime === "number") {
40381
+ const up = Date.now() - env4.pm_uptime;
40382
+ if (up > uptimeMax)
40383
+ uptimeMax = up;
40384
+ }
40385
+ if (env4.status)
40386
+ status2 = String(env4.status);
40387
+ }
40388
+ const env3 = first?.pm2_env || {};
40389
+ return {
40390
+ found: true,
40391
+ name,
40392
+ pmId: Number(first?.pm_id ?? -1),
40393
+ pid: Number(first?.pid ?? 0),
40394
+ status: status2,
40395
+ uptimeMs: uptimeMax || undefined,
40396
+ restarts,
40397
+ unstableRestarts: unstable,
40398
+ cpuPercent: Math.round(cpuSum * 100) / 100,
40399
+ memoryBytes: memSum,
40400
+ execMode: env3.exec_mode ? String(env3.exec_mode) : undefined,
40401
+ instances: matched.length,
40402
+ pm2_env_status: env3.status ? String(env3.status) : undefined,
40403
+ errorLogPath: env3.pm_err_log_path ? String(env3.pm_err_log_path) : undefined,
40404
+ outLogPath: env3.pm_out_log_path ? String(env3.pm_out_log_path) : undefined,
40405
+ createdAt: typeof env3.created_at === "number" ? env3.created_at : undefined
40406
+ };
40407
+ }
40408
+
40409
+ // src/routes/pm2.ts
40410
+ var pm2Routes = new Elysia().get("/api/pm2/available", async ({ headers, set: set3 }) => {
40411
+ if (!verifyAdminToken(headers)) {
40412
+ set3.status = 401;
40413
+ return { error: "Unauthorized" };
40414
+ }
40415
+ const available = await isPm2Available();
40416
+ return { available };
40417
+ }).get("/api/pm2/apps", async ({ headers, set: set3 }) => {
40418
+ if (!verifyAdminToken(headers)) {
40419
+ set3.status = 401;
40420
+ return { error: "Unauthorized" };
40421
+ }
40422
+ const apps = await listPm2Apps();
40423
+ return { apps };
40424
+ }).get("/api/projects/:id/pm2", async ({ headers, params, set: set3 }) => {
40425
+ if (!verifyAdminToken(headers)) {
40426
+ set3.status = 401;
40427
+ return { error: "Unauthorized" };
40428
+ }
40429
+ const project = await db.projects.findById(params.id);
40430
+ if (!project) {
40431
+ set3.status = 404;
40432
+ return { error: "Project not found" };
40433
+ }
40434
+ if (!project.pm2AppName) {
40435
+ return { bound: false, message: "project has no pm2 app bound" };
40436
+ }
40437
+ const status2 = await getPm2AppStatus(project.pm2AppName);
40438
+ return { bound: true, ...status2 };
40439
+ });
40440
+
40441
+ // src/routes/tags.ts
40442
+ init_dist3();
40443
+ var ALLOWED_COLORS2 = new Set(["blue", "green", "yellow", "purple", "pink", "cyan", "gray"]);
40444
+ var COLOR_PALETTE2 = ["blue", "green", "yellow", "purple", "pink", "cyan", "gray"];
40445
+ function normalizeColor2(input) {
40446
+ if (input === null || input === undefined || input === "")
40447
+ return null;
40448
+ if (typeof input !== "string")
40449
+ return null;
40450
+ return ALLOWED_COLORS2.has(input) ? input : null;
40451
+ }
40452
+ async function pickFreeColor2() {
40453
+ const list = await db.tags.findAll();
40454
+ const used = new Set;
40455
+ for (const t2 of list) {
40456
+ if (t2.color)
40457
+ used.add(t2.color);
40458
+ }
40459
+ for (const color of COLOR_PALETTE2) {
40460
+ if (!used.has(color))
40461
+ return color;
40462
+ }
40463
+ return COLOR_PALETTE2[list.length % COLOR_PALETTE2.length];
40464
+ }
40465
+ function normalizeName2(input) {
40466
+ if (typeof input !== "string")
40467
+ return "";
40468
+ return input.trim();
40469
+ }
40470
+ var tagRoutes = new Elysia().get("/api/tags", async ({ headers, set: set3 }) => {
40471
+ if (!verifyAdminToken(headers)) {
40472
+ set3.status = 401;
40473
+ return { error: "Unauthorized" };
40474
+ }
40475
+ const tags2 = await db.tags.findAll();
40476
+ const pairs = await db.projectTags.listAllPairs();
40477
+ const counts = new Map;
40478
+ for (const p of pairs)
40479
+ counts.set(p.tagId, (counts.get(p.tagId) ?? 0) + 1);
40480
+ return tags2.map((t2) => ({ ...t2, projectCount: counts.get(t2.id) ?? 0 }));
40481
+ }).post("/api/tags", async ({ headers, body, set: set3 }) => {
40482
+ if (!verifyAdminToken(headers)) {
40483
+ set3.status = 401;
40484
+ return { error: "Unauthorized" };
40485
+ }
40486
+ const name = normalizeName2(body.name);
40487
+ if (!name) {
40488
+ set3.status = 400;
40489
+ return { error: "标签名不能为空" };
40490
+ }
40491
+ if (name.length > 30) {
40492
+ set3.status = 400;
40493
+ return { error: "标签名过长(最多 30 字符)" };
40494
+ }
40495
+ const exist = await db.tags.findByName(name);
40496
+ if (exist) {
40497
+ set3.status = 409;
40498
+ return { error: "标签名已存在", conflictTag: exist.name };
40499
+ }
40500
+ let color = normalizeColor2(body.color);
40501
+ if (!color)
40502
+ color = await pickFreeColor2();
40503
+ const sortOrder = typeof body.sortOrder === "number" ? body.sortOrder : 0;
40504
+ const created = await db.tags.create({ name, color, sortOrder });
40505
+ await writeAudit({ headers }, {
40506
+ action: "tag.create",
40507
+ targetType: "tag",
40508
+ targetId: created.id,
40509
+ targetName: created.name,
40510
+ before: null,
40511
+ after: sanitize2(created),
40512
+ summary: `创建标签 ${created.name}`
40513
+ });
40514
+ return { success: true, tag: created };
40515
+ }, {
40516
+ body: t.Object({
40517
+ name: t.String(),
40518
+ color: t.Optional(t.Union([t.String(), t.Null()])),
40519
+ sortOrder: t.Optional(t.Number())
40520
+ })
40521
+ }).put("/api/tags/:id", async ({ headers, params, body, set: set3 }) => {
40522
+ if (!verifyAdminToken(headers)) {
40523
+ set3.status = 401;
40524
+ return { error: "Unauthorized" };
40525
+ }
40526
+ const before = await db.tags.findById(params.id);
40527
+ if (!before) {
40528
+ set3.status = 404;
40529
+ return { error: "Tag not found" };
40530
+ }
40531
+ const patch = {};
40532
+ if (body.name !== undefined) {
40533
+ const name = normalizeName2(body.name);
40534
+ if (!name) {
40535
+ set3.status = 400;
40536
+ return { error: "标签名不能为空" };
40537
+ }
40538
+ if (name.length > 30) {
40539
+ set3.status = 400;
40540
+ return { error: "标签名过长(最多 30 字符)" };
40541
+ }
40542
+ if (name !== before.name) {
40543
+ const conflict = await db.tags.findByName(name);
40544
+ if (conflict && conflict.id !== params.id) {
40545
+ set3.status = 409;
40546
+ return { error: "标签名已存在", conflictTag: conflict.name };
40547
+ }
40548
+ }
40549
+ patch.name = name;
40550
+ }
40551
+ if (body.color !== undefined)
40552
+ patch.color = normalizeColor2(body.color);
40553
+ if (body.sortOrder !== undefined && typeof body.sortOrder === "number") {
40554
+ patch.sortOrder = body.sortOrder;
40555
+ }
40556
+ const after = await db.tags.update(params.id, patch);
40557
+ if (!after) {
40558
+ set3.status = 404;
40559
+ return { error: "Tag not found" };
40560
+ }
40561
+ const diff = diffFields(before, after, Object.keys(patch));
40562
+ if (Object.keys(diff.after).length > 0) {
40563
+ await writeAudit({ headers }, {
40564
+ action: "tag.update",
40565
+ targetType: "tag",
40566
+ targetId: params.id,
40567
+ targetName: after.name,
40568
+ before: diff.before,
40569
+ after: diff.after,
40570
+ summary: `更新标签配置:${Object.keys(diff.after).join(", ")}`
40571
+ });
40572
+ }
40573
+ return { success: true, tag: after };
40574
+ }, {
40575
+ body: t.Object({
40576
+ name: t.Optional(t.String()),
40577
+ color: t.Optional(t.Union([t.String(), t.Null()])),
40578
+ sortOrder: t.Optional(t.Number())
40579
+ })
40580
+ }).delete("/api/tags/:id", async ({ headers, params, set: set3 }) => {
40581
+ if (!verifyAdminToken(headers)) {
40582
+ set3.status = 401;
40583
+ return { error: "Unauthorized" };
40584
+ }
40585
+ const before = await db.tags.findById(params.id);
40586
+ if (!before) {
40587
+ set3.status = 404;
40588
+ return { error: "Tag not found" };
40589
+ }
40590
+ const projectCount = await db.tags.countProjects(params.id);
40591
+ const success = await db.tags.remove(params.id);
40592
+ if (!success) {
40593
+ set3.status = 404;
40594
+ return { error: "Tag not found" };
40595
+ }
40596
+ await writeAudit({ headers }, {
40597
+ action: "tag.delete",
40598
+ targetType: "tag",
40599
+ targetId: before.id,
40600
+ targetName: before.name,
40601
+ before: { ...sanitize2(before), projectCount },
40602
+ after: null,
40603
+ summary: `删除标签 ${before.name}(解除 ${projectCount} 个项目关联)`
40604
+ });
40605
+ return { success: true, detachedProjects: projectCount };
40606
+ });
40607
+
40608
+ // src/routes/terminal.ts
40609
+ init_dist3();
40610
+
40611
+ // src/lib/ip-allowlist.ts
40612
+ function normalizeIp(ip) {
40613
+ if (!ip)
40614
+ return "";
40615
+ let s = ip.trim();
40616
+ if (s.startsWith("[") && s.endsWith("]"))
40617
+ s = s.slice(1, -1);
40618
+ const pct = s.indexOf("%");
40619
+ if (pct >= 0)
40620
+ s = s.slice(0, pct);
40621
+ const v4mapped = s.match(/^::ffff:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/i);
40622
+ if (v4mapped)
40623
+ return v4mapped[1];
40624
+ if (s === "::1")
40625
+ return "::1";
40626
+ return s.toLowerCase();
40627
+ }
40628
+ function isIPv4(s) {
40629
+ const m = s.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
40630
+ if (!m)
40631
+ return false;
40632
+ for (let i = 1;i <= 4; i++) {
40633
+ const n = Number(m[i]);
40634
+ if (!Number.isInteger(n) || n < 0 || n > 255)
40635
+ return false;
40636
+ }
40637
+ return true;
40638
+ }
40639
+ function ipv4ToInt(s) {
40640
+ const parts = s.split(".").map((p) => Number(p));
40641
+ return (parts[0] << 24 | parts[1] << 16 | parts[2] << 8 | parts[3]) >>> 0;
40642
+ }
40643
+ function isIPv6(s) {
40644
+ return s.includes(":") && /^[0-9a-fA-F:]+$/.test(s);
40645
+ }
40646
+ function expandIPv6(s) {
40647
+ if (s === "::")
40648
+ return 0n;
40649
+ const dq = s.indexOf("::");
40650
+ let head;
40651
+ let tail;
40652
+ if (dq >= 0) {
40653
+ head = s.slice(0, dq) ? s.slice(0, dq).split(":") : [];
40654
+ tail = s.slice(dq + 2) ? s.slice(dq + 2).split(":") : [];
40655
+ } else {
40656
+ head = s.split(":");
40657
+ tail = [];
40658
+ }
40659
+ const last = tail.length ? tail[tail.length - 1] : head.length ? head[head.length - 1] : "";
40660
+ if (last && last.includes(".")) {
40661
+ if (!isIPv4(last))
40662
+ return null;
40663
+ const v4 = ipv4ToInt(last);
40664
+ const hi = v4 >>> 16 & 65535;
40665
+ const lo = v4 & 65535;
40666
+ if (tail.length) {
40667
+ tail = tail.slice(0, -1).concat(hi.toString(16), lo.toString(16));
40668
+ } else {
40669
+ head = head.slice(0, -1).concat(hi.toString(16), lo.toString(16));
40670
+ }
40671
+ }
40672
+ const totalGroups = head.length + tail.length;
40673
+ if (totalGroups > 8)
40674
+ return null;
40675
+ if (dq < 0 && totalGroups !== 8)
40676
+ return null;
40677
+ const fill = 8 - totalGroups;
40678
+ const groups = [...head, ...Array.from({ length: fill }, () => "0"), ...tail];
40679
+ if (groups.length !== 8)
40680
+ return null;
40681
+ let result = 0n;
40682
+ for (const g of groups) {
40683
+ if (g.length === 0 || g.length > 4)
40684
+ return null;
40685
+ if (!/^[0-9a-fA-F]+$/.test(g))
40686
+ return null;
40687
+ result = result << 16n | BigInt(parseInt(g, 16));
40688
+ }
40689
+ return result;
40690
+ }
40691
+ function parseAllowlistEntry(raw) {
40692
+ const trimmed = raw.trim();
40693
+ if (!trimmed)
40694
+ return null;
40695
+ const slash = trimmed.indexOf("/");
40696
+ let host = trimmed;
40697
+ let prefix = null;
40698
+ if (slash >= 0) {
40699
+ host = trimmed.slice(0, slash);
40700
+ const p = Number(trimmed.slice(slash + 1));
40701
+ if (!Number.isInteger(p) || p < 0)
40702
+ return null;
40703
+ prefix = p;
40704
+ }
40705
+ const norm = normalizeIp(host);
40706
+ if (isIPv4(norm)) {
40707
+ if (prefix === null)
40708
+ prefix = 32;
40709
+ if (prefix > 32)
40710
+ return null;
40711
+ const ipInt = BigInt(ipv4ToInt(norm));
40712
+ const mask = prefix === 0 ? 0n : 0xffffffffn << BigInt(32 - prefix) & 0xffffffffn;
40713
+ return { kind: "v4", base: ipInt & mask, mask, prefixLen: prefix, raw: trimmed };
40714
+ }
40715
+ if (isIPv6(norm)) {
40716
+ if (prefix === null)
40717
+ prefix = 128;
40718
+ if (prefix > 128)
40719
+ return null;
40720
+ const ipInt = expandIPv6(norm);
40721
+ if (ipInt === null)
40722
+ return null;
40723
+ const full = (1n << 128n) - 1n;
40724
+ const mask = prefix === 0 ? 0n : full << BigInt(128 - prefix) & full;
40725
+ return { kind: "v6", base: ipInt & mask, mask, prefixLen: prefix, raw: trimmed };
40726
+ }
40727
+ return null;
40728
+ }
40729
+ function parseAllowlist(entries) {
40730
+ const parsed = [];
40731
+ const invalid = [];
40732
+ if (!Array.isArray(entries))
40733
+ return { parsed, invalid };
40734
+ for (const raw of entries) {
40735
+ if (typeof raw !== "string")
40736
+ continue;
40737
+ const trimmed = raw.trim();
40738
+ if (!trimmed)
40739
+ continue;
40740
+ const p = parseAllowlistEntry(trimmed);
40741
+ if (p)
40742
+ parsed.push(p);
40743
+ else
40744
+ invalid.push(trimmed);
40745
+ }
40746
+ return { parsed, invalid };
40747
+ }
40748
+ function ipMatchesEntry(ip, entry) {
40749
+ const norm = normalizeIp(ip);
40750
+ if (entry.kind === "v4") {
40751
+ if (!isIPv4(norm))
40752
+ return false;
40753
+ const ipInt2 = BigInt(ipv4ToInt(norm));
40754
+ return (ipInt2 & entry.mask) === entry.base;
40755
+ }
40756
+ if (!isIPv6(norm))
40757
+ return false;
40758
+ const ipInt = expandIPv6(norm);
40759
+ if (ipInt === null)
40760
+ return false;
40761
+ return (ipInt & entry.mask) === entry.base;
40762
+ }
40763
+ function ipAllowed(ip, allowlist) {
40764
+ if (!allowlist || allowlist.length === 0)
40765
+ return true;
40766
+ const { parsed } = parseAllowlist(allowlist);
40767
+ if (parsed.length === 0)
40768
+ return true;
40769
+ for (const entry of parsed) {
40770
+ if (ipMatchesEntry(ip, entry))
40771
+ return true;
40772
+ }
40773
+ return false;
40774
+ }
40775
+
40776
+ // src/lib/client-ip.ts
40777
+ function headerString(value2) {
40778
+ if (Array.isArray(value2))
40779
+ return value2[0];
40780
+ return value2;
40781
+ }
40782
+ function resolveClientIp(input) {
40783
+ const socketIp = normalizeIp(input.socketRemoteAddress || "");
40784
+ const headers = input.headers || {};
40785
+ const xff = headerString(headers["x-forwarded-for"]) || headerString(headers["X-Forwarded-For"]);
40786
+ const realIp = headerString(headers["x-real-ip"]) || headerString(headers["X-Real-IP"]);
40787
+ let forwardedIp = null;
40788
+ if (typeof xff === "string" && xff.length > 0) {
40789
+ forwardedIp = normalizeIp(xff.split(",")[0].trim());
40790
+ } else if (typeof realIp === "string" && realIp.length > 0) {
40791
+ forwardedIp = normalizeIp(realIp.trim());
40792
+ }
40793
+ return {
40794
+ socketIp,
40795
+ forwardedIp,
40796
+ trusted: socketIp || forwardedIp || ""
40797
+ };
40798
+ }
40799
+
40800
+ // src/lib/terminal.ts
40801
+ import os8 from "node:os";
40802
+ import fs12 from "node:fs";
40803
+ import path15 from "node:path";
40804
+ import { randomUUID as randomUUID3 } from "node:crypto";
40805
+ var termLog = moduleLogger("terminal");
40806
+ var TERMINAL_LIMITS = {
40807
+ maxSessionsPerIp: 4,
40808
+ maxTotalSessions: 16,
40809
+ maxLifetimeMs: 2 * 60 * 60 * 1000,
40810
+ idleTimeoutMs: 15 * 60 * 1000
40811
+ };
40812
+ var loadPromise = null;
40813
+ function isPlatformSupported() {
40814
+ return process.platform === "darwin" || process.platform === "linux";
40815
+ }
40816
+ function isBunRuntime() {
40817
+ return typeof globalThis.Bun !== "undefined" && typeof globalThis.Bun?.spawn === "function";
40818
+ }
40819
+ async function loadPty() {
40820
+ if (!isPlatformSupported()) {
40821
+ return { available: false, error: `当前平台不支持终端能力:${process.platform}` };
40822
+ }
40823
+ if (loadPromise)
40824
+ return loadPromise;
40825
+ loadPromise = (async () => {
40826
+ if (isBunRuntime()) {
40827
+ return { available: true, driver: "bun" };
40828
+ }
40829
+ try {
40830
+ const mod = await import("node-pty");
40831
+ const spawn3 = mod?.spawn || mod?.default?.spawn;
40832
+ if (typeof spawn3 !== "function") {
40833
+ return { available: false, error: "node-pty 加载成功但未导出 spawn 函数" };
40834
+ }
40835
+ return { available: true, driver: "node-pty", ptyMod: mod };
40836
+ } catch (err) {
40837
+ const msg = err?.message || String(err);
40838
+ termLog.warn({ err: msg }, "加载 node-pty 失败,终端能力将不可用");
40839
+ return { available: false, error: msg };
40840
+ }
40841
+ })();
40842
+ return loadPromise;
40843
+ }
40844
+ var sessions = new Map;
40845
+ var sessionsByIp = new Map;
40846
+ function listSessionsCounts() {
40847
+ const perIp = {};
40848
+ for (const [ip, ids] of sessionsByIp)
40849
+ perIp[ip] = ids.size;
40850
+ return { total: sessions.size, perIp };
40851
+ }
40852
+ function defaultShell() {
40853
+ if (process.platform === "darwin" || process.platform === "linux") {
40854
+ return process.env.SHELL || "/bin/bash";
40855
+ }
40856
+ return process.env.SHELL || "/bin/sh";
40857
+ }
40858
+ function resolveCwd(candidate) {
40859
+ const home = os8.homedir();
40860
+ if (!candidate || typeof candidate !== "string")
40861
+ return home;
40862
+ try {
40863
+ const resolved = path15.resolve(candidate);
40864
+ const stat2 = fs12.statSync(resolved);
40865
+ if (stat2.isDirectory())
40866
+ return resolved;
40867
+ } catch {}
40868
+ return home;
40869
+ }
40870
+ function clampCols(c) {
40871
+ return Math.max(2, Math.min(500, Math.floor(c) || 80));
40872
+ }
40873
+ function clampRows(r) {
40874
+ return Math.max(2, Math.min(200, Math.floor(r) || 24));
40875
+ }
40876
+ function attachToBookkeeping(session, ip) {
40877
+ sessions.set(session.id, session);
40878
+ const bucket = sessionsByIp.get(ip);
40879
+ if (!bucket)
40880
+ sessionsByIp.set(ip, new Set([session.id]));
40881
+ else
40882
+ bucket.add(session.id);
40883
+ }
40884
+ function detachFromBookkeeping(session, ip) {
40885
+ sessions.delete(session.id);
40886
+ const bucket = sessionsByIp.get(ip);
40887
+ if (bucket) {
40888
+ bucket.delete(session.id);
40889
+ if (bucket.size === 0)
40890
+ sessionsByIp.delete(ip);
40891
+ }
40892
+ }
40893
+ function makeSessionShell(params) {
40894
+ const dataListeners = new Set;
40895
+ const exitListeners = new Set;
40896
+ const session = {
40897
+ id: params.id,
40898
+ pid: params.pid,
40899
+ cwd: params.cwd,
40900
+ shell: params.shell,
40901
+ createdAt: Date.now(),
40902
+ ip: params.ip,
40903
+ dataListeners,
40904
+ exitListeners,
40905
+ lastActiveAt: Date.now(),
40906
+ pendingChunks: [],
40907
+ pendingExit: null,
40908
+ lifetimeTimer: undefined,
40909
+ idleTimer: undefined,
40910
+ _killImpl: () => {},
40911
+ _writeImpl: () => {},
40912
+ _resizeImpl: () => {},
40913
+ write(data) {
40914
+ session.touch();
40915
+ try {
40916
+ session._writeImpl(data);
40917
+ } catch {}
40918
+ },
40919
+ resize(c, r) {
40920
+ session.touch();
40921
+ try {
40922
+ session._resizeImpl(clampCols(c), clampRows(r));
40923
+ } catch {}
40924
+ },
40925
+ kill(signal) {
40926
+ try {
40927
+ session._killImpl(signal || "SIGTERM");
40928
+ } catch {}
40929
+ },
40930
+ onData(listener) {
40931
+ dataListeners.add(listener);
40932
+ if (session.pendingChunks.length > 0) {
40933
+ const buffered = session.pendingChunks.splice(0);
40934
+ for (const chunk of buffered) {
40935
+ try {
40936
+ listener(chunk);
40937
+ } catch {}
40938
+ }
40939
+ }
40940
+ },
40941
+ onExit(listener) {
40942
+ exitListeners.add(listener);
40943
+ if (session.pendingExit) {
40944
+ const evt = session.pendingExit;
40945
+ try {
40946
+ listener(evt);
40947
+ } catch {}
40948
+ }
40949
+ },
40950
+ touch() {
40951
+ session.lastActiveAt = Date.now();
40952
+ if (session.idleTimer)
40953
+ clearTimeout(session.idleTimer);
40954
+ session.idleTimer = setTimeout(() => {
40955
+ termLog.warn({ id: session.id, pid: session.pid }, "pty session idle timeout, killing");
40956
+ try {
40957
+ session.kill("SIGTERM");
40958
+ } catch {}
40959
+ }, TERMINAL_LIMITS.idleTimeoutMs);
40960
+ }
40961
+ };
40962
+ return session;
40963
+ }
40964
+ function emitData(session, chunk) {
40965
+ session.touch();
40966
+ if (session.dataListeners.size === 0) {
40967
+ session.pendingChunks.push(chunk);
40968
+ if (session.pendingChunks.length > 256) {
40969
+ session.pendingChunks.splice(0, session.pendingChunks.length - 256);
40970
+ }
40971
+ return;
40972
+ }
40973
+ for (const l of session.dataListeners) {
40974
+ try {
40975
+ l(chunk);
40976
+ } catch {}
40977
+ }
40978
+ }
40979
+ function emitExit(session, ip, evt) {
40980
+ if (session.lifetimeTimer)
40981
+ clearTimeout(session.lifetimeTimer);
40982
+ if (session.idleTimer)
40983
+ clearTimeout(session.idleTimer);
40984
+ detachFromBookkeeping(session, ip);
40985
+ if (session.exitListeners.size === 0) {
40986
+ session.pendingExit = evt;
40987
+ return;
40988
+ }
40989
+ for (const l of session.exitListeners) {
40990
+ try {
40991
+ l(evt);
40992
+ } catch {}
40993
+ }
40994
+ session.dataListeners.clear();
40995
+ session.exitListeners.clear();
40996
+ }
40997
+ async function spawnTerminalSession(params) {
40998
+ const load = await loadPty();
40999
+ if (!load.available) {
41000
+ return { ok: false, reason: "unavailable", message: load.error || "终端能力不可用" };
41001
+ }
41002
+ if (sessions.size >= TERMINAL_LIMITS.maxTotalSessions) {
41003
+ return { ok: false, reason: "limit-total", message: `已达到全局并发上限 (${TERMINAL_LIMITS.maxTotalSessions})` };
41004
+ }
41005
+ const ipBucket = sessionsByIp.get(params.ip);
41006
+ if (ipBucket && ipBucket.size >= TERMINAL_LIMITS.maxSessionsPerIp) {
41007
+ return { ok: false, reason: "limit-per-ip", message: `单 IP 并发上限 (${TERMINAL_LIMITS.maxSessionsPerIp})` };
41008
+ }
41009
+ const shell = defaultShell();
41010
+ const cwd = resolveCwd(params.cwd);
41011
+ const cols = clampCols(params.cols);
41012
+ const rows = clampRows(params.rows);
41013
+ const env3 = {
41014
+ ...process.env,
41015
+ TERM: process.env.TERM || "xterm-256color",
41016
+ LANG: process.env.LANG || "en_US.UTF-8",
41017
+ KITE_TERMINAL: "1"
41018
+ };
41019
+ delete env3.ADMIN_TOKEN;
41020
+ const id = "term_" + randomUUID3().replace(/-/g, "").slice(0, 16);
41021
+ if (load.driver === "bun") {
41022
+ return spawnViaBun({ id, shell, cwd, cols, rows, env: env3, ip: params.ip });
41023
+ }
41024
+ return spawnViaNodePty({ id, shell, cwd, cols, rows, env: env3, ip: params.ip, ptyMod: load.ptyMod });
41025
+ }
41026
+ function spawnViaNodePty(opts) {
41027
+ let pty;
41028
+ try {
41029
+ pty = opts.ptyMod.spawn(opts.shell, [], {
41030
+ name: opts.env.TERM,
41031
+ cols: opts.cols,
41032
+ rows: opts.rows,
41033
+ cwd: opts.cwd,
41034
+ env: opts.env
41035
+ });
41036
+ } catch (err) {
41037
+ termLog.error({ err: err?.message || String(err) }, "spawn pty failed (node-pty)");
41038
+ return { ok: false, reason: "spawn-error", message: err?.message || "spawn pty failed" };
41039
+ }
41040
+ const session = makeSessionShell({
41041
+ id: opts.id,
41042
+ pid: pty.pid,
41043
+ cwd: opts.cwd,
41044
+ shell: opts.shell,
41045
+ ip: opts.ip
41046
+ });
41047
+ session._writeImpl = (data) => pty.write(data);
41048
+ session._resizeImpl = (c, r) => pty.resize(c, r);
41049
+ session._killImpl = (sig) => pty.kill(sig || "SIGTERM");
41050
+ session.lifetimeTimer = setTimeout(() => {
41051
+ termLog.warn({ id: opts.id, pid: pty.pid }, "pty session exceeded max lifetime, killing");
41052
+ try {
41053
+ pty.kill("SIGTERM");
41054
+ } catch {}
41055
+ }, TERMINAL_LIMITS.maxLifetimeMs);
41056
+ session.idleTimer = setTimeout(() => {
41057
+ termLog.warn({ id: opts.id, pid: pty.pid }, "pty session idle timeout, killing");
41058
+ try {
41059
+ pty.kill("SIGTERM");
41060
+ } catch {}
41061
+ }, TERMINAL_LIMITS.idleTimeoutMs);
41062
+ attachToBookkeeping(session, opts.ip);
41063
+ pty.onData((chunk) => emitData(session, chunk));
41064
+ pty.onExit((evt) => emitExit(session, opts.ip, { exitCode: evt.exitCode ?? 0, signal: evt.signal }));
41065
+ termLog.info({ id: opts.id, pid: pty.pid, cwd: opts.cwd, shell: opts.shell, ip: opts.ip, driver: "node-pty" }, "pty session started");
41066
+ return { ok: true, handle: session };
41067
+ }
41068
+ function spawnViaBun(opts) {
41069
+ const Bun2 = globalThis.Bun;
41070
+ if (!Bun2 || typeof Bun2.spawn !== "function") {
41071
+ return { ok: false, reason: "spawn-error", message: "Bun.spawn 不可用" };
41072
+ }
41073
+ const session = makeSessionShell({
41074
+ id: opts.id,
41075
+ pid: 0,
41076
+ cwd: opts.cwd,
41077
+ shell: opts.shell,
41078
+ ip: opts.ip
41079
+ });
41080
+ let proc;
41081
+ try {
41082
+ proc = Bun2.spawn([opts.shell], {
41083
+ cwd: opts.cwd,
41084
+ env: opts.env,
41085
+ terminal: {
41086
+ cols: opts.cols,
41087
+ rows: opts.rows,
41088
+ data(_terminal, data) {
41089
+ let str;
41090
+ if (typeof data === "string")
41091
+ str = data;
41092
+ else if (data instanceof Uint8Array)
41093
+ str = new TextDecoder("utf-8").decode(data);
41094
+ else if (data?.toString)
41095
+ str = data.toString();
41096
+ else
41097
+ str = "";
41098
+ if (str)
41099
+ emitData(session, str);
41100
+ },
41101
+ exit(_terminal, exitCode) {
41102
+ emitExit(session, opts.ip, { exitCode: exitCode ?? 0 });
41103
+ }
41104
+ }
41105
+ });
41106
+ } catch (err) {
41107
+ termLog.error({ err: err?.message || String(err) }, "spawn pty failed (bun)");
41108
+ return { ok: false, reason: "spawn-error", message: err?.message || "spawn pty failed" };
41109
+ }
41110
+ const terminal = proc?.terminal;
41111
+ if (!terminal) {
41112
+ try {
41113
+ proc?.kill?.("SIGTERM");
41114
+ } catch {}
41115
+ return { ok: false, reason: "spawn-error", message: "Bun.spawn 未返回 terminal 句柄(请升级到 Bun ≥ 1.3)" };
41116
+ }
41117
+ session.pid = proc.pid || 0;
41118
+ session._writeImpl = (data) => terminal.write(data);
41119
+ session._resizeImpl = (c, r) => {
41120
+ try {
41121
+ terminal.resize(c, r);
41122
+ } catch {}
41123
+ };
41124
+ session._killImpl = (sig) => {
41125
+ try {
41126
+ proc.kill?.(sig || "SIGTERM");
41127
+ } catch {}
41128
+ try {
41129
+ terminal.close?.();
41130
+ } catch {}
41131
+ };
41132
+ session.lifetimeTimer = setTimeout(() => {
41133
+ termLog.warn({ id: opts.id, pid: session.pid }, "pty session exceeded max lifetime, killing");
41134
+ try {
41135
+ proc.kill?.("SIGTERM");
41136
+ } catch {}
41137
+ }, TERMINAL_LIMITS.maxLifetimeMs);
41138
+ session.idleTimer = setTimeout(() => {
41139
+ termLog.warn({ id: opts.id, pid: session.pid }, "pty session idle timeout, killing");
41140
+ try {
41141
+ proc.kill?.("SIGTERM");
41142
+ } catch {}
41143
+ }, TERMINAL_LIMITS.idleTimeoutMs);
41144
+ attachToBookkeeping(session, opts.ip);
41145
+ if (proc.exited && typeof proc.exited.then === "function") {
41146
+ proc.exited.then((code) => {
41147
+ if (!sessions.has(session.id))
41148
+ return;
41149
+ emitExit(session, opts.ip, { exitCode: typeof code === "number" ? code : 0 });
41150
+ }).catch(() => {
41151
+ if (!sessions.has(session.id))
41152
+ return;
41153
+ emitExit(session, opts.ip, { exitCode: 1 });
41154
+ });
41155
+ }
41156
+ termLog.info({ id: opts.id, pid: session.pid, cwd: opts.cwd, shell: opts.shell, ip: opts.ip, driver: "bun" }, "pty session started");
41157
+ return { ok: true, handle: session };
41158
+ }
41159
+ function shutdownAllSessions(reason = "shutdown") {
41160
+ for (const s of sessions.values()) {
41161
+ try {
41162
+ s.kill("SIGTERM");
41163
+ } catch {}
41164
+ }
41165
+ termLog.info({ count: sessions.size, reason }, "killed all pty sessions");
41166
+ }
41167
+
41168
+ // src/routes/terminal.ts
41169
+ import os9 from "node:os";
41170
+ var termLog2 = moduleLogger("terminal-route");
41171
+ var TERMINAL_ALLOWLIST_KEY = "terminal.ipAllowlist";
41172
+ var TERMINAL_SUBPROTOCOL = "kite-admin-token";
41173
+ async function loadTerminalAllowlist() {
41174
+ try {
41175
+ const raw = await db.settings.get(TERMINAL_ALLOWLIST_KEY);
41176
+ if (!raw)
41177
+ return [];
41178
+ const v = JSON.parse(raw);
41179
+ if (Array.isArray(v))
41180
+ return v.map((x) => String(x)).filter(Boolean);
41181
+ return [];
41182
+ } catch {
41183
+ return [];
41184
+ }
41185
+ }
41186
+ async function saveTerminalAllowlist(entries) {
41187
+ await db.settings.set(TERMINAL_ALLOWLIST_KEY, JSON.stringify(entries));
41188
+ }
41189
+ var terminalRoutes = new Elysia().get("/api/terminal/whoami", ({ request, headers, set: set3 }) => {
41190
+ if (!verifyAdminToken(headers)) {
41191
+ set3.status = 401;
41192
+ return { error: "Unauthorized" };
41193
+ }
41194
+ const sockRemote = request?.socket?.remoteAddress || headers["x-kite-socket-ip"] || null;
41195
+ const info = resolveClientIp({
41196
+ socketRemoteAddress: sockRemote || undefined,
41197
+ headers
41198
+ });
41199
+ return {
41200
+ socketIp: info.socketIp || null,
41201
+ forwardedIp: info.forwardedIp,
41202
+ trustedIp: info.trusted || null
41203
+ };
41204
+ }).get("/api/terminal/info", async ({ headers, set: set3 }) => {
41205
+ if (!verifyAdminToken(headers)) {
41206
+ set3.status = 401;
41207
+ return { error: "Unauthorized" };
41208
+ }
41209
+ const platformOk = isPlatformSupported();
41210
+ const load = platformOk ? await loadPty() : { available: false, error: `当前平台不支持终端能力:${process.platform}` };
41211
+ const allowlist = await loadTerminalAllowlist();
41212
+ const counts = listSessionsCounts();
41213
+ return {
41214
+ available: load.available,
41215
+ reason: load.available ? null : load.error || "unknown",
41216
+ shell: defaultShell(),
41217
+ platform: process.platform,
41218
+ defaultCwd: os9.homedir(),
41219
+ limits: TERMINAL_LIMITS,
41220
+ sessions: counts,
41221
+ allowlist,
41222
+ allowlistEnabled: allowlist.length > 0
41223
+ };
41224
+ }).get("/api/terminal/allowlist", async ({ headers, set: set3 }) => {
41225
+ if (!verifyAdminToken(headers)) {
41226
+ set3.status = 401;
41227
+ return { error: "Unauthorized" };
41228
+ }
41229
+ const allowlist = await loadTerminalAllowlist();
41230
+ const { invalid } = parseAllowlist(allowlist);
41231
+ return { entries: allowlist, invalid };
41232
+ }).put("/api/terminal/allowlist", async ({ headers, body, set: set3 }) => {
41233
+ if (!verifyAdminToken(headers)) {
41234
+ set3.status = 401;
41235
+ return { error: "Unauthorized" };
41236
+ }
41237
+ const input = body?.entries;
41238
+ if (!Array.isArray(input)) {
41239
+ set3.status = 400;
41240
+ return { error: "entries 必须是字符串数组" };
41241
+ }
41242
+ const normalized = [];
41243
+ const invalid = [];
41244
+ for (const raw of input) {
41245
+ if (typeof raw !== "string")
41246
+ continue;
41247
+ const trimmed = raw.trim();
41248
+ if (!trimmed)
41249
+ continue;
41250
+ const { parsed } = parseAllowlist([trimmed]);
41251
+ if (parsed.length === 1)
41252
+ normalized.push(trimmed);
41253
+ else
41254
+ invalid.push(trimmed);
41255
+ }
41256
+ if (invalid.length > 0) {
41257
+ set3.status = 400;
41258
+ return { error: `存在无效的 IP / CIDR:${invalid.join(", ")}` };
41259
+ }
41260
+ const before = await loadTerminalAllowlist();
41261
+ await saveTerminalAllowlist(normalized);
41262
+ await writeAudit({ headers }, {
41263
+ action: "terminal.allowlist.update",
41264
+ targetType: "settings",
41265
+ before: { entries: before },
41266
+ after: { entries: normalized },
41267
+ summary: `更新终端 IP 白名单(${normalized.length} 条)`
41268
+ });
41269
+ return { success: true, entries: normalized };
41270
+ });
41271
+ function parseSubprotocols(headerValue) {
41272
+ if (!headerValue)
41273
+ return [];
41274
+ const raw = Array.isArray(headerValue) ? headerValue.join(",") : headerValue;
41275
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
41276
+ }
41277
+ async function decideTerminalUpgrade(ctx) {
41278
+ if (!ctx.url.pathname.startsWith("/api/terminal/ws")) {
41279
+ return { ok: false, status: 404, reason: "not-terminal-route" };
41280
+ }
41281
+ const allowlist = await loadTerminalAllowlist();
41282
+ if (allowlist.length > 0 && ctx.origin && ctx.expectedOrigin) {
41283
+ try {
41284
+ const a = new URL(ctx.origin);
41285
+ const b = new URL(ctx.expectedOrigin);
41286
+ if (a.host !== b.host) {
41287
+ const isLocalDev = /(^|:)(localhost|127\.0\.0\.1)(:|$)/.test(a.host) && /(^|:)(localhost|127\.0\.0\.1)(:|$)/.test(b.host);
41288
+ if (!isLocalDev)
41289
+ return { ok: false, status: 403, reason: "origin-mismatch" };
41290
+ }
41291
+ } catch {
41292
+ return { ok: false, status: 400, reason: "invalid-origin" };
41293
+ }
41294
+ }
41295
+ const subs = parseSubprotocols(ctx.subprotocols.length ? ctx.subprotocols.join(", ") : ctx.headers["sec-websocket-protocol"]);
41296
+ const protoIdx = subs.findIndex((s) => s === TERMINAL_SUBPROTOCOL);
41297
+ if (protoIdx < 0 || protoIdx + 1 >= subs.length) {
41298
+ return { ok: false, status: 401, reason: "missing-token-protocol" };
41299
+ }
41300
+ const token = subs[protoIdx + 1];
41301
+ const ipInfo = resolveClientIp({
41302
+ socketRemoteAddress: ctx.socketRemoteAddress || undefined,
41303
+ headers: ctx.headers
41304
+ });
41305
+ const ip = ipInfo.socketIp || ipInfo.forwardedIp || "unknown";
41306
+ const guard4 = await loginGuard(ip);
41307
+ if (guard4.locked) {
41308
+ return { ok: false, status: 429, reason: "rate-limited" };
41309
+ }
41310
+ const tokenValid = verifyAdminTokenValue(token) && safeEqual(token, process.env.ADMIN_TOKEN || "");
41311
+ if (!tokenValid) {
41312
+ loginFailure(ip);
41313
+ return { ok: false, status: 401, reason: "invalid-token" };
41314
+ }
41315
+ loginSuccess(ip);
41316
+ if (allowlist.length > 0 && !ipAllowed(ip, allowlist)) {
41317
+ await writeAudit({ headers: ctx.headers }, {
41318
+ action: "terminal.denied",
41319
+ targetType: "terminal",
41320
+ summary: `终端访问被 IP 白名单拒绝:${ip}`,
41321
+ status: "failed",
41322
+ errorMessage: `IP ${ip} 不在白名单中`
41323
+ });
41324
+ termLog2.warn({ ip }, "terminal connection rejected by allowlist");
41325
+ return { ok: false, status: 403, reason: "ip-not-allowed" };
41326
+ }
41327
+ const projectId = ctx.url.searchParams.get("projectId");
41328
+ let cwd = os9.homedir();
41329
+ if (projectId) {
41330
+ const proj = await db.projects.findById(projectId);
41331
+ if (proj && proj.deployPath) {
41332
+ cwd = resolveCwd(proj.deployPath);
41333
+ }
41334
+ } else {
41335
+ const cwdParam = ctx.url.searchParams.get("cwd");
41336
+ if (cwdParam)
41337
+ cwd = resolveCwd(cwdParam);
41338
+ }
41339
+ const cols = Number(ctx.url.searchParams.get("cols")) || 80;
41340
+ const rows = Number(ctx.url.searchParams.get("rows")) || 24;
41341
+ return {
41342
+ ok: true,
41343
+ status: 101,
41344
+ selectedProtocol: TERMINAL_SUBPROTOCOL,
41345
+ token,
41346
+ ip,
41347
+ cwd,
41348
+ projectId: projectId || null,
41349
+ cols,
41350
+ rows
41351
+ };
41352
+ }
41353
+ var OPEN = 1;
41354
+ function safeJsonParse(value2) {
41355
+ try {
41356
+ return JSON.parse(value2);
41357
+ } catch {
41358
+ return null;
41359
+ }
41360
+ }
41361
+ async function attachTerminalSocket(params) {
41362
+ const startedAt = Date.now();
41363
+ let handle = null;
41364
+ let exitedCode = null;
41365
+ const sendJson = (obj) => {
41366
+ try {
41367
+ if (params.socket.readyState === OPEN)
41368
+ params.socket.send(JSON.stringify(obj));
41369
+ } catch {}
41370
+ };
41371
+ const spawnResult = await spawnTerminalSession({
41372
+ cwd: params.cwd,
41373
+ cols: params.cols,
41374
+ rows: params.rows,
41375
+ ip: params.ip
41376
+ });
41377
+ if (!spawnResult.ok || !spawnResult.handle) {
41378
+ sendJson({ type: "error", reason: spawnResult.reason, message: spawnResult.message });
41379
+ try {
41380
+ params.socket.close(4000, spawnResult.message || "spawn failed");
41381
+ } catch {}
41382
+ return;
41383
+ }
41384
+ handle = spawnResult.handle;
41385
+ handle.onData((chunk) => {
41386
+ if (params.socket.readyState !== OPEN)
41387
+ return;
41388
+ try {
41389
+ params.socket.send(JSON.stringify({ type: "data", data: chunk }));
41390
+ } catch {}
41391
+ });
41392
+ handle.onExit((info) => {
41393
+ exitedCode = info.exitCode ?? 0;
41394
+ sendJson({ type: "exit", exitCode: info.exitCode, signal: info.signal ?? null });
41395
+ try {
41396
+ params.socket.close(1000, "pty exited");
41397
+ } catch {}
41398
+ });
41399
+ sendJson({
41400
+ type: "ready",
41401
+ sessionId: handle.id,
41402
+ pid: handle.pid,
41403
+ shell: handle.shell,
41404
+ cwd: handle.cwd,
41405
+ projectId: params.projectId
41406
+ });
41407
+ writeAudit({ headers: params.headers }, {
41408
+ action: "terminal.open",
41409
+ targetType: "terminal",
41410
+ targetId: handle.id,
41411
+ targetName: params.projectId ? `project:${params.projectId}` : "global",
41412
+ summary: `打开终端 cwd=${handle.cwd} pid=${handle.pid}`
41413
+ });
41414
+ params.socket.on("message", (data, isBinary) => {
41415
+ if (!handle)
41416
+ return;
41417
+ let raw;
41418
+ if (typeof data === "string")
41419
+ raw = data;
41420
+ else if (data instanceof Buffer)
41421
+ raw = data.toString("utf8");
41422
+ else if (data?.toString)
41423
+ raw = data.toString();
41424
+ else
41425
+ raw = "";
41426
+ if (isBinary === false || typeof data === "string") {
41427
+ const msg = safeJsonParse(raw);
41428
+ if (!msg || typeof msg !== "object")
41429
+ return;
41430
+ if (msg.type === "input" && typeof msg.data === "string") {
41431
+ handle.write(msg.data);
41432
+ } else if (msg.type === "resize") {
41433
+ const c = Number(msg.cols) || 80;
41434
+ const r = Number(msg.rows) || 24;
41435
+ handle.resize(c, r);
41436
+ } else if (msg.type === "ping") {
41437
+ sendJson({ type: "pong", ts: Date.now() });
41438
+ }
41439
+ }
41440
+ });
41441
+ const cleanup = async () => {
41442
+ if (handle) {
41443
+ const id = handle.id;
41444
+ const pid = handle.pid;
41445
+ try {
41446
+ handle.kill("SIGHUP");
41447
+ } catch {}
41448
+ handle = null;
41449
+ await writeAudit({ headers: params.headers }, {
41450
+ action: "terminal.close",
41451
+ targetType: "terminal",
41452
+ targetId: id,
41453
+ summary: `关闭终端 pid=${pid} durationMs=${Date.now() - startedAt} exitCode=${exitedCode ?? "n/a"}`
41454
+ });
41455
+ }
41456
+ };
41457
+ params.socket.on("close", () => {
41458
+ cleanup();
41459
+ });
41460
+ params.socket.on("error", (err) => {
41461
+ termLog2.warn({ err: err?.message }, "terminal socket error");
41462
+ cleanup();
41463
+ });
41464
+ }
41465
+
41466
+ // src/static.ts
41467
+ init_dist3();
41468
+ import path16 from "node:path";
41469
+ var MIME_TYPES = {
41470
+ ".html": "text/html; charset=utf-8",
41471
+ ".css": "text/css; charset=utf-8",
41472
+ ".js": "application/javascript; charset=utf-8",
41473
+ ".json": "application/json; charset=utf-8",
41474
+ ".svg": "image/svg+xml",
41475
+ ".png": "image/png",
41476
+ ".jpg": "image/jpeg",
41477
+ ".jpeg": "image/jpeg",
41478
+ ".gif": "image/gif",
41479
+ ".ico": "image/x-icon",
41480
+ ".woff": "font/woff",
41481
+ ".woff2": "font/woff2",
41482
+ ".ttf": "font/ttf",
41483
+ ".eot": "application/vnd.ms-fontobject",
41484
+ ".map": "application/json",
41485
+ ".txt": "text/plain; charset=utf-8",
41486
+ ".xml": "application/xml"
41487
+ };
41488
+ function getMimeType(filePath) {
41489
+ const ext2 = path16.extname(filePath).toLowerCase();
41490
+ return MIME_TYPES[ext2] || "application/octet-stream";
41491
+ }
41492
+ var staticPlugin = new Elysia().get("/*", async ({ request, set: set3 }) => {
41493
+ const webDir = process.env.KITE_WEB_DIR;
41494
+ const url = new URL(request.url);
41495
+ let pathname = url.pathname;
41496
+ if (!webDir) {
41497
+ if (pathname === "/")
41498
+ return "Deploy Server is running!";
41499
+ set3.status = 404;
41500
+ return { error: "Web console not available" };
41501
+ }
41502
+ const filePath = path16.resolve(webDir, "." + pathname);
41503
+ if (!filePath.startsWith(path16.resolve(webDir))) {
41504
+ set3.status = 403;
41505
+ return { error: "Access denied" };
41506
+ }
41507
+ const { stat: stat2, access } = await import("node:fs/promises");
41508
+ try {
41509
+ const fileStat = await stat2(filePath);
41510
+ if (fileStat.isFile()) {
41511
+ const buffer = await readFileBuffer(filePath);
41512
+ set3.headers["Content-Type"] = getMimeType(filePath);
41513
+ set3.headers["Cache-Control"] = pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache";
41514
+ return new Response(buffer);
41515
+ }
38895
41516
  } catch {}
38896
- const indexPath = path12.resolve(webDir, "index.html");
41517
+ const indexPath = path16.resolve(webDir, "index.html");
38897
41518
  try {
38898
41519
  await access(indexPath);
38899
41520
  const buffer = await readFileBuffer(indexPath);
@@ -38907,7 +41528,7 @@ var staticPlugin = new Elysia().get("/*", async ({ request, set: set3 }) => {
38907
41528
  });
38908
41529
 
38909
41530
  // src/index.ts
38910
- import { randomUUID as randomUUID3 } from "node:crypto";
41531
+ import { randomUUID as randomUUID4 } from "node:crypto";
38911
41532
  import http from "node:http";
38912
41533
  await ensureDbReady();
38913
41534
  var port = Number(process.env.PORT) || 5430;
@@ -38921,8 +41542,60 @@ if (!isBun3) {
38921
41542
  adapter = node2();
38922
41543
  }
38923
41544
  var httpLog = moduleLogger("http");
38924
- var app = new Elysia({ adapter }).derive({ as: "global" }, ({ request, headers, set: set3 }) => {
38925
- const traceId = pickTraceId(headers) || randomUUID3();
41545
+ var wsLog = moduleLogger("ws");
41546
+ async function buildTerminalWss() {
41547
+ try {
41548
+ const { WebSocketServer } = await import("ws");
41549
+ return new WebSocketServer({ noServer: true });
41550
+ } catch (err) {
41551
+ wsLog.warn({ err: err?.message }, "加载 ws 包失败,终端 WebSocket 将不可用");
41552
+ return null;
41553
+ }
41554
+ }
41555
+ var terminalWss = await buildTerminalWss();
41556
+ function headersFromIncoming(req) {
41557
+ return req.headers;
41558
+ }
41559
+ async function handleTerminalUpgrade(req, socket, head, expectedOrigin) {
41560
+ if (!terminalWss) {
41561
+ socket.destroy();
41562
+ return;
41563
+ }
41564
+ const url = new URL(req.url || "/", `http://${req.headers.host || expectedOrigin}`);
41565
+ const subs = req.headers["sec-websocket-protocol"]?.split(",").map((s) => s.trim()).filter(Boolean) || [];
41566
+ const decision = await decideTerminalUpgrade({
41567
+ url,
41568
+ headers: headersFromIncoming(req),
41569
+ socketRemoteAddress: req.socket?.remoteAddress || null,
41570
+ origin: req.headers.origin || null,
41571
+ expectedOrigin,
41572
+ subprotocols: subs
41573
+ });
41574
+ if (!decision.ok) {
41575
+ const status2 = decision.status || 400;
41576
+ const reason = decision.reason || "rejected";
41577
+ wsLog.warn({ status: status2, reason, path: url.pathname }, "terminal upgrade rejected");
41578
+ socket.write(`HTTP/1.1 ${status2} ${reason}\r
41579
+ Content-Length: 0\r
41580
+ \r
41581
+ `);
41582
+ socket.destroy();
41583
+ return;
41584
+ }
41585
+ terminalWss.handleUpgrade(req, socket, head, (ws) => {
41586
+ attachTerminalSocket({
41587
+ socket: ws,
41588
+ ip: decision.ip,
41589
+ cwd: decision.cwd,
41590
+ projectId: decision.projectId ?? null,
41591
+ cols: decision.cols,
41592
+ rows: decision.rows,
41593
+ headers: headersFromIncoming(req)
41594
+ });
41595
+ });
41596
+ }
41597
+ var app = new Elysia({ adapter }).derive({ as: "global" }, ({ request: _request, headers, set: set3 }) => {
41598
+ const traceId = pickTraceId(headers) || randomUUID4();
38926
41599
  set3.headers = { ...set3.headers || {}, "x-kite-trace-id": traceId };
38927
41600
  return { _start: performance.now(), traceId };
38928
41601
  }).onAfterHandle({ as: "global" }, ({ request, set: set3, _start, traceId }) => {
@@ -38931,54 +41604,71 @@ var app = new Elysia({ adapter }).derive({ as: "global" }, ({ request, headers,
38931
41604
  httpLog.info({ traceId, method: request.method, path: new URL(request.url).pathname, status: status2, ms }, `${request.method} ${new URL(request.url).pathname} ${status2} ${ms}ms`);
38932
41605
  }).onError({ as: "global" }, ({ request, error: error3, traceId }) => {
38933
41606
  httpLog.error({ traceId, method: request?.method, path: request ? new URL(request.url).pathname : undefined, err: { name: error3?.name, message: error3?.message, stack: error3?.stack } }, "request error");
38934
- }).use(deployRoutes).use(settingsRoutes).use(migrationRoutes).use(auditRoutes).use(healthRoutes).use(diskRoutes).use(statsRoutes).use(fsRoutes).use(categoryRoutes).use(staticPlugin);
38935
- if (isBun3) {
38936
- app.listen({ port, hostname: host });
38937
- rootLogger.info({ module: "boot", runtime: runtimeName, version: serverVersion2, host: app.server?.hostname, port: app.server?.port }, `Kite server listening on http://${app.server?.hostname}:${app.server?.port}`);
38938
- } else {
38939
- const fetchHandler = app.fetch;
38940
- const server = http.createServer(async (req, res) => {
38941
- const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
38942
- const headers = new Headers;
38943
- for (const [key, value2] of Object.entries(req.headers)) {
38944
- if (value2) {
38945
- headers.set(key, Array.isArray(value2) ? value2.join(", ") : value2);
38946
- }
41607
+ }).use(deployRoutes).use(settingsRoutes).use(migrationRoutes).use(auditRoutes).use(healthRoutes).use(diskRoutes).use(statsRoutes).use(fsRoutes).use(categoryRoutes).use(logSourceRoutes).use(systemRoutes).use(pm2Routes).use(tagRoutes).use(terminalRoutes).use(staticPlugin);
41608
+ var fetchHandler = app.fetch;
41609
+ var server = http.createServer(async (req, res) => {
41610
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
41611
+ const headers = new Headers;
41612
+ for (const [key, value2] of Object.entries(req.headers)) {
41613
+ if (value2) {
41614
+ headers.set(key, Array.isArray(value2) ? value2.join(", ") : value2);
38947
41615
  }
38948
- const hasBody = req.method !== "GET" && req.method !== "HEAD";
38949
- const request = new Request(url.toString(), {
38950
- method: req.method,
38951
- headers,
38952
- body: hasBody ? new ReadableStream({
38953
- start(controller) {
38954
- req.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
38955
- req.on("end", () => controller.close());
38956
- req.on("error", (err) => controller.error(err));
38957
- }
38958
- }) : undefined,
38959
- duplex: hasBody ? "half" : undefined
38960
- });
38961
- const response = await fetchHandler(request);
38962
- res.writeHead(response.status, Object.fromEntries(response.headers));
38963
- if (response.body) {
38964
- const reader = response.body.getReader();
38965
- const pump = async () => {
38966
- const { done, value: value2 } = await reader.read();
38967
- if (done) {
38968
- res.end();
38969
- return;
38970
- }
38971
- res.write(value2);
38972
- await pump();
38973
- };
41616
+ }
41617
+ const hasBody = req.method !== "GET" && req.method !== "HEAD";
41618
+ const request = new Request(url.toString(), {
41619
+ method: req.method,
41620
+ headers,
41621
+ body: hasBody ? new ReadableStream({
41622
+ start(controller) {
41623
+ req.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
41624
+ req.on("end", () => controller.close());
41625
+ req.on("error", (err) => controller.error(err));
41626
+ }
41627
+ }) : undefined,
41628
+ duplex: hasBody ? "half" : undefined
41629
+ });
41630
+ const response = await fetchHandler(request);
41631
+ res.writeHead(response.status, Object.fromEntries(response.headers));
41632
+ if (response.body) {
41633
+ const reader = response.body.getReader();
41634
+ const pump = async () => {
41635
+ const { done, value: value2 } = await reader.read();
41636
+ if (done) {
41637
+ res.end();
41638
+ return;
41639
+ }
41640
+ res.write(value2);
38974
41641
  await pump();
38975
- } else {
38976
- res.end();
41642
+ };
41643
+ await pump();
41644
+ } else {
41645
+ res.end();
41646
+ }
41647
+ });
41648
+ if (terminalWss) {
41649
+ server.on("upgrade", (req, socket, head) => {
41650
+ const url = new URL(req.url || "/", `http://${req.headers.host || `${host}:${port}`}`);
41651
+ if (!url.pathname.startsWith("/api/terminal/ws")) {
41652
+ socket.destroy();
41653
+ return;
38977
41654
  }
41655
+ const reqHost = req.headers.host || `${host}:${port}`;
41656
+ const expectedOrigin = `http://${reqHost}`;
41657
+ handleTerminalUpgrade(req, socket, head, expectedOrigin);
38978
41658
  });
38979
- server.listen(port, host, () => {
38980
- rootLogger.info({ module: "boot", runtime: runtimeName, version: serverVersion2, host, port }, `Kite server listening on http://${host}:${port}`);
38981
- });
41659
+ } else {
41660
+ rootLogger.warn({ module: "terminal" }, "ws 包不可用,终端 WebSocket 已关闭");
38982
41661
  }
41662
+ server.listen(port, host, () => {
41663
+ rootLogger.info({ module: "boot", runtime: runtimeName, version: serverVersion2, host, port }, `Kite server listening on http://${host}:${port}`);
41664
+ });
38983
41665
  var adminTokenPreview = process.env.ADMIN_TOKEN ? `${process.env.ADMIN_TOKEN.slice(0, 4)}****${process.env.ADMIN_TOKEN.slice(-4)}` : "未设置 (请通过 .env.local 配置)";
38984
41666
  rootLogger.info({ module: "boot" }, `Admin token: ${adminTokenPreview}`);
41667
+ for (const sig of ["SIGINT", "SIGTERM"]) {
41668
+ process.on(sig, () => {
41669
+ try {
41670
+ shutdownAllSessions(sig);
41671
+ } catch {}
41672
+ process.exit(0);
41673
+ });
41674
+ }