sideby 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +111 -2
- package/README.zh-CN.md +112 -0
- package/dist/build.json +1 -0
- package/dist/src/cli/args.d.ts +13 -0
- package/dist/src/cli/args.js +38 -0
- package/dist/src/cli/commands.d.ts +50 -0
- package/dist/src/cli/commands.js +566 -0
- package/dist/src/cli/format.d.ts +16 -0
- package/dist/src/cli/format.js +80 -0
- package/dist/src/cli/json-schemas.d.ts +2 -0
- package/dist/src/cli/json-schemas.js +259 -0
- package/dist/src/cli/main.d.ts +2 -0
- package/dist/src/cli/main.js +276 -0
- package/dist/src/cli/schemas.d.ts +1 -0
- package/dist/src/cli/schemas.js +16 -0
- package/dist/src/core/account-name.d.ts +2 -0
- package/dist/src/core/account-name.js +2 -0
- package/dist/src/core/accounts.d.ts +20 -0
- package/dist/src/core/accounts.js +131 -0
- package/dist/src/core/config.d.ts +61 -0
- package/dist/src/core/config.js +184 -0
- package/dist/src/core/create.d.ts +30 -0
- package/dist/src/core/create.js +126 -0
- package/dist/src/core/doctor.d.ts +41 -0
- package/dist/src/core/doctor.js +171 -0
- package/dist/src/core/errors.d.ts +12 -0
- package/dist/src/core/errors.js +14 -0
- package/dist/src/core/file-memo.d.ts +24 -0
- package/dist/src/core/file-memo.js +41 -0
- package/dist/src/core/fs-safe.d.ts +38 -0
- package/dist/src/core/fs-safe.js +205 -0
- package/dist/src/core/handoff.d.ts +41 -0
- package/dist/src/core/handoff.js +54 -0
- package/dist/src/core/identity.d.ts +23 -0
- package/dist/src/core/identity.js +79 -0
- package/dist/src/core/last-doctor.d.ts +36 -0
- package/dist/src/core/last-doctor.js +69 -0
- package/dist/src/core/launch.d.ts +37 -0
- package/dist/src/core/launch.js +108 -0
- package/dist/src/core/login-path.d.ts +23 -0
- package/dist/src/core/login-path.js +96 -0
- package/dist/src/core/paths.d.ts +13 -0
- package/dist/src/core/paths.js +32 -0
- package/dist/src/core/quota-levels.d.ts +19 -0
- package/dist/src/core/quota-levels.js +34 -0
- package/dist/src/core/resume.d.ts +9 -0
- package/dist/src/core/resume.js +15 -0
- package/dist/src/core/secret-file.d.ts +30 -0
- package/dist/src/core/secret-file.js +116 -0
- package/dist/src/core/share-meaning.d.ts +6 -0
- package/dist/src/core/share-meaning.js +32 -0
- package/dist/src/core/share-mode-names.d.ts +1 -0
- package/dist/src/core/share-mode-names.js +10 -0
- package/dist/src/core/share-modes.d.ts +24 -0
- package/dist/src/core/share-modes.js +405 -0
- package/dist/src/core/shell-init.d.ts +31 -0
- package/dist/src/core/shell-init.js +84 -0
- package/dist/src/core/skill-check.d.ts +11 -0
- package/dist/src/core/skill-check.js +57 -0
- package/dist/src/core/usage-days.d.ts +15 -0
- package/dist/src/core/usage-days.js +33 -0
- package/dist/src/core/version.d.ts +9 -0
- package/dist/src/core/version.js +46 -0
- package/dist/src/desktop/app.d.ts +60 -0
- package/dist/src/desktop/app.js +316 -0
- package/dist/src/desktop/icon.d.ts +16 -0
- package/dist/src/desktop/icon.js +174 -0
- package/dist/src/families/claude/index.d.ts +10 -0
- package/dist/src/families/claude/index.js +127 -0
- package/dist/src/families/claude/layout.d.ts +6 -0
- package/dist/src/families/claude/layout.js +5 -0
- package/dist/src/families/claude/quota-cache.d.ts +31 -0
- package/dist/src/families/claude/quota-cache.js +111 -0
- package/dist/src/families/claude/quota.d.ts +3 -0
- package/dist/src/families/claude/quota.js +53 -0
- package/dist/src/families/claude/usage.d.ts +22 -0
- package/dist/src/families/claude/usage.js +146 -0
- package/dist/src/families/codex/index.d.ts +5 -0
- package/dist/src/families/codex/index.js +70 -0
- package/dist/src/families/codex/rollout.d.ts +67 -0
- package/dist/src/families/codex/rollout.js +298 -0
- package/dist/src/families/grok/index.d.ts +8 -0
- package/dist/src/families/grok/index.js +82 -0
- package/dist/src/families/index.d.ts +3 -0
- package/dist/src/families/index.js +40 -0
- package/dist/src/families/logos.d.ts +21 -0
- package/dist/src/families/logos.js +26 -0
- package/dist/src/families/pi/index.d.ts +3 -0
- package/dist/src/families/pi/index.js +54 -0
- package/dist/src/families/shared/sessions.d.ts +9 -0
- package/dist/src/families/shared/sessions.js +70 -0
- package/dist/src/families/shared/toml.d.ts +5 -0
- package/dist/src/families/shared/toml.js +16 -0
- package/dist/src/index.d.ts +5 -0
- package/dist/src/index.js +4 -0
- package/dist/src/panel/background.d.ts +62 -0
- package/dist/src/panel/background.js +274 -0
- package/dist/src/panel/handler.d.ts +91 -0
- package/dist/src/panel/handler.js +396 -0
- package/dist/src/panel/i18n.d.ts +287 -0
- package/dist/src/panel/i18n.js +570 -0
- package/dist/src/panel/page-logic.d.ts +154 -0
- package/dist/src/panel/page-logic.js +326 -0
- package/dist/src/panel/page-script.d.ts +2 -0
- package/dist/src/panel/page-script.js +1633 -0
- package/dist/src/panel/page-styles.d.ts +7 -0
- package/dist/src/panel/page-styles.js +514 -0
- package/dist/src/panel/page.d.ts +20 -0
- package/dist/src/panel/page.js +96 -0
- package/dist/src/panel/server.d.ts +32 -0
- package/dist/src/panel/server.js +133 -0
- package/dist/src/panel/theme.d.ts +116 -0
- package/dist/src/panel/theme.js +132 -0
- package/dist/src/plugins/account-script.d.ts +4 -0
- package/dist/src/plugins/account-script.js +66 -0
- package/dist/src/plugins/bus.d.ts +33 -0
- package/dist/src/plugins/bus.js +84 -0
- package/dist/src/plugins/loader.d.ts +58 -0
- package/dist/src/plugins/loader.js +250 -0
- package/dist/src/quota/claude-statusline.d.ts +28 -0
- package/dist/src/quota/claude-statusline.js +301 -0
- package/dist/src/quota/statusline-tap.d.ts +11 -0
- package/dist/src/quota/statusline-tap.js +104 -0
- package/dist/src/runtime.d.ts +195 -0
- package/dist/src/runtime.js +424 -0
- package/dist/src/types.d.ts +285 -0
- package/dist/src/types.js +3 -0
- package/dist/testing/demo.d.ts +8 -0
- package/dist/testing/demo.js +50 -0
- package/dist/testing/index.d.ts +36 -0
- package/dist/testing/index.js +118 -0
- package/package.json +66 -4
- package/schemas/alias.json +102 -0
- package/schemas/app-install.json +74 -0
- package/schemas/app-uninstall.json +54 -0
- package/schemas/config.json +170 -0
- package/schemas/doctor.json +229 -0
- package/schemas/error.json +25 -0
- package/schemas/families.json +205 -0
- package/schemas/list.json +177 -0
- package/schemas/new.json +199 -0
- package/schemas/next.json +127 -0
- package/schemas/plugins.json +74 -0
- package/schemas/quota-setup.json +60 -0
- package/schemas/quota-teardown.json +30 -0
- package/schemas/quota.json +203 -0
- package/skills/sideby/SKILL.md +61 -0
- package/skills/sideby/references/accounts.md +31 -0
- package/skills/sideby/references/diagnose.md +47 -0
- package/skills/sideby/references/handoff.md +41 -0
- package/skills/sideby/references/quota.md +14 -0
- package/skills/sideby/references/setup.md +31 -0
- package/skills/sideby/references/sharing.md +17 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// JSON Schemas for every `--json` output. Additive changes keep schemaVersion 1; removals, renames or
|
|
2
|
+
// meaning changes bump it (spec §3.9). `pnpm schemas` writes these to schemas/*.json.
|
|
3
|
+
import { Type } from 'typebox';
|
|
4
|
+
import { ConfigSchema } from '../core/config.js';
|
|
5
|
+
const Str = Type.String();
|
|
6
|
+
const Level = Type.Union([Type.Literal('ok'), Type.Literal('warn'), Type.Literal('fail')]);
|
|
7
|
+
const Kind = Type.Union([Type.Literal('subscription'), Type.Literal('api')]);
|
|
8
|
+
const Version = Type.Literal(1);
|
|
9
|
+
const AppPlatform = Type.Union([Type.Literal('darwin'), Type.Literal('linux')]);
|
|
10
|
+
const Account = Type.Object({
|
|
11
|
+
family: Str,
|
|
12
|
+
name: Str,
|
|
13
|
+
ref: Str,
|
|
14
|
+
dir: Str,
|
|
15
|
+
isMain: Type.Boolean(),
|
|
16
|
+
kind: Kind,
|
|
17
|
+
secretFile: Type.Optional(Str),
|
|
18
|
+
});
|
|
19
|
+
const AccountStatus = Type.Intersect([
|
|
20
|
+
Account,
|
|
21
|
+
Type.Object({
|
|
22
|
+
familyTitle: Str,
|
|
23
|
+
hostInstalled: Type.Boolean(),
|
|
24
|
+
login: Type.Union([
|
|
25
|
+
Type.Literal('logged-in'),
|
|
26
|
+
Type.Literal('logged-out'),
|
|
27
|
+
Type.Literal('unknown'),
|
|
28
|
+
Type.Literal('not-needed'),
|
|
29
|
+
]),
|
|
30
|
+
model: Type.Optional(Str),
|
|
31
|
+
identity: Type.Optional(Type.Object({ email: Type.Optional(Str), org: Type.Optional(Str) }, {
|
|
32
|
+
description: "Who the account is signed in as, from the host's login file: identity fields only, never a token (ADR-0003).",
|
|
33
|
+
})),
|
|
34
|
+
problems: Type.Optional(Type.Array(Str)),
|
|
35
|
+
}),
|
|
36
|
+
]);
|
|
37
|
+
const FamilyInfo = Type.Object({
|
|
38
|
+
id: Str,
|
|
39
|
+
title: Str,
|
|
40
|
+
bin: Str,
|
|
41
|
+
installUrl: Str,
|
|
42
|
+
installed: Type.Boolean(),
|
|
43
|
+
plugin: Str,
|
|
44
|
+
});
|
|
45
|
+
const ShareModeSchema = Type.Union(['link', 'copy', 'link-or-copy', 'link-or-local', 'local', 'local-if-api', 'info', 'json-key'].map((m) => Type.Literal(m)));
|
|
46
|
+
const FamilyDetail = Type.Intersect([
|
|
47
|
+
FamilyInfo,
|
|
48
|
+
Type.Object({
|
|
49
|
+
layout: Type.Object({ main: Str, account: Str }, { description: 'Relative to HOME, for example `.claude` and `.claude-<name>`.' }),
|
|
50
|
+
selectVar: Type.String({
|
|
51
|
+
description: 'Environment variable that selects a non-main Account directory.',
|
|
52
|
+
}),
|
|
53
|
+
login: Type.Object({
|
|
54
|
+
args: Type.Union([Type.Array(Str), Type.Null()], {
|
|
55
|
+
description: "Arguments of the Host's sign-in command; null when sign-in happens inside the Host.",
|
|
56
|
+
}),
|
|
57
|
+
hint: Str,
|
|
58
|
+
}),
|
|
59
|
+
quota: Type.Boolean(),
|
|
60
|
+
usage: Type.Boolean(),
|
|
61
|
+
quotaSetup: Type.Boolean({ description: 'Quota needs `sideby quota setup <family>` once.' }),
|
|
62
|
+
shared: Type.Array(Type.Object({
|
|
63
|
+
path: Str,
|
|
64
|
+
mode: ShareModeSchema,
|
|
65
|
+
key: Type.Optional(Str),
|
|
66
|
+
mainPath: Type.Optional(Str),
|
|
67
|
+
credential: Type.Optional(Type.Boolean()),
|
|
68
|
+
noSymlink: Type.Optional(Type.Boolean()),
|
|
69
|
+
source: Type.Union([Type.Literal('family'), Type.Literal('config')]),
|
|
70
|
+
meaning: Type.String({
|
|
71
|
+
description: 'What the item looks like in each Account, in one fixed sentence.',
|
|
72
|
+
}),
|
|
73
|
+
})),
|
|
74
|
+
}),
|
|
75
|
+
]);
|
|
76
|
+
const PluginError = Type.Object({ where: Str, message: Str });
|
|
77
|
+
const Finding = Type.Object({
|
|
78
|
+
level: Level,
|
|
79
|
+
account: Str,
|
|
80
|
+
item: Str,
|
|
81
|
+
code: Str,
|
|
82
|
+
message: Str,
|
|
83
|
+
hint: Type.Optional(Str),
|
|
84
|
+
source: Str,
|
|
85
|
+
fixable: Type.Boolean(),
|
|
86
|
+
});
|
|
87
|
+
const QuotaWindow = Type.Object({
|
|
88
|
+
label: Str,
|
|
89
|
+
windowMinutes: Type.Number(),
|
|
90
|
+
usedPercent: Type.Number(),
|
|
91
|
+
resetsAt: Str,
|
|
92
|
+
});
|
|
93
|
+
const Quota = Type.Union([
|
|
94
|
+
Type.Object({
|
|
95
|
+
status: Type.Literal('ok'),
|
|
96
|
+
windows: Type.Array(QuotaWindow),
|
|
97
|
+
observedAt: Str,
|
|
98
|
+
source: Str,
|
|
99
|
+
plan: Type.Optional(Str),
|
|
100
|
+
}),
|
|
101
|
+
Type.Object({ status: Type.Literal('unavailable'), reason: Str, detail: Type.Optional(Str) }),
|
|
102
|
+
]);
|
|
103
|
+
const Usage = Type.Union([
|
|
104
|
+
Type.Object({
|
|
105
|
+
status: Type.Literal('ok'),
|
|
106
|
+
days: Type.Number(),
|
|
107
|
+
sessions: Type.Number(),
|
|
108
|
+
inputTokens: Type.Number(),
|
|
109
|
+
outputTokens: Type.Number(),
|
|
110
|
+
cacheReadTokens: Type.Number(),
|
|
111
|
+
cacheWriteTokens: Type.Number(),
|
|
112
|
+
totalTokens: Type.Number(),
|
|
113
|
+
daily: Type.Optional(Type.Array(Type.Object({ date: Str, totalTokens: Type.Number() }), {
|
|
114
|
+
description: 'Tokens per local day (YYYY-MM-DD), oldest first, days without records as 0.',
|
|
115
|
+
})),
|
|
116
|
+
lastActivityAt: Type.Optional(Str),
|
|
117
|
+
}),
|
|
118
|
+
Type.Object({ status: Type.Literal('unavailable'), reason: Str, detail: Type.Optional(Str) }),
|
|
119
|
+
]);
|
|
120
|
+
const QuotaSetupPlan = Type.Object({
|
|
121
|
+
status: Type.Union([Type.Literal('enabled'), Type.Literal('ready'), Type.Literal('blocked')]),
|
|
122
|
+
file: Str,
|
|
123
|
+
diff: Str,
|
|
124
|
+
message: Str,
|
|
125
|
+
});
|
|
126
|
+
const ShellInitFiles = Type.Array(Type.Object({
|
|
127
|
+
shell: Type.Union([Type.Literal('zsh'), Type.Literal('bash')]),
|
|
128
|
+
path: Str,
|
|
129
|
+
ok: Type.Boolean(),
|
|
130
|
+
action: Type.Optional(Type.Union([Type.Literal('written'), Type.Literal('unchanged')])),
|
|
131
|
+
message: Type.Optional(Str),
|
|
132
|
+
}), { description: 'One entry per config `shellInitFile`, rewritten after the change.' });
|
|
133
|
+
export const OUTPUT_SCHEMAS = {
|
|
134
|
+
list: Type.Object({
|
|
135
|
+
schemaVersion: Version,
|
|
136
|
+
families: Type.Array(FamilyInfo),
|
|
137
|
+
accounts: Type.Array(AccountStatus),
|
|
138
|
+
pluginErrors: Type.Array(PluginError),
|
|
139
|
+
}, { title: 'sideby list --json' }),
|
|
140
|
+
doctor: Type.Object({
|
|
141
|
+
schemaVersion: Version,
|
|
142
|
+
status: Type.Union([Type.Literal('ok'), Type.Literal('issues')]),
|
|
143
|
+
accounts: Type.Array(Type.Object({
|
|
144
|
+
ref: Str,
|
|
145
|
+
family: Str,
|
|
146
|
+
name: Str,
|
|
147
|
+
kind: Kind,
|
|
148
|
+
dir: Str,
|
|
149
|
+
shared: Type.Object({ ok: Type.Number(), total: Type.Number() }),
|
|
150
|
+
backups: Type.Number(),
|
|
151
|
+
findings: Type.Array(Finding),
|
|
152
|
+
})),
|
|
153
|
+
general: Type.Array(Finding),
|
|
154
|
+
fixes: Type.Array(Type.Object({ account: Str, item: Str, code: Str, ok: Type.Boolean(), message: Str })),
|
|
155
|
+
}, { title: 'sideby doctor --json' }),
|
|
156
|
+
quota: Type.Object({
|
|
157
|
+
schemaVersion: Version,
|
|
158
|
+
accounts: Type.Array(Type.Object({ ref: Str, kind: Kind, quota: Quota, usage: Usage })),
|
|
159
|
+
}, { title: 'sideby quota --json' }),
|
|
160
|
+
next: Type.Object({
|
|
161
|
+
schemaVersion: Version,
|
|
162
|
+
family: Str,
|
|
163
|
+
pick: Type.Union([Str, Type.Null()], {
|
|
164
|
+
description: 'The recommended account; null when none can be used now (exit code 1).',
|
|
165
|
+
}),
|
|
166
|
+
accounts: Type.Array(Type.Object({
|
|
167
|
+
ref: Str,
|
|
168
|
+
kind: Kind,
|
|
169
|
+
state: Type.Union([
|
|
170
|
+
Type.Literal('ready'),
|
|
171
|
+
Type.Literal('unknown'),
|
|
172
|
+
Type.Literal('api'),
|
|
173
|
+
Type.Literal('full'),
|
|
174
|
+
Type.Literal('logged-out'),
|
|
175
|
+
], {
|
|
176
|
+
description: 'ready and unknown can be picked; api only with --include-api; full and logged-out never.',
|
|
177
|
+
}),
|
|
178
|
+
pressure: Type.Union([Type.Number(), Type.Null()], {
|
|
179
|
+
description: 'Used percent of the fullest window not reset yet; null without quota.',
|
|
180
|
+
}),
|
|
181
|
+
resetsAt: Type.Optional(Type.String({ description: 'For full: when its last full window resets.' })),
|
|
182
|
+
observedAt: Type.Optional(Type.String({ description: "When the account's quota was recorded (its last session)." })),
|
|
183
|
+
}), { description: 'Every account of the family, best first.' }),
|
|
184
|
+
hasQuota: Type.Boolean({
|
|
185
|
+
description: 'False when no account has quota data: the pick is then a guess, and `sideby next` does not start it (exit 1).',
|
|
186
|
+
}),
|
|
187
|
+
earliestReset: Type.Optional(Type.Object({ ref: Str, at: Str }, {
|
|
188
|
+
description: 'Set when nothing can be picked and some accounts are full: the first to come back.',
|
|
189
|
+
})),
|
|
190
|
+
}, { title: 'sideby next --json' }),
|
|
191
|
+
'quota-setup': Type.Object({ schemaVersion: Version, family: Str, applied: Type.Boolean(), plan: QuotaSetupPlan }, { title: 'sideby quota setup --json' }),
|
|
192
|
+
'quota-teardown': Type.Object({ schemaVersion: Version, family: Str, ok: Type.Boolean(), message: Str, diff: Type.Optional(Str) }, { title: 'sideby quota teardown --json' }),
|
|
193
|
+
new: Type.Object({
|
|
194
|
+
schemaVersion: Version,
|
|
195
|
+
ok: Type.Boolean(),
|
|
196
|
+
account: Account,
|
|
197
|
+
steps: Type.Array(Type.Object({ item: Str, action: Str, ok: Type.Boolean(), message: Type.Optional(Str) })),
|
|
198
|
+
hookErrors: Type.Array(Type.Object({ plugin: Str, message: Str })),
|
|
199
|
+
nextSteps: Type.Array(Str),
|
|
200
|
+
alias: Type.Optional(Type.Object({ name: Str, added: Type.Boolean(), message: Type.Optional(Str) }, { description: 'The `--alias` short command; `added: false` means the Account exists without it.' })),
|
|
201
|
+
shellInitFiles: Type.Optional(ShellInitFiles),
|
|
202
|
+
suggestion: Type.Optional(Type.Object({ name: Str, alias: Type.Optional(Str), aliasProblem: Type.Optional(Str) }, {
|
|
203
|
+
description: 'Only with `--next`: the picked name, the short command that follows the existing pattern, or why that short command was left out.',
|
|
204
|
+
})),
|
|
205
|
+
}, { title: 'sideby new --json' }),
|
|
206
|
+
alias: Type.Object({
|
|
207
|
+
schemaVersion: Version,
|
|
208
|
+
ok: Type.Boolean({ description: 'False when a config `shellInitFile` was not rewritten.' }),
|
|
209
|
+
alias: Str,
|
|
210
|
+
status: Type.Union([
|
|
211
|
+
Type.Literal('added'),
|
|
212
|
+
Type.Literal('exists'),
|
|
213
|
+
Type.Literal('removed'),
|
|
214
|
+
Type.Literal('absent'),
|
|
215
|
+
]),
|
|
216
|
+
account: Type.Optional(Str),
|
|
217
|
+
args: Type.Optional(Type.Array(Str, { description: 'Host arguments `sideby run <alias>` adds.' })),
|
|
218
|
+
shellInitFiles: Type.Optional(ShellInitFiles),
|
|
219
|
+
}, { title: 'sideby alias add|rm --json' }),
|
|
220
|
+
plugins: Type.Object({
|
|
221
|
+
schemaVersion: Version,
|
|
222
|
+
plugins: Type.Array(Type.Object({
|
|
223
|
+
name: Str,
|
|
224
|
+
version: Str,
|
|
225
|
+
source: Str,
|
|
226
|
+
description: Type.Optional(Str),
|
|
227
|
+
families: Type.Array(Str),
|
|
228
|
+
configKeys: Type.Array(Str),
|
|
229
|
+
})),
|
|
230
|
+
errors: Type.Array(PluginError),
|
|
231
|
+
}, { title: 'sideby plugins --json' }),
|
|
232
|
+
families: Type.Object({ schemaVersion: Version, families: Type.Array(FamilyDetail) }, { title: 'sideby families --json' }),
|
|
233
|
+
'app-install': Type.Object({
|
|
234
|
+
schemaVersion: Version,
|
|
235
|
+
platform: AppPlatform,
|
|
236
|
+
path: Str,
|
|
237
|
+
files: Type.Array(Str),
|
|
238
|
+
mode: Type.Union([Type.Literal('panel'), Type.Literal('url')]),
|
|
239
|
+
url: Type.Optional(Str),
|
|
240
|
+
node: Str,
|
|
241
|
+
entry: Str,
|
|
242
|
+
updated: Type.Boolean(),
|
|
243
|
+
hints: Type.Array(Str),
|
|
244
|
+
}, { title: 'sideby app install --json' }),
|
|
245
|
+
'app-uninstall': Type.Object({
|
|
246
|
+
schemaVersion: Version,
|
|
247
|
+
platform: AppPlatform,
|
|
248
|
+
removed: Type.Array(Str),
|
|
249
|
+
skipped: Type.Array(Type.Object({ path: Str, reason: Str })),
|
|
250
|
+
}, { title: 'sideby app uninstall --json' }),
|
|
251
|
+
error: Type.Object({
|
|
252
|
+
schemaVersion: Version,
|
|
253
|
+
error: Type.String({ description: 'What went wrong and what to do next.' }),
|
|
254
|
+
code: Type.String({
|
|
255
|
+
description: '`usage` for a malformed command line (exit 2), a reason such as `no-quota-source` or `alias-invalid`, or `error`.',
|
|
256
|
+
}),
|
|
257
|
+
}, { title: 'sideby error --json' }),
|
|
258
|
+
config: ConfigSchema,
|
|
259
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Entry point. Keeps imports lazy so `statusline-tap`, which Claude Code runs on every refresh, starts fast.
|
|
3
|
+
import { realpathSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
+
const MIN_NODE = [22, 18];
|
|
6
|
+
const HELP = `sideby — run every AI coding account side by side
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
sideby list accounts (same as \`sideby list\`)
|
|
10
|
+
sideby run <account> [-- args] start the host CLI for one account
|
|
11
|
+
sideby new <family> <name>|--next [--api] [--alias <short>]
|
|
12
|
+
create an account (families: claude, codex, grok, pi);
|
|
13
|
+
--next picks the next name and short command (cc008);
|
|
14
|
+
--alias also adds a short command such as cc008
|
|
15
|
+
sideby families [family] what each host's accounts share with the main account,
|
|
16
|
+
where they live and how they sign in
|
|
17
|
+
sideby alias add <short> <account> [-- args]
|
|
18
|
+
add a short command, optionally with host arguments
|
|
19
|
+
(sideby alias add pi-kimi pi:main -- --model kimi-coding/k3)
|
|
20
|
+
sideby alias rm <short> remove a short command
|
|
21
|
+
sideby login <account> sign in to an account
|
|
22
|
+
sideby resume <family> [-- args] run the host; a session resumed by id (claude --resume <id>,
|
|
23
|
+
codex resume <id>) starts in the account that holds it
|
|
24
|
+
sideby next <family> [--dry-run] [--include-api] [-- args]
|
|
25
|
+
recommend the account with the most quota left (claude, codex)
|
|
26
|
+
and start it; --dry-run only recommends
|
|
27
|
+
sideby doctor [target] [--fix] [--force]
|
|
28
|
+
check shared items and permissions; --fix repairs safe issues
|
|
29
|
+
sideby quota [account] quota and 7-day token usage
|
|
30
|
+
sideby quota setup claude [--yes] turn on Claude quota (shows the change first)
|
|
31
|
+
sideby quota teardown claude undo it, restoring the original file
|
|
32
|
+
sideby ui [--port n] [--no-open] open the local panel
|
|
33
|
+
sideby ui --background same, without a terminal: keeps running after this command exits
|
|
34
|
+
sideby ui --stop stop the background panel
|
|
35
|
+
sideby app install [--url <url>] add a sideby app that opens the panel (macOS, Linux)
|
|
36
|
+
sideby app uninstall remove that app
|
|
37
|
+
sideby shell-init zsh|bash print shell functions, e.g. eval "$(sideby shell-init zsh)"
|
|
38
|
+
sideby shell-init [zsh|bash] --write
|
|
39
|
+
rewrite the config's shellInitFile with them
|
|
40
|
+
sideby plugins list loaded plugins and load errors
|
|
41
|
+
|
|
42
|
+
Options:
|
|
43
|
+
--json machine-readable output (list, new, next, alias, doctor, quota, families, plugins, app)
|
|
44
|
+
-h, --help show this help; \`sideby <command> --help\` for new, alias, login, doctor, families
|
|
45
|
+
-v, --version show the version
|
|
46
|
+
|
|
47
|
+
An <account> is <family>:<name> (claude:work) or just <name> when it is unique.
|
|
48
|
+
|
|
49
|
+
For AI agents:
|
|
50
|
+
Add --json and read the result; JSON Schemas ship in the package under schemas/.
|
|
51
|
+
Install the skill: npx skills add Adonis0123/sideby -g
|
|
52
|
+
Setup steps and the full contract: https://github.com/Adonis0123/sideby/blob/main/llms.txt
|
|
53
|
+
Run everything else yourself, but hand the user these as one line each: \`sideby login <account>\`
|
|
54
|
+
(browser sign-in), filling in proxy.env, and \`sideby run\` or \`sideby next\` (interactive sessions).
|
|
55
|
+
Exit 10 means a change was shown: ask the user, then run it again with --yes.`;
|
|
56
|
+
/** `sideby <command> --help` for the commands agents use to set up and repair accounts (spec §3.16). */
|
|
57
|
+
const COMMAND_HELP = {
|
|
58
|
+
new: `sideby new <family> <name> [--api] [--alias <short>] [--json]
|
|
59
|
+
sideby new <family> --next [--api] [--alias <short>] [--json]
|
|
60
|
+
|
|
61
|
+
Create an account directory (~/.claude-<name>, ~/.codex-<name>, ~/.grok-<name>, ~/.pi-<name>/agent) and link or
|
|
62
|
+
copy the main account's shared items into it. \`sideby families <family>\` lists them.
|
|
63
|
+
|
|
64
|
+
--next pick the name: the next number after the highest numbered account (001…007 → 008), else work;
|
|
65
|
+
without --alias, also add a short command that follows your existing ones (cc007 → cc008)
|
|
66
|
+
--api API account: also writes an empty proxy.env template (mode 600) for the user to fill in
|
|
67
|
+
--alias <short> add a short command for it to the config (it becomes a shell function)
|
|
68
|
+
|
|
69
|
+
Examples:
|
|
70
|
+
sideby new claude --next --json # ~/.claude-008 plus cc008 when cc001…cc007 exist
|
|
71
|
+
sideby new claude work # ~/.claude-work
|
|
72
|
+
sideby new claude deepseek --api # then the user fills in ~/.claude-deepseek/proxy.env
|
|
73
|
+
|
|
74
|
+
Writes: the account directory; the config \`aliases\` and every \`shellInitFile\` when a short command is added.
|
|
75
|
+
Next: the user signs in with \`sideby login <account>\` (subscription) or fills in proxy.env (API).
|
|
76
|
+
Exit: 0 done; 1 invalid name or alias, directory exists, or partly failed; 2 malformed command line.`,
|
|
77
|
+
alias: `sideby alias add <short> <account> [-- host args] [--json]
|
|
78
|
+
sideby alias rm <short> [--json]
|
|
79
|
+
|
|
80
|
+
A short command starts one account: \`sideby run <short>\`, or just \`<short>\` once your shell loads
|
|
81
|
+
\`sideby shell-init\`. Host arguments after -- are added only when it starts through the short command; it is
|
|
82
|
+
the same account (same sign-in, same sessions), so do not create an account per model.
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
sideby alias add cc008 claude:008
|
|
86
|
+
sideby alias add pi-kimi pi:main -- --model kimi-coding/k3
|
|
87
|
+
sideby alias rm cc008
|
|
88
|
+
|
|
89
|
+
Writes: the config \`aliases\` and every \`shellInitFile\`. New shell functions work in a new shell, or after
|
|
90
|
+
eval "$(sideby shell-init zsh)" (put that line in ~/.zshrc once).
|
|
91
|
+
Exit: 0 done (also when it already exists or is absent); 1 invalid, reserved or taken; 2 malformed.`,
|
|
92
|
+
login: `sideby login <account>
|
|
93
|
+
|
|
94
|
+
Run the host's own sign-in for one account: claude auth login, codex login, grok login. For pi it starts pi;
|
|
95
|
+
then type /login. It opens a browser and needs the person who owns the account, so an agent hands this
|
|
96
|
+
line to the user instead of running it (in Claude Code: ! sideby login claude:008).
|
|
97
|
+
|
|
98
|
+
sideby never reads, copies or stores the credentials the host writes.
|
|
99
|
+
Exit: the host's exit code.`,
|
|
100
|
+
doctor: `sideby doctor [account|family] [--fix] [--force] [--json]
|
|
101
|
+
|
|
102
|
+
Check every account's shared items against the main account, credential file modes (600), backup leftovers,
|
|
103
|
+
and whether the installed sideby skill matches this version.
|
|
104
|
+
|
|
105
|
+
--fix repair what is safe: add missing links, refresh copies, chmod 600, sync MCP servers
|
|
106
|
+
--force with --fix, also let a synced MCP list (json-key) drop entries the account has
|
|
107
|
+
|
|
108
|
+
--fix never replaces a real file with a link and never changes where an existing link points; those are
|
|
109
|
+
reported with the command to run.
|
|
110
|
+
|
|
111
|
+
Examples:
|
|
112
|
+
sideby doctor --json
|
|
113
|
+
sideby doctor claude:008 --fix
|
|
114
|
+
Exit: 0 when nothing fails (warnings allowed); 1 when a check fails.`,
|
|
115
|
+
families: `sideby families [family] [--json]
|
|
116
|
+
|
|
117
|
+
For each host (built in or from a plugin): where its accounts live, the variable that selects one, how to sign
|
|
118
|
+
in, whether quota and usage can be read, and every shared item with what it looks like in each account
|
|
119
|
+
(a link to the main account's, a copy, or the account's own file). Everything else in an account directory
|
|
120
|
+
(sign-in, sessions, history) belongs to that account.
|
|
121
|
+
|
|
122
|
+
Examples:
|
|
123
|
+
sideby families
|
|
124
|
+
sideby families claude --json
|
|
125
|
+
Exit: 0; 1 unknown family; 2 malformed command line.`,
|
|
126
|
+
};
|
|
127
|
+
function nodeTooOld() {
|
|
128
|
+
const [maj = 0, min = 0] = process.versions.node.split('.').map(Number);
|
|
129
|
+
return maj < MIN_NODE[0] || (maj === MIN_NODE[0] && min < MIN_NODE[1]);
|
|
130
|
+
}
|
|
131
|
+
const COMMANDS = {
|
|
132
|
+
list: { flags: ['json'] },
|
|
133
|
+
run: { flags: [] },
|
|
134
|
+
resume: { flags: [] },
|
|
135
|
+
login: { flags: [] },
|
|
136
|
+
next: { flags: ['dry-run', 'include-api', 'json'] },
|
|
137
|
+
new: { flags: ['api', 'json', 'next'], valued: ['alias'] },
|
|
138
|
+
alias: { flags: ['json'] },
|
|
139
|
+
doctor: { flags: ['fix', 'force', 'json'] },
|
|
140
|
+
quota: { flags: ['json', 'yes'] },
|
|
141
|
+
ui: { flags: ['no-open', 'background', 'stop', 'serve-detached'], valued: ['port'] },
|
|
142
|
+
app: { flags: ['json'], valued: ['url'] },
|
|
143
|
+
'shell-init': { flags: ['write'] },
|
|
144
|
+
plugins: { flags: ['json'] },
|
|
145
|
+
families: { flags: ['json'] },
|
|
146
|
+
};
|
|
147
|
+
export async function main(argv) {
|
|
148
|
+
const [first, ...rest] = argv;
|
|
149
|
+
if (first === 'statusline-tap') {
|
|
150
|
+
const { runStatuslineTap } = await import('../quota/statusline-tap.js');
|
|
151
|
+
return runStatuslineTap(rest, process.env, process.stdin, process.stdout);
|
|
152
|
+
}
|
|
153
|
+
if (first === '-v' || first === '--version' || first === 'version') {
|
|
154
|
+
const { packageVersion } = await import('../core/version.js');
|
|
155
|
+
console.log(packageVersion());
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
if (first === '-h' || first === '--help' || first === 'help') {
|
|
159
|
+
console.log((rest[0] !== undefined && first === 'help' && COMMAND_HELP[rest[0]]) || HELP);
|
|
160
|
+
return 0;
|
|
161
|
+
}
|
|
162
|
+
if (nodeTooOld()) {
|
|
163
|
+
console.error(`sideby needs Node ${MIN_NODE.join('.')} or newer (you have ${process.versions.node}). Upgrade: https://nodejs.org`);
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
166
|
+
const cmd = first === undefined || first.startsWith('--') ? 'list' : first;
|
|
167
|
+
const args = first === undefined || first.startsWith('--') ? argv : rest;
|
|
168
|
+
const spec = COMMANDS[cmd];
|
|
169
|
+
const io = { out: (s) => console.log(s), err: (s) => console.error(s) };
|
|
170
|
+
const { parseArgs, UsageError } = await import('./args.js');
|
|
171
|
+
// Only sideby's own options count: arguments after `--` belong to the host.
|
|
172
|
+
const own = args.includes('--') ? args.slice(0, args.indexOf('--')) : args;
|
|
173
|
+
const wantsJson = Boolean(spec?.flags.includes('json')) && own.includes('--json');
|
|
174
|
+
try {
|
|
175
|
+
if (!spec)
|
|
176
|
+
throw new UsageError(`unknown command "${cmd}"; run \`sideby --help\``);
|
|
177
|
+
// `run` and `resume` pass everything after the account or family through to the host, with or without `--`.
|
|
178
|
+
let parsed;
|
|
179
|
+
if (cmd === 'run' || cmd === 'resume') {
|
|
180
|
+
const sep = args.indexOf('--');
|
|
181
|
+
const head = sep === -1 ? args : args.slice(0, sep);
|
|
182
|
+
const [ref, ...more] = head;
|
|
183
|
+
parsed = {
|
|
184
|
+
positionals: ref ? [ref] : [],
|
|
185
|
+
flags: new Map(),
|
|
186
|
+
rest: [...more, ...(sep === -1 ? [] : args.slice(sep + 1))],
|
|
187
|
+
};
|
|
188
|
+
if (ref === '-h' || ref === '--help')
|
|
189
|
+
parsed.flags.set('help', true);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
try {
|
|
193
|
+
parsed = parseArgs(args, [...spec.flags, 'help'], spec.valued ?? []);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
// `alias add` takes Host arguments too; the usual mistake is leaving out the `--` before them.
|
|
197
|
+
if (cmd === 'alias' && err instanceof UsageError && err.message.startsWith('unknown option'))
|
|
198
|
+
throw new UsageError(`${err.message}; put Host arguments after \`--\`, for example: sideby alias add pi-kimi pi:main -- --model kimi-coding/k3`);
|
|
199
|
+
if (cmd === 'next' && err instanceof UsageError && err.message.startsWith('unknown option'))
|
|
200
|
+
throw new UsageError(`${err.message}; put Host arguments after \`--\`, for example: sideby next claude -- --model opus`);
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (parsed.flags.has('help')) {
|
|
205
|
+
console.log(COMMAND_HELP[cmd] ?? HELP);
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
const cmds = await import('./commands.js');
|
|
209
|
+
switch (cmd) {
|
|
210
|
+
case 'list':
|
|
211
|
+
return await cmds.cmdList(parsed, io);
|
|
212
|
+
case 'run':
|
|
213
|
+
return await cmds.cmdRun(parsed, io, 'run');
|
|
214
|
+
case 'login':
|
|
215
|
+
return await cmds.cmdRun(parsed, io, 'login');
|
|
216
|
+
case 'resume':
|
|
217
|
+
return await cmds.cmdResume(parsed, io);
|
|
218
|
+
case 'next':
|
|
219
|
+
return await cmds.cmdNext(parsed, io);
|
|
220
|
+
case 'new':
|
|
221
|
+
return await cmds.cmdNew(parsed, io);
|
|
222
|
+
case 'alias':
|
|
223
|
+
return await cmds.cmdAlias(parsed, io);
|
|
224
|
+
case 'doctor':
|
|
225
|
+
return await cmds.cmdDoctor(parsed, io);
|
|
226
|
+
case 'quota':
|
|
227
|
+
return await cmds.cmdQuota(parsed, io);
|
|
228
|
+
case 'ui':
|
|
229
|
+
return await cmds.cmdUi(parsed, io);
|
|
230
|
+
case 'app':
|
|
231
|
+
return await cmds.cmdApp(parsed, io);
|
|
232
|
+
case 'shell-init':
|
|
233
|
+
return await cmds.cmdShellInit(parsed, io);
|
|
234
|
+
case 'plugins':
|
|
235
|
+
return await cmds.cmdPlugins(parsed, io);
|
|
236
|
+
case 'families':
|
|
237
|
+
return await cmds.cmdFamilies(parsed, io);
|
|
238
|
+
}
|
|
239
|
+
return 2;
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
const usage = err instanceof UsageError;
|
|
243
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
244
|
+
if (wantsJson) {
|
|
245
|
+
const { UserError } = await import('../core/errors.js');
|
|
246
|
+
const code = usage ? 'usage' : err instanceof UserError && err.code ? err.code : 'error';
|
|
247
|
+
console.log(JSON.stringify({ schemaVersion: 1, error: message, code }, null, 2));
|
|
248
|
+
}
|
|
249
|
+
else
|
|
250
|
+
console.error(`sideby: ${message}`);
|
|
251
|
+
if (!usage && process.env.SIDEBY_DEBUG && err instanceof Error)
|
|
252
|
+
console.error(err.stack);
|
|
253
|
+
return usage ? 2 : 1;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Run when executed directly, also through symlinks (npm's bin shim, a user alias like `sb`).
|
|
257
|
+
function isEntry() {
|
|
258
|
+
const arg = process.argv[1];
|
|
259
|
+
if (!arg)
|
|
260
|
+
return false;
|
|
261
|
+
try {
|
|
262
|
+
return (pathToFileURL(realpathSync(arg)).href ===
|
|
263
|
+
pathToFileURL(realpathSync(fileURLToPath(import.meta.url))).href);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (isEntry()) {
|
|
270
|
+
main(process.argv.slice(2)).then((code) => {
|
|
271
|
+
process.exitCode = code;
|
|
272
|
+
}, (err) => {
|
|
273
|
+
console.error(`sideby: ${err instanceof Error ? err.message : String(err)}`);
|
|
274
|
+
process.exitCode = 1;
|
|
275
|
+
});
|
|
276
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Writes schemas/*.json from OUTPUT_SCHEMAS. CI regenerates and fails if the result differs from git.
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { OUTPUT_SCHEMAS } from './json-schemas.js';
|
|
6
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'schemas');
|
|
7
|
+
await mkdir(root, { recursive: true });
|
|
8
|
+
for (const [name, schema] of Object.entries(OUTPUT_SCHEMAS)) {
|
|
9
|
+
const doc = {
|
|
10
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
11
|
+
$id: `sideby/${name}.json`,
|
|
12
|
+
...schema,
|
|
13
|
+
};
|
|
14
|
+
await writeFile(join(root, `${name}.json`), `${JSON.stringify(doc, null, 2)}\n`);
|
|
15
|
+
}
|
|
16
|
+
console.log(`wrote ${Object.keys(OUTPUT_SCHEMAS).length} schemas to schemas/`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Account, FamilyDef } from '../types.ts';
|
|
2
|
+
import { ACCOUNT_NAME } from './account-name.ts';
|
|
3
|
+
import { UserError } from './errors.ts';
|
|
4
|
+
export { ACCOUNT_NAME };
|
|
5
|
+
export declare function looksLikeBackup(name: string): boolean;
|
|
6
|
+
export declare function mainDirOf(family: FamilyDef, home: string): string;
|
|
7
|
+
export declare function accountDirOf(family: FamilyDef, home: string, name: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* The Account for this Family and name, read from its directory: the one place an Account is built.
|
|
10
|
+
* Whether it is an API Account follows the Secret File on disk (and the Family's secretFileMeansApi).
|
|
11
|
+
*/
|
|
12
|
+
export declare function accountAt(family: FamilyDef, home: string, name: string): Promise<Account>;
|
|
13
|
+
/** Discovers the Main Account (if its directory exists) and every `<prefix><name><suffix>` directory in HOME. */
|
|
14
|
+
export declare function discoverAccounts(family: FamilyDef, home: string, ignore?: ReadonlySet<string>): Promise<Account[]>;
|
|
15
|
+
export declare class AccountRefError extends UserError {
|
|
16
|
+
}
|
|
17
|
+
/** Resolves `<family>:<name>`, an alias from the config, or a name unique across Families. */
|
|
18
|
+
export declare function resolveRef(ref: string, accounts: readonly Account[], aliases?: Readonly<Record<string, string>>): Account;
|
|
19
|
+
/** Alias names per Account ref, sorted. An alias whose target matches no Account is left out. */
|
|
20
|
+
export declare function aliasesByAccount(aliases: Readonly<Record<string, string>> | undefined, accounts: readonly Account[]): Map<string, string[]>;
|