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.
- package/LICENSE +200 -0
- package/README.md +98 -0
- package/babel-plugin-awel-source.cjs +79 -0
- package/bin/awel.js +2 -0
- package/dist/cli/agent.d.ts +6 -0
- package/dist/cli/agent.js +266 -0
- package/dist/cli/babel-setup.d.ts +1 -0
- package/dist/cli/babel-setup.js +180 -0
- package/dist/cli/comment-popup.d.ts +2 -0
- package/dist/cli/comment-popup.js +206 -0
- package/dist/cli/config.d.ts +14 -0
- package/dist/cli/config.js +29 -0
- package/dist/cli/devserver.d.ts +17 -0
- package/dist/cli/devserver.js +43 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +34 -0
- package/dist/cli/inspector.d.ts +2 -0
- package/dist/cli/inspector.js +117 -0
- package/dist/cli/logger.d.ts +10 -0
- package/dist/cli/logger.js +40 -0
- package/dist/cli/plan-store.d.ts +14 -0
- package/dist/cli/plan-store.js +18 -0
- package/dist/cli/providers/registry.d.ts +17 -0
- package/dist/cli/providers/registry.js +112 -0
- package/dist/cli/providers/types.d.ts +17 -0
- package/dist/cli/providers/types.js +1 -0
- package/dist/cli/providers/vercel.d.ts +4 -0
- package/dist/cli/providers/vercel.js +483 -0
- package/dist/cli/proxy.d.ts +5 -0
- package/dist/cli/proxy.js +72 -0
- package/dist/cli/server.d.ts +7 -0
- package/dist/cli/server.js +104 -0
- package/dist/cli/session.d.ts +32 -0
- package/dist/cli/session.js +77 -0
- package/dist/cli/skills/react-best-practices.md +2934 -0
- package/dist/cli/skills/skills/react-best-practices.md +2934 -0
- package/dist/cli/sse.d.ts +17 -0
- package/dist/cli/sse.js +51 -0
- package/dist/cli/subprocess.d.ts +30 -0
- package/dist/cli/subprocess.js +163 -0
- package/dist/cli/tools/ask-user.d.ts +11 -0
- package/dist/cli/tools/ask-user.js +28 -0
- package/dist/cli/tools/bash.d.ts +4 -0
- package/dist/cli/tools/bash.js +30 -0
- package/dist/cli/tools/code-search.d.ts +4 -0
- package/dist/cli/tools/code-search.js +70 -0
- package/dist/cli/tools/edit.d.ts +6 -0
- package/dist/cli/tools/edit.js +37 -0
- package/dist/cli/tools/glob.d.ts +4 -0
- package/dist/cli/tools/glob.js +29 -0
- package/dist/cli/tools/grep.d.ts +5 -0
- package/dist/cli/tools/grep.js +146 -0
- package/dist/cli/tools/index.d.ts +86 -0
- package/dist/cli/tools/index.js +41 -0
- package/dist/cli/tools/ls.d.ts +3 -0
- package/dist/cli/tools/ls.js +31 -0
- package/dist/cli/tools/multi-edit.d.ts +8 -0
- package/dist/cli/tools/multi-edit.js +53 -0
- package/dist/cli/tools/propose-plan.d.ts +4 -0
- package/dist/cli/tools/propose-plan.js +21 -0
- package/dist/cli/tools/react-best-practices.d.ts +3 -0
- package/dist/cli/tools/react-best-practices.js +55 -0
- package/dist/cli/tools/read.d.ts +3 -0
- package/dist/cli/tools/read.js +24 -0
- package/dist/cli/tools/restart-dev-server.d.ts +3 -0
- package/dist/cli/tools/restart-dev-server.js +18 -0
- package/dist/cli/tools/todo.d.ts +8 -0
- package/dist/cli/tools/todo.js +59 -0
- package/dist/cli/tools/web-fetch.d.ts +5 -0
- package/dist/cli/tools/web-fetch.js +116 -0
- package/dist/cli/tools/web-search.d.ts +5 -0
- package/dist/cli/tools/web-search.js +74 -0
- package/dist/cli/tools/write.d.ts +4 -0
- package/dist/cli/tools/write.js +26 -0
- package/dist/cli/types.d.ts +16 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/undo.d.ts +49 -0
- package/dist/cli/undo.js +212 -0
- package/dist/cli/verbose.d.ts +7 -0
- package/dist/cli/verbose.js +60 -0
- package/dist/dashboard/assets/index-Bk--q3wu.js +313 -0
- package/dist/dashboard/assets/index-DkWV03So.css +1 -0
- package/dist/dashboard/index.html +16 -0
- package/dist/host/host.js +274 -0
- 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,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, '<').replace(/>/g, '>');
|
|
149
|
+
if (elName) {
|
|
150
|
+
let html = '<span class="element-name">' + escapeHtml(elName) + '</span>';
|
|
151
|
+
if (elFile) html += '<span class="element-sep">·</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,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;
|