sideby 0.14.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +2 -0
- package/dist/src/cli/commands.js +176 -9
- package/dist/src/cli/json-schemas.js +90 -2
- package/dist/src/cli/main.js +24 -5
- package/dist/src/core/config.d.ts +1 -0
- package/dist/src/core/config.js +8 -2
- package/dist/src/core/skill-check.d.ts +14 -0
- package/dist/src/core/skill-check.js +29 -3
- package/dist/src/core/update.d.ts +67 -1
- package/dist/src/core/update.js +147 -23
- package/dist/src/panel/handler.d.ts +7 -1
- package/dist/src/panel/handler.js +21 -5
- package/dist/src/panel/i18n.d.ts +118 -5
- package/dist/src/panel/i18n.js +266 -40
- package/dist/src/panel/page-logic.d.ts +41 -0
- package/dist/src/panel/page-logic.js +46 -0
- package/dist/src/panel/page-script.js +571 -162
- package/dist/src/panel/page-styles.js +71 -23
- package/dist/src/panel/page.js +3 -0
- package/dist/src/runtime.d.ts +2 -0
- package/package.json +3 -2
- package/schemas/alias.json +1 -1
- package/schemas/app-install.json +1 -1
- package/schemas/app-uninstall.json +1 -1
- package/schemas/browser-open.json +1 -1
- package/schemas/browser-switch.json +1 -1
- package/schemas/browser.json +1 -1
- package/schemas/config.json +15 -1
- package/schemas/desktop-remove.json +1 -1
- package/schemas/desktop.json +1 -1
- package/schemas/doctor.json +1 -1
- package/schemas/error.json +1 -1
- package/schemas/families.json +1 -1
- package/schemas/handoff-disable.json +1 -1
- package/schemas/handoff-enable.json +1 -1
- package/schemas/handoff-ready.json +1 -1
- package/schemas/handoff-setup.json +1 -1
- package/schemas/handoff-status.json +282 -1
- package/schemas/handoff-teardown.json +1 -1
- package/schemas/list.json +6 -1
- package/schemas/model.json +1 -1
- package/schemas/new.json +1 -1
- package/schemas/next.json +1 -1
- package/schemas/order.json +148 -0
- package/schemas/pin.json +126 -0
- package/schemas/plugins.json +1 -1
- package/schemas/quota-check.json +88 -0
- package/schemas/quota-setup.json +1 -1
- package/schemas/quota-teardown.json +1 -1
- package/schemas/quota.json +1 -1
- package/schemas/remote-control.json +1 -1
- package/schemas/tune-change.json +1 -1
- package/schemas/tune.json +1 -1
- package/schemas/unpin.json +126 -0
- package/schemas/update-auto.json +84 -0
- package/schemas/update.json +79 -1
- package/skills/sideby/SKILL.md +7 -3
- package/skills/sideby/references/handoff.md +8 -0
- package/skills/sideby/references/panel.md +28 -0
- package/skills/sideby/references/quota.md +6 -0
- package/skills/sideby/references/update.md +6 -2
|
@@ -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
|
}
|
|
@@ -87,6 +108,12 @@ type UpdateSettings = {
|
|
|
87
108
|
auto?: boolean;
|
|
88
109
|
};
|
|
89
110
|
};
|
|
111
|
+
export type ChecksOffCode = 'local-build' | 'env' | 'config' | 'ci';
|
|
112
|
+
/** Why Update Checks are off, with a code for the Panel, or null when they are on (spec §3.23). */
|
|
113
|
+
export declare function checksOffWhy(env: Env, config: UpdateSettings, local?: boolean): {
|
|
114
|
+
code: ChecksOffCode;
|
|
115
|
+
text: string;
|
|
116
|
+
} | null;
|
|
90
117
|
/** Why Update Checks are off, or null when they are on (spec §3.23). */
|
|
91
118
|
export declare function checksOff(env: Env, config: UpdateSettings, local?: boolean): string | null;
|
|
92
119
|
export declare function readUpdateCache(paths: Paths): Promise<UpdateCache>;
|
|
@@ -114,6 +141,29 @@ export declare function refreshUpdateCheck(paths: Paths, env: Env): Promise<Upda
|
|
|
114
141
|
export declare function noticeLine(cache: UpdateCache, current: string, now: Date): string | null;
|
|
115
142
|
/** What the CLI says once about the last Auto Update, or null. */
|
|
116
143
|
export declare function autoUpdateLine(cache: UpdateCache): string | null;
|
|
144
|
+
/** What the CLI says once about the skill Auto Update brought up to date alone, or null. */
|
|
145
|
+
export declare function skillAutoLine(cache: UpdateCache): string | null;
|
|
146
|
+
/**
|
|
147
|
+
* The sideby skill an agent reads, as the CLI sees it after a command: the version of the first one behind
|
|
148
|
+
* `current` (null: one without a version), and whether sideby may update it. Absent when none is behind.
|
|
149
|
+
*/
|
|
150
|
+
export interface SkillBehind {
|
|
151
|
+
installed: string | null;
|
|
152
|
+
canRun: boolean;
|
|
153
|
+
}
|
|
154
|
+
/** The skill part of a `SkillUpdatePlan` toward the running version, as `afterCommandPlan` takes it. */
|
|
155
|
+
export declare const skillBehind: (plan: SkillUpdatePlan | null, current: string) => SkillBehind | null;
|
|
156
|
+
/**
|
|
157
|
+
* The line about a skill behind this version the CLI may print now (spec §3.23), or null: once a day per CLI version,
|
|
158
|
+
* and never while Auto Update is on (the background check brings the skill along) and sideby may update it.
|
|
159
|
+
*/
|
|
160
|
+
export declare function skillNoticeLine(cache: UpdateCache, current: string, config: UpdateSettings, skill: SkillBehind | null, now: Date): string | null;
|
|
161
|
+
/**
|
|
162
|
+
* Auto Update is on, the CLI is the newest version the cache knows, and the skill is behind it where sideby may
|
|
163
|
+
* update it, not tried for this version yet (or a failed try a day ago). A newer CLI waiting is left to the Update
|
|
164
|
+
* of the CLI: the skills tool fetches the default branch, which would put the skill ahead of this CLI.
|
|
165
|
+
*/
|
|
166
|
+
export declare function skillAutoPending(cache: UpdateCache, current: string, config: UpdateSettings, skill: SkillBehind | null, now: Date): boolean;
|
|
117
167
|
/** The `_notice` of a `--json` output (spec §3.23): what an agent should mention once its task is done. */
|
|
118
168
|
export interface JsonNotice {
|
|
119
169
|
update?: {
|
|
@@ -140,7 +190,7 @@ export declare function jsonNotice(cache: UpdateCache, current: string, config:
|
|
|
140
190
|
* then the Update Notice), the change that records them, and whether to start a background check. The caller prints,
|
|
141
191
|
* writes `record` with `updateUpdateCache` and starts the check; nothing here touches the machine.
|
|
142
192
|
*/
|
|
143
|
-
export declare function afterCommandPlan(cache: UpdateCache, current: string, config: UpdateSettings, env: Env, now: Date): {
|
|
193
|
+
export declare function afterCommandPlan(cache: UpdateCache, current: string, config: UpdateSettings, env: Env, now: Date, skill?: SkillBehind | null): {
|
|
144
194
|
lines: string[];
|
|
145
195
|
record: ((cur: UpdateCache) => Partial<UpdateCache>) | null;
|
|
146
196
|
startCheck: boolean;
|
|
@@ -183,6 +233,8 @@ export declare const shownMethod: (m: InstallMethod) => {
|
|
|
183
233
|
kind: InstallKind;
|
|
184
234
|
canUpdate: boolean;
|
|
185
235
|
reason?: string | undefined;
|
|
236
|
+
reasonCode?: ReasonCode | undefined;
|
|
237
|
+
reasonAt?: string | undefined;
|
|
186
238
|
};
|
|
187
239
|
/** The plan as `sideby update --json` and the Panel show it (schemas/update.json). */
|
|
188
240
|
export declare function shownPlan(plan: UpdatePlan): {
|
|
@@ -193,6 +245,8 @@ export declare function shownPlan(plan: UpdatePlan): {
|
|
|
193
245
|
kind: InstallKind;
|
|
194
246
|
canUpdate: boolean;
|
|
195
247
|
reason?: string | undefined;
|
|
248
|
+
reasonCode?: ReasonCode | undefined;
|
|
249
|
+
reasonAt?: string | undefined;
|
|
196
250
|
};
|
|
197
251
|
command: string;
|
|
198
252
|
canRun: boolean;
|
|
@@ -209,6 +263,9 @@ export interface SkillUpdatePlan {
|
|
|
209
263
|
/** False when sideby must not run it; `reason` says why and the user runs `command` where it belongs. */
|
|
210
264
|
canRun: boolean;
|
|
211
265
|
reason?: string;
|
|
266
|
+
reasonCode?: ReasonCode;
|
|
267
|
+
/** The git repository that owns the skill (`skill-in-git`). */
|
|
268
|
+
reasonAt?: string;
|
|
212
269
|
}
|
|
213
270
|
/** The skill part of an Update toward `target`; null when no sideby skill is installed. Reads two files. */
|
|
214
271
|
export declare function skillUpdatePlan(home: string, target: string): Promise<SkillUpdatePlan | null>;
|
|
@@ -219,6 +276,15 @@ export declare function applySkillUpdate(plan: SkillUpdatePlan | undefined, env:
|
|
|
219
276
|
ok: boolean;
|
|
220
277
|
message: string;
|
|
221
278
|
} | null>;
|
|
279
|
+
/**
|
|
280
|
+
* Updates only the sideby skill, for a CLI that is already current (`sideby update`, the Panel, Auto Update). Holds
|
|
281
|
+
* the same lock as an Update, so a skill update never runs beside one; null when there was nothing to do.
|
|
282
|
+
*/
|
|
283
|
+
export declare function applySkillOnly(paths: Paths, plan: SkillUpdatePlan | undefined, env: Env, onOutput?: (text: string) => void): Promise<{
|
|
284
|
+
ok: boolean;
|
|
285
|
+
message: string;
|
|
286
|
+
busy?: boolean;
|
|
287
|
+
} | null>;
|
|
222
288
|
export interface UpdateResult {
|
|
223
289
|
ok: boolean;
|
|
224
290
|
/** Exit code of the install command; absent when it did not run. */
|
package/dist/src/core/update.js
CHANGED
|
@@ -6,7 +6,7 @@ import { access, constants, mkdir, open, readdir, readFile, realpath, rm, stat }
|
|
|
6
6
|
import { dirname, join, sep } from 'node:path';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
8
|
import { writeFileAtomic } from './fs-safe.js';
|
|
9
|
-
import {
|
|
9
|
+
import { installedSkills, SKILL_UPDATE, SKILL_UPDATE_SHOWN, skillOwnerRepo } from './skill-check.js';
|
|
10
10
|
import { localBuild, packageVersion, parseLocalBuild } from './version.js';
|
|
11
11
|
export const DEFAULT_REGISTRY = 'https://registry.npmjs.org';
|
|
12
12
|
export const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -151,21 +151,25 @@ export function sameLine(from, to) {
|
|
|
151
151
|
const set = (v) => v !== undefined && v !== '' && v !== '0' && v.toLowerCase() !== 'false';
|
|
152
152
|
/** True in CI, where a notice is noise and a check is a request nobody reads. */
|
|
153
153
|
export const inCi = (env) => set(env.CI) || set(env.CONTINUOUS_INTEGRATION) || set(env.BUILD_NUMBER);
|
|
154
|
-
/** Why Update Checks are off, or null when they are on (spec §3.23). */
|
|
155
|
-
export function
|
|
154
|
+
/** Why Update Checks are off, with a code for the Panel, or null when they are on (spec §3.23). */
|
|
155
|
+
export function checksOffWhy(env, config, local = localBuild() !== null) {
|
|
156
156
|
// A Local Build (ADR-0024) follows the checkout: a notice or an Auto Update would only replace it with npm's.
|
|
157
157
|
if (local)
|
|
158
|
-
return 'this is a local build (pnpm local)';
|
|
158
|
+
return { code: 'local-build', text: 'this is a local build (pnpm local)' };
|
|
159
159
|
if (set(env.SIDEBY_NO_UPDATE_NOTIFIER))
|
|
160
|
-
return 'SIDEBY_NO_UPDATE_NOTIFIER is set';
|
|
160
|
+
return { code: 'env', text: 'SIDEBY_NO_UPDATE_NOTIFIER is set' };
|
|
161
161
|
if (set(env.NO_UPDATE_NOTIFIER))
|
|
162
|
-
return 'NO_UPDATE_NOTIFIER is set';
|
|
162
|
+
return { code: 'env', text: 'NO_UPDATE_NOTIFIER is set' };
|
|
163
163
|
if (config.update?.check === false)
|
|
164
|
-
return 'config update.check is false';
|
|
164
|
+
return { code: 'config', text: 'config update.check is false' };
|
|
165
165
|
if (inCi(env))
|
|
166
|
-
return 'running in CI';
|
|
166
|
+
return { code: 'ci', text: 'running in CI' };
|
|
167
167
|
return null;
|
|
168
168
|
}
|
|
169
|
+
/** Why Update Checks are off, or null when they are on (spec §3.23). */
|
|
170
|
+
export function checksOff(env, config, local = localBuild() !== null) {
|
|
171
|
+
return checksOffWhy(env, config, local)?.text ?? null;
|
|
172
|
+
}
|
|
169
173
|
// ---- registry
|
|
170
174
|
/** The registry checks and installs use: `SIDEBY_UPDATE_REGISTRY` (http or https only), else npm's. */
|
|
171
175
|
function registryOf(env) {
|
|
@@ -217,6 +221,18 @@ export async function readUpdateCache(paths) {
|
|
|
217
221
|
message: a.message,
|
|
218
222
|
...(a.shown ? { shown: true } : {}),
|
|
219
223
|
};
|
|
224
|
+
const sn = d.skillNotified;
|
|
225
|
+
if (sn && str(sn.version) && str(sn.at))
|
|
226
|
+
out.skillNotified = { version: sn.version, at: sn.at };
|
|
227
|
+
const sa = d.skillAuto;
|
|
228
|
+
if (sa && str(sa.to) && str(sa.at) && typeof sa.ok === 'boolean' && str(sa.message))
|
|
229
|
+
out.skillAuto = {
|
|
230
|
+
to: sa.to,
|
|
231
|
+
at: sa.at,
|
|
232
|
+
ok: sa.ok,
|
|
233
|
+
message: sa.message,
|
|
234
|
+
...(sa.shown ? { shown: true } : {}),
|
|
235
|
+
};
|
|
220
236
|
return out;
|
|
221
237
|
}
|
|
222
238
|
catch {
|
|
@@ -352,6 +368,44 @@ export function autoUpdateLine(cache) {
|
|
|
352
368
|
? `sideby updated itself from ${a.from} to ${a.to} (${RELEASE_NOTES(a.to)})`
|
|
353
369
|
: `sideby could not update itself to ${a.to}: ${a.message}`;
|
|
354
370
|
}
|
|
371
|
+
/** What the CLI says once about the skill Auto Update brought up to date alone, or null. */
|
|
372
|
+
export function skillAutoLine(cache) {
|
|
373
|
+
const a = cache.skillAuto;
|
|
374
|
+
if (!a || a.shown)
|
|
375
|
+
return null;
|
|
376
|
+
return a.ok
|
|
377
|
+
? `sideby updated the sideby skill agents read to ${a.to}`
|
|
378
|
+
: `sideby could not update the sideby skill to ${a.to}: ${a.message}`;
|
|
379
|
+
}
|
|
380
|
+
/** The skill part of a `SkillUpdatePlan` toward the running version, as `afterCommandPlan` takes it. */
|
|
381
|
+
export const skillBehind = (plan, current) => plan?.outdated
|
|
382
|
+
? { installed: plan.installed.find((v) => v !== current) ?? null, canRun: plan.canRun }
|
|
383
|
+
: null;
|
|
384
|
+
/**
|
|
385
|
+
* The line about a skill behind this version the CLI may print now (spec §3.23), or null: once a day per CLI version,
|
|
386
|
+
* and never while Auto Update is on (the background check brings the skill along) and sideby may update it.
|
|
387
|
+
*/
|
|
388
|
+
export function skillNoticeLine(cache, current, config, skill, now) {
|
|
389
|
+
if (!skill || (config.update?.auto === true && skill.canRun))
|
|
390
|
+
return null;
|
|
391
|
+
const n = cache.skillNotified;
|
|
392
|
+
if (n && n.version === current && age(n.at, now) < NOTICE_INTERVAL_MS)
|
|
393
|
+
return null;
|
|
394
|
+
return `the sideby skill agents read is for ${skill.installed ?? 'an older sideby'} (this sideby is ${current}) · run \`sideby update\``;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Auto Update is on, the CLI is the newest version the cache knows, and the skill is behind it where sideby may
|
|
398
|
+
* update it, not tried for this version yet (or a failed try a day ago). A newer CLI waiting is left to the Update
|
|
399
|
+
* of the CLI: the skills tool fetches the default branch, which would put the skill ahead of this CLI.
|
|
400
|
+
*/
|
|
401
|
+
export function skillAutoPending(cache, current, config, skill, now) {
|
|
402
|
+
if (config.update?.auto !== true || !skill?.canRun || isNewer(cache.latest, current))
|
|
403
|
+
return false;
|
|
404
|
+
const a = cache.skillAuto;
|
|
405
|
+
if (!a || a.to !== current)
|
|
406
|
+
return true;
|
|
407
|
+
return !a.ok && !(age(a.at, now) < AUTO_RETRY_MS);
|
|
408
|
+
}
|
|
355
409
|
/**
|
|
356
410
|
* The `_notice` for a `--json` output, from the cached Update Check and the versions of the installed sideby skills
|
|
357
411
|
* (`null` for one written before skills had a version); null when there is nothing to say or checks are off. It
|
|
@@ -383,27 +437,40 @@ export function jsonNotice(cache, current, config, env, skills, skillCommand) {
|
|
|
383
437
|
* then the Update Notice), the change that records them, and whether to start a background check. The caller prints,
|
|
384
438
|
* writes `record` with `updateUpdateCache` and starts the check; nothing here touches the machine.
|
|
385
439
|
*/
|
|
386
|
-
export function afterCommandPlan(cache, current, config, env, now) {
|
|
440
|
+
export function afterCommandPlan(cache, current, config, env, now, skill = null) {
|
|
387
441
|
if (checksOff(env, config))
|
|
388
442
|
return { lines: [], record: null, startCheck: false };
|
|
389
443
|
const lines = [];
|
|
390
444
|
const auto = autoUpdateLine(cache);
|
|
445
|
+
const skillAuto = skillAutoLine(cache);
|
|
391
446
|
const notice = noticeLine(cache, current, now);
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
447
|
+
// A newer CLI's notice already leads to `sideby update`, which brings the skill along.
|
|
448
|
+
const skillNotice = notice ? null : skillNoticeLine(cache, current, config, skill, now);
|
|
449
|
+
for (const l of [auto, skillAuto, notice, skillNotice])
|
|
450
|
+
if (l)
|
|
451
|
+
lines.push(l);
|
|
396
452
|
const shownAuto = cache.auto;
|
|
453
|
+
const shownSkillAuto = cache.skillAuto;
|
|
397
454
|
const record = lines.length
|
|
398
455
|
? (cur) => ({
|
|
399
456
|
// Mark only the Auto Update that was printed; a newer one written meanwhile stays to be said.
|
|
400
457
|
...(auto && cur.auto && cur.auto.at === shownAuto?.at && cur.auto.to === shownAuto.to
|
|
401
458
|
? { auto: { ...cur.auto, shown: true } }
|
|
402
459
|
: {}),
|
|
460
|
+
...(skillAuto && cur.skillAuto && cur.skillAuto.at === shownSkillAuto?.at
|
|
461
|
+
? { skillAuto: { ...cur.skillAuto, shown: true } }
|
|
462
|
+
: {}),
|
|
403
463
|
...(notice ? { notified: { version: cache.latest, at: now.toISOString() } } : {}),
|
|
464
|
+
...(skillNotice ? { skillNotified: { version: current, at: now.toISOString() } } : {}),
|
|
404
465
|
})
|
|
405
466
|
: null;
|
|
406
|
-
return {
|
|
467
|
+
return {
|
|
468
|
+
lines,
|
|
469
|
+
record,
|
|
470
|
+
startCheck: checkDue(cache, now) ||
|
|
471
|
+
autoPending(cache, current, config, now) ||
|
|
472
|
+
skillAutoPending(cache, current, config, skill, now),
|
|
473
|
+
};
|
|
407
474
|
}
|
|
408
475
|
// ---- Install Method
|
|
409
476
|
/** Where the running copy comes from (spec §3.23 table); reads only paths. */
|
|
@@ -421,16 +488,20 @@ export async function installMethod(env) {
|
|
|
421
488
|
if (parseLocalBuild(JSON.parse(await readFile(join(packageDir, 'dist', 'build.json'), 'utf8'))))
|
|
422
489
|
return at('local', {
|
|
423
490
|
reason: 'it is a local build; run `pnpm local --off` in the sideby checkout to go back to npm',
|
|
491
|
+
reasonCode: 'local-build',
|
|
424
492
|
});
|
|
425
493
|
}
|
|
426
494
|
catch {
|
|
427
495
|
// no build file: not a Local Build
|
|
428
496
|
}
|
|
429
497
|
if (p.includes('/_npx/'))
|
|
430
|
-
return at('npx', {
|
|
498
|
+
return at('npx', {
|
|
499
|
+
reason: 'it runs from the npx cache, which fetches a version per call',
|
|
500
|
+
reasonCode: 'npx',
|
|
501
|
+
});
|
|
431
502
|
// Volta keeps a global package under its own npm prefix; installing there behind its back breaks its records.
|
|
432
503
|
if (p.includes('/.volta/'))
|
|
433
|
-
return at('volta', { reason: 'Volta manages this install' });
|
|
504
|
+
return at('volta', { reason: 'Volta manages this install', reasonCode: 'volta' });
|
|
434
505
|
const npm = /^(.*)\/lib\/node_modules\/sideby$/.exec(p);
|
|
435
506
|
if (npm) {
|
|
436
507
|
const prefix = npm[1];
|
|
@@ -442,7 +513,11 @@ export async function installMethod(env) {
|
|
|
442
513
|
prefix,
|
|
443
514
|
canUpdate: !blocked.length,
|
|
444
515
|
...(blocked.length
|
|
445
|
-
? {
|
|
516
|
+
? {
|
|
517
|
+
reason: `${blocked.join(' and ')} ${blocked.length > 1 ? 'are' : 'is'} not writable by you`,
|
|
518
|
+
reasonCode: 'not-writable',
|
|
519
|
+
reasonAt: blocked.join(', '),
|
|
520
|
+
}
|
|
446
521
|
: {}),
|
|
447
522
|
});
|
|
448
523
|
}
|
|
@@ -452,8 +527,8 @@ export async function installMethod(env) {
|
|
|
452
527
|
if (p.includes('/.bun/install/global/'))
|
|
453
528
|
return at('bun', { canUpdate: true });
|
|
454
529
|
if (p.includes('/node_modules/'))
|
|
455
|
-
return at('project', { reason: 'it is a dependency of a project' });
|
|
456
|
-
return at('source', { reason: 'it runs from a source checkout' });
|
|
530
|
+
return at('project', { reason: 'it is a dependency of a project', reasonCode: 'project' });
|
|
531
|
+
return at('source', { reason: 'it runs from a source checkout', reasonCode: 'source' });
|
|
457
532
|
}
|
|
458
533
|
/**
|
|
459
534
|
* The command an Update runs (or that the user runs) for `version`; `run` is set only when sideby may run it. With
|
|
@@ -548,6 +623,8 @@ export const shownMethod = (m) => ({
|
|
|
548
623
|
kind: m.kind,
|
|
549
624
|
canUpdate: m.canUpdate,
|
|
550
625
|
...(m.reason ? { reason: m.reason } : {}),
|
|
626
|
+
...(m.reasonCode ? { reasonCode: m.reasonCode } : {}),
|
|
627
|
+
...(m.reasonAt ? { reasonAt: m.reasonAt } : {}),
|
|
551
628
|
});
|
|
552
629
|
/** The plan as `sideby update --json` and the Panel show it (schemas/update.json). */
|
|
553
630
|
export function shownPlan(plan) {
|
|
@@ -575,14 +652,17 @@ export async function skillUpdatePlan(home, target) {
|
|
|
575
652
|
canRun: true,
|
|
576
653
|
};
|
|
577
654
|
for (const s of skills) {
|
|
578
|
-
|
|
655
|
+
// A git repository owns the skill unless the skills tool records installing it there (spec §3.23).
|
|
656
|
+
const root = await skillOwnerRepo(s.dir);
|
|
579
657
|
if (root) {
|
|
580
658
|
const at = root.startsWith(home) ? `~${root.slice(home.length)}` : root;
|
|
581
659
|
return {
|
|
582
660
|
...plan,
|
|
583
|
-
command:
|
|
661
|
+
command: SKILL_UPDATE_SHOWN,
|
|
584
662
|
canRun: false,
|
|
585
663
|
reason: `the skill is in the git repository ${at}, so sideby leaves it alone; update it where that repository comes from`,
|
|
664
|
+
reasonCode: 'skill-in-git',
|
|
665
|
+
reasonAt: at,
|
|
586
666
|
};
|
|
587
667
|
}
|
|
588
668
|
}
|
|
@@ -609,6 +689,27 @@ export async function applySkillUpdate(plan, env, onOutput) {
|
|
|
609
689
|
? { ok: true, message: 'updated the sideby skill' }
|
|
610
690
|
: { ok: false, message: `\`${plan.command}\` failed (exit ${r.code}); run it yourself to see why` };
|
|
611
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* Updates only the sideby skill, for a CLI that is already current (`sideby update`, the Panel, Auto Update). Holds
|
|
694
|
+
* the same lock as an Update, so a skill update never runs beside one; null when there was nothing to do.
|
|
695
|
+
*/
|
|
696
|
+
export async function applySkillOnly(paths, plan, env, onOutput) {
|
|
697
|
+
if (!plan?.outdated || !plan.canRun)
|
|
698
|
+
return null;
|
|
699
|
+
const release = await takeLock(paths, 'update-install.lock').catch(() => null);
|
|
700
|
+
if (!release)
|
|
701
|
+
return {
|
|
702
|
+
ok: false,
|
|
703
|
+
busy: true,
|
|
704
|
+
message: 'another sideby update is running; wait for it to finish, then run `sideby update` again',
|
|
705
|
+
};
|
|
706
|
+
try {
|
|
707
|
+
return await applySkillUpdate(plan, env, onOutput);
|
|
708
|
+
}
|
|
709
|
+
finally {
|
|
710
|
+
await release().catch(() => { });
|
|
711
|
+
}
|
|
712
|
+
}
|
|
612
713
|
/**
|
|
613
714
|
* Runs the Update the plan names, then starts a running background Panel again from the new code and, when the
|
|
614
715
|
* desktop app is installed, runs `sideby app install` again from the new code. The caller has
|
|
@@ -689,6 +790,22 @@ export async function autoUpdateBlocker(paths, plan) {
|
|
|
689
790
|
return `a session is running (pid ${running.join(', ')})`;
|
|
690
791
|
return null;
|
|
691
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* The skill half of Auto Update (spec §3.23): with the CLI current, a skill behind it is updated alone and the result
|
|
795
|
+
* recorded once per version, so a failure waits a day.
|
|
796
|
+
*/
|
|
797
|
+
async function skillAutoSync(paths, env, config, cache, current, now) {
|
|
798
|
+
const plan = await skillUpdatePlan(paths.home, current).catch(() => null);
|
|
799
|
+
if (!skillAutoPending(cache, current, config, skillBehind(plan, current), now))
|
|
800
|
+
return;
|
|
801
|
+
const r = await applySkillOnly(paths, plan ?? undefined, env);
|
|
802
|
+
// A manual Update holding the lock is not a failure; the next command looks again.
|
|
803
|
+
if (!r || r.busy)
|
|
804
|
+
return;
|
|
805
|
+
await updateUpdateCache(paths, {
|
|
806
|
+
skillAuto: { to: current, at: deps().now().toISOString(), ok: r.ok, message: r.message },
|
|
807
|
+
});
|
|
808
|
+
}
|
|
692
809
|
/**
|
|
693
810
|
* Body of the detached `sideby update-check`: an Update Check when one is due, then an Auto Update when config
|
|
694
811
|
* `update.auto` is on and nothing blocks it. Only one runs at a time; it never throws.
|
|
@@ -705,9 +822,16 @@ export async function backgroundCheck(paths, env, config, panelPort) {
|
|
|
705
822
|
if (checkDue(cache, now))
|
|
706
823
|
cache = await refreshUpdateCheck(paths, env);
|
|
707
824
|
const current = packageVersion();
|
|
708
|
-
if (!autoPending(cache, current, config, now))
|
|
825
|
+
if (!autoPending(cache, current, config, now)) {
|
|
826
|
+
await skillAutoSync(paths, env, config, cache, current, now);
|
|
709
827
|
return;
|
|
710
|
-
|
|
828
|
+
}
|
|
829
|
+
// The skill teaches agents the new version's commands, so an Auto Update carries it as `sideby update` does.
|
|
830
|
+
const skill = await skillUpdatePlan(paths.home, cache.latest).catch(() => null);
|
|
831
|
+
const plan = {
|
|
832
|
+
...planFrom(await installMethod(env), current, cache.latest, env),
|
|
833
|
+
...(skill ? { skill } : {}),
|
|
834
|
+
};
|
|
711
835
|
await updateUpdateCache(paths, { autoTriedAt: now.toISOString() });
|
|
712
836
|
// A blocker only the user can lift is said once and looked at again a day later, like a failed install; a
|
|
713
837
|
// running session only delays it (the next look is within the hour).
|
|
@@ -81,12 +81,15 @@ export interface PanelUpdate {
|
|
|
81
81
|
checkedAt?: string;
|
|
82
82
|
/** Why Update Checks are off; absent when they are on. */
|
|
83
83
|
checksOff?: string;
|
|
84
|
+
checksOffCode?: update.ChecksOffCode;
|
|
84
85
|
/** Config `update.auto`. */
|
|
85
86
|
auto: boolean;
|
|
86
87
|
method: {
|
|
87
88
|
kind: update.InstallKind;
|
|
88
89
|
canUpdate: boolean;
|
|
89
90
|
reason?: string;
|
|
91
|
+
reasonCode?: update.ReasonCode;
|
|
92
|
+
reasonAt?: string;
|
|
90
93
|
};
|
|
91
94
|
/** The command an Update runs, or the one to run by hand when `canRun` is false. */
|
|
92
95
|
command?: string;
|
|
@@ -94,7 +97,10 @@ export interface PanelUpdate {
|
|
|
94
97
|
releaseNotes?: string;
|
|
95
98
|
/** The command that installs the sideby skill by hand. */
|
|
96
99
|
skill: string;
|
|
97
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* The installed sideby skill, which an Update brings along (spec §3.23); absent when none is installed, and for a
|
|
102
|
+
* Local Build, which `pnpm local --off` brings back to npm together with the skill.
|
|
103
|
+
*/
|
|
98
104
|
skillUpdate?: import('../core/update.ts').SkillUpdatePlan;
|
|
99
105
|
}
|
|
100
106
|
export interface PanelState {
|
|
@@ -53,7 +53,7 @@ let checking = null;
|
|
|
53
53
|
*/
|
|
54
54
|
async function updateState(rt, readOnly) {
|
|
55
55
|
const current = packageVersion();
|
|
56
|
-
const off = update.
|
|
56
|
+
const off = update.checksOffWhy(rt.env, rt.config);
|
|
57
57
|
const cache = await update.readUpdateCache(rt.paths);
|
|
58
58
|
if (!off && !readOnly && !checking && update.checkDue(cache, new Date()))
|
|
59
59
|
checking = update.refreshUpdateCheck(rt.paths, rt.env).finally(() => {
|
|
@@ -61,15 +61,15 @@ async function updateState(rt, readOnly) {
|
|
|
61
61
|
});
|
|
62
62
|
const method = await update.installMethod(rt.env);
|
|
63
63
|
const plan = cache.latest ? update.shownPlan(update.planFrom(method, current, cache.latest, rt.env)) : null;
|
|
64
|
-
const skillUpdate =
|
|
65
|
-
|
|
66
|
-
.catch(() => null);
|
|
64
|
+
const skillUpdate = method.kind === 'local'
|
|
65
|
+
? null
|
|
66
|
+
: await update.skillUpdatePlan(rt.paths.home, plan?.available ? plan.latest : current).catch(() => null);
|
|
67
67
|
return {
|
|
68
68
|
current,
|
|
69
69
|
...(cache.latest ? { latest: cache.latest } : {}),
|
|
70
70
|
available: Boolean(plan?.available),
|
|
71
71
|
...(cache.checkedAt ? { checkedAt: cache.checkedAt } : {}),
|
|
72
|
-
...(off ? { checksOff: off } : {}),
|
|
72
|
+
...(off ? { checksOff: off.text, checksOffCode: off.code } : {}),
|
|
73
73
|
auto: rt.config.update?.auto === true,
|
|
74
74
|
method: update.shownMethod(method),
|
|
75
75
|
...(plan ? { command: plan.command, releaseNotes: plan.releaseNotes } : {}),
|
|
@@ -329,6 +329,11 @@ function displayBody(body) {
|
|
|
329
329
|
throw new HttpError(400, '"accounts" must be an object');
|
|
330
330
|
out.accounts = Object.fromEntries(Object.entries(v).map(([f, list]) => [f, names(list, `"accounts.${f}"`)]));
|
|
331
331
|
}
|
|
332
|
+
else if (k === 'pinned') {
|
|
333
|
+
if (!v || typeof v !== 'object' || Array.isArray(v))
|
|
334
|
+
throw new HttpError(400, '"pinned" must be an object');
|
|
335
|
+
out.pinned = Object.fromEntries(Object.entries(v).map(([f, list]) => [f, names(list, `"pinned.${f}"`)]));
|
|
336
|
+
}
|
|
332
337
|
else
|
|
333
338
|
throw new HttpError(400, `unknown key "${k}"`);
|
|
334
339
|
}
|
|
@@ -661,6 +666,17 @@ export function createPanelHandler(opts) {
|
|
|
661
666
|
const shown = update.shownPlan(plan);
|
|
662
667
|
if (!optBool(body, 'confirm'))
|
|
663
668
|
return { ...shown, updated: false };
|
|
669
|
+
// The CLI is current and the skill is behind: `skill: true` updates it alone, as `sideby update --yes` does.
|
|
670
|
+
if (optBool(body, 'skill')) {
|
|
671
|
+
if (plan.available || plan.method.kind === 'local')
|
|
672
|
+
return { ...shown, updated: false, changed: true };
|
|
673
|
+
const r = await update.applySkillOnly(rt.paths, plan.skill, rt.env);
|
|
674
|
+
if (!r)
|
|
675
|
+
return { ...shown, updated: false, changed: true };
|
|
676
|
+
if (!r.ok)
|
|
677
|
+
throw new UserError(r.message);
|
|
678
|
+
return { ...shown, updated: false, skillUpdated: true, message: r.message };
|
|
679
|
+
}
|
|
664
680
|
// A confirmation that runs nothing always says why: the registry names another version (or none newer) now.
|
|
665
681
|
if (body.version !== plan.latest || !plan.available)
|
|
666
682
|
return { ...shown, updated: false, changed: true };
|