@polderlabs/bizar 5.5.2 → 5.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/bizar-dash/dist/assets/{EnvVarsSection-DoIzqrlb.js → EnvVarsSection-Bi3Cptpr.js} +3 -3
  2. package/bizar-dash/dist/assets/{EnvVarsSection-DoIzqrlb.js.map → EnvVarsSection-Bi3Cptpr.js.map} +1 -1
  3. package/bizar-dash/dist/assets/{MobileChat-CoUCUsbr.js → MobileChat-H0ASvfzr.js} +1 -1
  4. package/bizar-dash/dist/assets/{MobileChat-CoUCUsbr.js.map → MobileChat-H0ASvfzr.js.map} +1 -1
  5. package/bizar-dash/dist/assets/MobileSettings-ndyqmLdP.js +1 -0
  6. package/bizar-dash/dist/assets/{MobileSettings-BeO04g52.js.map → MobileSettings-ndyqmLdP.js.map} +1 -1
  7. package/bizar-dash/dist/assets/{Toast-D9nv5N6m.js → Toast-BP5x9M-T.js} +1 -1
  8. package/bizar-dash/dist/assets/{Toast-D9nv5N6m.js.map → Toast-BP5x9M-T.js.map} +1 -1
  9. package/bizar-dash/dist/assets/{icons-Btipv2pp.js → icons-Cr5zfF_4.js} +96 -91
  10. package/bizar-dash/dist/assets/icons-Cr5zfF_4.js.map +1 -0
  11. package/bizar-dash/dist/assets/{main-UG4jKK-F.css → main-DAthMQSh.css} +1 -1
  12. package/bizar-dash/dist/assets/{main-DHXPuddY.js → main-dq0UZN8-.js} +10 -10
  13. package/bizar-dash/dist/assets/main-dq0UZN8-.js.map +1 -0
  14. package/bizar-dash/dist/assets/mobile-BIJt6dnI.js +1 -0
  15. package/bizar-dash/dist/assets/{mobile-H5KH9cTL.js.map → mobile-BIJt6dnI.js.map} +1 -1
  16. package/bizar-dash/dist/assets/mobile-layout-DKh7Pn9-.js +2 -0
  17. package/bizar-dash/dist/assets/{mobile-layout-BqHxVb6_.js.map → mobile-layout-DKh7Pn9-.js.map} +1 -1
  18. package/bizar-dash/dist/assets/{useSlashCommands-C24-O1e7.js → useSlashCommands-B8ixT4ub.js} +2 -2
  19. package/bizar-dash/dist/assets/{useSlashCommands-C24-O1e7.js.map → useSlashCommands-B8ixT4ub.js.map} +1 -1
  20. package/bizar-dash/dist/index.html +7 -7
  21. package/bizar-dash/dist/mobile.html +3 -3
  22. package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
  23. package/bizar-dash/src/server/memory-git.mjs +62 -19
  24. package/bizar-dash/src/server/memory-store.mjs +44 -7
  25. package/bizar-dash/src/web/components/SettingsNav.tsx +29 -9
  26. package/bizar-dash/src/web/styles/settings.css +0 -74
  27. package/bizar-dash/src/web/views/Settings.tsx +1 -1
  28. package/bizar-dash/tests/memory-git.test.mjs +18 -0
  29. package/bizar-dash/tests/settings-layout.test.tsx +1 -1
  30. package/bizar-dash/tests/settings-nav.test.tsx +3 -2
  31. package/cli/doctor.mjs +4 -1
  32. package/cli/doctor.test.mjs +5 -3
  33. package/cli/post-install-smoke.mjs +93 -67
  34. package/cli/post-install-smoke.test.mjs +72 -0
  35. package/cli/provision.mjs +33 -3
  36. package/package.json +1 -1
  37. package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js +0 -1
  38. package/bizar-dash/dist/assets/icons-Btipv2pp.js.map +0 -1
  39. package/bizar-dash/dist/assets/main-DHXPuddY.js.map +0 -1
  40. package/bizar-dash/dist/assets/mobile-H5KH9cTL.js +0 -1
  41. package/bizar-dash/dist/assets/mobile-layout-BqHxVb6_.js +0 -2
@@ -9,16 +9,60 @@
9
9
  * addFile, addAll.
10
10
  */
11
11
 
12
- import { execFileSync, execSync } from 'node:child_process';
12
+ import { execFileSync as _execFileSync, execSync } from 'node:child_process';
13
13
  import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'node:fs';
14
14
  import { join } from 'node:path';
15
15
 
16
+ /**
17
+ * Resolve the `git` binary path once at module load time.
18
+ *
19
+ * When the dashboard runs under systemd, PATH typically does not include
20
+ * /usr/bin — resulting in `spawnSync git ENOENT` even though git IS
21
+ * installed. This helper locates git using `which`, then falls back to
22
+ * common installation paths before returning an absolute path (or 'git'
23
+ * as a last-resort).
24
+ *
25
+ * @returns {string} absolute path to git, or 'git' as a best-effort fallback
26
+ */
27
+ export function resolveGitBinary() {
28
+ // Try `which git` first
29
+ try {
30
+ const out = _execFileSync('which', ['git'], {
31
+ encoding: 'utf8',
32
+ stdio: ['pipe', 'pipe', 'pipe'],
33
+ }).trim();
34
+ if (out) return out;
35
+ } catch { /* which not available or failed */ }
36
+
37
+ // Fallback to common absolute paths
38
+ const fallbacks = ['/usr/bin/git', '/usr/local/bin/git', '/opt/homebrew/bin/git', '/opt/git/bin/git'];
39
+ for (const f of fallbacks) {
40
+ try {
41
+ _execFileSync(f, ['--version'], { stdio: ['pipe', 'pipe', 'pipe'] });
42
+ return f;
43
+ } catch { /* keep looking */ }
44
+ }
45
+
46
+ return 'git'; // best-effort fallback — will only work if PATH is correct
47
+ }
48
+
49
+ /** Cached absolute path to the git binary. */
50
+ const GIT_BIN = resolveGitBinary();
51
+
52
+ /**
53
+ * Wrapper around execFileSync that always uses the resolved GIT_BIN path.
54
+ * This ensures git is found even when PATH is stripped (e.g. systemd units).
55
+ */
56
+ function execFileSync(args, opts) {
57
+ return _execFileSync(GIT_BIN, args, opts);
58
+ }
59
+
16
60
  /**
17
61
  * @returns {boolean} true if `git --version` exits 0.
18
62
  */
19
63
  export function isGitInstalled() {
20
64
  try {
21
- execFileSync('git', ['--version'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
65
+ execFileSync(GIT_BIN, ['--version'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
22
66
  return true;
23
67
  } catch {
24
68
  return false;
@@ -36,7 +80,7 @@ export function isGitInstalled() {
36
80
  export function clone(remote, targetDir, { branch = 'main', depth = 1 } = {}) {
37
81
  try {
38
82
  const args = ['clone', '--branch', branch, '--depth', String(depth), remote, targetDir];
39
- const output = execFileSync('git', args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
83
+ const output = execFileSync(args, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
40
84
  return { ok: true, output };
41
85
  } catch (err) {
42
86
  return { ok: false, output: '', error: err.message };
@@ -53,7 +97,7 @@ export function clone(remote, targetDir, { branch = 'main', depth = 1 } = {}) {
53
97
  export function pull(dir, { rebase = true } = {}) {
54
98
  try {
55
99
  const args = rebase ? ['pull', '--rebase'] : ['pull', '--ff-only'];
56
- const output = execFileSync('git', args, { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
100
+ const output = execFileSync(args, { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
57
101
  return { ok: true, output };
58
102
  } catch (err) {
59
103
  return { ok: false, output: '', error: err.message };
@@ -74,7 +118,7 @@ export function commit(dir, message, { author } = {}) {
74
118
  if (author) {
75
119
  args.push('--author', author);
76
120
  }
77
- const output = execFileSync('git', args, { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
121
+ const output = execFileSync(args, { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
78
122
  return { ok: true, output };
79
123
  } catch (err) {
80
124
  return { ok: false, output: '', error: err.message };
@@ -91,7 +135,6 @@ export function commit(dir, message, { author } = {}) {
91
135
  export function push(dir, { remote = 'origin', branch = 'main' } = {}) {
92
136
  try {
93
137
  const output = execFileSync(
94
- 'git',
95
138
  ['push', remote, branch],
96
139
  { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
97
140
  );
@@ -112,7 +155,7 @@ export function status(dir) {
112
155
  // Get branch name
113
156
  let branch = 'main';
114
157
  try {
115
- branch = execFileSync('git', ['branch', '--show-current'], {
158
+ branch = execFileSync(['branch', '--show-current'], {
116
159
  cwd: dir,
117
160
  encoding: 'utf8',
118
161
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -120,7 +163,7 @@ export function status(dir) {
120
163
  } catch { /* ignore */ }
121
164
 
122
165
  // Get status --porcelain
123
- const raw = execFileSync('git', ['status', '--porcelain'], {
166
+ const raw = execFileSync(['status', '--porcelain'], {
124
167
  cwd: dir,
125
168
  encoding: 'utf8',
126
169
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -143,13 +186,13 @@ export function status(dir) {
143
186
  let ahead = 0;
144
187
  let behind = 0;
145
188
  try {
146
- const revparse = execFileSync('git', ['revparse', '--abbrev-ref', '@{upstream}'], {
189
+ const revparse = execFileSync(['rev-parse', '--abbrev-ref', '@{upstream}'], {
147
190
  cwd: dir,
148
191
  encoding: 'utf8',
149
192
  stdio: ['pipe', 'pipe', 'pipe'],
150
193
  }).trim();
151
194
  if (revparse) {
152
- const aheadBehind = execFileSync('git', ['rev-list', '--left-right', '--count', `${branch}...${revparse}`], {
195
+ const aheadBehind = execFileSync(['rev-list', '--left-right', '--count', `${branch}...${revparse}`], {
153
196
  cwd: dir,
154
197
  encoding: 'utf8',
155
198
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -226,7 +269,7 @@ export function acquireLock(repoDir) {
226
269
  */
227
270
  export function addFile(dir, filePath) {
228
271
  try {
229
- execFileSync('git', ['add', filePath], { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
272
+ execFileSync(['add', filePath], { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
230
273
  return { ok: true };
231
274
  } catch (err) {
232
275
  return { ok: false, error: err.message };
@@ -241,7 +284,7 @@ export function addFile(dir, filePath) {
241
284
  */
242
285
  export function addAll(dir) {
243
286
  try {
244
- execFileSync('git', ['add', '-A'], { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
287
+ execFileSync(['add', '-A'], { cwd: dir, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
245
288
  return { ok: true };
246
289
  } catch (err) {
247
290
  return { ok: false, error: err.message };
@@ -262,7 +305,7 @@ export function addAll(dir) {
262
305
  export function addRemote(repoDir, remoteName, url, { overwrite = false } = {}) {
263
306
  let existing = null;
264
307
  try {
265
- existing = execFileSync('git', ['remote', 'get-url', remoteName], {
308
+ existing = execFileSync(['remote', 'get-url', remoteName], {
266
309
  cwd: repoDir,
267
310
  encoding: 'utf8',
268
311
  timeout: 5000,
@@ -287,7 +330,7 @@ export function addRemote(repoDir, remoteName, url, { overwrite = false } = {})
287
330
  }
288
331
 
289
332
  if (existing) {
290
- execFileSync('git', ['remote', 'set-url', remoteName, url], {
333
+ execFileSync(['remote', 'set-url', remoteName, url], {
291
334
  cwd: repoDir,
292
335
  timeout: 5000,
293
336
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -295,7 +338,7 @@ export function addRemote(repoDir, remoteName, url, { overwrite = false } = {})
295
338
  return { ok: true, action: 'updated' };
296
339
  }
297
340
 
298
- execFileSync('git', ['remote', 'add', remoteName, url], {
341
+ execFileSync(['remote', 'add', remoteName, url], {
299
342
  cwd: repoDir,
300
343
  timeout: 5000,
301
344
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -315,7 +358,7 @@ export function addRemote(repoDir, remoteName, url, { overwrite = false } = {})
315
358
  */
316
359
  export function lsRemote(repoDir, remoteName, { timeoutMs = 5000 } = {}) {
317
360
  try {
318
- const stdout = execFileSync('git', ['ls-remote', remoteName], {
361
+ const stdout = execFileSync(['ls-remote', remoteName], {
319
362
  cwd: repoDir,
320
363
  timeout: timeoutMs,
321
364
  env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
@@ -356,7 +399,7 @@ export function ensureUpstream(cwd, branch, remote = 'origin') {
356
399
  // Current upstream — empty string means none configured.
357
400
  let current = '';
358
401
  try {
359
- current = execFileSync('git', ['rev-parse', '--abbrev-ref', '@{upstream}'], {
402
+ current = execFileSync(['rev-parse', '--abbrev-ref', '@{upstream}'], {
360
403
  cwd,
361
404
  encoding: 'utf8',
362
405
  stdio: ['pipe', 'pipe', 'pipe'],
@@ -371,7 +414,7 @@ export function ensureUpstream(cwd, branch, remote = 'origin') {
371
414
 
372
415
  // Verify the remote exists before trying to set upstream to it.
373
416
  try {
374
- execFileSync('git', ['rev-parse', '--verify', remote], {
417
+ execFileSync(['rev-parse', '--verify', remote], {
375
418
  cwd,
376
419
  stdio: ['pipe', 'pipe', 'pipe'],
377
420
  });
@@ -380,7 +423,7 @@ export function ensureUpstream(cwd, branch, remote = 'origin') {
380
423
  }
381
424
 
382
425
  try {
383
- execFileSync('git', ['branch', '--set-upstream-to', `${remote}/${branch}`], {
426
+ execFileSync(['branch', '--set-upstream-to', `${remote}/${branch}`], {
384
427
  cwd,
385
428
  stdio: ['pipe', 'pipe', 'pipe'],
386
429
  });
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, lstatSync, unlinkSync } from 'node:fs';
21
- import { execFileSync } from 'node:child_process';
21
+ import { execFileSync as _execFileSync } from 'node:child_process';
22
22
  import { join, dirname, relative, resolve as pathResolve, sep } from 'node:path';
23
23
  import { homedir } from 'node:os';
24
24
  import { parseFrontmatter, serializeFrontmatter } from './yaml.mjs';
@@ -28,6 +28,43 @@ import { atomicWriteJson, safeReadJSON, safeReadText } from '../../../cli/atomic
28
28
  import * as memoryGit from './memory-git.mjs';
29
29
  import { warn as loggerWarn } from './logger.mjs';
30
30
 
31
+ /**
32
+ * Resolve the `git` binary path once at module load time.
33
+ *
34
+ * When the dashboard runs under systemd, PATH typically does not include
35
+ * /usr/bin — this helper finds git's absolute path so that all
36
+ * execFileSync calls work regardless of the process's environment.
37
+ *
38
+ * @returns {string} absolute path to git, or 'git' as a best-effort fallback
39
+ */
40
+ function resolveGitBinary() {
41
+ try {
42
+ const out = _execFileSync('which', ['git'], {
43
+ encoding: 'utf8',
44
+ stdio: ['pipe', 'pipe', 'pipe'],
45
+ }).trim();
46
+ if (out) return out;
47
+ } catch { /* which not available or failed */ }
48
+
49
+ const fallbacks = ['/usr/bin/git', '/usr/local/bin/git', '/opt/homebrew/bin/git', '/opt/git/bin/git'];
50
+ for (const f of fallbacks) {
51
+ try {
52
+ _execFileSync(f, ['--version'], { stdio: ['pipe', 'pipe', 'pipe'] });
53
+ return f;
54
+ } catch { /* keep looking */ }
55
+ }
56
+
57
+ return 'git';
58
+ }
59
+
60
+ /** Cached absolute path to the git binary. */
61
+ const GIT_BIN = resolveGitBinary();
62
+
63
+ /** Wrapper that always uses the resolved GIT_BIN path. */
64
+ function execFileSync(args, opts) {
65
+ return _execFileSync(GIT_BIN, args, opts);
66
+ }
67
+
31
68
  const HOME = homedir();
32
69
 
33
70
  /**
@@ -99,9 +136,9 @@ export function ensureVaultExists() {
99
136
  mkdirSync(vault, { recursive: true, mode: 0o700 });
100
137
  if (!existsSync(join(vault, '.git'))) {
101
138
  try {
102
- execFileSync('git', ['init'], { cwd: vault, stdio: 'pipe' });
103
- execFileSync('git', ['config', 'user.email', 'bizar@localhost'], { cwd: vault, stdio: 'pipe' });
104
- execFileSync('git', ['config', 'user.name', 'BizarHarness'], { cwd: vault, stdio: 'pipe' });
139
+ execFileSync(['init'], { cwd: vault, stdio: 'pipe' });
140
+ execFileSync(['config', 'user.email', 'bizar@localhost'], { cwd: vault, stdio: 'pipe' });
141
+ execFileSync(['config', 'user.name', 'BizarHarness'], { cwd: vault, stdio: 'pipe' });
105
142
  } catch (err) {
106
143
  loggerWarn('failed to init git in vault', { vault, err: err.message });
107
144
  }
@@ -318,7 +355,7 @@ export function initVault(projectRoot) {
318
355
  const isGit = existsSync(join(repoPath, '.git'));
319
356
  if (!isGit) {
320
357
  try {
321
- execFileSync('git', ['init', '-b', 'main'], { cwd: repoPath, stdio: 'pipe' });
358
+ execFileSync(['init', '-b', 'main'], { cwd: repoPath, stdio: 'pipe' });
322
359
  created.push(`${repoPath}/.git/`);
323
360
  } catch (err) {
324
361
  return { ok: false, vaultRoot, created, error: `git init failed: ${err.message}` };
@@ -339,8 +376,8 @@ export function initVault(projectRoot) {
339
376
  'utf8');
340
377
  created.push(`${repoPath}/.gitignore`);
341
378
  try {
342
- execFileSync('git', ['add', '.gitignore'], { cwd: repoPath });
343
- execFileSync('git', ['commit', '-m', 'chore: initial .gitignore for memory repo'], {
379
+ execFileSync(['add', '.gitignore'], { cwd: repoPath });
380
+ execFileSync(['commit', '-m', 'chore: initial .gitignore for memory repo'], {
344
381
  cwd: repoPath,
345
382
  env: {
346
383
  ...process.env,
@@ -1,7 +1,11 @@
1
1
  // src/components/SettingsNav.tsx — v4.9.0 full-sidebar settings navigation.
2
2
  // Shown in the sidebar rail when settingsMode is active. All sections are
3
3
  // visible at once; clicking one scrolls the Settings view to that section.
4
- import { ChevronRight, Palette, Terminal, Cpu, RefreshCw, Gauge, ArrowLeft, LayoutGrid, type LucideIcon } from 'lucide-react';
4
+ import {
5
+ ChevronRight, Palette, Terminal, Cpu, RefreshCw, Gauge, ArrowLeft,
6
+ LayoutGrid, Wifi, Bell, Shield, Users, Activity, FolderGit2,
7
+ type LucideIcon,
8
+ } from 'lucide-react';
5
9
  import { cn } from '../lib/utils';
6
10
 
7
11
  export type SettingsSection = {
@@ -20,17 +24,26 @@ const SECTION_GROUPS: SettingsSectionGroup[] = [
20
24
  label: 'General',
21
25
  sections: [
22
26
  { id: 'theme', label: 'Theme', icon: Palette },
27
+ // Layout & General are both rendered by GeneralSection; one nav item covers both
23
28
  { id: 'layout', label: 'Layout', icon: LayoutGrid },
24
- { id: 'general', label: 'General', icon: LayoutGrid },
25
29
  ],
26
30
  },
27
31
  {
28
32
  label: 'Core',
29
33
  sections: [
30
34
  { id: 'env-vars', label: 'Env Vars', icon: Terminal },
35
+ { id: 'network', label: 'Network', icon: Wifi },
36
+ { id: 'notifications', label: 'Notifications', icon: Bell },
37
+ { id: 'auth', label: 'Auth', icon: Shield },
31
38
  { id: 'system-llm', label: 'System LLM', icon: Cpu },
32
39
  ],
33
40
  },
41
+ {
42
+ label: 'Agents',
43
+ sections: [
44
+ { id: 'agents', label: 'Agents', icon: Users },
45
+ ],
46
+ },
34
47
  {
35
48
  label: 'Experience',
36
49
  sections: [
@@ -38,6 +51,13 @@ const SECTION_GROUPS: SettingsSectionGroup[] = [
38
51
  { id: 'headroom', label: 'Headroom', icon: Gauge },
39
52
  ],
40
53
  },
54
+ {
55
+ label: 'Data',
56
+ sections: [
57
+ { id: 'activity-log', label: 'Activity', icon: Activity },
58
+ { id: 'workspaces', label: 'Workspaces', icon: FolderGit2 },
59
+ ],
60
+ },
41
61
  ];
42
62
 
43
63
  export type SettingsNavProps = {
@@ -52,12 +72,12 @@ export function SettingsNav({ activeSection, onSectionChange, onExitSettings }:
52
72
  {/* Back button */}
53
73
  <button
54
74
  type="button"
55
- className="settings-nav-back"
75
+ className="sidebar-tab"
56
76
  onClick={onExitSettings}
57
77
  aria-label="Exit settings"
58
78
  >
59
- <ArrowLeft size={14} />
60
- <span>Back</span>
79
+ <ArrowLeft size={14} aria-hidden />
80
+ <span className="sidebar-tab-label">Back</span>
61
81
  </button>
62
82
 
63
83
  <div className="settings-nav-divider" aria-hidden="true" />
@@ -73,14 +93,14 @@ export function SettingsNav({ activeSection, onSectionChange, onExitSettings }:
73
93
  <button
74
94
  key={s.id}
75
95
  type="button"
76
- className={cn('settings-nav-item', active && 'settings-nav-item-active')}
96
+ className={cn('sidebar-tab', active && 'sidebar-tab-active')}
77
97
  onClick={() => onSectionChange(active ? null : s.id)}
78
98
  aria-current={active ? 'page' : undefined}
79
99
  >
80
- {Icon && <Icon size={14} className="settings-nav-item-icon" aria-hidden />}
81
- <span className="settings-nav-item-label">{s.label}</span>
100
+ {Icon && <Icon size={14} aria-hidden />}
101
+ <span className="sidebar-tab-label">{s.label}</span>
82
102
  {active && (
83
- <ChevronRight size={12} className="settings-nav-item-chevron" aria-hidden />
103
+ <ChevronRight size={12} aria-hidden />
84
104
  )}
85
105
  </button>
86
106
  );
@@ -617,28 +617,6 @@
617
617
  gap: 2px;
618
618
  }
619
619
 
620
- /* Back button */
621
- .settings-nav-back {
622
- display: flex;
623
- align-items: center;
624
- gap: 6px;
625
- padding: 6px 8px;
626
- border-radius: var(--radius);
627
- border: none;
628
- background: transparent;
629
- color: var(--text-dim);
630
- font-size: 13px;
631
- font-weight: 500;
632
- cursor: pointer;
633
- transition: background var(--motion-fast), color var(--motion-fast);
634
- margin-bottom: var(--space-2);
635
- }
636
-
637
- .settings-nav-back:hover {
638
- background: var(--bg-elev-2);
639
- color: var(--text);
640
- }
641
-
642
620
  .settings-nav-divider {
643
621
  height: 1px;
644
622
  background: var(--border);
@@ -659,58 +637,6 @@
659
637
  padding: 4px 8px 6px;
660
638
  }
661
639
 
662
- /* Individual nav items */
663
- .settings-nav-item {
664
- display: flex;
665
- align-items: center;
666
- gap: 8px;
667
- padding: 6px 8px;
668
- border-radius: var(--radius);
669
- border: 1px solid transparent;
670
- background: transparent;
671
- color: var(--text-dim);
672
- font-size: 13px;
673
- font-weight: 500;
674
- cursor: pointer;
675
- text-align: left;
676
- width: 100%;
677
- transition: background var(--motion-fast), color var(--motion-fast), border-color var(--motion-fast);
678
- }
679
-
680
- .settings-nav-item:hover {
681
- background: var(--bg-elev-2);
682
- color: var(--text);
683
- }
684
-
685
- .settings-nav-item-active {
686
- background: var(--accent-bg);
687
- color: var(--accent-2);
688
- border-color: var(--accent-border);
689
- }
690
-
691
- .settings-nav-item-active:hover {
692
- background: var(--accent-bg);
693
- color: var(--accent-2);
694
- }
695
-
696
- .settings-nav-item-icon {
697
- flex-shrink: 0;
698
- opacity: 0.7;
699
- }
700
-
701
- .settings-nav-item-active .settings-nav-item-icon {
702
- opacity: 1;
703
- }
704
-
705
- .settings-nav-item-label {
706
- flex: 1;
707
- }
708
-
709
- .settings-nav-item-chevron {
710
- flex-shrink: 0;
711
- opacity: 0.5;
712
- }
713
-
714
640
  /* ─────────────────────────────────────────────────────────────────────
715
641
  v5.4 — Mobile settings accordion (mobile-settings)
716
642
  ───────────────────────────────────────────────────────────────────── */
@@ -288,7 +288,7 @@ function SettingsViewInner({ settings: initial, refreshSnapshot, settingsMode, s
288
288
  <div className={cn('settings-grid', activeSection && 'settings-grid-filtered')} data-active-section={activeSection || undefined}>
289
289
  {(showAll || sectionOf('theme')) && <div id="settings-theme"><ThemeSection {...sp} /></div>}
290
290
  {(showAll || sectionOf('updates')) && <div id="settings-updates"><UpdatesSection /></div>}
291
- {(showAll || sectionOf('layout') || sectionOf('general')) && <div id="settings-general"><GeneralSection {...sp} autoSave={autoSave} /></div>}
291
+ {(showAll || sectionOf('layout')) && <div id="settings-general"><GeneralSection {...sp} autoSave={autoSave} /></div>}
292
292
  {(showAll || sectionOf('env-vars')) && <div id="settings-env-vars"><EnvVarsSection /></div>}
293
293
  {(showAll || sectionOf('network') || sectionOf('service') || sectionOf('tailscale')) && <div id="settings-network"><NetworkSection tailscale={tailscale} tailscaleDraft={tailscaleDraft} setTailscaleDraft={setTailscaleDraft} onTailscaleToggle={onTailscaleToggle} /></div>}
294
294
  {(showAll || sectionOf('network') || sectionOf('tailscale')) && <TailscaleSettings initialStatus={tailscale} />}
@@ -15,6 +15,24 @@ const TEST_GIT = await import('../src/server/memory-git.mjs').then((m) => m);
15
15
  const GIT_INSTALLED = TEST_GIT.isGitInstalled();
16
16
 
17
17
  describe('memory-git', () => {
18
+ describe('resolveGitBinary', () => {
19
+ it('returns a non-empty string', () => {
20
+ const { resolveGitBinary } = TEST_GIT;
21
+ const bin = resolveGitBinary();
22
+ assert.ok(typeof bin === 'string' && bin.length > 0, `expected non-empty string, got: ${bin}`);
23
+ });
24
+
25
+ it('is a valid git command (absolute path or "git")', () => {
26
+ const { resolveGitBinary } = TEST_GIT;
27
+ const bin = resolveGitBinary();
28
+ // Must be either 'git' or an absolute path (starts with /)
29
+ assert.ok(
30
+ bin === 'git' || bin.startsWith('/'),
31
+ `expected 'git' or absolute path, got: ${bin}`,
32
+ );
33
+ });
34
+ });
35
+
18
36
  let bareRemote;
19
37
  let workingDir;
20
38
 
@@ -93,7 +93,7 @@ describe('Sidebar — settingsMode', () => {
93
93
  />,
94
94
  );
95
95
  const themeBtn = screen.getByRole('button', { name: /theme/i });
96
- expect(themeBtn).toHaveClass('settings-nav-item-active');
96
+ expect(themeBtn).toHaveClass('sidebar-tab-active');
97
97
  });
98
98
 
99
99
  it('calls onSettingsSectionChange when a section is clicked', async () => {
@@ -51,7 +51,9 @@ describe('SettingsNav', () => {
51
51
  );
52
52
  expect(screen.getByText('General', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
53
53
  expect(screen.getByText('Core', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
54
+ expect(screen.getByText('Agents', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
54
55
  expect(screen.getByText('Experience', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
56
+ expect(screen.getByText('Data', { selector: '.settings-nav-group-label' })).toBeInTheDocument();
55
57
  });
56
58
 
57
59
  it('renders section items within each group', () => {
@@ -65,7 +67,6 @@ describe('SettingsNav', () => {
65
67
  // General group
66
68
  expect(screen.getByRole('button', { name: /theme/i })).toBeInTheDocument();
67
69
  expect(screen.getByRole('button', { name: /layout/i })).toBeInTheDocument();
68
- expect(screen.getByRole('button', { name: /general/i })).toBeInTheDocument();
69
70
  // Core group
70
71
  expect(screen.getByRole('button', { name: /env vars/i })).toBeInTheDocument();
71
72
  expect(screen.getByRole('button', { name: /system llm/i })).toBeInTheDocument();
@@ -96,7 +97,7 @@ describe('SettingsNav', () => {
96
97
  />,
97
98
  );
98
99
  const themeBtn = screen.getByRole('button', { name: /theme/i });
99
- expect(themeBtn).toHaveClass('settings-nav-item-active');
100
+ expect(themeBtn).toHaveClass('sidebar-tab-active');
100
101
  });
101
102
 
102
103
  it('toggles off the active section when clicking it again', async () => {
package/cli/doctor.mjs CHANGED
@@ -222,7 +222,10 @@ async function checkProviderConfigSanity() {
222
222
  const cfg = JSON.parse(readFileSync(cfgPath, 'utf8'));
223
223
  const minimax = cfg.provider && cfg.provider.minimax;
224
224
  if (!minimax) {
225
- throw new Error('provider.minimax block missing');
225
+ // Warn instead of throw provision.mjs auto-adds this block on
226
+ // install/update, but users with an older pre-v5 opencode.json
227
+ // may not have it yet.
228
+ return 'warn: provider.minimax block missing (run `bizar update` to patch)';
226
229
  }
227
230
  const models = minimax.models || {};
228
231
  const saneNames = Object.entries(models).filter(([, m]) => {
@@ -302,12 +302,14 @@ describe('runDoctor() with fixture HOME', () => {
302
302
  assert.equal(r.ok, true, r.message);
303
303
  });
304
304
 
305
- test('provider-config-sanity fails without minimax block', async () => {
305
+ test('provider-config-sanity warns (not fails) without minimax block', async () => {
306
+ // v5.x: checkProviderConfigSanity warns instead of throwing when the
307
+ // provider.minimax block is missing, since provision.mjs auto-adds it.
306
308
  writeOpencodeConfig({ provider: {} });
307
309
  const result = await runDoctor({ silent: true });
308
310
  const r = findCheck(result, 'provider-config-sanity');
309
- assert.equal(r.ok, false);
310
- assert.match(r.message, /minimax/);
311
+ assert.equal(r.ok, true, 'should pass with warning, not throw');
312
+ assert.match(r.message, /warn.*minimax|minimax.*missing/i);
311
313
  });
312
314
 
313
315
  test('provider-config-sanity fails when models lack interleaved+reasoning', async () => {