@nuraly/lumenjs 0.1.3 → 0.2.0

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 (306) hide show
  1. package/README.md +62 -282
  2. package/dist/auth/config.d.ts +23 -0
  3. package/dist/auth/config.js +115 -0
  4. package/dist/auth/guard.d.ts +12 -0
  5. package/dist/auth/guard.js +28 -0
  6. package/dist/auth/index.d.ts +3 -0
  7. package/dist/auth/index.js +1 -0
  8. package/dist/auth/middleware.d.ts +23 -0
  9. package/dist/auth/middleware.js +89 -0
  10. package/dist/auth/native-auth.d.ts +82 -0
  11. package/dist/auth/native-auth.js +340 -0
  12. package/dist/auth/oidc-client.d.ts +17 -0
  13. package/dist/auth/oidc-client.js +123 -0
  14. package/dist/auth/providers/google.d.ts +23 -0
  15. package/dist/auth/providers/google.js +25 -0
  16. package/dist/auth/providers/index.d.ts +2 -0
  17. package/dist/auth/providers/index.js +1 -0
  18. package/dist/auth/routes/login.d.ts +8 -0
  19. package/dist/auth/routes/login.js +121 -0
  20. package/dist/auth/routes/logout.d.ts +4 -0
  21. package/dist/auth/routes/logout.js +79 -0
  22. package/dist/auth/routes/oidc-callback.d.ts +3 -0
  23. package/dist/auth/routes/oidc-callback.js +70 -0
  24. package/dist/auth/routes/password.d.ts +5 -0
  25. package/dist/auth/routes/password.js +149 -0
  26. package/dist/auth/routes/signup.d.ts +3 -0
  27. package/dist/auth/routes/signup.js +81 -0
  28. package/dist/auth/routes/token.d.ts +4 -0
  29. package/dist/auth/routes/token.js +70 -0
  30. package/dist/auth/routes/totp.d.ts +22 -0
  31. package/dist/auth/routes/totp.js +232 -0
  32. package/dist/auth/routes/utils.d.ts +7 -0
  33. package/dist/auth/routes/utils.js +35 -0
  34. package/dist/auth/routes/verify.d.ts +3 -0
  35. package/dist/auth/routes/verify.js +26 -0
  36. package/dist/auth/routes.d.ts +8 -0
  37. package/dist/auth/routes.js +124 -0
  38. package/dist/auth/session.d.ts +8 -0
  39. package/dist/auth/session.js +54 -0
  40. package/dist/auth/token.d.ts +33 -0
  41. package/dist/auth/token.js +90 -0
  42. package/dist/auth/types.d.ts +156 -0
  43. package/dist/auth/types.js +2 -0
  44. package/dist/build/build-client.d.ts +15 -0
  45. package/dist/build/build-client.js +45 -0
  46. package/dist/build/build-prerender.d.ts +11 -0
  47. package/dist/build/build-prerender.js +159 -0
  48. package/dist/build/build-server.d.ts +18 -0
  49. package/dist/build/build-server.js +107 -0
  50. package/dist/build/build.js +60 -123
  51. package/dist/build/scan.d.ts +18 -0
  52. package/dist/build/scan.js +77 -6
  53. package/dist/build/serve-api.js +8 -2
  54. package/dist/build/serve-loaders.d.ts +4 -4
  55. package/dist/build/serve-loaders.js +26 -18
  56. package/dist/build/serve-ssr.js +38 -11
  57. package/dist/build/serve-static.js +3 -3
  58. package/dist/build/serve.js +341 -18
  59. package/dist/cli.js +37 -6
  60. package/dist/communication/encryption.d.ts +35 -0
  61. package/dist/communication/encryption.js +90 -0
  62. package/dist/communication/handlers/context.d.ts +27 -0
  63. package/dist/communication/handlers/context.js +1 -0
  64. package/dist/communication/handlers/conversation.d.ts +24 -0
  65. package/dist/communication/handlers/conversation.js +113 -0
  66. package/dist/communication/handlers/file-upload.d.ts +17 -0
  67. package/dist/communication/handlers/file-upload.js +62 -0
  68. package/dist/communication/handlers/messaging.d.ts +30 -0
  69. package/dist/communication/handlers/messaging.js +237 -0
  70. package/dist/communication/handlers/presence.d.ts +15 -0
  71. package/dist/communication/handlers/presence.js +76 -0
  72. package/dist/communication/handlers.d.ts +5 -0
  73. package/dist/communication/handlers.js +5 -0
  74. package/dist/communication/index.d.ts +9 -0
  75. package/dist/communication/index.js +7 -0
  76. package/dist/communication/link-preview.d.ts +18 -0
  77. package/dist/communication/link-preview.js +115 -0
  78. package/dist/communication/schema.d.ts +10 -0
  79. package/dist/communication/schema.js +101 -0
  80. package/dist/communication/server.d.ts +86 -0
  81. package/dist/communication/server.js +212 -0
  82. package/dist/communication/signaling.d.ts +43 -0
  83. package/dist/communication/signaling.js +271 -0
  84. package/dist/communication/store.d.ts +71 -0
  85. package/dist/communication/store.js +289 -0
  86. package/dist/communication/types.d.ts +454 -0
  87. package/dist/communication/types.js +1 -0
  88. package/dist/create.d.ts +1 -0
  89. package/dist/create.js +55 -0
  90. package/dist/db/auto-migrate.d.ts +3 -0
  91. package/dist/db/auto-migrate.js +100 -0
  92. package/dist/db/client.d.ts +3 -0
  93. package/dist/db/client.js +18 -0
  94. package/dist/db/index.d.ts +17 -13
  95. package/dist/db/index.js +205 -26
  96. package/dist/db/seed.d.ts +12 -0
  97. package/dist/db/seed.js +88 -0
  98. package/dist/db/table.d.ts +10 -0
  99. package/dist/db/table.js +12 -0
  100. package/dist/dev-server/config.d.ts +11 -0
  101. package/dist/dev-server/config.js +40 -20
  102. package/dist/dev-server/index-html.d.ts +4 -0
  103. package/dist/dev-server/index-html.js +21 -6
  104. package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
  105. package/dist/dev-server/nuralyui-aliases.js +115 -94
  106. package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
  107. package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
  108. package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
  109. package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
  110. package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
  111. package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
  112. package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
  113. package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
  114. package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
  115. package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
  116. package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
  117. package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
  118. package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
  119. package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
  120. package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
  121. package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
  122. package/dist/dev-server/plugins/vite-plugin-routes.js +16 -5
  123. package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
  124. package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
  125. package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
  126. package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
  127. package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
  128. package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
  129. package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +140 -3
  130. package/dist/dev-server/server.js +242 -70
  131. package/dist/dev-server/ssr-render.d.ts +2 -1
  132. package/dist/dev-server/ssr-render.js +117 -50
  133. package/dist/editor/ai/backend.d.ts +20 -0
  134. package/dist/editor/ai/backend.js +113 -0
  135. package/dist/editor/ai/claude-code-client.d.ts +20 -0
  136. package/dist/editor/ai/claude-code-client.js +145 -0
  137. package/dist/editor/ai/deepseek-client.d.ts +7 -0
  138. package/dist/editor/ai/deepseek-client.js +113 -0
  139. package/dist/editor/ai/opencode-client.d.ts +14 -0
  140. package/dist/editor/ai/opencode-client.js +99 -0
  141. package/dist/editor/ai/snapshot-store.d.ts +22 -0
  142. package/dist/editor/ai/snapshot-store.js +35 -0
  143. package/dist/editor/ai/types.d.ts +30 -0
  144. package/dist/editor/ai/types.js +136 -0
  145. package/dist/editor/ai-chat-panel.d.ts +13 -0
  146. package/dist/editor/ai-chat-panel.js +613 -0
  147. package/dist/editor/ai-markdown.d.ts +10 -0
  148. package/dist/editor/ai-markdown.js +70 -0
  149. package/dist/editor/ai-project-panel.d.ts +11 -0
  150. package/dist/editor/ai-project-panel.js +332 -0
  151. package/dist/editor/ast-modification.d.ts +11 -0
  152. package/dist/editor/ast-modification.js +1 -0
  153. package/dist/editor/ast-service.d.ts +30 -0
  154. package/dist/editor/ast-service.js +180 -0
  155. package/dist/editor/css-rules.d.ts +54 -0
  156. package/dist/editor/css-rules.js +423 -0
  157. package/dist/editor/editor-api-client.d.ts +51 -0
  158. package/dist/editor/editor-api-client.js +162 -0
  159. package/dist/editor/editor-bridge.d.ts +1 -0
  160. package/dist/editor/editor-bridge.js +18 -8
  161. package/dist/editor/editor-toolbar.d.ts +14 -0
  162. package/dist/editor/editor-toolbar.js +115 -0
  163. package/dist/editor/file-editor.d.ts +9 -0
  164. package/dist/editor/file-editor.js +236 -0
  165. package/dist/editor/file-service.d.ts +16 -0
  166. package/dist/editor/file-service.js +52 -0
  167. package/dist/editor/i18n-key-gen.d.ts +1 -0
  168. package/dist/editor/i18n-key-gen.js +7 -0
  169. package/dist/editor/inline-text-edit.d.ts +5 -0
  170. package/dist/editor/inline-text-edit.js +173 -92
  171. package/dist/editor/overlay-events.d.ts +5 -0
  172. package/dist/editor/overlay-events.js +364 -0
  173. package/dist/editor/overlay-hmr.d.ts +2 -0
  174. package/dist/editor/overlay-hmr.js +76 -0
  175. package/dist/editor/overlay-selection.d.ts +29 -0
  176. package/dist/editor/overlay-selection.js +148 -0
  177. package/dist/editor/overlay-utils.d.ts +12 -0
  178. package/dist/editor/overlay-utils.js +59 -0
  179. package/dist/editor/properties-panel-persist.d.ts +14 -0
  180. package/dist/editor/properties-panel-persist.js +70 -0
  181. package/dist/editor/properties-panel-rows.d.ts +10 -0
  182. package/dist/editor/properties-panel-rows.js +349 -0
  183. package/dist/editor/properties-panel-styles.d.ts +4 -0
  184. package/dist/editor/properties-panel-styles.js +174 -0
  185. package/dist/editor/properties-panel.d.ts +4 -0
  186. package/dist/editor/properties-panel.js +148 -0
  187. package/dist/editor/property-registry.d.ts +16 -0
  188. package/dist/editor/property-registry.js +303 -0
  189. package/dist/editor/standalone-file-panel.d.ts +0 -0
  190. package/dist/editor/standalone-file-panel.js +1 -0
  191. package/dist/editor/standalone-overlay-dom.d.ts +0 -0
  192. package/dist/editor/standalone-overlay-dom.js +1 -0
  193. package/dist/editor/standalone-overlay-styles.d.ts +0 -0
  194. package/dist/editor/standalone-overlay-styles.js +1 -0
  195. package/dist/editor/standalone-overlay.d.ts +1 -0
  196. package/dist/editor/standalone-overlay.js +76 -0
  197. package/dist/editor/syntax-highlighter.d.ts +4 -0
  198. package/dist/editor/syntax-highlighter.js +81 -0
  199. package/dist/editor/text-toolbar.d.ts +11 -0
  200. package/dist/editor/text-toolbar.js +327 -0
  201. package/dist/editor/toolbar-styles.d.ts +4 -0
  202. package/dist/editor/toolbar-styles.js +198 -0
  203. package/dist/email/index.d.ts +32 -0
  204. package/dist/email/index.js +154 -0
  205. package/dist/email/providers/resend.d.ts +2 -0
  206. package/dist/email/providers/resend.js +24 -0
  207. package/dist/email/providers/sendgrid.d.ts +2 -0
  208. package/dist/email/providers/sendgrid.js +31 -0
  209. package/dist/email/providers/smtp.d.ts +13 -0
  210. package/dist/email/providers/smtp.js +125 -0
  211. package/dist/email/template-engine.d.ts +18 -0
  212. package/dist/email/template-engine.js +116 -0
  213. package/dist/email/templates/base.d.ts +9 -0
  214. package/dist/email/templates/base.js +65 -0
  215. package/dist/email/templates/password-reset.d.ts +5 -0
  216. package/dist/email/templates/password-reset.js +15 -0
  217. package/dist/email/templates/verify-email.d.ts +5 -0
  218. package/dist/email/templates/verify-email.js +15 -0
  219. package/dist/email/templates/welcome.d.ts +5 -0
  220. package/dist/email/templates/welcome.js +13 -0
  221. package/dist/email/types.d.ts +49 -0
  222. package/dist/email/types.js +1 -0
  223. package/dist/llms/generate.d.ts +46 -0
  224. package/dist/llms/generate.js +185 -0
  225. package/dist/permissions/guard.d.ts +28 -0
  226. package/dist/permissions/guard.js +30 -0
  227. package/dist/permissions/index.d.ts +6 -0
  228. package/dist/permissions/index.js +3 -0
  229. package/dist/permissions/service.d.ts +80 -0
  230. package/dist/permissions/service.js +210 -0
  231. package/dist/permissions/tables.d.ts +5 -0
  232. package/dist/permissions/tables.js +68 -0
  233. package/dist/permissions/types.d.ts +33 -0
  234. package/dist/permissions/types.js +1 -0
  235. package/dist/runtime/app-shell.d.ts +1 -1
  236. package/dist/runtime/app-shell.js +164 -0
  237. package/dist/runtime/auth.d.ts +10 -0
  238. package/dist/runtime/auth.js +30 -0
  239. package/dist/runtime/communication.d.ts +137 -0
  240. package/dist/runtime/communication.js +228 -0
  241. package/dist/runtime/error-boundary.d.ts +23 -0
  242. package/dist/runtime/error-boundary.js +120 -0
  243. package/dist/runtime/i18n.d.ts +6 -1
  244. package/dist/runtime/i18n.js +42 -21
  245. package/dist/runtime/island.d.ts +16 -0
  246. package/dist/runtime/island.js +80 -0
  247. package/dist/runtime/router-data.d.ts +3 -0
  248. package/dist/runtime/router-data.js +102 -17
  249. package/dist/runtime/router-hydration.js +34 -2
  250. package/dist/runtime/router.d.ts +19 -2
  251. package/dist/runtime/router.js +237 -43
  252. package/dist/runtime/socket-client.d.ts +2 -0
  253. package/dist/runtime/socket-client.js +30 -0
  254. package/dist/runtime/webrtc.d.ts +91 -0
  255. package/dist/runtime/webrtc.js +428 -0
  256. package/dist/shared/dom-shims.js +4 -2
  257. package/dist/shared/graceful-shutdown.d.ts +8 -0
  258. package/dist/shared/graceful-shutdown.js +36 -0
  259. package/dist/shared/health.d.ts +8 -0
  260. package/dist/shared/health.js +25 -0
  261. package/dist/shared/llms-txt.d.ts +31 -0
  262. package/dist/shared/llms-txt.js +85 -0
  263. package/dist/shared/logger.d.ts +32 -0
  264. package/dist/shared/logger.js +93 -0
  265. package/dist/shared/meta.d.ts +27 -0
  266. package/dist/shared/meta.js +71 -0
  267. package/dist/shared/middleware-runner.d.ts +9 -0
  268. package/dist/shared/middleware-runner.js +29 -0
  269. package/dist/shared/rate-limit.d.ts +18 -0
  270. package/dist/shared/rate-limit.js +71 -0
  271. package/dist/shared/request-id.d.ts +5 -0
  272. package/dist/shared/request-id.js +18 -0
  273. package/dist/shared/route-matching.js +16 -1
  274. package/dist/shared/security-headers.d.ts +18 -0
  275. package/dist/shared/security-headers.js +38 -0
  276. package/dist/shared/socket-io-setup.d.ts +11 -0
  277. package/dist/shared/socket-io-setup.js +51 -0
  278. package/dist/shared/types.d.ts +15 -0
  279. package/dist/shared/utils.d.ts +33 -7
  280. package/dist/shared/utils.js +164 -27
  281. package/dist/storage/adapters/local.d.ts +44 -0
  282. package/dist/storage/adapters/local.js +85 -0
  283. package/dist/storage/adapters/s3.d.ts +32 -0
  284. package/dist/storage/adapters/s3.js +119 -0
  285. package/dist/storage/adapters/types.d.ts +53 -0
  286. package/dist/storage/adapters/types.js +1 -0
  287. package/dist/storage/index.d.ts +76 -0
  288. package/dist/storage/index.js +83 -0
  289. package/package.json +45 -7
  290. package/templates/blog/api/posts.ts +4 -18
  291. package/templates/blog/data/migrations/001_init.sql +6 -5
  292. package/templates/blog/lumenjs.config.ts +3 -0
  293. package/templates/blog/package.json +14 -0
  294. package/templates/blog/pages/_layout.ts +25 -0
  295. package/templates/blog/pages/index.ts +48 -22
  296. package/templates/blog/pages/posts/[slug].ts +45 -20
  297. package/templates/blog/pages/tag/[tag].ts +44 -0
  298. package/templates/dashboard/api/stats.ts +8 -5
  299. package/templates/dashboard/lumenjs.config.ts +3 -0
  300. package/templates/dashboard/package.json +14 -0
  301. package/templates/dashboard/pages/_layout.ts +25 -0
  302. package/templates/dashboard/pages/index.ts +54 -23
  303. package/templates/dashboard/pages/settings/index.ts +29 -0
  304. package/templates/default/lumenjs.config.ts +3 -0
  305. package/templates/default/package.json +14 -0
  306. package/templates/default/pages/index.ts +24 -0
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
3
  import { resolvePageFile, extractRouteParams } from './plugins/vite-plugin-loaders.js';
4
- import { stripOuterLitMarkers, dirToLayoutTagName, filePathToTagName } from '../shared/utils.js';
4
+ import { stripOuterLitMarkers, dirToLayoutTagName, filePathToTagName, patchLoaderDataSpread } from '../shared/utils.js';
5
5
  import { installDomShims } from '../shared/dom-shims.js';
6
6
  import { loadTranslationsFromDisk } from './plugins/vite-plugin-i18n.js';
7
7
  /**
@@ -10,7 +10,7 @@ import { loadTranslationsFromDisk } from './plugins/vite-plugin-i18n.js';
10
10
  *
11
11
  * Returns pre-rendered HTML and loader data, or null on failure (falls back to CSR).
12
12
  */
13
- export async function ssrRenderPage(server, pagesDir, pathname, headers, locale) {
13
+ export async function ssrRenderPage(server, pagesDir, pathname, headers, locale, user) {
14
14
  try {
15
15
  const filePath = resolvePageFile(pagesDir, pathname);
16
16
  if (!filePath)
@@ -20,32 +20,34 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers, locale)
20
20
  await server.ssrLoadModule('@lit-labs/ssr/lib/install-global-dom-shim.js');
21
21
  // Patch missing DOM APIs that NuralyUI components may use during SSR
22
22
  installDomShims();
23
- // Initialize i18n in the SSR context so t() works during render
24
- if (locale) {
25
- const projectDir = path.resolve(pagesDir, '..');
26
- const translations = loadTranslationsFromDisk(projectDir, locale);
27
- try {
28
- // Load the same i18n module the page will import (via resolve.alias)
29
- const i18nMod = await server.ssrLoadModule('@lumenjs/i18n');
30
- if (i18nMod?.initI18n) {
31
- i18nMod.initI18n({ locales: [], defaultLocale: locale, prefixDefault: false }, locale, translations);
32
- }
33
- }
34
- catch {
35
- // i18n module not available — translations will show keys
36
- }
37
- }
38
23
  // Invalidate SSR module cache so we always get fresh content after file edits.
39
24
  // Also clear the custom element from the SSR registry so the new class is used.
25
+ // IMPORTANT: This must happen BEFORE i18n/auth init, because invalidation
26
+ // recursively clears all SSR-imported modules (including @lumenjs/i18n).
27
+ // If we init i18n first, then invalidate, the page reload gets a fresh i18n
28
+ // module instance with empty translations.
40
29
  const g = globalThis;
41
30
  invalidateSsrModule(server, filePath);
42
31
  clearSsrCustomElement(g);
32
+ // Use root-relative paths for ssrLoadModule so the module graph entry
33
+ // gets url='/pages/index.ts' instead of the filesystem path. Vite's
34
+ // idToModuleMap is shared between SSR and client — if SSR creates the
35
+ // entry first with a filesystem-path URL, the client inherits it and
36
+ // HMR breaks (createHotContext gets the wrong path).
37
+ const projectRoot = path.resolve(pagesDir, '..');
38
+ const pageModuleUrl = '/' + path.relative(projectRoot, filePath).replace(/\\/g, '/');
43
39
  // Load the page module via Vite (registers the custom element, applies transforms)
44
- const mod = await server.ssrLoadModule(filePath);
40
+ // Bypass get() so auto-define re-registers fresh classes
41
+ const registry = g.customElements;
42
+ if (registry)
43
+ registry.__nk_bypass_get = true;
44
+ const mod = await server.ssrLoadModule(pageModuleUrl);
45
+ if (registry)
46
+ registry.__nk_bypass_get = false;
45
47
  // Run loader if present
46
48
  let loaderData = undefined;
47
49
  if (mod.loader && typeof mod.loader === 'function') {
48
- loaderData = await mod.loader({ params, query: {}, url: pathname, headers: headers || {}, locale });
50
+ loaderData = await mod.loader({ params, query: {}, url: pathname, headers: headers || {}, locale, user: user ?? null });
49
51
  if (loaderData && typeof loaderData === 'object' && loaderData.__nk_redirect) {
50
52
  return { html: '', loaderData: null, redirect: { location: loaderData.location, status: loaderData.status || 302 } };
51
53
  }
@@ -53,8 +55,8 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers, locale)
53
55
  // Determine the custom element tag name from file path (matches client router)
54
56
  const relPath = path.relative(pagesDir, filePath).replace(/\\/g, '/');
55
57
  const tagName = filePathToTagName(relPath);
56
- // Discover layout chain for this page
57
- const layoutChain = discoverLayoutChain(pagesDir, filePath);
58
+ // Standalone pages skip all layouts
59
+ const layoutChain = mod.standalone ? [] : discoverLayoutChain(pagesDir, filePath);
58
60
  const layoutsData = [];
59
61
  // Load layout modules and run their loaders
60
62
  const layoutModules = [];
@@ -62,10 +64,15 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers, locale)
62
64
  // Invalidate layout module cache and clear SSR element registry
63
65
  invalidateSsrModule(server, layout.filePath);
64
66
  clearSsrCustomElement(g);
65
- const layoutMod = await server.ssrLoadModule(layout.filePath);
67
+ const layoutModuleUrl = '/' + path.relative(projectRoot, layout.filePath).replace(/\\/g, '/');
68
+ if (registry)
69
+ registry.__nk_bypass_get = true;
70
+ const layoutMod = await server.ssrLoadModule(layoutModuleUrl);
71
+ if (registry)
72
+ registry.__nk_bypass_get = false;
66
73
  let layoutLoaderData = undefined;
67
74
  if (layoutMod.loader && typeof layoutMod.loader === 'function') {
68
- layoutLoaderData = await layoutMod.loader({ params: {}, query: {}, url: pathname, headers: headers || {}, locale });
75
+ layoutLoaderData = await layoutMod.loader({ params: {}, query: {}, url: pathname, headers: headers || {}, locale, user: user ?? null });
69
76
  if (layoutLoaderData && typeof layoutLoaderData === 'object' && layoutLoaderData.__nk_redirect) {
70
77
  return { html: '', loaderData: null, redirect: { location: layoutLoaderData.location, status: layoutLoaderData.status || 302 } };
71
78
  }
@@ -74,6 +81,38 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers, locale)
74
81
  layoutModules.push({ tagName: layoutTagName, loaderData: layoutLoaderData });
75
82
  layoutsData.push({ loaderPath: layout.dir, data: layoutLoaderData });
76
83
  }
84
+ // Patch element classes to spread loaderData into individual properties
85
+ for (const lm of layoutModules) {
86
+ patchLoaderDataSpread(lm.tagName);
87
+ }
88
+ patchLoaderDataSpread(tagName);
89
+ // Initialize i18n in the SSR context AFTER all page/layout modules are loaded.
90
+ // This ensures t() uses the same module instance the components imported.
91
+ // Must happen after invalidation + module loading, because invalidateSsrModule
92
+ // recursively clears cached modules — if i18n was initialized before, the
93
+ // reloaded page/layout would get a fresh i18n instance with empty translations.
94
+ if (locale) {
95
+ const projectDir = path.resolve(pagesDir, '..');
96
+ const translations = loadTranslationsFromDisk(projectDir, locale);
97
+ try {
98
+ const i18nMod = await server.ssrLoadModule('@lumenjs/i18n');
99
+ if (i18nMod?.initI18n) {
100
+ i18nMod.initI18n({ locales: [], defaultLocale: locale, prefixDefault: false }, locale, translations);
101
+ }
102
+ }
103
+ catch {
104
+ // i18n module not available — translations will show keys
105
+ }
106
+ }
107
+ // Initialize auth in the SSR context (same reason — after module loading)
108
+ if (user) {
109
+ try {
110
+ const authMod = await server.ssrLoadModule('@nuraly/lumenjs-auth');
111
+ if (authMod?.initAuth)
112
+ authMod.initAuth(user);
113
+ }
114
+ catch { }
115
+ }
77
116
  // Load SSR render + lit/static-html.js through Vite (same module registry as page)
78
117
  const { render } = await server.ssrLoadModule('@lit-labs/ssr');
79
118
  const { html, unsafeStatic } = await server.ssrLoadModule('lit/static-html.js');
@@ -106,7 +145,7 @@ export async function ssrRenderPage(server, pagesDir, pathname, headers, locale)
106
145
  htmlStr = layoutHtml + htmlStr;
107
146
  }
108
147
  }
109
- return { html: htmlStr, loaderData, layoutsData: layoutsData.length > 0 ? layoutsData : undefined };
148
+ return { html: htmlStr, loaderData, layoutsData: layoutsData.length > 0 ? layoutsData : undefined, authUser: user ?? undefined };
110
149
  }
111
150
  catch (err) {
112
151
  console.error('[LumenJS] SSR render failed, falling back to CSR:', err);
@@ -145,41 +184,69 @@ function findLayoutFile(dir) {
145
184
  return null;
146
185
  }
147
186
  /**
148
- * Aggressively invalidate a module for SSR re-execution.
187
+ * Aggressively invalidate a module and all its SSR-imported dependencies.
188
+ * Without this, editing a component imported by a page/layout serves stale SSR.
149
189
  */
150
190
  function invalidateSsrModule(server, filePath) {
151
- const byFile = server.moduleGraph.getModulesByFile(filePath);
152
- if (byFile) {
153
- for (const m of byFile) {
154
- server.moduleGraph.invalidateModule(m);
155
- m.ssrModule = null;
156
- m.ssrTransformResult = null;
191
+ const visited = new Set();
192
+ function invalidateRecursive(id) {
193
+ if (visited.has(id))
194
+ return;
195
+ visited.add(id);
196
+ const mods = server.moduleGraph.getModulesByFile(id);
197
+ if (mods) {
198
+ for (const m of mods) {
199
+ server.moduleGraph.invalidateModule(m);
200
+ m.ssrModule = null;
201
+ m.ssrTransformResult = null;
202
+ // Recurse into SSR-imported modules
203
+ if (m.ssrImportedModules) {
204
+ for (const dep of m.ssrImportedModules) {
205
+ if (dep.file)
206
+ invalidateRecursive(dep.file);
207
+ }
208
+ }
209
+ }
210
+ }
211
+ const urlMod = server.moduleGraph.getModuleById(id);
212
+ if (urlMod) {
213
+ server.moduleGraph.invalidateModule(urlMod);
214
+ urlMod.ssrModule = null;
215
+ urlMod.ssrTransformResult = null;
157
216
  }
158
217
  }
159
- const urlMod = server.moduleGraph.getModuleById(filePath);
160
- if (urlMod) {
161
- server.moduleGraph.invalidateModule(urlMod);
162
- urlMod.ssrModule = null;
163
- urlMod.ssrTransformResult = null;
164
- }
218
+ invalidateRecursive(filePath);
165
219
  }
166
220
  /**
167
- * Patch the SSR customElements registry to allow re-registration.
221
+ * Patch the SSR customElements registry to allow re-registration,
222
+ * and proactively clear all definitions so auto-define guards pass.
168
223
  */
169
224
  function clearSsrCustomElement(g) {
170
225
  const registry = g.customElements;
171
- if (!registry || registry.__nk_patched)
226
+ if (!registry)
172
227
  return;
173
- registry.__nk_patched = true;
174
- const origDefine = registry.define.bind(registry);
175
- registry.define = (name, ctor) => {
176
- if (registry.__definitions && registry.__definitions.has(name)) {
177
- const oldCtor = registry.__definitions.get(name)?.ctor;
178
- registry.__definitions.delete(name);
179
- if (oldCtor && registry.__reverseDefinitions) {
180
- registry.__reverseDefinitions.delete(oldCtor);
228
+ // Patch define() to allow re-registration and get() to bypass
229
+ // auto-define guards (one-time)
230
+ if (!registry.__nk_patched) {
231
+ registry.__nk_patched = true;
232
+ const origDefine = registry.define.bind(registry);
233
+ registry.define = (name, ctor) => {
234
+ if (registry.__definitions && registry.__definitions.has(name)) {
235
+ const oldCtor = registry.__definitions.get(name)?.ctor;
236
+ registry.__definitions.delete(name);
237
+ if (oldCtor && registry.__reverseDefinitions) {
238
+ registry.__reverseDefinitions.delete(oldCtor);
239
+ }
181
240
  }
182
- }
183
- return origDefine(name, ctor);
184
- };
241
+ return origDefine(name, ctor);
242
+ };
243
+ // Patch get() so auto-define's `if (!customElements.get('tag'))` guard
244
+ // always passes, allowing define() to re-register the fresh class.
245
+ const origGet = registry.get.bind(registry);
246
+ registry.get = (name) => {
247
+ if (registry.__nk_bypass_get)
248
+ return undefined;
249
+ return origGet(name);
250
+ };
251
+ }
185
252
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * AI backend router — selects between Claude Code and OpenCode based on:
3
+ * 1. AI_BACKEND env var ('claude-code' | 'opencode')
4
+ * 2. Auto-detection: Claude Code CLI first, then OpenCode server
5
+ */
6
+ import type { AiChatOptions, AiChatResult, AiStatusResult } from './types.js';
7
+ export type { AiChatOptions, AiChatResult, AiStatusResult } from './types.js';
8
+ /**
9
+ * Stream an AI chat message using the configured backend.
10
+ */
11
+ export declare function streamAiChat(projectDir: string, options: AiChatOptions): AiChatResult;
12
+ /**
13
+ * Warm up the AI backend session in the background.
14
+ * Call on editor startup so the first user request is fast.
15
+ */
16
+ export declare function warmUpAiSession(projectDir: string): Promise<void>;
17
+ /**
18
+ * Check if any AI backend is available.
19
+ */
20
+ export declare function checkAiStatus(): Promise<AiStatusResult>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * AI backend router — selects between Claude Code and OpenCode based on:
3
+ * 1. AI_BACKEND env var ('claude-code' | 'opencode')
4
+ * 2. Auto-detection: Claude Code CLI first, then OpenCode server
5
+ */
6
+ let resolvedBackend = null;
7
+ async function detectBackend() {
8
+ if (resolvedBackend)
9
+ return resolvedBackend;
10
+ const explicit = process.env.AI_BACKEND;
11
+ if (explicit === 'claude-code' || explicit === 'opencode' || explicit === 'deepseek') {
12
+ resolvedBackend = explicit;
13
+ console.log(`[LumenJS] AI backend: ${explicit} (from AI_BACKEND env)`);
14
+ return resolvedBackend;
15
+ }
16
+ // Auto-detect: try DeepSeek if API key is set
17
+ if (process.env.DEEPSEEK_API_KEY) {
18
+ resolvedBackend = 'deepseek';
19
+ console.log('[LumenJS] AI backend: deepseek (auto-detected from DEEPSEEK_API_KEY)');
20
+ return resolvedBackend;
21
+ }
22
+ // Auto-detect: try Claude Code (subscription-based, no server needed)
23
+ try {
24
+ const cc = await import('./claude-code-client.js');
25
+ const status = await cc.checkAiStatus();
26
+ if (status.configured) {
27
+ resolvedBackend = 'claude-code';
28
+ console.log('[LumenJS] AI backend: claude-code (auto-detected)');
29
+ return resolvedBackend;
30
+ }
31
+ }
32
+ catch {
33
+ // SDK not installed or CLI not found
34
+ }
35
+ // Fall back to OpenCode
36
+ resolvedBackend = 'opencode';
37
+ console.log('[LumenJS] AI backend: opencode (fallback)');
38
+ return resolvedBackend;
39
+ }
40
+ async function getClient() {
41
+ const backend = await detectBackend();
42
+ if (backend === 'claude-code') {
43
+ return import('./claude-code-client.js');
44
+ }
45
+ if (backend === 'deepseek') {
46
+ return import('./deepseek-client.js');
47
+ }
48
+ return import('./opencode-client.js');
49
+ }
50
+ /**
51
+ * Stream an AI chat message using the configured backend.
52
+ */
53
+ export function streamAiChat(projectDir, options) {
54
+ const tokenCallbacks = [];
55
+ const doneCallbacks = [];
56
+ const errorCallbacks = [];
57
+ let innerResult = null;
58
+ const run = async () => {
59
+ try {
60
+ const client = await getClient();
61
+ innerResult = client.streamAiChat(projectDir, options);
62
+ // Forward callbacks registered before the client loaded
63
+ for (const cb of tokenCallbacks)
64
+ innerResult.onToken(cb);
65
+ for (const cb of doneCallbacks)
66
+ innerResult.onDone(cb);
67
+ for (const cb of errorCallbacks)
68
+ innerResult.onError(cb);
69
+ }
70
+ catch (err) {
71
+ const error = err instanceof Error ? err : new Error(String(err));
72
+ for (const cb of errorCallbacks)
73
+ cb(error);
74
+ }
75
+ };
76
+ run();
77
+ return {
78
+ get sessionId() { return innerResult?.sessionId || options.sessionId || ''; },
79
+ onToken: (cb) => { tokenCallbacks.push(cb); innerResult?.onToken(cb); },
80
+ onDone: (cb) => { doneCallbacks.push(cb); innerResult?.onDone(cb); },
81
+ onError: (cb) => { errorCallbacks.push(cb); innerResult?.onError(cb); },
82
+ abort: () => innerResult?.abort(),
83
+ };
84
+ }
85
+ /**
86
+ * Warm up the AI backend session in the background.
87
+ * Call on editor startup so the first user request is fast.
88
+ */
89
+ export async function warmUpAiSession(projectDir) {
90
+ const backend = await detectBackend();
91
+ if (backend === 'claude-code') {
92
+ try {
93
+ const cc = await import('./claude-code-client.js');
94
+ await cc.warmUpSession(projectDir);
95
+ console.log('[LumenJS] AI session warmed up');
96
+ }
97
+ catch {
98
+ // Non-fatal
99
+ }
100
+ }
101
+ }
102
+ /**
103
+ * Check if any AI backend is available.
104
+ */
105
+ export async function checkAiStatus() {
106
+ try {
107
+ const client = await getClient();
108
+ return client.checkAiStatus();
109
+ }
110
+ catch {
111
+ return { configured: false, backend: 'opencode' };
112
+ }
113
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Claude Code AI client — uses the Claude Agent SDK (@anthropic-ai/claude-agent-sdk).
3
+ * Spawns the `claude` CLI as a subprocess. Requires Claude Code CLI installed and logged in.
4
+ * Works with Pro/Max subscription — no API key needed.
5
+ */
6
+ import type { AiChatOptions, AiChatResult, AiStatusResult } from './types.js';
7
+ /**
8
+ * Warm up by pre-spawning a Claude Code session. The first real request
9
+ * will resume this session, skipping CLI cold-start entirely.
10
+ */
11
+ export declare function warmUpSession(projectDir: string): Promise<void>;
12
+ /**
13
+ * Stream an AI chat message via Claude Code Agent SDK.
14
+ */
15
+ export declare function streamAiChat(projectDir: string, options: AiChatOptions): AiChatResult;
16
+ /**
17
+ * Check if Claude Code CLI is installed and the Agent SDK is importable.
18
+ * Both are required — the CLI alone isn't enough.
19
+ */
20
+ export declare function checkAiStatus(): Promise<AiStatusResult>;
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Claude Code AI client — uses the Claude Agent SDK (@anthropic-ai/claude-agent-sdk).
3
+ * Spawns the `claude` CLI as a subprocess. Requires Claude Code CLI installed and logged in.
4
+ * Works with Pro/Max subscription — no API key needed.
5
+ */
6
+ import { SYSTEM_PROMPT, buildPrompt } from './types.js';
7
+ import { execSync } from 'child_process';
8
+ // ── SDK Cache & Session Warm-up ──────────────────────────────────
9
+ let _sdkCache = null;
10
+ let _warmSessionId = null;
11
+ async function getSdk() {
12
+ if (_sdkCache)
13
+ return _sdkCache;
14
+ const sdk = await import('@anthropic-ai/claude-agent-sdk');
15
+ const query = sdk.query || sdk.default?.query;
16
+ if (!query)
17
+ throw new Error('Claude Agent SDK loaded but query() not found');
18
+ _sdkCache = { query };
19
+ return _sdkCache;
20
+ }
21
+ /**
22
+ * Warm up by pre-spawning a Claude Code session. The first real request
23
+ * will resume this session, skipping CLI cold-start entirely.
24
+ */
25
+ export async function warmUpSession(projectDir) {
26
+ try {
27
+ const { query } = await getSdk();
28
+ const stream = query({
29
+ prompt: 'Ready.',
30
+ options: {
31
+ cwd: projectDir,
32
+ systemPrompt: SYSTEM_PROMPT,
33
+ allowedTools: ['Read', 'Edit', 'Write', 'Glob', 'Grep', 'Bash'],
34
+ maxTurns: 1,
35
+ model: 'sonnet',
36
+ effort: 'low',
37
+ persistSession: true,
38
+ },
39
+ });
40
+ for await (const msg of stream) {
41
+ if (msg.session_id) {
42
+ _warmSessionId = msg.session_id;
43
+ break;
44
+ }
45
+ }
46
+ }
47
+ catch {
48
+ // Non-fatal — first request will just be slower
49
+ }
50
+ }
51
+ /**
52
+ * Stream an AI chat message via Claude Code Agent SDK.
53
+ */
54
+ export function streamAiChat(projectDir, options) {
55
+ const tokenCallbacks = [];
56
+ const doneCallbacks = [];
57
+ const errorCallbacks = [];
58
+ let aborted = false;
59
+ let sessionId = options.sessionId || '';
60
+ const enrichedPrompt = buildPrompt(options);
61
+ const run = async () => {
62
+ try {
63
+ const { query } = await getSdk();
64
+ let fullText = '';
65
+ const queryOptions = {
66
+ cwd: projectDir,
67
+ systemPrompt: SYSTEM_PROMPT,
68
+ allowedTools: ['Read', 'Edit', 'Write', 'Glob', 'Grep', 'Bash'],
69
+ maxTurns: 10,
70
+ persistSession: true,
71
+ model: 'sonnet',
72
+ };
73
+ // Use Sonnet + low effort for fast mode (quick actions like text improvement, spacing)
74
+ if (options.model === 'fast') {
75
+ queryOptions.model = 'sonnet';
76
+ queryOptions.effort = 'low';
77
+ queryOptions.maxTurns = 5;
78
+ }
79
+ // Resume existing session, or use pre-warmed session for instant first request
80
+ if (sessionId) {
81
+ queryOptions.resume = sessionId;
82
+ }
83
+ else if (_warmSessionId) {
84
+ queryOptions.resume = _warmSessionId;
85
+ sessionId = _warmSessionId;
86
+ _warmSessionId = null;
87
+ }
88
+ const stream = query({
89
+ prompt: enrichedPrompt,
90
+ options: queryOptions,
91
+ });
92
+ for await (const msg of stream) {
93
+ if (aborted)
94
+ break;
95
+ // Capture session ID from any message
96
+ if (msg.session_id && !sessionId) {
97
+ sessionId = msg.session_id;
98
+ }
99
+ if (msg.type === 'assistant' && msg.message?.content) {
100
+ for (const block of msg.message.content) {
101
+ if (block.type === 'text' && block.text) {
102
+ fullText += block.text;
103
+ for (const cb of tokenCallbacks)
104
+ cb(block.text);
105
+ }
106
+ }
107
+ }
108
+ }
109
+ if (!aborted) {
110
+ for (const cb of doneCallbacks)
111
+ cb(fullText);
112
+ }
113
+ }
114
+ catch (err) {
115
+ if (aborted)
116
+ return;
117
+ for (const cb of errorCallbacks)
118
+ cb(err instanceof Error ? err : new Error(String(err)));
119
+ }
120
+ };
121
+ // Start the async flow
122
+ run();
123
+ return {
124
+ get sessionId() { return sessionId; },
125
+ onToken: (cb) => { tokenCallbacks.push(cb); },
126
+ onDone: (cb) => { doneCallbacks.push(cb); },
127
+ onError: (cb) => { errorCallbacks.push(cb); },
128
+ abort: () => { aborted = true; },
129
+ };
130
+ }
131
+ /**
132
+ * Check if Claude Code CLI is installed and the Agent SDK is importable.
133
+ * Both are required — the CLI alone isn't enough.
134
+ */
135
+ export async function checkAiStatus() {
136
+ try {
137
+ execSync('claude --version', { timeout: 5000, stdio: 'pipe' });
138
+ // Verify the SDK is actually importable (it's an optional dependency)
139
+ await import('@anthropic-ai/claude-agent-sdk');
140
+ return { configured: true, backend: 'claude-code' };
141
+ }
142
+ catch {
143
+ return { configured: false, backend: 'claude-code' };
144
+ }
145
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * DeepSeek AI client — uses the OpenAI-compatible chat completions API with SSE streaming.
3
+ * Configure via DEEPSEEK_API_KEY env var. Optionally set DEEPSEEK_BASE_URL and DEEPSEEK_MODEL.
4
+ */
5
+ import type { AiChatOptions, AiChatResult, AiStatusResult } from './types.js';
6
+ export declare function streamAiChat(projectDir: string, options: AiChatOptions): AiChatResult;
7
+ export declare function checkAiStatus(): Promise<AiStatusResult>;
@@ -0,0 +1,113 @@
1
+ /**
2
+ * DeepSeek AI client — uses the OpenAI-compatible chat completions API with SSE streaming.
3
+ * Configure via DEEPSEEK_API_KEY env var. Optionally set DEEPSEEK_BASE_URL and DEEPSEEK_MODEL.
4
+ */
5
+ import { SYSTEM_PROMPT, buildPrompt } from './types.js';
6
+ const DEEPSEEK_API_KEY = process.env.DEEPSEEK_API_KEY || '';
7
+ const DEEPSEEK_BASE_URL = process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com';
8
+ const DEEPSEEK_MODEL = process.env.DEEPSEEK_MODEL || 'deepseek-chat';
9
+ // Simple per-session message history
10
+ const sessions = new Map();
11
+ export function streamAiChat(projectDir, options) {
12
+ const tokenCallbacks = [];
13
+ const doneCallbacks = [];
14
+ const errorCallbacks = [];
15
+ const controller = new AbortController();
16
+ let sessionId = options.sessionId || `ds-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
17
+ const enrichedPrompt = buildPrompt(options);
18
+ const run = async () => {
19
+ try {
20
+ // Build message history
21
+ if (!sessions.has(sessionId)) {
22
+ sessions.set(sessionId, [{ role: 'system', content: SYSTEM_PROMPT }]);
23
+ }
24
+ const messages = sessions.get(sessionId);
25
+ messages.push({ role: 'user', content: enrichedPrompt });
26
+ const res = await fetch(`${DEEPSEEK_BASE_URL}/v1/chat/completions`, {
27
+ method: 'POST',
28
+ headers: {
29
+ 'Content-Type': 'application/json',
30
+ 'Authorization': `Bearer ${DEEPSEEK_API_KEY}`,
31
+ },
32
+ body: JSON.stringify({
33
+ model: DEEPSEEK_MODEL,
34
+ messages,
35
+ stream: true,
36
+ }),
37
+ signal: controller.signal,
38
+ });
39
+ if (!res.ok) {
40
+ const errText = await res.text().catch(() => '');
41
+ throw new Error(`DeepSeek API error: ${res.status} ${errText}`);
42
+ }
43
+ // Parse SSE stream
44
+ const reader = res.body?.getReader();
45
+ if (!reader)
46
+ throw new Error('No response body');
47
+ const decoder = new TextDecoder();
48
+ let fullText = '';
49
+ let buffer = '';
50
+ while (true) {
51
+ const { done, value } = await reader.read();
52
+ if (done)
53
+ break;
54
+ buffer += decoder.decode(value, { stream: true });
55
+ const lines = buffer.split('\n');
56
+ buffer = lines.pop() || '';
57
+ for (const line of lines) {
58
+ const trimmed = line.trim();
59
+ if (!trimmed || !trimmed.startsWith('data:'))
60
+ continue;
61
+ const data = trimmed.slice(5).trim();
62
+ if (data === '[DONE]')
63
+ continue;
64
+ try {
65
+ const parsed = JSON.parse(data);
66
+ const delta = parsed.choices?.[0]?.delta?.content;
67
+ if (delta) {
68
+ fullText += delta;
69
+ for (const cb of tokenCallbacks)
70
+ cb(delta);
71
+ }
72
+ }
73
+ catch {
74
+ // Skip malformed SSE lines
75
+ }
76
+ }
77
+ }
78
+ // Save assistant response to history
79
+ messages.push({ role: 'assistant', content: fullText });
80
+ for (const cb of doneCallbacks)
81
+ cb(fullText);
82
+ }
83
+ catch (err) {
84
+ if (err?.name === 'AbortError')
85
+ return;
86
+ for (const cb of errorCallbacks)
87
+ cb(err instanceof Error ? err : new Error(String(err)));
88
+ }
89
+ };
90
+ run();
91
+ return {
92
+ get sessionId() { return sessionId; },
93
+ onToken: (cb) => { tokenCallbacks.push(cb); },
94
+ onDone: (cb) => { doneCallbacks.push(cb); },
95
+ onError: (cb) => { errorCallbacks.push(cb); },
96
+ abort: () => controller.abort(),
97
+ };
98
+ }
99
+ export async function checkAiStatus() {
100
+ if (!DEEPSEEK_API_KEY) {
101
+ return { configured: false, backend: 'opencode' };
102
+ }
103
+ try {
104
+ const res = await fetch(`${DEEPSEEK_BASE_URL}/v1/models`, {
105
+ headers: { 'Authorization': `Bearer ${DEEPSEEK_API_KEY}` },
106
+ signal: AbortSignal.timeout(5000),
107
+ });
108
+ return { configured: res.ok, backend: 'opencode' };
109
+ }
110
+ catch {
111
+ return { configured: false, backend: 'opencode' };
112
+ }
113
+ }