atris 3.74.7 → 3.74.9
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/commands/gm.js +41 -14
- package/commands/member.js +41 -7
- package/lib/gm-bet.js +768 -188
- package/lib/gm-card.js +45 -4
- package/lib/gm-phone-page.js +508 -74
- package/lib/text-table.js +138 -12
- package/lib/zen-apply.js +1 -1
- package/lib/zen.js +15 -2
- package/package.json +1 -1
- package/scripts/det/ytnotes +9 -3
package/lib/text-table.js
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
// than the terminal, columns marked `clip` or `wrap` shrink (rightmost
|
|
7
7
|
// first): a clip column ends in "…", a wrap column carries on underneath.
|
|
8
8
|
// Every other column keeps its full width.
|
|
9
|
+
//
|
|
10
|
+
// renderBorderedTable draws the same rows as a divided box, the look of the
|
|
11
|
+
// orb's team table. A box wider than the terminal narrows its `wrap`
|
|
12
|
+
// columns, the widest first, and their words carry on underneath; when it
|
|
13
|
+
// still does not fit it returns null and the caller prints its plain view.
|
|
14
|
+
//
|
|
15
|
+
// Every width here is terminal columns, not string length (displayWidth):
|
|
16
|
+
// a wide or fullwidth character takes 2, a joiner, variation selector, or
|
|
17
|
+
// combining mark takes 0, and a joined emoji is one 2-column cluster. Cuts
|
|
18
|
+
// and wraps fall between clusters, never inside one.
|
|
9
19
|
|
|
10
20
|
const GAP = ' ';
|
|
11
21
|
const ELLIPSIS = '…';
|
|
@@ -16,25 +26,110 @@ function terminalWidth(width) {
|
|
|
16
26
|
return Number.isFinite(value) && value >= 20 ? value : FALLBACK_WIDTH;
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
// East Asian wide and fullwidth code points, as [first, last] ranges.
|
|
30
|
+
const WIDE = [
|
|
31
|
+
[0x1100, 0x115f], [0x231a, 0x231b], [0x2329, 0x232a], [0x2e80, 0x303e], [0x3041, 0x33ff],
|
|
32
|
+
[0x3400, 0x4dbf], [0x4e00, 0x9fff], [0xa000, 0xa4cf], [0xa960, 0xa97f], [0xac00, 0xd7a3],
|
|
33
|
+
[0xf900, 0xfaff], [0xfe10, 0xfe19], [0xfe30, 0xfe6f], [0xff00, 0xff60], [0xffe0, 0xffe6],
|
|
34
|
+
[0x16fe0, 0x16fe4], [0x17000, 0x18cff], [0x1aff0, 0x1b2ff], [0x1f200, 0x1f2ff],
|
|
35
|
+
[0x20000, 0x2fffd], [0x30000, 0x3fffd],
|
|
36
|
+
];
|
|
37
|
+
// takes no column: combining marks, joiners and other format characters,
|
|
38
|
+
// variation selectors, and the Hangul vowel and final jamo that join a syllable
|
|
39
|
+
const ZERO = /^[\p{Mn}\p{Me}\p{Cf}\u1160-\u11ff\ud7b0-\ud7ff]$/u;
|
|
40
|
+
// drawn as a 2-column emoji: emoji presentation, or an emoji asked for one
|
|
41
|
+
// with the U+FE0F selector or a keycap
|
|
42
|
+
const EMOJI = /\p{Emoji_Presentation}|\p{Emoji}\ufe0f|\u20e3/u;
|
|
43
|
+
const SEGMENTER = typeof Intl === 'object' && typeof Intl.Segmenter === 'function'
|
|
44
|
+
? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
|
|
45
|
+
: null;
|
|
46
|
+
|
|
47
|
+
function isWide(code) {
|
|
48
|
+
for (const [first, last] of WIDE) {
|
|
49
|
+
if (code < first) return false;
|
|
50
|
+
if (code <= last) return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The text as grapheme clusters: what a reader sees as one character.
|
|
56
|
+
// Without Intl.Segmenter a zero-width code point joins the cluster before it.
|
|
57
|
+
function graphemes(text) {
|
|
58
|
+
const value = String(text);
|
|
59
|
+
if (SEGMENTER) return Array.from(SEGMENTER.segment(value), (part) => part.segment);
|
|
60
|
+
const out = [];
|
|
61
|
+
let joined = false;
|
|
62
|
+
for (const ch of value) {
|
|
63
|
+
if (out.length && (joined || ZERO.test(ch))) out[out.length - 1] += ch;
|
|
64
|
+
else out.push(ch);
|
|
65
|
+
joined = ch === '\u200d';
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Terminal columns for one cluster: 0, 1, or 2.
|
|
71
|
+
function clusterWidth(cluster) {
|
|
72
|
+
const code = cluster.codePointAt(0);
|
|
73
|
+
if (cluster.length === 1 && code >= 0x20 && code < 0x7f) return 1;
|
|
74
|
+
if (code < 0x20 || (code >= 0x7f && code < 0xa0)) return 0;
|
|
75
|
+
if (EMOJI.test(cluster)) return 2;
|
|
76
|
+
for (const ch of cluster) {
|
|
77
|
+
if (!ZERO.test(ch)) return isWide(ch.codePointAt(0)) ? 2 : 1;
|
|
78
|
+
}
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Terminal columns the text takes.
|
|
83
|
+
function displayWidth(text) {
|
|
84
|
+
const value = String(text);
|
|
85
|
+
// printable ascii is one column a character
|
|
86
|
+
if (/^[\x20-\x7e]*$/.test(value)) return value.length;
|
|
87
|
+
let total = 0;
|
|
88
|
+
for (const cluster of graphemes(value)) total += clusterWidth(cluster);
|
|
89
|
+
return total;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The longest start of the text that fits in that many columns, whole
|
|
93
|
+
// clusters only.
|
|
94
|
+
function sliceColumns(text, columns) {
|
|
95
|
+
let out = '';
|
|
96
|
+
let used = 0;
|
|
97
|
+
for (const cluster of graphemes(text)) {
|
|
98
|
+
const w = clusterWidth(cluster);
|
|
99
|
+
if (used + w > columns) break;
|
|
100
|
+
out += cluster;
|
|
101
|
+
used += w;
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Pads with spaces to that many columns.
|
|
107
|
+
function padColumns(text, columns) {
|
|
108
|
+
const value = String(text);
|
|
109
|
+
return value + ' '.repeat(Math.max(0, columns - displayWidth(value)));
|
|
110
|
+
}
|
|
111
|
+
|
|
19
112
|
function clipText(text, width) {
|
|
20
113
|
const value = String(text);
|
|
21
|
-
if (value
|
|
114
|
+
if (displayWidth(value) <= width) return value;
|
|
22
115
|
if (width <= 1) return ELLIPSIS.slice(0, Math.max(0, width));
|
|
23
|
-
return `${value
|
|
116
|
+
return `${sliceColumns(value, width - 1).trimEnd()}${ELLIPSIS}`;
|
|
24
117
|
}
|
|
25
118
|
|
|
26
|
-
// Word-wrap one line to a width; a word
|
|
119
|
+
// Word-wrap one line to a width in columns; a word wider than the width is
|
|
120
|
+
// split between clusters (a lone cluster wider than the width stands alone).
|
|
27
121
|
function wrapText(text, width) {
|
|
28
122
|
const out = [];
|
|
29
123
|
let line = '';
|
|
30
124
|
for (let word of String(text).split(' ')) {
|
|
31
|
-
while (word
|
|
125
|
+
while (displayWidth(word) > width) {
|
|
32
126
|
if (line) { out.push(line); line = ''; }
|
|
33
|
-
|
|
34
|
-
|
|
127
|
+
const head = sliceColumns(word, width) || graphemes(word)[0];
|
|
128
|
+
out.push(head);
|
|
129
|
+
word = word.slice(head.length);
|
|
35
130
|
}
|
|
36
131
|
const next = line ? `${line} ${word}` : word;
|
|
37
|
-
if (next
|
|
132
|
+
if (displayWidth(next) > width && line) { out.push(line); line = word; } else line = next;
|
|
38
133
|
}
|
|
39
134
|
out.push(line);
|
|
40
135
|
return out;
|
|
@@ -54,19 +149,19 @@ function renderTable(columns, rows, { width } = {}) {
|
|
|
54
149
|
const max = terminalWidth(width);
|
|
55
150
|
const cells = rows.map((row) => columns.map((_, index) => cellLines(row[index])));
|
|
56
151
|
const widths = columns.map((column, index) => Math.max(
|
|
57
|
-
|
|
58
|
-
...cells.map((row) => Math.max(...row[index].map(
|
|
152
|
+
displayWidth(column.header),
|
|
153
|
+
...cells.map((row) => Math.max(...row[index].map(displayWidth))),
|
|
59
154
|
));
|
|
60
155
|
let over = widths.reduce((sum, value) => sum + value, 0) + GAP.length * (columns.length - 1) - max;
|
|
61
156
|
for (let index = columns.length - 1; index >= 0 && over > 0; index -= 1) {
|
|
62
157
|
if (!columns[index].clip && !columns[index].wrap) continue;
|
|
63
|
-
const floor = Math.max(
|
|
158
|
+
const floor = Math.max(displayWidth(columns[index].header), columns[index].min || 8);
|
|
64
159
|
const cut = Math.min(over, Math.max(0, widths[index] - floor));
|
|
65
160
|
widths[index] -= cut;
|
|
66
161
|
over -= cut;
|
|
67
162
|
}
|
|
68
163
|
const line = (parts) => parts
|
|
69
|
-
.map((text, index) => clipText(text, widths[index])
|
|
164
|
+
.map((text, index) => padColumns(clipText(text, widths[index]), index === parts.length - 1 ? 0 : widths[index]))
|
|
70
165
|
.join(GAP)
|
|
71
166
|
.trimEnd();
|
|
72
167
|
const out = [line(columns.map((column) => column.header))];
|
|
@@ -78,4 +173,35 @@ function renderTable(columns, rows, { width } = {}) {
|
|
|
78
173
|
return out.join('\n');
|
|
79
174
|
}
|
|
80
175
|
|
|
81
|
-
|
|
176
|
+
// columns: [{ header, wrap?, min? }]; rows: arrays of cells. A divided box,
|
|
177
|
+
// or null when it cannot fit the terminal.
|
|
178
|
+
function renderBorderedTable(columns, rows, { width } = {}) {
|
|
179
|
+
const cells = rows.map((row) => columns.map((_, index) => cellLines(row[index])));
|
|
180
|
+
const widths = columns.map((column, index) => Math.max(
|
|
181
|
+
displayWidth(column.header),
|
|
182
|
+
...cells.map((row) => Math.max(...row[index].map(displayWidth))),
|
|
183
|
+
));
|
|
184
|
+
const floor = (index) => Math.max(displayWidth(columns[index].header), columns[index].min || 8);
|
|
185
|
+
let over = widths.reduce((sum, value) => sum + value + 3, 1) - terminalWidth(width);
|
|
186
|
+
while (over > 0) {
|
|
187
|
+
let widest = -1;
|
|
188
|
+
columns.forEach((column, index) => {
|
|
189
|
+
if (column.wrap && widths[index] > floor(index) && (widest < 0 || widths[index] > widths[widest])) widest = index;
|
|
190
|
+
});
|
|
191
|
+
if (widest < 0) return null;
|
|
192
|
+
widths[widest] -= 1;
|
|
193
|
+
over -= 1;
|
|
194
|
+
}
|
|
195
|
+
const rule = (left, mid, right) => `${left}${widths.map((w) => '─'.repeat(w + 2)).join(mid)}${right}`;
|
|
196
|
+
const line = (parts) => `│ ${parts.map((text, index) => padColumns(text, widths[index])).join(' │ ')} │`;
|
|
197
|
+
const out = [rule('┌', '┬', '┐'), line(columns.map((column) => column.header)), rule('├', '┼', '┤')];
|
|
198
|
+
for (const raw of cells) {
|
|
199
|
+
const row = raw.map((lines, index) => (columns[index].wrap ? lines.flatMap((text) => wrapText(text, widths[index])) : lines));
|
|
200
|
+
const height = Math.max(...row.map((lines) => lines.length));
|
|
201
|
+
for (let at = 0; at < height; at += 1) out.push(line(row.map((lines) => lines[at] || '')));
|
|
202
|
+
}
|
|
203
|
+
out.push(rule('└', '┴', '┘'));
|
|
204
|
+
return out.join('\n');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = { FALLBACK_WIDTH, clipText, displayWidth, renderBorderedTable, renderTable, sliceColumns, terminalWidth, wrapText };
|
package/lib/zen-apply.js
CHANGED
|
@@ -547,7 +547,7 @@ function applyZen({ cwd = process.cwd(), fetch = true, freshMinutes, abandonedDa
|
|
|
547
547
|
if (copies.busy.length) left.push(...sideCopyLines(copies.busy, staleDays, nextSaveIn(copies.busy, staleDays, now.getTime())));
|
|
548
548
|
if (copies.scheduled) left.push(`${plural(copies.scheduled, 'side copy is', 'side copies are')} used by a scheduled job, so ${copies.scheduled === 1 ? 'it stays' : 'they stay'}`);
|
|
549
549
|
if (copies.locked) left.push(`${plural(copies.locked, 'side copy is', 'side copies are')} locked by ${copies.locked === 1 ? 'its owner, so it stays' : 'their owners, so they stay'}`);
|
|
550
|
-
if (copies.inUse) left.push(`${plural(copies.inUse, 'side copy was', 'side copies were')} used in ${window}`);
|
|
550
|
+
if (copies.inUse) left.push(`${plural(copies.inUse, 'side copy was', 'side copies were')} used in ${window}, so ${copies.inUse === 1 ? 'it stays' : 'they stay'}`);
|
|
551
551
|
|
|
552
552
|
const stray = saveStrayEdits(root, teamState, tag, cutoff);
|
|
553
553
|
if (stray.saved) {
|
package/lib/zen.js
CHANGED
|
@@ -507,8 +507,21 @@ function shortPlace(p) {
|
|
|
507
507
|
function sideCopyLines(paths, days, waitMs = null) {
|
|
508
508
|
const n = paths.length;
|
|
509
509
|
const lines = [`${n} side ${n === 1 ? 'copy has' : 'copies have'} unsaved work, left for ${n === 1 ? 'its owner' : 'their owners'}`];
|
|
510
|
-
|
|
511
|
-
|
|
510
|
+
// Copies that share a folder (one workflow's runs, say) read as one line.
|
|
511
|
+
const folders = new Map();
|
|
512
|
+
for (const p of paths) {
|
|
513
|
+
const dir = path.dirname(p);
|
|
514
|
+
if (!folders.has(dir)) folders.set(dir, []);
|
|
515
|
+
folders.get(dir).push(p);
|
|
516
|
+
}
|
|
517
|
+
const rows = [];
|
|
518
|
+
for (const [dir, group] of folders) {
|
|
519
|
+
if (group.length >= 3) rows.push({ text: `${group.length} in ${shortPlace(dir)}/`, count: group.length });
|
|
520
|
+
else for (const p of group) rows.push({ text: shortPlace(p), count: 1 });
|
|
521
|
+
}
|
|
522
|
+
for (const row of rows.slice(0, 3)) lines.push(` ${row.text}`);
|
|
523
|
+
const rest = rows.slice(3).reduce((sum, row) => sum + row.count, 0);
|
|
524
|
+
if (rest) lines.push(` and ${rest} more`);
|
|
512
525
|
lines.push(`zen saves ${n === 1 ? 'it' : 'each one'} to zen-backup/ once nobody has touched it for ${days} ${days === 1 ? 'day' : 'days'}`);
|
|
513
526
|
if (waitMs > 0) lines.push(`${n === 1 ? 'that is' : 'the first one is'} due in ${waitText(waitMs)}`);
|
|
514
527
|
return lines;
|
package/package.json
CHANGED
package/scripts/det/ytnotes
CHANGED
|
@@ -428,10 +428,14 @@ run_haiku() {
|
|
|
428
428
|
# The Antigravity editor ships its own `agy` (a VS Code-style launcher script).
|
|
429
429
|
# It has no -p mode: it hands -p and --model to Electron and prints nothing, so
|
|
430
430
|
# it is not a writer. Only the Antigravity CLI is.
|
|
431
|
+
# Only grep's answer counts, never head's exit. In `head | grep -q` under
|
|
432
|
+
# pipefail, grep quits on the match and a head that touches the pipe once
|
|
433
|
+
# more (uutils head on Ubuntu 26.04 does, to find the end of the file) dies
|
|
434
|
+
# of SIGPIPE, so a busy machine read the launcher as the CLI.
|
|
431
435
|
agy_is_cli() {
|
|
432
436
|
local bin
|
|
433
437
|
bin=$(command -v agy 2>/dev/null) || return 1
|
|
434
|
-
! head -c 4096 "$bin" 2>/dev/null
|
|
438
|
+
! grep -q 'VSCODE_IPC_HOOK_CLI' < <(head -c 4096 "$bin" 2>/dev/null)
|
|
435
439
|
}
|
|
436
440
|
|
|
437
441
|
# An agy that is not signed in opens a Google login and waits up to 60s, which
|
|
@@ -441,7 +445,7 @@ agy_is_cli() {
|
|
|
441
445
|
agy_signed_in() {
|
|
442
446
|
local out
|
|
443
447
|
out=$(run_with_timeout 10 agy models </dev/null 2>&1) || true
|
|
444
|
-
!
|
|
448
|
+
! grep -qi 'sign in' <<< "$out"
|
|
445
449
|
}
|
|
446
450
|
|
|
447
451
|
run_gemini() {
|
|
@@ -524,7 +528,9 @@ run_auto() {
|
|
|
524
528
|
echo "$tried did not answer; writing the notes with $label instead." >&2
|
|
525
529
|
fi
|
|
526
530
|
rm -f "$WORK/yt_${ID}.kill"
|
|
527
|
-
|
|
531
|
+
# A here-string, not printf | grep -q: under pipefail printf can die of
|
|
532
|
+
# SIGPIPE after grep's match and turn a real answer into "did not answer".
|
|
533
|
+
if out=$(run_writer "$cmd" "$model" "$effort" 2>>"$WORK/yt_${ID}.engine.err") && grep -q '[[:alnum:]]' <<< "$out"; then
|
|
528
534
|
printf '%s\n' "$out"
|
|
529
535
|
echo "notes by $label" >&2
|
|
530
536
|
return 0
|