@krtclcdy/tersio 1.0.3 → 2.0.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.
@@ -1,31 +1,30 @@
1
- import { readFileSync } from "node:fs";
2
- import { fileURLToPath } from "node:url";
3
- import { dirname, join } from "node:path";
4
- import { COMBO_LEVELS, getSharedComboState, isOmpSubagentPrompt, setSharedComboMode } from "../shared/session-state.js";
5
- import { readCavemanDefault } from "../shared/plugin-settings.js";
1
+ import { readFileSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { dirname, join } from 'node:path';
4
+ import { asPromptArray, getSharedComboState, isComboPresetActive, isOmpSubagentPrompt, lastCustomValue, normalizeInputCommand, normalizeMode, paintStatusBar, sessionEntries, setSharedComboMode } from '../shared/session-state.js';
5
+ import { readCavemanDefault } from '../shared/plugin-settings.js';
6
6
  const CAVERN_DIR = dirname(fileURLToPath(import.meta.url));
7
- const RULE_PATH = join(CAVERN_DIR, "rule.md");
7
+ const RULE_PATH = join(CAVERN_DIR, 'rule.md');
8
8
  const FALLBACK_FULL_RULE = `Respond terse like smart caveman. All technical substance stay. Only fluff die.
9
9
 
10
10
  Rules:
11
11
  - Drop articles (a/an/the), filler (just/really/basically), pleasantries, hedging.
12
12
  - Fragments OK. Short synonyms. Technical terms exact. Code unchanged.
13
13
  - Pattern: [thing] [action] [reason]. [next step].
14
- - Not: "Sure! I'd be happy to help you with that."
15
- - Yes: "Bug in auth middleware. Fix:"
14
+ - Not: 'Sure! I'd be happy to help you with that.'
15
+ - Yes: 'Bug in auth middleware. Fix:'
16
16
 
17
17
  Auto-clarity: drop caveman for security warnings, irreversible actions, or when user seems confused. Resume after.`;
18
18
  // ponytail: synchronous read each full-mode injection; ceiling = small file, cold session start. Upgrade path: cache file contents + mtime, invalidate on change.
19
19
  function readFullRule() {
20
20
  try {
21
- return readFileSync(RULE_PATH, "utf8");
21
+ return readFileSync(RULE_PATH, 'utf8');
22
22
  }
23
23
  catch {
24
24
  return FALLBACK_FULL_RULE;
25
25
  }
26
26
  }
27
- const DEFAULT_MODE = "off";
28
- const MODES = new Set(["off", "lite", "full", "ultra", "wenyan"]);
27
+ const DEFAULT_MODE = 'off';
29
28
  const INSTRUCTIONS = {
30
29
  lite: `Caveman lite active for this session.
31
30
  Respond concise. Drop pleasantries, filler, and hedging. Keep complete technical substance. Code, commands, paths, errors, commits, and PR text stay normal/exact.`,
@@ -35,26 +34,12 @@ Maximum terse prose. Fragments preferred. No pleasantries, no tour, no recap unl
35
34
  wenyan: `Caveman wenyan active for this session.
36
35
  Use ultra-terse classical-Chinese-style prose only where it preserves clarity for the user. Keep technical terms, code, commands, commits, PR text, paths, and errors exact. If clarity would suffer, use caveman full instead.`,
37
36
  };
38
- function normalizeMode(value) {
39
- const mode = String(value || "").trim().toLowerCase();
40
- return MODES.has(mode) ? mode : null;
41
- }
42
37
  function resolveMode(entries, fallback = DEFAULT_MODE) {
43
- if (!Array.isArray(entries))
44
- return fallback;
45
- for (let i = entries.length - 1; i >= 0; i -= 1) {
46
- const entry = entries[i];
47
- if (entry?.type !== "custom" || entry?.customType !== "caveman-mode")
48
- continue;
49
- const mode = normalizeMode(entry?.data?.mode);
50
- if (mode)
51
- return mode;
52
- }
53
- return fallback;
38
+ return lastCustomValue(entries, 'caveman-mode', (data) => normalizeMode('caveman', data?.mode)) ?? fallback;
54
39
  }
55
40
  function isOffCommand(text) {
56
- const t = String(text || "").trim().toLowerCase().replace(/[.!?\s]+$/, "");
57
- return t === "stop caveman" || t === "normal mode" || t === "caveman off";
41
+ const t = normalizeInputCommand(text);
42
+ return t === 'stop caveman' || t === 'normal mode' || t === 'caveman off';
58
43
  }
59
44
  export default function cavemanSessionExtension(pi) {
60
45
  let currentMode = DEFAULT_MODE;
@@ -67,87 +52,83 @@ export default function cavemanSessionExtension(pi) {
67
52
  if (!c?.ui?.setStatus)
68
53
  return;
69
54
  // Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
70
- if (getSharedComboState().level in COMBO_LEVELS && getSharedComboState().level !== "off") {
71
- c.ui.setStatus("caveman", undefined);
55
+ if (isComboPresetActive() || currentMode === 'off') {
56
+ c.ui.setStatus('caveman', undefined);
72
57
  return;
73
58
  }
74
- if (currentMode === "off") {
75
- c.ui.setStatus("caveman", undefined);
76
- return;
77
- }
78
- const theme = c.ui.theme;
79
- const indicator = isActive && theme?.fg ? theme.fg("accent", "🪨") : "🪨";
80
- const label = `caveman: ${currentMode.toUpperCase()}`;
81
- c.ui.setStatus("caveman", theme?.fg ? `${indicator} ${theme.fg("muted", label)}` : `${indicator} ${label}`);
59
+ paintStatusBar(c.ui, 'caveman', '🪨', `caveman: ${currentMode.toUpperCase()}`, isActive);
82
60
  }
83
61
  function setMode(mode, ctx) {
84
- const normalized = normalizeMode(mode);
62
+ const normalized = normalizeMode('caveman', mode);
85
63
  if (!normalized)
86
64
  return false;
87
65
  currentMode = normalized;
88
- pi.appendEntry?.("caveman-mode", { mode: normalized });
89
- setSharedComboMode("caveman", normalized);
66
+ pi.appendEntry?.('caveman-mode', { mode: normalized });
67
+ setSharedComboMode('caveman', normalized);
90
68
  syncStatus(ctx);
91
- ctx?.ui?.notify?.(`Caveman mode ${normalized === "off" ? "off" : `set to ${normalized}`}.`, "info");
69
+ const msg = normalized === 'off' ? 'Caveman mode off.' : `Caveman mode set to ${normalized}.`;
70
+ ctx?.ui?.notify?.(msg, 'info');
92
71
  return true;
93
72
  }
94
- pi.setLabel?.("Caveman session toggle");
95
- pi.registerCommand?.("caveman", {
96
- description: "Toggle terse caveman replies for this session",
73
+ pi.setLabel?.('Caveman session toggle');
74
+ pi.registerCommand?.('caveman', {
75
+ description: 'Toggle terse caveman replies for this session',
97
76
  handler: async (args, ctx) => {
98
- const arg = String(args || "").trim().toLowerCase();
99
- if (!arg || arg === "on") {
100
- setMode("full", ctx);
77
+ const arg = String(args || '').trim().toLowerCase();
78
+ if (!arg || arg === 'on') {
79
+ setMode('full', ctx);
101
80
  return;
102
81
  }
103
- if (arg === "status") {
104
- ctx?.ui?.notify?.(`Caveman: ${currentMode}`, "info");
82
+ if (arg === 'status') {
83
+ ctx?.ui?.notify?.(`Caveman: ${currentMode}`, 'info');
105
84
  return;
106
85
  }
107
86
  if (!setMode(arg, ctx)) {
108
- ctx?.ui?.notify?.("Usage: /caveman [lite|full|ultra|wenyan|off|status]", "warning");
87
+ ctx?.ui?.notify?.('Usage: /caveman [lite|full|ultra|wenyan|off|status]', 'warning');
109
88
  }
110
89
  },
111
90
  });
112
- pi.on("input", async (event) => {
113
- if (event?.source === "extension")
91
+ pi.on('input', async (event) => {
92
+ if (event?.source === 'extension')
114
93
  return;
115
- if (currentMode !== "off" && isOffCommand(event?.text))
116
- setMode("off");
94
+ if (currentMode !== 'off' && isOffCommand(event?.text))
95
+ setMode('off');
117
96
  });
118
97
  function restoreMode(ctx) {
119
- const entries = ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
98
+ const entries = sessionEntries(ctx);
120
99
  // Persisted session state wins; a fresh session falls back to the
121
100
  // installer/user-configured default (off unless configured).
122
- const persisted = resolveMode(entries, "");
101
+ const persisted = resolveMode(entries, '');
123
102
  currentMode = persisted || readCavemanDefault();
124
103
  syncStatus(ctx);
125
104
  }
126
- pi.on("session_start", async (_event, ctx) => {
105
+ pi.on('session_start', async (_event, ctx) => {
127
106
  restoreMode(ctx);
128
- ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, "info");
107
+ ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
129
108
  });
130
- pi.on("session_branch", async (_event, ctx) => {
109
+ pi.on('session_branch', async (_event, ctx) => {
131
110
  restoreMode(ctx);
132
111
  });
133
- pi.on("session_tree", async (_event, ctx) => {
112
+ pi.on('session_tree', async (_event, ctx) => {
134
113
  restoreMode(ctx);
135
114
  });
136
- pi.on("agent_start", async (_event, ctx) => {
115
+ pi.on('agent_start', async (_event, ctx) => {
137
116
  isActive = true;
138
117
  syncStatus(ctx);
139
118
  });
140
- pi.on("agent_end", async (_event, ctx) => {
119
+ pi.on('agent_end', async (_event, ctx) => {
141
120
  isActive = false;
142
121
  syncStatus(ctx);
143
122
  });
144
- pi.on("before_agent_start", async (event) => {
123
+ pi.on('before_agent_start', async (event) => {
145
124
  const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().caveman : currentMode;
146
- if (!mode || mode === "off")
125
+ if (!mode || mode === 'off')
147
126
  return;
148
127
  const def = INSTRUCTIONS[mode];
149
- const instruction = typeof def === "function" ? def() : def;
150
- const base = Array.isArray(event.systemPrompt) ? event.systemPrompt : [event.systemPrompt];
128
+ if (!def)
129
+ return;
130
+ const instruction = typeof def === 'function' ? def() : def;
131
+ const base = asPromptArray(event.systemPrompt);
151
132
  return { systemPrompt: [...base, instruction] };
152
133
  });
153
134
  }
@@ -1,41 +1,34 @@
1
1
  // /combo session toggle — set all three (caveman, rtk, ponytail) at once.
2
2
  // Modes: off | medium | balanced | max
3
- import os from "node:os";
4
- import path from "node:path";
5
- import { createRequire } from "node:module";
6
- import { COMBO_LEVELS, getSharedComboState, isOmpSubagentPrompt, normalizeComboLevel, reconcileSharedComboEntries, setSharedComboLevel, setSharedComboListener, } from "../shared/session-state.js";
7
- import { readComboDefault } from "../shared/plugin-settings.js";
3
+ import os from 'node:os';
4
+ import path from 'node:path';
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';
7
+ import { readComboDefault } from '../shared/plugin-settings.js';
8
8
  const require = createRequire(import.meta.url);
9
+ const PONYTAIL_FALLBACK_INTENSITY = {
10
+ lite: 'Prefer the simplest correct solution.',
11
+ review: 'Review only for avoidable complexity; recommend the smallest correct replacement.',
12
+ };
9
13
  function ponytailFallback(mode) {
10
- const intensity = mode === "lite"
11
- ? "Prefer the simplest correct solution."
12
- : mode === "review"
13
- ? "Review only for avoidable complexity; recommend the smallest correct replacement."
14
- : "Use the minimum correct solution. Delete or reuse before adding.";
14
+ const intensity = PONYTAIL_FALLBACK_INTENSITY[mode] ?? 'Use the minimum correct solution. Delete or reuse before adding.';
15
15
  return `🦥 PONYTAIL MODE ACTIVE — level: ${mode}\n${intensity} Understand the path first and fix root causes, not symptoms. Prefer the standard library and YAGNI. Avoid speculative abstractions and dependencies. Preserve correctness. Verify changed behavior.`;
16
16
  }
17
- function entriesFrom(ctx) {
18
- return ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || [];
19
- }
20
17
  function levelSummary(state) {
21
18
  return `caveman=${state.caveman} rtk=${state.rtk} ponytail=${state.ponytail}`;
22
19
  }
23
- function hasPonytailInstructions(systemPrompt) {
24
- const prompts = Array.isArray(systemPrompt) ? systemPrompt : [systemPrompt];
25
- return prompts.some((prompt) => typeof prompt === "string" && prompt.includes("PONYTAIL MODE ACTIVE"));
26
- }
27
20
  function loadPonytailInstructions(mode) {
28
21
  try {
29
- const installed = path.join(os.homedir(), ".omp", "plugins", "node_modules", "@dietrichgebert", "ponytail", "hooks", "ponytail-instructions.js");
22
+ const installed = path.join(os.homedir(), '.omp', 'plugins', 'node_modules', '@dietrichgebert', 'ponytail', 'hooks', 'ponytail-instructions.js');
30
23
  const { getPonytailInstructions } = require(installed);
31
- if (typeof getPonytailInstructions === "function")
24
+ if (typeof getPonytailInstructions === 'function')
32
25
  return getPonytailInstructions(mode);
33
26
  }
34
27
  catch { }
35
28
  return ponytailFallback(mode);
36
29
  }
37
30
  export default function comboToggleExtension(pi) {
38
- pi.setLabel?.("Combo session toggle (all 3 add-ons)");
31
+ pi.setLabel?.('Combo session toggle (all 3 add-ons)');
39
32
  let currentState = getSharedComboState();
40
33
  let lastCtx = undefined;
41
34
  function syncStatus(ctx) {
@@ -46,8 +39,8 @@ export default function comboToggleExtension(pi) {
46
39
  return;
47
40
  // Single unified bar replaces the three per-extension bars while a preset is
48
41
  // active. In custom/off, the individual bars come back and combo stays clear.
49
- if (currentState.level !== "medium" && currentState.level !== "balanced" && currentState.level !== "max") {
50
- c.ui.setStatus("combo", undefined);
42
+ if (!isComboPresetActive()) {
43
+ c.ui.setStatus('combo', undefined);
51
44
  return;
52
45
  }
53
46
  const theme = c.ui.theme;
@@ -56,12 +49,22 @@ export default function comboToggleExtension(pi) {
56
49
  const p = currentState.ponytail.toUpperCase();
57
50
  const lvl = currentState.level.toUpperCase();
58
51
  const label = `combo ${lvl}: 🪨caveman=${c0} ⚡rtk=${r} 🦥ponytail=${p}`;
59
- c.ui.setStatus("combo", theme?.fg ? `${theme.fg("accent", "🧩")} ${theme.fg("muted", label)}` : `🧩 ${label}`);
52
+ c.ui.setStatus('combo', theme?.fg ? `${theme.fg('accent', '🧩')} ${theme.fg('muted', label)}` : `🧩 ${label}`);
60
53
  // Clobber any sibling bars another extension may have painted in a race
61
54
  // during session_start — our bar is canonical for the duration of the preset.
62
- c.ui.setStatus("caveman", undefined);
63
- c.ui.setStatus("rtk", undefined);
64
- c.ui.setStatus("ponytail", undefined);
55
+ c.ui.setStatus('caveman', undefined);
56
+ c.ui.setStatus('rtk', undefined);
57
+ c.ui.setStatus('ponytail', undefined);
58
+ }
59
+ // ponytail: same persistence as /combo — siblings (incl. upstream ponytail)
60
+ // restore from these entries, so the fallback must write them too or the
61
+ // preset evaporates on resume and ponytail never activates.
62
+ function persistPreset(level) {
63
+ const modes = COMBO_LEVELS[level];
64
+ pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
65
+ pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
66
+ pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
67
+ pi.appendEntry?.('combo-level', { level });
65
68
  }
66
69
  function useState(state, ctx) {
67
70
  currentState = state;
@@ -71,86 +74,75 @@ export default function comboToggleExtension(pi) {
71
74
  function reconcile(ctx) {
72
75
  if (!ctx?.hasUI)
73
76
  return currentState;
74
- return useState(reconcileSharedComboEntries(entriesFrom(ctx)), ctx);
77
+ return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
75
78
  }
76
79
  function listen(ctx) {
77
80
  if (ctx?.hasUI)
78
81
  setSharedComboListener((state) => useState(state));
79
82
  }
80
- pi.registerCommand?.("combo", {
81
- description: "Toggle all 3 OMP add-ons at once. Usage: /combo <off|medium|balanced|max|status>",
83
+ function track(ctx) {
84
+ listen(ctx);
85
+ if (ctx?.hasUI)
86
+ reconcile(ctx);
87
+ }
88
+ pi.registerCommand?.('combo', {
89
+ description: 'Toggle all 3 OMP add-ons at once. Usage: /combo <off|medium|balanced|max|status>',
82
90
  handler: async (args, ctx) => {
83
91
  listen(ctx);
84
- const arg = String(args || "").trim().toLowerCase();
85
- if (!arg || arg === "status") {
92
+ const arg = String(args || '').trim().toLowerCase();
93
+ if (!arg || arg === 'status') {
86
94
  const state = reconcile(ctx);
87
- ctx?.ui?.notify?.(`Combo: ${state.level === "custom" ? "INACTIVE" : state.level.toUpperCase()} (${levelSummary(state)})`, "info");
95
+ ctx?.ui?.notify?.(`Combo: ${state.level === 'custom' ? 'INACTIVE' : state.level.toUpperCase()} (${levelSummary(state)})`, 'info');
88
96
  return;
89
97
  }
90
- if (arg === "help") {
91
- ctx?.ui?.notify?.("/combo off — disables all 3 (caveman, rtk, ponytail)\n" +
92
- "/combo medium — light: caveman=lite, rtk=on, ponytail=lite\n" +
93
- "/combo balanced — middle: caveman=full, rtk=on, ponytail=full\n" +
94
- "/combo max — aggressive: caveman=ultra, rtk=on, ponytail=ultra", "info");
98
+ if (arg === 'help') {
99
+ ctx?.ui?.notify?.('/combo off — disables all 3 (caveman, rtk, ponytail)\n' +
100
+ '/combo medium — light: caveman=lite, rtk=on, ponytail=lite\n' +
101
+ '/combo balanced — middle: caveman=full, rtk=on, ponytail=full\n' +
102
+ '/combo max — aggressive: caveman=ultra, rtk=on, ponytail=ultra', 'info');
95
103
  return;
96
104
  }
97
105
  const level = normalizeComboLevel(arg);
98
106
  if (!level) {
99
- ctx?.ui?.notify?.(`Unknown combo level: ${arg}. Use: off | medium | balanced | max`, "warning");
107
+ ctx?.ui?.notify?.(`Unknown combo level: ${arg}. Use: off | medium | balanced | max`, 'warning');
100
108
  return;
101
109
  }
102
- const modes = COMBO_LEVELS[level];
103
- // Persist per-extension state so they restore on session_start
104
- pi.appendEntry?.("caveman-mode", { mode: modes.caveman });
105
- pi.appendEntry?.("rtk-mode", { enabled: modes.rtk === "on" });
106
- pi.appendEntry?.("ponytail-mode", { mode: modes.ponytail });
107
- pi.appendEntry?.("combo-level", { level });
110
+ persistPreset(level);
108
111
  useState(setSharedComboLevel(level), ctx);
109
- ctx?.ui?.notify?.(`Combo ${level} applied: ${levelSummary(currentState)}`, "info");
112
+ ctx?.ui?.notify?.(`Combo ${level} applied: ${levelSummary(currentState)}`, 'info');
110
113
  if (ctx?.reload)
111
114
  await ctx.reload();
112
115
  },
113
116
  });
114
- pi.on("session_start", async (_event, ctx) => {
115
- listen(ctx);
116
- if (ctx?.hasUI)
117
- reconcile(ctx);
118
- else
117
+ pi.on('session_start', async (_event, ctx) => {
118
+ track(ctx);
119
+ if (!ctx?.hasUI)
119
120
  syncStatus(ctx);
120
121
  // Installer/user-configured default applies only to a fresh session with
121
122
  // no persisted state — real session state always wins.
122
- if (getSharedComboState().level === "off" && !(ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || []).length) {
123
+ if (getSharedComboState().level === 'off' && !sessionEntries(ctx).length) {
123
124
  const fallback = readComboDefault();
124
- if (fallback !== "off") {
125
+ if (fallback !== 'off') {
126
+ persistPreset(fallback);
125
127
  useState(setSharedComboLevel(fallback), ctx);
126
- ctx?.ui?.notify?.(`Combo default applied: ${fallback}`, "info");
128
+ ctx?.ui?.notify?.(`Combo default applied: ${fallback}`, 'info');
127
129
  }
128
130
  }
129
131
  });
130
- pi.on("session_branch", async (_event, ctx) => {
131
- listen(ctx);
132
- if (ctx?.hasUI)
133
- reconcile(ctx);
134
- });
135
- pi.on("session_tree", async (_event, ctx) => {
136
- listen(ctx);
137
- if (ctx?.hasUI)
138
- reconcile(ctx);
139
- });
140
- pi.on("agent_start", async (_event, ctx) => {
141
- listen(ctx);
142
- if (ctx?.hasUI)
143
- reconcile(ctx);
144
- });
145
- pi.on("before_agent_start", async (event, ctx) => {
132
+ for (const event of ['session_branch', 'session_tree', 'agent_start']) {
133
+ pi.on(event, async (_event, ctx) => {
134
+ track(ctx);
135
+ });
136
+ }
137
+ pi.on('before_agent_start', async (event, ctx) => {
146
138
  if (ctx?.hasUI)
147
139
  reconcile(ctx);
148
140
  if (!isOmpSubagentPrompt(event.systemPrompt))
149
141
  return;
150
142
  const mode = getSharedComboState().ponytail;
151
- if (mode === "off" || hasPonytailInstructions(event.systemPrompt))
143
+ if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE'))
152
144
  return;
153
- const base = Array.isArray(event.systemPrompt) ? event.systemPrompt : [event.systemPrompt];
145
+ const base = asPromptArray(event.systemPrompt);
154
146
  return { systemPrompt: [...base, loadPonytailInstructions(mode)] };
155
147
  });
156
148
  // Slash commands only; natural-language input caused accidental toggles and has no reload context.
@@ -1,30 +1,49 @@
1
1
  // Shared helpers for the installer and the AI add-ons updater.
2
2
  // Node built-ins only — this module must stay dependency-free.
3
- import { createHash } from "node:crypto";
4
- import { createWriteStream } from "node:fs";
5
- import fs from "node:fs/promises";
6
- import https from "node:https";
7
- import path from "node:path";
3
+ import { createHash } from 'node:crypto';
4
+ import { createWriteStream } from 'node:fs';
5
+ import fs from 'node:fs/promises';
6
+ import https from 'node:https';
7
+ import path from 'node:path';
8
8
  // Promise.withResolvers is Node 22+; the package supports Node 18+.
9
9
  // ponytail: swap for the built-in when engines bumps to >=22.
10
- function withResolvers() {
10
+ export function withResolvers() {
11
11
  let resolve;
12
12
  let reject;
13
13
  const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
14
14
  return { promise, resolve, reject };
15
15
  }
16
+ export const RTK_RELEASE_API = 'https://api.github.com/repos/rtk-ai/rtk/releases/latest';
17
+ export const CAVEMAN_REMOTE_RULE = 'https://raw.githubusercontent.com/JuliusBrussee/caveman/main/src/rules/caveman-activate.md';
18
+ const RTK_PLATFORM_SPECS = {
19
+ 'win32/x64': { triple: 'x86_64-pc-windows-msvc', ext: '.zip', binary: 'rtk.exe' },
20
+ 'linux/x64': { triple: 'x86_64-unknown-linux-musl', ext: '.tar.gz', binary: 'rtk' },
21
+ 'linux/arm64': { triple: 'aarch64-unknown-linux-gnu', ext: '.tar.gz', binary: 'rtk' },
22
+ 'darwin/x64': { triple: 'x86_64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
23
+ 'darwin/arm64': { triple: 'aarch64-apple-darwin', ext: '.tar.gz', binary: 'rtk' },
24
+ };
25
+ export function rtkPlatformSpec(platform = process.platform, arch = process.arch) {
26
+ return RTK_PLATFORM_SPECS[`${platform}/${arch}`] || null;
27
+ }
28
+ // Single source for the 3xx+location redirect rule shared by httpsGet/httpsDownload.
29
+ function redirectNext(res, url) {
30
+ if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
31
+ return new URL(res.headers.location, url).href;
32
+ }
33
+ return null;
34
+ }
16
35
  export function httpsGet(url, opts = {}) {
17
36
  const { promise, resolve, reject } = withResolvers();
18
37
  const maxRedirects = opts.maxRedirects ?? 5;
19
- const req = https.get(url, { headers: { "User-Agent": "tersio", Accept: "application/json,*/*" } }, (res) => {
20
- if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
38
+ const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: 'application/json,*/*' } }, (res) => {
39
+ const next = redirectNext(res, url);
40
+ if (next) {
21
41
  if (maxRedirects <= 0) {
22
42
  res.resume();
23
43
  reject(new Error(`Too many redirects fetching ${url}`));
24
44
  return;
25
45
  }
26
46
  res.resume();
27
- const next = new URL(res.headers.location, url).href;
28
47
  resolve(httpsGet(next, { maxRedirects: maxRedirects - 1 }));
29
48
  return;
30
49
  }
@@ -33,28 +52,28 @@ export function httpsGet(url, opts = {}) {
33
52
  reject(new Error(`HTTP ${res.statusCode} for ${url}`));
34
53
  return;
35
54
  }
36
- let body = "";
55
+ let body = '';
37
56
  // ponytail: streamed accumulation — fine for tens of KB; stream-pipe if assets ever exceed a few MB.
38
- res.setEncoding("utf8");
39
- res.on("data", (chunk) => { body += chunk; });
40
- res.on("end", () => resolve(body));
57
+ res.setEncoding('utf8');
58
+ res.on('data', (chunk) => { body += chunk; });
59
+ res.on('end', () => resolve(body));
41
60
  });
42
- req.on("error", reject);
61
+ req.on('error', reject);
43
62
  req.setTimeout(30000, () => req.destroy(new Error(`Timeout fetching ${url}`)));
44
63
  return promise;
45
64
  }
46
65
  export function httpsDownload(url, dest, opts = {}) {
47
66
  const { promise, resolve, reject } = withResolvers();
48
67
  const maxRedirects = opts.maxRedirects ?? 5;
49
- const req = https.get(url, { headers: { "User-Agent": "tersio", Accept: "*/*" } }, (res) => {
50
- if (res.statusCode !== undefined && res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
68
+ const req = https.get(url, { headers: { 'User-Agent': 'tersio', Accept: '*/*' } }, (res) => {
69
+ const next = redirectNext(res, url);
70
+ if (next) {
51
71
  if (maxRedirects <= 0) {
52
72
  res.resume();
53
73
  reject(new Error(`Too many redirects downloading ${url}`));
54
74
  return;
55
75
  }
56
76
  res.resume();
57
- const next = new URL(res.headers.location, url).href;
58
77
  resolve(httpsDownload(next, dest, { maxRedirects: maxRedirects - 1 }));
59
78
  return;
60
79
  }
@@ -65,19 +84,26 @@ export function httpsDownload(url, dest, opts = {}) {
65
84
  }
66
85
  const file = createWriteStream(dest);
67
86
  res.pipe(file);
68
- file.on("finish", () => file.close(() => resolve()));
69
- file.on("error", reject);
87
+ file.on('finish', () => file.close(() => resolve()));
88
+ file.on('error', (err) => {
89
+ req.destroy();
90
+ res.resume();
91
+ reject(err);
92
+ });
70
93
  });
71
- req.on("error", reject);
94
+ req.on('error', reject);
72
95
  req.setTimeout(120000, () => req.destroy(new Error(`Timeout downloading ${url}`)));
73
96
  return promise;
74
97
  }
98
+ export async function fetchJson(url) {
99
+ return JSON.parse(await httpsGet(url));
100
+ }
75
101
  export function sha256Hex(text) {
76
- return createHash("sha256").update(text, "utf8").digest("hex");
102
+ return createHash('sha256').update(text, 'utf8').digest('hex');
77
103
  }
78
104
  export async function sha256File(filePath) {
79
105
  const buf = await fs.readFile(filePath);
80
- return createHash("sha256").update(buf).digest("hex");
106
+ return createHash('sha256').update(buf).digest('hex');
81
107
  }
82
108
  export function parseChecksum(checksumsText, assetName) {
83
109
  const target = path.basename(assetName);
@@ -88,14 +114,41 @@ export function parseChecksum(checksumsText, assetName) {
88
114
  }
89
115
  return null;
90
116
  }
117
+ export async function findFile(dir, name) {
118
+ try {
119
+ const ents = await fs.readdir(dir, { withFileTypes: true });
120
+ for (const e of ents) {
121
+ const full = path.join(dir, e.name);
122
+ if (e.isDirectory()) {
123
+ const found = await findFile(full, name);
124
+ if (found)
125
+ return found;
126
+ }
127
+ else if (e.isFile()) {
128
+ if (e.name === name)
129
+ return full;
130
+ // Match rtk-* asset names (e.g. rtk-x86_64-unknown-linux-musl).
131
+ if (name === 'rtk' || name === 'rtk.exe') {
132
+ const base = e.name.toLowerCase();
133
+ if (!/\.(txt|md|json|sha256|sig|asc|pem|crt|license)$/i.test(base)) {
134
+ if (base === 'rtk' || base === 'rtk.exe' || /^rtk[-_.]/.test(base))
135
+ return full;
136
+ }
137
+ }
138
+ }
139
+ }
140
+ }
141
+ catch { /* ignore */ }
142
+ return null;
143
+ }
91
144
  export async function readTextIfExists(p) {
92
145
  try {
93
- return await fs.readFile(p, "utf8");
146
+ return await fs.readFile(p, 'utf8');
94
147
  }
95
148
  catch {
96
149
  return null;
97
150
  }
98
151
  }
99
152
  export function normalizeRtkVersion(value) {
100
- return String(value || "").replace(/^rtk\s+/i, "").replace(/^v/i, "").trim();
153
+ return String(value || '').replace(/^rtk\s+/i, '').replace(/^v/i, '').trim();
101
154
  }