oharness 0.1.0
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/dist/bin/sync.d.ts +3 -0
- package/dist/bin/sync.js +4 -0
- package/dist/bin/sync.js.map +1 -0
- package/dist/bin/web.d.ts +10 -0
- package/dist/bin/web.js +11 -0
- package/dist/bin/web.js.map +1 -0
- package/dist/headless/main.d.ts +2 -0
- package/dist/headless/main.js +133 -0
- package/dist/headless/main.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/providers.d.ts +2 -0
- package/dist/providers.js +3 -0
- package/dist/providers.js.map +1 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +3 -0
- package/dist/tools.js.map +1 -0
- package/dist/ui/artifact.d.ts +32 -0
- package/dist/ui/artifact.js +49 -0
- package/dist/ui/artifact.js.map +1 -0
- package/dist/ui/auth.d.ts +14 -0
- package/dist/ui/auth.js +177 -0
- package/dist/ui/auth.js.map +1 -0
- package/dist/ui/colors.d.ts +21 -0
- package/dist/ui/colors.js +28 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/ui/ink/app.d.ts +13 -0
- package/dist/ui/ink/app.js +418 -0
- package/dist/ui/ink/app.js.map +1 -0
- package/dist/ui/ink/logo.d.ts +6 -0
- package/dist/ui/ink/logo.js +20 -0
- package/dist/ui/ink/logo.js.map +1 -0
- package/dist/ui/ink/main.d.ts +2 -0
- package/dist/ui/ink/main.js +55 -0
- package/dist/ui/ink/main.js.map +1 -0
- package/dist/ui/ink/screen.d.ts +15 -0
- package/dist/ui/ink/screen.js +76 -0
- package/dist/ui/ink/screen.js.map +1 -0
- package/dist/ui/ink/select.d.ts +24 -0
- package/dist/ui/ink/select.js +74 -0
- package/dist/ui/ink/select.js.map +1 -0
- package/dist/ui/main.d.ts +2 -0
- package/dist/ui/main.js +1062 -0
- package/dist/ui/main.js.map +1 -0
- package/dist/ui/render.d.ts +15 -0
- package/dist/ui/render.js +130 -0
- package/dist/ui/render.js.map +1 -0
- package/dist/ui/select.d.ts +26 -0
- package/dist/ui/select.js +107 -0
- package/dist/ui/select.js.map +1 -0
- package/dist/ui/tui/app.d.ts +96 -0
- package/dist/ui/tui/app.js +476 -0
- package/dist/ui/tui/app.js.map +1 -0
- package/dist/ui/tui/editor.d.ts +40 -0
- package/dist/ui/tui/editor.js +124 -0
- package/dist/ui/tui/editor.js.map +1 -0
- package/dist/ui/tui/keys.d.ts +25 -0
- package/dist/ui/tui/keys.js +133 -0
- package/dist/ui/tui/keys.js.map +1 -0
- package/dist/ui/tui/layout.d.ts +63 -0
- package/dist/ui/tui/layout.js +202 -0
- package/dist/ui/tui/layout.js.map +1 -0
- package/dist/ui/tui/logo.d.ts +18 -0
- package/dist/ui/tui/logo.js +33 -0
- package/dist/ui/tui/logo.js.map +1 -0
- package/dist/ui/tui/screen.d.ts +48 -0
- package/dist/ui/tui/screen.js +150 -0
- package/dist/ui/tui/screen.js.map +1 -0
- package/dist/ui/tui/width.d.ts +30 -0
- package/dist/ui/tui/width.js +197 -0
- package/dist/ui/tui/width.js.map +1 -0
- package/dist/ui/wordmark.d.ts +30 -0
- package/dist/ui/wordmark.js +63 -0
- package/dist/ui/wordmark.js.map +1 -0
- package/package.json +49 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal ownership: the alternate buffer, raw mode, and getting out again.
|
|
3
|
+
*
|
|
4
|
+
* The failure that matters here is not a misdrawn frame. It is exiting without
|
|
5
|
+
* restoring: the user is left in raw mode with no echo and no cursor, facing a
|
|
6
|
+
* terminal that appears frozen, and their only recovery is to type `reset`
|
|
7
|
+
* blind. So restoration is wired to every exit path there is — normal return,
|
|
8
|
+
* thrown exception, unhandled rejection, and the signals a terminal or a
|
|
9
|
+
* supervisor can deliver — and it is idempotent, because several of those can
|
|
10
|
+
* fire together.
|
|
11
|
+
*/
|
|
12
|
+
const ESC = "\u001b";
|
|
13
|
+
const ALT_SCREEN_ON = `${ESC}[?1049h`;
|
|
14
|
+
const ALT_SCREEN_OFF = `${ESC}[?1049l`;
|
|
15
|
+
const CURSOR_HIDE = `${ESC}[?25l`;
|
|
16
|
+
const CURSOR_SHOW = `${ESC}[?25h`;
|
|
17
|
+
const CURSOR_HOME = `${ESC}[H`;
|
|
18
|
+
const CLEAR_BELOW = `${ESC}[J`;
|
|
19
|
+
const CLEAR_LINE_END = `${ESC}[K`;
|
|
20
|
+
/**
|
|
21
|
+
* Synchronized output: the terminal buffers everything between these and
|
|
22
|
+
* presents it at once. Terminals that do not support it ignore the sequence,
|
|
23
|
+
* so it costs nothing and removes tearing on the ones that do.
|
|
24
|
+
*/
|
|
25
|
+
const SYNC_BEGIN = `${ESC}[?2026h`;
|
|
26
|
+
const SYNC_END = `${ESC}[?2026l`;
|
|
27
|
+
/**
|
|
28
|
+
* How long a signal waits for `beforeExit`.
|
|
29
|
+
*
|
|
30
|
+
* A tool that ignores its abort signal must not keep the process alive, and a
|
|
31
|
+
* supervisor that sent SIGTERM will follow with SIGKILL soon enough anyway.
|
|
32
|
+
*/
|
|
33
|
+
const EXIT_GRACE_MS = 5_000;
|
|
34
|
+
/**
|
|
35
|
+
* Streams are injectable so the restore path can be tested.
|
|
36
|
+
*
|
|
37
|
+
* Not a general-purpose seam: it exists because "does this leave the terminal
|
|
38
|
+
* usable" is the one behaviour here worth proving, and proving it against the
|
|
39
|
+
* real process streams would wreck the terminal running the tests.
|
|
40
|
+
*/
|
|
41
|
+
export function openScreen(streams, opts) {
|
|
42
|
+
const out = (streams?.out ?? process.stdout);
|
|
43
|
+
const input = (streams?.input ?? process.stdin);
|
|
44
|
+
const wasRaw = input.isRaw ?? false;
|
|
45
|
+
let closed = false;
|
|
46
|
+
let keyHandler;
|
|
47
|
+
let resizeHandler;
|
|
48
|
+
const restore = () => {
|
|
49
|
+
if (closed)
|
|
50
|
+
return;
|
|
51
|
+
closed = true;
|
|
52
|
+
// Order matters: leave raw mode before the alt buffer, so a terminal that
|
|
53
|
+
// redraws on buffer switch does it with echo already back on.
|
|
54
|
+
try {
|
|
55
|
+
if (input.isTTY)
|
|
56
|
+
input.setRawMode(wasRaw);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// A stdin that has already been torn down cannot be restored, and
|
|
60
|
+
// throwing here would mask whatever error is actually ending the process.
|
|
61
|
+
}
|
|
62
|
+
input.pause();
|
|
63
|
+
out.write(`${CURSOR_SHOW}${ALT_SCREEN_OFF}`);
|
|
64
|
+
process.off("exit", restore);
|
|
65
|
+
process.off("SIGINT", onSignal);
|
|
66
|
+
process.off("SIGTERM", onSignal);
|
|
67
|
+
process.off("SIGHUP", onSignal);
|
|
68
|
+
out.off("resize", onResize);
|
|
69
|
+
input.off("data", onData);
|
|
70
|
+
};
|
|
71
|
+
const onSignal = () => {
|
|
72
|
+
// Restore before waiting, not after: the terminal has to come back even if
|
|
73
|
+
// the flush below hangs, and `render` is a no-op once closed, so nothing
|
|
74
|
+
// can draw over the restored screen in the meantime. `restore` also
|
|
75
|
+
// unregisters these handlers, so a second signal takes the default action
|
|
76
|
+
// and kills us immediately — which is what an impatient user wants.
|
|
77
|
+
restore();
|
|
78
|
+
const pending = opts?.beforeExit?.();
|
|
79
|
+
// Exit code 130 by convention, so a supervisor reading the status can see
|
|
80
|
+
// that a signal ended this rather than a clean return.
|
|
81
|
+
if (!pending)
|
|
82
|
+
process.exit(130);
|
|
83
|
+
void Promise.race([
|
|
84
|
+
pending.catch(() => { }),
|
|
85
|
+
new Promise((resolve) => setTimeout(resolve, EXIT_GRACE_MS)),
|
|
86
|
+
]).then(() => process.exit(130));
|
|
87
|
+
};
|
|
88
|
+
const onData = (data) => keyHandler?.(data.toString());
|
|
89
|
+
const onResize = () => resizeHandler?.();
|
|
90
|
+
if (input.isTTY)
|
|
91
|
+
input.setRawMode(true);
|
|
92
|
+
input.resume();
|
|
93
|
+
out.write(`${ALT_SCREEN_ON}${CURSOR_HIDE}${CURSOR_HOME}${CLEAR_BELOW}`);
|
|
94
|
+
// `exit` covers a normal return and an explicit process.exit; the two
|
|
95
|
+
// handlers below cover the errors that would otherwise bypass it.
|
|
96
|
+
process.on("exit", restore);
|
|
97
|
+
process.on("SIGINT", onSignal);
|
|
98
|
+
process.on("SIGTERM", onSignal);
|
|
99
|
+
process.on("SIGHUP", onSignal);
|
|
100
|
+
process.on("uncaughtException", (err) => {
|
|
101
|
+
restore();
|
|
102
|
+
console.error(err);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
|
105
|
+
process.on("unhandledRejection", (err) => {
|
|
106
|
+
restore();
|
|
107
|
+
console.error(err);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
});
|
|
110
|
+
out.on("resize", onResize);
|
|
111
|
+
input.on("data", onData);
|
|
112
|
+
return {
|
|
113
|
+
get rows() {
|
|
114
|
+
// A terminal that reports nothing still has to be given a size, or every
|
|
115
|
+
// layout calculation divides into an undefined viewport.
|
|
116
|
+
return out.rows || 24;
|
|
117
|
+
},
|
|
118
|
+
get columns() {
|
|
119
|
+
return out.columns || 80;
|
|
120
|
+
},
|
|
121
|
+
render(lines, cursor) {
|
|
122
|
+
if (closed)
|
|
123
|
+
return;
|
|
124
|
+
const height = out.rows || 24;
|
|
125
|
+
const frame = lines.slice(0, height);
|
|
126
|
+
let buffer = `${SYNC_BEGIN}${CURSOR_HIDE}${CURSOR_HOME}`;
|
|
127
|
+
for (let i = 0; i < frame.length; i++) {
|
|
128
|
+
// Erase to end of line per row instead of clearing the screen first:
|
|
129
|
+
// a full clear makes every frame flash on terminals without
|
|
130
|
+
// synchronized output.
|
|
131
|
+
buffer += `${frame[i]}${CLEAR_LINE_END}`;
|
|
132
|
+
if (i < frame.length - 1)
|
|
133
|
+
buffer += "\r\n";
|
|
134
|
+
}
|
|
135
|
+
buffer += CLEAR_BELOW;
|
|
136
|
+
if (cursor)
|
|
137
|
+
buffer += `${ESC}[${cursor.row};${cursor.column}H${CURSOR_SHOW}`;
|
|
138
|
+
buffer += SYNC_END;
|
|
139
|
+
out.write(buffer);
|
|
140
|
+
},
|
|
141
|
+
onKey(handler) {
|
|
142
|
+
keyHandler = handler;
|
|
143
|
+
},
|
|
144
|
+
onResize(handler) {
|
|
145
|
+
resizeHandler = handler;
|
|
146
|
+
},
|
|
147
|
+
close: restore,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=screen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screen.js","sourceRoot":"","sources":["../../../src/ui/tui/screen.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How wide a string is on a terminal, and how to break it into rows.
|
|
3
|
+
*
|
|
4
|
+
* Every row count in the layout comes from here, so the two ways a naive
|
|
5
|
+
* `string.length` lies both have to be handled:
|
|
6
|
+
*
|
|
7
|
+
* - ANSI colour codes are in the string but occupy no columns.
|
|
8
|
+
* - East Asian characters occupy two columns each. Getting this wrong makes
|
|
9
|
+
* Chinese text wrap short, and every row below it lands in the wrong place.
|
|
10
|
+
*/
|
|
11
|
+
export declare function stripAnsi(text: string): string;
|
|
12
|
+
export declare function charWidth(codePoint: number): number;
|
|
13
|
+
/** Columns the string occupies once colour codes are discounted. */
|
|
14
|
+
export declare function visibleWidth(text: string): number;
|
|
15
|
+
/**
|
|
16
|
+
* Cut a string to at most `columns`, appending an ellipsis when it did not fit.
|
|
17
|
+
*
|
|
18
|
+
* Cuts on character boundaries, never inside a surrogate pair, and never
|
|
19
|
+
* halfway through a wide character — a half-drawn CJK glyph corrupts every
|
|
20
|
+
* column after it on that row.
|
|
21
|
+
*/
|
|
22
|
+
export declare function truncate(text: string, columns: number): string;
|
|
23
|
+
/**
|
|
24
|
+
* Break text into rows no wider than `columns`.
|
|
25
|
+
*
|
|
26
|
+
* Wraps at word boundaries when it can and mid-word when a single word is
|
|
27
|
+
* longer than the line, which is common for paths and stack traces. Explicit
|
|
28
|
+
* newlines always start a new row.
|
|
29
|
+
*/
|
|
30
|
+
export declare function wrap(text: string, columns: number): string[];
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How wide a string is on a terminal, and how to break it into rows.
|
|
3
|
+
*
|
|
4
|
+
* Every row count in the layout comes from here, so the two ways a naive
|
|
5
|
+
* `string.length` lies both have to be handled:
|
|
6
|
+
*
|
|
7
|
+
* - ANSI colour codes are in the string but occupy no columns.
|
|
8
|
+
* - East Asian characters occupy two columns each. Getting this wrong makes
|
|
9
|
+
* Chinese text wrap short, and every row below it lands in the wrong place.
|
|
10
|
+
*/
|
|
11
|
+
// eslint-disable-next-line no-control-regex
|
|
12
|
+
const ANSI = /\u001b\[[0-9;?]*[a-zA-Z]/g;
|
|
13
|
+
export function stripAnsi(text) {
|
|
14
|
+
return text.replace(ANSI, "");
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Columns occupied by one code point.
|
|
18
|
+
*
|
|
19
|
+
* Ranges follow Unicode's East Asian Width property (W and F). This is the
|
|
20
|
+
* same table every terminal uses; an approximation here would show up as text
|
|
21
|
+
* drifting one column further right with each CJK character on the line.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Code point ranges that occupy two columns, sorted and non-overlapping.
|
|
25
|
+
*
|
|
26
|
+
* A table rather than a chain of `||` conditions, because a reviewer can check
|
|
27
|
+
* a table against Unicode's EastAsianWidth.txt. The first version of this was
|
|
28
|
+
* a boolean expression and it silently gave U+1F680 (🚀) a width of one: the
|
|
29
|
+
* emoji block was written as ending at 1F64F, and nothing said otherwise.
|
|
30
|
+
*/
|
|
31
|
+
const WIDE = [
|
|
32
|
+
[0x1100, 0x115f], // Hangul Jamo
|
|
33
|
+
[0x2329, 0x232a], // angle brackets
|
|
34
|
+
[0x2e80, 0x303e], // CJK radicals, Kangxi, CJK symbols
|
|
35
|
+
[0x3041, 0x33ff], // kana, Hangul compat, CJK compat
|
|
36
|
+
[0x3400, 0x4dbf], // CJK ext A
|
|
37
|
+
[0x4e00, 0x9fff], // CJK unified
|
|
38
|
+
[0xa000, 0xa4cf], // Yi
|
|
39
|
+
[0xa960, 0xa97f], // Hangul Jamo ext A
|
|
40
|
+
[0xac00, 0xd7a3], // Hangul syllables
|
|
41
|
+
[0xf900, 0xfaff], // CJK compat ideographs
|
|
42
|
+
[0xfe10, 0xfe19], // vertical forms
|
|
43
|
+
[0xfe30, 0xfe6f], // CJK compat forms
|
|
44
|
+
[0xff00, 0xff60], // fullwidth forms
|
|
45
|
+
[0xffe0, 0xffe6], // fullwidth signs
|
|
46
|
+
[0x1f300, 0x1f64f], // misc symbols, emoticons
|
|
47
|
+
[0x1f680, 0x1f6ff], // transport and map symbols
|
|
48
|
+
[0x1f7e0, 0x1f7eb], // coloured shapes
|
|
49
|
+
[0x1f900, 0x1f9ff], // supplemental symbols, people
|
|
50
|
+
[0x1fa70, 0x1faff], // extended-A symbols
|
|
51
|
+
[0x20000, 0x2fffd], // CJK ext B onward
|
|
52
|
+
[0x30000, 0x3fffd],
|
|
53
|
+
];
|
|
54
|
+
export function charWidth(codePoint) {
|
|
55
|
+
// Control characters and combining marks take no space of their own.
|
|
56
|
+
if (codePoint === 0)
|
|
57
|
+
return 0;
|
|
58
|
+
if (codePoint < 32 || (codePoint >= 0x7f && codePoint < 0xa0))
|
|
59
|
+
return 0;
|
|
60
|
+
if (isCombining(codePoint))
|
|
61
|
+
return 0;
|
|
62
|
+
for (const [low, high] of WIDE) {
|
|
63
|
+
if (codePoint < low)
|
|
64
|
+
break; // sorted, so nothing further can match
|
|
65
|
+
if (codePoint <= high)
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
function isCombining(codePoint) {
|
|
71
|
+
return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
|
|
72
|
+
(codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
|
|
73
|
+
(codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
|
|
74
|
+
(codePoint >= 0xfe00 && codePoint <= 0xfe0f) || // variation selectors
|
|
75
|
+
(codePoint >= 0xfe20 && codePoint <= 0xfe2f));
|
|
76
|
+
}
|
|
77
|
+
/** Columns the string occupies once colour codes are discounted. */
|
|
78
|
+
export function visibleWidth(text) {
|
|
79
|
+
let total = 0;
|
|
80
|
+
for (const char of stripAnsi(text))
|
|
81
|
+
total += charWidth(char.codePointAt(0));
|
|
82
|
+
return total;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Cut a string to at most `columns`, appending an ellipsis when it did not fit.
|
|
86
|
+
*
|
|
87
|
+
* Cuts on character boundaries, never inside a surrogate pair, and never
|
|
88
|
+
* halfway through a wide character — a half-drawn CJK glyph corrupts every
|
|
89
|
+
* column after it on that row.
|
|
90
|
+
*/
|
|
91
|
+
export function truncate(text, columns) {
|
|
92
|
+
if (columns <= 0)
|
|
93
|
+
return "";
|
|
94
|
+
if (visibleWidth(text) <= columns)
|
|
95
|
+
return text;
|
|
96
|
+
const budget = columns - 1; // room for the ellipsis
|
|
97
|
+
let width = 0;
|
|
98
|
+
let out = "";
|
|
99
|
+
for (const char of stripAnsi(text)) {
|
|
100
|
+
const w = charWidth(char.codePointAt(0));
|
|
101
|
+
if (width + w > budget)
|
|
102
|
+
break;
|
|
103
|
+
width += w;
|
|
104
|
+
out += char;
|
|
105
|
+
}
|
|
106
|
+
return `${out}…`;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Break text into rows no wider than `columns`.
|
|
110
|
+
*
|
|
111
|
+
* Wraps at word boundaries when it can and mid-word when a single word is
|
|
112
|
+
* longer than the line, which is common for paths and stack traces. Explicit
|
|
113
|
+
* newlines always start a new row.
|
|
114
|
+
*/
|
|
115
|
+
export function wrap(text, columns) {
|
|
116
|
+
if (columns <= 0)
|
|
117
|
+
return [""];
|
|
118
|
+
const rows = [];
|
|
119
|
+
for (const paragraph of text.split("\n")) {
|
|
120
|
+
if (paragraph === "") {
|
|
121
|
+
rows.push("");
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
let row = "";
|
|
125
|
+
let width = 0;
|
|
126
|
+
for (const word of splitWords(paragraph)) {
|
|
127
|
+
const wordWidth = visibleWidth(word);
|
|
128
|
+
// A word longer than the whole line has to be broken; otherwise it would
|
|
129
|
+
// either overflow the row or loop forever looking for a fit.
|
|
130
|
+
if (wordWidth > columns) {
|
|
131
|
+
if (row) {
|
|
132
|
+
rows.push(row);
|
|
133
|
+
row = "";
|
|
134
|
+
width = 0;
|
|
135
|
+
}
|
|
136
|
+
for (const piece of hardBreak(word, columns)) {
|
|
137
|
+
if (visibleWidth(piece) === columns)
|
|
138
|
+
rows.push(piece);
|
|
139
|
+
else {
|
|
140
|
+
row = piece;
|
|
141
|
+
width = visibleWidth(piece);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (width + wordWidth > columns) {
|
|
147
|
+
rows.push(row);
|
|
148
|
+
row = word.trimStart();
|
|
149
|
+
width = visibleWidth(row);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
row += word;
|
|
153
|
+
width += wordWidth;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
rows.push(row);
|
|
157
|
+
}
|
|
158
|
+
return rows;
|
|
159
|
+
}
|
|
160
|
+
/** Words with their trailing spaces attached, so wrapping keeps the gaps. */
|
|
161
|
+
function splitWords(text) {
|
|
162
|
+
return text.match(/\S+\s*|\s+/g) ?? [];
|
|
163
|
+
}
|
|
164
|
+
function hardBreak(word, columns) {
|
|
165
|
+
const pieces = [];
|
|
166
|
+
let piece = "";
|
|
167
|
+
let width = 0;
|
|
168
|
+
for (const char of word) {
|
|
169
|
+
const w = charWidth(char.codePointAt(0));
|
|
170
|
+
// A glyph wider than the entire line has no faithful rendering — at one
|
|
171
|
+
// column, a CJK character simply cannot be drawn. Emitting it anyway would
|
|
172
|
+
// produce an over-wide row, and every row count below it would then be
|
|
173
|
+
// wrong. A marker keeps the invariant and still shows something is there.
|
|
174
|
+
if (w > columns) {
|
|
175
|
+
if (piece) {
|
|
176
|
+
pieces.push(piece);
|
|
177
|
+
piece = "";
|
|
178
|
+
width = 0;
|
|
179
|
+
}
|
|
180
|
+
pieces.push("…");
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (width + w > columns) {
|
|
184
|
+
pieces.push(piece);
|
|
185
|
+
piece = char;
|
|
186
|
+
width = w;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
piece += char;
|
|
190
|
+
width += w;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (piece)
|
|
194
|
+
pieces.push(piece);
|
|
195
|
+
return pieces;
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=width.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"width.js","sourceRoot":"","sources":["../../../src/ui/tui/width.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The brand: the mark from `website/public/logo.svg`, and the name beside it.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from either frontend because both draw it — the hand-written TUI
|
|
5
|
+
* writes rows into a frame, Ink wraps them in components — and two copies of a
|
|
6
|
+
* letterform drift into two different logos. Neither the mark nor the glyphs
|
|
7
|
+
* carry colour; the caller paints them, because Ink and raw ANSI have no way
|
|
8
|
+
* to share that part.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The mark: a ring broken on the two diagonals, with a dot at its centre.
|
|
12
|
+
*
|
|
13
|
+
* The SVG is two hooked strokes — a top edge rounding into a left edge, and a
|
|
14
|
+
* bottom edge rounding into a right edge — leaving gaps at top-right and
|
|
15
|
+
* bottom-left. Box-drawing reproduces that at three rows; a half-block circle
|
|
16
|
+
* would not, because the gaps are the whole idea.
|
|
17
|
+
*/
|
|
18
|
+
export declare const MARK: [string, string, string];
|
|
19
|
+
export declare const MARK_WIDTH = 5;
|
|
20
|
+
/** How the name is spelled everywhere it is drawn. */
|
|
21
|
+
export declare const NAME = "OHarness";
|
|
22
|
+
/** The three rows spelling `text`; characters with no glyph are skipped. */
|
|
23
|
+
export declare function word(text: string): string[];
|
|
24
|
+
/** Gap between the mark and the name, in columns. */
|
|
25
|
+
export declare const MARK_GAP = 3;
|
|
26
|
+
/** The name as three rows, plus the width of mark, gap and name together. */
|
|
27
|
+
export declare function wordmark(): {
|
|
28
|
+
rows: string[];
|
|
29
|
+
width: number;
|
|
30
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The brand: the mark from `website/public/logo.svg`, and the name beside it.
|
|
3
|
+
*
|
|
4
|
+
* Kept apart from either frontend because both draw it — the hand-written TUI
|
|
5
|
+
* writes rows into a frame, Ink wraps them in components — and two copies of a
|
|
6
|
+
* letterform drift into two different logos. Neither the mark nor the glyphs
|
|
7
|
+
* carry colour; the caller paints them, because Ink and raw ANSI have no way
|
|
8
|
+
* to share that part.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* The mark: a ring broken on the two diagonals, with a dot at its centre.
|
|
12
|
+
*
|
|
13
|
+
* The SVG is two hooked strokes — a top edge rounding into a left edge, and a
|
|
14
|
+
* bottom edge rounding into a right edge — leaving gaps at top-right and
|
|
15
|
+
* bottom-left. Box-drawing reproduces that at three rows; a half-block circle
|
|
16
|
+
* would not, because the gaps are the whole idea.
|
|
17
|
+
*/
|
|
18
|
+
export const MARK = [
|
|
19
|
+
"╭─── ",
|
|
20
|
+
"│ ● │",
|
|
21
|
+
" ───╯",
|
|
22
|
+
];
|
|
23
|
+
export const MARK_WIDTH = 5;
|
|
24
|
+
/** How the name is spelled everywhere it is drawn. */
|
|
25
|
+
export const NAME = "OHarness";
|
|
26
|
+
/**
|
|
27
|
+
* Drawn with half-block characters rather than figlet-style ASCII: at terminal
|
|
28
|
+
* resolution, slashes and underscores read as noise, while a block letterform
|
|
29
|
+
* reads as type.
|
|
30
|
+
*
|
|
31
|
+
* Each glyph is three rows of four columns, the last being the gap. Sized so
|
|
32
|
+
* the mark and the whole name fit inside eighty columns, which is still the
|
|
33
|
+
* width a terminal is assumed to have.
|
|
34
|
+
*/
|
|
35
|
+
const GLYPHS = {
|
|
36
|
+
O: ["█▀▀█", "█ █", "▀▀▀▀"],
|
|
37
|
+
H: ["█ █", "█▀▀█", "▀ ▀"],
|
|
38
|
+
a: ["█▀▀█", "█▀▀█", "▀ ▀"],
|
|
39
|
+
e: ["█▀▀█", "█▀▀▀", "▀▀▀▀"],
|
|
40
|
+
n: ["█▀▀█", "█ █", "▀ ▀"],
|
|
41
|
+
r: ["█▀▀█", "█▀▀ ", "▀ ▀"],
|
|
42
|
+
s: ["█▀▀▀", "▀▀▀█", "▀▀▀▀"],
|
|
43
|
+
};
|
|
44
|
+
/** The three rows spelling `text`; characters with no glyph are skipped. */
|
|
45
|
+
export function word(text) {
|
|
46
|
+
const rows = ["", "", ""];
|
|
47
|
+
for (const char of text) {
|
|
48
|
+
const glyph = GLYPHS[char];
|
|
49
|
+
if (!glyph)
|
|
50
|
+
continue;
|
|
51
|
+
for (let i = 0; i < 3; i++)
|
|
52
|
+
rows[i] += glyph[i] + " ";
|
|
53
|
+
}
|
|
54
|
+
return rows.map((row) => row.trimEnd());
|
|
55
|
+
}
|
|
56
|
+
/** Gap between the mark and the name, in columns. */
|
|
57
|
+
export const MARK_GAP = 3;
|
|
58
|
+
/** The name as three rows, plus the width of mark, gap and name together. */
|
|
59
|
+
export function wordmark() {
|
|
60
|
+
const rows = word(NAME);
|
|
61
|
+
return { rows, width: MARK_WIDTH + MARK_GAP + rows[0].length };
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=wordmark.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wordmark.js","sourceRoot":"","sources":["../../src/ui/wordmark.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,IAAI,GAA6B;IAC5C,OAAO;IACP,OAAO;IACP,OAAO;CACR,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC;AAE5B,sDAAsD;AACtD,MAAM,CAAC,MAAM,IAAI,GAAG,UAAU,CAAC;AAE/B;;;;;;;;GAQG;AACH,MAAM,MAAM,GAA6C;IACvD,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAC3B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAC3B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAC3B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAC3B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAC3B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;IAC3B,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,4EAA4E;AAC5E,MAAM,UAAU,IAAI,CAAC,IAAY;IAC/B,MAAM,IAAI,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;IAC1B,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3B,IAAI,CAAC,KAAK;YAAE,SAAS;QACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;YAAE,IAAI,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC;IACxD,CAAC;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED,qDAAqD;AACrD,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,CAAC;AAE1B,6EAA6E;AAC7E,MAAM,UAAU,QAAQ;IACtB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;IACxB,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,GAAG,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC,MAAM,EAAE,CAAC;AAClE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "oharness",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A model-agnostic agent harness: swappable providers, tool calling, MCP, subagents, compaction, hooks and permissions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.6.0"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"oharness": "./dist/ui/main.js",
|
|
12
|
+
"oharness-headless": "./dist/headless/main.js",
|
|
13
|
+
"oharness-web": "./dist/bin/web.js",
|
|
14
|
+
"oharness-sync": "./dist/bin/sync.js"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"oharness-source": "./src/index.ts",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./providers": {
|
|
25
|
+
"oharness-source": "./src/providers.ts",
|
|
26
|
+
"types": "./dist/providers.d.ts",
|
|
27
|
+
"default": "./dist/providers.js"
|
|
28
|
+
},
|
|
29
|
+
"./tools": {
|
|
30
|
+
"oharness-source": "./src/tools.ts",
|
|
31
|
+
"types": "./dist/tools.d.ts",
|
|
32
|
+
"default": "./dist/tools.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md"
|
|
38
|
+
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@oharness/engine": "0.1.0",
|
|
44
|
+
"@oharness/server": "0.1.0",
|
|
45
|
+
"@oharness/sync": "0.1.0",
|
|
46
|
+
"ink": "^7.1.1",
|
|
47
|
+
"react": "^19.2.8"
|
|
48
|
+
}
|
|
49
|
+
}
|