sideby 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +24 -18
  2. package/dist/build.json +1 -1
  3. package/dist/src/cli/commands.d.ts +11 -1
  4. package/dist/src/cli/commands.js +144 -7
  5. package/dist/src/cli/json-schemas.js +56 -1
  6. package/dist/src/cli/main.js +119 -48
  7. package/dist/src/core/app-profiles.d.ts +50 -0
  8. package/dist/src/core/app-profiles.js +206 -0
  9. package/dist/src/core/brand-mark.d.ts +83 -0
  10. package/dist/src/core/brand-mark.js +88 -0
  11. package/dist/src/core/config.d.ts +16 -0
  12. package/dist/src/core/config.js +17 -2
  13. package/dist/src/core/desktop-app.d.ts +18 -1
  14. package/dist/src/core/desktop-app.js +22 -12
  15. package/dist/src/core/handoff-chain.d.ts +16 -2
  16. package/dist/src/core/handoff-chain.js +19 -6
  17. package/dist/src/core/handoff-run.js +43 -18
  18. package/dist/src/core/handoff-select.d.ts +4 -0
  19. package/dist/src/core/handoff-select.js +3 -2
  20. package/dist/src/core/update.d.ts +209 -0
  21. package/dist/src/core/update.js +631 -0
  22. package/dist/src/desktop/icon.js +293 -62
  23. package/dist/src/families/claude/handoff.js +10 -0
  24. package/dist/src/families/desktop.d.ts +25 -0
  25. package/dist/src/families/desktop.js +8 -2
  26. package/dist/src/families/grok/bot.d.ts +8 -0
  27. package/dist/src/families/grok/bot.js +18 -0
  28. package/dist/src/handoff/hook.js +21 -4
  29. package/dist/src/handoff/messages.d.ts +4 -0
  30. package/dist/src/handoff/messages.js +12 -0
  31. package/dist/src/panel/background.d.ts +2 -0
  32. package/dist/src/panel/background.js +7 -0
  33. package/dist/src/panel/handler.d.ts +35 -0
  34. package/dist/src/panel/handler.js +132 -6
  35. package/dist/src/panel/i18n.d.ts +59 -0
  36. package/dist/src/panel/i18n.js +118 -0
  37. package/dist/src/panel/page-script.js +282 -6
  38. package/dist/src/panel/page-styles.js +21 -2
  39. package/dist/src/panel/page.d.ts +2 -0
  40. package/dist/src/panel/page.js +6 -3
  41. package/dist/src/runtime.d.ts +3 -1
  42. package/dist/src/runtime.js +7 -0
  43. package/dist/src/types.d.ts +5 -0
  44. package/dist/testing/index.js +2 -0
  45. package/package.json +2 -2
  46. package/schemas/config.json +15 -0
  47. package/schemas/desktop-remove.json +33 -0
  48. package/schemas/desktop.json +2 -1
  49. package/schemas/handoff-ready.json +4 -0
  50. package/schemas/list.json +47 -0
  51. package/schemas/update.json +115 -0
  52. package/skills/sideby/SKILL.md +7 -1
  53. package/skills/sideby/references/handoff.md +1 -1
@@ -14,6 +14,8 @@ export interface PlanFile {
14
14
  at: string;
15
15
  decision: 'pick' | 'wait' | 'none';
16
16
  pick?: string;
17
+ /** For `wait`: when the full window resets and the session goes on by itself. */
18
+ until?: string;
17
19
  prepareAt: number;
18
20
  threshold: number;
19
21
  /** False when the session cannot write its Brief: no preparing step, and the threshold only ends the turn. */
@@ -26,6 +28,8 @@ export interface RunState {
26
28
  prepareSentAt?: string;
27
29
  thresholdSentAt?: string;
28
30
  stopBlocked?: boolean;
31
+ /** The user was told this run waits for a reset instead of moving on (ADR-0018). */
32
+ waitNoticeSentAt?: string;
29
33
  sessionId?: string;
30
34
  transcriptPath?: string;
31
35
  }
@@ -42,6 +46,12 @@ export interface ReadyFile {
42
46
  sessionId?: string;
43
47
  transcriptPath?: string;
44
48
  }
49
+ /** The parent kept a session that hit its limit, to wait for the reset at `until` (ADR-0018). */
50
+ export interface WaitingFile {
51
+ runId: string;
52
+ until: string;
53
+ at: string;
54
+ }
45
55
  export interface ChainHop {
46
56
  ref: string;
47
57
  startedAt: string;
@@ -57,6 +67,7 @@ export declare const FILES: {
57
67
  readonly plan: 'plan.json';
58
68
  readonly ready: 'ready.json';
59
69
  readonly request: 'request.json';
70
+ readonly waiting: 'waiting.json';
60
71
  readonly chain: 'chain.json';
61
72
  readonly run: (runId: string) => string;
62
73
  };
@@ -73,7 +84,10 @@ export declare function writeJsonFile(path: string, value: unknown): Promise<voi
73
84
  /**
74
85
  * `sideby handoff ready` (spec §3.17): asks the parent to hand over after this turn. Works only inside a run sideby
75
86
  * started with Auto Handoff; `briefFile`, when given, is copied into the chain directory (mode 600); `to` is the
76
- * Account ref to hand over to.
87
+ * Account ref to hand over to. While the session waits for a reset (`waiting.json`, typed as `! sideby handoff ready`),
88
+ * no turn will end to pick up a request, so it writes `ready.json` at once (`now`).
77
89
  * Returns the request, or null outside such a run.
78
90
  */
79
- export declare function requestHandoff(env: Readonly<Record<string, string | undefined>>, briefFile?: string, to?: string): Promise<ReadyFile | null>;
91
+ export declare function requestHandoff(env: Readonly<Record<string, string | undefined>>, briefFile?: string, to?: string, now?: Date): Promise<(ReadyFile & {
92
+ now: boolean;
93
+ }) | null>;
@@ -1,7 +1,8 @@
1
1
  // The files one Handoff chain keeps under `<state>/handoffs/<chain>/` (spec §3.17). The handoff hook runs on every
2
2
  // tool call, so this module imports only Node built-ins and leaf modules (never the Runtime or a Family index).
3
- // The parent `sideby run` writes `plan.json` and `chain.json`; the hook writes `run-<id>.json` and `ready.json`;
4
- // `sideby handoff ready` writes `request.json`. Every file is mode 600 in a mode 700 directory.
3
+ // The parent `sideby run` writes `plan.json`, `chain.json` and, while a session waits for a reset, `waiting.json`; the
4
+ // hook writes `run-<id>.json` and `ready.json`; `sideby handoff ready` writes `request.json` (`ready.json` while the
5
+ // session waits). Every file is mode 600 in a mode 700 directory.
5
6
  import { randomBytes } from 'node:crypto';
6
7
  import { chmod, mkdir, readFile } from 'node:fs/promises';
7
8
  import { join } from 'node:path';
@@ -16,6 +17,7 @@ export const FILES = {
16
17
  plan: 'plan.json',
17
18
  ready: 'ready.json',
18
19
  request: 'request.json',
20
+ waiting: 'waiting.json',
19
21
  chain: 'chain.json',
20
22
  run: (runId) => `run-${runId}.json`,
21
23
  };
@@ -51,10 +53,11 @@ export async function writeJsonFile(path, value) {
51
53
  /**
52
54
  * `sideby handoff ready` (spec §3.17): asks the parent to hand over after this turn. Works only inside a run sideby
53
55
  * started with Auto Handoff; `briefFile`, when given, is copied into the chain directory (mode 600); `to` is the
54
- * Account ref to hand over to.
56
+ * Account ref to hand over to. While the session waits for a reset (`waiting.json`, typed as `! sideby handoff ready`),
57
+ * no turn will end to pick up a request, so it writes `ready.json` at once (`now`).
55
58
  * Returns the request, or null outside such a run.
56
59
  */
57
- export async function requestHandoff(env, briefFile, to) {
60
+ export async function requestHandoff(env, briefFile, to, now = new Date()) {
58
61
  const runId = env[HANDOFF_ENV.run];
59
62
  const dir = env[HANDOFF_ENV.dir];
60
63
  if (!runId || !dir)
@@ -66,11 +69,21 @@ export async function requestHandoff(env, briefFile, to) {
66
69
  }
67
70
  const request = {
68
71
  runId,
69
- at: new Date().toISOString(),
72
+ at: now.toISOString(),
70
73
  trigger: 'manual',
71
74
  ...(brief ? { brief, briefSource: 'user' } : {}),
72
75
  ...(to ? { to } : {}),
73
76
  };
77
+ const waiting = await readJsonFile(join(dir, FILES.waiting));
78
+ if (waiting && waiting.runId === runId && Date.parse(waiting.until) > now.getTime()) {
79
+ const state = (await readJsonFile(join(dir, FILES.run(runId)))) ?? {};
80
+ await writeJsonFile(join(dir, FILES.ready), {
81
+ ...request,
82
+ ...(state.sessionId ? { sessionId: state.sessionId } : {}),
83
+ ...(state.transcriptPath ? { transcriptPath: state.transcriptPath } : {}),
84
+ });
85
+ return { ...request, now: true };
86
+ }
74
87
  await writeJsonFile(join(dir, FILES.request), request);
75
- return request;
88
+ return { ...request, now: false };
76
89
  }
@@ -132,9 +132,12 @@ export function hookEntryWorks(env) {
132
132
  });
133
133
  }
134
134
  /** The decision, or `none` with the reason when sideby cannot tell (an account removed mid-chain, say). */
135
- async function decide(rt, account, used, to) {
135
+ async function decide(rt, account, used, to, manual = false) {
136
136
  try {
137
- return await rt.handoffDecision(account, used, to !== undefined ? { to } : {});
137
+ return await rt.handoffDecision(account, used, {
138
+ ...(to !== undefined ? { to } : {}),
139
+ ...(manual ? { manual: true } : {}),
140
+ });
138
141
  }
139
142
  catch (err) {
140
143
  return { kind: 'none', reason: err.message };
@@ -230,6 +233,7 @@ opts = {}) {
230
233
  at: new Date().toISOString(),
231
234
  decision: d.kind,
232
235
  ...(d.kind === 'pick' ? { pick: d.ref } : {}),
236
+ ...(d.kind === 'wait' ? { until: d.until } : {}),
233
237
  prepareAt: settings.prepareAt,
234
238
  threshold: settings.threshold,
235
239
  ...(elig.agentBrief === false ? { agentBrief: false } : {}),
@@ -246,27 +250,48 @@ opts = {}) {
246
250
  await writePlan();
247
251
  const refresh = setInterval(() => void writePlan(), PLAN_REFRESH_MS);
248
252
  const host = spawnHost({ ...launch, args, env });
249
- const watcher = watchReady(dir, runId);
250
- const outcome = await Promise.race([
251
- host.exited.then((code) => ({ kind: 'exit', code })),
252
- watcher.ready.then((ready) => ({ kind: 'ready', ready })),
253
- ]);
253
+ const exited = host.exited.then((code) => ({ kind: 'exit', code }));
254
+ // Nothing to move on to (or better to wait): leave the session alone, keep listening (the user may still ask to
255
+ // move on while it waits, ADR-0018), and say why once it ends.
256
+ let kept;
257
+ let outcome;
258
+ for (;;) {
259
+ const watcher = watchReady(dir, runId);
260
+ const got = await Promise.race([
261
+ exited,
262
+ watcher.ready.then((ready) => ({ kind: 'ready', ready })),
263
+ ]);
264
+ watcher.stop();
265
+ if (got.kind === 'exit') {
266
+ outcome = got;
267
+ break;
268
+ }
269
+ const d = await decide(rt, account, used, got.ready.to, got.ready.trigger === 'manual');
270
+ if (d.kind === 'pick') {
271
+ outcome = { ...got, decision: d };
272
+ break;
273
+ }
274
+ await rm(join(dir, FILES.ready), { force: true });
275
+ if (d.kind === 'wait')
276
+ await writeJsonFile(join(dir, FILES.waiting), {
277
+ runId,
278
+ until: d.until,
279
+ at: new Date().toISOString(),
280
+ });
281
+ kept = d;
282
+ }
254
283
  clearInterval(refresh);
255
- watcher.stop();
256
284
  stopped = true;
257
285
  await pending;
286
+ await rm(join(dir, FILES.waiting), { force: true });
258
287
  if (outcome.kind === 'exit') {
259
- await noteUnhandedHighQuota(rt, account, used, settings.threshold, io);
288
+ if (kept)
289
+ io.err(`sideby: ${account.ref} did not hand over: ${explain(kept)}`);
290
+ else
291
+ await noteUnhandedHighQuota(rt, account, used, settings.threshold, io);
260
292
  return outcome.code;
261
293
  }
262
- // Nothing to move on to (or better to wait): leave the session alone and say why once it ends.
263
- const decision = await decide(rt, account, used, outcome.ready.to);
264
- if (decision.kind !== 'pick') {
265
- await rm(join(dir, FILES.ready), { force: true });
266
- const code = await host.exited;
267
- io.err(`sideby: ${account.ref} did not hand over: ${explain(decision)}`);
268
- return code;
269
- }
294
+ const decision = outcome.decision;
270
295
  // The session is ready: end the Host, give it time to save, never kill it.
271
296
  host.child.kill('SIGTERM');
272
297
  const ended = await Promise.race([
@@ -356,5 +381,5 @@ export async function readyDecision(rt, env, to) {
356
381
  }
357
382
  const account = await rt.resolve(ref);
358
383
  const chain = await readJsonFile(join(dir, FILES.chain));
359
- return decide(rt, account, chain?.hops.map((h) => h.ref) ?? [account.ref], to);
384
+ return decide(rt, account, chain?.hops.map((h) => h.ref) ?? [account.ref], to, true);
360
385
  }
@@ -15,6 +15,8 @@ export interface SelectCandidate {
15
15
  installed: boolean;
16
16
  /** Whether its Family can start as the next Account (has `handoff`, and the run would be eligible). */
17
17
  receive: boolean;
18
+ /** A session stopped at the limit continues by itself at the reset (`FamilyHandoff.resumesAtReset`); set for the origin. */
19
+ resumesAtReset?: boolean;
18
20
  }
19
21
  /** An Account the Handoff Order lists that was left out, and why (spec §3.21). */
20
22
  export interface SkippedAccount {
@@ -55,4 +57,6 @@ export declare function selectNext(input: {
55
57
  * an Account whose Quota came back); every other rule still applies.
56
58
  */
57
59
  to?: string;
60
+ /** The user asked to hand over (`sideby handoff ready`): never wait for a reset (ADR-0018). */
61
+ manual?: boolean;
58
62
  }): HandoffDecision;
@@ -15,7 +15,8 @@ export function sameOrganization(a, b) {
15
15
  * reaches the threshold (spec §3.17 "选号" 1: plan.json and the Panel, decided before the moment comes).
16
16
  */
17
17
  function waitUntil(origin, s, now, ahead) {
18
- if (origin.quota.status !== 'ok' || s.waitIfResetWithinMinutes <= 0)
18
+ // Waiting only makes sense when the session goes on by itself at the reset (ADR-0018).
19
+ if (origin.quota.status !== 'ok' || s.waitIfResetWithinMinutes <= 0 || origin.resumesAtReset !== true)
19
20
  return undefined;
20
21
  const open = origin.quota.windows.filter((w) => !windowReset(w.resetsAt, now));
21
22
  const top = Math.max(0, ...open.map((w) => w.usedPercent));
@@ -60,7 +61,7 @@ export function selectNext(input) {
60
61
  return { kind: 'none', reason: `${input.to} is not an account sideby can find` };
61
62
  return toTarget(origin, target, s, new Set(s.crossOrganization.map(refOf)), now);
62
63
  }
63
- const until = waitUntil(origin, s, now, input.ahead === true);
64
+ const until = input.manual ? undefined : waitUntil(origin, s, now, input.ahead === true);
64
65
  if (until)
65
66
  return { kind: 'wait', until };
66
67
  const allowed = new Set(s.crossOrganization.map(refOf));
@@ -0,0 +1,209 @@
1
+ import type { Env } from '../types.ts';
2
+ import type { Paths } from './paths.ts';
3
+ export declare const DEFAULT_REGISTRY = "https://registry.npmjs.org";
4
+ export declare const CHECK_INTERVAL_MS: number;
5
+ export declare const NOTICE_INTERVAL_MS: number;
6
+ /** After a failed check (offline, a mirror without the endpoint) the next one waits this long. */
7
+ export declare const RETRY_INTERVAL_MS: number;
8
+ /** A failed Auto Update of one version is tried again after this long (a mirror may not have it yet). */
9
+ export declare const AUTO_RETRY_MS: number;
10
+ export declare const RELEASE_NOTES: (v: string) => string;
11
+ /** What `update.json` in the state directory holds. Every field is optional; a broken file reads as `{}`. */
12
+ export interface UpdateCache {
13
+ /** When the registry last answered. */
14
+ checkedAt?: string;
15
+ /** When a check was last tried, answered or not. */
16
+ triedAt?: string;
17
+ /** The `latest` dist-tag it answered. */
18
+ latest?: string;
19
+ /** When a background check last looked at Auto Update (installed, or waited for a running session). */
20
+ autoTriedAt?: string;
21
+ /** The last Update Notice the CLI printed. */
22
+ notified?: {
23
+ version: string;
24
+ at: string;
25
+ };
26
+ /** The last Auto Update; `shown` once the CLI has said so. */
27
+ auto?: {
28
+ from: string;
29
+ to: string;
30
+ at: string;
31
+ ok: boolean;
32
+ message: string;
33
+ shown?: boolean;
34
+ };
35
+ }
36
+ export type InstallKind = 'npm' | 'pnpm' | 'bun' | 'volta' | 'npx' | 'project' | 'source';
37
+ export interface InstallMethod {
38
+ kind: InstallKind;
39
+ /** The installed package folder (real path). */
40
+ packageDir: string;
41
+ /** True when sideby can run the Update itself (npm, pnpm or bun global, and npm's folders are writable). */
42
+ canUpdate: boolean;
43
+ /** Why it cannot, in a phrase that follows "sideby cannot update itself: ". */
44
+ reason?: string;
45
+ /** npm only: the prefix whose `lib/node_modules` holds sideby. */
46
+ prefix?: string;
47
+ }
48
+ /** Swapped in tests through `bindUpdateDeps`; the real machine otherwise. */
49
+ export interface UpdateDeps {
50
+ fetch: typeof fetch;
51
+ now: () => Date;
52
+ /** Real path of the running sideby package. */
53
+ packageDir: () => Promise<string>;
54
+ writable: (dir: string) => Promise<boolean>;
55
+ alive: (pid: number) => boolean;
56
+ /** Runs a program, streaming its output to `onOutput`; resolves with the exit code and the whole output. */
57
+ exec: (file: string, args: readonly string[], opts: {
58
+ env: Env;
59
+ onOutput?: (text: string) => void;
60
+ }) => Promise<{
61
+ code: number;
62
+ output: string;
63
+ }>;
64
+ /** Starts `sideby ui --background --no-open --port <port>` from the newly installed code, detached; false when it could not start. */
65
+ restartPanel: (env: Env, port: number) => Promise<boolean>;
66
+ }
67
+ /** Internal, for tests: replaces parts of the machine. Not part of the plugin or Portal contract. */
68
+ export declare function bindUpdateDeps(deps: Partial<UpdateDeps>): () => void;
69
+ /** -1, 0 or 1 like a sort comparator; a prerelease sorts before its release. Null when either is not a version. */
70
+ export declare function compareVersions(a: string, b: string): number | null;
71
+ /** `latest` is newer than `current`. */
72
+ export declare const isNewer: (latest: string | undefined, current: string) => latest is string;
73
+ /**
74
+ * The jump keeps the major version (0.11 → 0.12, 1.2 → 1.9). Auto Update only takes these; 0.x → 1.0 or 1.x → 2.0
75
+ * only gets a notice.
76
+ */
77
+ export declare function sameLine(from: string, to: string): boolean;
78
+ /** True in CI, where a notice is noise and a check is a request nobody reads. */
79
+ export declare const inCi: (env: Env) => boolean;
80
+ type UpdateSettings = {
81
+ update?: {
82
+ check?: boolean;
83
+ auto?: boolean;
84
+ };
85
+ };
86
+ /** Why Update Checks are off, or null when they are on (spec §3.23). */
87
+ export declare function checksOff(env: Env, config: UpdateSettings): string | null;
88
+ export declare function readUpdateCache(paths: Paths): Promise<UpdateCache>;
89
+ /**
90
+ * Merges a change into the cache file. The file is read again first, and `change` may be a function of what is there
91
+ * now, so a writer that decides from an older read (the CLI marking an Auto Update shown) never overwrites a newer
92
+ * field another process wrote meanwhile.
93
+ */
94
+ export declare function updateUpdateCache(paths: Paths, change: Partial<UpdateCache> | ((now: UpdateCache) => Partial<UpdateCache>)): Promise<UpdateCache>;
95
+ /** A check is due: no answer within a day, and no attempt within the hour (so being offline costs one try an hour). */
96
+ export declare function checkDue(cache: UpdateCache, now: Date): boolean;
97
+ /**
98
+ * Auto Update is on and the cache names a newer version it has not installed: never tried, or a failed try a day
99
+ * ago. Lets the CLI start a background check even when a Panel already ran today's check.
100
+ */
101
+ export declare function autoPending(cache: UpdateCache, current: string, config: UpdateSettings, now: Date): boolean;
102
+ /** Asks the registry for the `latest` dist-tag. Throws with a message that names the registry and what to do. */
103
+ export declare function fetchLatest(env: Env): Promise<string>;
104
+ /** Runs one Update Check and records it, answered or not. Never throws. */
105
+ export declare function refreshUpdateCheck(paths: Paths, env: Env): Promise<UpdateCache>;
106
+ /**
107
+ * The Update Notice line the CLI may print now, or null. Once per new version per day; `current` is the running
108
+ * version.
109
+ */
110
+ export declare function noticeLine(cache: UpdateCache, current: string, now: Date): string | null;
111
+ /** What the CLI says once about the last Auto Update, or null. */
112
+ export declare function autoUpdateLine(cache: UpdateCache): string | null;
113
+ /**
114
+ * What the CLI does after a command (spec §3.23), decided from the cache: the lines to print (the Auto Update result,
115
+ * then the Update Notice), the change that records them, and whether to start a background check. The caller prints,
116
+ * writes `record` with `updateUpdateCache` and starts the check; nothing here touches the machine.
117
+ */
118
+ export declare function afterCommandPlan(cache: UpdateCache, current: string, config: UpdateSettings, env: Env, now: Date): {
119
+ lines: string[];
120
+ record: ((cur: UpdateCache) => Partial<UpdateCache>) | null;
121
+ startCheck: boolean;
122
+ };
123
+ /** Where the running copy comes from (spec §3.23 table); reads only paths. */
124
+ export declare function installMethod(env: Env): Promise<InstallMethod>;
125
+ /**
126
+ * The command an Update runs (or that the user runs) for `version`; `run` is set only when sideby may run it. With
127
+ * `SIDEBY_UPDATE_REGISTRY`, installs use that registry too, so the version the check found is the one fetched.
128
+ */
129
+ export declare function updateCommand(method: InstallMethod, version: string, env?: Env): {
130
+ text: string;
131
+ run?: {
132
+ file: string;
133
+ args: string[];
134
+ };
135
+ };
136
+ /**
137
+ * Records that this process runs a Host (`run`, `login`, `next`, `resume`) so an Auto Update waits for it; returns the
138
+ * function that removes the record. Failing to write the record never stops a Launch.
139
+ */
140
+ export declare function markRunning(paths: Paths, pid?: number): Promise<() => Promise<void>>;
141
+ /** Pids of sideby processes that run a Host now; records of processes that ended are removed. */
142
+ export declare function runningSessions(paths: Paths): Promise<number[]>;
143
+ export interface UpdatePlan {
144
+ current: string;
145
+ latest: string;
146
+ available: boolean;
147
+ method: InstallMethod;
148
+ /** The command for the user to see; `canRun` says whether sideby runs it after a confirmation. */
149
+ command: string;
150
+ canRun: boolean;
151
+ releaseNotes: string;
152
+ }
153
+ export declare function planFrom(method: InstallMethod, current: string, latest: string, env?: Env): UpdatePlan;
154
+ /** The Install Method as outputs show it: no folder of this machine. */
155
+ export declare const shownMethod: (m: InstallMethod) => {
156
+ kind: InstallKind;
157
+ canUpdate: boolean;
158
+ reason?: string | undefined;
159
+ };
160
+ /** The plan as `sideby update --json` and the Panel show it (schemas/update.json). */
161
+ export declare function shownPlan(plan: UpdatePlan): {
162
+ current: string;
163
+ latest: string;
164
+ available: boolean;
165
+ method: {
166
+ kind: InstallKind;
167
+ canUpdate: boolean;
168
+ reason?: string | undefined;
169
+ };
170
+ command: string;
171
+ canRun: boolean;
172
+ releaseNotes: string;
173
+ };
174
+ /** Asks the registry now (whatever the switches say: the user asked) and returns what an Update would do. */
175
+ export declare function planUpdate(paths: Paths, env: Env): Promise<UpdatePlan>;
176
+ export interface UpdateResult {
177
+ ok: boolean;
178
+ /** Exit code of the install command; absent when it did not run. */
179
+ code?: number;
180
+ output: string;
181
+ message: string;
182
+ /** A background Panel was running and is starting again from the new code. */
183
+ restartedPanel: boolean;
184
+ /** Another Update held the lock; nothing ran. */
185
+ busy?: boolean;
186
+ }
187
+ /**
188
+ * Runs the Update the plan names, then starts a running background Panel again from the new code. The caller has
189
+ * shown the plan and got a confirmation (or this is an Auto Update). One Update runs at a time on this machine (the
190
+ * CLI, a Panel and an Auto Update share the lock). `panelPort` gives the port of the background Panel to restart, or
191
+ * null when none runs. A successful Update is recorded as said, so no notice for it follows.
192
+ */
193
+ export declare function applyUpdate(paths: Paths, plan: UpdatePlan, env: Env, opts?: {
194
+ onOutput?: (text: string) => void;
195
+ panelPort?: () => Promise<number | null>;
196
+ }): Promise<UpdateResult>;
197
+ /** What keeps an Auto Update from installing `plan` until the user acts (an install it cannot update, a major version). */
198
+ export declare function lastingBlocker(plan: UpdatePlan): string | null;
199
+ /**
200
+ * Whether an Auto Update may install `plan` now (spec §3.23), and if not, why. Running sessions are looked up
201
+ * last because that removes records of ended processes.
202
+ */
203
+ export declare function autoUpdateBlocker(paths: Paths, plan: UpdatePlan): Promise<string | null>;
204
+ /**
205
+ * Body of the detached `sideby update-check`: an Update Check when one is due, then an Auto Update when config
206
+ * `update.auto` is on and nothing blocks it. Only one runs at a time; it never throws.
207
+ */
208
+ export declare function backgroundCheck(paths: Paths, env: Env, config: UpdateSettings, panelPort: () => Promise<number | null>): Promise<void>;
209
+ export {};