herdr-remote 0.2.18 → 0.2.20
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/bin/herdr-remote.js +3 -225
- package/dist/agent-status.js +130 -0
- package/dist/binary-plist.js +157 -0
- package/dist/cli.js +255 -0
- package/dist/config.js +270 -0
- package/dist/connector/agent-watch.js +110 -0
- package/dist/connector/font-server.js +165 -0
- package/dist/connector/host-connector.js +430 -0
- package/dist/connector/lock.js +66 -0
- package/dist/connector/main.js +18 -0
- package/dist/connector/sessions.js +366 -0
- package/dist/connector/update-report.js +67 -0
- package/dist/connector/wire.js +42 -0
- package/{src → dist}/exit-codes.js +1 -4
- package/dist/font-subset.js +110 -0
- package/dist/herdr-api.js +246 -0
- package/dist/herdr-command.js +223 -0
- package/dist/herdr-plugin.js +116 -0
- package/dist/herdr-server.js +189 -0
- package/dist/i18n/en.js +223 -0
- package/dist/i18n/index.js +48 -0
- package/dist/i18n/zh.js +222 -0
- package/dist/keepalive/detached.js +78 -0
- package/dist/keepalive/environment.js +44 -0
- package/dist/keepalive/index.js +110 -0
- package/dist/keepalive/launchd.js +117 -0
- package/dist/keepalive/systemd.js +108 -0
- package/dist/keepalive/types.js +1 -0
- package/dist/lib/process.js +12 -0
- package/dist/lifecycle.js +59 -0
- package/dist/net-interfaces.js +73 -0
- package/dist/pasted-files.js +115 -0
- package/dist/paths.js +62 -0
- package/dist/pty-session.js +96 -0
- package/dist/relay-client.js +106 -0
- package/dist/relay-urls.js +99 -0
- package/dist/runtime.js +106 -0
- package/dist/service.js +260 -0
- package/dist/settings-model.js +298 -0
- package/dist/socket-discovery.js +48 -0
- package/dist/supervisor.js +319 -0
- package/dist/terminal-font/deps.js +73 -0
- package/dist/terminal-font/files.js +354 -0
- package/dist/terminal-font/formats.js +335 -0
- package/dist/terminal-font/index.js +187 -0
- package/dist/terminal-font/readers.js +316 -0
- package/dist/terminal-font/terminals.js +128 -0
- package/dist/terminal-palette.js +339 -0
- package/dist/tui/App.js +195 -0
- package/dist/tui/api.js +79 -0
- package/dist/tui/components/TextField.js +50 -0
- package/dist/tui/components/common.js +80 -0
- package/dist/tui/index.js +36 -0
- package/dist/tui/mouse/index.js +164 -0
- package/dist/tui/mouse/protocol.js +96 -0
- package/dist/tui/mouse/source.js +74 -0
- package/dist/tui/screens/About.js +166 -0
- package/dist/tui/screens/Herdr.js +74 -0
- package/dist/tui/screens/Keepalive.js +84 -0
- package/dist/tui/screens/Overview.js +36 -0
- package/dist/tui/screens/Pair.js +74 -0
- package/dist/tui/screens/Relay.js +242 -0
- package/dist/tui/screens/Services.js +63 -0
- package/dist/tui/screens/Wizard.js +235 -0
- package/dist/tui/terminal.js +39 -0
- package/dist/tui/theme.js +33 -0
- package/dist/updater.js +382 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +8 -8
- package/dist/tui.mjs +0 -5775
- package/src/agent-status.js +0 -139
- package/src/binary-plist.js +0 -146
- package/src/config.js +0 -454
- package/src/font-subset.js +0 -110
- package/src/herdr-api.js +0 -244
- package/src/herdr-command.js +0 -231
- package/src/herdr-plugin.js +0 -130
- package/src/herdr-server.js +0 -201
- package/src/host-connector.js +0 -1069
- package/src/i18n/en.js +0 -240
- package/src/i18n/index.js +0 -56
- package/src/i18n/zh.js +0 -239
- package/src/keepalive.js +0 -460
- package/src/lifecycle.js +0 -62
- package/src/net-interfaces.js +0 -76
- package/src/pasted-files.js +0 -176
- package/src/pty-session.js +0 -92
- package/src/service.js +0 -530
- package/src/settings-model.js +0 -323
- package/src/socket-discovery.js +0 -60
- package/src/state.js +0 -47
- package/src/supervisor.js +0 -250
- package/src/terminal-font.js +0 -1204
- package/src/terminal-palette.js +0 -325
- package/src/updater.js +0 -394
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
import { theme } from '../theme.js';
|
|
5
|
+
import { FieldRow, Message, Panel, Selectable } from '../components/common.js';
|
|
6
|
+
import { TextField } from '../components/TextField.js';
|
|
7
|
+
import { ChoiceList } from './Relay.js';
|
|
8
|
+
import { OFFICIAL_RELAY_URL, configPath, keepalive, listReachableAddresses, loadConfig, saveDraft, selectedMode, setField, setRelayPassword, startAll, } from '../api.js';
|
|
9
|
+
/**
|
|
10
|
+
* The questions worth asking for a given access mode, in order.
|
|
11
|
+
*
|
|
12
|
+
* `remoteUrl` matters because the official relay is a remote relay whose
|
|
13
|
+
* address and credentials are already known, so it asks neither question. The
|
|
14
|
+
* step counter is derived from this list, so it has to agree with the route
|
|
15
|
+
* actually taken or the wizard reports "step 5 of 5" on its third screen.
|
|
16
|
+
*/
|
|
17
|
+
function stepsFor(mode, remoteUrl) {
|
|
18
|
+
const steps = ['language', 'access'];
|
|
19
|
+
if (mode === 'lan')
|
|
20
|
+
steps.push('address');
|
|
21
|
+
if (mode === 'remote' && remoteUrl !== OFFICIAL_RELAY_URL)
|
|
22
|
+
steps.push('relayUrl', 'password');
|
|
23
|
+
steps.push('finish');
|
|
24
|
+
return steps;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* First-run setup.
|
|
28
|
+
*
|
|
29
|
+
* The one decision a new install cannot make for the user is how they intend to
|
|
30
|
+
* reach the machine, because it decides whether anything listens beyond
|
|
31
|
+
* loopback. Asking once, up front, beats a default that is either useless on a
|
|
32
|
+
* phone or wider than the user expected.
|
|
33
|
+
*/
|
|
34
|
+
export function Wizard({ ctx, onDone }) {
|
|
35
|
+
const { t, draft } = ctx;
|
|
36
|
+
const [step, setStep] = useState('language');
|
|
37
|
+
const [password, setPassword] = useState(ctx.runtime.relayPassword || '');
|
|
38
|
+
const [startNow, setStartNow] = useState(true);
|
|
39
|
+
const [installKeepalive, setInstallKeepalive] = useState(true);
|
|
40
|
+
const [finishSelection, setFinishSelection] = useState('startNow');
|
|
41
|
+
const addresses = useMemo(() => listReachableAddresses({ includeLoopback: false }), []);
|
|
42
|
+
const steps = useMemo(() => stepsFor(draft.relay.mode, draft.relay.remoteUrl), [draft.relay.mode, draft.relay.remoteUrl]);
|
|
43
|
+
const stepIndex = Math.max(0, steps.indexOf(step));
|
|
44
|
+
/**
|
|
45
|
+
* Apply several fields as one edit. Each `setField` builds on the previous
|
|
46
|
+
* result: `draft` is the value captured when this render began, so applying
|
|
47
|
+
* two fields through two separate calls committed the second over the first.
|
|
48
|
+
*/
|
|
49
|
+
const applyAll = (updates) => {
|
|
50
|
+
let next = draft;
|
|
51
|
+
for (const [id, value] of updates) {
|
|
52
|
+
const result = setField(next, id, value);
|
|
53
|
+
if (result.errorKey) {
|
|
54
|
+
ctx.notify(t(result.errorKey), 'error');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
next = result.draft;
|
|
58
|
+
}
|
|
59
|
+
ctx.updateDraft(next);
|
|
60
|
+
ctx.notify('', 'info');
|
|
61
|
+
return true;
|
|
62
|
+
};
|
|
63
|
+
const apply = (id, value) => applyAll([[id, value]]);
|
|
64
|
+
/**
|
|
65
|
+
* Move to the next step.
|
|
66
|
+
*
|
|
67
|
+
* `order` is passed explicitly by the access step because the step list
|
|
68
|
+
* depends on the mode that was just chosen, and the draft holding it has not
|
|
69
|
+
* re-rendered yet — reading the memo here would use the previous mode and
|
|
70
|
+
* skip straight past the relay questions.
|
|
71
|
+
*/
|
|
72
|
+
const advance = (from, order = steps) => {
|
|
73
|
+
const index = order.indexOf(from);
|
|
74
|
+
setStep(order[Math.min(order.length - 1, index + 1)]);
|
|
75
|
+
};
|
|
76
|
+
const back = () => {
|
|
77
|
+
const index = steps.indexOf(step);
|
|
78
|
+
if (index > 0)
|
|
79
|
+
setStep(steps[index - 1]);
|
|
80
|
+
};
|
|
81
|
+
const finish = () => {
|
|
82
|
+
ctx.run(async () => {
|
|
83
|
+
if (draft.relay.mode === 'remote') {
|
|
84
|
+
// Empty is meaningful: it is how you join a public relay.
|
|
85
|
+
setRelayPassword(password);
|
|
86
|
+
}
|
|
87
|
+
saveDraft(draft);
|
|
88
|
+
ctx.reloadConfig();
|
|
89
|
+
const finalConfig = loadConfig();
|
|
90
|
+
// Keep-alive first, deliberately. Installing it starts the services
|
|
91
|
+
// through the service manager, so starting them here beforehand would
|
|
92
|
+
// leave a second, detached copy holding the relay port and a second host
|
|
93
|
+
// connector fighting the first for the same host id — which browsers see
|
|
94
|
+
// as an endless reconnect loop. `startAll` defers to the manager once it
|
|
95
|
+
// is installed, so the two options can no longer both spawn.
|
|
96
|
+
let keepaliveInstalled = false;
|
|
97
|
+
if (installKeepalive) {
|
|
98
|
+
try {
|
|
99
|
+
keepalive.install(finalConfig);
|
|
100
|
+
keepaliveInstalled = true;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
// Keep-alive is a convenience; a container without systemd should not
|
|
104
|
+
// block a working setup.
|
|
105
|
+
ctx.notify(t('keepalive.failed', { message: error.message }), 'error');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (startNow && !keepaliveInstalled)
|
|
109
|
+
startAll(finalConfig);
|
|
110
|
+
onDone();
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
const header = (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { bold: true, children: t('wizard.title') }), _jsx(Text, { color: theme.muted, children: t('wizard.step', { current: stepIndex + 1, total: steps.length }) })] }));
|
|
114
|
+
if (step === 'language') {
|
|
115
|
+
return (_jsxs(Panel, { title: t('wizard.languageTitle'), children: [header, _jsx(ChoiceList, { options: [
|
|
116
|
+
{ id: 'auto', label: t('about.languageAuto', { detected: ctx.locale }) },
|
|
117
|
+
{ id: 'zh', label: t('about.languageZh') },
|
|
118
|
+
{ id: 'en', label: t('about.languageEn') },
|
|
119
|
+
], current: draft.ui.language, onPick: (language) => {
|
|
120
|
+
if (apply('language', language))
|
|
121
|
+
advance('language');
|
|
122
|
+
}, onCancel: () => { } }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
|
|
123
|
+
}
|
|
124
|
+
if (step === 'access') {
|
|
125
|
+
return (_jsxs(Panel, { title: t('wizard.accessTitle'), children: [header, _jsx(ChoiceList, { options: [
|
|
126
|
+
...['local', 'lan'].map((mode) => ({
|
|
127
|
+
id: mode,
|
|
128
|
+
label: t(`mode.${mode}`),
|
|
129
|
+
description: t(`mode.${mode}.description`),
|
|
130
|
+
})),
|
|
131
|
+
// Offered ahead of the self-hosted option because it is the one
|
|
132
|
+
// that needs no server. It is still a separate, explicit choice:
|
|
133
|
+
// it routes the terminal through a relay this user does not own.
|
|
134
|
+
{
|
|
135
|
+
id: 'official',
|
|
136
|
+
label: t('mode.official'),
|
|
137
|
+
description: t('mode.official.description'),
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'remote',
|
|
141
|
+
label: t('mode.remote'),
|
|
142
|
+
description: t('mode.remote.description'),
|
|
143
|
+
},
|
|
144
|
+
], current: selectedMode(draft), onPick: (choice) => {
|
|
145
|
+
if (choice === 'official') {
|
|
146
|
+
// The official relay is "remote" with the address already known,
|
|
147
|
+
// so the URL question is answered and skipped. It is a public
|
|
148
|
+
// relay, so there is no join password to ask for either — and any
|
|
149
|
+
// password left over from a previous self-hosted answer would
|
|
150
|
+
// only be sent to a relay that does not want it.
|
|
151
|
+
if (!apply('mode', 'official'))
|
|
152
|
+
return;
|
|
153
|
+
setRelayPassword('');
|
|
154
|
+
advance('access', stepsFor('remote', OFFICIAL_RELAY_URL));
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (apply('mode', choice)) {
|
|
158
|
+
// Leaving the official relay clears its address, so the URL step
|
|
159
|
+
// that follows starts empty rather than pre-loaded with ours.
|
|
160
|
+
advance('access', stepsFor(choice, choice === 'remote' ? '' : draft.relay.remoteUrl));
|
|
161
|
+
}
|
|
162
|
+
}, onCancel: back }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('wizard.accessHint') }) }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
|
|
163
|
+
}
|
|
164
|
+
if (step === 'address') {
|
|
165
|
+
return (_jsxs(Panel, { title: t('wizard.addressTitle'), children: [header, addresses.length === 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: theme.warn, children: t('relay.noAddresses') }), _jsx(Box, { marginTop: 1, children: _jsx(Selectable, { selected: true, onSelect: () => advance('address'), children: t('wizard.finish') }) }), _jsx(SkipKey, { onSkip: () => advance('address'), onBack: back })] })) : (_jsx(ChoiceList, { options: addresses.map((address) => ({
|
|
166
|
+
id: address.address,
|
|
167
|
+
label: `${address.address} (${address.name}, ${address.kind})`,
|
|
168
|
+
})), current: draft.relay.lanHost || addresses[0].address, onPick: (address) => {
|
|
169
|
+
if (apply('lanHost', address))
|
|
170
|
+
advance('address');
|
|
171
|
+
}, onCancel: back })), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
|
|
172
|
+
}
|
|
173
|
+
if (step === 'relayUrl') {
|
|
174
|
+
return (_jsxs(Panel, { title: t('wizard.relayTitle'), children: [header, _jsx(FieldRow, { label: t('wizard.relayUrlLabel'), selected: true, onSelect: () => { }, children: _jsx(TextField, { value: draft.relay.remoteUrl, placeholder: "wss://relay.example.com", active: true, onSubmit: (value) => {
|
|
175
|
+
if (!value) {
|
|
176
|
+
ctx.notify(t('error.remoteUrlRequired'), 'error');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (apply('remoteUrl', value))
|
|
180
|
+
advance('relayUrl');
|
|
181
|
+
}, onCancel: back }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('wizard.relayHint') }) }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
|
|
182
|
+
}
|
|
183
|
+
if (step === 'password') {
|
|
184
|
+
return (_jsxs(Panel, { title: t('wizard.passwordTitle'), children: [header, _jsx(FieldRow, { label: t('relay.password'), selected: true, onSelect: () => { }, children: _jsx(TextField, { value: password, placeholder: t('relay.passwordEmpty'), active: true, mask: true, onSubmit: (value) => {
|
|
185
|
+
setPassword(value);
|
|
186
|
+
advance('password');
|
|
187
|
+
}, onCancel: back }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('wizard.passwordHint') }) }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
|
|
188
|
+
}
|
|
189
|
+
const finishItems = [
|
|
190
|
+
{ id: 'startNow', label: `${t('wizard.startNow')} [${startNow ? '×' : ' '}]` },
|
|
191
|
+
{
|
|
192
|
+
id: 'installKeepalive',
|
|
193
|
+
label: `${t('wizard.installKeepalive')} [${installKeepalive ? '×' : ' '}]`,
|
|
194
|
+
},
|
|
195
|
+
{ id: 'finish', label: t('wizard.finish') },
|
|
196
|
+
];
|
|
197
|
+
const activateFinish = (id) => {
|
|
198
|
+
if (id === 'startNow') {
|
|
199
|
+
setStartNow((value) => !value);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (id === 'installKeepalive') {
|
|
203
|
+
setInstallKeepalive((value) => !value);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
finish();
|
|
207
|
+
};
|
|
208
|
+
return (_jsxs(Panel, { title: t('wizard.finishTitle'), children: [header, _jsx(FinishKeys, { items: finishItems.map((item) => item.id), selected: finishSelection, onSelect: setFinishSelection, onActivate: activateFinish, onBack: back }), finishItems.map((item) => (_jsx(Selectable, { selected: finishSelection === item.id, onSelect: () => {
|
|
209
|
+
setFinishSelection(item.id);
|
|
210
|
+
activateFinish(item.id);
|
|
211
|
+
}, onHover: () => setFinishSelection(item.id), children: item.label }, item.id))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { color: theme.muted, children: t('wizard.finishHint', { path: configPath() }) }) }), _jsx(Message, { text: ctx.message?.text ?? null, level: ctx.message?.level ?? 'info' })] }));
|
|
212
|
+
}
|
|
213
|
+
function FinishKeys({ items, selected, onSelect, onActivate, onBack, }) {
|
|
214
|
+
useInput((_input, key) => {
|
|
215
|
+
const index = Math.max(0, items.indexOf(selected));
|
|
216
|
+
if (key.escape)
|
|
217
|
+
onBack();
|
|
218
|
+
else if (key.upArrow)
|
|
219
|
+
onSelect(items[(index - 1 + items.length) % items.length]);
|
|
220
|
+
else if (key.downArrow)
|
|
221
|
+
onSelect(items[(index + 1) % items.length]);
|
|
222
|
+
else if (key.return)
|
|
223
|
+
onActivate(selected);
|
|
224
|
+
});
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
function SkipKey({ onSkip, onBack }) {
|
|
228
|
+
useInput((_input, key) => {
|
|
229
|
+
if (key.return)
|
|
230
|
+
onSkip();
|
|
231
|
+
else if (key.escape)
|
|
232
|
+
onBack();
|
|
233
|
+
});
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { DISABLE_MOUSE } from './mouse/protocol.js';
|
|
2
|
+
const ESC = String.fromCharCode(27);
|
|
3
|
+
const ENTER_ALT_SCREEN = `${ESC}[?1049h${ESC}[H`;
|
|
4
|
+
const EXIT_ALT_SCREEN = `${ESC}[?1049l`;
|
|
5
|
+
const SHOW_CURSOR = `${ESC}[?25h`;
|
|
6
|
+
/**
|
|
7
|
+
* Take over the alternate screen.
|
|
8
|
+
*
|
|
9
|
+
* Two reasons. The interface is full-screen, so the shell's scrollback should
|
|
10
|
+
* come back untouched on exit. More importantly, mouse reports arrive in
|
|
11
|
+
* absolute terminal coordinates while element positions are computed relative
|
|
12
|
+
* to the Ink root — those only agree when the app starts at row 1. Rendering
|
|
13
|
+
* under a shell prompt shifted every click down by however many lines happened
|
|
14
|
+
* to be on screen.
|
|
15
|
+
*/
|
|
16
|
+
export function enterFullScreen(output = process.stdout) {
|
|
17
|
+
try {
|
|
18
|
+
output.write(ENTER_ALT_SCREEN);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Not a terminal we can drive; rendering still works, clicks may be offset.
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Undo everything we turned on.
|
|
26
|
+
*
|
|
27
|
+
* Ink and the mouse layer both clean up on an orderly unmount, but a signal or
|
|
28
|
+
* an uncaught error bypasses that, and a terminal left in mouse-reporting mode
|
|
29
|
+
* swallows text selection in every command the user runs afterwards. Writing
|
|
30
|
+
* the sequences unconditionally is harmless when they were never enabled.
|
|
31
|
+
*/
|
|
32
|
+
export function restoreTerminal(output = process.stdout) {
|
|
33
|
+
try {
|
|
34
|
+
output.write(`${DISABLE_MOUSE}${SHOW_CURSOR}${EXIT_ALT_SCREEN}`);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// The stream may already be closed during shutdown; nothing useful to do.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// A deliberately quiet palette.
|
|
2
|
+
//
|
|
3
|
+
// The previous TUI painted 24-bit terracotta on filled backgrounds, which
|
|
4
|
+
// fought with every terminal theme it ran in. These are the eight basic ANSI
|
|
5
|
+
// colours instead, so the interface inherits whatever palette the user already
|
|
6
|
+
// chose: one accent for "where you are", and colour elsewhere only where it
|
|
7
|
+
// carries meaning (a service is up, down, or in between).
|
|
8
|
+
export const theme = {
|
|
9
|
+
/** Selection and focus. */
|
|
10
|
+
accent: 'cyan',
|
|
11
|
+
/** Frame lines and separators. */
|
|
12
|
+
border: 'gray',
|
|
13
|
+
/** Secondary text: labels, hints, paths. */
|
|
14
|
+
muted: 'gray',
|
|
15
|
+
ok: 'green',
|
|
16
|
+
warn: 'yellow',
|
|
17
|
+
bad: 'red',
|
|
18
|
+
};
|
|
19
|
+
export const SELECTED_MARKER = '▸';
|
|
20
|
+
export const UNSELECTED_MARKER = ' ';
|
|
21
|
+
export const STATUS_COLOR = {
|
|
22
|
+
ok: theme.ok,
|
|
23
|
+
warn: theme.warn,
|
|
24
|
+
bad: theme.bad,
|
|
25
|
+
idle: theme.muted,
|
|
26
|
+
};
|
|
27
|
+
/** Filled for a live state, hollow for an inactive one. */
|
|
28
|
+
export const STATUS_GLYPH = {
|
|
29
|
+
ok: '●',
|
|
30
|
+
warn: '●',
|
|
31
|
+
bad: '●',
|
|
32
|
+
idle: '○',
|
|
33
|
+
};
|