@krtclcdy/tersio 2.19.0 → 2.20.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/cli/common.js +13 -1
- package/cli/doctor.js +7 -7
- package/cli/install.js +28 -28
- package/extensions/ai-addons-updater/index.ts +377 -0
- package/extensions/caveman-session/index.ts +161 -0
- package/extensions/combo-toggle/index.ts +217 -0
- package/extensions/lib/utils.ts +162 -0
- package/extensions/rtk-session/index.ts +164 -0
- package/extensions/shared/carbon.ts +123 -0
- package/extensions/shared/mode-reinforcement.ts +29 -0
- package/extensions/shared/plugin-settings.ts +57 -0
- package/extensions/shared/pricing.ts +168 -0
- package/extensions/shared/rtk-gain.ts +105 -0
- package/extensions/shared/session-state.ts +201 -0
- package/extensions/shared/types.ts +85 -0
- package/extensions/shared/usage-ledger.ts +406 -0
- package/extensions/shared/usage-store.ts +325 -0
- package/extensions/tersio-commands/index.ts +110 -0
- package/package.json +19 -24
- package/extensions/ai-addons-updater/index.js +0 -331
- package/extensions/caveman-session/index.js +0 -152
- package/extensions/combo-toggle/index.js +0 -165
- package/extensions/lib/utils.js +0 -154
- package/extensions/rtk-session/index.js +0 -148
- package/extensions/shared/carbon.js +0 -88
- package/extensions/shared/mode-reinforcement.js +0 -26
- package/extensions/shared/plugin-settings.js +0 -51
- package/extensions/shared/pricing.js +0 -163
- package/extensions/shared/rtk-gain.js +0 -79
- package/extensions/shared/session-state.js +0 -177
- package/extensions/shared/types.js +0 -3
- package/extensions/shared/usage-ledger.js +0 -354
- package/extensions/shared/usage-store.js +0 -294
- package/extensions/tersio-commands/index.js +0 -102
package/cli/common.js
CHANGED
|
@@ -147,6 +147,13 @@ async function ensureExtensionInConfig(configPath, extensionPath, label, options
|
|
|
147
147
|
const line = ` - ${normalizedPath}`;
|
|
148
148
|
const raw = await readTextIfExists(configPath);
|
|
149
149
|
let lines = (raw || '').split('\n');
|
|
150
|
+
// Drop legacy compiled twins (same dir/name, .js): previous installs
|
|
151
|
+
// registered them, and OMP must never load both copies. Match on the
|
|
152
|
+
// trailing path so absolute and relative entries are both caught.
|
|
153
|
+
if (normalizedPath.endsWith('.ts')) {
|
|
154
|
+
const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
|
|
155
|
+
lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
|
|
156
|
+
}
|
|
150
157
|
if (lines.some((l) => l.includes(normalizedPath))) {
|
|
151
158
|
debug(`${label} already in config.yml`);
|
|
152
159
|
return false;
|
|
@@ -176,7 +183,12 @@ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterP
|
|
|
176
183
|
const normalizedAfterPath = afterPath.replace(/\\/g, '/');
|
|
177
184
|
const line = ` - ${normalizedPath}`;
|
|
178
185
|
const raw = await readTextIfExists(configPath);
|
|
179
|
-
|
|
186
|
+
let lines = (raw || '').split('\n');
|
|
187
|
+
// Same legacy-twin drop as ensureExtensionInConfig above.
|
|
188
|
+
if (normalizedPath.endsWith('.ts')) {
|
|
189
|
+
const legacyTail = `${normalizedPath.slice(0, -3)}.js`.split('/').slice(-2).join('/');
|
|
190
|
+
lines = lines.filter((l) => !l.trim().replace(/^\.\//, '').endsWith(legacyTail));
|
|
191
|
+
}
|
|
180
192
|
const existingIndex = lines.findIndex((entry) => entry.includes(normalizedPath));
|
|
181
193
|
const afterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
|
|
182
194
|
if (existingIndex !== -1 && afterIndex !== -1 && existingIndex === afterIndex + 1)
|
package/cli/doctor.js
CHANGED
|
@@ -19,13 +19,13 @@ async function runDoctor() {
|
|
|
19
19
|
// Ponytail
|
|
20
20
|
const ponytailPkg = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
21
21
|
const ponytailExt = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
|
|
22
|
-
const cavemanIndex = path.join(extDir, 'caveman-session', 'index.
|
|
22
|
+
const cavemanIndex = path.join(extDir, 'caveman-session', 'index.ts');
|
|
23
23
|
const cavemanRule = path.join(extDir, 'caveman-session', 'rule.md');
|
|
24
|
-
const rtkIndex = path.join(extDir, 'rtk-session', 'index.
|
|
25
|
-
const updaterIndex = path.join(extDir, 'ai-addons-updater', 'index.
|
|
26
|
-
const comboIndex = path.join(extDir, 'combo-toggle', 'index.
|
|
27
|
-
const tersioIndex = path.join(extDir, 'tersio-commands', 'index.
|
|
28
|
-
const modeReinforcement = path.join(extDir, 'shared', 'mode-reinforcement.
|
|
24
|
+
const rtkIndex = path.join(extDir, 'rtk-session', 'index.ts');
|
|
25
|
+
const updaterIndex = path.join(extDir, 'ai-addons-updater', 'index.ts');
|
|
26
|
+
const comboIndex = path.join(extDir, 'combo-toggle', 'index.ts');
|
|
27
|
+
const tersioIndex = path.join(extDir, 'tersio-commands', 'index.ts');
|
|
28
|
+
const modeReinforcement = path.join(extDir, 'shared', 'mode-reinforcement.ts');
|
|
29
29
|
const selfPkg = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json');
|
|
30
30
|
// Independent probes start concurrently; sections report in fixed order as
|
|
31
31
|
// their data settles. Every probe resolves instead of rejecting.
|
|
@@ -33,7 +33,7 @@ async function runDoctor() {
|
|
|
33
33
|
ompVersion: execP(OMP_BIN, ['--version']).then((r) => r.stdout.trim(), () => null),
|
|
34
34
|
agentEntries: fs.readdir(agentDir).catch(() => null),
|
|
35
35
|
extEntries: fs.readdir(extDir).catch(() => null),
|
|
36
|
-
sharedStateText: readTextIfExists(path.join(extDir, 'shared', 'session-state.
|
|
36
|
+
sharedStateText: readTextIfExists(path.join(extDir, 'shared', 'session-state.ts')),
|
|
37
37
|
configText: readTextIfExists(configPath),
|
|
38
38
|
ponytailPkgText: readTextIfExists(ponytailPkg),
|
|
39
39
|
ponytailExtText: readTextIfExists(ponytailExt),
|
package/cli/install.js
CHANGED
|
@@ -18,19 +18,19 @@ import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, findFile, httpsGet, ht
|
|
|
18
18
|
import { storedProfile, writePluginSettings } from './profile.js';
|
|
19
19
|
// Paths to extension source files (relative to this script)
|
|
20
20
|
const EXT_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'extensions');
|
|
21
|
-
const SHARED_SESSION_STATE = path.join(EXT_DIR, 'shared', 'session-state.
|
|
22
|
-
const CAVEMAN_INDEX = path.join(EXT_DIR, 'caveman-session', 'index.
|
|
23
|
-
const RTK_SESSION_INDEX = path.join(EXT_DIR, 'rtk-session', 'index.
|
|
24
|
-
const UPDATER_INDEX = path.join(EXT_DIR, 'ai-addons-updater', 'index.
|
|
25
|
-
const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, 'combo-toggle', 'index.
|
|
26
|
-
const TERSIO_COMMANDS_INDEX = path.join(EXT_DIR, 'tersio-commands', 'index.
|
|
27
|
-
const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, 'shared', 'mode-reinforcement.
|
|
28
|
-
const SHARED_TYPES = path.join(EXT_DIR, 'shared', 'types.
|
|
29
|
-
const LIB_UTILS = path.join(EXT_DIR, 'lib', 'utils.
|
|
30
|
-
const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.
|
|
31
|
-
const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.
|
|
32
|
-
const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.
|
|
33
|
-
const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.
|
|
21
|
+
const SHARED_SESSION_STATE = path.join(EXT_DIR, 'shared', 'session-state.ts');
|
|
22
|
+
const CAVEMAN_INDEX = path.join(EXT_DIR, 'caveman-session', 'index.ts');
|
|
23
|
+
const RTK_SESSION_INDEX = path.join(EXT_DIR, 'rtk-session', 'index.ts');
|
|
24
|
+
const UPDATER_INDEX = path.join(EXT_DIR, 'ai-addons-updater', 'index.ts');
|
|
25
|
+
const COMBO_TOGGLE_INDEX = path.join(EXT_DIR, 'combo-toggle', 'index.ts');
|
|
26
|
+
const TERSIO_COMMANDS_INDEX = path.join(EXT_DIR, 'tersio-commands', 'index.ts');
|
|
27
|
+
const MODE_REINFORCEMENT_INDEX = path.join(EXT_DIR, 'shared', 'mode-reinforcement.ts');
|
|
28
|
+
const SHARED_TYPES = path.join(EXT_DIR, 'shared', 'types.ts');
|
|
29
|
+
const LIB_UTILS = path.join(EXT_DIR, 'lib', 'utils.ts');
|
|
30
|
+
const SHARED_PLUGIN_SETTINGS = path.join(EXT_DIR, 'shared', 'plugin-settings.ts');
|
|
31
|
+
const SHARED_USAGE_LEDGER = path.join(EXT_DIR, 'shared', 'usage-ledger.ts');
|
|
32
|
+
const SHARED_PRICING = path.join(EXT_DIR, 'shared', 'pricing.ts');
|
|
33
|
+
const SHARED_CARBON = path.join(EXT_DIR, 'shared', 'carbon.ts');
|
|
34
34
|
const PONYTAIL_GITHUB_SPEC = 'github:DietrichGebert/ponytail';
|
|
35
35
|
const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
|
|
36
36
|
async function stepPonytail(pluginsDir, userDir, options) {
|
|
@@ -375,27 +375,27 @@ async function stepSharedSessionState(extDir, options) {
|
|
|
375
375
|
if (!options.quiet)
|
|
376
376
|
console.log(' Shared files — sync session bridge');
|
|
377
377
|
await copySources(extDir, [
|
|
378
|
-
[SHARED_SESSION_STATE, path.join('shared', 'session-state.
|
|
379
|
-
[SHARED_TYPES, path.join('shared', 'types.
|
|
380
|
-
[LIB_UTILS, path.join('lib', 'utils.
|
|
381
|
-
[SHARED_PLUGIN_SETTINGS, path.join('shared', 'plugin-settings.
|
|
382
|
-
[SHARED_USAGE_LEDGER, path.join('shared', 'usage-ledger.
|
|
383
|
-
[SHARED_PRICING, path.join('shared', 'pricing.
|
|
384
|
-
[SHARED_CARBON, path.join('shared', 'carbon.
|
|
378
|
+
[SHARED_SESSION_STATE, path.join('shared', 'session-state.ts')],
|
|
379
|
+
[SHARED_TYPES, path.join('shared', 'types.ts')],
|
|
380
|
+
[LIB_UTILS, path.join('lib', 'utils.ts')],
|
|
381
|
+
[SHARED_PLUGIN_SETTINGS, path.join('shared', 'plugin-settings.ts')],
|
|
382
|
+
[SHARED_USAGE_LEDGER, path.join('shared', 'usage-ledger.ts')],
|
|
383
|
+
[SHARED_PRICING, path.join('shared', 'pricing.ts')],
|
|
384
|
+
[SHARED_CARBON, path.join('shared', 'carbon.ts')],
|
|
385
385
|
], 'shared/session-state.js', options);
|
|
386
386
|
}
|
|
387
387
|
async function stepModeReinforcement(extDir, ponytailExtPath, options) {
|
|
388
388
|
if (!options.quiet)
|
|
389
389
|
console.log(' Session helpers — sync shared files');
|
|
390
|
-
const dest = path.join(extDir, 'shared', 'mode-reinforcement.
|
|
391
|
-
if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.
|
|
390
|
+
const dest = path.join(extDir, 'shared', 'mode-reinforcement.ts');
|
|
391
|
+
if (!await copySources(extDir, [[MODE_REINFORCEMENT_INDEX, path.join('shared', 'mode-reinforcement.ts')]], 'shared/mode-reinforcement.ts', options))
|
|
392
392
|
return;
|
|
393
393
|
await ensureExtensionAfterConfigEntry(path.join(path.dirname(extDir), 'config.yml'), dest, ponytailExtPath, 'mode reinforcement', options);
|
|
394
394
|
}
|
|
395
395
|
async function stepRtkSession(extDir, options) {
|
|
396
396
|
if (!options.quiet)
|
|
397
397
|
console.log(' RTK session — install session mode');
|
|
398
|
-
await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.
|
|
398
|
+
await copySources(extDir, [[RTK_SESSION_INDEX, path.join('rtk-session', 'index.ts')]], 'rtk-session/index.ts', options);
|
|
399
399
|
}
|
|
400
400
|
// One rule fetch serves the install; dry runs stay offline
|
|
401
401
|
// and fall back to the bundled rule to preview its destination.
|
|
@@ -427,21 +427,21 @@ async function stepCaveman(extDir, rule, options) {
|
|
|
427
427
|
else {
|
|
428
428
|
await writeIfChanged(ruleDest, rule, options);
|
|
429
429
|
}
|
|
430
|
-
await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.
|
|
430
|
+
await copySources(extDir, [[CAVEMAN_INDEX, path.join('caveman-session', 'index.ts')]], 'caveman-session/index.ts', options);
|
|
431
431
|
}
|
|
432
432
|
async function stepTersioCommands(extDir, options) {
|
|
433
433
|
if (!options.quiet)
|
|
434
434
|
console.log(' Tersio commands — install /tersio root command');
|
|
435
|
-
await copySources(extDir, [[TERSIO_COMMANDS_INDEX, path.join('tersio-commands', 'index.
|
|
435
|
+
await copySources(extDir, [[TERSIO_COMMANDS_INDEX, path.join('tersio-commands', 'index.ts')]], 'tersio-commands/index.ts', options);
|
|
436
436
|
}
|
|
437
437
|
async function stepUpdater(extDir, options) {
|
|
438
|
-
await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.
|
|
438
|
+
await copySources(extDir, [[UPDATER_INDEX, path.join('ai-addons-updater', 'index.ts')]], 'ai-addons-updater/index.ts', options);
|
|
439
439
|
}
|
|
440
440
|
async function stepCombo(extDir, options) {
|
|
441
441
|
if (!options.quiet)
|
|
442
442
|
console.log(' Combo — install preset switch');
|
|
443
|
-
const dest = path.join(extDir, 'combo-toggle', 'index.
|
|
444
|
-
if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.
|
|
443
|
+
const dest = path.join(extDir, 'combo-toggle', 'index.ts');
|
|
444
|
+
if (!await copySources(extDir, [[COMBO_TOGGLE_INDEX, path.join('combo-toggle', 'index.ts')]], 'combo-toggle/index.ts', options))
|
|
445
445
|
return;
|
|
446
446
|
// Auto-register combo in config.yml
|
|
447
447
|
const configPath = path.join(path.dirname(extDir), 'config.yml');
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// OMP extension: /ai-addons manual updater for Ponytail, RTK, Caveman.
|
|
2
|
+
// Built-in Node modules only. Default off; registers a single slash command.
|
|
3
|
+
// ponytail: `skipped: none` — semantics match one-liner: fetch + compare + run install.
|
|
4
|
+
// rtk: `skipped: signature verification` — checksums.txt ships only SHA256 of release assets; add sigchain when upstream publishes a signing key.
|
|
5
|
+
// caveman: `skipped: none` — exactly the ask: write rule.md, report old/new hash.
|
|
6
|
+
|
|
7
|
+
import { execFileSync } from 'node:child_process';
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import fs from 'node:fs/promises';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
CAVEMAN_REMOTE_RULE as CAVEMAN_REMOTE,
|
|
15
|
+
RTK_RELEASE_API,
|
|
16
|
+
RtkRelease,
|
|
17
|
+
fetchJson,
|
|
18
|
+
findFile,
|
|
19
|
+
httpsGet,
|
|
20
|
+
httpsDownload,
|
|
21
|
+
sha256Hex,
|
|
22
|
+
parseChecksum,
|
|
23
|
+
normalizeRtkVersion,
|
|
24
|
+
readTextIfExists,
|
|
25
|
+
rtkPlatformSpec,
|
|
26
|
+
} from '../lib/utils.ts';
|
|
27
|
+
|
|
28
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
29
|
+
const HOME = os.homedir();
|
|
30
|
+
|
|
31
|
+
const PONYTAIL_REMOTE = 'https://raw.githubusercontent.com/DietrichGebert/ponytail/main/package.json';
|
|
32
|
+
const PONYTAIL_NPM_SPEC = '@dietrichgebert/ponytail@latest';
|
|
33
|
+
const PONYTAIL_LOCAL = path.join(HOME, '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
34
|
+
const RTK_BINARY = path.join(HOME, '.bun', 'bin', IS_WINDOWS ? 'rtk.exe' : 'rtk');
|
|
35
|
+
const CAVEMAN_LOCAL = path.join(HOME, '.omp', 'agent', 'extensions', 'caveman-session', 'rule.md');
|
|
36
|
+
|
|
37
|
+
const RELOAD_MSG = 'Reminder: restart OMP (or reload extensions) for updates to take effect.';
|
|
38
|
+
|
|
39
|
+
// --- Types ---
|
|
40
|
+
|
|
41
|
+
interface AddonUpdaterCtx {
|
|
42
|
+
cwd?: string;
|
|
43
|
+
ui?: {
|
|
44
|
+
notify?: (message: string, level?: string) => void;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface AddonUpdaterPi {
|
|
49
|
+
setLabel?: (label: string) => void;
|
|
50
|
+
registerCommand?: (name: string, config: { description: string; handler: (args: string, ctx: AddonUpdaterCtx) => Promise<string> }) => void;
|
|
51
|
+
exec?: (cmd: string, args: string[], opts?: { cwd?: string }) => Promise<{ stdout: string; stderr: string; code: number }>;
|
|
52
|
+
cwd?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
type NotifyLevel = 'info' | 'warning';
|
|
56
|
+
|
|
57
|
+
function notify(ctx: AddonUpdaterCtx | undefined, msg: string, level: NotifyLevel): void {
|
|
58
|
+
ctx?.ui?.notify?.(String(msg), level);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function report(ctx: AddonUpdaterCtx | undefined, msg: string, level: NotifyLevel): string {
|
|
62
|
+
notify(ctx, msg, level);
|
|
63
|
+
return msg;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface AddonStatus {
|
|
67
|
+
text: string;
|
|
68
|
+
level: NotifyLevel;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function shortHash(text: string): string {
|
|
72
|
+
return sha256Hex(text).slice(0, 16);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function checkFailed(name: string, e: unknown): AddonStatus {
|
|
76
|
+
return { text: `${name} check failed: ${(e as Error).message}`, level: 'warning' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Single error-handling source for the three probes; messages unchanged.
|
|
80
|
+
function runCheck(name: string, probe: () => Promise<string>): Promise<AddonStatus> {
|
|
81
|
+
return probe().then((text) => ({ text, level: 'info' as const }), (e) => checkFailed(name, e));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check: no mutation. Probes run concurrently via checkAddons below.
|
|
85
|
+
function checkPonytail(): Promise<AddonStatus> {
|
|
86
|
+
return runCheck('Ponytail', async () => {
|
|
87
|
+
const remoteJson = await fetchJson<{ version?: string }>(PONYTAIL_REMOTE);
|
|
88
|
+
const localRaw = await readTextIfExists(PONYTAIL_LOCAL);
|
|
89
|
+
const localVer = localRaw ? (JSON.parse(localRaw) as { version?: string }).version ?? null : null;
|
|
90
|
+
const remoteVer = remoteJson.version;
|
|
91
|
+
const status = !localVer ? 'not installed'
|
|
92
|
+
: localVer === remoteVer ? 'up to date'
|
|
93
|
+
: 'update available';
|
|
94
|
+
return `Ponytail ${status}: local=${localVer || '—'} latest=${remoteVer}`;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function fetchRelease(): Promise<RtkRelease> {
|
|
99
|
+
return fetchJson<RtkRelease>(RTK_RELEASE_API);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function checkRtk(): Promise<AddonStatus> {
|
|
103
|
+
return runCheck('RTK', async () => {
|
|
104
|
+
const release = await fetchRelease();
|
|
105
|
+
const latestTag = release.tag_name || null;
|
|
106
|
+
let localVer: string | null = null;
|
|
107
|
+
try {
|
|
108
|
+
const out = execFileSync(RTK_BINARY, ['--version'], { encoding: 'utf8', windowsHide: true, shell: false, timeout: 10000 }) || '';
|
|
109
|
+
if (out) localVer = out.trim().split(/\r?\n/)[0];
|
|
110
|
+
} catch { localVer = null; }
|
|
111
|
+
const status = localVer === null ? 'not installed'
|
|
112
|
+
: normalizeRtkVersion(localVer) === normalizeRtkVersion(latestTag ?? undefined) ? 'up to date'
|
|
113
|
+
: 'update available';
|
|
114
|
+
return `RTK ${status}: local=${localVer || '—'} latest=${latestTag || '—'}`;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Caveman (rule.md)
|
|
119
|
+
function checkCaveman(): Promise<AddonStatus> {
|
|
120
|
+
return runCheck('Caveman', async () => {
|
|
121
|
+
const remote = await httpsGet(CAVEMAN_REMOTE);
|
|
122
|
+
const remoteHash = shortHash(remote);
|
|
123
|
+
const local = await readTextIfExists(CAVEMAN_LOCAL);
|
|
124
|
+
const localHash = local ? shortHash(local) : null;
|
|
125
|
+
const status = !local ? 'rule.md missing'
|
|
126
|
+
: localHash === remoteHash ? 'rule.md up to date'
|
|
127
|
+
: 'rule.md update available';
|
|
128
|
+
return `Caveman ${status}: local=${localHash || '—'} remote=${remoteHash}`;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export async function checkAddonsSummary(ctx: AddonUpdaterCtx): Promise<string> {
|
|
133
|
+
return checkAddons(ctx);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function runAddonUpdate(pi: { exec?: AddonUpdaterPi['exec'] }, ctx: AddonUpdaterCtx, target: string, dryRun = false): Promise<string> {
|
|
137
|
+
const results: string[] = [];
|
|
138
|
+
const updaters: Record<string, () => Promise<string>> = {
|
|
139
|
+
ponytail: () => updatePonytail(pi, ctx, dryRun),
|
|
140
|
+
rtk: () => updateRtk(ctx, dryRun),
|
|
141
|
+
caveman: () => updateCaveman(ctx, dryRun),
|
|
142
|
+
};
|
|
143
|
+
if (target === 'all') {
|
|
144
|
+
notify(ctx, `ai-addons update all${dryRun ? ' dry-run' : ''}: starting ponytail → rtk → caveman sequentially…`, 'info');
|
|
145
|
+
for (const name of ['ponytail', 'rtk', 'caveman']) results.push(await updaters[name]());
|
|
146
|
+
if (!dryRun) results.push(RELOAD_MSG);
|
|
147
|
+
notify(ctx, `ai-addons update all ${dryRun ? 'dry-run ' : ''}complete.${dryRun ? '' : ` ${RELOAD_MSG}`}`, 'info');
|
|
148
|
+
} else if (Object.hasOwn(updaters, target)) {
|
|
149
|
+
results.push(await updaters[target]());
|
|
150
|
+
} else {
|
|
151
|
+
const m = 'Usage: /ai-addons update <ponytail|rtk|caveman|all> [--dry-run]';
|
|
152
|
+
return report(ctx, m, 'warning');
|
|
153
|
+
}
|
|
154
|
+
return results.join('\n\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function checkAddons(ctx: AddonUpdaterCtx): Promise<string> {
|
|
158
|
+
const [ponytail, rtk, caveman] = await Promise.all([checkPonytail(), checkRtk(), checkCaveman()]);
|
|
159
|
+
const lines: string[] = [];
|
|
160
|
+
for (const status of [ponytail, rtk, caveman]) {
|
|
161
|
+
lines.push(status.text);
|
|
162
|
+
notify(ctx, status.text, status.level);
|
|
163
|
+
}
|
|
164
|
+
return lines.join('\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function updatePonytail(pi: AddonUpdaterPi, ctx: AddonUpdaterCtx, dryRun = false): Promise<string> {
|
|
168
|
+
const pluginsDir = path.join(HOME, '.omp', 'plugins');
|
|
169
|
+
if (dryRun) {
|
|
170
|
+
const m = `Ponytail dry-run: would run \`npm install ${PONYTAIL_NPM_SPEC} --save --no-audit --no-fund\` in ${pluginsDir}.`;
|
|
171
|
+
return report(ctx, m, 'info');
|
|
172
|
+
}
|
|
173
|
+
notify(ctx, 'Ponytail: ensuring plugin directory exists…', 'info');
|
|
174
|
+
try {
|
|
175
|
+
await fs.mkdir(pluginsDir, { recursive: true });
|
|
176
|
+
} catch (e) {
|
|
177
|
+
const m = `Ponytail update failed: failed to create ${pluginsDir}: ${(e as Error).message}`;
|
|
178
|
+
return report(ctx, m, 'warning');
|
|
179
|
+
}
|
|
180
|
+
if (!pi.exec) {
|
|
181
|
+
const m = 'Ponytail update failed: extension host does not provide exec';
|
|
182
|
+
return report(ctx, m, 'warning');
|
|
183
|
+
}
|
|
184
|
+
notify(ctx, 'Ponytail: running npm install…', 'info');
|
|
185
|
+
let out = '';
|
|
186
|
+
try {
|
|
187
|
+
const r = await pi.exec('npm', ['install', PONYTAIL_NPM_SPEC, '--save', '--no-audit', '--no-fund'], { cwd: pluginsDir });
|
|
188
|
+
out = [r.stdout, r.stderr].filter(Boolean).join('\n').trim();
|
|
189
|
+
if (r.code !== 0) throw new Error(r.stderr || `npm exited ${r.code}`);
|
|
190
|
+
} catch (e) {
|
|
191
|
+
const m = `Ponytail update failed: ${(e as Error).message}`;
|
|
192
|
+
return report(ctx, m, 'warning');
|
|
193
|
+
}
|
|
194
|
+
const m = `Ponytail update finished.${out ? `\n${out}` : ''}\n${RELOAD_MSG}`;
|
|
195
|
+
notify(ctx, 'Ponytail update finished. ' + RELOAD_MSG, 'info');
|
|
196
|
+
return m;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function updateRtk(ctx: AddonUpdaterCtx, dryRun = false): Promise<string> {
|
|
200
|
+
let release: RtkRelease;
|
|
201
|
+
try {
|
|
202
|
+
release = await fetchRelease();
|
|
203
|
+
} catch (e) {
|
|
204
|
+
const m = `RTK: cannot fetch release info: ${(e as Error).message}`;
|
|
205
|
+
return report(ctx, m, 'warning');
|
|
206
|
+
}
|
|
207
|
+
const tag = release.tag_name || 'unknown';
|
|
208
|
+
const assets = Array.isArray(release.assets) ? release.assets : [];
|
|
209
|
+
|
|
210
|
+
// Cross-platform asset selection (mirrors installer stepRtk)
|
|
211
|
+
const PLATFORM = process.platform;
|
|
212
|
+
const ARCH = process.arch;
|
|
213
|
+
const spec = rtkPlatformSpec(PLATFORM, ARCH);
|
|
214
|
+
if (!spec) {
|
|
215
|
+
const m = `RTK: unsupported platform ${PLATFORM}/${ARCH}`;
|
|
216
|
+
return report(ctx, m, 'warning');
|
|
217
|
+
}
|
|
218
|
+
const { triple: assetTriple, ext: assetExt, binary: binaryName } = spec;
|
|
219
|
+
|
|
220
|
+
const asset = assets.find((a) => a.name === `rtk-${assetTriple}${assetExt}`);
|
|
221
|
+
const checksAsset = assets.find((a) => a.name === 'checksums.txt');
|
|
222
|
+
if (!asset || !checksAsset) {
|
|
223
|
+
const m = `RTK: required assets not found in release ${tag} (need rtk-${assetTriple}${assetExt} and checksums.txt)`;
|
|
224
|
+
return report(ctx, m, 'warning');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (dryRun) {
|
|
228
|
+
const m = `RTK dry-run: would download ${asset.name} (${tag}), verify checksums.txt, and replace ${RTK_BINARY}.`;
|
|
229
|
+
return report(ctx, m, 'info');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const tmp = await fs.mkdtemp(path.join(os.tmpdir(), 'rtk-update-'));
|
|
233
|
+
const archivePath = path.join(tmp, asset.name);
|
|
234
|
+
const checksPath = path.join(tmp, 'checksums.txt');
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
notify(ctx, `RTK: downloading ${asset.name} (${tag})…`, 'info');
|
|
238
|
+
notify(ctx, 'RTK: downloading checksums.txt…', 'info');
|
|
239
|
+
await Promise.all([
|
|
240
|
+
httpsDownload(asset.browser_download_url, archivePath),
|
|
241
|
+
httpsDownload(checksAsset.browser_download_url, checksPath),
|
|
242
|
+
]);
|
|
243
|
+
// Verify SHA256 against checksums.txt
|
|
244
|
+
const checks = await fs.readFile(checksPath, 'utf8');
|
|
245
|
+
const expected = parseChecksum(checks, asset.name);
|
|
246
|
+
if (!expected) {
|
|
247
|
+
const m = `RTK: checksums.txt has no entry for ${asset.name}`;
|
|
248
|
+
return report(ctx, m, 'warning');
|
|
249
|
+
}
|
|
250
|
+
const archiveBuf = await fs.readFile(archivePath);
|
|
251
|
+
const actual = createHash('sha256').update(archiveBuf).digest('hex').toLowerCase();
|
|
252
|
+
if (actual !== expected) {
|
|
253
|
+
const m = `RTK: checksum mismatch! expected=${expected.slice(0, 12)}… actual=${actual.slice(0, 12)}…`;
|
|
254
|
+
return report(ctx, m, 'warning');
|
|
255
|
+
}
|
|
256
|
+
notify(ctx, 'RTK: checksum verified.', 'info');
|
|
257
|
+
|
|
258
|
+
// Extract by archive format
|
|
259
|
+
const extractDir = path.join(tmp, 'extracted');
|
|
260
|
+
await fs.mkdir(extractDir, { recursive: true });
|
|
261
|
+
|
|
262
|
+
if (asset.name.endsWith('.zip')) {
|
|
263
|
+
if (IS_WINDOWS) {
|
|
264
|
+
execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command',
|
|
265
|
+
`Expand-Archive -LiteralPath '${archivePath}' -DestinationPath '${extractDir}' -Force`],
|
|
266
|
+
{ encoding: 'utf8', windowsHide: true, shell: false });
|
|
267
|
+
} else {
|
|
268
|
+
execFileSync('unzip', [archivePath, '-d', extractDir], { encoding: 'utf8', shell: false });
|
|
269
|
+
}
|
|
270
|
+
} else if (asset.name.endsWith('.tar.gz') || asset.name.endsWith('.tgz')) {
|
|
271
|
+
try {
|
|
272
|
+
execFileSync('tar', ['xzf', archivePath, '-C', extractDir], { encoding: 'utf8', shell: false });
|
|
273
|
+
} catch (e) {
|
|
274
|
+
throw new Error(`tar could not extract ${asset.name}: ${(e as Error).message}`);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
throw new Error(`Unknown archive format: ${asset.name}`);
|
|
278
|
+
}
|
|
279
|
+
const rtkExtracted = await findFile(extractDir, binaryName);
|
|
280
|
+
if (!rtkExtracted) {
|
|
281
|
+
const m = `RTK: ${binaryName} not found in extracted archive`;
|
|
282
|
+
return report(ctx, m, 'warning');
|
|
283
|
+
}
|
|
284
|
+
await fs.mkdir(path.dirname(RTK_BINARY), { recursive: true });
|
|
285
|
+
const backupPath = `${RTK_BINARY}.bak`;
|
|
286
|
+
const backedUp = await fs.copyFile(RTK_BINARY, backupPath).then(() => true, () => false);
|
|
287
|
+
|
|
288
|
+
await fs.copyFile(rtkExtracted, RTK_BINARY);
|
|
289
|
+
|
|
290
|
+
// Set executable bit on Unix
|
|
291
|
+
if (!IS_WINDOWS) {
|
|
292
|
+
await fs.chmod(RTK_BINARY, 0o755);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
let versionOut = '';
|
|
296
|
+
try {
|
|
297
|
+
versionOut = execFileSync(RTK_BINARY, ['--version'], { encoding: 'utf8', windowsHide: true, shell: false, timeout: 10000 }).trim();
|
|
298
|
+
} catch (e) {
|
|
299
|
+
if (backedUp) await fs.copyFile(backupPath, RTK_BINARY);
|
|
300
|
+
throw new Error(`new ${binaryName} failed --version${backedUp ? '; restored backup' : ''}: ${(e as Error).message}`);
|
|
301
|
+
}
|
|
302
|
+
if (normalizeRtkVersion(versionOut) !== normalizeRtkVersion(tag)) {
|
|
303
|
+
if (backedUp) await fs.copyFile(backupPath, RTK_BINARY);
|
|
304
|
+
throw new Error(`new ${binaryName} reports ${versionOut}, expected ${tag}${backedUp ? '; restored backup' : ''}`);
|
|
305
|
+
}
|
|
306
|
+
const m = `RTK updated to ${tag} → ${RTK_BINARY}\nbackup=${backedUp ? backupPath : '—'}\n${RELOAD_MSG}`;
|
|
307
|
+
notify(ctx, 'RTK update finished. ' + RELOAD_MSG, 'info');
|
|
308
|
+
return m;
|
|
309
|
+
} catch (e) {
|
|
310
|
+
const m = `RTK update failed: ${(e as Error).message}`;
|
|
311
|
+
return report(ctx, m, 'warning');
|
|
312
|
+
} finally {
|
|
313
|
+
fs.rm(tmp, { recursive: true, force: true }).catch(() => { });
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function updateCaveman(ctx: AddonUpdaterCtx, dryRun = false): Promise<string> {
|
|
318
|
+
let remote: string;
|
|
319
|
+
try { remote = await httpsGet(CAVEMAN_REMOTE); }
|
|
320
|
+
catch (e) { return report(ctx, `Caveman update failed: ${(e as Error).message}`, 'warning'); }
|
|
321
|
+
|
|
322
|
+
const remoteHash = shortHash(remote);
|
|
323
|
+
const oldLocal = await readTextIfExists(CAVEMAN_LOCAL);
|
|
324
|
+
const oldHash = oldLocal ? shortHash(oldLocal) : null;
|
|
325
|
+
|
|
326
|
+
if (dryRun) {
|
|
327
|
+
const m = `Caveman dry-run: would write ${CAVEMAN_LOCAL}\nold=${oldHash || '—'} new=${remoteHash}.`;
|
|
328
|
+
return report(ctx, m, 'info');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
await fs.mkdir(path.dirname(CAVEMAN_LOCAL), { recursive: true });
|
|
333
|
+
const backupPath = `${CAVEMAN_LOCAL}.bak`;
|
|
334
|
+
if (oldLocal !== null) await fs.writeFile(backupPath, oldLocal, 'utf8');
|
|
335
|
+
await fs.writeFile(CAVEMAN_LOCAL, remote, 'utf8');
|
|
336
|
+
const written = await fs.readFile(CAVEMAN_LOCAL, 'utf8');
|
|
337
|
+
const writtenHash = shortHash(written);
|
|
338
|
+
if (writtenHash !== remoteHash) {
|
|
339
|
+
if (oldLocal !== null) await fs.writeFile(CAVEMAN_LOCAL, oldLocal, 'utf8');
|
|
340
|
+
throw new Error(`written hash ${writtenHash} did not match remote ${remoteHash}${oldLocal !== null ? '; restored backup' : ''}`);
|
|
341
|
+
}
|
|
342
|
+
const m = `Caveman rule.md updated → ${CAVEMAN_LOCAL}\nold=${oldHash || '—'} new=${remoteHash}\nbackup=${oldLocal !== null ? backupPath : '—'}\n${RELOAD_MSG}`;
|
|
343
|
+
notify(ctx, 'Caveman rule.md updated. ' + RELOAD_MSG, 'info');
|
|
344
|
+
return m;
|
|
345
|
+
} catch (e) {
|
|
346
|
+
const m = `Caveman update failed: ${(e as Error).message}`;
|
|
347
|
+
return report(ctx, m, 'warning');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export default function aiAddonsUpdaterExtension(pi: AddonUpdaterPi): void {
|
|
352
|
+
pi.setLabel?.('AI add-ons updater');
|
|
353
|
+
|
|
354
|
+
pi.registerCommand?.('ai-addons', {
|
|
355
|
+
description: 'Check or update AI add-ons (ponytail/rtk/caveman/all). Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]',
|
|
356
|
+
handler: async (args, ctx) => {
|
|
357
|
+
const arg = String(args || '').trim().toLowerCase();
|
|
358
|
+
const parts = arg.split(/\s+/).filter(Boolean);
|
|
359
|
+
const dryRun = parts.includes('--dry-run') || parts.includes('dry-run');
|
|
360
|
+
const cleanParts = parts.filter((p) => p !== '--dry-run' && p !== 'dry-run');
|
|
361
|
+
const sub = cleanParts[0];
|
|
362
|
+
|
|
363
|
+
if (sub === 'check' || sub === 'status') {
|
|
364
|
+
const summary = await checkAddons(ctx);
|
|
365
|
+
notify(ctx, 'ai-addons check complete.', 'info');
|
|
366
|
+
return summary;
|
|
367
|
+
}
|
|
368
|
+
if (sub === 'update' && cleanParts[1]) {
|
|
369
|
+
return runAddonUpdate(pi, ctx, cleanParts.slice(1).join(' '), dryRun);
|
|
370
|
+
}
|
|
371
|
+
const m = 'Usage: /ai-addons <check|status|update ponytail|rtk|caveman|all> [--dry-run]';
|
|
372
|
+
return report(ctx, m, 'warning');
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export { parseChecksum };
|