@tabbio-technologies/cli 1.2.8
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/README.md +763 -0
- package/dist/chunks/chunk-7LNC3FIV.js +2019 -0
- package/dist/chunks/chunk-7LNC3FIV.js.map +7 -0
- package/dist/chunks/chunk-NAWA6NCZ.js +259 -0
- package/dist/chunks/chunk-NAWA6NCZ.js.map +7 -0
- package/dist/chunks/chunk-NK5RPNSV.js +15 -0
- package/dist/chunks/chunk-NK5RPNSV.js.map +7 -0
- package/dist/chunks/chunk-QVR5KIEQ.js +1976 -0
- package/dist/chunks/chunk-QVR5KIEQ.js.map +7 -0
- package/dist/chunks/chunk-VHHZFMIF.js +162 -0
- package/dist/chunks/chunk-VHHZFMIF.js.map +7 -0
- package/dist/chunks/chunk-Y6GWIFHI.js +227 -0
- package/dist/chunks/chunk-Y6GWIFHI.js.map +7 -0
- package/dist/chunks/entry-WENOOT6W.js +3492 -0
- package/dist/chunks/entry-WENOOT6W.js.map +7 -0
- package/dist/chunks/mount-NGUARMCL.js +79 -0
- package/dist/chunks/mount-NGUARMCL.js.map +7 -0
- package/dist/chunks/program-O2BA5L6Z.js +2755 -0
- package/dist/chunks/program-O2BA5L6Z.js.map +7 -0
- package/dist/chunks/status-I5UAQPBY.js +16 -0
- package/dist/chunks/status-I5UAQPBY.js.map +7 -0
- package/dist/cli.js +66 -0
- package/dist/cli.js.map +7 -0
- package/package.json +65 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { createRequire as __tabbioCreateRequire } from 'node:module';
|
|
2
|
+
const require = __tabbioCreateRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
SPINNER_INTERVAL_MS,
|
|
5
|
+
theme
|
|
6
|
+
} from "./chunk-VHHZFMIF.js";
|
|
7
|
+
|
|
8
|
+
// src/ui/text.ts
|
|
9
|
+
var ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:;[-a-zA-Z\d/#&.:=?%@~_]+)*|[a-zA-Z\d]+(?:;[-a-zA-Z\d/#&.:=?%@~_]*)*)?\u0007|(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~])/g;
|
|
10
|
+
function stripAnsi(text) {
|
|
11
|
+
return text.replace(ANSI_PATTERN, "");
|
|
12
|
+
}
|
|
13
|
+
function charWidth(code) {
|
|
14
|
+
if (code < 32 || code >= 127 && code < 160) return 0;
|
|
15
|
+
if (code >= 768 && code <= 879 || code >= 8203 && code <= 8207 || code === 65039) return 0;
|
|
16
|
+
const wide = code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65103 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510 || code >= 127744 && code <= 129791 || code >= 131072 && code <= 262141;
|
|
17
|
+
return wide ? 2 : 1;
|
|
18
|
+
}
|
|
19
|
+
function displayWidth(text) {
|
|
20
|
+
let width = 0;
|
|
21
|
+
for (const char of stripAnsi(text)) width += charWidth(char.codePointAt(0) ?? 0);
|
|
22
|
+
return width;
|
|
23
|
+
}
|
|
24
|
+
function truncate(text, max, ellipsis = theme.symbols.ellipsis) {
|
|
25
|
+
if (max <= 0) return "";
|
|
26
|
+
if (displayWidth(text) <= max) return text;
|
|
27
|
+
const budget = Math.max(0, max - displayWidth(ellipsis));
|
|
28
|
+
let out = "";
|
|
29
|
+
let width = 0;
|
|
30
|
+
for (const char of text) {
|
|
31
|
+
const w = charWidth(char.codePointAt(0) ?? 0);
|
|
32
|
+
if (width + w > budget) break;
|
|
33
|
+
out += char;
|
|
34
|
+
width += w;
|
|
35
|
+
}
|
|
36
|
+
return displayWidth(ellipsis) > max ? out : `${out}${ellipsis}`;
|
|
37
|
+
}
|
|
38
|
+
function truncateStart(text, max, ellipsis = theme.symbols.ellipsis) {
|
|
39
|
+
if (max <= 0) return "";
|
|
40
|
+
if (displayWidth(text) <= max) return text;
|
|
41
|
+
const chars = [...text];
|
|
42
|
+
const budget = Math.max(0, max - displayWidth(ellipsis));
|
|
43
|
+
let out = "";
|
|
44
|
+
let width = 0;
|
|
45
|
+
for (let i = chars.length - 1; i >= 0; i -= 1) {
|
|
46
|
+
const char = chars[i];
|
|
47
|
+
const w = charWidth(char.codePointAt(0) ?? 0);
|
|
48
|
+
if (width + w > budget) break;
|
|
49
|
+
out = char + out;
|
|
50
|
+
width += w;
|
|
51
|
+
}
|
|
52
|
+
return `${ellipsis}${out}`;
|
|
53
|
+
}
|
|
54
|
+
function padEnd(text, width) {
|
|
55
|
+
return text + " ".repeat(Math.max(0, width - displayWidth(text)));
|
|
56
|
+
}
|
|
57
|
+
function padStart(text, width) {
|
|
58
|
+
return " ".repeat(Math.max(0, width - displayWidth(text))) + text;
|
|
59
|
+
}
|
|
60
|
+
function oneLine(text) {
|
|
61
|
+
return text.replace(/\s+/g, " ").trim();
|
|
62
|
+
}
|
|
63
|
+
function sentenceCase(text) {
|
|
64
|
+
const clean = oneLine(text.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2"));
|
|
65
|
+
if (!clean) return clean;
|
|
66
|
+
const lower = clean.split(" ").map((word, i) => i > 0 && /^[A-Z][a-z]/.test(word) ? word.toLowerCase() : word).join(" ");
|
|
67
|
+
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
68
|
+
}
|
|
69
|
+
function formatElapsed(ms) {
|
|
70
|
+
const safe = Math.max(0, ms);
|
|
71
|
+
if (safe < 1e4) return `${(safe / 1e3).toFixed(1)}s`;
|
|
72
|
+
if (safe < 6e4) return `${Math.floor(safe / 1e3)}s`;
|
|
73
|
+
const minutes = Math.floor(safe / 6e4);
|
|
74
|
+
const seconds = Math.floor(safe % 6e4 / 1e3);
|
|
75
|
+
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
76
|
+
}
|
|
77
|
+
function shortId(id, head = 8, tail = 2) {
|
|
78
|
+
return id.length > head + tail + 1 ? `${id.slice(0, head)}${theme.symbols.ellipsis}${id.slice(-tail)}` : id;
|
|
79
|
+
}
|
|
80
|
+
function scalarText(value) {
|
|
81
|
+
if (value === null || value === void 0) return null;
|
|
82
|
+
if (typeof value === "string") return value;
|
|
83
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
84
|
+
if (Array.isArray(value) && value.every((v) => typeof v !== "object" || v === null)) {
|
|
85
|
+
return value.map(String).join(", ");
|
|
86
|
+
}
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
function summarizeArgs(args, max = 60) {
|
|
90
|
+
if (!args) return "";
|
|
91
|
+
const sep = ` ${theme.symbols.middot} `;
|
|
92
|
+
const parts = [];
|
|
93
|
+
for (const [key, value] of Object.entries(args)) {
|
|
94
|
+
if (key === "userId" || key === "companyId") continue;
|
|
95
|
+
if (value === true) {
|
|
96
|
+
parts.push(key.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase());
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const text = scalarText(value);
|
|
100
|
+
if (text === null || text === "" || typeof value === "boolean") continue;
|
|
101
|
+
const clean = oneLine(text);
|
|
102
|
+
const part = /id$/i.test(key) || /^[\w-]+$/.test(clean) ? clean : `"${truncate(clean, 32)}"`;
|
|
103
|
+
const next = [...parts, part].join(sep);
|
|
104
|
+
if (displayWidth(next) > max) {
|
|
105
|
+
if (parts.length === 0) parts.push(truncate(part, max));
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
parts.push(part);
|
|
109
|
+
}
|
|
110
|
+
return parts.join(sep);
|
|
111
|
+
}
|
|
112
|
+
function wrapWords(text, width, maxLines = Number.POSITIVE_INFINITY) {
|
|
113
|
+
const words = oneLine(text).split(" ").filter(Boolean);
|
|
114
|
+
const lines = [];
|
|
115
|
+
let line = "";
|
|
116
|
+
for (const word of words) {
|
|
117
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
118
|
+
if (displayWidth(candidate) <= width) {
|
|
119
|
+
line = candidate;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (line) lines.push(line);
|
|
123
|
+
line = displayWidth(word) > width ? truncate(word, width) : word;
|
|
124
|
+
if (lines.length === maxLines) break;
|
|
125
|
+
}
|
|
126
|
+
if (line && lines.length < maxLines) lines.push(line);
|
|
127
|
+
if (lines.length === maxLines && words.join(" ") !== lines.join(" ")) {
|
|
128
|
+
lines[maxLines - 1] = truncate(`${lines[maxLines - 1]} \u2026`, width);
|
|
129
|
+
}
|
|
130
|
+
return lines.slice(0, maxLines);
|
|
131
|
+
}
|
|
132
|
+
function safeJson(value, indent = 2) {
|
|
133
|
+
try {
|
|
134
|
+
return JSON.stringify(value, null, indent) ?? String(value);
|
|
135
|
+
} catch {
|
|
136
|
+
return String(value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function clipLines(text, max) {
|
|
140
|
+
const lines = text.split("\n");
|
|
141
|
+
if (lines.length <= max) return { lines, hidden: 0 };
|
|
142
|
+
return { lines: lines.slice(0, Math.max(0, max)), hidden: lines.length - Math.max(0, max) };
|
|
143
|
+
}
|
|
144
|
+
function estimateHeight(text, width) {
|
|
145
|
+
const cols = Math.max(1, width);
|
|
146
|
+
return text.split("\n").reduce((sum, line) => sum + Math.max(1, Math.ceil(displayWidth(line) / cols)), 0);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/ui/components/Spinner.tsx
|
|
150
|
+
import { Text } from "ink";
|
|
151
|
+
import { createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
152
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
153
|
+
var TickContext = createContext(0);
|
|
154
|
+
var SubscribeContext = createContext(null);
|
|
155
|
+
function SpinnerProvider({ children }) {
|
|
156
|
+
const [tick, setTick] = useState(0);
|
|
157
|
+
const [subscribers, setSubscribers] = useState(0);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (subscribers === 0) return void 0;
|
|
160
|
+
const id = setInterval(() => setTick((t) => (t + 1) % 1e6), SPINNER_INTERVAL_MS);
|
|
161
|
+
return () => clearInterval(id);
|
|
162
|
+
}, [subscribers]);
|
|
163
|
+
const subscribe = useCallback(() => {
|
|
164
|
+
setSubscribers((n) => n + 1);
|
|
165
|
+
return () => setSubscribers((n) => Math.max(0, n - 1));
|
|
166
|
+
}, []);
|
|
167
|
+
return /* @__PURE__ */ jsx(SubscribeContext.Provider, { value: subscribe, children: /* @__PURE__ */ jsx(TickContext.Provider, { value: tick, children }) });
|
|
168
|
+
}
|
|
169
|
+
function useClockTick(active = true) {
|
|
170
|
+
const subscribe = useContext(SubscribeContext);
|
|
171
|
+
const tick = useContext(TickContext);
|
|
172
|
+
useEffect(() => active && subscribe ? subscribe() : void 0, [active, subscribe]);
|
|
173
|
+
return tick;
|
|
174
|
+
}
|
|
175
|
+
var Spinner = memo(function Spinner2({ label }) {
|
|
176
|
+
const tick = useClockTick(theme.animations);
|
|
177
|
+
const frames = theme.spinnerFrames;
|
|
178
|
+
const glyph = theme.animations ? frames[tick % frames.length] ?? theme.symbols.pending : theme.symbols.pending;
|
|
179
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
180
|
+
theme.accent(glyph),
|
|
181
|
+
label ? ` ${label}` : ""
|
|
182
|
+
] });
|
|
183
|
+
});
|
|
184
|
+
var Elapsed = memo(function Elapsed2({ since, prefix = "" }) {
|
|
185
|
+
useClockTick(true);
|
|
186
|
+
return /* @__PURE__ */ jsx(Text, { children: theme.dim(`${prefix}${formatElapsed(Date.now() - since)}`) });
|
|
187
|
+
});
|
|
188
|
+
function Working({ label, since }) {
|
|
189
|
+
const text = useMemo(() => theme.dim(label), [label]);
|
|
190
|
+
return /* @__PURE__ */ jsxs(Text, { children: [
|
|
191
|
+
/* @__PURE__ */ jsx(Spinner, {}),
|
|
192
|
+
" ",
|
|
193
|
+
text,
|
|
194
|
+
" ",
|
|
195
|
+
/* @__PURE__ */ jsx(Elapsed, { since })
|
|
196
|
+
] });
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// src/ui/layout.tsx
|
|
200
|
+
import { Box, useStdout } from "ink";
|
|
201
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
202
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
203
|
+
var PAGE_MARGIN = 2;
|
|
204
|
+
function readSize(stdout) {
|
|
205
|
+
return { columns: stdout.columns || 80, rows: stdout.rows || 24 };
|
|
206
|
+
}
|
|
207
|
+
function useTerminalSize() {
|
|
208
|
+
const { stdout } = useStdout();
|
|
209
|
+
const [size, setSize] = useState2(() => readSize(stdout));
|
|
210
|
+
useEffect2(() => {
|
|
211
|
+
const onResize = () => setSize(readSize(stdout));
|
|
212
|
+
stdout.on("resize", onResize);
|
|
213
|
+
return () => {
|
|
214
|
+
stdout.off("resize", onResize);
|
|
215
|
+
};
|
|
216
|
+
}, [stdout]);
|
|
217
|
+
return size;
|
|
218
|
+
}
|
|
219
|
+
function fromTerminal(columns) {
|
|
220
|
+
return columns - pageMargin(columns) * 2;
|
|
221
|
+
}
|
|
222
|
+
function pageMargin(columns) {
|
|
223
|
+
return columns < 50 ? 1 : PAGE_MARGIN;
|
|
224
|
+
}
|
|
225
|
+
function contentWidth(columns) {
|
|
226
|
+
return Math.max(20, columns - pageMargin(columns) * 2);
|
|
227
|
+
}
|
|
228
|
+
function Page({ columns, children }) {
|
|
229
|
+
const margin = pageMargin(columns);
|
|
230
|
+
return /* @__PURE__ */ jsx2(Box, { flexDirection: "column", paddingLeft: margin, paddingRight: margin, width: columns, children });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export {
|
|
234
|
+
stripAnsi,
|
|
235
|
+
displayWidth,
|
|
236
|
+
truncate,
|
|
237
|
+
truncateStart,
|
|
238
|
+
padEnd,
|
|
239
|
+
padStart,
|
|
240
|
+
oneLine,
|
|
241
|
+
sentenceCase,
|
|
242
|
+
formatElapsed,
|
|
243
|
+
shortId,
|
|
244
|
+
summarizeArgs,
|
|
245
|
+
wrapWords,
|
|
246
|
+
safeJson,
|
|
247
|
+
clipLines,
|
|
248
|
+
estimateHeight,
|
|
249
|
+
SpinnerProvider,
|
|
250
|
+
useClockTick,
|
|
251
|
+
Spinner,
|
|
252
|
+
Working,
|
|
253
|
+
useTerminalSize,
|
|
254
|
+
fromTerminal,
|
|
255
|
+
pageMargin,
|
|
256
|
+
contentWidth,
|
|
257
|
+
Page
|
|
258
|
+
};
|
|
259
|
+
//# sourceMappingURL=chunk-NAWA6NCZ.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/ui/text.ts", "../../src/ui/components/Spinner.tsx", "../../src/ui/layout.tsx"],
|
|
4
|
+
"sourcesContent": ["/**\n * Pure text helpers for the Ink screens: ANSI-aware width, truncation,\n * padding and the small formatters every screen shares. No React here.\n */\nimport { theme } from './theme';\n\n// eslint-disable-next-line no-control-regex\nconst ANSI_PATTERN = /[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:;[-a-zA-Z\\d/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d/#&.:=?%@~_]*)*)?\\u0007|(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~])/g;\n\nexport function stripAnsi(text: string): string {\n return text.replace(ANSI_PATTERN, '');\n}\n\nfunction charWidth(code: number): number {\n if (code < 0x20 || (code >= 0x7f && code < 0xa0)) return 0;\n if ((code >= 0x300 && code <= 0x36f) || (code >= 0x200b && code <= 0x200f) || code === 0xfe0f) return 0;\n const wide =\n (code >= 0x1100 && code <= 0x115f) ||\n (code >= 0x2e80 && code <= 0xa4cf) ||\n (code >= 0xac00 && code <= 0xd7a3) ||\n (code >= 0xf900 && code <= 0xfaff) ||\n (code >= 0xfe30 && code <= 0xfe4f) ||\n (code >= 0xff00 && code <= 0xff60) ||\n (code >= 0xffe0 && code <= 0xffe6) ||\n (code >= 0x1f300 && code <= 0x1faff) ||\n (code >= 0x20000 && code <= 0x3fffd);\n return wide ? 2 : 1;\n}\n\n/** Terminal cells a string occupies (ANSI stripped; wide glyphs count 2). */\nexport function displayWidth(text: string): number {\n let width = 0;\n for (const char of stripAnsi(text)) width += charWidth(char.codePointAt(0) ?? 0);\n return width;\n}\n\n/** Cuts a plain string to `max` cells, ending with the theme ellipsis when shortened. */\nexport function truncate(text: string, max: number, ellipsis: string = theme.symbols.ellipsis): string {\n if (max <= 0) return '';\n if (displayWidth(text) <= max) return text;\n const budget = Math.max(0, max - displayWidth(ellipsis));\n let out = '';\n let width = 0;\n for (const char of text) {\n const w = charWidth(char.codePointAt(0) ?? 0);\n if (width + w > budget) break;\n out += char;\n width += w;\n }\n return displayWidth(ellipsis) > max ? out : `${out}${ellipsis}`;\n}\n\n/** Keeps the end of a string (for inputs scrolled to the cursor). */\nexport function truncateStart(text: string, max: number, ellipsis: string = theme.symbols.ellipsis): string {\n if (max <= 0) return '';\n if (displayWidth(text) <= max) return text;\n const chars = [...text];\n const budget = Math.max(0, max - displayWidth(ellipsis));\n let out = '';\n let width = 0;\n for (let i = chars.length - 1; i >= 0; i -= 1) {\n const char = chars[i] as string;\n const w = charWidth(char.codePointAt(0) ?? 0);\n if (width + w > budget) break;\n out = char + out;\n width += w;\n }\n return `${ellipsis}${out}`;\n}\n\nexport function padEnd(text: string, width: number): string {\n return text + ' '.repeat(Math.max(0, width - displayWidth(text)));\n}\n\nexport function padStart(text: string, width: number): string {\n return ' '.repeat(Math.max(0, width - displayWidth(text))) + text;\n}\n\n/** One line: collapses whitespace (newlines included). */\nexport function oneLine(text: string): string {\n return text.replace(/\\s+/g, ' ').trim();\n}\n\n/** \"Job Search\" / \"job_search\" \u2192 \"Job search\". */\nexport function sentenceCase(text: string): string {\n const clean = oneLine(text.replace(/[_-]+/g, ' ').replace(/([a-z0-9])([A-Z])/g, '$1 $2'));\n if (!clean) return clean;\n const lower = clean\n .split(' ')\n .map((word, i) => (i > 0 && /^[A-Z][a-z]/.test(word) ? word.toLowerCase() : word))\n .join(' ');\n return lower.charAt(0).toUpperCase() + lower.slice(1);\n}\n\n/** `0.8s`, `12s`, `1m 04s`; under a second shows tenths. */\nexport function formatElapsed(ms: number): string {\n const safe = Math.max(0, ms);\n if (safe < 10_000) return `${(safe / 1000).toFixed(1)}s`;\n if (safe < 60_000) return `${Math.floor(safe / 1000)}s`;\n const minutes = Math.floor(safe / 60_000);\n const seconds = Math.floor((safe % 60_000) / 1000);\n return `${minutes}m ${String(seconds).padStart(2, '0')}s`;\n}\n\n/** `apr_2Hq9\u2026k1`: enough of an id to recognise it, never the whole thing. */\nexport function shortId(id: string, head = 8, tail = 2): string {\n return id.length > head + tail + 1 ? `${id.slice(0, head)}${theme.symbols.ellipsis}${id.slice(-tail)}` : id;\n}\n\nfunction scalarText(value: unknown): string | null {\n if (value === null || value === undefined) return null;\n if (typeof value === 'string') return value;\n if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n if (Array.isArray(value) && value.every((v) => typeof v !== 'object' || v === null)) {\n return value.map(String).join(', ');\n }\n return null;\n}\n\n/**\n * Dim one-line argument summary for tool rows: scalar values only, strings\n * quoted, ids bare (`\"product designer\" \u00B7 Dubai \u00B7 cv_8f2`).\n */\nexport function summarizeArgs(args: Record<string, unknown> | undefined, max = 60): string {\n if (!args) return '';\n const sep = ` ${theme.symbols.middot} `;\n const parts: string[] = [];\n for (const [key, value] of Object.entries(args)) {\n if (key === 'userId' || key === 'companyId') continue;\n if (value === true) {\n parts.push(key.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase());\n continue;\n }\n const text = scalarText(value);\n if (text === null || text === '' || typeof value === 'boolean') continue;\n const clean = oneLine(text);\n const part = /id$/i.test(key) || /^[\\w-]+$/.test(clean) ? clean : `\"${truncate(clean, 32)}\"`;\n const next = [...parts, part].join(sep);\n if (displayWidth(next) > max) {\n if (parts.length === 0) parts.push(truncate(part, max));\n break;\n }\n parts.push(part);\n }\n return parts.join(sep);\n}\n\n/** Word-wraps plain text into at most `maxLines` lines (the last one ends with `\u2026` when cut). */\nexport function wrapWords(text: string, width: number, maxLines = Number.POSITIVE_INFINITY): string[] {\n const words = oneLine(text).split(' ').filter(Boolean);\n const lines: string[] = [];\n let line = '';\n for (const word of words) {\n const candidate = line ? `${line} ${word}` : word;\n if (displayWidth(candidate) <= width) {\n line = candidate;\n continue;\n }\n if (line) lines.push(line);\n line = displayWidth(word) > width ? truncate(word, width) : word;\n if (lines.length === maxLines) break;\n }\n if (line && lines.length < maxLines) lines.push(line);\n if (lines.length === maxLines && words.join(' ') !== lines.join(' ')) {\n lines[maxLines - 1] = truncate(`${lines[maxLines - 1]} \u2026`, width);\n }\n return lines.slice(0, maxLines);\n}\n\n/** Pretty JSON, never throwing (cycles and BigInt fall back to String()). */\nexport function safeJson(value: unknown, indent = 2): string {\n try {\n return JSON.stringify(value, null, indent) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/** Keeps the first `max` lines and reports how many were cut. */\nexport function clipLines(text: string, max: number): { lines: string[]; hidden: number } {\n const lines = text.split('\\n');\n if (lines.length <= max) return { lines, hidden: 0 };\n return { lines: lines.slice(0, Math.max(0, max)), hidden: lines.length - Math.max(0, max) };\n}\n\n/** Rough visual height of text wrapped at `width` (used to budget live regions). */\nexport function estimateHeight(text: string, width: number): number {\n const cols = Math.max(1, width);\n return text.split('\\n').reduce((sum, line) => sum + Math.max(1, Math.ceil(displayWidth(line) / cols)), 0);\n}\n", "import { Text } from 'ink';\nimport { type ReactNode, createContext, memo, useCallback, useContext, useEffect, useMemo, useState } from 'react';\nimport { SPINNER_INTERVAL_MS, theme } from '../theme';\nimport { formatElapsed } from '../text';\n\n/**\n * One shared 80 ms clock for every spinner and elapsed counter (research\n * \u00A75.10). The interval only runs while at least one consumer is mounted and\n * animations are on, and it is cleared on unmount.\n */\nconst TickContext = createContext(0);\nconst SubscribeContext = createContext<(() => () => void) | null>(null);\n\nexport function SpinnerProvider({ children }: { children: ReactNode }) {\n const [tick, setTick] = useState(0);\n const [subscribers, setSubscribers] = useState(0);\n\n useEffect(() => {\n if (subscribers === 0) return undefined;\n const id = setInterval(() => setTick((t) => (t + 1) % 1_000_000), SPINNER_INTERVAL_MS);\n return () => clearInterval(id);\n }, [subscribers]);\n\n const subscribe = useCallback(() => {\n setSubscribers((n) => n + 1);\n return () => setSubscribers((n) => Math.max(0, n - 1));\n }, []);\n\n return (\n <SubscribeContext.Provider value={subscribe}>\n <TickContext.Provider value={tick}>{children}</TickContext.Provider>\n </SubscribeContext.Provider>\n );\n}\n\n/** Current clock tick; subscribes the caller to the shared clock while `active`. */\nexport function useClockTick(active = true): number {\n const subscribe = useContext(SubscribeContext);\n const tick = useContext(TickContext);\n useEffect(() => (active && subscribe ? subscribe() : undefined), [active, subscribe]);\n return tick;\n}\n\n/** Braille spinner in the accent colour; a static `\u25D0` without animation. */\nexport const Spinner = memo(function Spinner({ label }: { label?: string }) {\n const tick = useClockTick(theme.animations);\n const frames = theme.spinnerFrames;\n const glyph = theme.animations ? (frames[tick % frames.length] ?? theme.symbols.pending) : theme.symbols.pending;\n return (\n <Text>\n {theme.accent(glyph)}\n {label ? ` ${label}` : ''}\n </Text>\n );\n});\n\n/** Live elapsed time since `since` (dim), driven by the shared clock. */\nexport const Elapsed = memo(function Elapsed({ since, prefix = '' }: { since: number; prefix?: string }) {\n useClockTick(true);\n return <Text>{theme.dim(`${prefix}${formatElapsed(Date.now() - since)}`)}</Text>;\n});\n\n/** Spinner + label + elapsed, the standard \"working\" line. */\nexport function Working({ label, since }: { label: string; since: number }) {\n const text = useMemo(() => theme.dim(label), [label]);\n return (\n <Text>\n <Spinner /> {text} <Elapsed since={since} />\n </Text>\n );\n}\n", "import { Box, useStdout } from 'ink';\nimport { type ReactNode, useEffect, useState } from 'react';\n\n/** Page gutter (research mockups indent everything by two cells). */\nexport const PAGE_MARGIN = 2;\n\nexport type TerminalSize = { columns: number; rows: number };\n\nfunction readSize(stdout: NodeJS.WriteStream): TerminalSize {\n return { columns: stdout.columns || 80, rows: stdout.rows || 24 };\n}\n\n/** Terminal size that follows `resize` events (listener removed on unmount). */\nexport function useTerminalSize(): TerminalSize {\n const { stdout } = useStdout();\n const [size, setSize] = useState<TerminalSize>(() => readSize(stdout));\n useEffect(() => {\n const onResize = () => setSize(readSize(stdout));\n stdout.on('resize', onResize);\n return () => {\n stdout.off('resize', onResize);\n };\n }, [stdout]);\n return size;\n}\n\n/**\n * Breakpoints are in terminal columns (research \u00A75.5/\u00A75.6); screens get the\n * content width, so compare with `fromTerminal(n)`.\n */\nexport function fromTerminal(columns: number): number {\n return columns - pageMargin(columns) * 2;\n}\n\n/** Gutter on each side: two cells, one on very narrow terminals. */\nexport function pageMargin(columns: number): number {\n return columns < 50 ? 1 : PAGE_MARGIN;\n}\n\n/** Usable content width inside the page gutter. */\nexport function contentWidth(columns: number): number {\n return Math.max(20, columns - pageMargin(columns) * 2);\n}\n\n/**\n * Page frame: a gutter on both sides and no background. Ink clears the whole\n * terminal when the live region reaches the terminal height, so screens size\n * themselves from `rows` and stay at least one line shorter.\n */\nexport function Page({ columns, children }: { columns: number; children: ReactNode }) {\n const margin = pageMargin(columns);\n return (\n <Box flexDirection=\"column\" paddingLeft={margin} paddingRight={margin} width={columns}>\n {children}\n </Box>\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["Spinner", "Elapsed", "useEffect", "useState", "jsx"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRequire as __tabbioCreateRequire } from 'node:module';
|
|
2
|
+
const require = __tabbioCreateRequire(import.meta.url);
|
|
3
|
+
|
|
4
|
+
// src/core/cancellation.ts
|
|
5
|
+
var activeCommand = new AbortController();
|
|
6
|
+
var activeCommandSignal = activeCommand.signal;
|
|
7
|
+
function cancelActiveCommand() {
|
|
8
|
+
if (!activeCommand.signal.aborted) activeCommand.abort();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
activeCommandSignal,
|
|
13
|
+
cancelActiveCommand
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=chunk-NK5RPNSV.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/core/cancellation.ts"],
|
|
4
|
+
"sourcesContent": ["/** Shared cancellation for commands without their own SIGINT listener. */\nconst activeCommand = new AbortController();\n\nexport const activeCommandSignal = activeCommand.signal;\n\nexport function cancelActiveCommand(): void {\n if (!activeCommand.signal.aborted) activeCommand.abort();\n}\n"],
|
|
5
|
+
"mappings": ";;;;AACA,IAAM,gBAAgB,IAAI,gBAAgB;AAEnC,IAAM,sBAAsB,cAAc;AAE1C,SAAS,sBAA4B;AAC1C,MAAI,CAAC,cAAc,OAAO,QAAS,eAAc,MAAM;AACzD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|