sideby 0.13.0 → 0.13.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.
Files changed (47) hide show
  1. package/dist/build.json +1 -1
  2. package/dist/src/cli/commands.d.ts +1 -0
  3. package/dist/src/cli/commands.js +48 -3
  4. package/dist/src/cli/json-schemas.js +48 -1
  5. package/dist/src/cli/main.js +38 -1
  6. package/dist/src/core/skill-check.d.ts +13 -0
  7. package/dist/src/core/skill-check.js +29 -1
  8. package/dist/src/core/update.d.ts +45 -0
  9. package/dist/src/core/update.js +86 -1
  10. package/dist/src/panel/handler.d.ts +3 -1
  11. package/dist/src/panel/handler.js +5 -0
  12. package/dist/src/panel/i18n.d.ts +3 -0
  13. package/dist/src/panel/i18n.js +8 -2
  14. package/dist/src/panel/page-script.js +11 -2
  15. package/package.json +1 -1
  16. package/schemas/alias.json +62 -0
  17. package/schemas/app-install.json +62 -0
  18. package/schemas/app-uninstall.json +62 -0
  19. package/schemas/browser-open.json +62 -0
  20. package/schemas/browser-switch.json +62 -0
  21. package/schemas/browser.json +62 -0
  22. package/schemas/desktop-remove.json +62 -0
  23. package/schemas/desktop.json +62 -0
  24. package/schemas/doctor.json +62 -0
  25. package/schemas/error.json +62 -0
  26. package/schemas/families.json +62 -0
  27. package/schemas/handoff-disable.json +62 -0
  28. package/schemas/handoff-enable.json +62 -0
  29. package/schemas/handoff-ready.json +62 -0
  30. package/schemas/handoff-setup.json +62 -0
  31. package/schemas/handoff-status.json +62 -0
  32. package/schemas/handoff-teardown.json +62 -0
  33. package/schemas/list.json +62 -0
  34. package/schemas/model.json +62 -0
  35. package/schemas/new.json +62 -0
  36. package/schemas/next.json +62 -0
  37. package/schemas/plugins.json +62 -0
  38. package/schemas/quota-setup.json +62 -0
  39. package/schemas/quota-teardown.json +62 -0
  40. package/schemas/quota.json +62 -0
  41. package/schemas/remote-control.json +62 -0
  42. package/schemas/tune-change.json +62 -0
  43. package/schemas/tune.json +62 -0
  44. package/schemas/update.json +107 -1
  45. package/skills/sideby/SKILL.md +4 -2
  46. package/skills/sideby/references/diagnose.md +1 -1
  47. package/skills/sideby/references/update.md +16 -0
package/dist/build.json CHANGED
@@ -1 +1 @@
1
- {"id":"20261016T155413-627722"}
1
+ {"id":"20261016T161929-77384f"}
@@ -8,6 +8,7 @@ export interface Io {
8
8
  /** The terminal Auto Handoff counts down on (spec §3.17); `process.stdin` when left out. */
9
9
  stdin?: HandoffIo['stdin'];
10
10
  }
11
+ export declare function setJsonNotice(n: object | null): void;
11
12
  export declare function serializeDoctor(r: DoctorReport): {
12
13
  status: "issues" | "ok";
13
14
  accounts: {
@@ -51,7 +51,12 @@ async function runAccountNow(rt, ref, args, io) {
51
51
  }
52
52
  return code;
53
53
  }
54
- const json = (io, data) => io.out(JSON.stringify({ schemaVersion: 1, ...data }, null, 2));
54
+ // The `_notice` main.ts works out before a `--json` command (spec §3.23); every JSON output of this run carries it.
55
+ let notice = null;
56
+ export function setJsonNotice(n) {
57
+ notice = n;
58
+ }
59
+ const json = (io, data) => io.out(JSON.stringify({ schemaVersion: 1, ...data, ...(notice ? { _notice: notice } : {}) }, null, 2));
55
60
  function stripFix(f) {
56
61
  const { fix: _fix, ...rest } = f;
57
62
  return rest;
@@ -1051,11 +1056,41 @@ export async function cmdUpdate(a, io) {
1051
1056
  const asJson = a.flags.has('json');
1052
1057
  const plan = await u.planUpdate(paths, process.env);
1053
1058
  const shown = u.shownPlan(plan);
1059
+ const sk = plan.skill;
1060
+ const skillLine = (s) => s.canRun
1061
+ ? `the sideby skill (for ${s.installed.map((v) => v ?? 'no version').join(', ')}) is updated too:\n ${s.command}`
1062
+ : `the sideby skill is not updated by sideby: ${s.reason}.\n ${s.command}`;
1054
1063
  if (!plan.available) {
1064
+ // The CLI is current; an outdated skill is the one thing left to update.
1065
+ if (sk?.outdated && sk.canRun) {
1066
+ if (!a.flags.has('yes')) {
1067
+ if (asJson)
1068
+ json(io, { ...shown, updated: false });
1069
+ else {
1070
+ io.out(`${c.green('✓')} sideby ${plan.current} is the latest version.`);
1071
+ io.out(`\nUpdating ${skillLine(sk)}`);
1072
+ io.out(c.cyan('\nRun it with `sideby update --yes`.'));
1073
+ }
1074
+ return 10;
1075
+ }
1076
+ if (!asJson)
1077
+ io.out(c.dim(`$ ${sk.command}`));
1078
+ const r = await u.applySkillUpdate(sk, process.env, asJson ? undefined : (t) => process.stderr.write(t));
1079
+ if (asJson)
1080
+ json(io, { ...shown, updated: false, skillUpdated: r?.ok ?? false, message: r?.message ?? '' });
1081
+ else if (r?.ok)
1082
+ io.out(`${c.green('✓')} ${r.message}; agents load it in their next session.`);
1083
+ else
1084
+ io.err(c.red(`✗ ${r?.message}`));
1085
+ return r?.ok ? 0 : 1;
1086
+ }
1055
1087
  if (asJson)
1056
1088
  json(io, { ...shown, updated: false });
1057
- else
1089
+ else {
1058
1090
  io.out(`${c.green('✓')} sideby ${plan.current} is the latest version.`);
1091
+ if (sk?.outdated)
1092
+ io.out(c.yellow(skillLine(sk)));
1093
+ }
1059
1094
  return 0;
1060
1095
  }
1061
1096
  if (!plan.canRun) {
@@ -1073,6 +1108,8 @@ export async function cmdUpdate(a, io) {
1073
1108
  else {
1074
1109
  io.out(`sideby ${c.bold(plan.latest)} is available (you have ${plan.current}). What changed: ${plan.releaseNotes}`);
1075
1110
  io.out(`\nUpdating runs:\n ${plan.command}`);
1111
+ if (sk?.outdated)
1112
+ io.out(`Then ${skillLine(sk)}`);
1076
1113
  io.out(c.cyan('\nRun it with `sideby update --yes`.'));
1077
1114
  }
1078
1115
  return 10;
@@ -1091,6 +1128,7 @@ export async function cmdUpdate(a, io) {
1091
1128
  restartedPanel: r.restartedPanel,
1092
1129
  refreshedApp: r.refreshedApp,
1093
1130
  skill: SKILL_INSTALL,
1131
+ ...(r.skillUpdated === undefined ? {} : { skillUpdated: r.skillUpdated }),
1094
1132
  ...(r.ok ? {} : { output: r.output }),
1095
1133
  });
1096
1134
  return r.ok ? 0 : 1;
@@ -1105,7 +1143,14 @@ export async function cmdUpdate(a, io) {
1105
1143
  if (r.refreshedApp)
1106
1144
  io.out(' The desktop app now starts the new version and shows its icon.');
1107
1145
  io.out(' A `sideby ui` running in a terminal and sessions already running keep the old version until you restart them.');
1108
- io.out(` If you use the sideby skill, update it too: ${SKILL_INSTALL}`);
1146
+ if (r.skillUpdated)
1147
+ io.out(' The sideby skill was updated too; agents load it in their next session.');
1148
+ else if (r.skillUpdated === false)
1149
+ io.err(c.yellow(` ${r.skillMessage}`));
1150
+ else if (sk?.outdated)
1151
+ io.out(c.yellow(` ${skillLine(sk)}`));
1152
+ else if (!sk)
1153
+ io.out(` If you use the sideby skill, install it with: ${SKILL_INSTALL}`);
1109
1154
  return 0;
1110
1155
  }
1111
1156
  /** The internal `sideby update-check` the CLI starts detached: one Update Check, then Auto Update if it is on. */
@@ -212,6 +212,30 @@ const AppProfile = Type.Object({
212
212
  }, {
213
213
  description: 'A window of a desktop app that signs in apart from every account (Grok Bot, ADR-0019). Open or create one with `sideby desktop grok-bot:<name>`.',
214
214
  });
215
+ /**
216
+ * On any `--json` output when there is something to mention once the task is done (spec §3.23): a newer sideby in the
217
+ * cached Update Check, or an installed sideby skill for another version. Off with SIDEBY_NO_UPDATE_NOTIFIER.
218
+ */
219
+ const Notice = Type.Object({
220
+ update: Type.Optional(Type.Object({
221
+ current: Str,
222
+ latest: Str,
223
+ command: Type.String({
224
+ description: 'Run it after the user agrees: it shows the plan and exits 10; then --yes.',
225
+ }),
226
+ message: Str,
227
+ })),
228
+ skill: Type.Optional(Type.Object({
229
+ installed: Type.Union([Str, Type.Null()], {
230
+ description: "The skill's version; null when it has none.",
231
+ }),
232
+ current: Str,
233
+ command: Type.String({
234
+ description: 'Updates the installed sideby skill (run it after the user agrees).',
235
+ }),
236
+ message: Str,
237
+ })),
238
+ }, { description: 'Not the answer to the task: mention it briefly afterwards and offer to update.' });
215
239
  export const OUTPUT_SCHEMAS = {
216
240
  list: Type.Object({
217
241
  schemaVersion: Version,
@@ -564,7 +588,25 @@ export const OUTPUT_SCHEMAS = {
564
588
  refreshedApp: Type.Optional(Type.Boolean({
565
589
  description: 'The desktop app (`sideby app install`) was installed and was written again by the new version.',
566
590
  })),
567
- skill: Type.Optional(Type.String({ description: 'The command that updates the sideby skill; sideby never runs it.' })),
591
+ skill: Type.Optional(Type.String({ description: 'The command that installs the sideby skill by hand.' })),
592
+ skillUpdate: Type.Optional(Type.Object({
593
+ installed: Type.Array(Type.Union([Str, Type.Null()]), {
594
+ description: 'Versions of the installed sideby skills; null for one without a version.',
595
+ }),
596
+ outdated: Type.Boolean({
597
+ description: 'Some installed skill is for another version than sideby will run.',
598
+ }),
599
+ command: Str,
600
+ canRun: Type.Boolean({
601
+ description: 'False when sideby leaves the skill alone (see reason): give the user command.',
602
+ }),
603
+ reason: Type.Optional(Str),
604
+ }, {
605
+ description: 'The installed sideby skill, which the Update brings along; absent when none is installed.',
606
+ })),
607
+ skillUpdated: Type.Optional(Type.Boolean({
608
+ description: 'The skill was updated too (true) or that failed (false); absent when not tried.',
609
+ })),
568
610
  output: Type.Optional(Type.String({ description: 'What the failed install command printed.' })),
569
611
  }, { title: 'sideby update --json' }),
570
612
  error: Type.Object({
@@ -576,3 +618,8 @@ export const OUTPUT_SCHEMAS = {
576
618
  }, { title: 'sideby error --json' }),
577
619
  config: ConfigSchema,
578
620
  };
621
+ for (const [name, schema] of Object.entries(OUTPUT_SCHEMAS)) {
622
+ const props = schema.properties;
623
+ if (name !== 'config' && props)
624
+ props._notice = Type.Optional(Notice);
625
+ }
@@ -249,6 +249,38 @@ function noticeAllowed(cmd, parsed) {
249
249
  return false;
250
250
  return true;
251
251
  }
252
+ /**
253
+ * Before a `--json` command (spec §3.23): the `_notice` its output carries, from the cached Update Check and the
254
+ * installed sideby skills, so an agent that reads JSON learns of a newer version too. Starts the next check detached
255
+ * when the last one is a day old, as afterCommand does on a terminal. Never fails the command.
256
+ */
257
+ async function jsonNoticeFor() {
258
+ try {
259
+ const u = await import('../core/update.js');
260
+ const { resolvePaths } = await import('../core/paths.js');
261
+ const { loadConfig } = await import('../core/config.js');
262
+ const { packageVersion } = await import('../core/version.js');
263
+ const { installedSkillVersions, SKILL_INSTALL } = await import('../core/skill-check.js');
264
+ const paths = resolvePaths(process.env);
265
+ const config = await loadConfig(paths.configFile).catch(() => ({}));
266
+ const cache = await u.readUpdateCache(paths);
267
+ const skills = await installedSkillVersions(paths.home);
268
+ const n = u.jsonNotice(cache, packageVersion(), config, process.env, skills, SKILL_INSTALL);
269
+ if (!u.checksOff(process.env, config) && u.checkDue(cache, new Date())) {
270
+ const { spawn } = await import('node:child_process');
271
+ const child = spawn(process.execPath, [fileURLToPath(import.meta.url), 'update-check'], {
272
+ detached: true,
273
+ stdio: 'ignore',
274
+ });
275
+ child.on('error', () => { });
276
+ child.unref();
277
+ }
278
+ return n;
279
+ }
280
+ catch {
281
+ return null;
282
+ }
283
+ }
252
284
  /**
253
285
  * After a command (spec §3.23): print the Update Notice and the result of an Auto Update, each at most once, and
254
286
  * start the next Update Check detached when the last one is a day old. Only on a terminal; never fails the command.
@@ -319,6 +351,7 @@ export async function main(argv) {
319
351
  // Only sideby's own options count: arguments after `--` belong to the host.
320
352
  const own = args.includes('--') ? args.slice(0, args.indexOf('--')) : args;
321
353
  const wantsJson = Boolean(spec?.flags.includes('json')) && own.includes('--json');
354
+ let notice = null;
322
355
  try {
323
356
  if (!spec)
324
357
  throw new UsageError(`unknown command "${cmd}"; run \`sideby --help\``);
@@ -354,6 +387,10 @@ export async function main(argv) {
354
387
  return 0;
355
388
  }
356
389
  const cmds = await import('./commands.js');
390
+ if (wantsJson && noticeAllowed(cmd, parsed)) {
391
+ notice = await jsonNoticeFor();
392
+ cmds.setJsonNotice(notice);
393
+ }
357
394
  const code = await dispatch(cmd, parsed, io, cmds);
358
395
  if (!wantsJson && noticeAllowed(cmd, parsed))
359
396
  await afterCommand();
@@ -365,7 +402,7 @@ export async function main(argv) {
365
402
  if (wantsJson) {
366
403
  const { UserError } = await import('../core/errors.js');
367
404
  const code = usage ? 'usage' : err instanceof UserError && err.code ? err.code : 'error';
368
- console.log(JSON.stringify({ schemaVersion: 1, error: message, code }, null, 2));
405
+ console.log(JSON.stringify({ schemaVersion: 1, error: message, code, ...(notice ? { _notice: notice } : {}) }, null, 2));
369
406
  }
370
407
  else
371
408
  console.error(`sideby: ${message}`);
@@ -2,6 +2,19 @@ import type { FamilyDef, Finding } from '../types.ts';
2
2
  export declare const SKILL_INSTALL = "npx skills add Adonis0123/sideby -g";
3
3
  /** `metadata.version` from a SKILL.md frontmatter, or null when there is none. */
4
4
  export declare function skillVersionOf(text: string): string | null;
5
+ /**
6
+ * The versions of the sideby skills an agent reads on this machine (`null`: a skill without one): where
7
+ * `npx skills add -g` puts it (`~/.agents/skills`) and Claude Code's user skills. Read for a `--json` output's
8
+ * `_notice`, so it looks at two fixed files only; Doctor checks every Family's main directory.
9
+ */
10
+ export declare function installedSkillVersions(home: string): Promise<(string | null)[]>;
11
+ /** The same skills with the real folder each lives in (for `sideby update`, which may update them). */
12
+ export declare function installedSkills(home: string): Promise<{
13
+ dir: string;
14
+ version: string | null;
15
+ }[]>;
16
+ /** The command `sideby update` runs to update an installed sideby skill: it keeps the agents it was installed for. */
17
+ export declare const SKILL_UPDATE: readonly ['npx', '-y', 'skills', 'update', 'sideby', '-g', '-y'];
5
18
  /**
6
19
  * Checks, for each given Family, the sideby skill its Host reads: `<main>/skills/sideby`, and for Codex also
7
20
  * `~/.agents/skills/sideby` (where `npx skills add -g` installs and Codex reads user skills). A file is read
@@ -2,7 +2,7 @@
2
2
  // sideby version teaches commands this CLI may not have, so a version mismatch is a warning with the
3
3
  // reinstall command. Not installed is not a problem.
4
4
  import { readFile, realpath } from 'node:fs/promises';
5
- import { join } from 'node:path';
5
+ import { dirname, join } from 'node:path';
6
6
  import { mainDirOf } from './accounts.js';
7
7
  import { tildify } from './paths.js';
8
8
  export const SKILL_INSTALL = 'npx skills add Adonis0123/sideby -g';
@@ -12,6 +12,34 @@ export function skillVersionOf(text) {
12
12
  const meta = front && /^metadata:[ \t]*\r?\n((?:[ \t]+.*(?:\r?\n|$))*)/m.exec(front)?.[1];
13
13
  return (meta && /^[ \t]+version:[ \t]*['"]?([^'"\s]+)/m.exec(meta)?.[1]) || null;
14
14
  }
15
+ /**
16
+ * The versions of the sideby skills an agent reads on this machine (`null`: a skill without one): where
17
+ * `npx skills add -g` puts it (`~/.agents/skills`) and Claude Code's user skills. Read for a `--json` output's
18
+ * `_notice`, so it looks at two fixed files only; Doctor checks every Family's main directory.
19
+ */
20
+ export async function installedSkillVersions(home) {
21
+ return (await installedSkills(home)).map((s) => s.version);
22
+ }
23
+ /** The same skills with the real folder each lives in (for `sideby update`, which may update them). */
24
+ export async function installedSkills(home) {
25
+ const seen = new Set();
26
+ const out = [];
27
+ for (const dir of [join(home, '.agents', 'skills'), join(home, '.claude', 'skills')]) {
28
+ try {
29
+ const real = await realpath(join(dir, 'sideby', 'SKILL.md'));
30
+ if (seen.has(real))
31
+ continue;
32
+ seen.add(real);
33
+ out.push({ dir: dirname(real), version: skillVersionOf(await readFile(real, 'utf8')) });
34
+ }
35
+ catch {
36
+ // not installed there
37
+ }
38
+ }
39
+ return out;
40
+ }
41
+ /** The command `sideby update` runs to update an installed sideby skill: it keeps the agents it was installed for. */
42
+ export const SKILL_UPDATE = ['npx', '-y', 'skills', 'update', 'sideby', '-g', '-y'];
15
43
  /**
16
44
  * Checks, for each given Family, the sideby skill its Host reads: `<main>/skills/sideby`, and for Codex also
17
45
  * `~/.agents/skills/sideby` (where `npx skills add -g` installs and Codex reads user skills). A file is read
@@ -114,6 +114,27 @@ export declare function refreshUpdateCheck(paths: Paths, env: Env): Promise<Upda
114
114
  export declare function noticeLine(cache: UpdateCache, current: string, now: Date): string | null;
115
115
  /** What the CLI says once about the last Auto Update, or null. */
116
116
  export declare function autoUpdateLine(cache: UpdateCache): string | null;
117
+ /** The `_notice` of a `--json` output (spec §3.23): what an agent should mention once its task is done. */
118
+ export interface JsonNotice {
119
+ update?: {
120
+ current: string;
121
+ latest: string;
122
+ command: string;
123
+ message: string;
124
+ };
125
+ skill?: {
126
+ installed: string | null;
127
+ current: string;
128
+ command: string;
129
+ message: string;
130
+ };
131
+ }
132
+ /**
133
+ * The `_notice` for a `--json` output, from the cached Update Check and the versions of the installed sideby skills
134
+ * (`null` for one written before skills had a version); null when there is nothing to say or checks are off. It
135
+ * reads no network: an agent sees what the last check found, as a person sees the Update Notice.
136
+ */
137
+ export declare function jsonNotice(cache: UpdateCache, current: string, config: UpdateSettings, env: Env, skills: readonly (string | null)[], skillCommand: string): JsonNotice | null;
117
138
  /**
118
139
  * What the CLI does after a command (spec §3.23), decided from the cache: the lines to print (the Auto Update result,
119
140
  * then the Update Notice), the change that records them, and whether to start a background check. The caller prints,
@@ -153,6 +174,8 @@ export interface UpdatePlan {
153
174
  command: string;
154
175
  canRun: boolean;
155
176
  releaseNotes: string;
177
+ /** The installed sideby skill, which an Update brings along (absent when none is installed). */
178
+ skill?: SkillUpdatePlan;
156
179
  }
157
180
  export declare function planFrom(method: InstallMethod, current: string, latest: string, env?: Env): UpdatePlan;
158
181
  /** The Install Method as outputs show it: no folder of this machine. */
@@ -174,9 +197,28 @@ export declare function shownPlan(plan: UpdatePlan): {
174
197
  command: string;
175
198
  canRun: boolean;
176
199
  releaseNotes: string;
200
+ skillUpdate?: SkillUpdatePlan | undefined;
177
201
  };
202
+ /** What an Update does to the installed sideby skill (spec §3.23, ADR-0021 amendment 2026-10-17). */
203
+ export interface SkillUpdatePlan {
204
+ /** Versions of the installed skills (`null`: one without a version). */
205
+ installed: (string | null)[];
206
+ /** Some installed skill is for another version than the one sideby will run. */
207
+ outdated: boolean;
208
+ command: string;
209
+ /** False when sideby must not run it; `reason` says why and the user runs `command` where it belongs. */
210
+ canRun: boolean;
211
+ reason?: string;
212
+ }
213
+ /** The skill part of an Update toward `target`; null when no sideby skill is installed. Reads two files. */
214
+ export declare function skillUpdatePlan(home: string, target: string): Promise<SkillUpdatePlan | null>;
178
215
  /** Asks the registry now (whatever the switches say: the user asked) and returns what an Update would do. */
179
216
  export declare function planUpdate(paths: Paths, env: Env): Promise<UpdatePlan>;
217
+ /** Updates the installed sideby skill when the plan says it is outdated and sideby may; null when it did nothing. */
218
+ export declare function applySkillUpdate(plan: SkillUpdatePlan | undefined, env: Env, onOutput?: (text: string) => void): Promise<{
219
+ ok: boolean;
220
+ message: string;
221
+ } | null>;
180
222
  export interface UpdateResult {
181
223
  ok: boolean;
182
224
  /** Exit code of the install command; absent when it did not run. */
@@ -189,6 +231,9 @@ export interface UpdateResult {
189
231
  refreshedApp: boolean;
190
232
  /** Another Update held the lock; nothing ran. */
191
233
  busy?: boolean;
234
+ /** The installed sideby skill was updated too (true), or that failed (false); absent when it was not tried. */
235
+ skillUpdated?: boolean;
236
+ skillMessage?: string;
192
237
  }
193
238
  /**
194
239
  * Runs the Update the plan names, then starts a running background Panel again from the new code and, when the
@@ -6,6 +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 { installedSkills, SKILL_INSTALL, SKILL_UPDATE } from './skill-check.js';
9
10
  import { localBuild, packageVersion, parseLocalBuild } from './version.js';
10
11
  export const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
11
12
  export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
@@ -351,6 +352,32 @@ export function autoUpdateLine(cache) {
351
352
  ? `sideby updated itself from ${a.from} to ${a.to} (${RELEASE_NOTES(a.to)})`
352
353
  : `sideby could not update itself to ${a.to}: ${a.message}`;
353
354
  }
355
+ /**
356
+ * The `_notice` for a `--json` output, from the cached Update Check and the versions of the installed sideby skills
357
+ * (`null` for one written before skills had a version); null when there is nothing to say or checks are off. It
358
+ * reads no network: an agent sees what the last check found, as a person sees the Update Notice.
359
+ */
360
+ export function jsonNotice(cache, current, config, env, skills, skillCommand) {
361
+ if (checksOff(env, config))
362
+ return null;
363
+ const out = {};
364
+ if (isNewer(cache.latest, current))
365
+ out.update = {
366
+ current,
367
+ latest: cache.latest,
368
+ command: 'sideby update',
369
+ message: `sideby ${cache.latest} is available (this is ${current}); \`sideby update\` shows what it runs and updates after --yes`,
370
+ };
371
+ const stale = skills.find((v) => v !== current);
372
+ if (stale !== undefined)
373
+ out.skill = {
374
+ installed: stale,
375
+ current,
376
+ command: skillCommand,
377
+ message: `the installed sideby skill is ${stale ? `for sideby ${stale}` : 'from before skills had a version'}; this sideby is ${current}. \`${skillCommand}\` updates it`,
378
+ };
379
+ return out.update || out.skill ? out : null;
380
+ }
354
381
  /**
355
382
  * What the CLI does after a command (spec §3.23), decided from the cache: the lines to print (the Auto Update result,
356
383
  * then the Update Notice), the change that records them, and whether to start a background check. The caller prints,
@@ -532,14 +559,69 @@ export function shownPlan(plan) {
532
559
  command: plan.command,
533
560
  canRun: plan.canRun,
534
561
  releaseNotes: plan.releaseNotes,
562
+ ...(plan.skill ? { skillUpdate: plan.skill } : {}),
563
+ };
564
+ }
565
+ /** The folder holding `dir` that is a git work tree, if any: a skill there is managed by that repository. */
566
+ async function gitRootOf(dir) {
567
+ for (let d = dir;; d = dirname(d)) {
568
+ try {
569
+ await stat(join(d, '.git'));
570
+ return d;
571
+ }
572
+ catch {
573
+ // keep looking up
574
+ }
575
+ if (dirname(d) === d)
576
+ return null;
577
+ }
578
+ }
579
+ /** The skill part of an Update toward `target`; null when no sideby skill is installed. Reads two files. */
580
+ export async function skillUpdatePlan(home, target) {
581
+ const skills = await installedSkills(home);
582
+ if (!skills.length)
583
+ return null;
584
+ const installed = skills.map((s) => s.version);
585
+ const plan = {
586
+ installed,
587
+ outdated: installed.some((v) => v !== target),
588
+ command: SKILL_UPDATE.join(' '),
589
+ canRun: true,
535
590
  };
591
+ for (const s of skills) {
592
+ const root = await gitRootOf(s.dir);
593
+ if (root) {
594
+ const at = root.startsWith(home) ? `~${root.slice(home.length)}` : root;
595
+ return {
596
+ ...plan,
597
+ command: SKILL_INSTALL,
598
+ canRun: false,
599
+ reason: `the skill is in the git repository ${at}, so sideby leaves it alone; update it where that repository comes from`,
600
+ };
601
+ }
602
+ }
603
+ return plan;
536
604
  }
537
605
  /** Asks the registry now (whatever the switches say: the user asked) and returns what an Update would do. */
538
606
  export async function planUpdate(paths, env) {
539
607
  const latest = await fetchLatest(env);
540
608
  const at = deps().now().toISOString();
541
609
  await updateUpdateCache(paths, { checkedAt: at, triedAt: at, latest }).catch(() => { });
542
- return planFrom(await installMethod(env), packageVersion(), latest, env);
610
+ const plan = planFrom(await installMethod(env), packageVersion(), latest, env);
611
+ const skill = await skillUpdatePlan(paths.home, plan.available ? latest : plan.current).catch(() => null);
612
+ return skill ? { ...plan, skill } : plan;
613
+ }
614
+ /** Updates the installed sideby skill when the plan says it is outdated and sideby may; null when it did nothing. */
615
+ export async function applySkillUpdate(plan, env, onOutput) {
616
+ if (!plan?.outdated || !plan.canRun)
617
+ return null;
618
+ const [file, ...args] = SKILL_UPDATE;
619
+ const r = await deps()
620
+ .exec(file, args, { env, ...(onOutput ? { onOutput } : {}) })
621
+ .catch((e) => ({ code: 1, output: e instanceof Error ? e.message : String(e) }));
622
+ return r.code === 0
623
+ ? { ok: true, message: 'updated the sideby skill' }
624
+ : { ok: false, message: `\`${plan.command}\` failed (exit ${r.code}); run it yourself to see why` };
543
625
  }
544
626
  /**
545
627
  * Runs the Update the plan names, then starts a running background Panel again from the new code and, when the
@@ -582,6 +664,8 @@ export async function applyUpdate(paths, plan, env, opts = {}) {
582
664
  // the new code writes it again. Only where one is installed; a failure here never fails the Update.
583
665
  const installed = await d.appInstalled(paths.home, env).catch(() => false);
584
666
  const refreshed = installed && (await d.refreshApp(env).catch(() => false));
667
+ // The skill teaches agents this version's commands, so it follows the CLI; a failure there keeps the Update.
668
+ const skill = await applySkillUpdate(plan.skill, env, opts.onOutput);
585
669
  return {
586
670
  ok: true,
587
671
  code: 0,
@@ -589,6 +673,7 @@ export async function applyUpdate(paths, plan, env, opts = {}) {
589
673
  message: `updated sideby from ${plan.current} to ${plan.latest}`,
590
674
  restartedPanel: restarted,
591
675
  refreshedApp: refreshed,
676
+ ...(skill ? { skillUpdated: skill.ok, skillMessage: skill.message } : {}),
592
677
  };
593
678
  }
594
679
  finally {
@@ -92,8 +92,10 @@ export interface PanelUpdate {
92
92
  command?: string;
93
93
  canRun: boolean;
94
94
  releaseNotes?: string;
95
- /** The command that updates the sideby skill, which sideby never runs itself. */
95
+ /** The command that installs the sideby skill by hand. */
96
96
  skill: string;
97
+ /** The installed sideby skill, which an Update brings along (spec §3.23); absent when none is installed. */
98
+ skillUpdate?: import('../core/update.ts').SkillUpdatePlan;
97
99
  }
98
100
  export interface PanelState {
99
101
  schemaVersion: 1;
@@ -61,6 +61,9 @@ 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 = await update
65
+ .skillUpdatePlan(rt.paths.home, plan?.available ? plan.latest : current)
66
+ .catch(() => null);
64
67
  return {
65
68
  current,
66
69
  ...(cache.latest ? { latest: cache.latest } : {}),
@@ -72,6 +75,7 @@ async function updateState(rt, readOnly) {
72
75
  ...(plan ? { command: plan.command, releaseNotes: plan.releaseNotes } : {}),
73
76
  canRun: Boolean(plan?.canRun),
74
77
  skill: SKILL_INSTALL,
78
+ ...(skillUpdate ? { skillUpdate } : {}),
75
79
  };
76
80
  }
77
81
  /** The `handoff` map of the Panel state, from the statuses and Quota already read for the page. */
@@ -643,6 +647,7 @@ export function createPanelHandler(opts) {
643
647
  message: r.message,
644
648
  restartedPanel: r.restartedPanel,
645
649
  refreshedApp: r.refreshedApp,
650
+ ...(r.skillUpdated === undefined ? {} : { skillUpdated: r.skillUpdated }),
646
651
  };
647
652
  },
648
653
  },
@@ -20,6 +20,9 @@ declare const en: {
20
20
  'upd.after': string;
21
21
  'upd.cannot': string;
22
22
  'upd.skill': string;
23
+ 'upd.skillToo': string;
24
+ 'upd.skillDone': string;
25
+ 'upd.skillManaged': string;
23
26
  'upd.checksOff': string;
24
27
  'upd.latest': string;
25
28
  'upd.latestNow': string;
@@ -21,7 +21,10 @@ const en = {
21
21
  'upd.runs': 'Updating runs:',
22
22
  'upd.after': 'A background panel restarts by itself and this page reloads. A sideby ui running in a terminal and sessions already running keep the old version until you restart them.',
23
23
  'upd.cannot': 'sideby cannot update itself here: {why}. Run this yourself:',
24
- 'upd.skill': 'If you use the sideby skill, update it too:',
24
+ 'upd.skill': 'If you use the sideby skill, install it with:',
25
+ 'upd.skillToo': 'The sideby skill agents read is updated with it.',
26
+ 'upd.skillDone': 'The sideby skill was updated too; agents load it in their next session.',
27
+ 'upd.skillManaged': 'sideby leaves the skill alone ({why}). Update it with:',
25
28
  'upd.checksOff': 'Update checks are off ({why}).',
26
29
  'upd.latest': 'This is the newest version',
27
30
  'upd.latestNow': 'You have the newest version (checked just now).',
@@ -630,7 +633,10 @@ const zh = {
630
633
  'upd.runs': '更新会运行:',
631
634
  'upd.after': '后台面板会自己重启,本页随后自动刷新。终端里运行的 sideby ui 和已经在跑的会话要重启后才用新版本。',
632
635
  'upd.cannot': '这种安装方式下 sideby 不能自己更新:{why}。请自己运行:',
633
- 'upd.skill': '如果你装了 sideby skill,也更新一下:',
636
+ 'upd.skill': '如果要用 sideby skill,用这条命令安装:',
637
+ 'upd.skillToo': 'agent 读的 sideby skill 会一起更新。',
638
+ 'upd.skillDone': 'sideby skill 也已更新,agent 下次开会话时生效。',
639
+ 'upd.skillManaged': 'sideby 不会自动更新这个 skill({why})。请用这条命令更新:',
634
640
  'upd.checksOff': '检查更新已关闭({why})。',
635
641
  'upd.latest': '已是最新版本',
636
642
  'upd.latestNow': '已是最新版本(刚刚检查)。',
@@ -3198,6 +3198,14 @@ const MAIN = String.raw `
3198
3198
  up.autoBusy = false
3199
3199
  renderUpdate()
3200
3200
  }
3201
+ // The installed sideby skill follows the CLI (spec §3.23): said before the Update, or what to run when sideby may not.
3202
+ function updSkill(u) {
3203
+ const s = u.skillUpdate
3204
+ if (!s) return [h('p', { class: 'small' }, t('upd.skill')), codeCopy(u.skill)]
3205
+ if (!s.outdated) return []
3206
+ if (s.canRun) return [h('p', { class: 'muted small' }, t('upd.skillToo'))]
3207
+ return [h('p', { class: 'small' }, t('upd.skillManaged', { why: s.reason || '' })), codeCopy(s.command)]
3208
+ }
3201
3209
  function renderUpdate() {
3202
3210
  if (!up || dlg.kind !== 'update') return
3203
3211
  const u = updState()
@@ -3211,7 +3219,8 @@ const MAIN = String.raw `
3211
3219
  if (up.done) {
3212
3220
  body.push(h('p', { class: 'note' }, t('upd.done', { v: up.done.latest })))
3213
3221
  body.push(h('p', { class: 'muted small' }, Date.now() > up.waitUntil ? t('upd.restartHint') : [spinner(), ' ', t('upd.reloading')]))
3214
- body.push(h('p', { class: 'small' }, t('upd.skill')), codeCopy(u.skill))
3222
+ if (up.done.skillUpdated) body.push(h('p', { class: 'small' }, t('upd.skillDone')))
3223
+ else body.push(...updSkill(u))
3215
3224
  } else if (u.available) {
3216
3225
  if (up.changed) body.push(h('p', { class: 'note note-warn' }, t('upd.changed', { v: u.latest })))
3217
3226
  body.push(h('p', null, h('b', null, t('upd.available', { v: u.latest })), ' ',
@@ -3223,7 +3232,7 @@ const MAIN = String.raw `
3223
3232
  body.push(h('p', { class: 'note note-warn' }, t('upd.cannot', { why: u.method.reason || u.method.kind })))
3224
3233
  body.push(codeCopy(u.command))
3225
3234
  }
3226
- body.push(h('p', { class: 'small' }, t('upd.skill')), codeCopy(u.skill))
3235
+ body.push(...updSkill(u))
3227
3236
  } else if (up.checked && u.latest) {
3228
3237
  body.push(h('p', { class: 'note' }, t('upd.latestNow')))
3229
3238
  } else if (u.checksOff) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sideby",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Run work, personal and API-key accounts at the same time, without their configs drifting apart.",
5
5
  "license": "MIT",
6
6
  "author": "Adonis0123 (https://github.com/Adonis0123)",