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.
- package/README.md +71 -413
- package/README.zh-CN.md +71 -414
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +13 -0
- package/dist/src/cli/commands.js +86 -7
- package/dist/src/cli/json-schemas.js +35 -0
- package/dist/src/cli/main.js +99 -9
- package/dist/src/core/accounts.d.ts +2 -0
- package/dist/src/core/accounts.js +15 -0
- package/dist/src/core/config.d.ts +1 -0
- package/dist/src/core/config.js +3 -0
- package/dist/src/core/launch.d.ts +3 -1
- package/dist/src/core/resume.d.ts +9 -0
- package/dist/src/core/resume.js +15 -0
- package/dist/src/core/share-meaning.d.ts +6 -0
- package/dist/src/core/share-meaning.js +32 -0
- package/dist/src/core/shell-init.d.ts +7 -1
- package/dist/src/core/shell-init.js +25 -2
- package/dist/src/desktop/icon.js +2 -2
- package/dist/src/families/claude/index.js +3 -0
- package/dist/src/families/codex/index.js +3 -0
- package/dist/src/families/grok/index.js +4 -0
- package/dist/src/families/shared/sessions.d.ts +9 -0
- package/dist/src/families/shared/sessions.js +70 -0
- package/dist/src/index.d.ts +1 -1
- package/dist/src/panel/handler.d.ts +0 -2
- package/dist/src/panel/handler.js +1 -16
- package/dist/src/panel/page-styles.js +14 -13
- package/dist/src/runtime.d.ts +48 -2
- package/dist/src/runtime.js +54 -1
- package/dist/src/types.d.ts +7 -0
- package/dist/testing/index.js +2 -1
- package/package.json +1 -1
- package/schemas/config.json +4 -0
- package/schemas/families.json +205 -0
- package/schemas/new.json +18 -0
- package/skills/sideby/SKILL.md +15 -6
- package/skills/sideby/references/accounts.md +1 -1
- package/skills/sideby/references/setup.md +31 -0
- 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
|
+
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
--
|
|
7
|
-
--
|
|
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: #
|
|
20
|
-
--text: #15171c; --muted: #
|
|
21
|
-
--primary: #
|
|
22
|
-
--
|
|
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: #
|
|
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: #
|
|
30
|
-
--text: #
|
|
31
|
-
--primary: #
|
|
32
|
-
--
|
|
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: #
|
|
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
|
/**
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -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
|
/**
|
package/dist/src/runtime.js
CHANGED
|
@@ -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
|
-
|
|
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' };
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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 {
|
package/dist/testing/index.js
CHANGED
|
@@ -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
|
-
|
|
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
package/schemas/config.json
CHANGED
|
@@ -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"
|