sideby 0.15.0 → 0.16.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/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +5 -0
- package/dist/src/cli/commands.js +86 -0
- package/dist/src/cli/json-schemas.js +22 -0
- package/dist/src/cli/main.js +6 -1
- package/dist/src/core/local-build.d.ts +75 -0
- package/dist/src/core/local-build.js +205 -0
- package/dist/src/core/update.d.ts +12 -0
- package/dist/src/core/update.js +33 -1
- package/dist/src/panel/handler.d.ts +5 -0
- package/dist/src/panel/handler.js +66 -0
- package/dist/src/panel/i18n.d.ts +10 -0
- package/dist/src/panel/i18n.js +20 -0
- package/dist/src/panel/page-logic.d.ts +18 -0
- package/dist/src/panel/page-logic.js +17 -0
- package/dist/src/panel/page-script.js +77 -3
- package/dist/src/panel/page-styles.js +6 -3
- package/package.json +1 -1
- package/schemas/local.json +169 -0
- package/skills/sideby/SKILL.md +2 -1
- package/skills/sideby/references/update.md +9 -0
package/dist/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
1
|
+
{"id":"20261017T135126-a0a3cf"}
|
|
@@ -71,6 +71,11 @@ export declare function cmdUi(a: ParsedArgs, io: Io): Promise<number>;
|
|
|
71
71
|
export declare function cmdUpdate(a: ParsedArgs, io: Io): Promise<number>;
|
|
72
72
|
/** The internal `sideby update-check` the CLI starts detached: one Update Check, then Auto Update if it is on. */
|
|
73
73
|
export declare function cmdUpdateCheck(_a: ParsedArgs, _io: Io): Promise<number>;
|
|
74
|
+
/**
|
|
75
|
+
* `sideby local [on|off]` (ADR-0024): which side the global `sideby` runs, and a switch between the newest Local
|
|
76
|
+
* Build (`pnpm local` builds it) and the npm package. Shows the change first and exits 10 without `--yes`.
|
|
77
|
+
*/
|
|
78
|
+
export declare function cmdLocal(a: ParsedArgs, io: Io): Promise<number>;
|
|
74
79
|
export declare function cmdApp(a: ParsedArgs, io: Io): Promise<number>;
|
|
75
80
|
/**
|
|
76
81
|
* `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
|
@@ -1346,6 +1346,92 @@ export async function cmdUpdateCheck(_a, _io) {
|
|
|
1346
1346
|
await backgroundCheck(paths, process.env, config, () => backgroundPanelPort(paths));
|
|
1347
1347
|
return 0;
|
|
1348
1348
|
}
|
|
1349
|
+
/**
|
|
1350
|
+
* `sideby local [on|off]` (ADR-0024): which side the global `sideby` runs, and a switch between the newest Local
|
|
1351
|
+
* Build (`pnpm local` builds it) and the npm package. Shows the change first and exits 10 without `--yes`.
|
|
1352
|
+
*/
|
|
1353
|
+
export async function cmdLocal(a, io) {
|
|
1354
|
+
const usage = 'usage: sideby local [on|off] [--yes] [--json]';
|
|
1355
|
+
const sub = a.positionals[0];
|
|
1356
|
+
if (a.positionals.length > 1 || (sub !== undefined && sub !== 'on' && sub !== 'off'))
|
|
1357
|
+
throw new UsageError(usage);
|
|
1358
|
+
const lb = await import('../core/local-build.js');
|
|
1359
|
+
const asJson = a.flags.has('json');
|
|
1360
|
+
const d = await lb.localDirs(process.env);
|
|
1361
|
+
if (!d)
|
|
1362
|
+
throw new UserError('`npm prefix -g` failed; switching needs npm on PATH and an npm global install of sideby');
|
|
1363
|
+
const s = await lb.localStatus(d);
|
|
1364
|
+
const now = new Date();
|
|
1365
|
+
const build = (l) => `${l.version}-dev · ${l.commit || 'no commit'}${l.dirty ? '+' : ''}${l.builtAt ? `, built ${clock(l.builtAt, now)}` : ''}`;
|
|
1366
|
+
if (sub === undefined) {
|
|
1367
|
+
if (asJson)
|
|
1368
|
+
json(io, { ...s, switched: false });
|
|
1369
|
+
else {
|
|
1370
|
+
io.out(s.running === 'local' && s.local
|
|
1371
|
+
? `The global sideby runs the local build ${build(s.local)}.`
|
|
1372
|
+
: s.running === 'npm'
|
|
1373
|
+
? 'The global sideby runs the npm package.'
|
|
1374
|
+
: 'The global sideby is not an npm global install; switching supports `npm install -g sideby` only.');
|
|
1375
|
+
if (s.running === 'local')
|
|
1376
|
+
io.out(c.dim('Back to the release: sideby local off'));
|
|
1377
|
+
else if (s.running === 'npm' && s.local)
|
|
1378
|
+
io.out(c.dim(`A local build is kept (${build(s.local)}): sideby local on`));
|
|
1379
|
+
}
|
|
1380
|
+
return 0;
|
|
1381
|
+
}
|
|
1382
|
+
const to = sub === 'on' ? 'local' : 'npm';
|
|
1383
|
+
const blocked = lb.switchBlocker(s, to);
|
|
1384
|
+
if (blocked) {
|
|
1385
|
+
if (asJson)
|
|
1386
|
+
json(io, { ...s, to, switched: false, blocked });
|
|
1387
|
+
else
|
|
1388
|
+
io.err(c.yellow(`sideby cannot switch: ${blocked}`));
|
|
1389
|
+
return 1;
|
|
1390
|
+
}
|
|
1391
|
+
if (s.running === to) {
|
|
1392
|
+
if (asJson)
|
|
1393
|
+
json(io, { ...s, to, switched: false });
|
|
1394
|
+
else
|
|
1395
|
+
io.out(`${c.green('✓')} The global sideby already runs the ${to === 'local' ? 'local build' : 'npm package'}.`);
|
|
1396
|
+
return 0;
|
|
1397
|
+
}
|
|
1398
|
+
if (!a.flags.has('yes')) {
|
|
1399
|
+
if (asJson)
|
|
1400
|
+
json(io, { ...s, to, switched: false });
|
|
1401
|
+
else {
|
|
1402
|
+
io.out(to === 'local' && s.local
|
|
1403
|
+
? `Switching runs the local build ${build(s.local)} instead of the npm package. It is not rebuilt: \`pnpm local\` in the checkout builds a new one.`
|
|
1404
|
+
: `Switching goes back to the npm package: the kept ${s.kept ?? 'copy'} when it is the latest, else \`npm install -g sideby@latest\`.`);
|
|
1405
|
+
io.out(c.cyan(`\nRun it with \`sideby local ${sub} --yes\`.`));
|
|
1406
|
+
}
|
|
1407
|
+
return 10;
|
|
1408
|
+
}
|
|
1409
|
+
const { backgroundPanelPort } = await import('../panel/background.js');
|
|
1410
|
+
const paths = resolvePaths(process.env);
|
|
1411
|
+
const r = await lb.switchLocal(paths, d, to, process.env, {
|
|
1412
|
+
panelPort: () => backgroundPanelPort(paths),
|
|
1413
|
+
...(asJson ? {} : { onOutput: (t) => process.stderr.write(t) }),
|
|
1414
|
+
});
|
|
1415
|
+
if (asJson)
|
|
1416
|
+
json(io, {
|
|
1417
|
+
...(await lb.localStatus(d)),
|
|
1418
|
+
to,
|
|
1419
|
+
switched: r.ok,
|
|
1420
|
+
message: r.message,
|
|
1421
|
+
restartedPanel: r.restartedPanel,
|
|
1422
|
+
...(r.skillSynced === undefined ? {} : { skillSynced: r.skillSynced }),
|
|
1423
|
+
});
|
|
1424
|
+
else if (r.ok) {
|
|
1425
|
+
io.out(`${c.green('✓')} ${r.message}.`);
|
|
1426
|
+
if (r.skillSynced === false)
|
|
1427
|
+
io.err(c.yellow('The sideby skill was not brought along; run `sideby update` to see why.'));
|
|
1428
|
+
if (r.restartedPanel)
|
|
1429
|
+
io.out(c.dim('The background panel restarted with it; open panels reload by themselves.'));
|
|
1430
|
+
}
|
|
1431
|
+
else
|
|
1432
|
+
io.err(c.red(`✗ ${r.message}`));
|
|
1433
|
+
return r.ok ? 0 : 1;
|
|
1434
|
+
}
|
|
1349
1435
|
export async function cmdApp(a, io) {
|
|
1350
1436
|
const sub = a.positionals[0];
|
|
1351
1437
|
const usage = 'usage: sideby app install [--url <url>] | sideby app uninstall';
|
|
@@ -333,6 +333,28 @@ export const OUTPUT_SCHEMAS = {
|
|
|
333
333
|
changed: Type.Boolean(),
|
|
334
334
|
}),
|
|
335
335
|
'update-auto': Type.Object({ schemaVersion: Version, auto: Type.Boolean(), changed: Type.Boolean() }),
|
|
336
|
+
local: Type.Object({
|
|
337
|
+
schemaVersion: Version,
|
|
338
|
+
running: Type.Union([Type.Literal('local'), Type.Literal('npm'), Type.Null()], {
|
|
339
|
+
description: 'What the global sideby runs: the Local Build, an npm package, or neither (not an npm global install).',
|
|
340
|
+
}),
|
|
341
|
+
local: Type.Union([Type.Object({ commit: Str, dirty: Type.Boolean(), builtAt: Str, version: Str }), Type.Null()], {
|
|
342
|
+
description: 'The newest Local Build, the one `on` switches to; null when `pnpm local` never ran.',
|
|
343
|
+
}),
|
|
344
|
+
kept: Type.Union([Str, Type.Null()], {
|
|
345
|
+
description: 'The npm package version kept aside while a Local Build runs.',
|
|
346
|
+
}),
|
|
347
|
+
to: Type.Optional(Type.Union([Type.Literal('local'), Type.Literal('npm')])),
|
|
348
|
+
switched: Type.Boolean({
|
|
349
|
+
description: 'A switch ran and succeeded; false for status, a preview (exit 10) or a failure.',
|
|
350
|
+
}),
|
|
351
|
+
blocked: Type.Optional(Type.String({ description: 'Why the switch cannot run, with what to do.' })),
|
|
352
|
+
message: Type.Optional(Str),
|
|
353
|
+
restartedPanel: Type.Optional(Type.Boolean()),
|
|
354
|
+
skillSynced: Type.Optional(Type.Boolean({
|
|
355
|
+
description: 'Back on npm, `sideby update --yes` brought the sideby skill to that version (true) or failed (false).',
|
|
356
|
+
})),
|
|
357
|
+
}, { title: 'sideby local --json' }),
|
|
336
358
|
list: Type.Object({
|
|
337
359
|
schemaVersion: Version,
|
|
338
360
|
families: Type.Array(FamilyInfo),
|
package/dist/src/cli/main.js
CHANGED
|
@@ -72,10 +72,12 @@ Usage:
|
|
|
72
72
|
rewrite the config's shellInitFile with them
|
|
73
73
|
sideby plugins list loaded plugins and load errors
|
|
74
74
|
sideby update [--yes] update sideby to the newest version (shows the command first)
|
|
75
|
+
sideby local [on|off] [--yes] for sideby developers: switch the global sideby between the
|
|
76
|
+
last local build (pnpm local) and the npm package
|
|
75
77
|
|
|
76
78
|
Options:
|
|
77
79
|
--json machine-readable output (list, new, next, alias, doctor, quota, tune, model, remote-control, handoff,
|
|
78
|
-
families, plugins, app, desktop, browser, update)
|
|
80
|
+
families, plugins, app, desktop, browser, update, local)
|
|
79
81
|
-h, --help show this help; \`sideby <command> --help\` for new, alias, login, doctor, families, desktop, browser
|
|
80
82
|
-v, --version show the version
|
|
81
83
|
|
|
@@ -249,6 +251,7 @@ const COMMANDS = {
|
|
|
249
251
|
plugins: { flags: ['json'] },
|
|
250
252
|
families: { flags: ['json'] },
|
|
251
253
|
update: { flags: ['json', 'yes'], valued: ['auto'] },
|
|
254
|
+
local: { flags: ['json', 'yes'] },
|
|
252
255
|
'update-check': { flags: [] },
|
|
253
256
|
};
|
|
254
257
|
/** Commands that never print an Update Notice: their output is read by a shell or an agent, or they are internal. */
|
|
@@ -477,6 +480,8 @@ async function dispatch(cmd, parsed, io, cmds) {
|
|
|
477
480
|
return await cmds.cmdUpdate(parsed, io);
|
|
478
481
|
case 'update-check':
|
|
479
482
|
return await cmds.cmdUpdateCheck(parsed, io);
|
|
483
|
+
case 'local':
|
|
484
|
+
return await cmds.cmdLocal(parsed, io);
|
|
480
485
|
}
|
|
481
486
|
return 2;
|
|
482
487
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Env } from '../types.ts';
|
|
2
|
+
import type { Paths } from './paths.ts';
|
|
3
|
+
import { type LocalBuild } from './version.ts';
|
|
4
|
+
export interface LocalDirs {
|
|
5
|
+
/** `$XDG_DATA_HOME/sideby-dev`: `pkg` (the running build), `old/<id>`, `npm` (the package it replaced). */
|
|
6
|
+
dev: string;
|
|
7
|
+
/** npm's global prefix: `<prefix>/lib/node_modules/sideby` and `<prefix>/bin/sideby`. */
|
|
8
|
+
prefix: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const pkgDir: (d: LocalDirs) => string;
|
|
11
|
+
export declare const globalPkg: (d: LocalDirs) => string;
|
|
12
|
+
export declare const devDir: (env: Env) => string;
|
|
13
|
+
/** Where the switch works, with npm's global prefix from `npm prefix -g`; null when npm cannot be asked. */
|
|
14
|
+
export declare function localDirs(env: Env): Promise<LocalDirs | null>;
|
|
15
|
+
export declare function kind(p: string): Promise<'link' | 'dir' | 'file' | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Points the global npm package at `pkg`. A real npm install moves to `<dev>/npm` first, so going back needs no
|
|
18
|
+
* network; a link elsewhere (npm link) is replaced. Returns what it did, for the summary.
|
|
19
|
+
*/
|
|
20
|
+
export declare function linkGlobal(d: LocalDirs): Promise<string>;
|
|
21
|
+
/** Undoes linkGlobal: the npm package it moved comes back; false when there is none to restore (install it). */
|
|
22
|
+
export declare function unlinkGlobal(d: LocalDirs): Promise<boolean>;
|
|
23
|
+
/** The version of the npm package kept aside by linkGlobal, or null when none was kept or it cannot be read. */
|
|
24
|
+
export declare const keptVersion: (d: LocalDirs) => Promise<string | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Going back to npm: put the kept package back when it is the newest published one (or the registry cannot be
|
|
27
|
+
* asked), else install `latest`. After a release the kept copy is the version before it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function offPlan(kept: string | null, latest: string | null): 'restore' | 'install';
|
|
30
|
+
export interface LocalStatus {
|
|
31
|
+
/** What the global `sideby` runs: the Local Build in `pkg`, an npm package, or neither (not installed, a link elsewhere). */
|
|
32
|
+
running: 'local' | 'npm' | null;
|
|
33
|
+
/** The newest Local Build, the one `on` switches to; null when `pnpm local` never ran here. */
|
|
34
|
+
local: (LocalBuild & {
|
|
35
|
+
version: string;
|
|
36
|
+
}) | null;
|
|
37
|
+
/** The npm package kept aside while a Local Build runs; null when none is. */
|
|
38
|
+
kept: string | null;
|
|
39
|
+
}
|
|
40
|
+
export declare function localStatus(d: LocalDirs): Promise<LocalStatus>;
|
|
41
|
+
export type LocalTarget = 'local' | 'npm';
|
|
42
|
+
/** Why `to` cannot run from this status, or null when it can. Every reason says what to do. */
|
|
43
|
+
export declare function switchBlocker(s: LocalStatus, to: LocalTarget): string | null;
|
|
44
|
+
export interface SwitchResult {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
message: string;
|
|
47
|
+
/** What the global `sideby` runs afterwards. */
|
|
48
|
+
running: LocalStatus['running'];
|
|
49
|
+
/** True when it already ran `to`, and nothing changed. */
|
|
50
|
+
unchanged?: boolean;
|
|
51
|
+
restartedPanel: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Back on npm, `sideby update --yes` of the npm code brought the skill agents read to its version (true), or that
|
|
54
|
+
* failed (false); absent for a switch to the Local Build, which has no skill of its own to bring.
|
|
55
|
+
*/
|
|
56
|
+
skillSynced?: boolean;
|
|
57
|
+
/** With `skillSync: 'background'`: that `sideby update --yes` started on its own and is not waited for. */
|
|
58
|
+
skillSyncStarted?: boolean;
|
|
59
|
+
/** Another install held the lock; nothing ran. */
|
|
60
|
+
busy?: boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Puts `to` behind the global `sideby`. `local` links the newest Local Build; `npm` restores the kept package or
|
|
64
|
+
* installs `latest` (a failed install links the Local Build back, so `sideby` never goes missing). Afterwards a
|
|
65
|
+
* background Panel on `panelPort` starts again from the new code. One install at a time (the Update lock).
|
|
66
|
+
*/
|
|
67
|
+
export declare function switchLocal(paths: Paths, d: LocalDirs, to: LocalTarget, env: Env, opts?: {
|
|
68
|
+
panelPort?: () => Promise<number | null>;
|
|
69
|
+
onOutput?: (text: string) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Back on npm, wait for the skill (a terminal shows its progress) or start it on its own: it fetches from the
|
|
72
|
+
* network and can take minutes, longer than a Portal waits for an answer (`background`, the Panel).
|
|
73
|
+
*/
|
|
74
|
+
skillSync?: 'wait' | 'background';
|
|
75
|
+
}): Promise<SwitchResult>;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Switching the global `sideby` between a Local Build and the npm package (ADR-0024). `pnpm local` builds and calls
|
|
2
|
+
// these; `sideby local` and the Panel's version popover switch between the newest Local Build and npm without the
|
|
3
|
+
// checkout. Only an npm global install is handled: `<prefix>/lib/node_modules/sideby` is the folder every caller
|
|
4
|
+
// resolves, a link to `<dev>/pkg` while a Local Build runs.
|
|
5
|
+
import { lstat, mkdir, readFile, readlink, rename, rm, symlink } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { dirname, join, relative } from 'node:path';
|
|
8
|
+
import { compareVersions, fetchLatest, restartPanelByName, runProgram, startDetached, takeInstallLock, } from './update.js';
|
|
9
|
+
import { parseLocalBuild } from './version.js';
|
|
10
|
+
const BIN = 'dist/src/cli/main.js';
|
|
11
|
+
export const pkgDir = (d) => join(d.dev, 'pkg');
|
|
12
|
+
export const globalPkg = (d) => join(d.prefix, 'lib', 'node_modules', 'sideby');
|
|
13
|
+
const globalBin = (d) => join(d.prefix, 'bin', 'sideby');
|
|
14
|
+
export const devDir = (env) => join(env.XDG_DATA_HOME || join(env.HOME || homedir(), '.local', 'share'), 'sideby-dev');
|
|
15
|
+
/** Where the switch works, with npm's global prefix from `npm prefix -g`; null when npm cannot be asked. */
|
|
16
|
+
export async function localDirs(env) {
|
|
17
|
+
const r = await runProgram('npm', ['prefix', '-g'], { env }).catch(() => null);
|
|
18
|
+
const prefix = r && r.code === 0 ? r.output.trim().split('\n').at(-1)?.trim() : '';
|
|
19
|
+
return prefix ? { dev: devDir(env), prefix } : null;
|
|
20
|
+
}
|
|
21
|
+
export async function kind(p) {
|
|
22
|
+
try {
|
|
23
|
+
const s = await lstat(p);
|
|
24
|
+
return s.isSymbolicLink() ? 'link' : s.isDirectory() ? 'dir' : 'file';
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Points the global npm package at `pkg`. A real npm install moves to `<dev>/npm` first, so going back needs no
|
|
32
|
+
* network; a link elsewhere (npm link) is replaced. Returns what it did, for the summary.
|
|
33
|
+
*/
|
|
34
|
+
export async function linkGlobal(d) {
|
|
35
|
+
const target = globalPkg(d);
|
|
36
|
+
const pkg = pkgDir(d);
|
|
37
|
+
let did = 'the global sideby already runs the local build';
|
|
38
|
+
const k = await kind(target);
|
|
39
|
+
if (k !== 'link' || (await readlink(target)) !== pkg) {
|
|
40
|
+
if (k === 'dir') {
|
|
41
|
+
await rm(join(d.dev, 'npm'), { recursive: true, force: true });
|
|
42
|
+
await rename(target, join(d.dev, 'npm'));
|
|
43
|
+
did = `the npm package moved to ${join(d.dev, 'npm')}; the global sideby now runs the local build`;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
if (k)
|
|
47
|
+
await rm(target, { force: true });
|
|
48
|
+
did = 'the global sideby now runs the local build';
|
|
49
|
+
}
|
|
50
|
+
await mkdir(dirname(target), { recursive: true });
|
|
51
|
+
await symlink(pkg, target, 'dir');
|
|
52
|
+
}
|
|
53
|
+
const bin = globalBin(d);
|
|
54
|
+
const want = relative(dirname(bin), join(target, BIN));
|
|
55
|
+
if ((await kind(bin)) !== 'link' || (await readlink(bin)) !== want) {
|
|
56
|
+
await rm(bin, { force: true });
|
|
57
|
+
await mkdir(dirname(bin), { recursive: true });
|
|
58
|
+
await symlink(want, bin);
|
|
59
|
+
}
|
|
60
|
+
return did;
|
|
61
|
+
}
|
|
62
|
+
/** Undoes linkGlobal: the npm package it moved comes back; false when there is none to restore (install it). */
|
|
63
|
+
export async function unlinkGlobal(d) {
|
|
64
|
+
const target = globalPkg(d);
|
|
65
|
+
if ((await kind(target)) === 'link')
|
|
66
|
+
await rm(target, { force: true });
|
|
67
|
+
else if (await kind(target))
|
|
68
|
+
return true; // a real package is already there
|
|
69
|
+
if (!(await kind(join(d.dev, 'npm'))))
|
|
70
|
+
return false;
|
|
71
|
+
await rename(join(d.dev, 'npm'), target);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
async function versionOf(dir) {
|
|
75
|
+
try {
|
|
76
|
+
const v = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8')).version;
|
|
77
|
+
return typeof v === 'string' ? v : null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** The version of the npm package kept aside by linkGlobal, or null when none was kept or it cannot be read. */
|
|
84
|
+
export const keptVersion = (d) => versionOf(join(d.dev, 'npm'));
|
|
85
|
+
/**
|
|
86
|
+
* Going back to npm: put the kept package back when it is the newest published one (or the registry cannot be
|
|
87
|
+
* asked), else install `latest`. After a release the kept copy is the version before it.
|
|
88
|
+
*/
|
|
89
|
+
export function offPlan(kept, latest) {
|
|
90
|
+
if (!kept)
|
|
91
|
+
return 'install';
|
|
92
|
+
if (!latest)
|
|
93
|
+
return 'restore';
|
|
94
|
+
return (compareVersions(kept, latest) ?? -1) >= 0 ? 'restore' : 'install';
|
|
95
|
+
}
|
|
96
|
+
export async function localStatus(d) {
|
|
97
|
+
const k = await kind(globalPkg(d));
|
|
98
|
+
const running = k === 'link'
|
|
99
|
+
? (await readlink(globalPkg(d))) === pkgDir(d)
|
|
100
|
+
? 'local'
|
|
101
|
+
: null
|
|
102
|
+
: k === 'dir'
|
|
103
|
+
? 'npm'
|
|
104
|
+
: null;
|
|
105
|
+
let local = null;
|
|
106
|
+
try {
|
|
107
|
+
const b = parseLocalBuild(JSON.parse(await readFile(join(pkgDir(d), 'dist', 'build.json'), 'utf8')));
|
|
108
|
+
const version = await versionOf(pkgDir(d));
|
|
109
|
+
if (b && version)
|
|
110
|
+
local = { ...b, version };
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// no Local Build here
|
|
114
|
+
}
|
|
115
|
+
return { running, local, kept: await keptVersion(d) };
|
|
116
|
+
}
|
|
117
|
+
/** Why `to` cannot run from this status, or null when it can. Every reason says what to do. */
|
|
118
|
+
export function switchBlocker(s, to) {
|
|
119
|
+
if (s.running === null)
|
|
120
|
+
return 'the global sideby is not an npm global install; switching supports `npm install -g sideby` only';
|
|
121
|
+
if (to === 'local' && !s.local)
|
|
122
|
+
return 'there is no local build yet; run `pnpm local` in the sideby checkout';
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Puts `to` behind the global `sideby`. `local` links the newest Local Build; `npm` restores the kept package or
|
|
127
|
+
* installs `latest` (a failed install links the Local Build back, so `sideby` never goes missing). Afterwards a
|
|
128
|
+
* background Panel on `panelPort` starts again from the new code. One install at a time (the Update lock).
|
|
129
|
+
*/
|
|
130
|
+
export async function switchLocal(paths, d, to, env, opts = {}) {
|
|
131
|
+
const before = await localStatus(d);
|
|
132
|
+
const fail = (message, extra = {}) => ({
|
|
133
|
+
ok: false,
|
|
134
|
+
message,
|
|
135
|
+
running: before.running,
|
|
136
|
+
restartedPanel: false,
|
|
137
|
+
...extra,
|
|
138
|
+
});
|
|
139
|
+
const blocked = switchBlocker(before, to);
|
|
140
|
+
if (blocked)
|
|
141
|
+
return fail(blocked);
|
|
142
|
+
if (before.running === to)
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
unchanged: true,
|
|
146
|
+
message: to === 'local'
|
|
147
|
+
? 'the global sideby already runs the local build'
|
|
148
|
+
: 'the global sideby already runs the npm package',
|
|
149
|
+
running: to,
|
|
150
|
+
restartedPanel: false,
|
|
151
|
+
};
|
|
152
|
+
const release = await takeInstallLock(paths);
|
|
153
|
+
if (!release)
|
|
154
|
+
return fail('a sideby update is running; wait for it to finish, then switch again', { busy: true });
|
|
155
|
+
let message;
|
|
156
|
+
try {
|
|
157
|
+
if (to === 'local') {
|
|
158
|
+
await linkGlobal(d);
|
|
159
|
+
message = `the global sideby runs the local build ${before.local?.commit ?? ''}`.trim();
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const latest = await fetchLatest(env).catch(() => null);
|
|
163
|
+
if (offPlan(before.kept, latest) === 'restore') {
|
|
164
|
+
await unlinkGlobal(d);
|
|
165
|
+
message = `the global sideby runs the npm package ${before.kept} again`;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const want = latest ?? 'latest';
|
|
169
|
+
await rm(globalPkg(d), { force: true });
|
|
170
|
+
const r = await runProgram('npm', ['install', '-g', `sideby@${want}`], {
|
|
171
|
+
env,
|
|
172
|
+
...(opts.onOutput ? { onOutput: opts.onOutput } : {}),
|
|
173
|
+
});
|
|
174
|
+
if (r.code !== 0) {
|
|
175
|
+
await linkGlobal(d);
|
|
176
|
+
return fail(`\`npm install -g sideby@${want}\` failed (exit ${r.code}); the local build stays. Run it yourself to see why`);
|
|
177
|
+
}
|
|
178
|
+
await rm(join(d.dev, 'npm'), { recursive: true, force: true });
|
|
179
|
+
message = `the global sideby runs the npm package ${want}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
await release().catch(() => { });
|
|
185
|
+
}
|
|
186
|
+
const port = opts.panelPort ? await opts.panelPort().catch(() => null) : null;
|
|
187
|
+
const restartedPanel = port !== null && (await restartPanelByName(env, port));
|
|
188
|
+
if (to === 'local')
|
|
189
|
+
return { ok: true, message, running: to, restartedPanel };
|
|
190
|
+
// The skill is part of the version (spec §3.23): the npm code updates it as `sideby update --yes` does for anyone,
|
|
191
|
+
// by name so the new code runs, after the lock above is free. A failure keeps the switch.
|
|
192
|
+
if (opts.skillSync === 'background')
|
|
193
|
+
return {
|
|
194
|
+
ok: true,
|
|
195
|
+
message,
|
|
196
|
+
running: to,
|
|
197
|
+
restartedPanel,
|
|
198
|
+
skillSyncStarted: await startDetached('sideby', ['update', '--yes'], env),
|
|
199
|
+
};
|
|
200
|
+
const u = await runProgram('sideby', ['update', '--yes'], {
|
|
201
|
+
env,
|
|
202
|
+
...(opts.onOutput ? { onOutput: opts.onOutput } : {}),
|
|
203
|
+
}).catch(() => ({ code: 1, output: '' }));
|
|
204
|
+
return { ok: true, message, running: to, restartedPanel, skillSynced: u.code === 0 };
|
|
205
|
+
}
|
|
@@ -66,6 +66,8 @@ export interface InstallMethod {
|
|
|
66
66
|
/** npm only: the prefix whose `lib/node_modules` holds sideby. */
|
|
67
67
|
prefix?: string;
|
|
68
68
|
}
|
|
69
|
+
/** Whether a Linux `/proc/<pid>/stat` line is of a zombie: the state after the parenthesised command name is `Z`. */
|
|
70
|
+
export declare function isZombieStat(stat: string): boolean;
|
|
69
71
|
/** Swapped in tests through `bindUpdateDeps`; the real machine otherwise. */
|
|
70
72
|
export interface UpdateDeps {
|
|
71
73
|
fetch: typeof fetch;
|
|
@@ -84,6 +86,8 @@ export interface UpdateDeps {
|
|
|
84
86
|
}>;
|
|
85
87
|
/** Starts `sideby ui --background --no-open --port <port>` from the newly installed code, detached; false when it could not start. */
|
|
86
88
|
restartPanel: (env: Env, port: number) => Promise<boolean>;
|
|
89
|
+
/** Starts a program detached, nothing kept of its output; false when it could not start. */
|
|
90
|
+
startDetached: (file: string, args: readonly string[], env: Env) => Promise<boolean>;
|
|
87
91
|
/** Whether `sideby app install` put a desktop app in this home (its files carry the app marker). */
|
|
88
92
|
appInstalled: (home: string, env: Env) => Promise<boolean>;
|
|
89
93
|
/** Runs `sideby app install --json` from the newly installed code and waits; false when it failed. */
|
|
@@ -91,6 +95,14 @@ export interface UpdateDeps {
|
|
|
91
95
|
}
|
|
92
96
|
/** Internal, for tests: replaces parts of the machine. Not part of the plugin or Portal contract. */
|
|
93
97
|
export declare function bindUpdateDeps(deps: Partial<UpdateDeps>): () => void;
|
|
98
|
+
/** Runs a program through the swappable machine (tests bind `exec`); a Local Build switch installs with it. */
|
|
99
|
+
export declare const runProgram: UpdateDeps['exec'];
|
|
100
|
+
/** Starts the background Panel again from the code `sideby` now names; see `UpdateDeps.restartPanel`. */
|
|
101
|
+
export declare const restartPanelByName: (env: Env, port: number) => Promise<boolean>;
|
|
102
|
+
/** Starts a program detached through the swappable machine; see `UpdateDeps.startDetached`. */
|
|
103
|
+
export declare const startDetached: (file: string, args: readonly string[], env: Env) => Promise<boolean>;
|
|
104
|
+
/** The lock every install shares (`update-install.lock`), so a Local Build switch never races an Update. */
|
|
105
|
+
export declare const takeInstallLock: (paths: Paths) => Promise<(() => Promise<void>) | null>;
|
|
94
106
|
/** -1, 0 or 1 like a sort comparator; a prerelease sorts before its release. Null when either is not a version. */
|
|
95
107
|
export declare function compareVersions(a: string, b: string): number | null;
|
|
96
108
|
/** `latest` is newer than `current`. */
|
package/dist/src/core/update.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// only network request sideby makes on its own: one GET for the `latest` dist-tag, carrying nothing about the user.
|
|
3
3
|
// Everything it learns goes to one state file shared by the CLI and every Panel.
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
5
6
|
import { access, constants, mkdir, open, readdir, readFile, realpath, rm, stat } from 'node:fs/promises';
|
|
6
7
|
import { dirname, join, sep } from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
@@ -17,6 +18,10 @@ export const RETRY_INTERVAL_MS = 60 * 60 * 1000;
|
|
|
17
18
|
export const AUTO_RETRY_MS = 24 * 60 * 60 * 1000;
|
|
18
19
|
const FETCH_TIMEOUT_MS = 5000;
|
|
19
20
|
export const RELEASE_NOTES = (v) => `https://github.com/Adonis0123/sideby/releases/tag/v${v}`;
|
|
21
|
+
/** Whether a Linux `/proc/<pid>/stat` line is of a zombie: the state after the parenthesised command name is `Z`. */
|
|
22
|
+
export function isZombieStat(stat) {
|
|
23
|
+
return /\)\s+Z\s/.test(stat.slice(stat.lastIndexOf(')')));
|
|
24
|
+
}
|
|
20
25
|
/** The folder of the sideby package this module belongs to: two levels above src/, three above dist/src/. */
|
|
21
26
|
async function ownPackageDir() {
|
|
22
27
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -41,11 +46,18 @@ const realDeps = {
|
|
|
41
46
|
alive(pid) {
|
|
42
47
|
try {
|
|
43
48
|
process.kill(pid, 0);
|
|
44
|
-
return true;
|
|
45
49
|
}
|
|
46
50
|
catch (err) {
|
|
47
51
|
return err.code === 'EPERM';
|
|
48
52
|
}
|
|
53
|
+
// An exited process nobody waited for (its parent went first) still answers signal 0 on Linux: a zombie holds
|
|
54
|
+
// no lock and runs no session.
|
|
55
|
+
try {
|
|
56
|
+
return !isZombieStat(readFileSync(`/proc/${pid}/stat`, 'utf8'));
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return true; // no /proc (macOS): signal 0 is the answer
|
|
60
|
+
}
|
|
49
61
|
},
|
|
50
62
|
exec: (file, args, opts) => new Promise((resolve) => {
|
|
51
63
|
let output = '';
|
|
@@ -77,6 +89,14 @@ const realDeps = {
|
|
|
77
89
|
});
|
|
78
90
|
child.once('error', () => resolve(false));
|
|
79
91
|
}),
|
|
92
|
+
startDetached: (file, args, env) => new Promise((resolve) => {
|
|
93
|
+
const child = spawn(file, args, { env: env, detached: true, stdio: 'ignore' });
|
|
94
|
+
child.once('spawn', () => {
|
|
95
|
+
child.unref();
|
|
96
|
+
resolve(true);
|
|
97
|
+
});
|
|
98
|
+
child.once('error', () => resolve(false));
|
|
99
|
+
}),
|
|
80
100
|
appInstalled: async (home, env) => {
|
|
81
101
|
const { appInstalled } = await import('../desktop/app.js');
|
|
82
102
|
return appInstalled({ home, env, platform: process.platform });
|
|
@@ -102,6 +122,18 @@ export function bindUpdateDeps(deps) {
|
|
|
102
122
|
};
|
|
103
123
|
}
|
|
104
124
|
const deps = () => ({ ...realDeps, ...override });
|
|
125
|
+
/** Runs a program through the swappable machine (tests bind `exec`); a Local Build switch installs with it. */
|
|
126
|
+
export const runProgram = (file, args, opts) => deps().exec(file, args, opts);
|
|
127
|
+
/** Starts the background Panel again from the code `sideby` now names; see `UpdateDeps.restartPanel`. */
|
|
128
|
+
export const restartPanelByName = (env, port) => deps()
|
|
129
|
+
.restartPanel(env, port)
|
|
130
|
+
.catch(() => false);
|
|
131
|
+
/** Starts a program detached through the swappable machine; see `UpdateDeps.startDetached`. */
|
|
132
|
+
export const startDetached = (file, args, env) => deps()
|
|
133
|
+
.startDetached(file, args, env)
|
|
134
|
+
.catch(() => false);
|
|
135
|
+
/** The lock every install shares (`update-install.lock`), so a Local Build switch never races an Update. */
|
|
136
|
+
export const takeInstallLock = (paths) => takeLock(paths, 'update-install.lock').catch(() => null);
|
|
105
137
|
// Build metadata is limited to what semver allows, so a version never carries a terminal escape into the notice.
|
|
106
138
|
const VERSION = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
107
139
|
function parseVersion(v) {
|
|
@@ -102,6 +102,11 @@ export interface PanelUpdate {
|
|
|
102
102
|
* Local Build, which `pnpm local --off` brings back to npm together with the skill.
|
|
103
103
|
*/
|
|
104
104
|
skillUpdate?: import('../core/update.ts').SkillUpdatePlan;
|
|
105
|
+
/**
|
|
106
|
+
* The switch between a Local Build and the npm package (ADR-0024): present only while a Local Build runs or one is
|
|
107
|
+
* kept next to an npm install, so a plain install never sees it.
|
|
108
|
+
*/
|
|
109
|
+
localSwitch?: import('../core/local-build.ts').LocalStatus;
|
|
105
110
|
}
|
|
106
111
|
export interface PanelState {
|
|
107
112
|
schemaVersion: 1;
|
|
@@ -76,8 +76,40 @@ async function updateState(rt, readOnly) {
|
|
|
76
76
|
canRun: Boolean(plan?.canRun),
|
|
77
77
|
skill: SKILL_INSTALL,
|
|
78
78
|
...(skillUpdate ? { skillUpdate } : {}),
|
|
79
|
+
...(await localSwitchState(rt, method)),
|
|
79
80
|
};
|
|
80
81
|
}
|
|
82
|
+
// npm's global prefix, asked once per process: the page reads the state every 20 s and `npm prefix -g` takes a while.
|
|
83
|
+
let localDirsMemo = null;
|
|
84
|
+
async function localDirsFor(rt) {
|
|
85
|
+
const lb = await import('../core/local-build.js');
|
|
86
|
+
localDirsMemo ??= lb.localDirs(rt.env).then((d) => {
|
|
87
|
+
if (!d)
|
|
88
|
+
localDirsMemo = null; // ask again next time
|
|
89
|
+
return d;
|
|
90
|
+
});
|
|
91
|
+
return localDirsMemo;
|
|
92
|
+
}
|
|
93
|
+
// An npm install knows its prefix; only a running Local Build needs npm asked where the global package is.
|
|
94
|
+
async function switchDirs(rt, method) {
|
|
95
|
+
const lb = await import('../core/local-build.js');
|
|
96
|
+
return method.prefix ? { dev: lb.devDir(rt.env), prefix: method.prefix } : localDirsFor(rt);
|
|
97
|
+
}
|
|
98
|
+
async function localSwitchState(rt, method) {
|
|
99
|
+
if (method.kind !== 'local' && method.kind !== 'npm')
|
|
100
|
+
return {};
|
|
101
|
+
try {
|
|
102
|
+
const lb = await import('../core/local-build.js');
|
|
103
|
+
const d = await switchDirs(rt, method);
|
|
104
|
+
if (!d)
|
|
105
|
+
return {};
|
|
106
|
+
const s = await lb.localStatus(d);
|
|
107
|
+
return s.running === 'local' || (s.running === 'npm' && s.local) ? { localSwitch: s } : {};
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
81
113
|
/** The `handoff` map of the Panel state, from the statuses and Quota already read for the page. */
|
|
82
114
|
export function handoffByFamily(rt, accounts, now) {
|
|
83
115
|
const out = {};
|
|
@@ -696,6 +728,40 @@ export function createPanelHandler(opts) {
|
|
|
696
728
|
};
|
|
697
729
|
},
|
|
698
730
|
},
|
|
731
|
+
// The Local Build switch (ADR-0024): without `confirm` the current status; with it, `to` goes behind the global
|
|
732
|
+
// `sideby` and a background Panel restarts from it. The page then waits for the new build and reloads.
|
|
733
|
+
'/api/local': {
|
|
734
|
+
async run(body) {
|
|
735
|
+
if (!updates)
|
|
736
|
+
throw new HttpError(403, 'updates are managed by the page that shows this panel');
|
|
737
|
+
const to = body.to;
|
|
738
|
+
if (to !== 'local' && to !== 'npm')
|
|
739
|
+
throw new HttpError(400, '"to" must be "local" or "npm"');
|
|
740
|
+
const rt = await getRuntime();
|
|
741
|
+
const lb = await import('../core/local-build.js');
|
|
742
|
+
const d = await switchDirs(rt, await update.installMethod(rt.env));
|
|
743
|
+
if (!d)
|
|
744
|
+
throw new UserError('`npm prefix -g` failed; switching needs npm on PATH and an npm global install');
|
|
745
|
+
if (!optBool(body, 'confirm'))
|
|
746
|
+
return { ...(await lb.localStatus(d)), to, switched: false };
|
|
747
|
+
const { backgroundPanelPort } = await import('./background.js');
|
|
748
|
+
// The skill fetch can outlast what a Portal waits for: it runs on its own, and the answer comes at once.
|
|
749
|
+
const r = await lb.switchLocal(rt.paths, d, to, rt.env, {
|
|
750
|
+
panelPort: () => backgroundPanelPort(rt.paths),
|
|
751
|
+
skillSync: 'background',
|
|
752
|
+
});
|
|
753
|
+
if (!r.ok)
|
|
754
|
+
throw new UserError(r.message);
|
|
755
|
+
return {
|
|
756
|
+
...(await lb.localStatus(d)),
|
|
757
|
+
to,
|
|
758
|
+
switched: !r.unchanged,
|
|
759
|
+
message: r.message,
|
|
760
|
+
restartedPanel: r.restartedPanel,
|
|
761
|
+
...(r.skillSyncStarted === undefined ? {} : { skillSyncStarted: r.skillSyncStarted }),
|
|
762
|
+
};
|
|
763
|
+
},
|
|
764
|
+
},
|
|
699
765
|
// The Auto Update switch: config `update.auto`, shown as a diff first like every config change.
|
|
700
766
|
'/api/update/settings': {
|
|
701
767
|
writes: (body) => body.confirm === true,
|
package/dist/src/panel/i18n.d.ts
CHANGED
|
@@ -30,6 +30,16 @@ declare const en: {
|
|
|
30
30
|
'upd.verTitle': string;
|
|
31
31
|
'upd.local': string;
|
|
32
32
|
'upd.localDirty': string;
|
|
33
|
+
'loc.toNpm': string;
|
|
34
|
+
'loc.toNpmV': string;
|
|
35
|
+
'upd.localSwitch': string;
|
|
36
|
+
'upd.localManual': string;
|
|
37
|
+
'loc.toNpmHint': string;
|
|
38
|
+
'loc.toLocal': string;
|
|
39
|
+
'loc.toLocalHint': string;
|
|
40
|
+
'loc.switching': string;
|
|
41
|
+
'loc.stuck': string;
|
|
42
|
+
'loc.reload': string;
|
|
33
43
|
'upd.open': string;
|
|
34
44
|
'upd.autoIsOn': string;
|
|
35
45
|
'upd.autoIsOff': string;
|
package/dist/src/panel/i18n.js
CHANGED
|
@@ -32,6 +32,16 @@ const en = {
|
|
|
32
32
|
'upd.verTitle': 'sideby {v} · check for updates',
|
|
33
33
|
'upd.local': 'Local build from this checkout, built {at}.',
|
|
34
34
|
'upd.localDirty': 'Local build from this checkout, with changes not committed yet, built {at}.',
|
|
35
|
+
'loc.toNpm': 'Switch to the release',
|
|
36
|
+
'loc.toNpmV': 'Switch to the release {v}',
|
|
37
|
+
'upd.localSwitch': 'You are running a local build, and sideby {v} is out. Switching to the release puts it in place, and the skill comes along.',
|
|
38
|
+
'upd.localManual': 'Or run this in the sideby checkout:',
|
|
39
|
+
'loc.toNpmHint': 'Go back to the version published on npm; switch here again to come back.',
|
|
40
|
+
'loc.toLocal': 'Switch to the local build',
|
|
41
|
+
'loc.toLocalHint': 'A local build is kept: {b}, built {at}. It is not rebuilt; pnpm local in the checkout builds a new one.',
|
|
42
|
+
'loc.switching': 'Switching…',
|
|
43
|
+
'loc.stuck': 'Switched, but this page still runs the old code. Reload it; a panel started with sideby ui needs a restart.',
|
|
44
|
+
'loc.reload': 'Reload page',
|
|
35
45
|
'upd.open': 'Update…',
|
|
36
46
|
'upd.autoIsOn': 'Auto Update: on',
|
|
37
47
|
'upd.autoIsOff': 'Auto Update: off',
|
|
@@ -774,6 +784,16 @@ const zh = {
|
|
|
774
784
|
'upd.verTitle': '当前 sideby {v} · 检查更新',
|
|
775
785
|
'upd.local': '本地构建,来自仓库代码,构建于 {at}。',
|
|
776
786
|
'upd.localDirty': '本地构建,含还没提交的改动,构建于 {at}。',
|
|
787
|
+
'loc.toNpm': '切换到正式版',
|
|
788
|
+
'loc.toNpmV': '切换到正式版 {v}',
|
|
789
|
+
'upd.localSwitch': '你在用本地构建,sideby {v} 已发布。切换到正式版就会用上它,skill 也会一起同步。',
|
|
790
|
+
'upd.localManual': '也可以在 sideby 仓库里运行:',
|
|
791
|
+
'loc.toNpmHint': '回到 npm 上发布的版本,之后可以在这里再切回来。',
|
|
792
|
+
'loc.toLocal': '切换到本地构建',
|
|
793
|
+
'loc.toLocalHint': '本机留着一份本地构建:{b},构建于 {at}。切换不会重新构建;要新的就在仓库里运行 pnpm local。',
|
|
794
|
+
'loc.switching': '正在切换…',
|
|
795
|
+
'loc.stuck': '已经切换,但这个页面还在跑旧代码。刷新页面;用 sideby ui 打开的面板要重启一下。',
|
|
796
|
+
'loc.reload': '刷新页面',
|
|
777
797
|
'upd.open': '更新…',
|
|
778
798
|
'upd.autoIsOn': '自动更新:开',
|
|
779
799
|
'upd.autoIsOff': '自动更新:关',
|
|
@@ -177,6 +177,24 @@ export declare function panelHint(hint: string, fixable: boolean, action: string
|
|
|
177
177
|
* Whether the Panel fixes Finding `f` with a button of its own: a sideby skill behind this version while the
|
|
178
178
|
* Update plan says sideby may update it (`skillUpdatable`). Such Findings stay out of the prompt for an AI agent.
|
|
179
179
|
*/
|
|
180
|
+
/**
|
|
181
|
+
* A Local Build the Panel can leave for npm itself (ADR-0024): the global `sideby` still runs it and the Panel may
|
|
182
|
+
* write. Then switching to the release is the update, offered as a button instead of the `pnpm local --off` command.
|
|
183
|
+
*/
|
|
184
|
+
export declare function canLeaveLocal(u: {
|
|
185
|
+
localSwitch?: {
|
|
186
|
+
running: string | null;
|
|
187
|
+
};
|
|
188
|
+
} | null, readOnly: boolean): boolean;
|
|
189
|
+
/**
|
|
190
|
+
* What a failed Local Build switch request means (ADR-0024): a lost connection (a background Panel restarting) or a
|
|
191
|
+
* Portal's 5xx (it moved to the new code mid-request) may follow a switch that happened, so the state decides;
|
|
192
|
+
* anything else is the server saying it did not switch.
|
|
193
|
+
*/
|
|
194
|
+
export declare function switchFailure(e: {
|
|
195
|
+
network?: boolean;
|
|
196
|
+
status?: number;
|
|
197
|
+
}): 'wait' | 'check' | 'fail';
|
|
180
198
|
export declare function panelFixes(f: {
|
|
181
199
|
code?: string;
|
|
182
200
|
}, skillUpdatable: boolean): boolean;
|
|
@@ -361,6 +361,23 @@ export function panelHint(hint, fixable, action) {
|
|
|
361
361
|
* Whether the Panel fixes Finding `f` with a button of its own: a sideby skill behind this version while the
|
|
362
362
|
* Update plan says sideby may update it (`skillUpdatable`). Such Findings stay out of the prompt for an AI agent.
|
|
363
363
|
*/
|
|
364
|
+
/**
|
|
365
|
+
* A Local Build the Panel can leave for npm itself (ADR-0024): the global `sideby` still runs it and the Panel may
|
|
366
|
+
* write. Then switching to the release is the update, offered as a button instead of the `pnpm local --off` command.
|
|
367
|
+
*/
|
|
368
|
+
export function canLeaveLocal(u, readOnly) {
|
|
369
|
+
return !readOnly && !!u?.localSwitch && u.localSwitch.running === 'local';
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* What a failed Local Build switch request means (ADR-0024): a lost connection (a background Panel restarting) or a
|
|
373
|
+
* Portal's 5xx (it moved to the new code mid-request) may follow a switch that happened, so the state decides;
|
|
374
|
+
* anything else is the server saying it did not switch.
|
|
375
|
+
*/
|
|
376
|
+
export function switchFailure(e) {
|
|
377
|
+
if (e.network)
|
|
378
|
+
return 'wait';
|
|
379
|
+
return (e.status ?? 0) >= 500 ? 'check' : 'fail';
|
|
380
|
+
}
|
|
364
381
|
export function panelFixes(f, skillUpdatable) {
|
|
365
382
|
return skillUpdatable && f.code === 'skill.outdated';
|
|
366
383
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The Panel page's inline script. Plain browser JavaScript in a string (no template literals inside, so String.raw
|
|
2
2
|
// keeps it verbatim); the pure helpers from page-logic.ts are prepended so the tested code is the code that runs.
|
|
3
|
-
import { aliasArgsOf, aliasIssue, attentionReasons, attentionWhy, cacheHitRate, fixPrompt, formatDuration, handoffChangeOf, handoffHintAccount, inDisplayOrder, loginClusters, loginKey, maskEmail, matchesQuery, mergeDaily, newAccountDir, nextReset, panelFixes, panelHint, panelLang, passedWindow, pinnedIn, quotaPressure, remoteControlSummary, remoteControlTodo, reorder, sortAccounts, suggestAlias, suggestName, updatedAge, windowState, withPinned, } from './page-logic.js';
|
|
3
|
+
import { aliasArgsOf, aliasIssue, attentionReasons, attentionWhy, cacheHitRate, canLeaveLocal, fixPrompt, formatDuration, handoffChangeOf, handoffHintAccount, inDisplayOrder, loginClusters, loginKey, maskEmail, matchesQuery, mergeDaily, newAccountDir, nextReset, panelFixes, panelHint, panelLang, passedWindow, pinnedIn, quotaPressure, remoteControlSummary, remoteControlTodo, reorder, sortAccounts, suggestAlias, suggestName, switchFailure, updatedAge, windowState, withPinned, } from './page-logic.js';
|
|
4
4
|
const LOGIC = [
|
|
5
5
|
aliasArgsOf,
|
|
6
6
|
aliasIssue,
|
|
@@ -19,6 +19,8 @@ const LOGIC = [
|
|
|
19
19
|
mergeDaily,
|
|
20
20
|
newAccountDir,
|
|
21
21
|
nextReset,
|
|
22
|
+
canLeaveLocal,
|
|
23
|
+
switchFailure,
|
|
22
24
|
panelFixes,
|
|
23
25
|
panelHint,
|
|
24
26
|
panelLang,
|
|
@@ -273,6 +275,7 @@ const MAIN = String.raw `
|
|
|
273
275
|
x: 'M6 6l12 12M18 6L6 18',
|
|
274
276
|
arrowUp: 'M12 19V5M5 12l7-7 7 7',
|
|
275
277
|
arrowDown: 'M12 5v14M5 12l7 7 7-7',
|
|
278
|
+
swap: 'M7 4 3 8l4 4M3 8h14M17 20l4-4-4-4M21 16H7',
|
|
276
279
|
// Lucide "pin" (ISC license)
|
|
277
280
|
pin: 'M12 17v5M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z',
|
|
278
281
|
bot: 'M6 9h12a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2zM12 5v4M9.5 13.5v1M14.5 13.5v1',
|
|
@@ -3570,6 +3573,8 @@ const MAIN = String.raw `
|
|
|
3570
3573
|
function updState() { return BOOT.updates && S.state ? S.state.update || null : null }
|
|
3571
3574
|
// The skill agents read is part of the version (spec §3.23): behind a current CLI it is the one thing to update.
|
|
3572
3575
|
const isLocal = (u) => u.method.kind === 'local'
|
|
3576
|
+
// A Local Build the Panel can leave for npm itself (ADR-0024): then that switch is the update, not a command.
|
|
3577
|
+
const leavesLocal = (u) => canLeaveLocal(u, BOOT.readOnly)
|
|
3573
3578
|
const skillOld = (u) => !!(u && !u.available && !isLocal(u) && u.skillUpdate && u.skillUpdate.outdated)
|
|
3574
3579
|
const skillDue = (u) => skillOld(u) && u.skillUpdate.canRun
|
|
3575
3580
|
function skillFor(u) {
|
|
@@ -3689,7 +3694,8 @@ const MAIN = String.raw `
|
|
|
3689
3694
|
if (u && u.available) {
|
|
3690
3695
|
kids.push(h('p', { class: 'small' }, t('upd.available', { v: u.latest }), ' ',
|
|
3691
3696
|
h('a', { href: u.releaseNotes, target: '_blank', rel: 'noopener noreferrer' }, t('upd.notes'))))
|
|
3692
|
-
|
|
3697
|
+
if (!(isLocal(u) && leavesLocal(u)))
|
|
3698
|
+
kids.push(h('div', { class: 'popover-actions' }, h('button', { class: 'btn btn-sm btn-primary', type: 'button', onclick: toDialog }, t('upd.open'))))
|
|
3693
3699
|
} else if (skillOld(u)) {
|
|
3694
3700
|
kids.push(h('p', { class: 'small' }, t('upd.skillBehind', { s: skillFor(u) })))
|
|
3695
3701
|
kids.push(h('div', { class: 'popover-actions' }, h('button', { class: 'btn btn-sm' + (skillDue(u) ? ' btn-primary' : ''), type: 'button', onclick: toDialog }, t('upd.open'))))
|
|
@@ -3698,11 +3704,67 @@ const MAIN = String.raw `
|
|
|
3698
3704
|
vp.checking ? spinner() : icon('refresh'), vp.checking ? t('upd.checking') : t('upd.check'))))
|
|
3699
3705
|
}
|
|
3700
3706
|
if (vp.error) kids.push(errorNote(vp.error, null))
|
|
3707
|
+
const sw = localSwitchKids(u)
|
|
3708
|
+
if (sw) kids.push(sw)
|
|
3701
3709
|
if (u && !BOOT.readOnly && !isLocal(u))
|
|
3702
3710
|
kids.push(h('p', { class: 'muted small popover-foot' }, t(u.auto ? 'upd.autoIsOn' : 'upd.autoIsOff'), ' · ',
|
|
3703
3711
|
h('button', { class: 'link-btn', type: 'button', onclick: toDialog }, t('upd.settings'))))
|
|
3704
3712
|
return kids
|
|
3705
3713
|
}
|
|
3714
|
+
// The switch between a Local Build and the npm package (ADR-0024), only where the state offers it: the other side,
|
|
3715
|
+
// one button. After the switch the page waits for the server to run the new build and reloads into it.
|
|
3716
|
+
const vs = { busy: false, to: null, waitUntil: 0, stuck: false, error: null }
|
|
3717
|
+
function localSwitchKids(u) {
|
|
3718
|
+
const ls = u && u.localSwitch
|
|
3719
|
+
if (!ls || BOOT.readOnly) return null
|
|
3720
|
+
// While it switches the state already shows the other side; keep the words of the side clicked.
|
|
3721
|
+
const toNpm = vs.busy ? vs.to === 'npm' : ls.running === 'local'
|
|
3722
|
+
if (!toNpm && !ls.local) return null
|
|
3723
|
+
const l = ls.local
|
|
3724
|
+
const build = l ? l.version + '-dev · ' + (l.commit || '?') + (l.dirty ? '+' : '') : ''
|
|
3725
|
+
const kids = [h('p', { class: 'muted small' }, toNpm ? t('loc.toNpmHint') : t('loc.toLocalHint', { b: build, at: l.builtAt ? clock(l.builtAt) : '' }))]
|
|
3726
|
+
if (vs.stuck) {
|
|
3727
|
+
kids.push(h('p', { class: 'small' }, t('loc.stuck')))
|
|
3728
|
+
kids.push(h('div', { class: 'popover-actions' }, h('button', { class: 'btn btn-sm btn-primary', type: 'button', onclick: () => location.reload() }, icon('refresh'), t('loc.reload'))))
|
|
3729
|
+
} else {
|
|
3730
|
+
const newer = toNpm && u.available && u.latest
|
|
3731
|
+
kids.push(h('div', { class: 'popover-actions' }, h('button', { class: 'btn btn-sm' + (newer ? ' btn-primary' : ''), type: 'button', disabled: vs.busy, 'aria-busy': vs.busy ? 'true' : null, onclick: () => localSwitchTo(toNpm ? 'npm' : 'local') },
|
|
3732
|
+
vs.busy ? spinner() : icon('swap'), vs.busy ? t('loc.switching') : newer ? t('loc.toNpmV', { v: u.latest }) : t(toNpm ? 'loc.toNpm' : 'loc.toLocal'))))
|
|
3733
|
+
}
|
|
3734
|
+
if (vs.error) kids.push(errorNote(vs.error, null))
|
|
3735
|
+
return h('div', { class: 'popover-foot' }, kids)
|
|
3736
|
+
}
|
|
3737
|
+
function renderSwitch() {
|
|
3738
|
+
renderVerPop()
|
|
3739
|
+
if (dlg.kind === 'update' && up) renderUpdate()
|
|
3740
|
+
}
|
|
3741
|
+
async function localSwitchTo(to) {
|
|
3742
|
+
vs.busy = true
|
|
3743
|
+
vs.to = to
|
|
3744
|
+
vs.error = null
|
|
3745
|
+
vs.stuck = false
|
|
3746
|
+
renderSwitch()
|
|
3747
|
+
try {
|
|
3748
|
+
await api('/api/local', { to, confirm: true })
|
|
3749
|
+
} catch (e) {
|
|
3750
|
+
// A background Panel restarting from the new code may drop this answer, and a Portal moving to it may answer
|
|
3751
|
+
// 5xx (spec §3.23): the state then says whether the switch happened. Only a switch that did not is an error.
|
|
3752
|
+
const kind = switchFailure(e)
|
|
3753
|
+
if (kind === 'fail') { vs.busy = false; vs.error = e; renderSwitch(); return }
|
|
3754
|
+
if (kind === 'check') {
|
|
3755
|
+
await load({ silent: true })
|
|
3756
|
+
const ls = updState() && updState().localSwitch
|
|
3757
|
+
if (!ls || ls.running !== to) { vs.busy = false; vs.error = e; renderSwitch(); return }
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
vs.waitUntil = Date.now() + 20000
|
|
3761
|
+
localWait()
|
|
3762
|
+
}
|
|
3763
|
+
function localWait() {
|
|
3764
|
+
if (S.newBuild) { location.reload(); return }
|
|
3765
|
+
if (Date.now() > vs.waitUntil) { vs.busy = false; vs.stuck = true; renderSwitch(); return }
|
|
3766
|
+
setTimeout(() => { void load({ silent: true }).then(localWait, localWait) }, 1500)
|
|
3767
|
+
}
|
|
3706
3768
|
async function verPopCheck() {
|
|
3707
3769
|
vp.checking = true
|
|
3708
3770
|
vp.error = null
|
|
@@ -3789,6 +3851,14 @@ const MAIN = String.raw `
|
|
|
3789
3851
|
else body.push(...updSkill(u))
|
|
3790
3852
|
} else if (up.skillDone && !skillOld(u)) {
|
|
3791
3853
|
body.push(h('p', { class: 'note' }, t('upd.skillDoneOnly')))
|
|
3854
|
+
} else if (u.available && isLocal(u) && (leavesLocal(u) || vs.busy || vs.stuck)) {
|
|
3855
|
+
// A Local Build the Panel can leave (ADR-0024): switching to the release is the update, and brings the skill.
|
|
3856
|
+
body.push(h('p', null, t('upd.localSwitch', { v: u.latest }), ' ',
|
|
3857
|
+
h('a', { href: u.releaseNotes, target: '_blank', rel: 'noopener noreferrer' }, t('upd.notes'))))
|
|
3858
|
+
if (vs.busy) body.push(h('p', { class: 'muted small' }, spinner(), ' ', t('upd.reloading')))
|
|
3859
|
+
if (vs.stuck) body.push(h('p', { class: 'note' }, t('loc.stuck')))
|
|
3860
|
+
if (vs.error) body.push(errorNote(vs.error, null))
|
|
3861
|
+
body.push(updManual([h('p', { class: 'small' }, t('upd.localManual')), codeCopy(u.command)]))
|
|
3792
3862
|
} else if (u.available && isLocal(u)) {
|
|
3793
3863
|
// A Local Build (ADR-0024): one way back to npm, which brings the skill along.
|
|
3794
3864
|
body.push(h('p', null, t('upd.localAvail', { v: u.latest }), ' ',
|
|
@@ -3843,7 +3913,11 @@ const MAIN = String.raw `
|
|
|
3843
3913
|
if (up.error) body.push(errorNote(up.error, null))
|
|
3844
3914
|
const foot = [h('button', { class: 'btn', type: 'button', disabled: up.busy, onclick: () => dlg.el.close() }, t('btn.close'))]
|
|
3845
3915
|
const primary = (label, run) => h('button', { class: 'btn btn-primary', type: 'button', disabled: up.busy, 'aria-busy': up.busy ? 'true' : null, onclick: run }, up.busy ? spinner() : null, up.busy ? t('upd.updating') : label)
|
|
3846
|
-
if (u.available && u
|
|
3916
|
+
if (u.available && isLocal(u) && vs.stuck) foot.push(h('button', { class: 'btn btn-primary', type: 'button', onclick: () => location.reload() }, icon('refresh'), t('loc.reload')))
|
|
3917
|
+
else if (u.available && isLocal(u) && (leavesLocal(u) || vs.busy))
|
|
3918
|
+
foot.push(h('button', { class: 'btn btn-primary', type: 'button', disabled: vs.busy, 'aria-busy': vs.busy ? 'true' : null, onclick: () => localSwitchTo('npm') },
|
|
3919
|
+
vs.busy ? spinner() : icon('swap'), vs.busy ? t('loc.switching') : t('loc.toNpmV', { v: u.latest })))
|
|
3920
|
+
else if (u.available && u.canRun && !BOOT.readOnly && !up.done) foot.push(primary(t('upd.now', { v: u.latest }), updRun))
|
|
3847
3921
|
else if (skillDue(u) && !BOOT.readOnly && !up.done) foot.push(primary(t('upd.skillNow'), updSkillRun))
|
|
3848
3922
|
setKids(dlg.el, dialogFrame(t('upd.title'), null, body, foot))
|
|
3849
3923
|
}
|
|
@@ -200,8 +200,10 @@ code, .mono { font-family: var(--mono); font-size: 0.92em; }
|
|
|
200
200
|
.tag-login { font-weight: 400; }
|
|
201
201
|
.login-detail { flex-direction: column; gap: 8px; }
|
|
202
202
|
.login-detail .rd-note { margin: 0; }
|
|
203
|
-
|
|
204
|
-
.login-
|
|
203
|
+
/* The directories share one set of columns (subgrid), so a wider cell in one line moves every line alike. */
|
|
204
|
+
.login-dirs { display: grid; grid-template-columns: minmax(120px, 1fr) auto var(--l-w) minmax(0, 1.4fr) auto auto auto; column-gap: 14px; border: 1px solid var(--border); border-radius: var(--ctl-radius); background: var(--surface); }
|
|
205
|
+
.login-dir { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; align-items: center; padding: 6px 6px 6px 12px; }
|
|
206
|
+
.login-dir > * { justify-self: start; min-width: 0; }
|
|
205
207
|
.login-dir + .login-dir { border-top: 1px solid var(--border); }
|
|
206
208
|
.login-dir .ld-name { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
207
209
|
.login-dir .ld-name .name { font-weight: var(--strong-weight); }
|
|
@@ -697,7 +699,8 @@ select:is(:hover, :focus) { --chevron: var(--text); }
|
|
|
697
699
|
.stat:nth-child(n) { border-top: 0; }
|
|
698
700
|
.stat:nth-child(4) { border-left: 1px solid var(--border); }
|
|
699
701
|
.row-end .icon-btn { width: 36px; height: 36px; }
|
|
700
|
-
.login-
|
|
702
|
+
.login-dirs { grid-template-columns: minmax(0, 1fr); }
|
|
703
|
+
.login-dir { grid-template-columns: minmax(0, 1fr) auto auto; column-gap: 14px; row-gap: 6px; }
|
|
701
704
|
.login-dir .usage, .login-dir .health { display: none; }
|
|
702
705
|
.grid { grid-template-columns: 1fr; }
|
|
703
706
|
.panel { padding: 14px; }
|
package/package.json
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "sideby/local.json",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": [
|
|
6
|
+
"schemaVersion",
|
|
7
|
+
"running",
|
|
8
|
+
"local",
|
|
9
|
+
"kept",
|
|
10
|
+
"switched"
|
|
11
|
+
],
|
|
12
|
+
"properties": {
|
|
13
|
+
"schemaVersion": {
|
|
14
|
+
"type": "number",
|
|
15
|
+
"const": 1
|
|
16
|
+
},
|
|
17
|
+
"running": {
|
|
18
|
+
"anyOf": [
|
|
19
|
+
{
|
|
20
|
+
"type": "string",
|
|
21
|
+
"const": "local"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"type": "string",
|
|
25
|
+
"const": "npm"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"type": "null"
|
|
29
|
+
}
|
|
30
|
+
],
|
|
31
|
+
"description": "What the global sideby runs: the Local Build, an npm package, or neither (not an npm global install)."
|
|
32
|
+
},
|
|
33
|
+
"local": {
|
|
34
|
+
"anyOf": [
|
|
35
|
+
{
|
|
36
|
+
"type": "object",
|
|
37
|
+
"required": [
|
|
38
|
+
"commit",
|
|
39
|
+
"dirty",
|
|
40
|
+
"builtAt",
|
|
41
|
+
"version"
|
|
42
|
+
],
|
|
43
|
+
"properties": {
|
|
44
|
+
"commit": {
|
|
45
|
+
"type": "string"
|
|
46
|
+
},
|
|
47
|
+
"dirty": {
|
|
48
|
+
"type": "boolean"
|
|
49
|
+
},
|
|
50
|
+
"builtAt": {
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
"version": {
|
|
54
|
+
"type": "string"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"type": "null"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"description": "The newest Local Build, the one `on` switches to; null when `pnpm local` never ran."
|
|
63
|
+
},
|
|
64
|
+
"kept": {
|
|
65
|
+
"anyOf": [
|
|
66
|
+
{
|
|
67
|
+
"type": "string"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"type": "null"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"description": "The npm package version kept aside while a Local Build runs."
|
|
74
|
+
},
|
|
75
|
+
"to": {
|
|
76
|
+
"anyOf": [
|
|
77
|
+
{
|
|
78
|
+
"type": "string",
|
|
79
|
+
"const": "local"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"type": "string",
|
|
83
|
+
"const": "npm"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
"switched": {
|
|
88
|
+
"type": "boolean",
|
|
89
|
+
"description": "A switch ran and succeeded; false for status, a preview (exit 10) or a failure."
|
|
90
|
+
},
|
|
91
|
+
"blocked": {
|
|
92
|
+
"type": "string",
|
|
93
|
+
"description": "Why the switch cannot run, with what to do."
|
|
94
|
+
},
|
|
95
|
+
"message": {
|
|
96
|
+
"type": "string"
|
|
97
|
+
},
|
|
98
|
+
"restartedPanel": {
|
|
99
|
+
"type": "boolean"
|
|
100
|
+
},
|
|
101
|
+
"skillSynced": {
|
|
102
|
+
"type": "boolean",
|
|
103
|
+
"description": "Back on npm, `sideby update --yes` brought the sideby skill to that version (true) or failed (false)."
|
|
104
|
+
},
|
|
105
|
+
"_notice": {
|
|
106
|
+
"type": "object",
|
|
107
|
+
"properties": {
|
|
108
|
+
"update": {
|
|
109
|
+
"type": "object",
|
|
110
|
+
"required": [
|
|
111
|
+
"current",
|
|
112
|
+
"latest",
|
|
113
|
+
"command",
|
|
114
|
+
"message"
|
|
115
|
+
],
|
|
116
|
+
"properties": {
|
|
117
|
+
"current": {
|
|
118
|
+
"type": "string"
|
|
119
|
+
},
|
|
120
|
+
"latest": {
|
|
121
|
+
"type": "string"
|
|
122
|
+
},
|
|
123
|
+
"command": {
|
|
124
|
+
"type": "string",
|
|
125
|
+
"description": "Run it after the user agrees: it shows the plan and exits 10; then --yes."
|
|
126
|
+
},
|
|
127
|
+
"message": {
|
|
128
|
+
"type": "string"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
"skill": {
|
|
133
|
+
"type": "object",
|
|
134
|
+
"required": [
|
|
135
|
+
"installed",
|
|
136
|
+
"current",
|
|
137
|
+
"command",
|
|
138
|
+
"message"
|
|
139
|
+
],
|
|
140
|
+
"properties": {
|
|
141
|
+
"installed": {
|
|
142
|
+
"anyOf": [
|
|
143
|
+
{
|
|
144
|
+
"type": "string"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"type": "null"
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
"description": "The skill's version; null when it has none."
|
|
151
|
+
},
|
|
152
|
+
"current": {
|
|
153
|
+
"type": "string"
|
|
154
|
+
},
|
|
155
|
+
"command": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "`sideby update`, which updates the skill with sideby: run it after the user agrees; it shows the plan and exits 10, then --yes."
|
|
158
|
+
},
|
|
159
|
+
"message": {
|
|
160
|
+
"type": "string"
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
"description": "Not the answer to the task: mention it briefly afterwards and offer to update."
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"title": "sideby local --json"
|
|
169
|
+
}
|
package/skills/sideby/SKILL.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: sideby
|
|
3
3
|
description: Use when the user runs, or wants to run, several AI coding accounts (Claude Code, Codex, Grok Build, pi) side by side. Covers installing sideby and setting up a new account ("install sideby", "set up another Claude account", "add cc008", "配置一个 cc00x", "再加一个 Codex 账号"), explaining what accounts share and what stays separate ("what do my accounts share?", "这些账号共享了什么"), showing which account a session is ("show [cc001] in the status line", "加上账号标识"), seeing accounts, quota and usage, moving on when one hits its 5-hour or weekly limit, moving the current task to another account ("用 cc008 继续", "交回 cc008"), adding a short command (alias), and repairing drifted account directories (skills, hooks, rules or settings not shared, wrong symlinks, credential file permissions). Drives the `sideby` CLI with `--json`; the agent does every step except browser sign-in, filling in secrets and starting interactive sessions, which it hands to the user as one command (inside an Auto Handoff session, `sideby handoff ready --to` moves the task itself).
|
|
4
4
|
metadata:
|
|
5
|
-
version: "0.
|
|
5
|
+
version: "0.16.1"
|
|
6
6
|
requires:
|
|
7
7
|
bins: [sideby, node]
|
|
8
8
|
cliHelp: sideby --help
|
|
@@ -45,6 +45,7 @@ Account refs are `<family>:<name>`; a bare `<name>` works only when it is unique
|
|
|
45
45
|
| Turn quota checks on or off, or refresh fetched quota | `sideby quota check <family> on|off --json`, `sideby quota [family] --refresh --json` | `references/quota.md` |
|
|
46
46
|
| Set the model, effort or Remote Control | `sideby model <ref> --json`, `sideby remote-control --json` | `references/panel.md` |
|
|
47
47
|
| Turn Auto Update on or off | `sideby update --auto on|off --json` | `references/update.md` |
|
|
48
|
+
| Switch between a local build of sideby and the release ("切换到正式版", "切回 dev 版本") | `sideby local --json`, `sideby local on|off --json` (exits 10 first) | `references/update.md` |
|
|
48
49
|
| Repair drift or permissions | `sideby doctor [ref] --json`, then `--fix` after they agree | `references/diagnose.md` |
|
|
49
50
|
| Keep working because an account hit its limit | `sideby next <family> --json` | `references/handoff.md` |
|
|
50
51
|
| Turn on Auto Handoff ("自动交接", "hand over when quota runs low"), see what it still needs, hand this session over now, or pick an account with room for a subtask | `sideby handoff status --json`, `sideby handoff enable --json` (exit 10 first), `sideby handoff ready`, `sideby next <family> --json` | `references/handoff.md` |
|
|
@@ -18,3 +18,12 @@ What to do:
|
|
|
18
18
|
## Auto Update
|
|
19
19
|
|
|
20
20
|
When the user asks to change Auto Update, run `sideby update --auto on|off --json`. It saves immediately and returns `auto` and `changed` (exit 0, including no-op). It keeps the Update Check setting and does not check or install in this invocation. It needs no `--yes`; ordinary `sideby update --json` still previews an installation with exit 10. Never enable Auto Update unless the user asks.
|
|
21
|
+
|
|
22
|
+
## Local Build and the release
|
|
23
|
+
|
|
24
|
+
For people who work on sideby itself. A Local Build (`pnpm local` in a sideby checkout) runs the checkout's code behind the global `sideby`. When the user asks to go back to the release, or to the local build again ("切换到正式版", "切回 dev 版本"):
|
|
25
|
+
|
|
26
|
+
1. Run `sideby local --json`. `running` is what runs now (`local` or `npm`), `local` the kept Local Build (`version`, `commit`, `dirty`, `builtAt`), `kept` the npm version set aside.
|
|
27
|
+
2. Run `sideby local off --json` (back to npm) or `sideby local on --json` (the kept Local Build). It exits `10` with the change; after their yes, add `--yes`. `on` does not rebuild: `pnpm local` in the checkout makes a new build. `blocked` (exit 1) says why it cannot and what to do.
|
|
28
|
+
3. `off` brings the sideby skill to the npm version too (`skillSynced`; false means run `sideby update`). A background panel restarts by itself and open panels reload; a `sideby ui` in a terminal needs a restart.
|
|
29
|
+
|