@doppelgangerdev/doppelganger 0.2.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.
Files changed (63) hide show
  1. package/.dockerignore +9 -0
  2. package/.github/workflows/docker-publish.yml +59 -0
  3. package/CODE_OF_CONDUCT.md +28 -0
  4. package/CONTRIBUTING.md +42 -0
  5. package/Dockerfile +44 -0
  6. package/LICENSE +163 -0
  7. package/README.md +133 -0
  8. package/TERMS.md +16 -0
  9. package/THIRD_PARTY_LICENSES.md +3502 -0
  10. package/agent.js +1240 -0
  11. package/headful.js +171 -0
  12. package/index.html +21 -0
  13. package/n8n-nodes-doppelganger/LICENSE +201 -0
  14. package/n8n-nodes-doppelganger/README.md +42 -0
  15. package/n8n-nodes-doppelganger/package-lock.json +6128 -0
  16. package/n8n-nodes-doppelganger/package.json +36 -0
  17. package/n8n-nodes-doppelganger/src/credentials/DoppelgangerApi.credentials.ts +35 -0
  18. package/n8n-nodes-doppelganger/src/index.ts +4 -0
  19. package/n8n-nodes-doppelganger/src/nodes/Doppelganger/Doppelganger.node.ts +147 -0
  20. package/n8n-nodes-doppelganger/src/nodes/Doppelganger/icon.png +0 -0
  21. package/n8n-nodes-doppelganger/tsconfig.json +14 -0
  22. package/package.json +45 -0
  23. package/postcss.config.js +6 -0
  24. package/public/icon.png +0 -0
  25. package/public/novnc.html +151 -0
  26. package/public/styles.css +86 -0
  27. package/scrape.js +389 -0
  28. package/server.js +875 -0
  29. package/src/App.tsx +722 -0
  30. package/src/components/AuthScreen.tsx +95 -0
  31. package/src/components/CodeEditor.tsx +70 -0
  32. package/src/components/DashboardScreen.tsx +133 -0
  33. package/src/components/EditorScreen.tsx +1519 -0
  34. package/src/components/ExecutionDetailScreen.tsx +115 -0
  35. package/src/components/ExecutionsScreen.tsx +156 -0
  36. package/src/components/LoadingScreen.tsx +26 -0
  37. package/src/components/NotFoundScreen.tsx +34 -0
  38. package/src/components/RichInput.tsx +68 -0
  39. package/src/components/SettingsScreen.tsx +228 -0
  40. package/src/components/Sidebar.tsx +61 -0
  41. package/src/components/app/CenterAlert.tsx +44 -0
  42. package/src/components/app/CenterConfirm.tsx +33 -0
  43. package/src/components/app/EditorLoader.tsx +89 -0
  44. package/src/components/editor/ActionPalette.tsx +79 -0
  45. package/src/components/editor/JsonEditorPane.tsx +71 -0
  46. package/src/components/editor/ResultsPane.tsx +641 -0
  47. package/src/components/editor/actionCatalog.ts +23 -0
  48. package/src/components/settings/AgentAiPanel.tsx +105 -0
  49. package/src/components/settings/ApiKeyPanel.tsx +68 -0
  50. package/src/components/settings/CookiesPanel.tsx +154 -0
  51. package/src/components/settings/LayoutPanel.tsx +46 -0
  52. package/src/components/settings/ScreenshotsPanel.tsx +64 -0
  53. package/src/components/settings/SettingsHeader.tsx +28 -0
  54. package/src/components/settings/StoragePanel.tsx +35 -0
  55. package/src/index.css +287 -0
  56. package/src/main.tsx +13 -0
  57. package/src/types.ts +114 -0
  58. package/src/utils/syntaxHighlight.ts +140 -0
  59. package/start-vnc.sh +52 -0
  60. package/tailwind.config.js +22 -0
  61. package/tsconfig.json +39 -0
  62. package/tsconfig.node.json +12 -0
  63. package/vite.config.mts +27 -0
package/src/index.css ADDED
@@ -0,0 +1,287 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ color-scheme: dark;
7
+ }
8
+
9
+ body {
10
+ margin: 0;
11
+ display: flex;
12
+ min-width: 320px;
13
+ min-height: 100vh;
14
+ background-color: #020202;
15
+ }
16
+
17
+ #root {
18
+ width: 100%;
19
+ }
20
+
21
+ .glass {
22
+ background: rgba(10, 10, 10, 0.9);
23
+ backdrop-filter: blur(64px);
24
+ -webkit-backdrop-filter: blur(64px);
25
+ }
26
+
27
+ .glass-card {
28
+ background: rgba(255, 255, 255, 0.015);
29
+ backdrop-filter: blur(16px);
30
+ -webkit-backdrop-filter: blur(16px);
31
+ border: 1px solid rgba(255, 255, 255, 0.08);
32
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
33
+ }
34
+
35
+ .glass-card:hover {
36
+ background: rgba(255, 255, 255, 0.04);
37
+ border-color: rgba(255, 255, 255, 0.15);
38
+ }
39
+
40
+ .shine-effect {
41
+ position: relative;
42
+ overflow: hidden;
43
+ }
44
+
45
+ .shine-effect::after {
46
+ content: '';
47
+ position: absolute;
48
+ top: -50%;
49
+ left: -100%;
50
+ width: 33.333333%;
51
+ height: 200%;
52
+ background: rgba(255, 255, 255, 0.2);
53
+ transform: rotate(25deg);
54
+ pointer-events: none;
55
+ transition: none;
56
+ }
57
+
58
+ .shine-effect:hover::after {
59
+ left: 150%;
60
+ transition: left 0.7s ease-in-out;
61
+ }
62
+
63
+ .custom-scrollbar::-webkit-scrollbar {
64
+ width: 4px;
65
+ height: 4px;
66
+ }
67
+
68
+ .custom-scrollbar::-webkit-scrollbar-track {
69
+ background: transparent;
70
+ }
71
+
72
+ .custom-scrollbar::-webkit-scrollbar-thumb {
73
+ background: rgba(255, 255, 255, 0.1);
74
+ border-radius: 9999px;
75
+ }
76
+
77
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
78
+ background: rgba(255, 255, 255, 0.2);
79
+ }
80
+
81
+ .custom-select,
82
+ select {
83
+ appearance: none !important;
84
+ background-color: #000000 !important;
85
+ color: #ffffff !important;
86
+ border: 1px solid rgba(255, 255, 255, 0.1) !important;
87
+ border-radius: 12px !important;
88
+ padding: 0.4rem 2rem 0.4rem 0.75rem !important;
89
+ cursor: pointer !important;
90
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='white' stroke-width='3'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E") !important;
91
+ background-repeat: no-repeat !important;
92
+ background-position: right 0.6rem center !important;
93
+ background-size: 10px !important;
94
+ transition: all 0.2s ease !important;
95
+ }
96
+
97
+ .custom-select:hover,
98
+ select:hover {
99
+ border-color: rgba(255, 255, 255, 0.3);
100
+ }
101
+
102
+ .custom-select option,
103
+ select option {
104
+ background: #000000;
105
+ color: #ffffff;
106
+ padding: 10px;
107
+ }
108
+
109
+ .action-type-select {
110
+ min-width: 96px !important;
111
+ box-sizing: border-box !important;
112
+ border-radius: 10px !important;
113
+ border-color: rgba(255, 255, 255, 0.15) !important;
114
+ background-color: rgba(0, 0, 0, 0.6) !important;
115
+ padding: 0.3rem 2.2rem 0.3rem 0.65rem !important;
116
+ position: relative !important;
117
+ z-index: 9999 !important;
118
+ background-clip: padding-box !important;
119
+ }
120
+
121
+ .action-type-select:focus {
122
+ border-color: rgba(255, 255, 255, 0.35) !important;
123
+ }
124
+
125
+ .action-card-item.dragging {
126
+ opacity: 0.5;
127
+ }
128
+
129
+ .action-card-item.drag-over-top {
130
+ border-top: 2px solid rgba(59, 130, 246, 0.5);
131
+ }
132
+
133
+ .action-card-item.drag-over-bottom {
134
+ border-bottom: 2px solid rgba(59, 130, 246, 0.5);
135
+ }
136
+
137
+ /* Variable syntax highlighting */
138
+ .var-highlight {
139
+ background: rgba(59, 130, 246, 0.2);
140
+ color: #60a5fa;
141
+ font-family: 'JetBrains Mono', monospace;
142
+ padding: 0 2px;
143
+ border-radius: 3px;
144
+ font-weight: 500;
145
+ }
146
+
147
+ .var-highlight-undefined {
148
+ background: rgba(239, 68, 68, 0.2);
149
+ color: #f87171;
150
+ font-family: 'JetBrains Mono', monospace;
151
+ padding: 0 2px;
152
+ border-radius: 3px;
153
+ font-weight: 500;
154
+ }
155
+
156
+ .var-highlight-default {
157
+ background: rgba(34, 197, 94, 0.2);
158
+ color: #4ade80;
159
+ font-family: 'JetBrains Mono', monospace;
160
+ padding: 0 2px;
161
+ border-radius: 3px;
162
+ font-weight: 500;
163
+ }
164
+
165
+ .rich-input-content {
166
+ white-space: pre-wrap;
167
+ word-wrap: break-word;
168
+ }
169
+
170
+ .rich-input-content:empty:before {
171
+ content: attr(data-placeholder);
172
+ color: rgba(107, 114, 128, 0.6);
173
+ pointer-events: none;
174
+ }
175
+
176
+ .rich-input-content:focus {
177
+ outline: none;
178
+ }
179
+
180
+ .code-editor {
181
+ position: relative;
182
+ width: 100%;
183
+ min-height: 120px;
184
+ background: #050505;
185
+ border: 1px solid rgba(255, 255, 255, 0.1);
186
+ border-radius: 14px;
187
+ overflow: hidden;
188
+ }
189
+
190
+ .code-editor-pre {
191
+ position: absolute;
192
+ inset: 0;
193
+ margin: 0;
194
+ padding: 1rem;
195
+ font-family: 'JetBrains Mono', monospace;
196
+ font-size: 11px;
197
+ line-height: 1.6;
198
+ font-variant-ligatures: none;
199
+ tab-size: 4;
200
+ color: rgba(147, 197, 253, 0.7);
201
+ white-space: pre;
202
+ overflow: hidden;
203
+ pointer-events: none;
204
+ }
205
+
206
+ .code-editor-textarea {
207
+ position: absolute;
208
+ inset: 0;
209
+ width: 100%;
210
+ height: 100%;
211
+ resize: none;
212
+ background: transparent;
213
+ color: transparent;
214
+ caret-color: #ffffff;
215
+ border: none;
216
+ padding: 1rem;
217
+ font-family: 'JetBrains Mono', monospace;
218
+ font-size: 11px;
219
+ line-height: 1.6;
220
+ font-variant-ligatures: none;
221
+ tab-size: 4;
222
+ outline: none;
223
+ overflow-x: auto;
224
+ overflow-y: auto;
225
+ scrollbar-width: none;
226
+ -ms-overflow-style: none;
227
+ }
228
+
229
+ .code-editor-textarea::-webkit-scrollbar {
230
+ display: none;
231
+ }
232
+
233
+ .code-editor-textarea-readonly {
234
+ pointer-events: none;
235
+ }
236
+
237
+ .code-editor-placeholder {
238
+ color: rgba(107, 114, 128, 0.6);
239
+ }
240
+
241
+ .code-token-keyword {
242
+ color: #93c5fd;
243
+ }
244
+
245
+ .code-token-string {
246
+ color: #86efac;
247
+ }
248
+
249
+ .code-token-number {
250
+ color: #fbbf24;
251
+ }
252
+
253
+ .code-token-comment {
254
+ color: #6b7280;
255
+ font-style: italic;
256
+ }
257
+
258
+ .code-token-boolean,
259
+ .code-token-null {
260
+ color: #fca5a5;
261
+ }
262
+
263
+ .code-token-identifier {
264
+ color: #e5e7eb;
265
+ }
266
+
267
+ .code-token-key {
268
+ color: #67e8f9;
269
+ }
270
+
271
+ .code-token-tag {
272
+ color: #60a5fa;
273
+ }
274
+
275
+ .code-token-attr {
276
+ color: #fcd34d;
277
+ }
278
+
279
+ .code-token-punct {
280
+ color: rgba(255, 255, 255, 0.6);
281
+ }
282
+
283
+ .text-vertical {
284
+ writing-mode: vertical-rl;
285
+ text-orientation: mixed;
286
+ transform: rotate(180deg);
287
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,13 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import { BrowserRouter } from 'react-router-dom'
4
+ import App from './App.tsx'
5
+ import './index.css'
6
+
7
+ ReactDOM.createRoot(document.getElementById('root')!).render(
8
+ <React.StrictMode>
9
+ <BrowserRouter>
10
+ <App />
11
+ </BrowserRouter>
12
+ </React.StrictMode>,
13
+ )
package/src/types.ts ADDED
@@ -0,0 +1,114 @@
1
+ export type TaskMode = 'scrape' | 'agent' | 'headful';
2
+ export type ViewMode = 'visual' | 'json' | 'api' | 'history';
3
+ export type VarType = 'string' | 'number' | 'boolean';
4
+
5
+ export interface Variable {
6
+ type: VarType;
7
+ value: any;
8
+ }
9
+
10
+ export interface StealthConfig {
11
+ allowTypos: boolean;
12
+ idleMovements: boolean;
13
+ overscroll: boolean;
14
+ deadClicks: boolean;
15
+ fatigue: boolean;
16
+ naturalTyping: boolean;
17
+ }
18
+
19
+ export interface Action {
20
+ id: string;
21
+ type:
22
+ | 'click'
23
+ | 'type'
24
+ | 'wait'
25
+ | 'press'
26
+ | 'scroll'
27
+ | 'javascript'
28
+ | 'csv'
29
+ | 'hover'
30
+ | 'merge'
31
+ | 'if'
32
+ | 'else'
33
+ | 'end'
34
+ | 'while'
35
+ | 'repeat'
36
+ | 'foreach'
37
+ | 'stop'
38
+ | 'set'
39
+ | 'on_error'
40
+ | 'start';
41
+ selector?: string;
42
+ value?: string;
43
+ key?: string;
44
+ disabled?: boolean;
45
+ varName?: string;
46
+ conditionVar?: string;
47
+ conditionVarType?: VarType;
48
+ conditionOp?: string;
49
+ conditionValue?: string;
50
+ }
51
+
52
+ export interface Task {
53
+ id?: string;
54
+ name: string;
55
+ url: string;
56
+ mode: TaskMode;
57
+ wait: number;
58
+ selector?: string;
59
+ rotateUserAgents: boolean;
60
+ humanTyping: boolean;
61
+ stealth: StealthConfig;
62
+ actions: Action[];
63
+ variables: Record<string, Variable>;
64
+ last_opened?: number;
65
+ extractionScript?: string;
66
+ extractionFormat?: 'json' | 'csv';
67
+ includeShadowDom?: boolean;
68
+ versions?: TaskVersion[];
69
+ }
70
+
71
+ export interface TaskVersion {
72
+ id: string;
73
+ timestamp: number;
74
+ snapshot: Task;
75
+ }
76
+
77
+ export interface Results {
78
+ url: string;
79
+ finalUrl?: string;
80
+ html?: string;
81
+ data?: any;
82
+ screenshotUrl?: string;
83
+ logs: string[];
84
+ timestamp: string;
85
+ }
86
+
87
+ export interface Execution {
88
+ id: string;
89
+ timestamp: number;
90
+ method: string;
91
+ path: string;
92
+ status: number;
93
+ durationMs: number;
94
+ source: string;
95
+ mode: string;
96
+ taskId?: string | null;
97
+ taskName?: string | null;
98
+ url?: string | null;
99
+ taskSnapshot?: Task | null;
100
+ result?: any;
101
+ }
102
+
103
+ export interface User {
104
+ id: number;
105
+ name: string;
106
+ email: string;
107
+ }
108
+
109
+ export interface ConfirmRequest {
110
+ message: string;
111
+ confirmLabel?: string;
112
+ cancelLabel?: string;
113
+ title?: string;
114
+ }
@@ -0,0 +1,140 @@
1
+ export type SyntaxLanguage = 'plain' | 'javascript' | 'json' | 'html';
2
+
3
+ const escapeHtml = (text: string) => {
4
+ return text
5
+ .replace(/&/g, '&amp;')
6
+ .replace(/</g, '&lt;')
7
+ .replace(/>/g, '&gt;')
8
+ .replace(/"/g, '&quot;')
9
+ .replace(/'/g, '&#39;');
10
+ };
11
+
12
+ const jsKeywords = new Set([
13
+ 'const', 'let', 'var', 'function', 'return', 'if', 'else', 'for', 'while', 'do',
14
+ 'switch', 'case', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'new',
15
+ 'class', 'extends', 'super', 'import', 'from', 'export', 'default', 'await', 'async',
16
+ 'typeof', 'instanceof', 'in', 'of', 'this'
17
+ ]);
18
+
19
+ const highlightVariables = (text: string, variables?: Record<string, any>) => {
20
+ const regex = /\{\$(\w+)\}/g;
21
+ let html = '';
22
+ let lastIndex = 0;
23
+ let match;
24
+
25
+ const varExists = (name: string) => !!variables && ((name in variables) || name === 'now');
26
+ const hasValue = (name: string) => {
27
+ if (name === 'now') return true;
28
+ const v = variables ? variables[name] : undefined;
29
+ return v && (v.value !== '' && v.value !== undefined && v.value !== null);
30
+ };
31
+
32
+ while ((match = regex.exec(text)) !== null) {
33
+ if (match.index > lastIndex) {
34
+ html += escapeHtml(text.substring(lastIndex, match.index));
35
+ }
36
+ const varName = match[1];
37
+ const exists = varExists(varName);
38
+ const isSet = hasValue(varName);
39
+
40
+ const className = exists
41
+ ? (isSet ? 'var-highlight-default' : 'var-highlight')
42
+ : 'var-highlight-undefined';
43
+
44
+ html += `<span class="${className}">${escapeHtml(match[0])}</span>`;
45
+ lastIndex = regex.lastIndex;
46
+ }
47
+ html += escapeHtml(text.substring(lastIndex));
48
+ return html;
49
+ };
50
+
51
+ const highlightJson = (text: string) => {
52
+ const tokenRegex = /("(?:\\.|[^"\\])*")|(-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|\b(true|false|null)\b/g;
53
+ let html = '';
54
+ let lastIndex = 0;
55
+ let match;
56
+
57
+ while ((match = tokenRegex.exec(text)) !== null) {
58
+ if (match.index > lastIndex) {
59
+ html += escapeHtml(text.substring(lastIndex, match.index));
60
+ }
61
+ const token = match[0];
62
+ if (match[1]) {
63
+ let isKey = false;
64
+ let j = match.index + token.length;
65
+ while (j < text.length && /\s/.test(text[j])) j += 1;
66
+ if (text[j] === ':') isKey = true;
67
+ const cls = isKey ? 'code-token-key' : 'code-token-string';
68
+ html += `<span class="${cls}">${escapeHtml(token)}</span>`;
69
+ } else if (match[2]) {
70
+ html += `<span class="code-token-number">${escapeHtml(token)}</span>`;
71
+ } else {
72
+ html += `<span class="code-token-boolean">${escapeHtml(token)}</span>`;
73
+ }
74
+ lastIndex = tokenRegex.lastIndex;
75
+ }
76
+ html += escapeHtml(text.substring(lastIndex));
77
+ return html;
78
+ };
79
+
80
+ const highlightJavascript = (text: string, variables?: Record<string, any>) => {
81
+ const tokenRegex = /(\{\$\w+\})|(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|(\btrue\b|\bfalse\b|\bnull\b|\bundefined\b)|(\b[A-Za-z_]\w*\b)/g;
82
+ let html = '';
83
+ let lastIndex = 0;
84
+ let match;
85
+
86
+ while ((match = tokenRegex.exec(text)) !== null) {
87
+ if (match.index > lastIndex) {
88
+ html += escapeHtml(text.substring(lastIndex, match.index));
89
+ }
90
+ const token = match[0];
91
+ if (match[1]) {
92
+ html += highlightVariables(token, variables);
93
+ } else if (match[2]) {
94
+ html += `<span class="code-token-comment">${escapeHtml(token)}</span>`;
95
+ } else if (match[3]) {
96
+ html += `<span class="code-token-string">${escapeHtml(token)}</span>`;
97
+ } else if (match[4]) {
98
+ html += `<span class="code-token-number">${escapeHtml(token)}</span>`;
99
+ } else if (match[5]) {
100
+ html += `<span class="code-token-boolean">${escapeHtml(token)}</span>`;
101
+ } else if (match[6]) {
102
+ const cls = jsKeywords.has(token) ? 'code-token-keyword' : 'code-token-identifier';
103
+ html += `<span class="${cls}">${escapeHtml(token)}</span>`;
104
+ } else {
105
+ html += escapeHtml(token);
106
+ }
107
+ lastIndex = tokenRegex.lastIndex;
108
+ }
109
+ html += escapeHtml(text.substring(lastIndex));
110
+ return html;
111
+ };
112
+
113
+ const highlightHtml = (text: string) => {
114
+ const tagRegex = /<\/?[^>]+>/g;
115
+ let html = '';
116
+ let lastIndex = 0;
117
+ let match;
118
+
119
+ while ((match = tagRegex.exec(text)) !== null) {
120
+ if (match.index > lastIndex) {
121
+ html += escapeHtml(text.substring(lastIndex, match.index));
122
+ }
123
+ const rawTag = match[0];
124
+ let escaped = escapeHtml(rawTag);
125
+ escaped = escaped.replace(/^(&lt;\/?)([A-Za-z0-9-]+)/, '$1<span class="code-token-tag">$2</span>');
126
+ escaped = escaped.replace(/(\s)([A-Za-z0-9-:]+)(=)/g, '$1<span class="code-token-attr">$2</span>$3');
127
+ escaped = escaped.replace(/(&quot;.*?&quot;|&#39;.*?&#39;)/g, '<span class="code-token-string">$1</span>');
128
+ html += `<span class="code-token-punct">${escaped}</span>`;
129
+ lastIndex = tagRegex.lastIndex;
130
+ }
131
+ html += escapeHtml(text.substring(lastIndex));
132
+ return html;
133
+ };
134
+
135
+ export const highlightCode = (text: string, language: SyntaxLanguage, variables?: Record<string, any>) => {
136
+ if (language === 'javascript') return highlightJavascript(text, variables);
137
+ if (language === 'json') return highlightJson(text);
138
+ if (language === 'html') return highlightHtml(text);
139
+ return highlightVariables(text, variables);
140
+ };
package/start-vnc.sh ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ export DISPLAY="${DISPLAY:-:99}"
5
+
6
+ mkdir -p /app/data
7
+
8
+ echo "[vnc] Starting Xvfb on $DISPLAY"
9
+ Xvfb "$DISPLAY" -screen 0 1280x720x24 -nolisten tcp -ac &
10
+
11
+ echo "[vnc] Starting x11vnc on :5900"
12
+ X11VNC_OPTS="-display $DISPLAY -forever -shared -nopw -rfbport 5900 -wait 5"
13
+ x11vnc $X11VNC_OPTS >> /app/data/x11vnc.log 2>&1 &
14
+
15
+ NOVNC_DIR="/opt/novnc"
16
+ if [ ! -d "$NOVNC_DIR" ]; then
17
+ echo "[vnc] noVNC not found, downloading..."
18
+ mkdir -p /opt/novnc
19
+ if curl -fsSL https://github.com/novnc/noVNC/archive/refs/tags/v1.4.0.tar.gz \
20
+ | tar -xz --strip-components=1 -C /opt/novnc; then
21
+ NOVNC_DIR="/opt/novnc"
22
+ elif [ -d "/usr/share/novnc" ]; then
23
+ NOVNC_DIR="/usr/share/novnc"
24
+ fi
25
+ fi
26
+
27
+ echo "[vnc] Serving noVNC on 0.0.0.0:54311"
28
+ pkill -f websockify >/dev/null 2>&1 || true
29
+ pkill -f novnc_proxy >/dev/null 2>&1 || true
30
+ NOVNC_PROXY="$NOVNC_DIR/utils/novnc_proxy"
31
+ if [ -x "$NOVNC_PROXY" ]; then
32
+ "$NOVNC_PROXY" --web "$NOVNC_DIR" --listen 54311 --vnc localhost:5900 --heartbeat 30 --idle-timeout 0 >> /app/data/novnc.log 2>&1 &
33
+ elif command -v websockify >/dev/null 2>&1; then
34
+ for _ in {1..50}; do
35
+ if bash -c "echo > /dev/tcp/127.0.0.1/5900" >/dev/null 2>&1; then
36
+ break
37
+ fi
38
+ sleep 0.1
39
+ done
40
+
41
+ (
42
+ while true; do
43
+ websockify --web "$NOVNC_DIR" 0.0.0.0:54311 localhost:5900 >> /app/data/novnc.log 2>&1
44
+ sleep 1
45
+ done
46
+ ) &
47
+ else
48
+ echo "[vnc] websockify not found" >> /app/data/novnc.log
49
+ fi
50
+
51
+ echo "[vnc] Starting server"
52
+ exec node /app/server.js
@@ -0,0 +1,22 @@
1
+ /** @type {import('tailwindcss').Config} */
2
+ export default {
3
+ content: [
4
+ "./index.html",
5
+ "./src/**/*.{js,ts,jsx,tsx}",
6
+ ],
7
+ theme: {
8
+ extend: {
9
+ colors: {
10
+ bg: '#020202',
11
+ panel: 'rgba(10, 10, 10, 0.85)',
12
+ border: 'rgba(255, 255, 255, 0.08)',
13
+ accent: '#ffffff',
14
+ },
15
+ fontFamily: {
16
+ sans: ['Inter', 'sans-serif'],
17
+ mono: ['JetBrains Mono', 'monospace'],
18
+ },
19
+ },
20
+ },
21
+ plugins: [],
22
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": [
6
+ "ES2020",
7
+ "DOM",
8
+ "DOM.Iterable"
9
+ ],
10
+ "module": "ESNext",
11
+ "skipLibCheck": true,
12
+ /* Bundler mode */
13
+ "moduleResolution": "bundler",
14
+ "allowImportingTsExtensions": true,
15
+ "resolveJsonModule": true,
16
+ "isolatedModules": true,
17
+ "noEmit": true,
18
+ "jsx": "react-jsx",
19
+ /* Linting */
20
+ "strict": true,
21
+ "noUnusedLocals": true,
22
+ "noUnusedParameters": true,
23
+ "noFallthroughCasesInSwitch": true,
24
+ "baseUrl": ".",
25
+ "paths": {
26
+ "@/*": [
27
+ "./src/*"
28
+ ]
29
+ }
30
+ },
31
+ "include": [
32
+ "src"
33
+ ],
34
+ "references": [
35
+ {
36
+ "path": "./tsconfig.node.json"
37
+ }
38
+ ]
39
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": [
10
+ "vite.config.mts"
11
+ ]
12
+ }