@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.
- package/bizar-dash/dist/assets/{EnvVarsSection-DoIzqrlb.js → EnvVarsSection-Bi3Cptpr.js} +3 -3
- package/bizar-dash/dist/assets/{EnvVarsSection-DoIzqrlb.js.map → EnvVarsSection-Bi3Cptpr.js.map} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-CoUCUsbr.js → MobileChat-H0ASvfzr.js} +1 -1
- package/bizar-dash/dist/assets/{MobileChat-CoUCUsbr.js.map → MobileChat-H0ASvfzr.js.map} +1 -1
- package/bizar-dash/dist/assets/MobileSettings-ndyqmLdP.js +1 -0
- package/bizar-dash/dist/assets/{MobileSettings-BeO04g52.js.map → MobileSettings-ndyqmLdP.js.map} +1 -1
- package/bizar-dash/dist/assets/{Toast-D9nv5N6m.js → Toast-BP5x9M-T.js} +1 -1
- package/bizar-dash/dist/assets/{Toast-D9nv5N6m.js.map → Toast-BP5x9M-T.js.map} +1 -1
- package/bizar-dash/dist/assets/{icons-Btipv2pp.js → icons-Cr5zfF_4.js} +96 -91
- package/bizar-dash/dist/assets/icons-Cr5zfF_4.js.map +1 -0
- package/bizar-dash/dist/assets/{main-UG4jKK-F.css → main-DAthMQSh.css} +1 -1
- package/bizar-dash/dist/assets/{main-DHXPuddY.js → main-dq0UZN8-.js} +10 -10
- package/bizar-dash/dist/assets/main-dq0UZN8-.js.map +1 -0
- package/bizar-dash/dist/assets/mobile-BIJt6dnI.js +1 -0
- package/bizar-dash/dist/assets/{mobile-H5KH9cTL.js.map → mobile-BIJt6dnI.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-layout-DKh7Pn9-.js +2 -0
- package/bizar-dash/dist/assets/{mobile-layout-BqHxVb6_.js.map → mobile-layout-DKh7Pn9-.js.map} +1 -1
- package/bizar-dash/dist/assets/{useSlashCommands-C24-O1e7.js → useSlashCommands-B8ixT4ub.js} +2 -2
- package/bizar-dash/dist/assets/{useSlashCommands-C24-O1e7.js.map → useSlashCommands-B8ixT4ub.js.map} +1 -1
- package/bizar-dash/dist/index.html +7 -7
- package/bizar-dash/dist/mobile.html +3 -3
- package/bizar-dash/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -1
- package/bizar-dash/src/server/memory-git.mjs +62 -19
- package/bizar-dash/src/server/memory-store.mjs +44 -7
- package/bizar-dash/src/web/components/SettingsNav.tsx +29 -9
- package/bizar-dash/src/web/styles/settings.css +0 -74
- package/bizar-dash/src/web/views/Settings.tsx +1 -1
- package/bizar-dash/tests/memory-git.test.mjs +18 -0
- package/bizar-dash/tests/settings-layout.test.tsx +1 -1
- package/bizar-dash/tests/settings-nav.test.tsx +3 -2
- package/cli/doctor.mjs +4 -1
- package/cli/doctor.test.mjs +5 -3
- package/cli/post-install-smoke.mjs +93 -67
- package/cli/post-install-smoke.test.mjs +72 -0
- package/cli/provision.mjs +33 -3
- package/package.json +1 -1
- package/bizar-dash/dist/assets/MobileSettings-BeO04g52.js +0 -1
- package/bizar-dash/dist/assets/icons-Btipv2pp.js.map +0 -1
- package/bizar-dash/dist/assets/main-DHXPuddY.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-H5KH9cTL.js +0 -1
- 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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
103
|
-
execFileSync(
|
|
104
|
-
execFileSync(
|
|
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(
|
|
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(
|
|
343
|
-
execFileSync(
|
|
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 {
|
|
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="
|
|
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('
|
|
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}
|
|
81
|
-
<span className="
|
|
100
|
+
{Icon && <Icon size={14} aria-hidden />}
|
|
101
|
+
<span className="sidebar-tab-label">{s.label}</span>
|
|
82
102
|
{active && (
|
|
83
|
-
<ChevronRight size={12}
|
|
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')
|
|
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('
|
|
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('
|
|
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
|
|
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]) => {
|
package/cli/doctor.test.mjs
CHANGED
|
@@ -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,
|
|
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 () => {
|