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.
Files changed (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -2
  3. package/README.zh-CN.md +112 -0
  4. package/dist/build.json +1 -0
  5. package/dist/src/cli/args.d.ts +13 -0
  6. package/dist/src/cli/args.js +38 -0
  7. package/dist/src/cli/commands.d.ts +50 -0
  8. package/dist/src/cli/commands.js +566 -0
  9. package/dist/src/cli/format.d.ts +16 -0
  10. package/dist/src/cli/format.js +80 -0
  11. package/dist/src/cli/json-schemas.d.ts +2 -0
  12. package/dist/src/cli/json-schemas.js +259 -0
  13. package/dist/src/cli/main.d.ts +2 -0
  14. package/dist/src/cli/main.js +276 -0
  15. package/dist/src/cli/schemas.d.ts +1 -0
  16. package/dist/src/cli/schemas.js +16 -0
  17. package/dist/src/core/account-name.d.ts +2 -0
  18. package/dist/src/core/account-name.js +2 -0
  19. package/dist/src/core/accounts.d.ts +20 -0
  20. package/dist/src/core/accounts.js +131 -0
  21. package/dist/src/core/config.d.ts +61 -0
  22. package/dist/src/core/config.js +184 -0
  23. package/dist/src/core/create.d.ts +30 -0
  24. package/dist/src/core/create.js +126 -0
  25. package/dist/src/core/doctor.d.ts +41 -0
  26. package/dist/src/core/doctor.js +171 -0
  27. package/dist/src/core/errors.d.ts +12 -0
  28. package/dist/src/core/errors.js +14 -0
  29. package/dist/src/core/file-memo.d.ts +24 -0
  30. package/dist/src/core/file-memo.js +41 -0
  31. package/dist/src/core/fs-safe.d.ts +38 -0
  32. package/dist/src/core/fs-safe.js +205 -0
  33. package/dist/src/core/handoff.d.ts +41 -0
  34. package/dist/src/core/handoff.js +54 -0
  35. package/dist/src/core/identity.d.ts +23 -0
  36. package/dist/src/core/identity.js +79 -0
  37. package/dist/src/core/last-doctor.d.ts +36 -0
  38. package/dist/src/core/last-doctor.js +69 -0
  39. package/dist/src/core/launch.d.ts +37 -0
  40. package/dist/src/core/launch.js +108 -0
  41. package/dist/src/core/login-path.d.ts +23 -0
  42. package/dist/src/core/login-path.js +96 -0
  43. package/dist/src/core/paths.d.ts +13 -0
  44. package/dist/src/core/paths.js +32 -0
  45. package/dist/src/core/quota-levels.d.ts +19 -0
  46. package/dist/src/core/quota-levels.js +34 -0
  47. package/dist/src/core/resume.d.ts +9 -0
  48. package/dist/src/core/resume.js +15 -0
  49. package/dist/src/core/secret-file.d.ts +30 -0
  50. package/dist/src/core/secret-file.js +116 -0
  51. package/dist/src/core/share-meaning.d.ts +6 -0
  52. package/dist/src/core/share-meaning.js +32 -0
  53. package/dist/src/core/share-mode-names.d.ts +1 -0
  54. package/dist/src/core/share-mode-names.js +10 -0
  55. package/dist/src/core/share-modes.d.ts +24 -0
  56. package/dist/src/core/share-modes.js +405 -0
  57. package/dist/src/core/shell-init.d.ts +31 -0
  58. package/dist/src/core/shell-init.js +84 -0
  59. package/dist/src/core/skill-check.d.ts +11 -0
  60. package/dist/src/core/skill-check.js +57 -0
  61. package/dist/src/core/usage-days.d.ts +15 -0
  62. package/dist/src/core/usage-days.js +33 -0
  63. package/dist/src/core/version.d.ts +9 -0
  64. package/dist/src/core/version.js +46 -0
  65. package/dist/src/desktop/app.d.ts +60 -0
  66. package/dist/src/desktop/app.js +316 -0
  67. package/dist/src/desktop/icon.d.ts +16 -0
  68. package/dist/src/desktop/icon.js +174 -0
  69. package/dist/src/families/claude/index.d.ts +10 -0
  70. package/dist/src/families/claude/index.js +127 -0
  71. package/dist/src/families/claude/layout.d.ts +6 -0
  72. package/dist/src/families/claude/layout.js +5 -0
  73. package/dist/src/families/claude/quota-cache.d.ts +31 -0
  74. package/dist/src/families/claude/quota-cache.js +111 -0
  75. package/dist/src/families/claude/quota.d.ts +3 -0
  76. package/dist/src/families/claude/quota.js +53 -0
  77. package/dist/src/families/claude/usage.d.ts +22 -0
  78. package/dist/src/families/claude/usage.js +146 -0
  79. package/dist/src/families/codex/index.d.ts +5 -0
  80. package/dist/src/families/codex/index.js +70 -0
  81. package/dist/src/families/codex/rollout.d.ts +67 -0
  82. package/dist/src/families/codex/rollout.js +298 -0
  83. package/dist/src/families/grok/index.d.ts +8 -0
  84. package/dist/src/families/grok/index.js +82 -0
  85. package/dist/src/families/index.d.ts +3 -0
  86. package/dist/src/families/index.js +40 -0
  87. package/dist/src/families/logos.d.ts +21 -0
  88. package/dist/src/families/logos.js +26 -0
  89. package/dist/src/families/pi/index.d.ts +3 -0
  90. package/dist/src/families/pi/index.js +54 -0
  91. package/dist/src/families/shared/sessions.d.ts +9 -0
  92. package/dist/src/families/shared/sessions.js +70 -0
  93. package/dist/src/families/shared/toml.d.ts +5 -0
  94. package/dist/src/families/shared/toml.js +16 -0
  95. package/dist/src/index.d.ts +5 -0
  96. package/dist/src/index.js +4 -0
  97. package/dist/src/panel/background.d.ts +62 -0
  98. package/dist/src/panel/background.js +274 -0
  99. package/dist/src/panel/handler.d.ts +91 -0
  100. package/dist/src/panel/handler.js +396 -0
  101. package/dist/src/panel/i18n.d.ts +287 -0
  102. package/dist/src/panel/i18n.js +570 -0
  103. package/dist/src/panel/page-logic.d.ts +154 -0
  104. package/dist/src/panel/page-logic.js +326 -0
  105. package/dist/src/panel/page-script.d.ts +2 -0
  106. package/dist/src/panel/page-script.js +1633 -0
  107. package/dist/src/panel/page-styles.d.ts +7 -0
  108. package/dist/src/panel/page-styles.js +514 -0
  109. package/dist/src/panel/page.d.ts +20 -0
  110. package/dist/src/panel/page.js +96 -0
  111. package/dist/src/panel/server.d.ts +32 -0
  112. package/dist/src/panel/server.js +133 -0
  113. package/dist/src/panel/theme.d.ts +116 -0
  114. package/dist/src/panel/theme.js +132 -0
  115. package/dist/src/plugins/account-script.d.ts +4 -0
  116. package/dist/src/plugins/account-script.js +66 -0
  117. package/dist/src/plugins/bus.d.ts +33 -0
  118. package/dist/src/plugins/bus.js +84 -0
  119. package/dist/src/plugins/loader.d.ts +58 -0
  120. package/dist/src/plugins/loader.js +250 -0
  121. package/dist/src/quota/claude-statusline.d.ts +28 -0
  122. package/dist/src/quota/claude-statusline.js +301 -0
  123. package/dist/src/quota/statusline-tap.d.ts +11 -0
  124. package/dist/src/quota/statusline-tap.js +104 -0
  125. package/dist/src/runtime.d.ts +195 -0
  126. package/dist/src/runtime.js +424 -0
  127. package/dist/src/types.d.ts +285 -0
  128. package/dist/src/types.js +3 -0
  129. package/dist/testing/demo.d.ts +8 -0
  130. package/dist/testing/demo.js +50 -0
  131. package/dist/testing/index.d.ts +36 -0
  132. package/dist/testing/index.js +118 -0
  133. package/package.json +66 -4
  134. package/schemas/alias.json +102 -0
  135. package/schemas/app-install.json +74 -0
  136. package/schemas/app-uninstall.json +54 -0
  137. package/schemas/config.json +170 -0
  138. package/schemas/doctor.json +229 -0
  139. package/schemas/error.json +25 -0
  140. package/schemas/families.json +205 -0
  141. package/schemas/list.json +177 -0
  142. package/schemas/new.json +199 -0
  143. package/schemas/next.json +127 -0
  144. package/schemas/plugins.json +74 -0
  145. package/schemas/quota-setup.json +60 -0
  146. package/schemas/quota-teardown.json +30 -0
  147. package/schemas/quota.json +203 -0
  148. package/skills/sideby/SKILL.md +61 -0
  149. package/skills/sideby/references/accounts.md +31 -0
  150. package/skills/sideby/references/diagnose.md +47 -0
  151. package/skills/sideby/references/handoff.md +41 -0
  152. package/skills/sideby/references/quota.md +14 -0
  153. package/skills/sideby/references/setup.md +31 -0
  154. 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,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv: string[]): Promise<number>;
@@ -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,2 @@
1
+ /** Valid Account names. A leaf module with no I/O so the status line tap can import it cheaply. */
2
+ export declare const ACCOUNT_NAME: RegExp;
@@ -0,0 +1,2 @@
1
+ /** Valid Account names. A leaf module with no I/O so the status line tap can import it cheaply. */
2
+ export const ACCOUNT_NAME = /^[a-z0-9][a-z0-9-]{0,31}$/;
@@ -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[]>;