awel 0.1.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 (85) hide show
  1. package/LICENSE +200 -0
  2. package/README.md +98 -0
  3. package/babel-plugin-awel-source.cjs +79 -0
  4. package/bin/awel.js +2 -0
  5. package/dist/cli/agent.d.ts +6 -0
  6. package/dist/cli/agent.js +266 -0
  7. package/dist/cli/babel-setup.d.ts +1 -0
  8. package/dist/cli/babel-setup.js +180 -0
  9. package/dist/cli/comment-popup.d.ts +2 -0
  10. package/dist/cli/comment-popup.js +206 -0
  11. package/dist/cli/config.d.ts +14 -0
  12. package/dist/cli/config.js +29 -0
  13. package/dist/cli/devserver.d.ts +17 -0
  14. package/dist/cli/devserver.js +43 -0
  15. package/dist/cli/index.d.ts +1 -0
  16. package/dist/cli/index.js +34 -0
  17. package/dist/cli/inspector.d.ts +2 -0
  18. package/dist/cli/inspector.js +117 -0
  19. package/dist/cli/logger.d.ts +10 -0
  20. package/dist/cli/logger.js +40 -0
  21. package/dist/cli/plan-store.d.ts +14 -0
  22. package/dist/cli/plan-store.js +18 -0
  23. package/dist/cli/providers/registry.d.ts +17 -0
  24. package/dist/cli/providers/registry.js +112 -0
  25. package/dist/cli/providers/types.d.ts +17 -0
  26. package/dist/cli/providers/types.js +1 -0
  27. package/dist/cli/providers/vercel.d.ts +4 -0
  28. package/dist/cli/providers/vercel.js +483 -0
  29. package/dist/cli/proxy.d.ts +5 -0
  30. package/dist/cli/proxy.js +72 -0
  31. package/dist/cli/server.d.ts +7 -0
  32. package/dist/cli/server.js +104 -0
  33. package/dist/cli/session.d.ts +32 -0
  34. package/dist/cli/session.js +77 -0
  35. package/dist/cli/skills/react-best-practices.md +2934 -0
  36. package/dist/cli/skills/skills/react-best-practices.md +2934 -0
  37. package/dist/cli/sse.d.ts +17 -0
  38. package/dist/cli/sse.js +51 -0
  39. package/dist/cli/subprocess.d.ts +30 -0
  40. package/dist/cli/subprocess.js +163 -0
  41. package/dist/cli/tools/ask-user.d.ts +11 -0
  42. package/dist/cli/tools/ask-user.js +28 -0
  43. package/dist/cli/tools/bash.d.ts +4 -0
  44. package/dist/cli/tools/bash.js +30 -0
  45. package/dist/cli/tools/code-search.d.ts +4 -0
  46. package/dist/cli/tools/code-search.js +70 -0
  47. package/dist/cli/tools/edit.d.ts +6 -0
  48. package/dist/cli/tools/edit.js +37 -0
  49. package/dist/cli/tools/glob.d.ts +4 -0
  50. package/dist/cli/tools/glob.js +29 -0
  51. package/dist/cli/tools/grep.d.ts +5 -0
  52. package/dist/cli/tools/grep.js +146 -0
  53. package/dist/cli/tools/index.d.ts +86 -0
  54. package/dist/cli/tools/index.js +41 -0
  55. package/dist/cli/tools/ls.d.ts +3 -0
  56. package/dist/cli/tools/ls.js +31 -0
  57. package/dist/cli/tools/multi-edit.d.ts +8 -0
  58. package/dist/cli/tools/multi-edit.js +53 -0
  59. package/dist/cli/tools/propose-plan.d.ts +4 -0
  60. package/dist/cli/tools/propose-plan.js +21 -0
  61. package/dist/cli/tools/react-best-practices.d.ts +3 -0
  62. package/dist/cli/tools/react-best-practices.js +55 -0
  63. package/dist/cli/tools/read.d.ts +3 -0
  64. package/dist/cli/tools/read.js +24 -0
  65. package/dist/cli/tools/restart-dev-server.d.ts +3 -0
  66. package/dist/cli/tools/restart-dev-server.js +18 -0
  67. package/dist/cli/tools/todo.d.ts +8 -0
  68. package/dist/cli/tools/todo.js +59 -0
  69. package/dist/cli/tools/web-fetch.d.ts +5 -0
  70. package/dist/cli/tools/web-fetch.js +116 -0
  71. package/dist/cli/tools/web-search.d.ts +5 -0
  72. package/dist/cli/tools/web-search.js +74 -0
  73. package/dist/cli/tools/write.d.ts +4 -0
  74. package/dist/cli/tools/write.js +26 -0
  75. package/dist/cli/types.d.ts +16 -0
  76. package/dist/cli/types.js +2 -0
  77. package/dist/cli/undo.d.ts +49 -0
  78. package/dist/cli/undo.js +212 -0
  79. package/dist/cli/verbose.d.ts +7 -0
  80. package/dist/cli/verbose.js +60 -0
  81. package/dist/dashboard/assets/index-Bk--q3wu.js +313 -0
  82. package/dist/dashboard/assets/index-DkWV03So.css +1 -0
  83. package/dist/dashboard/index.html +16 -0
  84. package/dist/host/host.js +274 -0
  85. package/package.json +67 -0
@@ -0,0 +1,180 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const BABEL_CONFIG_FILES = [
6
+ 'babel.config.js',
7
+ 'babel.config.cjs',
8
+ 'babel.config.mjs',
9
+ '.babelrc',
10
+ '.babelrc.json',
11
+ '.babelrc.js',
12
+ '.babelrc.cjs',
13
+ ];
14
+ function getPluginPath() {
15
+ // babel-plugin-awel-source.cjs lives at the root of the cli package,
16
+ // which is one level up from dist/ (where this compiled file lives).
17
+ return join(__dirname, '..', '..', 'babel-plugin-awel-source.cjs');
18
+ }
19
+ function hasPackageJsonBabelKey(projectCwd) {
20
+ const pkgPath = join(projectCwd, 'package.json');
21
+ if (!existsSync(pkgPath))
22
+ return false;
23
+ try {
24
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
25
+ return 'babel' in pkg;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function findExistingBabelConfig(projectCwd) {
32
+ for (const file of BABEL_CONFIG_FILES) {
33
+ if (existsSync(join(projectCwd, file)))
34
+ return file;
35
+ }
36
+ if (hasPackageJsonBabelKey(projectCwd))
37
+ return 'package.json';
38
+ return null;
39
+ }
40
+ function readAwelConfig(projectCwd) {
41
+ const configPath = join(projectCwd, '.awel', 'config.json');
42
+ if (!existsSync(configPath))
43
+ return {};
44
+ try {
45
+ return JSON.parse(readFileSync(configPath, 'utf-8'));
46
+ }
47
+ catch {
48
+ return {};
49
+ }
50
+ }
51
+ function writeAwelConfig(projectCwd, config) {
52
+ const dir = join(projectCwd, '.awel');
53
+ if (!existsSync(dir)) {
54
+ mkdirSync(dir, { recursive: true });
55
+ }
56
+ writeFileSync(join(dir, 'config.json'), JSON.stringify(config, null, 2) + '\n', 'utf-8');
57
+ }
58
+ // ANSI 256-color helpers — darker shades that stay visible on light backgrounds
59
+ const bold = (s) => `\x1b[1m${s}\x1b[22m`;
60
+ const dim = (s) => `\x1b[2m${s}\x1b[22m`;
61
+ const green = (s) => `\x1b[38;5;34m${s}\x1b[39m`;
62
+ const cyan = (s) => `\x1b[38;5;30m${s}\x1b[39m`;
63
+ function promptSelect(title, description, options) {
64
+ return new Promise((resolve) => {
65
+ let selected = 0;
66
+ const { stdin, stdout } = process;
67
+ const hide = '\x1b[?25l'; // hide cursor
68
+ const show = '\x1b[?25h'; // show cursor
69
+ function render() {
70
+ // Move to start and clear from cursor down
71
+ let out = `\x1b[${options.length}A\x1b[J`;
72
+ for (let i = 0; i < options.length; i++) {
73
+ const pointer = i === selected ? green('❯') : ' ';
74
+ const label = i === selected ? bold(options[i].label) : dim(options[i].label);
75
+ out += ` ${pointer} ${label}\n`;
76
+ }
77
+ stdout.write(out);
78
+ }
79
+ function cleanup() {
80
+ stdin.setRawMode(false);
81
+ stdin.removeListener('data', onKey);
82
+ stdin.pause();
83
+ stdout.write(show);
84
+ }
85
+ // Print header + initial render
86
+ stdout.write(`\n${bold(green('?'))} ${bold(title)}\n`);
87
+ stdout.write(` ${dim(description)}\n\n`);
88
+ stdout.write(hide);
89
+ // Print placeholder lines so render() can overwrite them
90
+ for (let i = 0; i < options.length; i++)
91
+ stdout.write('\n');
92
+ render();
93
+ function onKey(data) {
94
+ const key = data.toString();
95
+ // Up arrow or k
96
+ if (key === '\x1b[A' || key === 'k') {
97
+ selected = (selected - 1 + options.length) % options.length;
98
+ render();
99
+ }
100
+ // Down arrow or j
101
+ else if (key === '\x1b[B' || key === 'j') {
102
+ selected = (selected + 1) % options.length;
103
+ render();
104
+ }
105
+ // Enter
106
+ else if (key === '\r' || key === '\n') {
107
+ cleanup();
108
+ // Overwrite options with the final selection
109
+ stdout.write(`\x1b[${options.length}A\x1b[J`);
110
+ stdout.write(` ${green('❯')} ${bold(options[selected].label)}\n\n`);
111
+ resolve(options[selected].value);
112
+ }
113
+ // Ctrl-C
114
+ else if (key === '\x03') {
115
+ cleanup();
116
+ stdout.write('\n');
117
+ resolve(false);
118
+ }
119
+ }
120
+ stdin.setRawMode(true);
121
+ stdin.resume();
122
+ stdin.on('data', onKey);
123
+ });
124
+ }
125
+ function createBabelConfig(projectCwd) {
126
+ const pluginPath = getPluginPath();
127
+ const configContent = `module.exports = {
128
+ presets: ['next/babel'],
129
+ plugins: [${JSON.stringify(pluginPath)}],
130
+ };
131
+ `;
132
+ writeFileSync(join(projectCwd, 'babel.config.js'), configContent, 'utf-8');
133
+ }
134
+ export async function ensureBabelPlugin(projectCwd) {
135
+ const pluginPath = getPluginPath();
136
+ const existing = findExistingBabelConfig(projectCwd);
137
+ if (existing) {
138
+ // Config exists — check if the awel plugin is already referenced
139
+ const configPath = existing === 'package.json'
140
+ ? join(projectCwd, 'package.json')
141
+ : join(projectCwd, existing);
142
+ const content = readFileSync(configPath, 'utf-8');
143
+ if (content.includes('awel-source'))
144
+ return;
145
+ console.log(`[Awel] Babel config found (${existing}) but Awel source plugin is not configured.`);
146
+ console.log(` Add this to your plugins array:`);
147
+ console.log(` require.resolve(${JSON.stringify(pluginPath)})`);
148
+ console.log(` Inspector source mapping will use runtime fiber fallback.`);
149
+ return;
150
+ }
151
+ // No babel config exists — check stored preference
152
+ const config = readAwelConfig(projectCwd);
153
+ if (config.babelPlugin === true) {
154
+ createBabelConfig(projectCwd);
155
+ console.log('[Awel] Created babel.config.js with source-mapping plugin (previously opted in).');
156
+ return;
157
+ }
158
+ if (config.babelPlugin === false) {
159
+ return;
160
+ }
161
+ // Never prompted — ask interactively (skip if not a TTY)
162
+ if (!process.stdin.isTTY) {
163
+ return;
164
+ }
165
+ const accepted = await promptSelect('Inspector source mapping', 'The Babel plugin gives click-to-source with exact line numbers,\n' +
166
+ ' but replaces Next.js SWC with Babel (slower builds).\n' +
167
+ ' Without it, the inspector still works via React fiber detection.', [
168
+ { label: '⚡ Skip — use runtime fiber detection (no build impact)', value: false },
169
+ { label: '🌸 Enable — create babel.config.js (best experience, slower builds)', value: true },
170
+ ]);
171
+ writeAwelConfig(projectCwd, { ...config, babelPlugin: accepted });
172
+ if (accepted) {
173
+ createBabelConfig(projectCwd);
174
+ console.log(`${green('✔')} Created ${cyan('babel.config.js')} with source-mapping plugin.`);
175
+ }
176
+ else {
177
+ console.log(`${dim('→')} Skipped Babel plugin. Inspector will use runtime fiber fallback.`);
178
+ console.log(` ${dim('Run with a fresh .awel/ to be asked again.')}`);
179
+ }
180
+ }
@@ -0,0 +1,2 @@
1
+ import { Hono } from 'hono';
2
+ export declare function createCommentPopupRoute(): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
@@ -0,0 +1,206 @@
1
+ import { Hono } from 'hono';
2
+ const commentPopupHtml = `<!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ background: #18181b;
12
+ color: #fafafa;
13
+ padding: 0 12px 12px;
14
+ height: 100vh;
15
+ display: flex;
16
+ flex-direction: column;
17
+ }
18
+ .header {
19
+ display: flex;
20
+ align-items: center;
21
+ gap: 6px;
22
+ padding: 10px 0 10px;
23
+ }
24
+ .header-left {
25
+ display: flex;
26
+ align-items: center;
27
+ gap: 6px;
28
+ flex-shrink: 0;
29
+ }
30
+ .header-title {
31
+ font-size: 12px;
32
+ font-weight: 600;
33
+ color: #e4e4e7;
34
+ }
35
+ .element-info {
36
+ margin-left: auto;
37
+ display: flex;
38
+ align-items: center;
39
+ gap: 4px;
40
+ font-size: 11px;
41
+ color: #a1a1aa;
42
+ overflow: hidden;
43
+ }
44
+ .element-name {
45
+ font-weight: 500;
46
+ white-space: nowrap;
47
+ overflow: hidden;
48
+ text-overflow: ellipsis;
49
+ max-width: 100px;
50
+ }
51
+ .element-sep {
52
+ color: #52525b;
53
+ }
54
+ .element-file {
55
+ color: #71717a;
56
+ font-family: ui-monospace, SFMono-Regular, monospace;
57
+ font-size: 10px;
58
+ white-space: nowrap;
59
+ overflow: hidden;
60
+ text-overflow: ellipsis;
61
+ max-width: 90px;
62
+ }
63
+ textarea {
64
+ flex: 1;
65
+ width: 100%;
66
+ background: #09090b;
67
+ border: 1px solid #27272a;
68
+ border-radius: 6px;
69
+ color: #fafafa;
70
+ font-family: inherit;
71
+ font-size: 13px;
72
+ padding: 8px 10px;
73
+ resize: none;
74
+ outline: none;
75
+ transition: border-color 0.15s ease;
76
+ }
77
+ textarea:focus {
78
+ border-color: #a1a1aa;
79
+ }
80
+ textarea::placeholder {
81
+ color: #71717a;
82
+ }
83
+ .buttons {
84
+ display: flex;
85
+ gap: 6px;
86
+ margin-top: 8px;
87
+ justify-content: flex-end;
88
+ }
89
+ button {
90
+ font-family: inherit;
91
+ font-size: 12px;
92
+ font-weight: 500;
93
+ padding: 6px 14px;
94
+ border-radius: 6px;
95
+ border: 1px solid #27272a;
96
+ cursor: pointer;
97
+ transition: all 0.15s ease;
98
+ }
99
+ button:active { transform: scale(0.97); }
100
+ .btn-close {
101
+ background: #27272a;
102
+ color: #a1a1aa;
103
+ }
104
+ .btn-close:hover {
105
+ background: #3f3f46;
106
+ color: #fafafa;
107
+ }
108
+ .btn-submit {
109
+ background: #fafafa;
110
+ border-color: #fafafa;
111
+ color: #18181b;
112
+ }
113
+ .btn-submit:hover {
114
+ background: #e4e4e7;
115
+ }
116
+ .btn-submit:disabled {
117
+ opacity: 0.4;
118
+ cursor: default;
119
+ }
120
+ .btn-submit kbd {
121
+ display: inline-block;
122
+ font-family: inherit;
123
+ font-size: 10px;
124
+ font-weight: 600;
125
+ margin-left: 6px;
126
+ opacity: 0.5;
127
+ }
128
+ </style>
129
+ </head>
130
+ <body>
131
+ <div class="header">
132
+ <div class="header-left">
133
+ <span style="font-size:13px;line-height:1">🌸</span>
134
+ <span class="header-title">Awel</span>
135
+ </div>
136
+ <div class="element-info" id="elementInfo"></div>
137
+ </div>
138
+ <textarea id="comment" placeholder="Describe what you want to change..." autofocus></textarea>
139
+ <div class="buttons">
140
+ <button class="btn-close" id="closeBtn">Cancel</button>
141
+ <button class="btn-submit" id="submitBtn" disabled>Send<kbd id="shortcutHint"></kbd></button>
142
+ </div>
143
+ <script>
144
+ const params = new URLSearchParams(window.location.search);
145
+ const elName = params.get('name');
146
+ const elFile = params.get('file');
147
+ const infoEl = document.getElementById('elementInfo');
148
+ const escapeHtml = (value) => value.replace(/</g, '&lt;').replace(/>/g, '&gt;');
149
+ if (elName) {
150
+ let html = '<span class="element-name">' + escapeHtml(elName) + '</span>';
151
+ if (elFile) html += '<span class="element-sep">&middot;</span><span class="element-file">' + escapeHtml(elFile) + '</span>';
152
+ infoEl.innerHTML = html;
153
+ } else {
154
+ infoEl.style.display = 'none';
155
+ }
156
+
157
+ const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
158
+ document.getElementById('shortcutHint').textContent = isMac ? '\\u2318\\u21A9' : 'Ctrl\\u21A9';
159
+
160
+ const textarea = document.getElementById('comment');
161
+ const submitBtn = document.getElementById('submitBtn');
162
+ const closeBtn = document.getElementById('closeBtn');
163
+
164
+ textarea.addEventListener('input', () => {
165
+ submitBtn.disabled = !textarea.value.trim();
166
+ });
167
+
168
+ function submit() {
169
+ const text = textarea.value.trim();
170
+ if (!text) return;
171
+ window.parent.postMessage({ type: 'AWEL_COMMENT_SUBMIT', comment: text }, '*');
172
+ }
173
+
174
+ function close() {
175
+ window.parent.postMessage({ type: 'AWEL_COMMENT_CLOSE' }, '*');
176
+ }
177
+
178
+ submitBtn.addEventListener('click', submit);
179
+ closeBtn.addEventListener('click', close);
180
+
181
+ textarea.addEventListener('keydown', (e) => {
182
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
183
+ e.preventDefault();
184
+ submit();
185
+ }
186
+ });
187
+
188
+ document.addEventListener('keydown', (e) => {
189
+ if (e.key === 'Escape') {
190
+ close();
191
+ }
192
+ });
193
+
194
+ // Explicitly focus — autofocus attribute is ignored inside iframes
195
+ window.addEventListener('focus', () => textarea.focus());
196
+ textarea.focus();
197
+ </script>
198
+ </body>
199
+ </html>`;
200
+ export function createCommentPopupRoute() {
201
+ const app = new Hono();
202
+ app.get('/_awel/comment-popup', (c) => {
203
+ return c.html(commentPopupHtml);
204
+ });
205
+ return app;
206
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared configuration constants for Awel
3
+ */
4
+ export declare const AWEL_PORT = 3001;
5
+ export declare const USER_APP_PORT = 3000;
6
+ export declare const DASHBOARD_URL = "http://localhost:3001/_awel/dashboard";
7
+ /**
8
+ * MIME type mappings for static file serving
9
+ */
10
+ export declare const MIME_TYPES: Record<string, string>;
11
+ /**
12
+ * Get MIME type for a file extension
13
+ */
14
+ export declare function getMimeType(filePath: string): string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Shared configuration constants for Awel
3
+ */
4
+ export const AWEL_PORT = 3001;
5
+ export const USER_APP_PORT = 3000;
6
+ export const DASHBOARD_URL = `http://localhost:${AWEL_PORT}/_awel/dashboard`;
7
+ /**
8
+ * MIME type mappings for static file serving
9
+ */
10
+ export const MIME_TYPES = {
11
+ 'js': 'application/javascript',
12
+ 'css': 'text/css',
13
+ 'svg': 'image/svg+xml',
14
+ 'png': 'image/png',
15
+ 'jpg': 'image/jpeg',
16
+ 'jpeg': 'image/jpeg',
17
+ 'html': 'text/html',
18
+ 'json': 'application/json',
19
+ 'woff': 'font/woff',
20
+ 'woff2': 'font/woff2',
21
+ 'ttf': 'font/ttf',
22
+ };
23
+ /**
24
+ * Get MIME type for a file extension
25
+ */
26
+ export function getMimeType(filePath) {
27
+ const ext = filePath.split('.').pop()?.toLowerCase() || '';
28
+ return MIME_TYPES[ext] || 'application/octet-stream';
29
+ }
@@ -0,0 +1,17 @@
1
+ import type { Socket } from 'net';
2
+ /**
3
+ * Register a target-side proxy socket.
4
+ * Called from the http-proxy 'open' event in server.ts.
5
+ */
6
+ export declare function trackProxySocket(socket: Socket): void;
7
+ /**
8
+ * Called when an agent stream starts.
9
+ * Pauses all target-side sockets so HMR data is buffered, not forwarded.
10
+ */
11
+ export declare function pauseDevServer(_port: number): void;
12
+ /**
13
+ * Called when an agent stream ends.
14
+ * Resumes target-side sockets — buffered HMR messages flow through
15
+ * and the browser picks up all accumulated changes at once.
16
+ */
17
+ export declare function resumeDevServer(_port: number): void;
@@ -0,0 +1,43 @@
1
+ // ─── HMR gate ────────────────────────────────────────────────
2
+ // While the agent is streaming we pause the target-side proxy
3
+ // sockets so HMR messages are buffered in the kernel rather than
4
+ // forwarded to the browser. The browser-side WebSocket stays
5
+ // connected (no disconnection-triggered reload). When the stream
6
+ // ends we resume the sockets — buffered messages flow through and
7
+ // the page picks up all changes in a single reload.
8
+ let streaming = false;
9
+ /** Target-side proxy sockets (the connection *to* the dev server). */
10
+ const proxySockets = new Set();
11
+ /**
12
+ * Register a target-side proxy socket.
13
+ * Called from the http-proxy 'open' event in server.ts.
14
+ */
15
+ export function trackProxySocket(socket) {
16
+ proxySockets.add(socket);
17
+ socket.once('close', () => proxySockets.delete(socket));
18
+ // If we're already streaming, pause immediately
19
+ if (streaming) {
20
+ socket.pause();
21
+ }
22
+ }
23
+ /**
24
+ * Called when an agent stream starts.
25
+ * Pauses all target-side sockets so HMR data is buffered, not forwarded.
26
+ */
27
+ export function pauseDevServer(_port) {
28
+ streaming = true;
29
+ for (const socket of proxySockets) {
30
+ socket.pause();
31
+ }
32
+ }
33
+ /**
34
+ * Called when an agent stream ends.
35
+ * Resumes target-side sockets — buffered HMR messages flow through
36
+ * and the browser picks up all accumulated changes at once.
37
+ */
38
+ export function resumeDevServer(_port) {
39
+ streaming = false;
40
+ for (const socket of proxySockets) {
41
+ socket.resume();
42
+ }
43
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,34 @@
1
+ import { program } from 'commander';
2
+ import { startServer } from './server.js';
3
+ import { AWEL_PORT, USER_APP_PORT } from './config.js';
4
+ import { setVerbose } from './verbose.js';
5
+ import { ensureBabelPlugin } from './babel-setup.js';
6
+ import { awel } from './logger.js';
7
+ import { spawnDevServer } from './subprocess.js';
8
+ program
9
+ .name('awel')
10
+ .description('AI-powered development overlay for Next.js')
11
+ .version('0.1.0');
12
+ program
13
+ .command('dev')
14
+ .description('Start the development server with Awel overlay')
15
+ .option('-p, --port <port>', 'Port for target app', String(USER_APP_PORT))
16
+ .option('-v, --verbose', 'Print all LLM stream events to stderr')
17
+ .action(async (options) => {
18
+ const targetPort = parseInt(options.port, 10);
19
+ if (options.verbose)
20
+ setVerbose(true);
21
+ await ensureBabelPlugin(process.cwd());
22
+ awel.log('🌟 Starting Awel...');
23
+ awel.log(` Target app port: ${targetPort}`);
24
+ awel.log(` Awel control server: http://localhost:${AWEL_PORT}`);
25
+ awel.log('');
26
+ // Start the Awel control server (proxy + dashboard)
27
+ await startServer({ awelPort: AWEL_PORT, targetPort, projectCwd: process.cwd() });
28
+ // Start the user's Next.js app via subprocess manager (handles auto-restart)
29
+ await spawnDevServer({ port: targetPort, cwd: process.cwd() });
30
+ awel.log('');
31
+ awel.log(`✨ Awel is ready! Open http://localhost:${AWEL_PORT}`);
32
+ awel.log(' Look for the floating button in the bottom-right corner.');
33
+ });
34
+ program.parse();
@@ -0,0 +1,2 @@
1
+ import { Hono } from 'hono';
2
+ export declare function createInspectorRoute(projectCwd: string): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
@@ -0,0 +1,117 @@
1
+ import { Hono } from 'hono';
2
+ import { streamSSE } from 'hono/streaming';
3
+ import { EventEmitter } from 'node:events';
4
+ import { readFileSync } from 'fs';
5
+ import { resolve, isAbsolute } from 'path';
6
+ const inspectorBus = new EventEmitter();
7
+ let bufferedSelection = null;
8
+ let sseClientConnected = false;
9
+ /**
10
+ * Enrich a selection with server-side context:
11
+ * - Source code snippet around the target line
12
+ * - Props type definition near the component
13
+ * - Whether the file has uncommitted changes
14
+ */
15
+ function enrichSelection(selection, projectCwd) {
16
+ if (!selection.source || !selection.line)
17
+ return selection;
18
+ const filePath = isAbsolute(selection.source)
19
+ ? selection.source
20
+ : resolve(projectCwd, selection.source);
21
+ // Read source snippet
22
+ try {
23
+ const content = readFileSync(filePath, 'utf-8');
24
+ const lines = content.split('\n');
25
+ const targetLine = selection.line;
26
+ const start = Math.max(0, targetLine - 10);
27
+ const end = Math.min(lines.length, targetLine + 10);
28
+ const snippetLines = [];
29
+ for (let i = start; i < end; i++) {
30
+ const lineNum = i + 1;
31
+ const marker = lineNum === targetLine ? ' > ' : ' ';
32
+ snippetLines.push(`${marker}${String(lineNum).padStart(4)} ${lines[i]}`);
33
+ }
34
+ selection.sourceSnippet = snippetLines.join('\n');
35
+ // Look for props type definition near the component
36
+ const componentName = selection.component;
37
+ if (componentName) {
38
+ const escapedName = componentName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
+ const propsPattern = new RegExp(`(?:interface|type)\\s+${escapedName}Props[\\s{<]`);
40
+ for (let i = 0; i < lines.length; i++) {
41
+ if (propsPattern.test(lines[i])) {
42
+ const defStart = i;
43
+ let defEnd = i;
44
+ // Grab lines until closing brace or 20 lines max
45
+ let braceDepth = 0;
46
+ for (let j = i; j < Math.min(lines.length, i + 20); j++) {
47
+ for (const ch of lines[j]) {
48
+ if (ch === '{')
49
+ braceDepth++;
50
+ if (ch === '}')
51
+ braceDepth--;
52
+ }
53
+ defEnd = j;
54
+ if (braceDepth <= 0 && j > i)
55
+ break;
56
+ }
57
+ selection.propsTypeDefinition = lines.slice(defStart, defEnd + 1).join('\n');
58
+ break;
59
+ }
60
+ }
61
+ }
62
+ }
63
+ catch {
64
+ // File not readable, skip enrichment
65
+ }
66
+ return selection;
67
+ }
68
+ export function createInspectorRoute(projectCwd) {
69
+ const inspector = new Hono();
70
+ // Host script POSTs the selected element here
71
+ inspector.post('/api/inspector/select', async (c) => {
72
+ let selection = await c.req.json();
73
+ selection = enrichSelection(selection, projectCwd);
74
+ if (sseClientConnected) {
75
+ inspectorBus.emit('selection', selection);
76
+ }
77
+ else {
78
+ bufferedSelection = selection;
79
+ }
80
+ return c.json({ ok: true });
81
+ });
82
+ // Dashboard connects here to receive selections in real time
83
+ inspector.get('/api/inspector/events', (c) => {
84
+ return streamSSE(c, async (stream) => {
85
+ sseClientConnected = true;
86
+ // Flush any buffered selection that arrived before we connected
87
+ if (bufferedSelection) {
88
+ await stream.writeSSE({
89
+ event: 'selection',
90
+ data: JSON.stringify(bufferedSelection),
91
+ });
92
+ bufferedSelection = null;
93
+ }
94
+ const onSelection = async (sel) => {
95
+ try {
96
+ await stream.writeSSE({
97
+ event: 'selection',
98
+ data: JSON.stringify(sel),
99
+ });
100
+ }
101
+ catch {
102
+ // Stream closed, ignore
103
+ }
104
+ };
105
+ inspectorBus.on('selection', onSelection);
106
+ // Keep the stream open until the client disconnects
107
+ await new Promise((resolve) => {
108
+ stream.onAbort(() => {
109
+ sseClientConnected = false;
110
+ inspectorBus.removeListener('selection', onSelection);
111
+ resolve();
112
+ });
113
+ });
114
+ });
115
+ });
116
+ return inspector;
117
+ }
@@ -0,0 +1,10 @@
1
+ import type { ResultPromise } from 'execa';
2
+ export declare const awel: {
3
+ log: (...args: unknown[]) => void;
4
+ error: (...args: unknown[]) => void;
5
+ };
6
+ /**
7
+ * Pipe a child process's stdout/stderr line-by-line, prefixing
8
+ * each line with the magenta [next] tag.
9
+ */
10
+ export declare function pipeChildOutput(child: ResultPromise): void;