prior-cli 1.7.12 → 1.7.14
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/bin/prior.js +278 -19
- package/package.json +1 -1
package/bin/prior.js
CHANGED
|
@@ -15,6 +15,36 @@ const { renderMarkdown } = require('../lib/render');
|
|
|
15
15
|
const { getToken, getUsername, saveAuth, clearAuth } = require('../lib/config');
|
|
16
16
|
const { runAgent, CONFIRM_TOOLS } = require('../lib/agent');
|
|
17
17
|
|
|
18
|
+
// ── Packaged-executable detection + self-update support ─────────
|
|
19
|
+
// True when running as a bundled single-file binary (pkg) rather than via Node+npm.
|
|
20
|
+
const _execBase = path.basename(process.execPath).toLowerCase();
|
|
21
|
+
const IS_EXE = !!process.pkg || (_execBase !== 'node.exe' && _execBase !== 'node');
|
|
22
|
+
const GH_REPO = 'PriorNetwork/prior-cli';
|
|
23
|
+
|
|
24
|
+
// Best-effort cleanup of a leftover *.old.exe from a previous self-update.
|
|
25
|
+
if (IS_EXE) {
|
|
26
|
+
try {
|
|
27
|
+
const oldExe = process.execPath.replace(/\.exe$/i, '') + '.old.exe';
|
|
28
|
+
if (fs.existsSync(oldExe)) fs.unlinkSync(oldExe);
|
|
29
|
+
} catch { /* still locked from last run — retry next launch */ }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Download the latest release exe and swap it in (Windows lets us rename a
|
|
33
|
+
// running exe, so: current → .old, downloaded → current; .old is deleted next launch).
|
|
34
|
+
async function selfUpdateExe(assetUrl) {
|
|
35
|
+
const _fetch = require('node-fetch');
|
|
36
|
+
const exePath = process.execPath;
|
|
37
|
+
const stem = exePath.replace(/\.exe$/i, '');
|
|
38
|
+
const tmp = stem + '.new.exe';
|
|
39
|
+
const old = stem + '.old.exe';
|
|
40
|
+
const res = await _fetch(assetUrl, { timeout: 180000, redirect: 'follow', headers: { 'User-Agent': 'prior-cli' } });
|
|
41
|
+
if (!res.ok) throw new Error(`download HTTP ${res.status}`);
|
|
42
|
+
fs.writeFileSync(tmp, Buffer.from(await res.arrayBuffer()));
|
|
43
|
+
try { if (fs.existsSync(old)) fs.unlinkSync(old); } catch {}
|
|
44
|
+
fs.renameSync(exePath, old); // rename the running exe out of the way
|
|
45
|
+
fs.renameSync(tmp, exePath); // move the new build into place
|
|
46
|
+
}
|
|
47
|
+
|
|
18
48
|
function decodeToken(token) {
|
|
19
49
|
try {
|
|
20
50
|
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
|
@@ -231,9 +261,9 @@ function expandFileRefs(input, cwd) {
|
|
|
231
261
|
}
|
|
232
262
|
|
|
233
263
|
// Live completions for an @file token being typed. `partial` is the text
|
|
234
|
-
// after @ (e.g. "lib/to"). Returns up to `limit` matches,
|
|
264
|
+
// after @ (e.g. "lib/to"). Returns up to `limit` matches, alphabetical (files + folders mixed).
|
|
235
265
|
const REF_SKIP_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.cache', 'coverage', 'vendor']);
|
|
236
|
-
function fileRefSuggestions(partial, cwd, limit =
|
|
266
|
+
function fileRefSuggestions(partial, cwd, limit = 10) {
|
|
237
267
|
const slash = Math.max(partial.lastIndexOf('/'), partial.lastIndexOf('\\'));
|
|
238
268
|
const dirPart = slash >= 0 ? partial.slice(0, slash) : '';
|
|
239
269
|
const prefix = (slash >= 0 ? partial.slice(slash + 1) : partial).toLowerCase();
|
|
@@ -250,7 +280,7 @@ function fileRefSuggestions(partial, cwd, limit = 6) {
|
|
|
250
280
|
const ref = (dirPart ? dirPart.replace(/\\/g, '/') + '/' : '') + e.name;
|
|
251
281
|
out.push({ ref, isDir: e.isDirectory() });
|
|
252
282
|
}
|
|
253
|
-
out.sort((a, b) =>
|
|
283
|
+
out.sort((a, b) => a.ref.toLowerCase().localeCompare(b.ref.toLowerCase())); // alphabetical, files + folders mixed
|
|
254
284
|
return out.slice(0, limit);
|
|
255
285
|
}
|
|
256
286
|
|
|
@@ -1003,22 +1033,6 @@ async function startChat(opts = {}) {
|
|
|
1003
1033
|
}
|
|
1004
1034
|
}
|
|
1005
1035
|
|
|
1006
|
-
// @file completions — when the token at the end of the line starts with @
|
|
1007
|
-
const atMatch = (line || '').match(/(?:^|\s)@([^\s]*)$/);
|
|
1008
|
-
if (atMatch) {
|
|
1009
|
-
const sugg = fileRefSuggestions(atMatch[1], process.cwd());
|
|
1010
|
-
for (const s of sugg) {
|
|
1011
|
-
const label = s.isDir ? s.ref + '/' : s.ref;
|
|
1012
|
-
const icon = s.isDir ? '📁' : '📄';
|
|
1013
|
-
process.stdout.write(`\x1b[B\r\x1b[2K ${c.brand('@')}${c.white(label.padEnd(30))}${c.dim(icon)}`);
|
|
1014
|
-
rows++;
|
|
1015
|
-
}
|
|
1016
|
-
if (sugg.length) {
|
|
1017
|
-
process.stdout.write(`\x1b[B\r\x1b[2K ${c.dim('tab to complete · attaches file contents as context')}`);
|
|
1018
|
-
rows++;
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
1036
|
process.stdout.write('\x1b[u');
|
|
1023
1037
|
_subRowCount = rows;
|
|
1024
1038
|
}
|
|
@@ -1032,6 +1046,158 @@ async function startChat(opts = {}) {
|
|
|
1032
1046
|
_subRowCount = 1;
|
|
1033
1047
|
}
|
|
1034
1048
|
|
|
1049
|
+
// ── Interactive @file picker ─────────────────────────────────
|
|
1050
|
+
// While open, it OWNS the keyboard (↑/↓/Enter/Tab/Esc/typing) so the
|
|
1051
|
+
// arrow keys don't fall through to readline's history. We swap readline's
|
|
1052
|
+
// keypress listener out for ours, then restore it when the picker closes.
|
|
1053
|
+
let _atActive = false;
|
|
1054
|
+
let _atItems = [];
|
|
1055
|
+
let _atSel = 0;
|
|
1056
|
+
let _atToken = '';
|
|
1057
|
+
let _atStart = 0;
|
|
1058
|
+
let _savedKp = null;
|
|
1059
|
+
let _atRows = 0; // rows currently drawn below the input by the picker
|
|
1060
|
+
let _atView = 0; // index of the first visible item (scrolling viewport)
|
|
1061
|
+
const AT_MAX_VISIBLE = 8;
|
|
1062
|
+
|
|
1063
|
+
function currentAtToken() {
|
|
1064
|
+
const line = rl.line || '';
|
|
1065
|
+
const before = line.slice(0, rl.cursor);
|
|
1066
|
+
const m = before.match(/(?:^|\s)@([^\s]*)$/);
|
|
1067
|
+
if (!m) return null;
|
|
1068
|
+
return { token: m[1], start: rl.cursor - m[1].length - 1 };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function refreshAtItems() {
|
|
1072
|
+
const t = currentAtToken();
|
|
1073
|
+
if (!t) return false;
|
|
1074
|
+
_atToken = t.token;
|
|
1075
|
+
_atStart = t.start;
|
|
1076
|
+
_atItems = fileRefSuggestions(_atToken, process.cwd(), 500); // fetch all; viewport scrolls
|
|
1077
|
+
_atView = 0;
|
|
1078
|
+
if (_atSel >= _atItems.length) _atSel = Math.max(0, _atItems.length - 1);
|
|
1079
|
+
return _atItems.length > 0;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Column of the readline cursor on the input line (visible prompt is 4 cols wide)
|
|
1083
|
+
function atInputCol() { return 4 + (rl.cursor || 0); }
|
|
1084
|
+
|
|
1085
|
+
// Return the terminal cursor to the input line, `fromRows` lines above us.
|
|
1086
|
+
// Uses RELATIVE moves so it survives the terminal scrolling when the picker
|
|
1087
|
+
// is drawn near the bottom of the screen (the bug where the hint overlapped @).
|
|
1088
|
+
function atMoveToInput(fromRows) {
|
|
1089
|
+
if (fromRows > 0) process.stdout.write(`\x1b[${fromRows}A`);
|
|
1090
|
+
process.stdout.write('\r');
|
|
1091
|
+
const col = atInputCol();
|
|
1092
|
+
if (col > 0) process.stdout.write(`\x1b[${col}C`);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function clearAtRows() {
|
|
1096
|
+
if (_atRows > 0) {
|
|
1097
|
+
for (let i = 0; i < _atRows; i++) process.stdout.write('\x1b[B\r\x1b[2K');
|
|
1098
|
+
atMoveToInput(_atRows);
|
|
1099
|
+
_atRows = 0;
|
|
1100
|
+
}
|
|
1101
|
+
_subRowCount = 0;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function renderAtList() {
|
|
1105
|
+
clearAtRows(); // assumes cursor is on the input line
|
|
1106
|
+
const total = _atItems.length;
|
|
1107
|
+
// Keep the selected item inside the visible window
|
|
1108
|
+
if (_atSel < _atView) _atView = _atSel;
|
|
1109
|
+
if (_atSel >= _atView + AT_MAX_VISIBLE) _atView = _atSel - AT_MAX_VISIBLE + 1;
|
|
1110
|
+
const end = Math.min(_atView + AT_MAX_VISIBLE, total);
|
|
1111
|
+
|
|
1112
|
+
const lines = [];
|
|
1113
|
+
for (let i = _atView; i < end; i++) {
|
|
1114
|
+
const it = _atItems[i];
|
|
1115
|
+
const label = it.isDir ? it.ref + '/' : it.ref;
|
|
1116
|
+
const arrow = (i === end - 1 && end < total) ? c.dim(' ↓')
|
|
1117
|
+
: (i === _atView && _atView > 0) ? c.dim(' ↑') : '';
|
|
1118
|
+
lines.push((i === _atSel
|
|
1119
|
+
? c.brand('❯ ') + c.bold(label)
|
|
1120
|
+
: c.muted(' ') + c.white(label)) + arrow);
|
|
1121
|
+
}
|
|
1122
|
+
const pos = total > AT_MAX_VISIBLE ? ` · ${_atSel + 1}/${total}` : '';
|
|
1123
|
+
lines.push(c.dim('↑↓ select · enter/tab insert · esc dismiss' + pos));
|
|
1124
|
+
|
|
1125
|
+
for (const ln of lines) process.stdout.write(`\x1b[B\r\x1b[2K ${ln}`);
|
|
1126
|
+
atMoveToInput(lines.length); // relative — scroll-safe
|
|
1127
|
+
_atRows = lines.length;
|
|
1128
|
+
_subRowCount = lines.length;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function openAtPicker() {
|
|
1132
|
+
_atSel = 0;
|
|
1133
|
+
if (!refreshAtItems()) return;
|
|
1134
|
+
_atActive = true;
|
|
1135
|
+
// Take over keypress handling from readline
|
|
1136
|
+
_savedKp = process.stdin.listeners('keypress').slice();
|
|
1137
|
+
process.stdin.removeAllListeners('keypress');
|
|
1138
|
+
process.stdin.on('keypress', atKeyHandler);
|
|
1139
|
+
renderAtList();
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function closeAtPicker() {
|
|
1143
|
+
if (!_atActive) return;
|
|
1144
|
+
_atActive = false;
|
|
1145
|
+
_atItems = [];
|
|
1146
|
+
_atSel = 0;
|
|
1147
|
+
if (_savedKp) {
|
|
1148
|
+
process.stdin.removeListener('keypress', atKeyHandler);
|
|
1149
|
+
for (const l of _savedKp) process.stdin.on('keypress', l);
|
|
1150
|
+
_savedKp = null;
|
|
1151
|
+
}
|
|
1152
|
+
clearAtRows();
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function acceptAt() {
|
|
1156
|
+
const it = _atItems[_atSel];
|
|
1157
|
+
if (!it) { closeAtPicker(); return; }
|
|
1158
|
+
const insert = it.isDir ? it.ref + '/' : it.ref;
|
|
1159
|
+
const line = rl.line;
|
|
1160
|
+
const before = line.slice(0, _atStart + 1); // up to & incl. '@'
|
|
1161
|
+
const after = line.slice(_atStart + 1 + _atToken.length);
|
|
1162
|
+
const newLine = before + insert + after;
|
|
1163
|
+
rl.line = newLine;
|
|
1164
|
+
rl.cursor = (before + insert).length;
|
|
1165
|
+
rl._refreshLine();
|
|
1166
|
+
if (it.isDir) { // drill into the folder, keep picking
|
|
1167
|
+
_atSel = 0;
|
|
1168
|
+
if (refreshAtItems()) { renderAtList(); return; }
|
|
1169
|
+
}
|
|
1170
|
+
closeAtPicker();
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function atKeyHandler(ch, key) {
|
|
1174
|
+
if (!key) return;
|
|
1175
|
+
if (key.name === 'up') { _atSel = (_atSel - 1 + _atItems.length) % _atItems.length; renderAtList(); return; }
|
|
1176
|
+
if (key.name === 'down') { _atSel = (_atSel + 1) % _atItems.length; renderAtList(); return; }
|
|
1177
|
+
if (key.name === 'escape') { closeAtPicker(); return; }
|
|
1178
|
+
if (key.name === 'tab' || key.name === 'return' || key.name === 'enter') { acceptAt(); return; }
|
|
1179
|
+
if (key.ctrl && key.name === 'c') { closeAtPicker(); process.exit(0); }
|
|
1180
|
+
if (key.name === 'backspace') {
|
|
1181
|
+
if (rl.cursor > 0) {
|
|
1182
|
+
rl.line = rl.line.slice(0, rl.cursor - 1) + rl.line.slice(rl.cursor);
|
|
1183
|
+
rl.cursor = rl.cursor - 1;
|
|
1184
|
+
rl._refreshLine();
|
|
1185
|
+
}
|
|
1186
|
+
if (!refreshAtItems()) { closeAtPicker(); return; }
|
|
1187
|
+
renderAtList();
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
// Printable character — insert it, keep filtering
|
|
1191
|
+
if (ch && !key.ctrl && !key.meta && ch.length === 1 && ch >= ' ') {
|
|
1192
|
+
rl.line = rl.line.slice(0, rl.cursor) + ch + rl.line.slice(rl.cursor);
|
|
1193
|
+
rl.cursor = rl.cursor + 1;
|
|
1194
|
+
rl._refreshLine();
|
|
1195
|
+
if (ch === ' ') { closeAtPicker(); return; } // space ends the @token
|
|
1196
|
+
if (!refreshAtItems()) { closeAtPicker(); return; }
|
|
1197
|
+
renderAtList();
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1035
1201
|
process.stdin.on('keypress', (ch, key) => {
|
|
1036
1202
|
if (!key) return;
|
|
1037
1203
|
|
|
@@ -1070,6 +1236,14 @@ async function startChat(opts = {}) {
|
|
|
1070
1236
|
// Redraw sub-rows on every keypress so backspace / typing never wipes them
|
|
1071
1237
|
_suggTimer = setTimeout(() => {
|
|
1072
1238
|
_suggTimer = null;
|
|
1239
|
+
// If the user just started an @file token, hand control to the picker
|
|
1240
|
+
if (!_atActive) {
|
|
1241
|
+
const t = currentAtToken();
|
|
1242
|
+
if (t && fileRefSuggestions(t.token, process.cwd(), 1).length) {
|
|
1243
|
+
openAtPicker();
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1073
1247
|
renderSubRows(rl.line || '');
|
|
1074
1248
|
}, 50);
|
|
1075
1249
|
});
|
|
@@ -1505,6 +1679,49 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
1505
1679
|
console.log('');
|
|
1506
1680
|
process.stdout.write(c.dim(' Checking for updates…'));
|
|
1507
1681
|
const _fetch = require('node-fetch');
|
|
1682
|
+
|
|
1683
|
+
// ── Standalone exe → self-update from GitHub Releases ──
|
|
1684
|
+
if (IS_EXE) {
|
|
1685
|
+
let _rel;
|
|
1686
|
+
try {
|
|
1687
|
+
const _r = await _fetch(`https://api.github.com/repos/${GH_REPO}/releases/latest`,
|
|
1688
|
+
{ timeout: 8000, headers: { 'User-Agent': 'prior-cli', Accept: 'application/vnd.github+json' } });
|
|
1689
|
+
if (!_r.ok) throw new Error(`HTTP ${_r.status}`);
|
|
1690
|
+
_rel = await _r.json();
|
|
1691
|
+
} catch (err) {
|
|
1692
|
+
process.stdout.clearLine(0); process.stdout.cursorTo(0);
|
|
1693
|
+
console.log(c.err(` ✗ Could not reach GitHub Releases: ${err.message}\n`));
|
|
1694
|
+
return loop();
|
|
1695
|
+
}
|
|
1696
|
+
const _ghLatest = (_rel.tag_name || '').replace(/^v/, '');
|
|
1697
|
+
process.stdout.clearLine(0); process.stdout.cursorTo(0);
|
|
1698
|
+
if (!_ghLatest || _ghLatest === version) {
|
|
1699
|
+
console.log(c.ok(' ✓ Already up to date ') + c.muted(`v${version}\n`));
|
|
1700
|
+
return loop();
|
|
1701
|
+
}
|
|
1702
|
+
const _asset = (_rel.assets || []).find(a => /prior.*\.exe$/i.test(a.name));
|
|
1703
|
+
if (!_asset) {
|
|
1704
|
+
console.log(c.err(' ✗ Latest release has no .exe asset'));
|
|
1705
|
+
console.log(c.muted(` Get it manually: https://github.com/${GH_REPO}/releases/latest\n`));
|
|
1706
|
+
return loop();
|
|
1707
|
+
}
|
|
1708
|
+
console.log(` ${c.muted('Current :')} ${c.white(`v${version}`)}`);
|
|
1709
|
+
console.log(` ${c.muted('Latest :')} ${c.bold(`v${_ghLatest}`)}`);
|
|
1710
|
+
console.log('');
|
|
1711
|
+
process.stdout.write(c.dim(' Downloading update…'));
|
|
1712
|
+
try {
|
|
1713
|
+
await selfUpdateExe(_asset.browser_download_url);
|
|
1714
|
+
process.stdout.clearLine(0); process.stdout.cursorTo(0);
|
|
1715
|
+
console.log(c.ok(` ✓ Updated to v${_ghLatest} `) + c.muted('restart prior to apply\n'));
|
|
1716
|
+
} catch (err) {
|
|
1717
|
+
process.stdout.clearLine(0); process.stdout.cursorTo(0);
|
|
1718
|
+
console.log(c.err(` ✗ Update failed: ${err.message}`));
|
|
1719
|
+
console.log(c.muted(` Download manually: https://github.com/${GH_REPO}/releases/latest\n`));
|
|
1720
|
+
}
|
|
1721
|
+
return loop();
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// ── npm install → update via npm ──────────────────────
|
|
1508
1725
|
let _latest;
|
|
1509
1726
|
try {
|
|
1510
1727
|
const _res = await _fetch('https://registry.npmjs.org/prior-cli/latest', { timeout: 8000 });
|
|
@@ -2141,6 +2358,48 @@ program
|
|
|
2141
2358
|
process.stdout.write(c.dim(' Checking for updates…'));
|
|
2142
2359
|
|
|
2143
2360
|
const fetch = require('node-fetch');
|
|
2361
|
+
|
|
2362
|
+
// Standalone exe → self-update from GitHub Releases
|
|
2363
|
+
if (IS_EXE) {
|
|
2364
|
+
let rel;
|
|
2365
|
+
try {
|
|
2366
|
+
const r = await fetch(`https://api.github.com/repos/${GH_REPO}/releases/latest`,
|
|
2367
|
+
{ timeout: 8000, headers: { 'User-Agent': 'prior-cli', Accept: 'application/vnd.github+json' } });
|
|
2368
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
2369
|
+
rel = await r.json();
|
|
2370
|
+
} catch (err) {
|
|
2371
|
+
clearLine();
|
|
2372
|
+
console.error(c.err(` ✗ Could not reach GitHub Releases: ${err.message}\n`));
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
const ghLatest = (rel.tag_name || '').replace(/^v/, '');
|
|
2376
|
+
clearLine();
|
|
2377
|
+
if (!ghLatest || ghLatest === version) {
|
|
2378
|
+
console.log(c.ok(' ✓ Already up to date ') + c.muted(`v${version}\n`));
|
|
2379
|
+
return;
|
|
2380
|
+
}
|
|
2381
|
+
const asset = (rel.assets || []).find(a => /prior.*\.exe$/i.test(a.name));
|
|
2382
|
+
if (!asset) {
|
|
2383
|
+
console.error(c.err(' ✗ Latest release has no .exe asset'));
|
|
2384
|
+
console.error(c.muted(` Get it manually: https://github.com/${GH_REPO}/releases/latest\n`));
|
|
2385
|
+
return;
|
|
2386
|
+
}
|
|
2387
|
+
console.log(` ${c.muted('Current :')} ${c.white(`v${version}`)}`);
|
|
2388
|
+
console.log(` ${c.muted('Latest :')} ${c.bold(`v${ghLatest}`)}`);
|
|
2389
|
+
console.log('');
|
|
2390
|
+
process.stdout.write(c.dim(' Downloading update…'));
|
|
2391
|
+
try {
|
|
2392
|
+
await selfUpdateExe(asset.browser_download_url);
|
|
2393
|
+
clearLine();
|
|
2394
|
+
console.log(c.ok(` ✓ Updated to v${ghLatest} `) + c.muted('restart prior to apply\n'));
|
|
2395
|
+
} catch (err) {
|
|
2396
|
+
clearLine();
|
|
2397
|
+
console.error(c.err(` ✗ Update failed: ${err.message}`));
|
|
2398
|
+
console.error(c.muted(` Download manually: https://github.com/${GH_REPO}/releases/latest\n`));
|
|
2399
|
+
}
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
|
|
2144
2403
|
let latest;
|
|
2145
2404
|
try {
|
|
2146
2405
|
const res = await fetch('https://registry.npmjs.org/prior-cli/latest', { timeout: 8000 });
|