vibora 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/server/index.js CHANGED
@@ -3503,17 +3503,204 @@ import * as fs from "fs";
3503
3503
  import * as path from "path";
3504
3504
  import * as os from "os";
3505
3505
  import * as crypto3 from "crypto";
3506
- var DEFAULT_SETTINGS = {
3507
- port: 3333,
3508
- defaultGitReposDir: os.homedir(),
3509
- remoteHost: "",
3510
- sshPort: 22,
3511
- basicAuthUsername: null,
3512
- basicAuthPassword: null,
3513
- linearApiKey: null,
3514
- githubPat: null,
3515
- language: null
3506
+
3507
+ // server/lib/logger.ts
3508
+ import { appendFileSync } from "fs";
3509
+ import { join } from "path";
3510
+
3511
+ // shared/logger/types.ts
3512
+ var LOG_LEVELS = {
3513
+ debug: 0,
3514
+ info: 1,
3515
+ warn: 2,
3516
+ error: 3
3517
+ };
3518
+ function formatLogEntry(entry) {
3519
+ return JSON.stringify(entry);
3520
+ }
3521
+ // server/lib/logger.ts
3522
+ function getMinLevel() {
3523
+ const level = process.env.LOG_LEVEL;
3524
+ if (level && level in LOG_LEVELS) {
3525
+ return level;
3526
+ }
3527
+ return "info";
3528
+ }
3529
+ var logFilePath = null;
3530
+ function getLogFile() {
3531
+ if (logFilePath !== null) {
3532
+ return logFilePath || null;
3533
+ }
3534
+ try {
3535
+ ensureViboraDir();
3536
+ logFilePath = join(getViboraDir(), "vibora.log");
3537
+ return logFilePath;
3538
+ } catch {
3539
+ logFilePath = "";
3540
+ return null;
3541
+ }
3542
+ }
3543
+ function writeEntry(entry) {
3544
+ const line = formatLogEntry(entry);
3545
+ console.log(line);
3546
+ const logFile = getLogFile();
3547
+ if (logFile) {
3548
+ try {
3549
+ appendFileSync(logFile, line + `
3550
+ `);
3551
+ } catch {}
3552
+ }
3553
+ }
3554
+
3555
+ class ServerLogger {
3556
+ component;
3557
+ minLevel;
3558
+ constructor(component, minLevel) {
3559
+ this.component = component;
3560
+ this.minLevel = minLevel ?? getMinLevel();
3561
+ }
3562
+ shouldLog(level) {
3563
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.minLevel];
3564
+ }
3565
+ log(level, msg, ctx) {
3566
+ if (!this.shouldLog(level))
3567
+ return;
3568
+ writeEntry({
3569
+ ts: new Date().toISOString(),
3570
+ lvl: level,
3571
+ src: this.component,
3572
+ msg,
3573
+ ...ctx && Object.keys(ctx).length > 0 ? { ctx } : {}
3574
+ });
3575
+ }
3576
+ debug(msg, ctx) {
3577
+ this.log("debug", msg, ctx);
3578
+ }
3579
+ info(msg, ctx) {
3580
+ this.log("info", msg, ctx);
3581
+ }
3582
+ warn(msg, ctx) {
3583
+ this.log("warn", msg, ctx);
3584
+ }
3585
+ error(msg, ctx) {
3586
+ this.log("error", msg, ctx);
3587
+ }
3588
+ child(component) {
3589
+ return new ServerLogger(`${this.component}/${component}`, this.minLevel);
3590
+ }
3591
+ }
3592
+ function createLogger(component) {
3593
+ return new ServerLogger(component);
3594
+ }
3595
+ var log2 = {
3596
+ pty: createLogger("PTYManager"),
3597
+ ws: createLogger("WS"),
3598
+ terminal: createLogger("Terminal"),
3599
+ buffer: createLogger("BufferManager"),
3600
+ desktop: createLogger("Desktop"),
3601
+ api: createLogger("API"),
3602
+ metrics: createLogger("MetricsCollector"),
3603
+ pr: createLogger("PRMonitor"),
3604
+ github: createLogger("GitHub"),
3605
+ linear: createLogger("Linear"),
3606
+ notification: createLogger("Notification"),
3607
+ server: createLogger("Server"),
3608
+ settings: createLogger("Settings")
3516
3609
  };
3610
+
3611
+ // server/lib/settings.ts
3612
+ var CURRENT_SCHEMA_VERSION = 2;
3613
+ var DEFAULT_SETTINGS = {
3614
+ _schemaVersion: CURRENT_SCHEMA_VERSION,
3615
+ server: {
3616
+ port: 7777
3617
+ },
3618
+ paths: {
3619
+ defaultGitReposDir: os.homedir()
3620
+ },
3621
+ authentication: {
3622
+ username: null,
3623
+ password: null
3624
+ },
3625
+ remoteVibora: {
3626
+ host: "",
3627
+ port: 7777
3628
+ },
3629
+ editor: {
3630
+ app: "vscode",
3631
+ host: "",
3632
+ sshPort: 22
3633
+ },
3634
+ integrations: {
3635
+ linearApiKey: null,
3636
+ githubPat: null
3637
+ },
3638
+ appearance: {
3639
+ language: null
3640
+ }
3641
+ };
3642
+ var OLD_DEFAULT_PORT = 3333;
3643
+ var MIGRATION_MAP = {
3644
+ port: "server.port",
3645
+ defaultGitReposDir: "paths.defaultGitReposDir",
3646
+ basicAuthUsername: "authentication.username",
3647
+ basicAuthPassword: "authentication.password",
3648
+ remoteHost: "remoteVibora.host",
3649
+ hostname: "remoteVibora.host",
3650
+ sshPort: "editor.sshPort",
3651
+ linearApiKey: "integrations.linearApiKey",
3652
+ githubPat: "integrations.githubPat",
3653
+ language: "appearance.language"
3654
+ };
3655
+ function getNestedValue(obj, path2) {
3656
+ return path2.split(".").reduce((o, k) => {
3657
+ if (o && typeof o === "object") {
3658
+ return o[k];
3659
+ }
3660
+ return;
3661
+ }, obj);
3662
+ }
3663
+ function setNestedValue(obj, path2, value) {
3664
+ const keys = path2.split(".");
3665
+ const lastKey = keys.pop();
3666
+ let current = obj;
3667
+ for (const key of keys) {
3668
+ if (!(key in current) || typeof current[key] !== "object" || current[key] === null) {
3669
+ current[key] = {};
3670
+ }
3671
+ current = current[key];
3672
+ }
3673
+ current[lastKey] = value;
3674
+ }
3675
+ function migrateSettings(parsed) {
3676
+ const result = { migrated: false, migratedKeys: [], warnings: [] };
3677
+ const version = parsed._schemaVersion ?? 1;
3678
+ if (version >= CURRENT_SCHEMA_VERSION) {
3679
+ return result;
3680
+ }
3681
+ for (const [oldKey, newPath] of Object.entries(MIGRATION_MAP)) {
3682
+ if (oldKey in parsed && parsed[oldKey] !== undefined) {
3683
+ const oldValue = parsed[oldKey];
3684
+ if (oldKey === "port" && oldValue === OLD_DEFAULT_PORT) {
3685
+ delete parsed[oldKey];
3686
+ result.migrated = true;
3687
+ continue;
3688
+ }
3689
+ const existingValue = getNestedValue(parsed, newPath);
3690
+ if (existingValue !== undefined) {
3691
+ result.warnings.push(`Key "${oldKey}" exists but "${newPath}" already set. Removing old key.`);
3692
+ } else {
3693
+ setNestedValue(parsed, newPath, oldValue);
3694
+ result.migratedKeys.push(oldKey);
3695
+ }
3696
+ delete parsed[oldKey];
3697
+ result.migrated = true;
3698
+ }
3699
+ }
3700
+ parsed._schemaVersion = CURRENT_SCHEMA_VERSION;
3701
+ result.migrated = true;
3702
+ return result;
3703
+ }
3517
3704
  function expandPath(p) {
3518
3705
  if (p.startsWith("~/")) {
3519
3706
  return path.join(os.homedir(), p.slice(2));
@@ -3575,60 +3762,127 @@ function getSettings() {
3575
3762
  parsed = JSON.parse(content);
3576
3763
  } catch {}
3577
3764
  }
3578
- const allKeys = Object.keys(DEFAULT_SETTINGS);
3579
- const hasMissingKeys = allKeys.some((key) => !(key in parsed));
3765
+ const migrationResult = migrateSettings(parsed);
3766
+ if (migrationResult.migrated) {
3767
+ if (migrationResult.migratedKeys.length > 0) {
3768
+ log2.settings.info("Migrated settings to nested structure", {
3769
+ migratedKeys: migrationResult.migratedKeys
3770
+ });
3771
+ }
3772
+ if (migrationResult.warnings.length > 0) {
3773
+ log2.settings.warn("Settings migration warnings", {
3774
+ warnings: migrationResult.warnings
3775
+ });
3776
+ }
3777
+ fs.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2), "utf-8");
3778
+ }
3580
3779
  const fileSettings = {
3581
- port: parsed.port ?? DEFAULT_SETTINGS.port,
3582
- defaultGitReposDir: expandPath(parsed.defaultGitReposDir ?? DEFAULT_SETTINGS.defaultGitReposDir),
3583
- remoteHost: parsed.remoteHost ?? parsed.hostname ?? DEFAULT_SETTINGS.remoteHost,
3584
- sshPort: parsed.sshPort ?? DEFAULT_SETTINGS.sshPort,
3585
- basicAuthUsername: parsed.basicAuthUsername ?? null,
3586
- basicAuthPassword: parsed.basicAuthPassword ?? null,
3587
- linearApiKey: parsed.linearApiKey ?? null,
3588
- githubPat: parsed.githubPat ?? null,
3589
- language: parsed.language ?? null
3780
+ _schemaVersion: CURRENT_SCHEMA_VERSION,
3781
+ server: {
3782
+ port: parsed.server?.port ?? DEFAULT_SETTINGS.server.port
3783
+ },
3784
+ paths: {
3785
+ defaultGitReposDir: expandPath(parsed.paths?.defaultGitReposDir ?? DEFAULT_SETTINGS.paths.defaultGitReposDir)
3786
+ },
3787
+ authentication: {
3788
+ username: parsed.authentication?.username ?? null,
3789
+ password: parsed.authentication?.password ?? null
3790
+ },
3791
+ remoteVibora: {
3792
+ host: parsed.remoteVibora?.host ?? DEFAULT_SETTINGS.remoteVibora.host,
3793
+ port: parsed.remoteVibora?.port ?? DEFAULT_SETTINGS.remoteVibora.port
3794
+ },
3795
+ editor: {
3796
+ app: parsed.editor?.app ?? DEFAULT_SETTINGS.editor.app,
3797
+ host: parsed.editor?.host ?? DEFAULT_SETTINGS.editor.host,
3798
+ sshPort: parsed.editor?.sshPort ?? DEFAULT_SETTINGS.editor.sshPort
3799
+ },
3800
+ integrations: {
3801
+ linearApiKey: parsed.integrations?.linearApiKey ?? null,
3802
+ githubPat: parsed.integrations?.githubPat ?? null
3803
+ },
3804
+ appearance: {
3805
+ language: parsed.appearance?.language ?? null
3806
+ }
3590
3807
  };
3591
- if (hasMissingKeys) {
3592
- fs.writeFileSync(settingsPath, JSON.stringify(fileSettings, null, 2), "utf-8");
3593
- }
3594
3808
  const portEnv = parseInt(process.env.PORT || "", 10);
3595
- const sshPortEnv = parseInt(process.env.VIBORA_SSH_PORT || "", 10);
3809
+ const editorSshPortEnv = parseInt(process.env.VIBORA_SSH_PORT || "", 10);
3596
3810
  return {
3597
- port: !isNaN(portEnv) && portEnv > 0 ? portEnv : fileSettings.port,
3598
- defaultGitReposDir: process.env.VIBORA_GIT_REPOS_DIR ? expandPath(process.env.VIBORA_GIT_REPOS_DIR) : fileSettings.defaultGitReposDir,
3599
- remoteHost: process.env.VIBORA_REMOTE_HOST ?? process.env.VIBORA_HOSTNAME ?? fileSettings.remoteHost,
3600
- sshPort: !isNaN(sshPortEnv) && sshPortEnv > 0 ? sshPortEnv : fileSettings.sshPort,
3601
- basicAuthUsername: process.env.VIBORA_BASIC_AUTH_USERNAME ?? fileSettings.basicAuthUsername,
3602
- basicAuthPassword: process.env.VIBORA_BASIC_AUTH_PASSWORD ?? fileSettings.basicAuthPassword,
3603
- linearApiKey: process.env.LINEAR_API_KEY ?? fileSettings.linearApiKey,
3604
- githubPat: process.env.GITHUB_PAT ?? fileSettings.githubPat,
3605
- language: fileSettings.language
3811
+ ...fileSettings,
3812
+ server: {
3813
+ port: !isNaN(portEnv) && portEnv > 0 ? portEnv : fileSettings.server.port
3814
+ },
3815
+ paths: {
3816
+ defaultGitReposDir: process.env.VIBORA_GIT_REPOS_DIR ? expandPath(process.env.VIBORA_GIT_REPOS_DIR) : fileSettings.paths.defaultGitReposDir
3817
+ },
3818
+ authentication: {
3819
+ username: process.env.VIBORA_BASIC_AUTH_USERNAME ?? fileSettings.authentication.username,
3820
+ password: process.env.VIBORA_BASIC_AUTH_PASSWORD ?? fileSettings.authentication.password
3821
+ },
3822
+ remoteVibora: {
3823
+ host: process.env.VIBORA_REMOTE_HOST ?? process.env.VIBORA_HOSTNAME ?? fileSettings.remoteVibora.host,
3824
+ port: fileSettings.remoteVibora.port
3825
+ },
3826
+ editor: {
3827
+ app: fileSettings.editor.app,
3828
+ host: process.env.VIBORA_EDITOR_HOST ?? fileSettings.editor.host,
3829
+ sshPort: !isNaN(editorSshPortEnv) && editorSshPortEnv > 0 ? editorSshPortEnv : fileSettings.editor.sshPort
3830
+ },
3831
+ integrations: {
3832
+ linearApiKey: process.env.LINEAR_API_KEY ?? fileSettings.integrations.linearApiKey,
3833
+ githubPat: process.env.GITHUB_PAT ?? fileSettings.integrations.githubPat
3834
+ },
3835
+ appearance: fileSettings.appearance
3606
3836
  };
3607
3837
  }
3608
- function getSetting(key) {
3609
- return getSettings()[key];
3838
+ function getSetting(path2) {
3839
+ const settings = getSettings();
3840
+ return getNestedValue(settings, path2);
3841
+ }
3842
+ function getSettingByKey(key) {
3843
+ const settings = getSettings();
3844
+ const legacySettings = toLegacySettings(settings);
3845
+ return legacySettings[key];
3846
+ }
3847
+ function toLegacySettings(settings) {
3848
+ return {
3849
+ port: settings.server.port,
3850
+ defaultGitReposDir: settings.paths.defaultGitReposDir,
3851
+ remoteHost: settings.remoteVibora.host,
3852
+ sshPort: settings.editor.sshPort,
3853
+ basicAuthUsername: settings.authentication.username,
3854
+ basicAuthPassword: settings.authentication.password,
3855
+ linearApiKey: settings.integrations.linearApiKey,
3856
+ githubPat: settings.integrations.githubPat,
3857
+ language: settings.appearance.language
3858
+ };
3610
3859
  }
3611
3860
  function isDeveloperMode() {
3612
3861
  return process.env.VIBORA_DEVELOPER === "1" || process.env.VIBORA_DEVELOPER === "true";
3613
3862
  }
3614
3863
  function getSessionSecret() {
3615
3864
  const settings = getSettings();
3616
- if (!settings.basicAuthPassword) {
3865
+ if (!settings.authentication.password) {
3617
3866
  return null;
3618
3867
  }
3619
- return crypto3.createHash("sha256").update(settings.basicAuthPassword + "vibora-session").digest("hex");
3868
+ return crypto3.createHash("sha256").update(settings.authentication.password + "vibora-session").digest("hex");
3620
3869
  }
3621
- function updateSettings(updates) {
3870
+ function updateSettingByPath(settingPath, value) {
3622
3871
  ensureViboraDir();
3623
- const current = getSettings();
3624
- const updated = { ...current, ...updates };
3625
- fs.writeFileSync(getSettingsPath(), JSON.stringify(updated, null, 2), "utf-8");
3626
- return updated;
3872
+ const settingsPath = getSettingsPath();
3873
+ let parsed = {};
3874
+ if (fs.existsSync(settingsPath)) {
3875
+ try {
3876
+ parsed = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
3877
+ } catch {}
3878
+ }
3879
+ setNestedValue(parsed, settingPath, value);
3880
+ parsed._schemaVersion = CURRENT_SCHEMA_VERSION;
3881
+ fs.writeFileSync(settingsPath, JSON.stringify(parsed, null, 2), "utf-8");
3882
+ return getSettings();
3627
3883
  }
3628
- function resetSettings() {
3629
- ensureViboraDir();
3630
- fs.writeFileSync(getSettingsPath(), JSON.stringify(DEFAULT_SETTINGS, null, 2), "utf-8");
3631
- return { ...DEFAULT_SETTINGS };
3884
+ function getDefaultValue(settingPath) {
3885
+ return getNestedValue(DEFAULT_SETTINGS, settingPath);
3632
3886
  }
3633
3887
  var DEFAULT_NOTIFICATION_SETTINGS = {
3634
3888
  enabled: false,
@@ -4444,7 +4698,7 @@ function sql(strings, ...params) {
4444
4698
  return new SQL([new StringChunk(str)]);
4445
4699
  }
4446
4700
  sql2.raw = raw2;
4447
- function join2(chunks, separator) {
4701
+ function join3(chunks, separator) {
4448
4702
  const result = [];
4449
4703
  for (const [i, chunk] of chunks.entries()) {
4450
4704
  if (i > 0 && separator !== undefined) {
@@ -4454,7 +4708,7 @@ function sql(strings, ...params) {
4454
4708
  }
4455
4709
  return new SQL(result);
4456
4710
  }
4457
- sql2.join = join2;
4711
+ sql2.join = join3;
4458
4712
  function identifier(value) {
4459
4713
  return new Name(value);
4460
4714
  }
@@ -6577,7 +6831,7 @@ class SQLiteSelectQueryBuilderBase extends TypedQueryBuilder {
6577
6831
  const tableName = getTableLikeName(table);
6578
6832
  for (const item of extractUsedTable(table))
6579
6833
  this.usedTables.add(item);
6580
- if (typeof tableName === "string" && this.config.joins?.some((join2) => join2.alias === tableName)) {
6834
+ if (typeof tableName === "string" && this.config.joins?.some((join3) => join3.alias === tableName)) {
6581
6835
  throw new Error(`Alias "${tableName}" is already used in this query`);
6582
6836
  }
6583
6837
  if (!this.isPartialSelect) {
@@ -6991,7 +7245,7 @@ class SQLiteUpdateBase extends QueryPromise {
6991
7245
  createJoin(joinType) {
6992
7246
  return (table, on) => {
6993
7247
  const tableName = getTableLikeName(table);
6994
- if (typeof tableName === "string" && this.config.joins.some((join2) => join2.alias === tableName)) {
7248
+ if (typeof tableName === "string" && this.config.joins.some((join3) => join3.alias === tableName)) {
6995
7249
  throw new Error(`Alias "${tableName}" is already used in this query`);
6996
7250
  }
6997
7251
  if (typeof on === "function") {
@@ -7734,7 +7988,7 @@ function migrate(db, config) {
7734
7988
 
7735
7989
  // server/db/index.ts
7736
7990
  import { Database as Database2 } from "bun:sqlite";
7737
- import { join as join2 } from "path";
7991
+ import { join as join3 } from "path";
7738
7992
  import { readdirSync } from "fs";
7739
7993
 
7740
7994
  // server/db/schema.ts
@@ -7804,6 +8058,7 @@ var repositories = sqliteTable("repositories", {
7804
8058
  startupScript: text("startup_script"),
7805
8059
  copyFiles: text("copy_files"),
7806
8060
  remoteUrl: text("remote_url"),
8061
+ lastUsedAt: text("last_used_at"),
7807
8062
  createdAt: text("created_at").notNull(),
7808
8063
  updatedAt: text("updated_at").notNull()
7809
8064
  });
@@ -7825,7 +8080,7 @@ var sqlite = new Database2(dbPath);
7825
8080
  sqlite.exec("PRAGMA journal_mode = WAL");
7826
8081
  var db = drizzle(sqlite, { schema: exports_schema });
7827
8082
  if (process.env.VIBORA_PACKAGE_ROOT) {
7828
- const migrationsPath = join2(process.env.VIBORA_PACKAGE_ROOT, "drizzle");
8083
+ const migrationsPath = join3(process.env.VIBORA_PACKAGE_ROOT, "drizzle");
7829
8084
  const hasTasksTable = sqlite.query("SELECT name FROM sqlite_master WHERE type='table' AND name='tasks'").get();
7830
8085
  if (hasTasksTable) {
7831
8086
  sqlite.exec(`
@@ -13401,7 +13656,7 @@ glob.glob = glob;
13401
13656
  // node_modules/bun-pty/dist/index.js
13402
13657
  import { dlopen, FFIType, ptr } from "bun:ffi";
13403
13658
  import { Buffer as Buffer2 } from "buffer";
13404
- import { join as join3, dirname as dirname2, basename } from "path";
13659
+ import { join as join4, dirname as dirname2, basename } from "path";
13405
13660
  import { existsSync as existsSync2 } from "fs";
13406
13661
 
13407
13662
  class EventEmitter2 {
@@ -13444,14 +13699,14 @@ function resolveLibPath() {
13444
13699
  const dirName = basename(fileDir);
13445
13700
  const here = dirName === "src" || dirName === "dist" ? dirname2(fileDir) : fileDir;
13446
13701
  const basePaths = [
13447
- join3(here, "rust-pty", "target", "release"),
13448
- join3(here, "..", "bun-pty", "rust-pty", "target", "release"),
13449
- join3(process.cwd(), "node_modules", "bun-pty", "rust-pty", "target", "release")
13702
+ join4(here, "rust-pty", "target", "release"),
13703
+ join4(here, "..", "bun-pty", "rust-pty", "target", "release"),
13704
+ join4(process.cwd(), "node_modules", "bun-pty", "rust-pty", "target", "release")
13450
13705
  ];
13451
13706
  const fallbackPaths = [];
13452
13707
  for (const basePath of basePaths) {
13453
13708
  for (const filename of filenames) {
13454
- fallbackPaths.push(join3(basePath, filename));
13709
+ fallbackPaths.push(join4(basePath, filename));
13455
13710
  }
13456
13711
  }
13457
13712
  for (const path3 of fallbackPaths) {
@@ -13724,111 +13979,6 @@ function getDtachService() {
13724
13979
  // server/terminal/buffer-manager.ts
13725
13980
  import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, unlinkSync } from "fs";
13726
13981
  import * as path4 from "path";
13727
-
13728
- // server/lib/logger.ts
13729
- import { appendFileSync } from "fs";
13730
- import { join as join5 } from "path";
13731
-
13732
- // shared/logger/types.ts
13733
- var LOG_LEVELS = {
13734
- debug: 0,
13735
- info: 1,
13736
- warn: 2,
13737
- error: 3
13738
- };
13739
- function formatLogEntry(entry) {
13740
- return JSON.stringify(entry);
13741
- }
13742
- // server/lib/logger.ts
13743
- function getMinLevel() {
13744
- const level = process.env.LOG_LEVEL;
13745
- if (level && level in LOG_LEVELS) {
13746
- return level;
13747
- }
13748
- return "info";
13749
- }
13750
- var logFilePath = null;
13751
- function getLogFile() {
13752
- if (logFilePath !== null) {
13753
- return logFilePath || null;
13754
- }
13755
- try {
13756
- ensureViboraDir();
13757
- logFilePath = join5(getViboraDir(), "vibora.log");
13758
- return logFilePath;
13759
- } catch {
13760
- logFilePath = "";
13761
- return null;
13762
- }
13763
- }
13764
- function writeEntry(entry) {
13765
- const line = formatLogEntry(entry);
13766
- console.log(line);
13767
- const logFile = getLogFile();
13768
- if (logFile) {
13769
- try {
13770
- appendFileSync(logFile, line + `
13771
- `);
13772
- } catch {}
13773
- }
13774
- }
13775
-
13776
- class ServerLogger {
13777
- component;
13778
- minLevel;
13779
- constructor(component, minLevel) {
13780
- this.component = component;
13781
- this.minLevel = minLevel ?? getMinLevel();
13782
- }
13783
- shouldLog(level) {
13784
- return LOG_LEVELS[level] >= LOG_LEVELS[this.minLevel];
13785
- }
13786
- log(level, msg, ctx) {
13787
- if (!this.shouldLog(level))
13788
- return;
13789
- writeEntry({
13790
- ts: new Date().toISOString(),
13791
- lvl: level,
13792
- src: this.component,
13793
- msg,
13794
- ...ctx && Object.keys(ctx).length > 0 ? { ctx } : {}
13795
- });
13796
- }
13797
- debug(msg, ctx) {
13798
- this.log("debug", msg, ctx);
13799
- }
13800
- info(msg, ctx) {
13801
- this.log("info", msg, ctx);
13802
- }
13803
- warn(msg, ctx) {
13804
- this.log("warn", msg, ctx);
13805
- }
13806
- error(msg, ctx) {
13807
- this.log("error", msg, ctx);
13808
- }
13809
- child(component) {
13810
- return new ServerLogger(`${this.component}/${component}`, this.minLevel);
13811
- }
13812
- }
13813
- function createLogger(component) {
13814
- return new ServerLogger(component);
13815
- }
13816
- var log2 = {
13817
- pty: createLogger("PTYManager"),
13818
- ws: createLogger("WS"),
13819
- terminal: createLogger("Terminal"),
13820
- buffer: createLogger("BufferManager"),
13821
- desktop: createLogger("Desktop"),
13822
- api: createLogger("API"),
13823
- metrics: createLogger("MetricsCollector"),
13824
- pr: createLogger("PRMonitor"),
13825
- github: createLogger("GitHub"),
13826
- linear: createLogger("Linear"),
13827
- notification: createLogger("Notification"),
13828
- server: createLogger("Server")
13829
- };
13830
-
13831
- // server/terminal/buffer-manager.ts
13832
13982
  var MAX_BUFFER_BYTES = 1e6;
13833
13983
  function getBuffersDir() {
13834
13984
  const dir = path4.join(getViboraDir(), "buffers");
@@ -137757,7 +137907,7 @@ var LinearClient = class extends LinearSdk {
137757
137907
  // server/services/linear.ts
137758
137908
  var linearClient = null;
137759
137909
  function getLinearClient() {
137760
- const apiKey = getSetting("linearApiKey");
137910
+ const apiKey = getSetting("integrations.linearApiKey");
137761
137911
  if (!apiKey)
137762
137912
  return null;
137763
137913
  if (!linearClient) {
@@ -137935,7 +138085,7 @@ async function sendPushoverNotification(config, payload) {
137935
138085
  return { channel: "pushover", success: false, error: message };
137936
138086
  }
137937
138087
  }
137938
- function broadcastUINotification(payload) {
138088
+ function broadcastUINotification(payload, playSound) {
137939
138089
  const notificationType = payload.type === "pr_merged" || payload.type === "plan_complete" ? "success" : "info";
137940
138090
  broadcast({
137941
138091
  type: "notification",
@@ -137944,7 +138094,8 @@ function broadcastUINotification(payload) {
137944
138094
  title: payload.title,
137945
138095
  message: payload.message,
137946
138096
  notificationType,
137947
- taskId: payload.taskId
138097
+ taskId: payload.taskId,
138098
+ playSound
137948
138099
  }
137949
138100
  });
137950
138101
  }
@@ -137955,7 +138106,8 @@ async function sendNotification(payload) {
137955
138106
  }
137956
138107
  const results = [];
137957
138108
  const promises = [];
137958
- broadcastUINotification(payload);
138109
+ const playSound = settings.sound?.enabled ?? false;
138110
+ broadcastUINotification(payload, playSound);
137959
138111
  if (settings.sound?.enabled) {
137960
138112
  promises.push(sendSoundNotification(settings.sound).then((r) => results.push(r)).catch((e) => results.push({ channel: "sound", success: false, error: e.message })));
137961
138113
  }
@@ -138172,6 +138324,9 @@ app2.post("/", async (c) => {
138172
138324
  }
138173
138325
  }
138174
138326
  db.insert(tasks).values(newTask).run();
138327
+ if (body.repoPath) {
138328
+ db.update(repositories).set({ lastUsedAt: now, updatedAt: now }).where(eq(repositories.path, body.repoPath)).run();
138329
+ }
138175
138330
  const created = db.select().from(tasks).where(eq(tasks.id, newTask.id)).get();
138176
138331
  broadcast({ type: "task:updated", payload: { taskId: newTask.id } });
138177
138332
  return c.json(created ? parseViewState(created) : null, 201);
@@ -139200,16 +139355,61 @@ var filesystem_default = app4;
139200
139355
  // server/routes/config.ts
139201
139356
  import { spawn as spawn3 } from "child_process";
139202
139357
  var CONFIG_KEYS = {
139203
- PORT: "port",
139204
- DEFAULT_GIT_REPOS_DIR: "defaultGitReposDir",
139205
- REMOTE_HOST: "remoteHost",
139206
- SSH_PORT: "sshPort",
139207
- LINEAR_API_KEY: "linearApiKey",
139208
- GITHUB_PAT: "githubPat",
139209
- LANGUAGE: "language",
139210
- BASIC_AUTH_USERNAME: "basicAuthUsername",
139211
- BASIC_AUTH_PASSWORD: "basicAuthPassword"
139212
- };
139358
+ PORT: "server.port",
139359
+ DEFAULT_GIT_REPOS_DIR: "paths.defaultGitReposDir",
139360
+ BASIC_AUTH_USERNAME: "authentication.username",
139361
+ BASIC_AUTH_PASSWORD: "authentication.password",
139362
+ REMOTE_HOST: "remoteVibora.host",
139363
+ REMOTE_PORT: "remoteVibora.port",
139364
+ EDITOR_APP: "editor.app",
139365
+ EDITOR_HOST: "editor.host",
139366
+ EDITOR_SSH_PORT: "editor.sshPort",
139367
+ LINEAR_API_KEY: "integrations.linearApiKey",
139368
+ GITHUB_PAT: "integrations.githubPat",
139369
+ LANGUAGE: "appearance.language"
139370
+ };
139371
+ var LEGACY_KEY_MAP = {
139372
+ port: "server.port",
139373
+ default_git_repos_dir: "paths.defaultGitReposDir",
139374
+ basic_auth_username: "authentication.username",
139375
+ basic_auth_password: "authentication.password",
139376
+ remote_host: "remoteVibora.host",
139377
+ hostname: "remoteVibora.host",
139378
+ ssh_port: "editor.sshPort",
139379
+ linear_api_key: "integrations.linearApiKey",
139380
+ github_pat: "integrations.githubPat",
139381
+ language: "appearance.language",
139382
+ defaultGitReposDir: "paths.defaultGitReposDir",
139383
+ basicAuthUsername: "authentication.username",
139384
+ basicAuthPassword: "authentication.password",
139385
+ remoteHost: "remoteVibora.host",
139386
+ sshPort: "editor.sshPort",
139387
+ linearApiKey: "integrations.linearApiKey",
139388
+ githubPat: "integrations.githubPat"
139389
+ };
139390
+ var VALID_PATHS = new Set(Object.values(CONFIG_KEYS));
139391
+ function resolveConfigKey(key) {
139392
+ if (VALID_PATHS.has(key)) {
139393
+ return key;
139394
+ }
139395
+ if (key in LEGACY_KEY_MAP) {
139396
+ return LEGACY_KEY_MAP[key];
139397
+ }
139398
+ return null;
139399
+ }
139400
+ function getSettingValue(path8) {
139401
+ const settings = getSettings();
139402
+ const parts = path8.split(".");
139403
+ let current = settings;
139404
+ for (const part of parts) {
139405
+ if (current && typeof current === "object" && part in current) {
139406
+ current = current[part];
139407
+ } else {
139408
+ return;
139409
+ }
139410
+ }
139411
+ return current;
139412
+ }
139213
139413
  var app5 = new Hono2;
139214
139414
  app5.get("/notifications", (c) => {
139215
139415
  const notifications = getNotificationSettings();
@@ -139299,134 +139499,81 @@ app5.post("/restart", (c) => {
139299
139499
  return c.json({ error: "Restart only available in developer mode" }, 403);
139300
139500
  }
139301
139501
  setTimeout(() => {
139302
- spawn3("bash", ["-c", "cd ~/projects/vibora && mise run build && systemctl --user restart vibora-dev"], {
139502
+ spawn3("bash", ["-c", "cd ~/projects/vibora && mise run build && bun run drizzle-kit push && systemctl --user restart vibora-dev"], {
139303
139503
  detached: true,
139304
139504
  stdio: "ignore"
139305
139505
  }).unref();
139306
139506
  }, 100);
139307
- return c.json({ success: true, message: "Restart initiated (building first)" });
139507
+ return c.json({ success: true, message: "Restart initiated (build + migrate + restart)" });
139308
139508
  });
139309
139509
  app5.get("/:key", (c) => {
139310
139510
  const key = c.req.param("key");
139311
- const settings = getSettings();
139312
- let value = null;
139313
- if (key === "port" || key === CONFIG_KEYS.PORT) {
139314
- value = settings.port;
139315
- } else if (key === "default_git_repos_dir" || key === CONFIG_KEYS.DEFAULT_GIT_REPOS_DIR) {
139316
- value = settings.defaultGitReposDir;
139317
- } else if (key === "remote_host" || key === "remoteHost" || key === CONFIG_KEYS.REMOTE_HOST || key === "hostname") {
139318
- value = settings.remoteHost;
139319
- } else if (key === "ssh_port" || key === CONFIG_KEYS.SSH_PORT) {
139320
- value = settings.sshPort;
139321
- } else if (key === "linear_api_key" || key === CONFIG_KEYS.LINEAR_API_KEY) {
139322
- value = settings.linearApiKey;
139323
- } else if (key === "github_pat" || key === CONFIG_KEYS.GITHUB_PAT) {
139324
- value = settings.githubPat;
139325
- } else if (key === "language" || key === CONFIG_KEYS.LANGUAGE) {
139326
- return c.json({ key, value: settings.language, isDefault: settings.language === null });
139327
- } else if (key === "basic_auth_username" || key === CONFIG_KEYS.BASIC_AUTH_USERNAME) {
139328
- return c.json({ key, value: settings.basicAuthUsername, isDefault: settings.basicAuthUsername === null });
139329
- } else if (key === "basic_auth_password" || key === CONFIG_KEYS.BASIC_AUTH_PASSWORD) {
139330
- return c.json({ key, value: settings.basicAuthPassword ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : null, isDefault: settings.basicAuthPassword === null });
139331
- } else if (key === "worktree_base_path") {
139511
+ if (key === "worktree_base_path") {
139332
139512
  return c.json({ key, value: getWorktreeBasePath(), isDefault: true });
139333
139513
  }
139334
- if (value === null) {
139335
- return c.json({ key, value: null, isDefault: true });
139514
+ const path8 = resolveConfigKey(key);
139515
+ if (!path8) {
139516
+ return c.json({ key, value: null, isDefault: true, error: "Unknown config key" }, 404);
139517
+ }
139518
+ const value = getSettingValue(path8);
139519
+ const defaultValue = getDefaultValue(path8);
139520
+ const isDefault = value === defaultValue || value === undefined || value === null;
139521
+ if (path8 === CONFIG_KEYS.BASIC_AUTH_PASSWORD) {
139522
+ return c.json({
139523
+ key,
139524
+ value: value ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : null,
139525
+ isDefault: value === null || value === undefined
139526
+ });
139336
139527
  }
139337
- return c.json({ key, value, isDefault: false });
139528
+ return c.json({ key, value: value ?? defaultValue, isDefault });
139338
139529
  });
139339
139530
  app5.put("/:key", async (c) => {
139340
139531
  const key = c.req.param("key");
139532
+ const path8 = resolveConfigKey(key);
139533
+ if (!path8) {
139534
+ return c.json({ error: `Unknown or read-only config key: ${key}` }, 400);
139535
+ }
139341
139536
  try {
139342
139537
  const body = await c.req.json();
139343
- if (key === "port" || key === CONFIG_KEYS.PORT) {
139344
- const port = typeof body.value === "number" ? body.value : parseInt(body.value, 10);
139538
+ let { value } = body;
139539
+ if (path8 === CONFIG_KEYS.PORT || path8 === CONFIG_KEYS.REMOTE_PORT || path8 === CONFIG_KEYS.EDITOR_SSH_PORT) {
139540
+ const port = typeof value === "number" ? value : parseInt(value, 10);
139345
139541
  if (isNaN(port) || port < 1 || port > 65535) {
139346
139542
  return c.json({ error: "Port must be a number between 1 and 65535" }, 400);
139347
139543
  }
139348
- updateSettings({ port });
139349
- return c.json({ key, value: port });
139350
- } else if (key === "default_git_repos_dir" || key === CONFIG_KEYS.DEFAULT_GIT_REPOS_DIR) {
139351
- if (typeof body.value !== "string") {
139352
- return c.json({ error: "Value must be a string" }, 400);
139353
- }
139354
- updateSettings({ defaultGitReposDir: body.value });
139355
- return c.json({ key, value: body.value });
139356
- } else if (key === "remote_host" || key === "remoteHost" || key === CONFIG_KEYS.REMOTE_HOST || key === "hostname") {
139357
- if (typeof body.value !== "string") {
139358
- return c.json({ error: "Value must be a string" }, 400);
139359
- }
139360
- updateSettings({ remoteHost: body.value });
139361
- return c.json({ key, value: body.value });
139362
- } else if (key === "ssh_port" || key === CONFIG_KEYS.SSH_PORT) {
139363
- const sshPort = typeof body.value === "number" ? body.value : parseInt(body.value, 10);
139364
- if (isNaN(sshPort) || sshPort < 1 || sshPort > 65535) {
139365
- return c.json({ error: "SSH port must be a number between 1 and 65535" }, 400);
139366
- }
139367
- updateSettings({ sshPort });
139368
- return c.json({ key, value: sshPort });
139369
- } else if (key === "linear_api_key" || key === CONFIG_KEYS.LINEAR_API_KEY) {
139370
- if (typeof body.value !== "string") {
139371
- return c.json({ error: "Value must be a string" }, 400);
139372
- }
139373
- updateSettings({ linearApiKey: body.value || null });
139374
- return c.json({ key, value: body.value });
139375
- } else if (key === "github_pat" || key === CONFIG_KEYS.GITHUB_PAT) {
139376
- if (typeof body.value !== "string") {
139377
- return c.json({ error: "Value must be a string" }, 400);
139378
- }
139379
- updateSettings({ githubPat: body.value || null });
139380
- return c.json({ key, value: body.value });
139381
- } else if (key === "language" || key === CONFIG_KEYS.LANGUAGE) {
139382
- const langValue = body.value === "" || body.value === null ? null : body.value;
139383
- if (langValue !== null && langValue !== "en" && langValue !== "zh") {
139544
+ value = port;
139545
+ } else if (path8 === CONFIG_KEYS.LANGUAGE) {
139546
+ if (value !== null && value !== "" && value !== "en" && value !== "zh") {
139384
139547
  return c.json({ error: 'Language must be "en", "zh", or null' }, 400);
139385
139548
  }
139386
- updateSettings({ language: langValue });
139387
- return c.json({ key, value: langValue });
139388
- } else if (key === "basic_auth_username" || key === CONFIG_KEYS.BASIC_AUTH_USERNAME) {
139389
- if (typeof body.value !== "string") {
139390
- return c.json({ error: "Value must be a string" }, 400);
139549
+ value = value === "" ? null : value;
139550
+ } else if (path8 === CONFIG_KEYS.EDITOR_APP) {
139551
+ const validApps = ["vscode", "cursor", "windsurf", "zed"];
139552
+ if (!validApps.includes(value)) {
139553
+ return c.json({ error: `Editor app must be one of: ${validApps.join(", ")}` }, 400);
139391
139554
  }
139392
- updateSettings({ basicAuthUsername: body.value || null });
139393
- return c.json({ key, value: body.value });
139394
- } else if (key === "basic_auth_password" || key === CONFIG_KEYS.BASIC_AUTH_PASSWORD) {
139395
- if (typeof body.value !== "string") {
139396
- return c.json({ error: "Value must be a string" }, 400);
139555
+ } else if (typeof value === "string" && value === "") {
139556
+ if (path8 === CONFIG_KEYS.LINEAR_API_KEY || path8 === CONFIG_KEYS.GITHUB_PAT || path8 === CONFIG_KEYS.BASIC_AUTH_USERNAME || path8 === CONFIG_KEYS.BASIC_AUTH_PASSWORD || path8 === CONFIG_KEYS.REMOTE_HOST || path8 === CONFIG_KEYS.EDITOR_HOST) {
139557
+ value = null;
139397
139558
  }
139398
- updateSettings({ basicAuthPassword: body.value || null });
139399
- return c.json({ key, value: body.value ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : null });
139400
- } else {
139401
- return c.json({ error: `Unknown or read-only config key: ${key}` }, 400);
139402
139559
  }
139560
+ updateSettingByPath(path8, value);
139561
+ if (path8 === CONFIG_KEYS.BASIC_AUTH_PASSWORD) {
139562
+ return c.json({ key, value: value ? "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022" : null });
139563
+ }
139564
+ return c.json({ key, value });
139403
139565
  } catch (err) {
139404
139566
  return c.json({ error: err instanceof Error ? err.message : "Failed to set config" }, 400);
139405
139567
  }
139406
139568
  });
139407
139569
  app5.delete("/:key", (c) => {
139408
139570
  const key = c.req.param("key");
139409
- const defaults2 = resetSettings();
139410
- let defaultValue = null;
139411
- if (key === "port" || key === CONFIG_KEYS.PORT) {
139412
- defaultValue = defaults2.port;
139413
- } else if (key === "default_git_repos_dir" || key === CONFIG_KEYS.DEFAULT_GIT_REPOS_DIR) {
139414
- defaultValue = defaults2.defaultGitReposDir;
139415
- } else if (key === "remote_host" || key === "remoteHost" || key === CONFIG_KEYS.REMOTE_HOST || key === "hostname") {
139416
- defaultValue = defaults2.remoteHost;
139417
- } else if (key === "ssh_port" || key === CONFIG_KEYS.SSH_PORT) {
139418
- defaultValue = defaults2.sshPort;
139419
- } else if (key === "linear_api_key" || key === CONFIG_KEYS.LINEAR_API_KEY) {
139420
- defaultValue = defaults2.linearApiKey;
139421
- } else if (key === "github_pat" || key === CONFIG_KEYS.GITHUB_PAT) {
139422
- defaultValue = defaults2.githubPat;
139423
- } else if (key === "language" || key === CONFIG_KEYS.LANGUAGE) {
139424
- defaultValue = defaults2.language;
139425
- } else if (key === "basic_auth_username" || key === CONFIG_KEYS.BASIC_AUTH_USERNAME) {
139426
- defaultValue = defaults2.basicAuthUsername;
139427
- } else if (key === "basic_auth_password" || key === CONFIG_KEYS.BASIC_AUTH_PASSWORD) {
139428
- defaultValue = defaults2.basicAuthPassword;
139571
+ const path8 = resolveConfigKey(key);
139572
+ if (!path8) {
139573
+ return c.json({ error: `Unknown config key: ${key}` }, 400);
139429
139574
  }
139575
+ const defaultValue = getDefaultValue(path8);
139576
+ updateSettingByPath(path8, defaultValue);
139430
139577
  return c.json({ key, value: defaultValue, isDefault: true });
139431
139578
  });
139432
139579
  var config_default = app5;
@@ -139934,7 +140081,7 @@ var terminal_view_state_default = app8;
139934
140081
  // server/routes/repositories.ts
139935
140082
  var app9 = new Hono2;
139936
140083
  app9.get("/", (c) => {
139937
- const allRepos = db.select().from(repositories).all();
140084
+ const allRepos = db.select().from(repositories).orderBy(desc(sql`COALESCE(${repositories.lastUsedAt}, '1970-01-01')`), desc(repositories.createdAt)).all();
139938
140085
  return c.json(allRepos);
139939
140086
  });
139940
140087
  app9.get("/:id", (c) => {
@@ -143486,7 +143633,7 @@ var Octokit2 = Octokit.plugin(requestLog, legacyRestEndpointMethods, paginateRes
143486
143633
  var octokitClient = null;
143487
143634
  var cachedPat = null;
143488
143635
  function getOctokit() {
143489
- const pat = getSetting("githubPat");
143636
+ const pat = getSetting("integrations.githubPat");
143490
143637
  if (!pat)
143491
143638
  return null;
143492
143639
  if (pat !== cachedPat) {
@@ -144490,7 +144637,7 @@ function findViboraInstances() {
144490
144637
  const isDevBackend = isBunProcess && cmdline.includes("server/index.ts") && env.NODE_ENV !== "production";
144491
144638
  const isProdBackend = isBunProcess && (!!env.VIBORA_PACKAGE_ROOT || cmdline.includes("server/index.ts") && env.NODE_ENV === "production");
144492
144639
  if (isDevBackend || isProdBackend) {
144493
- const port = parseInt(env.PORT || "3333", 10);
144640
+ const port = parseInt(env.PORT || "7777", 10);
144494
144641
  const cwd = getProcessCwd(pid);
144495
144642
  let viboraDir = env.VIBORA_DIR || (isDevBackend ? "~/.vibora/dev" : "~/.vibora");
144496
144643
  if (viboraDir.startsWith(".") && cwd !== "(unknown)") {
@@ -144899,7 +145046,7 @@ function stopPRMonitor() {
144899
145046
  }
144900
145047
 
144901
145048
  // server/index.ts
144902
- var PORT = getSetting("port");
145049
+ var PORT = getSettingByKey("port");
144903
145050
  var ptyManager2 = initPTYManager({
144904
145051
  onData: (terminalId, data) => {
144905
145052
  broadcastToTerminal(terminalId, {