@krtclcdy/tersio 2.4.0 → 2.5.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 +252 -0
- package/cli/doctor.js +146 -0
- package/cli/install.js +610 -0
- package/cli/interactive.js +72 -0
- package/cli/uninstall.js +171 -0
- package/cli/update.js +202 -0
- package/extensions/caveman-session/index.js +5 -2
- package/extensions/combo-toggle/index.js +4 -3
- package/extensions/rtk-session/index.js +3 -2
- package/extensions/shared/session-state.js +14 -12
- package/package.json +3 -2
- package/tersio.js +7 -1342
package/cli/uninstall.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// cli/uninstall.ts — remove managed extensions, plugins, and binaries.
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { cancel as clackCancel, confirm as clackConfirm } from '@clack/prompts';
|
|
5
|
+
import { BUN_BIN_DIR, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_NAME, RTK_BINARY_NAME, dryRun, keepPonytail, removePonytail, removeRtk, yes, debug, writeConfigLines, } from './common.js';
|
|
6
|
+
import { ask, closeRL, tty } from './interactive.js';
|
|
7
|
+
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
8
|
+
// Read-modify-write a JSON file. mutate returns true when it changed
|
|
9
|
+
// something; no change (or unreadable file) means no output at all.
|
|
10
|
+
async function updateJsonFile(filePath, mutate, dryRunNote, writeNote, dryRun) {
|
|
11
|
+
const raw = await readTextIfExists(filePath);
|
|
12
|
+
if (!raw)
|
|
13
|
+
return;
|
|
14
|
+
let data;
|
|
15
|
+
try {
|
|
16
|
+
data = JSON.parse(raw);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
debug(`Could not parse ${filePath}`);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!mutate(data))
|
|
23
|
+
return;
|
|
24
|
+
if (dryRun) {
|
|
25
|
+
console.log(` [dry-run] ${dryRunNote}`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
29
|
+
console.log(` [write] ${writeNote}`);
|
|
30
|
+
}
|
|
31
|
+
function dropKey(section, key) {
|
|
32
|
+
if (!section || typeof section !== 'object')
|
|
33
|
+
return false;
|
|
34
|
+
const record = section;
|
|
35
|
+
if (!(key in record))
|
|
36
|
+
return false;
|
|
37
|
+
delete record[key];
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
async function removeUninstallTarget(target, shouldDryRun, recursive = true) {
|
|
41
|
+
try {
|
|
42
|
+
if (shouldDryRun) {
|
|
43
|
+
console.log(` [dry-run] would remove ${target}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (recursive)
|
|
47
|
+
await fs.rm(target, { recursive: true, force: true });
|
|
48
|
+
else
|
|
49
|
+
await fs.unlink(target);
|
|
50
|
+
console.log(` [rm] ${target}`);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
debug(`Could not remove ${target}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function runUninstall(options = {}) {
|
|
57
|
+
const shouldDryRun = options.dryRun ?? dryRun;
|
|
58
|
+
const confirmed = (options.yes ?? yes) || shouldDryRun;
|
|
59
|
+
// Ponytail ships with Tersio's presets, so a full uninstall removes it too;
|
|
60
|
+
// --keep-ponytail opts out and reinstall always preserves it.
|
|
61
|
+
const shouldRemovePonytail = options.removePonytail ?? (removePonytail || !keepPonytail);
|
|
62
|
+
const shouldRemoveRtk = options.removeRtk ?? removeRtk;
|
|
63
|
+
console.log('\n=== Tersio Uninstall ===\n');
|
|
64
|
+
const extDir = path.join(OMP_AGENT_DIR, 'extensions');
|
|
65
|
+
const configPath = path.join(OMP_AGENT_DIR, 'config.yml');
|
|
66
|
+
const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
|
|
67
|
+
const pluginsDir = OMP_PLUGINS_DIR;
|
|
68
|
+
const ponytailPkgDir = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail');
|
|
69
|
+
const targets = [
|
|
70
|
+
'caveman-session',
|
|
71
|
+
'rtk-session',
|
|
72
|
+
'ai-addons-updater',
|
|
73
|
+
'combo-toggle',
|
|
74
|
+
'shared',
|
|
75
|
+
// Only consumed by ai-addons-updater (removed above); otherwise orphaned.
|
|
76
|
+
'lib',
|
|
77
|
+
// Legacy always-on combo helper; imports shared/session-state.js, so it
|
|
78
|
+
// breaks with a module-not-found warning once the shared dir is removed.
|
|
79
|
+
'aaa-combo-boot',
|
|
80
|
+
].map((dir) => path.join(extDir, dir));
|
|
81
|
+
console.log('Will remove:');
|
|
82
|
+
for (const t of targets) {
|
|
83
|
+
console.log(` ${t}`);
|
|
84
|
+
}
|
|
85
|
+
if (shouldRemovePonytail) {
|
|
86
|
+
console.log(` ${ponytailPkgDir} (ponytail plugin package)`);
|
|
87
|
+
}
|
|
88
|
+
if (shouldRemoveRtk) {
|
|
89
|
+
console.log(` ${rtkBin}`);
|
|
90
|
+
}
|
|
91
|
+
if (!confirmed) {
|
|
92
|
+
if (tty()) {
|
|
93
|
+
closeRL();
|
|
94
|
+
const confirmedChoice = await clackConfirm({ message: 'Remove the listed Tersio files?', initialValue: false });
|
|
95
|
+
if (typeof confirmedChoice !== 'boolean') {
|
|
96
|
+
clackCancel('Aborted.');
|
|
97
|
+
closeRL();
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
if (!confirmedChoice) {
|
|
101
|
+
console.log('Aborted.');
|
|
102
|
+
closeRL();
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
const answer = await ask('\nProceed? [y/N]: ');
|
|
108
|
+
if (!answer.toLowerCase().startsWith('y')) {
|
|
109
|
+
console.log('Aborted.');
|
|
110
|
+
closeRL();
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Remove extension directories (independent paths, so concurrently).
|
|
116
|
+
await Promise.all(targets.map((t) => removeUninstallTarget(t, shouldDryRun)));
|
|
117
|
+
// Remove Combo and mode-reinforcement registrations; Ponytail only when requested.
|
|
118
|
+
const configRaw = await readTextIfExists(configPath);
|
|
119
|
+
if (configRaw) {
|
|
120
|
+
let lines = configRaw.split('\n');
|
|
121
|
+
const before = lines.length;
|
|
122
|
+
lines = lines.filter((l) => {
|
|
123
|
+
if (l.includes('combo-toggle') || l.includes('mode-reinforcement'))
|
|
124
|
+
return false;
|
|
125
|
+
if (shouldRemovePonytail && l.includes('ponytail') && l.includes('pi-extension'))
|
|
126
|
+
return false;
|
|
127
|
+
return true;
|
|
128
|
+
});
|
|
129
|
+
if (lines.length !== before) {
|
|
130
|
+
if (shouldDryRun)
|
|
131
|
+
console.log(` [dry-run] would remove ${before - lines.length} config.yml entries`);
|
|
132
|
+
else
|
|
133
|
+
await writeConfigLines(configPath, lines, ` [write] Updated config.yml (removed ${before - lines.length} entries)`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Remove the Ponytail plugin package the installer added (dep, files,
|
|
137
|
+
// lock entry); the config.yml entry was filtered above.
|
|
138
|
+
if (shouldRemovePonytail) {
|
|
139
|
+
const pluginsPkgPath = path.join(pluginsDir, 'package.json');
|
|
140
|
+
await updateJsonFile(pluginsPkgPath, (data) => dropKey(data.dependencies, '@dietrichgebert/ponytail'), `would remove @dietrichgebert/ponytail from ${pluginsPkgPath}`, 'Removed @dietrichgebert/ponytail from plugins/package.json', shouldDryRun);
|
|
141
|
+
try {
|
|
142
|
+
if (shouldDryRun)
|
|
143
|
+
console.log(` [dry-run] would remove ${ponytailPkgDir}`);
|
|
144
|
+
else {
|
|
145
|
+
await fs.rm(ponytailPkgDir, { recursive: true, force: true });
|
|
146
|
+
console.log(` [rm] ${ponytailPkgDir}`);
|
|
147
|
+
await fs.rm(path.dirname(ponytailPkgDir));
|
|
148
|
+
console.log(` [rm] ${path.dirname(ponytailPkgDir)} (empty scope)`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
debug('Could not remove ponytail package dir (scope may hold other packages)');
|
|
153
|
+
}
|
|
154
|
+
const lockPath = path.join(pluginsDir, 'omp-plugins.lock.json');
|
|
155
|
+
await updateJsonFile(lockPath, (data) => dropKey(data.plugins, '@dietrichgebert/ponytail'), `would remove @dietrichgebert/ponytail from ${lockPath}`, `Removed @dietrichgebert/ponytail from ${lockPath}`, shouldDryRun);
|
|
156
|
+
}
|
|
157
|
+
// Remove our plugin registration from ~/.omp/plugins
|
|
158
|
+
const pluginsPkgPath = path.join(pluginsDir, 'package.json');
|
|
159
|
+
await updateJsonFile(pluginsPkgPath, (data) => dropKey(data.dependencies, PACKAGE_NAME), `would remove ${PACKAGE_NAME} from ${pluginsPkgPath}`, `Removed ${PACKAGE_NAME} from plugins/package.json`, shouldDryRun);
|
|
160
|
+
const selfPluginDir = path.join(pluginsDir, 'node_modules', PACKAGE_NAME);
|
|
161
|
+
if ((await readTextIfExists(path.join(selfPluginDir, 'package.json'))) !== null) {
|
|
162
|
+
await removeUninstallTarget(selfPluginDir, shouldDryRun);
|
|
163
|
+
}
|
|
164
|
+
// Remove RTK binary if requested
|
|
165
|
+
if (shouldRemoveRtk)
|
|
166
|
+
await removeUninstallTarget(rtkBin, shouldDryRun, false);
|
|
167
|
+
console.log('\nDone. Restart OMP for changes to take effect.');
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
const SCOPE_MAP = { user: '1', project: '2', both: '3' };
|
|
171
|
+
export { runUninstall };
|
package/cli/update.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// cli/update.ts — update check, per-add-on plan, latest-installer run.
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { BUN_BIN_DIR, IS_WINDOWS, OMP_AGENT_DIR, OMP_PLUGINS_DIR, PACKAGE_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, SCOPE_MAP, dryRun, execP, parseJsonObject, scopeFlag, verbose, } from './common.js';
|
|
6
|
+
import { execNetwork } from './interactive.js';
|
|
7
|
+
import { readTextIfExists } from '../extensions/lib/utils.js';
|
|
8
|
+
import { CAVEMAN_REMOTE_RULE, RTK_RELEASE_API, fetchJson, httpsGet, normalizeRtkVersion, sha256Hex, } from '../extensions/lib/utils.js';
|
|
9
|
+
const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
|
|
10
|
+
function newerThan(a, b) {
|
|
11
|
+
const pa = a.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
|
|
12
|
+
const pb = b.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
|
|
13
|
+
for (let i = 0; i < 3; i++) {
|
|
14
|
+
if ((pa[i] || 0) !== (pb[i] || 0))
|
|
15
|
+
return (pa[i] || 0) > (pb[i] || 0);
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
async function latestPublishedVersion() {
|
|
20
|
+
try {
|
|
21
|
+
const args = IS_WINDOWS
|
|
22
|
+
? ['/d', '/s', '/c', 'npm', 'view', PACKAGE_NAME, 'version', '--prefer-online']
|
|
23
|
+
: ['view', PACKAGE_NAME, 'version', '--prefer-online'];
|
|
24
|
+
const r = await execNetwork('Checking npm registry for tersio updates', IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm', args, {
|
|
25
|
+
timeout: 4000,
|
|
26
|
+
maxBuffer: 1024 * 1024,
|
|
27
|
+
windowsHide: true,
|
|
28
|
+
shell: false,
|
|
29
|
+
});
|
|
30
|
+
return r.stdout.trim() || null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// Returns a newer published version, or null. Reads the registry at most once
|
|
37
|
+
// per TTL (cached under OMP_PLUGINS_DIR) and stays silent on failure.
|
|
38
|
+
async function checkForUpdate() {
|
|
39
|
+
const cachePath = path.join(OMP_PLUGINS_DIR, 'tersio-update-check.json');
|
|
40
|
+
const cached = parseJsonObject(await readTextIfExists(cachePath));
|
|
41
|
+
const cacheFresh = typeof cached?.lastCheck === 'number' && Date.now() - cached.lastCheck < UPDATE_CHECK_TTL_MS;
|
|
42
|
+
if (cacheFresh && cached?.latest)
|
|
43
|
+
return newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : null;
|
|
44
|
+
const latest = await latestPublishedVersion();
|
|
45
|
+
if (latest) {
|
|
46
|
+
await fs.mkdir(OMP_PLUGINS_DIR, { recursive: true }).catch(() => { });
|
|
47
|
+
await fs.writeFile(cachePath, JSON.stringify({ latest, lastCheck: Date.now() }) + '\n', 'utf8').catch(() => { });
|
|
48
|
+
return newerThan(latest, PACKAGE_VERSION) ? latest : null;
|
|
49
|
+
}
|
|
50
|
+
// Registry unreachable: fall back to a stale cache rather than staying silent.
|
|
51
|
+
return cached?.latest && newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : null;
|
|
52
|
+
}
|
|
53
|
+
// Race a probe against a timeout; slow or failing probes resolve null so the
|
|
54
|
+
// update plan stays best-effort and never blocks the update itself.
|
|
55
|
+
async function settle(work, ms) {
|
|
56
|
+
let timer;
|
|
57
|
+
try {
|
|
58
|
+
return await Promise.race([
|
|
59
|
+
work.then((value) => value, () => null),
|
|
60
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(null), ms); }),
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Plain `npm view <spec> version` with no spinner, for background probes.
|
|
68
|
+
async function npmViewVersion(spec, timeoutMs) {
|
|
69
|
+
try {
|
|
70
|
+
const exe = IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm';
|
|
71
|
+
const args = IS_WINDOWS
|
|
72
|
+
? ['/d', '/s', '/c', 'npm', 'view', spec, 'version', '--prefer-online']
|
|
73
|
+
: ['view', spec, 'version', '--prefer-online'];
|
|
74
|
+
const r = await execP(exe, args, { timeout: timeoutMs, maxBuffer: 1024 * 1024, windowsHide: true, shell: false });
|
|
75
|
+
return r.stdout.trim() || null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Run a child with inherited stdio for live output. Used when the child
|
|
82
|
+
// renders its own interactive UI (the delegated installer runs Clack
|
|
83
|
+
// spinners), which a concurrent outer spinner would corrupt into stray bars.
|
|
84
|
+
async function execInherit(cmd, args) {
|
|
85
|
+
const code = await new Promise((resolve, reject) => {
|
|
86
|
+
const child = spawn(cmd, args, { stdio: 'inherit' });
|
|
87
|
+
child.on('error', reject);
|
|
88
|
+
child.on('close', resolve);
|
|
89
|
+
});
|
|
90
|
+
if (code !== 0)
|
|
91
|
+
throw new Error(`${cmd} exited with code ${code}`);
|
|
92
|
+
}
|
|
93
|
+
// Current → latest per add-on. Every probe is capped and nullable; the plan
|
|
94
|
+
// prints `unknown` for anything unreachable and the update proceeds anyway.
|
|
95
|
+
async function probeUpdatePlan(cliLatest) {
|
|
96
|
+
const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
|
|
97
|
+
const ponytailPkg = path.join(OMP_PLUGINS_DIR, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
|
|
98
|
+
const ruleDest = path.join(OMP_AGENT_DIR, 'extensions', 'caveman-session', 'rule.md');
|
|
99
|
+
const [rtkLocal, rtkRelease, ruleLocalText, ruleRemoteText, ponytailLocalText, ponytailLatest] = await Promise.all([
|
|
100
|
+
settle(execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => normalizeRtkVersion(r.stdout.trim() || r.stderr.trim()) || null), 6000),
|
|
101
|
+
settle(fetchJson(RTK_RELEASE_API).then((r) => normalizeRtkVersion(r.tag_name) || null), 4000),
|
|
102
|
+
readTextIfExists(ruleDest),
|
|
103
|
+
settle(httpsGet(CAVEMAN_REMOTE_RULE), 4000),
|
|
104
|
+
readTextIfExists(ponytailPkg),
|
|
105
|
+
settle(npmViewVersion('@dietrichgebert/ponytail', 4000), 5000),
|
|
106
|
+
]);
|
|
107
|
+
const shortHash = (text) => text === null ? null : sha256Hex(text).slice(0, 8);
|
|
108
|
+
return {
|
|
109
|
+
cli: cliLatest,
|
|
110
|
+
rtk: [rtkLocal, rtkRelease],
|
|
111
|
+
rule: [shortHash(ruleLocalText), shortHash(ruleRemoteText)],
|
|
112
|
+
ponytail: [ponytailLocalText ? parseJsonObject(ponytailLocalText)?.version ?? null : null, ponytailLatest],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function planLine(name, current, latest) {
|
|
116
|
+
if (!current && !latest)
|
|
117
|
+
console.log(` ${name}: unknown`);
|
|
118
|
+
else if (!latest)
|
|
119
|
+
console.log(` ${name}: ${current} (latest unknown)`);
|
|
120
|
+
else if (current === latest)
|
|
121
|
+
console.log(` ${name}: ${current} (up to date)`);
|
|
122
|
+
else
|
|
123
|
+
console.log(` ${name}: ${current ?? 'missing'} → ${latest}`);
|
|
124
|
+
}
|
|
125
|
+
async function runLatestUpdate() {
|
|
126
|
+
const updateScope = scopeFlag || 'user';
|
|
127
|
+
if (!Object.hasOwn(SCOPE_MAP, updateScope)) {
|
|
128
|
+
console.error(`[fail] Invalid --scope: ${updateScope}. Use: user, project, both`);
|
|
129
|
+
process.exitCode = 1;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const forwardedArgs = ['--yes', '--scope', updateScope];
|
|
133
|
+
if (dryRun)
|
|
134
|
+
forwardedArgs.push('--dry-run');
|
|
135
|
+
if (verbose)
|
|
136
|
+
forwardedArgs.push('--verbose');
|
|
137
|
+
const npmCommand = IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm';
|
|
138
|
+
console.log('=== Updating Tersio ===');
|
|
139
|
+
// Resolve the target explicitly: `npm view --prefer-online` beats the local
|
|
140
|
+
// metadata cache, so a stale `@latest` can never pin an older release.
|
|
141
|
+
const cliLatest = await latestPublishedVersion();
|
|
142
|
+
const target = cliLatest && /^\d+\.\d+\.\d+$/.test(cliLatest) ? `@${cliLatest}` : '@latest';
|
|
143
|
+
// Current → latest per add-on. Best-effort; unreachable probes print as
|
|
144
|
+
// unknown and never block the update.
|
|
145
|
+
const plan = await probeUpdatePlan(cliLatest);
|
|
146
|
+
planLine('Tersio', PACKAGE_VERSION, plan.cli);
|
|
147
|
+
planLine('RTK', plan.rtk[0], plan.rtk[1]);
|
|
148
|
+
planLine('Caveman rule', plan.rule[0], plan.rule[1]);
|
|
149
|
+
planLine('Ponytail', plan.ponytail[0], plan.ponytail[1]);
|
|
150
|
+
console.log('');
|
|
151
|
+
// The npx delegation below only refreshes the OMP-side files; the globally
|
|
152
|
+
// installed CLI keeps its old version until npm -g runs. Refresh both.
|
|
153
|
+
const globalArgs = IS_WINDOWS
|
|
154
|
+
? ['/d', '/s', '/c', 'npm', 'install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online']
|
|
155
|
+
: ['install', '-g', `${PACKAGE_NAME}${target}`, '--no-audit', '--no-fund', '--prefer-online'];
|
|
156
|
+
if (dryRun) {
|
|
157
|
+
console.log(` [dry-run] would run: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
try {
|
|
161
|
+
const globalResult = await execNetwork(`Updating global Tersio CLI to ${PACKAGE_NAME}${target}`, npmCommand, globalArgs, {
|
|
162
|
+
timeout: 300000,
|
|
163
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
164
|
+
windowsHide: true,
|
|
165
|
+
shell: false,
|
|
166
|
+
});
|
|
167
|
+
if (globalResult.stdout)
|
|
168
|
+
process.stdout.write(globalResult.stdout);
|
|
169
|
+
if (globalResult.stderr)
|
|
170
|
+
process.stderr.write(globalResult.stderr);
|
|
171
|
+
console.log(` [ok] CLI updated to ${PACKAGE_NAME}${target}`);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
console.log(` [warn] Global CLI update skipped: ${e.message}`);
|
|
175
|
+
console.log(` [hint] Manual: npm install -g ${PACKAGE_NAME}${target} --no-audit --no-fund --prefer-online`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const npmArgs = [
|
|
179
|
+
'exec',
|
|
180
|
+
'--yes',
|
|
181
|
+
'--prefer-online',
|
|
182
|
+
`--package=${PACKAGE_NAME}${target}`,
|
|
183
|
+
'--',
|
|
184
|
+
PACKAGE_BIN,
|
|
185
|
+
'--apply-update',
|
|
186
|
+
...forwardedArgs,
|
|
187
|
+
];
|
|
188
|
+
const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
|
|
189
|
+
// Inherited stdio, no outer spinner: the delegated installer renders its
|
|
190
|
+
// own Clack UI live, which capture-then-dump would corrupt into stray bars.
|
|
191
|
+
console.log(` Running ${PACKAGE_NAME}${target} installer...`);
|
|
192
|
+
try {
|
|
193
|
+
await execInherit(npmCommand, npmCommandArgs);
|
|
194
|
+
console.log('\n=== Update complete ===');
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
const err = e;
|
|
198
|
+
console.error(`\n[fail] Could not run ${PACKAGE_NAME}${target}: ${err.message}`);
|
|
199
|
+
process.exitCode = 1;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export { checkForUpdate, runLatestUpdate };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
-
import { asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
|
|
4
|
+
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
|
|
5
5
|
import { readCavemanDefault } from '../shared/plugin-settings.js';
|
|
6
6
|
const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
const RULE_PATH = join(CAVERN_DIR, 'rule.md');
|
|
@@ -66,7 +66,10 @@ export default function cavemanSessionExtension(pi) {
|
|
|
66
66
|
pi.appendEntry?.('caveman-mode', { mode: normalized });
|
|
67
67
|
setSharedComboMode('caveman', normalized);
|
|
68
68
|
syncStatus(ctx);
|
|
69
|
-
const
|
|
69
|
+
const active = activeModesSummary(getSharedComboState());
|
|
70
|
+
const msg = normalized === 'off'
|
|
71
|
+
? `Caveman off. Active: ${active}.`
|
|
72
|
+
: `Caveman ${normalized} on — terse replies for this session. Active: ${active}.`;
|
|
70
73
|
ctx?.ui?.notify?.(msg, 'info');
|
|
71
74
|
return true;
|
|
72
75
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
-
import { asPromptArray, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, systemPromptIncludes, } from '../shared/session-state.js';
|
|
6
|
+
import { activeModesSummary, asPromptArray, COMBO_LEVELS, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, normalizeComboLevel, reconcileSharedComboEntries, sessionEntries, setSharedComboLevel, setSharedComboListener, systemPromptIncludes, } from '../shared/session-state.js';
|
|
7
7
|
import { readComboDefault } from '../shared/plugin-settings.js';
|
|
8
8
|
const require = createRequire(import.meta.url);
|
|
9
9
|
const PONYTAIL_FALLBACK_INTENSITY = {
|
|
@@ -110,7 +110,8 @@ export default function comboToggleExtension(pi) {
|
|
|
110
110
|
}
|
|
111
111
|
persistPreset(level);
|
|
112
112
|
useState(setSharedComboLevel(level), ctx);
|
|
113
|
-
|
|
113
|
+
const active = activeModesSummary(getSharedComboState());
|
|
114
|
+
ctx?.ui?.notify?.(level === 'off' ? `Combo off — all tersio modes inactive for this session. Active: ${active}.` : `Combo ${level} on — ${active} active for this session.`, 'info');
|
|
114
115
|
if (ctx?.reload)
|
|
115
116
|
await ctx.reload();
|
|
116
117
|
},
|
|
@@ -126,7 +127,7 @@ export default function comboToggleExtension(pi) {
|
|
|
126
127
|
if (fallback !== 'off') {
|
|
127
128
|
persistPreset(fallback);
|
|
128
129
|
useState(setSharedComboLevel(fallback), ctx);
|
|
129
|
-
ctx?.ui?.notify?.(`Combo default applied: ${fallback}
|
|
130
|
+
ctx?.ui?.notify?.(`Combo default applied: ${fallback} — ${activeModesSummary(getSharedComboState())} active for this session.`, 'info');
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
133
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
|
|
3
|
+
import { activeModesSummary, asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, paintStatusBar, reconcileSharedComboEntries, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
|
|
4
4
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
5
5
|
const DEFAULT_ENABLED = false;
|
|
6
6
|
function asBoolean(value) {
|
|
@@ -44,7 +44,8 @@ export default function rtkSessionExtension(pi) {
|
|
|
44
44
|
pi.appendEntry?.('rtk-mode', { enabled });
|
|
45
45
|
setSharedComboMode('rtk', enabled);
|
|
46
46
|
syncStatus(ctx);
|
|
47
|
-
|
|
47
|
+
const active = activeModesSummary(getSharedComboState());
|
|
48
|
+
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
48
49
|
}
|
|
49
50
|
pi.setLabel?.('RTK session toggle');
|
|
50
51
|
pi.registerCommand?.('rtk', {
|
|
@@ -36,9 +36,6 @@ function isPresetLevel(value) {
|
|
|
36
36
|
function isKnownLevel(value) {
|
|
37
37
|
return value === 'custom' || isPresetLevel(value);
|
|
38
38
|
}
|
|
39
|
-
function levelForIndividualModes(modes) {
|
|
40
|
-
return modes.caveman === 'off' && modes.rtk === 'off' && modes.ponytail === 'off' ? 'off' : 'custom';
|
|
41
|
-
}
|
|
42
39
|
function normalizedState(modes, level = deriveLevel(modes)) {
|
|
43
40
|
const state = {
|
|
44
41
|
caveman: normalizeMode('caveman', modes?.caveman) || 'off',
|
|
@@ -115,21 +112,21 @@ export function setSharedComboMode(name, value) {
|
|
|
115
112
|
if (!mode)
|
|
116
113
|
return getSharedComboState();
|
|
117
114
|
const modes = { ...getSharedComboState(), [name]: mode };
|
|
118
|
-
|
|
115
|
+
// Derive the level from the final triplet so a redundant same-value write
|
|
116
|
+
// (entry replay on resume, upstream ponytail re-affirming its mode) cannot
|
|
117
|
+
// strand a matching preset at 'custom' and drop the combo bar.
|
|
118
|
+
return publish(normalizedState(modes));
|
|
119
119
|
}
|
|
120
120
|
export function reconcileSharedComboEntries(entries) {
|
|
121
121
|
let modes = { ...COMBO_LEVELS.off };
|
|
122
|
-
let level = 'off';
|
|
123
122
|
if (Array.isArray(entries)) {
|
|
124
123
|
for (const entry of entries) {
|
|
125
124
|
if (entry?.type !== 'custom')
|
|
126
125
|
continue;
|
|
127
126
|
if (entry.customType === 'combo-level') {
|
|
128
127
|
const preset = normalizeComboLevel(entry?.data?.level);
|
|
129
|
-
if (preset)
|
|
128
|
+
if (preset)
|
|
130
129
|
modes = { ...COMBO_LEVELS[preset] };
|
|
131
|
-
level = preset;
|
|
132
|
-
}
|
|
133
130
|
continue;
|
|
134
131
|
}
|
|
135
132
|
const name = MODE_ENTRY_TYPES[entry.customType ?? ''] ?? null;
|
|
@@ -137,13 +134,18 @@ export function reconcileSharedComboEntries(entries) {
|
|
|
137
134
|
continue;
|
|
138
135
|
const value = name === 'rtk' ? entry?.data?.enabled : entry?.data?.mode;
|
|
139
136
|
const mode = normalizeMode(name, value);
|
|
140
|
-
if (mode)
|
|
137
|
+
if (mode)
|
|
141
138
|
modes[name] = mode;
|
|
142
|
-
level = levelForIndividualModes(modes);
|
|
143
|
-
}
|
|
144
139
|
}
|
|
145
140
|
}
|
|
146
|
-
|
|
141
|
+
// The level always reflects the final mode triplet, never the write order:
|
|
142
|
+
// a preset entry followed by the same values reconciles back to the preset.
|
|
143
|
+
return publish(normalizedState(modes));
|
|
144
|
+
}
|
|
145
|
+
// One-line session-wide mode summary for change confirmations, e.g.
|
|
146
|
+
// `caveman=ULTRA, rtk=ON, ponytail=ULTRA`.
|
|
147
|
+
export function activeModesSummary(state) {
|
|
148
|
+
return `caveman=${state.caveman.toUpperCase()}, rtk=${state.rtk.toUpperCase()}, ponytail=${state.ponytail.toUpperCase()}`;
|
|
147
149
|
}
|
|
148
150
|
export function setSharedComboListener(listener) {
|
|
149
151
|
bridge().listener = typeof listener === 'function' ? listener : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@krtclcdy/tersio",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"description": "✂️ Token-saving OMP add-ons: terse Caveman replies, compact RTK shell output, lean Ponytail code calls — one-command combo presets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"omp",
|
|
@@ -107,10 +107,11 @@
|
|
|
107
107
|
},
|
|
108
108
|
"files": [
|
|
109
109
|
"tersio.js",
|
|
110
|
+
"cli/**/*.js",
|
|
110
111
|
"extensions/**/*.js"
|
|
111
112
|
],
|
|
112
113
|
"scripts": {
|
|
113
|
-
"clean": "rm -f tersio.js extensions/**/*.js",
|
|
114
|
+
"clean": "rm -f tersio.js cli/*.js extensions/**/*.js",
|
|
114
115
|
"build": "tsc",
|
|
115
116
|
"prepublishOnly": "npm run build",
|
|
116
117
|
"check": "tsc --noEmit",
|