sideby 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +7 -0
- package/dist/src/cli/commands.js +259 -9
- package/dist/src/cli/json-schemas.js +109 -2
- package/dist/src/cli/main.js +30 -6
- package/dist/src/core/config.d.ts +1 -0
- package/dist/src/core/config.js +8 -2
- package/dist/src/core/local-build.d.ts +63 -0
- package/dist/src/core/local-build.js +189 -0
- 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 +73 -1
- package/dist/src/core/update.js +155 -23
- package/dist/src/panel/handler.d.ts +12 -1
- package/dist/src/panel/handler.js +84 -5
- package/dist/src/panel/i18n.d.ts +125 -5
- package/dist/src/panel/i18n.js +280 -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 +616 -162
- package/dist/src/panel/page-styles.js +74 -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/local.json +165 -0
- 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 +8 -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 +15 -2
package/dist/src/core/config.js
CHANGED
|
@@ -114,9 +114,12 @@ export const ConfigSchema = Type.Object({
|
|
|
114
114
|
accounts: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }), {
|
|
115
115
|
description: 'Per family, account names (not refs or short commands) in the order they are shown; accounts not listed follow, main first, then by name.',
|
|
116
116
|
})),
|
|
117
|
+
pinned: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String({ minLength: 1 }), { uniqueItems: true }), {
|
|
118
|
+
description: 'Per family, the names of the accounts pinned to the top of their group, in the order they are shown; they lead the group in every sort. Resetting the order keeps them.',
|
|
119
|
+
})),
|
|
117
120
|
}, {
|
|
118
121
|
additionalProperties: false,
|
|
119
|
-
description: 'Display Order (spec §3.25): the order you
|
|
122
|
+
description: 'Display Order (spec §3.25): the order you gave families and accounts in the Panel, and the accounts you pinned. Display only: it never changes which account a Handoff picks.',
|
|
120
123
|
})),
|
|
121
124
|
quota: Type.Optional(Type.Object({
|
|
122
125
|
check: Type.Optional(Type.Record(Type.String(), Type.Boolean(), {
|
|
@@ -275,10 +278,13 @@ export function editConfigBrowser(file, edit, opts = {}) {
|
|
|
275
278
|
* or empty (see `editConfigKey` for how the file is written).
|
|
276
279
|
*/
|
|
277
280
|
export function editConfigDisplay(file, display) {
|
|
278
|
-
const
|
|
281
|
+
const lists = (r) => Object.fromEntries(Object.entries(r ?? {}).filter(([, names]) => names.length));
|
|
282
|
+
const accounts = lists(display?.accounts);
|
|
283
|
+
const pinned = lists(display?.pinned);
|
|
279
284
|
const next = {
|
|
280
285
|
...(display?.families?.length ? { families: [...display.families] } : {}),
|
|
281
286
|
...(Object.keys(accounts).length ? { accounts } : {}),
|
|
287
|
+
...(Object.keys(pinned).length ? { pinned } : {}),
|
|
282
288
|
};
|
|
283
289
|
return editConfigKey(file, 'display', 'move it again', () => (Object.keys(next).length ? next : undefined));
|
|
284
290
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Env } from '../types.ts';
|
|
2
|
+
import type { Paths } from './paths.ts';
|
|
3
|
+
import { type LocalBuild } from './version.ts';
|
|
4
|
+
export interface LocalDirs {
|
|
5
|
+
/** `$XDG_DATA_HOME/sideby-dev`: `pkg` (the running build), `old/<id>`, `npm` (the package it replaced). */
|
|
6
|
+
dev: string;
|
|
7
|
+
/** npm's global prefix: `<prefix>/lib/node_modules/sideby` and `<prefix>/bin/sideby`. */
|
|
8
|
+
prefix: string;
|
|
9
|
+
}
|
|
10
|
+
export declare const pkgDir: (d: LocalDirs) => string;
|
|
11
|
+
export declare const globalPkg: (d: LocalDirs) => string;
|
|
12
|
+
export declare const devDir: (env: Env) => string;
|
|
13
|
+
/** Where the switch works, with npm's global prefix from `npm prefix -g`; null when npm cannot be asked. */
|
|
14
|
+
export declare function localDirs(env: Env): Promise<LocalDirs | null>;
|
|
15
|
+
export declare function kind(p: string): Promise<'link' | 'dir' | 'file' | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Points the global npm package at `pkg`. A real npm install moves to `<dev>/npm` first, so going back needs no
|
|
18
|
+
* network; a link elsewhere (npm link) is replaced. Returns what it did, for the summary.
|
|
19
|
+
*/
|
|
20
|
+
export declare function linkGlobal(d: LocalDirs): Promise<string>;
|
|
21
|
+
/** Undoes linkGlobal: the npm package it moved comes back; false when there is none to restore (install it). */
|
|
22
|
+
export declare function unlinkGlobal(d: LocalDirs): Promise<boolean>;
|
|
23
|
+
/** The version of the npm package kept aside by linkGlobal, or null when none was kept or it cannot be read. */
|
|
24
|
+
export declare const keptVersion: (d: LocalDirs) => Promise<string | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Going back to npm: put the kept package back when it is the newest published one (or the registry cannot be
|
|
27
|
+
* asked), else install `latest`. After a release the kept copy is the version before it.
|
|
28
|
+
*/
|
|
29
|
+
export declare function offPlan(kept: string | null, latest: string | null): 'restore' | 'install';
|
|
30
|
+
export interface LocalStatus {
|
|
31
|
+
/** What the global `sideby` runs: the Local Build in `pkg`, an npm package, or neither (not installed, a link elsewhere). */
|
|
32
|
+
running: 'local' | 'npm' | null;
|
|
33
|
+
/** The newest Local Build, the one `on` switches to; null when `pnpm local` never ran here. */
|
|
34
|
+
local: (LocalBuild & {
|
|
35
|
+
version: string;
|
|
36
|
+
}) | null;
|
|
37
|
+
/** The npm package kept aside while a Local Build runs; null when none is. */
|
|
38
|
+
kept: string | null;
|
|
39
|
+
}
|
|
40
|
+
export declare function localStatus(d: LocalDirs): Promise<LocalStatus>;
|
|
41
|
+
export type LocalTarget = 'local' | 'npm';
|
|
42
|
+
/** Why `to` cannot run from this status, or null when it can. Every reason says what to do. */
|
|
43
|
+
export declare function switchBlocker(s: LocalStatus, to: LocalTarget): string | null;
|
|
44
|
+
export interface SwitchResult {
|
|
45
|
+
ok: boolean;
|
|
46
|
+
message: string;
|
|
47
|
+
/** What the global `sideby` runs afterwards. */
|
|
48
|
+
running: LocalStatus['running'];
|
|
49
|
+
/** True when it already ran `to`, and nothing changed. */
|
|
50
|
+
unchanged?: boolean;
|
|
51
|
+
restartedPanel: boolean;
|
|
52
|
+
/** Another install held the lock; nothing ran. */
|
|
53
|
+
busy?: boolean;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Puts `to` behind the global `sideby`. `local` links the newest Local Build; `npm` restores the kept package or
|
|
57
|
+
* installs `latest` (a failed install links the Local Build back, so `sideby` never goes missing). Afterwards a
|
|
58
|
+
* background Panel on `panelPort` starts again from the new code. One install at a time (the Update lock).
|
|
59
|
+
*/
|
|
60
|
+
export declare function switchLocal(paths: Paths, d: LocalDirs, to: LocalTarget, env: Env, opts?: {
|
|
61
|
+
panelPort?: () => Promise<number | null>;
|
|
62
|
+
onOutput?: (text: string) => void;
|
|
63
|
+
}): Promise<SwitchResult>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// Switching the global `sideby` between a Local Build and the npm package (ADR-0024). `pnpm local` builds and calls
|
|
2
|
+
// these; `sideby local` and the Panel's version popover switch between the newest Local Build and npm without the
|
|
3
|
+
// checkout. Only an npm global install is handled: `<prefix>/lib/node_modules/sideby` is the folder every caller
|
|
4
|
+
// resolves, a link to `<dev>/pkg` while a Local Build runs.
|
|
5
|
+
import { lstat, mkdir, readFile, readlink, rename, rm, symlink } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { dirname, join, relative } from 'node:path';
|
|
8
|
+
import { compareVersions, fetchLatest, restartPanelByName, runProgram, takeInstallLock } from './update.js';
|
|
9
|
+
import { parseLocalBuild } from './version.js';
|
|
10
|
+
const BIN = 'dist/src/cli/main.js';
|
|
11
|
+
export const pkgDir = (d) => join(d.dev, 'pkg');
|
|
12
|
+
export const globalPkg = (d) => join(d.prefix, 'lib', 'node_modules', 'sideby');
|
|
13
|
+
const globalBin = (d) => join(d.prefix, 'bin', 'sideby');
|
|
14
|
+
export const devDir = (env) => join(env.XDG_DATA_HOME || join(env.HOME || homedir(), '.local', 'share'), 'sideby-dev');
|
|
15
|
+
/** Where the switch works, with npm's global prefix from `npm prefix -g`; null when npm cannot be asked. */
|
|
16
|
+
export async function localDirs(env) {
|
|
17
|
+
const r = await runProgram('npm', ['prefix', '-g'], { env }).catch(() => null);
|
|
18
|
+
const prefix = r && r.code === 0 ? r.output.trim().split('\n').at(-1)?.trim() : '';
|
|
19
|
+
return prefix ? { dev: devDir(env), prefix } : null;
|
|
20
|
+
}
|
|
21
|
+
export async function kind(p) {
|
|
22
|
+
try {
|
|
23
|
+
const s = await lstat(p);
|
|
24
|
+
return s.isSymbolicLink() ? 'link' : s.isDirectory() ? 'dir' : 'file';
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Points the global npm package at `pkg`. A real npm install moves to `<dev>/npm` first, so going back needs no
|
|
32
|
+
* network; a link elsewhere (npm link) is replaced. Returns what it did, for the summary.
|
|
33
|
+
*/
|
|
34
|
+
export async function linkGlobal(d) {
|
|
35
|
+
const target = globalPkg(d);
|
|
36
|
+
const pkg = pkgDir(d);
|
|
37
|
+
let did = 'the global sideby already runs the local build';
|
|
38
|
+
const k = await kind(target);
|
|
39
|
+
if (k !== 'link' || (await readlink(target)) !== pkg) {
|
|
40
|
+
if (k === 'dir') {
|
|
41
|
+
await rm(join(d.dev, 'npm'), { recursive: true, force: true });
|
|
42
|
+
await rename(target, join(d.dev, 'npm'));
|
|
43
|
+
did = `the npm package moved to ${join(d.dev, 'npm')}; the global sideby now runs the local build`;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
if (k)
|
|
47
|
+
await rm(target, { force: true });
|
|
48
|
+
did = 'the global sideby now runs the local build';
|
|
49
|
+
}
|
|
50
|
+
await mkdir(dirname(target), { recursive: true });
|
|
51
|
+
await symlink(pkg, target, 'dir');
|
|
52
|
+
}
|
|
53
|
+
const bin = globalBin(d);
|
|
54
|
+
const want = relative(dirname(bin), join(target, BIN));
|
|
55
|
+
if ((await kind(bin)) !== 'link' || (await readlink(bin)) !== want) {
|
|
56
|
+
await rm(bin, { force: true });
|
|
57
|
+
await mkdir(dirname(bin), { recursive: true });
|
|
58
|
+
await symlink(want, bin);
|
|
59
|
+
}
|
|
60
|
+
return did;
|
|
61
|
+
}
|
|
62
|
+
/** Undoes linkGlobal: the npm package it moved comes back; false when there is none to restore (install it). */
|
|
63
|
+
export async function unlinkGlobal(d) {
|
|
64
|
+
const target = globalPkg(d);
|
|
65
|
+
if ((await kind(target)) === 'link')
|
|
66
|
+
await rm(target, { force: true });
|
|
67
|
+
else if (await kind(target))
|
|
68
|
+
return true; // a real package is already there
|
|
69
|
+
if (!(await kind(join(d.dev, 'npm'))))
|
|
70
|
+
return false;
|
|
71
|
+
await rename(join(d.dev, 'npm'), target);
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
async function versionOf(dir) {
|
|
75
|
+
try {
|
|
76
|
+
const v = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8')).version;
|
|
77
|
+
return typeof v === 'string' ? v : null;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** The version of the npm package kept aside by linkGlobal, or null when none was kept or it cannot be read. */
|
|
84
|
+
export const keptVersion = (d) => versionOf(join(d.dev, 'npm'));
|
|
85
|
+
/**
|
|
86
|
+
* Going back to npm: put the kept package back when it is the newest published one (or the registry cannot be
|
|
87
|
+
* asked), else install `latest`. After a release the kept copy is the version before it.
|
|
88
|
+
*/
|
|
89
|
+
export function offPlan(kept, latest) {
|
|
90
|
+
if (!kept)
|
|
91
|
+
return 'install';
|
|
92
|
+
if (!latest)
|
|
93
|
+
return 'restore';
|
|
94
|
+
return (compareVersions(kept, latest) ?? -1) >= 0 ? 'restore' : 'install';
|
|
95
|
+
}
|
|
96
|
+
export async function localStatus(d) {
|
|
97
|
+
const k = await kind(globalPkg(d));
|
|
98
|
+
const running = k === 'link'
|
|
99
|
+
? (await readlink(globalPkg(d))) === pkgDir(d)
|
|
100
|
+
? 'local'
|
|
101
|
+
: null
|
|
102
|
+
: k === 'dir'
|
|
103
|
+
? 'npm'
|
|
104
|
+
: null;
|
|
105
|
+
let local = null;
|
|
106
|
+
try {
|
|
107
|
+
const b = parseLocalBuild(JSON.parse(await readFile(join(pkgDir(d), 'dist', 'build.json'), 'utf8')));
|
|
108
|
+
const version = await versionOf(pkgDir(d));
|
|
109
|
+
if (b && version)
|
|
110
|
+
local = { ...b, version };
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// no Local Build here
|
|
114
|
+
}
|
|
115
|
+
return { running, local, kept: await keptVersion(d) };
|
|
116
|
+
}
|
|
117
|
+
/** Why `to` cannot run from this status, or null when it can. Every reason says what to do. */
|
|
118
|
+
export function switchBlocker(s, to) {
|
|
119
|
+
if (s.running === null)
|
|
120
|
+
return 'the global sideby is not an npm global install; switching supports `npm install -g sideby` only';
|
|
121
|
+
if (to === 'local' && !s.local)
|
|
122
|
+
return 'there is no local build yet; run `pnpm local` in the sideby checkout';
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Puts `to` behind the global `sideby`. `local` links the newest Local Build; `npm` restores the kept package or
|
|
127
|
+
* installs `latest` (a failed install links the Local Build back, so `sideby` never goes missing). Afterwards a
|
|
128
|
+
* background Panel on `panelPort` starts again from the new code. One install at a time (the Update lock).
|
|
129
|
+
*/
|
|
130
|
+
export async function switchLocal(paths, d, to, env, opts = {}) {
|
|
131
|
+
const before = await localStatus(d);
|
|
132
|
+
const fail = (message, extra = {}) => ({
|
|
133
|
+
ok: false,
|
|
134
|
+
message,
|
|
135
|
+
running: before.running,
|
|
136
|
+
restartedPanel: false,
|
|
137
|
+
...extra,
|
|
138
|
+
});
|
|
139
|
+
const blocked = switchBlocker(before, to);
|
|
140
|
+
if (blocked)
|
|
141
|
+
return fail(blocked);
|
|
142
|
+
if (before.running === to)
|
|
143
|
+
return {
|
|
144
|
+
ok: true,
|
|
145
|
+
unchanged: true,
|
|
146
|
+
message: to === 'local'
|
|
147
|
+
? 'the global sideby already runs the local build'
|
|
148
|
+
: 'the global sideby already runs the npm package',
|
|
149
|
+
running: to,
|
|
150
|
+
restartedPanel: false,
|
|
151
|
+
};
|
|
152
|
+
const release = await takeInstallLock(paths);
|
|
153
|
+
if (!release)
|
|
154
|
+
return fail('a sideby update is running; wait for it to finish, then switch again', { busy: true });
|
|
155
|
+
let message;
|
|
156
|
+
try {
|
|
157
|
+
if (to === 'local') {
|
|
158
|
+
await linkGlobal(d);
|
|
159
|
+
message = `the global sideby runs the local build ${before.local?.commit ?? ''}`.trim();
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const latest = await fetchLatest(env).catch(() => null);
|
|
163
|
+
if (offPlan(before.kept, latest) === 'restore') {
|
|
164
|
+
await unlinkGlobal(d);
|
|
165
|
+
message = `the global sideby runs the npm package ${before.kept} again`;
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
const want = latest ?? 'latest';
|
|
169
|
+
await rm(globalPkg(d), { force: true });
|
|
170
|
+
const r = await runProgram('npm', ['install', '-g', `sideby@${want}`], {
|
|
171
|
+
env,
|
|
172
|
+
...(opts.onOutput ? { onOutput: opts.onOutput } : {}),
|
|
173
|
+
});
|
|
174
|
+
if (r.code !== 0) {
|
|
175
|
+
await linkGlobal(d);
|
|
176
|
+
return fail(`\`npm install -g sideby@${want}\` failed (exit ${r.code}); the local build stays. Run it yourself to see why`);
|
|
177
|
+
}
|
|
178
|
+
await rm(join(d.dev, 'npm'), { recursive: true, force: true });
|
|
179
|
+
message = `the global sideby runs the npm package ${want}`;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
await release().catch(() => { });
|
|
185
|
+
}
|
|
186
|
+
const port = opts.panelPort ? await opts.panelPort().catch(() => null) : null;
|
|
187
|
+
const restartedPanel = port !== null && (await restartPanelByName(env, port));
|
|
188
|
+
return { ok: true, message, running: to, restartedPanel };
|
|
189
|
+
}
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
import type { FamilyDef, Finding } from '../types.ts';
|
|
2
|
+
export declare const SKILL_SOURCE = "Adonis0123/sideby";
|
|
2
3
|
export declare const SKILL_INSTALL = "npx skills add Adonis0123/sideby -g";
|
|
4
|
+
/** The command a person runs to update the skill where sideby leaves it alone. */
|
|
5
|
+
export declare const SKILL_UPDATE_SHOWN = "npx skills update sideby -g";
|
|
3
6
|
/** The folder holding `dir` that is a git work tree, if any: a skill there is managed by that repository. */
|
|
4
7
|
export declare function gitRootOf(dir: string): Promise<string | null>;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the skills tool's install record (`.skill-lock.json` beside the `skills` folder holding `dir`) names this
|
|
10
|
+
* repository as the source of `sideby`: then `npx skills update` owns the skill, even inside a git repository (a
|
|
11
|
+
* dotfiles clone of `~/.agents`, say). Reads only that one entry's `source`.
|
|
12
|
+
*/
|
|
13
|
+
export declare function installedBySkillsTool(dir: string): Promise<boolean>;
|
|
14
|
+
/**
|
|
15
|
+
* The git repository that owns the sideby skill in `dir`, or null when sideby may update it: not in a repository, or
|
|
16
|
+
* the skills tool installed it there.
|
|
17
|
+
*/
|
|
18
|
+
export declare function skillOwnerRepo(dir: string): Promise<string | null>;
|
|
5
19
|
/** `metadata.version` from a SKILL.md frontmatter, or null when there is none. */
|
|
6
20
|
export declare function skillVersionOf(text: string): string | null;
|
|
7
21
|
/**
|
|
@@ -5,7 +5,10 @@ import { readFile, realpath, stat } from 'node:fs/promises';
|
|
|
5
5
|
import { dirname, join } from 'node:path';
|
|
6
6
|
import { mainDirOf } from './accounts.js';
|
|
7
7
|
import { tildify } from './paths.js';
|
|
8
|
-
export const
|
|
8
|
+
export const SKILL_SOURCE = 'Adonis0123/sideby';
|
|
9
|
+
export const SKILL_INSTALL = `npx skills add ${SKILL_SOURCE} -g`;
|
|
10
|
+
/** The command a person runs to update the skill where sideby leaves it alone. */
|
|
11
|
+
export const SKILL_UPDATE_SHOWN = 'npx skills update sideby -g';
|
|
9
12
|
/** The folder holding `dir` that is a git work tree, if any: a skill there is managed by that repository. */
|
|
10
13
|
export async function gitRootOf(dir) {
|
|
11
14
|
for (let d = dir;; d = dirname(d)) {
|
|
@@ -20,6 +23,29 @@ export async function gitRootOf(dir) {
|
|
|
20
23
|
return null;
|
|
21
24
|
}
|
|
22
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether the skills tool's install record (`.skill-lock.json` beside the `skills` folder holding `dir`) names this
|
|
28
|
+
* repository as the source of `sideby`: then `npx skills update` owns the skill, even inside a git repository (a
|
|
29
|
+
* dotfiles clone of `~/.agents`, say). Reads only that one entry's `source`.
|
|
30
|
+
*/
|
|
31
|
+
export async function installedBySkillsTool(dir) {
|
|
32
|
+
try {
|
|
33
|
+
const lock = JSON.parse(await readFile(join(dirname(dirname(dir)), '.skill-lock.json'), 'utf8'));
|
|
34
|
+
const source = lock.skills?.sideby?.source;
|
|
35
|
+
return typeof source === 'string' && source.toLowerCase() === SKILL_SOURCE.toLowerCase();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The git repository that owns the sideby skill in `dir`, or null when sideby may update it: not in a repository, or
|
|
43
|
+
* the skills tool installed it there.
|
|
44
|
+
*/
|
|
45
|
+
export async function skillOwnerRepo(dir) {
|
|
46
|
+
const repo = await gitRootOf(dir);
|
|
47
|
+
return repo && !(await installedBySkillsTool(dir)) ? repo : null;
|
|
48
|
+
}
|
|
23
49
|
/** `metadata.version` from a SKILL.md frontmatter, or null when there is none. */
|
|
24
50
|
export function skillVersionOf(text) {
|
|
25
51
|
const front = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1];
|
|
@@ -85,7 +111,7 @@ export async function checkInstalledSkills(home, families, version) {
|
|
|
85
111
|
continue;
|
|
86
112
|
const where = tildify(path, home);
|
|
87
113
|
// `sideby update` brings the skill along, except one a git repository owns (a dotfiles clone, say).
|
|
88
|
-
const repo = await
|
|
114
|
+
const repo = await skillOwnerRepo(dirname(real));
|
|
89
115
|
out.push({
|
|
90
116
|
level: 'warn',
|
|
91
117
|
account: `${family.id}:main`,
|
|
@@ -93,7 +119,7 @@ export async function checkInstalledSkills(home, families, version) {
|
|
|
93
119
|
code: 'skill.outdated',
|
|
94
120
|
message: `the sideby skill at ${where} is ${found ? `for sideby ${found}` : 'from before skills had a version'}; this sideby is ${version}`,
|
|
95
121
|
hint: repo
|
|
96
|
-
? `it lives in the git repository ${tildify(repo, home)}: update it where that repository comes from (\`
|
|
122
|
+
? `it lives in the git repository ${tildify(repo, home)}: update it where that repository comes from (\`${SKILL_UPDATE_SHOWN}\` there), so agents learn this version's commands`
|
|
97
123
|
: `run \`sideby update\`, which updates the skill too (or \`${SKILL_INSTALL}\` by hand), so agents learn this version's commands`,
|
|
98
124
|
source: 'core',
|
|
99
125
|
fixable: false,
|
|
@@ -32,7 +32,25 @@ export interface UpdateCache {
|
|
|
32
32
|
message: string;
|
|
33
33
|
shown?: boolean;
|
|
34
34
|
};
|
|
35
|
+
/** The last line the CLI printed about a sideby skill behind this version (`version` is the CLI's). */
|
|
36
|
+
skillNotified?: {
|
|
37
|
+
version: string;
|
|
38
|
+
at: string;
|
|
39
|
+
};
|
|
40
|
+
/** The last time Auto Update brought the skill to `to` alone; `shown` once the CLI has said so. */
|
|
41
|
+
skillAuto?: {
|
|
42
|
+
to: string;
|
|
43
|
+
at: string;
|
|
44
|
+
ok: boolean;
|
|
45
|
+
message: string;
|
|
46
|
+
shown?: boolean;
|
|
47
|
+
};
|
|
35
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Why sideby cannot do something itself, for a Panel to say in the viewer's language; `reason` keeps the English
|
|
51
|
+
* text the CLI prints. `reasonAt` carries the path a code needs.
|
|
52
|
+
*/
|
|
53
|
+
export type ReasonCode = 'local-build' | 'npx' | 'volta' | 'not-writable' | 'project' | 'source' | 'skill-in-git';
|
|
36
54
|
export type InstallKind = 'npm' | 'pnpm' | 'bun' | 'volta' | 'npx' | 'project' | 'source' | 'local';
|
|
37
55
|
export interface InstallMethod {
|
|
38
56
|
kind: InstallKind;
|
|
@@ -42,6 +60,9 @@ export interface InstallMethod {
|
|
|
42
60
|
canUpdate: boolean;
|
|
43
61
|
/** Why it cannot, in a phrase that follows "sideby cannot update itself: ". */
|
|
44
62
|
reason?: string;
|
|
63
|
+
reasonCode?: ReasonCode;
|
|
64
|
+
/** The folders that are not writable (`not-writable`). */
|
|
65
|
+
reasonAt?: string;
|
|
45
66
|
/** npm only: the prefix whose `lib/node_modules` holds sideby. */
|
|
46
67
|
prefix?: string;
|
|
47
68
|
}
|
|
@@ -70,6 +91,12 @@ export interface UpdateDeps {
|
|
|
70
91
|
}
|
|
71
92
|
/** Internal, for tests: replaces parts of the machine. Not part of the plugin or Portal contract. */
|
|
72
93
|
export declare function bindUpdateDeps(deps: Partial<UpdateDeps>): () => void;
|
|
94
|
+
/** Runs a program through the swappable machine (tests bind `exec`); a Local Build switch installs with it. */
|
|
95
|
+
export declare const runProgram: UpdateDeps['exec'];
|
|
96
|
+
/** Starts the background Panel again from the code `sideby` now names; see `UpdateDeps.restartPanel`. */
|
|
97
|
+
export declare const restartPanelByName: (env: Env, port: number) => Promise<boolean>;
|
|
98
|
+
/** The lock every install shares (`update-install.lock`), so a Local Build switch never races an Update. */
|
|
99
|
+
export declare const takeInstallLock: (paths: Paths) => Promise<(() => Promise<void>) | null>;
|
|
73
100
|
/** -1, 0 or 1 like a sort comparator; a prerelease sorts before its release. Null when either is not a version. */
|
|
74
101
|
export declare function compareVersions(a: string, b: string): number | null;
|
|
75
102
|
/** `latest` is newer than `current`. */
|
|
@@ -87,6 +114,12 @@ type UpdateSettings = {
|
|
|
87
114
|
auto?: boolean;
|
|
88
115
|
};
|
|
89
116
|
};
|
|
117
|
+
export type ChecksOffCode = 'local-build' | 'env' | 'config' | 'ci';
|
|
118
|
+
/** Why Update Checks are off, with a code for the Panel, or null when they are on (spec §3.23). */
|
|
119
|
+
export declare function checksOffWhy(env: Env, config: UpdateSettings, local?: boolean): {
|
|
120
|
+
code: ChecksOffCode;
|
|
121
|
+
text: string;
|
|
122
|
+
} | null;
|
|
90
123
|
/** Why Update Checks are off, or null when they are on (spec §3.23). */
|
|
91
124
|
export declare function checksOff(env: Env, config: UpdateSettings, local?: boolean): string | null;
|
|
92
125
|
export declare function readUpdateCache(paths: Paths): Promise<UpdateCache>;
|
|
@@ -114,6 +147,29 @@ export declare function refreshUpdateCheck(paths: Paths, env: Env): Promise<Upda
|
|
|
114
147
|
export declare function noticeLine(cache: UpdateCache, current: string, now: Date): string | null;
|
|
115
148
|
/** What the CLI says once about the last Auto Update, or null. */
|
|
116
149
|
export declare function autoUpdateLine(cache: UpdateCache): string | null;
|
|
150
|
+
/** What the CLI says once about the skill Auto Update brought up to date alone, or null. */
|
|
151
|
+
export declare function skillAutoLine(cache: UpdateCache): string | null;
|
|
152
|
+
/**
|
|
153
|
+
* The sideby skill an agent reads, as the CLI sees it after a command: the version of the first one behind
|
|
154
|
+
* `current` (null: one without a version), and whether sideby may update it. Absent when none is behind.
|
|
155
|
+
*/
|
|
156
|
+
export interface SkillBehind {
|
|
157
|
+
installed: string | null;
|
|
158
|
+
canRun: boolean;
|
|
159
|
+
}
|
|
160
|
+
/** The skill part of a `SkillUpdatePlan` toward the running version, as `afterCommandPlan` takes it. */
|
|
161
|
+
export declare const skillBehind: (plan: SkillUpdatePlan | null, current: string) => SkillBehind | null;
|
|
162
|
+
/**
|
|
163
|
+
* The line about a skill behind this version the CLI may print now (spec §3.23), or null: once a day per CLI version,
|
|
164
|
+
* and never while Auto Update is on (the background check brings the skill along) and sideby may update it.
|
|
165
|
+
*/
|
|
166
|
+
export declare function skillNoticeLine(cache: UpdateCache, current: string, config: UpdateSettings, skill: SkillBehind | null, now: Date): string | null;
|
|
167
|
+
/**
|
|
168
|
+
* Auto Update is on, the CLI is the newest version the cache knows, and the skill is behind it where sideby may
|
|
169
|
+
* update it, not tried for this version yet (or a failed try a day ago). A newer CLI waiting is left to the Update
|
|
170
|
+
* of the CLI: the skills tool fetches the default branch, which would put the skill ahead of this CLI.
|
|
171
|
+
*/
|
|
172
|
+
export declare function skillAutoPending(cache: UpdateCache, current: string, config: UpdateSettings, skill: SkillBehind | null, now: Date): boolean;
|
|
117
173
|
/** The `_notice` of a `--json` output (spec §3.23): what an agent should mention once its task is done. */
|
|
118
174
|
export interface JsonNotice {
|
|
119
175
|
update?: {
|
|
@@ -140,7 +196,7 @@ export declare function jsonNotice(cache: UpdateCache, current: string, config:
|
|
|
140
196
|
* then the Update Notice), the change that records them, and whether to start a background check. The caller prints,
|
|
141
197
|
* writes `record` with `updateUpdateCache` and starts the check; nothing here touches the machine.
|
|
142
198
|
*/
|
|
143
|
-
export declare function afterCommandPlan(cache: UpdateCache, current: string, config: UpdateSettings, env: Env, now: Date): {
|
|
199
|
+
export declare function afterCommandPlan(cache: UpdateCache, current: string, config: UpdateSettings, env: Env, now: Date, skill?: SkillBehind | null): {
|
|
144
200
|
lines: string[];
|
|
145
201
|
record: ((cur: UpdateCache) => Partial<UpdateCache>) | null;
|
|
146
202
|
startCheck: boolean;
|
|
@@ -183,6 +239,8 @@ export declare const shownMethod: (m: InstallMethod) => {
|
|
|
183
239
|
kind: InstallKind;
|
|
184
240
|
canUpdate: boolean;
|
|
185
241
|
reason?: string | undefined;
|
|
242
|
+
reasonCode?: ReasonCode | undefined;
|
|
243
|
+
reasonAt?: string | undefined;
|
|
186
244
|
};
|
|
187
245
|
/** The plan as `sideby update --json` and the Panel show it (schemas/update.json). */
|
|
188
246
|
export declare function shownPlan(plan: UpdatePlan): {
|
|
@@ -193,6 +251,8 @@ export declare function shownPlan(plan: UpdatePlan): {
|
|
|
193
251
|
kind: InstallKind;
|
|
194
252
|
canUpdate: boolean;
|
|
195
253
|
reason?: string | undefined;
|
|
254
|
+
reasonCode?: ReasonCode | undefined;
|
|
255
|
+
reasonAt?: string | undefined;
|
|
196
256
|
};
|
|
197
257
|
command: string;
|
|
198
258
|
canRun: boolean;
|
|
@@ -209,6 +269,9 @@ export interface SkillUpdatePlan {
|
|
|
209
269
|
/** False when sideby must not run it; `reason` says why and the user runs `command` where it belongs. */
|
|
210
270
|
canRun: boolean;
|
|
211
271
|
reason?: string;
|
|
272
|
+
reasonCode?: ReasonCode;
|
|
273
|
+
/** The git repository that owns the skill (`skill-in-git`). */
|
|
274
|
+
reasonAt?: string;
|
|
212
275
|
}
|
|
213
276
|
/** The skill part of an Update toward `target`; null when no sideby skill is installed. Reads two files. */
|
|
214
277
|
export declare function skillUpdatePlan(home: string, target: string): Promise<SkillUpdatePlan | null>;
|
|
@@ -219,6 +282,15 @@ export declare function applySkillUpdate(plan: SkillUpdatePlan | undefined, env:
|
|
|
219
282
|
ok: boolean;
|
|
220
283
|
message: string;
|
|
221
284
|
} | null>;
|
|
285
|
+
/**
|
|
286
|
+
* Updates only the sideby skill, for a CLI that is already current (`sideby update`, the Panel, Auto Update). Holds
|
|
287
|
+
* the same lock as an Update, so a skill update never runs beside one; null when there was nothing to do.
|
|
288
|
+
*/
|
|
289
|
+
export declare function applySkillOnly(paths: Paths, plan: SkillUpdatePlan | undefined, env: Env, onOutput?: (text: string) => void): Promise<{
|
|
290
|
+
ok: boolean;
|
|
291
|
+
message: string;
|
|
292
|
+
busy?: boolean;
|
|
293
|
+
} | null>;
|
|
222
294
|
export interface UpdateResult {
|
|
223
295
|
ok: boolean;
|
|
224
296
|
/** Exit code of the install command; absent when it did not run. */
|