sideby 0.14.0 → 0.16.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 +1 -0
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +7 -0
- package/dist/src/cli/commands.js +259 -9
- package/dist/src/cli/json-schemas.js +109 -2
- package/dist/src/cli/main.js +30 -6
- package/dist/src/core/config.d.ts +1 -0
- package/dist/src/core/config.js +8 -2
- package/dist/src/core/local-build.d.ts +63 -0
- package/dist/src/core/local-build.js +189 -0
- package/dist/src/core/skill-check.d.ts +14 -0
- package/dist/src/core/skill-check.js +29 -3
- package/dist/src/core/update.d.ts +73 -1
- package/dist/src/core/update.js +155 -23
- package/dist/src/panel/handler.d.ts +12 -1
- package/dist/src/panel/handler.js +84 -5
- package/dist/src/panel/i18n.d.ts +125 -5
- package/dist/src/panel/i18n.js +280 -40
- package/dist/src/panel/page-logic.d.ts +41 -0
- package/dist/src/panel/page-logic.js +46 -0
- package/dist/src/panel/page-script.js +616 -162
- package/dist/src/panel/page-styles.js +74 -23
- package/dist/src/panel/page.js +3 -0
- package/dist/src/runtime.d.ts +2 -0
- package/package.json +3 -2
- package/schemas/alias.json +1 -1
- package/schemas/app-install.json +1 -1
- package/schemas/app-uninstall.json +1 -1
- package/schemas/browser-open.json +1 -1
- package/schemas/browser-switch.json +1 -1
- package/schemas/browser.json +1 -1
- package/schemas/config.json +15 -1
- package/schemas/desktop-remove.json +1 -1
- package/schemas/desktop.json +1 -1
- package/schemas/doctor.json +1 -1
- package/schemas/error.json +1 -1
- package/schemas/families.json +1 -1
- package/schemas/handoff-disable.json +1 -1
- package/schemas/handoff-enable.json +1 -1
- package/schemas/handoff-ready.json +1 -1
- package/schemas/handoff-setup.json +1 -1
- package/schemas/handoff-status.json +282 -1
- package/schemas/handoff-teardown.json +1 -1
- package/schemas/list.json +6 -1
- package/schemas/local.json +165 -0
- package/schemas/model.json +1 -1
- package/schemas/new.json +1 -1
- package/schemas/next.json +1 -1
- package/schemas/order.json +148 -0
- package/schemas/pin.json +126 -0
- package/schemas/plugins.json +1 -1
- package/schemas/quota-check.json +88 -0
- package/schemas/quota-setup.json +1 -1
- package/schemas/quota-teardown.json +1 -1
- package/schemas/quota.json +1 -1
- package/schemas/remote-control.json +1 -1
- package/schemas/tune-change.json +1 -1
- package/schemas/tune.json +1 -1
- package/schemas/unpin.json +126 -0
- package/schemas/update-auto.json +84 -0
- package/schemas/update.json +79 -1
- package/skills/sideby/SKILL.md +8 -3
- package/skills/sideby/references/handoff.md +8 -0
- package/skills/sideby/references/panel.md +28 -0
- package/skills/sideby/references/quota.md +6 -0
- package/skills/sideby/references/update.md +15 -2
package/dist/src/core/update.js
CHANGED
|
@@ -6,7 +6,7 @@ import { access, constants, mkdir, open, readdir, readFile, realpath, rm, stat }
|
|
|
6
6
|
import { dirname, join, sep } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { writeFileAtomic } from './fs-safe.js';
|
|
9
|
-
import {
|
|
9
|
+
import { installedSkills, SKILL_UPDATE, SKILL_UPDATE_SHOWN, skillOwnerRepo } from './skill-check.js';
|
|
10
10
|
import { localBuild, packageVersion, parseLocalBuild } from './version.js';
|
|
11
11
|
export const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
12
12
|
export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -102,6 +102,14 @@ export function bindUpdateDeps(deps) {
|
|
|
102
102
|
};
|
|
103
103
|
}
|
|
104
104
|
const deps = () => ({ ...realDeps, ...override });
|
|
105
|
+
/** Runs a program through the swappable machine (tests bind `exec`); a Local Build switch installs with it. */
|
|
106
|
+
export const runProgram = (file, args, opts) => deps().exec(file, args, opts);
|
|
107
|
+
/** Starts the background Panel again from the code `sideby` now names; see `UpdateDeps.restartPanel`. */
|
|
108
|
+
export const restartPanelByName = (env, port) => deps()
|
|
109
|
+
.restartPanel(env, port)
|
|
110
|
+
.catch(() => false);
|
|
111
|
+
/** The lock every install shares (`update-install.lock`), so a Local Build switch never races an Update. */
|
|
112
|
+
export const takeInstallLock = (paths) => takeLock(paths, 'update-install.lock').catch(() => null);
|
|
105
113
|
// Build metadata is limited to what semver allows, so a version never carries a terminal escape into the notice.
|
|
106
114
|
const VERSION = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
|
|
107
115
|
function parseVersion(v) {
|
|
@@ -151,21 +159,25 @@ export function sameLine(from, to) {
|
|
|
151
159
|
const set = (v) => v !== undefined && v !== '' && v !== '0' && v.toLowerCase() !== 'false';
|
|
152
160
|
/** True in CI, where a notice is noise and a check is a request nobody reads. */
|
|
153
161
|
export const inCi = (env) => set(env.CI) || set(env.CONTINUOUS_INTEGRATION) || set(env.BUILD_NUMBER);
|
|
154
|
-
/** Why Update Checks are off, or null when they are on (spec §3.23). */
|
|
155
|
-
export function
|
|
162
|
+
/** Why Update Checks are off, with a code for the Panel, or null when they are on (spec §3.23). */
|
|
163
|
+
export function checksOffWhy(env, config, local = localBuild() !== null) {
|
|
156
164
|
// A Local Build (ADR-0024) follows the checkout: a notice or an Auto Update would only replace it with npm's.
|
|
157
165
|
if (local)
|
|
158
|
-
return 'this is a local build (pnpm local)';
|
|
166
|
+
return { code: 'local-build', text: 'this is a local build (pnpm local)' };
|
|
159
167
|
if (set(env.SIDEBY_NO_UPDATE_NOTIFIER))
|
|
160
|
-
return 'SIDEBY_NO_UPDATE_NOTIFIER is set';
|
|
168
|
+
return { code: 'env', text: 'SIDEBY_NO_UPDATE_NOTIFIER is set' };
|
|
161
169
|
if (set(env.NO_UPDATE_NOTIFIER))
|
|
162
|
-
return 'NO_UPDATE_NOTIFIER is set';
|
|
170
|
+
return { code: 'env', text: 'NO_UPDATE_NOTIFIER is set' };
|
|
163
171
|
if (config.update?.check === false)
|
|
164
|
-
return 'config update.check is false';
|
|
172
|
+
return { code: 'config', text: 'config update.check is false' };
|
|
165
173
|
if (inCi(env))
|
|
166
|
-
return 'running in CI';
|
|
174
|
+
return { code: 'ci', text: 'running in CI' };
|
|
167
175
|
return null;
|
|
168
176
|
}
|
|
177
|
+
/** Why Update Checks are off, or null when they are on (spec §3.23). */
|
|
178
|
+
export function checksOff(env, config, local = localBuild() !== null) {
|
|
179
|
+
return checksOffWhy(env, config, local)?.text ?? null;
|
|
180
|
+
}
|
|
169
181
|
// ---- registry
|
|
170
182
|
/** The registry checks and installs use: `SIDEBY_UPDATE_REGISTRY` (http or https only), else npm's. */
|
|
171
183
|
function registryOf(env) {
|
|
@@ -217,6 +229,18 @@ export async function readUpdateCache(paths) {
|
|
|
217
229
|
message: a.message,
|
|
218
230
|
...(a.shown ? { shown: true } : {}),
|
|
219
231
|
};
|
|
232
|
+
const sn = d.skillNotified;
|
|
233
|
+
if (sn && str(sn.version) && str(sn.at))
|
|
234
|
+
out.skillNotified = { version: sn.version, at: sn.at };
|
|
235
|
+
const sa = d.skillAuto;
|
|
236
|
+
if (sa && str(sa.to) && str(sa.at) && typeof sa.ok === 'boolean' && str(sa.message))
|
|
237
|
+
out.skillAuto = {
|
|
238
|
+
to: sa.to,
|
|
239
|
+
at: sa.at,
|
|
240
|
+
ok: sa.ok,
|
|
241
|
+
message: sa.message,
|
|
242
|
+
...(sa.shown ? { shown: true } : {}),
|
|
243
|
+
};
|
|
220
244
|
return out;
|
|
221
245
|
}
|
|
222
246
|
catch {
|
|
@@ -352,6 +376,44 @@ export function autoUpdateLine(cache) {
|
|
|
352
376
|
? `sideby updated itself from ${a.from} to ${a.to} (${RELEASE_NOTES(a.to)})`
|
|
353
377
|
: `sideby could not update itself to ${a.to}: ${a.message}`;
|
|
354
378
|
}
|
|
379
|
+
/** What the CLI says once about the skill Auto Update brought up to date alone, or null. */
|
|
380
|
+
export function skillAutoLine(cache) {
|
|
381
|
+
const a = cache.skillAuto;
|
|
382
|
+
if (!a || a.shown)
|
|
383
|
+
return null;
|
|
384
|
+
return a.ok
|
|
385
|
+
? `sideby updated the sideby skill agents read to ${a.to}`
|
|
386
|
+
: `sideby could not update the sideby skill to ${a.to}: ${a.message}`;
|
|
387
|
+
}
|
|
388
|
+
/** The skill part of a `SkillUpdatePlan` toward the running version, as `afterCommandPlan` takes it. */
|
|
389
|
+
export const skillBehind = (plan, current) => plan?.outdated
|
|
390
|
+
? { installed: plan.installed.find((v) => v !== current) ?? null, canRun: plan.canRun }
|
|
391
|
+
: null;
|
|
392
|
+
/**
|
|
393
|
+
* The line about a skill behind this version the CLI may print now (spec §3.23), or null: once a day per CLI version,
|
|
394
|
+
* and never while Auto Update is on (the background check brings the skill along) and sideby may update it.
|
|
395
|
+
*/
|
|
396
|
+
export function skillNoticeLine(cache, current, config, skill, now) {
|
|
397
|
+
if (!skill || (config.update?.auto === true && skill.canRun))
|
|
398
|
+
return null;
|
|
399
|
+
const n = cache.skillNotified;
|
|
400
|
+
if (n && n.version === current && age(n.at, now) < NOTICE_INTERVAL_MS)
|
|
401
|
+
return null;
|
|
402
|
+
return `the sideby skill agents read is for ${skill.installed ?? 'an older sideby'} (this sideby is ${current}) · run \`sideby update\``;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Auto Update is on, the CLI is the newest version the cache knows, and the skill is behind it where sideby may
|
|
406
|
+
* update it, not tried for this version yet (or a failed try a day ago). A newer CLI waiting is left to the Update
|
|
407
|
+
* of the CLI: the skills tool fetches the default branch, which would put the skill ahead of this CLI.
|
|
408
|
+
*/
|
|
409
|
+
export function skillAutoPending(cache, current, config, skill, now) {
|
|
410
|
+
if (config.update?.auto !== true || !skill?.canRun || isNewer(cache.latest, current))
|
|
411
|
+
return false;
|
|
412
|
+
const a = cache.skillAuto;
|
|
413
|
+
if (!a || a.to !== current)
|
|
414
|
+
return true;
|
|
415
|
+
return !a.ok && !(age(a.at, now) < AUTO_RETRY_MS);
|
|
416
|
+
}
|
|
355
417
|
/**
|
|
356
418
|
* The `_notice` for a `--json` output, from the cached Update Check and the versions of the installed sideby skills
|
|
357
419
|
* (`null` for one written before skills had a version); null when there is nothing to say or checks are off. It
|
|
@@ -383,27 +445,40 @@ export function jsonNotice(cache, current, config, env, skills, skillCommand) {
|
|
|
383
445
|
* then the Update Notice), the change that records them, and whether to start a background check. The caller prints,
|
|
384
446
|
* writes `record` with `updateUpdateCache` and starts the check; nothing here touches the machine.
|
|
385
447
|
*/
|
|
386
|
-
export function afterCommandPlan(cache, current, config, env, now) {
|
|
448
|
+
export function afterCommandPlan(cache, current, config, env, now, skill = null) {
|
|
387
449
|
if (checksOff(env, config))
|
|
388
450
|
return { lines: [], record: null, startCheck: false };
|
|
389
451
|
const lines = [];
|
|
390
452
|
const auto = autoUpdateLine(cache);
|
|
453
|
+
const skillAuto = skillAutoLine(cache);
|
|
391
454
|
const notice = noticeLine(cache, current, now);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
455
|
+
// A newer CLI's notice already leads to `sideby update`, which brings the skill along.
|
|
456
|
+
const skillNotice = notice ? null : skillNoticeLine(cache, current, config, skill, now);
|
|
457
|
+
for (const l of [auto, skillAuto, notice, skillNotice])
|
|
458
|
+
if (l)
|
|
459
|
+
lines.push(l);
|
|
396
460
|
const shownAuto = cache.auto;
|
|
461
|
+
const shownSkillAuto = cache.skillAuto;
|
|
397
462
|
const record = lines.length
|
|
398
463
|
? (cur) => ({
|
|
399
464
|
// Mark only the Auto Update that was printed; a newer one written meanwhile stays to be said.
|
|
400
465
|
...(auto && cur.auto && cur.auto.at === shownAuto?.at && cur.auto.to === shownAuto.to
|
|
401
466
|
? { auto: { ...cur.auto, shown: true } }
|
|
402
467
|
: {}),
|
|
468
|
+
...(skillAuto && cur.skillAuto && cur.skillAuto.at === shownSkillAuto?.at
|
|
469
|
+
? { skillAuto: { ...cur.skillAuto, shown: true } }
|
|
470
|
+
: {}),
|
|
403
471
|
...(notice ? { notified: { version: cache.latest, at: now.toISOString() } } : {}),
|
|
472
|
+
...(skillNotice ? { skillNotified: { version: current, at: now.toISOString() } } : {}),
|
|
404
473
|
})
|
|
405
474
|
: null;
|
|
406
|
-
return {
|
|
475
|
+
return {
|
|
476
|
+
lines,
|
|
477
|
+
record,
|
|
478
|
+
startCheck: checkDue(cache, now) ||
|
|
479
|
+
autoPending(cache, current, config, now) ||
|
|
480
|
+
skillAutoPending(cache, current, config, skill, now),
|
|
481
|
+
};
|
|
407
482
|
}
|
|
408
483
|
// ---- Install Method
|
|
409
484
|
/** Where the running copy comes from (spec §3.23 table); reads only paths. */
|
|
@@ -421,16 +496,20 @@ export async function installMethod(env) {
|
|
|
421
496
|
if (parseLocalBuild(JSON.parse(await readFile(join(packageDir, 'dist', 'build.json'), 'utf8'))))
|
|
422
497
|
return at('local', {
|
|
423
498
|
reason: 'it is a local build; run `pnpm local --off` in the sideby checkout to go back to npm',
|
|
499
|
+
reasonCode: 'local-build',
|
|
424
500
|
});
|
|
425
501
|
}
|
|
426
502
|
catch {
|
|
427
503
|
// no build file: not a Local Build
|
|
428
504
|
}
|
|
429
505
|
if (p.includes('/_npx/'))
|
|
430
|
-
return at('npx', {
|
|
506
|
+
return at('npx', {
|
|
507
|
+
reason: 'it runs from the npx cache, which fetches a version per call',
|
|
508
|
+
reasonCode: 'npx',
|
|
509
|
+
});
|
|
431
510
|
// Volta keeps a global package under its own npm prefix; installing there behind its back breaks its records.
|
|
432
511
|
if (p.includes('/.volta/'))
|
|
433
|
-
return at('volta', { reason: 'Volta manages this install' });
|
|
512
|
+
return at('volta', { reason: 'Volta manages this install', reasonCode: 'volta' });
|
|
434
513
|
const npm = /^(.*)\/lib\/node_modules\/sideby$/.exec(p);
|
|
435
514
|
if (npm) {
|
|
436
515
|
const prefix = npm[1];
|
|
@@ -442,7 +521,11 @@ export async function installMethod(env) {
|
|
|
442
521
|
prefix,
|
|
443
522
|
canUpdate: !blocked.length,
|
|
444
523
|
...(blocked.length
|
|
445
|
-
? {
|
|
524
|
+
? {
|
|
525
|
+
reason: `${blocked.join(' and ')} ${blocked.length > 1 ? 'are' : 'is'} not writable by you`,
|
|
526
|
+
reasonCode: 'not-writable',
|
|
527
|
+
reasonAt: blocked.join(', '),
|
|
528
|
+
}
|
|
446
529
|
: {}),
|
|
447
530
|
});
|
|
448
531
|
}
|
|
@@ -452,8 +535,8 @@ export async function installMethod(env) {
|
|
|
452
535
|
if (p.includes('/.bun/install/global/'))
|
|
453
536
|
return at('bun', { canUpdate: true });
|
|
454
537
|
if (p.includes('/node_modules/'))
|
|
455
|
-
return at('project', { reason: 'it is a dependency of a project' });
|
|
456
|
-
return at('source', { reason: 'it runs from a source checkout' });
|
|
538
|
+
return at('project', { reason: 'it is a dependency of a project', reasonCode: 'project' });
|
|
539
|
+
return at('source', { reason: 'it runs from a source checkout', reasonCode: 'source' });
|
|
457
540
|
}
|
|
458
541
|
/**
|
|
459
542
|
* The command an Update runs (or that the user runs) for `version`; `run` is set only when sideby may run it. With
|
|
@@ -548,6 +631,8 @@ export const shownMethod = (m) => ({
|
|
|
548
631
|
kind: m.kind,
|
|
549
632
|
canUpdate: m.canUpdate,
|
|
550
633
|
...(m.reason ? { reason: m.reason } : {}),
|
|
634
|
+
...(m.reasonCode ? { reasonCode: m.reasonCode } : {}),
|
|
635
|
+
...(m.reasonAt ? { reasonAt: m.reasonAt } : {}),
|
|
551
636
|
});
|
|
552
637
|
/** The plan as `sideby update --json` and the Panel show it (schemas/update.json). */
|
|
553
638
|
export function shownPlan(plan) {
|
|
@@ -575,14 +660,17 @@ export async function skillUpdatePlan(home, target) {
|
|
|
575
660
|
canRun: true,
|
|
576
661
|
};
|
|
577
662
|
for (const s of skills) {
|
|
578
|
-
|
|
663
|
+
// A git repository owns the skill unless the skills tool records installing it there (spec §3.23).
|
|
664
|
+
const root = await skillOwnerRepo(s.dir);
|
|
579
665
|
if (root) {
|
|
580
666
|
const at = root.startsWith(home) ? `~${root.slice(home.length)}` : root;
|
|
581
667
|
return {
|
|
582
668
|
...plan,
|
|
583
|
-
command:
|
|
669
|
+
command: SKILL_UPDATE_SHOWN,
|
|
584
670
|
canRun: false,
|
|
585
671
|
reason: `the skill is in the git repository ${at}, so sideby leaves it alone; update it where that repository comes from`,
|
|
672
|
+
reasonCode: 'skill-in-git',
|
|
673
|
+
reasonAt: at,
|
|
586
674
|
};
|
|
587
675
|
}
|
|
588
676
|
}
|
|
@@ -609,6 +697,27 @@ export async function applySkillUpdate(plan, env, onOutput) {
|
|
|
609
697
|
? { ok: true, message: 'updated the sideby skill' }
|
|
610
698
|
: { ok: false, message: `\`${plan.command}\` failed (exit ${r.code}); run it yourself to see why` };
|
|
611
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Updates only the sideby skill, for a CLI that is already current (`sideby update`, the Panel, Auto Update). Holds
|
|
702
|
+
* the same lock as an Update, so a skill update never runs beside one; null when there was nothing to do.
|
|
703
|
+
*/
|
|
704
|
+
export async function applySkillOnly(paths, plan, env, onOutput) {
|
|
705
|
+
if (!plan?.outdated || !plan.canRun)
|
|
706
|
+
return null;
|
|
707
|
+
const release = await takeLock(paths, 'update-install.lock').catch(() => null);
|
|
708
|
+
if (!release)
|
|
709
|
+
return {
|
|
710
|
+
ok: false,
|
|
711
|
+
busy: true,
|
|
712
|
+
message: 'another sideby update is running; wait for it to finish, then run `sideby update` again',
|
|
713
|
+
};
|
|
714
|
+
try {
|
|
715
|
+
return await applySkillUpdate(plan, env, onOutput);
|
|
716
|
+
}
|
|
717
|
+
finally {
|
|
718
|
+
await release().catch(() => { });
|
|
719
|
+
}
|
|
720
|
+
}
|
|
612
721
|
/**
|
|
613
722
|
* Runs the Update the plan names, then starts a running background Panel again from the new code and, when the
|
|
614
723
|
* desktop app is installed, runs `sideby app install` again from the new code. The caller has
|
|
@@ -689,6 +798,22 @@ export async function autoUpdateBlocker(paths, plan) {
|
|
|
689
798
|
return `a session is running (pid ${running.join(', ')})`;
|
|
690
799
|
return null;
|
|
691
800
|
}
|
|
801
|
+
/**
|
|
802
|
+
* The skill half of Auto Update (spec §3.23): with the CLI current, a skill behind it is updated alone and the result
|
|
803
|
+
* recorded once per version, so a failure waits a day.
|
|
804
|
+
*/
|
|
805
|
+
async function skillAutoSync(paths, env, config, cache, current, now) {
|
|
806
|
+
const plan = await skillUpdatePlan(paths.home, current).catch(() => null);
|
|
807
|
+
if (!skillAutoPending(cache, current, config, skillBehind(plan, current), now))
|
|
808
|
+
return;
|
|
809
|
+
const r = await applySkillOnly(paths, plan ?? undefined, env);
|
|
810
|
+
// A manual Update holding the lock is not a failure; the next command looks again.
|
|
811
|
+
if (!r || r.busy)
|
|
812
|
+
return;
|
|
813
|
+
await updateUpdateCache(paths, {
|
|
814
|
+
skillAuto: { to: current, at: deps().now().toISOString(), ok: r.ok, message: r.message },
|
|
815
|
+
});
|
|
816
|
+
}
|
|
692
817
|
/**
|
|
693
818
|
* Body of the detached `sideby update-check`: an Update Check when one is due, then an Auto Update when config
|
|
694
819
|
* `update.auto` is on and nothing blocks it. Only one runs at a time; it never throws.
|
|
@@ -705,9 +830,16 @@ export async function backgroundCheck(paths, env, config, panelPort) {
|
|
|
705
830
|
if (checkDue(cache, now))
|
|
706
831
|
cache = await refreshUpdateCheck(paths, env);
|
|
707
832
|
const current = packageVersion();
|
|
708
|
-
if (!autoPending(cache, current, config, now))
|
|
833
|
+
if (!autoPending(cache, current, config, now)) {
|
|
834
|
+
await skillAutoSync(paths, env, config, cache, current, now);
|
|
709
835
|
return;
|
|
710
|
-
|
|
836
|
+
}
|
|
837
|
+
// The skill teaches agents the new version's commands, so an Auto Update carries it as `sideby update` does.
|
|
838
|
+
const skill = await skillUpdatePlan(paths.home, cache.latest).catch(() => null);
|
|
839
|
+
const plan = {
|
|
840
|
+
...planFrom(await installMethod(env), current, cache.latest, env),
|
|
841
|
+
...(skill ? { skill } : {}),
|
|
842
|
+
};
|
|
711
843
|
await updateUpdateCache(paths, { autoTriedAt: now.toISOString() });
|
|
712
844
|
// A blocker only the user can lift is said once and looked at again a day later, like a failed install; a
|
|
713
845
|
// running session only delays it (the next look is within the hour).
|
|
@@ -81,12 +81,15 @@ export interface PanelUpdate {
|
|
|
81
81
|
checkedAt?: string;
|
|
82
82
|
/** Why Update Checks are off; absent when they are on. */
|
|
83
83
|
checksOff?: string;
|
|
84
|
+
checksOffCode?: update.ChecksOffCode;
|
|
84
85
|
/** Config `update.auto`. */
|
|
85
86
|
auto: boolean;
|
|
86
87
|
method: {
|
|
87
88
|
kind: update.InstallKind;
|
|
88
89
|
canUpdate: boolean;
|
|
89
90
|
reason?: string;
|
|
91
|
+
reasonCode?: update.ReasonCode;
|
|
92
|
+
reasonAt?: string;
|
|
90
93
|
};
|
|
91
94
|
/** The command an Update runs, or the one to run by hand when `canRun` is false. */
|
|
92
95
|
command?: string;
|
|
@@ -94,8 +97,16 @@ export interface PanelUpdate {
|
|
|
94
97
|
releaseNotes?: string;
|
|
95
98
|
/** The command that installs the sideby skill by hand. */
|
|
96
99
|
skill: string;
|
|
97
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* The installed sideby skill, which an Update brings along (spec §3.23); absent when none is installed, and for a
|
|
102
|
+
* Local Build, which `pnpm local --off` brings back to npm together with the skill.
|
|
103
|
+
*/
|
|
98
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;
|
|
99
110
|
}
|
|
100
111
|
export interface PanelState {
|
|
101
112
|
schemaVersion: 1;
|
|
@@ -53,7 +53,7 @@ let checking = null;
|
|
|
53
53
|
*/
|
|
54
54
|
async function updateState(rt, readOnly) {
|
|
55
55
|
const current = packageVersion();
|
|
56
|
-
const off = update.
|
|
56
|
+
const off = update.checksOffWhy(rt.env, rt.config);
|
|
57
57
|
const cache = await update.readUpdateCache(rt.paths);
|
|
58
58
|
if (!off && !readOnly && !checking && update.checkDue(cache, new Date()))
|
|
59
59
|
checking = update.refreshUpdateCheck(rt.paths, rt.env).finally(() => {
|
|
@@ -61,23 +61,55 @@ async function updateState(rt, readOnly) {
|
|
|
61
61
|
});
|
|
62
62
|
const method = await update.installMethod(rt.env);
|
|
63
63
|
const plan = cache.latest ? update.shownPlan(update.planFrom(method, current, cache.latest, rt.env)) : null;
|
|
64
|
-
const skillUpdate =
|
|
65
|
-
|
|
66
|
-
.catch(() => null);
|
|
64
|
+
const skillUpdate = method.kind === 'local'
|
|
65
|
+
? null
|
|
66
|
+
: await update.skillUpdatePlan(rt.paths.home, plan?.available ? plan.latest : current).catch(() => null);
|
|
67
67
|
return {
|
|
68
68
|
current,
|
|
69
69
|
...(cache.latest ? { latest: cache.latest } : {}),
|
|
70
70
|
available: Boolean(plan?.available),
|
|
71
71
|
...(cache.checkedAt ? { checkedAt: cache.checkedAt } : {}),
|
|
72
|
-
...(off ? { checksOff: off } : {}),
|
|
72
|
+
...(off ? { checksOff: off.text, checksOffCode: off.code } : {}),
|
|
73
73
|
auto: rt.config.update?.auto === true,
|
|
74
74
|
method: update.shownMethod(method),
|
|
75
75
|
...(plan ? { command: plan.command, releaseNotes: plan.releaseNotes } : {}),
|
|
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 = {};
|
|
@@ -329,6 +361,11 @@ function displayBody(body) {
|
|
|
329
361
|
throw new HttpError(400, '"accounts" must be an object');
|
|
330
362
|
out.accounts = Object.fromEntries(Object.entries(v).map(([f, list]) => [f, names(list, `"accounts.${f}"`)]));
|
|
331
363
|
}
|
|
364
|
+
else if (k === 'pinned') {
|
|
365
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
366
|
+
throw new HttpError(400, '"pinned" must be an object');
|
|
367
|
+
out.pinned = Object.fromEntries(Object.entries(v).map(([f, list]) => [f, names(list, `"pinned.${f}"`)]));
|
|
368
|
+
}
|
|
332
369
|
else
|
|
333
370
|
throw new HttpError(400, `unknown key "${k}"`);
|
|
334
371
|
}
|
|
@@ -661,6 +698,17 @@ export function createPanelHandler(opts) {
|
|
|
661
698
|
const shown = update.shownPlan(plan);
|
|
662
699
|
if (!optBool(body, 'confirm'))
|
|
663
700
|
return { ...shown, updated: false };
|
|
701
|
+
// The CLI is current and the skill is behind: `skill: true` updates it alone, as `sideby update --yes` does.
|
|
702
|
+
if (optBool(body, 'skill')) {
|
|
703
|
+
if (plan.available || plan.method.kind === 'local')
|
|
704
|
+
return { ...shown, updated: false, changed: true };
|
|
705
|
+
const r = await update.applySkillOnly(rt.paths, plan.skill, rt.env);
|
|
706
|
+
if (!r)
|
|
707
|
+
return { ...shown, updated: false, changed: true };
|
|
708
|
+
if (!r.ok)
|
|
709
|
+
throw new UserError(r.message);
|
|
710
|
+
return { ...shown, updated: false, skillUpdated: true, message: r.message };
|
|
711
|
+
}
|
|
664
712
|
// A confirmation that runs nothing always says why: the registry names another version (or none newer) now.
|
|
665
713
|
if (body.version !== plan.latest || !plan.available)
|
|
666
714
|
return { ...shown, updated: false, changed: true };
|
|
@@ -680,6 +728,37 @@ export function createPanelHandler(opts) {
|
|
|
680
728
|
};
|
|
681
729
|
},
|
|
682
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
|
+
const r = await lb.switchLocal(rt.paths, d, to, rt.env, {
|
|
749
|
+
panelPort: () => backgroundPanelPort(rt.paths),
|
|
750
|
+
});
|
|
751
|
+
if (!r.ok)
|
|
752
|
+
throw new UserError(r.message);
|
|
753
|
+
return {
|
|
754
|
+
...(await lb.localStatus(d)),
|
|
755
|
+
to,
|
|
756
|
+
switched: !r.unchanged,
|
|
757
|
+
message: r.message,
|
|
758
|
+
restartedPanel: r.restartedPanel,
|
|
759
|
+
};
|
|
760
|
+
},
|
|
761
|
+
},
|
|
683
762
|
// The Auto Update switch: config `update.auto`, shown as a diff first like every config change.
|
|
684
763
|
'/api/update/settings': {
|
|
685
764
|
writes: (body) => body.confirm === true,
|