toolcraft 0.0.25 → 0.0.26
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/cli.js +11 -9
- package/dist/error-report.js +14 -11
- package/dist/redaction.d.ts +4 -0
- package/dist/redaction.js +70 -0
- package/node_modules/@poe-code/config-mutations/dist/execution/apply-mutation.js +33 -9
- package/node_modules/@poe-code/config-mutations/dist/formats/json.d.ts +2 -1
- package/node_modules/@poe-code/config-mutations/dist/formats/json.js +36 -9
- package/node_modules/@poe-code/config-mutations/dist/types.d.ts +2 -0
- package/node_modules/@poe-code/design-system/dist/components/browser.js +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/actions.js +1 -1
- package/node_modules/@poe-code/design-system/dist/explorer/keymap.js +11 -1
- package/node_modules/@poe-code/design-system/dist/explorer/reducer.js +64 -8
- package/node_modules/@poe-code/design-system/dist/explorer/render/detail.js +9 -11
- package/node_modules/@poe-code/design-system/dist/explorer/render/footer.js +18 -8
- package/node_modules/@poe-code/design-system/dist/explorer/render/header.js +11 -18
- package/node_modules/@poe-code/design-system/dist/explorer/render/index.js +2 -10
- package/node_modules/@poe-code/design-system/dist/explorer/render/list.js +32 -22
- package/node_modules/@poe-code/design-system/dist/explorer/render/modal.js +5 -9
- package/node_modules/@poe-code/design-system/dist/explorer/render/text.d.ts +12 -0
- package/node_modules/@poe-code/design-system/dist/explorer/render/text.js +81 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.d.ts +1 -0
- package/node_modules/@poe-code/design-system/dist/explorer/state.js +2 -0
- package/node_modules/@poe-code/design-system/dist/prompts/index.js +3 -3
- package/node_modules/@poe-code/process-runner/dist/docker/docker-execution-env.js +24 -3
- package/node_modules/@poe-code/process-runner/dist/docker/docker-runner.js +1 -0
- package/node_modules/auth-store/dist/keychain-store.js +20 -1
- package/node_modules/mcp-oauth/dist/client/auth-store-session-store.js +6 -3
- package/node_modules/tiny-mcp-client/dist/internal.d.ts +2 -0
- package/node_modules/tiny-mcp-client/dist/internal.js +30 -13
- package/node_modules/tiny-mcp-client/src/internal.ts +35 -16
- package/node_modules/tiny-mcp-client/src/transports.test.ts +68 -0
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getExplorerStyles } from "../theme.js";
|
|
2
|
+
import { cellWidth, fitToWidth } from "./text.js";
|
|
2
3
|
export function renderFooter(state, screen, layout) {
|
|
3
4
|
const rect = layout.footer;
|
|
4
5
|
const styles = getExplorerStyles();
|
|
@@ -9,22 +10,31 @@ export function renderFooter(state, screen, layout) {
|
|
|
9
10
|
const hints = footerHints(state);
|
|
10
11
|
let x = rect.x + 2;
|
|
11
12
|
const y = rect.y;
|
|
13
|
+
const endX = rect.x + rect.width;
|
|
12
14
|
for (const hint of hints) {
|
|
13
|
-
if (x >=
|
|
15
|
+
if (x >= endX) {
|
|
14
16
|
break;
|
|
15
17
|
}
|
|
16
18
|
if (hint.bracketed === false) {
|
|
17
19
|
const text = `${hint.key} ${hint.label}`;
|
|
18
|
-
screen
|
|
19
|
-
x += text.length + 2;
|
|
20
|
+
x += putFooterText(screen, x, y, endX, text, hint.running ? styles.muted : {}) + 2;
|
|
20
21
|
continue;
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
x
|
|
24
|
-
|
|
25
|
-
x
|
|
23
|
+
const keyText = `[${hint.key}]`;
|
|
24
|
+
const keyWidth = putFooterText(screen, x, y, endX, keyText, hint.running ? styles.muted : styles.accent);
|
|
25
|
+
x += keyWidth;
|
|
26
|
+
if (keyWidth < cellWidth(keyText) || x >= endX) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
x += putFooterText(screen, x, y, endX, ` ${hint.label}`, hint.running ? styles.muted : {}) + 2;
|
|
26
30
|
}
|
|
27
31
|
}
|
|
32
|
+
function putFooterText(screen, x, y, endX, text, style = {}) {
|
|
33
|
+
const remaining = Math.max(0, endX - x);
|
|
34
|
+
const fitted = fitToWidth(text, remaining, x);
|
|
35
|
+
screen.put(x, y, fitted, style);
|
|
36
|
+
return cellWidth(fitted, x);
|
|
37
|
+
}
|
|
28
38
|
function footerHints(state) {
|
|
29
39
|
const hints = [];
|
|
30
40
|
if (state.focused === "detail") {
|
|
@@ -36,7 +46,7 @@ function footerHints(state) {
|
|
|
36
46
|
continue;
|
|
37
47
|
}
|
|
38
48
|
const key = actionKey(entry, id);
|
|
39
|
-
const label = state.selected.size > 0 && entry.source === "row"
|
|
49
|
+
const label = state.multiSelect && state.selected.size > 0 && entry.source === "row"
|
|
40
50
|
? `${entry.label} ${state.selected.size}`
|
|
41
51
|
: entry.label;
|
|
42
52
|
hints.push({ key, label, running: entry.running === true });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getExplorerStyles } from "../theme.js";
|
|
2
|
+
import { cellWidth, fitToWidth, padEndCells } from "./text.js";
|
|
2
3
|
export function renderHeader(state, screen, layout) {
|
|
3
4
|
const rect = layout.header;
|
|
4
5
|
const styles = getExplorerStyles();
|
|
@@ -7,7 +8,7 @@ export function renderHeader(state, screen, layout) {
|
|
|
7
8
|
return;
|
|
8
9
|
}
|
|
9
10
|
if (layout.mode === "too-narrow") {
|
|
10
|
-
screen.put(0, 0,
|
|
11
|
+
screen.put(0, 0, fitToWidth("Terminal too narrow", rect.width), styles.borderFocused);
|
|
11
12
|
return;
|
|
12
13
|
}
|
|
13
14
|
drawTopBorder(screen, state.title, rect.width, styles.border);
|
|
@@ -15,13 +16,15 @@ export function renderHeader(state, screen, layout) {
|
|
|
15
16
|
const prompt = `${state.title.toLocaleLowerCase()}>`;
|
|
16
17
|
const filter = state.filter.length > 0 ? ` ${state.filter}` : "";
|
|
17
18
|
const count = `${state.filtered.length}/${state.rows.length}`;
|
|
18
|
-
const selected = state.selected.size > 0 ? ` (${state.selected.size} selected)` : "";
|
|
19
|
+
const selected = state.multiSelect && state.selected.size > 0 ? ` (${state.selected.size} selected)` : "";
|
|
19
20
|
const spinner = state.detail.loading ? " *" : "";
|
|
20
21
|
const right = `${count}${selected}${spinner}`;
|
|
21
22
|
screen.put(0, 1, "│", styles.border);
|
|
22
23
|
screen.put(Math.max(0, rect.width - 1), 1, "│", styles.border);
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
const rightWidth = cellWidth(right);
|
|
25
|
+
const promptWidth = Math.max(0, rect.width - rightWidth - 5);
|
|
26
|
+
screen.put(2, 1, fitToWidth(`${prompt}${filter}`, promptWidth, 2), styles.accent);
|
|
27
|
+
screen.put(Math.max(2, rect.width - rightWidth - 2), 1, right, styles.muted);
|
|
25
28
|
}
|
|
26
29
|
if (rect.height > 2) {
|
|
27
30
|
drawHorizontal(screen, 2, rect.width, styles.border);
|
|
@@ -32,11 +35,10 @@ function drawTopBorder(screen, title, width, style) {
|
|
|
32
35
|
screen.put(0, 0, "┌", style);
|
|
33
36
|
return;
|
|
34
37
|
}
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
screen.put(0, 0, `┌${middle.slice(0, Math.max(0, width - 2))}┐`, style);
|
|
38
|
+
const innerWidth = Math.max(0, width - 2);
|
|
39
|
+
const label = fitToWidth(`─ ${title} `, innerWidth, 1);
|
|
40
|
+
const middle = padEndCells(label, innerWidth, "─", 1);
|
|
41
|
+
screen.put(0, 0, `┌${middle}┐`, style);
|
|
40
42
|
}
|
|
41
43
|
function drawHorizontal(screen, y, width, style) {
|
|
42
44
|
if (width === 1) {
|
|
@@ -45,12 +47,3 @@ function drawHorizontal(screen, y, width, style) {
|
|
|
45
47
|
}
|
|
46
48
|
screen.put(0, y, `├${"─".repeat(Math.max(0, width - 2))}┤`, style);
|
|
47
49
|
}
|
|
48
|
-
function fit(text, width) {
|
|
49
|
-
if (width <= 0) {
|
|
50
|
-
return "";
|
|
51
|
-
}
|
|
52
|
-
if (text.length <= width) {
|
|
53
|
-
return text;
|
|
54
|
-
}
|
|
55
|
-
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
|
|
56
|
-
}
|
|
@@ -6,6 +6,7 @@ import { renderFooter } from "./footer.js";
|
|
|
6
6
|
import { renderHeader } from "./header.js";
|
|
7
7
|
import { renderList } from "./list.js";
|
|
8
8
|
import { renderModal } from "./modal.js";
|
|
9
|
+
import { fitToWidth } from "./text.js";
|
|
9
10
|
const REGION_RENDERERS = [
|
|
10
11
|
[REGION_HEADER, renderHeader],
|
|
11
12
|
[REGION_LIST, renderList],
|
|
@@ -42,18 +43,9 @@ function renderToast(state, screen) {
|
|
|
42
43
|
return;
|
|
43
44
|
}
|
|
44
45
|
const styles = getExplorerStyles();
|
|
45
|
-
const message =
|
|
46
|
+
const message = fitToWidth(` ${state.toast.message} `, screen.width);
|
|
46
47
|
screen.put(0, y, message, styles.accent);
|
|
47
48
|
}
|
|
48
|
-
function fit(text, width) {
|
|
49
|
-
if (width <= 0) {
|
|
50
|
-
return "";
|
|
51
|
-
}
|
|
52
|
-
if (text.length <= width) {
|
|
53
|
-
return text;
|
|
54
|
-
}
|
|
55
|
-
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
|
|
56
|
-
}
|
|
57
49
|
export { renderDetail } from "./detail.js";
|
|
58
50
|
export { renderFooter } from "./footer.js";
|
|
59
51
|
export { renderHeader } from "./header.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getExplorerStyles } from "../theme.js";
|
|
2
|
+
import { cellWidth, centerCells, fitToWidth, splitGraphemeCells, stripAnsi } from "./text.js";
|
|
2
3
|
const listLineCache = new WeakMap();
|
|
3
4
|
export function renderList(state, screen, layout) {
|
|
4
5
|
const styles = getExplorerStyles();
|
|
@@ -17,7 +18,7 @@ export function renderList(state, screen, layout) {
|
|
|
17
18
|
listLineCache.set(screen, { rectKey, lines: cache });
|
|
18
19
|
if (state.filtered.length === 0) {
|
|
19
20
|
const hint = state.emptyHint;
|
|
20
|
-
writeLine(screen, rect, Math.floor(rect.height / 2),
|
|
21
|
+
writeLine(screen, rect, Math.floor(rect.height / 2), centerCells(hint, rect.width, rect.x), styles.muted);
|
|
21
22
|
cache.clear();
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
@@ -43,7 +44,7 @@ export function renderList(state, screen, layout) {
|
|
|
43
44
|
if (y >= rect.height) {
|
|
44
45
|
break;
|
|
45
46
|
}
|
|
46
|
-
const selected = state.selected.has(row.id);
|
|
47
|
+
const selected = state.multiSelect && state.selected.has(row.id);
|
|
47
48
|
const cursor = rowIndex === state.filtered[state.cursor];
|
|
48
49
|
const positions = state.matchPositions.get(rowIndex) ?? [];
|
|
49
50
|
const hash = lineHash(row, selected, cursor, positions);
|
|
@@ -67,40 +68,49 @@ function renderRow(screen, rect, rowY, row, opts) {
|
|
|
67
68
|
const marker = opts.selected ? "┃" : " ";
|
|
68
69
|
const cursor = opts.cursor ? "●" : "◌";
|
|
69
70
|
const focus = opts.cursor && opts.focused ? " ▌" : "";
|
|
70
|
-
const badge = row.badge ? ` ${row.badge.text}` : "";
|
|
71
71
|
const prefix = `${marker} ${cursor} `;
|
|
72
|
-
const
|
|
73
|
-
const
|
|
72
|
+
const prefixWidth = cellWidth(prefix, rect.x);
|
|
73
|
+
const focusWidth = cellWidth(focus);
|
|
74
|
+
const badge = row.badge
|
|
75
|
+
? fitToWidth(` ${row.badge.text}`, Math.max(0, rect.width - prefixWidth - focusWidth), rect.x + prefixWidth)
|
|
76
|
+
: "";
|
|
77
|
+
const badgeWidth = cellWidth(badge);
|
|
78
|
+
const available = Math.max(0, rect.width - prefixWidth - focusWidth - badgeWidth);
|
|
79
|
+
const rawTitle = stripAnsi(row.title);
|
|
80
|
+
const titleX = rect.x + prefixWidth;
|
|
81
|
+
const title = fitToWidth(rawTitle, available, titleX);
|
|
82
|
+
const titleWasTruncated = cellWidth(rawTitle, titleX) > available;
|
|
83
|
+
const positions = new Set(opts.positions);
|
|
74
84
|
let x = rect.x;
|
|
75
85
|
const y = rect.y + rowY;
|
|
76
86
|
screen.put(x, y, prefix, opts.cursor ? styles.accent : styles.muted);
|
|
77
|
-
x +=
|
|
78
|
-
for (
|
|
79
|
-
const
|
|
80
|
-
|
|
87
|
+
x += prefixWidth;
|
|
88
|
+
for (const segment of splitGraphemeCells(title, x)) {
|
|
89
|
+
const isTruncationMarker = titleWasTruncated && segment.end === title.length && segment.value === "…";
|
|
90
|
+
const style = !isTruncationMarker && hasMatchPosition(segment.start, segment.end, positions)
|
|
91
|
+
? styles.matchHighlight
|
|
92
|
+
: {};
|
|
93
|
+
screen.put(x, y, segment.value, style);
|
|
94
|
+
x += segment.width;
|
|
81
95
|
}
|
|
82
96
|
if (row.badge) {
|
|
83
|
-
screen.put(rect.x + rect.width -
|
|
97
|
+
screen.put(rect.x + rect.width - badgeWidth - focusWidth, y, badge, styles.tones[row.badge.tone ?? "muted"]);
|
|
84
98
|
}
|
|
85
99
|
if (focus) {
|
|
86
|
-
screen.put(rect.x + rect.width -
|
|
100
|
+
screen.put(rect.x + rect.width - focusWidth, y, focus, styles.borderFocused);
|
|
87
101
|
}
|
|
88
102
|
}
|
|
89
103
|
function writeLine(screen, rect, row, text, style = {}) {
|
|
90
|
-
screen.put(rect.x, rect.y + row,
|
|
104
|
+
screen.put(rect.x, rect.y + row, fitToWidth(text, rect.width, rect.x), style);
|
|
91
105
|
}
|
|
92
106
|
function lineHash(row, selected, cursor, positions) {
|
|
93
107
|
return `${row.id}:${selected ? 1 : 0}:${cursor ? 1 : 0}:${positions.join(",")}`;
|
|
94
108
|
}
|
|
95
|
-
function
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return text;
|
|
109
|
+
function hasMatchPosition(start, end, positions) {
|
|
110
|
+
for (let position = start; position < end; position += 1) {
|
|
111
|
+
if (positions.has(position)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
101
114
|
}
|
|
102
|
-
return
|
|
103
|
-
}
|
|
104
|
-
function stripAnsi(value) {
|
|
105
|
-
return value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
115
|
+
return false;
|
|
106
116
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getExplorerStyles } from "../theme.js";
|
|
2
|
+
import { fitToWidth, padEndCells } from "./text.js";
|
|
2
3
|
export function renderModal(state, screen) {
|
|
3
4
|
if (state.modal === null || screen.width <= 0 || screen.height <= 0) {
|
|
4
5
|
return;
|
|
@@ -11,7 +12,7 @@ export function renderModal(state, screen) {
|
|
|
11
12
|
drawBox(screen, x, y, width, height, title(state), styles.borderFocused);
|
|
12
13
|
const lines = modalLines(state);
|
|
13
14
|
for (let row = 0; row < Math.min(lines.length, height - 2); row += 1) {
|
|
14
|
-
screen.put(x + 2, y + 1 + row,
|
|
15
|
+
screen.put(x + 2, y + 1 + row, fitToWidth(lines[row], width - 4, x + 2), row === 1 ? styles.accent : {});
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
18
|
function modalLines(state) {
|
|
@@ -54,8 +55,9 @@ function modalHeight(state) {
|
|
|
54
55
|
}
|
|
55
56
|
function drawBox(screen, x, y, width, height, boxTitle, style) {
|
|
56
57
|
screen.clearRect({ x, y, width, height });
|
|
57
|
-
const
|
|
58
|
-
|
|
58
|
+
const innerWidth = Math.max(0, width - 2);
|
|
59
|
+
const titleSegment = fitToWidth(`─ ${boxTitle} `, innerWidth, x + 1);
|
|
60
|
+
screen.put(x, y, `╭${padEndCells(titleSegment, innerWidth, "─", x + 1)}╮`, style);
|
|
59
61
|
for (let row = 1; row < height - 1; row += 1) {
|
|
60
62
|
screen.put(x, y + row, "│", style);
|
|
61
63
|
screen.put(x + width - 1, y + row, "│", style);
|
|
@@ -83,9 +85,3 @@ function paletteLines(state) {
|
|
|
83
85
|
function labelFor(action) {
|
|
84
86
|
return typeof action.label === "function" ? action.label() : action.label;
|
|
85
87
|
}
|
|
86
|
-
function fit(text, width) {
|
|
87
|
-
if (text.length <= width) {
|
|
88
|
-
return text;
|
|
89
|
-
}
|
|
90
|
-
return width <= 1 ? text.slice(0, width) : `${text.slice(0, width - 1)}…`;
|
|
91
|
-
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface GraphemeCell {
|
|
2
|
+
value: string;
|
|
3
|
+
start: number;
|
|
4
|
+
end: number;
|
|
5
|
+
width: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function cellWidth(value: string, startColumn?: number): number;
|
|
8
|
+
export declare function fitToWidth(text: string, width: number, startColumn?: number): string;
|
|
9
|
+
export declare function centerCells(text: string, width: number, startColumn?: number): string;
|
|
10
|
+
export declare function padEndCells(text: string, width: number, fill?: string, startColumn?: number): string;
|
|
11
|
+
export declare function splitGraphemeCells(value: string, startColumn?: number): GraphemeCell[];
|
|
12
|
+
export declare function stripAnsi(value: string): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { displayWidth, graphemes } from "../../dashboard/terminal-width.js";
|
|
2
|
+
const ELLIPSIS = "…";
|
|
3
|
+
export function cellWidth(value, startColumn = 0) {
|
|
4
|
+
return displayWidth(value, startColumn);
|
|
5
|
+
}
|
|
6
|
+
export function fitToWidth(text, width, startColumn = 0) {
|
|
7
|
+
if (width <= 0) {
|
|
8
|
+
return "";
|
|
9
|
+
}
|
|
10
|
+
if (cellWidth(text, startColumn) <= width) {
|
|
11
|
+
return text;
|
|
12
|
+
}
|
|
13
|
+
const ellipsisWidth = cellWidth(ELLIPSIS, startColumn);
|
|
14
|
+
if (ellipsisWidth > width) {
|
|
15
|
+
return takeCells(text, width, startColumn);
|
|
16
|
+
}
|
|
17
|
+
const prefix = takeCells(text, width - ellipsisWidth, startColumn);
|
|
18
|
+
return `${prefix}${ELLIPSIS}`;
|
|
19
|
+
}
|
|
20
|
+
export function centerCells(text, width, startColumn = 0) {
|
|
21
|
+
const fitted = fitToWidth(text, width, startColumn);
|
|
22
|
+
const padding = Math.max(0, Math.floor((width - cellWidth(fitted, startColumn)) / 2));
|
|
23
|
+
return `${" ".repeat(padding)}${fitted}`;
|
|
24
|
+
}
|
|
25
|
+
export function padEndCells(text, width, fill = " ", startColumn = 0) {
|
|
26
|
+
let output = takeCells(text, width, startColumn);
|
|
27
|
+
let used = cellWidth(output, startColumn);
|
|
28
|
+
let column = startColumn + used;
|
|
29
|
+
while (used < width) {
|
|
30
|
+
const fillWidth = cellWidth(fill, column);
|
|
31
|
+
if (fill.length === 0 || fillWidth <= 0 || used + fillWidth > width) {
|
|
32
|
+
const spaces = width - used;
|
|
33
|
+
output += " ".repeat(spaces);
|
|
34
|
+
used += spaces;
|
|
35
|
+
column += spaces;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
output += fill;
|
|
39
|
+
used += fillWidth;
|
|
40
|
+
column += fillWidth;
|
|
41
|
+
}
|
|
42
|
+
return output;
|
|
43
|
+
}
|
|
44
|
+
export function splitGraphemeCells(value, startColumn = 0) {
|
|
45
|
+
const cells = [];
|
|
46
|
+
let offset = 0;
|
|
47
|
+
let column = startColumn;
|
|
48
|
+
for (const segment of graphemes(value)) {
|
|
49
|
+
const width = cellWidth(segment, column);
|
|
50
|
+
cells.push({
|
|
51
|
+
value: segment,
|
|
52
|
+
start: offset,
|
|
53
|
+
end: offset + segment.length,
|
|
54
|
+
width
|
|
55
|
+
});
|
|
56
|
+
offset += segment.length;
|
|
57
|
+
column += width;
|
|
58
|
+
}
|
|
59
|
+
return cells;
|
|
60
|
+
}
|
|
61
|
+
export function stripAnsi(value) {
|
|
62
|
+
return value.replace(/\u001b\[[0-9;]*m/g, "");
|
|
63
|
+
}
|
|
64
|
+
function takeCells(text, width, startColumn) {
|
|
65
|
+
if (width <= 0) {
|
|
66
|
+
return "";
|
|
67
|
+
}
|
|
68
|
+
let output = "";
|
|
69
|
+
let used = 0;
|
|
70
|
+
let column = startColumn;
|
|
71
|
+
for (const segment of graphemes(text)) {
|
|
72
|
+
const segmentWidth = cellWidth(segment, column);
|
|
73
|
+
if (used + segmentWidth > width) {
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
output += segment;
|
|
77
|
+
used += segmentWidth;
|
|
78
|
+
column += segmentWidth;
|
|
79
|
+
}
|
|
80
|
+
return output;
|
|
81
|
+
}
|
|
@@ -16,6 +16,7 @@ export function createInitialState(config, size) {
|
|
|
16
16
|
cols: normalizeSize(size.cols),
|
|
17
17
|
rows: normalizeSize(size.rows)
|
|
18
18
|
};
|
|
19
|
+
const multiSelect = config.multiSelect ?? true;
|
|
19
20
|
return {
|
|
20
21
|
title: config.title,
|
|
21
22
|
emptyHint: config.emptyHint ?? "No detail",
|
|
@@ -35,6 +36,7 @@ export function createInitialState(config, size) {
|
|
|
35
36
|
loading: false
|
|
36
37
|
},
|
|
37
38
|
selected: new Set(),
|
|
39
|
+
multiSelect,
|
|
38
40
|
modal: null,
|
|
39
41
|
toast: null,
|
|
40
42
|
dirty: REGION_ALL,
|
|
@@ -95,12 +95,12 @@ export async function withSpinner(options) {
|
|
|
95
95
|
const result = await fn();
|
|
96
96
|
const msg = stopMessage ? stopMessage(result) : undefined;
|
|
97
97
|
if (msg) {
|
|
98
|
-
process.stdout.write(
|
|
98
|
+
process.stdout.write(`${color.green("◆")} ${msg}\n`);
|
|
99
99
|
}
|
|
100
100
|
const sub = subtext ? subtext(result) : undefined;
|
|
101
101
|
if (sub) {
|
|
102
102
|
for (const line of sub.split("\n")) {
|
|
103
|
-
process.stdout.write(
|
|
103
|
+
process.stdout.write(`${color.gray("│")} ${line}\n`);
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
return result;
|
|
@@ -120,7 +120,7 @@ export async function withSpinner(options) {
|
|
|
120
120
|
const sub = subtext ? subtext(result) : undefined;
|
|
121
121
|
if (sub) {
|
|
122
122
|
for (const line of sub.split("\n")) {
|
|
123
|
-
process.stdout.write(
|
|
123
|
+
process.stdout.write(`${color.gray("│")} ${line}\n`);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
return result;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
2
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
-
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
+
import { readdir, readFile, realpath, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { buildDockerEnvArgs, buildDockerRunArgs } from "./args.js";
|
|
@@ -183,8 +183,7 @@ export async function buildDockerRuntimeTemplate(input) {
|
|
|
183
183
|
const runner = input.runner ?? createHostRunner();
|
|
184
184
|
const engine = input.runtime.engine ?? detectEngine();
|
|
185
185
|
const context = detectContext();
|
|
186
|
-
const dockerfilePath =
|
|
187
|
-
const buildContext = path.resolve(input.cwd, input.runtime.build_context ?? ".");
|
|
186
|
+
const { dockerfilePath, buildContext } = await resolveRuntimeBuildPaths(input.cwd, input.runtime);
|
|
188
187
|
const dockerfileBytes = await readFile(dockerfilePath);
|
|
189
188
|
const buildContextFiles = await readBuildContextFiles(buildContext);
|
|
190
189
|
const hash = hashDockerTemplate(dockerfileBytes, buildContextFiles, input.runtime.build_args ?? {}, engine);
|
|
@@ -221,6 +220,28 @@ export async function buildDockerRuntimeTemplate(input) {
|
|
|
221
220
|
cached: false
|
|
222
221
|
};
|
|
223
222
|
}
|
|
223
|
+
async function resolveRuntimeBuildPaths(cwd, runtime) {
|
|
224
|
+
const dockerfilePath = path.resolve(cwd, runtime.dockerfile ?? path.join(".poe-code", "Dockerfile"));
|
|
225
|
+
const buildContext = path.resolve(cwd, runtime.build_context ?? ".");
|
|
226
|
+
const canonicalCwd = await realpath(cwd);
|
|
227
|
+
const canonicalDockerfilePath = await realpath(dockerfilePath);
|
|
228
|
+
const canonicalBuildContext = await realpath(buildContext);
|
|
229
|
+
assertRuntimePathInsideCwd(canonicalCwd, canonicalDockerfilePath, "runtime.dockerfile");
|
|
230
|
+
assertRuntimePathInsideCwd(canonicalCwd, canonicalBuildContext, "runtime.build_context");
|
|
231
|
+
return {
|
|
232
|
+
dockerfilePath: canonicalDockerfilePath,
|
|
233
|
+
buildContext: canonicalBuildContext
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function assertRuntimePathInsideCwd(cwd, targetPath, fieldName) {
|
|
237
|
+
if (!isPathInsideOrEqual(cwd, targetPath)) {
|
|
238
|
+
throw new Error(`${fieldName} must remain inside runtime cwd ${cwd}.`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function isPathInsideOrEqual(rootPath, targetPath) {
|
|
242
|
+
const relativePath = path.relative(rootPath, targetPath);
|
|
243
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
|
|
244
|
+
}
|
|
224
245
|
function hashDockerTemplate(dockerfileBytes, buildContextFiles, buildArgs, engine) {
|
|
225
246
|
const hash = createHash("sha256");
|
|
226
247
|
hash.update(dockerfileBytes);
|
|
@@ -64,6 +64,19 @@ function runSecurityCommand(command, args, options) {
|
|
|
64
64
|
});
|
|
65
65
|
let stdout = "";
|
|
66
66
|
let stderr = "";
|
|
67
|
+
let stdinErrorMessage;
|
|
68
|
+
const appendStderr = (message) => {
|
|
69
|
+
stderr = stderr.length === 0
|
|
70
|
+
? message
|
|
71
|
+
: `${stderr}${stderr.endsWith("\n") ? "" : "\n"}${message}`;
|
|
72
|
+
};
|
|
73
|
+
const appendStdinError = () => {
|
|
74
|
+
if (stdinErrorMessage === undefined) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
appendStderr(stdinErrorMessage);
|
|
78
|
+
stdinErrorMessage = undefined;
|
|
79
|
+
};
|
|
67
80
|
child.stdout?.setEncoding("utf8");
|
|
68
81
|
child.stdout?.on("data", (chunk) => {
|
|
69
82
|
stdout += chunk.toString();
|
|
@@ -73,17 +86,23 @@ function runSecurityCommand(command, args, options) {
|
|
|
73
86
|
stderr += chunk.toString();
|
|
74
87
|
});
|
|
75
88
|
if (options?.stdin !== undefined) {
|
|
89
|
+
child.stdin?.once("error", (error) => {
|
|
90
|
+
stdinErrorMessage = error instanceof Error ? error.message : String(error);
|
|
91
|
+
});
|
|
76
92
|
child.stdin?.end(options.stdin);
|
|
77
93
|
}
|
|
78
94
|
child.on("error", (error) => {
|
|
79
95
|
const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
96
|
+
appendStdinError();
|
|
97
|
+
appendStderr(message);
|
|
80
98
|
resolve({
|
|
81
99
|
stdout,
|
|
82
|
-
stderr
|
|
100
|
+
stderr,
|
|
83
101
|
exitCode: 127
|
|
84
102
|
});
|
|
85
103
|
});
|
|
86
104
|
child.on("close", (code) => {
|
|
105
|
+
appendStdinError();
|
|
87
106
|
resolve({
|
|
88
107
|
stdout,
|
|
89
108
|
stderr,
|
|
@@ -61,12 +61,15 @@ export function createAuthStoreClientStore(options) {
|
|
|
61
61
|
}
|
|
62
62
|
function createNamedSecretStore(key, options, defaults) {
|
|
63
63
|
const hash = crypto.createHash("sha256").update(key).digest("hex");
|
|
64
|
-
const
|
|
64
|
+
const configuredFilePath = options.fileStore?.filePath;
|
|
65
|
+
const parsedFilePath = configuredFilePath === undefined ? null : path.parse(configuredFilePath);
|
|
65
66
|
const fileStore = {
|
|
66
67
|
...options.fileStore,
|
|
68
|
+
filePath: parsedFilePath === null
|
|
69
|
+
? undefined
|
|
70
|
+
: path.join(parsedFilePath.dir, `${parsedFilePath.name}-${hash}${parsedFilePath.ext || ".enc"}`),
|
|
67
71
|
salt: options.fileStore?.salt ?? defaults.salt,
|
|
68
|
-
defaultDirectory:
|
|
69
|
-
options.fileStore?.defaultDirectory ||
|
|
72
|
+
defaultDirectory: options.fileStore?.defaultDirectory ||
|
|
70
73
|
defaults.directory,
|
|
71
74
|
defaultFileName: parsedFilePath === null
|
|
72
75
|
? `${hash}.enc`
|
|
@@ -400,6 +400,7 @@ export declare class StdioTransport implements McpTransport {
|
|
|
400
400
|
private static readonly STDERR_MAX_LENGTH;
|
|
401
401
|
constructor({ command, args, cwd, env, spawn: spawnProcess, }: StdioTransportOptions);
|
|
402
402
|
getStderrOutput(): string;
|
|
403
|
+
private appendStderrOutput;
|
|
403
404
|
dispose(reason?: Error): void;
|
|
404
405
|
}
|
|
405
406
|
export declare class HttpTransport implements McpTransport {
|
|
@@ -542,6 +543,7 @@ export declare class JsonRpcMessageLayer {
|
|
|
542
543
|
onRequest(method: string, handler: JsonRpcRequestHandler): void;
|
|
543
544
|
onNotification(method: string, handler: JsonRpcNotificationHandler): void;
|
|
544
545
|
sendRequest(method: string, params?: unknown, options?: JsonRpcRequestOptions): Promise<unknown>;
|
|
546
|
+
cancelRequest(requestId: RequestId, reason: unknown): boolean;
|
|
545
547
|
dispose(reason?: Error): void;
|
|
546
548
|
private consumeInput;
|
|
547
549
|
private resolveInputStreamClosedReason;
|
|
@@ -285,6 +285,9 @@ export class McpClient {
|
|
|
285
285
|
const abortPromise = new Promise((_, reject) => {
|
|
286
286
|
const rejectWithAbortReason = () => {
|
|
287
287
|
sendCancellationNotification();
|
|
288
|
+
if (requestId !== undefined) {
|
|
289
|
+
messageLayer.cancelRequest(requestId, signal.reason);
|
|
290
|
+
}
|
|
288
291
|
reject(signal.reason);
|
|
289
292
|
};
|
|
290
293
|
abortListener = rejectWithAbortReason;
|
|
@@ -1511,11 +1514,15 @@ export class StdioTransport {
|
|
|
1511
1514
|
const child = this.child;
|
|
1512
1515
|
this.readable = child.stdout;
|
|
1513
1516
|
this.writable = child.stdin;
|
|
1517
|
+
const stderrDecoder = new TextDecoder();
|
|
1514
1518
|
child.stderr.on("data", (chunk) => {
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
+
const decoded = chunk instanceof Uint8Array
|
|
1520
|
+
? stderrDecoder.decode(chunk, { stream: true })
|
|
1521
|
+
: `${stderrDecoder.decode()}${String(chunk)}`;
|
|
1522
|
+
this.appendStderrOutput(decoded);
|
|
1523
|
+
});
|
|
1524
|
+
child.stderr.once("end", () => {
|
|
1525
|
+
this.appendStderrOutput(stderrDecoder.decode());
|
|
1519
1526
|
});
|
|
1520
1527
|
this.closed = new Promise((resolve) => {
|
|
1521
1528
|
let settled = false;
|
|
@@ -1555,6 +1562,15 @@ export class StdioTransport {
|
|
|
1555
1562
|
getStderrOutput() {
|
|
1556
1563
|
return this.stderrOutput;
|
|
1557
1564
|
}
|
|
1565
|
+
appendStderrOutput(chunk) {
|
|
1566
|
+
if (chunk.length === 0) {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
this.stderrOutput += chunk;
|
|
1570
|
+
if (this.stderrOutput.length > StdioTransport.STDERR_MAX_LENGTH) {
|
|
1571
|
+
this.stderrOutput = this.stderrOutput.slice(-StdioTransport.STDERR_MAX_LENGTH);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1558
1574
|
dispose(reason = new Error("Stdio transport disposed")) {
|
|
1559
1575
|
void reason;
|
|
1560
1576
|
if (this.disposed) {
|
|
@@ -1966,15 +1982,6 @@ export class McpError extends Error {
|
|
|
1966
1982
|
export function serializeJsonRpcMessage(message) {
|
|
1967
1983
|
return `${JSON.stringify(message)}\n`;
|
|
1968
1984
|
}
|
|
1969
|
-
function chunkToString(chunk) {
|
|
1970
|
-
if (typeof chunk === "string") {
|
|
1971
|
-
return chunk;
|
|
1972
|
-
}
|
|
1973
|
-
if (chunk instanceof Uint8Array) {
|
|
1974
|
-
return Buffer.from(chunk).toString("utf8");
|
|
1975
|
-
}
|
|
1976
|
-
return String(chunk);
|
|
1977
|
-
}
|
|
1978
1985
|
function normalizeLine(line) {
|
|
1979
1986
|
return line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
1980
1987
|
}
|
|
@@ -2168,6 +2175,16 @@ export class JsonRpcMessageLayer {
|
|
|
2168
2175
|
}
|
|
2169
2176
|
});
|
|
2170
2177
|
}
|
|
2178
|
+
cancelRequest(requestId, reason) {
|
|
2179
|
+
const pending = this.pendingRequests.get(requestId);
|
|
2180
|
+
if (pending === undefined) {
|
|
2181
|
+
return false;
|
|
2182
|
+
}
|
|
2183
|
+
this.pendingRequests.delete(requestId);
|
|
2184
|
+
clearTimeout(pending.timeout);
|
|
2185
|
+
pending.reject(reason);
|
|
2186
|
+
return true;
|
|
2187
|
+
}
|
|
2171
2188
|
dispose(reason = new Error("JSON-RPC message layer disposed")) {
|
|
2172
2189
|
if (this.disposedError !== undefined) {
|
|
2173
2190
|
return;
|