@kitlangton/tailcode 0.2.3 → 0.2.5

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/src/app.tsx DELETED
@@ -1,567 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
-
3
- import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
4
- import { TextAttributes } from "@opentui/core"
5
- import { createMemo, createSignal, onCleanup, onMount, Show } from "solid-js"
6
- import { useAtomSet, useAtomValue } from "@effect/atom-solid/Hooks"
7
- import type { MissingBinary, Step } from "./state.js"
8
- import { phase, step, log, url, error, missingBinary } from "./state.js"
9
- import { renderQR, copyToClipboard, openInBrowser } from "./qr.js"
10
- import { flowFn, signalExit, waitForFlowStop } from "./flow.js"
11
-
12
- // -- Palette --
13
- export const color = {
14
- bg: "#05070b",
15
- panel: "#141820",
16
- panelSoft: "#191e28",
17
- tail: "#8e96a3",
18
- code: "#dde1e8",
19
- codeShadow: "#444c5c",
20
- accent: "#5fa8ff",
21
- success: "#7ad8ae",
22
- error: "#ff7f7f",
23
- notice: "#f2b85d",
24
- text: "#d2d7e2",
25
- muted: "#8a93a5",
26
- dim: "#666f82",
27
- }
28
-
29
- // -- Wordmark (same pixel font as opencode's logo.ts) --
30
- const LOGO = {
31
- left: [" ", "▀▀█▀ ▄▀█ ▀▀█▀ ▄▀ ", " ▄▀ ▄▀▀█ ▄▀ ▄▀ ", " ▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀"],
32
- right: [" ▄ ", "█▀▀▀ █▀▀█ █▀▀█ █▀▀█", "█___ █__█ █__█ █^^^", "▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀"],
33
- }
34
-
35
- type InstallGuide = {
36
- readonly title: string
37
- readonly docsUrl: string
38
- readonly installCommand: string
39
- readonly installHint: string
40
- }
41
-
42
- type StageState = "pending" | "active" | "done" | "error"
43
- const STAGE_ORDER: ReadonlyArray<Step> = ["tailscale", "opencode", "publish"]
44
-
45
- function getInstallGuide(binary: MissingBinary, platform: NodeJS.Platform): InstallGuide {
46
- if (binary === "opencode") {
47
- if (platform === "darwin") {
48
- return {
49
- title: "Install OpenCode for macOS",
50
- docsUrl: "https://opencode.ai/docs/#install",
51
- installCommand: "brew install anomalyco/tap/opencode",
52
- installHint: "Alternative: bun install -g opencode-ai",
53
- }
54
- }
55
-
56
- if (platform === "win32") {
57
- return {
58
- title: "Install OpenCode for Windows",
59
- docsUrl: "https://opencode.ai/docs/windows-wsl",
60
- installCommand: "choco install opencode",
61
- installHint: "Recommended path is WSL2. Open the docs for setup details.",
62
- }
63
- }
64
-
65
- return {
66
- title: "Install OpenCode for Linux",
67
- docsUrl: "https://opencode.ai/docs/#install",
68
- installCommand: "curl -fsSL https://opencode.ai/install | bash",
69
- installHint: "Alternative: bun install -g opencode-ai",
70
- }
71
- }
72
-
73
- if (platform === "darwin") {
74
- return {
75
- title: "Install Tailscale for macOS",
76
- docsUrl: "https://tailscale.com/docs/install/mac",
77
- installCommand: "brew install --cask tailscale-app",
78
- installHint: "If Homebrew is unavailable, open the docs link for installer options.",
79
- }
80
- }
81
-
82
- if (platform === "win32") {
83
- return {
84
- title: "Install Tailscale for Windows",
85
- docsUrl: "https://tailscale.com/docs/install/windows",
86
- installCommand: "winget install --id tailscale.tailscale --exact",
87
- installHint: "If winget is unavailable, use the installer from the docs link.",
88
- }
89
- }
90
-
91
- return {
92
- title: "Install Tailscale for Linux",
93
- docsUrl: "https://tailscale.com/docs/install/linux",
94
- installCommand: "curl -fsSL https://tailscale.com/install.sh | sh",
95
- installHint: "Install may require sudo depending on your distro.",
96
- }
97
- }
98
-
99
- export function logoLine(line: string, fg: string, shadow: string, bold: boolean) {
100
- const attrs = bold ? TextAttributes.BOLD : undefined
101
- const els: any[] = []
102
- let i = 0
103
- while (i < line.length) {
104
- const rest = line.slice(i)
105
- const m = rest.search(/[_^~]/)
106
- if (m === -1) {
107
- els.push(
108
- <text fg={fg} attributes={attrs}>
109
- {rest}
110
- </text>,
111
- )
112
- break
113
- }
114
- if (m > 0)
115
- els.push(
116
- <text fg={fg} attributes={attrs}>
117
- {rest.slice(0, m)}
118
- </text>,
119
- )
120
- const c = rest[m]
121
- if (c === "_")
122
- els.push(
123
- <text fg={fg} bg={shadow} attributes={attrs}>
124
- {" "}
125
- </text>,
126
- )
127
- else if (c === "^")
128
- els.push(
129
- <text fg={fg} bg={shadow} attributes={attrs}>
130
-
131
- </text>,
132
- )
133
- else if (c === "~")
134
- els.push(
135
- <text fg={shadow} attributes={attrs}>
136
-
137
- </text>,
138
- )
139
- i += m + 1
140
- }
141
- return els
142
- }
143
-
144
- type AppProps = {
145
- readonly onStart?: () => void
146
- readonly onQuit?: () => void
147
- readonly resetToWelcomeOnMount?: boolean
148
- }
149
-
150
- type PanelTone = "primary" | "soft"
151
- type PanelProps = {
152
- readonly tone?: PanelTone
153
- readonly gap?: number
154
- readonly paddingTop?: number
155
- readonly paddingBottom?: number
156
- readonly children: any
157
- }
158
-
159
- function Panel(props: PanelProps) {
160
- return (
161
- <box
162
- flexDirection="column"
163
- backgroundColor={props.tone === "soft" ? color.panelSoft : color.panel}
164
- paddingLeft={2}
165
- paddingRight={2}
166
- paddingTop={props.paddingTop ?? 1}
167
- paddingBottom={props.paddingBottom ?? 1}
168
- gap={props.gap ?? 0}
169
- >
170
- {props.children}
171
- </box>
172
- )
173
- }
174
-
175
- type ActionItem = {
176
- readonly key: string
177
- readonly label: string
178
- readonly keyFg?: string
179
- readonly labelFg?: string
180
- }
181
-
182
- function ActionRow(props: { readonly items: ReadonlyArray<ActionItem> }) {
183
- return (
184
- <box flexDirection="row" gap={1}>
185
- {props.items.map((item, index) => (
186
- <>
187
- <Show when={index > 0}>
188
- <text fg={color.dim}>·</text>
189
- </Show>
190
- <text fg={item.keyFg ?? color.muted}>{item.key}</text>
191
- <text fg={item.labelFg ?? color.dim}>{item.label}</text>
192
- </>
193
- ))}
194
- </box>
195
- )
196
- }
197
-
198
- export function App(props: AppProps = {}) {
199
- const dims = useTerminalDimensions()
200
- const currentPhase = useAtomValue(phase)
201
- const logText = useAtomValue(log)
202
- const remoteUrl = useAtomValue(url)
203
- const errorMessage = useAtomValue(error)
204
- const currentStep = useAtomValue(step)
205
- const currentMissingBinary = useAtomValue(missingBinary)
206
- const installGuide = createMemo(() => getInstallGuide(currentMissingBinary() || "tailscale", process.platform))
207
-
208
- const setPhase = useAtomSet(phase)
209
- const setError = useAtomSet(error)
210
- const setUrl = useAtomSet(url)
211
- const setLog = useAtomSet(log)
212
- const setMissingBinary = useAtomSet(missingBinary)
213
- const triggerFlow = useAtomSet(flowFn)
214
-
215
- const [flashedKey, setFlashedKey] = createSignal("")
216
- const [spinnerFrame, setSpinnerFrame] = createSignal(0)
217
- const [showTailscaleQR, setShowTailscaleQR] = createSignal(false)
218
-
219
- const TAILSCALE_DOWNLOAD_URL = "https://tailscale.com/download"
220
-
221
- const qrCode = createMemo(() => {
222
- const targetUrl = showTailscaleQR() ? TAILSCALE_DOWNLOAD_URL : remoteUrl()
223
- if (!targetUrl) return ""
224
- try {
225
- return renderQR(targetUrl)
226
- } catch {
227
- return ""
228
- }
229
- })
230
-
231
- const localCmd = () => `opencode attach http://127.0.0.1:4096`
232
-
233
- const terminalWidth = createMemo(() => {
234
- const width = Number(dims().width ?? 80)
235
- return Number.isFinite(width) && width > 0 ? width : 80
236
- })
237
-
238
- const panelWidth = createMemo(() => {
239
- const available = Math.max(26, terminalWidth() - 4)
240
- return Math.min(104, available)
241
- })
242
-
243
- const compact = createMemo(() => panelWidth() < 74)
244
- const spinnerGlyph = createMemo(() => {
245
- const frames = ["|", "/", "-", "\\"]
246
- return frames[spinnerFrame() % frames.length] ?? "|"
247
- })
248
- const recentLog = createMemo(() => {
249
- const lines = logText()
250
- .split("\n")
251
- .map((line) => line.trim())
252
- .filter(Boolean)
253
- return lines.slice(-4).join("\n")
254
- })
255
-
256
- const flash = (key: string) => {
257
- setFlashedKey(key)
258
- setTimeout(() => setFlashedKey(""), 1500)
259
- }
260
-
261
- const quit = () => {
262
- if (props.onQuit) {
263
- props.onQuit()
264
- return
265
- }
266
-
267
- signalExit()
268
- const fallback = setTimeout(() => process.exit(0), 5000)
269
- void waitForFlowStop().finally(() => {
270
- clearTimeout(fallback)
271
- process.exit(0)
272
- })
273
- }
274
-
275
- const keyColor = (key: string) => (flashedKey() === key ? color.notice : color.muted)
276
- const labelColor = (key: string) => (flashedKey() === key ? color.notice : color.dim)
277
-
278
- const start = () => {
279
- if (props.onStart) {
280
- props.onStart()
281
- return
282
- }
283
-
284
- setError("")
285
- setMissingBinary("")
286
- setUrl("")
287
- setLog("")
288
- setPhase("running")
289
- triggerFlow(undefined)
290
- }
291
-
292
- useKeyboard((evt) => {
293
- if (evt.name === "q" || evt.name === "escape" || (evt.ctrl && evt.name === "c")) {
294
- quit()
295
- return
296
- }
297
- if (currentPhase() === "welcome" && evt.name === "return") {
298
- evt.preventDefault()
299
- start()
300
- return
301
- }
302
- if (currentPhase() === "install" && evt.name === "return") {
303
- evt.preventDefault()
304
- start()
305
- return
306
- }
307
- if (currentPhase() === "error" && evt.name === "return") {
308
- evt.preventDefault()
309
- start()
310
- return
311
- }
312
- if (currentPhase() === "install" && (evt.name === "1" || evt.name === "2")) {
313
- evt.preventDefault()
314
- if (evt.name === "1") {
315
- void copyToClipboard(installGuide().installCommand)
316
- .then(() => flash("1"))
317
- .catch(() => {})
318
- } else if (evt.name === "2") {
319
- void openInBrowser(installGuide().docsUrl)
320
- .then(() => flash("2"))
321
- .catch(() => {})
322
- }
323
- return
324
- }
325
- if (currentPhase() === "done" && (evt.name === "1" || evt.name === "2" || evt.name === "3" || evt.name === "4")) {
326
- evt.preventDefault()
327
- if (evt.name === "1") {
328
- void copyToClipboard(remoteUrl())
329
- .then(() => flash("1"))
330
- .catch(() => {})
331
- } else if (evt.name === "2") {
332
- void openInBrowser(remoteUrl())
333
- .then(() => flash("2"))
334
- .catch(() => {})
335
- } else if (evt.name === "3") {
336
- void copyToClipboard(localCmd())
337
- .then(() => flash("3"))
338
- .catch(() => {})
339
- } else {
340
- setShowTailscaleQR((v) => !v)
341
- flash("4")
342
- }
343
- return
344
- }
345
- })
346
-
347
- onMount(() => {
348
- const timer = setInterval(() => {
349
- setSpinnerFrame((value) => value + 1)
350
- }, 120)
351
-
352
- onCleanup(() => {
353
- clearInterval(timer)
354
- })
355
-
356
- if (props.resetToWelcomeOnMount === false) return
357
- setPhase("welcome")
358
- })
359
-
360
- const stageState = (id: Step): StageState => {
361
- const active = currentStep()
362
-
363
- if (currentPhase() === "done") return "done"
364
- if (currentPhase() === "install") {
365
- const failedStep: Step = currentMissingBinary() === "opencode" ? "opencode" : "tailscale"
366
- return id === failedStep ? "error" : "pending"
367
- }
368
-
369
- const activeIndex = STAGE_ORDER.indexOf(active)
370
- const currentIndex = STAGE_ORDER.indexOf(id)
371
-
372
- if (currentPhase() === "error") {
373
- if (active === id) return "error"
374
- if (activeIndex > currentIndex) return "done"
375
- return "pending"
376
- }
377
-
378
- if (active === id && currentPhase() === "running") return "active"
379
- if (activeIndex > currentIndex) return "done"
380
- return "pending"
381
- }
382
-
383
- const stageColor = (id: Step) => {
384
- const state = stageState(id)
385
- if (state === "done") return color.success
386
- if (state === "active") return color.accent
387
- if (state === "error") return color.error
388
- return color.muted
389
- }
390
-
391
- const stageBg = (id: Step) => {
392
- const state = stageState(id)
393
- if (state === "done") return "#1d2d27"
394
- if (state === "active") return "#1f3046"
395
- if (state === "error") return "#382429"
396
- return color.panelSoft
397
- }
398
-
399
- const stageBadge = (id: Step, label: string) => {
400
- const state = stageState(id)
401
- const glyph = state === "done" ? "✓" : state === "active" ? spinnerGlyph() : state === "error" ? "✕" : "·"
402
-
403
- const glyphColor = state === "pending" ? color.dim : stageColor(id)
404
-
405
- return (
406
- <box backgroundColor={stageBg(id)} paddingLeft={1} paddingRight={1} flexDirection="row">
407
- <text fg={glyphColor}>{glyph}</text>
408
- <text fg={stageColor(id)}>{` ${label}`}</text>
409
- </box>
410
- )
411
- }
412
-
413
- const stageRow = () => (
414
- <box flexDirection="row" gap={0}>
415
- {stageBadge("tailscale", "Tailscale")}
416
- {stageBadge("opencode", "OpenCode")}
417
- {stageBadge("publish", "Publish")}
418
- </box>
419
- )
420
-
421
- return (
422
- <box flexDirection="column" flexGrow={1} backgroundColor={color.bg}>
423
- <box
424
- flexDirection="column"
425
- flexGrow={1}
426
- justifyContent="center"
427
- alignItems="center"
428
- paddingLeft={2}
429
- paddingRight={2}
430
- >
431
- <box flexDirection="column" width={panelWidth()}>
432
- <box flexDirection="column" alignItems="center">
433
- {LOGO.left.map((line, i) => (
434
- <box flexDirection="row" gap={1}>
435
- <box flexDirection="row">{logoLine(line, color.tail, color.dim, false)}</box>
436
- <box flexDirection="row">{logoLine(LOGO.right[i]!, color.code, color.codeShadow, true)}</box>
437
- </box>
438
- ))}
439
- </box>
440
- <text>{""}</text>
441
- <box flexDirection="column" alignItems="flex-start">
442
- {stageRow()}
443
- </box>
444
- <Show when={currentPhase() === "welcome"}>
445
- <Panel>
446
- <text fg={color.dim}>Guided setup for OpenCode over Tailscale</text>
447
- <text>{""}</text>
448
- <text fg={color.text}>This wizard will:</text>
449
- <text fg={color.muted}>· Connect to Tailscale</text>
450
- <text fg={color.muted}>· Start OpenCode server on localhost</text>
451
- <text fg={color.muted}>· Publish with tailscale serve</text>
452
- <text>{""}</text>
453
- <ActionRow
454
- items={[
455
- { key: "enter", label: "start", keyFg: color.accent, labelFg: color.dim },
456
- { key: "q", label: "quit", keyFg: color.muted, labelFg: color.dim },
457
- ]}
458
- />
459
- </Panel>
460
- </Show>
461
-
462
- <Show when={currentPhase() === "running"}>
463
- <Panel>
464
- <text fg={color.dim}>Usually finishes in a few seconds.</text>
465
- <text>{""}</text>
466
- <ActionRow items={[{ key: "q", label: "quit", keyFg: color.muted, labelFg: color.dim }]} />
467
- </Panel>
468
- </Show>
469
-
470
- <Show when={currentPhase() === "install"}>
471
- <Panel>
472
- <text fg={color.error}>{() => errorMessage()}</text>
473
- </Panel>
474
-
475
- <Panel tone="soft">
476
- <text fg={color.dim}>{() => installGuide().title}</text>
477
- <text fg={color.accent}>{() => installGuide().installCommand}</text>
478
- <ActionRow
479
- items={[
480
- { key: "1", label: "copy install command", keyFg: keyColor("1"), labelFg: labelColor("1") },
481
- { key: "2", label: "open install guide", keyFg: keyColor("2"), labelFg: labelColor("2") },
482
- ]}
483
- />
484
- <text>{""}</text>
485
- <text fg={color.dim}>{() => installGuide().installHint}</text>
486
- <text fg={color.muted}>{() => installGuide().docsUrl}</text>
487
- <text>{""}</text>
488
- <ActionRow
489
- items={[
490
- { key: "enter", label: "check again", keyFg: color.accent, labelFg: color.dim },
491
- { key: "q", label: "quit", keyFg: color.muted, labelFg: color.dim },
492
- ]}
493
- />
494
- </Panel>
495
- </Show>
496
-
497
- <Show when={currentPhase() === "error"}>
498
- <Panel>
499
- <text fg={color.error}>Setup Failed</text>
500
- </Panel>
501
-
502
- <Panel tone="soft">
503
- <text fg={color.error}>{() => `Error: ${errorMessage()}`}</text>
504
- <Show when={recentLog()}>
505
- <text>{""}</text>
506
- <text fg={color.muted}>Recent output</text>
507
- <text fg={color.dim}>{() => recentLog()}</text>
508
- </Show>
509
- <text>{""}</text>
510
- <ActionRow
511
- items={[
512
- { key: "enter", label: "retry", keyFg: color.accent, labelFg: color.dim },
513
- { key: "q", label: "quit", keyFg: color.muted, labelFg: color.dim },
514
- ]}
515
- />
516
- </Panel>
517
- </Show>
518
-
519
- <Show when={currentPhase() === "done"}>
520
- <Panel gap={0}>
521
- <text fg={color.dim}>Connect from any device on your tailnet</text>
522
- <text fg={color.accent}>{() => `${remoteUrl()}`}</text>
523
- <ActionRow
524
- items={[
525
- { key: "1", label: "copy URL", keyFg: keyColor("1"), labelFg: labelColor("1") },
526
- { key: "2", label: "open browser", keyFg: keyColor("2"), labelFg: labelColor("2") },
527
- ]}
528
- />
529
- <text>{""}</text>
530
- <text fg={color.dim}>Attach from this machine</text>
531
- <text fg={color.text}>{() => `${localCmd()}`}</text>
532
- <ActionRow
533
- items={[
534
- { key: "3", label: "copy command", keyFg: keyColor("3"), labelFg: labelColor("3") },
535
- { key: "q", label: "quit", keyFg: color.muted, labelFg: color.dim },
536
- ]}
537
- />
538
- </Panel>
539
- </Show>
540
-
541
- <Show when={currentPhase() === "done" && qrCode() && !compact()}>
542
- <Panel tone="soft" paddingBottom={0}>
543
- <Show when={showTailscaleQR()}>
544
- <text fg={color.accent}>Scan to install Tailscale on your phone</text>
545
- <text fg={color.dim}>{TAILSCALE_DOWNLOAD_URL}</text>
546
- </Show>
547
- <Show when={!showTailscaleQR()}>
548
- <text fg={color.muted}>Scan to connect from mobile</text>
549
- <text fg={color.dim}>Phone must be on your tailnet</text>
550
- <ActionRow
551
- items={[{ key: "4", label: "download Tailscale mobile app", keyFg: color.muted, labelFg: color.dim }]}
552
- />
553
- </Show>
554
- <text>{() => qrCode()}</text>
555
- </Panel>
556
- </Show>
557
-
558
- <Show when={currentPhase() === "done" && qrCode() && compact()}>
559
- <Panel tone="soft">
560
- <text fg={color.dim}>Terminal is narrow, use [1] to copy the URL.</text>
561
- </Panel>
562
- </Show>
563
- </box>
564
- </box>
565
- </box>
566
- )
567
- }
package/src/flow.ts DELETED
@@ -1,115 +0,0 @@
1
- import { Deferred, Effect, Layer } from "effect"
2
- import { appRuntime } from "./runtime.js"
3
- import { registry, phase, step, log, url, error, missingBinary } from "./state.js"
4
- import { Tailscale } from "./services/tailscale.js"
5
- import { OpenCode } from "./services/opencode.js"
6
- import { AppConfig } from "./services/config.js"
7
- import { stripTerminalControl, trim } from "./qr.js"
8
- import { BinaryNotFound } from "./services/errors.js"
9
-
10
- function append(line: string) {
11
- const clean = stripTerminalControl(line)
12
- if (!clean) return
13
- registry.update(log, (prev) => trim(prev + clean, 16_000))
14
- }
15
-
16
- // Resolved when the user requests a clean exit so the scope finalizer runs
17
- const exitSignal = Deferred.makeUnsafe<void>()
18
- let exitSignaled = false
19
- let flowRunning = false
20
- const shutdownWaiters = new Set<() => void>()
21
-
22
- function resolveShutdownWaiters() {
23
- for (const resolve of shutdownWaiters) resolve()
24
- shutdownWaiters.clear()
25
- }
26
-
27
- export function signalExit() {
28
- if (exitSignaled) return
29
- exitSignaled = true
30
- Deferred.doneUnsafe(exitSignal, Effect.succeed(undefined))
31
- }
32
-
33
- export function waitForFlowStop() {
34
- if (!flowRunning) return Promise.resolve()
35
- return new Promise<void>((resolve) => {
36
- shutdownWaiters.add(resolve)
37
- })
38
- }
39
-
40
- export const flowFn = appRuntime.fn<void>()(() =>
41
- Effect.gen(function* () {
42
- flowRunning = true
43
- registry.set(missingBinary, "")
44
-
45
- const config = yield* AppConfig
46
- const tailscale = yield* Tailscale
47
- const opencode = yield* OpenCode
48
-
49
- // Step 1: Tailscale — ensure connected
50
- registry.set(step, "tailscale")
51
- const bin = yield* tailscale.ensure(append)
52
-
53
- // Step 2: OpenCode — start server (handle lives in scope via ChildProcess.spawn)
54
- registry.set(step, "opencode")
55
- yield* opencode.start(config.port, config.password, append)
56
-
57
- // Step 3: Publish via tailscale serve
58
- registry.set(step, "publish")
59
- const remote = yield* tailscale.publish(bin, config.port, append)
60
-
61
- registry.set(url, remote)
62
- registry.set(log, "")
63
- registry.set(step, "idle")
64
- registry.set(missingBinary, "")
65
- registry.set(phase, "done")
66
-
67
- // Hold scope open (keeping finalizer alive) until quit is signalled.
68
- // Re-triggering flowFn interrupts this fiber instead.
69
- yield* Deferred.await(exitSignal)
70
- }).pipe(
71
- Effect.catch((err) =>
72
- Effect.sync(() => {
73
- if ((err as any)?._tag === "BinaryNotFound") {
74
- const missing = err as BinaryNotFound
75
- registry.set(error, `${missing.binary} is not installed`)
76
- if (missing.binary === "tailscale" || missing.binary === "opencode") {
77
- registry.set(missingBinary, missing.binary)
78
- } else {
79
- registry.set(missingBinary, "")
80
- }
81
- registry.set(phase, "install")
82
- return
83
- }
84
-
85
- registry.set(missingBinary, "")
86
- registry.set(error, "message" in err ? (err as any).message : String(err))
87
- registry.set(phase, "error")
88
- }),
89
- ),
90
- Effect.provide(Layer.mergeAll(Tailscale.layer, OpenCode.layer)),
91
- Effect.ensuring(
92
- Effect.sync(() => {
93
- flowRunning = false
94
- resolveShutdownWaiters()
95
- }),
96
- ),
97
- ),
98
- )
99
-
100
- function gracefulProcessExit() {
101
- signalExit()
102
- const fallback = setTimeout(() => process.exit(0), 5000)
103
- void waitForFlowStop().finally(() => {
104
- clearTimeout(fallback)
105
- process.exit(0)
106
- })
107
- }
108
-
109
- // Signal handlers
110
- process.on("SIGINT", () => {
111
- gracefulProcessExit()
112
- })
113
- process.on("SIGTERM", () => {
114
- gracefulProcessExit()
115
- })
package/src/main.tsx DELETED
@@ -1,23 +0,0 @@
1
- /** @jsxImportSource @opentui/solid */
2
-
3
- // Explicitly load the OpenTUI Solid preload (normally done via bunfig.toml in dev)
4
- import "@opentui/solid/preload"
5
-
6
- import { render } from "@opentui/solid"
7
- import { RegistryContext } from "@effect/atom-solid/RegistryContext"
8
- import { App } from "./app.js"
9
- import { registry } from "./state.js"
10
-
11
- // -- Render --
12
-
13
- render(
14
- () => (
15
- <RegistryContext.Provider value={registry}>
16
- <App />
17
- </RegistryContext.Provider>
18
- ),
19
- {
20
- targetFps: 60,
21
- exitOnCtrlC: false,
22
- },
23
- )