@krtclcdy/tersio 2.0.1 → 2.2.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.
Files changed (3) hide show
  1. package/README.md +7 -1
  2. package/package.json +1 -1
  3. package/tersio.js +185 -53
package/README.md CHANGED
@@ -11,6 +11,12 @@ Terse replies, compact shell output, and minimal code decisions for [Oh My Pi (O
11
11
 
12
12
  ## Install
13
13
 
14
+ Quick install (macOS/Linux/WSL — checks npm, then bootstraps the CLI):
15
+
16
+ ```bash
17
+ curl -fsSL https://raw.githubusercontent.com/KurutoDenzeru/tersio/main/install.sh | sh
18
+ ```
19
+
14
20
  OMP plugin (recommended — updates and feature flags handled by OMP):
15
21
 
16
22
  ```bash
@@ -79,7 +85,7 @@ Break-even math, balanced preset (caveman full + rtk + ponytail): `⌈overhead
79
85
  | Command | Purpose |
80
86
  |---|---|
81
87
  | `tersio install` | Install (user scope by default; `--scope project\|both` for more) |
82
- | `tersio update` | Refresh from the latest release |
88
+ | `tersio update` | Refresh the CLI, extensions, and add-ons (RTK binary, Caveman rule, Ponytail) |
83
89
  | `tersio reinstall` | Fresh install, preserving the Ponytail package |
84
90
  | `tersio doctor` | Check OMP, extension, Ponytail, and RTK health |
85
91
  | `tersio uninstall` | Remove extensions, registration, and the Ponytail plugin (`--keep-ponytail` keeps Ponytail; `--remove-rtk` also removes the RTK binary) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@krtclcdy/tersio",
3
- "version": "2.0.1",
3
+ "version": "2.2.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",
package/tersio.js CHANGED
@@ -84,7 +84,7 @@ function printHelp() {
84
84
 
85
85
  Commands:
86
86
  install Install the add-ons (user scope by default)
87
- update Run the latest published installer
87
+ update Refresh the CLI and add-ons (RTK binary, Caveman rule, Ponytail)
88
88
  reinstall Clean and reinstall the user-scope add-ons
89
89
  doctor Check the current installation
90
90
  uninstall Remove the managed extensions
@@ -661,17 +661,8 @@ async function stepCombo(extDir, options) {
661
661
  await ensureExtensionInConfig(configPath, dest, 'combo', options);
662
662
  }
663
663
  // --- Doctor ---
664
- function presence(label, text) {
665
- console.log(` ${label}: ${text !== null ? 'installed' : 'MISSING'}`);
666
- }
667
- function locationLine(label, ok, loc) {
668
- console.log(` ${label}: ${ok ? 'ok' : 'MISSING'} ${loc}`);
669
- }
670
664
  async function runDoctor() {
671
- console.log('\n=== Tersio Doctor ===\n');
672
- // Node
673
- console.log(` Node: ok ${process.version}`);
674
- // OMP CLI version joins FS probes below; logs keep fixed order.
665
+ console.log('\n=== Tersio Doctor ===');
675
666
  // Directories
676
667
  const agentDir = OMP_AGENT_DIR;
677
668
  const extDir = path.join(agentDir, 'extensions');
@@ -689,7 +680,7 @@ async function runDoctor() {
689
680
  const modeReinforcement = path.join(extDir, 'shared', 'mode-reinforcement.js');
690
681
  const selfPkg = path.join(pluginsDir, 'node_modules', PACKAGE_NAME, 'package.json');
691
682
  // Independent probes run concurrently; logs below keep fixed order.
692
- const [ompVersion, agentEntries, extEntries, sharedStateText, configText, ponytailPkgText, ponytailExtText, pluginsPkgRaw, selfPkgText, rtkBinText, cavemanIndexText, cavemanRuleText, rtkIndexText, updaterIndexText, comboIndexText, modeReinforcementText,] = await Promise.all([
683
+ const [ompVersion, agentEntries, extEntries, sharedStateText, configText, ponytailPkgText, ponytailExtText, pluginsPkgRaw, selfPkgText, rtkBinText, cavemanIndexText, cavemanRuleText, rtkIndexText, updaterIndexText, comboIndexText, modeReinforcementText, ruleMtime, rtkMtime, ponytailMtime, updateVersion,] = await Promise.all([
693
684
  execP(OMP_BIN, ['--version']).then((r) => r.stdout.trim(), () => null),
694
685
  fs.readdir(agentDir).catch(() => null),
695
686
  fs.readdir(extDir).catch(() => null),
@@ -706,49 +697,79 @@ async function runDoctor() {
706
697
  readTextIfExists(updaterIndex),
707
698
  readTextIfExists(comboIndex),
708
699
  readTextIfExists(modeReinforcement),
700
+ fs.stat(cavemanRule).catch(() => null),
701
+ fs.stat(rtkBin).catch(() => null),
702
+ fs.stat(ponytailPkg).catch(() => null),
703
+ checkForUpdate(),
709
704
  ]);
710
- console.log(ompVersion !== null ? ` OMP CLI: ok ${ompVersion}` : ' OMP CLI: MISSING');
711
- console.log(` Home: ${HOME}`);
712
- locationLine('OMP agent dir', agentEntries !== null, agentDir);
713
- locationLine('OMP extensions dir', extEntries !== null, extDir);
714
- console.log(` Shared session bridge: ${sharedStateText !== null ? 'installed' : 'MISSING'}`);
715
- locationLine('OMP config.yml', configText !== null, configPath);
716
- presence('Ponytail package', ponytailPkgText);
717
- presence('Ponytail extension', ponytailExtText);
718
- if (configText) {
719
- const hasPonytailPath = configText.includes('ponytail') && configText.includes('pi-extension');
720
- console.log(` Ponytail in config.yml: ${hasPonytailPath ? 'registered' : 'MISSING'}`);
721
- }
722
- // Self plugin registration (Settings → Plugins listing)
723
- const selfDep = PACKAGE_NAME in (parseJsonObject(pluginsPkgRaw)?.dependencies || {});
724
- console.log(` Self plugin in plugins/package.json: ${selfDep ? 'registered' : 'MISSING'}`);
725
- console.log(` Self plugin package: ${selfPkgText !== null ? 'installed' : 'MISSING'}`);
726
- // RTK
727
- const rtkExists = rtkBinText !== null;
728
- console.log(` RTK binary: ${rtkExists ? 'installed' : 'MISSING'} ${rtkBin}`);
729
- if (rtkExists) {
730
- try {
731
- const v = (await execP(rtkBin, ['--version'], { timeout: 5000 })).stdout.trim();
732
- console.log(` RTK version: ${v}`);
705
+ // Categorized output with a tally; section headers group related probes.
706
+ const tally = { ok: 0, missing: 0, warn: 0 };
707
+ function check(label, ok, detail = '') {
708
+ if (ok) {
709
+ tally.ok++;
710
+ console.log(` ${label}: ok${detail ? ` ${detail}` : ''}`);
733
711
  }
734
- catch {
735
- console.log(' RTK version: unavailable (may not be executable)');
712
+ else {
713
+ tally.missing++;
714
+ console.log(` ${label}: MISSING${detail ? ` ${detail}` : ''}`);
736
715
  }
737
716
  }
738
- // Caveman
739
- presence('Caveman extension', cavemanIndexText);
740
- presence('Caveman rule.md', cavemanRuleText);
741
- // RTK extension
742
- presence('RTK extension', rtkIndexText);
743
- // Updater
744
- presence('Updater extension', updaterIndexText);
745
- // Combo
746
- presence('Combo extension', comboIndexText);
747
- presence('Mode reinforcement extension', modeReinforcementText);
717
+ function warnLine(label, detail) {
718
+ tally.warn++;
719
+ console.log(` ${label}: warn ${detail}`);
720
+ }
721
+ section('Environment');
722
+ check('Node', true, process.version);
723
+ check('OMP CLI', ompVersion !== null, ompVersion ?? '');
724
+ if (updateVersion)
725
+ warnLine('Tersio CLI', `${updateVersion} available — run tersio update`);
726
+ else
727
+ check('Tersio CLI', true, PACKAGE_VERSION);
728
+ console.log(` Home: ${HOME}`);
729
+ section('Installation');
730
+ check('OMP agent dir', agentEntries !== null, agentDir);
731
+ check('OMP extensions dir', extEntries !== null, extDir);
732
+ check('OMP config.yml', configText !== null, configPath);
733
+ check('Shared session bridge', sharedStateText !== null);
734
+ section('Extensions');
735
+ check('Caveman extension', cavemanIndexText !== null);
736
+ check('RTK extension', rtkIndexText !== null);
737
+ check('Updater extension', updaterIndexText !== null);
738
+ check('Combo extension', comboIndexText !== null);
739
+ check('Mode reinforcement extension', modeReinforcementText !== null);
740
+ section('Plugins');
741
+ check('Ponytail package', ponytailPkgText !== null);
742
+ check('Ponytail extension', ponytailExtText !== null);
748
743
  if (configText) {
749
- const hasComboPath = configText.includes('combo-toggle');
750
- console.log(` Combo in config.yml: ${hasComboPath ? 'registered' : 'MISSING'}`);
744
+ check('Ponytail in config.yml', configText.includes('ponytail') && configText.includes('pi-extension'));
745
+ check('Combo in config.yml', configText.includes('combo-toggle'));
751
746
  }
747
+ check('Self plugin package', selfPkgText !== null, parseJsonObject(selfPkgText)?.version ?? '');
748
+ const selfDep = PACKAGE_NAME in (parseJsonObject(pluginsPkgRaw)?.dependencies || {});
749
+ check('Self plugin in plugins/package.json', selfDep);
750
+ section('Add-ons');
751
+ const ruleAge = ruleMtime ? `updated ${relTime(Date.now() - ruleMtime.mtimeMs)}` : '';
752
+ check('Caveman rule', cavemanRuleText !== null, ruleAge);
753
+ const rtkVersion = rtkBinText === null ? null : await execP(rtkBin, ['--version'], { timeout: 5000 }).then((r) => r.stdout.trim() || r.stderr.trim() || null, (e) => {
754
+ const err = e;
755
+ return err.stdout?.trim() || err.stderr?.trim() || null;
756
+ });
757
+ const rtkAge = rtkMtime ? `updated ${relTime(Date.now() - rtkMtime.mtimeMs)}` : '';
758
+ if (rtkBinText === null)
759
+ check('RTK binary', false, rtkBin);
760
+ else {
761
+ const bits = [rtkVersion, rtkAge].filter(Boolean).join(', ');
762
+ check('RTK binary', true, bits);
763
+ if (!rtkVersion)
764
+ warnLine('RTK version', 'unavailable — binary may not be executable');
765
+ }
766
+ const ponytailAge = ponytailMtime ? `updated ${relTime(Date.now() - ponytailMtime.mtimeMs)}` : '';
767
+ check('Ponytail', ponytailPkgText !== null, [parseJsonObject(ponytailPkgText)?.version ?? '', ponytailAge].filter(Boolean).join(', '));
768
+ const total = tally.ok + tally.missing + tally.warn;
769
+ console.log(`\n Summary: ${total} checks — ${tally.ok} ok, ${tally.warn} warn, ${tally.missing} missing`);
770
+ }
771
+ function section(name) {
772
+ console.log(`\n${name}`);
752
773
  }
753
774
  // Read-modify-write a JSON file. mutate returns true when it changed
754
775
  // something; no change (or unreadable file) means no output at all.
@@ -897,6 +918,67 @@ async function runUninstall(options = {}) {
897
918
  return true;
898
919
  }
899
920
  const SCOPE_MAP = { user: '1', project: '2', both: '3' };
921
+ const COMBO_MENU = { '1': 'off', '2': 'medium', '3': 'balanced', '4': 'max' };
922
+ // --- Update check (cached; used by the bare-invocation banner and doctor) ---
923
+ const UPDATE_CHECK_TTL_MS = 6 * 60 * 60 * 1000;
924
+ function newerThan(a, b) {
925
+ const pa = a.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
926
+ const pb = b.replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
927
+ for (let i = 0; i < 3; i++) {
928
+ if ((pa[i] || 0) !== (pb[i] || 0))
929
+ return (pa[i] || 0) > (pb[i] || 0);
930
+ }
931
+ return false;
932
+ }
933
+ async function latestPublishedVersion() {
934
+ try {
935
+ const args = IS_WINDOWS
936
+ ? ['/d', '/s', '/c', 'npm', 'view', PACKAGE_NAME, 'version', '--prefer-online']
937
+ : ['view', PACKAGE_NAME, 'version', '--prefer-online'];
938
+ const r = await execP(IS_WINDOWS ? process.env.ComSpec || 'cmd.exe' : 'npm', args, {
939
+ timeout: 4000,
940
+ maxBuffer: 1024 * 1024,
941
+ windowsHide: true,
942
+ shell: false,
943
+ });
944
+ return r.stdout.trim() || null;
945
+ }
946
+ catch {
947
+ return null;
948
+ }
949
+ }
950
+ // Returns a newer published version, or null. Reads the registry at most once
951
+ // per TTL (cached under OMP_PLUGINS_DIR) and stays silent on failure.
952
+ async function checkForUpdate() {
953
+ const cachePath = path.join(OMP_PLUGINS_DIR, 'tersio-update-check.json');
954
+ const cached = parseJsonObject(await readTextIfExists(cachePath));
955
+ const cacheFresh = typeof cached?.lastCheck === 'number' && Date.now() - cached.lastCheck < UPDATE_CHECK_TTL_MS;
956
+ if (cacheFresh && cached?.latest)
957
+ return newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : null;
958
+ const latest = await latestPublishedVersion();
959
+ if (latest) {
960
+ await fs.mkdir(OMP_PLUGINS_DIR, { recursive: true }).catch(() => { });
961
+ await fs.writeFile(cachePath, JSON.stringify({ latest, lastCheck: Date.now() }) + '\n', 'utf8').catch(() => { });
962
+ return newerThan(latest, PACKAGE_VERSION) ? latest : null;
963
+ }
964
+ // Registry unreachable: fall back to a stale cache rather than staying silent.
965
+ return cached?.latest && newerThan(cached.latest, PACKAGE_VERSION) ? cached.latest : null;
966
+ }
967
+ function relTime(ageMs) {
968
+ const mins = Math.floor(ageMs / 60000);
969
+ if (mins < 1)
970
+ return 'just now';
971
+ if (mins < 60)
972
+ return `${mins} min${mins === 1 ? '' : 's'} ago`;
973
+ const hours = Math.floor(mins / 60);
974
+ if (hours < 24)
975
+ return `${hours} hour${hours === 1 ? '' : 's'} ago`;
976
+ const days = Math.floor(hours / 24);
977
+ if (days < 14)
978
+ return `${days} day${days === 1 ? '' : 's'} ago`;
979
+ const weeks = Math.floor(days / 7);
980
+ return `${weeks} week${weeks === 1 ? '' : 's'} ago`;
981
+ }
900
982
  async function runLatestUpdate() {
901
983
  const updateScope = scopeFlag || 'user';
902
984
  if (!Object.hasOwn(SCOPE_MAP, updateScope)) {
@@ -923,6 +1005,31 @@ async function runLatestUpdate() {
923
1005
  const npmCommandArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', ...npmArgs] : npmArgs;
924
1006
  console.log('=== Updating Tersio ===');
925
1007
  console.log(` Running the latest ${PACKAGE_NAME} installer...\n`);
1008
+ // The npx delegation below only refreshes the OMP-side files; the globally
1009
+ // installed CLI keeps its old version until npm -g runs. Refresh both.
1010
+ if (dryRun) {
1011
+ console.log(` [dry-run] would run: npm install -g ${PACKAGE_NAME}@latest --no-audit --no-fund`);
1012
+ }
1013
+ else {
1014
+ try {
1015
+ const globalArgs = IS_WINDOWS ? ['/d', '/s', '/c', 'npm', 'install', '-g', `${PACKAGE_NAME}@latest`, '--no-audit', '--no-fund'] : ['install', '-g', `${PACKAGE_NAME}@latest`, '--no-audit', '--no-fund'];
1016
+ const globalResult = await execP(npmCommand, globalArgs, {
1017
+ timeout: 300000,
1018
+ maxBuffer: 10 * 1024 * 1024,
1019
+ windowsHide: true,
1020
+ shell: false,
1021
+ });
1022
+ if (globalResult.stdout)
1023
+ process.stdout.write(globalResult.stdout);
1024
+ if (globalResult.stderr)
1025
+ process.stderr.write(globalResult.stderr);
1026
+ console.log(` [ok] CLI updated to ${PACKAGE_NAME}@latest`);
1027
+ }
1028
+ catch (e) {
1029
+ console.log(` [warn] Global CLI update skipped: ${e.message}`);
1030
+ console.log(` [hint] Manual: npm install -g ${PACKAGE_NAME}@latest`);
1031
+ }
1032
+ }
926
1033
  try {
927
1034
  const result = await execP(npmCommand, npmCommandArgs, {
928
1035
  timeout: 300000,
@@ -992,12 +1099,27 @@ async function resolveScope() {
992
1099
  async function resolveProfile() {
993
1100
  const profile = defaultProfile();
994
1101
  // Single interactive prompt: the Combo preset implies all three modes.
1102
+ // Numbered menu, same pattern as the scope prompt — no typing preset names.
995
1103
  // Only for a real user at a terminal, only when no default flags were
996
1104
  // given, and never for --apply-update runs.
997
1105
  if (tty() && !profileFlagsGiven && !applyUpdate && (install || reinstall)) {
998
- const comboAnswer = (await ask(' Default Combo preset on session start? [off/medium/balanced/max] (off): ')).trim().toLowerCase();
999
- if (comboAnswer && COMBO_DEFAULTS.has(comboAnswer))
1000
- profile.comboDefault = comboAnswer;
1106
+ console.log(' Session-start defaults — Combo preset:');
1107
+ console.log(' 1) off');
1108
+ console.log(' 2) medium (caveman=lite, rtk=on, ponytail=lite)');
1109
+ console.log(' 3) balanced (caveman=full, rtk=on, ponytail=full)');
1110
+ console.log(' 4) max (caveman=ultra, rtk=on, ponytail=ultra)');
1111
+ for (;;) {
1112
+ const answer = (await ask(' Choose [1-4] (default 1): ')).trim();
1113
+ if (!answer) {
1114
+ profile.comboDefault = 'off';
1115
+ break;
1116
+ }
1117
+ if (COMBO_MENU[answer]) {
1118
+ profile.comboDefault = COMBO_MENU[answer];
1119
+ break;
1120
+ }
1121
+ console.log(` [fail] Invalid choice: ${answer}. Choose 1, 2, 3, or 4.`);
1122
+ }
1001
1123
  }
1002
1124
  // Explicit flags always win over the Combo preset.
1003
1125
  if (comboDefaultFlag !== undefined)
@@ -1113,13 +1235,23 @@ async function main() {
1113
1235
  console.log(` Platform: ${process.platform}`);
1114
1236
  console.log(` Arch: ${process.arch}`);
1115
1237
  console.log(` Home: ${HOME}`);
1238
+ // Remind humans a newer release exists; silent for scripts (no TTY) and for
1239
+ // the apply-update payload, which is itself an update run.
1240
+ if (tty() && !applyUpdate && command !== 'uninstall') {
1241
+ const newer = await checkForUpdate();
1242
+ if (newer)
1243
+ console.log(`\n [update] tersio ${newer} available (installed ${PACKAGE_VERSION}) — run \`tersio update\``);
1244
+ }
1116
1245
  const scope = await resolveScope();
1117
1246
  // Resolve session defaults: flags > interactive prompt > defaults.
1118
1247
  const profile = await resolveProfile();
1119
1248
  const userDir = OMP_AGENT_DIR;
1120
1249
  const userExtDir = path.join(userDir, 'extensions');
1121
1250
  const projectExtDir = path.join(process.cwd(), '.omp', 'extensions');
1122
- const options = { dryRun, verbose, yes, scope, reinstall };
1251
+ // apply-update is `tersio update`'s payload run: treat it like reinstall so
1252
+ // the add-ons refresh too — Ponytail package via npm, self plugin, RTK
1253
+ // binary (always re-downloaded), and the Caveman rule (always re-fetched).
1254
+ const options = { dryRun, verbose, yes, scope, reinstall: reinstall || applyUpdate };
1123
1255
  // Check prerequisites
1124
1256
  console.log('\nPrerequisites:');
1125
1257
  try {