@pixelbyte-software/pixcode 1.51.7 → 1.51.9

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 (307) hide show
  1. package/CODE_OF_CONDUCT.md +41 -41
  2. package/CONTRIBUTING.md +155 -155
  3. package/LICENSE +718 -718
  4. package/README.de.md +169 -169
  5. package/README.ja.md +167 -167
  6. package/README.ko.md +167 -167
  7. package/README.md +419 -419
  8. package/README.ru.md +169 -169
  9. package/README.tr.md +298 -298
  10. package/README.zh-CN.md +167 -167
  11. package/SECURITY.md +46 -46
  12. package/dist/api-automation.html +110 -110
  13. package/dist/api-docs.html +548 -548
  14. package/dist/assets/{index-BvQgSoyC.js → index-Gs7C4hZT.js} +70 -70
  15. package/dist/clear-cache.html +85 -85
  16. package/dist/convert-icons.md +52 -52
  17. package/dist/docs.html +308 -308
  18. package/dist/favicon.svg +8 -8
  19. package/dist/features.html +133 -133
  20. package/dist/generate-icons.js +48 -48
  21. package/dist/humans.txt +15 -15
  22. package/dist/icons/codex-white.svg +3 -3
  23. package/dist/icons/codex.svg +3 -3
  24. package/dist/icons/cursor-white.svg +11 -11
  25. package/dist/icons/icon-128x128.svg +9 -9
  26. package/dist/icons/icon-144x144.svg +9 -9
  27. package/dist/icons/icon-152x152.svg +9 -9
  28. package/dist/icons/icon-192x192.svg +9 -9
  29. package/dist/icons/icon-384x384.svg +9 -9
  30. package/dist/icons/icon-512x512.svg +9 -9
  31. package/dist/icons/icon-72x72.svg +9 -9
  32. package/dist/icons/icon-96x96.svg +9 -9
  33. package/dist/icons/icon-template.svg +9 -9
  34. package/dist/icons/qwen-logo.svg +14 -14
  35. package/dist/index.html +58 -58
  36. package/dist/landing.html +268 -268
  37. package/dist/llms-full.txt +119 -119
  38. package/dist/llms.txt +53 -53
  39. package/dist/logo.svg +12 -12
  40. package/dist/manifest.json +60 -60
  41. package/dist/openapi.yaml +1696 -1696
  42. package/dist/orchestration.html +125 -125
  43. package/dist/robots.txt +4 -4
  44. package/dist/site.css +692 -692
  45. package/dist/sitemap.xml +51 -51
  46. package/dist/sw.js +132 -132
  47. package/dist-server/server/cli.js +96 -96
  48. package/dist-server/server/daemon/manager.js +33 -33
  49. package/dist-server/server/daemon-manager.js +64 -64
  50. package/dist-server/server/index.js +91 -65
  51. package/dist-server/server/index.js.map +1 -1
  52. package/dist-server/server/routes/commands.js +25 -25
  53. package/dist-server/server/routes/git.js +17 -17
  54. package/dist-server/server/routes/live-view.js +46 -46
  55. package/dist-server/server/services/hermes-gateway.js +5 -5
  56. package/dist-server/server/services/public-api-manifest.js +51 -51
  57. package/package.json +222 -222
  58. package/scripts/fix-node-pty.js +67 -67
  59. package/scripts/github/create-v1.38-issues.mjs +351 -351
  60. package/scripts/github/create-vscode-workbench-issues.mjs +121 -121
  61. package/scripts/hermes/configure-pixcode-mcp.mjs +165 -165
  62. package/scripts/hermes/pixcode-mcp-server.mjs +1009 -1009
  63. package/scripts/smoke/changes-panel-layout.mjs +48 -48
  64. package/scripts/smoke/chat-composer-fixed-layout.mjs +55 -55
  65. package/scripts/smoke/chat-message-timeline-order.mjs +41 -41
  66. package/scripts/smoke/chat-realtime-hydration.mjs +44 -44
  67. package/scripts/smoke/chat-session-provider-pools.mjs +35 -35
  68. package/scripts/smoke/chat-session-state.mjs +19 -19
  69. package/scripts/smoke/code-editor-theme.mjs +55 -55
  70. package/scripts/smoke/code-editor-vscode-engine.mjs +91 -91
  71. package/scripts/smoke/command-center-agent-writes.mjs +79 -79
  72. package/scripts/smoke/command-center-non-git.mjs +46 -46
  73. package/scripts/smoke/context-packet.mjs +43 -43
  74. package/scripts/smoke/control-room-ux-redesign.mjs +91 -91
  75. package/scripts/smoke/daemon-entrypoint.mjs +20 -20
  76. package/scripts/smoke/default-landing-routing.mjs +33 -33
  77. package/scripts/smoke/desktop-native-notifications.mjs +30 -30
  78. package/scripts/smoke/desktop-tray-icon.mjs +33 -33
  79. package/scripts/smoke/discord-release-workflow.mjs +24 -24
  80. package/scripts/smoke/git-install-update.mjs +255 -255
  81. package/scripts/smoke/handoff-artifact-protocol.mjs +50 -50
  82. package/scripts/smoke/hermes-api-install.mjs +56 -56
  83. package/scripts/smoke/hermes-gateway-persistence.mjs +104 -104
  84. package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +426 -426
  85. package/scripts/smoke/hermes-rest-chat-api.mjs +162 -162
  86. package/scripts/smoke/hermes-rest-chat-live.mjs +45 -45
  87. package/scripts/smoke/hermes-rest-codex-launch.mjs +209 -209
  88. package/scripts/smoke/hermes-rest-gateway.mjs +79 -79
  89. package/scripts/smoke/hermes-rest-live.mjs +42 -42
  90. package/scripts/smoke/hermes-roundtrip.mjs +167 -167
  91. package/scripts/smoke/hermes-settings-commands.mjs +349 -349
  92. package/scripts/smoke/hermes-smoke-launcher-guard.mjs +34 -34
  93. package/scripts/smoke/live-view-diagnostics.mjs +53 -53
  94. package/scripts/smoke/live-view-environment.mjs +92 -92
  95. package/scripts/smoke/live-view-integration.mjs +450 -450
  96. package/scripts/smoke/mac-desktop-runtime.mjs +37 -37
  97. package/scripts/smoke/mobile-tunnel-guidance.mjs +29 -29
  98. package/scripts/smoke/model-registry.mjs +36 -36
  99. package/scripts/smoke/multi-project-ui.mjs +45 -45
  100. package/scripts/smoke/multi-worker-slots.mjs +42 -42
  101. package/scripts/smoke/notification-center.mjs +87 -87
  102. package/scripts/smoke/notification-inapp-preference.mjs +23 -23
  103. package/scripts/smoke/notification-taxonomy.mjs +58 -58
  104. package/scripts/smoke/orchestration-api.mjs +172 -172
  105. package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -33
  106. package/scripts/smoke/orchestration-live-run.mjs +176 -176
  107. package/scripts/smoke/orchestration-mobile-scroll.mjs +29 -29
  108. package/scripts/smoke/orchestration-model-sync.mjs +30 -30
  109. package/scripts/smoke/orchestration-permission-fallback.mjs +34 -34
  110. package/scripts/smoke/orchestration-runtime-guards.mjs +48 -48
  111. package/scripts/smoke/orchestration-user-facing-output.mjs +25 -25
  112. package/scripts/smoke/permission-policy.mjs +50 -50
  113. package/scripts/smoke/pixcode-workbench-1-48.mjs +167 -167
  114. package/scripts/smoke/provider-models-opencode-live.mjs +66 -66
  115. package/scripts/smoke/provider-rest-api.mjs +124 -124
  116. package/scripts/smoke/provider-selection-status.mjs +52 -52
  117. package/scripts/smoke/run-state-refresh.mjs +52 -52
  118. package/scripts/smoke/runtime-manager.mjs +99 -99
  119. package/scripts/smoke/shell-manual-disconnect.mjs +30 -30
  120. package/scripts/smoke/side-panel-editor-layout.mjs +34 -34
  121. package/scripts/smoke/static-root-routing.mjs +21 -21
  122. package/scripts/smoke/strict-handoff-compact.mjs +60 -60
  123. package/scripts/smoke/taskmaster-config.mjs +24 -24
  124. package/scripts/smoke/taskmaster-execution-telegram.mjs +3 -3
  125. package/scripts/smoke/taskmaster-onboarding.mjs +3 -3
  126. package/scripts/smoke/taskmaster-run-graph.mjs +3 -3
  127. package/scripts/smoke/telegram-control.mjs +242 -242
  128. package/scripts/smoke/tunnel-persistence.mjs +56 -56
  129. package/scripts/smoke/update-issue-progress.mjs +69 -69
  130. package/scripts/smoke/update-ux.mjs +55 -55
  131. package/scripts/smoke/v138-completion.mjs +132 -132
  132. package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -69
  133. package/scripts/smoke/v138-diagnostics.mjs +63 -63
  134. package/scripts/smoke/v138-issue-planner.mjs +33 -33
  135. package/scripts/smoke/v143-remote-control.mjs +76 -76
  136. package/scripts/smoke/v144-production-loop.mjs +47 -47
  137. package/scripts/smoke/v145-platformization.mjs +46 -46
  138. package/scripts/smoke/v146-control-room-ui.mjs +150 -150
  139. package/scripts/smoke/version-modal-autoshow.mjs +29 -29
  140. package/scripts/smoke/vscode-workbench-layout.mjs +63 -63
  141. package/scripts/smoke/vscode-workbench-polish.mjs +461 -461
  142. package/scripts/smoke/workflow-fallback-replay.mjs +56 -56
  143. package/scripts/smoke/workflow-templates.mjs +43 -43
  144. package/scripts/smoke/workflow-trace-timeline.mjs +46 -46
  145. package/scripts/update-git-install.mjs +283 -283
  146. package/server/claude-sdk.js +920 -920
  147. package/server/cli.js +1035 -1035
  148. package/server/constants/config.js +4 -4
  149. package/server/cursor-cli.js +344 -344
  150. package/server/daemon/manager.js +563 -563
  151. package/server/daemon-manager.js +964 -964
  152. package/server/database/db.js +895 -895
  153. package/server/database/json-store.js +197 -197
  154. package/server/gemini-cli.js +550 -550
  155. package/server/gemini-response-handler.js +79 -79
  156. package/server/index.js +111 -81
  157. package/server/load-env.js +35 -35
  158. package/server/middleware/auth.js +155 -155
  159. package/server/modules/orchestration/a2a/adapter-registry.ts +108 -108
  160. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +63 -63
  161. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +286 -286
  162. package/server/modules/orchestration/a2a/adapters/codex.adapter.ts +244 -244
  163. package/server/modules/orchestration/a2a/adapters/cursor.adapter.ts +249 -249
  164. package/server/modules/orchestration/a2a/adapters/gemini.adapter.ts +248 -248
  165. package/server/modules/orchestration/a2a/adapters/json-event.adapter.ts +100 -100
  166. package/server/modules/orchestration/a2a/adapters/opencode.adapter.ts +248 -248
  167. package/server/modules/orchestration/a2a/adapters/qwen.adapter.ts +248 -248
  168. package/server/modules/orchestration/a2a/agent-card.ts +55 -55
  169. package/server/modules/orchestration/a2a/routes.ts +590 -590
  170. package/server/modules/orchestration/a2a/task-store.ts +178 -178
  171. package/server/modules/orchestration/a2a/types.ts +126 -126
  172. package/server/modules/orchestration/a2a/validator.ts +113 -113
  173. package/server/modules/orchestration/hermes/hermes.routes.ts +642 -642
  174. package/server/modules/orchestration/index.ts +101 -101
  175. package/server/modules/orchestration/preview/port-watcher.ts +112 -112
  176. package/server/modules/orchestration/preview/preview-proxy.ts +60 -60
  177. package/server/modules/orchestration/preview/types.ts +19 -19
  178. package/server/modules/orchestration/security/permission-policy.ts +401 -401
  179. package/server/modules/orchestration/tasks/orchestration-task-store.ts +41 -41
  180. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +64 -64
  181. package/server/modules/orchestration/tasks/orchestration-task.service.ts +209 -209
  182. package/server/modules/orchestration/tasks/orchestration-task.types.ts +40 -40
  183. package/server/modules/orchestration/tasks/task-run-graph.ts +155 -155
  184. package/server/modules/orchestration/workflows/approval-queue.ts +106 -106
  185. package/server/modules/orchestration/workflows/built-in-workflows.ts +127 -127
  186. package/server/modules/orchestration/workflows/context-packet.ts +186 -186
  187. package/server/modules/orchestration/workflows/handoff-artifact.ts +175 -175
  188. package/server/modules/orchestration/workflows/workflow-fallback-policy.ts +161 -161
  189. package/server/modules/orchestration/workflows/workflow-replay.ts +254 -254
  190. package/server/modules/orchestration/workflows/workflow-runner.ts +2070 -2070
  191. package/server/modules/orchestration/workflows/workflow-store.ts +97 -97
  192. package/server/modules/orchestration/workflows/workflow-templates.ts +272 -272
  193. package/server/modules/orchestration/workflows/workflow-trace.ts +424 -424
  194. package/server/modules/orchestration/workflows/workflow.routes.ts +586 -586
  195. package/server/modules/orchestration/workflows/workflow.types.ts +111 -111
  196. package/server/modules/orchestration/workflows/workspace-target.ts +122 -122
  197. package/server/modules/orchestration/workspace/docker-workspace.ts +136 -136
  198. package/server/modules/orchestration/workspace/path-safety.ts +55 -55
  199. package/server/modules/orchestration/workspace/types.ts +52 -52
  200. package/server/modules/orchestration/workspace/workspace-manager.ts +102 -102
  201. package/server/modules/orchestration/workspace/worktree-workspace.ts +126 -126
  202. package/server/modules/providers/index.ts +2 -2
  203. package/server/modules/providers/list/claude/claude-auth.provider.ts +146 -146
  204. package/server/modules/providers/list/claude/claude-mcp.provider.ts +135 -135
  205. package/server/modules/providers/list/claude/claude-sessions.provider.ts +306 -306
  206. package/server/modules/providers/list/claude/claude.provider.ts +15 -15
  207. package/server/modules/providers/list/codex/codex-auth.provider.ts +117 -117
  208. package/server/modules/providers/list/codex/codex-mcp.provider.ts +135 -135
  209. package/server/modules/providers/list/codex/codex-sessions.provider.ts +319 -319
  210. package/server/modules/providers/list/codex/codex.provider.ts +15 -15
  211. package/server/modules/providers/list/cursor/cursor-auth.provider.ts +147 -147
  212. package/server/modules/providers/list/cursor/cursor-mcp.provider.ts +108 -108
  213. package/server/modules/providers/list/cursor/cursor-sessions.provider.ts +421 -421
  214. package/server/modules/providers/list/cursor/cursor.provider.ts +15 -15
  215. package/server/modules/providers/list/gemini/gemini-auth.provider.ts +173 -173
  216. package/server/modules/providers/list/gemini/gemini-mcp.provider.ts +110 -110
  217. package/server/modules/providers/list/gemini/gemini-sessions.provider.ts +227 -227
  218. package/server/modules/providers/list/gemini/gemini.provider.ts +15 -15
  219. package/server/modules/providers/list/opencode/opencode-auth.provider.ts +131 -131
  220. package/server/modules/providers/list/opencode/opencode-mcp.provider.ts +126 -126
  221. package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +286 -286
  222. package/server/modules/providers/list/opencode/opencode.provider.ts +29 -29
  223. package/server/modules/providers/list/qwen/qwen-auth.provider.ts +146 -146
  224. package/server/modules/providers/list/qwen/qwen-mcp.provider.ts +114 -114
  225. package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +265 -265
  226. package/server/modules/providers/list/qwen/qwen.provider.ts +21 -21
  227. package/server/modules/providers/provider.registry.ts +40 -40
  228. package/server/modules/providers/provider.routes.ts +891 -891
  229. package/server/modules/providers/services/mcp.service.ts +86 -86
  230. package/server/modules/providers/services/provider-auth.service.ts +26 -26
  231. package/server/modules/providers/services/sessions.service.ts +45 -45
  232. package/server/modules/providers/shared/base/abstract.provider.ts +20 -20
  233. package/server/modules/providers/shared/mcp/mcp.provider.ts +151 -151
  234. package/server/modules/providers/shared/provider-configs.ts +142 -142
  235. package/server/modules/providers/tests/mcp.test.ts +293 -293
  236. package/server/openai-codex.js +462 -462
  237. package/server/opencode-cli.js +491 -491
  238. package/server/opencode-response-handler.js +111 -111
  239. package/server/projects.js +3008 -3008
  240. package/server/qwen-code-cli.js +410 -410
  241. package/server/qwen-response-handler.js +73 -73
  242. package/server/routes/agent.js +1435 -1435
  243. package/server/routes/auth.js +142 -142
  244. package/server/routes/codex.js +20 -20
  245. package/server/routes/commands.js +538 -538
  246. package/server/routes/cursor.js +61 -61
  247. package/server/routes/diagnostics.js +41 -41
  248. package/server/routes/gemini.js +25 -25
  249. package/server/routes/git.js +1641 -1641
  250. package/server/routes/live-view.js +387 -387
  251. package/server/routes/mcp-utils.js +13 -13
  252. package/server/routes/messages.js +50 -50
  253. package/server/routes/network.js +117 -117
  254. package/server/routes/platformization.js +182 -182
  255. package/server/routes/plugins.js +320 -320
  256. package/server/routes/production-agent-loop.js +90 -90
  257. package/server/routes/projects.js +911 -911
  258. package/server/routes/public-api.js +34 -34
  259. package/server/routes/qwen.js +27 -27
  260. package/server/routes/remote.js +50 -50
  261. package/server/routes/settings.js +321 -321
  262. package/server/routes/telegram.js +140 -140
  263. package/server/routes/user.js +125 -125
  264. package/server/routes/webhooks.js +63 -63
  265. package/server/services/control-room.js +102 -102
  266. package/server/services/diagnostics.js +165 -165
  267. package/server/services/external-access.js +375 -375
  268. package/server/services/hermes-gateway.js +1562 -1562
  269. package/server/services/hermes-install-jobs.js +729 -729
  270. package/server/services/install-jobs.js +715 -715
  271. package/server/services/live-view.js +956 -956
  272. package/server/services/managed-runtimes.js +493 -493
  273. package/server/services/model-registry.js +144 -144
  274. package/server/services/notification-orchestrator.js +365 -365
  275. package/server/services/notification-taxonomy.js +204 -204
  276. package/server/services/platformization.js +773 -773
  277. package/server/services/production-agent-loop.js +248 -248
  278. package/server/services/provider-cli-versions.js +149 -149
  279. package/server/services/provider-credentials.js +189 -189
  280. package/server/services/provider-models.js +396 -396
  281. package/server/services/public-api-manifest.js +190 -190
  282. package/server/services/remote-connection.js +127 -127
  283. package/server/services/runtime-manager.js +323 -323
  284. package/server/services/startup-update.js +234 -234
  285. package/server/services/telegram/bot.js +331 -331
  286. package/server/services/telegram/control-center.js +979 -979
  287. package/server/services/telegram/telegram-http-client.js +151 -151
  288. package/server/services/telegram/translations.js +340 -340
  289. package/server/services/vapid-keys.js +36 -36
  290. package/server/services/webhooks.js +216 -216
  291. package/server/sessionManager.js +225 -225
  292. package/server/shared/interfaces.ts +54 -54
  293. package/server/shared/types.ts +172 -172
  294. package/server/shared/utils.ts +193 -193
  295. package/server/tsconfig.json +36 -36
  296. package/server/utils/colors.js +21 -21
  297. package/server/utils/commandParser.js +305 -305
  298. package/server/utils/frontmatter.js +18 -18
  299. package/server/utils/gitConfig.js +34 -34
  300. package/server/utils/plugin-loader.js +457 -457
  301. package/server/utils/plugin-process-manager.js +185 -185
  302. package/server/utils/port-access.js +209 -209
  303. package/server/utils/runtime-paths.js +37 -37
  304. package/server/utils/url-detection.js +71 -71
  305. package/server/vite-daemon.js +79 -79
  306. package/shared/modelConstants.js +161 -161
  307. package/shared/networkHosts.js +22 -22
@@ -1,10 +1,10 @@
1
- import { promises as fs } from 'node:fs';
2
- import path from 'node:path';
3
-
4
- import express from 'express';
5
-
6
- import { extractProjectDirectory } from '../projects.js';
7
- import { getTunnelState } from '../services/external-access.js';
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import express from 'express';
5
+
6
+ import { extractProjectDirectory } from '../projects.js';
7
+ import { getTunnelState } from '../services/external-access.js';
8
8
  import {
9
9
  getLiveViewSessionByShareId,
10
10
  getLiveViewState,
@@ -13,211 +13,211 @@ import {
13
13
  stopLiveView,
14
14
  } from '../services/live-view.js';
15
15
  import { userHasProjectPathAccess } from '../services/platformization.js';
16
-
17
- const router = express.Router();
18
-
19
- function requestBaseUrl(req) {
20
- return `${req.protocol}://${req.get('host')}`;
21
- }
22
-
23
- function buildUrls(req, session) {
24
- if (!session?.sharePath) return { local: null, external: null, preferred: null };
25
- const local = `${requestBaseUrl(req)}${session.sharePath}`;
26
- const tunnel = getTunnelState();
27
- const external = tunnel?.running && tunnel.url ? `${tunnel.url}${session.sharePath}` : null;
28
- return {
29
- local,
30
- external,
31
- preferred: external || local,
32
- };
33
- }
34
-
35
- function buildEnvironmentTunnel(tunnel, urls) {
36
- const active = Boolean(tunnel?.running && urls?.external);
37
- return {
38
- status: active ? 'active' : 'local-only',
39
- url: active ? urls.external : null,
40
- localUrl: urls?.local || null,
41
- preferredUrl: urls?.preferred || urls?.local || null,
42
- };
43
- }
44
-
45
- function attachEnvironmentRuntimeState(environment, urls, tunnel) {
46
- if (!environment) return environment;
47
- return {
48
- ...environment,
49
- urls,
50
- tunnel: buildEnvironmentTunnel(tunnel, urls),
51
- diagnostics: {
52
- ...environment.diagnostics,
53
- publicTunnelReady: Boolean(tunnel?.running && urls?.external),
54
- },
55
- };
56
- }
57
-
58
- function escapeHtml(value) {
59
- return String(value ?? '')
60
- .replaceAll('&', '&')
61
- .replaceAll('<', '&lt;')
62
- .replaceAll('>', '&gt;')
63
- .replaceAll('"', '&quot;')
64
- .replaceAll("'", '&#39;');
65
- }
66
-
67
- function latestErrorFromLogs(logs) {
68
- return [...logs].reverse().find((line) => /error|enoent|eaddrinuse|failed|fatal|exception/i.test(line)) || null;
69
- }
70
-
71
- function buildLiveViewSuggestions(session, reason) {
72
- const framework = String(session?.framework || session?.label || '').toLowerCase();
73
- const status = String(session?.status || '').toLowerCase();
74
- const suggestions = [];
75
-
76
- if (framework.includes('php')) {
77
- suggestions.push('Pixcode can prepare a local PHP runtime automatically and keep it under your user profile.');
78
- suggestions.push('If the automatic runtime download fails, check the Live View panel logs and retry.');
79
- suggestions.push('Check that the project has an index.php or a valid PHP router file in the selected project root.');
80
- } else if (
81
- framework.includes('javascript')
82
- || framework.includes('vite')
83
- || framework.includes('next')
84
- || framework.includes('nuxt')
85
- || framework.includes('astro')
86
- || framework.includes('react')
87
- || framework.includes('node')
88
- ) {
89
- suggestions.push('Run the project command in a terminal first and confirm it opens a local HTTP port.');
90
- suggestions.push('Install project dependencies if node_modules is missing, then retry Live View.');
91
- suggestions.push('If the dev server ignores PORT, set a custom command that binds to 127.0.0.1 and the chosen port.');
92
- } else {
93
- suggestions.push('Run the displayed command in a terminal and fix the first process error shown there.');
94
- suggestions.push('Use a custom command if Pixcode detected the wrong runner for this project.');
95
- }
96
-
97
- if (status === 'starting' || reason === 'upstream_not_ready') {
98
- suggestions.push('Wait for the app to finish booting, then press Refresh or Restart if the port never opens.');
99
- }
100
-
101
- if (reason === 'proxy_failed') {
102
- suggestions.push('The Live View session exists, but Pixcode could not reach the local upstream URL.');
103
- }
104
-
105
- return suggestions;
106
- }
107
-
108
- export function buildLiveViewUnavailablePayload(session, options = {}) {
109
- const logs = Array.isArray(session?.log) ? session.log.slice(-40) : [];
110
- const errorDetail = options.errorMessage || session?.error || latestErrorFromLogs(logs);
111
- const label = session?.label || session?.framework || 'Live View';
112
- const status = session?.status || 'missing';
113
-
114
- return {
115
- error: 'Live View session is not available.',
116
- message: `${label} is ${status}.`,
117
- reason: options.reason || status,
118
- status,
119
- framework: session?.framework || null,
120
- label,
121
- projectName: session?.projectName || null,
122
- shareId: session?.shareId || options.shareId || null,
123
- sharePath: session?.sharePath || null,
124
- upstreamUrl: session?.upstreamUrl || null,
125
- port: session?.port || null,
126
- errorDetail,
127
- diagnostics: {
128
- command: session?.command?.displayCommand || null,
129
- upstreamUrl: session?.upstreamUrl || null,
130
- port: session?.port || null,
131
- startedAt: session?.startedAt || null,
132
- stoppedAt: session?.stoppedAt || null,
133
- exitCode: session?.exitCode ?? null,
134
- exitSignal: session?.exitSignal ?? null,
135
- logs,
136
- },
137
- suggestions: buildLiveViewSuggestions(session, options.reason),
138
- };
139
- }
140
-
141
- export function renderLiveViewDiagnosticHtml(payload) {
142
- const suggestions = payload.suggestions?.length
143
- ? payload.suggestions.map((item) => `<li>${escapeHtml(item)}</li>`).join('')
144
- : '<li>Open the Live View panel, check the latest logs, and restart the session.</li>';
145
- const logs = payload.diagnostics?.logs?.length
146
- ? payload.diagnostics.logs.map((line) => escapeHtml(line)).join('\n')
147
- : 'No process logs were captured yet.';
148
-
149
- return `<!doctype html>
150
- <html lang="en">
151
- <head>
152
- <meta charset="utf-8">
153
- <meta name="viewport" content="width=device-width, initial-scale=1">
154
- <title>Pixcode Live View diagnostics</title>
155
- <style>
156
- :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
157
- body { margin: 0; min-height: 100vh; background: #0b0f14; color: #e5edf5; }
158
- main { box-sizing: border-box; max-width: 980px; margin: 0 auto; padding: 32px 20px; }
159
- .eyebrow { color: #f97316; font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
160
- h1 { margin: 8px 0 10px; font-size: clamp(26px, 5vw, 44px); line-height: 1.05; }
161
- p { color: #a8b3c2; line-height: 1.6; }
162
- section { margin-top: 18px; border: 1px solid rgba(148, 163, 184, .24); border-radius: 12px; background: rgba(15, 23, 42, .72); padding: 16px; }
163
- dl { display: grid; grid-template-columns: minmax(110px, 180px) minmax(0, 1fr); gap: 10px 14px; margin: 0; }
164
- dt { color: #94a3b8; font-size: 12px; text-transform: uppercase; }
165
- dd { margin: 0; overflow-wrap: anywhere; }
166
- pre { margin: 0; max-height: 280px; overflow: auto; white-space: pre-wrap; color: #dbeafe; background: #020617; border-radius: 10px; padding: 14px; }
167
- code { color: #bfdbfe; }
168
- li { margin: 8px 0; color: #d0d8e4; }
169
- </style>
170
- </head>
171
- <body>
172
- <main>
173
- <div class="eyebrow">Pixcode Live View</div>
174
- <h1>${escapeHtml(payload.message)}</h1>
175
- <p>${escapeHtml(payload.errorDetail || 'The preview link is active, but the local runner is not ready.')}</p>
176
- <section>
177
- <dl>
178
- <dt>Status</dt><dd>${escapeHtml(payload.status)}</dd>
179
- <dt>Framework</dt><dd>${escapeHtml(payload.framework || 'Unknown')}</dd>
180
- <dt>Command</dt><dd><code>${escapeHtml(payload.diagnostics?.command || 'No command captured')}</code></dd>
181
- <dt>Upstream</dt><dd>${escapeHtml(payload.upstreamUrl || 'Not ready')}</dd>
182
- <dt>Share ID</dt><dd>${escapeHtml(payload.shareId || 'Unknown')}</dd>
183
- </dl>
184
- </section>
185
- <section>
186
- <h2>What to check</h2>
187
- <ul>${suggestions}</ul>
188
- </section>
189
- <section>
190
- <h2>Process logs</h2>
191
- <pre>${logs}</pre>
192
- </section>
193
- </main>
194
- </body>
195
- </html>`;
196
- }
197
-
198
- function requestPrefersHtml(req) {
199
- const accept = req.header('accept') || '';
200
- return accept.includes('text/html') || (!accept.includes('application/json') && accept.includes('*/*'));
201
- }
202
-
203
- function sendLiveViewDiagnostic(req, res, session, statusCode, options = {}) {
204
- const payload = buildLiveViewUnavailablePayload(session, options);
205
- res.status(statusCode);
206
- if (requestPrefersHtml(req)) {
207
- res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
208
- res.type('html').send(renderLiveViewDiagnosticHtml(payload));
209
- return;
210
- }
211
- res.json(payload);
212
- }
213
-
16
+
17
+ const router = express.Router();
18
+
19
+ function requestBaseUrl(req) {
20
+ return `${req.protocol}://${req.get('host')}`;
21
+ }
22
+
23
+ function buildUrls(req, session) {
24
+ if (!session?.sharePath) return { local: null, external: null, preferred: null };
25
+ const local = `${requestBaseUrl(req)}${session.sharePath}`;
26
+ const tunnel = getTunnelState();
27
+ const external = tunnel?.running && tunnel.url ? `${tunnel.url}${session.sharePath}` : null;
28
+ return {
29
+ local,
30
+ external,
31
+ preferred: external || local,
32
+ };
33
+ }
34
+
35
+ function buildEnvironmentTunnel(tunnel, urls) {
36
+ const active = Boolean(tunnel?.running && urls?.external);
37
+ return {
38
+ status: active ? 'active' : 'local-only',
39
+ url: active ? urls.external : null,
40
+ localUrl: urls?.local || null,
41
+ preferredUrl: urls?.preferred || urls?.local || null,
42
+ };
43
+ }
44
+
45
+ function attachEnvironmentRuntimeState(environment, urls, tunnel) {
46
+ if (!environment) return environment;
47
+ return {
48
+ ...environment,
49
+ urls,
50
+ tunnel: buildEnvironmentTunnel(tunnel, urls),
51
+ diagnostics: {
52
+ ...environment.diagnostics,
53
+ publicTunnelReady: Boolean(tunnel?.running && urls?.external),
54
+ },
55
+ };
56
+ }
57
+
58
+ function escapeHtml(value) {
59
+ return String(value ?? '')
60
+ .replaceAll('&', '&amp;')
61
+ .replaceAll('<', '&lt;')
62
+ .replaceAll('>', '&gt;')
63
+ .replaceAll('"', '&quot;')
64
+ .replaceAll("'", '&#39;');
65
+ }
66
+
67
+ function latestErrorFromLogs(logs) {
68
+ return [...logs].reverse().find((line) => /error|enoent|eaddrinuse|failed|fatal|exception/i.test(line)) || null;
69
+ }
70
+
71
+ function buildLiveViewSuggestions(session, reason) {
72
+ const framework = String(session?.framework || session?.label || '').toLowerCase();
73
+ const status = String(session?.status || '').toLowerCase();
74
+ const suggestions = [];
75
+
76
+ if (framework.includes('php')) {
77
+ suggestions.push('Pixcode can prepare a local PHP runtime automatically and keep it under your user profile.');
78
+ suggestions.push('If the automatic runtime download fails, check the Live View panel logs and retry.');
79
+ suggestions.push('Check that the project has an index.php or a valid PHP router file in the selected project root.');
80
+ } else if (
81
+ framework.includes('javascript')
82
+ || framework.includes('vite')
83
+ || framework.includes('next')
84
+ || framework.includes('nuxt')
85
+ || framework.includes('astro')
86
+ || framework.includes('react')
87
+ || framework.includes('node')
88
+ ) {
89
+ suggestions.push('Run the project command in a terminal first and confirm it opens a local HTTP port.');
90
+ suggestions.push('Install project dependencies if node_modules is missing, then retry Live View.');
91
+ suggestions.push('If the dev server ignores PORT, set a custom command that binds to 127.0.0.1 and the chosen port.');
92
+ } else {
93
+ suggestions.push('Run the displayed command in a terminal and fix the first process error shown there.');
94
+ suggestions.push('Use a custom command if Pixcode detected the wrong runner for this project.');
95
+ }
96
+
97
+ if (status === 'starting' || reason === 'upstream_not_ready') {
98
+ suggestions.push('Wait for the app to finish booting, then press Refresh or Restart if the port never opens.');
99
+ }
100
+
101
+ if (reason === 'proxy_failed') {
102
+ suggestions.push('The Live View session exists, but Pixcode could not reach the local upstream URL.');
103
+ }
104
+
105
+ return suggestions;
106
+ }
107
+
108
+ export function buildLiveViewUnavailablePayload(session, options = {}) {
109
+ const logs = Array.isArray(session?.log) ? session.log.slice(-40) : [];
110
+ const errorDetail = options.errorMessage || session?.error || latestErrorFromLogs(logs);
111
+ const label = session?.label || session?.framework || 'Live View';
112
+ const status = session?.status || 'missing';
113
+
114
+ return {
115
+ error: 'Live View session is not available.',
116
+ message: `${label} is ${status}.`,
117
+ reason: options.reason || status,
118
+ status,
119
+ framework: session?.framework || null,
120
+ label,
121
+ projectName: session?.projectName || null,
122
+ shareId: session?.shareId || options.shareId || null,
123
+ sharePath: session?.sharePath || null,
124
+ upstreamUrl: session?.upstreamUrl || null,
125
+ port: session?.port || null,
126
+ errorDetail,
127
+ diagnostics: {
128
+ command: session?.command?.displayCommand || null,
129
+ upstreamUrl: session?.upstreamUrl || null,
130
+ port: session?.port || null,
131
+ startedAt: session?.startedAt || null,
132
+ stoppedAt: session?.stoppedAt || null,
133
+ exitCode: session?.exitCode ?? null,
134
+ exitSignal: session?.exitSignal ?? null,
135
+ logs,
136
+ },
137
+ suggestions: buildLiveViewSuggestions(session, options.reason),
138
+ };
139
+ }
140
+
141
+ export function renderLiveViewDiagnosticHtml(payload) {
142
+ const suggestions = payload.suggestions?.length
143
+ ? payload.suggestions.map((item) => `<li>${escapeHtml(item)}</li>`).join('')
144
+ : '<li>Open the Live View panel, check the latest logs, and restart the session.</li>';
145
+ const logs = payload.diagnostics?.logs?.length
146
+ ? payload.diagnostics.logs.map((line) => escapeHtml(line)).join('\n')
147
+ : 'No process logs were captured yet.';
148
+
149
+ return `<!doctype html>
150
+ <html lang="en">
151
+ <head>
152
+ <meta charset="utf-8">
153
+ <meta name="viewport" content="width=device-width, initial-scale=1">
154
+ <title>Pixcode Live View diagnostics</title>
155
+ <style>
156
+ :root { color-scheme: light dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
157
+ body { margin: 0; min-height: 100vh; background: #0b0f14; color: #e5edf5; }
158
+ main { box-sizing: border-box; max-width: 980px; margin: 0 auto; padding: 32px 20px; }
159
+ .eyebrow { color: #f97316; font-size: 12px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
160
+ h1 { margin: 8px 0 10px; font-size: clamp(26px, 5vw, 44px); line-height: 1.05; }
161
+ p { color: #a8b3c2; line-height: 1.6; }
162
+ section { margin-top: 18px; border: 1px solid rgba(148, 163, 184, .24); border-radius: 12px; background: rgba(15, 23, 42, .72); padding: 16px; }
163
+ dl { display: grid; grid-template-columns: minmax(110px, 180px) minmax(0, 1fr); gap: 10px 14px; margin: 0; }
164
+ dt { color: #94a3b8; font-size: 12px; text-transform: uppercase; }
165
+ dd { margin: 0; overflow-wrap: anywhere; }
166
+ pre { margin: 0; max-height: 280px; overflow: auto; white-space: pre-wrap; color: #dbeafe; background: #020617; border-radius: 10px; padding: 14px; }
167
+ code { color: #bfdbfe; }
168
+ li { margin: 8px 0; color: #d0d8e4; }
169
+ </style>
170
+ </head>
171
+ <body>
172
+ <main>
173
+ <div class="eyebrow">Pixcode Live View</div>
174
+ <h1>${escapeHtml(payload.message)}</h1>
175
+ <p>${escapeHtml(payload.errorDetail || 'The preview link is active, but the local runner is not ready.')}</p>
176
+ <section>
177
+ <dl>
178
+ <dt>Status</dt><dd>${escapeHtml(payload.status)}</dd>
179
+ <dt>Framework</dt><dd>${escapeHtml(payload.framework || 'Unknown')}</dd>
180
+ <dt>Command</dt><dd><code>${escapeHtml(payload.diagnostics?.command || 'No command captured')}</code></dd>
181
+ <dt>Upstream</dt><dd>${escapeHtml(payload.upstreamUrl || 'Not ready')}</dd>
182
+ <dt>Share ID</dt><dd>${escapeHtml(payload.shareId || 'Unknown')}</dd>
183
+ </dl>
184
+ </section>
185
+ <section>
186
+ <h2>What to check</h2>
187
+ <ul>${suggestions}</ul>
188
+ </section>
189
+ <section>
190
+ <h2>Process logs</h2>
191
+ <pre>${logs}</pre>
192
+ </section>
193
+ </main>
194
+ </body>
195
+ </html>`;
196
+ }
197
+
198
+ function requestPrefersHtml(req) {
199
+ const accept = req.header('accept') || '';
200
+ return accept.includes('text/html') || (!accept.includes('application/json') && accept.includes('*/*'));
201
+ }
202
+
203
+ function sendLiveViewDiagnostic(req, res, session, statusCode, options = {}) {
204
+ const payload = buildLiveViewUnavailablePayload(session, options);
205
+ res.status(statusCode);
206
+ if (requestPrefersHtml(req)) {
207
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
208
+ res.type('html').send(renderLiveViewDiagnosticHtml(payload));
209
+ return;
210
+ }
211
+ res.json(payload);
212
+ }
213
+
214
214
  async function resolveProjectPath(projectName) {
215
- const projectPath = await extractProjectDirectory(projectName);
216
- if (!projectPath || typeof projectPath !== 'string') {
217
- const error = new Error('Project path could not be resolved.');
218
- error.statusCode = 404;
219
- throw error;
220
- }
215
+ const projectPath = await extractProjectDirectory(projectName);
216
+ if (!projectPath || typeof projectPath !== 'string') {
217
+ const error = new Error('Project path could not be resolved.');
218
+ error.statusCode = 404;
219
+ throw error;
220
+ }
221
221
  return projectPath;
222
222
  }
223
223
 
@@ -241,20 +241,20 @@ router.get('/:projectName/status', requireLiveViewProjectAccess('viewFiles'), as
241
241
  try {
242
242
  const { projectName } = req.params;
243
243
  const projectPath = req.projectPath;
244
- const state = await getLiveViewState(projectName, projectPath);
245
- const urls = buildUrls(req, state.session);
246
- const tunnel = getTunnelState();
247
- res.json({
248
- ...state,
249
- urls,
250
- tunnel,
251
- environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
252
- });
253
- } catch (error) {
254
- res.status(error.statusCode || 500).json({ error: error.message || 'Failed to read Live View state' });
255
- }
256
- });
257
-
244
+ const state = await getLiveViewState(projectName, projectPath);
245
+ const urls = buildUrls(req, state.session);
246
+ const tunnel = getTunnelState();
247
+ res.json({
248
+ ...state,
249
+ urls,
250
+ tunnel,
251
+ environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
252
+ });
253
+ } catch (error) {
254
+ res.status(error.statusCode || 500).json({ error: error.message || 'Failed to read Live View state' });
255
+ }
256
+ });
257
+
258
258
  router.post('/:projectName/start', requireLiveViewProjectAccess('useShell'), async (req, res) => {
259
259
  try {
260
260
  const { projectName } = req.params;
@@ -262,23 +262,23 @@ router.post('/:projectName/start', requireLiveViewProjectAccess('useShell'), asy
262
262
  if (req.body?.customCommand && !['admin', 'owner'].includes(req.user?.role)) {
263
263
  return res.status(403).json({ error: 'Custom Live View commands require admin access.' });
264
264
  }
265
- const session = await startLiveView(projectName, projectPath, req.body || {});
266
- const state = await getLiveViewState(projectName, projectPath);
267
- const urls = buildUrls(req, session);
268
- const tunnel = getTunnelState();
269
- res.json({
270
- success: true,
271
- session,
272
- urls,
273
- tunnel,
274
- environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
275
- });
276
- } catch (error) {
277
- const status = error.code === 'LIVE_VIEW_NOT_AVAILABLE' ? 422 : 500;
278
- res.status(status).json({ error: error.message || 'Failed to start Live View' });
279
- }
280
- });
281
-
265
+ const session = await startLiveView(projectName, projectPath, req.body || {});
266
+ const state = await getLiveViewState(projectName, projectPath);
267
+ const urls = buildUrls(req, session);
268
+ const tunnel = getTunnelState();
269
+ res.json({
270
+ success: true,
271
+ session,
272
+ urls,
273
+ tunnel,
274
+ environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
275
+ });
276
+ } catch (error) {
277
+ const status = error.code === 'LIVE_VIEW_NOT_AVAILABLE' ? 422 : 500;
278
+ res.status(status).json({ error: error.message || 'Failed to start Live View' });
279
+ }
280
+ });
281
+
282
282
  router.post('/:projectName/restart', requireLiveViewProjectAccess('useShell'), async (req, res) => {
283
283
  try {
284
284
  const { projectName } = req.params;
@@ -286,149 +286,149 @@ router.post('/:projectName/restart', requireLiveViewProjectAccess('useShell'), a
286
286
  if (req.body?.customCommand && !['admin', 'owner'].includes(req.user?.role)) {
287
287
  return res.status(403).json({ error: 'Custom Live View commands require admin access.' });
288
288
  }
289
- const session = await restartLiveView(projectName, projectPath, req.body || {});
290
- const state = await getLiveViewState(projectName, projectPath);
291
- const urls = buildUrls(req, session);
292
- const tunnel = getTunnelState();
293
- res.json({
294
- success: true,
295
- session,
296
- urls,
297
- tunnel,
298
- environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
299
- });
300
- } catch (error) {
301
- res.status(500).json({ error: error.message || 'Failed to restart Live View' });
302
- }
303
- });
304
-
289
+ const session = await restartLiveView(projectName, projectPath, req.body || {});
290
+ const state = await getLiveViewState(projectName, projectPath);
291
+ const urls = buildUrls(req, session);
292
+ const tunnel = getTunnelState();
293
+ res.json({
294
+ success: true,
295
+ session,
296
+ urls,
297
+ tunnel,
298
+ environment: attachEnvironmentRuntimeState(state.environment, urls, tunnel),
299
+ });
300
+ } catch (error) {
301
+ res.status(500).json({ error: error.message || 'Failed to restart Live View' });
302
+ }
303
+ });
304
+
305
305
  router.post('/:projectName/stop', requireLiveViewProjectAccess('useShell'), async (req, res) => {
306
- try {
307
- const session = await stopLiveView(req.params.projectName);
308
- const urls = buildUrls(req, session);
309
- const tunnel = getTunnelState();
310
- res.json({
311
- success: true,
312
- session,
313
- urls,
314
- tunnel,
315
- environment: attachEnvironmentRuntimeState(null, urls, tunnel),
316
- });
317
- } catch (error) {
318
- res.status(500).json({ error: error.message || 'Failed to stop Live View' });
319
- }
320
- });
321
-
322
- function resolveStaticFile(staticRoot, requestUrl) {
323
- const parsed = new URL(requestUrl, 'http://pixcode.local');
324
- const rawPath = decodeURIComponent(parsed.pathname || '/');
325
- const relativePath = rawPath.replace(/^\/+/, '') || 'index.html';
326
- const root = path.resolve(staticRoot);
327
- const candidate = path.resolve(root, relativePath);
328
- const relative = path.relative(root, candidate);
329
-
330
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
331
- const error = new Error('Live View path is outside the static root.');
332
- error.statusCode = 403;
333
- throw error;
334
- }
335
-
336
- return candidate;
337
- }
338
-
339
- async function sendStaticLiveView(req, res, session) {
340
- const root = session.staticRoot;
341
- let filePath = resolveStaticFile(root, req.url);
342
- try {
343
- const stats = await fs.stat(filePath);
344
- if (stats.isDirectory()) {
345
- filePath = path.join(filePath, 'index.html');
346
- }
347
- await fs.access(filePath);
348
- } catch {
349
- filePath = path.join(root, 'index.html');
350
- }
351
-
352
- res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
353
- res.sendFile(filePath);
354
- }
355
-
356
- async function proxyLiveView(req, res, session) {
357
- if (!session.upstreamUrl) {
358
- sendLiveViewDiagnostic(req, res, session, 503, {
359
- reason: 'upstream_not_ready',
360
- errorMessage: 'Live View upstream is not ready yet.',
361
- });
362
- return;
363
- }
364
-
365
- const targetUrl = new URL(req.url || '/', session.upstreamUrl);
366
- try {
367
- const upstream = await fetch(targetUrl, {
368
- method: req.method,
369
- headers: {
370
- accept: req.header('accept') || '*/*',
371
- 'user-agent': req.header('user-agent') || 'pixcode-live-view',
372
- },
373
- });
374
- res.status(upstream.status);
375
- upstream.headers.forEach((value, key) => {
376
- const lower = key.toLowerCase();
377
- if (lower === 'content-encoding' || lower === 'content-length') return;
378
- if (lower === 'x-frame-options' || lower === 'content-security-policy') return;
379
- res.setHeader(key, value);
380
- });
381
- res.send(Buffer.from(await upstream.arrayBuffer()));
382
- } catch (error) {
383
- sendLiveViewDiagnostic(req, res, session, 502, {
384
- reason: 'proxy_failed',
385
- errorMessage: error.message || 'Live View proxy failed',
386
- });
387
- }
388
- }
389
-
390
- export function createLiveViewPublicRouter() {
391
- const publicRouter = express.Router();
392
-
393
- publicRouter.use('/:shareId', async (req, res) => {
394
- const session = getLiveViewSessionByShareId(req.params.shareId);
395
- if (!session) {
396
- sendLiveViewDiagnostic(req, res, {
397
- shareId: req.params.shareId,
398
- status: 'missing',
399
- label: 'Live View session',
400
- log: [],
401
- }, 404, {
402
- reason: 'missing_session',
403
- errorMessage: 'This preview link does not match an active Live View session on this Pixcode server.',
404
- });
405
- return;
406
- }
407
-
408
- if (session.status === 'error') {
409
- sendLiveViewDiagnostic(req, res, session, 502, { reason: 'session_error' });
410
- return;
411
- }
412
-
413
- if (session.status === 'stopped') {
414
- sendLiveViewDiagnostic(req, res, session, 410, { reason: 'session_stopped' });
415
- return;
416
- }
417
-
418
- if (session.status === 'starting') {
419
- sendLiveViewDiagnostic(req, res, session, 202, { reason: 'upstream_not_ready' });
420
- return;
421
- }
422
-
423
- if (session.kind === 'static') {
424
- await sendStaticLiveView(req, res, session);
425
- return;
426
- }
427
-
428
- await proxyLiveView(req, res, session);
429
- });
430
-
431
- return publicRouter;
432
- }
433
-
434
- export default router;
306
+ try {
307
+ const session = await stopLiveView(req.params.projectName);
308
+ const urls = buildUrls(req, session);
309
+ const tunnel = getTunnelState();
310
+ res.json({
311
+ success: true,
312
+ session,
313
+ urls,
314
+ tunnel,
315
+ environment: attachEnvironmentRuntimeState(null, urls, tunnel),
316
+ });
317
+ } catch (error) {
318
+ res.status(500).json({ error: error.message || 'Failed to stop Live View' });
319
+ }
320
+ });
321
+
322
+ function resolveStaticFile(staticRoot, requestUrl) {
323
+ const parsed = new URL(requestUrl, 'http://pixcode.local');
324
+ const rawPath = decodeURIComponent(parsed.pathname || '/');
325
+ const relativePath = rawPath.replace(/^\/+/, '') || 'index.html';
326
+ const root = path.resolve(staticRoot);
327
+ const candidate = path.resolve(root, relativePath);
328
+ const relative = path.relative(root, candidate);
329
+
330
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
331
+ const error = new Error('Live View path is outside the static root.');
332
+ error.statusCode = 403;
333
+ throw error;
334
+ }
335
+
336
+ return candidate;
337
+ }
338
+
339
+ async function sendStaticLiveView(req, res, session) {
340
+ const root = session.staticRoot;
341
+ let filePath = resolveStaticFile(root, req.url);
342
+ try {
343
+ const stats = await fs.stat(filePath);
344
+ if (stats.isDirectory()) {
345
+ filePath = path.join(filePath, 'index.html');
346
+ }
347
+ await fs.access(filePath);
348
+ } catch {
349
+ filePath = path.join(root, 'index.html');
350
+ }
351
+
352
+ res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
353
+ res.sendFile(filePath);
354
+ }
355
+
356
+ async function proxyLiveView(req, res, session) {
357
+ if (!session.upstreamUrl) {
358
+ sendLiveViewDiagnostic(req, res, session, 503, {
359
+ reason: 'upstream_not_ready',
360
+ errorMessage: 'Live View upstream is not ready yet.',
361
+ });
362
+ return;
363
+ }
364
+
365
+ const targetUrl = new URL(req.url || '/', session.upstreamUrl);
366
+ try {
367
+ const upstream = await fetch(targetUrl, {
368
+ method: req.method,
369
+ headers: {
370
+ accept: req.header('accept') || '*/*',
371
+ 'user-agent': req.header('user-agent') || 'pixcode-live-view',
372
+ },
373
+ });
374
+ res.status(upstream.status);
375
+ upstream.headers.forEach((value, key) => {
376
+ const lower = key.toLowerCase();
377
+ if (lower === 'content-encoding' || lower === 'content-length') return;
378
+ if (lower === 'x-frame-options' || lower === 'content-security-policy') return;
379
+ res.setHeader(key, value);
380
+ });
381
+ res.send(Buffer.from(await upstream.arrayBuffer()));
382
+ } catch (error) {
383
+ sendLiveViewDiagnostic(req, res, session, 502, {
384
+ reason: 'proxy_failed',
385
+ errorMessage: error.message || 'Live View proxy failed',
386
+ });
387
+ }
388
+ }
389
+
390
+ export function createLiveViewPublicRouter() {
391
+ const publicRouter = express.Router();
392
+
393
+ publicRouter.use('/:shareId', async (req, res) => {
394
+ const session = getLiveViewSessionByShareId(req.params.shareId);
395
+ if (!session) {
396
+ sendLiveViewDiagnostic(req, res, {
397
+ shareId: req.params.shareId,
398
+ status: 'missing',
399
+ label: 'Live View session',
400
+ log: [],
401
+ }, 404, {
402
+ reason: 'missing_session',
403
+ errorMessage: 'This preview link does not match an active Live View session on this Pixcode server.',
404
+ });
405
+ return;
406
+ }
407
+
408
+ if (session.status === 'error') {
409
+ sendLiveViewDiagnostic(req, res, session, 502, { reason: 'session_error' });
410
+ return;
411
+ }
412
+
413
+ if (session.status === 'stopped') {
414
+ sendLiveViewDiagnostic(req, res, session, 410, { reason: 'session_stopped' });
415
+ return;
416
+ }
417
+
418
+ if (session.status === 'starting') {
419
+ sendLiveViewDiagnostic(req, res, session, 202, { reason: 'upstream_not_ready' });
420
+ return;
421
+ }
422
+
423
+ if (session.kind === 'static') {
424
+ await sendStaticLiveView(req, res, session);
425
+ return;
426
+ }
427
+
428
+ await proxyLiveView(req, res, session);
429
+ });
430
+
431
+ return publicRouter;
432
+ }
433
+
434
+ export default router;