repoburg 1.3.217 → 1.3.218

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.
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const http_1 = __importDefault(require("http"));
7
+ const express_1 = __importDefault(require("express"));
8
+ const routes_1 = require("./routes");
9
+ jest.mock('./tunnel-state');
10
+ jest.mock('./provisioner');
11
+ jest.mock('./cloudflared');
12
+ jest.mock('./orchestrator');
13
+ const tunnel_state_1 = require("./tunnel-state");
14
+ const provisioner_1 = require("./provisioner");
15
+ const orchestrator_1 = require("./orchestrator");
16
+ const cloudflared_1 = require("./cloudflared");
17
+ const mocked = {
18
+ loadTunnelState: tunnel_state_1.loadTunnelState,
19
+ updateTunnelState: tunnel_state_1.updateTunnelState,
20
+ clearTunnelState: tunnel_state_1.clearTunnelState,
21
+ provisionTunnel: provisioner_1.provisionTunnel,
22
+ uninstallTunnel: provisioner_1.uninstallTunnel,
23
+ listTunnels: provisioner_1.listTunnels,
24
+ isInstalled: cloudflared_1.isInstalled,
25
+ downloadCloudflared: cloudflared_1.downloadCloudflared,
26
+ installService: cloudflared_1.installService,
27
+ removeService: cloudflared_1.removeService,
28
+ isServiceRunning: cloudflared_1.isServiceRunning,
29
+ cloudflaredMetricsUrl: cloudflared_1.cloudflaredMetricsUrl,
30
+ };
31
+ const METRICS_URL = 'http://127.0.0.1:20241/metrics';
32
+ /** Real cloudflared exports the gauge label-less — HELP/TYPE lines included. */
33
+ const METRICS_BODY_CONNECTED = '# HELP cloudflared_tunnel_ha_connections Number of active ha connections\n' +
34
+ '# TYPE cloudflared_tunnel_ha_connections gauge\n' +
35
+ 'cloudflared_tunnel_ha_connections 4\n';
36
+ /** fetch mock: metrics endpoint answers connected by default, health 200. */
37
+ function makeFetch(opts = {}) {
38
+ return jest.fn(async (url) => {
39
+ const href = String(url);
40
+ if (href === METRICS_URL) {
41
+ return {
42
+ ok: true,
43
+ status: 200,
44
+ text: async () => opts.metricsBody ?? METRICS_BODY_CONNECTED,
45
+ };
46
+ }
47
+ return {
48
+ ok: opts.healthOk ?? true,
49
+ status: (opts.healthOk ?? true) ? 200 : 530,
50
+ };
51
+ });
52
+ }
53
+ const STATE = {
54
+ mahlas: 'workstation',
55
+ hostname: 'workstation.repoburg.com',
56
+ accountTunnelId: 'tunnel-uuid',
57
+ tunnelToken: 'connector-token',
58
+ };
59
+ function request(server, method, path, body) {
60
+ const { port } = server.address();
61
+ return new Promise((resolve, reject) => {
62
+ const payload = body === undefined ? undefined : JSON.stringify(body);
63
+ const req = http_1.default.request({
64
+ host: '127.0.0.1',
65
+ port,
66
+ method,
67
+ path,
68
+ headers: payload ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } : undefined,
69
+ }, (res) => {
70
+ let data = '';
71
+ res.on('data', (chunk) => (data += chunk));
72
+ // Non-JSON replies (e.g. express 404 HTML) are status-only checks.
73
+ res.on('end', () => {
74
+ let json = null;
75
+ try {
76
+ json = JSON.parse(data || '{}');
77
+ }
78
+ catch {
79
+ json = null;
80
+ }
81
+ resolve({ status: res.statusCode ?? 0, json });
82
+ });
83
+ });
84
+ req.on('error', reject);
85
+ if (payload)
86
+ req.write(payload);
87
+ req.end();
88
+ });
89
+ }
90
+ describe('remote routes', () => {
91
+ let server;
92
+ beforeEach(async () => {
93
+ for (const mock of Object.values(mocked))
94
+ mock.mockReset();
95
+ mocked.cloudflaredMetricsUrl.mockReturnValue(METRICS_URL);
96
+ const app = (0, express_1.default)();
97
+ app.use(express_1.default.json());
98
+ app.use('/remote', (0, routes_1.createRemoteRouter)());
99
+ server = (await new Promise((resolve) => {
100
+ const s = app.listen(0, '127.0.0.1', () => resolve(s));
101
+ }));
102
+ });
103
+ afterEach(() => new Promise((resolve) => server.close(() => resolve())));
104
+ it('GET /status aggregates tunnel, cloudflared, service, connection, and access', async () => {
105
+ mocked.loadTunnelState.mockReturnValue({
106
+ ...STATE,
107
+ accessTeamDomain: 'team.cloudflareaccess.com',
108
+ accessAud: 'aud-tag',
109
+ ownerEmail: 'owner@repoburg.com',
110
+ });
111
+ mocked.isInstalled.mockReturnValue(true);
112
+ mocked.isServiceRunning.mockResolvedValue(true);
113
+ global.fetch = makeFetch();
114
+ const res = await request(server, 'GET', '/remote/status');
115
+ expect(res.status).toBe(200);
116
+ expect(res.json.tunnel).toEqual({ mahlas: 'workstation', hostname: 'workstation.repoburg.com', provisioned: true });
117
+ expect(res.json.cloudflared).toEqual({ installed: true });
118
+ expect(res.json.service).toEqual({ running: true });
119
+ expect(res.json.connection).toEqual({ ok: true });
120
+ expect(res.json.access).toEqual({
121
+ configured: true,
122
+ ownerEmail: 'owner@repoburg.com',
123
+ teamDomain: 'team.cloudflareaccess.com',
124
+ });
125
+ expect(res.json.publicHealth).toEqual({ ok: true });
126
+ });
127
+ it('GET /status reports connection false when cloudflared metrics show 0 ha connections', async () => {
128
+ mocked.loadTunnelState.mockReturnValue(STATE);
129
+ mocked.isServiceRunning.mockResolvedValue(false);
130
+ global.fetch = makeFetch({ metricsBody: 'cloudflared_tunnel_ha_connections 0\n' });
131
+ const res = await request(server, 'GET', '/remote/status');
132
+ expect(res.json.connection).toEqual({ ok: false });
133
+ });
134
+ it('GET /status accepts labeled metric lines too (other cloudflared versions)', async () => {
135
+ mocked.loadTunnelState.mockReturnValue(STATE);
136
+ mocked.isServiceRunning.mockResolvedValue(true);
137
+ global.fetch = makeFetch({ metricsBody: 'cloudflared_tunnel_ha_connections{region="us"} 2\n' });
138
+ const res = await request(server, 'GET', '/remote/status');
139
+ expect(res.json.connection).toEqual({ ok: true });
140
+ });
141
+ it('GET /status reports connection false when metrics are unreachable', async () => {
142
+ mocked.loadTunnelState.mockReturnValue(STATE);
143
+ mocked.isServiceRunning.mockResolvedValue(false);
144
+ global.fetch = jest.fn().mockRejectedValue(new Error('ECONNREFUSED'));
145
+ const res = await request(server, 'GET', '/remote/status');
146
+ expect(res.json.connection).toEqual({ ok: false });
147
+ expect(res.json.publicHealth).toEqual({ ok: false });
148
+ });
149
+ it('GET /status reports false health when no tunnel exists', async () => {
150
+ mocked.loadTunnelState.mockReturnValue(null);
151
+ mocked.isInstalled.mockReturnValue(false);
152
+ mocked.isServiceRunning.mockResolvedValue(false);
153
+ const res = await request(server, 'GET', '/remote/status');
154
+ expect(res.json.tunnel).toEqual({ provisioned: false });
155
+ expect(res.json.publicHealth).toEqual({ ok: false });
156
+ expect(res.json.connection).toEqual({ ok: false });
157
+ });
158
+ it('POST /provision validates the mahlas body', async () => {
159
+ const res = await request(server, 'POST', '/remote/provision', {});
160
+ expect(res.status).toBe(400);
161
+ expect(mocked.provisionTunnel).not.toHaveBeenCalled();
162
+ });
163
+ it('POST /provision provisions and reports the hostname', async () => {
164
+ mocked.provisionTunnel.mockResolvedValue(STATE);
165
+ const res = await request(server, 'POST', '/remote/provision', { mahlas: 'workstation' });
166
+ expect(res.status).toBe(200);
167
+ expect(res.json.hostname).toBe('workstation.repoburg.com');
168
+ expect(mocked.provisionTunnel).toHaveBeenCalledWith('workstation');
169
+ });
170
+ it('POST /provision maps ProvisioningError status codes', async () => {
171
+ const err = new Error('taken');
172
+ err.statusCode = 409;
173
+ mocked.provisionTunnel.mockRejectedValue(err);
174
+ const res = await request(server, 'POST', '/remote/provision', { mahlas: 'x' });
175
+ expect(res.status).toBe(409);
176
+ });
177
+ it('POST /install requires a provisioned tunnel', async () => {
178
+ mocked.loadTunnelState.mockReturnValue(null);
179
+ const res = await request(server, 'POST', '/remote/install');
180
+ expect(res.status).toBe(409);
181
+ });
182
+ it('POST /install downloads (when missing), writes the service, and probes metrics connectivity', async () => {
183
+ mocked.loadTunnelState.mockReturnValue(STATE);
184
+ mocked.isInstalled.mockReturnValue(false);
185
+ mocked.downloadCloudflared.mockResolvedValue('/bin/cloudflared');
186
+ mocked.installService.mockResolvedValue('/unit/path');
187
+ mocked.isServiceRunning.mockResolvedValue(true);
188
+ global.fetch = makeFetch();
189
+ const res = await request(server, 'POST', '/remote/install');
190
+ expect(res.status).toBe(200);
191
+ expect(mocked.downloadCloudflared).toHaveBeenCalled();
192
+ expect(mocked.installService).toHaveBeenCalledWith('connector-token');
193
+ expect(res.json.connection).toEqual({ ok: true });
194
+ expect(res.json.message).toContain('https://workstation.repoburg.com');
195
+ });
196
+ it('POST /install skips the download when cloudflared is present', async () => {
197
+ mocked.loadTunnelState.mockReturnValue(STATE);
198
+ mocked.isInstalled.mockReturnValue(true);
199
+ mocked.installService.mockResolvedValue('/unit/path');
200
+ mocked.isServiceRunning.mockResolvedValue(true);
201
+ global.fetch = makeFetch({ metricsBody: 'cloudflared_tunnel_ha_connections{region="us"} 0\n' });
202
+ const res = await request(server, 'POST', '/remote/install');
203
+ expect(mocked.downloadCloudflared).not.toHaveBeenCalled();
204
+ expect(res.json.connection).toEqual({ ok: false });
205
+ expect(res.json.message).toContain('no live connections');
206
+ });
207
+ it('POST /access-config no longer exists (Access is provisioned automatically)', async () => {
208
+ const res = await request(server, 'POST', '/remote/access-config', { teamDomain: 't', aud: 'a' });
209
+ expect(res.status).toBe(404);
210
+ expect(mocked.updateTunnelState).not.toHaveBeenCalled();
211
+ });
212
+ it('DELETE /uninstall removes service, server tunnel, and local state', async () => {
213
+ mocked.removeService.mockResolvedValue(undefined);
214
+ mocked.uninstallTunnel.mockResolvedValue(undefined);
215
+ const res = await request(server, 'DELETE', '/remote/uninstall');
216
+ expect(res.status).toBe(200);
217
+ expect(mocked.removeService).toHaveBeenCalled();
218
+ expect(mocked.uninstallTunnel).toHaveBeenCalled();
219
+ expect(mocked.clearTunnelState).toHaveBeenCalled();
220
+ });
221
+ it('POST /enable validates the mahlas and starts the orchestration job', async () => {
222
+ orchestrator_1.startEnableJob.mockReturnValue({ id: 'job-1', state: 'running' });
223
+ const res = await request(server, 'POST', '/remote/enable', { mahlas: 'my-box' });
224
+ expect(res.status).toBe(200);
225
+ expect(res.json.id).toBe('job-1');
226
+ expect(orchestrator_1.startEnableJob).toHaveBeenCalledWith('my-box');
227
+ });
228
+ it('POST /enable rejects an invalid mahlas', async () => {
229
+ orchestrator_1.startEnableJob.mockClear();
230
+ const res = await request(server, 'POST', '/remote/enable', { mahlas: 'BAD NAME' });
231
+ expect(res.status).toBe(400);
232
+ expect(orchestrator_1.startEnableJob).not.toHaveBeenCalled();
233
+ });
234
+ it('GET /enable returns the current (or last) job', async () => {
235
+ orchestrator_1.getEnableJob.mockReturnValue({ id: 'job-9', state: 'ready' });
236
+ const res = await request(server, 'GET', '/remote/enable');
237
+ expect(res.status).toBe(200);
238
+ expect(res.json.id).toBe('job-9');
239
+ });
240
+ it('GET /machines lists the account-server machines', async () => {
241
+ mocked.listTunnels.mockResolvedValue([{ id: '1', mahlas: 'workstation', hostname: 'workstation.repoburg.com', status: 'active' }]);
242
+ const res = await request(server, 'GET', '/remote/machines');
243
+ expect(res.json).toHaveLength(1);
244
+ });
245
+ });
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.remoteStatePath = remoteStatePath;
7
+ exports.loadTunnelState = loadTunnelState;
8
+ exports.saveTunnelState = saveTunnelState;
9
+ exports.updateTunnelState = updateTunnelState;
10
+ exports.clearTunnelState = clearTunnelState;
11
+ /**
12
+ * Tunnel state persisted on the machine (~/.repoburg/remote.json).
13
+ *
14
+ * The tunnel token stored here is BY DESIGN machine-local: it is the
15
+ * single-purpose Cloudflare connector token that can only run this one
16
+ * tunnel — it cannot create, read, or delete tunnels, DNS, or anything else.
17
+ */
18
+ const fs_extra_1 = __importDefault(require("fs-extra"));
19
+ const path_1 = __importDefault(require("path"));
20
+ const os_1 = __importDefault(require("os"));
21
+ function remoteStatePath(home = os_1.default.homedir()) {
22
+ return path_1.default.join(home, '.repoburg', 'remote.json');
23
+ }
24
+ function loadTunnelState(home = os_1.default.homedir()) {
25
+ const statePath = remoteStatePath(home);
26
+ if (!fs_extra_1.default.existsSync(statePath))
27
+ return null;
28
+ try {
29
+ const state = fs_extra_1.default.readJsonSync(statePath);
30
+ if (!state.hostname || !state.tunnelToken || !state.accountTunnelId)
31
+ return null;
32
+ return state;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ function saveTunnelState(state, home = os_1.default.homedir()) {
39
+ const statePath = remoteStatePath(home);
40
+ fs_extra_1.default.ensureDirSync(path_1.default.dirname(statePath));
41
+ fs_extra_1.default.writeJsonSync(statePath, state, { spaces: 2 });
42
+ }
43
+ function updateTunnelState(patch, home = os_1.default.homedir()) {
44
+ const current = loadTunnelState(home);
45
+ if (!current)
46
+ return null;
47
+ const next = { ...current, ...patch };
48
+ saveTunnelState(next, home);
49
+ return next;
50
+ }
51
+ function clearTunnelState(home = os_1.default.homedir()) {
52
+ const statePath = remoteStatePath(home);
53
+ fs_extra_1.default.removeSync(statePath);
54
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const tunnel_state_1 = require("./tunnel-state");
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const os_1 = __importDefault(require("os"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const tmpHome = fs_extra_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'remote-state-'));
11
+ afterEach(() => (0, tunnel_state_1.clearTunnelState)(tmpHome));
12
+ describe('tunnel state', () => {
13
+ const state = {
14
+ mahlas: 'workstation',
15
+ hostname: 'workstation.repoburg.com',
16
+ accountTunnelId: 'tunnel-uuid',
17
+ tunnelToken: 'connector-token',
18
+ accessTeamDomain: 'team.cloudflareaccess.com',
19
+ accessAud: 'aud-tag',
20
+ ownerEmail: 'owner@repoburg.com',
21
+ };
22
+ it('round-trips the state to ~/.repoburg/remote.json', () => {
23
+ (0, tunnel_state_1.saveTunnelState)(state, tmpHome);
24
+ expect((0, tunnel_state_1.loadTunnelState)(tmpHome)).toEqual(state);
25
+ expect(fs_extra_1.default.existsSync((0, tunnel_state_1.remoteStatePath)(tmpHome))).toBe(true);
26
+ });
27
+ it('returns null when no state exists', () => {
28
+ expect((0, tunnel_state_1.loadTunnelState)(tmpHome)).toBeNull();
29
+ });
30
+ it('returns null for malformed or incomplete state files', () => {
31
+ (0, tunnel_state_1.saveTunnelState)(state, tmpHome);
32
+ fs_extra_1.default.writeJsonSync((0, tunnel_state_1.remoteStatePath)(tmpHome), { hostname: 'x.repoburg.com' }); // missing fields
33
+ expect((0, tunnel_state_1.loadTunnelState)(tmpHome)).toBeNull();
34
+ });
35
+ it('clears the state file', () => {
36
+ (0, tunnel_state_1.saveTunnelState)(state, tmpHome);
37
+ (0, tunnel_state_1.clearTunnelState)(tmpHome);
38
+ expect((0, tunnel_state_1.loadTunnelState)(tmpHome)).toBeNull();
39
+ });
40
+ });
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolvePaneFrontendDist = resolvePaneFrontendDist;
7
+ exports.createPaneFrontendRouter = createPaneFrontendRouter;
8
+ /**
9
+ * Static serving for the pane frontend SPA.
10
+ *
11
+ * Spec: a machine's daemon serves the pane at https://<mahlas>.repoburg.com
12
+ * from its own origin — SPA and API share one origin (no CORS, no
13
+ * mixed-content). The router is mounted LAST: every API path (/api, /ws,
14
+ * /services, /auth, /registry, /mcp, /remote) is matched by earlier
15
+ * routers and never reaches the fallback.
16
+ *
17
+ * Path resolution is ABSOLUTE (pm2 God-daemon-cwd bug — see agentPane.ts).
18
+ */
19
+ const express_1 = __importDefault(require("express"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const fs_extra_1 = __importDefault(require("fs-extra"));
22
+ /** Resolve the pane frontend dist from the daemon's module directory. */
23
+ function resolvePaneFrontendDist(fromDir) {
24
+ return path_1.default.resolve(fromDir, '../../agent-pane/frontend/dist');
25
+ }
26
+ /**
27
+ * Build the static router: assets from dist, SPA fallback to index.html for
28
+ * client-side routes. Returns null when the dist bundle is missing (dev
29
+ * installs serve the frontend via vite instead).
30
+ */
31
+ function createPaneFrontendRouter(distPath) {
32
+ const indexHtml = path_1.default.join(distPath, 'index.html');
33
+ if (!fs_extra_1.default.existsSync(indexHtml)) {
34
+ return null;
35
+ }
36
+ const router = express_1.default.Router();
37
+ // Static assets with fallthrough so the SPA fallback handles routes.
38
+ router.use(express_1.default.static(distPath, { fallthrough: true }));
39
+ // SPA fallback: any GET that no earlier route handled becomes the app.
40
+ router.get('*', (_req, res) => {
41
+ res.sendFile(indexHtml);
42
+ });
43
+ return router;
44
+ }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const http_1 = __importDefault(require("http"));
7
+ const express_1 = __importDefault(require("express"));
8
+ const fs_extra_1 = __importDefault(require("fs-extra"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const staticServing_1 = require("./staticServing");
12
+ const tmpDist = fs_extra_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'pane-dist-'));
13
+ // Fake dist bundle: index.html + one asset.
14
+ fs_extra_1.default.writeFileSync(path_1.default.join(tmpDist, 'index.html'), '<!DOCTYPE html><html><body>pane-spa</body></html>');
15
+ fs_extra_1.default.ensureDirSync(path_1.default.join(tmpDist, 'assets'));
16
+ fs_extra_1.default.writeFileSync(path_1.default.join(tmpDist, 'assets', 'app.js'), 'console.log("pane");');
17
+ function buildApp() {
18
+ const app = (0, express_1.default)();
19
+ // API routes are matched before the static router — simulate the delegate.
20
+ app.all('/api/*', (_req, res) => {
21
+ res.status(503).json({ error: 'pane unavailable' });
22
+ });
23
+ app.get('/health', (_req, res) => {
24
+ res.status(200).json({ status: 'ok' });
25
+ });
26
+ app.use((0, staticServing_1.createPaneFrontendRouter)(tmpDist));
27
+ return app.listen(0, '127.0.0.1');
28
+ }
29
+ function request(server, method, url) {
30
+ const { port } = server.address();
31
+ return new Promise((resolve, reject) => {
32
+ const req = http_1.default.request({ host: '127.0.0.1', port, method, path: url }, (res) => {
33
+ let data = '';
34
+ res.on('data', (chunk) => (data += chunk));
35
+ res.on('end', () => resolve({ status: res.statusCode ?? 0, body: data, type: res.headers['content-type'] }));
36
+ });
37
+ req.on('error', reject);
38
+ req.end();
39
+ });
40
+ }
41
+ describe('staticServing', () => {
42
+ let server;
43
+ beforeAll(async () => {
44
+ server = await new Promise((resolve) => {
45
+ const s = buildApp();
46
+ s.on('listening', () => resolve(s));
47
+ });
48
+ });
49
+ afterAll(() => new Promise((resolve) => {
50
+ server.close(() => resolve());
51
+ fs_extra_1.default.removeSync(tmpDist);
52
+ }));
53
+ it('serves the SPA index at the root', async () => {
54
+ const res = await request(server, 'GET', '/');
55
+ expect(res.status).toBe(200);
56
+ expect(res.body).toContain('pane-spa');
57
+ });
58
+ it('serves static assets with correct content type', async () => {
59
+ const res = await request(server, 'GET', '/assets/app.js');
60
+ expect(res.status).toBe(200);
61
+ expect(res.body).toContain('pane');
62
+ expect(res.type).toContain('javascript');
63
+ });
64
+ it('falls back to the SPA for client-side routes', async () => {
65
+ const res = await request(server, 'GET', '/sessions/abc-123');
66
+ expect(res.status).toBe(200);
67
+ expect(res.body).toContain('pane-spa');
68
+ });
69
+ it('never shadows /api paths (handled by the pane delegate first)', async () => {
70
+ const res = await request(server, 'GET', '/api/sessions');
71
+ expect(res.status).toBe(503);
72
+ });
73
+ it('never shadows /health', async () => {
74
+ const res = await request(server, 'GET', '/health');
75
+ expect(res.status).toBe(200);
76
+ expect(JSON.parse(res.body)).toEqual({ status: 'ok' });
77
+ });
78
+ it('resolves the dist path absolutely from the daemon module directory', () => {
79
+ expect((0, staticServing_1.resolvePaneFrontendDist)('/repo/daemon/src')).toBe(path_1.default.resolve('/repo/agent-pane/frontend/dist'));
80
+ expect((0, staticServing_1.resolvePaneFrontendDist)('/repo/daemon/dist')).toBe(path_1.default.resolve('/repo/agent-pane/frontend/dist'));
81
+ });
82
+ it('returns null when the dist bundle is missing', () => {
83
+ expect((0, staticServing_1.createPaneFrontendRouter)('/nonexistent/pane/dist')).toBeNull();
84
+ });
85
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repoburg",
3
- "version": "1.3.217",
3
+ "version": "1.3.218",
4
4
  "description": "A local AI-powered software developer assistant that runs on your own machine.",
5
5
  "author": "Celal Ertug",
6
6
  "license": "SEE LICENSE IN LICENSE",