klio 1.4.9 → 1.5.1

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.
Binary file
@@ -0,0 +1,3 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
@@ -0,0 +1 @@
1
+ *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.static{position:static}.fixed{position:fixed}.bottom-0{bottom:0}.right-0{right:0}.top-0{top:0}.top-1\/2{top:50%}.m-0{margin:0}.m-4{margin:1rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-auto{margin-top:auto}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.contents{display:contents}.hidden{display:none}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-screen{height:100vh}.min-h-0{min-height:0}.w-2\.5{width:.625rem}.w-3{width:.75rem}.w-5{width:1.25rem}.w-\[85vw\]{width:85vw}.w-full{width:100%}.min-w-0{min-width:0}.flex-1{flex:1 1 0%}.-translate-x-\[85vw\]{--tw-translate-x:-85vw}.-translate-x-\[85vw\],.-translate-y-1\/2{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.-translate-y-1\/2{--tw-translate-y:-50%}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-full{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-x-full{--tw-translate-x:100%}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-between{justify-content:space-between}.gap-2{gap:.5rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.whitespace-pre-wrap{white-space:pre-wrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-l-md{border-top-left-radius:.375rem;border-bottom-left-radius:.375rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border-0{border-width:0}.border-l-2{border-left-width:2px}.border-t{border-top-width:1px}.border-\[\#4dabf7\]{--tw-border-opacity:1;border-color:rgb(77 171 247/var(--tw-border-opacity,1))}.border-gray-700{--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity,1))}.bg-\[\#1e1e1e\]{--tw-bg-opacity:1;background-color:rgb(30 30 30/var(--tw-bg-opacity,1))}.bg-\[\#27ca3f\]{--tw-bg-opacity:1;background-color:rgb(39 202 63/var(--tw-bg-opacity,1))}.bg-\[\#2d2d2d\]{--tw-bg-opacity:1;background-color:rgb(45 45 45/var(--tw-bg-opacity,1))}.bg-\[\#3d3d3d\]{--tw-bg-opacity:1;background-color:rgb(61 61 61/var(--tw-bg-opacity,1))}.bg-\[\#4dabf7\]{--tw-bg-opacity:1;background-color:rgb(77 171 247/var(--tw-bg-opacity,1))}.bg-\[\#e0e0e0\]{--tw-bg-opacity:1;background-color:rgb(224 224 224/var(--tw-bg-opacity,1))}.bg-\[\#ff5f56\]{--tw-bg-opacity:1;background-color:rgb(255 95 86/var(--tw-bg-opacity,1))}.bg-\[\#ffbd2e\]{--tw-bg-opacity:1;background-color:rgb(255 189 46/var(--tw-bg-opacity,1))}.bg-transparent{background-color:transparent}.p-4{padding:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.pt-4{padding-top:1rem}.text-left{text-align:left}.align-middle{vertical-align:middle}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.text-\[\#4dabf7\]{--tw-text-opacity:1;color:rgb(77 171 247/var(--tw-text-opacity,1))}.text-\[\#e0e0e0\]{--tw-text-opacity:1;color:rgb(224 224 224/var(--tw-text-opacity,1))}.text-blue-400{--tw-text-opacity:1;color:rgb(96 165 250/var(--tw-text-opacity,1))}.text-gray-300{--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity,1))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity,1))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity,1))}.text-green-400{--tw-text-opacity:1;color:rgb(74 222 128/var(--tw-text-opacity,1))}.text-red-400{--tw-text-opacity:1;color:rgb(248 113 113/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}.text-yellow-400{--tw-text-opacity:1;color:rgb(250 204 21/var(--tw-text-opacity,1))}.shadow-\[0_4px_20px_rgba\(0\2c 0\2c 0\2c 0\.5\)\]{--tw-shadow:0 4px 20px rgba(0,0,0,.5);--tw-shadow-colored:0 4px 20px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-300{transition-duration:.3s}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.hover\:bg-\[\#4a4a4a\]:hover{--tw-bg-opacity:1;background-color:rgb(74 74 74/var(--tw-bg-opacity,1))}.hover\:text-blue-300:hover{--tw-text-opacity:1;color:rgb(147 197 253/var(--tw-text-opacity,1))}.hover\:text-white:hover{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity,1))}@media (min-width:768px){.md\:fixed{position:fixed}.md\:bottom-0{bottom:0}.md\:left-0{left:0}.md\:top-0{top:0}.md\:ml-\[250px\]{margin-left:250px}.md\:flex{display:flex}.md\:w-\[250px\]{width:250px}.md\:w-\[300px\]{width:300px}.md\:-translate-x-\[300px\]{--tw-translate-x:-300px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.md\:flex-col{flex-direction:column}.md\:overflow-y-auto{overflow-y:auto}.md\:border-r{border-right-width:1px}.md\:border-\[\#3d3d3d\]{--tw-border-opacity:1;border-color:rgb(61 61 61/var(--tw-border-opacity,1))}.md\:bg-\[\#1e1e1e\]{--tw-bg-opacity:1;background-color:rgb(30 30 30/var(--tw-bg-opacity,1))}.md\:p-4{padding:1rem}}
@@ -0,0 +1,435 @@
1
+ // WebContainer Service for browser-based execution
2
+ // This service allows running CLI commands directly in the browser using WebContainers
3
+
4
+ class WebContainerService {
5
+ constructor() {
6
+ this.webcontainerInstance = null;
7
+ this.initialized = false;
8
+ this.setupPromise = null;
9
+ this.initializing = false;
10
+ this.filesLoaded = false;
11
+ this.dependenciesInstalled = false;
12
+ }
13
+
14
+ // Initialize WebContainer
15
+ async initialize() {
16
+ if (this.initialized) {
17
+ return true;
18
+ }
19
+
20
+ if (this.initializing) {
21
+ // Already initializing, wait for it to complete
22
+ return this.setupPromise;
23
+ }
24
+
25
+ this.initializing = true;
26
+
27
+ try {
28
+ this._notifyStatus('Loading project files...');
29
+ // Check if WebContainer is available
30
+ if (typeof WebContainer === 'undefined') {
31
+ console.warn('WebContainer API not available - WebContainer functionality will be disabled');
32
+ this.initialized = true;
33
+ this.initializing = false;
34
+ return false;
35
+ }
36
+
37
+ // Check if the page is cross-origin isolated
38
+ if (!globalThis.crossOriginIsolated) {
39
+ console.error('WebContainer requires cross-origin isolation. Please ensure the server is configured with proper COOP/COEP headers.');
40
+
41
+ // Check if we're on localhost (which allows COOP/COEP without HTTPS)
42
+ if (typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
43
+ console.log('Running on localhost - COOP/COEP headers should work without HTTPS');
44
+ } else if (typeof window !== 'undefined' && window.location.protocol !== 'https:') {
45
+ console.warn('For non-localhost domains, HTTPS is required for COOP/COEP headers to work.');
46
+ }
47
+
48
+ this.initialized = true;
49
+ this.initializing = false;
50
+ return false;
51
+ }
52
+
53
+ // Load project files from server
54
+ if (!this.filesLoaded) {
55
+ await this._loadProjectFiles();
56
+ }
57
+
58
+ this._notifyStatus('Booting WebContainer...');
59
+ // Create a new WebContainer instance
60
+ this.webcontainerInstance = await WebContainer.boot();
61
+
62
+ this._notifyStatus('Mounting project files...');
63
+ // Mount the project files
64
+ await this._mountProjectFiles();
65
+
66
+ console.log('WebContainer initialized successfully');
67
+ this.initialized = true;
68
+ this.initializing = false;
69
+ this._notifyStatus('WebContainer ready.');
70
+ return true;
71
+
72
+ } catch (error) {
73
+ console.error('Failed to initialize WebContainer:', error);
74
+ this.initializing = false;
75
+
76
+ // Check for specific SharedArrayBuffer error
77
+ if (error.message && error.message.includes('SharedArrayBuffer')) {
78
+ console.error('WebContainer requires SharedArrayBuffer support. Please ensure the page is served with proper cross-origin isolation headers (COOP/COEP).');
79
+
80
+ // Check if we're on localhost (which allows COOP/COEP without HTTPS)
81
+ if (typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
82
+ console.log('Running on localhost - COOP/COEP headers should work without HTTPS');
83
+ } else if (typeof window !== 'undefined' && window.location.protocol !== 'https:') {
84
+ console.warn('For non-localhost domains, HTTPS is required for COOP/COEP headers to work.');
85
+ }
86
+ }
87
+
88
+ // If initialization fails, mark as initialized but not functional
89
+ this.initialized = true;
90
+ this.webcontainerInstance = null;
91
+
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ // Write config to the WebContainer filesystem
97
+ async writeConfig(config) {
98
+ const initialized = await this.initialize();
99
+
100
+ if (!initialized || !this.webcontainerInstance) {
101
+ throw new Error('WebContainer is not available in this environment.');
102
+ }
103
+
104
+ const configPath = '/src/.astrocli/config.json';
105
+ const configDir = '/src/.astrocli';
106
+
107
+ await this.webcontainerInstance.fs.mkdir(configDir, { recursive: true });
108
+ await this.webcontainerInstance.fs.writeFile(configPath, JSON.stringify(config, null, 2));
109
+ }
110
+
111
+ // Read config from the WebContainer filesystem
112
+ async readConfig() {
113
+ const initialized = await this.initialize();
114
+
115
+ if (!initialized || !this.webcontainerInstance) {
116
+ throw new Error('WebContainer is not available in this environment.');
117
+ }
118
+
119
+ const configPath = '/src/.astrocli/config.json';
120
+
121
+ try {
122
+ const data = await this.webcontainerInstance.fs.readFile(configPath, 'utf8');
123
+ return data ? JSON.parse(data) : null;
124
+ } catch (error) {
125
+ return null;
126
+ }
127
+ }
128
+
129
+ // Load project files from server
130
+ async _loadProjectFiles() {
131
+ try {
132
+ const response = await fetch('/api/webcontainer-files');
133
+ const result = await response.json();
134
+
135
+ if (!result.success) {
136
+ throw new Error(result.error || 'Failed to load WebContainer files');
137
+ }
138
+
139
+ this.projectFiles = result.files;
140
+ this.filesLoaded = true;
141
+ console.log('WebContainer files loaded successfully');
142
+
143
+ } catch (error) {
144
+ console.error('Error loading WebContainer files:', error);
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ // Mount project files
150
+ async _mountProjectFiles() {
151
+ if (!this.projectFiles) {
152
+ await this._loadProjectFiles();
153
+ }
154
+
155
+ const filesToMount = this._buildFileTree(this.projectFiles);
156
+
157
+ await this.webcontainerInstance.mount(filesToMount);
158
+
159
+ await this._syncSwephAssets();
160
+
161
+ if (!this.dependenciesInstalled) {
162
+ this._notifyStatus('Installing dependencies...');
163
+ await this._installDependencies();
164
+ this.dependenciesInstalled = true;
165
+ }
166
+ }
167
+
168
+ async _installDependencies() {
169
+ const installProcess = await this.webcontainerInstance.spawn('npm', [
170
+ 'install',
171
+ '--no-fund',
172
+ '--no-audit'
173
+ ]);
174
+
175
+ this._notifyStatus('Installing dependencies (npm install)...');
176
+ const output = await this._readProcessOutput(installProcess);
177
+ const exitCode = await installProcess.exit;
178
+
179
+ if (exitCode !== 0) {
180
+ throw new Error(`npm install failed (${exitCode}). ${output}`);
181
+ }
182
+ }
183
+
184
+ async _syncSwephAssets() {
185
+ try {
186
+ const wasmResponse = await fetch('/sweph/astro.wasm');
187
+ const dataResponse = await fetch('/sweph/astro.data');
188
+
189
+ if (!wasmResponse.ok || !dataResponse.ok) {
190
+ throw new Error('Failed to fetch Sweph assets from server.');
191
+ }
192
+
193
+ const wasmBuffer = await wasmResponse.arrayBuffer();
194
+ const dataBuffer = await dataResponse.arrayBuffer();
195
+
196
+ const targetDir = '/src/gui/public/sweph';
197
+ await this.webcontainerInstance.fs.mkdir(targetDir, { recursive: true });
198
+
199
+ await this.webcontainerInstance.fs.writeFile(
200
+ `${targetDir}/astro.wasm`,
201
+ new Uint8Array(wasmBuffer)
202
+ );
203
+
204
+ await this.webcontainerInstance.fs.writeFile(
205
+ `${targetDir}/astro.data`,
206
+ new Uint8Array(dataBuffer)
207
+ );
208
+ } catch (error) {
209
+ console.error('Error syncing Sweph assets:', error);
210
+ throw error;
211
+ }
212
+ }
213
+
214
+ _notifyStatus(message) {
215
+ if (typeof window !== 'undefined' && window.dispatchEvent) {
216
+ window.dispatchEvent(new CustomEvent('webcontainer-status', { detail: message }));
217
+ }
218
+ }
219
+
220
+ // Build WebContainer file tree from flat file map
221
+ _buildFileTree(fileMap) {
222
+ const tree = {};
223
+
224
+ for (const [filePath, content] of Object.entries(fileMap)) {
225
+ const fileContents = this._normalizeFileContents(content);
226
+ if (fileContents === null) {
227
+ continue;
228
+ }
229
+
230
+ const normalizedPath = filePath.replace(/\\/g, '/');
231
+ const parts = normalizedPath.split('/').filter(Boolean);
232
+ let node = tree;
233
+
234
+ for (let i = 0; i < parts.length; i++) {
235
+ const part = parts[i];
236
+ const isFile = i === parts.length - 1;
237
+
238
+ if (isFile) {
239
+ node[part] = { file: { contents: fileContents } };
240
+ } else {
241
+ if (!node[part]) {
242
+ node[part] = { directory: {} };
243
+ }
244
+ node = node[part].directory;
245
+ }
246
+ }
247
+ }
248
+
249
+ return tree;
250
+ }
251
+
252
+ _normalizeFileContents(content) {
253
+ if (typeof content === 'string') {
254
+ return content;
255
+ }
256
+
257
+ if (!content || typeof content !== 'object') {
258
+ return null;
259
+ }
260
+
261
+ if (content.encoding === 'base64' && typeof content.contents === 'string') {
262
+ const binary = atob(content.contents);
263
+ const bytes = new Uint8Array(binary.length);
264
+ for (let i = 0; i < binary.length; i++) {
265
+ bytes[i] = binary.charCodeAt(i);
266
+ }
267
+ return bytes;
268
+ }
269
+
270
+ return null;
271
+ }
272
+
273
+ // Read process output
274
+ async _readProcessOutput(process) {
275
+ let output = '';
276
+
277
+ const readStream = async (stream) => {
278
+ if (!stream || typeof stream.getReader !== 'function') {
279
+ return;
280
+ }
281
+
282
+ const reader = stream.getReader();
283
+ while (true) {
284
+ const { done, value } = await reader.read();
285
+ if (done) break;
286
+ if (value == null) {
287
+ continue;
288
+ }
289
+ if (typeof value === 'string') {
290
+ output += value;
291
+ continue;
292
+ }
293
+ if (value instanceof ArrayBuffer) {
294
+ output += new TextDecoder().decode(value);
295
+ continue;
296
+ }
297
+ if (ArrayBuffer.isView(value)) {
298
+ output += new TextDecoder().decode(value);
299
+ continue;
300
+ }
301
+ output += String(value);
302
+ }
303
+ };
304
+
305
+ if (process.output) {
306
+ await readStream(process.output);
307
+ return output;
308
+ }
309
+
310
+ await readStream(process.stdout);
311
+ await readStream(process.stderr);
312
+
313
+ return output;
314
+ }
315
+
316
+ // Execute a command in the WebContainer
317
+ async executeCommand(command) {
318
+ try {
319
+ // Ensure WebContainer is initialized
320
+ const initialized = await this.initialize();
321
+
322
+ // If WebContainer is not available, return a fallback response
323
+ if (!initialized || !this.webcontainerInstance) {
324
+ return {
325
+ success: false,
326
+ output: 'WebContainer is not available in this environment. Falling back to server execution.'
327
+ };
328
+ }
329
+
330
+ const configJson = (() => {
331
+ try {
332
+ return localStorage.getItem('astrocliConfig') || '';
333
+ } catch (error) {
334
+ return '';
335
+ }
336
+ })();
337
+
338
+ // Run the command in the WebContainer
339
+ const commandProcess = await this.webcontainerInstance.spawn('node', ['webcontainer-entry.js', command], {
340
+ env: {
341
+ KLIO_CONFIG_JSON: configJson
342
+ }
343
+ });
344
+
345
+ // Capture output
346
+ const output = await this._readProcessOutput(commandProcess);
347
+
348
+ // Wait for command to complete
349
+ const exitCode = await commandProcess.exit;
350
+
351
+ if (exitCode !== 0) {
352
+ throw new Error(`Command failed with exit code ${exitCode}: ${output}`);
353
+ }
354
+
355
+ return {
356
+ success: true,
357
+ output: output
358
+ };
359
+
360
+ } catch (error) {
361
+ console.error('Error executing command in WebContainer:', error);
362
+
363
+ // Check for specific SharedArrayBuffer error
364
+ if (error.message && error.message.includes('SharedArrayBuffer')) {
365
+ console.error('WebContainer requires SharedArrayBuffer support. Please ensure the page is served with proper cross-origin isolation headers (COOP/COEP).');
366
+
367
+ // Check if we're on localhost (which allows COOP/COEP without HTTPS)
368
+ if (typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
369
+ console.log('Running on localhost - COOP/COEP headers should work without HTTPS');
370
+ } else if (typeof window !== 'undefined' && window.location.protocol !== 'https:') {
371
+ console.warn('For non-localhost domains, HTTPS is required for COOP/COEP headers to work.');
372
+ }
373
+
374
+ return {
375
+ success: false,
376
+ output: 'WebContainer requires cross-origin isolation. Please check server configuration for COOP/COEP headers.'
377
+ };
378
+ }
379
+
380
+ return {
381
+ success: false,
382
+ output: error.message || 'Unknown error in WebContainer'
383
+ };
384
+ }
385
+ }
386
+
387
+ // Check if WebContainers are supported
388
+ static isSupported() {
389
+ try {
390
+ // Check basic browser capabilities
391
+ if (typeof WebAssembly !== 'object' ||
392
+ typeof globalThis !== 'object' ||
393
+ typeof globalThis.crypto !== 'object') {
394
+ return false;
395
+ }
396
+
397
+ // Check if WebContainer API is available
398
+ if (typeof WebContainer === 'undefined') {
399
+ return false;
400
+ }
401
+
402
+ // Check if the page is cross-origin isolated (required for SharedArrayBuffer)
403
+ if (!globalThis.crossOriginIsolated) {
404
+ console.warn('WebContainer requires cross-origin isolation. Please ensure the server is configured with proper COOP/COEP headers.');
405
+
406
+ // Check if we're on localhost (which allows COOP/COEP without HTTPS)
407
+ if (typeof window !== 'undefined' && (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1')) {
408
+ console.log('Running on localhost - COOP/COEP headers should work without HTTPS');
409
+ } else if (typeof window !== 'undefined' && window.location.protocol !== 'https:') {
410
+ console.warn('For non-localhost domains, HTTPS is required for COOP/COEP headers to work.');
411
+ }
412
+
413
+ return false;
414
+ }
415
+
416
+ return true;
417
+ } catch (error) {
418
+ console.error('Error checking WebContainer support:', error);
419
+ return false;
420
+ }
421
+ }
422
+ }
423
+
424
+ // Singleton instance
425
+ const webcontainerService = new WebContainerService();
426
+
427
+ // Only set global variables if we're in a browser environment
428
+ if (typeof window !== 'undefined') {
429
+ window.webcontainerService = webcontainerService;
430
+ window.WebContainerService = WebContainerService; // Expose the class globally
431
+ }
432
+
433
+ console.log('WebContainer service loaded');
434
+
435
+ export { webcontainerService, WebContainerService };
@@ -1,134 +1,97 @@
1
1
  const express = require('express');
2
2
  const router = express.Router();
3
- const commandDatabase = require('../database');
4
3
  const { exec } = require('child_process');
5
4
  const path = require('path');
6
5
 
7
- // Initialize database
8
- commandDatabase.initialize().catch(err => {
9
- console.error('Failed to initialize database:', err);
10
- });
11
6
 
12
- // Get all commands
13
- router.get('/commands', async (req, res) => {
14
- try {
15
- const commands = await commandDatabase.getAllCommands();
16
- res.json(commands);
17
- } catch (err) {
18
- console.error('Error fetching commands:', err);
19
- res.status(500).json({ error: 'Failed to fetch commands' });
20
- }
7
+ // Health check endpoint
8
+ router.get('/health', (req, res) => {
9
+ res.json({ status: 'healthy', timestamp: new Date().toISOString() });
21
10
  });
22
11
 
23
- // Search commands
24
- router.get('/commands/search', async (req, res) => {
25
- const query = req.query.q || '';
26
-
12
+ // Get WebContainer files endpoint
13
+ router.get('/webcontainer-files', async (req, res) => {
27
14
  try {
28
- const results = await commandDatabase.searchCommands(query);
29
- res.json(results);
30
- } catch (err) {
31
- console.error('Error searching commands:', err);
32
- res.status(500).json({ error: 'Failed to search commands' });
15
+ const webcontainerSetup = require('../webcontainerSetup');
16
+ const files = await webcontainerSetup.getProjectFiles();
17
+
18
+ res.json({
19
+ success: true,
20
+ files: files
21
+ });
22
+ } catch (error) {
23
+ console.error('Error getting WebContainer files:', error);
24
+ res.status(500).json({
25
+ success: false,
26
+ error: error.message
27
+ });
33
28
  }
34
29
  });
35
30
 
36
- // Get specific command by ID
37
- router.get('/commands/:id', async (req, res) => {
31
+ // User session information endpoint
32
+ router.get('/user-session', (req, res) => {
38
33
  try {
39
- const command = await commandDatabase.getCommandById(req.params.id);
40
- if (command) {
41
- res.json(command);
42
- } else {
43
- res.status(404).json({ error: 'Command not found' });
44
- }
34
+ const userId = req.session.userId;
35
+ res.json({
36
+ userId: userId,
37
+ sessionId: req.sessionID,
38
+ isNewSession: !req.session.cookie.originalMaxAge
39
+ });
45
40
  } catch (err) {
46
- console.error('Error fetching command:', err);
47
- res.status(500).json({ error: 'Failed to fetch command' });
41
+ console.error('Error getting user session:', err);
42
+ res.status(500).json({ error: 'Failed to get user session' });
48
43
  }
49
44
  });
50
45
 
51
- // Get commands by category
52
- router.get('/commands/category/:category', async (req, res) => {
53
- try {
54
- const commands = await commandDatabase.getCommandsByCategory(req.params.category);
55
- res.json(commands);
56
- } catch (err) {
57
- console.error('Error fetching commands by category:', err);
58
- res.status(500).json({ error: 'Failed to fetch commands by category' });
59
- }
60
- });
61
46
 
62
- // Save a new command
63
- router.post('/commands', async (req, res) => {
47
+ // Get user-specific commands endpoint
48
+ router.get('/user-commands', async (req, res) => {
64
49
  try {
65
- const { command, args, result, tags, category } = req.body;
50
+ const userId = req.session.userId;
51
+ const commands = await commandDatabase.getAllCommands();
66
52
 
67
- if (!command) {
68
- return res.status(400).json({ error: 'Command is required' });
69
- }
53
+ // Filter commands by user
54
+ const userCommands = commands.filter(cmd =>
55
+ cmd.tags && cmd.tags.includes(`user:${userId}`)
56
+ );
70
57
 
71
- const commandId = await commandDatabase.saveCommand(command, args, result, tags, category);
72
- res.status(201).json({ id: commandId, message: 'Command saved successfully' });
58
+ res.json(userCommands);
73
59
  } catch (err) {
74
- console.error('Error saving command:', err);
75
- res.status(500).json({ error: 'Failed to save command' });
60
+ console.error('Error fetching user commands:', err);
61
+ res.status(500).json({ error: 'Failed to fetch user commands' });
76
62
  }
77
63
  });
78
64
 
79
- // Health check endpoint
80
- router.get('/health', (req, res) => {
81
- res.json({ status: 'healthy', timestamp: new Date().toISOString() });
65
+ // User logout endpoint
66
+ router.post('/logout', (req, res) => {
67
+ try {
68
+ req.session.destroy((err) => {
69
+ if (err) {
70
+ console.error('Error destroying session:', err);
71
+ return res.status(500).json({ error: 'Failed to logout' });
72
+ }
73
+ res.clearCookie('connect.sid'); // Clear the session cookie
74
+ res.json({ success: true, message: 'Logged out successfully' });
75
+ });
76
+ } catch (err) {
77
+ console.error('Error in logout endpoint:', err);
78
+ res.status(500).json({ error: 'Failed to logout' });
79
+ }
82
80
  });
83
81
 
84
- // Run command endpoint
85
- router.post('/run-command', async (req, res) => {
82
+ // Get all user sessions (for admin/debug purposes)
83
+ router.get('/sessions', async (req, res) => {
86
84
  try {
87
- const { command } = req.body;
88
-
89
- if (!command) {
90
- return res.status(400).json({ error: 'Command is required' });
91
- }
92
-
93
- // Execute the command using child_process
94
- const commandToRun = command.startsWith('klio ') ? command : `klio ${command}`;
95
- const mainJsPath = path.join(__dirname, '..', '..', '..', 'src', 'main.js');
96
-
97
- exec(`node ${mainJsPath} ${command}`, {
98
- cwd: path.join(__dirname, '..', '..', '..'),
99
- maxBuffer: 1024 * 1024 * 10 // 10MB buffer
100
- }, (error, stdout, stderr) => {
101
- if (error) {
102
- console.error('Command execution error:', error);
103
- console.error('Stderr:', stderr);
104
- return res.status(500).json({
105
- error: 'Command execution failed',
106
- details: stderr || error.message,
107
- command: commandToRun
108
- });
109
- }
110
-
111
- // Save command to history
112
- commandDatabase.saveCommand(
113
- command,
114
- '',
115
- stdout,
116
- '',
117
- 'executed'
118
- ).catch(err => {
119
- console.error('Failed to save command to history:', err);
120
- });
121
-
122
- res.json({
123
- output: stdout,
124
- command: command
125
- });
85
+ // This would require accessing the session store directly
86
+ // For now, just return the current session info
87
+ res.json({
88
+ currentUser: req.session.userId,
89
+ message: 'Session management implemented'
126
90
  });
127
-
128
91
  } catch (err) {
129
- console.error('Error in run-command endpoint:', err);
130
- res.status(500).json({ error: 'Failed to execute command' });
92
+ console.error('Error getting sessions:', err);
93
+ res.status(500).json({ error: 'Failed to get sessions' });
131
94
  }
132
95
  });
133
96
 
134
- module.exports = router;
97
+ module.exports = router;