@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.
- package/README.md +62 -282
- package/dist/auth/config.d.ts +23 -0
- package/dist/auth/config.js +115 -0
- package/dist/auth/guard.d.ts +12 -0
- package/dist/auth/guard.js +28 -0
- package/dist/auth/index.d.ts +3 -0
- package/dist/auth/index.js +1 -0
- package/dist/auth/middleware.d.ts +23 -0
- package/dist/auth/middleware.js +89 -0
- package/dist/auth/native-auth.d.ts +82 -0
- package/dist/auth/native-auth.js +340 -0
- package/dist/auth/oidc-client.d.ts +17 -0
- package/dist/auth/oidc-client.js +123 -0
- package/dist/auth/providers/google.d.ts +23 -0
- package/dist/auth/providers/google.js +25 -0
- package/dist/auth/providers/index.d.ts +2 -0
- package/dist/auth/providers/index.js +1 -0
- package/dist/auth/routes/login.d.ts +8 -0
- package/dist/auth/routes/login.js +121 -0
- package/dist/auth/routes/logout.d.ts +4 -0
- package/dist/auth/routes/logout.js +79 -0
- package/dist/auth/routes/oidc-callback.d.ts +3 -0
- package/dist/auth/routes/oidc-callback.js +70 -0
- package/dist/auth/routes/password.d.ts +5 -0
- package/dist/auth/routes/password.js +149 -0
- package/dist/auth/routes/signup.d.ts +3 -0
- package/dist/auth/routes/signup.js +81 -0
- package/dist/auth/routes/token.d.ts +4 -0
- package/dist/auth/routes/token.js +70 -0
- package/dist/auth/routes/totp.d.ts +22 -0
- package/dist/auth/routes/totp.js +232 -0
- package/dist/auth/routes/utils.d.ts +7 -0
- package/dist/auth/routes/utils.js +35 -0
- package/dist/auth/routes/verify.d.ts +3 -0
- package/dist/auth/routes/verify.js +26 -0
- package/dist/auth/routes.d.ts +8 -0
- package/dist/auth/routes.js +124 -0
- package/dist/auth/session.d.ts +8 -0
- package/dist/auth/session.js +54 -0
- package/dist/auth/token.d.ts +33 -0
- package/dist/auth/token.js +90 -0
- package/dist/auth/types.d.ts +156 -0
- package/dist/auth/types.js +2 -0
- package/dist/build/build-client.d.ts +15 -0
- package/dist/build/build-client.js +45 -0
- package/dist/build/build-prerender.d.ts +11 -0
- package/dist/build/build-prerender.js +159 -0
- package/dist/build/build-server.d.ts +18 -0
- package/dist/build/build-server.js +107 -0
- package/dist/build/build.js +60 -123
- package/dist/build/scan.d.ts +18 -0
- package/dist/build/scan.js +77 -6
- package/dist/build/serve-api.js +8 -2
- package/dist/build/serve-loaders.d.ts +4 -4
- package/dist/build/serve-loaders.js +26 -18
- package/dist/build/serve-ssr.js +38 -11
- package/dist/build/serve-static.js +3 -3
- package/dist/build/serve.js +341 -18
- package/dist/cli.js +37 -6
- package/dist/communication/encryption.d.ts +35 -0
- package/dist/communication/encryption.js +90 -0
- package/dist/communication/handlers/context.d.ts +27 -0
- package/dist/communication/handlers/context.js +1 -0
- package/dist/communication/handlers/conversation.d.ts +24 -0
- package/dist/communication/handlers/conversation.js +113 -0
- package/dist/communication/handlers/file-upload.d.ts +17 -0
- package/dist/communication/handlers/file-upload.js +62 -0
- package/dist/communication/handlers/messaging.d.ts +30 -0
- package/dist/communication/handlers/messaging.js +237 -0
- package/dist/communication/handlers/presence.d.ts +15 -0
- package/dist/communication/handlers/presence.js +76 -0
- package/dist/communication/handlers.d.ts +5 -0
- package/dist/communication/handlers.js +5 -0
- package/dist/communication/index.d.ts +9 -0
- package/dist/communication/index.js +7 -0
- package/dist/communication/link-preview.d.ts +18 -0
- package/dist/communication/link-preview.js +115 -0
- package/dist/communication/schema.d.ts +10 -0
- package/dist/communication/schema.js +101 -0
- package/dist/communication/server.d.ts +86 -0
- package/dist/communication/server.js +212 -0
- package/dist/communication/signaling.d.ts +43 -0
- package/dist/communication/signaling.js +271 -0
- package/dist/communication/store.d.ts +71 -0
- package/dist/communication/store.js +289 -0
- package/dist/communication/types.d.ts +454 -0
- package/dist/communication/types.js +1 -0
- package/dist/create.d.ts +1 -0
- package/dist/create.js +55 -0
- package/dist/db/auto-migrate.d.ts +3 -0
- package/dist/db/auto-migrate.js +100 -0
- package/dist/db/client.d.ts +3 -0
- package/dist/db/client.js +18 -0
- package/dist/db/index.d.ts +17 -13
- package/dist/db/index.js +205 -26
- package/dist/db/seed.d.ts +12 -0
- package/dist/db/seed.js +88 -0
- package/dist/db/table.d.ts +10 -0
- package/dist/db/table.js +12 -0
- package/dist/dev-server/config.d.ts +11 -0
- package/dist/dev-server/config.js +40 -20
- package/dist/dev-server/index-html.d.ts +4 -0
- package/dist/dev-server/index-html.js +21 -6
- package/dist/dev-server/nuralyui-aliases.d.ts +0 -4
- package/dist/dev-server/nuralyui-aliases.js +115 -94
- package/dist/dev-server/plugins/vite-plugin-api-routes.js +29 -5
- package/dist/dev-server/plugins/vite-plugin-auth.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-auth.js +223 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.d.ts +16 -0
- package/dist/dev-server/plugins/vite-plugin-auto-define.js +111 -0
- package/dist/dev-server/plugins/vite-plugin-communication.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-communication.js +205 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-editor-api.js +318 -0
- package/dist/dev-server/plugins/vite-plugin-i18n.js +69 -2
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.d.ts +6 -0
- package/dist/dev-server/plugins/vite-plugin-lit-dedup.js +78 -34
- package/dist/dev-server/plugins/vite-plugin-lit-hmr.js +44 -2
- package/dist/dev-server/plugins/vite-plugin-llms.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-llms.js +92 -0
- package/dist/dev-server/plugins/vite-plugin-loaders.js +146 -13
- package/dist/dev-server/plugins/vite-plugin-routes.js +16 -5
- package/dist/dev-server/plugins/vite-plugin-socketio.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-socketio.js +51 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.d.ts +2 -0
- package/dist/dev-server/plugins/vite-plugin-source-annotator.js +26 -3
- package/dist/dev-server/plugins/vite-plugin-storage.d.ts +10 -0
- package/dist/dev-server/plugins/vite-plugin-storage.js +126 -0
- package/dist/dev-server/plugins/vite-plugin-virtual-modules.js +140 -3
- package/dist/dev-server/server.js +242 -70
- package/dist/dev-server/ssr-render.d.ts +2 -1
- package/dist/dev-server/ssr-render.js +117 -50
- package/dist/editor/ai/backend.d.ts +20 -0
- package/dist/editor/ai/backend.js +113 -0
- package/dist/editor/ai/claude-code-client.d.ts +20 -0
- package/dist/editor/ai/claude-code-client.js +145 -0
- package/dist/editor/ai/deepseek-client.d.ts +7 -0
- package/dist/editor/ai/deepseek-client.js +113 -0
- package/dist/editor/ai/opencode-client.d.ts +14 -0
- package/dist/editor/ai/opencode-client.js +99 -0
- package/dist/editor/ai/snapshot-store.d.ts +22 -0
- package/dist/editor/ai/snapshot-store.js +35 -0
- package/dist/editor/ai/types.d.ts +30 -0
- package/dist/editor/ai/types.js +136 -0
- package/dist/editor/ai-chat-panel.d.ts +13 -0
- package/dist/editor/ai-chat-panel.js +613 -0
- package/dist/editor/ai-markdown.d.ts +10 -0
- package/dist/editor/ai-markdown.js +70 -0
- package/dist/editor/ai-project-panel.d.ts +11 -0
- package/dist/editor/ai-project-panel.js +332 -0
- package/dist/editor/ast-modification.d.ts +11 -0
- package/dist/editor/ast-modification.js +1 -0
- package/dist/editor/ast-service.d.ts +30 -0
- package/dist/editor/ast-service.js +180 -0
- package/dist/editor/css-rules.d.ts +54 -0
- package/dist/editor/css-rules.js +423 -0
- package/dist/editor/editor-api-client.d.ts +51 -0
- package/dist/editor/editor-api-client.js +162 -0
- package/dist/editor/editor-bridge.d.ts +1 -0
- package/dist/editor/editor-bridge.js +18 -8
- package/dist/editor/editor-toolbar.d.ts +14 -0
- package/dist/editor/editor-toolbar.js +115 -0
- package/dist/editor/file-editor.d.ts +9 -0
- package/dist/editor/file-editor.js +236 -0
- package/dist/editor/file-service.d.ts +16 -0
- package/dist/editor/file-service.js +52 -0
- package/dist/editor/i18n-key-gen.d.ts +1 -0
- package/dist/editor/i18n-key-gen.js +7 -0
- package/dist/editor/inline-text-edit.d.ts +5 -0
- package/dist/editor/inline-text-edit.js +173 -92
- package/dist/editor/overlay-events.d.ts +5 -0
- package/dist/editor/overlay-events.js +364 -0
- package/dist/editor/overlay-hmr.d.ts +2 -0
- package/dist/editor/overlay-hmr.js +76 -0
- package/dist/editor/overlay-selection.d.ts +29 -0
- package/dist/editor/overlay-selection.js +148 -0
- package/dist/editor/overlay-utils.d.ts +12 -0
- package/dist/editor/overlay-utils.js +59 -0
- package/dist/editor/properties-panel-persist.d.ts +14 -0
- package/dist/editor/properties-panel-persist.js +70 -0
- package/dist/editor/properties-panel-rows.d.ts +10 -0
- package/dist/editor/properties-panel-rows.js +349 -0
- package/dist/editor/properties-panel-styles.d.ts +4 -0
- package/dist/editor/properties-panel-styles.js +174 -0
- package/dist/editor/properties-panel.d.ts +4 -0
- package/dist/editor/properties-panel.js +148 -0
- package/dist/editor/property-registry.d.ts +16 -0
- package/dist/editor/property-registry.js +303 -0
- package/dist/editor/standalone-file-panel.d.ts +0 -0
- package/dist/editor/standalone-file-panel.js +1 -0
- package/dist/editor/standalone-overlay-dom.d.ts +0 -0
- package/dist/editor/standalone-overlay-dom.js +1 -0
- package/dist/editor/standalone-overlay-styles.d.ts +0 -0
- package/dist/editor/standalone-overlay-styles.js +1 -0
- package/dist/editor/standalone-overlay.d.ts +1 -0
- package/dist/editor/standalone-overlay.js +76 -0
- package/dist/editor/syntax-highlighter.d.ts +4 -0
- package/dist/editor/syntax-highlighter.js +81 -0
- package/dist/editor/text-toolbar.d.ts +11 -0
- package/dist/editor/text-toolbar.js +327 -0
- package/dist/editor/toolbar-styles.d.ts +4 -0
- package/dist/editor/toolbar-styles.js +198 -0
- package/dist/email/index.d.ts +32 -0
- package/dist/email/index.js +154 -0
- package/dist/email/providers/resend.d.ts +2 -0
- package/dist/email/providers/resend.js +24 -0
- package/dist/email/providers/sendgrid.d.ts +2 -0
- package/dist/email/providers/sendgrid.js +31 -0
- package/dist/email/providers/smtp.d.ts +13 -0
- package/dist/email/providers/smtp.js +125 -0
- package/dist/email/template-engine.d.ts +18 -0
- package/dist/email/template-engine.js +116 -0
- package/dist/email/templates/base.d.ts +9 -0
- package/dist/email/templates/base.js +65 -0
- package/dist/email/templates/password-reset.d.ts +5 -0
- package/dist/email/templates/password-reset.js +15 -0
- package/dist/email/templates/verify-email.d.ts +5 -0
- package/dist/email/templates/verify-email.js +15 -0
- package/dist/email/templates/welcome.d.ts +5 -0
- package/dist/email/templates/welcome.js +13 -0
- package/dist/email/types.d.ts +49 -0
- package/dist/email/types.js +1 -0
- package/dist/llms/generate.d.ts +46 -0
- package/dist/llms/generate.js +185 -0
- package/dist/permissions/guard.d.ts +28 -0
- package/dist/permissions/guard.js +30 -0
- package/dist/permissions/index.d.ts +6 -0
- package/dist/permissions/index.js +3 -0
- package/dist/permissions/service.d.ts +80 -0
- package/dist/permissions/service.js +210 -0
- package/dist/permissions/tables.d.ts +5 -0
- package/dist/permissions/tables.js +68 -0
- package/dist/permissions/types.d.ts +33 -0
- package/dist/permissions/types.js +1 -0
- package/dist/runtime/app-shell.d.ts +1 -1
- package/dist/runtime/app-shell.js +164 -0
- package/dist/runtime/auth.d.ts +10 -0
- package/dist/runtime/auth.js +30 -0
- package/dist/runtime/communication.d.ts +137 -0
- package/dist/runtime/communication.js +228 -0
- package/dist/runtime/error-boundary.d.ts +23 -0
- package/dist/runtime/error-boundary.js +120 -0
- package/dist/runtime/i18n.d.ts +6 -1
- package/dist/runtime/i18n.js +42 -21
- package/dist/runtime/island.d.ts +16 -0
- package/dist/runtime/island.js +80 -0
- package/dist/runtime/router-data.d.ts +3 -0
- package/dist/runtime/router-data.js +102 -17
- package/dist/runtime/router-hydration.js +34 -2
- package/dist/runtime/router.d.ts +19 -2
- package/dist/runtime/router.js +237 -43
- package/dist/runtime/socket-client.d.ts +2 -0
- package/dist/runtime/socket-client.js +30 -0
- package/dist/runtime/webrtc.d.ts +91 -0
- package/dist/runtime/webrtc.js +428 -0
- package/dist/shared/dom-shims.js +4 -2
- package/dist/shared/graceful-shutdown.d.ts +8 -0
- package/dist/shared/graceful-shutdown.js +36 -0
- package/dist/shared/health.d.ts +8 -0
- package/dist/shared/health.js +25 -0
- package/dist/shared/llms-txt.d.ts +31 -0
- package/dist/shared/llms-txt.js +85 -0
- package/dist/shared/logger.d.ts +32 -0
- package/dist/shared/logger.js +93 -0
- package/dist/shared/meta.d.ts +27 -0
- package/dist/shared/meta.js +71 -0
- package/dist/shared/middleware-runner.d.ts +9 -0
- package/dist/shared/middleware-runner.js +29 -0
- package/dist/shared/rate-limit.d.ts +18 -0
- package/dist/shared/rate-limit.js +71 -0
- package/dist/shared/request-id.d.ts +5 -0
- package/dist/shared/request-id.js +18 -0
- package/dist/shared/route-matching.js +16 -1
- package/dist/shared/security-headers.d.ts +18 -0
- package/dist/shared/security-headers.js +38 -0
- package/dist/shared/socket-io-setup.d.ts +11 -0
- package/dist/shared/socket-io-setup.js +51 -0
- package/dist/shared/types.d.ts +15 -0
- package/dist/shared/utils.d.ts +33 -7
- package/dist/shared/utils.js +164 -27
- package/dist/storage/adapters/local.d.ts +44 -0
- package/dist/storage/adapters/local.js +85 -0
- package/dist/storage/adapters/s3.d.ts +32 -0
- package/dist/storage/adapters/s3.js +119 -0
- package/dist/storage/adapters/types.d.ts +53 -0
- package/dist/storage/adapters/types.js +1 -0
- package/dist/storage/index.d.ts +76 -0
- package/dist/storage/index.js +83 -0
- package/package.json +45 -7
- package/templates/blog/api/posts.ts +4 -18
- package/templates/blog/data/migrations/001_init.sql +6 -5
- package/templates/blog/lumenjs.config.ts +3 -0
- package/templates/blog/package.json +14 -0
- package/templates/blog/pages/_layout.ts +25 -0
- package/templates/blog/pages/index.ts +48 -22
- package/templates/blog/pages/posts/[slug].ts +45 -20
- package/templates/blog/pages/tag/[tag].ts +44 -0
- package/templates/dashboard/api/stats.ts +8 -5
- package/templates/dashboard/lumenjs.config.ts +3 -0
- package/templates/dashboard/package.json +14 -0
- package/templates/dashboard/pages/_layout.ts +25 -0
- package/templates/dashboard/pages/index.ts +54 -23
- package/templates/dashboard/pages/settings/index.ts +29 -0
- package/templates/default/lumenjs.config.ts +3 -0
- package/templates/default/package.json +14 -0
- package/templates/default/pages/index.ts +24 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { filePathToTagName, dirToLayoutTagName } from '../../shared/utils.js';
|
|
3
|
+
/**
|
|
4
|
+
* Auto-registers custom elements for page and layout files.
|
|
5
|
+
*
|
|
6
|
+
* Scans for the exported class that extends LitElement and appends a
|
|
7
|
+
* `customElements.define('tag-name', ClassName)` call. The tag name is
|
|
8
|
+
* derived from the file path using the same convention the router uses:
|
|
9
|
+
*
|
|
10
|
+
* pages/index.ts → page-index
|
|
11
|
+
* pages/docs/routing.ts → page-docs-routing
|
|
12
|
+
* pages/_layout.ts → layout-root
|
|
13
|
+
* pages/docs/_layout.ts → layout-docs
|
|
14
|
+
*
|
|
15
|
+
* This removes the need for `@customElement('...')` in page/layout files.
|
|
16
|
+
*/
|
|
17
|
+
export function autoDefinePlugin(pagesDir) {
|
|
18
|
+
return {
|
|
19
|
+
name: 'lumenjs-auto-define',
|
|
20
|
+
enforce: 'pre',
|
|
21
|
+
transform(code, id) {
|
|
22
|
+
if (!id.startsWith(pagesDir) || !id.endsWith('.ts'))
|
|
23
|
+
return;
|
|
24
|
+
const relative = path.relative(pagesDir, id).replace(/\\/g, '/');
|
|
25
|
+
const basename = path.basename(relative, '.ts');
|
|
26
|
+
// Determine if this is a layout or a page
|
|
27
|
+
const isLayout = basename === '_layout';
|
|
28
|
+
if (!isLayout && basename.startsWith('_'))
|
|
29
|
+
return; // skip other _ files
|
|
30
|
+
// Derive the tag name
|
|
31
|
+
const tagName = isLayout
|
|
32
|
+
? dirToLayoutTagName(path.dirname(relative) === '.' ? '' : path.dirname(relative))
|
|
33
|
+
: filePathToTagName(relative);
|
|
34
|
+
// Find the exported class that extends LitElement or any parent class
|
|
35
|
+
const classMatch = code.match(/export\s+class\s+(\w+)\s+extends\s+(\w+)\b/);
|
|
36
|
+
if (!classMatch)
|
|
37
|
+
return;
|
|
38
|
+
const className = classMatch[1];
|
|
39
|
+
// Skip if already has a customElements.define or @customElement for this class
|
|
40
|
+
if (code.includes(`customElements.define('${tagName}'`))
|
|
41
|
+
return;
|
|
42
|
+
// Check for actual decorator usage (not mentions in HTML/text content)
|
|
43
|
+
if (/^\s*@customElement\s*\(/m.test(code))
|
|
44
|
+
return;
|
|
45
|
+
// Append the define call + HMR support
|
|
46
|
+
const defineCall = `
|
|
47
|
+
if (!customElements.get('${tagName}')) {
|
|
48
|
+
customElements.define('${tagName}', ${className});
|
|
49
|
+
}
|
|
50
|
+
if (import.meta.hot) {
|
|
51
|
+
import.meta.hot.accept((newModule) => {
|
|
52
|
+
if (!newModule) return;
|
|
53
|
+
const NewClass = newModule.${className};
|
|
54
|
+
if (!NewClass) return;
|
|
55
|
+
const OldClass = customElements.get('${tagName}');
|
|
56
|
+
if (!OldClass) return;
|
|
57
|
+
const descriptors = Object.getOwnPropertyDescriptors(NewClass.prototype);
|
|
58
|
+
for (const [key, desc] of Object.entries(descriptors)) {
|
|
59
|
+
if (key === 'constructor') continue;
|
|
60
|
+
Object.defineProperty(OldClass.prototype, key, desc);
|
|
61
|
+
}
|
|
62
|
+
const newCssText = NewClass.styles?.cssText || '';
|
|
63
|
+
if (NewClass.styles) {
|
|
64
|
+
OldClass.styles = NewClass.styles;
|
|
65
|
+
OldClass.elementStyles = undefined;
|
|
66
|
+
OldClass.finalized = false;
|
|
67
|
+
try { OldClass.finalizeStyles(); } catch {}
|
|
68
|
+
}
|
|
69
|
+
if (NewClass.properties) {
|
|
70
|
+
OldClass.properties = NewClass.properties;
|
|
71
|
+
}
|
|
72
|
+
function __queryShadowAll(root, sel) {
|
|
73
|
+
const results = [...root.querySelectorAll(sel)];
|
|
74
|
+
for (const el of root.querySelectorAll('*')) {
|
|
75
|
+
if (el.shadowRoot) results.push(...__queryShadowAll(el.shadowRoot, sel));
|
|
76
|
+
}
|
|
77
|
+
return results;
|
|
78
|
+
}
|
|
79
|
+
__queryShadowAll(document, '${tagName}').forEach((el) => {
|
|
80
|
+
if (el.renderRoot) {
|
|
81
|
+
// Update styles: try adoptedStyleSheets first, fall back to <style> tag
|
|
82
|
+
if (OldClass.elementStyles && OldClass.elementStyles.length > 0) {
|
|
83
|
+
const sheets = OldClass.elementStyles
|
|
84
|
+
.filter(s => s instanceof CSSStyleSheet || (s && s.styleSheet))
|
|
85
|
+
.map(s => s instanceof CSSStyleSheet ? s : s.styleSheet);
|
|
86
|
+
if (sheets.length && el.renderRoot.adoptedStyleSheets !== undefined) {
|
|
87
|
+
el.renderRoot.adoptedStyleSheets = sheets;
|
|
88
|
+
}
|
|
89
|
+
} else if (newCssText) {
|
|
90
|
+
const styleEl = el.renderRoot.querySelector('style');
|
|
91
|
+
if (styleEl) styleEl.textContent = newCssText;
|
|
92
|
+
}
|
|
93
|
+
// Clear stale SSR inline styles from child elements
|
|
94
|
+
el.renderRoot.querySelectorAll('[style]').forEach((child) => {
|
|
95
|
+
child.removeAttribute('style');
|
|
96
|
+
});
|
|
97
|
+
// Clear Lit's template cache to force re-render with new template
|
|
98
|
+
const childPart = Object.getOwnPropertySymbols(el.renderRoot)
|
|
99
|
+
.map(s => el.renderRoot[s])
|
|
100
|
+
.find(v => v && typeof v === 'object' && '_$committedValue' in v);
|
|
101
|
+
if (childPart) childPart._$committedValue = undefined;
|
|
102
|
+
}
|
|
103
|
+
if (el.requestUpdate) el.requestUpdate();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
return { code: code + defineCall, map: null };
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import crypto from 'crypto';
|
|
4
|
+
import { setProjectDir } from '../../db/context.js';
|
|
5
|
+
import { useDb } from '../../db/index.js';
|
|
6
|
+
import { ensureCommunicationTables } from '../../communication/schema.js';
|
|
7
|
+
import { createCommunicationApiHandlers } from '../../communication/server.js';
|
|
8
|
+
import { fetchLinkPreview, extractUrls } from '../../communication/link-preview.js';
|
|
9
|
+
/**
|
|
10
|
+
* Vite dev plugin for the communication module.
|
|
11
|
+
* Initializes DB tables and provides REST API endpoints for conversations/messages.
|
|
12
|
+
*/
|
|
13
|
+
export function communicationPlugin(projectDir) {
|
|
14
|
+
let db = null;
|
|
15
|
+
let api = null;
|
|
16
|
+
async function getApi() {
|
|
17
|
+
if (api)
|
|
18
|
+
return api;
|
|
19
|
+
try {
|
|
20
|
+
setProjectDir(projectDir);
|
|
21
|
+
db = useDb();
|
|
22
|
+
await ensureCommunicationTables(db);
|
|
23
|
+
api = createCommunicationApiHandlers(db);
|
|
24
|
+
console.log('[LumenJS] Communication module initialized');
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.warn('[LumenJS Communication] DB init failed:', err?.message);
|
|
28
|
+
}
|
|
29
|
+
return api;
|
|
30
|
+
}
|
|
31
|
+
const MAX_BODY_SIZE = 1024 * 1024; // 1 MB
|
|
32
|
+
function readBody(req, maxSize = MAX_BODY_SIZE) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
const chunks = [];
|
|
35
|
+
let size = 0;
|
|
36
|
+
req.on('data', (c) => {
|
|
37
|
+
size += c.length;
|
|
38
|
+
if (size > maxSize) {
|
|
39
|
+
req.destroy();
|
|
40
|
+
reject(new Error('Request body too large'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
chunks.push(c);
|
|
44
|
+
});
|
|
45
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
46
|
+
req.on('error', reject);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
function sendJson(res, status, data) {
|
|
50
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
51
|
+
res.end(JSON.stringify(data));
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
name: 'lumenjs-communication',
|
|
55
|
+
configureServer(server) {
|
|
56
|
+
// REST API for conversations and messages
|
|
57
|
+
server.middlewares.use(async (req, res, next) => {
|
|
58
|
+
const url = req.url || '';
|
|
59
|
+
if (!url.startsWith('/__nk_comm/'))
|
|
60
|
+
return next();
|
|
61
|
+
const commApi = await getApi();
|
|
62
|
+
if (!commApi) {
|
|
63
|
+
sendJson(res, 500, { error: 'Communication module not available' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const userId = req.nkAuth?.user?.sub;
|
|
67
|
+
const rest = url.split('?')[0].slice('/__nk_comm/'.length);
|
|
68
|
+
try {
|
|
69
|
+
// GET /__nk_comm/conversations
|
|
70
|
+
if (rest === 'conversations' && req.method === 'GET') {
|
|
71
|
+
if (!userId) {
|
|
72
|
+
sendJson(res, 401, { error: 'Unauthorized' });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const data = await commApi.getConversations(userId);
|
|
76
|
+
sendJson(res, 200, data);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// POST /__nk_comm/conversations
|
|
80
|
+
if (rest === 'conversations' && req.method === 'POST') {
|
|
81
|
+
if (!userId) {
|
|
82
|
+
sendJson(res, 401, { error: 'Unauthorized' });
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const body = JSON.parse(await readBody(req));
|
|
86
|
+
const conv = await commApi.createConversation({ ...body, participantIds: [userId, ...(body.participantIds || [])] });
|
|
87
|
+
sendJson(res, 201, conv);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// GET /__nk_comm/messages/<conversationId>
|
|
91
|
+
if (rest.startsWith('messages/') && req.method === 'GET') {
|
|
92
|
+
if (!userId) {
|
|
93
|
+
sendJson(res, 401, { error: 'Unauthorized' });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const conversationId = rest.slice('messages/'.length);
|
|
97
|
+
const data = await commApi.getMessages(conversationId);
|
|
98
|
+
sendJson(res, 200, data);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// GET /__nk_comm/search?q=...
|
|
102
|
+
if (rest === 'search' && req.method === 'GET') {
|
|
103
|
+
if (!userId) {
|
|
104
|
+
sendJson(res, 401, { error: 'Unauthorized' });
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const params = new URL(url, 'http://localhost').searchParams;
|
|
108
|
+
const data = await commApi.searchMessages(params.get('q') || '');
|
|
109
|
+
sendJson(res, 200, data);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// POST /__nk_comm/upload — file upload
|
|
113
|
+
if (rest === 'upload' && req.method === 'POST') {
|
|
114
|
+
if (!userId) {
|
|
115
|
+
sendJson(res, 401, { error: 'Unauthorized' });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const uploadDir = path.join(projectDir, 'data', 'uploads');
|
|
119
|
+
if (!fs.existsSync(uploadDir))
|
|
120
|
+
fs.mkdirSync(uploadDir, { recursive: true });
|
|
121
|
+
const MAX_UPLOAD_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
122
|
+
const chunks = [];
|
|
123
|
+
let uploadSize = 0;
|
|
124
|
+
req.on('data', (c) => {
|
|
125
|
+
uploadSize += c.length;
|
|
126
|
+
if (uploadSize > MAX_UPLOAD_SIZE) {
|
|
127
|
+
req.destroy();
|
|
128
|
+
sendJson(res, 413, { error: 'File too large' });
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
chunks.push(c);
|
|
132
|
+
});
|
|
133
|
+
req.on('end', () => {
|
|
134
|
+
if (uploadSize > MAX_UPLOAD_SIZE)
|
|
135
|
+
return;
|
|
136
|
+
const body = Buffer.concat(chunks);
|
|
137
|
+
const id = crypto.randomUUID();
|
|
138
|
+
const contentType = req.headers['content-type'] || '';
|
|
139
|
+
// Simple raw upload (client sends encrypted blob or raw file)
|
|
140
|
+
const ext = contentType.includes('image') ? '.bin' : '.bin';
|
|
141
|
+
const filePath = path.join(uploadDir, `${id}${ext}`);
|
|
142
|
+
fs.writeFileSync(filePath, body);
|
|
143
|
+
const fileUrl = `/__nk_comm/files/${id}`;
|
|
144
|
+
if (db) {
|
|
145
|
+
db.run('INSERT INTO attachments (id, filename, mimetype, size, url, uploaded_by, encrypted) VALUES (?, ?, ?, ?, ?, ?, ?)', id, req.headers['x-filename'] || `file-${id}`, contentType, body.length, fileUrl, userId, req.headers['x-encrypted'] === '1' ? 1 : 0).catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
sendJson(res, 201, { id, url: fileUrl, size: body.length });
|
|
148
|
+
});
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
// GET /__nk_comm/files/:id — serve uploaded file
|
|
152
|
+
if (rest.startsWith('files/') && req.method === 'GET') {
|
|
153
|
+
const fileId = rest.slice('files/'.length);
|
|
154
|
+
// Validate fileId contains only safe characters (UUID format)
|
|
155
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(fileId)) {
|
|
156
|
+
sendJson(res, 400, { error: 'Invalid file ID' });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const uploadDir = path.join(projectDir, 'data', 'uploads');
|
|
160
|
+
const filePath = path.resolve(uploadDir, `${fileId}.bin`);
|
|
161
|
+
if (!filePath.startsWith(uploadDir + path.sep)) {
|
|
162
|
+
sendJson(res, 400, { error: 'Invalid file ID' });
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (!fs.existsSync(filePath)) {
|
|
166
|
+
sendJson(res, 404, { error: 'File not found' });
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const stat = fs.statSync(filePath);
|
|
170
|
+
let contentType = 'application/octet-stream';
|
|
171
|
+
if (db) {
|
|
172
|
+
const att = await db.get('SELECT mimetype FROM attachments WHERE id = ?', fileId);
|
|
173
|
+
if (att)
|
|
174
|
+
contentType = att.mimetype;
|
|
175
|
+
}
|
|
176
|
+
res.writeHead(200, { 'Content-Type': contentType, 'Content-Length': stat.size, 'Cache-Control': 'public, max-age=86400' });
|
|
177
|
+
fs.createReadStream(filePath).pipe(res);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// POST /__nk_comm/link-preview — fetch link preview
|
|
181
|
+
if (rest === 'link-preview' && req.method === 'POST') {
|
|
182
|
+
if (!userId) {
|
|
183
|
+
sendJson(res, 401, { error: 'Unauthorized' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const body = JSON.parse(await readBody(req));
|
|
187
|
+
const urls = extractUrls(body.text || '');
|
|
188
|
+
const previews = [];
|
|
189
|
+
for (const u of urls) {
|
|
190
|
+
const preview = await fetchLinkPreview(u, db);
|
|
191
|
+
if (preview)
|
|
192
|
+
previews.push(preview);
|
|
193
|
+
}
|
|
194
|
+
sendJson(res, 200, { previews });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
sendJson(res, 404, { error: 'Not found' });
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
sendJson(res, 500, { error: err?.message || 'Internal error' });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { EditorFileService } from '../../editor/file-service.js';
|
|
2
|
+
import { AstService } from '../../editor/ast-service.js';
|
|
3
|
+
import { streamAiChat, checkAiStatus, warmUpAiSession } from '../../editor/ai/backend.js';
|
|
4
|
+
import * as snapshotStore from '../../editor/ai/snapshot-store.js';
|
|
5
|
+
import crypto from 'crypto';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
const EDITOR_PREFIX = '/__nk_editor/';
|
|
9
|
+
/**
|
|
10
|
+
* Vite plugin exposing editor API endpoints (only active in editor mode).
|
|
11
|
+
* Provides file CRUD and AST modification via /__nk_editor/* routes.
|
|
12
|
+
*/
|
|
13
|
+
export function editorApiPlugin(projectDir) {
|
|
14
|
+
const fileService = new EditorFileService(projectDir);
|
|
15
|
+
const astService = new AstService();
|
|
16
|
+
// Per-file write lock to prevent race conditions
|
|
17
|
+
const writeLocks = new Map();
|
|
18
|
+
async function withLock(filePath, fn) {
|
|
19
|
+
const prev = writeLocks.get(filePath) || Promise.resolve();
|
|
20
|
+
let resolve;
|
|
21
|
+
const next = new Promise(r => { resolve = r; });
|
|
22
|
+
writeLocks.set(filePath, next);
|
|
23
|
+
try {
|
|
24
|
+
await prev;
|
|
25
|
+
return await fn();
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
resolve();
|
|
29
|
+
if (writeLocks.get(filePath) === next) {
|
|
30
|
+
writeLocks.delete(filePath);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const MAX_EDITOR_BODY = 5 * 1024 * 1024; // 5 MB
|
|
35
|
+
function readBody(req, maxSize = MAX_EDITOR_BODY) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const chunks = [];
|
|
38
|
+
let size = 0;
|
|
39
|
+
req.on('data', (chunk) => {
|
|
40
|
+
size += chunk.length;
|
|
41
|
+
if (size > maxSize) {
|
|
42
|
+
req.destroy();
|
|
43
|
+
reject(new Error('Request body too large'));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
chunks.push(chunk);
|
|
47
|
+
});
|
|
48
|
+
req.on('end', () => resolve(Buffer.concat(chunks).toString()));
|
|
49
|
+
req.on('error', reject);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function sendJson(res, status, data) {
|
|
53
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
54
|
+
res.end(JSON.stringify(data));
|
|
55
|
+
}
|
|
56
|
+
function sendError(res, status, message) {
|
|
57
|
+
sendJson(res, status, { error: message });
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
name: 'lumenjs-editor-api',
|
|
61
|
+
configureServer(server) {
|
|
62
|
+
// Warm up AI session in background so first request is fast
|
|
63
|
+
warmUpAiSession(projectDir).catch(() => { });
|
|
64
|
+
server.middlewares.use(async (req, res, next) => {
|
|
65
|
+
if (!req.url || !req.url.startsWith(EDITOR_PREFIX))
|
|
66
|
+
return next();
|
|
67
|
+
const urlPath = req.url.split('?')[0];
|
|
68
|
+
const rest = urlPath.slice(EDITOR_PREFIX.length);
|
|
69
|
+
try {
|
|
70
|
+
// GET /__nk_editor/files — list all files
|
|
71
|
+
if (rest === 'files' && req.method === 'GET') {
|
|
72
|
+
const files = fileService.listFiles();
|
|
73
|
+
sendJson(res, 200, { files });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// GET /__nk_editor/files/<path> — read file
|
|
77
|
+
if (rest.startsWith('files/') && req.method === 'GET') {
|
|
78
|
+
const filePath = decodeURIComponent(rest.slice('files/'.length));
|
|
79
|
+
const content = fileService.readFile(filePath);
|
|
80
|
+
sendJson(res, 200, { content });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// PUT /__nk_editor/files/<path> — write file
|
|
84
|
+
if (rest.startsWith('files/') && req.method === 'PUT') {
|
|
85
|
+
const filePath = decodeURIComponent(rest.slice('files/'.length));
|
|
86
|
+
const body = JSON.parse(await readBody(req));
|
|
87
|
+
await withLock(filePath, async () => {
|
|
88
|
+
fileService.writeFile(filePath, body.content);
|
|
89
|
+
});
|
|
90
|
+
// Notify i18n HMR when a locale file is written
|
|
91
|
+
const localeMatch = filePath.match(/^locales\/([a-z]{2}(?:-[a-zA-Z]+)?)\.json$/);
|
|
92
|
+
if (localeMatch) {
|
|
93
|
+
server.ws.send({
|
|
94
|
+
type: 'custom',
|
|
95
|
+
event: 'lumenjs:i18n-update',
|
|
96
|
+
data: { locale: localeMatch[1] },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
res.writeHead(204);
|
|
100
|
+
res.end();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// POST /__nk_editor/ast/<path> — apply AST modification
|
|
104
|
+
if (rest.startsWith('ast/') && req.method === 'POST') {
|
|
105
|
+
const filePath = decodeURIComponent(rest.slice('ast/'.length));
|
|
106
|
+
const mod = JSON.parse(await readBody(req));
|
|
107
|
+
const content = await withLock(filePath, async () => {
|
|
108
|
+
const source = fileService.readFile(filePath);
|
|
109
|
+
const modified = await astService.applyModification(source, mod);
|
|
110
|
+
fileService.writeFile(filePath, modified);
|
|
111
|
+
return modified;
|
|
112
|
+
});
|
|
113
|
+
sendJson(res, 200, { content });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// GET /__nk_editor/ai/status — check if OpenCode is reachable
|
|
117
|
+
if (rest === 'ai/status' && req.method === 'GET') {
|
|
118
|
+
const status = await checkAiStatus();
|
|
119
|
+
sendJson(res, 200, status);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
// POST /__nk_editor/ai/chat — proxy to OpenCode with SSE streaming
|
|
123
|
+
if (rest === 'ai/chat' && req.method === 'POST') {
|
|
124
|
+
const body = JSON.parse(await readBody(req));
|
|
125
|
+
const { mode, prompt, context, sessionId, model } = body;
|
|
126
|
+
if (!prompt) {
|
|
127
|
+
sendError(res, 400, 'Missing prompt');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Snapshot files before AI modifies them
|
|
131
|
+
// For element mode, only snapshot the source file (much faster)
|
|
132
|
+
// For project mode, snapshot all files
|
|
133
|
+
const turnId = crypto.randomUUID();
|
|
134
|
+
try {
|
|
135
|
+
const fileContents = new Map();
|
|
136
|
+
if (mode === 'element' && context?.sourceFile) {
|
|
137
|
+
try {
|
|
138
|
+
fileContents.set(context.sourceFile, fileService.readFile(context.sourceFile));
|
|
139
|
+
}
|
|
140
|
+
catch { /* skip */ }
|
|
141
|
+
// Multi-element: also snapshot additional source files
|
|
142
|
+
if (context.sourceFiles && Array.isArray(context.sourceFiles)) {
|
|
143
|
+
for (const sf of context.sourceFiles) {
|
|
144
|
+
if (!fileContents.has(sf)) {
|
|
145
|
+
try {
|
|
146
|
+
fileContents.set(sf, fileService.readFile(sf));
|
|
147
|
+
}
|
|
148
|
+
catch { /* skip */ }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
const files = fileService.listFiles();
|
|
155
|
+
for (const f of files) {
|
|
156
|
+
try {
|
|
157
|
+
fileContents.set(f, fileService.readFile(f));
|
|
158
|
+
}
|
|
159
|
+
catch { /* skip */ }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
snapshotStore.save(turnId, fileContents);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
// Non-fatal — rollback just won't be available
|
|
166
|
+
}
|
|
167
|
+
// If element mode with a source file, read its content for richer context
|
|
168
|
+
let enrichedContext = context || {};
|
|
169
|
+
if (mode === 'element' && context?.sourceFile) {
|
|
170
|
+
try {
|
|
171
|
+
const fullContent = fileService.readFile(context.sourceFile);
|
|
172
|
+
let sourceContent = fullContent;
|
|
173
|
+
// Trim to ±20 lines around the target line to reduce token usage
|
|
174
|
+
if (context.sourceLine && typeof context.sourceLine === 'number') {
|
|
175
|
+
const lines = fullContent.split('\n');
|
|
176
|
+
const start = Math.max(0, context.sourceLine - 20);
|
|
177
|
+
const end = Math.min(lines.length, context.sourceLine + 20);
|
|
178
|
+
const trimmed = [];
|
|
179
|
+
if (start > 0)
|
|
180
|
+
trimmed.push(`// ... (lines 1-${start} omitted)`);
|
|
181
|
+
trimmed.push(...lines.slice(start, end));
|
|
182
|
+
if (end < lines.length)
|
|
183
|
+
trimmed.push(`// ... (lines ${end + 1}-${lines.length} omitted)`);
|
|
184
|
+
sourceContent = trimmed.join('\n');
|
|
185
|
+
}
|
|
186
|
+
enrichedContext = { ...context, sourceContent };
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
// File might not exist, continue without content
|
|
190
|
+
}
|
|
191
|
+
// Multi-element: read additional source files beyond the primary
|
|
192
|
+
if (context.sourceFiles && Array.isArray(context.sourceFiles)) {
|
|
193
|
+
const additionalSources = {};
|
|
194
|
+
for (const sf of context.sourceFiles) {
|
|
195
|
+
if (sf === context.sourceFile)
|
|
196
|
+
continue; // already read above
|
|
197
|
+
try {
|
|
198
|
+
additionalSources[sf] = fileService.readFile(sf);
|
|
199
|
+
}
|
|
200
|
+
catch { /* skip */ }
|
|
201
|
+
}
|
|
202
|
+
if (Object.keys(additionalSources).length > 0) {
|
|
203
|
+
enrichedContext = { ...enrichedContext, additionalSources };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// If project has i18n, include locale translations so the AI knows
|
|
208
|
+
// to edit locale JSON files instead of hardcoding text in templates.
|
|
209
|
+
const localesDir = path.join(projectDir, 'locales');
|
|
210
|
+
if (fs.existsSync(localesDir)) {
|
|
211
|
+
try {
|
|
212
|
+
const localeFiles = fs.readdirSync(localesDir).filter(f => f.endsWith('.json'));
|
|
213
|
+
const translations = {};
|
|
214
|
+
for (const f of localeFiles) {
|
|
215
|
+
const locale = f.replace('.json', '');
|
|
216
|
+
translations[locale] = JSON.parse(fs.readFileSync(path.join(localesDir, f), 'utf-8'));
|
|
217
|
+
}
|
|
218
|
+
enrichedContext = { ...enrichedContext, i18n: { translations } };
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
// Non-fatal
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Set up SSE response
|
|
225
|
+
res.writeHead(200, {
|
|
226
|
+
'Content-Type': 'text/event-stream',
|
|
227
|
+
'Cache-Control': 'no-cache',
|
|
228
|
+
'Connection': 'keep-alive',
|
|
229
|
+
'X-Accel-Buffering': 'no',
|
|
230
|
+
});
|
|
231
|
+
const result = streamAiChat(projectDir, {
|
|
232
|
+
mode: mode || 'project',
|
|
233
|
+
prompt,
|
|
234
|
+
context: enrichedContext,
|
|
235
|
+
sessionId,
|
|
236
|
+
model: model || 'default',
|
|
237
|
+
});
|
|
238
|
+
// Handle client disconnect
|
|
239
|
+
req.on('close', () => result.abort());
|
|
240
|
+
result.onToken((text) => {
|
|
241
|
+
res.write(`event: token\ndata: ${JSON.stringify({ text })}\n\n`);
|
|
242
|
+
});
|
|
243
|
+
result.onDone((fullText) => {
|
|
244
|
+
res.write(`event: done\ndata: ${JSON.stringify({ sessionId: result.sessionId, turnId, fullText })}\n\n`);
|
|
245
|
+
res.end();
|
|
246
|
+
});
|
|
247
|
+
result.onError((err) => {
|
|
248
|
+
res.write(`event: error\ndata: ${JSON.stringify({ message: err.message })}\n\n`);
|
|
249
|
+
res.end();
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
// POST /__nk_editor/ai/rollback — restore file snapshots
|
|
254
|
+
if (rest === 'ai/rollback' && req.method === 'POST') {
|
|
255
|
+
const body = JSON.parse(await readBody(req));
|
|
256
|
+
const { turnId } = body;
|
|
257
|
+
if (!turnId) {
|
|
258
|
+
sendError(res, 400, 'Missing turnId');
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const files = snapshotStore.restore(turnId);
|
|
262
|
+
if (!files) {
|
|
263
|
+
sendError(res, 404, 'Snapshot not found');
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const restoredFiles = [];
|
|
267
|
+
for (const [filePath, content] of files) {
|
|
268
|
+
try {
|
|
269
|
+
fileService.writeFile(filePath, content);
|
|
270
|
+
restoredFiles.push(filePath);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Skip files that can't be written
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
sendJson(res, 200, { restored: true, files: restoredFiles });
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
sendError(res, 404, 'Not found');
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
const message = err?.message || 'Internal server error';
|
|
283
|
+
const status = message === 'Path traversal detected' ? 403 : 500;
|
|
284
|
+
sendError(res, status, message);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
// Watch package.json for changes (e.g. after AI runs npm install).
|
|
288
|
+
// When dependencies change, restart Vite to re-run dependency optimization.
|
|
289
|
+
const pkgJsonPath = path.join(projectDir, 'package.json');
|
|
290
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
291
|
+
let lastPkgDeps = readDepsHash(pkgJsonPath);
|
|
292
|
+
const watcher = fs.watch(pkgJsonPath, { persistent: false }, () => {
|
|
293
|
+
const currentDeps = readDepsHash(pkgJsonPath);
|
|
294
|
+
if (currentDeps !== lastPkgDeps) {
|
|
295
|
+
lastPkgDeps = currentDeps;
|
|
296
|
+
console.log('[LumenJS] Dependencies changed — restarting dev server...');
|
|
297
|
+
server.restart();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
server.httpServer?.on('close', () => watcher.close());
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
/** Hash the dependencies/devDependencies from package.json to detect changes. */
|
|
306
|
+
function readDepsHash(pkgJsonPath) {
|
|
307
|
+
try {
|
|
308
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
309
|
+
const deps = JSON.stringify({
|
|
310
|
+
d: pkg.dependencies || {},
|
|
311
|
+
v: pkg.devDependencies || {},
|
|
312
|
+
});
|
|
313
|
+
return deps;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return '';
|
|
317
|
+
}
|
|
318
|
+
}
|