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.
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +1 -0
- package/dist/src/cli/commands.js +48 -3
- package/dist/src/cli/json-schemas.js +48 -1
- package/dist/src/cli/main.js +38 -1
- package/dist/src/core/skill-check.d.ts +13 -0
- package/dist/src/core/skill-check.js +29 -1
- package/dist/src/core/update.d.ts +45 -0
- package/dist/src/core/update.js +86 -1
- package/dist/src/panel/handler.d.ts +3 -1
- package/dist/src/panel/handler.js +5 -0
- package/dist/src/panel/i18n.d.ts +3 -0
- package/dist/src/panel/i18n.js +8 -2
- package/dist/src/panel/page-script.js +11 -2
- package/package.json +1 -1
- package/schemas/alias.json +62 -0
- package/schemas/app-install.json +62 -0
- package/schemas/app-uninstall.json +62 -0
- package/schemas/browser-open.json +62 -0
- package/schemas/browser-switch.json +62 -0
- package/schemas/browser.json +62 -0
- package/schemas/desktop-remove.json +62 -0
- package/schemas/desktop.json +62 -0
- package/schemas/doctor.json +62 -0
- package/schemas/error.json +62 -0
- package/schemas/families.json +62 -0
- package/schemas/handoff-disable.json +62 -0
- package/schemas/handoff-enable.json +62 -0
- package/schemas/handoff-ready.json +62 -0
- package/schemas/handoff-setup.json +62 -0
- package/schemas/handoff-status.json +62 -0
- package/schemas/handoff-teardown.json +62 -0
- package/schemas/list.json +62 -0
- package/schemas/model.json +62 -0
- package/schemas/new.json +62 -0
- package/schemas/next.json +62 -0
- package/schemas/plugins.json +62 -0
- package/schemas/quota-setup.json +62 -0
- package/schemas/quota-teardown.json +62 -0
- package/schemas/quota.json +62 -0
- package/schemas/remote-control.json +62 -0
- package/schemas/tune-change.json +62 -0
- package/schemas/tune.json +62 -0
- package/schemas/update.json +107 -1
- package/skills/sideby/SKILL.md +4 -2
- package/skills/sideby/references/diagnose.md +1 -1
- package/skills/sideby/references/update.md +16 -0
package/dist/build.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"id":"
|
|
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: {
|
package/dist/src/cli/commands.js
CHANGED
|
@@ -51,7 +51,12 @@ async function runAccountNow(rt, ref, args, io) {
|
|
|
51
51
|
}
|
|
52
52
|
return code;
|
|
53
53
|
}
|
|
54
|
-
|
|
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
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/src/cli/main.js
CHANGED
|
@@ -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
|
package/dist/src/core/update.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
},
|
package/dist/src/panel/i18n.d.ts
CHANGED
|
@@ -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;
|
package/dist/src/panel/i18n.js
CHANGED
|
@@ -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,
|
|
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': '
|
|
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.
|
|
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(
|
|
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