sideby 0.12.1 → 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/README.md +1 -1
- package/dist/build.json +1 -1
- package/dist/src/cli/commands.d.ts +5 -2
- package/dist/src/cli/commands.js +165 -20
- package/dist/src/cli/json-schemas.js +100 -2
- package/dist/src/cli/main.js +72 -4
- package/dist/src/core/brand-mark.d.ts +5 -7
- package/dist/src/core/brand-mark.js +18 -5
- package/dist/src/core/browser-profiles.d.ts +71 -0
- package/dist/src/core/browser-profiles.js +361 -0
- package/dist/src/core/config.d.ts +15 -0
- package/dist/src/core/config.js +13 -1
- package/dist/src/core/create.js +2 -1
- package/dist/src/core/desktop-app.d.ts +3 -1
- package/dist/src/core/desktop-app.js +29 -8
- package/dist/src/core/launch.d.ts +7 -0
- package/dist/src/core/launch.js +22 -2
- package/dist/src/core/leveldb-keys.d.ts +14 -0
- package/dist/src/core/leveldb-keys.js +291 -0
- package/dist/src/core/remote-control.d.ts +17 -6
- package/dist/src/core/remote-control.js +40 -17
- 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 +55 -3
- package/dist/src/core/update.js +124 -4
- package/dist/src/core/version.d.ts +11 -0
- package/dist/src/core/version.js +26 -0
- package/dist/src/desktop/app.d.ts +2 -0
- package/dist/src/desktop/app.js +39 -3
- package/dist/src/families/browser.d.ts +20 -0
- package/dist/src/families/browser.js +23 -0
- package/dist/src/families/claude/browser.d.ts +3 -0
- package/dist/src/families/claude/browser.js +14 -0
- package/dist/src/families/claude/desktop.js +1 -0
- package/dist/src/families/codex/browser.d.ts +7 -0
- package/dist/src/families/codex/browser.js +26 -0
- package/dist/src/families/desktop.d.ts +5 -1
- package/dist/src/families/pi/index.js +2 -0
- package/dist/src/panel/handler.d.ts +9 -1
- package/dist/src/panel/handler.js +54 -6
- package/dist/src/panel/i18n.d.ts +74 -4
- package/dist/src/panel/i18n.js +160 -20
- package/dist/src/panel/page-logic.d.ts +23 -0
- package/dist/src/panel/page-logic.js +44 -0
- package/dist/src/panel/page-script.js +587 -183
- package/dist/src/panel/page-styles.js +61 -18
- package/dist/src/panel/page.d.ts +5 -0
- package/dist/src/panel/page.js +10 -9
- package/dist/src/runtime.d.ts +6 -1
- package/dist/src/runtime.js +22 -11
- package/dist/src/types.d.ts +2 -0
- package/package.json +5 -3
- 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 +109 -0
- package/schemas/browser-switch.json +104 -0
- package/schemas/browser.json +161 -0
- package/schemas/config.json +11 -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 +68 -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 +68 -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 +66 -0
- package/schemas/tune-change.json +62 -0
- package/schemas/tune.json +62 -0
- package/schemas/update.json +111 -1
- package/skills/sideby/SKILL.md +8 -4
- package/skills/sideby/references/diagnose.md +1 -1
- package/skills/sideby/references/setup.md +1 -1
- package/skills/sideby/references/update.md +16 -0
|
@@ -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
|
|
@@ -33,7 +33,7 @@ export interface UpdateCache {
|
|
|
33
33
|
shown?: boolean;
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
export type InstallKind = 'npm' | 'pnpm' | 'bun' | 'volta' | 'npx' | 'project' | 'source';
|
|
36
|
+
export type InstallKind = 'npm' | 'pnpm' | 'bun' | 'volta' | 'npx' | 'project' | 'source' | 'local';
|
|
37
37
|
export interface InstallMethod {
|
|
38
38
|
kind: InstallKind;
|
|
39
39
|
/** The installed package folder (real path). */
|
|
@@ -63,6 +63,10 @@ export interface UpdateDeps {
|
|
|
63
63
|
}>;
|
|
64
64
|
/** Starts `sideby ui --background --no-open --port <port>` from the newly installed code, detached; false when it could not start. */
|
|
65
65
|
restartPanel: (env: Env, port: number) => Promise<boolean>;
|
|
66
|
+
/** Whether `sideby app install` put a desktop app in this home (its files carry the app marker). */
|
|
67
|
+
appInstalled: (home: string, env: Env) => Promise<boolean>;
|
|
68
|
+
/** Runs `sideby app install --json` from the newly installed code and waits; false when it failed. */
|
|
69
|
+
refreshApp: (env: Env) => Promise<boolean>;
|
|
66
70
|
}
|
|
67
71
|
/** Internal, for tests: replaces parts of the machine. Not part of the plugin or Portal contract. */
|
|
68
72
|
export declare function bindUpdateDeps(deps: Partial<UpdateDeps>): () => void;
|
|
@@ -84,7 +88,7 @@ type UpdateSettings = {
|
|
|
84
88
|
};
|
|
85
89
|
};
|
|
86
90
|
/** Why Update Checks are off, or null when they are on (spec §3.23). */
|
|
87
|
-
export declare function checksOff(env: Env, config: UpdateSettings): string | null;
|
|
91
|
+
export declare function checksOff(env: Env, config: UpdateSettings, local?: boolean): string | null;
|
|
88
92
|
export declare function readUpdateCache(paths: Paths): Promise<UpdateCache>;
|
|
89
93
|
/**
|
|
90
94
|
* Merges a change into the cache file. The file is read again first, and `change` may be a function of what is there
|
|
@@ -110,6 +114,27 @@ export declare function refreshUpdateCheck(paths: Paths, env: Env): Promise<Upda
|
|
|
110
114
|
export declare function noticeLine(cache: UpdateCache, current: string, now: Date): string | null;
|
|
111
115
|
/** What the CLI says once about the last Auto Update, or null. */
|
|
112
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;
|
|
113
138
|
/**
|
|
114
139
|
* What the CLI does after a command (spec §3.23), decided from the cache: the lines to print (the Auto Update result,
|
|
115
140
|
* then the Update Notice), the change that records them, and whether to start a background check. The caller prints,
|
|
@@ -149,6 +174,8 @@ export interface UpdatePlan {
|
|
|
149
174
|
command: string;
|
|
150
175
|
canRun: boolean;
|
|
151
176
|
releaseNotes: string;
|
|
177
|
+
/** The installed sideby skill, which an Update brings along (absent when none is installed). */
|
|
178
|
+
skill?: SkillUpdatePlan;
|
|
152
179
|
}
|
|
153
180
|
export declare function planFrom(method: InstallMethod, current: string, latest: string, env?: Env): UpdatePlan;
|
|
154
181
|
/** The Install Method as outputs show it: no folder of this machine. */
|
|
@@ -170,9 +197,28 @@ export declare function shownPlan(plan: UpdatePlan): {
|
|
|
170
197
|
command: string;
|
|
171
198
|
canRun: boolean;
|
|
172
199
|
releaseNotes: string;
|
|
200
|
+
skillUpdate?: SkillUpdatePlan | undefined;
|
|
173
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>;
|
|
174
215
|
/** Asks the registry now (whatever the switches say: the user asked) and returns what an Update would do. */
|
|
175
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>;
|
|
176
222
|
export interface UpdateResult {
|
|
177
223
|
ok: boolean;
|
|
178
224
|
/** Exit code of the install command; absent when it did not run. */
|
|
@@ -181,11 +227,17 @@ export interface UpdateResult {
|
|
|
181
227
|
message: string;
|
|
182
228
|
/** A background Panel was running and is starting again from the new code. */
|
|
183
229
|
restartedPanel: boolean;
|
|
230
|
+
/** The desktop app was installed and `sideby app install` ran again from the new code. */
|
|
231
|
+
refreshedApp: boolean;
|
|
184
232
|
/** Another Update held the lock; nothing ran. */
|
|
185
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;
|
|
186
237
|
}
|
|
187
238
|
/**
|
|
188
|
-
* Runs the Update the plan names, then starts a running background Panel again from the new code
|
|
239
|
+
* Runs the Update the plan names, then starts a running background Panel again from the new code and, when the
|
|
240
|
+
* desktop app is installed, runs `sideby app install` again from the new code. The caller has
|
|
189
241
|
* shown the plan and got a confirmation (or this is an Auto Update). One Update runs at a time on this machine (the
|
|
190
242
|
* CLI, a Panel and an Auto Update share the lock). `panelPort` gives the port of the background Panel to restart, or
|
|
191
243
|
* null when none runs. A successful Update is recorded as said, so no notice for it follows.
|
package/dist/src/core/update.js
CHANGED
|
@@ -6,7 +6,8 @@ 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_INSTALL, SKILL_UPDATE } from './skill-check.js';
|
|
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;
|
|
12
13
|
export const NOTICE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -76,6 +77,20 @@ const realDeps = {
|
|
|
76
77
|
});
|
|
77
78
|
child.once('error', () => resolve(false));
|
|
78
79
|
}),
|
|
80
|
+
appInstalled: async (home, env) => {
|
|
81
|
+
const { appInstalled } = await import('../desktop/app.js');
|
|
82
|
+
return appInstalled({ home, env, platform: process.platform });
|
|
83
|
+
},
|
|
84
|
+
refreshApp: (env) => new Promise((resolve) => {
|
|
85
|
+
// By name, like restartPanel: the new code records its own Node and entry and draws the current mark.
|
|
86
|
+
const child = spawn('sideby', ['app', 'install', '--json'], {
|
|
87
|
+
env: env,
|
|
88
|
+
stdio: 'ignore',
|
|
89
|
+
timeout: 120_000,
|
|
90
|
+
});
|
|
91
|
+
child.once('error', () => resolve(false));
|
|
92
|
+
child.once('close', (code) => resolve(code === 0));
|
|
93
|
+
}),
|
|
79
94
|
};
|
|
80
95
|
let override = {};
|
|
81
96
|
/** Internal, for tests: replaces parts of the machine. Not part of the plugin or Portal contract. */
|
|
@@ -137,7 +152,10 @@ const set = (v) => v !== undefined && v !== '' && v !== '0' && v.toLowerCase() !
|
|
|
137
152
|
/** True in CI, where a notice is noise and a check is a request nobody reads. */
|
|
138
153
|
export const inCi = (env) => set(env.CI) || set(env.CONTINUOUS_INTEGRATION) || set(env.BUILD_NUMBER);
|
|
139
154
|
/** Why Update Checks are off, or null when they are on (spec §3.23). */
|
|
140
|
-
export function checksOff(env, config) {
|
|
155
|
+
export function checksOff(env, config, local = localBuild() !== null) {
|
|
156
|
+
// A Local Build (ADR-0024) follows the checkout: a notice or an Auto Update would only replace it with npm's.
|
|
157
|
+
if (local)
|
|
158
|
+
return 'this is a local build (pnpm local)';
|
|
141
159
|
if (set(env.SIDEBY_NO_UPDATE_NOTIFIER))
|
|
142
160
|
return 'SIDEBY_NO_UPDATE_NOTIFIER is set';
|
|
143
161
|
if (set(env.NO_UPDATE_NOTIFIER))
|
|
@@ -334,6 +352,32 @@ export function autoUpdateLine(cache) {
|
|
|
334
352
|
? `sideby updated itself from ${a.from} to ${a.to} (${RELEASE_NOTES(a.to)})`
|
|
335
353
|
: `sideby could not update itself to ${a.to}: ${a.message}`;
|
|
336
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
|
+
}
|
|
337
381
|
/**
|
|
338
382
|
* What the CLI does after a command (spec §3.23), decided from the cache: the lines to print (the Auto Update result,
|
|
339
383
|
* then the Update Notice), the change that records them, and whether to start a background check. The caller prints,
|
|
@@ -373,6 +417,15 @@ export async function installMethod(env) {
|
|
|
373
417
|
canUpdate: false,
|
|
374
418
|
...extra,
|
|
375
419
|
});
|
|
420
|
+
try {
|
|
421
|
+
if (parseLocalBuild(JSON.parse(await readFile(join(packageDir, 'dist', 'build.json'), 'utf8'))))
|
|
422
|
+
return at('local', {
|
|
423
|
+
reason: 'it is a local build; run `pnpm local --off` in the sideby checkout to go back to npm',
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
// no build file: not a Local Build
|
|
428
|
+
}
|
|
376
429
|
if (p.includes('/_npx/'))
|
|
377
430
|
return at('npx', { reason: 'it runs from the npx cache, which fetches a version per call' });
|
|
378
431
|
// Volta keeps a global package under its own npm prefix; installing there behind its back breaks its records.
|
|
@@ -436,6 +489,8 @@ export function updateCommand(method, version, env = {}) {
|
|
|
436
489
|
return { text: `npm install ${spec}` };
|
|
437
490
|
case 'source':
|
|
438
491
|
return { text: 'git pull && pnpm install' };
|
|
492
|
+
case 'local':
|
|
493
|
+
return { text: 'pnpm local --off' };
|
|
439
494
|
}
|
|
440
495
|
}
|
|
441
496
|
// ---- running sessions
|
|
@@ -504,17 +559,73 @@ export function shownPlan(plan) {
|
|
|
504
559
|
command: plan.command,
|
|
505
560
|
canRun: plan.canRun,
|
|
506
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,
|
|
507
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;
|
|
508
604
|
}
|
|
509
605
|
/** Asks the registry now (whatever the switches say: the user asked) and returns what an Update would do. */
|
|
510
606
|
export async function planUpdate(paths, env) {
|
|
511
607
|
const latest = await fetchLatest(env);
|
|
512
608
|
const at = deps().now().toISOString();
|
|
513
609
|
await updateUpdateCache(paths, { checkedAt: at, triedAt: at, latest }).catch(() => { });
|
|
514
|
-
|
|
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` };
|
|
515
625
|
}
|
|
516
626
|
/**
|
|
517
|
-
* Runs the Update the plan names, then starts a running background Panel again from the new code
|
|
627
|
+
* Runs the Update the plan names, then starts a running background Panel again from the new code and, when the
|
|
628
|
+
* desktop app is installed, runs `sideby app install` again from the new code. The caller has
|
|
518
629
|
* shown the plan and got a confirmation (or this is an Auto Update). One Update runs at a time on this machine (the
|
|
519
630
|
* CLI, a Panel and an Auto Update share the lock). `panelPort` gives the port of the background Panel to restart, or
|
|
520
631
|
* null when none runs. A successful Update is recorded as said, so no notice for it follows.
|
|
@@ -526,6 +637,7 @@ export async function applyUpdate(paths, plan, env, opts = {}) {
|
|
|
526
637
|
output: '',
|
|
527
638
|
message,
|
|
528
639
|
restartedPanel: false,
|
|
640
|
+
refreshedApp: false,
|
|
529
641
|
...extra,
|
|
530
642
|
});
|
|
531
643
|
if (!cmd.run)
|
|
@@ -548,12 +660,20 @@ export async function applyUpdate(paths, plan, env, opts = {}) {
|
|
|
548
660
|
}).catch(() => { });
|
|
549
661
|
const port = opts.panelPort ? await opts.panelPort().catch(() => null) : null;
|
|
550
662
|
const restarted = port !== null && (await d.restartPanel(env, port).catch(() => false));
|
|
663
|
+
// The desktop app records the Node and entry it starts and carries the icon of the version that wrote it, so
|
|
664
|
+
// the new code writes it again. Only where one is installed; a failure here never fails the Update.
|
|
665
|
+
const installed = await d.appInstalled(paths.home, env).catch(() => false);
|
|
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);
|
|
551
669
|
return {
|
|
552
670
|
ok: true,
|
|
553
671
|
code: 0,
|
|
554
672
|
output: r.output,
|
|
555
673
|
message: `updated sideby from ${plan.current} to ${plan.latest}`,
|
|
556
674
|
restartedPanel: restarted,
|
|
675
|
+
refreshedApp: refreshed,
|
|
676
|
+
...(skill ? { skillUpdated: skill.ok, skillMessage: skill.message } : {}),
|
|
557
677
|
};
|
|
558
678
|
}
|
|
559
679
|
finally {
|
|
@@ -7,3 +7,14 @@ export declare function packageVersion(): string;
|
|
|
7
7
|
* id for tests.
|
|
8
8
|
*/
|
|
9
9
|
export declare function buildId(env?: NodeJS.ProcessEnv): string;
|
|
10
|
+
/** What `pnpm local` records about a Local Build (ADR-0024). */
|
|
11
|
+
export interface LocalBuild {
|
|
12
|
+
commit: string;
|
|
13
|
+
/** The working tree had changes that were not committed. */
|
|
14
|
+
dirty: boolean;
|
|
15
|
+
builtAt: string;
|
|
16
|
+
}
|
|
17
|
+
/** The Local Build fields of a `dist/build.json`, or null for a published build or anything unexpected. */
|
|
18
|
+
export declare function parseLocalBuild(data: unknown): LocalBuild | null;
|
|
19
|
+
/** The running code's Local Build, or null when it was published or runs from source. */
|
|
20
|
+
export declare function localBuild(): LocalBuild | null;
|
package/dist/src/core/version.js
CHANGED
|
@@ -44,3 +44,29 @@ export function buildId(env = process.env) {
|
|
|
44
44
|
cachedBuild = `${packageVersion()}+${id}`;
|
|
45
45
|
return cachedBuild;
|
|
46
46
|
}
|
|
47
|
+
/** The Local Build fields of a `dist/build.json`, or null for a published build or anything unexpected. */
|
|
48
|
+
export function parseLocalBuild(data) {
|
|
49
|
+
if (!data || typeof data !== 'object')
|
|
50
|
+
return null;
|
|
51
|
+
const d = data;
|
|
52
|
+
if (d.local !== true)
|
|
53
|
+
return null;
|
|
54
|
+
return {
|
|
55
|
+
commit: typeof d.commit === 'string' ? d.commit : '',
|
|
56
|
+
dirty: d.dirty === true,
|
|
57
|
+
builtAt: typeof d.builtAt === 'string' ? d.builtAt : '',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
let cachedLocal;
|
|
61
|
+
/** The running code's Local Build, or null when it was published or runs from source. */
|
|
62
|
+
export function localBuild() {
|
|
63
|
+
if (cachedLocal !== undefined)
|
|
64
|
+
return cachedLocal;
|
|
65
|
+
try {
|
|
66
|
+
cachedLocal = parseLocalBuild(JSON.parse(readFileSync(new URL('../../build.json', import.meta.url), 'utf8')));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
cachedLocal = null;
|
|
70
|
+
}
|
|
71
|
+
return cachedLocal;
|
|
72
|
+
}
|
|
@@ -56,5 +56,7 @@ export interface LauncherSpec {
|
|
|
56
56
|
export declare function launcherScript(s: LauncherSpec): string;
|
|
57
57
|
export declare function infoPlist(version: string): string;
|
|
58
58
|
export declare function desktopEntry(launcher: string): string;
|
|
59
|
+
/** Whether a sideby desktop app is installed for this home: its main file exists and carries APP_MARKER. */
|
|
60
|
+
export declare function appInstalled(ctx: AppContext): Promise<boolean>;
|
|
59
61
|
export declare function installApp(ctx: AppContext, opts: InstallOptions): Promise<InstallResult>;
|
|
60
62
|
export declare function uninstallApp(ctx: AppContext): Promise<UninstallResult>;
|
package/dist/src/desktop/app.js
CHANGED
|
@@ -39,7 +39,7 @@ export function launcherScript(s) {
|
|
|
39
39
|
: `command -v notify-send >/dev/null 2>&1 && notify-send "sideby could not start" "Run sideby app install again in a terminal. Details: $LOG"`;
|
|
40
40
|
const lines = [
|
|
41
41
|
'#!/bin/sh',
|
|
42
|
-
`# ${APP_MARKER}: written by \`sideby app install
|
|
42
|
+
`# ${APP_MARKER}: written by \`sideby app install\` (\`sideby update\` runs it again). Run it yourself after upgrading Node.`,
|
|
43
43
|
`NODE=${shq(s.node)}`,
|
|
44
44
|
`ENTRY=${shq(s.entry)}`,
|
|
45
45
|
`URL=${shq(s.url ?? '')}`,
|
|
@@ -185,6 +185,38 @@ async function ownership(entry) {
|
|
|
185
185
|
}
|
|
186
186
|
return markerTest(entry)(data) ? { state: 'ours' } : foreign(entry, NOT_OURS);
|
|
187
187
|
}
|
|
188
|
+
/** The launcher script inside what install writes: the bundle's executable on macOS, the launcher file on Linux. */
|
|
189
|
+
function launcherPath(l, platform) {
|
|
190
|
+
return platform === 'darwin' ? join(l.primary, 'Contents', 'MacOS', APP_NAME) : l.linux.launcher;
|
|
191
|
+
}
|
|
192
|
+
/** The `URL='…'` an earlier install wrote into the launcher, when it is a valid http(s) address; never follows a link. */
|
|
193
|
+
async function recordedUrl(launcher) {
|
|
194
|
+
let text;
|
|
195
|
+
try {
|
|
196
|
+
text = (await readNoFollow(launcher)).toString('utf8');
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
if (text.split('\n', 2)[1]?.startsWith(`# ${APP_MARKER}: `) !== true)
|
|
202
|
+
return undefined;
|
|
203
|
+
const m = /^URL='((?:[^']|'\\'')*)'$/m.exec(text);
|
|
204
|
+
const value = m?.[1]?.replace(/'\\''/g, "'");
|
|
205
|
+
if (!value)
|
|
206
|
+
return undefined;
|
|
207
|
+
try {
|
|
208
|
+
return checkAppUrl(value);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** Whether a sideby desktop app is installed for this home: its main file exists and carries APP_MARKER. */
|
|
215
|
+
export async function appInstalled(ctx) {
|
|
216
|
+
if (ctx.platform !== 'darwin' && ctx.platform !== 'linux')
|
|
217
|
+
return false;
|
|
218
|
+
return (await ownership(layout(ctx, ctx.platform).primary)).state === 'ours';
|
|
219
|
+
}
|
|
188
220
|
/** Refuses to replace anything at `entry` that `sideby app install` did not write; true when it is ours. */
|
|
189
221
|
async function assertOwned(entry, verb) {
|
|
190
222
|
const o = await ownership(entry);
|
|
@@ -199,12 +231,14 @@ export async function installApp(ctx, opts) {
|
|
|
199
231
|
const run = ctx.run ?? defaultRunner;
|
|
200
232
|
if (!isAbsolute(opts.node) || !isAbsolute(opts.entry))
|
|
201
233
|
throw new UserError('node and the sideby entry must be absolute paths');
|
|
202
|
-
const url = opts.url === undefined ? undefined : checkAppUrl(opts.url);
|
|
203
234
|
const l = layout(ctx, platform);
|
|
204
235
|
let updated = false;
|
|
205
236
|
for (const e of l.entries)
|
|
206
237
|
if (await assertOwned(e, 'install'))
|
|
207
238
|
updated = true;
|
|
239
|
+
// Without --url, a reinstall (by hand or after `sideby update`) keeps the address an earlier --url recorded.
|
|
240
|
+
const kept = opts.url === undefined && updated ? await recordedUrl(launcherPath(l, platform)) : undefined;
|
|
241
|
+
const url = opts.url === undefined ? kept : checkAppUrl(opts.url);
|
|
208
242
|
const xdg = {};
|
|
209
243
|
for (const k of ['XDG_CONFIG_HOME', 'XDG_STATE_HOME']) {
|
|
210
244
|
const v = ctx.env[k];
|
|
@@ -274,8 +308,10 @@ export async function installApp(ctx, opts) {
|
|
|
274
308
|
}
|
|
275
309
|
if (url)
|
|
276
310
|
hints.push(`The app opens ${url}.`);
|
|
311
|
+
if (kept)
|
|
312
|
+
hints.push('Kept the address from the earlier install; to start the panel instead, run `sideby app uninstall`, then `sideby app install`.');
|
|
277
313
|
else
|
|
278
|
-
hints.push('The app starts the panel in the background and opens it; stop the panel with `sideby ui --stop`.', '
|
|
314
|
+
hints.push('The app starts the panel in the background and opens it; stop the panel with `sideby ui --stop`.', '`sideby update` refreshes the app; run `sideby app install` again after upgrading Node or installing sideby another way.');
|
|
279
315
|
if (/[/\\]_npx[/\\]/.test(opts.entry))
|
|
280
316
|
hints.push('sideby runs from the npx cache, which npm may clear; install it with `npm i -g sideby` and run this again.');
|
|
281
317
|
return {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Account } from '../types.ts';
|
|
2
|
+
export interface WebsiteDef {
|
|
3
|
+
/** The Family id. */
|
|
4
|
+
family: string;
|
|
5
|
+
/** Where "Open in browser" goes. */
|
|
6
|
+
url: string;
|
|
7
|
+
/**
|
|
8
|
+
* The Local Storage origin whose key names may hold `signInId`, and the reader of that id from the Host's login
|
|
9
|
+
* file. The id is compared in memory only; it is never shown, returned, logged or cached. Missing when the website
|
|
10
|
+
* keeps no id the Host also has (grok.com), so the Account can only be opened in a Browser Profile the user picks.
|
|
11
|
+
*/
|
|
12
|
+
match?: {
|
|
13
|
+
origin: string;
|
|
14
|
+
signInId(account: Account): Promise<string | undefined>;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** The website of a built-in Family id, if it has one. */
|
|
18
|
+
export declare function websiteDef(familyId: string): WebsiteDef | undefined;
|
|
19
|
+
/** Every website that can be matched, for the origins to read. */
|
|
20
|
+
export declare function matchedOrigins(): string[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { claudeSignInId } from './claude/browser.js';
|
|
2
|
+
import { codexSignInId } from './codex/browser.js';
|
|
3
|
+
const WEBSITES = new Map([
|
|
4
|
+
{
|
|
5
|
+
family: 'claude',
|
|
6
|
+
url: 'https://claude.ai',
|
|
7
|
+
match: { origin: 'https://claude.ai', signInId: claudeSignInId },
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
family: 'codex',
|
|
11
|
+
url: 'https://chatgpt.com',
|
|
12
|
+
match: { origin: 'https://chatgpt.com', signInId: codexSignInId },
|
|
13
|
+
},
|
|
14
|
+
{ family: 'grok', url: 'https://grok.com' },
|
|
15
|
+
].map((w) => [w.family, w]));
|
|
16
|
+
/** The website of a built-in Family id, if it has one. */
|
|
17
|
+
export function websiteDef(familyId) {
|
|
18
|
+
return WEBSITES.get(familyId);
|
|
19
|
+
}
|
|
20
|
+
/** Every website that can be matched, for the origins to read. */
|
|
21
|
+
export function matchedOrigins() {
|
|
22
|
+
return [...WEBSITES.values()].flatMap((w) => (w.match ? [w.match.origin] : []));
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// The Claude account id that claude.ai's Local Storage key names also carry (spec §3.24, ADR-0023).
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { claudeJsonOf } from './index.js';
|
|
4
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5
|
+
/** `oauthAccount.accountUuid` of `.claude.json`, only for matching; every other field is dropped at once. */
|
|
6
|
+
export async function claudeSignInId(account) {
|
|
7
|
+
try {
|
|
8
|
+
const id = JSON.parse(await readFile(claudeJsonOf(account), 'utf8'))?.oauthAccount?.accountUuid;
|
|
9
|
+
return typeof id === 'string' && UUID.test(id) ? id.toLowerCase() : undefined;
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Account } from '../../types.ts';
|
|
2
|
+
/**
|
|
3
|
+
* `chatgpt_user_id` (else `user_id`, as Codex itself falls back: codex-rs/login/src/token_data.rs) under the auth
|
|
4
|
+
* claim of `tokens.id_token`, only for matching. The payload is decoded as for the email (core/identity.ts); the
|
|
5
|
+
* token and every other claim are dropped at once. `tokens.account_id` is the workspace and is not used.
|
|
6
|
+
*/
|
|
7
|
+
export declare function codexSignInId(account: Account): Promise<string | undefined>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// The ChatGPT user id that chatgpt.com's Local Storage key names also carry (spec §3.24, ADR-0023).
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
const USER_ID = /^user-[A-Za-z0-9]{8,64}$/;
|
|
5
|
+
const AUTH_CLAIM = 'https://api.openai.com/auth';
|
|
6
|
+
/**
|
|
7
|
+
* `chatgpt_user_id` (else `user_id`, as Codex itself falls back: codex-rs/login/src/token_data.rs) under the auth
|
|
8
|
+
* claim of `tokens.id_token`, only for matching. The payload is decoded as for the email (core/identity.ts); the
|
|
9
|
+
* token and every other claim are dropped at once. `tokens.account_id` is the workspace and is not used.
|
|
10
|
+
*/
|
|
11
|
+
export async function codexSignInId(account) {
|
|
12
|
+
try {
|
|
13
|
+
const token = JSON.parse(await readFile(join(account.dir, 'auth.json'), 'utf8'))?.tokens?.id_token;
|
|
14
|
+
if (typeof token !== 'string')
|
|
15
|
+
return undefined;
|
|
16
|
+
const payload = token.split('.')[1];
|
|
17
|
+
if (!payload)
|
|
18
|
+
return undefined;
|
|
19
|
+
const auth = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'))?.[AUTH_CLAIM];
|
|
20
|
+
const id = auth?.chatgpt_user_id ?? auth?.user_id;
|
|
21
|
+
return typeof id === 'string' && USER_ID.test(id) ? id : undefined;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -17,10 +17,14 @@ export interface DesktopAppDef {
|
|
|
17
17
|
clearVars: readonly string[];
|
|
18
18
|
/** The first sign-in happens inside the app, through a URL callback another running instance may catch. */
|
|
19
19
|
signInInApp: boolean;
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Files of the App Data shared from the Main Account's default App Data as links (`mainPath` relative to HOME).
|
|
22
|
+
* With `key`, a real file the app made in place of the link is fine while that JSON key matches the main one.
|
|
23
|
+
*/
|
|
21
24
|
sharedFiles: readonly {
|
|
22
25
|
path: string;
|
|
23
26
|
mainPath: string;
|
|
27
|
+
key?: string;
|
|
24
28
|
}[];
|
|
25
29
|
}
|
|
26
30
|
/** The Desktop App of a built-in Family id, if it has one. */
|
|
@@ -28,6 +28,8 @@ export const piFamily = {
|
|
|
28
28
|
logo: BUILTIN_LOGOS.pi,
|
|
29
29
|
bin: 'pi',
|
|
30
30
|
installUrl: 'https://github.com/earendil-works/pi',
|
|
31
|
+
// The managed install stages and smoke-tests each release before switching to it; npm upgrades in place.
|
|
32
|
+
installCommand: 'curl -fsSL https://pi.dev/install.sh | sh',
|
|
31
33
|
selectVar: 'PI_CODING_AGENT_DIR',
|
|
32
34
|
secretFileMeansApi: false,
|
|
33
35
|
layout: { main: '.pi/agent', account: '.pi-<name>/agent' },
|