awel 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +8 -0
  2. package/README.zh-CN.md +8 -0
  3. package/dist/cli/agent.d.ts +2 -1
  4. package/dist/cli/agent.js +3 -2
  5. package/dist/cli/awel-config.d.ts +10 -0
  6. package/dist/cli/awel-config.js +28 -0
  7. package/dist/cli/babel-setup.js +2 -19
  8. package/dist/cli/create.d.ts +1 -0
  9. package/dist/cli/create.js +136 -0
  10. package/dist/cli/index.js +60 -4
  11. package/dist/cli/onboarding.d.ts +1 -0
  12. package/dist/cli/onboarding.js +58 -0
  13. package/dist/cli/providers/registry.d.ts +12 -0
  14. package/dist/cli/providers/registry.js +41 -0
  15. package/dist/cli/providers/types.d.ts +2 -0
  16. package/dist/cli/providers/vercel.js +44 -1
  17. package/dist/cli/proxy.d.ts +1 -1
  18. package/dist/cli/proxy.js +40 -7
  19. package/dist/cli/server.d.ts +2 -1
  20. package/dist/cli/server.js +14 -3
  21. package/dist/cli/templates/blank/AGENT.md +24 -0
  22. package/dist/cli/templates/blank/src/app/page.tsx +10 -0
  23. package/dist/cli/templates/blog/AGENT.md +26 -0
  24. package/dist/cli/templates/blog/src/app/blog/[slug]/page.tsx +38 -0
  25. package/dist/cli/templates/blog/src/app/page.tsx +35 -0
  26. package/dist/cli/templates/blog/src/lib/posts.ts +42 -0
  27. package/dist/cli/templates/dashboard/AGENT.md +26 -0
  28. package/dist/cli/templates/dashboard/src/app/layout.tsx +28 -0
  29. package/dist/cli/templates/dashboard/src/app/page.tsx +30 -0
  30. package/dist/cli/templates/dashboard/src/components/Sidebar.tsx +31 -0
  31. package/dist/cli/templates/dashboard/src/components/StatsCard.tsx +24 -0
  32. package/dist/cli/templates/landing/AGENT.md +26 -0
  33. package/dist/cli/templates/landing/src/app/page.tsx +13 -0
  34. package/dist/cli/templates/landing/src/components/CTA.tsx +23 -0
  35. package/dist/cli/templates/landing/src/components/Features.tsx +46 -0
  36. package/dist/cli/templates/landing/src/components/Hero.tsx +26 -0
  37. package/dist/cli/templates/portfolio/AGENT.md +24 -0
  38. package/dist/cli/templates/portfolio/src/app/page.tsx +50 -0
  39. package/dist/cli/templates/portfolio/src/components/ProjectCard.tsx +30 -0
  40. package/dist/cli/templates/saas/AGENT.md +28 -0
  41. package/dist/cli/templates/saas/src/app/dashboard/page.tsx +10 -0
  42. package/dist/cli/templates/saas/src/app/page.tsx +37 -0
  43. package/dist/cli/templates/saas/src/app/pricing/page.tsx +53 -0
  44. package/dist/cli/templates/saas/src/components/Navbar.tsx +25 -0
  45. package/dist/cli/templates/saas/src/components/PricingCard.tsx +46 -0
  46. package/dist/cli/templates/templates/blank/AGENT.md +24 -0
  47. package/dist/cli/templates/templates/blank/src/app/page.tsx +10 -0
  48. package/dist/cli/templates/templates/blog/AGENT.md +26 -0
  49. package/dist/cli/templates/templates/blog/src/app/blog/[slug]/page.tsx +38 -0
  50. package/dist/cli/templates/templates/blog/src/app/page.tsx +35 -0
  51. package/dist/cli/templates/templates/blog/src/lib/posts.ts +42 -0
  52. package/dist/cli/templates/templates/dashboard/AGENT.md +26 -0
  53. package/dist/cli/templates/templates/dashboard/src/app/layout.tsx +28 -0
  54. package/dist/cli/templates/templates/dashboard/src/app/page.tsx +30 -0
  55. package/dist/cli/templates/templates/dashboard/src/components/Sidebar.tsx +31 -0
  56. package/dist/cli/templates/templates/dashboard/src/components/StatsCard.tsx +24 -0
  57. package/dist/cli/templates/templates/landing/AGENT.md +26 -0
  58. package/dist/cli/templates/templates/landing/src/app/page.tsx +13 -0
  59. package/dist/cli/templates/templates/landing/src/components/CTA.tsx +23 -0
  60. package/dist/cli/templates/templates/landing/src/components/Features.tsx +46 -0
  61. package/dist/cli/templates/templates/landing/src/components/Hero.tsx +26 -0
  62. package/dist/cli/templates/templates/portfolio/AGENT.md +24 -0
  63. package/dist/cli/templates/templates/portfolio/src/app/page.tsx +50 -0
  64. package/dist/cli/templates/templates/portfolio/src/components/ProjectCard.tsx +30 -0
  65. package/dist/cli/templates/templates/saas/AGENT.md +28 -0
  66. package/dist/cli/templates/templates/saas/src/app/dashboard/page.tsx +10 -0
  67. package/dist/cli/templates/templates/saas/src/app/page.tsx +37 -0
  68. package/dist/cli/templates/templates/saas/src/app/pricing/page.tsx +53 -0
  69. package/dist/cli/templates/templates/saas/src/components/Navbar.tsx +25 -0
  70. package/dist/cli/templates/templates/saas/src/components/PricingCard.tsx +46 -0
  71. package/dist/dashboard/assets/index-3djfaQxV.js +323 -0
  72. package/dist/dashboard/assets/index-BQNq3kAP.css +1 -0
  73. package/dist/dashboard/index.html +2 -2
  74. package/dist/host/host.js +2 -8
  75. package/package.json +2 -1
  76. package/dist/dashboard/assets/index-B374_cjZ.css +0 -1
  77. package/dist/dashboard/assets/index-BJ6wUfxa.js +0 -318
package/README.md CHANGED
@@ -12,9 +12,17 @@ AI-powered development overlay for Next.js. Awel runs a proxy in front of your d
12
12
  # Skip if you're already in a Next.js app
13
13
  npx create-next-app@latest my-app && cd my-app
14
14
 
15
+ # Set up at least one AI provider (pick one):
16
+ export ANTHROPIC_API_KEY="sk-ant-..." # Anthropic API
17
+ export OPENAI_API_KEY="sk-..." # OpenAI
18
+ export GOOGLE_GENERATIVE_AI_API_KEY="..." # Google AI
19
+ # Or install the Claude CLI: https://docs.anthropic.com/en/docs/claude-code
20
+
15
21
  npx awel dev
16
22
  ```
17
23
 
24
+ Awel needs at least one configured provider to function. See [Supported Models](#supported-models) for the full list.
25
+
18
26
  This starts Awel on port 3001 and proxies your Next.js dev server on port 3000. Open `http://localhost:3001` to see your app with the Awel overlay.
19
27
 
20
28
  ### Options
package/README.zh-CN.md CHANGED
@@ -12,9 +12,17 @@
12
12
  # 如果你已经在一个 Next.js 项目中,可以跳过这一步
13
13
  npx create-next-app@latest my-app && cd my-app
14
14
 
15
+ # 至少配置一个 AI 服务商(任选其一):
16
+ export ANTHROPIC_API_KEY="sk-ant-..." # Anthropic API
17
+ export OPENAI_API_KEY="sk-..." # OpenAI
18
+ export GOOGLE_GENERATIVE_AI_API_KEY="..." # Google AI
19
+ # 或安装 Claude CLI:https://docs.anthropic.com/en/docs/claude-code
20
+
15
21
  npx awel dev
16
22
  ```
17
23
 
24
+ Awel 需要至少一个已配置的服务商才能运行。完整列表见[支持的模型](#支持的模型)。
25
+
18
26
  Awel 会在端口 3001 启动,并代理运行在端口 3000 的 Next.js 开发服务器。打开 `http://localhost:3001` 即可看到带有 Awel 浮层的应用。
19
27
 
20
28
  ### 选项
@@ -2,5 +2,6 @@ import { Hono } from 'hono';
2
2
  /**
3
3
  * Creates the agent API routes with SSE streaming.
4
4
  * @param projectCwd - The user's project directory (where they ran `awel dev`)
5
+ * @param isFresh - Getter for whether the project is in creation mode
5
6
  */
6
- export declare function createAgentRoute(projectCwd: string, targetPort: number): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
7
+ export declare function createAgentRoute(projectCwd: string, targetPort: number, isFresh?: () => boolean): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
package/dist/cli/agent.js CHANGED
@@ -95,8 +95,9 @@ function setSSEHeaders(c) {
95
95
  /**
96
96
  * Creates the agent API routes with SSE streaming.
97
97
  * @param projectCwd - The user's project directory (where they ran `awel dev`)
98
+ * @param isFresh - Getter for whether the project is in creation mode
98
99
  */
99
- export function createAgentRoute(projectCwd, targetPort) {
100
+ export function createAgentRoute(projectCwd, targetPort, isFresh) {
100
101
  const agent = new Hono();
101
102
  // ─── Model Catalog ───────────────────────────────────────
102
103
  agent.get('/api/models', (c) => {
@@ -158,7 +159,7 @@ export function createAgentRoute(projectCwd, targetPort) {
158
159
  // Appending eagerly would leave orphan user messages in the session when
159
160
  // the stream is aborted or paused for user input, causing consecutive
160
161
  // user messages that trigger API 400 errors (especially with Anthropic).
161
- provider.streamResponse(adapter, messages, { projectCwd, targetPort, signal })
162
+ provider.streamResponse(adapter, messages, { projectCwd, targetPort, signal, creationMode: isFresh?.() })
162
163
  .then((responseMessages) => {
163
164
  if (responseMessages.length > 0) {
164
165
  appendUserMessage(userContent);
@@ -0,0 +1,10 @@
1
+ export interface AwelConfig {
2
+ babelPlugin?: boolean;
3
+ onboarded?: boolean;
4
+ fresh?: boolean;
5
+ createdAt?: string;
6
+ }
7
+ export declare function readAwelConfig(projectCwd: string): AwelConfig;
8
+ export declare function writeAwelConfig(projectCwd: string, config: AwelConfig): void;
9
+ export declare function isProjectFresh(projectCwd: string): boolean;
10
+ export declare function markProjectReady(projectCwd: string): void;
@@ -0,0 +1,28 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ export function readAwelConfig(projectCwd) {
4
+ const configPath = join(projectCwd, '.awel', 'config.json');
5
+ if (!existsSync(configPath))
6
+ return {};
7
+ try {
8
+ return JSON.parse(readFileSync(configPath, 'utf-8'));
9
+ }
10
+ catch {
11
+ return {};
12
+ }
13
+ }
14
+ export function writeAwelConfig(projectCwd, config) {
15
+ const dir = join(projectCwd, '.awel');
16
+ if (!existsSync(dir)) {
17
+ mkdirSync(dir, { recursive: true });
18
+ }
19
+ writeFileSync(join(dir, 'config.json'), JSON.stringify(config, null, 2) + '\n', 'utf-8');
20
+ }
21
+ export function isProjectFresh(projectCwd) {
22
+ return readAwelConfig(projectCwd).fresh === true;
23
+ }
24
+ export function markProjectReady(projectCwd) {
25
+ const config = readAwelConfig(projectCwd);
26
+ config.fresh = false;
27
+ writeAwelConfig(projectCwd, config);
28
+ }
@@ -1,6 +1,7 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
1
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
2
2
  import { join, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { readAwelConfig, writeAwelConfig } from './awel-config.js';
4
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
5
6
  const BABEL_CONFIG_FILES = [
6
7
  'babel.config.js',
@@ -37,24 +38,6 @@ function findExistingBabelConfig(projectCwd) {
37
38
  return 'package.json';
38
39
  return null;
39
40
  }
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
41
  // ANSI 256-color helpers — darker shades that stay visible on light backgrounds
59
42
  const bold = (s) => `\x1b[1m${s}\x1b[22m`;
60
43
  const dim = (s) => `\x1b[2m${s}\x1b[22m`;
@@ -0,0 +1 @@
1
+ export declare function runCreate(projectNameArg?: string): Promise<void>;
@@ -0,0 +1,136 @@
1
+ import { existsSync, cpSync, readdirSync } from 'fs';
2
+ import { join, dirname, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { execSync } from 'child_process';
5
+ import * as p from '@clack/prompts';
6
+ import { writeAwelConfig } from './awel-config.js';
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ const TEMPLATES = [
9
+ { value: 'blank', label: 'Blank', hint: 'Empty Next.js app with AGENT.md' },
10
+ { value: 'landing', label: 'Landing Page', hint: 'Hero, features, and CTA sections' },
11
+ { value: 'dashboard', label: 'Dashboard', hint: 'Sidebar layout with stats cards' },
12
+ { value: 'blog', label: 'Blog', hint: 'Blog listing with dynamic post pages' },
13
+ { value: 'portfolio', label: 'Portfolio', hint: 'Project showcase with cards' },
14
+ { value: 'saas', label: 'SaaS', hint: 'Landing, pricing, and dashboard pages' },
15
+ ];
16
+ function detectPackageManager() {
17
+ const agent = process.env.npm_config_user_agent;
18
+ if (agent) {
19
+ if (agent.startsWith('pnpm'))
20
+ return 'pnpm';
21
+ if (agent.startsWith('yarn'))
22
+ return 'yarn';
23
+ if (agent.startsWith('bun'))
24
+ return 'bun';
25
+ }
26
+ return 'npm';
27
+ }
28
+ function getTemplatesDir() {
29
+ return join(__dirname, 'templates');
30
+ }
31
+ export async function runCreate(projectNameArg) {
32
+ p.intro('Create a new Awel project');
33
+ // 1. Project name
34
+ let projectName;
35
+ if (projectNameArg) {
36
+ projectName = projectNameArg;
37
+ p.log.info(`Project name: ${projectName}`);
38
+ }
39
+ else {
40
+ const result = await p.text({
41
+ message: 'What is your project name?',
42
+ placeholder: 'my-app',
43
+ validate(value) {
44
+ if (!value || !value.trim())
45
+ return 'Project name is required';
46
+ if (!/^[a-zA-Z0-9_-]+$/.test(value.trim()))
47
+ return 'Project name can only contain letters, numbers, hyphens, and underscores';
48
+ },
49
+ });
50
+ if (p.isCancel(result)) {
51
+ p.cancel('Operation cancelled.');
52
+ process.exit(0);
53
+ }
54
+ projectName = result;
55
+ }
56
+ const projectDir = resolve(process.cwd(), projectName);
57
+ // Check if directory already exists
58
+ if (existsSync(projectDir)) {
59
+ p.cancel(`Directory "${projectName}" already exists.`);
60
+ process.exit(1);
61
+ }
62
+ // 2. Template selection
63
+ const template = await p.select({
64
+ message: 'Which template would you like to use?',
65
+ options: TEMPLATES,
66
+ });
67
+ if (p.isCancel(template)) {
68
+ p.cancel('Operation cancelled.');
69
+ process.exit(0);
70
+ }
71
+ // 3. Package manager
72
+ const detected = detectPackageManager();
73
+ const packageManager = await p.select({
74
+ message: 'Which package manager do you want to use?',
75
+ options: [
76
+ { value: 'npm', label: 'npm', hint: detected === 'npm' ? 'detected' : undefined },
77
+ { value: 'pnpm', label: 'pnpm', hint: detected === 'pnpm' ? 'detected' : undefined },
78
+ { value: 'yarn', label: 'yarn', hint: detected === 'yarn' ? 'detected' : undefined },
79
+ { value: 'bun', label: 'bun', hint: detected === 'bun' ? 'detected' : undefined },
80
+ ],
81
+ initialValue: detected,
82
+ });
83
+ if (p.isCancel(packageManager)) {
84
+ p.cancel('Operation cancelled.');
85
+ process.exit(0);
86
+ }
87
+ const pm = packageManager;
88
+ const templateName = template;
89
+ // 4. Scaffold with create-next-app
90
+ const s = p.spinner();
91
+ s.start('Creating Next.js app...');
92
+ const useFlag = pm === 'npm' ? '--use-npm' : pm === 'pnpm' ? '--use-pnpm' : pm === 'yarn' ? '--use-yarn' : '--use-bun';
93
+ const cnaArgs = [
94
+ 'create-next-app@latest',
95
+ projectDir,
96
+ '--ts',
97
+ '--tailwind',
98
+ '--eslint',
99
+ '--app',
100
+ '--src-dir',
101
+ '--import-alias', '@/*',
102
+ useFlag,
103
+ ];
104
+ try {
105
+ execSync(`npx ${cnaArgs.map(a => `"${a}"`).join(' ')}`, {
106
+ stdio: 'inherit',
107
+ timeout: 120000,
108
+ });
109
+ }
110
+ catch (err) {
111
+ s.stop('Failed to create Next.js app.');
112
+ const message = err instanceof Error ? err.message : String(err);
113
+ p.log.error(message);
114
+ process.exit(1);
115
+ }
116
+ s.stop('Next.js app created.');
117
+ // 5. Copy template overlay files
118
+ s.start('Applying template...');
119
+ const templateDir = join(getTemplatesDir(), templateName);
120
+ if (existsSync(templateDir) && readdirSync(templateDir).length > 0) {
121
+ cpSync(templateDir, projectDir, { recursive: true, force: true });
122
+ }
123
+ s.stop('Template applied.');
124
+ // 6. Write Awel config
125
+ writeAwelConfig(projectDir, {
126
+ template: templateName,
127
+ packageManager: pm,
128
+ createdAt: new Date().toISOString(),
129
+ });
130
+ // 7. Done
131
+ p.log.success(`Project "${projectName}" created with the "${templateName}" template.`);
132
+ p.log.info('Next steps:');
133
+ p.log.step(` cd ${projectName}`);
134
+ p.log.step(' npx awel dev');
135
+ p.outro('Happy building!');
136
+ }
package/dist/cli/index.js CHANGED
@@ -3,8 +3,11 @@ import { startServer } from './server.js';
3
3
  import { AWEL_PORT, USER_APP_PORT } from './config.js';
4
4
  import { setVerbose } from './verbose.js';
5
5
  import { ensureBabelPlugin } from './babel-setup.js';
6
+ import { ensureProvider } from './onboarding.js';
6
7
  import { awel } from './logger.js';
7
8
  import { spawnDevServer } from './subprocess.js';
9
+ import { writeAwelConfig, isProjectFresh } from './awel-config.js';
10
+ import { resolve } from 'path';
8
11
  program
9
12
  .name('awel')
10
13
  .description('AI-powered development overlay for Next.js')
@@ -18,17 +21,70 @@ program
18
21
  const targetPort = parseInt(options.port, 10);
19
22
  if (options.verbose)
20
23
  setVerbose(true);
21
- await ensureBabelPlugin(process.cwd());
24
+ const fresh = isProjectFresh(process.cwd());
25
+ await ensureProvider(process.cwd());
26
+ if (!fresh)
27
+ await ensureBabelPlugin(process.cwd());
22
28
  awel.log('🌟 Starting Awel...');
29
+ if (fresh)
30
+ awel.log(' Mode: Creation (new project)');
23
31
  awel.log(` Target app port: ${targetPort}`);
24
32
  awel.log(` Awel control server: http://localhost:${AWEL_PORT}`);
25
33
  awel.log('');
26
34
  // Start the Awel control server (proxy + dashboard)
27
- await startServer({ awelPort: AWEL_PORT, targetPort, projectCwd: process.cwd() });
35
+ await startServer({ awelPort: AWEL_PORT, targetPort, projectCwd: process.cwd(), fresh });
28
36
  // Start the user's Next.js app via subprocess manager (handles auto-restart)
29
37
  await spawnDevServer({ port: targetPort, cwd: process.cwd() });
30
38
  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.');
39
+ if (fresh) {
40
+ awel.log(`✨ Awel is ready! Open http://localhost:${AWEL_PORT}`);
41
+ awel.log(' Describe what you want to build and Awel will create it for you.');
42
+ }
43
+ else {
44
+ awel.log(`✨ Awel is ready! Open http://localhost:${AWEL_PORT}`);
45
+ awel.log(' Look for the floating button in the bottom-right corner.');
46
+ }
47
+ });
48
+ program
49
+ .command('create')
50
+ .description('Create a new Next.js project with Awel')
51
+ .action(async () => {
52
+ const p = await import('@clack/prompts');
53
+ const { execa } = await import('execa');
54
+ p.intro('Create a new project with Awel');
55
+ const name = await p.text({
56
+ message: 'Project name',
57
+ placeholder: 'my-app',
58
+ validate: (value) => {
59
+ if (!value)
60
+ return 'Project name is required';
61
+ if (!/^[a-z0-9._-]+$/i.test(value))
62
+ return 'Project name must only contain letters, numbers, dashes, dots, and underscores';
63
+ },
64
+ });
65
+ if (p.isCancel(name)) {
66
+ p.cancel('Cancelled');
67
+ process.exit(0);
68
+ }
69
+ const s = p.spinner();
70
+ s.start('Creating Next.js project...');
71
+ try {
72
+ await execa('npx', [
73
+ 'create-next-app@latest', name,
74
+ '--yes',
75
+ '--typescript', '--tailwind', '--eslint',
76
+ '--app', '--use-npm',
77
+ ], { cwd: process.cwd(), stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' });
78
+ }
79
+ catch (err) {
80
+ s.stop('Failed to create project');
81
+ const stderr = err.stderr || err.message || String(err);
82
+ p.log.error(stderr);
83
+ process.exit(1);
84
+ }
85
+ s.stop('Project created');
86
+ const projectDir = resolve(process.cwd(), name);
87
+ writeAwelConfig(projectDir, { fresh: true, createdAt: new Date().toISOString() });
88
+ p.outro(`Done! Now run:\n\n cd ${name}\n npx awel dev`);
33
89
  });
34
90
  program.parse();
@@ -0,0 +1 @@
1
+ export declare function ensureProvider(projectCwd: string): Promise<void>;
@@ -0,0 +1,58 @@
1
+ import { readAwelConfig, writeAwelConfig } from './awel-config.js';
2
+ import { getAvailableProviders, PROVIDER_ENV_KEYS, PROVIDER_LABELS } from './providers/registry.js';
3
+ import { awel } from './logger.js';
4
+ function printSetupInstructions() {
5
+ awel.log('No LLM providers are configured.');
6
+ awel.log('Awel needs at least one AI provider to function.');
7
+ awel.log('');
8
+ awel.log('Set up a provider by exporting its API key:');
9
+ awel.log('');
10
+ // List all providers with their setup commands
11
+ for (const [provider, envKey] of Object.entries(PROVIDER_ENV_KEYS)) {
12
+ const label = PROVIDER_LABELS[provider] ?? provider;
13
+ awel.log(` ${label}`);
14
+ awel.log(` export ${envKey}="..."`);
15
+ awel.log('');
16
+ }
17
+ // Claude Code is special — no env var, needs CLI install
18
+ const label = PROVIDER_LABELS['claude-code'] ?? 'Claude Code';
19
+ awel.log(` ${label}`);
20
+ awel.log(' Install the Claude CLI: https://docs.anthropic.com/en/docs/claude-code');
21
+ awel.log('');
22
+ awel.log('Then run `awel dev` again.');
23
+ }
24
+ export async function ensureProvider(projectCwd) {
25
+ const config = readAwelConfig(projectCwd);
26
+ const providers = getAvailableProviders();
27
+ const available = providers.filter(p => p.available);
28
+ const isFirstRun = !config.onboarded;
29
+ if (isFirstRun && available.length > 0) {
30
+ // First run with providers available — show welcome
31
+ awel.log('');
32
+ awel.log('Welcome to Awel!');
33
+ awel.log('AI-powered development overlay for Next.js');
34
+ awel.log('');
35
+ awel.log(`\u2714 ${available.length} provider${available.length === 1 ? '' : 's'} available:`);
36
+ for (const p of available) {
37
+ awel.log(` \u25CF ${p.label}`);
38
+ }
39
+ awel.log('');
40
+ writeAwelConfig(projectCwd, { ...config, onboarded: true });
41
+ return;
42
+ }
43
+ if (isFirstRun && available.length === 0) {
44
+ // First run with NO providers — show welcome + instructions, exit
45
+ awel.log('');
46
+ awel.log('Welcome to Awel!');
47
+ awel.log('AI-powered development overlay for Next.js');
48
+ awel.log('');
49
+ printSetupInstructions();
50
+ process.exit(1);
51
+ }
52
+ if (!isFirstRun && available.length === 0) {
53
+ // Subsequent run with NO providers — instructions only, exit
54
+ printSetupInstructions();
55
+ process.exit(1);
56
+ }
57
+ // Subsequent run with providers available — silent pass-through
58
+ }
@@ -1,5 +1,17 @@
1
1
  import type { StreamProvider, ModelDefinition } from './types.js';
2
2
  export declare const PROVIDER_ENV_KEYS: Record<string, string>;
3
+ export declare const PROVIDER_LABELS: Record<string, string>;
4
+ export interface ProviderAvailability {
5
+ provider: string;
6
+ label: string;
7
+ available: boolean;
8
+ envVar: string | null;
9
+ }
10
+ /**
11
+ * Returns deduplicated provider availability info.
12
+ * Checks each provider once (claude-code via binary check, others via env var).
13
+ */
14
+ export declare function getAvailableProviders(): ProviderAvailability[];
3
15
  export declare function resolveProvider(modelId: string): {
4
16
  provider: StreamProvider;
5
17
  modelProvider: string;
@@ -59,6 +59,47 @@ function isClaudeBinaryAvailable() {
59
59
  }
60
60
  return _claudeBinaryAvailable;
61
61
  }
62
+ // ─── Provider Availability ────────────────────────────────────
63
+ export const PROVIDER_LABELS = {
64
+ 'claude-code': 'Claude Code',
65
+ anthropic: 'Anthropic API',
66
+ openai: 'OpenAI',
67
+ 'google-ai': 'Google AI',
68
+ 'vercel-gateway': 'Vercel AI Gateway',
69
+ qwen: 'Qwen',
70
+ minimax: 'MiniMax',
71
+ };
72
+ /**
73
+ * Returns deduplicated provider availability info.
74
+ * Checks each provider once (claude-code via binary check, others via env var).
75
+ */
76
+ export function getAvailableProviders() {
77
+ const seen = new Set();
78
+ const result = [];
79
+ for (const model of MODEL_CATALOG) {
80
+ if (seen.has(model.provider))
81
+ continue;
82
+ seen.add(model.provider);
83
+ const label = PROVIDER_LABELS[model.provider] ?? model.provider;
84
+ if (model.provider === 'claude-code') {
85
+ result.push({
86
+ provider: model.provider,
87
+ label,
88
+ available: isClaudeBinaryAvailable(),
89
+ envVar: null,
90
+ });
91
+ continue;
92
+ }
93
+ const envKey = PROVIDER_ENV_KEYS[model.provider];
94
+ result.push({
95
+ provider: model.provider,
96
+ label,
97
+ available: envKey ? !!process.env[envKey] : true,
98
+ envVar: envKey ?? null,
99
+ });
100
+ }
101
+ return result;
102
+ }
62
103
  // ─── Provider Resolution ─────────────────────────────────────
63
104
  export function resolveProvider(modelId) {
64
105
  const model = MODEL_CATALOG.find(m => m.id === modelId);
@@ -6,6 +6,8 @@ export interface ProviderConfig {
6
6
  targetPort: number;
7
7
  /** When aborted, the provider should stop the LLM stream and exit early. */
8
8
  signal?: AbortSignal;
9
+ /** When true, the agent uses a creation-mode system prompt for building new apps. */
10
+ creationMode?: boolean;
9
11
  }
10
12
  export interface StreamProvider {
11
13
  streamResponse(stream: SSEStreamingApi, messages: ModelMessage[], config: ProviderConfig): Promise<ResponseMessage[]>;
@@ -67,6 +67,48 @@ Inspector Context:
67
67
 
68
68
  Language:
69
69
  - IMPORTANT: Always respond in the same language the user writes in. If the user writes in Chinese, respond in Chinese. If the user writes in English, respond in English. Match the user's language throughout the conversation.`;
70
+ const CREATION_SYSTEM_PROMPT = `You are Awel in creation mode. The user has just created a fresh Next.js project and wants to build something new.
71
+
72
+ You have access to the same tools as in normal mode:
73
+ - Read: Read file contents
74
+ - Write: Create or overwrite files (creates parent directories automatically)
75
+ - Edit: Find-and-replace edits in files
76
+ - Bash: Execute shell commands
77
+ - Glob: Find files by glob pattern
78
+ - Ls: List directory contents
79
+ - AskUser: Ask the user clarifying questions with selectable options
80
+ - Grep: Search file contents for a regex pattern
81
+ - MultiEdit: Apply multiple find-and-replace edits to a single file in one call
82
+ - WebSearch: Search the web for real-time information
83
+ - WebFetch: Fetch content from a URL
84
+ - CodeSearch: Search the web for code examples
85
+ - TodoRead: Read the current task list
86
+ - TodoWrite: Create or update the task list
87
+ - RestartDevServer: Restart the dev server
88
+
89
+ Your workflow:
90
+
91
+ 1. UNDERSTAND: If the user's request is vague or could go multiple directions, use AskUser to ask 1-3 clarifying questions about the app type, key features, and design preferences. Keep questions focused and provide concrete options.
92
+
93
+ 2. GENERATE: Create a complete, working Next.js app using:
94
+ - App Router with TypeScript
95
+ - Tailwind CSS for all styling
96
+ - Clean component structure in the app/ directory
97
+ - Modern, responsive design with good typography and spacing
98
+ - Proper error handling and loading states
99
+ - No placeholder code or TODOs — everything should work
100
+
101
+ 3. VERIFY: After generating all files, check that the dev server is running without errors. If there are build errors, fix them.
102
+
103
+ Clarifying Questions:
104
+ - When a user's request is ambiguous or has multiple valid approaches, use the AskUser tool to ask clarifying questions before proceeding.
105
+ - Present 1-4 questions, each with 2-4 concrete options. Use multiSelect when choices are not mutually exclusive.
106
+ - Keep header labels short (max 12 chars). Put the recommended option first with "(Recommended)" in the label.
107
+ - CRITICAL: All fields in the AskUser tool must be plain text. Do NOT use markdown formatting.
108
+ - After calling AskUser, STOP and wait for the user's answers before continuing.
109
+
110
+ Language:
111
+ - IMPORTANT: Always respond in the same language the user writes in. If the user writes in Chinese, respond in Chinese. If the user writes in English, respond in English.`;
70
112
  /** Detects files Claude Code uses for plan output (.claude/plans/*.md, plan.md) */
71
113
  function isPlanFile(filePath) {
72
114
  const normalized = filePath.replace(/\\/g, '/');
@@ -184,9 +226,10 @@ export function createVercelProvider(modelId, providerType) {
184
226
  try {
185
227
  // Self-contained providers handle their own system prompt and tools internally.
186
228
  // All other providers get our system prompt + tools.
229
+ const basePrompt = config.creationMode ? CREATION_SYSTEM_PROMPT : SYSTEM_PROMPT;
187
230
  const systemPrompt = isSelfContained
188
231
  ? undefined
189
- : `${SYSTEM_PROMPT}\n\nThe user's project directory is: ${config.projectCwd}`;
232
+ : `${basePrompt}\n\nThe user's project directory is: ${config.projectCwd}`;
190
233
  const streamTextArgs = {
191
234
  model,
192
235
  ...(systemPrompt && { system: systemPrompt }),
@@ -2,4 +2,4 @@
2
2
  * Creates proxy middleware that forwards requests to the target app
3
3
  * and injects the Awel host script into HTML responses.
4
4
  */
5
- export declare function createProxyMiddleware(targetPort: number, projectCwd?: string): (c: any, _next: () => Promise<void>) => Promise<any>;
5
+ export declare function createProxyMiddleware(targetPort: number, projectCwd?: string, isFresh?: () => boolean): (c: any, _next: () => Promise<void>) => Promise<any>;
package/dist/cli/proxy.js CHANGED
@@ -1,10 +1,48 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = dirname(__filename);
6
+ /**
7
+ * Reads the dashboard index.html and injects the creation mode flag.
8
+ * Returns the modified HTML or null if the file doesn't exist.
9
+ */
10
+ function getCreationModeHtml() {
11
+ const indexPath = join(__dirname, '../dashboard/index.html');
12
+ if (!existsSync(indexPath))
13
+ return null;
14
+ let html = readFileSync(indexPath, 'utf-8');
15
+ const creationScript = `<script>window.__AWEL_CREATION_MODE__=true</script>`;
16
+ if (html.includes('</head>')) {
17
+ html = html.replace('</head>', `${creationScript}</head>`);
18
+ }
19
+ else {
20
+ html = creationScript + html;
21
+ }
22
+ return html;
23
+ }
1
24
  /**
2
25
  * Creates proxy middleware that forwards requests to the target app
3
26
  * and injects the Awel host script into HTML responses.
4
27
  */
5
- export function createProxyMiddleware(targetPort, projectCwd) {
28
+ export function createProxyMiddleware(targetPort, projectCwd, isFresh) {
6
29
  return async (c, _next) => {
7
30
  const url = new URL(c.req.url);
31
+ // In creation mode, serve the dashboard at all HTML navigation requests.
32
+ // Non-HTML requests (JS, CSS, HMR) still proxy through to the dev server.
33
+ if (isFresh?.()) {
34
+ const accept = c.req.header('accept') || '';
35
+ const isNavigation = accept.includes('text/html');
36
+ if (isNavigation) {
37
+ const html = getCreationModeHtml();
38
+ if (html) {
39
+ return new Response(html, {
40
+ status: 200,
41
+ headers: { 'Content-Type': 'text/html; charset=utf-8' },
42
+ });
43
+ }
44
+ }
45
+ }
8
46
  const targetUrl = `http://localhost:${targetPort}${url.pathname}${url.search}`;
9
47
  try {
10
48
  // Clone headers and remove Accept-Encoding to get uncompressed response
@@ -18,7 +56,7 @@ export function createProxyMiddleware(targetPort, projectCwd) {
18
56
  : undefined,
19
57
  });
20
58
  const contentType = response.headers.get('content-type') || '';
21
- // If it's HTML, inject our script
59
+ // If it's HTML, inject the Awel host script
22
60
  if (contentType.includes('text/html')) {
23
61
  let html = await response.text();
24
62
  // Inject project CWD (for source-map resolution) and the host script
@@ -26,12 +64,8 @@ export function createProxyMiddleware(targetPort, projectCwd) {
26
64
  ? `<script>window.__AWEL_PROJECT_CWD__=${JSON.stringify(projectCwd)}</script>`
27
65
  : '';
28
66
  // Constrain Next.js error overlay stacking context below Awel's UI.
29
- // Covers both legacy and modern Next.js error overlay custom elements.
30
67
  const awelOverlayStyle = `<style id="awel-overlay-fix">nextjs-portal, nextjs-portal-root, next-error-overlay, nextjs-dev-tools { position: relative !important; z-index: 999997 !important; }</style>`;
31
68
  const scriptTag = `${awelOverlayStyle}${cwdScript}<script src="/_awel/host.js"></script>`;
32
- // Inject into <head> so the script loads early — even on error pages
33
- // where the body might be minimal or replaced by Next.js error recovery.
34
- // Fall back to </body> then </html> if <head> isn't found.
35
69
  if (html.includes('</head>')) {
36
70
  html = html.replace('</head>', `${scriptTag}</head>`);
37
71
  }
@@ -56,7 +90,6 @@ export function createProxyMiddleware(targetPort, projectCwd) {
56
90
  // For non-HTML responses, pass through as-is but clean up encoding headers
57
91
  const body = await response.arrayBuffer();
58
92
  const responseHeaders = new Headers(response.headers);
59
- // Remove content-encoding since we've already decoded the response
60
93
  responseHeaders.delete('content-encoding');
61
94
  responseHeaders.delete('content-length');
62
95
  return new Response(body, {