opendocker 0.1.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/AGENTS.md +22 -0
- package/README.md +27 -0
- package/TODO.md +53 -0
- package/docker-compose.yml +24 -0
- package/package.json +26 -0
- package/src/assets/images/banner.jpg +0 -0
- package/src/components/Border.tsx +19 -0
- package/src/components/ContainersPane.tsx +142 -0
- package/src/components/ImagesPane.tsx +83 -0
- package/src/components/LogsPane.tsx +128 -0
- package/src/components/Pane.tsx +34 -0
- package/src/components/VolumesPanes.tsx +84 -0
- package/src/layouts/BaseLayout.tsx +60 -0
- package/src/lib/docker.ts +68 -0
- package/src/main.tsx +40 -0
- package/src/stores/application.ts +11 -0
- package/src/stores/containers.ts +22 -0
- package/src/stores/images.ts +11 -0
- package/src/stores/volumes.ts +11 -0
- package/src/utils/styling.ts +198 -0
- package/tsconfig.json +15 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Agent Development Guide
|
|
2
|
+
|
|
3
|
+
## Commands
|
|
4
|
+
- **Run**: `bun run src/main.tsx`
|
|
5
|
+
- **Dev**: `bun --watch run src/main.tsx`
|
|
6
|
+
- **Test**: `bun test` (uses `bun:test` imports)
|
|
7
|
+
- **Install**: `bun install`
|
|
8
|
+
|
|
9
|
+
## Code Style
|
|
10
|
+
- **Framework**: React with @opentui/react for terminal UI
|
|
11
|
+
- **Imports**: Use path aliases `@/*` for src/ and `@tui/*` for src/cli/cmd/tui/
|
|
12
|
+
- **Components**: Default exports, PascalCase naming
|
|
13
|
+
- **Keyboard**: Vim-style navigation (j/k for up/down, q to quit)
|
|
14
|
+
- **Colors**: Use centralized colors from `src/utils/colors.ts`
|
|
15
|
+
- **Error Handling**: Try-catch with state-based error handling
|
|
16
|
+
- **Shell Commands**: Use `Bun.$` for shell execution
|
|
17
|
+
|
|
18
|
+
## Key Patterns
|
|
19
|
+
- Use `useKeyboard()` hook for terminal input handling
|
|
20
|
+
- Components use flexbox layout with terminal dimensions
|
|
21
|
+
- State management with React hooks (useState, useEffect)
|
|
22
|
+
- Terminal UI elements: `<box>`, `<text>` with styling props
|
package/README.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<p align="center">The AI coding agent built for the terminal.</p>
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
To install dependencies:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun install
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
To run:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bun start
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## FAQ
|
|
20
|
+
|
|
21
|
+
### How is this different than Claude Code?
|
|
22
|
+
|
|
23
|
+
It's very similar to lazydocker in terms of capability. Here are the key differences:
|
|
24
|
+
|
|
25
|
+
- It looks good
|
|
26
|
+
- It's easier on the eyes
|
|
27
|
+
- It looks good
|
package/TODO.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# OpenDocker TODO List
|
|
2
|
+
|
|
3
|
+
## Code Quality Improvements
|
|
4
|
+
|
|
5
|
+
### High Priority
|
|
6
|
+
- [ ] Fix inconsistent component naming (`VolumesPanes.tsx` → `VolumesPane.tsx`)
|
|
7
|
+
- [ ] Add comprehensive error handling and error boundaries
|
|
8
|
+
- [ ] Implement proper null safety checks (especially for `activeContainer`)
|
|
9
|
+
- [ ] Add TypeScript interfaces for Docker API responses
|
|
10
|
+
- [ ] Standardize state management across all components
|
|
11
|
+
|
|
12
|
+
### Medium Priority
|
|
13
|
+
- [ ] Add loading states to Images and Volumes panes
|
|
14
|
+
- [ ] Implement proper cleanup for processes and event listeners
|
|
15
|
+
- [ ] Add prop type documentation to components
|
|
16
|
+
- [ ] Standardize error message formatting
|
|
17
|
+
|
|
18
|
+
## Performance & Architecture
|
|
19
|
+
|
|
20
|
+
### High Priority
|
|
21
|
+
- [ ] Create centralized Docker service layer (abstract CLI calls)
|
|
22
|
+
- [ ] Implement debouncing/throttling for Docker commands
|
|
23
|
+
- [ ] Add process cleanup to prevent memory leaks
|
|
24
|
+
- [ ] Implement data caching mechanism
|
|
25
|
+
|
|
26
|
+
### Medium Priority
|
|
27
|
+
- [ ] Separate data fetching logic from UI components
|
|
28
|
+
- [ ] Add configuration management (environment variables)
|
|
29
|
+
- [ ] Implement proper logging system
|
|
30
|
+
|
|
31
|
+
## Testing & Documentation
|
|
32
|
+
|
|
33
|
+
## Features & UX
|
|
34
|
+
|
|
35
|
+
### High Priority
|
|
36
|
+
- [ ] Implement filter logs input
|
|
37
|
+
- [ ] Add real-time updates with proper polling mechanism
|
|
38
|
+
- [ ] Create help screen with keyboard shortcuts
|
|
39
|
+
- [ ] Add visual feedback for user actions
|
|
40
|
+
|
|
41
|
+
### Medium Priority
|
|
42
|
+
- [ ] Implement container management actions (start/stop/remove)
|
|
43
|
+
- [ ] Add confirmation dialogs for destructive operations
|
|
44
|
+
- [ ] Create settings/configuration screen
|
|
45
|
+
- [ ] Add export/import functionality for logs
|
|
46
|
+
|
|
47
|
+
## Infrastructure
|
|
48
|
+
|
|
49
|
+
### Medium Priority
|
|
50
|
+
- [ ] Add Docker health checks and connection validation
|
|
51
|
+
- [ ] Implement proper error recovery mechanisms
|
|
52
|
+
- [ ] Add performance monitoring
|
|
53
|
+
- [ ] Create build/development optimization
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
|
|
3
|
+
services:
|
|
4
|
+
logger1:
|
|
5
|
+
image: alpine:latest
|
|
6
|
+
container_name: logger1
|
|
7
|
+
command: >
|
|
8
|
+
sh -c '
|
|
9
|
+
while true; do
|
|
10
|
+
echo "[$$(date +%Y-%m-%d\ %H:%M:%S)] Container1: $$(openssl rand -hex 8)"
|
|
11
|
+
sleep 1
|
|
12
|
+
done'
|
|
13
|
+
restart: unless-stopped
|
|
14
|
+
|
|
15
|
+
logger2:
|
|
16
|
+
image: alpine:latest
|
|
17
|
+
container_name: logger2
|
|
18
|
+
command: >
|
|
19
|
+
sh -c '
|
|
20
|
+
while true; do
|
|
21
|
+
echo "[$$(date +%Y-%m-%d\ %H:%M:%S)] Container2: $$(openssl rand -hex 8)"
|
|
22
|
+
sleep 1
|
|
23
|
+
done'
|
|
24
|
+
restart: unless-stopped
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opendocker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"module": "index.ts",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"bin": {
|
|
8
|
+
"opendocker": "./src/main.tsx"
|
|
9
|
+
},
|
|
10
|
+
"file": ["src/", "package.json", "README.md"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "bun run src/main.tsx",
|
|
13
|
+
"dev": "bun --watch run src/main.tsx"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/bun": "latest"
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"typescript": "^5"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@opentui/core": "^0.1.34",
|
|
23
|
+
"@opentui/react": "^0.1.34",
|
|
24
|
+
"zustand": "^5.0.8"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { colors } from "../utils/styling";
|
|
2
|
+
|
|
3
|
+
export const SplitBorder = {
|
|
4
|
+
border: ["left" as const],
|
|
5
|
+
borderColor: colors.border,
|
|
6
|
+
customBorderChars: {
|
|
7
|
+
topLeft: "",
|
|
8
|
+
bottomLeft: "",
|
|
9
|
+
vertical: "┃",
|
|
10
|
+
topRight: "",
|
|
11
|
+
bottomRight: "",
|
|
12
|
+
horizontal: "",
|
|
13
|
+
bottomT: "",
|
|
14
|
+
topT: "",
|
|
15
|
+
cross: "",
|
|
16
|
+
leftT: "",
|
|
17
|
+
rightT: "",
|
|
18
|
+
},
|
|
19
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import { useKeyboard } from "@opentui/react";
|
|
3
|
+
import { colors, termColors } from "../utils/styling";
|
|
4
|
+
import Pane from "./Pane";
|
|
5
|
+
import { useContainerStore } from "../stores/containers";
|
|
6
|
+
import { useApplicationStore } from "../stores/application";
|
|
7
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
8
|
+
import { Docker } from "../lib/docker";
|
|
9
|
+
import { TextAttributes } from "@opentui/core";
|
|
10
|
+
|
|
11
|
+
export default function ContainersPane() {
|
|
12
|
+
const { activePane, setActivePane } = useApplicationStore((state) => state);
|
|
13
|
+
const { containers, setContainers, activeContainer, setActiveContainer } = useContainerStore((state) => state);
|
|
14
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
15
|
+
const paneActive = activePane === "containers";
|
|
16
|
+
const scrollBoxRef = useRef<ScrollBoxRenderable>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!paneActive) return;
|
|
20
|
+
|
|
21
|
+
const docker = new Docker();
|
|
22
|
+
|
|
23
|
+
docker.watch((dockerContainers) => {
|
|
24
|
+
console.log(dockerContainers[0]);
|
|
25
|
+
const transformed = dockerContainers.map(container => ({
|
|
26
|
+
name: container.Names[0].replace("/", ""),
|
|
27
|
+
status: container.Status,
|
|
28
|
+
state: container.State,
|
|
29
|
+
health: container.Health,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
setContainers(transformed);
|
|
33
|
+
|
|
34
|
+
// Get the CURRENT active container from store, not from stale closure
|
|
35
|
+
const currentActive = useContainerStore.getState().activeContainer;
|
|
36
|
+
|
|
37
|
+
if (currentActive) {
|
|
38
|
+
const stillExists = transformed.find((c) => c.name === currentActive.name);
|
|
39
|
+
if (stillExists) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (transformed.length > 0) {
|
|
45
|
+
setActiveContainer(transformed[0]);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}, [paneActive]);
|
|
49
|
+
|
|
50
|
+
useKeyboard((key) => {
|
|
51
|
+
if (!paneActive) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (key.name === "left") {
|
|
56
|
+
setActivePane("volumes");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (key.name === "right" || key.name === "tab") {
|
|
60
|
+
setActivePane("images");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (key.name === 'j' || key.name === 'down') {
|
|
64
|
+
const index = Math.min(selectedIndex + 1, containers.length - 1);
|
|
65
|
+
setSelectedIndex(index);
|
|
66
|
+
setActiveContainer(containers[index]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (key.name === 'k' || key.name === 'up') {
|
|
70
|
+
const index = Math.max(selectedIndex - 1, 0);
|
|
71
|
+
setActiveContainer(containers[index]);
|
|
72
|
+
setSelectedIndex(index);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<Pane
|
|
78
|
+
title="Containers"
|
|
79
|
+
flexDirection="column"
|
|
80
|
+
width="100%"
|
|
81
|
+
active={paneActive}
|
|
82
|
+
>
|
|
83
|
+
<scrollbox
|
|
84
|
+
ref={scrollBoxRef}
|
|
85
|
+
scrollY={true}
|
|
86
|
+
stickyScroll={true}
|
|
87
|
+
stickyStart="bottom"
|
|
88
|
+
viewportOptions={{
|
|
89
|
+
flexGrow: 1
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
{containers.map((item, index) => {
|
|
93
|
+
function getStateColor() {
|
|
94
|
+
if (paneActive && activeContainer?.name === item.name) {
|
|
95
|
+
return colors.backgroundPanel;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (item?.state === "running") {
|
|
99
|
+
if (item.status.includes("starting")) {
|
|
100
|
+
return termColors.orange11;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (item.status.includes("unhealthy")) {
|
|
104
|
+
return termColors.red11;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return termColors.green11;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (item?.state === "exited") {
|
|
111
|
+
return termColors.red11;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return termColors.blue11;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<box
|
|
119
|
+
key={index}
|
|
120
|
+
backgroundColor={paneActive && activeContainer?.name === item.name ? colors.primary : undefined}
|
|
121
|
+
flexDirection="row"
|
|
122
|
+
justifyContent="space-between"
|
|
123
|
+
paddingLeft={1}
|
|
124
|
+
paddingRight={1}
|
|
125
|
+
>
|
|
126
|
+
<text
|
|
127
|
+
content={item.name}
|
|
128
|
+
fg={paneActive && activeContainer?.name === item.name ? colors.backgroundPanel : colors.textMuted}
|
|
129
|
+
attributes={paneActive && activeContainer?.name === item.name && TextAttributes.BOLD}
|
|
130
|
+
/>
|
|
131
|
+
<text
|
|
132
|
+
content={item.state}
|
|
133
|
+
fg={getStateColor()}
|
|
134
|
+
/>
|
|
135
|
+
</box>
|
|
136
|
+
)
|
|
137
|
+
})}
|
|
138
|
+
{containers.length < 1 && <text fg={colors.textMuted}>No Containers</text>}
|
|
139
|
+
</scrollbox>
|
|
140
|
+
</Pane>
|
|
141
|
+
)
|
|
142
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useKeyboard } from "@opentui/react";
|
|
3
|
+
import Pane from "./Pane";
|
|
4
|
+
import { colors } from "../utils/styling";
|
|
5
|
+
import { useImageStore } from "../stores/images";
|
|
6
|
+
import { useApplicationStore } from "../stores/application";
|
|
7
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
8
|
+
|
|
9
|
+
export default function ImagesPane() {
|
|
10
|
+
const { activePane, setActivePane } = useApplicationStore((state) => state);
|
|
11
|
+
const { images, setImages } = useImageStore();
|
|
12
|
+
const scrollBoxRef = useRef<ScrollBoxRenderable>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const process = Bun.spawn(
|
|
16
|
+
["docker", "images", "--format", "{{.Repository}}"],
|
|
17
|
+
{stdout: "pipe", stderr: "pipe"},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
async function read() {
|
|
21
|
+
try {
|
|
22
|
+
const reader = process.stdout.getReader();
|
|
23
|
+
|
|
24
|
+
while (true) {
|
|
25
|
+
const { done, value } = await reader.read();
|
|
26
|
+
if (done) break;
|
|
27
|
+
const decodedValues = new TextDecoder().decode(value);
|
|
28
|
+
const lines = decodedValues.split("\n").filter(Boolean);
|
|
29
|
+
setImages(lines);
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
read();
|
|
37
|
+
|
|
38
|
+
return () => process.kill();
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useKeyboard((key) => {
|
|
42
|
+
if (activePane !== "images") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (key.name === "left") {
|
|
47
|
+
setActivePane("containers");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (key.name === "right" || key.name === "tab") {
|
|
51
|
+
setActivePane("volumes");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Pane
|
|
57
|
+
title="Images"
|
|
58
|
+
width="100%"
|
|
59
|
+
active={activePane === "images"}
|
|
60
|
+
flexDirection="column"
|
|
61
|
+
>
|
|
62
|
+
<scrollbox
|
|
63
|
+
ref={scrollBoxRef}
|
|
64
|
+
scrollY={true}
|
|
65
|
+
stickyScroll={true}
|
|
66
|
+
stickyStart="top"
|
|
67
|
+
>
|
|
68
|
+
{images.map((item: string, index: number) => {
|
|
69
|
+
return (
|
|
70
|
+
<box key={index}>
|
|
71
|
+
<text
|
|
72
|
+
key={index}
|
|
73
|
+
content={item}
|
|
74
|
+
fg={colors.textMuted}
|
|
75
|
+
/>
|
|
76
|
+
</box>
|
|
77
|
+
)
|
|
78
|
+
})}
|
|
79
|
+
{images.length < 1 && <text fg={colors.textMuted}>No Images</text>}
|
|
80
|
+
</scrollbox>
|
|
81
|
+
</Pane>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useState, useEffect, useRef, useMemo } from "react";
|
|
2
|
+
import { colors, termColors } from "../utils/styling";
|
|
3
|
+
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core";
|
|
4
|
+
import Pane from "./Pane";
|
|
5
|
+
import { useContainerStore } from "../stores/containers";
|
|
6
|
+
import { useApplicationStore } from "../stores/application";
|
|
7
|
+
|
|
8
|
+
type LogEntry = {
|
|
9
|
+
text: string;
|
|
10
|
+
type: 'stdout' | 'stderr';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default function LogsPane() {
|
|
14
|
+
const { activePane } = useApplicationStore((state) => state);
|
|
15
|
+
const { activeContainer } = useContainerStore((state) => state);
|
|
16
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
17
|
+
const scrollBoxRef = useRef<ScrollBoxRenderable>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!activeContainer || activePane !== "containers") {
|
|
21
|
+
cleanup();
|
|
22
|
+
return;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
setLogs([]);
|
|
26
|
+
const process = Bun.spawn([
|
|
27
|
+
"docker",
|
|
28
|
+
"logs",
|
|
29
|
+
"--follow",
|
|
30
|
+
"--tail",
|
|
31
|
+
"100",
|
|
32
|
+
activeContainer.name,
|
|
33
|
+
], {
|
|
34
|
+
stdout: "pipe",
|
|
35
|
+
stderr: "pipe",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
async function readStream(stream: ReadableStream, type: 'stdout' | 'stderr') {
|
|
39
|
+
const decoder = new TextDecoder();
|
|
40
|
+
const reader = stream.getReader();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
while (true) {
|
|
44
|
+
const { done, value } = await reader.read();
|
|
45
|
+
if (done) break;
|
|
46
|
+
|
|
47
|
+
const text = decoder.decode(value, { stream: true });
|
|
48
|
+
// Split by newlines and filter out empty lines
|
|
49
|
+
const lines = text.split('\n').filter(line => line.trim().length > 0);
|
|
50
|
+
|
|
51
|
+
if (lines.length > 0) {
|
|
52
|
+
setLogs(prev => {
|
|
53
|
+
const newEntries = lines.map(line => ({
|
|
54
|
+
text: line,
|
|
55
|
+
type
|
|
56
|
+
}));
|
|
57
|
+
return [...prev, ...newEntries];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
readStream(process.stdout, 'stdout');
|
|
65
|
+
readStream(process.stderr, 'stderr');
|
|
66
|
+
|
|
67
|
+
return () => {
|
|
68
|
+
process.kill();
|
|
69
|
+
cleanup();
|
|
70
|
+
}
|
|
71
|
+
}, [activePane, activeContainer]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (scrollBoxRef.current && logs.length > 0) {
|
|
75
|
+
scrollBoxRef.current.scrollTo({ x: 0, y: scrollBoxRef.current.scrollHeight });
|
|
76
|
+
}
|
|
77
|
+
}, [logs]);
|
|
78
|
+
|
|
79
|
+
function cleanup() {
|
|
80
|
+
setLogs([]);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const output = useMemo(() => {
|
|
84
|
+
if (logs.length === 0) {
|
|
85
|
+
return <text fg={colors.textMuted}>No logs available</text>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return logs.map(log => (
|
|
89
|
+
<text
|
|
90
|
+
fg={log.type === 'stderr' ? termColors.red11 : colors.textMuted}
|
|
91
|
+
>
|
|
92
|
+
{log.text}
|
|
93
|
+
</text>
|
|
94
|
+
));
|
|
95
|
+
}, [logs]);
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<Pane
|
|
99
|
+
title="Logs"
|
|
100
|
+
flexDirection="column"
|
|
101
|
+
width="70%"
|
|
102
|
+
>
|
|
103
|
+
<box flexDirection="column" gap={1} height="100%">
|
|
104
|
+
<box flexDirection="row" gap={2} height="auto">
|
|
105
|
+
<box flexDirection="column" gap={1}>
|
|
106
|
+
<text fg={termColors.purple11} attributes={TextAttributes.BOLD}>Container</text>
|
|
107
|
+
<text>{activeContainer?.name}</text>
|
|
108
|
+
</box>
|
|
109
|
+
<box flexDirection="column">
|
|
110
|
+
<text fg={termColors.purple11} attributes={TextAttributes.BOLD}>Status</text>
|
|
111
|
+
<text>{activeContainer?.status || "None"}</text>
|
|
112
|
+
</box>
|
|
113
|
+
</box>
|
|
114
|
+
<scrollbox
|
|
115
|
+
ref={scrollBoxRef}
|
|
116
|
+
scrollY={true}
|
|
117
|
+
stickyScroll={true}
|
|
118
|
+
stickyStart="bottom"
|
|
119
|
+
height="100%"
|
|
120
|
+
>
|
|
121
|
+
<box flexDirection="column" height="auto">
|
|
122
|
+
{output}
|
|
123
|
+
</box>
|
|
124
|
+
</scrollbox>
|
|
125
|
+
</box>
|
|
126
|
+
</Pane>
|
|
127
|
+
)
|
|
128
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { SplitBorder } from "./Border";
|
|
2
|
+
import type { BoxProps } from "@opentui/react";
|
|
3
|
+
import { padding, colors, termColors } from "../utils/styling";
|
|
4
|
+
import { TextAttributes } from "@opentui/core";
|
|
5
|
+
|
|
6
|
+
interface PaneProps extends BoxProps {
|
|
7
|
+
title: string;
|
|
8
|
+
active?: boolean;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function Pane({
|
|
13
|
+
title,
|
|
14
|
+
active = false,
|
|
15
|
+
children,
|
|
16
|
+
...props
|
|
17
|
+
}: PaneProps) {
|
|
18
|
+
return (
|
|
19
|
+
<box
|
|
20
|
+
{...SplitBorder}
|
|
21
|
+
{...props}
|
|
22
|
+
borderColor={active ? termColors.purple11 : colors.backgroundPanel}
|
|
23
|
+
>
|
|
24
|
+
<box
|
|
25
|
+
flexGrow={1}
|
|
26
|
+
backgroundColor={colors.backgroundPanel}
|
|
27
|
+
{...padding}
|
|
28
|
+
>
|
|
29
|
+
<text marginBottom={1} attributes={TextAttributes.BOLD}>{title}</text>
|
|
30
|
+
{children}
|
|
31
|
+
</box>
|
|
32
|
+
</box>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { useKeyboard } from "@opentui/react";
|
|
3
|
+
import Pane from "./Pane";
|
|
4
|
+
import { colors } from "../utils/styling";
|
|
5
|
+
import { useVolumeStore } from "../stores/volumes";
|
|
6
|
+
import { useApplicationStore } from "../stores/application";
|
|
7
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
8
|
+
|
|
9
|
+
export default function ImagesPane() {
|
|
10
|
+
const { activePane, setActivePane } = useApplicationStore((state) => state);
|
|
11
|
+
const { volumes, setVolumes } = useVolumeStore();
|
|
12
|
+
const scrollBoxRef = useRef<ScrollBoxRenderable>(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const process = Bun.spawn(
|
|
16
|
+
["docker", "volume", "ls", "--format", "{{.Name}}"],
|
|
17
|
+
{stdout: "pipe", stderr: "pipe"},
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
async function read() {
|
|
21
|
+
try {
|
|
22
|
+
const reader = process.stdout.getReader();
|
|
23
|
+
|
|
24
|
+
while (true) {
|
|
25
|
+
const { done, value } = await reader.read();
|
|
26
|
+
if (done) break;
|
|
27
|
+
const decodedValues = new TextDecoder().decode(value);
|
|
28
|
+
const lines = decodedValues.split("\n").filter(Boolean);
|
|
29
|
+
setVolumes(lines);
|
|
30
|
+
}
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(error);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
read();
|
|
37
|
+
|
|
38
|
+
return () => process.kill();
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useKeyboard((key) => {
|
|
42
|
+
if (activePane !== "volumes") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (key.name === "left") {
|
|
47
|
+
setActivePane("images");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (key.name === "right" || key.name === "tab") {
|
|
51
|
+
setActivePane("containers");
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Pane
|
|
57
|
+
title="Volumes"
|
|
58
|
+
active={activePane === "volumes"}
|
|
59
|
+
width="100%"
|
|
60
|
+
flexDirection="column"
|
|
61
|
+
>
|
|
62
|
+
<scrollbox
|
|
63
|
+
ref={scrollBoxRef}
|
|
64
|
+
scrollY={true}
|
|
65
|
+
stickyScroll={true}
|
|
66
|
+
stickyStart="bottom"
|
|
67
|
+
viewportOptions={{
|
|
68
|
+
flexGrow: 1
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{volumes.map((item, index) => {
|
|
72
|
+
return <box>
|
|
73
|
+
<text
|
|
74
|
+
key={index}
|
|
75
|
+
content={`[*] ${item}`}
|
|
76
|
+
fg={colors.textMuted}
|
|
77
|
+
/>
|
|
78
|
+
</box>
|
|
79
|
+
})}
|
|
80
|
+
{volumes.length < 1 && <text fg={colors.textMuted}>No Volumes</text>}
|
|
81
|
+
</scrollbox>
|
|
82
|
+
</Pane>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
|
|
2
|
+
import { useTerminalDimensions } from "@opentui/react";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { colors, padding } from "../utils/styling";
|
|
5
|
+
import { TextAttributes } from "@opentui/core";
|
|
6
|
+
import { useApplicationStore } from "../stores/application";
|
|
7
|
+
import { SplitBorder } from "../components/Border";
|
|
8
|
+
|
|
9
|
+
export default function BaseLayout({ children }: { children: React.ReactNode }) {
|
|
10
|
+
const { activePane, setActivePane } = useApplicationStore((state) => state);
|
|
11
|
+
const dimensions = useTerminalDimensions();
|
|
12
|
+
const [pwd, setPwd] = useState<string>("");
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
Bun.$`pwd`.quiet().then(result => setPwd(result.text()));
|
|
16
|
+
setActivePane("containers");
|
|
17
|
+
|
|
18
|
+
}, [setActivePane]);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<box
|
|
22
|
+
width={dimensions.width}
|
|
23
|
+
height={dimensions.height}
|
|
24
|
+
backgroundColor={colors.background}
|
|
25
|
+
>
|
|
26
|
+
<box
|
|
27
|
+
flexDirection="row"
|
|
28
|
+
gap={1}
|
|
29
|
+
width="100%"
|
|
30
|
+
{...padding}
|
|
31
|
+
>
|
|
32
|
+
{children}
|
|
33
|
+
</box>
|
|
34
|
+
<box
|
|
35
|
+
height={1}
|
|
36
|
+
backgroundColor={colors.backgroundPanel}
|
|
37
|
+
flexDirection="row"
|
|
38
|
+
justifyContent="space-between"
|
|
39
|
+
flexShrink={0}
|
|
40
|
+
>
|
|
41
|
+
<box flexDirection="row">
|
|
42
|
+
<box flexDirection="row" backgroundColor={colors.backgroundElement} paddingLeft={1} paddingRight={1}>
|
|
43
|
+
<text fg={colors.textMuted}>open</text>
|
|
44
|
+
<text attributes={TextAttributes.BOLD}>docker </text>
|
|
45
|
+
<text fg={colors.textMuted}>alpha</text>
|
|
46
|
+
</box>
|
|
47
|
+
<box paddingLeft={1} paddingRight={1}>
|
|
48
|
+
<text fg={colors.textMuted}>~{pwd}</text>
|
|
49
|
+
</box>
|
|
50
|
+
</box>
|
|
51
|
+
<box flexDirection="row" gap={1}>
|
|
52
|
+
<text fg={colors.textMuted}>tab</text>
|
|
53
|
+
<box backgroundColor={colors.accent} paddingLeft={1} paddingRight={1} {...SplitBorder} borderColor={colors.backgroundPanel}>
|
|
54
|
+
<text fg={colors.backgroundPanel} attributes={TextAttributes.BOLD}>{activePane}</text>
|
|
55
|
+
</box>
|
|
56
|
+
</box>
|
|
57
|
+
</box>
|
|
58
|
+
</box>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
|
|
2
|
+
export class Docker {
|
|
3
|
+
private socket = "/var/run/docker.sock";
|
|
4
|
+
|
|
5
|
+
private async request(path: string) {
|
|
6
|
+
try {
|
|
7
|
+
const process = Bun.spawn([
|
|
8
|
+
"curl", "-s", "--unix-socket", this.socket,
|
|
9
|
+
`http://localhost${path}`
|
|
10
|
+
], { stdout: "pipe", stderr: "pipe" });
|
|
11
|
+
|
|
12
|
+
const output = await new Response(process.stdout).text();
|
|
13
|
+
process.kill();
|
|
14
|
+
|
|
15
|
+
return JSON.parse(output);
|
|
16
|
+
} catch (error) {
|
|
17
|
+
console.error('Docker socket request failed:', error);
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async getContainers() {
|
|
23
|
+
return this.request("/v1.41/containers/json?all=true");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async watch(callback: (containers: any[]) => void) {
|
|
27
|
+
try {
|
|
28
|
+
let containers = await this.getContainers();
|
|
29
|
+
callback(containers);
|
|
30
|
+
|
|
31
|
+
const process = Bun.spawn([
|
|
32
|
+
"curl", "-s", "--no-buffer", "--unix-socket", this.socket,
|
|
33
|
+
"http://localhost/v1.41/events"
|
|
34
|
+
], { stdout: "pipe", stderr: "pipe" });
|
|
35
|
+
|
|
36
|
+
const reader = process.stdout.getReader();
|
|
37
|
+
const decoder = new TextDecoder();
|
|
38
|
+
let buffer = "";
|
|
39
|
+
|
|
40
|
+
while (true) {
|
|
41
|
+
const { value, done } = await reader.read();
|
|
42
|
+
if (done) break;
|
|
43
|
+
|
|
44
|
+
buffer += decoder.decode(value, { stream: true });
|
|
45
|
+
const lines = buffer.split('\n');
|
|
46
|
+
buffer = lines.pop() || "";
|
|
47
|
+
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
if (line.trim()) {
|
|
50
|
+
try {
|
|
51
|
+
const event = JSON.parse(line);
|
|
52
|
+
if (event.Type === "container") {
|
|
53
|
+
containers = await this.getContainers();
|
|
54
|
+
callback(containers);
|
|
55
|
+
}
|
|
56
|
+
} catch (parseError) {
|
|
57
|
+
console.error('Failed to parse event:', line, parseError);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.error('Docker watch failed', error);
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { useRenderer, render, useKeyboard } from "@opentui/react";
|
|
4
|
+
import ContainersPane from "./components/ContainersPane";
|
|
5
|
+
import LogsPane from "./components/LogsPane";
|
|
6
|
+
import ImagesPane from "./components/ImagesPane";
|
|
7
|
+
import VolumesPane from "./components/VolumesPanes";
|
|
8
|
+
import BaseLayout from "./layouts/BaseLayout";
|
|
9
|
+
|
|
10
|
+
function App() {
|
|
11
|
+
const renderer = useRenderer();
|
|
12
|
+
|
|
13
|
+
useKeyboard((key) => {
|
|
14
|
+
if (key.name === "q") {
|
|
15
|
+
process.exit(0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (key.ctrl && key.name === "k") {
|
|
19
|
+
renderer?.console.toggle()
|
|
20
|
+
renderer?.toggleDebugOverlay()
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<BaseLayout>
|
|
26
|
+
<box
|
|
27
|
+
flexDirection="column"
|
|
28
|
+
width="30%"
|
|
29
|
+
gap={1}
|
|
30
|
+
>
|
|
31
|
+
<ContainersPane />
|
|
32
|
+
<ImagesPane />
|
|
33
|
+
<VolumesPane />
|
|
34
|
+
</box>
|
|
35
|
+
<LogsPane />
|
|
36
|
+
</BaseLayout>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
render (<App />);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
interface ApplicationStore {
|
|
4
|
+
activePane: string;
|
|
5
|
+
setActivePane: (activePane: string) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const useApplicationStore = create<ApplicationStore>((set) => ({
|
|
9
|
+
activePane: "containers",
|
|
10
|
+
setActivePane: (activePane) => set({ activePane }),
|
|
11
|
+
}));
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
export type Container = {
|
|
4
|
+
name: string;
|
|
5
|
+
status: string;
|
|
6
|
+
health: string;
|
|
7
|
+
state: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ContainerStore {
|
|
11
|
+
containers: Container[];
|
|
12
|
+
activeContainer: Container;
|
|
13
|
+
setContainers: (containers: Container[]) => void;
|
|
14
|
+
setActiveContainer: (activeContainer: Container) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useContainerStore = create<ContainerStore>((set) => ({
|
|
18
|
+
containers: [],
|
|
19
|
+
activeContainer: null,
|
|
20
|
+
setContainers: (containers: Container[]) => set({ containers }),
|
|
21
|
+
setActiveContainer: (activeContainer) => set({ activeContainer }),
|
|
22
|
+
}));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
export interface ImageStore {
|
|
4
|
+
images: string[];
|
|
5
|
+
setImages: (images: string[]) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const useImageStore = create<ImageStore>((set) => ({
|
|
9
|
+
images: [],
|
|
10
|
+
setImages: (images: string[]) => set({ images }),
|
|
11
|
+
}));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { create } from "zustand";
|
|
2
|
+
|
|
3
|
+
interface VolumeStore {
|
|
4
|
+
volumes: string[];
|
|
5
|
+
setVolumes: (volumes: string[]) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const useVolumeStore = create<VolumeStore>((set) => ({
|
|
9
|
+
volumes: [],
|
|
10
|
+
setVolumes: (volumes: string[]) => set({ volumes }),
|
|
11
|
+
}));
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
export const colors = {
|
|
2
|
+
primary: '#fab283',
|
|
3
|
+
secondary: '#5c9cf5',
|
|
4
|
+
accent: '#9d7cd8',
|
|
5
|
+
error: '#e06c75',
|
|
6
|
+
warning: '#f5a742',
|
|
7
|
+
success: '#7fd88f',
|
|
8
|
+
info: '#56b6c2',
|
|
9
|
+
text: '#eeeeee',
|
|
10
|
+
textMuted: '#808080',
|
|
11
|
+
background: '#0a0a0a',
|
|
12
|
+
backgroundPanel: '#141414',
|
|
13
|
+
backgroundElement: '#1e1e1e',
|
|
14
|
+
border: '#484848',
|
|
15
|
+
borderActive: '#606060',
|
|
16
|
+
borderSubtle: '#3c3c3c',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const padding = {
|
|
20
|
+
paddingTop: 1,
|
|
21
|
+
paddingBottom: 1,
|
|
22
|
+
paddingLeft: 2,
|
|
23
|
+
paddingRight: 2,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const termColors = {
|
|
27
|
+
gray01: "#111111",
|
|
28
|
+
gray02: "#191919",
|
|
29
|
+
gray03: "#222222",
|
|
30
|
+
gray04: "#2a2a2a",
|
|
31
|
+
gray05: "#313131",
|
|
32
|
+
gray06: "#3a3a3a",
|
|
33
|
+
gray07: "#484848",
|
|
34
|
+
gray08: "#606060",
|
|
35
|
+
gray09: "#6e6e6e",
|
|
36
|
+
gray10: "#7b7b7b",
|
|
37
|
+
gray11: "#b4b4b4",
|
|
38
|
+
gray12: "#eeeeee",
|
|
39
|
+
gray13: "#ffffff",
|
|
40
|
+
gray1: "#0b0b0b",
|
|
41
|
+
gray00: "#0d0d0d",
|
|
42
|
+
red01: "#191111",
|
|
43
|
+
red02: "#201314",
|
|
44
|
+
red03: "#3b1219",
|
|
45
|
+
red04: "#500f1c",
|
|
46
|
+
red05: "#611623",
|
|
47
|
+
red06: "#72232d",
|
|
48
|
+
red07: "#8c333a",
|
|
49
|
+
red08: "#b54548",
|
|
50
|
+
red09: "#e5484d",
|
|
51
|
+
red10: "#ec5d5e",
|
|
52
|
+
red11: "#ff9592",
|
|
53
|
+
red12: "#ffd1d9",
|
|
54
|
+
green01: "#0e1512",
|
|
55
|
+
green02: "#121b17",
|
|
56
|
+
green03: "#132d21",
|
|
57
|
+
green04: "#113b29",
|
|
58
|
+
green05: "#174933",
|
|
59
|
+
green06: "#20573e",
|
|
60
|
+
green07: "#28684a",
|
|
61
|
+
green08: "#2f7c57",
|
|
62
|
+
green09: "#30a46c",
|
|
63
|
+
green10: "#33b074",
|
|
64
|
+
green11: "#3dd68c",
|
|
65
|
+
green12: "#b1f1cb",
|
|
66
|
+
blue01: "#0d1520",
|
|
67
|
+
blue02: "#111927",
|
|
68
|
+
blue03: "#0d2847",
|
|
69
|
+
blue04: "#003362",
|
|
70
|
+
blue05: "#004074",
|
|
71
|
+
blue06: "#104d87",
|
|
72
|
+
blue07: "#205d9e",
|
|
73
|
+
blue08: "#2870bd",
|
|
74
|
+
blue09: "#0090ff",
|
|
75
|
+
blue10: "#3b9eff",
|
|
76
|
+
blue11: "#70b8ff",
|
|
77
|
+
blue12: "#c2e6ff",
|
|
78
|
+
orange01: "#17120e",
|
|
79
|
+
orange02: "#1e160f",
|
|
80
|
+
orange03: "#331e0b",
|
|
81
|
+
orange04: "#462100",
|
|
82
|
+
orange05: "#562800",
|
|
83
|
+
orange06: "#66350c",
|
|
84
|
+
orange07: "#7e451d",
|
|
85
|
+
orange08: "#a35829",
|
|
86
|
+
orange09: "#f76b15",
|
|
87
|
+
orange10: "#ff801f",
|
|
88
|
+
orange11: "#ffa057",
|
|
89
|
+
orange12: "#ffe0c2",
|
|
90
|
+
purple01: "#18111b",
|
|
91
|
+
purple02: "#1e1523",
|
|
92
|
+
purple03: "#301c3b",
|
|
93
|
+
purple04: "#3d224e",
|
|
94
|
+
purple05: "#48295c",
|
|
95
|
+
purple06: "#54346b",
|
|
96
|
+
purple07: "#664282",
|
|
97
|
+
purple08: "#8457aa",
|
|
98
|
+
purple09: "#8e4ec6",
|
|
99
|
+
purple10: "#9a5cd0",
|
|
100
|
+
purple11: "#d19dff",
|
|
101
|
+
purple12: "#ecd9fa",
|
|
102
|
+
teal01: "#0d1514",
|
|
103
|
+
teal02: "#111c1b",
|
|
104
|
+
teal03: "#0d2d2a",
|
|
105
|
+
teal04: "#023b37",
|
|
106
|
+
teal05: "#084843",
|
|
107
|
+
teal06: "#145750",
|
|
108
|
+
teal07: "#1c6961",
|
|
109
|
+
teal08: "#207e73",
|
|
110
|
+
teal09: "#12a594",
|
|
111
|
+
teal10: "#0eb39e",
|
|
112
|
+
teal11: "#0bd8b6",
|
|
113
|
+
teal12: "#adf0dd",
|
|
114
|
+
yellow01: "#14120b",
|
|
115
|
+
yellow02: "#1b180f",
|
|
116
|
+
yellow03: "#2d2305",
|
|
117
|
+
yellow04: "#362b00",
|
|
118
|
+
yellow05: "#433500",
|
|
119
|
+
yellow06: "#524202",
|
|
120
|
+
yellow07: "#665417",
|
|
121
|
+
yellow08: "#836a21",
|
|
122
|
+
yellow09: "#ffe629",
|
|
123
|
+
yellow10: "#ffff57",
|
|
124
|
+
yellow11: "#f5e147",
|
|
125
|
+
yellow12: "#f6eeb4",
|
|
126
|
+
pink01: "#191117",
|
|
127
|
+
pink02: "#21121d",
|
|
128
|
+
pink03: "#37172f",
|
|
129
|
+
pink04: "#4b143d",
|
|
130
|
+
pink05: "#591c47",
|
|
131
|
+
pink06: "#692955",
|
|
132
|
+
pink07: "#833869",
|
|
133
|
+
pink08: "#a84885",
|
|
134
|
+
pink09: "#d6409f",
|
|
135
|
+
pink10: "#de51a8",
|
|
136
|
+
pink11: "#ff8dcc",
|
|
137
|
+
pink12: "#fdd1ea",
|
|
138
|
+
brown01: "#12110f",
|
|
139
|
+
brown02: "#1c1816",
|
|
140
|
+
brown03: "#28211d",
|
|
141
|
+
brown04: "#322922",
|
|
142
|
+
brown05: "#3e3128",
|
|
143
|
+
brown06: "#4d3c2f",
|
|
144
|
+
brown07: "#614a39",
|
|
145
|
+
brown08: "#7c5f46",
|
|
146
|
+
brown09: "#ad7f58",
|
|
147
|
+
brown10: "#b88c67",
|
|
148
|
+
brown11: "#dbb594",
|
|
149
|
+
brown12: "#f2e1ca",
|
|
150
|
+
iris01: "#13131e",
|
|
151
|
+
iris02: "#171625",
|
|
152
|
+
iris03: "#202248",
|
|
153
|
+
iris04: "#262a65",
|
|
154
|
+
iris05: "#303374",
|
|
155
|
+
iris06: "#3d3e82",
|
|
156
|
+
iris07: "#4a4a95",
|
|
157
|
+
iris08: "#5958b1",
|
|
158
|
+
iris09: "#5b5bd6",
|
|
159
|
+
iris10: "#6e6ade",
|
|
160
|
+
iris11: "#b1a9ff",
|
|
161
|
+
iris12: "#e0dffe",
|
|
162
|
+
lime01: "#11130c",
|
|
163
|
+
lime02: "#151a10",
|
|
164
|
+
lime03: "#1f2917",
|
|
165
|
+
lime04: "#29371d",
|
|
166
|
+
lime05: "#334423",
|
|
167
|
+
lime06: "#3d522a",
|
|
168
|
+
lime07: "#496231",
|
|
169
|
+
lime08: "#577538",
|
|
170
|
+
lime09: "#bdee63",
|
|
171
|
+
lime10: "#d4ff70",
|
|
172
|
+
lime11: "#bde56c",
|
|
173
|
+
lime12: "#e3f7ba",
|
|
174
|
+
amber01: "#16120c",
|
|
175
|
+
amber02: "#1d180f",
|
|
176
|
+
amber03: "#302008",
|
|
177
|
+
amber04: "#3f2700",
|
|
178
|
+
amber05: "#4d3000",
|
|
179
|
+
amber06: "#5c3d05",
|
|
180
|
+
amber07: "#714f19",
|
|
181
|
+
amber08: "#8f6424",
|
|
182
|
+
amber09: "#ffc53d",
|
|
183
|
+
amber10: "#ffd60a",
|
|
184
|
+
amber11: "#ffca16",
|
|
185
|
+
amber12: "#ffe7b3",
|
|
186
|
+
tomato01: "#181111",
|
|
187
|
+
tomato02: "#1f1513",
|
|
188
|
+
tomato03: "#391714",
|
|
189
|
+
tomato04: "#4e1511",
|
|
190
|
+
tomato05: "#5e1c16",
|
|
191
|
+
tomato06: "#6e2920",
|
|
192
|
+
tomato07: "#853a2d",
|
|
193
|
+
tomato08: "#ac4d39",
|
|
194
|
+
tomato09: "#e54d2e",
|
|
195
|
+
tomato10: "#ec6142",
|
|
196
|
+
tomato11: "#ff977d",
|
|
197
|
+
tomato12: "#fbd3cb",
|
|
198
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
|
3
|
+
"extends": "@tsconfig/bun/tsconfig.json",
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"jsx": "preserve",
|
|
6
|
+
"jsxImportSource": "@opentui/react",
|
|
7
|
+
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
|
8
|
+
"customConditions": ["browser"],
|
|
9
|
+
"baseUrl": ".",
|
|
10
|
+
"paths": {
|
|
11
|
+
"@/*": ["./src/*"],
|
|
12
|
+
"@tui/*": ["./src/cli/cmd/tui/*"]
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|