@havocrao/picktui 0.1.0 → 0.1.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,24 +1,95 @@
1
1
  /**
2
- * confirm — 自动匹配首次确认(文件状态,直调引擎 confirm 子命令)。
2
+ * confirm — 自动匹配的首次用户确认记录(confirm.toml)。
3
3
  *
4
- * 状态文件位于引擎数据目录,与 pick --auto 的确认记录同一份:
5
- * 首次自动匹配某 (label, value) 需确认,确认后不再询问。
4
+ * 文件格式与 Go 引擎 BurntSushi/toml 输出完全兼容:
5
+ *
6
+ * # picktui confirm — user-confirmed auto resolutions (auto-managed)
7
+ * [confirmed]
8
+ * "npm run" = ["release", "dev"]
9
+ *
10
+ * label 为空时不做任何落盘(返回 false)。文件不存在或损坏 → 空记录
11
+ * (确认记录只是安全辅助,缺失最多导致首次匹配再次询问)。
6
12
  */
7
- import { assertSuccess, invokeEngine, parseJSON } from './engine.js';
13
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ import { dataDir } from './config.js';
16
+ import { parseToml, escapeTomlString, keyLiteral } from './toml.js';
17
+ import { PicktuiError } from './types.js';
18
+ /** 自动匹配确认记录文件名。 */
19
+ export const CONFIRM_FILE = 'confirm.toml';
20
+ /** 注释头(对齐引擎输出)。 */
21
+ const HEADER = '# picktui confirm — user-confirmed auto resolutions (auto-managed)';
22
+ /** 返回确认记录文件路径。 */
23
+ export function confirmPath() {
24
+ return join(dataDir(), CONFIRM_FILE);
25
+ }
26
+ /** 解码 confirm.toml 为 label → 已确认值列表;缺失/损坏时返回空 Map。 */
27
+ export function loadConfirmed() {
28
+ const p = confirmPath();
29
+ if (!existsSync(p)) {
30
+ return new Map();
31
+ }
32
+ const doc = parseToml(readFileSync(p, 'utf8'));
33
+ if (doc === null) {
34
+ return new Map();
35
+ }
36
+ const confirmed = doc.get('confirmed');
37
+ if (!confirmed) {
38
+ return new Map();
39
+ }
40
+ const out = new Map();
41
+ for (const [label, values] of confirmed) {
42
+ if (Array.isArray(values)) {
43
+ out.set(label, values.map(String));
44
+ }
45
+ }
46
+ return out;
47
+ }
48
+ /** 报告 (label, value) 是否已被确认过;label 为空时恒为 false。 */
49
+ export function isConfirmed(label, value) {
50
+ if (label === '') {
51
+ return false;
52
+ }
53
+ return (loadConfirmed().get(label) ?? []).includes(value);
54
+ }
55
+ /** 记录 (label, value) 为已确认(幂等;label 为空不落盘)。 */
56
+ export function confirm(label, value) {
57
+ if (label === '') {
58
+ return;
59
+ }
60
+ const m = loadConfirmed();
61
+ const list = m.get(label) ?? [];
62
+ if (list.includes(value)) {
63
+ return;
64
+ }
65
+ list.push(value);
66
+ m.set(label, list);
67
+ const lines = [HEADER, '[confirmed]'];
68
+ const labels = [...m.keys()].sort();
69
+ for (const l of labels) {
70
+ const values = (m.get(l) ?? []).map(escapeTomlString).map((v) => `"${v}"`);
71
+ lines.push(`${keyLiteral(l)} = [${values.join(', ')}]`);
72
+ }
73
+ const p = confirmPath();
74
+ try {
75
+ mkdirSync(dirname(p), { recursive: true });
76
+ writeFileSync(p, lines.join('\n') + '\n', { mode: 0o644 });
77
+ }
78
+ catch (err) {
79
+ throw new PicktuiError(`写入确认记录失败:${err instanceof Error ? err.message : String(err)}`, 1, '');
80
+ }
81
+ }
82
+ /** 解析确认回答:y/yes(大小写不敏感,容忍首尾空白)为确认。 */
83
+ export function parseConfirmAnswer(s) {
84
+ const t = s.trim().toLowerCase();
85
+ return t === 'y' || t === 'yes';
86
+ }
8
87
  /** 报告 (label, value) 是否已被确认过。 */
9
88
  export async function confirmCheck(label, value) {
10
- const inv = await invokeEngine(['confirm', 'check', label, value]);
11
- assertSuccess(inv, 'picktui confirm check');
12
- const parsed = parseJSON(inv.stdout, 'picktui confirm check');
13
- return parsed.confirmed;
89
+ return isConfirmed(label, value);
14
90
  }
15
- /**
16
- * 记录 (label, value) 为已确认(幂等)。
17
- * @returns 确认后的实际状态(label 为空时不落盘 → false)
18
- */
91
+ /** 记录 (label, value) 为已确认(幂等);返回确认后的实际状态。 */
19
92
  export async function confirmAdd(label, value) {
20
- const inv = await invokeEngine(['confirm', 'add', label, value]);
21
- assertSuccess(inv, 'picktui confirm add');
22
- const parsed = parseJSON(inv.stdout, 'picktui confirm add');
23
- return parsed.confirmed;
93
+ confirm(label, value);
94
+ return isConfirmed(label, value);
24
95
  }
@@ -1,45 +1,297 @@
1
1
  /**
2
- * filter / resolve — 纯函数数据面(直调引擎 filter/resolve --json)。
2
+ * filter / resolve — TS 过滤匹配引擎(对齐 Go filter.go,零依赖)。
3
3
  *
4
- * 过滤与高亮区间由引擎权威计算;本模块只做「候选传入、JSON 取回」,
5
- * 禁止重实现匹配(详见 docs/integration/protocol.md 兼容性承诺)。
4
+ * 三种匹配模式(与协议一致):
5
+ * - 子串 AND(默认):空格分关键字,全部命中才保留,大小写不敏感
6
+ * - token 前缀(--sep <chars>):按分隔符集合切 token(默认 _,可多字符如 ":_"),
7
+ * 每关键字匹配某 token 前缀
8
+ * - 子序列模糊(--fuzzy):关键字为子序列
9
+ *
10
+ * 高亮区间为 rune(code point)索引 [start, end),排序合并;空查询恒 []。
6
11
  */
7
- import { assertSuccess, invokeEngine, parseJSON, serializeCandidates, } from './engine.js';
8
- /** FilterOptions 转为引擎参数。 */
9
- function modeArgs(opts) {
10
- const args = [];
11
- if (opts.mode && opts.mode !== 'substring') {
12
- args.push('--mode', opts.mode);
12
+ import { toStructuredCands } from './cand.js';
13
+ /** 默认分隔符集合(token 模式)。 */
14
+ const DEFAULT_SEP = '_';
15
+ /** 将查询字符串拆分为小写关键字列表(空格分词、小写化、过滤空串)。 */
16
+ export function splitKeywords(query) {
17
+ const fields = query.trim().split(/\s+/).filter(Boolean);
18
+ return fields.map((f) => f.toLowerCase());
19
+ }
20
+ /** 按 rune 切分字符串为 code point 数组(与 Go []rune 对齐)。 */
21
+ function toRunes(s) {
22
+ return [...s];
23
+ }
24
+ /** 某 rune 是否在分隔符集合中。 */
25
+ function sepSetOf(sep) {
26
+ return new Set(toRunes(sep));
27
+ }
28
+ /** 按分隔符集合切分 runes,返回每个非空 token 的 [start, end) rune 索引区间。 */
29
+ function splitRuneRanges(runes, seps) {
30
+ if (seps.size === 0) {
31
+ return runes.length > 0 ? [[0, runes.length]] : [];
32
+ }
33
+ const ranges = [];
34
+ let start = 0;
35
+ for (let i = 0; i < runes.length; i++) {
36
+ if (seps.has(runes[i])) {
37
+ if (i > start) {
38
+ ranges.push([start, i]);
39
+ }
40
+ start = i + 1;
41
+ }
42
+ }
43
+ if (start < runes.length) {
44
+ ranges.push([start, runes.length]);
45
+ }
46
+ return ranges;
47
+ }
48
+ /** 按分隔符集合切分字符串为 token 列表(供匹配用)。 */
49
+ function splitTokens(s, seps) {
50
+ if (seps.size === 0) {
51
+ return [s];
52
+ }
53
+ const tokens = [];
54
+ let cur = '';
55
+ for (const r of s) {
56
+ if (seps.has(r)) {
57
+ if (cur !== '') {
58
+ tokens.push(cur);
59
+ cur = '';
60
+ }
61
+ }
62
+ else {
63
+ cur += r;
64
+ }
65
+ }
66
+ if (cur !== '') {
67
+ tokens.push(cur);
68
+ }
69
+ return tokens;
70
+ }
71
+ /** 子串 AND:所有关键字都需作为子串出现(大小写不敏感)。 */
72
+ export function matchSubstringAll(s, kws) {
73
+ const lower = s.toLowerCase();
74
+ return kws.every((kw) => lower.includes(kw));
75
+ }
76
+ /** token 前缀:按分隔符集合切 token,每关键字须匹配某 token 的前缀。 */
77
+ export function matchTokenPrefixAll(s, kws, sep) {
78
+ const seps = sepSetOf(sep === '' ? DEFAULT_SEP : sep);
79
+ const tokens = splitTokens(s.toLowerCase(), seps);
80
+ return kws.every((kw) => tokens.some((tok) => tok.startsWith(kw)));
81
+ }
82
+ /** 子序列模糊:所有关键字都需作为子序列出现(大小写不敏感)。 */
83
+ export function matchFuzzyAll(s, kws) {
84
+ const lower = s.toLowerCase();
85
+ return kws.every((kw) => isSubsequence(kw, lower));
86
+ }
87
+ /** pat 是否为 s 的子序列(不要求连续)。 */
88
+ export function isSubsequence(pat, s) {
89
+ if (pat === '') {
90
+ return true;
13
91
  }
14
- if (opts.mode === 'token' && opts.sep) {
15
- args.push('--sep', opts.sep);
92
+ let pi = 0;
93
+ for (let si = 0; si < s.length && pi < pat.length; si++) {
94
+ if (s[si] === pat[pi]) {
95
+ pi++;
96
+ }
16
97
  }
17
- return args;
98
+ return pi === pat.length;
99
+ }
100
+ /** rune 索引:在 s 中查找 substr 的首个出现位置,未找到返回 -1。 */
101
+ function runeIndex(s, substr) {
102
+ if (substr.length === 0 || substr.length > s.length) {
103
+ return -1;
104
+ }
105
+ for (let i = 0; i <= s.length - substr.length; i++) {
106
+ let match = true;
107
+ for (let j = 0; j < substr.length; j++) {
108
+ if (s[i + j] !== substr[j]) {
109
+ match = false;
110
+ break;
111
+ }
112
+ }
113
+ if (match) {
114
+ return i;
115
+ }
116
+ }
117
+ return -1;
118
+ }
119
+ /** 排序并合并重叠的 [start, end) 区间(与 Go mergeRanges 一致)。 */
120
+ export function mergeRanges(ranges) {
121
+ if (ranges.length === 0) {
122
+ return [];
123
+ }
124
+ const sorted = [...ranges].sort((a, b) => (a[0] !== b[0] ? a[0] - b[0] : a[1] - b[1]));
125
+ const merged = [sorted[0]];
126
+ for (let i = 1; i < sorted.length; i++) {
127
+ const last = merged[merged.length - 1];
128
+ const r = sorted[i];
129
+ if (r[0] <= last[1]) {
130
+ if (r[1] > last[1]) {
131
+ last[1] = r[1];
132
+ }
133
+ }
134
+ else {
135
+ merged.push(r);
136
+ }
137
+ }
138
+ return merged;
139
+ }
140
+ /**
141
+ * highlightRanges 返回 s 中匹配关键字的 [start, end) rune 索引区间(已排序合并)。
142
+ * 空查询返回 []。
143
+ */
144
+ export function highlightRanges(s, query, opts = {}) {
145
+ const kws = splitKeywords(query);
146
+ if (kws.length === 0) {
147
+ return [];
148
+ }
149
+ const lowerRunes = toRunes(s.toLowerCase());
150
+ const ranges = [];
151
+ const mode = opts.mode ?? 'substring';
152
+ if (mode === 'token') {
153
+ const seps = sepSetOf(opts.sep === undefined || opts.sep === '' ? DEFAULT_SEP : opts.sep);
154
+ const tokRanges = splitRuneRanges(lowerRunes, seps);
155
+ for (const kw of kws) {
156
+ const kwRunes = toRunes(kw);
157
+ if (kwRunes.length === 0) {
158
+ continue;
159
+ }
160
+ for (const tr of tokRanges) {
161
+ const tok = lowerRunes.slice(tr[0], tr[1]);
162
+ if (hasRunePrefix(tok, kwRunes)) {
163
+ ranges.push([tr[0], tr[0] + kwRunes.length]);
164
+ break;
165
+ }
166
+ }
167
+ }
168
+ }
169
+ else if (mode === 'fuzzy') {
170
+ for (const kw of kws) {
171
+ const positions = subsequenceRunePositions(toRunes(kw), lowerRunes);
172
+ for (const p of positions) {
173
+ ranges.push([p, p + 1]);
174
+ }
175
+ }
176
+ }
177
+ else {
178
+ for (const kw of kws) {
179
+ const kwRunes = toRunes(kw);
180
+ if (kwRunes.length === 0) {
181
+ continue;
182
+ }
183
+ const idx = runeIndex(lowerRunes, kwRunes);
184
+ if (idx >= 0) {
185
+ ranges.push([idx, idx + kwRunes.length]);
186
+ }
187
+ }
188
+ }
189
+ return mergeRanges(ranges);
190
+ }
191
+ function hasRunePrefix(s, prefix) {
192
+ if (prefix.length > s.length) {
193
+ return false;
194
+ }
195
+ for (let i = 0; i < prefix.length; i++) {
196
+ if (s[i] !== prefix[i]) {
197
+ return false;
198
+ }
199
+ }
200
+ return true;
201
+ }
202
+ /** 返回 pat 作为 s 子序列匹配时的各字符 rune 位置;不完整匹配返回 []。 */
203
+ function subsequenceRunePositions(pat, s) {
204
+ if (pat.length === 0) {
205
+ return [];
206
+ }
207
+ const positions = [];
208
+ let pi = 0;
209
+ for (let si = 0; si < s.length && pi < pat.length; si++) {
210
+ if (s[si] === pat[pi]) {
211
+ positions.push(si);
212
+ pi++;
213
+ }
214
+ }
215
+ if (pi < pat.length) {
216
+ return [];
217
+ }
218
+ return positions;
219
+ }
220
+ /** 匹配模式分派:单候选是否命中全部关键字。 */
221
+ export function matchCandidate(s, kws, opts = {}) {
222
+ switch (opts.mode ?? 'substring') {
223
+ case 'token':
224
+ return matchTokenPrefixAll(s, kws, opts.sep ?? '');
225
+ case 'fuzzy':
226
+ return matchFuzzyAll(s, kws);
227
+ default:
228
+ return matchSubstringAll(s, kws);
229
+ }
230
+ }
231
+ /** 归一化 FilterOptions(对齐协议 --mode/--sep 语义)。 */
232
+ export function normalizeOpts(opts = {}) {
233
+ return { mode: opts.mode ?? 'substring', sep: opts.sep ?? '' };
234
+ }
235
+ /**
236
+ * filterCands — 过滤 + 高亮区间(纯同步,保留输入顺序)。
237
+ * 描述不参与过滤——过滤永远针对"选中什么"而非"展示什么"。空查询返回全部。
238
+ */
239
+ export function filterCands(cands, query, opts = {}) {
240
+ const kws = splitKeywords(query);
241
+ if (kws.length === 0) {
242
+ return cands.map((c) => ({ value: c.value, desc: c.desc ?? '', ranges: [] }));
243
+ }
244
+ const norm = normalizeOpts(opts);
245
+ const out = [];
246
+ for (const c of cands) {
247
+ if (matchCandidate(c.value, kws, norm)) {
248
+ out.push({
249
+ value: c.value,
250
+ desc: c.desc ?? '',
251
+ ranges: highlightRanges(c.value, query, norm),
252
+ });
253
+ }
254
+ }
255
+ return out;
256
+ }
257
+ /**
258
+ * autoResolve — 把查询字符串解析为唯一候选(供 --auto 自动选中):
259
+ * 1. 精确匹配(大小写敏感)直接命中;
260
+ * 2. 否则大小写不敏感的唯一前缀匹配;
261
+ * 无匹配或多个前缀匹配时返回 null。
262
+ */
263
+ export function autoResolve(cands, query) {
264
+ if (query === '') {
265
+ return null;
266
+ }
267
+ for (const c of cands) {
268
+ if (c.value === query) {
269
+ return c.value;
270
+ }
271
+ }
272
+ const lq = query.toLowerCase();
273
+ let only = null;
274
+ for (const c of cands) {
275
+ if (c.value.toLowerCase().startsWith(lq)) {
276
+ if (only !== null && only !== c.value) {
277
+ return null; // 多个前缀匹配,无法唯一确定
278
+ }
279
+ only = c.value;
280
+ }
281
+ }
282
+ return only;
18
283
  }
19
284
  /**
20
285
  * filter — 过滤候选并返回命中项(保留输入顺序)+ 高亮区间。
21
- *
22
- * @param cands 候选(字符串或结构化 Candidate)
23
- * @param query 过滤关键字(空格分词,空串返回全部)
24
- * @param opts 匹配模式(默认 substring)
25
- * @returns 命中候选,`ranges` 为命中的 rune 区间(空查询恒为 [])
286
+ * 与协议 filter --json 等价;空查询返回全部(ranges 恒为 [])。
26
287
  */
27
- export async function filter(cands, query = '', opts = {}) {
28
- const inv = await invokeEngine(['filter', '--json', '--query', query, ...modeArgs(opts)], { input: serializeCandidates(cands) });
29
- assertSuccess(inv, 'picktui filter');
30
- return parseJSON(inv.stdout, 'picktui filter');
288
+ export function filter(cands, query = '', opts = {}) {
289
+ return Promise.resolve(filterCands(toStructuredCands(cands), query, opts));
31
290
  }
32
291
  /**
33
292
  * resolve — 把 query 解析为唯一候选(精确 → 唯一前缀,大小写不敏感前缀)。
34
- *
35
- * @returns 解析出的选中值;无匹配/多个前缀匹配返回 null(引擎退出码 1)
293
+ * @returns 解析出的选中值;无匹配/多个前缀匹配/空 query 返回 null
36
294
  */
37
- export async function resolve(cands, query, opts = {}) {
38
- const inv = await invokeEngine(['resolve', '--json', '--query', query, ...modeArgs(opts)], { input: serializeCandidates(cands) });
39
- if (inv.exitCode === 1) {
40
- return null; // 协议:无唯一解 → stdout 空 + 退出码 1
41
- }
42
- assertSuccess(inv, 'picktui resolve');
43
- const parsed = parseJSON(inv.stdout, 'picktui resolve');
44
- return parsed.value;
295
+ export function resolve(cands, query, _opts = {}) {
296
+ return Promise.resolve(autoResolve(toStructuredCands(cands), query));
45
297
  }
@@ -1,19 +1,108 @@
1
1
  /**
2
- * history — 选择记忆(文件状态,直调引擎 hist 子命令)。
2
+ * history — 选择记忆:记住每个选择器/歧义点上次选中的候选(history.toml)。
3
3
  *
4
- * 状态文件位于引擎数据目录($PICKTUI_CONFIG_DIR $XDG_CONFIG_HOME/picktui
5
- * ~/.config/picktui),与 pick --label 的 TUI 记忆同一份。
4
+ * 文件格式与 Go 引擎 BurntSushi/toml 输出完全兼容(同一数据目录共用):
5
+ *
6
+ * # picktui history — last selection per key (auto-managed)
7
+ * ["git p"]
8
+ * last = "pull"
9
+ *
10
+ * 曾用名 picks.toml 仍会被自动迁移到新位置(与引擎行为一致)。
11
+ * 文件不存在或损坏 → 空记录(记忆只是锦上添花,静默容错)。
6
12
  */
7
- import { assertSuccess, invokeEngine, parseJSON } from './engine.js';
13
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
14
+ import { dirname, join } from 'node:path';
15
+ import { dataDir } from './config.js';
16
+ import { parseToml, escapeTomlString, keyLiteral } from './toml.js';
17
+ import { PicktuiError } from './types.js';
18
+ /** 选择记忆文件名;曾用名 picks.toml 因易与 pick 命令混淆而弃用。 */
19
+ export const HISTORY_FILE = 'history.toml';
20
+ /** 旧版文件名,仅用于首次读取时自动迁移。 */
21
+ export const LEGACY_HISTORY_FILE = 'picks.toml';
22
+ /** 注释头(对齐引擎输出,便于用户辨识文件来源)。 */
23
+ const HEADER = '# picktui history — last selection per key (auto-managed)';
24
+ /** 返回选择记忆文件路径。 */
25
+ export function historyPath() {
26
+ return join(dataDir(), HISTORY_FILE);
27
+ }
28
+ /** history.toml 原始内容(label → { last }),与引擎同一份。 */
29
+ export function loadPickHistory() {
30
+ const doc = loadHistoryToml();
31
+ const out = new Map();
32
+ for (const label of doc.keys()) {
33
+ const table = doc.get(label);
34
+ if (!table) {
35
+ continue;
36
+ }
37
+ const last = table.get('last');
38
+ if (typeof last === 'string') {
39
+ out.set(label, last);
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+ /** 读取/迁移 history.toml:不存在时静默返回空 Map。 */
45
+ function loadHistoryToml() {
46
+ const p = historyPath();
47
+ if (existsSync(p)) {
48
+ return parseHistoryFile(readFileSync(p, 'utf8'));
49
+ }
50
+ const legacy = join(dataDir(), LEGACY_HISTORY_FILE);
51
+ if (existsSync(legacy)) {
52
+ const raw = parseHistoryFile(readFileSync(legacy, 'utf8'));
53
+ try {
54
+ writeHistoryEntries(raw);
55
+ rmSync(legacy, { force: true });
56
+ }
57
+ catch {
58
+ // 迁移写入失败不阻断读取——旧数据仍在内存中可用
59
+ }
60
+ return raw;
61
+ }
62
+ return new Map();
63
+ }
64
+ /** 解析 history.toml 文本;损坏时静默返回空(对齐引擎 LoadPickHistory)。 */
65
+ function parseHistoryFile(text) {
66
+ const doc = parseToml(text);
67
+ if (doc === null) {
68
+ return new Map();
69
+ }
70
+ return doc;
71
+ }
72
+ /** 取回 label 上次选中的值;无记录返回 ""。 */
73
+ export function lastPick(raw, label) {
74
+ return raw.get(label) ?? '';
75
+ }
76
+ /** 记录 label 的上次选中值(保留其它 label 的记忆后写回)。 */
77
+ export function savePick(label, value) {
78
+ const raw = loadPickHistory();
79
+ raw.set(label, value);
80
+ writeHistoryEntries(raw);
81
+ }
82
+ /** 将记忆写回 history.toml(自动建目录,对齐引擎输出格式)。 */
83
+ function writeHistoryEntries(entries) {
84
+ const lines = [HEADER];
85
+ const labels = [...entries.keys()].sort();
86
+ for (const label of labels) {
87
+ lines.push(`[${keyLiteral(label)}]`);
88
+ const table = entries.get(label);
89
+ const last = table instanceof Map ? table.get('last') : table;
90
+ lines.push(`last = "${escapeTomlString(last ?? '')}"`);
91
+ }
92
+ const p = historyPath();
93
+ try {
94
+ mkdirSync(dirname(p), { recursive: true });
95
+ writeFileSync(p, lines.join('\n') + '\n', { mode: 0o644 });
96
+ }
97
+ catch (err) {
98
+ throw new PicktuiError(`写入选择记忆失败:${err instanceof Error ? err.message : String(err)}`, 1, '');
99
+ }
100
+ }
8
101
  /** 读取 label 上次选中的值;无记录返回 ""。 */
9
102
  export async function histGet(label) {
10
- const inv = await invokeEngine(['hist', 'get', label]);
11
- assertSuccess(inv, 'picktui hist get');
12
- const parsed = parseJSON(inv.stdout, 'picktui hist get');
13
- return parsed.last ?? '';
103
+ return lastPick(loadPickHistory(), label);
14
104
  }
15
- /** 记录 label 的上次选中值(成功无输出;失败抛 PicktuiError)。 */
105
+ /** 记录 label 的上次选中值(失败抛 PicktuiError,含退出码 1)。 */
16
106
  export async function histSet(label, value) {
17
- const inv = await invokeEngine(['hist', 'set', label, value]);
18
- assertSuccess(inv, 'picktui hist set');
107
+ savePick(label, value);
19
108
  }
package/dist/esm/index.js CHANGED
@@ -4,8 +4,9 @@
4
4
  * import { pick } from '@havocrao/picktui/tui'
5
5
  * import { histGet } from '@havocrao/picktui/history'
6
6
  */
7
+ /** 本 TS 实现的语义版本(对齐 npm 包版本号)。 */
8
+ export const VERSION = '0.1.0';
7
9
  export * from './types.js';
8
- export * from './engine.js';
9
10
  export * from './filter.js';
10
11
  export * from './tui.js';
11
12
  export * from './history.js';