vibora 2.2.2 → 2.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.
package/server/index.js CHANGED
@@ -3610,6 +3610,7 @@ var log2 = {
3610
3610
 
3611
3611
  // server/lib/settings.ts
3612
3612
  var CURRENT_SCHEMA_VERSION = 3;
3613
+ var CLAUDE_CODE_THEMES = ["light", "light-ansi", "light-daltonized", "dark", "dark-ansi", "dark-daltonized"];
3613
3614
  var DEFAULT_SETTINGS = {
3614
3615
  _schemaVersion: CURRENT_SCHEMA_VERSION,
3615
3616
  server: {
@@ -3622,9 +3623,6 @@ var DEFAULT_SETTINGS = {
3622
3623
  username: null,
3623
3624
  password: null
3624
3625
  },
3625
- remoteVibora: {
3626
- url: ""
3627
- },
3628
3626
  editor: {
3629
3627
  app: "vscode",
3630
3628
  host: "",
@@ -3635,7 +3633,11 @@ var DEFAULT_SETTINGS = {
3635
3633
  githubPat: null
3636
3634
  },
3637
3635
  appearance: {
3638
- language: null
3636
+ language: null,
3637
+ theme: null,
3638
+ syncClaudeCodeTheme: false,
3639
+ claudeCodeLightTheme: "light-ansi",
3640
+ claudeCodeDarkTheme: "dark-ansi"
3639
3641
  }
3640
3642
  };
3641
3643
  var OLD_DEFAULT_PORT = 3333;
@@ -3647,7 +3649,11 @@ var MIGRATION_MAP = {
3647
3649
  sshPort: "editor.sshPort",
3648
3650
  linearApiKey: "integrations.linearApiKey",
3649
3651
  githubPat: "integrations.githubPat",
3650
- language: "appearance.language"
3652
+ language: "appearance.language",
3653
+ theme: "appearance.theme",
3654
+ syncClaudeCodeTheme: "appearance.syncClaudeCodeTheme",
3655
+ claudeCodeLightTheme: "appearance.claudeCodeLightTheme",
3656
+ claudeCodeDarkTheme: "appearance.claudeCodeDarkTheme"
3651
3657
  };
3652
3658
  function getNestedValue(obj, path2) {
3653
3659
  return path2.split(".").reduce((o, k) => {
@@ -3669,13 +3675,6 @@ function setNestedValue(obj, path2, value) {
3669
3675
  }
3670
3676
  current[lastKey] = value;
3671
3677
  }
3672
- function constructRemoteUrl(host, port) {
3673
- if (!host)
3674
- return "";
3675
- const effectivePort = port || 7777;
3676
- const portSuffix = effectivePort === 80 || effectivePort === 443 ? "" : `:${effectivePort}`;
3677
- return `http://${host}${portSuffix}`;
3678
- }
3679
3678
  function migrateSettings(parsed) {
3680
3679
  const result = { migrated: false, migratedKeys: [], warnings: [] };
3681
3680
  const version = parsed._schemaVersion ?? 1;
@@ -3702,26 +3701,9 @@ function migrateSettings(parsed) {
3702
3701
  result.migrated = true;
3703
3702
  }
3704
3703
  }
3705
- const flatHost = parsed.remoteHost || parsed.hostname || "";
3706
- if (flatHost) {
3707
- const url = constructRemoteUrl(flatHost, 7777);
3708
- setNestedValue(parsed, "remoteVibora.url", url);
3709
- result.migratedKeys.push("remoteHost");
3710
- delete parsed.remoteHost;
3711
- delete parsed.hostname;
3712
- result.migrated = true;
3713
- }
3714
- }
3715
- if (version < 3) {
3716
- const remoteVibora = parsed.remoteVibora;
3717
- if (remoteVibora && "host" in remoteVibora) {
3718
- const host = remoteVibora.host || "";
3719
- const port = remoteVibora.port || 7777;
3720
- const url = constructRemoteUrl(host, port);
3721
- parsed.remoteVibora = { url };
3722
- result.migratedKeys.push("remoteVibora.host");
3723
- result.migrated = true;
3724
- }
3704
+ delete parsed.remoteHost;
3705
+ delete parsed.hostname;
3706
+ delete parsed.remoteVibora;
3725
3707
  }
3726
3708
  parsed._schemaVersion = CURRENT_SCHEMA_VERSION;
3727
3709
  result.migrated = true;
@@ -3814,9 +3796,6 @@ function getSettings() {
3814
3796
  username: parsed.authentication?.username ?? null,
3815
3797
  password: parsed.authentication?.password ?? null
3816
3798
  },
3817
- remoteVibora: {
3818
- url: parsed.remoteVibora?.url ?? DEFAULT_SETTINGS.remoteVibora.url
3819
- },
3820
3799
  editor: {
3821
3800
  app: parsed.editor?.app ?? DEFAULT_SETTINGS.editor.app,
3822
3801
  host: parsed.editor?.host ?? DEFAULT_SETTINGS.editor.host,
@@ -3827,7 +3806,11 @@ function getSettings() {
3827
3806
  githubPat: parsed.integrations?.githubPat ?? null
3828
3807
  },
3829
3808
  appearance: {
3830
- language: parsed.appearance?.language ?? null
3809
+ language: parsed.appearance?.language ?? null,
3810
+ theme: parsed.appearance?.theme ?? null,
3811
+ syncClaudeCodeTheme: parsed.appearance?.syncClaudeCodeTheme ?? false,
3812
+ claudeCodeLightTheme: parsed.appearance?.claudeCodeLightTheme ?? "light-ansi",
3813
+ claudeCodeDarkTheme: parsed.appearance?.claudeCodeDarkTheme ?? "dark-ansi"
3831
3814
  }
3832
3815
  };
3833
3816
  const portEnv = parseInt(process.env.PORT || "", 10);
@@ -3844,9 +3827,6 @@ function getSettings() {
3844
3827
  username: process.env.VIBORA_BASIC_AUTH_USERNAME ?? fileSettings.authentication.username,
3845
3828
  password: process.env.VIBORA_BASIC_AUTH_PASSWORD ?? fileSettings.authentication.password
3846
3829
  },
3847
- remoteVibora: {
3848
- url: process.env.VIBORA_REMOTE_URL ?? fileSettings.remoteVibora.url
3849
- },
3850
3830
  editor: {
3851
3831
  app: fileSettings.editor.app,
3852
3832
  host: process.env.VIBORA_EDITOR_HOST ?? fileSettings.editor.host,
@@ -3872,13 +3852,16 @@ function toLegacySettings(settings) {
3872
3852
  return {
3873
3853
  port: settings.server.port,
3874
3854
  defaultGitReposDir: settings.paths.defaultGitReposDir,
3875
- remoteUrl: settings.remoteVibora.url,
3876
3855
  sshPort: settings.editor.sshPort,
3877
3856
  basicAuthUsername: settings.authentication.username,
3878
3857
  basicAuthPassword: settings.authentication.password,
3879
3858
  linearApiKey: settings.integrations.linearApiKey,
3880
3859
  githubPat: settings.integrations.githubPat,
3881
- language: settings.appearance.language
3860
+ language: settings.appearance.language,
3861
+ theme: settings.appearance.theme,
3862
+ syncClaudeCodeTheme: settings.appearance.syncClaudeCodeTheme,
3863
+ claudeCodeLightTheme: settings.appearance.claudeCodeLightTheme,
3864
+ claudeCodeDarkTheme: settings.appearance.claudeCodeDarkTheme
3882
3865
  };
3883
3866
  }
3884
3867
  function isDeveloperMode() {
@@ -3909,8 +3892,8 @@ function getDefaultValue(settingPath) {
3909
3892
  return getNestedValue(DEFAULT_SETTINGS, settingPath);
3910
3893
  }
3911
3894
  var DEFAULT_NOTIFICATION_SETTINGS = {
3912
- enabled: false,
3913
- sound: { enabled: false },
3895
+ enabled: true,
3896
+ sound: { enabled: true },
3914
3897
  slack: { enabled: false },
3915
3898
  discord: { enabled: false },
3916
3899
  pushover: { enabled: false }
@@ -3982,6 +3965,33 @@ function updateClaudeSettings(updates) {
3982
3965
  const merged = { ...current, ...updates };
3983
3966
  fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2), "utf-8");
3984
3967
  }
3968
+ function getClaudeConfigPath() {
3969
+ return path.join(os.homedir(), ".claude.json");
3970
+ }
3971
+ function getClaudeConfig() {
3972
+ const configPath = getClaudeConfigPath();
3973
+ if (!fs.existsSync(configPath))
3974
+ return {};
3975
+ try {
3976
+ return JSON.parse(fs.readFileSync(configPath, "utf-8"));
3977
+ } catch {
3978
+ return {};
3979
+ }
3980
+ }
3981
+ function updateClaudeConfig(updates) {
3982
+ const configPath = getClaudeConfigPath();
3983
+ const current = getClaudeConfig();
3984
+ const merged = { ...current, ...updates };
3985
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2), "utf-8");
3986
+ }
3987
+ function syncClaudeCodeTheme(resolvedTheme) {
3988
+ const settings = getSettings();
3989
+ if (!settings.appearance.syncClaudeCodeTheme)
3990
+ return;
3991
+ const claudeTheme = resolvedTheme === "light" ? settings.appearance.claudeCodeLightTheme : settings.appearance.claudeCodeDarkTheme;
3992
+ updateClaudeConfig({ theme: claudeTheme });
3993
+ log2.settings.info("Synced Claude Code theme", { claudeTheme, resolvedTheme });
3994
+ }
3985
3995
  var DEFAULT_ZAI_SETTINGS = {
3986
3996
  enabled: false,
3987
3997
  apiKey: null,
@@ -137999,25 +138009,8 @@ async function updateLinearTicketStatus(identifier, viboraStatus) {
137999
138009
  }
138000
138010
 
138001
138011
  // server/services/notification-service.ts
138002
- import { spawn as spawn2 } from "child_process";
138003
- async function sendSoundNotification(config) {
138004
- if (process.platform !== "darwin") {
138005
- return { channel: "sound", success: false, error: "Sound only supported on macOS" };
138006
- }
138007
- const soundFile = config.soundFile || "/System/Library/Sounds/Glass.aiff";
138008
- return new Promise((resolve2) => {
138009
- const proc2 = spawn2("afplay", [soundFile]);
138010
- proc2.on("close", (code) => {
138011
- if (code === 0) {
138012
- resolve2({ channel: "sound", success: true });
138013
- } else {
138014
- resolve2({ channel: "sound", success: false, error: `afplay exited with code ${code}` });
138015
- }
138016
- });
138017
- proc2.on("error", (err) => {
138018
- resolve2({ channel: "sound", success: false, error: err.message });
138019
- });
138020
- });
138012
+ async function sendSoundNotification(_config) {
138013
+ return { channel: "sound", success: true };
138021
138014
  }
138022
138015
  async function sendSlackNotification(config, payload) {
138023
138016
  if (!config.webhookUrl) {
@@ -139028,6 +139021,53 @@ app3.post("/merge-to-main", async (c) => {
139028
139021
  return c.json({ error: err instanceof Error ? err.message : "Failed to merge" }, 500);
139029
139022
  }
139030
139023
  });
139024
+ app3.post("/push", async (c) => {
139025
+ try {
139026
+ const body = await c.req.json();
139027
+ const { worktreePath } = body;
139028
+ if (!worktreePath) {
139029
+ return c.json({ error: "Missing required field: worktreePath" }, 400);
139030
+ }
139031
+ if (!fs4.existsSync(worktreePath)) {
139032
+ return c.json({ error: "Worktree path does not exist" }, 404);
139033
+ }
139034
+ let branch;
139035
+ try {
139036
+ branch = gitExec(worktreePath, "rev-parse --abbrev-ref HEAD");
139037
+ } catch {
139038
+ return c.json({ error: "Failed to determine current branch" }, 500);
139039
+ }
139040
+ try {
139041
+ const status = gitExec(worktreePath, "status --porcelain");
139042
+ if (status.trim()) {
139043
+ return c.json({
139044
+ error: "Worktree has uncommitted changes. Please commit or stash changes before pushing.",
139045
+ hasUncommittedChanges: true
139046
+ }, 409);
139047
+ }
139048
+ } catch {}
139049
+ try {
139050
+ gitExec(worktreePath, `push origin ${branch}`);
139051
+ } catch (pushErr) {
139052
+ const errorMsg = pushErr instanceof Error ? pushErr.message : "Unknown error";
139053
+ if (errorMsg.includes("rejected") || errorMsg.includes("non-fast-forward")) {
139054
+ return c.json({
139055
+ error: "Push rejected. The remote has changes you do not have locally. Pull first.",
139056
+ pushRejected: true
139057
+ }, 409);
139058
+ }
139059
+ return c.json({
139060
+ error: `Failed to push: ${errorMsg}`
139061
+ }, 500);
139062
+ }
139063
+ return c.json({
139064
+ success: true,
139065
+ branch
139066
+ });
139067
+ } catch (err) {
139068
+ return c.json({ error: err instanceof Error ? err.message : "Failed to push" }, 500);
139069
+ }
139070
+ });
139031
139071
  app3.post("/sync-parent", async (c) => {
139032
139072
  try {
139033
139073
  const body = await c.req.json();
@@ -139377,7 +139417,7 @@ app4.get("/is-git-repo", (c) => {
139377
139417
  var filesystem_default = app4;
139378
139418
 
139379
139419
  // server/routes/config.ts
139380
- import { spawn as spawn3 } from "child_process";
139420
+ import { spawn as spawn2 } from "child_process";
139381
139421
  var CONFIG_KEYS = {
139382
139422
  PORT: "server.port",
139383
139423
  DEFAULT_GIT_REPOS_DIR: "paths.defaultGitReposDir",
@@ -139390,7 +139430,11 @@ var CONFIG_KEYS = {
139390
139430
  EDITOR_SSH_PORT: "editor.sshPort",
139391
139431
  LINEAR_API_KEY: "integrations.linearApiKey",
139392
139432
  GITHUB_PAT: "integrations.githubPat",
139393
- LANGUAGE: "appearance.language"
139433
+ LANGUAGE: "appearance.language",
139434
+ THEME: "appearance.theme",
139435
+ SYNC_CLAUDE_CODE_THEME: "appearance.syncClaudeCodeTheme",
139436
+ CLAUDE_CODE_LIGHT_THEME: "appearance.claudeCodeLightTheme",
139437
+ CLAUDE_CODE_DARK_THEME: "appearance.claudeCodeDarkTheme"
139394
139438
  };
139395
139439
  var LEGACY_KEY_MAP = {
139396
139440
  port: "server.port",
@@ -139403,6 +139447,7 @@ var LEGACY_KEY_MAP = {
139403
139447
  linear_api_key: "integrations.linearApiKey",
139404
139448
  github_pat: "integrations.githubPat",
139405
139449
  language: "appearance.language",
139450
+ theme: "appearance.theme",
139406
139451
  defaultGitReposDir: "paths.defaultGitReposDir",
139407
139452
  basicAuthUsername: "authentication.username",
139408
139453
  basicAuthPassword: "authentication.password",
@@ -139523,13 +139568,26 @@ app5.post("/restart", (c) => {
139523
139568
  return c.json({ error: "Restart only available in developer mode" }, 403);
139524
139569
  }
139525
139570
  setTimeout(() => {
139526
- spawn3("bash", ["-c", "cd ~/projects/vibora && mise run build && bun run drizzle-kit push && systemctl --user restart vibora-dev"], {
139571
+ spawn2("bash", ["-c", "cd ~/projects/vibora && mise run build && bun run drizzle-kit push && systemctl --user restart vibora-dev"], {
139527
139572
  detached: true,
139528
139573
  stdio: "ignore"
139529
139574
  }).unref();
139530
139575
  }, 100);
139531
139576
  return c.json({ success: true, message: "Restart initiated (build + migrate + restart)" });
139532
139577
  });
139578
+ app5.post("/sync-claude-theme", async (c) => {
139579
+ try {
139580
+ const body = await c.req.json();
139581
+ const { resolvedTheme } = body;
139582
+ if (resolvedTheme !== "light" && resolvedTheme !== "dark") {
139583
+ return c.json({ error: 'resolvedTheme must be "light" or "dark"' }, 400);
139584
+ }
139585
+ syncClaudeCodeTheme(resolvedTheme);
139586
+ return c.json({ success: true, resolvedTheme });
139587
+ } catch (err) {
139588
+ return c.json({ error: err instanceof Error ? err.message : "Failed to sync theme" }, 400);
139589
+ }
139590
+ });
139533
139591
  app5.get("/:key", (c) => {
139534
139592
  const key = c.req.param("key");
139535
139593
  if (key === "worktree_base_path") {
@@ -139571,6 +139629,19 @@ app5.put("/:key", async (c) => {
139571
139629
  return c.json({ error: 'Language must be "en", "zh", or null' }, 400);
139572
139630
  }
139573
139631
  value = value === "" ? null : value;
139632
+ } else if (path8 === CONFIG_KEYS.THEME) {
139633
+ if (value !== null && value !== "" && value !== "system" && value !== "light" && value !== "dark") {
139634
+ return c.json({ error: 'Theme must be "system", "light", "dark", or null' }, 400);
139635
+ }
139636
+ value = value === "" || value === "system" ? null : value;
139637
+ } else if (path8 === CONFIG_KEYS.SYNC_CLAUDE_CODE_THEME || path8 === CONFIG_KEYS.SYNC_STARSHIP_THEME) {
139638
+ if (typeof value !== "boolean") {
139639
+ return c.json({ error: "Sync setting must be a boolean" }, 400);
139640
+ }
139641
+ } else if (path8 === CONFIG_KEYS.CLAUDE_CODE_LIGHT_THEME || path8 === CONFIG_KEYS.CLAUDE_CODE_DARK_THEME) {
139642
+ if (!CLAUDE_CODE_THEMES.includes(value)) {
139643
+ return c.json({ error: `Claude Code theme must be one of: ${CLAUDE_CODE_THEMES.join(", ")}` }, 400);
139644
+ }
139574
139645
  } else if (path8 === CONFIG_KEYS.EDITOR_APP) {
139575
139646
  const validApps = ["vscode", "cursor", "windsurf", "zed"];
139576
139647
  if (!validApps.includes(value)) {
@@ -139603,7 +139674,7 @@ app5.delete("/:key", (c) => {
139603
139674
  var config_default = app5;
139604
139675
 
139605
139676
  // server/routes/uploads.ts
139606
- import { mkdir, writeFile, readFile } from "fs/promises";
139677
+ import { mkdir, writeFile, readFile, unlink } from "fs/promises";
139607
139678
  import { existsSync as existsSync8 } from "fs";
139608
139679
  import { join as join10 } from "path";
139609
139680
  var mimeTypes = {
@@ -139612,7 +139683,10 @@ var mimeTypes = {
139612
139683
  jpeg: "image/jpeg",
139613
139684
  gif: "image/gif",
139614
139685
  webp: "image/webp",
139615
- svg: "image/svg+xml"
139686
+ svg: "image/svg+xml",
139687
+ mp3: "audio/mpeg",
139688
+ wav: "audio/wav",
139689
+ ogg: "audio/ogg"
139616
139690
  };
139617
139691
  var app6 = new Hono2;
139618
139692
  function generateFilename(extension) {
@@ -139664,6 +139738,75 @@ app6.get("/:filename", async (c) => {
139664
139738
  headers: { "Content-Type": contentType }
139665
139739
  });
139666
139740
  });
139741
+ app6.post("/sound", async (c) => {
139742
+ const body = await c.req.parseBody();
139743
+ const file = body["file"];
139744
+ if (!file || !(file instanceof File)) {
139745
+ return c.json({ error: "No file provided" }, 400);
139746
+ }
139747
+ const audioMimeTypes = {
139748
+ "audio/mpeg": "mp3",
139749
+ "audio/mp3": "mp3",
139750
+ "audio/wav": "wav",
139751
+ "audio/wave": "wav",
139752
+ "audio/ogg": "ogg"
139753
+ };
139754
+ const extension = audioMimeTypes[file.type];
139755
+ if (!extension) {
139756
+ return c.json({ error: "File must be an audio file (mp3, wav, or ogg)" }, 400);
139757
+ }
139758
+ const viboraDir = getViboraDir();
139759
+ const filename = `notification-sound.${extension}`;
139760
+ const filePath = join10(viboraDir, filename);
139761
+ for (const ext2 of ["mp3", "wav", "ogg"]) {
139762
+ const oldPath = join10(viboraDir, `notification-sound.${ext2}`);
139763
+ if (existsSync8(oldPath)) {
139764
+ try {
139765
+ await unlink(oldPath);
139766
+ } catch {}
139767
+ }
139768
+ }
139769
+ const arrayBuffer = await file.arrayBuffer();
139770
+ await writeFile(filePath, Buffer.from(arrayBuffer));
139771
+ updateNotificationSettings({
139772
+ sound: {
139773
+ ...getNotificationSettings().sound,
139774
+ customSoundFile: filePath
139775
+ }
139776
+ });
139777
+ return c.json({ path: filePath, filename });
139778
+ });
139779
+ app6.delete("/sound", async (c) => {
139780
+ const viboraDir = getViboraDir();
139781
+ for (const ext2 of ["mp3", "wav", "ogg"]) {
139782
+ const filePath = join10(viboraDir, `notification-sound.${ext2}`);
139783
+ if (existsSync8(filePath)) {
139784
+ try {
139785
+ await unlink(filePath);
139786
+ } catch {}
139787
+ }
139788
+ }
139789
+ updateNotificationSettings({
139790
+ sound: {
139791
+ ...getNotificationSettings().sound,
139792
+ customSoundFile: undefined
139793
+ }
139794
+ });
139795
+ return c.json({ success: true });
139796
+ });
139797
+ app6.get("/sound", async (c) => {
139798
+ const settings = getNotificationSettings();
139799
+ const customSoundFile = settings.sound?.customSoundFile;
139800
+ if (!customSoundFile || !existsSync8(customSoundFile)) {
139801
+ return c.notFound();
139802
+ }
139803
+ const ext2 = customSoundFile.split(".").pop()?.toLowerCase() || "";
139804
+ const contentType = mimeTypes[ext2] || "audio/mpeg";
139805
+ const content = await readFile(customSoundFile);
139806
+ return new Response(content, {
139807
+ headers: { "Content-Type": contentType }
139808
+ });
139809
+ });
139667
139810
  var uploads_default = app6;
139668
139811
 
139669
139812
  // node_modules/hono/dist/utils/stream.js
@@ -145071,6 +145214,7 @@ function stopPRMonitor() {
145071
145214
 
145072
145215
  // server/index.ts
145073
145216
  var PORT = getSettingByKey("port");
145217
+ var HOST = process.env.HOST || "localhost";
145074
145218
  var ptyManager2 = initPTYManager({
145075
145219
  onData: (terminalId, data) => {
145076
145220
  broadcastToTerminal(terminalId, {
@@ -145098,7 +145242,7 @@ app13.get("/ws/terminal", upgradeWebSocket(() => terminalWebSocketHandlers));
145098
145242
  var server = serve({
145099
145243
  fetch: app13.fetch,
145100
145244
  port: PORT,
145101
- hostname: "0.0.0.0"
145245
+ hostname: HOST
145102
145246
  }, (info) => {
145103
145247
  log2.server.info("Vibora server running", {
145104
145248
  port: info.port,