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,424 @@
|
|
|
1
|
+
// The one service layer the CLI and the Panel share. Frozen contract for v0.1 (plan: 审核修订).
|
|
2
|
+
import { discoverAccounts, resolveRef } from './core/accounts.js';
|
|
3
|
+
import { addConfigAlias, aliasAccount, aliasArgs, aliasProblem, aliasRefs, loadConfig, removeConfigAlias, } from './core/config.js';
|
|
4
|
+
import { createAccount } from './core/create.js';
|
|
5
|
+
import { applyFixes, runDoctor } from './core/doctor.js';
|
|
6
|
+
import { UserError } from './core/errors.js';
|
|
7
|
+
import { planHandoff } from './core/handoff.js';
|
|
8
|
+
import { identityOf } from './core/identity.js';
|
|
9
|
+
import { mergeDoctorHistory, readDoctorHistory, writeDoctorHistory, } from './core/last-doctor.js';
|
|
10
|
+
import { prepareLaunch, which } from './core/launch.js';
|
|
11
|
+
import { resolvePaths } from './core/paths.js';
|
|
12
|
+
import { findResumeRoute } from './core/resume.js';
|
|
13
|
+
import { shareMeaning } from './core/share-meaning.js';
|
|
14
|
+
import { SHELLS, shellInitScript, writeShellInitFile, } from './core/shell-init.js';
|
|
15
|
+
import { packageVersion } from './core/version.js';
|
|
16
|
+
import { BUILTIN_PLUGINS } from './families/index.js';
|
|
17
|
+
import { HookBus } from './plugins/bus.js';
|
|
18
|
+
import { loadPlugins } from './plugins/loader.js';
|
|
19
|
+
export class UnknownFamilyError extends UserError {
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A Family's identity reader, kept to the identity fields with the expected types. A reader that throws, or returns
|
|
23
|
+
* anything else, gives undefined: identity is a display nicety and never a problem of the Account.
|
|
24
|
+
*/
|
|
25
|
+
async function readIdentity(f, account) {
|
|
26
|
+
if (!f.identity || account.kind === 'api')
|
|
27
|
+
return undefined;
|
|
28
|
+
try {
|
|
29
|
+
const r = await f.identity(account);
|
|
30
|
+
return identityOf(r?.email, r?.org);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function createRuntime(opts = {}) {
|
|
37
|
+
const env = opts.env ?? process.env;
|
|
38
|
+
const paths = resolvePaths(env);
|
|
39
|
+
const config = await loadConfig(paths.configFile);
|
|
40
|
+
const bus = new HookBus();
|
|
41
|
+
const loaded = await loadPlugins({
|
|
42
|
+
builtins: opts.builtins ?? BUILTIN_PLUGINS,
|
|
43
|
+
config,
|
|
44
|
+
bus,
|
|
45
|
+
userPluginsDir: paths.userPluginsDir,
|
|
46
|
+
home: paths.home,
|
|
47
|
+
});
|
|
48
|
+
const families = loaded.families;
|
|
49
|
+
const ignore = new Set(config.ignore ?? []);
|
|
50
|
+
const now = opts.now ?? (() => new Date());
|
|
51
|
+
const familyOf = (id) => {
|
|
52
|
+
const f = families.get(id);
|
|
53
|
+
if (!f)
|
|
54
|
+
throw new UnknownFamilyError(`unknown family "${id}"; available: ${[...families.keys()].join(', ') || 'none (check `sideby plugins`)'}`);
|
|
55
|
+
return f;
|
|
56
|
+
};
|
|
57
|
+
// Merge `extraSharedItems` from the config into each Family's list.
|
|
58
|
+
for (const [id, extra] of Object.entries(config.extraSharedItems ?? {})) {
|
|
59
|
+
const f = families.get(id);
|
|
60
|
+
if (f)
|
|
61
|
+
families.set(id, { ...f, sharedItems: [...f.sharedItems, ...extra] });
|
|
62
|
+
}
|
|
63
|
+
const accounts = async () => {
|
|
64
|
+
const out = [];
|
|
65
|
+
for (const f of families.values())
|
|
66
|
+
out.push(...(await discoverAccounts(f, paths.home, ignore)));
|
|
67
|
+
return out;
|
|
68
|
+
};
|
|
69
|
+
const readContext = () => ({
|
|
70
|
+
home: paths.home,
|
|
71
|
+
now: now(),
|
|
72
|
+
stateDir: paths.stateDir,
|
|
73
|
+
env,
|
|
74
|
+
});
|
|
75
|
+
/** The config value of the alias `name`, from own keys only (`constructor` is not an alias unless configured). */
|
|
76
|
+
const ownAlias = (name) => config.aliases && Object.hasOwn(config.aliases, name) ? config.aliases[name] : undefined;
|
|
77
|
+
/** The config value of `ref` when it names an alias (no colon), as `resolveRef` reads it. */
|
|
78
|
+
const aliasValue = (ref) => (ref.includes(':') ? undefined : ownAlias(ref));
|
|
79
|
+
const aliasEdit = async (alias, status, value) => {
|
|
80
|
+
const args = aliasArgs(value);
|
|
81
|
+
const files = await rt.writeShellInitFiles();
|
|
82
|
+
return {
|
|
83
|
+
ok: files.every((f) => f.ok),
|
|
84
|
+
alias,
|
|
85
|
+
status,
|
|
86
|
+
...(value === undefined ? {} : { account: aliasAccount(value) }),
|
|
87
|
+
...(args.length ? { args } : {}),
|
|
88
|
+
...(files.length ? { shellInitFiles: files } : {}),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
const rt = {
|
|
92
|
+
paths,
|
|
93
|
+
config,
|
|
94
|
+
env,
|
|
95
|
+
families,
|
|
96
|
+
plugins: loaded.plugins,
|
|
97
|
+
pluginErrors: loaded.errors,
|
|
98
|
+
async familyInfo() {
|
|
99
|
+
return Promise.all([...families.values()].map(async (f) => ({
|
|
100
|
+
id: f.id,
|
|
101
|
+
title: f.title,
|
|
102
|
+
bin: f.bin,
|
|
103
|
+
installUrl: f.installUrl,
|
|
104
|
+
installed: (await which(f.bin, env)) !== null,
|
|
105
|
+
plugin: loaded.familyOwner.get(f.id) ?? 'unknown',
|
|
106
|
+
})));
|
|
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
|
+
},
|
|
134
|
+
accounts,
|
|
135
|
+
async resolve(ref) {
|
|
136
|
+
return resolveRef(ref, await accounts(), aliasRefs(config.aliases));
|
|
137
|
+
},
|
|
138
|
+
async status(account) {
|
|
139
|
+
const f = familyOf(account.family);
|
|
140
|
+
const problems = [];
|
|
141
|
+
// Plugin readers may throw synchronously or reject; either way the Account still gets a status.
|
|
142
|
+
const safe = async (what, fn) => {
|
|
143
|
+
if (!fn)
|
|
144
|
+
return undefined;
|
|
145
|
+
try {
|
|
146
|
+
return await fn();
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
problems.push(`${what}: ${err?.message ?? String(err)}`);
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
const [installed, login, model, identity] = await Promise.all([
|
|
154
|
+
which(f.bin, env),
|
|
155
|
+
account.kind === 'api'
|
|
156
|
+
? undefined
|
|
157
|
+
: safe('login state', f.loginState && (() => f.loginState(account))),
|
|
158
|
+
safe('model', f.model && (() => f.model(account))),
|
|
159
|
+
readIdentity(f, account),
|
|
160
|
+
]);
|
|
161
|
+
return {
|
|
162
|
+
...account,
|
|
163
|
+
familyTitle: f.title,
|
|
164
|
+
hostInstalled: installed !== null,
|
|
165
|
+
login: account.kind === 'api' ? 'not-needed' : (login ?? 'unknown'),
|
|
166
|
+
...(model ? { model } : {}),
|
|
167
|
+
...(identity ? { identity } : {}),
|
|
168
|
+
...(problems.length ? { problems } : {}),
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
async doctor(o = {}) {
|
|
172
|
+
return (await rt.doctorWithHistory(o)).report;
|
|
173
|
+
},
|
|
174
|
+
async doctorWithHistory(o = {}) {
|
|
175
|
+
let list = await accounts();
|
|
176
|
+
let fams = families;
|
|
177
|
+
let scope = { kind: 'all' };
|
|
178
|
+
if (o.target) {
|
|
179
|
+
if (families.has(o.target)) {
|
|
180
|
+
list = list.filter((a) => a.family === o.target);
|
|
181
|
+
fams = new Map([[o.target, families.get(o.target)]]);
|
|
182
|
+
scope = { kind: 'family', family: o.target };
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
const one = resolveRef(o.target, list, aliasRefs(config.aliases));
|
|
186
|
+
list = [one];
|
|
187
|
+
fams = new Map([[one.family, familyOf(one.family)]]);
|
|
188
|
+
scope = { kind: 'account', ref: one.ref };
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const input = {
|
|
192
|
+
home: paths.home,
|
|
193
|
+
families: fams,
|
|
194
|
+
accounts: list,
|
|
195
|
+
bus,
|
|
196
|
+
force: Boolean(o.force),
|
|
197
|
+
// Checked for every Family in the run, also when Doctor is aimed at one Account, so the saved result
|
|
198
|
+
// of that Family keeps its skill warning.
|
|
199
|
+
skillVersion: packageVersion(),
|
|
200
|
+
};
|
|
201
|
+
let report = await runDoctor(input);
|
|
202
|
+
if (o.fix) {
|
|
203
|
+
const fixes = await applyFixes(report);
|
|
204
|
+
report = { ...(await runDoctor(input)), fixes };
|
|
205
|
+
}
|
|
206
|
+
const history = mergeDoctorHistory(await readDoctorHistory(paths.stateDir), report, scope, now());
|
|
207
|
+
if (o.persist !== false)
|
|
208
|
+
await writeDoctorHistory(paths.stateDir, history);
|
|
209
|
+
return { report, history };
|
|
210
|
+
},
|
|
211
|
+
doctorHistory: () => readDoctorHistory(paths.stateDir),
|
|
212
|
+
async quotaSetups() {
|
|
213
|
+
const out = [];
|
|
214
|
+
for (const f of families.values()) {
|
|
215
|
+
if (!f.quotaSetup)
|
|
216
|
+
continue;
|
|
217
|
+
const setup = rt.quotaSetup(f.id);
|
|
218
|
+
out.push({ family: f.id, summary: setup.summary, plan: await setup.plan() });
|
|
219
|
+
}
|
|
220
|
+
return out;
|
|
221
|
+
},
|
|
222
|
+
quotaSetup(familyId) {
|
|
223
|
+
const f = familyOf(familyId);
|
|
224
|
+
const setup = f.quotaSetup;
|
|
225
|
+
if (!setup)
|
|
226
|
+
throw new UserError(`${f.title} needs no quota setup`);
|
|
227
|
+
// One rule for every adapter: a setup that throws is reported, never crashes the caller. The message
|
|
228
|
+
// is the error's own text; setup implementations must not put file contents into it.
|
|
229
|
+
const blocked = (err) => ({
|
|
230
|
+
status: 'blocked',
|
|
231
|
+
file: '',
|
|
232
|
+
diff: '',
|
|
233
|
+
message: err?.message ?? String(err),
|
|
234
|
+
});
|
|
235
|
+
return {
|
|
236
|
+
family: f.id,
|
|
237
|
+
summary: setup.summary,
|
|
238
|
+
// Promise.resolve().then also catches a setup that throws synchronously.
|
|
239
|
+
plan: () => Promise.resolve()
|
|
240
|
+
.then(() => setup.plan(readContext()))
|
|
241
|
+
.catch(blocked),
|
|
242
|
+
apply: () => Promise.resolve()
|
|
243
|
+
.then(() => setup.apply(readContext()))
|
|
244
|
+
.catch(blocked),
|
|
245
|
+
teardown: () => Promise.resolve()
|
|
246
|
+
.then(() => setup.teardown(readContext()))
|
|
247
|
+
.catch((err) => ({
|
|
248
|
+
ok: false,
|
|
249
|
+
message: err?.message ?? String(err),
|
|
250
|
+
})),
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
async createAccount(familyId, name, o = {}) {
|
|
254
|
+
const family = familyOf(familyId);
|
|
255
|
+
const alias = o.alias?.trim() || undefined;
|
|
256
|
+
const ref = `${family.id}:${name}`;
|
|
257
|
+
if (alias) {
|
|
258
|
+
const problem = rt.aliasProblem(alias, ref);
|
|
259
|
+
if (problem)
|
|
260
|
+
throw new UserError(`${problem}; nothing was created`, { code: 'alias-invalid' });
|
|
261
|
+
}
|
|
262
|
+
const created = await createAccount({
|
|
263
|
+
family,
|
|
264
|
+
name,
|
|
265
|
+
api: Boolean(o.api),
|
|
266
|
+
home: paths.home,
|
|
267
|
+
bus,
|
|
268
|
+
accounts: await discoverAccounts(family, paths.home, ignore),
|
|
269
|
+
});
|
|
270
|
+
if (alias) {
|
|
271
|
+
try {
|
|
272
|
+
// An alias that already starts this Account keeps its arguments; creating the Account does not change them.
|
|
273
|
+
const r = await addConfigAlias(paths.configFile, alias, created.account.ref, aliasArgs(ownAlias(alias)));
|
|
274
|
+
config.aliases = r.aliases;
|
|
275
|
+
created.alias = {
|
|
276
|
+
name: alias,
|
|
277
|
+
added: true,
|
|
278
|
+
...(r.status === 'exists' ? { message: 'already in the config' } : {}),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
created.alias = {
|
|
283
|
+
name: alias,
|
|
284
|
+
added: false,
|
|
285
|
+
message: `${created.account.ref} was created, but the alias was not added: ${err.message}`,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const files = await rt.writeShellInitFiles();
|
|
290
|
+
if (files.length)
|
|
291
|
+
created.shellInitFiles = files;
|
|
292
|
+
created.ok = created.ok && created.alias?.added !== false && files.every((f) => f.ok);
|
|
293
|
+
return created;
|
|
294
|
+
},
|
|
295
|
+
async addAlias(alias, target, args = []) {
|
|
296
|
+
const bad = aliasProblem(alias);
|
|
297
|
+
if (bad)
|
|
298
|
+
throw new UserError(`alias "${alias}" ${bad} (it becomes a shell function name)`);
|
|
299
|
+
// Resolve first so the config only ever names an Account that exists, by its ref.
|
|
300
|
+
const account = resolveRef(target, await accounts(), aliasRefs(config.aliases));
|
|
301
|
+
const r = await addConfigAlias(paths.configFile, alias, account.ref, args);
|
|
302
|
+
config.aliases = r.aliases;
|
|
303
|
+
return aliasEdit(alias, r.status, r.aliases[alias]);
|
|
304
|
+
},
|
|
305
|
+
async removeAlias(alias) {
|
|
306
|
+
const before = ownAlias(alias);
|
|
307
|
+
const r = await removeConfigAlias(paths.configFile, alias);
|
|
308
|
+
config.aliases = r.aliases;
|
|
309
|
+
return aliasEdit(alias, r.status, r.status === 'removed' ? before : undefined);
|
|
310
|
+
},
|
|
311
|
+
aliasProblem(alias, ref) {
|
|
312
|
+
const bad = aliasProblem(alias);
|
|
313
|
+
if (bad)
|
|
314
|
+
return `alias "${alias}" ${bad} (it becomes a shell function name)`;
|
|
315
|
+
const value = ownAlias(alias);
|
|
316
|
+
const taken = value === undefined ? undefined : aliasAccount(value);
|
|
317
|
+
if (taken !== undefined && taken !== ref)
|
|
318
|
+
return `alias "${alias}" already starts ${taken}; pick another name`;
|
|
319
|
+
return null;
|
|
320
|
+
},
|
|
321
|
+
async shellInitScript() {
|
|
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);
|
|
330
|
+
},
|
|
331
|
+
async writeShellInitFiles(shells = SHELLS) {
|
|
332
|
+
const out = [];
|
|
333
|
+
for (const shell of shells) {
|
|
334
|
+
const file = config.shellInitFile?.[shell];
|
|
335
|
+
if (file)
|
|
336
|
+
out.push(await writeShellInitFile(shell, file, paths.home, await rt.shellInitScript(shell)));
|
|
337
|
+
}
|
|
338
|
+
return out;
|
|
339
|
+
},
|
|
340
|
+
async prepareLaunch(ref, userArgs, command) {
|
|
341
|
+
const account = resolveRef(ref, await accounts(), aliasRefs(config.aliases));
|
|
342
|
+
return prepareLaunch({
|
|
343
|
+
account,
|
|
344
|
+
family: familyOf(account.family),
|
|
345
|
+
baseEnv: env,
|
|
346
|
+
accountArgs: config.accounts?.[account.ref]?.args ?? [],
|
|
347
|
+
// An alias's arguments come before the user's; the Family sees both as the user's choice (ADR-0005).
|
|
348
|
+
userArgs: command === 'run' ? [...aliasArgs(aliasValue(ref)), ...userArgs] : userArgs,
|
|
349
|
+
command,
|
|
350
|
+
bus,
|
|
351
|
+
});
|
|
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
|
+
},
|
|
371
|
+
async quota(account) {
|
|
372
|
+
if (account.kind === 'api')
|
|
373
|
+
return { status: 'unavailable', reason: 'api-account' };
|
|
374
|
+
const f = familyOf(account.family);
|
|
375
|
+
if (!f.readQuota)
|
|
376
|
+
return { status: 'unavailable', reason: 'no-source' };
|
|
377
|
+
try {
|
|
378
|
+
return await f.readQuota(account, readContext());
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
return { status: 'unavailable', reason: 'unrecognized', detail: err.message };
|
|
382
|
+
}
|
|
383
|
+
},
|
|
384
|
+
async usage(account) {
|
|
385
|
+
const f = familyOf(account.family);
|
|
386
|
+
if (!f.readUsage)
|
|
387
|
+
return { status: 'unavailable', reason: 'no-source' };
|
|
388
|
+
try {
|
|
389
|
+
return await f.readUsage(account, readContext());
|
|
390
|
+
}
|
|
391
|
+
catch (err) {
|
|
392
|
+
return { status: 'unavailable', reason: 'unrecognized', detail: err.message };
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
async handoff(familyId, o = {}) {
|
|
396
|
+
const f = familyOf(familyId);
|
|
397
|
+
const list = (await accounts()).filter((a) => a.family === f.id);
|
|
398
|
+
if (!f.readQuota)
|
|
399
|
+
throw new UserError(`${f.title} has no public quota source, so sideby cannot tell which account has room; pick one yourself: ${list.map((a) => `sideby run ${a.ref}`).join(', ') || `sideby new ${f.id} <name>`}`, { code: 'no-quota-source' });
|
|
400
|
+
if (list.length === 0)
|
|
401
|
+
throw new UserError(`no ${f.title} accounts yet; create one with \`sideby new ${f.id} <name>\``, {
|
|
402
|
+
code: 'no-accounts',
|
|
403
|
+
});
|
|
404
|
+
// Only the login state and Quota: no identity, model or Host lookups are needed to rank.
|
|
405
|
+
const loginOf = async (a) => {
|
|
406
|
+
if (a.kind === 'api')
|
|
407
|
+
return 'not-needed';
|
|
408
|
+
try {
|
|
409
|
+
return (await f.loginState?.(a)) ?? 'unknown';
|
|
410
|
+
}
|
|
411
|
+
catch {
|
|
412
|
+
return 'unknown';
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
const candidates = await Promise.all(list.map(async (a) => {
|
|
416
|
+
const [login, quota] = await Promise.all([loginOf(a), rt.quota(a)]);
|
|
417
|
+
return { ref: a.ref, name: a.name, isMain: a.isMain, kind: a.kind, login, quota };
|
|
418
|
+
}));
|
|
419
|
+
return planHandoff(f.id, candidates, readContext().now, o);
|
|
420
|
+
},
|
|
421
|
+
readContext,
|
|
422
|
+
};
|
|
423
|
+
return rt;
|
|
424
|
+
}
|