sideby 0.1.0 → 0.2.2

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 (40) hide show
  1. package/README.md +71 -413
  2. package/README.zh-CN.md +71 -414
  3. package/dist/build.json +1 -1
  4. package/dist/src/cli/commands.d.ts +13 -0
  5. package/dist/src/cli/commands.js +86 -7
  6. package/dist/src/cli/json-schemas.js +35 -0
  7. package/dist/src/cli/main.js +99 -9
  8. package/dist/src/core/accounts.d.ts +2 -0
  9. package/dist/src/core/accounts.js +15 -0
  10. package/dist/src/core/config.d.ts +1 -0
  11. package/dist/src/core/config.js +3 -0
  12. package/dist/src/core/launch.d.ts +3 -1
  13. package/dist/src/core/resume.d.ts +9 -0
  14. package/dist/src/core/resume.js +15 -0
  15. package/dist/src/core/share-meaning.d.ts +6 -0
  16. package/dist/src/core/share-meaning.js +32 -0
  17. package/dist/src/core/shell-init.d.ts +7 -1
  18. package/dist/src/core/shell-init.js +25 -2
  19. package/dist/src/desktop/icon.js +2 -2
  20. package/dist/src/families/claude/index.js +3 -0
  21. package/dist/src/families/codex/index.js +3 -0
  22. package/dist/src/families/grok/index.js +4 -0
  23. package/dist/src/families/shared/sessions.d.ts +9 -0
  24. package/dist/src/families/shared/sessions.js +70 -0
  25. package/dist/src/index.d.ts +1 -1
  26. package/dist/src/panel/handler.d.ts +0 -2
  27. package/dist/src/panel/handler.js +1 -16
  28. package/dist/src/panel/page-styles.js +14 -13
  29. package/dist/src/runtime.d.ts +48 -2
  30. package/dist/src/runtime.js +54 -1
  31. package/dist/src/types.d.ts +7 -0
  32. package/dist/testing/index.js +2 -1
  33. package/package.json +1 -1
  34. package/schemas/config.json +4 -0
  35. package/schemas/families.json +205 -0
  36. package/schemas/new.json +18 -0
  37. package/skills/sideby/SKILL.md +15 -6
  38. package/skills/sideby/references/accounts.md +1 -1
  39. package/skills/sideby/references/setup.md +31 -0
  40. package/skills/sideby/references/sharing.md +17 -0
@@ -0,0 +1,70 @@
1
+ // Reading which session a Host command resumes, and whether an Account holds it (ADR-0007).
2
+ import { readdir, stat } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5
+ export const isSessionId = (s) => s !== undefined && SESSION_ID.test(s);
6
+ /** Arguments before the first `--`, the ones the Host reads as its own options. */
7
+ const options = (args) => {
8
+ const end = args.indexOf('--');
9
+ return end === -1 ? args : args.slice(0, end);
10
+ };
11
+ /** The id after `--resume <id>`, `--resume=<id>` or `-r <id>`; Claude Code and Grok Build share this form. */
12
+ export function resumeFlagSession(args) {
13
+ const opts = options(args);
14
+ for (let i = 0; i < opts.length; i++) {
15
+ const a = opts[i];
16
+ const id = a.startsWith('--resume=')
17
+ ? a.slice('--resume='.length)
18
+ : a === '--resume' || a === '-r'
19
+ ? opts[i + 1]
20
+ : undefined;
21
+ if (isSessionId(id))
22
+ return id;
23
+ }
24
+ return undefined;
25
+ }
26
+ /** The first UUID after the `resume` subcommand (`codex resume [options] <id>`). */
27
+ export function resumeSubcommandSession(args) {
28
+ const opts = options(args);
29
+ const at = opts.indexOf('resume');
30
+ return at === -1 ? undefined : opts.slice(at + 1).find(isSessionId);
31
+ }
32
+ /** Modification time in ms of the first of `paths` that exists with the wanted type. */
33
+ async function firstWrittenAt(paths, type) {
34
+ for (const p of paths) {
35
+ try {
36
+ const st = await stat(p);
37
+ if (type === 'file' ? st.isFile() : st.isDirectory())
38
+ return st.mtimeMs;
39
+ }
40
+ catch {
41
+ // not here
42
+ }
43
+ }
44
+ return undefined;
45
+ }
46
+ async function subdirs(dir) {
47
+ try {
48
+ return (await readdir(dir, { withFileTypes: true }))
49
+ .filter((e) => e.isDirectory())
50
+ .map((e) => join(dir, e.name));
51
+ }
52
+ catch {
53
+ return [];
54
+ }
55
+ }
56
+ /** `<dir>/*` + `/<entry>`: one level of grouping directories (Claude projects, Grok working directories). */
57
+ export async function groupedEntryWrittenAt(dir, entry, type) {
58
+ return firstWrittenAt((await subdirs(dir)).map((d) => join(d, entry)), type);
59
+ }
60
+ /** A file anywhere under `dir` whose name ends with `suffix` (Codex `sessions/YYYY/MM/DD/rollout-…-<id>.jsonl`). */
61
+ export async function nestedFileWrittenAt(dir, suffix) {
62
+ let names;
63
+ try {
64
+ names = await readdir(dir, { recursive: true });
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ return firstWrittenAt(names.filter((n) => n.endsWith(suffix)).map((n) => join(dir, n)), 'file');
70
+ }
@@ -1,5 +1,5 @@
1
1
  export { createPanelHandler, type PanelFamily, type PanelHandler, type PanelHandlerOptions, type PanelState, } from './panel/handler.ts';
2
2
  export { startPanelServer } from './panel/server.ts';
3
3
  export type { PanelTheme, PanelThemeTokens } from './panel/theme.ts';
4
- export { type AccountStatus, createRuntime, type DoctorOptions, type FamilyInfo, type Runtime, } from './runtime.ts';
4
+ export { type AccountStatus, createRuntime, type DoctorOptions, type FamilyDetail, type FamilyInfo, type Runtime, type SharedItemInfo, } from './runtime.ts';
5
5
  export type * from './types.ts';
@@ -54,8 +54,6 @@ export declare function healthByAccount(history: DoctorHistory | undefined): Map
54
54
  fail: number;
55
55
  warn: number;
56
56
  }>;
57
- /** Alias names per Account ref, sorted. An alias whose target matches no Account is left out. */
58
- export declare function aliasesByAccount(aliases: Readonly<Record<string, string>> | undefined, accounts: readonly Account[]): Map<string, string[]>;
59
57
  /** A Family as the page sees it: FamilyInfo plus the mark from its FamilyDef. */
60
58
  export type PanelFamily = FamilyInfo & {
61
59
  logo?: FamilyLogo;
@@ -1,6 +1,6 @@
1
1
  // HTTP handler for the local Panel. Mountable under any basePath; standalone and embedded use the same rules.
2
2
  import { randomBytes, timingSafeEqual } from 'node:crypto';
3
- import { resolveRef } from '../core/accounts.js';
3
+ import { aliasesByAccount } from '../core/accounts.js';
4
4
  import { aliasArgs, aliasRefs } from '../core/config.js';
5
5
  import { UserError } from '../core/errors.js';
6
6
  import { planHandoff } from '../core/handoff.js';
@@ -31,21 +31,6 @@ export function healthByAccount(history) {
31
31
  });
32
32
  return out;
33
33
  }
34
- /** Alias names per Account ref, sorted. An alias whose target matches no Account is left out. */
35
- export function aliasesByAccount(aliases, accounts) {
36
- const out = new Map();
37
- for (const [alias, target] of Object.entries(aliases ?? {})) {
38
- let ref;
39
- try {
40
- ref = resolveRef(target, accounts).ref;
41
- }
42
- catch {
43
- continue;
44
- }
45
- out.set(ref, [...(out.get(ref) ?? []), alias].sort());
46
- }
47
- return out;
48
- }
49
34
  /** The `handoff` map of the Panel state, from the statuses and Quota already read for the page. */
50
35
  export function handoffByFamily(rt, accounts, now) {
51
36
  const out = {};
@@ -3,9 +3,8 @@
3
3
  import { resolveTheme } from './theme.js';
4
4
  // Colors defined from other tokens; repeated in both schemes so they follow that scheme's base colors.
5
5
  const DERIVED_COLORS = `
6
- --primary-border: var(--primary); --primary-hover-border: var(--primary-hover); --btn-text: var(--text);
7
- --btn-hover-border: var(--border-strong); --btn-hover-text: var(--text); --check: var(--primary);
8
- --logo-bg: var(--text); --logo-fg: var(--bg); --logo-line: transparent;
6
+ --btn-text: var(--text); --btn-hover-text: var(--accent); --check: var(--focus);
7
+ --logo-bg: var(--accent-soft); --logo-fg: var(--focus); --logo-line: var(--border);
9
8
  --focus-outline: 2px solid var(--focus); --focus-ring: none;`;
10
9
  // Design tokens. A host theme (theme.ts) layers its values over these: light values after LIGHT_TOKENS, dark values after
11
10
  // DARK_TOKENS. DARK_TOKENS sets every color token again, so a host's light colors never leak into dark mode, while
@@ -16,22 +15,24 @@ const LIGHT_TOKENS = `
16
15
  --title-weight: 750; --heading-weight: 650; --strong-weight: 600; --button-weight: 600; --primary-weight: var(--button-weight);
17
16
  --radius: 14px; --ctl-radius: 8px; --chip-radius: 999px; --ctl-h: 34px; --ctl-h-sm: 28px; --field-h: 36px;
18
17
  --content-width: 1180px; --gutter: 24px;
19
- --bg: #f5f6f8; --surface: #ffffff; --surface-2: #f0f2f5; --border: #e2e5ea; --border-strong: #cfd4dc;
20
- --text: #15171c; --muted: #5b6372; --faint: #8b93a1; --accent: #4f46e5; --accent-soft: #eef0ff;
21
- --primary: #4f46e5; --primary-hover: #4338ca; --primary-active: #3730a3; --on-primary: #ffffff;
22
- --hover: #eceef2; --active: #e2e5ea; --focus: #6366f1;
18
+ --bg: #f2f8fc; --surface: #fcfcfa; --surface-2: #f6fafd; --border: #dce8f0; --border-strong: #c5d7e4;
19
+ --text: #15171c; --muted: #555b63; --faint: #8b93a1; --accent: #27658f; --accent-soft: #e5f2fb;
20
+ --primary: #e1f0fb; --primary-hover: #d2e8f9; --primary-active: #c3e0f6; --on-primary: #1f6396;
21
+ --primary-border: #b9d8ef; --primary-hover-border: #9cc7e8; --btn-hover-border: #8fbde3;
22
+ --hover: #f0f7fc; --active: #e6f1fa; --focus: #3f8fc4;
23
23
  --ok: #15803d; --ok-bar: #22c55e; --ok-soft: #e8f7ee; --warn: #b45309; --warn-bar: #f59e0b; --warn-soft: #fdf3e2;
24
- --fail: #c81e1e; --fail-bar: #ef4444; --fail-soft: #fdecec; --track: #e9ecf1;
24
+ --fail: #c81e1e; --fail-bar: #ef4444; --fail-soft: #fdecec; --track: #e4edf4;
25
25
  --spark: oklch(from var(--accent) clamp(0.55, l, 0.62) max(c, 0.14) h); --spark-soft: oklch(from var(--accent) 0.83 calc(max(c, 0.14) * 0.6) h);
26
26
  --shadow: 0 1px 2px rgba(16,24,40,.04), 0 2px 8px rgba(16,24,40,.05);
27
27
  --float-shadow: 0 10px 30px rgba(16,24,40,.14), 0 2px 6px rgba(16,24,40,.06);${DERIVED_COLORS}`;
28
28
  const DARK_TOKENS = `
29
- --bg: #0d0f12; --surface: #15181d; --surface-2: #1c2026; --border: #262b33; --border-strong: #353c47;
30
- --text: #e7e9ee; --muted: #a0a8b5; --faint: #6c7480; --accent: #8b87ff; --accent-soft: #23224a;
31
- --primary: #5b54e8; --primary-hover: #6a64ef; --primary-active: #4f46e5; --on-primary: #ffffff;
32
- --hover: #22272e; --active: #2a3038; --focus: #8b87ff;
29
+ --bg: #0e1621; --surface: #141e2a; --surface-2: #111a25; --border: #233244; --border-strong: #30455b;
30
+ --text: #e6edf3; --muted: #9aa9b8; --faint: #6b7a8a; --accent: #8cc4ee; --accent-soft: #16314a;
31
+ --primary: #173a58; --primary-hover: #1d4769; --primary-active: #22537a; --on-primary: #d3e9fa;
32
+ --primary-border: #2b5b82; --primary-hover-border: #3a6f99; --btn-hover-border: #4a7fa8;
33
+ --hover: #182736; --active: #1c2f42; --focus: #5aa7dc;
33
34
  --ok: #4ade80; --ok-bar: #22c55e; --ok-soft: #13291c; --warn: #fbbf24; --warn-bar: #f59e0b; --warn-soft: #2d2410;
34
- --fail: #f87171; --fail-bar: #ef4444; --fail-soft: #331717; --track: #252a32;
35
+ --fail: #f87171; --fail-bar: #ef4444; --fail-soft: #331717; --track: #1f2c3a;
35
36
  --spark: oklch(from var(--accent) clamp(0.7, l, 0.78) max(c, 0.14) h); --spark-soft: oklch(from var(--accent) 0.45 calc(max(c, 0.14) * 0.6) h);
36
37
  --shadow: 0 1px 2px rgba(0,0,0,.3); --float-shadow: 0 12px 32px rgba(0,0,0,.5), 0 2px 6px rgba(0,0,0,.3);${DERIVED_COLORS}`;
37
38
  /**
@@ -5,12 +5,20 @@ import { type DoctorReport } from './core/doctor.ts';
5
5
  import { UserError } from './core/errors.ts';
6
6
  import { type HandoffPlan } from './core/handoff.ts';
7
7
  import { type DoctorHistory } from './core/last-doctor.ts';
8
- import { type PreparedLaunch } from './core/launch.ts';
8
+ import { type HostLaunch, type PreparedLaunch } from './core/launch.ts';
9
9
  import { type Paths } from './core/paths.ts';
10
10
  import { type Shell, type ShellInitWrite } from './core/shell-init.ts';
11
11
  import { type BuiltinPlugin, type LoadedPlugin, type PluginLoadError } from './plugins/loader.ts';
12
- import type { Account, AccountIdentity, Env, FamilyDef, LoginState, QuotaResult, QuotaSetupPlan, ReadContext, UsageResult } from './types.ts';
12
+ import type { Account, AccountIdentity, Env, FamilyDef, LoginState, QuotaResult, QuotaSetupPlan, ReadContext, ShareMode, UsageResult } from './types.ts';
13
13
  export type { AccountIdentity, AccountRefError, DoctorHistory, Shell, ShellInitWrite };
14
+ /** How `sideby resume` starts the Host (spec §3.15). */
15
+ export interface ResumeLaunch {
16
+ launch: HostLaunch;
17
+ /** The session the Host arguments resume by id, when they name one. */
18
+ sessionId?: string;
19
+ /** Set when the session lives in a non-main Account and `launch` starts that Account as `run` would. */
20
+ account?: Account;
21
+ }
14
22
  /** What happened to the alias asked for with a new Account. */
15
23
  export interface AliasResult {
16
24
  name: string;
@@ -45,6 +53,37 @@ export interface FamilyInfo {
45
53
  /** Plugin that provides the Family. */
46
54
  plugin: string;
47
55
  }
56
+ /** One Shared Item as `sideby families` reports it (spec §3.16). */
57
+ export interface SharedItemInfo {
58
+ path: string;
59
+ mode: ShareMode;
60
+ key?: string;
61
+ mainPath?: string;
62
+ credential?: boolean;
63
+ noSymlink?: boolean;
64
+ /** `family` from the FamilyDef, `config` from `extraSharedItems`. */
65
+ source: 'family' | 'config';
66
+ /** What the item looks like in an Account, in one fixed sentence. */
67
+ meaning: string;
68
+ }
69
+ /** A Family's facts for agents and docs: FamilyInfo plus layout, sign-in, sources and Shared Items (spec §3.16). */
70
+ export interface FamilyDetail extends FamilyInfo {
71
+ /** Relative to HOME: `{ main: '.claude', account: '.claude-<name>' }`. */
72
+ layout: {
73
+ main: string;
74
+ account: string;
75
+ };
76
+ selectVar: string;
77
+ /** `args` is null when sign-in happens inside the Host. */
78
+ login: {
79
+ args: string[] | null;
80
+ hint: string;
81
+ };
82
+ quota: boolean;
83
+ usage: boolean;
84
+ quotaSetup: boolean;
85
+ shared: SharedItemInfo[];
86
+ }
48
87
  export interface AccountStatus extends Account {
49
88
  familyTitle: string;
50
89
  hostInstalled: boolean;
@@ -84,6 +123,8 @@ export interface Runtime {
84
123
  plugins: readonly LoadedPlugin[];
85
124
  pluginErrors: readonly PluginLoadError[];
86
125
  familyInfo(): Promise<FamilyInfo[]>;
126
+ /** Every loaded Family with its layout, sign-in, quota support and Shared Items (config extras included). */
127
+ familyDetails(): Promise<FamilyDetail[]>;
87
128
  accounts(): Promise<Account[]>;
88
129
  resolve(ref: string): Promise<Account>;
89
130
  status(account: Account): Promise<AccountStatus>;
@@ -129,6 +170,11 @@ export interface Runtime {
129
170
  /** Rewrites the configured `shellInitFile` of each shell (all configured ones by default). */
130
171
  writeShellInitFiles(shells?: readonly Shell[]): Promise<ShellInitWrite[]>;
131
172
  prepareLaunch(ref: string, userArgs: readonly string[], command: 'run' | 'login'): Promise<PreparedLaunch>;
173
+ /**
174
+ * Starts the non-main Account that holds the session the Host arguments resume, or else the Host unchanged
175
+ * (spec §3.15). Throws UnknownFamilyError, or a UserError when the Family cannot route sessions.
176
+ */
177
+ resumeLaunch(familyId: string, userArgs: readonly string[]): Promise<ResumeLaunch>;
132
178
  quota(account: Account): Promise<QuotaResult>;
133
179
  usage(account: Account): Promise<UsageResult>;
134
180
  /**
@@ -9,6 +9,8 @@ import { identityOf } from './core/identity.js';
9
9
  import { mergeDoctorHistory, readDoctorHistory, writeDoctorHistory, } from './core/last-doctor.js';
10
10
  import { prepareLaunch, which } from './core/launch.js';
11
11
  import { resolvePaths } from './core/paths.js';
12
+ import { findResumeRoute } from './core/resume.js';
13
+ import { shareMeaning } from './core/share-meaning.js';
12
14
  import { SHELLS, shellInitScript, writeShellInitFile, } from './core/shell-init.js';
13
15
  import { packageVersion } from './core/version.js';
14
16
  import { BUILTIN_PLUGINS } from './families/index.js';
@@ -103,6 +105,32 @@ export async function createRuntime(opts = {}) {
103
105
  plugin: loaded.familyOwner.get(f.id) ?? 'unknown',
104
106
  })));
105
107
  },
108
+ async familyDetails() {
109
+ const info = new Map((await rt.familyInfo()).map((f) => [f.id, f]));
110
+ return [...families.values()].map((f) => {
111
+ // The merged list holds the config's own objects, so identity tells which ones came from `extraSharedItems`.
112
+ const fromConfig = new Set(config.extraSharedItems?.[f.id] ?? []);
113
+ return {
114
+ ...info.get(f.id),
115
+ layout: { main: f.layout.main, account: f.layout.account },
116
+ selectVar: f.selectVar,
117
+ login: { args: f.login.args ? [...f.login.args] : null, hint: f.login.hint },
118
+ quota: Boolean(f.readQuota),
119
+ usage: Boolean(f.readUsage),
120
+ quotaSetup: Boolean(f.quotaSetup),
121
+ shared: f.sharedItems.map((item) => ({
122
+ path: item.path,
123
+ mode: item.mode,
124
+ ...(item.key ? { key: item.key } : {}),
125
+ ...(item.mainPath ? { mainPath: item.mainPath } : {}),
126
+ ...(item.credential ? { credential: true } : {}),
127
+ ...(item.noSymlink ? { noSymlink: true } : {}),
128
+ source: fromConfig.has(item) ? 'config' : 'family',
129
+ meaning: shareMeaning(item),
130
+ })),
131
+ };
132
+ });
133
+ },
106
134
  accounts,
107
135
  async resolve(ref) {
108
136
  return resolveRef(ref, await accounts(), aliasRefs(config.aliases));
@@ -291,7 +319,14 @@ export async function createRuntime(opts = {}) {
291
319
  return null;
292
320
  },
293
321
  async shellInitScript() {
294
- return shellInitScript(await accounts(), config.aliases);
322
+ const list = await accounts();
323
+ // Routing helps only a Family that can route and has somewhere else to route to (spec §3.15).
324
+ const routes = config.resumeRouting
325
+ ? [...families.values()]
326
+ .filter((f) => f.resumedSession && f.sessionWrittenAt && list.some((a) => a.family === f.id && !a.isMain))
327
+ .map((f) => ({ family: f.id, bin: f.bin, selectVar: f.selectVar }))
328
+ : [];
329
+ return shellInitScript(list, config.aliases, routes);
295
330
  },
296
331
  async writeShellInitFiles(shells = SHELLS) {
297
332
  const out = [];
@@ -315,6 +350,24 @@ export async function createRuntime(opts = {}) {
315
350
  bus,
316
351
  });
317
352
  },
353
+ async resumeLaunch(familyId, userArgs) {
354
+ const family = familyOf(familyId);
355
+ if (!family.resumedSession || !family.sessionWrittenAt)
356
+ throw new UserError(`${family.title} sessions cannot be routed; start an account yourself with \`sideby run <account> -- …\``);
357
+ const host = { family, bin: family.bin, args: [...userArgs], env };
358
+ // A select variable already set means an Account was chosen on purpose: run the Host as it is.
359
+ if (env[family.selectVar])
360
+ return { launch: host };
361
+ const route = await findResumeRoute(family, await accounts(), userArgs);
362
+ const id = route.sessionId ? { sessionId: route.sessionId } : {};
363
+ if (!route.account || route.account.isMain)
364
+ return { launch: host, ...id };
365
+ return {
366
+ launch: await rt.prepareLaunch(route.account.ref, userArgs, 'run'),
367
+ ...id,
368
+ account: route.account,
369
+ };
370
+ },
318
371
  async quota(account) {
319
372
  if (account.kind === 'api')
320
373
  return { status: 'unavailable', reason: 'api-account' };
@@ -79,6 +79,13 @@ export interface FamilyDef {
79
79
  readUsage?(account: Account, ctx: ReadContext): Promise<UsageResult>;
80
80
  /** One-time, reversible change that lets the Host report Quota (Claude: status line wrapper). */
81
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>;
82
89
  }
83
90
  /** Identity fields of a signed-in Account. */
84
91
  export interface AccountIdentity {
@@ -55,8 +55,9 @@ export async function fakeHost(h, name, opts = {}) {
55
55
  const fs = require('node:fs')
56
56
  const log = ${JSON.stringify(log)}
57
57
  const rec = (o) => fs.appendFileSync(log, JSON.stringify(o) + '\\n')
58
- rec({ argv: process.argv.slice(2), env: process.env, pid: process.pid })
58
+ // Handlers first: tests signal the Host as soon as the start record appears.
59
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 })
60
61
  const finish = () => {
61
62
  ${opts.selfSignal ? `process.removeAllListeners(${JSON.stringify(opts.selfSignal)}); process.kill(process.pid, ${JSON.stringify(opts.selfSignal)})` : `process.exit(${opts.exitCode ?? 0})`}
62
63
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.1.0",
3
+ "version": "0.2.2",
4
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
5
  "license": "MIT",
6
6
  "type": "module",
@@ -145,6 +145,10 @@
145
145
  }
146
146
  }
147
147
  },
148
+ "resumeRouting": {
149
+ "type": "boolean",
150
+ "description": "Make `sideby shell-init` also define Host-named functions (`claude`, `codex`, `grok`) that resume a session by id in the Account that holds it. Off by default."
151
+ },
148
152
  "shellInitFile": {
149
153
  "type": "object",
150
154
  "properties": {
@@ -0,0 +1,205 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "sideby/families.json",
4
+ "type": "object",
5
+ "required": [
6
+ "schemaVersion",
7
+ "families"
8
+ ],
9
+ "properties": {
10
+ "schemaVersion": {
11
+ "type": "number",
12
+ "const": 1
13
+ },
14
+ "families": {
15
+ "type": "array",
16
+ "items": {
17
+ "allOf": [
18
+ {
19
+ "type": "object",
20
+ "required": [
21
+ "id",
22
+ "title",
23
+ "bin",
24
+ "installUrl",
25
+ "installed",
26
+ "plugin"
27
+ ],
28
+ "properties": {
29
+ "id": {
30
+ "type": "string"
31
+ },
32
+ "title": {
33
+ "type": "string"
34
+ },
35
+ "bin": {
36
+ "type": "string"
37
+ },
38
+ "installUrl": {
39
+ "type": "string"
40
+ },
41
+ "installed": {
42
+ "type": "boolean"
43
+ },
44
+ "plugin": {
45
+ "type": "string"
46
+ }
47
+ }
48
+ },
49
+ {
50
+ "type": "object",
51
+ "required": [
52
+ "layout",
53
+ "selectVar",
54
+ "login",
55
+ "quota",
56
+ "usage",
57
+ "quotaSetup",
58
+ "shared"
59
+ ],
60
+ "properties": {
61
+ "layout": {
62
+ "type": "object",
63
+ "required": [
64
+ "main",
65
+ "account"
66
+ ],
67
+ "properties": {
68
+ "main": {
69
+ "type": "string"
70
+ },
71
+ "account": {
72
+ "type": "string"
73
+ }
74
+ },
75
+ "description": "Relative to HOME, for example `.claude` and `.claude-<name>`."
76
+ },
77
+ "selectVar": {
78
+ "type": "string",
79
+ "description": "Environment variable that selects a non-main Account directory."
80
+ },
81
+ "login": {
82
+ "type": "object",
83
+ "required": [
84
+ "args",
85
+ "hint"
86
+ ],
87
+ "properties": {
88
+ "args": {
89
+ "anyOf": [
90
+ {
91
+ "type": "array",
92
+ "items": {
93
+ "type": "string"
94
+ }
95
+ },
96
+ {
97
+ "type": "null"
98
+ }
99
+ ],
100
+ "description": "Arguments of the Host's sign-in command; null when sign-in happens inside the Host."
101
+ },
102
+ "hint": {
103
+ "type": "string"
104
+ }
105
+ }
106
+ },
107
+ "quota": {
108
+ "type": "boolean"
109
+ },
110
+ "usage": {
111
+ "type": "boolean"
112
+ },
113
+ "quotaSetup": {
114
+ "type": "boolean",
115
+ "description": "Quota needs `sideby quota setup <family>` once."
116
+ },
117
+ "shared": {
118
+ "type": "array",
119
+ "items": {
120
+ "type": "object",
121
+ "required": [
122
+ "path",
123
+ "mode",
124
+ "source",
125
+ "meaning"
126
+ ],
127
+ "properties": {
128
+ "path": {
129
+ "type": "string"
130
+ },
131
+ "mode": {
132
+ "anyOf": [
133
+ {
134
+ "type": "string",
135
+ "const": "link"
136
+ },
137
+ {
138
+ "type": "string",
139
+ "const": "copy"
140
+ },
141
+ {
142
+ "type": "string",
143
+ "const": "link-or-copy"
144
+ },
145
+ {
146
+ "type": "string",
147
+ "const": "link-or-local"
148
+ },
149
+ {
150
+ "type": "string",
151
+ "const": "local"
152
+ },
153
+ {
154
+ "type": "string",
155
+ "const": "local-if-api"
156
+ },
157
+ {
158
+ "type": "string",
159
+ "const": "info"
160
+ },
161
+ {
162
+ "type": "string",
163
+ "const": "json-key"
164
+ }
165
+ ]
166
+ },
167
+ "key": {
168
+ "type": "string"
169
+ },
170
+ "mainPath": {
171
+ "type": "string"
172
+ },
173
+ "credential": {
174
+ "type": "boolean"
175
+ },
176
+ "noSymlink": {
177
+ "type": "boolean"
178
+ },
179
+ "source": {
180
+ "anyOf": [
181
+ {
182
+ "type": "string",
183
+ "const": "family"
184
+ },
185
+ {
186
+ "type": "string",
187
+ "const": "config"
188
+ }
189
+ ]
190
+ },
191
+ "meaning": {
192
+ "type": "string",
193
+ "description": "What the item looks like in each Account, in one fixed sentence."
194
+ }
195
+ }
196
+ }
197
+ }
198
+ }
199
+ }
200
+ ]
201
+ }
202
+ }
203
+ },
204
+ "title": "sideby families --json"
205
+ }
package/schemas/new.json CHANGED
@@ -175,6 +175,24 @@
175
175
  }
176
176
  },
177
177
  "description": "One entry per config `shellInitFile`, rewritten after the change."
178
+ },
179
+ "suggestion": {
180
+ "type": "object",
181
+ "required": [
182
+ "name"
183
+ ],
184
+ "properties": {
185
+ "name": {
186
+ "type": "string"
187
+ },
188
+ "alias": {
189
+ "type": "string"
190
+ },
191
+ "aliasProblem": {
192
+ "type": "string"
193
+ }
194
+ },
195
+ "description": "Only with `--next`: the picked name, the short command that follows the existing pattern, or why that short command was left out."
178
196
  }
179
197
  },
180
198
  "title": "sideby new --json"