@myrialabs/clopen 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/{release.yml → ci.yml} +38 -12
- package/backend/index.ts +22 -13
- package/backend/lib/vite-dev.ts +37 -95
- package/package.json +1 -1
- package/shared/utils/logger.ts +1 -15
- package/.github/workflows/test.yml +0 -40
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
name:
|
|
1
|
+
name: CI & Release
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
push:
|
|
5
|
+
branches: [main, dev]
|
|
5
6
|
tags:
|
|
6
7
|
- 'v*.*.*'
|
|
8
|
+
pull_request:
|
|
9
|
+
branches: [main]
|
|
7
10
|
|
|
8
11
|
jobs:
|
|
9
|
-
|
|
10
|
-
name:
|
|
12
|
+
ci:
|
|
13
|
+
name: Test & Build
|
|
11
14
|
runs-on: ubuntu-latest
|
|
12
|
-
permissions:
|
|
13
|
-
contents: write
|
|
14
|
-
id-token: write
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
17
|
- name: Checkout
|
|
@@ -22,12 +22,6 @@ jobs:
|
|
|
22
22
|
with:
|
|
23
23
|
bun-version: latest
|
|
24
24
|
|
|
25
|
-
- name: Setup Node.js
|
|
26
|
-
uses: actions/setup-node@v4
|
|
27
|
-
with:
|
|
28
|
-
node-version: '22'
|
|
29
|
-
registry-url: 'https://registry.npmjs.org'
|
|
30
|
-
|
|
31
25
|
- name: Install
|
|
32
26
|
run: bun install
|
|
33
27
|
|
|
@@ -47,8 +41,40 @@ jobs:
|
|
|
47
41
|
exit 1
|
|
48
42
|
fi
|
|
49
43
|
|
|
44
|
+
publish:
|
|
45
|
+
name: Publish & Release
|
|
46
|
+
needs: ci
|
|
47
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
permissions:
|
|
50
|
+
contents: write
|
|
51
|
+
id-token: write
|
|
52
|
+
|
|
53
|
+
steps:
|
|
54
|
+
- name: Checkout
|
|
55
|
+
uses: actions/checkout@v4
|
|
56
|
+
|
|
57
|
+
- name: Setup Bun
|
|
58
|
+
uses: oven-sh/setup-bun@v1
|
|
59
|
+
with:
|
|
60
|
+
bun-version: latest
|
|
61
|
+
|
|
62
|
+
- name: Setup Node.js
|
|
63
|
+
uses: actions/setup-node@v4
|
|
64
|
+
with:
|
|
65
|
+
node-version: '22'
|
|
66
|
+
registry-url: 'https://registry.npmjs.org'
|
|
67
|
+
|
|
68
|
+
- name: Install
|
|
69
|
+
run: bun install
|
|
70
|
+
|
|
71
|
+
- name: Build
|
|
72
|
+
run: bun run build
|
|
73
|
+
|
|
50
74
|
- name: Publish to npm
|
|
51
75
|
run: npm publish --provenance --access public
|
|
76
|
+
env:
|
|
77
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
52
78
|
|
|
53
79
|
- name: Create GitHub release
|
|
54
80
|
uses: softprops/action-gh-release@v2
|
package/backend/index.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { Elysia } from 'elysia';
|
|
3
|
-
import { staticPlugin } from '@elysiajs/static';
|
|
4
3
|
import { corsMiddleware } from './middleware/cors';
|
|
5
4
|
import { errorHandlerMiddleware } from './middleware/error-handler';
|
|
6
5
|
import { loggerMiddleware } from './middleware/logger';
|
|
@@ -11,6 +10,8 @@ import { disposeAllEngines } from './lib/engine';
|
|
|
11
10
|
import { debug } from '$shared/utils/logger';
|
|
12
11
|
import { isPortInUse } from './lib/shared/port-utils';
|
|
13
12
|
import { networkInterfaces } from 'os';
|
|
13
|
+
import { resolve } from 'node:path';
|
|
14
|
+
import { statSync } from 'node:fs';
|
|
14
15
|
|
|
15
16
|
// Import WebSocket router
|
|
16
17
|
import { wsRouter } from './ws';
|
|
@@ -66,18 +67,26 @@ if (isDevelopment) {
|
|
|
66
67
|
// Store cleanup function for graceful shutdown
|
|
67
68
|
(globalThis as any).__closeViteDev = closeViteDev;
|
|
68
69
|
} else {
|
|
69
|
-
// Production:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
// Production: Read index.html once at startup as a string so Content-Length
|
|
71
|
+
// is always correct. Bun.file() streaming can hang on some platforms.
|
|
72
|
+
const distDir = resolve(process.cwd(), 'dist');
|
|
73
|
+
const indexHtml = await Bun.file(resolve(distDir, 'index.html')).text();
|
|
74
|
+
|
|
75
|
+
app.all('/*', ({ path }) => {
|
|
76
|
+
// Serve static files from dist/
|
|
77
|
+
if (path !== '/' && !path.includes('..')) {
|
|
78
|
+
const filePath = resolve(distDir, path.slice(1));
|
|
79
|
+
if (filePath.startsWith(distDir)) {
|
|
80
|
+
try {
|
|
81
|
+
if (statSync(filePath).isFile()) {
|
|
82
|
+
return new Response(Bun.file(filePath));
|
|
83
|
+
}
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// SPA fallback: serve cached index.html
|
|
89
|
+
return new Response(indexHtml, {
|
|
81
90
|
headers: { 'Content-Type': 'text/html; charset=utf-8' }
|
|
82
91
|
});
|
|
83
92
|
});
|
package/backend/lib/vite-dev.ts
CHANGED
|
@@ -1,49 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Vite Dev Server Integration
|
|
2
|
+
* Vite Dev Server Integration
|
|
3
3
|
*
|
|
4
4
|
* Embeds Vite as middleware inside the Elysia/Bun server.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - HTML requests use vite.transformIndexHtml() directly (bypass Node compat layer)
|
|
8
|
-
* - Module requests use vite.transformRequest() directly (bypass Node compat layer)
|
|
9
|
-
* - Only edge cases (Vite internals, pre-bundled deps) go through middleware adapter
|
|
10
|
-
*
|
|
11
|
-
* Reliability:
|
|
12
|
-
* - All async operations have timeouts to prevent hanging promises
|
|
13
|
-
* - HTML has raw fallback if Vite transform hangs
|
|
14
|
-
* - Middleware adapter has safety timeout
|
|
5
|
+
* Uses Vite's direct APIs (transformIndexHtml, transformRequest) for speed,
|
|
6
|
+
* with Node.js compat middleware adapter only for Vite internals (HMR, pre-bundled deps).
|
|
15
7
|
*/
|
|
16
8
|
|
|
17
9
|
import { createServer as createViteServer, type ViteDevServer, type Connect } from 'vite';
|
|
18
10
|
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
19
11
|
import { resolve } from 'node:path';
|
|
12
|
+
import { statSync } from 'node:fs';
|
|
20
13
|
|
|
21
14
|
let vite: ViteDevServer | null = null;
|
|
22
15
|
|
|
23
|
-
// Resolved paths (computed once at startup)
|
|
24
16
|
const PUBLIC_DIR = resolve(process.cwd(), 'static');
|
|
25
17
|
const INDEX_PATH = resolve(process.cwd(), 'index.html');
|
|
26
18
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
const MODULE_TRANSFORM_TIMEOUT = 10000;
|
|
19
|
+
// Safety timeout for the Node.js compat middleware adapter (ms).
|
|
20
|
+
// Prevents hanging if Vite middleware never calls res.end() or next().
|
|
30
21
|
const MIDDLEWARE_TIMEOUT = 10000;
|
|
31
22
|
|
|
32
|
-
// ============================================================================
|
|
33
|
-
// Utilities
|
|
34
|
-
// ============================================================================
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Race a promise against a timeout. Returns null on timeout (never rejects).
|
|
38
|
-
*/
|
|
39
|
-
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | null> {
|
|
40
|
-
let timer: ReturnType<typeof setTimeout>;
|
|
41
|
-
return Promise.race([
|
|
42
|
-
promise,
|
|
43
|
-
new Promise<null>((resolve) => { timer = setTimeout(() => resolve(null), ms); })
|
|
44
|
-
]).finally(() => clearTimeout(timer));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
23
|
// ============================================================================
|
|
48
24
|
// Lifecycle
|
|
49
25
|
// ============================================================================
|
|
@@ -73,45 +49,26 @@ export async function closeViteDev(): Promise<void> {
|
|
|
73
49
|
// Main Request Handler
|
|
74
50
|
// ============================================================================
|
|
75
51
|
|
|
76
|
-
/**
|
|
77
|
-
* Handle an HTTP request in dev mode.
|
|
78
|
-
* Uses Vite's direct APIs for speed, middleware only as fallback.
|
|
79
|
-
* All paths have timeouts to guarantee a response — never hangs.
|
|
80
|
-
*/
|
|
81
52
|
export async function handleDevRequest(viteServer: ViteDevServer, request: Request): Promise<Response> {
|
|
82
53
|
const url = new URL(request.url);
|
|
83
54
|
const pathname = url.pathname;
|
|
84
55
|
|
|
85
|
-
// 1. Static public files
|
|
86
|
-
const publicFile =
|
|
56
|
+
// 1. Static public files
|
|
57
|
+
const publicFile = servePublicFile(pathname);
|
|
87
58
|
if (publicFile) return publicFile;
|
|
88
59
|
|
|
89
|
-
// 2. HTML / SPA routes
|
|
60
|
+
// 2. HTML / SPA routes
|
|
90
61
|
if (isHtmlRequest(pathname)) {
|
|
91
|
-
|
|
92
|
-
if (htmlResponse) return htmlResponse;
|
|
93
|
-
|
|
94
|
-
// Fallback: serve raw HTML without Vite transforms (HMR won't work but page loads)
|
|
95
|
-
try {
|
|
96
|
-
const rawHtml = await Bun.file(INDEX_PATH).text();
|
|
97
|
-
return new Response(rawHtml, {
|
|
98
|
-
headers: { 'Content-Type': 'text/html; charset=utf-8' }
|
|
99
|
-
});
|
|
100
|
-
} catch {
|
|
101
|
-
// INDEX_PATH doesn't exist — fall through
|
|
102
|
-
}
|
|
62
|
+
return serveHtml(viteServer, pathname);
|
|
103
63
|
}
|
|
104
64
|
|
|
105
|
-
// 3. Module requests
|
|
65
|
+
// 3. Module requests (skip Vite internals like /__vite_hmr)
|
|
106
66
|
if (!pathname.startsWith('/__')) {
|
|
107
|
-
const moduleResponse = await
|
|
108
|
-
serveModule(viteServer, pathname + url.search, request),
|
|
109
|
-
MODULE_TRANSFORM_TIMEOUT
|
|
110
|
-
);
|
|
67
|
+
const moduleResponse = await serveModule(viteServer, pathname + url.search, request);
|
|
111
68
|
if (moduleResponse) return moduleResponse;
|
|
112
69
|
}
|
|
113
70
|
|
|
114
|
-
// 4. Fallback:
|
|
71
|
+
// 4. Fallback: Vite connect middleware (for HMR, pre-bundled deps, etc.)
|
|
115
72
|
const middlewareResponse = await pipeViteMiddleware(viteServer.middlewares, request);
|
|
116
73
|
if (middlewareResponse) return middlewareResponse;
|
|
117
74
|
|
|
@@ -119,23 +76,26 @@ export async function handleDevRequest(viteServer: ViteDevServer, request: Reque
|
|
|
119
76
|
}
|
|
120
77
|
|
|
121
78
|
// ============================================================================
|
|
122
|
-
//
|
|
79
|
+
// Static Public Files
|
|
123
80
|
// ============================================================================
|
|
124
81
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
* Uses BunFile directly (not .stream()) so Bun sets Content-Length automatically,
|
|
128
|
-
* preventing endless loading when the browser can't detect stream end.
|
|
129
|
-
*/
|
|
130
|
-
async function servePublicFile(pathname: string): Promise<Response | null> {
|
|
131
|
-
if (pathname.includes('..')) return null;
|
|
82
|
+
function servePublicFile(pathname: string): Response | null {
|
|
83
|
+
if (pathname === '/' || pathname.includes('..')) return null;
|
|
132
84
|
|
|
133
85
|
const filePath = resolve(PUBLIC_DIR, pathname.slice(1));
|
|
134
86
|
if (!filePath.startsWith(PUBLIC_DIR)) return null;
|
|
135
87
|
|
|
136
|
-
|
|
137
|
-
|
|
88
|
+
// Use statSync to verify the path is a regular file, not a directory.
|
|
89
|
+
// Bun.file().exists() returns inconsistent results for directories across
|
|
90
|
+
// platforms (Linux/macOS/Windows) and Bun versions, which can cause
|
|
91
|
+
// Response(Bun.file(directory)) to hang indefinitely.
|
|
92
|
+
try {
|
|
93
|
+
if (!statSync(filePath).isFile()) return null;
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
138
97
|
|
|
98
|
+
const file = Bun.file(filePath);
|
|
139
99
|
return new Response(file, {
|
|
140
100
|
headers: {
|
|
141
101
|
'Content-Type': file.type || 'application/octet-stream',
|
|
@@ -145,12 +105,9 @@ async function servePublicFile(pathname: string): Promise<Response | null> {
|
|
|
145
105
|
}
|
|
146
106
|
|
|
147
107
|
// ============================================================================
|
|
148
|
-
//
|
|
108
|
+
// HTML / SPA
|
|
149
109
|
// ============================================================================
|
|
150
110
|
|
|
151
|
-
/**
|
|
152
|
-
* Check if a request is for an HTML page (root or SPA client-side route).
|
|
153
|
-
*/
|
|
154
111
|
function isHtmlRequest(pathname: string): boolean {
|
|
155
112
|
if (pathname === '/') return true;
|
|
156
113
|
if (pathname.startsWith('/@') || pathname.startsWith('/__')) return false;
|
|
@@ -159,9 +116,6 @@ function isHtmlRequest(pathname: string): boolean {
|
|
|
159
116
|
return !lastSegment.includes('.');
|
|
160
117
|
}
|
|
161
118
|
|
|
162
|
-
/**
|
|
163
|
-
* Serve transformed HTML directly via Vite's API.
|
|
164
|
-
*/
|
|
165
119
|
async function serveHtml(viteServer: ViteDevServer, pathname: string): Promise<Response> {
|
|
166
120
|
const rawHtml = await Bun.file(INDEX_PATH).text();
|
|
167
121
|
const html = await viteServer.transformIndexHtml(pathname, rawHtml);
|
|
@@ -171,23 +125,19 @@ async function serveHtml(viteServer: ViteDevServer, pathname: string): Promise<R
|
|
|
171
125
|
}
|
|
172
126
|
|
|
173
127
|
// ============================================================================
|
|
174
|
-
//
|
|
128
|
+
// Module Requests
|
|
175
129
|
// ============================================================================
|
|
176
130
|
|
|
177
131
|
function getModuleContentType(url: string): string {
|
|
178
132
|
const pathname = url.split('?')[0];
|
|
179
|
-
//
|
|
180
|
-
// CSS imported via JS is
|
|
181
|
-
// (for HMR / style injection), so it must be served as application/javascript.
|
|
133
|
+
// Raw CSS only when explicitly requested with ?direct (e.g. <link> tags).
|
|
134
|
+
// CSS imported via JS is transformed to a JS module by Vite (for HMR).
|
|
182
135
|
if (pathname.endsWith('.css') && url.includes('direct')) {
|
|
183
136
|
return 'text/css';
|
|
184
137
|
}
|
|
185
138
|
return 'application/javascript';
|
|
186
139
|
}
|
|
187
140
|
|
|
188
|
-
/**
|
|
189
|
-
* Try to serve a module using Vite's transformRequest API.
|
|
190
|
-
*/
|
|
191
141
|
async function serveModule(viteServer: ViteDevServer, url: string, request: Request): Promise<Response | null> {
|
|
192
142
|
try {
|
|
193
143
|
const result = await viteServer.transformRequest(url);
|
|
@@ -214,7 +164,7 @@ async function serveModule(viteServer: ViteDevServer, url: string, request: Requ
|
|
|
214
164
|
}
|
|
215
165
|
|
|
216
166
|
// ============================================================================
|
|
217
|
-
//
|
|
167
|
+
// Vite Connect Middleware Adapter
|
|
218
168
|
// ============================================================================
|
|
219
169
|
|
|
220
170
|
const MOCK_SOCKET = {
|
|
@@ -242,9 +192,8 @@ const MOCK_SOCKET = {
|
|
|
242
192
|
};
|
|
243
193
|
|
|
244
194
|
/**
|
|
245
|
-
*
|
|
246
|
-
*
|
|
247
|
-
* (e.g., middleware errors without calling res.end() or next()).
|
|
195
|
+
* Bridges Web API Request → Node.js IncomingMessage/ServerResponse → Web API Response.
|
|
196
|
+
* Required because Vite's connect middleware uses the Node.js HTTP API.
|
|
248
197
|
*/
|
|
249
198
|
function pipeViteMiddleware(
|
|
250
199
|
middleware: Connect.Server,
|
|
@@ -263,8 +212,6 @@ function pipeViteMiddleware(
|
|
|
263
212
|
let ended = false;
|
|
264
213
|
const chunks: Uint8Array[] = [];
|
|
265
214
|
|
|
266
|
-
// Safety timeout — if middleware never calls res.end() or next(),
|
|
267
|
-
// resolve with null to prevent hanging the HTTP response forever.
|
|
268
215
|
const safetyTimer = setTimeout(() => {
|
|
269
216
|
if (!ended) {
|
|
270
217
|
ended = true;
|
|
@@ -280,12 +227,9 @@ function pipeViteMiddleware(
|
|
|
280
227
|
const nodeHeaders = res.getHeaders();
|
|
281
228
|
const h = new Headers();
|
|
282
229
|
for (const key in nodeHeaders) {
|
|
283
|
-
// Skip hop-by-hop headers —
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
// Forwarding stale Content-Length from the Node.js ServerResponse
|
|
287
|
-
// can cause a mismatch with the collected body, making the browser
|
|
288
|
-
// wait for more data that never arrives (infinite loading spinner).
|
|
230
|
+
// Skip hop-by-hop headers — Bun sets correct Content-Length/Transfer-Encoding
|
|
231
|
+
// based on the actual body. Forwarding stale values from ServerResponse
|
|
232
|
+
// causes browser to wait for data that never arrives.
|
|
289
233
|
const lower = key.toLowerCase();
|
|
290
234
|
if (lower === 'transfer-encoding' || lower === 'content-length' ||
|
|
291
235
|
lower === 'connection' || lower === 'keep-alive') continue;
|
|
@@ -301,9 +245,7 @@ function pipeViteMiddleware(
|
|
|
301
245
|
|
|
302
246
|
const body = chunks.length === 0
|
|
303
247
|
? null
|
|
304
|
-
: chunks
|
|
305
|
-
? chunks[0]
|
|
306
|
-
: Buffer.concat(chunks);
|
|
248
|
+
: Buffer.concat(chunks);
|
|
307
249
|
|
|
308
250
|
resolve(new Response(body, { status: res.statusCode, headers: h }));
|
|
309
251
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myrialabs/clopen",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "All-in-one web workspace for Claude Code & OpenCode — chat, terminal, git, browser preview, checkpoints, and real-time collaboration",
|
|
5
5
|
"author": "Myria Labs",
|
|
6
6
|
"license": "MIT",
|
package/shared/utils/logger.ts
CHANGED
|
@@ -3,19 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides filtered logging capabilities with label-based categorization.
|
|
5
5
|
* All logs can be filtered by label, method type, and text content.
|
|
6
|
-
*
|
|
7
|
-
* Label Categories (22 total):
|
|
8
|
-
* - Communication & Real-time: WebSocket, MCP operations
|
|
9
|
-
* - File Operations: File I/O operations
|
|
10
|
-
* - Chat & Notifications: Messages and notifications
|
|
11
|
-
* - Terminal: Shell/PTY operations
|
|
12
|
-
* - Preview: Browser preview and video encoding
|
|
13
|
-
* - Data Persistence: Database, migrations, snapshots, checkpoints
|
|
14
|
-
* - Engine & Processing: Engine operations
|
|
15
|
-
* - User & Session: User and session management
|
|
16
|
-
* - Configuration & Settings: Settings
|
|
17
|
-
* - Infrastructure & Utilities: Server, git, project ops
|
|
18
|
-
* - Frontend/UI State: Workspace and checkpoint UI state
|
|
19
6
|
*/
|
|
20
7
|
|
|
21
8
|
export type LogLabel =
|
|
@@ -71,8 +58,7 @@ interface LoggerConfig {
|
|
|
71
58
|
// enabled = true in development, false in production
|
|
72
59
|
const config: LoggerConfig = {
|
|
73
60
|
enabled: process.env.NODE_ENV !== 'production',
|
|
74
|
-
filterLabels:
|
|
75
|
-
// filterLabels: ['file'],
|
|
61
|
+
filterLabels: null,
|
|
76
62
|
filterMethods: null,
|
|
77
63
|
filterText: null
|
|
78
64
|
};
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
name: Test & Build
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main, dev]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
test:
|
|
11
|
-
name: Run Tests
|
|
12
|
-
runs-on: ubuntu-latest
|
|
13
|
-
|
|
14
|
-
steps:
|
|
15
|
-
- name: Checkout
|
|
16
|
-
uses: actions/checkout@v4
|
|
17
|
-
|
|
18
|
-
- name: Setup Bun
|
|
19
|
-
uses: oven-sh/setup-bun@v1
|
|
20
|
-
with:
|
|
21
|
-
bun-version: latest
|
|
22
|
-
|
|
23
|
-
- name: Install
|
|
24
|
-
run: bun install
|
|
25
|
-
|
|
26
|
-
- name: Type check
|
|
27
|
-
run: bun run check
|
|
28
|
-
|
|
29
|
-
- name: Lint
|
|
30
|
-
run: bun run lint
|
|
31
|
-
|
|
32
|
-
- name: Build
|
|
33
|
-
run: bun run build
|
|
34
|
-
|
|
35
|
-
- name: Verify build
|
|
36
|
-
run: |
|
|
37
|
-
if [ ! -d "dist" ]; then
|
|
38
|
-
echo "Build failed - dist directory not found"
|
|
39
|
-
exit 1
|
|
40
|
-
fi
|