@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 ADDED
@@ -0,0 +1,252 @@
1
+ // cli/common.ts — shared foundation: CLI flags, paths, exec, file/config helpers.
2
+ import { execFile } from 'node:child_process';
3
+ import { promises as fs } from 'node:fs';
4
+ import path from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { createRequire } from 'node:module';
7
+ import { readTextIfExists } from '../extensions/lib/utils.js';
8
+ const IS_WINDOWS = process.platform === 'win32';
9
+ const RTK_BINARY_NAME = IS_WINDOWS ? 'rtk.exe' : 'rtk';
10
+ const HOME = process.env.HOME || process.env.USERPROFILE || '';
11
+ const OMP_AGENT_DIR = path.join(HOME, '.omp', 'agent');
12
+ const OMP_PLUGINS_DIR = path.join(HOME, '.omp', 'plugins');
13
+ const BUN_BIN_DIR = path.join(HOME, '.bun', 'bin');
14
+ const OMP_BIN = IS_WINDOWS ? 'omp.cmd' : 'omp';
15
+ const PACKAGE_NAME = '@krtclcdy/tersio';
16
+ const PACKAGE_BIN = 'tersio';
17
+ const { version: PACKAGE_VERSION } = createRequire(import.meta.url)('../package.json');
18
+ // Session-start mode defaults for fresh installs.
19
+ const CAVEMAN_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'wenyan']);
20
+ const PONYTAIL_DEFAULTS = new Set(['off', 'lite', 'full', 'ultra', 'review']);
21
+ const RTK_DEFAULTS = new Set(['on', 'off']);
22
+ // ponytail: Combo preset implies all three modes. Mirrors COMBO_LEVELS in
23
+ // extensions/shared/session-state.ts (rtk as boolean here). Single source.
24
+ const COMBO_PRESET_MODES = {
25
+ off: { caveman: 'off', rtk: false, ponytail: 'off' },
26
+ medium: { caveman: 'lite', rtk: true, ponytail: 'lite' },
27
+ balanced: { caveman: 'full', rtk: true, ponytail: 'full' },
28
+ max: { caveman: 'ultra', rtk: true, ponytail: 'ultra' },
29
+ };
30
+ const COMBO_DEFAULTS = new Set(Object.keys(COMBO_PRESET_MODES));
31
+ function parseEnum(value, valid, flag) {
32
+ if (value === undefined)
33
+ return undefined;
34
+ const v = value.trim().toLowerCase();
35
+ if (!valid.has(v)) {
36
+ console.error(`[fail] Invalid ${flag}: ${value}. Valid: ${[...valid].join(', ')}`);
37
+ process.exit(1);
38
+ }
39
+ return v;
40
+ }
41
+ function flagValue(name) {
42
+ const i = args.indexOf(name);
43
+ if (i !== -1)
44
+ return args[i + 1];
45
+ const prefix = `${name}=`;
46
+ return args.find((arg) => arg.startsWith(prefix))?.slice(prefix.length);
47
+ }
48
+ // --- CLI flags ---
49
+ const COMMANDS = new Set(['install', 'update', 'reinstall', 'doctor', 'uninstall', 'version', 'help']);
50
+ const args = process.argv.slice(2);
51
+ const commandArg = args.find((arg, index) => !arg.startsWith('-') && args[index - 1] !== '--scope');
52
+ const command = commandArg?.toLowerCase() || null;
53
+ const unknownCommand = command !== null && !COMMANDS.has(command);
54
+ const install = command === 'install';
55
+ const update = command === 'update';
56
+ const reinstall = command === 'reinstall';
57
+ const showVersion = command === 'version' || args.includes('--version') || args.includes('-v');
58
+ const showHelp = command === 'help' || args.includes('--help') || args.includes('-h');
59
+ const applyUpdate = args.includes('--apply-update');
60
+ const dryRun = args.includes('--dry-run');
61
+ const yes = args.includes('--yes') || args.includes('-y') || install || update || reinstall || applyUpdate;
62
+ const verbose = args.includes('--verbose');
63
+ const doctor = command === 'doctor' || args.includes('--doctor');
64
+ const uninstall = command === 'uninstall' || args.includes('--uninstall');
65
+ const removePonytail = args.includes('--remove-ponytail');
66
+ const keepPonytail = args.includes('--keep-ponytail');
67
+ const removeRtk = args.includes('--remove-rtk');
68
+ const scopeFlag = flagValue('--scope')?.toLowerCase() ?? null;
69
+ // --- Profile selection (session-start mode defaults) ---
70
+ const comboDefaultFlag = parseEnum(flagValue('--combo-default'), COMBO_DEFAULTS, '--combo-default');
71
+ const cavemanDefaultFlag = parseEnum(flagValue('--caveman-default'), CAVEMAN_DEFAULTS, '--caveman-default');
72
+ const ponytailDefaultFlag = parseEnum(flagValue('--ponytail-default'), PONYTAIL_DEFAULTS, '--ponytail-default');
73
+ const rtkDefaultFlag = parseEnum(flagValue('--rtk-default'), RTK_DEFAULTS, '--rtk-default');
74
+ const profileFlagsGiven = [comboDefaultFlag, cavemanDefaultFlag, rtkDefaultFlag, ponytailDefaultFlag]
75
+ .some((flag) => flag !== undefined);
76
+ function debug(...a) {
77
+ if (verbose)
78
+ console.log(' [debug]', ...a);
79
+ }
80
+ const execFileP = promisify(execFile);
81
+ async function execP(cmd, args, opts = {}) {
82
+ return execFileP(cmd, args, {
83
+ timeout: opts.timeout || 120000,
84
+ encoding: 'utf8',
85
+ ...opts,
86
+ });
87
+ }
88
+ async function writeIfChanged(dest, content, options = {}) {
89
+ const existing = await readTextIfExists(dest);
90
+ if (existing === content) {
91
+ debug(`${dest} already up to date`);
92
+ return false;
93
+ }
94
+ if (options.dryRun) {
95
+ console.log(` [dry-run] would write ${dest}`);
96
+ return true;
97
+ }
98
+ await fs.mkdir(path.dirname(dest), { recursive: true });
99
+ if (existing !== null) {
100
+ await fs.copyFile(dest, `${dest}.bak`);
101
+ debug(`${path.basename(dest)} → ${path.basename(dest)}.bak`);
102
+ }
103
+ await fs.writeFile(dest, content, 'utf8');
104
+ console.log(` [write] ${dest}`);
105
+ return true;
106
+ }
107
+ // omp ships 'extensions: null'; appending list items under a null scalar breaks
108
+ // YAML parsing. Normalize null/~/[]/empty to a mapping key first.
109
+ const EXTENSIONS_KEY_RE = /^\s*extensions\s*:/i;
110
+ const EXTENSIONS_NULL_RE = /^\s*extensions\s*:\s*(?:\[\s*\]|null|~)?\s*$/i;
111
+ function normalizeExtensionsKey(lines) {
112
+ const idx = lines.findIndex((l) => EXTENSIONS_NULL_RE.test(l));
113
+ if (idx === -1)
114
+ return false;
115
+ lines[idx] = 'extensions:';
116
+ return true;
117
+ }
118
+ async function writeConfigLines(configPath, lines, logMsg) {
119
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
120
+ await fs.copyFile(configPath, `${configPath}.bak`).catch(() => { });
121
+ await fs.writeFile(configPath, lines.join('\n'), 'utf8');
122
+ console.log(logMsg);
123
+ }
124
+ async function ensureExtensionInConfig(configPath, extensionPath, label, options = {}) {
125
+ const normalizedPath = extensionPath.replace(/\\/g, '/');
126
+ const line = ` - ${normalizedPath}`;
127
+ const raw = await readTextIfExists(configPath);
128
+ let lines = (raw || '').split('\n');
129
+ if (lines.some((l) => l.includes(normalizedPath))) {
130
+ debug(`${label} already in config.yml`);
131
+ return false;
132
+ }
133
+ const extLineIdx = lines.findIndex((l) => EXTENSIONS_KEY_RE.test(l));
134
+ if (options.dryRun) {
135
+ console.log(` [dry-run] would add ${label} to config.yml: ${normalizedPath}`);
136
+ return true;
137
+ }
138
+ if (normalizeExtensionsKey(lines)) {
139
+ lines.splice(extLineIdx + 1, 0, line, '');
140
+ }
141
+ else if (extLineIdx === -1) {
142
+ lines.push('extensions:');
143
+ lines.push(line);
144
+ lines.push('');
145
+ }
146
+ else {
147
+ lines.splice(extLineIdx + 1, 0, line);
148
+ }
149
+ await writeConfigLines(configPath, lines, ` [write] Added ${label} to config.yml`);
150
+ return true;
151
+ }
152
+ async function ensureExtensionAfterConfigEntry(configPath, extensionPath, afterPath, label, options = {}) {
153
+ const normalizedPath = extensionPath.replace(/\\/g, '/');
154
+ const normalizedAfterPath = afterPath.replace(/\\/g, '/');
155
+ const line = ` - ${normalizedPath}`;
156
+ const raw = await readTextIfExists(configPath);
157
+ const lines = (raw || '').split('\n');
158
+ const existingIndex = lines.findIndex((entry) => entry.includes(normalizedPath));
159
+ const afterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
160
+ if (existingIndex !== -1 && afterIndex !== -1 && existingIndex === afterIndex + 1)
161
+ return false;
162
+ if (options.dryRun) {
163
+ console.log(` [dry-run] would place ${label} after Ponytail in config.yml: ${normalizedPath}`);
164
+ return true;
165
+ }
166
+ if (existingIndex !== -1)
167
+ lines.splice(existingIndex, 1);
168
+ const refreshedAfterIndex = lines.findIndex((entry) => entry.includes(normalizedAfterPath));
169
+ if (refreshedAfterIndex !== -1) {
170
+ lines.splice(refreshedAfterIndex + 1, 0, line);
171
+ }
172
+ else {
173
+ const extensionsIndex = lines.findIndex((entry) => EXTENSIONS_KEY_RE.test(entry));
174
+ if (extensionsIndex === -1) {
175
+ lines.push('extensions:', line, '');
176
+ }
177
+ else {
178
+ normalizeExtensionsKey(lines);
179
+ lines.splice(extensionsIndex + 1, 0, line);
180
+ }
181
+ }
182
+ await writeConfigLines(configPath, lines, ` [write] Placed ${label} after Ponytail in config.yml`);
183
+ return true;
184
+ }
185
+ function readPonytailConfig() {
186
+ const dir = process.env.XDG_CONFIG_HOME
187
+ ? path.join(process.env.XDG_CONFIG_HOME, 'ponytail')
188
+ : path.join(HOME, '.config', 'ponytail');
189
+ return { dir, path: path.join(dir, 'config.json') };
190
+ }
191
+ function parseJsonObject(text) {
192
+ if (!text)
193
+ return null;
194
+ try {
195
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, ''));
196
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
197
+ return null;
198
+ return parsed;
199
+ }
200
+ catch {
201
+ return null;
202
+ }
203
+ }
204
+ function parsePonytailConfig(existing) {
205
+ return parseJsonObject(existing) ?? {};
206
+ }
207
+ async function patchPonytailConfig(apply, dryRunNote, writeNote, options = {}) {
208
+ const config = readPonytailConfig();
209
+ if (options.dryRun) {
210
+ console.log(` [dry-run] ${dryRunNote} in ${config.path}`);
211
+ return;
212
+ }
213
+ const cfg = parsePonytailConfig(await readTextIfExists(config.path));
214
+ if (!apply(cfg)) {
215
+ debug('Ponytail config already up to date');
216
+ return;
217
+ }
218
+ await fs.mkdir(config.dir, { recursive: true });
219
+ await fs.writeFile(config.path, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
220
+ console.log(` [write] ${writeNote} in ${config.path}`);
221
+ }
222
+ async function ensurePonytailConfigValue(key, value, options = {}) {
223
+ await patchPonytailConfig((cfg) => {
224
+ if (cfg[key] === value)
225
+ return false;
226
+ cfg[key] = value;
227
+ return true;
228
+ }, `would set Ponytail ${key}=${value}`, `Set Ponytail ${key}=${value}`, options);
229
+ }
230
+ // Read plugins/package.json tolerantly; corrupt or missing files start fresh
231
+ // with installer-managed defaults.
232
+ async function readPluginsPackage(pkgPath) {
233
+ const pkg = parseJsonObject(await readTextIfExists(pkgPath)) ?? {};
234
+ return { ...pkg, name: pkg.name || 'omp-plugins', private: true, dependencies: pkg.dependencies || {} };
235
+ }
236
+ const SCOPE_MAP = { user: '1', project: '2', both: '3' };
237
+ function relTime(ageMs) {
238
+ const mins = Math.floor(ageMs / 60000);
239
+ if (mins < 1)
240
+ return 'just now';
241
+ if (mins < 60)
242
+ return `${mins} min${mins === 1 ? '' : 's'} ago`;
243
+ const hours = Math.floor(mins / 60);
244
+ if (hours < 24)
245
+ return `${hours} hour${hours === 1 ? '' : 's'} ago`;
246
+ const days = Math.floor(hours / 24);
247
+ if (days < 14)
248
+ return `${days} day${days === 1 ? '' : 's'} ago`;
249
+ const weeks = Math.floor(days / 7);
250
+ return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
251
+ }
252
+ export { IS_WINDOWS, RTK_BINARY_NAME, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, BUN_BIN_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_BIN, PACKAGE_VERSION, CAVEMAN_DEFAULTS, PONYTAIL_DEFAULTS, RTK_DEFAULTS, COMBO_PRESET_MODES, COMBO_DEFAULTS, parseEnum, flagValue, COMMANDS, commandArg, command, unknownCommand, install, update, reinstall, showVersion, showHelp, applyUpdate, dryRun, yes, verbose, doctor, uninstall, removePonytail, keepPonytail, removeRtk, scopeFlag, comboDefaultFlag, cavemanDefaultFlag, ponytailDefaultFlag, rtkDefaultFlag, profileFlagsGiven, debug, execFileP, execP, writeIfChanged, normalizeExtensionsKey, EXTENSIONS_KEY_RE, writeConfigLines, ensureExtensionInConfig, ensureExtensionAfterConfigEntry, readPonytailConfig, parseJsonObject, parsePonytailConfig, patchPonytailConfig, ensurePonytailConfigValue, readPluginsPackage, SCOPE_MAP, relTime, };
package/cli/doctor.js ADDED
@@ -0,0 +1,146 @@
1
+ // cli/doctor.ts — installation health checks.
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { BUN_BIN_DIR, HOME, OMP_AGENT_DIR, OMP_PLUGINS_DIR, OMP_BIN, PACKAGE_NAME, PACKAGE_VERSION, RTK_BINARY_NAME, execP, parseJsonObject, relTime, } from './common.js';
5
+ import { runInteractivePhase } from './interactive.js';
6
+ import { checkForUpdate } from './update.js';
7
+ import { readTextIfExists } from '../extensions/lib/utils.js';
8
+ async function runDoctor() {
9
+ console.log('\n=== Tersio Doctor ===');
10
+ // Directories
11
+ const agentDir = OMP_AGENT_DIR;
12
+ const extDir = path.join(agentDir, 'extensions');
13
+ const configPath = path.join(agentDir, 'config.yml');
14
+ const pluginsDir = OMP_PLUGINS_DIR;
15
+ const rtkBin = path.join(BUN_BIN_DIR, RTK_BINARY_NAME);
16
+ // Ponytail
17
+ const ponytailPkg = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'package.json');
18
+ const ponytailExt = path.join(pluginsDir, 'node_modules', '@dietrichgebert', 'ponytail', 'pi-extension', 'index.js');
19
+ const cavemanIndex = path.join(extDir, 'caveman-session', 'index.js');
20
+ const cavemanRule = path.join(extDir, 'caveman-session', 'rule.md');
21
+ const rtkIndex = path.join(extDir, 'rtk-session', 'index.js');
22
+ const updaterIndex = path.join(extDir, 'ai-addons-updater', 'index.js');
23
+ const comboIndex = path.join(extDir, 'combo-toggle', 'index.js');
24
+ const modeReinforcement = path.join(extDir, 'shared', 'mode-reinforcement.js');
25
+ const selfPkg = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json');
26
+ // Independent probes start concurrently; sections report in fixed order as
27
+ // their data settles. Every probe resolves instead of rejecting.
28
+ const probes = {
29
+ ompVersion: execP(OMP_BIN, ['--version']).then((r) => r.stdout.trim(), () => null),
30
+ agentEntries: fs.readdir(agentDir).catch(() => null),
31
+ extEntries: fs.readdir(extDir).catch(() => null),
32
+ sharedStateText: readTextIfExists(path.join(extDir, 'shared', 'session-state.js')),
33
+ configText: readTextIfExists(configPath),
34
+ ponytailPkgText: readTextIfExists(ponytailPkg),
35
+ ponytailExtText: readTextIfExists(ponytailExt),
36
+ pluginsPkgRaw: readTextIfExists(path.join(pluginsDir, 'package.json')),
37
+ selfPkgText: readTextIfExists(selfPkg),
38
+ rtkBinText: readTextIfExists(rtkBin),
39
+ cavemanIndexText: readTextIfExists(cavemanIndex),
40
+ cavemanRuleText: readTextIfExists(cavemanRule),
41
+ rtkIndexText: readTextIfExists(rtkIndex),
42
+ updaterIndexText: readTextIfExists(updaterIndex),
43
+ comboIndexText: readTextIfExists(comboIndex),
44
+ modeReinforcementText: readTextIfExists(modeReinforcement),
45
+ ruleMtime: fs.stat(cavemanRule).catch(() => null),
46
+ rtkMtime: fs.stat(rtkBin).catch(() => null),
47
+ ponytailMtime: fs.stat(ponytailPkg).catch(() => null),
48
+ updateVersion: checkForUpdate(),
49
+ };
50
+ const rtkVersionProbe = probes.rtkBinText.then((text) => text === null ? null : execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => r.stdout.trim() || r.stderr.trim() || null, (e) => {
51
+ const err = e;
52
+ return err.stdout?.trim() || err.stderr?.trim() || null;
53
+ }));
54
+ const [ompVersion, agentEntries, extEntries, sharedStateText, configText, updateVersion] = await runInteractivePhase('Checking environment and installation', () => Promise.all([
55
+ probes.ompVersion,
56
+ probes.agentEntries,
57
+ probes.extEntries,
58
+ probes.sharedStateText,
59
+ probes.configText,
60
+ probes.updateVersion,
61
+ ]));
62
+ const [cavemanIndexText, rtkIndexText, updaterIndexText, comboIndexText, modeReinforcementText, ponytailPkgText, ponytailExtText, pluginsPkgRaw, selfPkgText] = await runInteractivePhase('Checking extensions and plugins', () => Promise.all([
63
+ probes.cavemanIndexText,
64
+ probes.rtkIndexText,
65
+ probes.updaterIndexText,
66
+ probes.comboIndexText,
67
+ probes.modeReinforcementText,
68
+ probes.ponytailPkgText,
69
+ probes.ponytailExtText,
70
+ probes.pluginsPkgRaw,
71
+ probes.selfPkgText,
72
+ ]));
73
+ const [cavemanRuleText, ruleMtime, rtkBinText, rtkMtime, rtkVersion, ponytailMtime] = await runInteractivePhase('Checking add-ons', () => Promise.all([
74
+ probes.cavemanRuleText,
75
+ probes.ruleMtime,
76
+ probes.rtkBinText,
77
+ probes.rtkMtime,
78
+ rtkVersionProbe,
79
+ probes.ponytailMtime,
80
+ ]));
81
+ // Categorized output with a tally; section headers group related probes.
82
+ const tally = { ok: 0, missing: 0, warn: 0 };
83
+ function check(label, ok, detail = '') {
84
+ if (ok) {
85
+ tally.ok++;
86
+ console.log(` ${label}: ok${detail ? ` ${detail}` : ''}`);
87
+ }
88
+ else {
89
+ tally.missing++;
90
+ console.log(` ${label}: MISSING${detail ? ` ${detail}` : ''}`);
91
+ }
92
+ }
93
+ function warnLine(label, detail) {
94
+ tally.warn++;
95
+ console.log(` ${label}: warn ${detail}`);
96
+ }
97
+ section('Environment');
98
+ check('Node', true, process.version);
99
+ check('OMP CLI', ompVersion !== null, ompVersion ?? '');
100
+ if (updateVersion)
101
+ warnLine('Tersio CLI', `${updateVersion} available — run tersio update`);
102
+ else
103
+ check('Tersio CLI', true, PACKAGE_VERSION);
104
+ console.log(` Home: ${HOME}`);
105
+ section('Installation');
106
+ check('OMP agent dir', agentEntries !== null, agentDir);
107
+ check('OMP extensions dir', extEntries !== null, extDir);
108
+ check('OMP config.yml', configText !== null, configPath);
109
+ check('Shared session bridge', sharedStateText !== null);
110
+ section('Extensions');
111
+ check('Caveman extension', cavemanIndexText !== null);
112
+ check('RTK extension', rtkIndexText !== null);
113
+ check('Updater extension', updaterIndexText !== null);
114
+ check('Combo extension', comboIndexText !== null);
115
+ check('Mode reinforcement extension', modeReinforcementText !== null);
116
+ section('Plugins');
117
+ check('Ponytail package', ponytailPkgText !== null);
118
+ check('Ponytail extension', ponytailExtText !== null);
119
+ if (configText) {
120
+ check('Ponytail in config.yml', configText.includes('ponytail') && configText.includes('pi-extension'));
121
+ check('Combo in config.yml', configText.includes('combo-toggle'));
122
+ }
123
+ check('Self plugin package', selfPkgText !== null, parseJsonObject(selfPkgText)?.version ?? '');
124
+ const selfDep = PACKAGE_NAME in (parseJsonObject(pluginsPkgRaw)?.dependencies || {});
125
+ check('Self plugin in plugins/package.json', selfDep);
126
+ section('Add-ons');
127
+ const ruleAge = ruleMtime ? `updated ${relTime(Date.now() - ruleMtime.mtimeMs)}` : '';
128
+ check('Caveman rule', cavemanRuleText !== null, ruleAge);
129
+ const rtkAge = rtkMtime ? `updated ${relTime(Date.now() - rtkMtime.mtimeMs)}` : '';
130
+ if (rtkBinText === null)
131
+ check('RTK binary', false, rtkBin);
132
+ else {
133
+ const bits = [rtkVersion, rtkAge].filter(Boolean).join(', ');
134
+ check('RTK binary', true, bits);
135
+ if (!rtkVersion)
136
+ warnLine('RTK version', 'unavailable — binary may not be executable');
137
+ }
138
+ const ponytailAge = ponytailMtime ? `updated ${relTime(Date.now() - ponytailMtime.mtimeMs)}` : '';
139
+ check('Ponytail', ponytailPkgText !== null, [parseJsonObject(ponytailPkgText)?.version ?? '', ponytailAge].filter(Boolean).join(', '));
140
+ const total = tally.ok + tally.missing + tally.warn;
141
+ console.log(`\n Summary: ${total} checks — ${tally.ok} ok, ${tally.warn} warn, ${tally.missing} missing`);
142
+ }
143
+ function section(name) {
144
+ console.log(`\n${name}`);
145
+ }
146
+ export { runDoctor };