@mafdet/cli 0.3.0 → 0.4.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/CHANGELOG.md +43 -0
- package/README.md +22 -3
- package/README.zh-CN.md +17 -1
- package/dist/agent/loop.d.ts +17 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +17 -7
- package/dist/agent/session.d.ts +8 -1
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +1 -1
- package/dist/commands/agent.d.ts +9 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -6
- package/dist/tui/adapters/auth.d.ts +26 -0
- package/dist/tui/adapters/auth.d.ts.map +1 -0
- package/dist/tui/adapters/auth.js +76 -0
- package/dist/tui/adapters/models.d.ts +47 -0
- package/dist/tui/adapters/models.d.ts.map +1 -0
- package/dist/tui/adapters/models.js +146 -0
- package/dist/tui/agent-surface.d.ts +25 -0
- package/dist/tui/agent-surface.d.ts.map +1 -0
- package/dist/tui/agent-surface.js +95 -0
- package/dist/tui/app.d.ts +25 -0
- package/dist/tui/app.d.ts.map +1 -0
- package/dist/tui/app.js +159 -0
- package/dist/tui/approval.d.ts +27 -0
- package/dist/tui/approval.d.ts.map +1 -0
- package/dist/tui/approval.js +79 -0
- package/dist/tui/banner.d.ts +13 -0
- package/dist/tui/banner.d.ts.map +1 -0
- package/dist/tui/banner.js +97 -0
- package/dist/tui/input.d.ts +17 -0
- package/dist/tui/input.d.ts.map +1 -0
- package/dist/tui/input.js +57 -0
- package/dist/tui/interrupt.d.ts +32 -0
- package/dist/tui/interrupt.d.ts.map +1 -0
- package/dist/tui/interrupt.js +47 -0
- package/dist/tui/keys.d.ts +30 -0
- package/dist/tui/keys.d.ts.map +1 -0
- package/dist/tui/keys.js +86 -0
- package/dist/tui/onboarding.d.ts +28 -0
- package/dist/tui/onboarding.d.ts.map +1 -0
- package/dist/tui/onboarding.js +117 -0
- package/dist/tui/picker.d.ts +24 -0
- package/dist/tui/picker.d.ts.map +1 -0
- package/dist/tui/picker.js +146 -0
- package/dist/tui/resize.d.ts +22 -0
- package/dist/tui/resize.d.ts.map +1 -0
- package/dist/tui/resize.js +37 -0
- package/dist/tui/route.d.ts +26 -0
- package/dist/tui/route.d.ts.map +1 -0
- package/dist/tui/route.js +68 -0
- package/dist/tui/start-agent.d.ts +31 -0
- package/dist/tui/start-agent.d.ts.map +1 -0
- package/dist/tui/start-agent.js +82 -0
- package/dist/tui/terminal.d.ts +41 -0
- package/dist/tui/terminal.d.ts.map +1 -0
- package/dist/tui/terminal.js +78 -0
- package/dist/tui/theme.d.ts +22 -0
- package/dist/tui/theme.d.ts.map +1 -0
- package/dist/tui/theme.js +59 -0
- package/dist/tui/tool-card.d.ts +41 -0
- package/dist/tui/tool-card.d.ts.map +1 -0
- package/dist/tui/tool-card.js +132 -0
- package/dist/tui/widgets.d.ts +44 -0
- package/dist/tui/widgets.d.ts.map +1 -0
- package/dist/tui/widgets.js +137 -0
- package/dist/tui/workspace-summary.d.ts +10 -0
- package/dist/tui/workspace-summary.d.ts.map +1 -0
- package/dist/tui/workspace-summary.js +49 -0
- package/package.json +1 -1
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// CLI-020 · Choosing a model.
|
|
2
|
+
//
|
|
3
|
+
// A list that narrows as you type. It shows only what this key can call, and
|
|
4
|
+
// it marks - rather than hides - the models the agent may not drive, so the
|
|
5
|
+
// difference between "you cannot use this" and "the agent cannot drive this"
|
|
6
|
+
// stays visible instead of looking like a missing model.
|
|
7
|
+
import { describeModel, filterModels } from "./adapters/models.js";
|
|
8
|
+
import { palette } from "./theme.js";
|
|
9
|
+
const ESC = String.fromCharCode(27);
|
|
10
|
+
const CSI = `${ESC}[`;
|
|
11
|
+
const CLEAR_LINE = `${CSI}2K`;
|
|
12
|
+
const TO_COL_0 = `${CSI}G`;
|
|
13
|
+
const up = (n) => (n > 0 ? `${CSI}${n}A` : "");
|
|
14
|
+
/** How many models to show at once, before the list starts scrolling. */
|
|
15
|
+
const WINDOW = 8;
|
|
16
|
+
export async function pickModel(io, options) {
|
|
17
|
+
const { bold, dim, accent } = palette(io.capabilities);
|
|
18
|
+
const pool = options.agentOnly
|
|
19
|
+
? options.models.filter((m) => m.agentEligible)
|
|
20
|
+
: options.models;
|
|
21
|
+
let query = "";
|
|
22
|
+
let index = startIndex(pool, options.current);
|
|
23
|
+
let painted = 0;
|
|
24
|
+
const paint = (lines) => {
|
|
25
|
+
io.write(up(painted) +
|
|
26
|
+
TO_COL_0 +
|
|
27
|
+
lines.map((line) => CLEAR_LINE + line).join("\n") +
|
|
28
|
+
"\n");
|
|
29
|
+
painted = lines.length;
|
|
30
|
+
};
|
|
31
|
+
const draw = (matches) => {
|
|
32
|
+
const lines = [
|
|
33
|
+
bold(options.agentOnly ? "Select a model for the agent" : "Select a model"),
|
|
34
|
+
dim(` filter: ${query.length > 0 ? query : "(type to narrow)"}`),
|
|
35
|
+
"",
|
|
36
|
+
];
|
|
37
|
+
if (matches.length === 0) {
|
|
38
|
+
lines.push(dim(" no model matches that filter"));
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
for (const { model, i } of windowOf(matches, index)) {
|
|
42
|
+
const selected = i === index;
|
|
43
|
+
const name = model.displayName ?? model.modelId;
|
|
44
|
+
const head = `${model.modelId}${name !== model.modelId ? ` (${name})` : ""}`;
|
|
45
|
+
lines.push(selected ? accent(` > ${head}`) : ` ${head}`);
|
|
46
|
+
lines.push(dim(` ${describeModel(model)}`));
|
|
47
|
+
}
|
|
48
|
+
if (matches.length > WINDOW) {
|
|
49
|
+
lines.push(dim(` ${index + 1} of ${matches.length}`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
lines.push("");
|
|
53
|
+
// The catalogue does not publish a per-route RPM. Saying so once is
|
|
54
|
+
// honest; repeating "unknown" on every row would be noise, and inventing
|
|
55
|
+
// a number from the model id is what plan 19.5 forbids outright.
|
|
56
|
+
lines.push(dim(" route RPM is not published by the catalogue"));
|
|
57
|
+
lines.push(dim(" type to filter, up/down to move, Enter to select, Ctrl+C to cancel"));
|
|
58
|
+
paint(lines);
|
|
59
|
+
};
|
|
60
|
+
draw(filterModels(pool, query));
|
|
61
|
+
for (;;) {
|
|
62
|
+
const keys = await io.nextKeys();
|
|
63
|
+
if (keys === undefined)
|
|
64
|
+
return settle(cancelled());
|
|
65
|
+
for (const key of keys) {
|
|
66
|
+
const matches = filterModels(pool, query);
|
|
67
|
+
if (key.kind === "up") {
|
|
68
|
+
index = index === 0 ? Math.max(matches.length - 1, 0) : index - 1;
|
|
69
|
+
}
|
|
70
|
+
else if (key.kind === "down") {
|
|
71
|
+
index = index >= matches.length - 1 ? 0 : index + 1;
|
|
72
|
+
}
|
|
73
|
+
else if (key.kind === "backspace") {
|
|
74
|
+
query = query.slice(0, -1);
|
|
75
|
+
index = 0;
|
|
76
|
+
}
|
|
77
|
+
else if (key.kind === "char") {
|
|
78
|
+
// Every printable character types into the filter. There is no
|
|
79
|
+
// j/k navigation here: it would make "kimi" unsearchable.
|
|
80
|
+
query += key.value;
|
|
81
|
+
index = 0;
|
|
82
|
+
}
|
|
83
|
+
else if (key.kind === "enter") {
|
|
84
|
+
const chosen = filterModels(pool, query)[index];
|
|
85
|
+
if (chosen === undefined)
|
|
86
|
+
continue;
|
|
87
|
+
return settle({ kind: "picked", model: chosen });
|
|
88
|
+
}
|
|
89
|
+
else if (key.kind === "interrupt" ||
|
|
90
|
+
key.kind === "escape" ||
|
|
91
|
+
key.kind === "eof") {
|
|
92
|
+
return settle(cancelled());
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
draw(filterModels(pool, query));
|
|
96
|
+
}
|
|
97
|
+
function cancelled() {
|
|
98
|
+
return { kind: "cancelled" };
|
|
99
|
+
}
|
|
100
|
+
function settle(result) {
|
|
101
|
+
const line = result.kind === "picked" && result.model !== undefined
|
|
102
|
+
? `${dim("Model")} ${bold(result.model.modelId)}`
|
|
103
|
+
: dim("Model selection cancelled");
|
|
104
|
+
io.write(up(painted) + TO_COL_0);
|
|
105
|
+
if (painted > 0) {
|
|
106
|
+
io.write(Array.from({ length: painted }, () => CLEAR_LINE).join("\n") +
|
|
107
|
+
up(painted - 1) +
|
|
108
|
+
TO_COL_0);
|
|
109
|
+
}
|
|
110
|
+
io.write(CLEAR_LINE + line + "\n");
|
|
111
|
+
painted = 0;
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function startIndex(models, current) {
|
|
116
|
+
if (current === undefined)
|
|
117
|
+
return 0;
|
|
118
|
+
const found = models.findIndex((model) => model.modelId === current);
|
|
119
|
+
return found >= 0 ? found : 0;
|
|
120
|
+
}
|
|
121
|
+
/** A scrolling window that keeps the selection visible. */
|
|
122
|
+
function windowOf(models, index) {
|
|
123
|
+
if (models.length <= WINDOW) {
|
|
124
|
+
return models.map((model, i) => ({ model, i }));
|
|
125
|
+
}
|
|
126
|
+
const half = Math.floor(WINDOW / 2);
|
|
127
|
+
const start = Math.min(Math.max(index - half, 0), Math.max(models.length - WINDOW, 0));
|
|
128
|
+
return models
|
|
129
|
+
.slice(start, start + WINDOW)
|
|
130
|
+
.map((model, offset) => ({ model, i: start + offset }));
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* What to say when this key can call chat models but the agent may drive none
|
|
134
|
+
* of them. Not an error: chat still works, and the reason is checkable.
|
|
135
|
+
*/
|
|
136
|
+
export function noAgentModelsNotice(chatCount, capabilities) {
|
|
137
|
+
const { dim, bold } = palette(capabilities);
|
|
138
|
+
return ([
|
|
139
|
+
"",
|
|
140
|
+
bold(" No model on this key has verified tool calling."),
|
|
141
|
+
dim(` ${chatCount} chat model(s) are available, so chat still works.`),
|
|
142
|
+
dim(" Run 'mafdet models list --agent' to see which models qualify,"),
|
|
143
|
+
dim(" or check the key's model permissions in the console."),
|
|
144
|
+
"",
|
|
145
|
+
].join("\n") + "\n");
|
|
146
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TerminalCapabilities } from "./theme.js";
|
|
2
|
+
export interface ResizeSource {
|
|
3
|
+
on(event: "resize", listener: () => void): unknown;
|
|
4
|
+
off?(event: "resize", listener: () => void): unknown;
|
|
5
|
+
readonly columns?: number | undefined;
|
|
6
|
+
}
|
|
7
|
+
export interface LiveCapabilities {
|
|
8
|
+
/** The same object every reader holds; its `columns` is kept up to date. */
|
|
9
|
+
readonly capabilities: TerminalCapabilities;
|
|
10
|
+
/** Re-read the width now. Returns true if it changed. */
|
|
11
|
+
refresh(): boolean;
|
|
12
|
+
stop(): void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Keep `capabilities.columns` following the terminal.
|
|
16
|
+
*
|
|
17
|
+
* The object is mutated rather than replaced because it is already shared by
|
|
18
|
+
* every component; handing out a new object would leave the old one in place
|
|
19
|
+
* wherever it had been captured.
|
|
20
|
+
*/
|
|
21
|
+
export declare function followResize(base: TerminalCapabilities, source: ResizeSource): LiveCapabilities;
|
|
22
|
+
//# sourceMappingURL=resize.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resize.d.ts","sourceRoot":"","sources":["../../src/tui/resize.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,YAAY;IAC3B,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;IACnD,GAAG,CAAC,CAAC,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;IACrD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,gBAAgB;IAC/B,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,yDAAyD;IACzD,OAAO,IAAI,OAAO,CAAC;IACnB,IAAI,IAAI,IAAI,CAAC;CACd;AAKD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,IAAI,EAAE,oBAAoB,EAC1B,MAAM,EAAE,YAAY,GACnB,gBAAgB,CAuBlB"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// CLI-022 · Following the terminal's width.
|
|
2
|
+
//
|
|
3
|
+
// Capabilities were read once at startup, so a window resized mid-session kept
|
|
4
|
+
// laying out for the old width - lines wrapping or being clipped to a size the
|
|
5
|
+
// terminal no longer is. Everything already reads `capabilities.columns` at
|
|
6
|
+
// draw time, so the fix is for that number to stay current rather than for
|
|
7
|
+
// every component to learn about resizing.
|
|
8
|
+
/** Used when the terminal stops reporting a width mid-session. */
|
|
9
|
+
const FALLBACK_COLUMNS = 80;
|
|
10
|
+
/**
|
|
11
|
+
* Keep `capabilities.columns` following the terminal.
|
|
12
|
+
*
|
|
13
|
+
* The object is mutated rather than replaced because it is already shared by
|
|
14
|
+
* every component; handing out a new object would leave the old one in place
|
|
15
|
+
* wherever it had been captured.
|
|
16
|
+
*/
|
|
17
|
+
export function followResize(base, source) {
|
|
18
|
+
const live = { ...base };
|
|
19
|
+
const refresh = () => {
|
|
20
|
+
const next = source.columns !== undefined && source.columns > 0
|
|
21
|
+
? source.columns
|
|
22
|
+
: FALLBACK_COLUMNS;
|
|
23
|
+
if (next === live.columns)
|
|
24
|
+
return false;
|
|
25
|
+
live.columns = next;
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
const listener = () => void refresh();
|
|
29
|
+
source.on("resize", listener);
|
|
30
|
+
return {
|
|
31
|
+
capabilities: live,
|
|
32
|
+
refresh,
|
|
33
|
+
stop() {
|
|
34
|
+
source.off?.("resize", listener);
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Why a bare `mafdet` did not open the TUI. Shown when the user asks. */
|
|
2
|
+
export type TuiRefusal = "machine-output-flag" | "no-tui-flag" | "stdin-not-tty" | "stdout-not-tty" | "term-dumb" | "ci";
|
|
3
|
+
export type EntryDecision =
|
|
4
|
+
/** Bare `mafdet` on an interactive terminal. */
|
|
5
|
+
{
|
|
6
|
+
readonly kind: "tui";
|
|
7
|
+
}
|
|
8
|
+
/** `mafdet <command> ...` — the 0.3 command surface, contract unchanged. */
|
|
9
|
+
| {
|
|
10
|
+
readonly kind: "command";
|
|
11
|
+
}
|
|
12
|
+
/** Bare `mafdet` where a TUI would be unsafe: help on stderr, exit 2. */
|
|
13
|
+
| {
|
|
14
|
+
readonly kind: "usage";
|
|
15
|
+
readonly refusedBecause: TuiRefusal;
|
|
16
|
+
};
|
|
17
|
+
export interface EntryEnvironment {
|
|
18
|
+
readonly argv: readonly string[];
|
|
19
|
+
readonly stdinIsTTY: boolean;
|
|
20
|
+
readonly stdoutIsTTY: boolean;
|
|
21
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
22
|
+
}
|
|
23
|
+
export declare function decideEntry(input: EntryEnvironment): EntryDecision;
|
|
24
|
+
/** A sentence for the user, not a code. Printed under the usage text. */
|
|
25
|
+
export declare function explainRefusal(reason: TuiRefusal): string;
|
|
26
|
+
//# sourceMappingURL=route.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/tui/route.ts"],"names":[],"mappings":"AAOA,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAClB,qBAAqB,GACrB,aAAa,GACb,eAAe,GACf,gBAAgB,GAChB,WAAW,GACX,IAAI,CAAC;AAET,MAAM,MAAM,aAAa;AACvB,gDAAgD;AAC9C;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE;AAC1B,4EAA4E;GAC1E;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAA;CAAE;AAC9B,yEAAyE;GACvE;IAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,cAAc,EAAE,UAAU,CAAA;CAAE,CAAC;AAEpE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;CAC5D;AAyBD,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,aAAa,CAoBlE;AAED,yEAAyE;AACzE,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAezD"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// CLI-018 · Where a bare `mafdet` goes.
|
|
2
|
+
//
|
|
3
|
+
// This is a pure function on purpose. Whether the TUI starts decides whether
|
|
4
|
+
// we put the terminal into raw mode and start drawing over it, and getting
|
|
5
|
+
// that wrong in a pipeline or in CI corrupts someone's output. A pure decision
|
|
6
|
+
// can be tested across every combination without a terminal.
|
|
7
|
+
/**
|
|
8
|
+
* Flags that mean "a machine is reading this". They are checked even on a bare
|
|
9
|
+
* `mafdet` so that `mafdet --json` can never be answered with a drawing.
|
|
10
|
+
*/
|
|
11
|
+
const MACHINE_FLAGS = new Set(["--json", "--quiet", "-q"]);
|
|
12
|
+
/** Treated as a command rather than a bare invocation. */
|
|
13
|
+
function hasCommand(argv) {
|
|
14
|
+
return argv.some((arg) => !arg.startsWith("-"));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* CI systems attach a pipe often enough that the TTY checks usually catch them,
|
|
18
|
+
* but not always — some allocate a pty. An explicit check keeps the promise
|
|
19
|
+
* that CI behaviour does not depend on that detail.
|
|
20
|
+
*/
|
|
21
|
+
function isCi(env) {
|
|
22
|
+
const value = env.CI;
|
|
23
|
+
if (value === undefined)
|
|
24
|
+
return false;
|
|
25
|
+
// "false" and "0" are how several providers spell "not CI".
|
|
26
|
+
return value !== "" && value !== "0" && value.toLowerCase() !== "false";
|
|
27
|
+
}
|
|
28
|
+
export function decideEntry(input) {
|
|
29
|
+
if (hasCommand(input.argv))
|
|
30
|
+
return { kind: "command" };
|
|
31
|
+
// Order matters only for which reason the user is told; every branch below
|
|
32
|
+
// refuses. The most specific and most actionable reasons come first.
|
|
33
|
+
const refuse = (refusedBecause) => ({
|
|
34
|
+
kind: "usage",
|
|
35
|
+
refusedBecause,
|
|
36
|
+
});
|
|
37
|
+
if (input.argv.includes("--no-tui"))
|
|
38
|
+
return refuse("no-tui-flag");
|
|
39
|
+
if (input.argv.some((arg) => MACHINE_FLAGS.has(arg))) {
|
|
40
|
+
return refuse("machine-output-flag");
|
|
41
|
+
}
|
|
42
|
+
if (!input.stdoutIsTTY)
|
|
43
|
+
return refuse("stdout-not-tty");
|
|
44
|
+
if (!input.stdinIsTTY)
|
|
45
|
+
return refuse("stdin-not-tty");
|
|
46
|
+
if (input.env.TERM === "dumb")
|
|
47
|
+
return refuse("term-dumb");
|
|
48
|
+
if (isCi(input.env))
|
|
49
|
+
return refuse("ci");
|
|
50
|
+
return { kind: "tui" };
|
|
51
|
+
}
|
|
52
|
+
/** A sentence for the user, not a code. Printed under the usage text. */
|
|
53
|
+
export function explainRefusal(reason) {
|
|
54
|
+
switch (reason) {
|
|
55
|
+
case "machine-output-flag":
|
|
56
|
+
return "not starting the interactive UI because output was requested in a machine-readable form";
|
|
57
|
+
case "no-tui-flag":
|
|
58
|
+
return "not starting the interactive UI because --no-tui was given";
|
|
59
|
+
case "stdin-not-tty":
|
|
60
|
+
return "not starting the interactive UI because input is not a terminal";
|
|
61
|
+
case "stdout-not-tty":
|
|
62
|
+
return "not starting the interactive UI because output is not a terminal";
|
|
63
|
+
case "term-dumb":
|
|
64
|
+
return "not starting the interactive UI because TERM=dumb";
|
|
65
|
+
case "ci":
|
|
66
|
+
return "not starting the interactive UI because CI is set";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ResolvedModel } from "../api/models.js";
|
|
2
|
+
import { type CommandIo } from "../commands/runtime.js";
|
|
3
|
+
import { ExitCode } from "../output/exit-codes.js";
|
|
4
|
+
import { type TerminalCapabilities } from "./theme.js";
|
|
5
|
+
import type { WidgetIo } from "./widgets.js";
|
|
6
|
+
export interface AgentPermissions {
|
|
7
|
+
readonly allowWrite: boolean;
|
|
8
|
+
readonly allowedCommands: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Read the permission flags off a bare `mafdet` invocation.
|
|
12
|
+
*
|
|
13
|
+
* The same spellings the agent command uses, so there is one grammar for
|
|
14
|
+
* granting these and no second one to keep in step.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parsePermissions(argv: readonly string[]): AgentPermissions;
|
|
17
|
+
export interface StartAgentOptions {
|
|
18
|
+
readonly io: WidgetIo;
|
|
19
|
+
readonly capabilities: TerminalCapabilities;
|
|
20
|
+
readonly stdout: {
|
|
21
|
+
write(text: string): unknown;
|
|
22
|
+
};
|
|
23
|
+
readonly commandIo: CommandIo;
|
|
24
|
+
readonly apiKey: string;
|
|
25
|
+
readonly model: ResolvedModel;
|
|
26
|
+
readonly workspace: string;
|
|
27
|
+
readonly permissions: AgentPermissions;
|
|
28
|
+
readonly signal?: AbortSignal;
|
|
29
|
+
}
|
|
30
|
+
export declare function startAgent(options: StartAgentOptions): Promise<ExitCode>;
|
|
31
|
+
//# sourceMappingURL=start-agent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"start-agent.d.ts","sourceRoot":"","sources":["../../src/tui/start-agent.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAe1E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,QAAQ,CAAC,CAwDnB"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// CLI-021 · Assembling the agent session for the TUI.
|
|
2
|
+
//
|
|
3
|
+
// Every permission decision reuses the command surface's own pieces:
|
|
4
|
+
// `buildToolset`, the same prompts, and the same flags. The TUI adds no way to
|
|
5
|
+
// grant anything - `mafdet --allow-write` is the same grant `mafdet agent
|
|
6
|
+
// --allow-write` is, and a run given neither flag has no write or command tool
|
|
7
|
+
// loaded at all, which is what makes "read-only" mean something.
|
|
8
|
+
import { buildToolset, READ_ONLY_PROMPT, WRITE_PROMPT, } from "../commands/agent.js";
|
|
9
|
+
import { buildRuntime } from "../commands/runtime.js";
|
|
10
|
+
import { Workspace } from "../filesystem/workspace.js";
|
|
11
|
+
import { ExitCode } from "../output/exit-codes.js";
|
|
12
|
+
import { runAgentSurface } from "./agent-surface.js";
|
|
13
|
+
import { palette } from "./theme.js";
|
|
14
|
+
/**
|
|
15
|
+
* Read the permission flags off a bare `mafdet` invocation.
|
|
16
|
+
*
|
|
17
|
+
* The same spellings the agent command uses, so there is one grammar for
|
|
18
|
+
* granting these and no second one to keep in step.
|
|
19
|
+
*/
|
|
20
|
+
export function parsePermissions(argv) {
|
|
21
|
+
let allowWrite = false;
|
|
22
|
+
const allowedCommands = [];
|
|
23
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
24
|
+
const arg = argv[i];
|
|
25
|
+
if (arg === "--allow-write")
|
|
26
|
+
allowWrite = true;
|
|
27
|
+
else if (arg === "--allow-command") {
|
|
28
|
+
const value = argv[i + 1];
|
|
29
|
+
if (value !== undefined && !value.startsWith("-")) {
|
|
30
|
+
allowedCommands.push(value);
|
|
31
|
+
i += 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { allowWrite, allowedCommands };
|
|
36
|
+
}
|
|
37
|
+
export async function startAgent(options) {
|
|
38
|
+
const { dim, bold } = palette(options.capabilities);
|
|
39
|
+
let workspace;
|
|
40
|
+
try {
|
|
41
|
+
workspace = Workspace.open(options.workspace);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
45
|
+
options.stdout.write(`\n ${bold("cannot use this directory")}: ${message}\n`);
|
|
46
|
+
return ExitCode.General;
|
|
47
|
+
}
|
|
48
|
+
// The TUI is interactive by construction, so `--allow-write` can be honoured
|
|
49
|
+
// here without the non-interactive downgrade the command surface applies.
|
|
50
|
+
const mode = options.permissions.allowWrite ? "suggest" : "read-only";
|
|
51
|
+
const hasCommands = options.permissions.allowedCommands.length > 0;
|
|
52
|
+
const runtime = buildRuntime({
|
|
53
|
+
...options.commandIo,
|
|
54
|
+
env: { ...options.commandIo.env, MAFDET_API_KEY: options.apiKey },
|
|
55
|
+
});
|
|
56
|
+
options.stdout.write(dim(`\n ${options.model.modelId} - ${mode}` +
|
|
57
|
+
(hasCommands
|
|
58
|
+
? ` - commands: ${options.permissions.allowedCommands.join(", ")}`
|
|
59
|
+
: "") +
|
|
60
|
+
"\n type a goal, or /help for the session commands\n\n"));
|
|
61
|
+
await runAgentSurface({
|
|
62
|
+
io: options.io,
|
|
63
|
+
capabilities: options.capabilities,
|
|
64
|
+
stdout: options.stdout,
|
|
65
|
+
session: {
|
|
66
|
+
client: runtime.dataPlane,
|
|
67
|
+
model: options.model.modelId,
|
|
68
|
+
pricing: options.model,
|
|
69
|
+
catalog: [options.model],
|
|
70
|
+
systemPrompt: mode === "read-only" ? READ_ONLY_PROMPT : WRITE_PROMPT,
|
|
71
|
+
tools: buildToolset(mode, hasCommands),
|
|
72
|
+
toolContext: {
|
|
73
|
+
workspace,
|
|
74
|
+
allowedCommands: options.permissions.allowedCommands,
|
|
75
|
+
},
|
|
76
|
+
mode,
|
|
77
|
+
allowedCommands: options.permissions.allowedCommands,
|
|
78
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
return ExitCode.Ok;
|
|
82
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** The bits of a stream the guard touches, so tests can pass a fake. */
|
|
2
|
+
export interface GuardedStdout {
|
|
3
|
+
write(text: string): unknown;
|
|
4
|
+
}
|
|
5
|
+
export interface GuardedStdin {
|
|
6
|
+
readonly isTTY?: boolean;
|
|
7
|
+
readonly isRaw?: boolean;
|
|
8
|
+
setRawMode?(mode: boolean): unknown;
|
|
9
|
+
}
|
|
10
|
+
export interface GuardHost {
|
|
11
|
+
on(event: string, listener: (...args: unknown[]) => void): unknown;
|
|
12
|
+
removeListener(event: string, listener: (...args: unknown[]) => void): unknown;
|
|
13
|
+
removeAllListeners(event: string): unknown;
|
|
14
|
+
kill(pid: number, signal: string): unknown;
|
|
15
|
+
exit(code: number): unknown;
|
|
16
|
+
readonly pid: number;
|
|
17
|
+
}
|
|
18
|
+
export interface TerminalGuard {
|
|
19
|
+
/** Undo everything, at most once. Safe to call from anywhere. */
|
|
20
|
+
restore(): void;
|
|
21
|
+
/** Stop listening. For a TUI that ended normally and wants its signals back. */
|
|
22
|
+
release(): void;
|
|
23
|
+
hideCursor(): void;
|
|
24
|
+
}
|
|
25
|
+
export interface GuardOptions {
|
|
26
|
+
readonly stdout: GuardedStdout;
|
|
27
|
+
readonly stdin: GuardedStdin;
|
|
28
|
+
readonly host: GuardHost;
|
|
29
|
+
/** Called before the process dies, for a last summary. Must not throw. */
|
|
30
|
+
readonly onFatal?: (error: unknown) => void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Installs handlers that restore the terminal on every exit path: a normal
|
|
34
|
+
* return, SIGINT, SIGTERM, and an uncaught exception or rejection.
|
|
35
|
+
*
|
|
36
|
+
* Signals are re-raised with the default disposition rather than turned into
|
|
37
|
+
* `exit(0)`, so a process killed by SIGINT still reports 130. Claiming success
|
|
38
|
+
* after being interrupted would be a lie a script could act on.
|
|
39
|
+
*/
|
|
40
|
+
export declare function installTerminalGuard(options: GuardOptions): TerminalGuard;
|
|
41
|
+
//# sourceMappingURL=terminal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"terminal.d.ts","sourceRoot":"","sources":["../../src/tui/terminal.ts"],"names":[],"mappings":"AAYA,wEAAwE;AACxE,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG,OAAO,CAAC;IACnE,cAAc,CACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,GACrC,OAAO,CAAC;IACX,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3C,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;IAC3C,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,iEAAiE;IACjE,OAAO,IAAI,IAAI,CAAC;IAChB,gFAAgF;IAChF,OAAO,IAAI,IAAI,CAAC;IAChB,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC7C;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa,CA2DzE"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// CLI-018 · Putting the terminal back.
|
|
2
|
+
//
|
|
3
|
+
// The spike (cli-018-tui-renderer-adr) found that Ink hooks only
|
|
4
|
+
// `process.once('beforeExit')`, which does not fire on SIGINT, on
|
|
5
|
+
// process.exit(), or on an uncaught throw — so a component library would not
|
|
6
|
+
// have given us this for free either. It is written here, once, and covers
|
|
7
|
+
// every exit path the plan names.
|
|
8
|
+
const ESC = String.fromCharCode(27);
|
|
9
|
+
const SHOW_CURSOR = `${ESC}[?25h`;
|
|
10
|
+
const HIDE_CURSOR = `${ESC}[?25l`;
|
|
11
|
+
/**
|
|
12
|
+
* Installs handlers that restore the terminal on every exit path: a normal
|
|
13
|
+
* return, SIGINT, SIGTERM, and an uncaught exception or rejection.
|
|
14
|
+
*
|
|
15
|
+
* Signals are re-raised with the default disposition rather than turned into
|
|
16
|
+
* `exit(0)`, so a process killed by SIGINT still reports 130. Claiming success
|
|
17
|
+
* after being interrupted would be a lie a script could act on.
|
|
18
|
+
*/
|
|
19
|
+
export function installTerminalGuard(options) {
|
|
20
|
+
const { stdout, stdin, host } = options;
|
|
21
|
+
let restored = false;
|
|
22
|
+
let cursorHidden = false;
|
|
23
|
+
const restore = () => {
|
|
24
|
+
if (restored)
|
|
25
|
+
return;
|
|
26
|
+
restored = true;
|
|
27
|
+
try {
|
|
28
|
+
if (stdin.isTTY === true && stdin.isRaw === true)
|
|
29
|
+
stdin.setRawMode?.(false);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// A closed or detached stdin is not a reason to skip the rest.
|
|
33
|
+
}
|
|
34
|
+
if (cursorHidden) {
|
|
35
|
+
try {
|
|
36
|
+
stdout.write(SHOW_CURSOR);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Nothing more we can do; the process is going away regardless.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const onSignal = (signal) => () => {
|
|
44
|
+
restore();
|
|
45
|
+
// Remove our own handler so the re-raise is handled by the default
|
|
46
|
+
// disposition and the exit status is the real one.
|
|
47
|
+
host.removeAllListeners(signal);
|
|
48
|
+
host.kill(host.pid, signal);
|
|
49
|
+
};
|
|
50
|
+
const onFatal = (error) => {
|
|
51
|
+
restore();
|
|
52
|
+
options.onFatal?.(error);
|
|
53
|
+
host.exit(1);
|
|
54
|
+
};
|
|
55
|
+
const listeners = [
|
|
56
|
+
["SIGINT", onSignal("SIGINT")],
|
|
57
|
+
["SIGTERM", onSignal("SIGTERM")],
|
|
58
|
+
["exit", restore],
|
|
59
|
+
["uncaughtException", onFatal],
|
|
60
|
+
["unhandledRejection", onFatal],
|
|
61
|
+
];
|
|
62
|
+
for (const [event, listener] of listeners)
|
|
63
|
+
host.on(event, listener);
|
|
64
|
+
return {
|
|
65
|
+
restore,
|
|
66
|
+
release() {
|
|
67
|
+
for (const [event, listener] of listeners) {
|
|
68
|
+
host.removeListener(event, listener);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
hideCursor() {
|
|
72
|
+
if (cursorHidden)
|
|
73
|
+
return;
|
|
74
|
+
cursorHidden = true;
|
|
75
|
+
stdout.write(HIDE_CURSOR);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface TerminalCapabilities {
|
|
2
|
+
readonly colour: boolean;
|
|
3
|
+
readonly unicode: boolean;
|
|
4
|
+
/** Columns to lay out for. Narrow terminals collapse rather than wrap badly. */
|
|
5
|
+
readonly columns: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CapabilityInput {
|
|
8
|
+
readonly stdoutIsTTY: boolean;
|
|
9
|
+
readonly columns: number | undefined;
|
|
10
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
11
|
+
}
|
|
12
|
+
/** Below this, the wide banner is replaced by a single line. */
|
|
13
|
+
export declare const NARROW_COLUMNS = 60;
|
|
14
|
+
export declare function detectCapabilities(input: CapabilityInput): TerminalCapabilities;
|
|
15
|
+
/** The subset of SGR we use, resolved once so no component branches on colour. */
|
|
16
|
+
export interface Palette {
|
|
17
|
+
readonly bold: (text: string) => string;
|
|
18
|
+
readonly dim: (text: string) => string;
|
|
19
|
+
readonly accent: (text: string) => string;
|
|
20
|
+
}
|
|
21
|
+
export declare function palette(capabilities: TerminalCapabilities): Palette;
|
|
22
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/tui/theme.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;CAC5D;AAED,gEAAgE;AAChE,eAAO,MAAM,cAAc,KAAK,CAAC;AA0BjC,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,GACrB,oBAAoB,CAatB;AAED,kFAAkF;AAClF,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,QAAQ,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;CAC3C;AAUD,wBAAgB,OAAO,CAAC,YAAY,EAAE,oBAAoB,GAAG,OAAO,CASnE"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// CLI-018 · What this terminal can be trusted to render.
|
|
2
|
+
//
|
|
3
|
+
// Pure, because the answer decides which bytes we write. CLI-016 established
|
|
4
|
+
// that every user-visible byte stays inside ASCII: a Windows console on a
|
|
5
|
+
// non-UTF-8 code page (cp936 is the PowerShell 5.1 default on a Chinese
|
|
6
|
+
// install) turns anything else into mojibake, and the CLI cannot set the code
|
|
7
|
+
// page. So Unicode is opt-in on evidence, never assumed.
|
|
8
|
+
/** Below this, the wide banner is replaced by a single line. */
|
|
9
|
+
export const NARROW_COLUMNS = 60;
|
|
10
|
+
/** Used when the terminal does not report a width. */
|
|
11
|
+
const ASSUMED_COLUMNS = 80;
|
|
12
|
+
function wantsNoColour(env) {
|
|
13
|
+
// https://no-color.org: any value, including empty, means disable.
|
|
14
|
+
return env.NO_COLOR !== undefined;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Unicode is allowed only where the encoding is known to be UTF-8.
|
|
18
|
+
*
|
|
19
|
+
* On POSIX that is the locale. On Windows there is no reliable way to read the
|
|
20
|
+
* console code page from Node, so the only accepted evidence is Windows
|
|
21
|
+
* Terminal, which is UTF-8 throughout. Everything else — including
|
|
22
|
+
* conhost/PowerShell 5.1 — gets ASCII.
|
|
23
|
+
*/
|
|
24
|
+
function unicodeIsSafe(env) {
|
|
25
|
+
if (env.MAFDET_ASCII === "1")
|
|
26
|
+
return false;
|
|
27
|
+
if (env.WT_SESSION !== undefined && env.WT_SESSION !== "")
|
|
28
|
+
return true;
|
|
29
|
+
const locale = env.LC_ALL ?? env.LC_CTYPE ?? env.LANG ?? "";
|
|
30
|
+
return /utf-?8/i.test(locale);
|
|
31
|
+
}
|
|
32
|
+
export function detectCapabilities(input) {
|
|
33
|
+
const dumb = input.env.TERM === "dumb";
|
|
34
|
+
const colour = input.stdoutIsTTY && !dumb && !wantsNoColour(input.env);
|
|
35
|
+
return {
|
|
36
|
+
colour,
|
|
37
|
+
// A non-TTY still gets ASCII rather than Unicode: the bytes may end up in
|
|
38
|
+
// a log or a Windows console either way.
|
|
39
|
+
unicode: input.stdoutIsTTY && !dumb && unicodeIsSafe(input.env),
|
|
40
|
+
columns: input.columns !== undefined && input.columns > 0
|
|
41
|
+
? input.columns
|
|
42
|
+
: ASSUMED_COLUMNS,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const ESC = String.fromCharCode(27);
|
|
46
|
+
function sgr(open, close) {
|
|
47
|
+
return (text) => `${ESC}[${open}m${text}${ESC}[${close}m`;
|
|
48
|
+
}
|
|
49
|
+
const PLAIN = (text) => text;
|
|
50
|
+
export function palette(capabilities) {
|
|
51
|
+
if (!capabilities.colour) {
|
|
52
|
+
return { bold: PLAIN, dim: PLAIN, accent: PLAIN };
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
bold: sgr("1", "22"),
|
|
56
|
+
dim: sgr("2", "22"),
|
|
57
|
+
accent: sgr("36", "39"),
|
|
58
|
+
};
|
|
59
|
+
}
|