sideby 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +453 -2
- package/README.zh-CN.md +455 -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 +37 -0
- package/dist/src/cli/commands.js +487 -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 +224 -0
- package/dist/src/cli/main.d.ts +2 -0
- package/dist/src/cli/main.js +186 -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 +18 -0
- package/dist/src/core/accounts.js +116 -0
- package/dist/src/core/config.d.ts +60 -0
- package/dist/src/core/config.js +181 -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 +35 -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/secret-file.d.ts +30 -0
- package/dist/src/core/secret-file.js +116 -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 +25 -0
- package/dist/src/core/shell-init.js +61 -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 +124 -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 +67 -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 +78 -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/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 +93 -0
- package/dist/src/panel/handler.js +411 -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 +149 -0
- package/dist/src/runtime.js +371 -0
- package/dist/src/types.d.ts +278 -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 +117 -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 +166 -0
- package/schemas/doctor.json +229 -0
- package/schemas/error.json +25 -0
- package/schemas/list.json +177 -0
- package/schemas/new.json +181 -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 +52 -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
|
@@ -0,0 +1,224 @@
|
|
|
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 PluginError = Type.Object({ where: Str, message: Str });
|
|
46
|
+
const Finding = Type.Object({
|
|
47
|
+
level: Level,
|
|
48
|
+
account: Str,
|
|
49
|
+
item: Str,
|
|
50
|
+
code: Str,
|
|
51
|
+
message: Str,
|
|
52
|
+
hint: Type.Optional(Str),
|
|
53
|
+
source: Str,
|
|
54
|
+
fixable: Type.Boolean(),
|
|
55
|
+
});
|
|
56
|
+
const QuotaWindow = Type.Object({
|
|
57
|
+
label: Str,
|
|
58
|
+
windowMinutes: Type.Number(),
|
|
59
|
+
usedPercent: Type.Number(),
|
|
60
|
+
resetsAt: Str,
|
|
61
|
+
});
|
|
62
|
+
const Quota = Type.Union([
|
|
63
|
+
Type.Object({
|
|
64
|
+
status: Type.Literal('ok'),
|
|
65
|
+
windows: Type.Array(QuotaWindow),
|
|
66
|
+
observedAt: Str,
|
|
67
|
+
source: Str,
|
|
68
|
+
plan: Type.Optional(Str),
|
|
69
|
+
}),
|
|
70
|
+
Type.Object({ status: Type.Literal('unavailable'), reason: Str, detail: Type.Optional(Str) }),
|
|
71
|
+
]);
|
|
72
|
+
const Usage = Type.Union([
|
|
73
|
+
Type.Object({
|
|
74
|
+
status: Type.Literal('ok'),
|
|
75
|
+
days: Type.Number(),
|
|
76
|
+
sessions: Type.Number(),
|
|
77
|
+
inputTokens: Type.Number(),
|
|
78
|
+
outputTokens: Type.Number(),
|
|
79
|
+
cacheReadTokens: Type.Number(),
|
|
80
|
+
cacheWriteTokens: Type.Number(),
|
|
81
|
+
totalTokens: Type.Number(),
|
|
82
|
+
daily: Type.Optional(Type.Array(Type.Object({ date: Str, totalTokens: Type.Number() }), {
|
|
83
|
+
description: 'Tokens per local day (YYYY-MM-DD), oldest first, days without records as 0.',
|
|
84
|
+
})),
|
|
85
|
+
lastActivityAt: Type.Optional(Str),
|
|
86
|
+
}),
|
|
87
|
+
Type.Object({ status: Type.Literal('unavailable'), reason: Str, detail: Type.Optional(Str) }),
|
|
88
|
+
]);
|
|
89
|
+
const QuotaSetupPlan = Type.Object({
|
|
90
|
+
status: Type.Union([Type.Literal('enabled'), Type.Literal('ready'), Type.Literal('blocked')]),
|
|
91
|
+
file: Str,
|
|
92
|
+
diff: Str,
|
|
93
|
+
message: Str,
|
|
94
|
+
});
|
|
95
|
+
const ShellInitFiles = Type.Array(Type.Object({
|
|
96
|
+
shell: Type.Union([Type.Literal('zsh'), Type.Literal('bash')]),
|
|
97
|
+
path: Str,
|
|
98
|
+
ok: Type.Boolean(),
|
|
99
|
+
action: Type.Optional(Type.Union([Type.Literal('written'), Type.Literal('unchanged')])),
|
|
100
|
+
message: Type.Optional(Str),
|
|
101
|
+
}), { description: 'One entry per config `shellInitFile`, rewritten after the change.' });
|
|
102
|
+
export const OUTPUT_SCHEMAS = {
|
|
103
|
+
list: Type.Object({
|
|
104
|
+
schemaVersion: Version,
|
|
105
|
+
families: Type.Array(FamilyInfo),
|
|
106
|
+
accounts: Type.Array(AccountStatus),
|
|
107
|
+
pluginErrors: Type.Array(PluginError),
|
|
108
|
+
}, { title: 'sideby list --json' }),
|
|
109
|
+
doctor: Type.Object({
|
|
110
|
+
schemaVersion: Version,
|
|
111
|
+
status: Type.Union([Type.Literal('ok'), Type.Literal('issues')]),
|
|
112
|
+
accounts: Type.Array(Type.Object({
|
|
113
|
+
ref: Str,
|
|
114
|
+
family: Str,
|
|
115
|
+
name: Str,
|
|
116
|
+
kind: Kind,
|
|
117
|
+
dir: Str,
|
|
118
|
+
shared: Type.Object({ ok: Type.Number(), total: Type.Number() }),
|
|
119
|
+
backups: Type.Number(),
|
|
120
|
+
findings: Type.Array(Finding),
|
|
121
|
+
})),
|
|
122
|
+
general: Type.Array(Finding),
|
|
123
|
+
fixes: Type.Array(Type.Object({ account: Str, item: Str, code: Str, ok: Type.Boolean(), message: Str })),
|
|
124
|
+
}, { title: 'sideby doctor --json' }),
|
|
125
|
+
quota: Type.Object({
|
|
126
|
+
schemaVersion: Version,
|
|
127
|
+
accounts: Type.Array(Type.Object({ ref: Str, kind: Kind, quota: Quota, usage: Usage })),
|
|
128
|
+
}, { title: 'sideby quota --json' }),
|
|
129
|
+
next: Type.Object({
|
|
130
|
+
schemaVersion: Version,
|
|
131
|
+
family: Str,
|
|
132
|
+
pick: Type.Union([Str, Type.Null()], {
|
|
133
|
+
description: 'The recommended account; null when none can be used now (exit code 1).',
|
|
134
|
+
}),
|
|
135
|
+
accounts: Type.Array(Type.Object({
|
|
136
|
+
ref: Str,
|
|
137
|
+
kind: Kind,
|
|
138
|
+
state: Type.Union([
|
|
139
|
+
Type.Literal('ready'),
|
|
140
|
+
Type.Literal('unknown'),
|
|
141
|
+
Type.Literal('api'),
|
|
142
|
+
Type.Literal('full'),
|
|
143
|
+
Type.Literal('logged-out'),
|
|
144
|
+
], {
|
|
145
|
+
description: 'ready and unknown can be picked; api only with --include-api; full and logged-out never.',
|
|
146
|
+
}),
|
|
147
|
+
pressure: Type.Union([Type.Number(), Type.Null()], {
|
|
148
|
+
description: 'Used percent of the fullest window not reset yet; null without quota.',
|
|
149
|
+
}),
|
|
150
|
+
resetsAt: Type.Optional(Type.String({ description: 'For full: when its last full window resets.' })),
|
|
151
|
+
observedAt: Type.Optional(Type.String({ description: "When the account's quota was recorded (its last session)." })),
|
|
152
|
+
}), { description: 'Every account of the family, best first.' }),
|
|
153
|
+
hasQuota: Type.Boolean({
|
|
154
|
+
description: 'False when no account has quota data: the pick is then a guess, and `sideby next` does not start it (exit 1).',
|
|
155
|
+
}),
|
|
156
|
+
earliestReset: Type.Optional(Type.Object({ ref: Str, at: Str }, {
|
|
157
|
+
description: 'Set when nothing can be picked and some accounts are full: the first to come back.',
|
|
158
|
+
})),
|
|
159
|
+
}, { title: 'sideby next --json' }),
|
|
160
|
+
'quota-setup': Type.Object({ schemaVersion: Version, family: Str, applied: Type.Boolean(), plan: QuotaSetupPlan }, { title: 'sideby quota setup --json' }),
|
|
161
|
+
'quota-teardown': Type.Object({ schemaVersion: Version, family: Str, ok: Type.Boolean(), message: Str, diff: Type.Optional(Str) }, { title: 'sideby quota teardown --json' }),
|
|
162
|
+
new: Type.Object({
|
|
163
|
+
schemaVersion: Version,
|
|
164
|
+
ok: Type.Boolean(),
|
|
165
|
+
account: Account,
|
|
166
|
+
steps: Type.Array(Type.Object({ item: Str, action: Str, ok: Type.Boolean(), message: Type.Optional(Str) })),
|
|
167
|
+
hookErrors: Type.Array(Type.Object({ plugin: Str, message: Str })),
|
|
168
|
+
nextSteps: Type.Array(Str),
|
|
169
|
+
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.' })),
|
|
170
|
+
shellInitFiles: Type.Optional(ShellInitFiles),
|
|
171
|
+
}, { title: 'sideby new --json' }),
|
|
172
|
+
alias: Type.Object({
|
|
173
|
+
schemaVersion: Version,
|
|
174
|
+
ok: Type.Boolean({ description: 'False when a config `shellInitFile` was not rewritten.' }),
|
|
175
|
+
alias: Str,
|
|
176
|
+
status: Type.Union([
|
|
177
|
+
Type.Literal('added'),
|
|
178
|
+
Type.Literal('exists'),
|
|
179
|
+
Type.Literal('removed'),
|
|
180
|
+
Type.Literal('absent'),
|
|
181
|
+
]),
|
|
182
|
+
account: Type.Optional(Str),
|
|
183
|
+
args: Type.Optional(Type.Array(Str, { description: 'Host arguments `sideby run <alias>` adds.' })),
|
|
184
|
+
shellInitFiles: Type.Optional(ShellInitFiles),
|
|
185
|
+
}, { title: 'sideby alias add|rm --json' }),
|
|
186
|
+
plugins: Type.Object({
|
|
187
|
+
schemaVersion: Version,
|
|
188
|
+
plugins: Type.Array(Type.Object({
|
|
189
|
+
name: Str,
|
|
190
|
+
version: Str,
|
|
191
|
+
source: Str,
|
|
192
|
+
description: Type.Optional(Str),
|
|
193
|
+
families: Type.Array(Str),
|
|
194
|
+
configKeys: Type.Array(Str),
|
|
195
|
+
})),
|
|
196
|
+
errors: Type.Array(PluginError),
|
|
197
|
+
}, { title: 'sideby plugins --json' }),
|
|
198
|
+
'app-install': Type.Object({
|
|
199
|
+
schemaVersion: Version,
|
|
200
|
+
platform: AppPlatform,
|
|
201
|
+
path: Str,
|
|
202
|
+
files: Type.Array(Str),
|
|
203
|
+
mode: Type.Union([Type.Literal('panel'), Type.Literal('url')]),
|
|
204
|
+
url: Type.Optional(Str),
|
|
205
|
+
node: Str,
|
|
206
|
+
entry: Str,
|
|
207
|
+
updated: Type.Boolean(),
|
|
208
|
+
hints: Type.Array(Str),
|
|
209
|
+
}, { title: 'sideby app install --json' }),
|
|
210
|
+
'app-uninstall': Type.Object({
|
|
211
|
+
schemaVersion: Version,
|
|
212
|
+
platform: AppPlatform,
|
|
213
|
+
removed: Type.Array(Str),
|
|
214
|
+
skipped: Type.Array(Type.Object({ path: Str, reason: Str })),
|
|
215
|
+
}, { title: 'sideby app uninstall --json' }),
|
|
216
|
+
error: Type.Object({
|
|
217
|
+
schemaVersion: Version,
|
|
218
|
+
error: Type.String({ description: 'What went wrong and what to do next.' }),
|
|
219
|
+
code: Type.String({
|
|
220
|
+
description: '`usage` for a malformed command line (exit 2), a reason such as `no-quota-source` or `alias-invalid`, or `error`.',
|
|
221
|
+
}),
|
|
222
|
+
}, { title: 'sideby error --json' }),
|
|
223
|
+
config: ConfigSchema,
|
|
224
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
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> [--api] [--alias <short>]
|
|
12
|
+
create an account (families: claude, codex, grok, pi);
|
|
13
|
+
--alias also adds a short command such as cc008
|
|
14
|
+
sideby alias add <short> <account> [-- args]
|
|
15
|
+
add a short command, optionally with host arguments
|
|
16
|
+
(sideby alias add pi-kimi pi:main -- --model kimi-coding/k3)
|
|
17
|
+
sideby alias rm <short> remove a short command
|
|
18
|
+
sideby login <account> sign in to an account
|
|
19
|
+
sideby next <family> [--dry-run] [--include-api] [-- args]
|
|
20
|
+
recommend the account with the most quota left (claude, codex)
|
|
21
|
+
and start it; --dry-run only recommends
|
|
22
|
+
sideby doctor [target] [--fix] [--force]
|
|
23
|
+
check shared items and permissions; --fix repairs safe issues
|
|
24
|
+
sideby quota [account] quota and 7-day token usage
|
|
25
|
+
sideby quota setup claude [--yes] turn on Claude quota (shows the change first)
|
|
26
|
+
sideby quota teardown claude undo it, restoring the original file
|
|
27
|
+
sideby ui [--port n] [--no-open] open the local panel
|
|
28
|
+
sideby ui --background same, without a terminal: keeps running after this command exits
|
|
29
|
+
sideby ui --stop stop the background panel
|
|
30
|
+
sideby app install [--url <url>] add a sideby app that opens the panel (macOS, Linux)
|
|
31
|
+
sideby app uninstall remove that app
|
|
32
|
+
sideby shell-init zsh|bash print shell functions, e.g. eval "$(sideby shell-init zsh)"
|
|
33
|
+
sideby shell-init [zsh|bash] --write
|
|
34
|
+
rewrite the config's shellInitFile with them
|
|
35
|
+
sideby plugins list loaded plugins and load errors
|
|
36
|
+
|
|
37
|
+
Options:
|
|
38
|
+
--json machine-readable output (list, new, next, alias, doctor, quota, plugins, app)
|
|
39
|
+
-h, --help show this help
|
|
40
|
+
-v, --version show the version
|
|
41
|
+
|
|
42
|
+
An <account> is <family>:<name> (claude:work) or just <name> when it is unique.`;
|
|
43
|
+
function nodeTooOld() {
|
|
44
|
+
const [maj = 0, min = 0] = process.versions.node.split('.').map(Number);
|
|
45
|
+
return maj < MIN_NODE[0] || (maj === MIN_NODE[0] && min < MIN_NODE[1]);
|
|
46
|
+
}
|
|
47
|
+
const COMMANDS = {
|
|
48
|
+
list: { flags: ['json'] },
|
|
49
|
+
run: { flags: [] },
|
|
50
|
+
login: { flags: [] },
|
|
51
|
+
next: { flags: ['dry-run', 'include-api', 'json'] },
|
|
52
|
+
new: { flags: ['api', 'json'], valued: ['alias'] },
|
|
53
|
+
alias: { flags: ['json'] },
|
|
54
|
+
doctor: { flags: ['fix', 'force', 'json'] },
|
|
55
|
+
quota: { flags: ['json', 'yes'] },
|
|
56
|
+
ui: { flags: ['no-open', 'background', 'stop', 'serve-detached'], valued: ['port'] },
|
|
57
|
+
app: { flags: ['json'], valued: ['url'] },
|
|
58
|
+
'shell-init': { flags: ['write'] },
|
|
59
|
+
plugins: { flags: ['json'] },
|
|
60
|
+
};
|
|
61
|
+
export async function main(argv) {
|
|
62
|
+
const [first, ...rest] = argv;
|
|
63
|
+
if (first === 'statusline-tap') {
|
|
64
|
+
const { runStatuslineTap } = await import('../quota/statusline-tap.js');
|
|
65
|
+
return runStatuslineTap(rest, process.env, process.stdin, process.stdout);
|
|
66
|
+
}
|
|
67
|
+
if (first === '-v' || first === '--version' || first === 'version') {
|
|
68
|
+
const { packageVersion } = await import('../core/version.js');
|
|
69
|
+
console.log(packageVersion());
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
if (first === '-h' || first === '--help' || first === 'help') {
|
|
73
|
+
console.log(HELP);
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
if (nodeTooOld()) {
|
|
77
|
+
console.error(`sideby needs Node ${MIN_NODE.join('.')} or newer (you have ${process.versions.node}). Upgrade: https://nodejs.org`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
const cmd = first === undefined || first.startsWith('--') ? 'list' : first;
|
|
81
|
+
const args = first === undefined || first.startsWith('--') ? argv : rest;
|
|
82
|
+
const spec = COMMANDS[cmd];
|
|
83
|
+
const io = { out: (s) => console.log(s), err: (s) => console.error(s) };
|
|
84
|
+
const { parseArgs, UsageError } = await import('./args.js');
|
|
85
|
+
// Only sideby's own options count: arguments after `--` belong to the host.
|
|
86
|
+
const own = args.includes('--') ? args.slice(0, args.indexOf('--')) : args;
|
|
87
|
+
const wantsJson = Boolean(spec?.flags.includes('json')) && own.includes('--json');
|
|
88
|
+
try {
|
|
89
|
+
if (!spec)
|
|
90
|
+
throw new UsageError(`unknown command "${cmd}"; run \`sideby --help\``);
|
|
91
|
+
// `run` passes everything after the account through to the host, with or without `--`.
|
|
92
|
+
let parsed;
|
|
93
|
+
if (cmd === 'run') {
|
|
94
|
+
const sep = args.indexOf('--');
|
|
95
|
+
const head = sep === -1 ? args : args.slice(0, sep);
|
|
96
|
+
const [ref, ...more] = head;
|
|
97
|
+
parsed = {
|
|
98
|
+
positionals: ref ? [ref] : [],
|
|
99
|
+
flags: new Map(),
|
|
100
|
+
rest: [...more, ...(sep === -1 ? [] : args.slice(sep + 1))],
|
|
101
|
+
};
|
|
102
|
+
if (ref === '-h' || ref === '--help')
|
|
103
|
+
parsed.flags.set('help', true);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
try {
|
|
107
|
+
parsed = parseArgs(args, [...spec.flags, 'help'], spec.valued ?? []);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
// `alias add` takes Host arguments too; the usual mistake is leaving out the `--` before them.
|
|
111
|
+
if (cmd === 'alias' && err instanceof UsageError && err.message.startsWith('unknown option'))
|
|
112
|
+
throw new UsageError(`${err.message}; put Host arguments after \`--\`, for example: sideby alias add pi-kimi pi:main -- --model kimi-coding/k3`);
|
|
113
|
+
if (cmd === 'next' && err instanceof UsageError && err.message.startsWith('unknown option'))
|
|
114
|
+
throw new UsageError(`${err.message}; put Host arguments after \`--\`, for example: sideby next claude -- --model opus`);
|
|
115
|
+
throw err;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (parsed.flags.has('help')) {
|
|
119
|
+
console.log(HELP);
|
|
120
|
+
return 0;
|
|
121
|
+
}
|
|
122
|
+
const cmds = await import('./commands.js');
|
|
123
|
+
switch (cmd) {
|
|
124
|
+
case 'list':
|
|
125
|
+
return await cmds.cmdList(parsed, io);
|
|
126
|
+
case 'run':
|
|
127
|
+
return await cmds.cmdRun(parsed, io, 'run');
|
|
128
|
+
case 'login':
|
|
129
|
+
return await cmds.cmdRun(parsed, io, 'login');
|
|
130
|
+
case 'next':
|
|
131
|
+
return await cmds.cmdNext(parsed, io);
|
|
132
|
+
case 'new':
|
|
133
|
+
return await cmds.cmdNew(parsed, io);
|
|
134
|
+
case 'alias':
|
|
135
|
+
return await cmds.cmdAlias(parsed, io);
|
|
136
|
+
case 'doctor':
|
|
137
|
+
return await cmds.cmdDoctor(parsed, io);
|
|
138
|
+
case 'quota':
|
|
139
|
+
return await cmds.cmdQuota(parsed, io);
|
|
140
|
+
case 'ui':
|
|
141
|
+
return await cmds.cmdUi(parsed, io);
|
|
142
|
+
case 'app':
|
|
143
|
+
return await cmds.cmdApp(parsed, io);
|
|
144
|
+
case 'shell-init':
|
|
145
|
+
return await cmds.cmdShellInit(parsed, io);
|
|
146
|
+
case 'plugins':
|
|
147
|
+
return await cmds.cmdPlugins(parsed, io);
|
|
148
|
+
}
|
|
149
|
+
return 2;
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
const usage = err instanceof UsageError;
|
|
153
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
154
|
+
if (wantsJson) {
|
|
155
|
+
const { UserError } = await import('../core/errors.js');
|
|
156
|
+
const code = usage ? 'usage' : err instanceof UserError && err.code ? err.code : 'error';
|
|
157
|
+
console.log(JSON.stringify({ schemaVersion: 1, error: message, code }, null, 2));
|
|
158
|
+
}
|
|
159
|
+
else
|
|
160
|
+
console.error(`sideby: ${message}`);
|
|
161
|
+
if (!usage && process.env.SIDEBY_DEBUG && err instanceof Error)
|
|
162
|
+
console.error(err.stack);
|
|
163
|
+
return usage ? 2 : 1;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Run when executed directly, also through symlinks (npm's bin shim, a user alias like `sb`).
|
|
167
|
+
function isEntry() {
|
|
168
|
+
const arg = process.argv[1];
|
|
169
|
+
if (!arg)
|
|
170
|
+
return false;
|
|
171
|
+
try {
|
|
172
|
+
return (pathToFileURL(realpathSync(arg)).href ===
|
|
173
|
+
pathToFileURL(realpathSync(fileURLToPath(import.meta.url))).href);
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (isEntry()) {
|
|
180
|
+
main(process.argv.slice(2)).then((code) => {
|
|
181
|
+
process.exitCode = code;
|
|
182
|
+
}, (err) => {
|
|
183
|
+
console.error(`sideby: ${err instanceof Error ? err.message : String(err)}`);
|
|
184
|
+
process.exitCode = 1;
|
|
185
|
+
});
|
|
186
|
+
}
|
|
@@ -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,18 @@
|
|
|
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;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { ACCOUNT_NAME } from './account-name.js';
|
|
4
|
+
import { UserError } from './errors.js';
|
|
5
|
+
import { lstatOrNull, statOrNull } from './fs-safe.js';
|
|
6
|
+
export { ACCOUNT_NAME };
|
|
7
|
+
const BACKUP_LIKE = /(bak|backup|old|tmp)/;
|
|
8
|
+
export function looksLikeBackup(name) {
|
|
9
|
+
return BACKUP_LIKE.test(name);
|
|
10
|
+
}
|
|
11
|
+
export function mainDirOf(family, home) {
|
|
12
|
+
return join(home, family.layout.main);
|
|
13
|
+
}
|
|
14
|
+
export function accountDirOf(family, home, name) {
|
|
15
|
+
return name === 'main' ? mainDirOf(family, home) : join(home, family.layout.account.replace('<name>', name));
|
|
16
|
+
}
|
|
17
|
+
function splitLayout(family) {
|
|
18
|
+
const [first, ...rest] = family.layout.account.split('/');
|
|
19
|
+
const at = first.indexOf('<name>');
|
|
20
|
+
if (at === -1)
|
|
21
|
+
throw new Error(`family ${family.id}: layout.account must contain <name> in its first segment`);
|
|
22
|
+
return { prefix: first.slice(0, at), suffix: first.slice(at + '<name>'.length), rest: rest.join('/') };
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The Account for this Family and name, read from its directory: the one place an Account is built.
|
|
26
|
+
* Whether it is an API Account follows the Secret File on disk (and the Family's secretFileMeansApi).
|
|
27
|
+
*/
|
|
28
|
+
export async function accountAt(family, home, name) {
|
|
29
|
+
const dir = accountDirOf(family, home, name);
|
|
30
|
+
const secretFile = join(dir, 'proxy.env');
|
|
31
|
+
const hasSecret = (await lstatOrNull(secretFile)) !== null;
|
|
32
|
+
return {
|
|
33
|
+
family: family.id,
|
|
34
|
+
name,
|
|
35
|
+
ref: `${family.id}:${name}`,
|
|
36
|
+
dir,
|
|
37
|
+
isMain: name === 'main',
|
|
38
|
+
kind: hasSecret && family.secretFileMeansApi !== false ? 'api' : 'subscription',
|
|
39
|
+
...(hasSecret ? { secretFile } : {}),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A directory that merely matches the name pattern (for example `~/.claude-code-router`, another tool's
|
|
44
|
+
* config) is not an Account: it must hold a Secret File or one of the Family's Shared Items, or be empty.
|
|
45
|
+
*/
|
|
46
|
+
async function looksLikeAccount(family, dir) {
|
|
47
|
+
for (const p of ['proxy.env', ...(family.markers ?? family.sharedItems.map((i) => i.path))])
|
|
48
|
+
if (await lstatOrNull(join(dir, p)))
|
|
49
|
+
return true;
|
|
50
|
+
// A freshly created Account may have nothing to share yet; another tool's directory is never empty.
|
|
51
|
+
try {
|
|
52
|
+
return (await readdir(dir)).length === 0;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Discovers the Main Account (if its directory exists) and every `<prefix><name><suffix>` directory in HOME. */
|
|
59
|
+
export async function discoverAccounts(family, home, ignore = new Set()) {
|
|
60
|
+
const out = [];
|
|
61
|
+
const main = mainDirOf(family, home);
|
|
62
|
+
if ((await statOrNull(main))?.isDirectory())
|
|
63
|
+
out.push(await accountAt(family, home, 'main'));
|
|
64
|
+
const { prefix, suffix, rest } = splitLayout(family);
|
|
65
|
+
let entries = [];
|
|
66
|
+
try {
|
|
67
|
+
entries = await readdir(home);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
const names = [];
|
|
73
|
+
for (const e of entries) {
|
|
74
|
+
if (!e.startsWith(prefix) || !e.endsWith(suffix))
|
|
75
|
+
continue;
|
|
76
|
+
const name = e.slice(prefix.length, e.length - suffix.length);
|
|
77
|
+
if (!ACCOUNT_NAME.test(name) || name === 'main')
|
|
78
|
+
continue;
|
|
79
|
+
if (ignore.has(`${family.id}:${name}`))
|
|
80
|
+
continue;
|
|
81
|
+
// A non-main Account must be a real directory at every level: a symlinked `~/.claude-work` pointing at
|
|
82
|
+
// `~/.claude` would make every repair write into the Main Account. Only the Main Account may be a link.
|
|
83
|
+
const top = join(home, e);
|
|
84
|
+
const dir = rest ? join(top, rest) : top;
|
|
85
|
+
if (!(await lstatOrNull(top))?.isDirectory())
|
|
86
|
+
continue;
|
|
87
|
+
if (rest && !(await lstatOrNull(dir))?.isDirectory())
|
|
88
|
+
continue;
|
|
89
|
+
if (!(await looksLikeAccount(family, dir)))
|
|
90
|
+
continue;
|
|
91
|
+
names.push(name);
|
|
92
|
+
}
|
|
93
|
+
names.sort();
|
|
94
|
+
for (const name of names)
|
|
95
|
+
out.push(await accountAt(family, home, name));
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
export class AccountRefError extends UserError {
|
|
99
|
+
}
|
|
100
|
+
/** Resolves `<family>:<name>`, an alias from the config, or a name unique across Families. */
|
|
101
|
+
export function resolveRef(ref, accounts, aliases = {}) {
|
|
102
|
+
if (!ref.includes(':') && Object.hasOwn(aliases, ref))
|
|
103
|
+
return resolveRef(aliases[ref], accounts);
|
|
104
|
+
if (ref.includes(':')) {
|
|
105
|
+
const hit = accounts.find((a) => a.ref === ref);
|
|
106
|
+
if (hit)
|
|
107
|
+
return hit;
|
|
108
|
+
throw new AccountRefError(`no account ${ref}; run \`sideby list\` to see accounts or \`sideby new\` to create one`);
|
|
109
|
+
}
|
|
110
|
+
const hits = accounts.filter((a) => a.name === ref);
|
|
111
|
+
if (hits.length === 1)
|
|
112
|
+
return hits[0];
|
|
113
|
+
if (hits.length === 0)
|
|
114
|
+
throw new AccountRefError(`no account named ${ref}; run \`sideby list\` to see accounts`);
|
|
115
|
+
throw new AccountRefError(`${ref} is ambiguous; use one of: ${hits.map((a) => a.ref).join(', ')}`);
|
|
116
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type Static, Type } from 'typebox';
|
|
2
|
+
import { UserError } from './errors.ts';
|
|
3
|
+
export declare const ALIAS_NAME: RegExp;
|
|
4
|
+
/** Shell keywords and builtins an alias must not shadow (`command() { command sideby … }` would recurse). */
|
|
5
|
+
export declare const RESERVED_ALIASES: Set<string>;
|
|
6
|
+
export declare function aliasProblem(alias: string): string | null;
|
|
7
|
+
/** One `aliases` value: an Account ref, or a ref plus Host arguments that only `sideby run` adds (ADR-0005). */
|
|
8
|
+
declare const AliasSchema: Type.TUnion<[Type.TString, Type.TObject<{
|
|
9
|
+
account: Type.TString;
|
|
10
|
+
args: Type.TOptional<Type.TArray<Type.TString>>;
|
|
11
|
+
}>]>;
|
|
12
|
+
export type AliasValue = Static<typeof AliasSchema>;
|
|
13
|
+
/** The Account ref an Alias starts. */
|
|
14
|
+
export declare const aliasAccount: (value: AliasValue) => string;
|
|
15
|
+
/** The Host arguments an Alias adds to `sideby run`; none for a plain ref. */
|
|
16
|
+
export declare const aliasArgs: (value: AliasValue | undefined) => string[];
|
|
17
|
+
/** `aliases` as alias → Account ref, for lookups that ignore the arguments. */
|
|
18
|
+
export declare function aliasRefs(aliases: Readonly<Record<string, AliasValue>> | undefined): Record<string, string>;
|
|
19
|
+
export declare const ConfigSchema: Type.TObject<{
|
|
20
|
+
$schema: Type.TOptional<Type.TString>;
|
|
21
|
+
accounts: Type.TOptional<Type.TRecord<"^.*$", Type.TObject<{
|
|
22
|
+
args: Type.TOptional<Type.TArray<Type.TString>>;
|
|
23
|
+
}>>>;
|
|
24
|
+
aliases: Type.TOptional<Type.TRecord<"^.*$", Type.TUnion<[Type.TString, Type.TObject<{
|
|
25
|
+
account: Type.TString;
|
|
26
|
+
args: Type.TOptional<Type.TArray<Type.TString>>;
|
|
27
|
+
}>]>>>;
|
|
28
|
+
ignore: Type.TOptional<Type.TArray<Type.TString>>;
|
|
29
|
+
pluginDirs: Type.TOptional<Type.TArray<Type.TString>>;
|
|
30
|
+
plugins: Type.TOptional<Type.TRecord<"^.*$", Type.TRecord<"^.*$", Type.TUnknown>>>;
|
|
31
|
+
extraSharedItems: Type.TOptional<Type.TRecord<"^.*$", Type.TArray<Type.TObject<{
|
|
32
|
+
path: Type.TString;
|
|
33
|
+
mode: Type.TUnion<Type.TLiteral<"copy" | "info" | "link" | "link-or-copy" | "link-or-local" | "local" | "local-if-api">[]>;
|
|
34
|
+
noSymlink: Type.TOptional<Type.TBoolean>;
|
|
35
|
+
credential: Type.TOptional<Type.TBoolean>;
|
|
36
|
+
}>>>>;
|
|
37
|
+
shellInitFile: Type.TOptional<Type.TObject<{
|
|
38
|
+
zsh: Type.TOptional<Type.TString>;
|
|
39
|
+
bash: Type.TOptional<Type.TString>;
|
|
40
|
+
}>>;
|
|
41
|
+
}>;
|
|
42
|
+
export type Config = Static<typeof ConfigSchema>;
|
|
43
|
+
export declare class ConfigError extends UserError {
|
|
44
|
+
}
|
|
45
|
+
export declare function loadConfig(file: string): Promise<Config>;
|
|
46
|
+
/**
|
|
47
|
+
* Adds `alias → ref` to the config file's `aliases`, as `{ account, args }` when `args` is not empty (see
|
|
48
|
+
* `editConfigAliases` for how the file is written). Refuses an invalid alias, or one that already starts another
|
|
49
|
+
* Account or the same Account with other arguments. Returns the aliases now in the file.
|
|
50
|
+
*/
|
|
51
|
+
export declare function addConfigAlias(file: string, alias: string, ref: string, args?: readonly string[]): Promise<{
|
|
52
|
+
status: 'added' | 'exists';
|
|
53
|
+
aliases: Record<string, AliasValue>;
|
|
54
|
+
}>;
|
|
55
|
+
/** Removes `alias` from the config file's `aliases`; `absent` when it was not there (nothing is written then). */
|
|
56
|
+
export declare function removeConfigAlias(file: string, alias: string): Promise<{
|
|
57
|
+
status: 'removed' | 'absent';
|
|
58
|
+
aliases: Record<string, AliasValue>;
|
|
59
|
+
}>;
|
|
60
|
+
export {};
|