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.
Files changed (66) hide show
  1. package/README.md +1 -0
  2. package/dist/build.json +1 -1
  3. package/dist/src/cli/commands.d.ts +7 -0
  4. package/dist/src/cli/commands.js +259 -9
  5. package/dist/src/cli/json-schemas.js +109 -2
  6. package/dist/src/cli/main.js +30 -6
  7. package/dist/src/core/config.d.ts +1 -0
  8. package/dist/src/core/config.js +8 -2
  9. package/dist/src/core/local-build.d.ts +63 -0
  10. package/dist/src/core/local-build.js +189 -0
  11. package/dist/src/core/skill-check.d.ts +14 -0
  12. package/dist/src/core/skill-check.js +29 -3
  13. package/dist/src/core/update.d.ts +73 -1
  14. package/dist/src/core/update.js +155 -23
  15. package/dist/src/panel/handler.d.ts +12 -1
  16. package/dist/src/panel/handler.js +84 -5
  17. package/dist/src/panel/i18n.d.ts +125 -5
  18. package/dist/src/panel/i18n.js +280 -40
  19. package/dist/src/panel/page-logic.d.ts +41 -0
  20. package/dist/src/panel/page-logic.js +46 -0
  21. package/dist/src/panel/page-script.js +616 -162
  22. package/dist/src/panel/page-styles.js +74 -23
  23. package/dist/src/panel/page.js +3 -0
  24. package/dist/src/runtime.d.ts +2 -0
  25. package/package.json +3 -2
  26. package/schemas/alias.json +1 -1
  27. package/schemas/app-install.json +1 -1
  28. package/schemas/app-uninstall.json +1 -1
  29. package/schemas/browser-open.json +1 -1
  30. package/schemas/browser-switch.json +1 -1
  31. package/schemas/browser.json +1 -1
  32. package/schemas/config.json +15 -1
  33. package/schemas/desktop-remove.json +1 -1
  34. package/schemas/desktop.json +1 -1
  35. package/schemas/doctor.json +1 -1
  36. package/schemas/error.json +1 -1
  37. package/schemas/families.json +1 -1
  38. package/schemas/handoff-disable.json +1 -1
  39. package/schemas/handoff-enable.json +1 -1
  40. package/schemas/handoff-ready.json +1 -1
  41. package/schemas/handoff-setup.json +1 -1
  42. package/schemas/handoff-status.json +282 -1
  43. package/schemas/handoff-teardown.json +1 -1
  44. package/schemas/list.json +6 -1
  45. package/schemas/local.json +165 -0
  46. package/schemas/model.json +1 -1
  47. package/schemas/new.json +1 -1
  48. package/schemas/next.json +1 -1
  49. package/schemas/order.json +148 -0
  50. package/schemas/pin.json +126 -0
  51. package/schemas/plugins.json +1 -1
  52. package/schemas/quota-check.json +88 -0
  53. package/schemas/quota-setup.json +1 -1
  54. package/schemas/quota-teardown.json +1 -1
  55. package/schemas/quota.json +1 -1
  56. package/schemas/remote-control.json +1 -1
  57. package/schemas/tune-change.json +1 -1
  58. package/schemas/tune.json +1 -1
  59. package/schemas/unpin.json +126 -0
  60. package/schemas/update-auto.json +84 -0
  61. package/schemas/update.json +79 -1
  62. package/skills/sideby/SKILL.md +8 -3
  63. package/skills/sideby/references/handoff.md +8 -0
  64. package/skills/sideby/references/panel.md +28 -0
  65. package/skills/sideby/references/quota.md +6 -0
  66. package/skills/sideby/references/update.md +15 -2
@@ -114,9 +114,12 @@ export const ConfigSchema = Type.Object({
114
114
  accounts: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }), {
115
115
  description: 'Per family, account names (not refs or short commands) in the order they are shown; accounts not listed follow, main first, then by name.',
116
116
  })),
117
+ pinned: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }), {
118
+ description: 'Per family, the names of the accounts pinned to the top of their group, in the order they are shown; they lead the group in every sort. Resetting the order keeps them.',
119
+ })),
117
120
  }, {
118
121
  additionalProperties: false,
119
- description: 'Display Order (spec §3.25): the order you dragged families and accounts into in the Panel. Display only: it never changes which account a Handoff picks.',
122
+ description: 'Display Order (spec §3.25): the order you gave families and accounts in the Panel, and the accounts you pinned. Display only: it never changes which account a Handoff picks.',
120
123
  })),
121
124
  quota: Type.Optional(Type.Object({
122
125
  check: Type.Optional(Type.Record(Type.String(), Type.Boolean(), {
@@ -275,10 +278,13 @@ export function editConfigBrowser(file, edit, opts = {}) {
275
278
  * or empty (see `editConfigKey` for how the file is written).
276
279
  */
277
280
  export function editConfigDisplay(file, display) {
278
- const accounts = Object.fromEntries(Object.entries(display?.accounts ?? {}).filter(([, names]) => names.length));
281
+ const lists = (r) => Object.fromEntries(Object.entries(r ?? {}).filter(([, names]) => names.length));
282
+ const accounts = lists(display?.accounts);
283
+ const pinned = lists(display?.pinned);
279
284
  const next = {
280
285
  ...(display?.families?.length ? { families: [...display.families] } : {}),
281
286
  ...(Object.keys(accounts).length ? { accounts } : {}),
287
+ ...(Object.keys(pinned).length ? { pinned } : {}),
282
288
  };
283
289
  return editConfigKey(file, 'display', 'move it again', () => (Object.keys(next).length ? next : undefined));
284
290
  }
@@ -0,0 +1,63 @@
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
+ /** Another install held the lock; nothing ran. */
53
+ busy?: boolean;
54
+ }
55
+ /**
56
+ * Puts `to` behind the global `sideby`. `local` links the newest Local Build; `npm` restores the kept package or
57
+ * installs `latest` (a failed install links the Local Build back, so `sideby` never goes missing). Afterwards a
58
+ * background Panel on `panelPort` starts again from the new code. One install at a time (the Update lock).
59
+ */
60
+ export declare function switchLocal(paths: Paths, d: LocalDirs, to: LocalTarget, env: Env, opts?: {
61
+ panelPort?: () => Promise<number | null>;
62
+ onOutput?: (text: string) => void;
63
+ }): Promise<SwitchResult>;
@@ -0,0 +1,189 @@
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, 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
+ return { ok: true, message, running: to, restartedPanel };
189
+ }
@@ -1,7 +1,21 @@
1
1
  import type { FamilyDef, Finding } from '../types.ts';
2
+ export declare const SKILL_SOURCE = "Adonis0123/sideby";
2
3
  export declare const SKILL_INSTALL = "npx skills add Adonis0123/sideby -g";
4
+ /** The command a person runs to update the skill where sideby leaves it alone. */
5
+ export declare const SKILL_UPDATE_SHOWN = "npx skills update sideby -g";
3
6
  /** The folder holding `dir` that is a git work tree, if any: a skill there is managed by that repository. */
4
7
  export declare function gitRootOf(dir: string): Promise<string | null>;
8
+ /**
9
+ * Whether the skills tool's install record (`.skill-lock.json` beside the `skills` folder holding `dir`) names this
10
+ * repository as the source of `sideby`: then `npx skills update` owns the skill, even inside a git repository (a
11
+ * dotfiles clone of `~/.agents`, say). Reads only that one entry's `source`.
12
+ */
13
+ export declare function installedBySkillsTool(dir: string): Promise<boolean>;
14
+ /**
15
+ * The git repository that owns the sideby skill in `dir`, or null when sideby may update it: not in a repository, or
16
+ * the skills tool installed it there.
17
+ */
18
+ export declare function skillOwnerRepo(dir: string): Promise<string | null>;
5
19
  /** `metadata.version` from a SKILL.md frontmatter, or null when there is none. */
6
20
  export declare function skillVersionOf(text: string): string | null;
7
21
  /**
@@ -5,7 +5,10 @@ import { readFile, realpath, stat } from 'node:fs/promises';
5
5
  import { dirname, join } from 'node:path';
6
6
  import { mainDirOf } from './accounts.js';
7
7
  import { tildify } from './paths.js';
8
- export const SKILL_INSTALL = 'npx skills add Adonis0123/sideby -g';
8
+ export const SKILL_SOURCE = 'Adonis0123/sideby';
9
+ export const SKILL_INSTALL = `npx skills add ${SKILL_SOURCE} -g`;
10
+ /** The command a person runs to update the skill where sideby leaves it alone. */
11
+ export const SKILL_UPDATE_SHOWN = 'npx skills update sideby -g';
9
12
  /** The folder holding `dir` that is a git work tree, if any: a skill there is managed by that repository. */
10
13
  export async function gitRootOf(dir) {
11
14
  for (let d = dir;; d = dirname(d)) {
@@ -20,6 +23,29 @@ export async function gitRootOf(dir) {
20
23
  return null;
21
24
  }
22
25
  }
26
+ /**
27
+ * Whether the skills tool's install record (`.skill-lock.json` beside the `skills` folder holding `dir`) names this
28
+ * repository as the source of `sideby`: then `npx skills update` owns the skill, even inside a git repository (a
29
+ * dotfiles clone of `~/.agents`, say). Reads only that one entry's `source`.
30
+ */
31
+ export async function installedBySkillsTool(dir) {
32
+ try {
33
+ const lock = JSON.parse(await readFile(join(dirname(dirname(dir)), '.skill-lock.json'), 'utf8'));
34
+ const source = lock.skills?.sideby?.source;
35
+ return typeof source === 'string' && source.toLowerCase() === SKILL_SOURCE.toLowerCase();
36
+ }
37
+ catch {
38
+ return false;
39
+ }
40
+ }
41
+ /**
42
+ * The git repository that owns the sideby skill in `dir`, or null when sideby may update it: not in a repository, or
43
+ * the skills tool installed it there.
44
+ */
45
+ export async function skillOwnerRepo(dir) {
46
+ const repo = await gitRootOf(dir);
47
+ return repo && !(await installedBySkillsTool(dir)) ? repo : null;
48
+ }
23
49
  /** `metadata.version` from a SKILL.md frontmatter, or null when there is none. */
24
50
  export function skillVersionOf(text) {
25
51
  const front = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1];
@@ -85,7 +111,7 @@ export async function checkInstalledSkills(home, families, version) {
85
111
  continue;
86
112
  const where = tildify(path, home);
87
113
  // `sideby update` brings the skill along, except one a git repository owns (a dotfiles clone, say).
88
- const repo = await gitRootOf(dirname(real));
114
+ const repo = await skillOwnerRepo(dirname(real));
89
115
  out.push({
90
116
  level: 'warn',
91
117
  account: `${family.id}:main`,
@@ -93,7 +119,7 @@ export async function checkInstalledSkills(home, families, version) {
93
119
  code: 'skill.outdated',
94
120
  message: `the sideby skill at ${where} is ${found ? `for sideby ${found}` : 'from before skills had a version'}; this sideby is ${version}`,
95
121
  hint: repo
96
- ? `it lives in the git repository ${tildify(repo, home)}: update it where that repository comes from (\`npx skills update sideby -g\` there), so agents learn this version's commands`
122
+ ? `it lives in the git repository ${tildify(repo, home)}: update it where that repository comes from (\`${SKILL_UPDATE_SHOWN}\` there), so agents learn this version's commands`
97
123
  : `run \`sideby update\`, which updates the skill too (or \`${SKILL_INSTALL}\` by hand), so agents learn this version's commands`,
98
124
  source: 'core',
99
125
  fixable: false,
@@ -32,7 +32,25 @@ export interface UpdateCache {
32
32
  message: string;
33
33
  shown?: boolean;
34
34
  };
35
+ /** The last line the CLI printed about a sideby skill behind this version (`version` is the CLI's). */
36
+ skillNotified?: {
37
+ version: string;
38
+ at: string;
39
+ };
40
+ /** The last time Auto Update brought the skill to `to` alone; `shown` once the CLI has said so. */
41
+ skillAuto?: {
42
+ to: string;
43
+ at: string;
44
+ ok: boolean;
45
+ message: string;
46
+ shown?: boolean;
47
+ };
35
48
  }
49
+ /**
50
+ * Why sideby cannot do something itself, for a Panel to say in the viewer's language; `reason` keeps the English
51
+ * text the CLI prints. `reasonAt` carries the path a code needs.
52
+ */
53
+ export type ReasonCode = 'local-build' | 'npx' | 'volta' | 'not-writable' | 'project' | 'source' | 'skill-in-git';
36
54
  export type InstallKind = 'npm' | 'pnpm' | 'bun' | 'volta' | 'npx' | 'project' | 'source' | 'local';
37
55
  export interface InstallMethod {
38
56
  kind: InstallKind;
@@ -42,6 +60,9 @@ export interface InstallMethod {
42
60
  canUpdate: boolean;
43
61
  /** Why it cannot, in a phrase that follows "sideby cannot update itself: ". */
44
62
  reason?: string;
63
+ reasonCode?: ReasonCode;
64
+ /** The folders that are not writable (`not-writable`). */
65
+ reasonAt?: string;
45
66
  /** npm only: the prefix whose `lib/node_modules` holds sideby. */
46
67
  prefix?: string;
47
68
  }
@@ -70,6 +91,12 @@ export interface UpdateDeps {
70
91
  }
71
92
  /** Internal, for tests: replaces parts of the machine. Not part of the plugin or Portal contract. */
72
93
  export declare function bindUpdateDeps(deps: Partial<UpdateDeps>): () => void;
94
+ /** Runs a program through the swappable machine (tests bind `exec`); a Local Build switch installs with it. */
95
+ export declare const runProgram: UpdateDeps['exec'];
96
+ /** Starts the background Panel again from the code `sideby` now names; see `UpdateDeps.restartPanel`. */
97
+ export declare const restartPanelByName: (env: Env, port: number) => Promise<boolean>;
98
+ /** The lock every install shares (`update-install.lock`), so a Local Build switch never races an Update. */
99
+ export declare const takeInstallLock: (paths: Paths) => Promise<(() => Promise<void>) | null>;
73
100
  /** -1, 0 or 1 like a sort comparator; a prerelease sorts before its release. Null when either is not a version. */
74
101
  export declare function compareVersions(a: string, b: string): number | null;
75
102
  /** `latest` is newer than `current`. */
@@ -87,6 +114,12 @@ type UpdateSettings = {
87
114
  auto?: boolean;
88
115
  };
89
116
  };
117
+ export type ChecksOffCode = 'local-build' | 'env' | 'config' | 'ci';
118
+ /** Why Update Checks are off, with a code for the Panel, or null when they are on (spec §3.23). */
119
+ export declare function checksOffWhy(env: Env, config: UpdateSettings, local?: boolean): {
120
+ code: ChecksOffCode;
121
+ text: string;
122
+ } | null;
90
123
  /** Why Update Checks are off, or null when they are on (spec §3.23). */
91
124
  export declare function checksOff(env: Env, config: UpdateSettings, local?: boolean): string | null;
92
125
  export declare function readUpdateCache(paths: Paths): Promise<UpdateCache>;
@@ -114,6 +147,29 @@ export declare function refreshUpdateCheck(paths: Paths, env: Env): Promise<Upda
114
147
  export declare function noticeLine(cache: UpdateCache, current: string, now: Date): string | null;
115
148
  /** What the CLI says once about the last Auto Update, or null. */
116
149
  export declare function autoUpdateLine(cache: UpdateCache): string | null;
150
+ /** What the CLI says once about the skill Auto Update brought up to date alone, or null. */
151
+ export declare function skillAutoLine(cache: UpdateCache): string | null;
152
+ /**
153
+ * The sideby skill an agent reads, as the CLI sees it after a command: the version of the first one behind
154
+ * `current` (null: one without a version), and whether sideby may update it. Absent when none is behind.
155
+ */
156
+ export interface SkillBehind {
157
+ installed: string | null;
158
+ canRun: boolean;
159
+ }
160
+ /** The skill part of a `SkillUpdatePlan` toward the running version, as `afterCommandPlan` takes it. */
161
+ export declare const skillBehind: (plan: SkillUpdatePlan | null, current: string) => SkillBehind | null;
162
+ /**
163
+ * The line about a skill behind this version the CLI may print now (spec §3.23), or null: once a day per CLI version,
164
+ * and never while Auto Update is on (the background check brings the skill along) and sideby may update it.
165
+ */
166
+ export declare function skillNoticeLine(cache: UpdateCache, current: string, config: UpdateSettings, skill: SkillBehind | null, now: Date): string | null;
167
+ /**
168
+ * Auto Update is on, the CLI is the newest version the cache knows, and the skill is behind it where sideby may
169
+ * update it, not tried for this version yet (or a failed try a day ago). A newer CLI waiting is left to the Update
170
+ * of the CLI: the skills tool fetches the default branch, which would put the skill ahead of this CLI.
171
+ */
172
+ export declare function skillAutoPending(cache: UpdateCache, current: string, config: UpdateSettings, skill: SkillBehind | null, now: Date): boolean;
117
173
  /** The `_notice` of a `--json` output (spec §3.23): what an agent should mention once its task is done. */
118
174
  export interface JsonNotice {
119
175
  update?: {
@@ -140,7 +196,7 @@ export declare function jsonNotice(cache: UpdateCache, current: string, config:
140
196
  * then the Update Notice), the change that records them, and whether to start a background check. The caller prints,
141
197
  * writes `record` with `updateUpdateCache` and starts the check; nothing here touches the machine.
142
198
  */
143
- export declare function afterCommandPlan(cache: UpdateCache, current: string, config: UpdateSettings, env: Env, now: Date): {
199
+ export declare function afterCommandPlan(cache: UpdateCache, current: string, config: UpdateSettings, env: Env, now: Date, skill?: SkillBehind | null): {
144
200
  lines: string[];
145
201
  record: ((cur: UpdateCache) => Partial<UpdateCache>) | null;
146
202
  startCheck: boolean;
@@ -183,6 +239,8 @@ export declare const shownMethod: (m: InstallMethod) => {
183
239
  kind: InstallKind;
184
240
  canUpdate: boolean;
185
241
  reason?: string | undefined;
242
+ reasonCode?: ReasonCode | undefined;
243
+ reasonAt?: string | undefined;
186
244
  };
187
245
  /** The plan as `sideby update --json` and the Panel show it (schemas/update.json). */
188
246
  export declare function shownPlan(plan: UpdatePlan): {
@@ -193,6 +251,8 @@ export declare function shownPlan(plan: UpdatePlan): {
193
251
  kind: InstallKind;
194
252
  canUpdate: boolean;
195
253
  reason?: string | undefined;
254
+ reasonCode?: ReasonCode | undefined;
255
+ reasonAt?: string | undefined;
196
256
  };
197
257
  command: string;
198
258
  canRun: boolean;
@@ -209,6 +269,9 @@ export interface SkillUpdatePlan {
209
269
  /** False when sideby must not run it; `reason` says why and the user runs `command` where it belongs. */
210
270
  canRun: boolean;
211
271
  reason?: string;
272
+ reasonCode?: ReasonCode;
273
+ /** The git repository that owns the skill (`skill-in-git`). */
274
+ reasonAt?: string;
212
275
  }
213
276
  /** The skill part of an Update toward `target`; null when no sideby skill is installed. Reads two files. */
214
277
  export declare function skillUpdatePlan(home: string, target: string): Promise<SkillUpdatePlan | null>;
@@ -219,6 +282,15 @@ export declare function applySkillUpdate(plan: SkillUpdatePlan | undefined, env:
219
282
  ok: boolean;
220
283
  message: string;
221
284
  } | null>;
285
+ /**
286
+ * Updates only the sideby skill, for a CLI that is already current (`sideby update`, the Panel, Auto Update). Holds
287
+ * the same lock as an Update, so a skill update never runs beside one; null when there was nothing to do.
288
+ */
289
+ export declare function applySkillOnly(paths: Paths, plan: SkillUpdatePlan | undefined, env: Env, onOutput?: (text: string) => void): Promise<{
290
+ ok: boolean;
291
+ message: string;
292
+ busy?: boolean;
293
+ } | null>;
222
294
  export interface UpdateResult {
223
295
  ok: boolean;
224
296
  /** Exit code of the install command; absent when it did not run. */