@krtclcdy/tersio 1.0.4 → 2.0.1

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, reconcileSharedComboEntries, 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,88 @@ 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);
99
+ // Publish the persisted combo state (incl. combo-level) before painting:
100
+ // bar suppression reads the in-process bridge, which is empty in a fresh
101
+ // host until the combo extension reconciles — and its reconcile is
102
+ // UI-gated. Deriving it here makes suppression independent of load order.
103
+ reconcileSharedComboEntries(entries);
120
104
  // Persisted session state wins; a fresh session falls back to the
121
105
  // installer/user-configured default (off unless configured).
122
- const persisted = resolveMode(entries, "");
106
+ const persisted = resolveMode(entries, '');
123
107
  currentMode = persisted || readCavemanDefault();
124
108
  syncStatus(ctx);
125
109
  }
126
- pi.on("session_start", async (_event, ctx) => {
110
+ pi.on('session_start', async (_event, ctx) => {
127
111
  restoreMode(ctx);
128
- ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, "info");
112
+ ctx?.ui?.notify?.(`Caveman loaded: ${currentMode}`, 'info');
129
113
  });
130
- pi.on("session_branch", async (_event, ctx) => {
114
+ pi.on('session_branch', async (_event, ctx) => {
131
115
  restoreMode(ctx);
132
116
  });
133
- pi.on("session_tree", async (_event, ctx) => {
117
+ pi.on('session_tree', async (_event, ctx) => {
134
118
  restoreMode(ctx);
135
119
  });
136
- pi.on("agent_start", async (_event, ctx) => {
120
+ pi.on('agent_start', async (_event, ctx) => {
137
121
  isActive = true;
138
122
  syncStatus(ctx);
139
123
  });
140
- pi.on("agent_end", async (_event, ctx) => {
124
+ pi.on('agent_end', async (_event, ctx) => {
141
125
  isActive = false;
142
126
  syncStatus(ctx);
143
127
  });
144
- pi.on("before_agent_start", async (event) => {
128
+ pi.on('before_agent_start', async (event) => {
145
129
  const mode = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().caveman : currentMode;
146
- if (!mode || mode === "off")
130
+ if (!mode || mode === 'off')
147
131
  return;
148
132
  const def = INSTRUCTIONS[mode];
149
- const instruction = typeof def === "function" ? def() : def;
150
- const base = Array.isArray(event.systemPrompt) ? event.systemPrompt : [event.systemPrompt];
133
+ if (!def)
134
+ return;
135
+ const instruction = typeof def === 'function' ? def() : def;
136
+ const base = asPromptArray(event.systemPrompt);
151
137
  return { systemPrompt: [...base, instruction] };
152
138
  });
153
139
  }
@@ -1,42 +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)");
39
- let currentState = getSharedComboState();
31
+ pi.setLabel?.('Combo session toggle (all 3 add-ons)');
40
32
  let lastCtx = undefined;
41
33
  function syncStatus(ctx) {
42
34
  if (ctx)
@@ -46,117 +38,112 @@ export default function comboToggleExtension(pi) {
46
38
  return;
47
39
  // Single unified bar replaces the three per-extension bars while a preset is
48
40
  // 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);
41
+ if (!isComboPresetActive()) {
42
+ c.ui.setStatus('combo', undefined);
51
43
  return;
52
44
  }
45
+ // Read the live bridge, not cached extension state: sibling extensions
46
+ // reconcile it from persisted entries before this one runs.
47
+ const state = getSharedComboState();
53
48
  const theme = c.ui.theme;
54
- const c0 = currentState.caveman.toUpperCase();
55
- const r = currentState.rtk.toUpperCase();
56
- const p = currentState.ponytail.toUpperCase();
57
- const lvl = currentState.level.toUpperCase();
49
+ const c0 = state.caveman.toUpperCase();
50
+ const r = state.rtk.toUpperCase();
51
+ const p = state.ponytail.toUpperCase();
52
+ const lvl = state.level.toUpperCase();
58
53
  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}`);
54
+ c.ui.setStatus('combo', theme?.fg ? `${theme.fg('accent', '🧩')} ${theme.fg('muted', label)}` : `🧩 ${label}`);
60
55
  // Clobber any sibling bars another extension may have painted in a race
61
56
  // 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);
57
+ c.ui.setStatus('caveman', undefined);
58
+ c.ui.setStatus('rtk', undefined);
59
+ c.ui.setStatus('ponytail', undefined);
65
60
  }
66
61
  // ponytail: same persistence as /combo — siblings (incl. upstream ponytail)
67
62
  // restore from these entries, so the fallback must write them too or the
68
63
  // preset evaporates on resume and ponytail never activates.
69
64
  function persistPreset(level) {
70
65
  const modes = COMBO_LEVELS[level];
71
- pi.appendEntry?.("caveman-mode", { mode: modes.caveman });
72
- pi.appendEntry?.("rtk-mode", { enabled: modes.rtk === "on" });
73
- pi.appendEntry?.("ponytail-mode", { mode: modes.ponytail });
74
- pi.appendEntry?.("combo-level", { level });
66
+ pi.appendEntry?.('caveman-mode', { mode: modes.caveman });
67
+ pi.appendEntry?.('rtk-mode', { enabled: modes.rtk === 'on' });
68
+ pi.appendEntry?.('ponytail-mode', { mode: modes.ponytail });
69
+ pi.appendEntry?.('combo-level', { level });
75
70
  }
76
71
  function useState(state, ctx) {
77
- currentState = state;
78
72
  syncStatus(ctx);
79
73
  return state;
80
74
  }
81
75
  function reconcile(ctx) {
82
76
  if (!ctx?.hasUI)
83
- return currentState;
84
- return useState(reconcileSharedComboEntries(entriesFrom(ctx)), ctx);
77
+ return getSharedComboState();
78
+ return useState(reconcileSharedComboEntries(sessionEntries(ctx)), ctx);
85
79
  }
86
80
  function listen(ctx) {
87
81
  if (ctx?.hasUI)
88
82
  setSharedComboListener((state) => useState(state));
89
83
  }
90
- pi.registerCommand?.("combo", {
91
- description: "Toggle all 3 OMP add-ons at once. Usage: /combo <off|medium|balanced|max|status>",
84
+ function track(ctx) {
85
+ listen(ctx);
86
+ if (ctx?.hasUI)
87
+ reconcile(ctx);
88
+ }
89
+ pi.registerCommand?.('combo', {
90
+ description: 'Toggle all 3 OMP add-ons at once. Usage: /combo <off|medium|balanced|max|status>',
92
91
  handler: async (args, ctx) => {
93
92
  listen(ctx);
94
- const arg = String(args || "").trim().toLowerCase();
95
- if (!arg || arg === "status") {
93
+ const arg = String(args || '').trim().toLowerCase();
94
+ if (!arg || arg === 'status') {
96
95
  const state = reconcile(ctx);
97
- ctx?.ui?.notify?.(`Combo: ${state.level === "custom" ? "INACTIVE" : state.level.toUpperCase()} (${levelSummary(state)})`, "info");
96
+ ctx?.ui?.notify?.(`Combo: ${state.level === 'custom' ? 'INACTIVE' : state.level.toUpperCase()} (${levelSummary(state)})`, 'info');
98
97
  return;
99
98
  }
100
- if (arg === "help") {
101
- ctx?.ui?.notify?.("/combo off — disables all 3 (caveman, rtk, ponytail)\n" +
102
- "/combo medium — light: caveman=lite, rtk=on, ponytail=lite\n" +
103
- "/combo balanced — middle: caveman=full, rtk=on, ponytail=full\n" +
104
- "/combo max — aggressive: caveman=ultra, rtk=on, ponytail=ultra", "info");
99
+ if (arg === 'help') {
100
+ ctx?.ui?.notify?.('/combo off — disables all 3 (caveman, rtk, ponytail)\n' +
101
+ '/combo medium — light: caveman=lite, rtk=on, ponytail=lite\n' +
102
+ '/combo balanced — middle: caveman=full, rtk=on, ponytail=full\n' +
103
+ '/combo max — aggressive: caveman=ultra, rtk=on, ponytail=ultra', 'info');
105
104
  return;
106
105
  }
107
106
  const level = normalizeComboLevel(arg);
108
107
  if (!level) {
109
- ctx?.ui?.notify?.(`Unknown combo level: ${arg}. Use: off | medium | balanced | max`, "warning");
108
+ ctx?.ui?.notify?.(`Unknown combo level: ${arg}. Use: off | medium | balanced | max`, 'warning');
110
109
  return;
111
110
  }
112
111
  persistPreset(level);
113
112
  useState(setSharedComboLevel(level), ctx);
114
- ctx?.ui?.notify?.(`Combo ${level} applied: ${levelSummary(currentState)}`, "info");
113
+ ctx?.ui?.notify?.(`Combo ${level} applied: ${levelSummary(getSharedComboState())}`, 'info');
115
114
  if (ctx?.reload)
116
115
  await ctx.reload();
117
116
  },
118
117
  });
119
- pi.on("session_start", async (_event, ctx) => {
120
- listen(ctx);
121
- if (ctx?.hasUI)
122
- reconcile(ctx);
123
- else
118
+ pi.on('session_start', async (_event, ctx) => {
119
+ track(ctx);
120
+ if (!ctx?.hasUI)
124
121
  syncStatus(ctx);
125
122
  // Installer/user-configured default applies only to a fresh session with
126
123
  // no persisted state — real session state always wins.
127
- if (getSharedComboState().level === "off" && !(ctx?.sessionManager?.getBranch?.() || ctx?.sessionManager?.getEntries?.() || []).length) {
124
+ if (getSharedComboState().level === 'off' && !sessionEntries(ctx).length) {
128
125
  const fallback = readComboDefault();
129
- if (fallback !== "off") {
126
+ if (fallback !== 'off') {
130
127
  persistPreset(fallback);
131
128
  useState(setSharedComboLevel(fallback), ctx);
132
- ctx?.ui?.notify?.(`Combo default applied: ${fallback}`, "info");
129
+ ctx?.ui?.notify?.(`Combo default applied: ${fallback}`, 'info');
133
130
  }
134
131
  }
135
132
  });
136
- pi.on("session_branch", async (_event, ctx) => {
137
- listen(ctx);
138
- if (ctx?.hasUI)
139
- reconcile(ctx);
140
- });
141
- pi.on("session_tree", async (_event, ctx) => {
142
- listen(ctx);
143
- if (ctx?.hasUI)
144
- reconcile(ctx);
145
- });
146
- pi.on("agent_start", async (_event, ctx) => {
147
- listen(ctx);
148
- if (ctx?.hasUI)
149
- reconcile(ctx);
150
- });
151
- pi.on("before_agent_start", async (event, ctx) => {
133
+ for (const event of ['session_branch', 'session_tree', 'agent_start']) {
134
+ pi.on(event, async (_event, ctx) => {
135
+ track(ctx);
136
+ });
137
+ }
138
+ pi.on('before_agent_start', async (event, ctx) => {
152
139
  if (ctx?.hasUI)
153
140
  reconcile(ctx);
154
141
  if (!isOmpSubagentPrompt(event.systemPrompt))
155
142
  return;
156
143
  const mode = getSharedComboState().ponytail;
157
- if (mode === "off" || hasPonytailInstructions(event.systemPrompt))
144
+ if (mode === 'off' || systemPromptIncludes(event.systemPrompt, 'PONYTAIL MODE ACTIVE'))
158
145
  return;
159
- const base = Array.isArray(event.systemPrompt) ? event.systemPrompt : [event.systemPrompt];
146
+ const base = asPromptArray(event.systemPrompt);
160
147
  return { systemPrompt: [...base, loadPonytailInstructions(mode)] };
161
148
  });
162
149
  // 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
  }