spinuptui 1.2.0 → 1.3.0-alpha.2
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 +31 -1
- package/README.md +13 -1
- package/package.json +1 -1
- package/src/config.ts +45 -0
- package/src/index.tsx +37 -38
- package/src/lib/appUpdate.test.ts +26 -0
- package/src/lib/appUpdate.ts +33 -7
- package/src/lib/beacon.test.ts +140 -0
- package/src/lib/beacon.ts +349 -0
- package/src/lib/beaconSetup.test.ts +65 -0
- package/src/lib/beaconSetup.ts +143 -0
- package/src/lib/cliBeacon.ts +290 -0
- package/src/lib/cliScheduler.ts +534 -0
- package/src/lib/flock.ts +51 -0
- package/src/lib/prompt.ts +47 -0
- package/src/lib/schedule.ts +156 -0
- package/src/lib/scheduler.test.ts +353 -0
- package/src/lib/schedulerGit.ts +119 -0
- package/src/lib/schedulerInstall.ts +325 -0
- package/src/lib/schedulerJobs.ts +288 -0
- package/src/lib/schedulerRun.ts +246 -0
- package/src/lib/schedulerTick.ts +307 -0
- package/src/modules/beacon.ts +52 -0
- package/src/modules/contract.ts +82 -0
- package/src/modules/index.ts +29 -0
- package/src/modules/scheduler.ts +146 -0
- package/src/ui/App.tsx +15 -1
- package/src/ui/Explain.tsx +15 -2
- package/src/ui/Header.tsx +2 -0
- package/src/ui/Help.tsx +1 -1
- package/src/ui/main.tsx +42 -0
- package/src/ui/store.tsx +13 -1
- package/src/ui/views/BeaconSetup.tsx +268 -0
- package/src/ui/views/Modules.tsx +232 -0
- package/src/ui/views/SchedulerInstall.tsx +172 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// Modules tab: the parts of SpinupTUI that keep working after the TUI is closed.
|
|
2
|
+
// A view over the module contract (src/modules): each shipped module's status on
|
|
3
|
+
// this machine, plus the ones still to come, greyed out. This is also where a
|
|
4
|
+
// module gets set up — discovery and turning it on happen in the same place.
|
|
5
|
+
|
|
6
|
+
import { useEffect, useState } from "react"
|
|
7
|
+
import { useKeyboard } from "@opentui/react"
|
|
8
|
+
import { theme } from "../../lib/theme.ts"
|
|
9
|
+
import { truncate } from "../../lib/format.ts"
|
|
10
|
+
import { Panel } from "../components.tsx"
|
|
11
|
+
import { List, moveSelection } from "../List.tsx"
|
|
12
|
+
import { StatusBar } from "../StatusBar.tsx"
|
|
13
|
+
import { useStore } from "../store.tsx"
|
|
14
|
+
import { toast } from "../toast.ts"
|
|
15
|
+
import { MODULES, UPCOMING_MODULES } from "../../modules/index.ts"
|
|
16
|
+
import type { ModuleId, ModuleStatus } from "../../modules/contract.ts"
|
|
17
|
+
import { beaconFromConfig, beaconSend, testMessage } from "../../lib/beacon.ts"
|
|
18
|
+
import { loadConfig } from "../../config.ts"
|
|
19
|
+
import { upcomingRuns } from "../../modules/scheduler.ts"
|
|
20
|
+
|
|
21
|
+
interface Card {
|
|
22
|
+
id: ModuleId
|
|
23
|
+
name: string
|
|
24
|
+
summary: string
|
|
25
|
+
dependsOn: ModuleId[]
|
|
26
|
+
upcoming: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CARDS: Card[] = [
|
|
30
|
+
...MODULES.map((m) => ({ id: m.id, name: m.name, summary: m.summary, dependsOn: m.dependsOn, upcoming: false })),
|
|
31
|
+
...UPCOMING_MODULES.map((m) => ({ ...m, upcoming: true })),
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const NAMES = new Map(CARDS.map((c) => [c.id, c.name]))
|
|
35
|
+
|
|
36
|
+
function stateLabel(card: Card, status: ModuleStatus | undefined): { dot: string; label: string; color: string } {
|
|
37
|
+
if (card.upcoming) return { dot: "·", label: "coming soon", color: theme.textFaint }
|
|
38
|
+
if (!status) return { dot: "○", label: "checking…", color: theme.textDim }
|
|
39
|
+
if (status.state === "ok") return { dot: "●", label: "set up", color: theme.good }
|
|
40
|
+
if (status.state === "degraded") return { dot: "●", label: "needs attention", color: theme.warn }
|
|
41
|
+
return { dot: "○", label: "not set up", color: theme.textDim }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function Modules({ rows }: { rows: number }) {
|
|
45
|
+
const store = useStore()
|
|
46
|
+
const { route, inputMode, overlayOpen, beaconSetupOpen, setBeaconSetupOpen, schedulerInstallOpen, setSchedulerInstallOpen } = store
|
|
47
|
+
const [selected, setSelected] = useState(0)
|
|
48
|
+
const [statuses, setStatuses] = useState<Map<ModuleId, ModuleStatus>>(new Map())
|
|
49
|
+
const [testing, setTesting] = useState(false)
|
|
50
|
+
|
|
51
|
+
// Recompute on open and whenever a setup overlay closes (it may have changed
|
|
52
|
+
// the config). status() is cheap and never sends anything.
|
|
53
|
+
const [tick, setTick] = useState(0)
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (beaconSetupOpen || schedulerInstallOpen) return
|
|
56
|
+
let live = true
|
|
57
|
+
void Promise.all(MODULES.map((m) => m.status())).then((list) => {
|
|
58
|
+
if (live) setStatuses(new Map(list.map((s) => [s.id, s])))
|
|
59
|
+
})
|
|
60
|
+
return () => {
|
|
61
|
+
live = false
|
|
62
|
+
}
|
|
63
|
+
}, [beaconSetupOpen, schedulerInstallOpen, tick])
|
|
64
|
+
|
|
65
|
+
const isActive = route === "modules" && !inputMode && !overlayOpen
|
|
66
|
+
const card = CARDS[selected]!
|
|
67
|
+
const status = statuses.get(card.id)
|
|
68
|
+
|
|
69
|
+
const sendTest = () => {
|
|
70
|
+
if (testing) return
|
|
71
|
+
setTesting(true)
|
|
72
|
+
void beaconSend(beaconFromConfig(loadConfig()), testMessage()).then((r) => {
|
|
73
|
+
setTesting(false)
|
|
74
|
+
setTick((t) => t + 1)
|
|
75
|
+
if (r.ok) toast.success(`Test page sent via ${r.channel}`)
|
|
76
|
+
else toast.error(r.message)
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
useKeyboard((key) => {
|
|
81
|
+
if (!isActive) return
|
|
82
|
+
switch (key.name) {
|
|
83
|
+
case "up":
|
|
84
|
+
case "k":
|
|
85
|
+
return setSelected((i) => moveSelection(i, -1, CARDS.length))
|
|
86
|
+
case "down":
|
|
87
|
+
case "j":
|
|
88
|
+
return setSelected((i) => moveSelection(i, 1, CARDS.length))
|
|
89
|
+
case "return":
|
|
90
|
+
case "s":
|
|
91
|
+
if (card.id === "beacon") return setBeaconSetupOpen(true)
|
|
92
|
+
if (card.id === "scheduler") return setSchedulerInstallOpen(true)
|
|
93
|
+
return
|
|
94
|
+
case "t":
|
|
95
|
+
if (card.id === "beacon" && status && status.state !== "not_configured") return sendTest()
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const beaconReady = card.id === "beacon" && status !== undefined && status.state !== "not_configured"
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<box style={{ flexGrow: 1, flexDirection: "column" }}>
|
|
104
|
+
<box style={{ flexGrow: 1, flexDirection: "row", padding: 1, gap: 1 }}>
|
|
105
|
+
<Panel title=" Modules " active width={40}>
|
|
106
|
+
<List
|
|
107
|
+
items={CARDS}
|
|
108
|
+
selectedIndex={selected}
|
|
109
|
+
viewportRows={Math.max(3, rows - 4)}
|
|
110
|
+
focused
|
|
111
|
+
keyFor={(c) => c.id}
|
|
112
|
+
renderRow={(c, sel) => {
|
|
113
|
+
const s = stateLabel(c, statuses.get(c.id))
|
|
114
|
+
return (
|
|
115
|
+
<>
|
|
116
|
+
<text content={s.dot + " "} fg={s.color} style={{ flexShrink: 0 }} />
|
|
117
|
+
<text
|
|
118
|
+
content={c.name}
|
|
119
|
+
fg={c.upcoming ? theme.textFaint : sel ? theme.text : theme.textDim}
|
|
120
|
+
wrapMode="none"
|
|
121
|
+
style={{ flexGrow: 1, flexShrink: 1 }}
|
|
122
|
+
/>
|
|
123
|
+
<text content={" " + s.label} fg={s.color} wrapMode="none" style={{ flexShrink: 0 }} />
|
|
124
|
+
</>
|
|
125
|
+
)
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
</Panel>
|
|
129
|
+
|
|
130
|
+
<Panel title={` ${card.name} `} flexGrow={1}>
|
|
131
|
+
<box style={{ flexDirection: "column" }}>
|
|
132
|
+
<text content={card.summary} fg={card.upcoming ? theme.textDim : theme.text} />
|
|
133
|
+
<box style={{ height: 1 }} />
|
|
134
|
+
{card.dependsOn.length > 0 && (
|
|
135
|
+
<text content={`Builds on: ${card.dependsOn.map((d) => NAMES.get(d) ?? d).join(", ")}`} fg={theme.textFaint} />
|
|
136
|
+
)}
|
|
137
|
+
{card.upcoming ? <UpcomingDetail /> : <ModuleDetail card={card} status={status} />}
|
|
138
|
+
</box>
|
|
139
|
+
</Panel>
|
|
140
|
+
</box>
|
|
141
|
+
<StatusBar
|
|
142
|
+
hints={[
|
|
143
|
+
{ key: "↑↓/jk", label: "select" },
|
|
144
|
+
...(card.id === "beacon" ? [{ key: "⏎", label: beaconReady ? "reconfigure" : "set up" }] : []),
|
|
145
|
+
...(card.id === "scheduler" ? [{ key: "⏎", label: status && status.state !== "not_configured" ? "reinstall" : "install" }] : []),
|
|
146
|
+
...(beaconReady ? [{ key: "t", label: testing ? "sending…" : "test page" }] : []),
|
|
147
|
+
]}
|
|
148
|
+
/>
|
|
149
|
+
</box>
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function UpcomingDetail() {
|
|
154
|
+
return (
|
|
155
|
+
<box style={{ flexDirection: "column" }}>
|
|
156
|
+
<box style={{ height: 1 }} />
|
|
157
|
+
<text content="Coming soon" fg={theme.purple} attributes={1} />
|
|
158
|
+
<text content="Not in this version yet. It will appear here, ready to set up, when it ships." fg={theme.textFaint} />
|
|
159
|
+
</box>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function ModuleDetail({ card, status }: { card: Card; status: ModuleStatus | undefined }) {
|
|
164
|
+
if (!status) return <text content="Checking…" fg={theme.textDim} />
|
|
165
|
+
const s = stateLabel(card, status)
|
|
166
|
+
return (
|
|
167
|
+
<box style={{ flexDirection: "column" }}>
|
|
168
|
+
<box style={{ height: 1 }} />
|
|
169
|
+
<box style={{ flexDirection: "row" }}>
|
|
170
|
+
<text content={`${s.dot} `} fg={s.color} style={{ flexShrink: 0 }} />
|
|
171
|
+
<text content={status.summary} fg={s.color} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
172
|
+
</box>
|
|
173
|
+
{status.checks.length > 0 && (
|
|
174
|
+
<>
|
|
175
|
+
<box style={{ height: 1 }} />
|
|
176
|
+
{status.checks.map((c) => (
|
|
177
|
+
<box key={c.name} style={{ flexDirection: "row" }}>
|
|
178
|
+
<text content={c.ok ? "✓ " : "✗ "} fg={c.ok ? theme.good : theme.bad} style={{ flexShrink: 0 }} />
|
|
179
|
+
<text content={`${c.name}: `} fg={theme.textDim} wrapMode="none" style={{ flexShrink: 0 }} />
|
|
180
|
+
<text content={truncate(c.detail, 80)} fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
181
|
+
</box>
|
|
182
|
+
))}
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
185
|
+
<box style={{ height: 1 }} />
|
|
186
|
+
{card.id === "beacon" && status.state === "not_configured" && (
|
|
187
|
+
<>
|
|
188
|
+
<text content="Beacon pages you on Telegram when something needs you. Scripts, cron jobs and" fg={theme.textDim} />
|
|
189
|
+
<text content={'the other modules all send through it: spinuptui beacon send "…".'} fg={theme.textDim} />
|
|
190
|
+
<box style={{ height: 1 }} />
|
|
191
|
+
<text content="Press ⏎ to connect a Telegram bot. It takes about two minutes." fg={theme.accent} />
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
{card.id === "beacon" && status.state !== "not_configured" && (
|
|
195
|
+
<text content="⏎ reconfigure the pager · t send a test page · CLI: spinuptui beacon status" fg={theme.textFaint} />
|
|
196
|
+
)}
|
|
197
|
+
{card.id === "scheduler" && <SchedulerDetail installed={status.state !== "not_configured"} />}
|
|
198
|
+
</box>
|
|
199
|
+
)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function SchedulerDetail({ installed }: { installed: boolean }) {
|
|
203
|
+
const [runs] = useState(() => upcomingRuns())
|
|
204
|
+
return (
|
|
205
|
+
<>
|
|
206
|
+
<box style={{ height: 1 }} />
|
|
207
|
+
{!installed && (
|
|
208
|
+
<>
|
|
209
|
+
<text
|
|
210
|
+
content="Scheduler runs follow-ups on this machine while you're away: queue a check for tomorrow at 8 and the result arrives on your pager. It's also the clock the other modules run on, so turning it on is what makes this machine a station."
|
|
211
|
+
fg={theme.textDim}
|
|
212
|
+
/>
|
|
213
|
+
<box style={{ height: 1 }} />
|
|
214
|
+
<text content="Press ⏎ to see exactly what it installs, then confirm." fg={theme.accent} />
|
|
215
|
+
<box style={{ height: 1 }} />
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
<text content={runs.length ? "Next up:" : "No follow-ups queued."} fg={theme.textDim} />
|
|
219
|
+
{runs.map((r) => (
|
|
220
|
+
<box key={`${r.source}/${r.id}`} style={{ flexDirection: "row" }}>
|
|
221
|
+
<text content={` ${r.next.padEnd(17)} `} fg={theme.text} wrapMode="none" style={{ flexShrink: 0 }} />
|
|
222
|
+
<text content={`${r.source}/${r.id}`} fg={theme.textDim} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
223
|
+
</box>
|
|
224
|
+
))}
|
|
225
|
+
<box style={{ height: 1 }} />
|
|
226
|
+
<text
|
|
227
|
+
content={`${installed ? "⏎ reinstall (repairs the unit) · " : ""}CLI: spinuptui scheduler add | list | status`}
|
|
228
|
+
fg={theme.textFaint}
|
|
229
|
+
/>
|
|
230
|
+
</>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// Scheduler's install overlay (Modules tab, ⏎ on Scheduler): shows exactly what
|
|
2
|
+
// `spinuptui scheduler install --plan` would write and run, and applies it on
|
|
3
|
+
// `y` — the confirm-before-firing pattern. Applying ends with the proving tick
|
|
4
|
+
// (a tick that really ran under launchd), so "done" here means the station's
|
|
5
|
+
// clock is running, not just that a file was written.
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState } from "react"
|
|
8
|
+
import { useKeyboard } from "@opentui/react"
|
|
9
|
+
import { theme } from "../../lib/theme.ts"
|
|
10
|
+
import { truncate } from "../../lib/format.ts"
|
|
11
|
+
import { Panel, Spinner, Centered } from "../components.tsx"
|
|
12
|
+
import { StatusBar } from "../StatusBar.tsx"
|
|
13
|
+
import { useStore } from "../store.tsx"
|
|
14
|
+
import { applyInstall, binHasScheduler, NO_SCHEDULER, planInstall, type ApplyResult, type PlanResult } from "../../lib/schedulerInstall.ts"
|
|
15
|
+
|
|
16
|
+
type Phase = "confirm" | "applying" | "done" | "error"
|
|
17
|
+
|
|
18
|
+
export function SchedulerInstall() {
|
|
19
|
+
const { setSchedulerInstallOpen } = useStore()
|
|
20
|
+
const [plan, setPlan] = useState<PlanResult>(() => planInstall())
|
|
21
|
+
// Same refusal as `install --plan`: don't offer a plan whose binary has no
|
|
22
|
+
// Scheduler (it would launch the dashboard under launchd).
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!plan.ok) return
|
|
25
|
+
void binHasScheduler(plan).then((has) => {
|
|
26
|
+
if (!has) {
|
|
27
|
+
setPlan(NO_SCHEDULER)
|
|
28
|
+
setPhase("error")
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
// Checked once, for the plan computed on open.
|
|
32
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
33
|
+
}, [])
|
|
34
|
+
const [phase, setPhase] = useState<Phase>(plan.ok ? "confirm" : "error")
|
|
35
|
+
const [stage, setStage] = useState("")
|
|
36
|
+
const [result, setResult] = useState<ApplyResult | null>(null)
|
|
37
|
+
const [live, setLive] = useState(true)
|
|
38
|
+
useEffect(() => () => setLive(false), [])
|
|
39
|
+
|
|
40
|
+
const close = () => setSchedulerInstallOpen(false)
|
|
41
|
+
|
|
42
|
+
const apply = () => {
|
|
43
|
+
if (!plan.ok) return
|
|
44
|
+
setPhase("applying")
|
|
45
|
+
void applyInstall(plan, (s) => live && setStage(s)).then((r) => {
|
|
46
|
+
setResult(r)
|
|
47
|
+
setPhase(r.ok ? "done" : "error")
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
useKeyboard((key) => {
|
|
52
|
+
const name = key.name ?? ""
|
|
53
|
+
if (name === "escape" || name === "q") return close()
|
|
54
|
+
if (phase === "confirm" && name === "y" && plan.ok && plan.platform === "darwin") return apply()
|
|
55
|
+
if (phase === "error" && name === "r" && plan.ok) return setPhase("confirm")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<box
|
|
60
|
+
style={{
|
|
61
|
+
position: "absolute",
|
|
62
|
+
top: 0,
|
|
63
|
+
left: 0,
|
|
64
|
+
width: "100%",
|
|
65
|
+
height: "100%",
|
|
66
|
+
flexDirection: "column",
|
|
67
|
+
backgroundColor: theme.bg,
|
|
68
|
+
zIndex: 210,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
<box style={{ flexDirection: "row", height: 1, backgroundColor: theme.bgAlt, paddingLeft: 1, paddingRight: 1, alignItems: "center" }}>
|
|
72
|
+
<text content="⏱ Scheduler " fg={theme.brand} style={{ flexShrink: 0 }} />
|
|
73
|
+
<text content="install the station's clock" fg={theme.text} wrapMode="none" style={{ flexShrink: 1 }} />
|
|
74
|
+
</box>
|
|
75
|
+
<Centered>{body()}</Centered>
|
|
76
|
+
<StatusBar hints={hints()} showGlobal={false} />
|
|
77
|
+
</box>
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
function body() {
|
|
81
|
+
if (phase === "applying") {
|
|
82
|
+
return (
|
|
83
|
+
<box style={{ flexDirection: "column", alignItems: "center" }}>
|
|
84
|
+
<box style={{ flexDirection: "row" }}>
|
|
85
|
+
<Spinner />
|
|
86
|
+
<text content={` ${stage || "Installing…"}`} fg={theme.textDim} wrapMode="none" />
|
|
87
|
+
</box>
|
|
88
|
+
<box style={{ height: 1 }} />
|
|
89
|
+
<text content="Install finishes with a real tick from launchd (up to a minute)." fg={theme.textFaint} wrapMode="none" />
|
|
90
|
+
</box>
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
if (phase === "done" && result?.ok) {
|
|
94
|
+
return (
|
|
95
|
+
<Panel title=" Installed " active>
|
|
96
|
+
<box style={{ flexDirection: "column", width: 70, paddingTop: 1, paddingBottom: 1 }}>
|
|
97
|
+
<text content={`✓ ${result.label} is ticking: its first run under launchd was at ${new Date(result.tickAt).toLocaleTimeString()}.`} fg={theme.good} />
|
|
98
|
+
<box style={{ height: 1 }} />
|
|
99
|
+
<text content="Queue a follow-up from any session:" fg={theme.textDim} />
|
|
100
|
+
<text content={' spinuptui scheduler add <id> --kind spinuptui --when "YYYY-MM-DD 08:00" \\'} fg={theme.accent} wrapMode="none" />
|
|
101
|
+
<text content={' --expires YYYY-MM-DD --created-by "…" -- incidents example.com'} fg={theme.accent} wrapMode="none" />
|
|
102
|
+
<box style={{ height: 1 }} />
|
|
103
|
+
{result.notes.map((n) => (
|
|
104
|
+
<text key={n} content={`Note: ${n}`} fg={theme.textFaint} />
|
|
105
|
+
))}
|
|
106
|
+
<text content="Esc to close" fg={theme.textFaint} />
|
|
107
|
+
</box>
|
|
108
|
+
</Panel>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
if (phase === "error" || !plan.ok) {
|
|
112
|
+
const r = !plan.ok ? plan : result && !result.ok ? result : null
|
|
113
|
+
return (
|
|
114
|
+
<Panel title=" Can't install " active>
|
|
115
|
+
<box style={{ flexDirection: "column", width: 72, paddingTop: 1, paddingBottom: 1 }}>
|
|
116
|
+
<text content={`✕ ${r?.message ?? "Something went wrong."}`} fg={theme.bad} />
|
|
117
|
+
{r?.remedy && (
|
|
118
|
+
<>
|
|
119
|
+
<box style={{ height: 1 }} />
|
|
120
|
+
<text content={r.remedy} fg={theme.textDim} />
|
|
121
|
+
</>
|
|
122
|
+
)}
|
|
123
|
+
<box style={{ height: 1 }} />
|
|
124
|
+
<text content={plan.ok ? "Press r to see the plan again · Esc to close" : "Esc to close"} fg={theme.textFaint} />
|
|
125
|
+
</box>
|
|
126
|
+
</Panel>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
const darwin = plan.platform === "darwin"
|
|
130
|
+
return (
|
|
131
|
+
<Panel title=" Install Scheduler's dispatcher " active>
|
|
132
|
+
<box style={{ flexDirection: "column", width: 76, paddingTop: 1, paddingBottom: 1 }}>
|
|
133
|
+
<text content="Runs at login and every 5 minutes, unattended:" fg={theme.text} />
|
|
134
|
+
<text content={` ${truncate(plan.programArguments.join(" "), 72)}`} fg={theme.accent} wrapMode="none" />
|
|
135
|
+
<box style={{ height: 1 }} />
|
|
136
|
+
<text content={`Unit: ${truncate(plan.unitPath, 68)}`} fg={theme.textDim} wrapMode="none" />
|
|
137
|
+
<text content={`PATH: ${truncate(plan.env.PATH ?? "", 68)}`} fg={theme.textDim} wrapMode="none" />
|
|
138
|
+
<box style={{ height: 1 }} />
|
|
139
|
+
{plan.commands.map((c) => (
|
|
140
|
+
<text key={c} content={`· ${truncate(c, 72)}`} fg={theme.textDim} wrapMode="none" />
|
|
141
|
+
))}
|
|
142
|
+
<box style={{ height: 1 }} />
|
|
143
|
+
{plan.notes.map((n) => (
|
|
144
|
+
<text key={n} content={`Note: ${n}`} fg={theme.textFaint} />
|
|
145
|
+
))}
|
|
146
|
+
<box style={{ height: 1 }} />
|
|
147
|
+
<text
|
|
148
|
+
content={darwin ? "Press y to install · Esc to cancel" : "Installing on Linux isn't supported yet · Esc to close"}
|
|
149
|
+
fg={darwin ? theme.text : theme.warn}
|
|
150
|
+
wrapMode="none"
|
|
151
|
+
/>
|
|
152
|
+
</box>
|
|
153
|
+
</Panel>
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function hints() {
|
|
158
|
+
if (phase === "confirm" && plan.ok && plan.platform === "darwin") {
|
|
159
|
+
return [
|
|
160
|
+
{ key: "y", label: "install" },
|
|
161
|
+
{ key: "esc", label: "cancel" },
|
|
162
|
+
]
|
|
163
|
+
}
|
|
164
|
+
if (phase === "error" && plan.ok) {
|
|
165
|
+
return [
|
|
166
|
+
{ key: "r", label: "back to plan" },
|
|
167
|
+
{ key: "esc", label: "close" },
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
return [{ key: "esc", label: "close" }]
|
|
171
|
+
}
|
|
172
|
+
}
|