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,285 @@
|
|
|
1
|
+
export type ShareMode = 'link' | 'copy' | 'link-or-copy' | 'link-or-local' | 'local' | 'local-if-api' | 'info' | 'json-key';
|
|
2
|
+
export interface SharedItemDef {
|
|
3
|
+
/** Path inside the Account directory, for example `skills` or `.claude.json`. */
|
|
4
|
+
path: string;
|
|
5
|
+
mode: ShareMode;
|
|
6
|
+
/** Main-side source relative to HOME, when it does not live in the Main Account directory (`.claude.json`). */
|
|
7
|
+
mainPath?: string;
|
|
8
|
+
/** For `json-key`: the top-level key mirrored from the main file. */
|
|
9
|
+
key?: string;
|
|
10
|
+
/** The Host refuses to start when this path is a symlink (Grok sandbox). */
|
|
11
|
+
noSymlink?: boolean;
|
|
12
|
+
/** Holds credentials: must be a regular file with mode 600. Contents are never read. */
|
|
13
|
+
credential?: boolean;
|
|
14
|
+
/** Host message quoted when the item breaks a Host rule. */
|
|
15
|
+
hostMessage?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface FamilyLayout {
|
|
18
|
+
/** Main Account directory relative to HOME, for example `.claude` or `.pi/agent`. */
|
|
19
|
+
main: string;
|
|
20
|
+
/** Other Accounts relative to HOME, with `<name>` in the first path segment: `.claude-<name>`, `.pi-<name>/agent`. */
|
|
21
|
+
account: string;
|
|
22
|
+
}
|
|
23
|
+
export type LoginState = 'logged-in' | 'logged-out' | 'unknown';
|
|
24
|
+
/**
|
|
25
|
+
* A Family's mark for the Panel: one SVG path in a 24x24 view box, drawn inline. sideby checks it when the
|
|
26
|
+
* Plugin registers; an invalid logo is dropped with a plugin error and the Panel shows the Family's initials.
|
|
27
|
+
*/
|
|
28
|
+
export interface FamilyLogo {
|
|
29
|
+
/** SVG path data: only commands, numbers, spaces, commas, dots, `+` and `-`. At most 20000 characters. */
|
|
30
|
+
path: string;
|
|
31
|
+
/** Hex brand color such as `#D97757`. Without one the mark uses the text color, so it works in light and dark mode. */
|
|
32
|
+
color?: string;
|
|
33
|
+
/** Name of the mark for tooltips; defaults to the Family title. */
|
|
34
|
+
title?: string;
|
|
35
|
+
fillRule?: 'nonzero' | 'evenodd';
|
|
36
|
+
}
|
|
37
|
+
export interface FamilyDef {
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
/** Mark shown on the Panel's cards, lists and the Tool select; without one the Panel shows the id's initials. */
|
|
41
|
+
logo?: FamilyLogo;
|
|
42
|
+
/** Host executable looked up on PATH. */
|
|
43
|
+
bin: string;
|
|
44
|
+
installUrl: string;
|
|
45
|
+
/** Environment variable that selects the Account directory. Not set for the Main Account. */
|
|
46
|
+
selectVar: string;
|
|
47
|
+
layout: FamilyLayout;
|
|
48
|
+
/**
|
|
49
|
+
* Entries that prove a directory is this Family's Account (besides a Secret File or an empty directory).
|
|
50
|
+
* Defaults to the Shared Item paths; set it when a shared name is generic, like Claude's `plugins`.
|
|
51
|
+
*/
|
|
52
|
+
markers?: string[];
|
|
53
|
+
/**
|
|
54
|
+
* Whether a Secret File makes an Account an API Account (default true). pi keeps `proxy.env` in every
|
|
55
|
+
* Account for provider keys, so for pi it is loaded at Launch but says nothing about the login.
|
|
56
|
+
*/
|
|
57
|
+
secretFileMeansApi?: boolean;
|
|
58
|
+
/** Cleared before every Launch, for every Account. */
|
|
59
|
+
hijackVars: string[];
|
|
60
|
+
/** Variables written, commented out, into a new API Account's Secret File. */
|
|
61
|
+
apiVars: string[];
|
|
62
|
+
sharedItems: SharedItemDef[];
|
|
63
|
+
/** Arguments for the Host's sign-in command, or null when sign-in happens inside the Host. */
|
|
64
|
+
login: {
|
|
65
|
+
args: string[] | null;
|
|
66
|
+
hint: string;
|
|
67
|
+
};
|
|
68
|
+
/** Arguments sideby adds before the Account's and the user's arguments. */
|
|
69
|
+
defaultArgs?(account: Account, userArgs: readonly string[]): string[];
|
|
70
|
+
loginState?(account: Account): Promise<LoginState>;
|
|
71
|
+
model?(account: Account): Promise<string | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* Who the Account is signed in as, from the Host's own login file: identity fields such as the email and the
|
|
74
|
+
* organization only, never a token or any other credential value (ADR-0003). Return undefined when unknown; an
|
|
75
|
+
* error is treated the same way and never shown as a problem.
|
|
76
|
+
*/
|
|
77
|
+
identity?(account: Account): Promise<AccountIdentity | undefined>;
|
|
78
|
+
readQuota?(account: Account, ctx: ReadContext): Promise<QuotaResult>;
|
|
79
|
+
readUsage?(account: Account, ctx: ReadContext): Promise<UsageResult>;
|
|
80
|
+
/** One-time, reversible change that lets the Host report Quota (Claude: status line wrapper). */
|
|
81
|
+
quotaSetup?: QuotaSetup;
|
|
82
|
+
/**
|
|
83
|
+
* The session these Host arguments resume by id (UUID-shaped), or undefined when they start a new session or
|
|
84
|
+
* pick one another way. With `sessionWrittenAt` it lets `sideby resume` find the Account (ADR-0007).
|
|
85
|
+
*/
|
|
86
|
+
resumedSession?(args: readonly string[]): string | undefined;
|
|
87
|
+
/** When the Account holds the session, the last write time of its file in ms; otherwise undefined. */
|
|
88
|
+
sessionWrittenAt?(account: Account, sessionId: string): Promise<number | undefined>;
|
|
89
|
+
}
|
|
90
|
+
/** Identity fields of a signed-in Account. */
|
|
91
|
+
export interface AccountIdentity {
|
|
92
|
+
email?: string;
|
|
93
|
+
/** Organization or team, when it is not just the personal default. */
|
|
94
|
+
org?: string;
|
|
95
|
+
}
|
|
96
|
+
export interface QuotaSetupPlan {
|
|
97
|
+
/** `enabled`: already on; `ready`: apply would make `diff`; `blocked`: cannot apply, see `message`. */
|
|
98
|
+
status: 'enabled' | 'ready' | 'blocked';
|
|
99
|
+
/** File the change touches. */
|
|
100
|
+
file: string;
|
|
101
|
+
/** Unified diff of the change, empty when nothing would change. */
|
|
102
|
+
diff: string;
|
|
103
|
+
message: string;
|
|
104
|
+
}
|
|
105
|
+
export interface QuotaSetup {
|
|
106
|
+
/** One sentence shown before the user confirms. */
|
|
107
|
+
summary: string;
|
|
108
|
+
plan(ctx: ReadContext): Promise<QuotaSetupPlan>;
|
|
109
|
+
/** Re-plans and applies only when the plan is `ready`. */
|
|
110
|
+
apply(ctx: ReadContext): Promise<QuotaSetupPlan>;
|
|
111
|
+
/** Restores the original bytes only if the file is still exactly what `apply` wrote. */
|
|
112
|
+
teardown(ctx: ReadContext): Promise<{
|
|
113
|
+
ok: boolean;
|
|
114
|
+
message: string;
|
|
115
|
+
diff?: string;
|
|
116
|
+
}>;
|
|
117
|
+
}
|
|
118
|
+
export interface Account {
|
|
119
|
+
family: string;
|
|
120
|
+
/** `main` for the Main Account. */
|
|
121
|
+
name: string;
|
|
122
|
+
/** `<family>:<name>` */
|
|
123
|
+
ref: string;
|
|
124
|
+
dir: string;
|
|
125
|
+
isMain: boolean;
|
|
126
|
+
kind: 'subscription' | 'api';
|
|
127
|
+
/** Path of the Secret File for API Accounts. */
|
|
128
|
+
secretFile?: string;
|
|
129
|
+
}
|
|
130
|
+
export interface ReadContext {
|
|
131
|
+
home: string;
|
|
132
|
+
now: Date;
|
|
133
|
+
/** sideby state directory (quota caches live here). */
|
|
134
|
+
stateDir: string;
|
|
135
|
+
/** sideby's own environment (PATH lookups); never the Host's. */
|
|
136
|
+
env: Env;
|
|
137
|
+
}
|
|
138
|
+
export interface QuotaWindow {
|
|
139
|
+
/** `5h`, `7d`, or `<n>m` for unknown window lengths. */
|
|
140
|
+
label: string;
|
|
141
|
+
windowMinutes: number;
|
|
142
|
+
usedPercent: number;
|
|
143
|
+
/** ISO timestamp. */
|
|
144
|
+
resetsAt: string;
|
|
145
|
+
}
|
|
146
|
+
export type QuotaUnavailableReason = 'not-enabled' | 'no-session' | 'unrecognized' | 'no-source' | 'api-account';
|
|
147
|
+
export type QuotaResult = {
|
|
148
|
+
status: 'ok';
|
|
149
|
+
windows: QuotaWindow[];
|
|
150
|
+
observedAt: string;
|
|
151
|
+
source: string;
|
|
152
|
+
plan?: string;
|
|
153
|
+
} | {
|
|
154
|
+
status: 'unavailable';
|
|
155
|
+
reason: QuotaUnavailableReason;
|
|
156
|
+
detail?: string;
|
|
157
|
+
};
|
|
158
|
+
export interface TokenTotals {
|
|
159
|
+
inputTokens: number;
|
|
160
|
+
outputTokens: number;
|
|
161
|
+
cacheReadTokens: number;
|
|
162
|
+
cacheWriteTokens: number;
|
|
163
|
+
totalTokens: number;
|
|
164
|
+
}
|
|
165
|
+
export type UsageResult = ({
|
|
166
|
+
status: 'ok';
|
|
167
|
+
days: number;
|
|
168
|
+
sessions: number;
|
|
169
|
+
/**
|
|
170
|
+
* Tokens per local calendar day (`YYYY-MM-DD`), `days` entries ending today, oldest first, days without
|
|
171
|
+
* records as 0. It can sum to less than `totalTokens`, which also covers the part of the oldest day
|
|
172
|
+
* inside the rolling window.
|
|
173
|
+
*/
|
|
174
|
+
daily?: {
|
|
175
|
+
date: string;
|
|
176
|
+
totalTokens: number;
|
|
177
|
+
}[];
|
|
178
|
+
/** ISO time of the newest record the reader saw, not later than now. */
|
|
179
|
+
lastActivityAt?: string;
|
|
180
|
+
} & TokenTotals) | {
|
|
181
|
+
status: 'unavailable';
|
|
182
|
+
reason: 'no-source' | 'no-session' | 'unrecognized';
|
|
183
|
+
detail?: string;
|
|
184
|
+
};
|
|
185
|
+
export type FindingLevel = 'ok' | 'warn' | 'fail';
|
|
186
|
+
export interface FixResult {
|
|
187
|
+
ok: boolean;
|
|
188
|
+
message: string;
|
|
189
|
+
}
|
|
190
|
+
export interface Finding {
|
|
191
|
+
level: FindingLevel;
|
|
192
|
+
/** Account ref the Finding is about. */
|
|
193
|
+
account: string;
|
|
194
|
+
/** Shared Item path or check name. */
|
|
195
|
+
item: string;
|
|
196
|
+
/** Stable machine code such as `link.real-file`. */
|
|
197
|
+
code: string;
|
|
198
|
+
message: string;
|
|
199
|
+
/** What the user should do; never contains credential values. */
|
|
200
|
+
hint?: string;
|
|
201
|
+
/** `core` or the Plugin name. */
|
|
202
|
+
source: string;
|
|
203
|
+
fixable: boolean;
|
|
204
|
+
/** Present when `fixable`; not serialized. */
|
|
205
|
+
fix?: () => Promise<FixResult>;
|
|
206
|
+
}
|
|
207
|
+
/** What a `doctor.check` hook returns; sideby fills `account` and `source`. */
|
|
208
|
+
export type CheckResult = Omit<Finding, 'account' | 'source' | 'fixable'> & {
|
|
209
|
+
fixable?: boolean;
|
|
210
|
+
};
|
|
211
|
+
export type Env = Record<string, string | undefined>;
|
|
212
|
+
export interface LaunchContext {
|
|
213
|
+
account: Account;
|
|
214
|
+
family: FamilyDef;
|
|
215
|
+
/** Environment the Host will receive; mutate to change it. */
|
|
216
|
+
env: Env;
|
|
217
|
+
/** Arguments the Host will receive; mutate to change them. */
|
|
218
|
+
args: string[];
|
|
219
|
+
config: Record<string, unknown>;
|
|
220
|
+
/** `run` or `login`. */
|
|
221
|
+
command: 'run' | 'login';
|
|
222
|
+
}
|
|
223
|
+
export interface CreateBeforeContext {
|
|
224
|
+
family: FamilyDef;
|
|
225
|
+
/** Name of the Account about to be created, already checked against the name rules. */
|
|
226
|
+
name: string;
|
|
227
|
+
/** Whether it will be an API Account (`sideby new --api`, or "API key account" in the Panel). */
|
|
228
|
+
api: boolean;
|
|
229
|
+
/** The Family's existing Accounts, the Main Account included. */
|
|
230
|
+
accounts: Account[];
|
|
231
|
+
config: Record<string, unknown>;
|
|
232
|
+
}
|
|
233
|
+
export interface CreatedContext {
|
|
234
|
+
account: Account;
|
|
235
|
+
family: FamilyDef;
|
|
236
|
+
config: Record<string, unknown>;
|
|
237
|
+
log(message: string): void;
|
|
238
|
+
}
|
|
239
|
+
export interface DoctorContext {
|
|
240
|
+
account: Account;
|
|
241
|
+
family: FamilyDef;
|
|
242
|
+
config: Record<string, unknown>;
|
|
243
|
+
}
|
|
244
|
+
export interface HookFilter {
|
|
245
|
+
family?: string;
|
|
246
|
+
}
|
|
247
|
+
export interface HookEvents {
|
|
248
|
+
'launch.before': {
|
|
249
|
+
ctx: LaunchContext;
|
|
250
|
+
result: void;
|
|
251
|
+
};
|
|
252
|
+
'account.create.before': {
|
|
253
|
+
ctx: CreateBeforeContext;
|
|
254
|
+
result: void;
|
|
255
|
+
};
|
|
256
|
+
'account.created': {
|
|
257
|
+
ctx: CreatedContext;
|
|
258
|
+
result: void;
|
|
259
|
+
};
|
|
260
|
+
'doctor.check': {
|
|
261
|
+
ctx: DoctorContext;
|
|
262
|
+
result: CheckResult[] | undefined;
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
export type HookEvent = keyof HookEvents;
|
|
266
|
+
export type HookHandler<E extends HookEvent> = (ctx: HookEvents[E]['ctx']) => HookEvents[E]['result'] | Promise<HookEvents[E]['result']>;
|
|
267
|
+
export interface PluginFs {
|
|
268
|
+
/** Atomic write via a temp file in the same directory; `mode` defaults to 0o600. */
|
|
269
|
+
writeFileAtomic(path: string, data: string | Uint8Array, mode?: number): Promise<void>;
|
|
270
|
+
}
|
|
271
|
+
export interface PluginApi {
|
|
272
|
+
family(def: FamilyDef): void;
|
|
273
|
+
on<E extends HookEvent>(event: E, handler: HookHandler<E>): void;
|
|
274
|
+
on<E extends HookEvent>(event: E, filter: HookFilter, handler: HookHandler<E>): void;
|
|
275
|
+
/**
|
|
276
|
+
* Returns an error that, when thrown from `launch.before` or `account.create.before`, stops the Launch or the
|
|
277
|
+
* new Account with this message. Say what to do instead: the CLI and the Panel show it as is.
|
|
278
|
+
*/
|
|
279
|
+
abort(message: string): Error;
|
|
280
|
+
fs: PluginFs;
|
|
281
|
+
}
|
|
282
|
+
export interface Plugin {
|
|
283
|
+
name: string;
|
|
284
|
+
register(api: PluginApi): void | Promise<void>;
|
|
285
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BuiltinPlugin } from '../src/plugins/loader.ts';
|
|
2
|
+
import type { FamilyDef, Plugin, SharedItemDef } from '../src/types.ts';
|
|
3
|
+
export declare const DEMO_ITEMS: SharedItemDef[];
|
|
4
|
+
export declare function demoFamily(overrides?: Partial<FamilyDef>): FamilyDef;
|
|
5
|
+
export declare function demoPlugin(def?: FamilyDef, extra?: Plugin['register']): Plugin;
|
|
6
|
+
export declare function asBuiltin(plugin: Plugin, defaultEnabled?: boolean): BuiltinPlugin;
|
|
7
|
+
/** Populates a Main Account with one source for every demo Shared Item. */
|
|
8
|
+
export declare function seedDemoMain(write: (rel: string, data: string, mode?: number) => Promise<string>): Promise<void>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const DEMO_ITEMS = [
|
|
2
|
+
{ path: 'skills', mode: 'link' },
|
|
3
|
+
{ path: 'rules.md', mode: 'link' },
|
|
4
|
+
{ path: 'hooks', mode: 'copy', hostMessage: 'hooks must be a real directory' },
|
|
5
|
+
{ path: 'theme.json', mode: 'link-or-copy' },
|
|
6
|
+
{ path: 'trusted.toml', mode: 'link-or-local' },
|
|
7
|
+
{ path: 'cli.json', mode: 'local' },
|
|
8
|
+
{ path: 'config.toml', mode: 'local-if-api' },
|
|
9
|
+
{ path: 'auth.json', mode: 'info', credential: true, noSymlink: true },
|
|
10
|
+
{ path: '.demo.json', mode: 'json-key', key: 'servers', mainPath: '.demo.json', credential: true },
|
|
11
|
+
];
|
|
12
|
+
export function demoFamily(overrides = {}) {
|
|
13
|
+
return {
|
|
14
|
+
id: 'demo',
|
|
15
|
+
title: 'Demo',
|
|
16
|
+
bin: 'demo',
|
|
17
|
+
installUrl: 'https://example.invalid/demo',
|
|
18
|
+
selectVar: 'DEMO_HOME',
|
|
19
|
+
layout: { main: '.demo', account: '.demo-<name>' },
|
|
20
|
+
hijackVars: ['DEMO_API_KEY', 'DEMO_BASE_URL'],
|
|
21
|
+
apiVars: ['DEMO_BASE_URL', 'DEMO_API_KEY'],
|
|
22
|
+
sharedItems: DEMO_ITEMS,
|
|
23
|
+
login: { args: ['login'], hint: 'run demo login' },
|
|
24
|
+
...overrides,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function demoPlugin(def = demoFamily(), extra) {
|
|
28
|
+
return {
|
|
29
|
+
name: 'demo-family',
|
|
30
|
+
async register(api) {
|
|
31
|
+
api.family(def);
|
|
32
|
+
await extra?.(api);
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function asBuiltin(plugin, defaultEnabled = true) {
|
|
37
|
+
return { plugin, version: '0.0.0-test', description: 'test', defaultEnabled };
|
|
38
|
+
}
|
|
39
|
+
/** Populates a Main Account with one source for every demo Shared Item. */
|
|
40
|
+
export async function seedDemoMain(write) {
|
|
41
|
+
await write('.demo/skills/a/SKILL.md', '# a\n');
|
|
42
|
+
await write('.demo/rules.md', 'rules\n');
|
|
43
|
+
await write('.demo/hooks/pre.sh', '#!/bin/sh\n', 0o755);
|
|
44
|
+
await write('.demo/theme.json', '{"dark":true}\n');
|
|
45
|
+
await write('.demo/trusted.toml', 'trusted = []\n');
|
|
46
|
+
await write('.demo/cli.json', '{"model":"x"}\n');
|
|
47
|
+
await write('.demo/config.toml', 'model = "main"\n');
|
|
48
|
+
await write('.demo/auth.json', '{"token":"fake-main"}\n', 0o600);
|
|
49
|
+
await write('.demo.json', '{"servers":{"a":{"cmd":"a"}},"user":"main"}\n', 0o600);
|
|
50
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Env } from '../src/types.ts';
|
|
2
|
+
export interface FakeHome {
|
|
3
|
+
home: string;
|
|
4
|
+
/** Directory prepended to PATH; fake Host binaries live here. */
|
|
5
|
+
bin: string;
|
|
6
|
+
/** Environment for code under test: HOME, XDG dirs and PATH all point inside the fake HOME. */
|
|
7
|
+
env: Env;
|
|
8
|
+
path(...parts: string[]): string;
|
|
9
|
+
write(rel: string, data: string, mode?: number): Promise<string>;
|
|
10
|
+
mkdir(rel: string): Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
/** Runs `fn` with a fresh fake HOME and removes it afterwards. */
|
|
13
|
+
export declare function withFakeHome<T>(fn: (h: FakeHome) => Promise<T>): Promise<T>;
|
|
14
|
+
export interface FakeHostOptions {
|
|
15
|
+
exitCode?: number;
|
|
16
|
+
/** Signal the fake Host sends itself after recording, for example `SIGTERM`. */
|
|
17
|
+
selfSignal?: string;
|
|
18
|
+
/** Seconds to sleep before exiting, so tests can signal it. */
|
|
19
|
+
sleep?: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Installs a fake Host executable named `name` that appends its argv, selected env and received signals
|
|
23
|
+
* to `<bin>/<name>.log` as JSON lines.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fakeHost(h: FakeHome, name: string, opts?: FakeHostOptions): Promise<string>;
|
|
26
|
+
export interface HostRecord {
|
|
27
|
+
argv: string[];
|
|
28
|
+
env: Record<string, string>;
|
|
29
|
+
pid: number;
|
|
30
|
+
signal?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare function readHostLog(h: FakeHome, name: string): Promise<HostRecord[]>;
|
|
33
|
+
/** Snapshot of every path under `root` with its type, symlink target or content hash, for write audits. */
|
|
34
|
+
export declare function snapshot(root: string): Promise<Map<string, string>>;
|
|
35
|
+
/** Lists keys whose snapshot value differs, appeared or disappeared. */
|
|
36
|
+
export declare function diffSnapshots(before: Map<string, string>, after: Map<string, string>): string[];
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Test helpers for sideby and its Plugins. Every test runs against a throwaway HOME and fake Host binaries,
|
|
2
|
+
// so nothing a test does can reach the developer's real accounts.
|
|
3
|
+
import { chmod, mkdir, mkdtemp, readdir, readFile, readlink, realpath, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
/** Runs `fn` with a fresh fake HOME and removes it afterwards. */
|
|
7
|
+
export async function withFakeHome(fn) {
|
|
8
|
+
const base = await realpath(await mkdtemp(join(tmpdir(), 'sideby-test-')));
|
|
9
|
+
const home = join(base, 'home');
|
|
10
|
+
const bin = join(base, 'bin');
|
|
11
|
+
await mkdir(home, { recursive: true });
|
|
12
|
+
await mkdir(bin, { recursive: true });
|
|
13
|
+
const realHome = homedir();
|
|
14
|
+
if (home === realHome || home.startsWith(`${realHome}/.`))
|
|
15
|
+
throw new Error('fake HOME resolved inside the real HOME');
|
|
16
|
+
const env = {
|
|
17
|
+
HOME: home,
|
|
18
|
+
XDG_CONFIG_HOME: join(home, '.config'),
|
|
19
|
+
XDG_STATE_HOME: join(home, '.local', 'state'),
|
|
20
|
+
PATH: `${bin}:/usr/bin:/bin`,
|
|
21
|
+
NO_COLOR: '1',
|
|
22
|
+
};
|
|
23
|
+
const h = {
|
|
24
|
+
home,
|
|
25
|
+
bin,
|
|
26
|
+
env,
|
|
27
|
+
path: (...parts) => join(home, ...parts),
|
|
28
|
+
async write(rel, data, mode = 0o644) {
|
|
29
|
+
const p = join(home, rel);
|
|
30
|
+
await mkdir(join(p, '..'), { recursive: true });
|
|
31
|
+
await writeFile(p, data, { mode });
|
|
32
|
+
await chmod(p, mode);
|
|
33
|
+
return p;
|
|
34
|
+
},
|
|
35
|
+
async mkdir(rel) {
|
|
36
|
+
const p = join(home, rel);
|
|
37
|
+
await mkdir(p, { recursive: true });
|
|
38
|
+
return p;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
return await fn(h);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await rm(base, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Installs a fake Host executable named `name` that appends its argv, selected env and received signals
|
|
50
|
+
* to `<bin>/<name>.log` as JSON lines.
|
|
51
|
+
*/
|
|
52
|
+
export async function fakeHost(h, name, opts = {}) {
|
|
53
|
+
const log = join(h.bin, `${name}.log`);
|
|
54
|
+
const script = `#!/usr/bin/env node
|
|
55
|
+
const fs = require('node:fs')
|
|
56
|
+
const log = ${JSON.stringify(log)}
|
|
57
|
+
const rec = (o) => fs.appendFileSync(log, JSON.stringify(o) + '\\n')
|
|
58
|
+
// Handlers first: tests signal the Host as soon as the start record appears.
|
|
59
|
+
for (const s of ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGQUIT']) process.on(s, () => { rec({ signal: s }); process.exit(${opts.sleep ? 100 : 0}) })
|
|
60
|
+
rec({ argv: process.argv.slice(2), env: process.env, pid: process.pid })
|
|
61
|
+
const finish = () => {
|
|
62
|
+
${opts.selfSignal ? `process.removeAllListeners(${JSON.stringify(opts.selfSignal)}); process.kill(process.pid, ${JSON.stringify(opts.selfSignal)})` : `process.exit(${opts.exitCode ?? 0})`}
|
|
63
|
+
}
|
|
64
|
+
${opts.sleep ? `setTimeout(finish, ${Math.round(opts.sleep * 1000)})` : 'finish()'}
|
|
65
|
+
`;
|
|
66
|
+
const p = join(h.bin, name);
|
|
67
|
+
await writeFile(p, script.replace('#!/usr/bin/env node', `#!${process.execPath}`), { mode: 0o755 });
|
|
68
|
+
return p;
|
|
69
|
+
}
|
|
70
|
+
export async function readHostLog(h, name) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = await readFile(join(h.bin, `${name}.log`), 'utf8');
|
|
73
|
+
return raw
|
|
74
|
+
.split('\n')
|
|
75
|
+
.filter(Boolean)
|
|
76
|
+
.map((l) => JSON.parse(l));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Snapshot of every path under `root` with its type, symlink target or content hash, for write audits. */
|
|
83
|
+
export async function snapshot(root) {
|
|
84
|
+
const out = new Map();
|
|
85
|
+
const { createHash } = await import('node:crypto');
|
|
86
|
+
const { lstat } = await import('node:fs/promises');
|
|
87
|
+
async function walk(dir, rel) {
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = await readdir(dir);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
for (const e of entries.sort()) {
|
|
96
|
+
const p = join(dir, e);
|
|
97
|
+
const r = rel ? `${rel}/${e}` : e;
|
|
98
|
+
const st = await lstat(p);
|
|
99
|
+
if (st.isSymbolicLink())
|
|
100
|
+
out.set(r, `link:${await readlink(p)}`);
|
|
101
|
+
else if (st.isDirectory()) {
|
|
102
|
+
out.set(r, `dir:${(st.mode & 0o777).toString(8)}`);
|
|
103
|
+
await walk(p, r);
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
out.set(r, `file:${(st.mode & 0o777).toString(8)}:${createHash('sha256')
|
|
107
|
+
.update(await readFile(p))
|
|
108
|
+
.digest('hex')}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
await walk(root, '');
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
/** Lists keys whose snapshot value differs, appeared or disappeared. */
|
|
115
|
+
export function diffSnapshots(before, after) {
|
|
116
|
+
const keys = new Set([...before.keys(), ...after.keys()]);
|
|
117
|
+
return [...keys].filter((k) => before.get(k) !== after.get(k)).sort();
|
|
118
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sideby",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Run every AI coding account side by side: Claude Code, Codex, Grok and pi, with one shared config and a quota panel.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"claude-code",
|
|
9
|
+
"codex",
|
|
10
|
+
"grok",
|
|
11
|
+
"pi",
|
|
12
|
+
"multi-account",
|
|
13
|
+
"ai-coding",
|
|
14
|
+
"cli",
|
|
15
|
+
"quota"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/Adonis0123/sideby.git"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/Adonis0123/sideby#readme",
|
|
22
|
+
"bugs": "https://github.com/Adonis0123/sideby/issues",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.18.0"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"sideby": "dist/src/cli/main.js"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/src/index.d.ts",
|
|
32
|
+
"default": "./dist/src/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./testing": {
|
|
35
|
+
"types": "./dist/testing/index.d.ts",
|
|
36
|
+
"default": "./dist/testing/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"schemas",
|
|
43
|
+
"skills",
|
|
44
|
+
"README.md",
|
|
45
|
+
"README.zh-CN.md",
|
|
46
|
+
"LICENSE"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && chmod +x dist/src/cli/main.js && node scripts/write-build-id.ts",
|
|
50
|
+
"typecheck": "tsc --noEmit",
|
|
51
|
+
"lint": "biome check .",
|
|
52
|
+
"format": "biome check --write .",
|
|
53
|
+
"test": "node --test --test-concurrency=4 \"src/**/*.test.ts\" \"testing/**/*.test.ts\"",
|
|
54
|
+
"schemas": "node src/cli/schemas.ts",
|
|
55
|
+
"leak-check": "sh scripts/leak-check.sh",
|
|
56
|
+
"check": "pnpm typecheck && pnpm lint && pnpm test && pnpm leak-check",
|
|
57
|
+
"version": "node scripts/sync-skill-version.ts && git add skills/sideby/SKILL.md"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"typebox": "^1.3.34"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@biomejs/biome": "2.5.15",
|
|
64
|
+
"@types/node": "^22.18.0",
|
|
65
|
+
"typescript": "^7.0.2"
|
|
66
|
+
},
|
|
67
|
+
"packageManager": "pnpm@10.15.0"
|
|
68
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "sideby/alias.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"schemaVersion",
|
|
7
|
+
"ok",
|
|
8
|
+
"alias",
|
|
9
|
+
"status"
|
|
10
|
+
],
|
|
11
|
+
"properties": {
|
|
12
|
+
"schemaVersion": {
|
|
13
|
+
"type": "number",
|
|
14
|
+
"const": 1
|
|
15
|
+
},
|
|
16
|
+
"ok": {
|
|
17
|
+
"type": "boolean",
|
|
18
|
+
"description": "False when a config `shellInitFile` was not rewritten."
|
|
19
|
+
},
|
|
20
|
+
"alias": {
|
|
21
|
+
"type": "string"
|
|
22
|
+
},
|
|
23
|
+
"status": {
|
|
24
|
+
"anyOf": [
|
|
25
|
+
{
|
|
26
|
+
"type": "string",
|
|
27
|
+
"const": "added"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "string",
|
|
31
|
+
"const": "exists"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"type": "string",
|
|
35
|
+
"const": "removed"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"type": "string",
|
|
39
|
+
"const": "absent"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"account": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"args": {
|
|
47
|
+
"type": "array",
|
|
48
|
+
"items": {
|
|
49
|
+
"type": "string"
|
|
50
|
+
},
|
|
51
|
+
"description": "Host arguments `sideby run <alias>` adds."
|
|
52
|
+
},
|
|
53
|
+
"shellInitFiles": {
|
|
54
|
+
"type": "array",
|
|
55
|
+
"items": {
|
|
56
|
+
"type": "object",
|
|
57
|
+
"required": [
|
|
58
|
+
"shell",
|
|
59
|
+
"path",
|
|
60
|
+
"ok"
|
|
61
|
+
],
|
|
62
|
+
"properties": {
|
|
63
|
+
"shell": {
|
|
64
|
+
"anyOf": [
|
|
65
|
+
{
|
|
66
|
+
"type": "string",
|
|
67
|
+
"const": "zsh"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "string",
|
|
71
|
+
"const": "bash"
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
},
|
|
75
|
+
"path": {
|
|
76
|
+
"type": "string"
|
|
77
|
+
},
|
|
78
|
+
"ok": {
|
|
79
|
+
"type": "boolean"
|
|
80
|
+
},
|
|
81
|
+
"action": {
|
|
82
|
+
"anyOf": [
|
|
83
|
+
{
|
|
84
|
+
"type": "string",
|
|
85
|
+
"const": "written"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"type": "string",
|
|
89
|
+
"const": "unchanged"
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
"message": {
|
|
94
|
+
"type": "string"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"description": "One entry per config `shellInitFile`, rewritten after the change."
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
"title": "sideby alias add|rm --json"
|
|
102
|
+
}
|