sideby 0.11.0 → 0.12.0
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 +2 -2
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +7 -0
- package/dist/src/cli/commands.js +94 -2
- package/dist/src/cli/json-schemas.js +25 -0
- package/dist/src/cli/main.js +104 -43
- package/dist/src/core/brand-mark.d.ts +75 -21
- package/dist/src/core/brand-mark.js +80 -34
- package/dist/src/core/config.d.ts +16 -0
- package/dist/src/core/config.js +17 -2
- package/dist/src/core/update.d.ts +209 -0
- package/dist/src/core/update.js +631 -0
- package/dist/src/desktop/icon.js +292 -61
- package/dist/src/panel/background.d.ts +2 -0
- package/dist/src/panel/background.js +7 -0
- package/dist/src/panel/handler.d.ts +32 -0
- package/dist/src/panel/handler.js +95 -5
- package/dist/src/panel/i18n.d.ts +29 -0
- package/dist/src/panel/i18n.js +58 -0
- package/dist/src/panel/page-script.js +133 -1
- package/dist/src/panel/page-styles.js +6 -2
- package/dist/src/panel/page.d.ts +2 -0
- package/dist/src/panel/page.js +4 -2
- package/dist/testing/index.js +2 -0
- package/package.json +2 -2
- package/schemas/config.json +15 -0
- package/schemas/update.json +115 -0
- package/skills/sideby/SKILL.md +5 -1
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@ sideby quota setup claude --yes # Claude quota in the panel; undo: sideby quota
|
|
|
29
29
|
sideby ui # every account's quota at http://127.0.0.1:17420
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
`sideby run claude:main` keeps using `~/.claude` in another terminal at the same time. Arguments after `--` go to the Host: `sideby run work -- --resume`. To try it without installing, run `npx sideby`.
|
|
32
|
+
`sideby update` installs a newer sideby when there is one; sideby says so after a command and in the panel. `sideby run claude:main` keeps using `~/.claude` in another terminal at the same time. Arguments after `--` go to the Host: `sideby run work -- --resume`. To try it without installing, run `npx sideby`.
|
|
33
33
|
|
|
34
34
|
## Set up with your AI agent
|
|
35
35
|
|
|
@@ -110,7 +110,7 @@ macOS and Linux. CI runs the suite on `ubuntu-latest` and `macos-latest`, Node 2
|
|
|
110
110
|
## Safe by design
|
|
111
111
|
|
|
112
112
|
- It never edits a Host's global config to switch accounts. It moves a task to another account only when you turn on Auto Handoff, and never proxies requests or pools logins.
|
|
113
|
-
- It never reads, prints or sends a credential value. Nothing leaves your machine: sideby
|
|
113
|
+
- It never reads, prints or sends a credential value. Nothing about your accounts leaves your machine: the only request sideby makes on its own is a daily check of the npm registry for its newest version (off with `NO_UPDATE_NOTIFIER=1`; see [Updates](docs/guide/configuration.md#updates)).
|
|
114
114
|
- `doctor --fix` never replaces a real file and never changes where a link points.
|
|
115
115
|
|
|
116
116
|
Details and the terms-of-service question are in the [FAQ](docs/guide/faq.md).
|
package/dist/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
1
|
+
{"id":"20261016T071613-e5f0f1"}
|
|
@@ -61,6 +61,13 @@ export declare function cmdHandoff(a: ParsedArgs, io: Io): Promise<number>;
|
|
|
61
61
|
export declare function cmdPlugins(a: ParsedArgs, io: Io): Promise<number>;
|
|
62
62
|
export declare function cmdShellInit(a: ParsedArgs, io: Io): Promise<number>;
|
|
63
63
|
export declare function cmdUi(a: ParsedArgs, io: Io): Promise<number>;
|
|
64
|
+
/**
|
|
65
|
+
* `sideby update [--yes] [--json]` (spec §3.23): asks the registry now, shows what an Update runs, and runs it with
|
|
66
|
+
* `--yes` (exit 10 before). An install sideby cannot update gets the command to run and exit 1.
|
|
67
|
+
*/
|
|
68
|
+
export declare function cmdUpdate(a: ParsedArgs, io: Io): Promise<number>;
|
|
69
|
+
/** The internal `sideby update-check` the CLI starts detached: one Update Check, then Auto Update if it is on. */
|
|
70
|
+
export declare function cmdUpdateCheck(_a: ParsedArgs, _io: Io): Promise<number>;
|
|
64
71
|
export declare function cmdApp(a: ParsedArgs, io: Io): Promise<number>;
|
|
65
72
|
/**
|
|
66
73
|
* `sideby desktop <account>`: the Family's Desktop App as that Account (spec §3.20); `sideby desktop grok-bot:<name>`:
|
package/dist/src/cli/commands.js
CHANGED
|
@@ -17,8 +17,22 @@ import { suggestAlias, suggestName } from '../panel/page-logic.js';
|
|
|
17
17
|
import { createRuntime, UnknownFamilyError } from '../runtime.js';
|
|
18
18
|
import { UsageError } from './args.js';
|
|
19
19
|
import { c, clock, compactNumber, quotaText, table, tuningLines, usageText } from './format.js';
|
|
20
|
+
/** Runs `fn` while a record under the state directory says a Host runs (also for `login`), so an Auto Update waits (spec §3.23). */
|
|
21
|
+
async function whileRunning(rt, fn) {
|
|
22
|
+
const { markRunning } = await import('../core/update.js');
|
|
23
|
+
const done = await markRunning(rt.paths);
|
|
24
|
+
try {
|
|
25
|
+
return await fn();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await done().catch(() => { });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
20
31
|
/** Runs the Account with Auto Handoff when it applies (spec §3.17), else the Host as usual. */
|
|
21
32
|
async function runAccount(rt, ref, args, io) {
|
|
33
|
+
return whileRunning(rt, () => runAccountNow(rt, ref, args, io));
|
|
34
|
+
}
|
|
35
|
+
async function runAccountNow(rt, ref, args, io) {
|
|
22
36
|
const handed = await runWithHandoff(rt, ref, args, {
|
|
23
37
|
err: (l) => io.err(c.dim(l)),
|
|
24
38
|
stdin: io.stdin ?? process.stdin,
|
|
@@ -208,7 +222,7 @@ export async function cmdRun(a, io, command) {
|
|
|
208
222
|
const prepared = await rt.prepareLaunch(ref, [...extra, ...a.rest], command);
|
|
209
223
|
if (prepared.notice)
|
|
210
224
|
io.err(c.cyan(prepared.notice));
|
|
211
|
-
return runHost(prepared);
|
|
225
|
+
return whileRunning(rt, () => runHost(prepared));
|
|
212
226
|
}
|
|
213
227
|
/**
|
|
214
228
|
* `sideby resume <family>`: what the Host-named shell function calls (spec §3.15). Starts the Account that holds the
|
|
@@ -224,7 +238,7 @@ export async function cmdResume(a, io) {
|
|
|
224
238
|
io.err(c.dim(`sideby: session ${route.sessionId} was never saved; starting a new session in ${route.account.ref}`));
|
|
225
239
|
else if (route.account)
|
|
226
240
|
io.err(c.dim(`sideby: session ${route.sessionId} is in ${route.account.ref}; starting it there`));
|
|
227
|
-
return runHost(route.launch);
|
|
241
|
+
return whileRunning(rt, () => runHost(route.launch));
|
|
228
242
|
}
|
|
229
243
|
/** Quotes a Host argument for a command line the user may paste; plain words stay as they are. */
|
|
230
244
|
function shellQuote(arg) {
|
|
@@ -1003,6 +1017,84 @@ export async function cmdUi(a, io) {
|
|
|
1003
1017
|
});
|
|
1004
1018
|
return 0;
|
|
1005
1019
|
}
|
|
1020
|
+
/**
|
|
1021
|
+
* `sideby update [--yes] [--json]` (spec §3.23): asks the registry now, shows what an Update runs, and runs it with
|
|
1022
|
+
* `--yes` (exit 10 before). An install sideby cannot update gets the command to run and exit 1.
|
|
1023
|
+
*/
|
|
1024
|
+
export async function cmdUpdate(a, io) {
|
|
1025
|
+
if (a.positionals.length)
|
|
1026
|
+
throw new UsageError('usage: sideby update [--yes] [--json]');
|
|
1027
|
+
const u = await import('../core/update.js');
|
|
1028
|
+
const { SKILL_INSTALL } = await import('../core/skill-check.js');
|
|
1029
|
+
const { backgroundPanelPort } = await import('../panel/background.js');
|
|
1030
|
+
const paths = resolvePaths(process.env);
|
|
1031
|
+
const asJson = a.flags.has('json');
|
|
1032
|
+
const plan = await u.planUpdate(paths, process.env);
|
|
1033
|
+
const shown = u.shownPlan(plan);
|
|
1034
|
+
if (!plan.available) {
|
|
1035
|
+
if (asJson)
|
|
1036
|
+
json(io, { ...shown, updated: false });
|
|
1037
|
+
else
|
|
1038
|
+
io.out(`${c.green('✓')} sideby ${plan.current} is the latest version.`);
|
|
1039
|
+
return 0;
|
|
1040
|
+
}
|
|
1041
|
+
if (!plan.canRun) {
|
|
1042
|
+
if (asJson)
|
|
1043
|
+
json(io, { ...shown, updated: false });
|
|
1044
|
+
else {
|
|
1045
|
+
io.out(`sideby ${c.bold(plan.latest)} is available (you have ${plan.current}). What changed: ${plan.releaseNotes}`);
|
|
1046
|
+
io.err(c.yellow(`sideby cannot update itself: ${plan.method.reason}. Run it yourself:\n ${plan.command}`));
|
|
1047
|
+
}
|
|
1048
|
+
return 1;
|
|
1049
|
+
}
|
|
1050
|
+
if (!a.flags.has('yes')) {
|
|
1051
|
+
if (asJson)
|
|
1052
|
+
json(io, { ...shown, updated: false });
|
|
1053
|
+
else {
|
|
1054
|
+
io.out(`sideby ${c.bold(plan.latest)} is available (you have ${plan.current}). What changed: ${plan.releaseNotes}`);
|
|
1055
|
+
io.out(`\nUpdating runs:\n ${plan.command}`);
|
|
1056
|
+
io.out(c.cyan('\nRun it with `sideby update --yes`.'));
|
|
1057
|
+
}
|
|
1058
|
+
return 10;
|
|
1059
|
+
}
|
|
1060
|
+
if (!asJson)
|
|
1061
|
+
io.out(c.dim(`$ ${plan.command}`));
|
|
1062
|
+
const r = await u.applyUpdate(paths, plan, process.env, {
|
|
1063
|
+
...(asJson ? {} : { onOutput: (text) => process.stderr.write(text) }),
|
|
1064
|
+
panelPort: () => backgroundPanelPort(paths),
|
|
1065
|
+
});
|
|
1066
|
+
if (asJson) {
|
|
1067
|
+
json(io, {
|
|
1068
|
+
...shown,
|
|
1069
|
+
updated: r.ok,
|
|
1070
|
+
message: r.message,
|
|
1071
|
+
restartedPanel: r.restartedPanel,
|
|
1072
|
+
skill: SKILL_INSTALL,
|
|
1073
|
+
...(r.ok ? {} : { output: r.output }),
|
|
1074
|
+
});
|
|
1075
|
+
return r.ok ? 0 : 1;
|
|
1076
|
+
}
|
|
1077
|
+
if (!r.ok) {
|
|
1078
|
+
io.err(c.red(`✗ ${r.message}`));
|
|
1079
|
+
return 1;
|
|
1080
|
+
}
|
|
1081
|
+
io.out(`${c.green('✓')} ${r.message}.`);
|
|
1082
|
+
if (r.restartedPanel)
|
|
1083
|
+
io.out(' The background panel is restarting with the new version; open pages reload by themselves.');
|
|
1084
|
+
io.out(' A `sideby ui` running in a terminal and sessions already running keep the old version until you restart them.');
|
|
1085
|
+
io.out(` If you use the sideby skill, update it too: ${SKILL_INSTALL}`);
|
|
1086
|
+
return 0;
|
|
1087
|
+
}
|
|
1088
|
+
/** The internal `sideby update-check` the CLI starts detached: one Update Check, then Auto Update if it is on. */
|
|
1089
|
+
export async function cmdUpdateCheck(_a, _io) {
|
|
1090
|
+
const { backgroundCheck } = await import('../core/update.js');
|
|
1091
|
+
const { loadConfig } = await import('../core/config.js');
|
|
1092
|
+
const { backgroundPanelPort } = await import('../panel/background.js');
|
|
1093
|
+
const paths = resolvePaths(process.env);
|
|
1094
|
+
const config = await loadConfig(paths.configFile).catch(() => ({}));
|
|
1095
|
+
await backgroundCheck(paths, process.env, config, () => backgroundPanelPort(paths));
|
|
1096
|
+
return 0;
|
|
1097
|
+
}
|
|
1006
1098
|
export async function cmdApp(a, io) {
|
|
1007
1099
|
const sub = a.positionals[0];
|
|
1008
1100
|
const usage = 'usage: sideby app install [--url <url>] | sideby app uninstall';
|
|
@@ -491,6 +491,31 @@ export const OUTPUT_SCHEMAS = {
|
|
|
491
491
|
description: 'False when only the plan was shown; run again with --yes (exit 10).',
|
|
492
492
|
}),
|
|
493
493
|
}, { title: 'sideby desktop grok-bot:<name> --remove --json' }),
|
|
494
|
+
update: Type.Object({
|
|
495
|
+
schemaVersion: Version,
|
|
496
|
+
current: Type.String({ description: 'The running sideby version.' }),
|
|
497
|
+
latest: Type.String({ description: 'The `latest` version on the npm registry.' }),
|
|
498
|
+
available: Type.Boolean({ description: '`latest` is newer than `current`.' }),
|
|
499
|
+
method: Type.Object({
|
|
500
|
+
kind: Type.Union(['npm', 'pnpm', 'bun', 'volta', 'npx', 'project', 'source'].map((k) => Type.Literal(k)), { description: 'How this copy was installed.' }),
|
|
501
|
+
canUpdate: Type.Boolean({ description: 'sideby can run the Update itself.' }),
|
|
502
|
+
reason: Type.Optional(Type.String({ description: 'Why it cannot.' })),
|
|
503
|
+
}),
|
|
504
|
+
command: Type.String({
|
|
505
|
+
description: 'What an Update runs, or what to run by hand when `canRun` is false.',
|
|
506
|
+
}),
|
|
507
|
+
canRun: Type.Boolean(),
|
|
508
|
+
releaseNotes: Type.String({ description: 'The release notes of `latest`.' }),
|
|
509
|
+
updated: Type.Boolean({
|
|
510
|
+
description: 'False when only the plan was shown (exit 10 with --yes still to add) or nothing to do.',
|
|
511
|
+
}),
|
|
512
|
+
message: Type.Optional(Type.String()),
|
|
513
|
+
restartedPanel: Type.Optional(Type.Boolean({
|
|
514
|
+
description: 'A background Panel was running and starts again with the new version.',
|
|
515
|
+
})),
|
|
516
|
+
skill: Type.Optional(Type.String({ description: 'The command that updates the sideby skill; sideby never runs it.' })),
|
|
517
|
+
output: Type.Optional(Type.String({ description: 'What the failed install command printed.' })),
|
|
518
|
+
}, { title: 'sideby update --json' }),
|
|
494
519
|
error: Type.Object({
|
|
495
520
|
schemaVersion: Version,
|
|
496
521
|
error: Type.String({ description: 'What went wrong and what to do next.' }),
|
package/dist/src/cli/main.js
CHANGED
|
@@ -58,10 +58,11 @@ Usage:
|
|
|
58
58
|
sideby shell-init [zsh|bash] --write
|
|
59
59
|
rewrite the config's shellInitFile with them
|
|
60
60
|
sideby plugins list loaded plugins and load errors
|
|
61
|
+
sideby update [--yes] update sideby to the newest version (shows the command first)
|
|
61
62
|
|
|
62
63
|
Options:
|
|
63
64
|
--json machine-readable output (list, new, next, alias, doctor, quota, tune, model, remote-control, handoff,
|
|
64
|
-
families, plugins, app, desktop)
|
|
65
|
+
families, plugins, app, desktop, update)
|
|
65
66
|
-h, --help show this help; \`sideby <command> --help\` for new, alias, login, doctor, families, desktop
|
|
66
67
|
-v, --version show the version
|
|
67
68
|
|
|
@@ -73,7 +74,10 @@ For AI agents:
|
|
|
73
74
|
Setup steps and the full contract: https://github.com/Adonis0123/sideby/blob/main/llms.txt
|
|
74
75
|
Run everything else yourself, but hand the user these as one line each: \`sideby login <account>\`
|
|
75
76
|
(browser sign-in), filling in proxy.env, and \`sideby run\` or \`sideby next\` (interactive sessions).
|
|
76
|
-
Exit 10 means a change was shown: ask the user, then run it again with --yes
|
|
77
|
+
Exit 10 means a change was shown: ask the user, then run it again with --yes.
|
|
78
|
+
|
|
79
|
+
Updates: once a day sideby asks the npm registry for its newest version and says so after a command.
|
|
80
|
+
Turn it off with NO_UPDATE_NOTIFIER=1 or config "update": { "check": false }; "auto": true installs by itself.`;
|
|
77
81
|
/** `sideby <command> --help` for the commands agents use to set up and repair accounts (spec §3.16). */
|
|
78
82
|
const COMMAND_HELP = {
|
|
79
83
|
new: `sideby new <family> <name> [--api] [--alias <short>] [--json]
|
|
@@ -203,7 +207,54 @@ const COMMANDS = {
|
|
|
203
207
|
'shell-init': { flags: ['write'] },
|
|
204
208
|
plugins: { flags: ['json'] },
|
|
205
209
|
families: { flags: ['json'] },
|
|
210
|
+
update: { flags: ['json', 'yes'] },
|
|
211
|
+
'update-check': { flags: [] },
|
|
206
212
|
};
|
|
213
|
+
/** Commands that never print an Update Notice: their output is read by a shell or an agent, or they are internal. */
|
|
214
|
+
function noticeAllowed(cmd, parsed) {
|
|
215
|
+
if (cmd === 'shell-init' || cmd === 'update' || cmd === 'update-check')
|
|
216
|
+
return false;
|
|
217
|
+
if (cmd === 'ui' && parsed.flags.has('serve-detached'))
|
|
218
|
+
return false;
|
|
219
|
+
if (cmd === 'handoff' && parsed.positionals[0] === 'ready')
|
|
220
|
+
return false;
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* After a command (spec §3.23): print the Update Notice and the result of an Auto Update, each at most once, and
|
|
225
|
+
* start the next Update Check detached when the last one is a day old. Only on a terminal; never fails the command.
|
|
226
|
+
*/
|
|
227
|
+
async function afterCommand() {
|
|
228
|
+
if (!process.stderr.isTTY)
|
|
229
|
+
return;
|
|
230
|
+
try {
|
|
231
|
+
const u = await import('../core/update.js');
|
|
232
|
+
const { resolvePaths } = await import('../core/paths.js');
|
|
233
|
+
const { loadConfig } = await import('../core/config.js');
|
|
234
|
+
const { packageVersion } = await import('../core/version.js');
|
|
235
|
+
const paths = resolvePaths(process.env);
|
|
236
|
+
const config = await loadConfig(paths.configFile).catch(() => ({}));
|
|
237
|
+
const cache = await u.readUpdateCache(paths);
|
|
238
|
+
const plan = u.afterCommandPlan(cache, packageVersion(), config, process.env, new Date());
|
|
239
|
+
const dim = process.env.NO_COLOR ? (t) => t : (t) => `\x1b[2m${t}\x1b[22m`;
|
|
240
|
+
for (const l of plan.lines)
|
|
241
|
+
console.error(dim(l));
|
|
242
|
+
if (plan.record)
|
|
243
|
+
await u.updateUpdateCache(paths, plan.record);
|
|
244
|
+
if (plan.startCheck) {
|
|
245
|
+
const { spawn } = await import('node:child_process');
|
|
246
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), 'update-check'], {
|
|
247
|
+
detached: true,
|
|
248
|
+
stdio: 'ignore',
|
|
249
|
+
});
|
|
250
|
+
child.on('error', () => { });
|
|
251
|
+
child.unref();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// an Update Notice is never worth failing a command
|
|
256
|
+
}
|
|
257
|
+
}
|
|
207
258
|
export async function main(argv) {
|
|
208
259
|
const [first, ...rest] = argv;
|
|
209
260
|
// Claude Code runs this on every status-line refresh. It must still launch the original command when Node is
|
|
@@ -274,47 +325,10 @@ export async function main(argv) {
|
|
|
274
325
|
return 0;
|
|
275
326
|
}
|
|
276
327
|
const cmds = await import('./commands.js');
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return await cmds.cmdRun(parsed, io, 'run');
|
|
282
|
-
case 'login':
|
|
283
|
-
return await cmds.cmdRun(parsed, io, 'login');
|
|
284
|
-
case 'resume':
|
|
285
|
-
return await cmds.cmdResume(parsed, io);
|
|
286
|
-
case 'next':
|
|
287
|
-
return await cmds.cmdNext(parsed, io);
|
|
288
|
-
case 'new':
|
|
289
|
-
return await cmds.cmdNew(parsed, io);
|
|
290
|
-
case 'alias':
|
|
291
|
-
return await cmds.cmdAlias(parsed, io);
|
|
292
|
-
case 'doctor':
|
|
293
|
-
return await cmds.cmdDoctor(parsed, io);
|
|
294
|
-
case 'quota':
|
|
295
|
-
return await cmds.cmdQuota(parsed, io);
|
|
296
|
-
case 'tune':
|
|
297
|
-
return await cmds.cmdTune(parsed, io);
|
|
298
|
-
case 'model':
|
|
299
|
-
return await cmds.cmdModel(parsed, io);
|
|
300
|
-
case 'remote-control':
|
|
301
|
-
return await cmds.cmdRemoteControl(parsed, io);
|
|
302
|
-
case 'handoff':
|
|
303
|
-
return await cmds.cmdHandoff(parsed, io);
|
|
304
|
-
case 'ui':
|
|
305
|
-
return await cmds.cmdUi(parsed, io);
|
|
306
|
-
case 'app':
|
|
307
|
-
return await cmds.cmdApp(parsed, io);
|
|
308
|
-
case 'desktop':
|
|
309
|
-
return await cmds.cmdDesktop(parsed, io);
|
|
310
|
-
case 'shell-init':
|
|
311
|
-
return await cmds.cmdShellInit(parsed, io);
|
|
312
|
-
case 'plugins':
|
|
313
|
-
return await cmds.cmdPlugins(parsed, io);
|
|
314
|
-
case 'families':
|
|
315
|
-
return await cmds.cmdFamilies(parsed, io);
|
|
316
|
-
}
|
|
317
|
-
return 2;
|
|
328
|
+
const code = await dispatch(cmd, parsed, io, cmds);
|
|
329
|
+
if (!wantsJson && noticeAllowed(cmd, parsed))
|
|
330
|
+
await afterCommand();
|
|
331
|
+
return code;
|
|
318
332
|
}
|
|
319
333
|
catch (err) {
|
|
320
334
|
const usage = err instanceof UsageError;
|
|
@@ -331,6 +345,53 @@ export async function main(argv) {
|
|
|
331
345
|
return usage ? 2 : 1;
|
|
332
346
|
}
|
|
333
347
|
}
|
|
348
|
+
async function dispatch(cmd, parsed, io, cmds) {
|
|
349
|
+
switch (cmd) {
|
|
350
|
+
case 'list':
|
|
351
|
+
return await cmds.cmdList(parsed, io);
|
|
352
|
+
case 'run':
|
|
353
|
+
return await cmds.cmdRun(parsed, io, 'run');
|
|
354
|
+
case 'login':
|
|
355
|
+
return await cmds.cmdRun(parsed, io, 'login');
|
|
356
|
+
case 'resume':
|
|
357
|
+
return await cmds.cmdResume(parsed, io);
|
|
358
|
+
case 'next':
|
|
359
|
+
return await cmds.cmdNext(parsed, io);
|
|
360
|
+
case 'new':
|
|
361
|
+
return await cmds.cmdNew(parsed, io);
|
|
362
|
+
case 'alias':
|
|
363
|
+
return await cmds.cmdAlias(parsed, io);
|
|
364
|
+
case 'doctor':
|
|
365
|
+
return await cmds.cmdDoctor(parsed, io);
|
|
366
|
+
case 'quota':
|
|
367
|
+
return await cmds.cmdQuota(parsed, io);
|
|
368
|
+
case 'tune':
|
|
369
|
+
return await cmds.cmdTune(parsed, io);
|
|
370
|
+
case 'model':
|
|
371
|
+
return await cmds.cmdModel(parsed, io);
|
|
372
|
+
case 'remote-control':
|
|
373
|
+
return await cmds.cmdRemoteControl(parsed, io);
|
|
374
|
+
case 'handoff':
|
|
375
|
+
return await cmds.cmdHandoff(parsed, io);
|
|
376
|
+
case 'ui':
|
|
377
|
+
return await cmds.cmdUi(parsed, io);
|
|
378
|
+
case 'app':
|
|
379
|
+
return await cmds.cmdApp(parsed, io);
|
|
380
|
+
case 'desktop':
|
|
381
|
+
return await cmds.cmdDesktop(parsed, io);
|
|
382
|
+
case 'shell-init':
|
|
383
|
+
return await cmds.cmdShellInit(parsed, io);
|
|
384
|
+
case 'plugins':
|
|
385
|
+
return await cmds.cmdPlugins(parsed, io);
|
|
386
|
+
case 'families':
|
|
387
|
+
return await cmds.cmdFamilies(parsed, io);
|
|
388
|
+
case 'update':
|
|
389
|
+
return await cmds.cmdUpdate(parsed, io);
|
|
390
|
+
case 'update-check':
|
|
391
|
+
return await cmds.cmdUpdateCheck(parsed, io);
|
|
392
|
+
}
|
|
393
|
+
return 2;
|
|
394
|
+
}
|
|
334
395
|
// Run when executed directly, also through symlinks (npm's bin shim, a user alias like `sb`).
|
|
335
396
|
function isEntry() {
|
|
336
397
|
const arg = process.argv[1];
|
|
@@ -1,29 +1,83 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
/** An outlined shape of one buddy, as SVG path data on its 64-unit grid. */
|
|
2
|
+
export interface BuddyPath {
|
|
3
|
+
d: string;
|
|
4
|
+
/** `body`, `shade`: the buddy's own colours; `none`: an open line. */
|
|
5
|
+
fill: 'body' | 'shade' | 'none';
|
|
6
|
+
/** Line colour; `chevron` is the buddy's accent colour. Default ink. */
|
|
7
|
+
stroke?: 'ink' | 'chevron';
|
|
8
|
+
/** Line width on the 64-unit grid, before the buddy's scale. Default: the outline width. */
|
|
9
|
+
width?: number;
|
|
10
|
+
}
|
|
11
|
+
/** A filled ellipse without outline: the eyes and their highlights. */
|
|
12
|
+
export interface BuddyDot {
|
|
13
|
+
cx: number;
|
|
14
|
+
cy: number;
|
|
15
|
+
rx: number;
|
|
16
|
+
ry: number;
|
|
17
|
+
fill: 'ink' | 'light';
|
|
18
|
+
}
|
|
19
|
+
/** Back to front: feet, arms, body, then the face. */
|
|
20
|
+
export declare const BUDDY: {
|
|
21
|
+
paths: readonly BuddyPath[];
|
|
22
|
+
eyes: readonly BuddyDot[];
|
|
9
23
|
};
|
|
24
|
+
/** The eye highlights sit towards where the buddy looks: `1` right, `-1` left. */
|
|
25
|
+
export declare function highlights(look: 1 | -1): BuddyDot[];
|
|
26
|
+
export interface BuddyColors {
|
|
27
|
+
body: string;
|
|
28
|
+
shade: string;
|
|
29
|
+
chevron: string;
|
|
30
|
+
}
|
|
10
31
|
export declare const MARK_COLORS: {
|
|
11
|
-
readonly
|
|
12
|
-
|
|
13
|
-
|
|
32
|
+
readonly blue: {
|
|
33
|
+
readonly body: '#3f8fc4';
|
|
34
|
+
readonly shade: '#29658e';
|
|
35
|
+
readonly chevron: '#f2b35e';
|
|
36
|
+
};
|
|
37
|
+
readonly amber: {
|
|
38
|
+
readonly body: '#f2b35e';
|
|
39
|
+
readonly shade: '#c98a35';
|
|
40
|
+
readonly chevron: '#3f8fc4';
|
|
41
|
+
};
|
|
42
|
+
readonly ink: '#1e2a44';
|
|
43
|
+
readonly light: '#f6f0e5';
|
|
44
|
+
};
|
|
45
|
+
export interface PlacedBuddy {
|
|
46
|
+
colors: BuddyColors;
|
|
47
|
+
/** Where the buddy looks (and which way its eye highlights sit). */
|
|
48
|
+
look: 1 | -1;
|
|
49
|
+
/** 64-unit grid → mark units: translate by (x, y), then scale. */
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
scale: number;
|
|
53
|
+
}
|
|
54
|
+
/** The two buddies in mark units, back first. The blue one in front is the one a Portal's `logoColor` recolours. */
|
|
55
|
+
export declare const MARK: {
|
|
56
|
+
line: number;
|
|
57
|
+
placedLine: number;
|
|
58
|
+
back: PlacedBuddy;
|
|
59
|
+
front: PlacedBuddy;
|
|
60
|
+
};
|
|
61
|
+
/** The square the pair is drawn in, in mark units (the Panel's 40 px tile and the app icon tile). */
|
|
62
|
+
export declare const MARK_VIEWBOX: {
|
|
63
|
+
readonly x: -2.5;
|
|
64
|
+
readonly y: 3;
|
|
65
|
+
readonly size: 69;
|
|
14
66
|
};
|
|
15
|
-
/** The square
|
|
16
|
-
export declare const
|
|
17
|
-
readonly x:
|
|
18
|
-
readonly y:
|
|
19
|
-
readonly size:
|
|
67
|
+
/** The square around one buddy at full size, for the small cut (favicon). */
|
|
68
|
+
export declare const BUDDY_VIEWBOX: {
|
|
69
|
+
readonly x: 3;
|
|
70
|
+
readonly y: 10.5;
|
|
71
|
+
readonly size: 58;
|
|
20
72
|
};
|
|
21
|
-
export declare function roundedRectPath([x, y, w, h, r]: RoundedRect): string;
|
|
22
73
|
/**
|
|
23
|
-
* The mark as SVG children (no outer <svg>).
|
|
24
|
-
* `fill` (the Panel's `--logo-fg`, which Portals
|
|
25
|
-
* `id` names the gap mask; keep it unique per document.
|
|
74
|
+
* The mark as SVG children (no outer <svg>). With `front` omitted the blue buddy's body has no fill of its own, so it
|
|
75
|
+
* takes the surrounding `fill` (the Panel's `--logo-fg`, which Portals set with `logoColor`); everything else is fixed.
|
|
26
76
|
*/
|
|
27
|
-
export declare function markSvgBody(
|
|
77
|
+
export declare function markSvgBody(front?: string): string;
|
|
28
78
|
/** The Panel's header logo: the mark sized for its 40 px tile. */
|
|
29
79
|
export declare const MARK_SVG: string;
|
|
80
|
+
/** `<basePath>/icon.svg`: the whole mark on a transparent square, for a Portal's tab or menu (ADR-0006 amendment). */
|
|
81
|
+
export declare const ICON_SVG: string;
|
|
82
|
+
/** `<basePath>/favicon.svg`: the small cut, the blue buddy alone, for sizes below about 24 px (ADR-0022). */
|
|
83
|
+
export declare const FAVICON_SVG: string;
|
|
@@ -1,42 +1,88 @@
|
|
|
1
|
-
// The sideby mark: two
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
// The sideby mark: two buddies side by side, the amber one behind on the left and the blue one in front on the right,
|
|
2
|
+
// looking at each other (ADR-0022, the characters of the intro film). One source for the Panel header, the empty
|
|
3
|
+
// state, the Portal icon, the favicon and the app icon.
|
|
4
|
+
// One buddy is drawn on a 64-unit grid; the mark places two of them with a translate and a scale.
|
|
5
|
+
/** Back to front: feet, arms, body, then the face. */
|
|
6
|
+
export const BUDDY = {
|
|
7
|
+
paths: [
|
|
8
|
+
{ d: 'M17 48 Q15 58 22 57 Q27 57 27 50 M37 50 Q37 57 42 57 Q49 58 47 48', fill: 'shade' },
|
|
9
|
+
{ d: 'M14 37 C4 32 4 41 14 43 M50 37 C60 32 60 41 50 43', fill: 'body' },
|
|
10
|
+
{
|
|
11
|
+
d: 'M12 37 C12 27 17 23 25 23 H39 C47 23 52 27 52 37 C52 48 45 53 32 53 C19 53 12 48 12 37 Z',
|
|
12
|
+
fill: 'body',
|
|
13
|
+
},
|
|
14
|
+
{ d: 'M30 27 L34 29.31 L30 31.62', fill: 'none', stroke: 'chevron', width: 2.5 },
|
|
15
|
+
{ d: 'M29 46 Q32 49 35 46', fill: 'none', width: 2.5 },
|
|
12
16
|
],
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
eyes: [
|
|
18
|
+
{ cx: 22, cy: 38.5, rx: 4.5, ry: 6, fill: 'ink' },
|
|
19
|
+
{ cx: 42, cy: 38.5, rx: 4.5, ry: 6, fill: 'ink' },
|
|
16
20
|
],
|
|
17
21
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
/** The eye highlights sit towards where the buddy looks: `1` right, `-1` left. */
|
|
23
|
+
export function highlights(look) {
|
|
24
|
+
return BUDDY.eyes.map((e) => ({
|
|
25
|
+
cx: e.cx + 1.5 * look,
|
|
26
|
+
cy: e.cy + (look > 0 ? 0.5 : -2),
|
|
27
|
+
rx: 1.65,
|
|
28
|
+
ry: 2.1,
|
|
29
|
+
fill: 'light',
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
export const MARK_COLORS = {
|
|
33
|
+
blue: { body: '#3f8fc4', shade: '#29658e', chevron: '#f2b35e' },
|
|
34
|
+
amber: { body: '#f2b35e', shade: '#c98a35', chevron: '#3f8fc4' },
|
|
35
|
+
ink: '#1e2a44',
|
|
36
|
+
light: '#f6f0e5',
|
|
37
|
+
};
|
|
38
|
+
/** The two buddies in mark units, back first. The blue one in front is the one a Portal's `logoColor` recolours. */
|
|
39
|
+
export const MARK = {
|
|
40
|
+
/** Outline width of a buddy drawn alone, on its 64-unit grid. */
|
|
41
|
+
line: 2.75,
|
|
42
|
+
/** Outline width of a placed buddy before its scale, so the outline stays about 2.5 mark units. */
|
|
43
|
+
placedLine: 3.6,
|
|
44
|
+
back: { colors: MARK_COLORS.amber, look: 1, x: -2, y: 2, scale: 0.7 },
|
|
45
|
+
front: { colors: MARK_COLORS.blue, look: -1, x: 22, y: 16, scale: 0.7 },
|
|
46
|
+
};
|
|
47
|
+
/** The square the pair is drawn in, in mark units (the Panel's 40 px tile and the app icon tile). */
|
|
48
|
+
export const MARK_VIEWBOX = { x: -2.5, y: 3, size: 69 };
|
|
49
|
+
/** The square around one buddy at full size, for the small cut (favicon). */
|
|
50
|
+
export const BUDDY_VIEWBOX = { x: 3, y: 10.5, size: 58 };
|
|
51
|
+
function buddySvg(b, line, bodyFill) {
|
|
52
|
+
const color = (fill) => {
|
|
53
|
+
if (fill === 'none')
|
|
54
|
+
return ' fill="none"';
|
|
55
|
+
if (fill === 'shade')
|
|
56
|
+
return ` fill="${b.colors.shade}"`;
|
|
57
|
+
return bodyFill === null ? '' : ` fill="${bodyFill ?? b.colors.body}"`;
|
|
58
|
+
};
|
|
59
|
+
const paths = BUDDY.paths
|
|
60
|
+
.map((p) => {
|
|
61
|
+
const stroke = p.stroke === 'chevron' ? ` stroke="${b.colors.chevron}"` : '';
|
|
62
|
+
const width = p.width ? ` stroke-width="${p.width}"` : '';
|
|
63
|
+
return `<path d="${p.d}"${color(p.fill)}${stroke}${width}/>`;
|
|
64
|
+
})
|
|
65
|
+
.join('');
|
|
66
|
+
const dots = [...BUDDY.eyes, ...highlights(b.look)]
|
|
67
|
+
.map((e) => `<ellipse cx="${e.cx}" cy="${e.cy}" rx="${e.rx}" ry="${e.ry}" fill="${e.fill === 'ink' ? MARK_COLORS.ink : MARK_COLORS.light}"/>`)
|
|
68
|
+
.join('');
|
|
69
|
+
const transform = b.scale === 1 && b.x === 0 && b.y === 0 ? '' : ` transform="translate(${b.x} ${b.y}) scale(${b.scale})"`;
|
|
70
|
+
return `<g${transform} stroke-width="${line}">${paths}<g stroke="none">${dots}</g></g>`;
|
|
24
71
|
}
|
|
72
|
+
const OUTLINE = `stroke="${MARK_COLORS.ink}" stroke-linecap="round" stroke-linejoin="round"`;
|
|
25
73
|
/**
|
|
26
|
-
* The mark as SVG children (no outer <svg>).
|
|
27
|
-
* `fill` (the Panel's `--logo-fg`, which Portals
|
|
28
|
-
* `id` names the gap mask; keep it unique per document.
|
|
74
|
+
* The mark as SVG children (no outer <svg>). With `front` omitted the blue buddy's body has no fill of its own, so it
|
|
75
|
+
* takes the surrounding `fill` (the Panel's `--logo-fg`, which Portals set with `logoColor`); everything else is fixed.
|
|
29
76
|
*/
|
|
30
|
-
export function markSvgBody(
|
|
31
|
-
|
|
32
|
-
const frontFill = front ? ` fill="${front}"` : '';
|
|
33
|
-
return (`<mask id="${id}" maskUnits="userSpaceOnUse" x="0" y="0" width="256" height="256">` +
|
|
34
|
-
`<rect width="256" height="256" fill="#fff"/><path fill="#000" d="${roundedRectPath(MARK.gap)}"/></mask>` +
|
|
35
|
-
`<g mask="url(#${id})"><path fill="${MARK_COLORS.back}" d="${roundedRectPath(MARK.back)}"/>` +
|
|
36
|
-
`<path fill="${MARK_COLORS.eye}" d="${eyes(MARK.backEyes)}"/></g>` +
|
|
37
|
-
`<path${frontFill} d="${roundedRectPath(MARK.front)}"/>` +
|
|
38
|
-
`<path fill="${MARK_COLORS.eye}" d="${eyes(MARK.frontEyes)}"/>`);
|
|
77
|
+
export function markSvgBody(front) {
|
|
78
|
+
return `<g ${OUTLINE}>${buddySvg(MARK.back, MARK.placedLine)}${buddySvg(MARK.front, MARK.placedLine, front ?? null)}</g>`;
|
|
39
79
|
}
|
|
40
|
-
const
|
|
80
|
+
const viewBox = (v) => `${v.x} ${v.y} ${v.size} ${v.size}`;
|
|
41
81
|
/** The Panel's header logo: the mark sized for its 40 px tile. */
|
|
42
|
-
export const MARK_SVG = `<svg viewBox="${
|
|
82
|
+
export const MARK_SVG = `<svg viewBox="${viewBox(MARK_VIEWBOX)}">${markSvgBody()}</svg>`;
|
|
83
|
+
/** `<basePath>/icon.svg`: the whole mark on a transparent square, for a Portal's tab or menu (ADR-0006 amendment). */
|
|
84
|
+
export const ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox(MARK_VIEWBOX)}" role="img" aria-label="sideby">` +
|
|
85
|
+
`${markSvgBody(MARK_COLORS.blue.body)}</svg>`;
|
|
86
|
+
/** `<basePath>/favicon.svg`: the small cut, the blue buddy alone, for sizes below about 24 px (ADR-0022). */
|
|
87
|
+
export const FAVICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox(BUDDY_VIEWBOX)}">` +
|
|
88
|
+
`<g ${OUTLINE}>${buddySvg({ ...MARK.front, x: 0, y: 0, scale: 1 }, MARK.line)}</g></svg>`;
|
|
@@ -62,6 +62,10 @@ export declare const ConfigSchema: Type.TObject<{
|
|
|
62
62
|
order: Type.TOptional<Type.TArray<Type.TString>>;
|
|
63
63
|
}>>>;
|
|
64
64
|
}>>;
|
|
65
|
+
update: Type.TOptional<Type.TObject<{
|
|
66
|
+
check: Type.TOptional<Type.TBoolean>;
|
|
67
|
+
auto: Type.TOptional<Type.TBoolean>;
|
|
68
|
+
}>>;
|
|
65
69
|
shellInitFile: Type.TOptional<Type.TObject<{
|
|
66
70
|
zsh: Type.TOptional<Type.TString>;
|
|
67
71
|
bash: Type.TOptional<Type.TString>;
|
|
@@ -85,6 +89,18 @@ export declare function editConfigHandoff(file: string, edit: (current: HandoffC
|
|
|
85
89
|
before: HandoffConfig | undefined;
|
|
86
90
|
after: HandoffConfig | undefined;
|
|
87
91
|
}>;
|
|
92
|
+
export type UpdateConfig = NonNullable<Config['update']>;
|
|
93
|
+
/**
|
|
94
|
+
* The Panel's Auto Update switch (spec §3.23): changes the config `update` key with `edit` (see `editConfigKey` for
|
|
95
|
+
* how the file is written). With `dryRun`, shows the change without writing.
|
|
96
|
+
*/
|
|
97
|
+
export declare function editConfigUpdate(file: string, edit: (current: UpdateConfig) => UpdateConfig, opts?: {
|
|
98
|
+
dryRun?: boolean;
|
|
99
|
+
}): Promise<{
|
|
100
|
+
changed: boolean;
|
|
101
|
+
before: UpdateConfig | undefined;
|
|
102
|
+
after: UpdateConfig | undefined;
|
|
103
|
+
}>;
|
|
88
104
|
export type AccountsConfig = NonNullable<Config['accounts']>;
|
|
89
105
|
/**
|
|
90
106
|
* `sideby model` (spec §3.19): changes the config `accounts` key with `edit` (see `editConfigKey` for how the file is
|