sideby 0.11.0 → 0.12.1
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/README.md +2 -2
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +7 -0
- package/dist/src/cli/commands.js +94 -2
- package/dist/src/cli/json-schemas.js +25 -0
- package/dist/src/cli/main.js +104 -43
- package/dist/src/core/brand-mark.d.ts +75 -21
- package/dist/src/core/brand-mark.js +80 -34
- package/dist/src/core/config.d.ts +16 -0
- package/dist/src/core/config.js +17 -2
- package/dist/src/core/update.d.ts +209 -0
- package/dist/src/core/update.js +631 -0
- package/dist/src/desktop/icon.js +292 -61
- package/dist/src/panel/background.d.ts +2 -0
- package/dist/src/panel/background.js +7 -0
- package/dist/src/panel/handler.d.ts +32 -0
- package/dist/src/panel/handler.js +95 -5
- package/dist/src/panel/i18n.d.ts +33 -0
- package/dist/src/panel/i18n.js +66 -0
- package/dist/src/panel/page-script.js +167 -1
- package/dist/src/panel/page-styles.js +18 -2
- package/dist/src/panel/page.d.ts +2 -0
- package/dist/src/panel/page.js +5 -3
- package/dist/testing/index.js +2 -0
- package/package.json +2 -2
- package/schemas/config.json +15 -0
- package/schemas/update.json +115 -0
- package/skills/sideby/SKILL.md +5 -1
package/dist/src/panel/i18n.d.ts
CHANGED
|
@@ -8,6 +8,39 @@ declare const en: {
|
|
|
8
8
|
'top.updatedTitle': string;
|
|
9
9
|
'top.language': string;
|
|
10
10
|
'foot.text': string;
|
|
11
|
+
'foot.textChecks': string;
|
|
12
|
+
'upd.label': string;
|
|
13
|
+
'upd.labelLong': string;
|
|
14
|
+
'upd.title': string;
|
|
15
|
+
'upd.loading': string;
|
|
16
|
+
'upd.current': string;
|
|
17
|
+
'upd.available': string;
|
|
18
|
+
'upd.notes': string;
|
|
19
|
+
'upd.runs': string;
|
|
20
|
+
'upd.after': string;
|
|
21
|
+
'upd.cannot': string;
|
|
22
|
+
'upd.skill': string;
|
|
23
|
+
'upd.checksOff': string;
|
|
24
|
+
'upd.latest': string;
|
|
25
|
+
'upd.latestNow': string;
|
|
26
|
+
'upd.check': string;
|
|
27
|
+
'upd.checking': string;
|
|
28
|
+
'upd.verTitle': string;
|
|
29
|
+
'upd.checked': string;
|
|
30
|
+
'upd.notYet': string;
|
|
31
|
+
'upd.auto': string;
|
|
32
|
+
'upd.autoHint': string;
|
|
33
|
+
'upd.autoNo': string;
|
|
34
|
+
'upd.autoChange': string;
|
|
35
|
+
'upd.save': string;
|
|
36
|
+
'upd.changed': string;
|
|
37
|
+
'upd.autoOnSaved': string;
|
|
38
|
+
'upd.autoOffSaved': string;
|
|
39
|
+
'upd.now': string;
|
|
40
|
+
'upd.updating': string;
|
|
41
|
+
'upd.done': string;
|
|
42
|
+
'upd.reloading': string;
|
|
43
|
+
'upd.restartHint': string;
|
|
11
44
|
'acct.title': string;
|
|
12
45
|
'theme.system': string;
|
|
13
46
|
'theme.light': string;
|
package/dist/src/panel/i18n.js
CHANGED
|
@@ -10,6 +10,39 @@ const en = {
|
|
|
10
10
|
'top.updatedTitle': 'Last loaded at {at}. Refreshes on its own every {s} s while this page is open.',
|
|
11
11
|
'top.language': 'Switch language',
|
|
12
12
|
'foot.text': 'runs on this machine; nothing leaves it.',
|
|
13
|
+
'foot.textChecks': 'runs on this machine; once a day it asks npm for the newest sideby version, nothing else leaves it.',
|
|
14
|
+
'upd.label': 'v{v} available',
|
|
15
|
+
'upd.labelLong': 'sideby {v} is available (you have {cur}). Click to see what updating does.',
|
|
16
|
+
'upd.title': 'sideby updates',
|
|
17
|
+
'upd.loading': 'Loading…',
|
|
18
|
+
'upd.current': 'You have sideby {v}.',
|
|
19
|
+
'upd.available': 'sideby {v} is available.',
|
|
20
|
+
'upd.notes': 'What changed',
|
|
21
|
+
'upd.runs': 'Updating runs:',
|
|
22
|
+
'upd.after': 'A background panel restarts by itself and this page reloads. A sideby ui running in a terminal and sessions already running keep the old version until you restart them.',
|
|
23
|
+
'upd.cannot': 'sideby cannot update itself here: {why}. Run this yourself:',
|
|
24
|
+
'upd.skill': 'If you use the sideby skill, update it too:',
|
|
25
|
+
'upd.checksOff': 'Update checks are off ({why}).',
|
|
26
|
+
'upd.latest': 'This is the newest version',
|
|
27
|
+
'upd.latestNow': 'You have the newest version (checked just now).',
|
|
28
|
+
'upd.check': 'Check for updates',
|
|
29
|
+
'upd.checking': 'Checking…',
|
|
30
|
+
'upd.verTitle': 'sideby {v} · check for updates',
|
|
31
|
+
'upd.checked': 'checked {ago}',
|
|
32
|
+
'upd.notYet': 'sideby has not checked for a new version yet. It checks once a day.',
|
|
33
|
+
'upd.auto': 'Update by itself',
|
|
34
|
+
'upd.autoHint': 'Installs a new version after the daily check, when no sideby session is running. Never a major version.',
|
|
35
|
+
'upd.autoNo': 'Not available for this install: sideby cannot update itself here.',
|
|
36
|
+
'upd.autoChange': 'Sets update.auto to {value} in {file}.',
|
|
37
|
+
'upd.save': 'Save',
|
|
38
|
+
'upd.changed': 'A newer version, {v}, came out while this was open. Nothing was installed; look again and update.',
|
|
39
|
+
'upd.autoOnSaved': 'sideby now updates by itself',
|
|
40
|
+
'upd.autoOffSaved': 'sideby no longer updates by itself',
|
|
41
|
+
'upd.now': 'Update to {v}',
|
|
42
|
+
'upd.updating': 'Updating…',
|
|
43
|
+
'upd.done': 'Updated to sideby {v}.',
|
|
44
|
+
'upd.reloading': 'Waiting for the new version; this page reloads by itself.',
|
|
45
|
+
'upd.restartHint': 'This page still runs the old version. Restart sideby ui (or reload the page in your portal) to use the new one.',
|
|
13
46
|
'acct.title': 'Accounts',
|
|
14
47
|
'theme.system': 'Theme: follow system',
|
|
15
48
|
'theme.light': 'Theme: light',
|
|
@@ -519,6 +552,39 @@ const zh = {
|
|
|
519
552
|
'top.updatedTitle': '上次加载于 {at}。页面打开时每 {s} 秒自动刷新。',
|
|
520
553
|
'top.language': '切换语言',
|
|
521
554
|
'foot.text': '只在本机运行,数据不会离开这台电脑。',
|
|
555
|
+
'foot.textChecks': '只在本机运行;每天向 npm 查一次 sideby 最新版本号,除此之外没有数据离开这台电脑。',
|
|
556
|
+
'upd.label': 'v{v} 可更新',
|
|
557
|
+
'upd.labelLong': 'sideby {v} 已发布(当前 {cur})。点击查看更新会做什么。',
|
|
558
|
+
'upd.title': 'sideby 更新',
|
|
559
|
+
'upd.loading': '加载中…',
|
|
560
|
+
'upd.current': '当前版本 sideby {v}。',
|
|
561
|
+
'upd.available': 'sideby {v} 已发布。',
|
|
562
|
+
'upd.notes': '更新内容',
|
|
563
|
+
'upd.runs': '更新会运行:',
|
|
564
|
+
'upd.after': '后台面板会自己重启,本页随后自动刷新。终端里运行的 sideby ui 和已经在跑的会话要重启后才用新版本。',
|
|
565
|
+
'upd.cannot': '这种安装方式下 sideby 不能自己更新:{why}。请自己运行:',
|
|
566
|
+
'upd.skill': '如果你装了 sideby skill,也更新一下:',
|
|
567
|
+
'upd.checksOff': '检查更新已关闭({why})。',
|
|
568
|
+
'upd.latest': '已是最新版本',
|
|
569
|
+
'upd.latestNow': '已是最新版本(刚刚检查)。',
|
|
570
|
+
'upd.check': '检查更新',
|
|
571
|
+
'upd.checking': '正在检查…',
|
|
572
|
+
'upd.verTitle': '当前 sideby {v} · 检查更新',
|
|
573
|
+
'upd.checked': '{ago}检查',
|
|
574
|
+
'upd.notYet': 'sideby 还没检查过新版本,每天检查一次。',
|
|
575
|
+
'upd.auto': '自动更新',
|
|
576
|
+
'upd.autoHint': '每天检查后,在没有 sideby 会话运行时安装新版本。不会自动装大版本。',
|
|
577
|
+
'upd.autoNo': '这种安装方式不支持:sideby 不能自己更新。',
|
|
578
|
+
'upd.autoChange': '会把 {file} 里的 update.auto 改成 {value}。',
|
|
579
|
+
'upd.save': '保存',
|
|
580
|
+
'upd.changed': '对话框打开期间又发布了新版本 {v}。什么都没安装;请再确认一次后更新。',
|
|
581
|
+
'upd.autoOnSaved': '已开启自动更新',
|
|
582
|
+
'upd.autoOffSaved': '已关闭自动更新',
|
|
583
|
+
'upd.now': '更新到 {v}',
|
|
584
|
+
'upd.updating': '更新中…',
|
|
585
|
+
'upd.done': '已更新到 sideby {v}。',
|
|
586
|
+
'upd.reloading': '等待新版本启动,本页会自动刷新。',
|
|
587
|
+
'upd.restartHint': '本页仍在运行旧版本。重启 sideby ui(在 Portal 里就刷新页面)后使用新版本。',
|
|
522
588
|
'acct.title': '账号',
|
|
523
589
|
'theme.system': '主题:跟随系统',
|
|
524
590
|
'theme.light': '主题:浅色',
|
|
@@ -254,6 +254,7 @@ const MAIN = String.raw `
|
|
|
254
254
|
check: 'M9 12l2 2 4-4M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18z',
|
|
255
255
|
userPlus: 'M15 19a6 6 0 0 0-12 0M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM19 8v6M16 11h6',
|
|
256
256
|
x: 'M6 6l12 12M18 6L6 18',
|
|
257
|
+
arrowUp: 'M12 19V5M5 12l7-7 7 7',
|
|
257
258
|
bot: 'M6 9h12a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2zM12 5v4M9.5 13.5v1M14.5 13.5v1',
|
|
258
259
|
grid: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z',
|
|
259
260
|
list: 'M9 6h11M9 12h11M9 18h11M4 6h.01M4 12h.01M4 18h.01',
|
|
@@ -633,6 +634,9 @@ const MAIN = String.raw `
|
|
|
633
634
|
function renderTop() {
|
|
634
635
|
renderUpdated()
|
|
635
636
|
renderHandoffButton()
|
|
637
|
+
renderUpdateButton()
|
|
638
|
+
const u = updState()
|
|
639
|
+
$('foot-text').textContent = t(u && !u.checksOff ? 'foot.textChecks' : 'foot.text')
|
|
636
640
|
// A background refresh keeps the button still; only one the viewer asked for spins it.
|
|
637
641
|
const busy = S.loading && !S.silent
|
|
638
642
|
const refresh = $('refresh')
|
|
@@ -2674,8 +2678,169 @@ const MAIN = String.raw `
|
|
|
2674
2678
|
if (Date.now() - S.lastTry > 3000) load({ silent: true })
|
|
2675
2679
|
}
|
|
2676
2680
|
|
|
2681
|
+
// ---------- updates (spec §3.23, ADR-0021) ----------
|
|
2682
|
+
// A label in the header only when a newer sideby exists; the footer version opens the same dialog any time, with
|
|
2683
|
+
// the Auto Update switch. Updating is one plan shown first and one click; afterwards the page waits for the new
|
|
2684
|
+
// build and reloads (a background Panel restarts itself, a Portal loads the new code by itself).
|
|
2685
|
+
let up = null
|
|
2686
|
+
function updState() { return BOOT.updates && S.state ? S.state.update || null : null }
|
|
2687
|
+
function renderUpdateButton() {
|
|
2688
|
+
const b = $('update')
|
|
2689
|
+
const u = updState()
|
|
2690
|
+
// The version the page runs, a pill after the name; it opens the same dialog (with "Check for updates"), and
|
|
2691
|
+
// takes a dot while a newer version waits.
|
|
2692
|
+
const v = $('ver-top')
|
|
2693
|
+
v.hidden = !BOOT.version
|
|
2694
|
+
if (!v.hidden) {
|
|
2695
|
+
const newer = !!(u && u.available)
|
|
2696
|
+
v.textContent = 'v' + BOOT.version
|
|
2697
|
+
v.classList.toggle('has-update', newer)
|
|
2698
|
+
v.disabled = !BOOT.updates
|
|
2699
|
+
const tip = !BOOT.updates ? '' : newer ? t('upd.labelLong', { v: u.latest, cur: u.current }) : t('upd.verTitle', { v: BOOT.version })
|
|
2700
|
+
v.title = tip
|
|
2701
|
+
if (tip) v.setAttribute('aria-label', tip)
|
|
2702
|
+
else v.removeAttribute('aria-label')
|
|
2703
|
+
}
|
|
2704
|
+
b.hidden = !(u && u.available)
|
|
2705
|
+
if (!b.hidden) {
|
|
2706
|
+
setKids(b, [icon('arrowUp'), h('span', null, t('upd.label', { v: u.latest }))])
|
|
2707
|
+
b.setAttribute('aria-label', t('upd.labelLong', { v: u.latest, cur: u.current }))
|
|
2708
|
+
b.title = t('upd.labelLong', { v: u.latest, cur: u.current })
|
|
2709
|
+
}
|
|
2710
|
+
if (dlg.kind === 'update' && up && !up.busy) renderUpdate()
|
|
2711
|
+
}
|
|
2712
|
+
function openUpdate() {
|
|
2713
|
+
if (!BOOT.updates) return
|
|
2714
|
+
up = { busy: false, error: null, done: null, changed: false, autoPlan: null, autoBusy: false, waitUntil: 0, checking: false, checked: false }
|
|
2715
|
+
showDialog('update')
|
|
2716
|
+
renderUpdate()
|
|
2717
|
+
}
|
|
2718
|
+
async function updRun() {
|
|
2719
|
+
const u = updState()
|
|
2720
|
+
if (!u) return
|
|
2721
|
+
up.busy = true
|
|
2722
|
+
dlg.busy = true
|
|
2723
|
+
up.error = null
|
|
2724
|
+
renderUpdate()
|
|
2725
|
+
try {
|
|
2726
|
+
const r = await api('/api/update', { confirm: true, version: u.latest })
|
|
2727
|
+
if (!r.updated) {
|
|
2728
|
+
// The registry names another version than the dialog showed: show the new plan and let the person look again.
|
|
2729
|
+
Object.assign(S.state.update, { latest: r.latest, available: r.available, command: r.command, canRun: r.canRun, releaseNotes: r.releaseNotes, method: r.method })
|
|
2730
|
+
up.changed = true
|
|
2731
|
+
} else {
|
|
2732
|
+
up.done = r
|
|
2733
|
+
up.waitUntil = Date.now() + 20000
|
|
2734
|
+
updWait()
|
|
2735
|
+
}
|
|
2736
|
+
} catch (e) { up.error = e }
|
|
2737
|
+
up.busy = false
|
|
2738
|
+
dlg.busy = false
|
|
2739
|
+
renderUpdate()
|
|
2740
|
+
}
|
|
2741
|
+
// "Check for updates": asks the registry now (the daily check only runs once a day), then shows the answer.
|
|
2742
|
+
async function updCheck() {
|
|
2743
|
+
up.checking = true
|
|
2744
|
+
up.error = null
|
|
2745
|
+
renderUpdate()
|
|
2746
|
+
try {
|
|
2747
|
+
await api('/api/update', {})
|
|
2748
|
+
await load({ silent: true })
|
|
2749
|
+
up.checked = true
|
|
2750
|
+
} catch (e) { up.error = e }
|
|
2751
|
+
up.checking = false
|
|
2752
|
+
renderUpdate()
|
|
2753
|
+
}
|
|
2754
|
+
// After an Update the server (or the Portal's worker) comes back with a new build; load() then reloads the page.
|
|
2755
|
+
function updWait() {
|
|
2756
|
+
if (!up || !up.done || S.newBuild) { if (S.newBuild) location.reload(); return }
|
|
2757
|
+
if (Date.now() > up.waitUntil) { renderUpdate(); return }
|
|
2758
|
+
setTimeout(() => { void load({ silent: true }).then(updWait) }, 1500)
|
|
2759
|
+
}
|
|
2760
|
+
async function updAuto(on) {
|
|
2761
|
+
up.autoBusy = true
|
|
2762
|
+
up.error = null
|
|
2763
|
+
renderUpdate()
|
|
2764
|
+
try {
|
|
2765
|
+
const r = await api('/api/update/settings', { auto: on })
|
|
2766
|
+
up.autoPlan = r.changed ? { auto: on, file: r.file } : null
|
|
2767
|
+
} catch (e) { up.error = e }
|
|
2768
|
+
up.autoBusy = false
|
|
2769
|
+
renderUpdate()
|
|
2770
|
+
}
|
|
2771
|
+
async function updAutoSave() {
|
|
2772
|
+
if (!up.autoPlan) return
|
|
2773
|
+
up.autoBusy = true
|
|
2774
|
+
renderUpdate()
|
|
2775
|
+
try {
|
|
2776
|
+
await api('/api/update/settings', { auto: up.autoPlan.auto, confirm: true })
|
|
2777
|
+
toast(t(up.autoPlan.auto ? 'upd.autoOnSaved' : 'upd.autoOffSaved'))
|
|
2778
|
+
up.autoPlan = null
|
|
2779
|
+
await load({ silent: true })
|
|
2780
|
+
} catch (e) { up.error = e }
|
|
2781
|
+
up.autoBusy = false
|
|
2782
|
+
renderUpdate()
|
|
2783
|
+
}
|
|
2784
|
+
function renderUpdate() {
|
|
2785
|
+
if (!up || dlg.kind !== 'update') return
|
|
2786
|
+
const u = updState()
|
|
2787
|
+
const body = []
|
|
2788
|
+
if (!u) {
|
|
2789
|
+
body.push(h('p', { class: 'muted' }, spinner(), ' ', t('upd.loading')))
|
|
2790
|
+
setKids(dlg.el, dialogFrame(t('upd.title'), null, body, null))
|
|
2791
|
+
return
|
|
2792
|
+
}
|
|
2793
|
+
body.push(h('p', null, t('upd.current', { v: u.current })))
|
|
2794
|
+
if (up.done) {
|
|
2795
|
+
body.push(h('p', { class: 'note' }, t('upd.done', { v: up.done.latest })))
|
|
2796
|
+
body.push(h('p', { class: 'muted small' }, Date.now() > up.waitUntil ? t('upd.restartHint') : [spinner(), ' ', t('upd.reloading')]))
|
|
2797
|
+
body.push(h('p', { class: 'small' }, t('upd.skill')), codeCopy(u.skill))
|
|
2798
|
+
} else if (u.available) {
|
|
2799
|
+
if (up.changed) body.push(h('p', { class: 'note note-warn' }, t('upd.changed', { v: u.latest })))
|
|
2800
|
+
body.push(h('p', null, h('b', null, t('upd.available', { v: u.latest })), ' ',
|
|
2801
|
+
h('a', { href: u.releaseNotes, target: '_blank', rel: 'noopener noreferrer' }, t('upd.notes'))))
|
|
2802
|
+
if (u.canRun) {
|
|
2803
|
+
body.push(h('p', { class: 'small' }, t('upd.runs')), codeCopy(u.command))
|
|
2804
|
+
body.push(h('p', { class: 'muted small' }, t('upd.after')))
|
|
2805
|
+
} else {
|
|
2806
|
+
body.push(h('p', { class: 'note note-warn' }, t('upd.cannot', { why: u.method.reason || u.method.kind })))
|
|
2807
|
+
body.push(codeCopy(u.command))
|
|
2808
|
+
}
|
|
2809
|
+
body.push(h('p', { class: 'small' }, t('upd.skill')), codeCopy(u.skill))
|
|
2810
|
+
} else if (up.checked && u.latest) {
|
|
2811
|
+
body.push(h('p', { class: 'note' }, t('upd.latestNow')))
|
|
2812
|
+
} else if (u.checksOff) {
|
|
2813
|
+
body.push(h('p', { class: 'muted small' }, t('upd.checksOff', { why: u.checksOff })))
|
|
2814
|
+
} else if (u.latest) {
|
|
2815
|
+
body.push(h('p', { class: 'muted small' }, t('upd.latest') + (u.checkedAt ? ' · ' + t('upd.checked', { ago: ago(u.checkedAt) }) : '')))
|
|
2816
|
+
} else body.push(h('p', { class: 'muted small' }, t('upd.notYet')))
|
|
2817
|
+
// A read-only Panel sends no request and writes nothing, so it cannot check (spec §3.23).
|
|
2818
|
+
if (!BOOT.readOnly && !up.done && !u.available)
|
|
2819
|
+
body.push(h('p', null, h('button', { class: 'btn btn-sm', type: 'button', disabled: up.checking || up.busy, 'aria-busy': up.checking ? 'true' : null, onclick: updCheck },
|
|
2820
|
+
up.checking ? spinner() : icon('refresh'), up.checking ? t('upd.checking') : t('upd.check'))))
|
|
2821
|
+
if (!BOOT.readOnly && !up.done) {
|
|
2822
|
+
const box = h('input', { type: 'checkbox', disabled: up.autoBusy || !u.method.canUpdate })
|
|
2823
|
+
box.checked = up.autoPlan ? up.autoPlan.auto : u.auto
|
|
2824
|
+
box.addEventListener('change', () => updAuto(box.checked))
|
|
2825
|
+
body.push(h('div', { class: 'check-row' }, h('label', { class: 'check' }, box,
|
|
2826
|
+
h('span', null, h('b', null, t('upd.auto')), h('span', { class: 'muted small' }, u.method.canUpdate ? t('upd.autoHint') : t('upd.autoNo'))))))
|
|
2827
|
+
if (up.autoPlan) body.push(h('p', { class: 'small' }, t('upd.autoChange', { file: up.autoPlan.file, value: String(up.autoPlan.auto) }), ' ',
|
|
2828
|
+
h('button', { class: 'btn btn-sm btn-primary', type: 'button', disabled: up.autoBusy, onclick: updAutoSave }, t('upd.save'))))
|
|
2829
|
+
}
|
|
2830
|
+
if (BOOT.readOnly) body.push(h('p', { class: 'note' }, t('banner.readOnly')))
|
|
2831
|
+
if (up.error) body.push(errorNote(up.error, null))
|
|
2832
|
+
const foot = [h('button', { class: 'btn', type: 'button', disabled: up.busy, onclick: () => dlg.el.close() }, t('btn.close'))]
|
|
2833
|
+
if (u.available && u.canRun && !BOOT.readOnly && !up.done)
|
|
2834
|
+
foot.push(h('button', { class: 'btn btn-primary', type: 'button', disabled: up.busy, 'aria-busy': up.busy ? 'true' : null, onclick: updRun }, up.busy ? spinner() : null, up.busy ? t('upd.updating') : t('upd.now', { v: u.latest })))
|
|
2835
|
+
setKids(dlg.el, dialogFrame(t('upd.title'), null, body, foot))
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2677
2838
|
// ---------- start ----------
|
|
2678
|
-
$('ver').textContent = BOOT.version ? 'v' + BOOT.version : ''
|
|
2839
|
+
$('ver').textContent = BOOT.version ? 'sideby v' + BOOT.version : 'sideby'
|
|
2840
|
+
$('ver').disabled = !BOOT.updates
|
|
2841
|
+
$('ver').addEventListener('click', openUpdate)
|
|
2842
|
+
$('update').addEventListener('click', openUpdate)
|
|
2843
|
+
$('ver-top').addEventListener('click', openUpdate)
|
|
2679
2844
|
$('refresh').addEventListener('click', () => load({ check: true, announce: true }))
|
|
2680
2845
|
$('handoff').addEventListener('click', () => setPage('handoff'))
|
|
2681
2846
|
$('lang').addEventListener('click', () => {
|
|
@@ -2690,6 +2855,7 @@ const MAIN = String.raw `
|
|
|
2690
2855
|
else if (dlg.kind === 'setup' && qs) renderSetup()
|
|
2691
2856
|
else if (dlg.kind === 'tune' && tuneState) renderTune()
|
|
2692
2857
|
else if (dlg.kind === 'model' && md) { md.form = null; renderModel() }
|
|
2858
|
+
else if (dlg.kind === 'update' && up) renderUpdate()
|
|
2693
2859
|
})
|
|
2694
2860
|
$('theme').addEventListener('click', () => {
|
|
2695
2861
|
const order = CHOICES.theme
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The Panel page's styles: design tokens (a host theme layers its values over them, see theme.ts) and the rules
|
|
2
2
|
// that read them. Every color, radius and size below comes from a token, so a host theme reaches every component.
|
|
3
3
|
import { resolveTheme } from './theme.js';
|
|
4
|
-
// Colors defined from other tokens; repeated in both schemes so they follow that scheme's base colors.
|
|
4
|
+
// Colors defined from other tokens; repeated in both schemes so they follow that scheme's base colors. Dark mode keeps
|
|
5
|
+
// the logo tile light: the mark's ink outline (core/brand-mark.ts) needs a light ground.
|
|
5
6
|
const DERIVED_COLORS = `
|
|
6
7
|
--btn-text: var(--text); --btn-hover-text: var(--accent); --check: var(--focus);
|
|
7
8
|
--logo-bg: var(--accent-soft); --logo-fg: var(--focus); --logo-line: var(--border);
|
|
@@ -34,7 +35,8 @@ const DARK_TOKENS = `
|
|
|
34
35
|
--ok: #4ade80; --ok-bar: #22c55e; --ok-soft: #13291c; --warn: #fbbf24; --warn-bar: #f59e0b; --warn-soft: #2d2410;
|
|
35
36
|
--fail: #f87171; --fail-bar: #ef4444; --fail-soft: #331717; --track: #1f2c3a;
|
|
36
37
|
--spark: oklch(from var(--accent) clamp(0.7, l, 0.78) max(c, 0.14) h); --spark-soft: oklch(from var(--accent) 0.45 calc(max(c, 0.14) * 0.6) h);
|
|
37
|
-
--shadow: 0 1px 2px rgba(0,0,0,.3); --float-shadow: 0 12px 32px rgba(0,0,0,.5), 0 2px 6px rgba(0,0,0,.3);${DERIVED_COLORS}
|
|
38
|
+
--shadow: 0 1px 2px rgba(0,0,0,.3); --float-shadow: 0 12px 32px rgba(0,0,0,.5), 0 2px 6px rgba(0,0,0,.3);${DERIVED_COLORS}
|
|
39
|
+
--logo-bg: #dcebf7; --logo-line: transparent;`;
|
|
38
40
|
/**
|
|
39
41
|
* The page's style block: default tokens, the host's tokens over them, then the rules that read them. With
|
|
40
42
|
* `colorScheme: 'auto'` the page follows the system unless the viewer picked light or dark in the Panel, which sets
|
|
@@ -61,6 +63,9 @@ body { margin: 0; background: var(--bg); color: var(--text); font: var(--font-si
|
|
|
61
63
|
[hidden] { display: none !important; }
|
|
62
64
|
.top-in, .wrap { max-width: var(--content-width); margin: 0 auto; }
|
|
63
65
|
.top-in { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 28px var(--gutter) 0; }
|
|
66
|
+
/* When the name, its version pill and the buttons do not fit one row, the buttons wrap below, on the right. */
|
|
67
|
+
.top-in { flex-wrap: wrap; row-gap: 10px; }
|
|
68
|
+
.top-in > .top-actions { margin-left: auto; flex-wrap: wrap; justify-content: flex-end; }
|
|
64
69
|
.wrap { padding: 20px var(--gutter) 40px; }
|
|
65
70
|
.header-bar .top { background: var(--surface); border-bottom: 1px solid var(--border); }
|
|
66
71
|
.header-bar .top-in { min-height: 84px; padding: 14px var(--gutter); }
|
|
@@ -80,6 +85,14 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
|
|
|
80
85
|
.brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
|
81
86
|
.brand h1 { font-size: var(--title-size); font-weight: var(--title-weight); line-height: 1.3; letter-spacing: -0.02em; }
|
|
82
87
|
.tagline { color: var(--muted); font-size: 13px; }
|
|
88
|
+
/* The running version, a quiet pill after the name; with a newer version it takes the accent and a dot (spec §3.23). */
|
|
89
|
+
.title-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
90
|
+
.ver-pill { display: inline-flex; align-items: center; gap: 5px; padding: 1px 8px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface); color: var(--muted); font: 500 11.5px/18px var(--mono); cursor: pointer; white-space: nowrap; }
|
|
91
|
+
.ver-pill:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }
|
|
92
|
+
.ver-pill:disabled { cursor: default; }
|
|
93
|
+
.ver-pill:focus-visible { outline: var(--focus-outline); outline-offset: 2px; }
|
|
94
|
+
.ver-pill.has-update { color: var(--accent); border-color: var(--accent); background: var(--accent-soft); }
|
|
95
|
+
.ver-pill.has-update::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
|
|
83
96
|
.logo { display: inline-flex; flex: none; width: 40px; height: 40px; border-radius: 10px; background: var(--logo-bg); box-shadow: inset 0 0 0 1px var(--logo-line); }
|
|
84
97
|
/* desktop/icon.ts draws the same two half discs for the app icon. */
|
|
85
98
|
.logo svg { display: block; width: 100%; height: 100%; fill: var(--logo-fg); }
|
|
@@ -221,6 +234,8 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
|
|
|
221
234
|
.link-btn { padding: 0; border: 0; background: none; color: var(--accent); font: inherit; font-weight: var(--strong-weight); cursor: pointer; }
|
|
222
235
|
.link-btn:hover:not(:disabled) { text-decoration: underline; }
|
|
223
236
|
.link-btn:disabled { color: var(--faint); cursor: not-allowed; }
|
|
237
|
+
.foot .link-btn { color: inherit; font-weight: inherit; }
|
|
238
|
+
.update-btn { border-color: var(--accent); color: var(--accent); }
|
|
224
239
|
.link-btn:focus-visible { outline: var(--focus-outline); outline-offset: 2px; border-radius: 3px; }
|
|
225
240
|
|
|
226
241
|
/* Usage: total over cache hits, with the 7 day bars beside both lines. In the wide list the bars sit at the column's
|
|
@@ -555,6 +570,7 @@ select:is(:hover, :focus) { --chevron: var(--text); }
|
|
|
555
570
|
:root { --gutter: 14px; }
|
|
556
571
|
.top-in { padding: 16px var(--gutter) 0; }
|
|
557
572
|
.header-bar .top-in { min-height: 0; padding: 12px var(--gutter); }
|
|
573
|
+
.update-btn span { display: none; }
|
|
558
574
|
.wrap { padding: 14px var(--gutter) 32px; }
|
|
559
575
|
.hide-sm { display: none; }
|
|
560
576
|
.tagline { display: none; }
|
package/dist/src/panel/page.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export interface PageOptions {
|
|
|
14
14
|
theme?: ResolvedTheme;
|
|
15
15
|
/** Host language from createPanelHandler's `lang`; `auto` (the default) lets the viewer choose. */
|
|
16
16
|
lang?: 'auto' | 'en' | 'zh';
|
|
17
|
+
/** createPanelHandler's `updates`: false hides everything about sideby updates. */
|
|
18
|
+
updates?: boolean;
|
|
17
19
|
}
|
|
18
20
|
/** JSON that is safe inside a `<script>` element: no `</script>`, `<!--` or line separators survive. */
|
|
19
21
|
export declare function jsonForScript(value: unknown): string;
|
package/dist/src/panel/page.js
CHANGED
|
@@ -34,6 +34,7 @@ export function renderPage(opts) {
|
|
|
34
34
|
version: opts.version ?? '',
|
|
35
35
|
build: opts.build ?? '',
|
|
36
36
|
readOnly: Boolean(opts.readOnly),
|
|
37
|
+
updates: opts.updates !== false,
|
|
37
38
|
colorScheme: theme.colorScheme,
|
|
38
39
|
lang,
|
|
39
40
|
warn: QUOTA_WARN_PERCENT,
|
|
@@ -51,7 +52,7 @@ export function renderPage(opts) {
|
|
|
51
52
|
<meta name="color-scheme" content="${scheme}">
|
|
52
53
|
<meta name="referrer" content="no-referrer">
|
|
53
54
|
<title>sideby</title>
|
|
54
|
-
<link rel="icon" href="
|
|
55
|
+
<link rel="icon" type="image/svg+xml" href="${attr(opts.basePath)}/favicon.svg">
|
|
55
56
|
<style${nonce}>${pageStyles(theme)}</style>
|
|
56
57
|
</head>
|
|
57
58
|
<body${theme.header === 'bar' ? ' class="header-bar"' : ''}>
|
|
@@ -60,7 +61,7 @@ export function renderPage(opts) {
|
|
|
60
61
|
<div class="brand">
|
|
61
62
|
<span class="logo" aria-hidden="true">${MARK_SVG}</span>
|
|
62
63
|
<div>
|
|
63
|
-
<h1>sideby</h1>
|
|
64
|
+
<div class="title-row"><h1>sideby</h1><button id="ver-top" class="ver-pill" type="button" hidden></button></div>
|
|
64
65
|
<p class="tagline" data-i18n="top.tagline">Every AI coding account, side by side.</p>
|
|
65
66
|
</div>
|
|
66
67
|
</div>
|
|
@@ -68,6 +69,7 @@ export function renderPage(opts) {
|
|
|
68
69
|
<span id="updated" class="muted small">Loading…</span>
|
|
69
70
|
<button id="lang" class="icon-btn lang-btn" type="button"${lang === 'auto' ? '' : ' hidden'}>${lang === 'zh' ? 'EN' : '中文'}</button>
|
|
70
71
|
<button id="theme" class="icon-btn" type="button" hidden></button>
|
|
72
|
+
<button id="update" class="btn update-btn" type="button" hidden></button>
|
|
71
73
|
<button id="handoff" class="btn" type="button" hidden></button>
|
|
72
74
|
<button id="refresh" class="btn" type="button" aria-label="Refresh"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 11a8 8 0 1 0-2.3 5.7"/><path d="M20 4v7h-7"/></svg><span class="hide-sm" data-i18n="top.refresh">Refresh</span></button>
|
|
73
75
|
</div>
|
|
@@ -88,7 +90,7 @@ export function renderPage(opts) {
|
|
|
88
90
|
<div class="section-head"><h2 id="h-checkup" data-i18n="health.title">Health check</h2></div>
|
|
89
91
|
<span class="sk sk-line w70"></span><span class="sk sk-line w50"></span>
|
|
90
92
|
</section>
|
|
91
|
-
<footer class="foot muted small"
|
|
93
|
+
<footer class="foot muted small"><button id="ver" class="link-btn" type="button">sideby</button> · <span id="foot-text" data-i18n="foot.text">runs on this machine; nothing leaves it.</span></footer>
|
|
92
94
|
</main>
|
|
93
95
|
<div id="conn" class="conn" role="status" hidden></div>
|
|
94
96
|
<div id="toasts" class="toasts" aria-live="polite" popover="manual"></div>
|
package/dist/testing/index.js
CHANGED
|
@@ -19,6 +19,8 @@ export async function withFakeHome(fn) {
|
|
|
19
19
|
XDG_STATE_HOME: join(home, '.local', 'state'),
|
|
20
20
|
PATH: `${bin}:/usr/bin:/bin`,
|
|
21
21
|
NO_COLOR: '1',
|
|
22
|
+
// No Update Check from tests (spec §3.23): it is sideby's only network request. Delete it to test checks.
|
|
23
|
+
SIDEBY_NO_UPDATE_NOTIFIER: '1',
|
|
22
24
|
};
|
|
23
25
|
const h = {
|
|
24
26
|
home,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sideby",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
4
4
|
"description": "Run work, personal and API-key accounts at the same time, without their configs drifting apart.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Adonis0123 (https://github.com/Adonis0123)",
|
|
@@ -77,5 +77,5 @@
|
|
|
77
77
|
},
|
|
78
78
|
"packageManager": "pnpm@10.15.0",
|
|
79
79
|
"readmeFilename": "README.md",
|
|
80
|
-
"readme": "<p align=\"center\">\n <img alt=\"sideby: work, personal and API-key accounts, one terminal each, one shared config\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/banner.jpg\" width=\"880\">\n</p>\n\n<p align=\"center\">\n <b>Run work, personal and API-key accounts at the same time, without their configs drifting apart.</b><br>\n Each account is its own directory and its own terminal. Skills, hooks and settings stay linked to your main account. Claude Code, Codex, Grok Build and pi.\n</p>\n\n<p align=\"center\">\n <a href=\"https://www.npmjs.com/package/sideby\"><img alt=\"npm\" src=\"https://img.shields.io/npm/v/sideby\"></a>\n <a href=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml\"><img alt=\"CI\" src=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml/badge.svg\"></a>\n <img alt=\"Node 22.18+\" src=\"https://img.shields.io/badge/node-%E2%89%A522.18-339933\">\n <a href=\"LICENSE\"><img alt=\"MIT\" src=\"https://img.shields.io/badge/license-MIT-blue\"></a>\n</p>\n\n<p align=\"center\">English | <a href=\"README.zh-CN.md\">中文</a></p>\n\n## Quick start\n\nRequires Node 22.18+ and at least one Host CLI (Claude Code, Codex, Grok Build or pi) installed. AI agents: follow the setup steps in [`llms.txt`](llms.txt) (\"Set up sideby for a user\") instead.\n\n```sh\nnpm i -g sideby\nsideby new claude work # create ~/.claude-work, sharing ~/.claude's skills and hooks\nsideby login claude:work # sign in once\nsideby run work # Claude Code on the work account\nsideby quota setup claude --yes # Claude quota in the panel; undo: sideby quota teardown claude\nsideby ui # every account's quota at http://127.0.0.1:17420\n```\n\n`sideby run claude:main` keeps using `~/.claude` in another terminal at the same time. Arguments after `--` go to the Host: `sideby run work -- --resume`. To try it without installing, run `npx sideby`.\n\n## Set up with your AI agent\n\nPaste this into Claude Code, Codex or any coding agent:\n\n```text\nInstall the sideby CLI (npm package `sideby`) and its agent skill, then set up a second Claude Code account for me.\nFollow \"Set up sideby for a user\" in https://raw.githubusercontent.com/Adonis0123/sideby/main/llms.txt\n```\n\nThe agent checks Node and your Host CLIs, installs sideby and the skill, creates the account, tells you what it shares with your main one and makes its short command work. You sign in once in your browser; the agent gives you that one command. Once the skill is installed, short requests work too: \"add another Claude account, cc008\", \"what do my Codex accounts share?\", \"which account still has quota?\".\n\n## What it looks like\n\nThe 25-second story: one account buried in tasks, then accounts side by side sharing one setup, and a task moving on when one runs out.\n\nhttps://github.com/user-attachments/assets/3a470c03-9009-4f1d-b7f6-8b4625d74958\n\n<img alt=\"Two terminals: the personal account and the work account running at the same time, both linked to the main account's skills and settings, then the local quota panel, and at 95% Auto Handoff moving work's session to the next account\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/demo.gif\" width=\"1280\">\n\n<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel-dark.png\">\n <source media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel.png\">\n <img alt=\"sideby panel: every account's 5h and 7d quota, 7-day token usage and health in one page\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel.png\" width=\"1280\">\n</picture>\n\n## Why not just an alias?\n\n`CLAUDE_CONFIG_DIR` plus a shell alias already starts a second account. It does not keep skills and settings in one place, and it does not show quota for every account. [Does this break a provider's terms?](docs/guide/faq.md#terms)\n\n| | At the same time | Skills, hooks, settings | Quota and usage | Setup |\n|---|---|---|---|---|\n| `CLAUDE_CONFIG_DIR` and an alias | Yes. Each terminal exports the variable. | Each directory is its own copy. They drift unless you symlink by hand. | That session's status line | One alias per account |\n| [cc-switch](https://github.com/farion1231/cc-switch) | Enabling a provider writes the host's live config, so open sessions share that switch. | Plugin settings are copied across providers as a separate step. | The active provider, from its quota endpoint | Install the desktop app |\n| sideby | Yes. `sideby run` selects the directory for that process and leaves your shell alone. | Linked to the main account. `sideby doctor --fix` repairs copies. Sign-in and sessions stay separate. | Every account, from local files. Claude needs `quota setup`. | `npm i -g sideby`, then `sideby new` and sign in |\n\ncc-switch's docs describe that switch: it writes the live settings file ([README](https://github.com/farion1231/cc-switch#readme)). [#1105](https://github.com/farion1231/cc-switch/issues/1105) and [#2908](https://github.com/farion1231/cc-switch/issues/2908) ask for several sessions at once. [#1106](https://github.com/farion1231/cc-switch/issues/1106) is closed; the maintainer says Claude Code's \"open terminal\" can start one provider without changing the global config.\n\n## What else it does\n\n| You want to | Run | Guide |\n|---|---|---|\n| Add an API key account (DeepSeek, a gateway, …) | `sideby new claude deepseek --api` | [API accounts](docs/guide/usage.md#api-accounts) |\n| Find and fix config drift between accounts | `sideby doctor --fix` | [Doctor](docs/guide/usage.md#doctor) |\n| See Claude quota (Codex needs no setup) | `sideby quota setup claude --yes` | [Claude quota](docs/guide/usage.md#claude-quota) |\n| Find the cheapest auto-compact size from your own sessions, and use it in one click | `sideby tune` (or the panel's Context tuning tab) | [Context tuning](docs/guide/usage.md#context-tuning) |\n| See each account's model and effort, and set them per account | `sideby model <account>` (or click the model in the panel) | [Model and effort](docs/guide/usage.md#model-and-effort) |\n| Stop Claude sessions from connecting to claude.ai by themselves (Remote Control), for every account at once | `sideby remote-control off` (or the panel's banner) | [Remote Control](docs/guide/usage.md#remote-control) |\n| Move on when an account hits its limit | `sideby next claude` | [Handoff](docs/guide/usage.md#when-an-account-hits-its-limit) |\n| Let a running task move to the next account by itself when quota runs low (off by default; see the risk) | `sideby handoff enable` (or the panel) | [Auto Handoff](docs/guide/usage.md#auto-handoff) |\n| Short commands such as `ccw` | `sideby alias add ccw claude:work` | [Shell functions](docs/guide/configuration.md#shell-functions) |\n| Reopen a session from Orca and the like in the account that holds it | `\"resumeRouting\": true` in config | [Resuming from other tools](docs/guide/configuration.md#resuming-from-other-tools) |\n| Open Claude Desktop or the Codex app as one account, several side by side (macOS) | `sideby desktop claude:work` (or the panel) | [Desktop apps](docs/guide/usage.md#desktop-apps) |\n| Keep several Grok Bot windows open, each signed in to its own account (macOS) | `sideby desktop grok-bot:002` (or the panel) | [Grok Bot windows](docs/guide/usage.md#grok-bot-windows) |\n| Open the panel from the Dock or app menu | `sideby app install` | [Panel app](docs/guide/panel.md#panel-app) |\n| Show the panel inside your own local page | `createPanelHandler()` | [Embed the panel](docs/guide/panel.md#embed-the-panel) |\n| Add a Host or a pre-launch check | a local plugin | [Plugins](docs/guide/plugins.md) |\n\n## Supported hosts\n\n| Host | Account directories | Sign-in | Quota | Usage (7 days) |\n|---|---|---|---|---|\n| Claude Code | `~/.claude`, `~/.claude-<name>` | `claude auth login` | ✓ after `quota setup claude` | ✓ |\n| Codex CLI | `~/.codex`, `~/.codex-<name>` | `codex login` | ✓ | ✓ |\n| Grok Build | `~/.grok`, `~/.grok-<name>` | `grok login` | no public source | no public source |\n| pi | `~/.pi/agent`, `~/.pi-<name>/agent` | `/login` inside pi | no public source | no public source |\n\nsideby starts the official binary on your `PATH` and never patches it. Tested versions and per-Host notes are in the [guide](docs/guide/usage.md#host-notes).\n\n## Platforms\n\nmacOS and Linux. CI runs the suite on `ubuntu-latest` and `macos-latest`, Node 22 and 24. **Windows is untested.** `sideby app` only installs a launcher on macOS and Linux. Linked items are symlinks, `sideby shell-init` prints bash and zsh functions, and the Claude quota wrapper runs `/bin/sh`.\n\n## Roadmap\n\n**Gemini CLI.** Gemini can point at another config directory with `GEMINI_CONFIG_DIR`. Older docs use `GEMINI_CLI_HOME`; current builds treat that name as deprecated, and exit on startup if it is set together with an exact directory override. A sideby family still needs a tested Gemini version, a login check that does not read tokens, and a list of which files to link or copy. That is more than a small patch, so it is not in this version.\n\n## Safe by design\n\n- It never edits a Host's global config to switch accounts. It moves a task to another account only when you turn on Auto Handoff, and never proxies requests or pools logins.\n- It never reads, prints or sends a credential value. Nothing leaves your machine: sideby calls no remote service.\n- `doctor --fix` never replaces a real file and never changes where a link points.\n\nDetails and the terms-of-service question are in the [FAQ](docs/guide/faq.md).\n\nUsage questions and ideas go in [Discussions](https://github.com/Adonis0123/sideby/discussions).\n\n## Docs\n\n- [Using sideby](docs/guide/usage.md): accounts, API accounts, Doctor, quota, Handoff, Host notes\n- [Commands](docs/guide/commands.md): every command, exit codes and `--json`\n- [Configuration](docs/guide/configuration.md): `config.json`, aliases, shell functions\n- [Panel](docs/guide/panel.md): the panel, panel app and embedding\n- [Plugins](docs/guide/plugins.md): write your own Family or launch hook\n- [FAQ](docs/guide/faq.md): terms of service, data locations, alternatives, uninstall\n- [Changelog](https://github.com/Adonis0123/sideby/releases)\n\n## For AI agents\n\n- [`llms.txt`](llms.txt): setup steps, commands, JSON contract and plugin API in one index. `sideby families --json` states what each Host's accounts share.\n- [`skills/sideby/SKILL.md`](skills/sideby/SKILL.md): one Agent Skill for seeing, repairing and creating accounts and for moving on when one hits its limit. Install with `npx skills add Adonis0123/sideby -g`; `sideby doctor` warns when the installed copy is for another sideby version.\n- [`AGENTS.md`](AGENTS.md): rules for agents working on this repository.\n\n## License\n\n[MIT](LICENSE)\n"
|
|
80
|
+
"readme": "<p align=\"center\">\n <img alt=\"sideby: work, personal and API-key accounts, one terminal each, one shared config\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/banner.jpg\" width=\"880\">\n</p>\n\n<p align=\"center\">\n <b>Run work, personal and API-key accounts at the same time, without their configs drifting apart.</b><br>\n Each account is its own directory and its own terminal. Skills, hooks and settings stay linked to your main account. Claude Code, Codex, Grok Build and pi.\n</p>\n\n<p align=\"center\">\n <a href=\"https://www.npmjs.com/package/sideby\"><img alt=\"npm\" src=\"https://img.shields.io/npm/v/sideby\"></a>\n <a href=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml\"><img alt=\"CI\" src=\"https://github.com/Adonis0123/sideby/actions/workflows/ci.yml/badge.svg\"></a>\n <img alt=\"Node 22.18+\" src=\"https://img.shields.io/badge/node-%E2%89%A522.18-339933\">\n <a href=\"LICENSE\"><img alt=\"MIT\" src=\"https://img.shields.io/badge/license-MIT-blue\"></a>\n</p>\n\n<p align=\"center\">English | <a href=\"README.zh-CN.md\">中文</a></p>\n\n## Quick start\n\nRequires Node 22.18+ and at least one Host CLI (Claude Code, Codex, Grok Build or pi) installed. AI agents: follow the setup steps in [`llms.txt`](llms.txt) (\"Set up sideby for a user\") instead.\n\n```sh\nnpm i -g sideby\nsideby new claude work # create ~/.claude-work, sharing ~/.claude's skills and hooks\nsideby login claude:work # sign in once\nsideby run work # Claude Code on the work account\nsideby quota setup claude --yes # Claude quota in the panel; undo: sideby quota teardown claude\nsideby ui # every account's quota at http://127.0.0.1:17420\n```\n\n`sideby update` installs a newer sideby when there is one; sideby says so after a command and in the panel. `sideby run claude:main` keeps using `~/.claude` in another terminal at the same time. Arguments after `--` go to the Host: `sideby run work -- --resume`. To try it without installing, run `npx sideby`.\n\n## Set up with your AI agent\n\nPaste this into Claude Code, Codex or any coding agent:\n\n```text\nInstall the sideby CLI (npm package `sideby`) and its agent skill, then set up a second Claude Code account for me.\nFollow \"Set up sideby for a user\" in https://raw.githubusercontent.com/Adonis0123/sideby/main/llms.txt\n```\n\nThe agent checks Node and your Host CLIs, installs sideby and the skill, creates the account, tells you what it shares with your main one and makes its short command work. You sign in once in your browser; the agent gives you that one command. Once the skill is installed, short requests work too: \"add another Claude account, cc008\", \"what do my Codex accounts share?\", \"which account still has quota?\".\n\n## What it looks like\n\nThe 25-second story: one account buried in tasks, then accounts side by side sharing one setup, and a task moving on when one runs out.\n\nhttps://github.com/user-attachments/assets/3a470c03-9009-4f1d-b7f6-8b4625d74958\n\n<img alt=\"Two terminals: the personal account and the work account running at the same time, both linked to the main account's skills and settings, then the local quota panel, and at 95% Auto Handoff moving work's session to the next account\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/demo.gif\" width=\"1280\">\n\n<picture>\n <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel-dark.png\">\n <source media=\"(prefers-color-scheme: light)\" srcset=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel.png\">\n <img alt=\"sideby panel: every account's 5h and 7d quota, 7-day token usage and health in one page\" src=\"https://raw.githubusercontent.com/Adonis0123/sideby/main/docs/assets/panel.png\" width=\"1280\">\n</picture>\n\n## Why not just an alias?\n\n`CLAUDE_CONFIG_DIR` plus a shell alias already starts a second account. It does not keep skills and settings in one place, and it does not show quota for every account. [Does this break a provider's terms?](docs/guide/faq.md#terms)\n\n| | At the same time | Skills, hooks, settings | Quota and usage | Setup |\n|---|---|---|---|---|\n| `CLAUDE_CONFIG_DIR` and an alias | Yes. Each terminal exports the variable. | Each directory is its own copy. They drift unless you symlink by hand. | That session's status line | One alias per account |\n| [cc-switch](https://github.com/farion1231/cc-switch) | Enabling a provider writes the host's live config, so open sessions share that switch. | Plugin settings are copied across providers as a separate step. | The active provider, from its quota endpoint | Install the desktop app |\n| sideby | Yes. `sideby run` selects the directory for that process and leaves your shell alone. | Linked to the main account. `sideby doctor --fix` repairs copies. Sign-in and sessions stay separate. | Every account, from local files. Claude needs `quota setup`. | `npm i -g sideby`, then `sideby new` and sign in |\n\ncc-switch's docs describe that switch: it writes the live settings file ([README](https://github.com/farion1231/cc-switch#readme)). [#1105](https://github.com/farion1231/cc-switch/issues/1105) and [#2908](https://github.com/farion1231/cc-switch/issues/2908) ask for several sessions at once. [#1106](https://github.com/farion1231/cc-switch/issues/1106) is closed; the maintainer says Claude Code's \"open terminal\" can start one provider without changing the global config.\n\n## What else it does\n\n| You want to | Run | Guide |\n|---|---|---|\n| Add an API key account (DeepSeek, a gateway, …) | `sideby new claude deepseek --api` | [API accounts](docs/guide/usage.md#api-accounts) |\n| Find and fix config drift between accounts | `sideby doctor --fix` | [Doctor](docs/guide/usage.md#doctor) |\n| See Claude quota (Codex needs no setup) | `sideby quota setup claude --yes` | [Claude quota](docs/guide/usage.md#claude-quota) |\n| Find the cheapest auto-compact size from your own sessions, and use it in one click | `sideby tune` (or the panel's Context tuning tab) | [Context tuning](docs/guide/usage.md#context-tuning) |\n| See each account's model and effort, and set them per account | `sideby model <account>` (or click the model in the panel) | [Model and effort](docs/guide/usage.md#model-and-effort) |\n| Stop Claude sessions from connecting to claude.ai by themselves (Remote Control), for every account at once | `sideby remote-control off` (or the panel's banner) | [Remote Control](docs/guide/usage.md#remote-control) |\n| Move on when an account hits its limit | `sideby next claude` | [Handoff](docs/guide/usage.md#when-an-account-hits-its-limit) |\n| Let a running task move to the next account by itself when quota runs low (off by default; see the risk) | `sideby handoff enable` (or the panel) | [Auto Handoff](docs/guide/usage.md#auto-handoff) |\n| Short commands such as `ccw` | `sideby alias add ccw claude:work` | [Shell functions](docs/guide/configuration.md#shell-functions) |\n| Reopen a session from Orca and the like in the account that holds it | `\"resumeRouting\": true` in config | [Resuming from other tools](docs/guide/configuration.md#resuming-from-other-tools) |\n| Open Claude Desktop or the Codex app as one account, several side by side (macOS) | `sideby desktop claude:work` (or the panel) | [Desktop apps](docs/guide/usage.md#desktop-apps) |\n| Keep several Grok Bot windows open, each signed in to its own account (macOS) | `sideby desktop grok-bot:002` (or the panel) | [Grok Bot windows](docs/guide/usage.md#grok-bot-windows) |\n| Open the panel from the Dock or app menu | `sideby app install` | [Panel app](docs/guide/panel.md#panel-app) |\n| Show the panel inside your own local page | `createPanelHandler()` | [Embed the panel](docs/guide/panel.md#embed-the-panel) |\n| Add a Host or a pre-launch check | a local plugin | [Plugins](docs/guide/plugins.md) |\n\n## Supported hosts\n\n| Host | Account directories | Sign-in | Quota | Usage (7 days) |\n|---|---|---|---|---|\n| Claude Code | `~/.claude`, `~/.claude-<name>` | `claude auth login` | ✓ after `quota setup claude` | ✓ |\n| Codex CLI | `~/.codex`, `~/.codex-<name>` | `codex login` | ✓ | ✓ |\n| Grok Build | `~/.grok`, `~/.grok-<name>` | `grok login` | no public source | no public source |\n| pi | `~/.pi/agent`, `~/.pi-<name>/agent` | `/login` inside pi | no public source | no public source |\n\nsideby starts the official binary on your `PATH` and never patches it. Tested versions and per-Host notes are in the [guide](docs/guide/usage.md#host-notes).\n\n## Platforms\n\nmacOS and Linux. CI runs the suite on `ubuntu-latest` and `macos-latest`, Node 22 and 24. **Windows is untested.** `sideby app` only installs a launcher on macOS and Linux. Linked items are symlinks, `sideby shell-init` prints bash and zsh functions, and the Claude quota wrapper runs `/bin/sh`.\n\n## Roadmap\n\n**Gemini CLI.** Gemini can point at another config directory with `GEMINI_CONFIG_DIR`. Older docs use `GEMINI_CLI_HOME`; current builds treat that name as deprecated, and exit on startup if it is set together with an exact directory override. A sideby family still needs a tested Gemini version, a login check that does not read tokens, and a list of which files to link or copy. That is more than a small patch, so it is not in this version.\n\n## Safe by design\n\n- It never edits a Host's global config to switch accounts. It moves a task to another account only when you turn on Auto Handoff, and never proxies requests or pools logins.\n- It never reads, prints or sends a credential value. Nothing about your accounts leaves your machine: the only request sideby makes on its own is a daily check of the npm registry for its newest version (off with `NO_UPDATE_NOTIFIER=1`; see [Updates](docs/guide/configuration.md#updates)).\n- `doctor --fix` never replaces a real file and never changes where a link points.\n\nDetails and the terms-of-service question are in the [FAQ](docs/guide/faq.md).\n\nUsage questions and ideas go in [Discussions](https://github.com/Adonis0123/sideby/discussions).\n\n## Docs\n\n- [Using sideby](docs/guide/usage.md): accounts, API accounts, Doctor, quota, Handoff, Host notes\n- [Commands](docs/guide/commands.md): every command, exit codes and `--json`\n- [Configuration](docs/guide/configuration.md): `config.json`, aliases, shell functions\n- [Panel](docs/guide/panel.md): the panel, panel app and embedding\n- [Plugins](docs/guide/plugins.md): write your own Family or launch hook\n- [FAQ](docs/guide/faq.md): terms of service, data locations, alternatives, uninstall\n- [Changelog](https://github.com/Adonis0123/sideby/releases)\n\n## For AI agents\n\n- [`llms.txt`](llms.txt): setup steps, commands, JSON contract and plugin API in one index. `sideby families --json` states what each Host's accounts share.\n- [`skills/sideby/SKILL.md`](skills/sideby/SKILL.md): one Agent Skill for seeing, repairing and creating accounts and for moving on when one hits its limit. Install with `npx skills add Adonis0123/sideby -g`; `sideby doctor` warns when the installed copy is for another sideby version.\n- [`AGENTS.md`](AGENTS.md): rules for agents working on this repository.\n\n## License\n\n[MIT](LICENSE)\n"
|
|
81
81
|
}
|
package/schemas/config.json
CHANGED
|
@@ -226,6 +226,21 @@
|
|
|
226
226
|
},
|
|
227
227
|
"additionalProperties": false
|
|
228
228
|
},
|
|
229
|
+
"update": {
|
|
230
|
+
"type": "object",
|
|
231
|
+
"properties": {
|
|
232
|
+
"check": {
|
|
233
|
+
"type": "boolean",
|
|
234
|
+
"description": "Ask the npm registry once a day whether a newer sideby exists, and say so after a command and in the Panel. On by default; `NO_UPDATE_NOTIFIER` or `SIDEBY_NO_UPDATE_NOTIFIER` also turn it off."
|
|
235
|
+
},
|
|
236
|
+
"auto": {
|
|
237
|
+
"type": "boolean",
|
|
238
|
+
"description": "Install a newer version by itself after a check, when no `sideby run` is running and the version is not a major one. Off by default."
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
"additionalProperties": false,
|
|
242
|
+
"description": "Update Check and Auto Update (ADR-0021)."
|
|
243
|
+
},
|
|
229
244
|
"shellInitFile": {
|
|
230
245
|
"type": "object",
|
|
231
246
|
"properties": {
|