@openbuilder/cli 0.50.27 → 0.50.28

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/dist/index.js CHANGED
@@ -10,9 +10,10 @@ import 'zod/v4';
10
10
  import 'zod/v3';
11
11
  import { parse } from 'jsonc-parser';
12
12
  import { z } from 'zod';
13
- import { existsSync } from 'fs';
13
+ import { existsSync, mkdirSync as mkdirSync$1 } from 'fs';
14
14
  import { existsSync as existsSync$1, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
15
15
  import { readFile } from 'fs/promises';
16
+ import * as path from 'path';
16
17
  import { join } from 'path';
17
18
  import WebSocket$1, { WebSocketServer, WebSocket } from 'ws';
18
19
  import { drizzle } from 'drizzle-orm/node-postgres';
@@ -30,6 +31,7 @@ import { EventEmitter } from 'node:events';
30
31
  import { createServer, createConnection } from 'node:net';
31
32
  import { readFile as readFile$1, rm, writeFile, readdir } from 'node:fs/promises';
32
33
  import { simpleGit } from 'simple-git';
34
+ import * as os from 'os';
33
35
  import { tunnelManager } from './chunks/manager-CvGX9qqe.js';
34
36
  import 'chalk';
35
37
  import 'node:util';
@@ -2139,7 +2141,7 @@ var init_design$1 = __esm$1({
2139
2141
 
2140
2142
  // src/config/tags.ts
2141
2143
  function findTagDefinition$1(key) {
2142
- for (const def of TAG_DEFINITIONS$1) {
2144
+ for (const def of TAG_DEFINITIONS$2) {
2143
2145
  if (def.key === key) return def;
2144
2146
  if (def.children) {
2145
2147
  const child = def.children.find((c) => c.key === key);
@@ -2148,10 +2150,10 @@ function findTagDefinition$1(key) {
2148
2150
  }
2149
2151
  return void 0;
2150
2152
  }
2151
- var TAG_DEFINITIONS$1;
2153
+ var TAG_DEFINITIONS$2;
2152
2154
  var init_tags$1 = __esm$1({
2153
2155
  "src/config/tags.ts"() {
2154
- TAG_DEFINITIONS$1 = [
2156
+ TAG_DEFINITIONS$2 = [
2155
2157
  // Model Selection (explicit provider + model mapping)
2156
2158
  {
2157
2159
  key: "model",
@@ -3914,6 +3916,7 @@ Create as many tasks as needed for the request (3-15+ tasks based on complexity)
3914
3916
 
3915
3917
  // src/shared/runner/messages.ts
3916
3918
  var COMMAND_TYPES = [
3919
+ "analyze-project",
3917
3920
  "start-build",
3918
3921
  "cancel-build",
3919
3922
  "start-dev-server",
@@ -6382,9 +6385,62 @@ async function runMigrations(migrationsFolder = "./drizzle") {
6382
6385
  await pool.end();
6383
6386
  }
6384
6387
  }
6388
+ var ProjectMetadataSchema = z.object({
6389
+ slug: z.string().describe("URL-friendly project identifier (lowercase, hyphens)"),
6390
+ friendlyName: z.string().describe("Human-readable project name"),
6391
+ description: z.string().describe("Brief description of what the project does"),
6392
+ icon: z.enum([
6393
+ "Folder",
6394
+ "Code",
6395
+ "Layout",
6396
+ "Database",
6397
+ "Zap",
6398
+ "Globe",
6399
+ "Lock",
6400
+ "Users",
6401
+ "ShoppingCart",
6402
+ "Calendar",
6403
+ "MessageSquare",
6404
+ "FileText",
6405
+ "Image",
6406
+ "Music",
6407
+ "Video",
6408
+ "CheckCircle",
6409
+ "Star"
6410
+ ]).describe("Icon name from available Lucide icons")
6411
+ });
6412
+ var TemplateAnalysisSchema = z.object({
6413
+ templateId: z.string().describe("ID of the selected template"),
6414
+ reasoning: z.string().describe("Brief explanation of why this template was chosen"),
6415
+ confidence: z.number().min(0).max(1).describe("Confidence score from 0.0 to 1.0")
6416
+ });
6417
+ var ProjectNamingSchema = z.object({
6418
+ slug: z.string().describe("URL-friendly project identifier (lowercase, hyphens, 2-4 words)"),
6419
+ friendlyName: z.string().describe("Human-readable project name (Title Case, 2-5 words)")
6420
+ });
6421
+ var AVAILABLE_ICONS = [
6422
+ "Folder",
6423
+ "Code",
6424
+ "Layout",
6425
+ "Database",
6426
+ "Zap",
6427
+ "Globe",
6428
+ "Lock",
6429
+ "Users",
6430
+ "ShoppingCart",
6431
+ "Calendar",
6432
+ "MessageSquare",
6433
+ "FileText",
6434
+ "Image",
6435
+ "Music",
6436
+ "Video",
6437
+ "CheckCircle",
6438
+ "Star"
6439
+ ];
6385
6440
 
6386
6441
  var AgentCore = /*#__PURE__*/Object.freeze({
6387
6442
  __proto__: null,
6443
+ AVAILABLE_ICONS: AVAILABLE_ICONS,
6388
6444
  CLAUDE_SYSTEM_PROMPT: CLAUDE_SYSTEM_PROMPT,
6389
6445
  CODEX_SYSTEM_PROMPT: CODEX_SYSTEM_PROMPT,
6390
6446
  DEFAULT_AGENT_ID: DEFAULT_AGENT_ID,
@@ -6394,6 +6450,9 @@ var AgentCore = /*#__PURE__*/Object.freeze({
6394
6450
  LEGACY_MODEL_MAP: LEGACY_MODEL_MAP,
6395
6451
  MODEL_METADATA: MODEL_METADATA,
6396
6452
  NEONDB_CHAT_MESSAGES: NEONDB_CHAT_MESSAGES,
6453
+ ProjectMetadataSchema: ProjectMetadataSchema,
6454
+ ProjectNamingSchema: ProjectNamingSchema,
6455
+ TemplateAnalysisSchema: TemplateAnalysisSchema,
6397
6456
  buildLogger: buildLogger$2,
6398
6457
  buildWebSocketServer: buildWebSocketServer,
6399
6458
  db: db,
@@ -7600,7 +7659,7 @@ var init_design = __esm({
7600
7659
 
7601
7660
  // src/config/tags.ts
7602
7661
  function findTagDefinition(key) {
7603
- for (const def of TAG_DEFINITIONS) {
7662
+ for (const def of TAG_DEFINITIONS$1) {
7604
7663
  if (def.key === key) return def;
7605
7664
  if (def.children) {
7606
7665
  const child = def.children.find((c) => c.key === key);
@@ -7609,10 +7668,10 @@ function findTagDefinition(key) {
7609
7668
  }
7610
7669
  return void 0;
7611
7670
  }
7612
- var TAG_DEFINITIONS;
7671
+ var TAG_DEFINITIONS$1;
7613
7672
  var init_tags = __esm({
7614
7673
  "src/config/tags.ts"() {
7615
- TAG_DEFINITIONS = [
7674
+ TAG_DEFINITIONS$1 = [
7616
7675
  // Model Selection (explicit provider + model mapping)
7617
7676
  {
7618
7677
  key: "model",
@@ -11543,6 +11602,644 @@ async function orchestrateBuild(context) {
11543
11602
  };
11544
11603
  }
11545
11604
 
11605
+ // src/config/tags.ts
11606
+ var TAG_DEFINITIONS = [
11607
+ // Model Selection (explicit provider + model mapping)
11608
+ {
11609
+ key: "model",
11610
+ label: "Model",
11611
+ description: "AI agent and model to use for generation",
11612
+ category: "model",
11613
+ inputType: "select",
11614
+ options: [
11615
+ {
11616
+ value: "claude-sonnet-4-5",
11617
+ label: "Claude Sonnet 4.5",
11618
+ description: "Anthropic Claude - Balanced performance and speed",
11619
+ logo: "/claude.png",
11620
+ provider: "claude-code",
11621
+ model: "claude-sonnet-4-5"
11622
+ },
11623
+ {
11624
+ value: "claude-opus-4-5",
11625
+ label: "Claude Opus 4.5",
11626
+ description: "Anthropic Claude - Most capable for complex tasks",
11627
+ logo: "/claude.png",
11628
+ provider: "claude-code",
11629
+ model: "claude-opus-4-5"
11630
+ },
11631
+ {
11632
+ value: "claude-haiku-4-5",
11633
+ label: "Claude Haiku 4.5",
11634
+ description: "Anthropic Claude - Fastest, good for iterations",
11635
+ logo: "/claude.png",
11636
+ provider: "claude-code",
11637
+ model: "claude-haiku-4-5"
11638
+ },
11639
+ {
11640
+ value: "gpt-5.2-codex",
11641
+ label: "GPT-5.2 Codex",
11642
+ description: "OpenAI Codex - Advanced code generation",
11643
+ logo: "/openai.png",
11644
+ provider: "openai-codex",
11645
+ model: "openai/gpt-5.2-codex"
11646
+ }
11647
+ ]
11648
+ },
11649
+ // Framework Selection
11650
+ {
11651
+ key: "framework",
11652
+ label: "Framework",
11653
+ description: "Frontend framework to use",
11654
+ category: "framework",
11655
+ inputType: "select",
11656
+ options: [
11657
+ {
11658
+ value: "next",
11659
+ label: "Next.js",
11660
+ description: "Full-stack React with SSR, App Router, and file-based routing",
11661
+ logo: "/logos/nextjs.svg",
11662
+ repository: "github:codyde/template-nextjs",
11663
+ branch: "main"
11664
+ },
11665
+ {
11666
+ value: "tanstack",
11667
+ label: "TanStack Start",
11668
+ description: "Minimal TanStack Start foundation with React 19, Router, Query, and Tailwind",
11669
+ logo: "/logos/tanstack.png",
11670
+ repository: "github:codyde/template-tanstackstart",
11671
+ branch: "main"
11672
+ },
11673
+ {
11674
+ value: "vite",
11675
+ label: "React + Vite",
11676
+ description: "Fast React SPA with Vite - perfect for client-side apps",
11677
+ logo: "/logos/react.svg",
11678
+ repository: "github:codyde/template-reactvite",
11679
+ branch: "main"
11680
+ },
11681
+ {
11682
+ value: "astro",
11683
+ label: "Astro",
11684
+ description: "Content-focused static sites with islands architecture",
11685
+ logo: "/astro.png",
11686
+ repository: "github:codyde/template-astro",
11687
+ branch: "main"
11688
+ }
11689
+ ],
11690
+ promptTemplate: "CRITICAL: You MUST use the {label} template. Clone it using: npx degit {repository}#{branch} {{projectName}}"
11691
+ },
11692
+ // Runner Selection (options populated dynamically)
11693
+ {
11694
+ key: "runner",
11695
+ label: "Runner",
11696
+ description: "Build runner to execute the build",
11697
+ category: "runner",
11698
+ inputType: "select",
11699
+ options: []
11700
+ // Populated dynamically from connected runners
11701
+ },
11702
+ // Design Configuration (nested)
11703
+ {
11704
+ key: "design",
11705
+ label: "Design",
11706
+ description: "Visual design preferences",
11707
+ category: "design",
11708
+ inputType: "nested",
11709
+ children: [
11710
+ // Brand Themes
11711
+ {
11712
+ key: "brand",
11713
+ label: "Brand",
11714
+ description: "Pre-configured brand color themes",
11715
+ category: "design",
11716
+ inputType: "select",
11717
+ maxSelections: 1,
11718
+ options: [
11719
+ {
11720
+ value: "sentry",
11721
+ label: "Sentry",
11722
+ description: "Error monitoring and performance - vibrant purple and pink",
11723
+ logo: "/logos/sentry.svg",
11724
+ values: {
11725
+ primaryColor: "#9D58BF",
11726
+ secondaryColor: "#FF708C",
11727
+ accentColor: "#FF9838",
11728
+ neutralLight: "#F0ECF3",
11729
+ neutralDark: "#2B2233"
11730
+ }
11731
+ },
11732
+ {
11733
+ value: "stripe",
11734
+ label: "Stripe",
11735
+ description: "Modern payments aesthetic",
11736
+ logo: "/logos/stripe.svg",
11737
+ values: {
11738
+ primaryColor: "#635bff",
11739
+ secondaryColor: "#0a2540",
11740
+ accentColor: "#00d4ff",
11741
+ neutralLight: "#f6f9fc",
11742
+ neutralDark: "#0a2540"
11743
+ }
11744
+ },
11745
+ {
11746
+ value: "vercel",
11747
+ label: "Vercel",
11748
+ description: "Clean developer tools",
11749
+ logo: "/logos/vercel.svg",
11750
+ values: {
11751
+ primaryColor: "#000000",
11752
+ secondaryColor: "#ffffff",
11753
+ accentColor: "#0070f3",
11754
+ neutralLight: "#fafafa",
11755
+ neutralDark: "#000000"
11756
+ }
11757
+ },
11758
+ {
11759
+ value: "linear",
11760
+ label: "Linear",
11761
+ description: "Sleek project management",
11762
+ logo: "/logos/linear.svg",
11763
+ values: {
11764
+ primaryColor: "#5e6ad2",
11765
+ secondaryColor: "#26b5ce",
11766
+ accentColor: "#f2994a",
11767
+ neutralLight: "#f7f8f8",
11768
+ neutralDark: "#1a1a1a"
11769
+ }
11770
+ },
11771
+ {
11772
+ value: "notion",
11773
+ label: "Notion",
11774
+ description: "Warm productivity",
11775
+ logo: "/logos/notion.svg",
11776
+ values: {
11777
+ primaryColor: "#2383e2",
11778
+ secondaryColor: "#e69138",
11779
+ accentColor: "#d44c47",
11780
+ neutralLight: "#f7f6f3",
11781
+ neutralDark: "#37352f"
11782
+ }
11783
+ },
11784
+ {
11785
+ value: "github",
11786
+ label: "GitHub",
11787
+ description: "Developer-first",
11788
+ logo: "/logos/github.svg",
11789
+ values: {
11790
+ primaryColor: "#238636",
11791
+ secondaryColor: "#1f6feb",
11792
+ accentColor: "#f85149",
11793
+ neutralLight: "#f6f8fa",
11794
+ neutralDark: "#24292f"
11795
+ }
11796
+ },
11797
+ {
11798
+ value: "airbnb",
11799
+ label: "Airbnb",
11800
+ description: "Friendly travel",
11801
+ logo: "/logos/airbnb.svg",
11802
+ values: {
11803
+ primaryColor: "#ff385c",
11804
+ secondaryColor: "#00a699",
11805
+ accentColor: "#fc642d",
11806
+ neutralLight: "#f7f7f7",
11807
+ neutralDark: "#222222"
11808
+ }
11809
+ },
11810
+ {
11811
+ value: "spotify",
11812
+ label: "Spotify",
11813
+ description: "Bold music streaming",
11814
+ logo: "/logos/spotify.svg",
11815
+ values: {
11816
+ primaryColor: "#1db954",
11817
+ secondaryColor: "#191414",
11818
+ accentColor: "#1ed760",
11819
+ neutralLight: "#f6f6f6",
11820
+ neutralDark: "#000000"
11821
+ }
11822
+ }
11823
+ ],
11824
+ promptTemplate: "Use the {value} brand aesthetic with the following color palette: Primary: {primaryColor}, Secondary: {secondaryColor}, Accent: {accentColor}, Neutral Light: {neutralLight}, Neutral Dark: {neutralDark}. Match the design style and feel of {value}."
11825
+ }
11826
+ ]
11827
+ }
11828
+ ];
11829
+
11830
+ /**
11831
+ * Project Analyzer Module
11832
+ *
11833
+ * Handles AI-based project analysis before builds:
11834
+ * - Template selection (or uses framework tag fast-path)
11835
+ * - Project name/slug generation
11836
+ * - Project metadata generation (icon, description)
11837
+ *
11838
+ * This consolidates AI calls that were previously split between frontend and runner,
11839
+ * ensuring the runner is the single source of truth for all project decisions.
11840
+ */
11841
+ // Map model IDs to Claude Agent SDK model names
11842
+ const MODEL_MAP = {
11843
+ 'claude-haiku-4-5': 'claude-sonnet-4-5', // Haiku 4.5 not yet available, use Sonnet
11844
+ 'claude-sonnet-4-5': 'claude-sonnet-4-5',
11845
+ 'claude-opus-4-5': 'claude-opus-4-5',
11846
+ };
11847
+ function resolveModelName(modelId) {
11848
+ return MODEL_MAP[modelId] || 'claude-sonnet-4-5';
11849
+ }
11850
+ /**
11851
+ * Get a clean env object with only string values
11852
+ */
11853
+ function getCleanEnv() {
11854
+ const env = {};
11855
+ for (const [key, value] of Object.entries(process.env)) {
11856
+ if (value !== undefined) {
11857
+ env[key] = value;
11858
+ }
11859
+ }
11860
+ if (!env.PATH) {
11861
+ env.PATH = '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin';
11862
+ }
11863
+ return env;
11864
+ }
11865
+ /**
11866
+ * Ensure a directory exists
11867
+ */
11868
+ function ensureDir(dir) {
11869
+ if (!existsSync(dir)) {
11870
+ mkdirSync$1(dir, { recursive: true });
11871
+ }
11872
+ }
11873
+ /**
11874
+ * Generate structured output using Claude Agent SDK
11875
+ */
11876
+ async function generateStructuredOutput(options) {
11877
+ const modelName = resolveModelName(options.model);
11878
+ const jsonSchema = z.toJSONSchema(options.schema);
11879
+ const jsonInstructions = `You must respond with ONLY valid JSON that matches this schema. Do not include any text before or after the JSON object. Do not wrap in markdown code blocks.
11880
+
11881
+ JSON Schema:
11882
+ ${JSON.stringify(jsonSchema, null, 2)}
11883
+
11884
+ CRITICAL: Your response must START with { and END with }. Output only the JSON object.`;
11885
+ const fullPrompt = options.system
11886
+ ? `${options.system}\n\n${jsonInstructions}\n\nUser request: ${options.prompt}`
11887
+ : `${jsonInstructions}\n\nUser request: ${options.prompt}`;
11888
+ const tempDir = path.join(os.tmpdir(), 'runner-ai');
11889
+ ensureDir(tempDir);
11890
+ const sdkOptions = {
11891
+ model: modelName,
11892
+ maxTurns: 1,
11893
+ tools: [],
11894
+ cwd: tempDir,
11895
+ permissionMode: 'bypassPermissions',
11896
+ allowDangerouslySkipPermissions: true,
11897
+ env: getCleanEnv(),
11898
+ };
11899
+ let responseText = '';
11900
+ try {
11901
+ for await (const message of query({ prompt: fullPrompt, options: sdkOptions })) {
11902
+ if (message.type === 'assistant') {
11903
+ for (const block of message.message.content) {
11904
+ if (block.type === 'text') {
11905
+ responseText += block.text;
11906
+ }
11907
+ }
11908
+ }
11909
+ }
11910
+ }
11911
+ catch (error) {
11912
+ console.error('[project-analyzer] SDK query failed:', error);
11913
+ captureException(error);
11914
+ throw error;
11915
+ }
11916
+ if (!responseText) {
11917
+ throw new Error('No text response from Claude Agent SDK');
11918
+ }
11919
+ // Clean up any markdown code blocks if present
11920
+ let jsonText = responseText.trim();
11921
+ const codeBlockMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
11922
+ if (codeBlockMatch) {
11923
+ jsonText = codeBlockMatch[1].trim();
11924
+ }
11925
+ const jsonMatch = jsonText.match(/\{[\s\S]*\}/);
11926
+ if (jsonMatch) {
11927
+ jsonText = jsonMatch[0];
11928
+ }
11929
+ const parsed = JSON.parse(jsonText);
11930
+ const validated = options.schema.parse(parsed);
11931
+ return { object: validated };
11932
+ }
11933
+ /**
11934
+ * Generate project name/slug from user prompt
11935
+ */
11936
+ async function generateProjectName(prompt, model) {
11937
+ const namePrompt = `Extract the core project concept from this request and create appropriate names:
11938
+
11939
+ User's project request: "${prompt}"
11940
+
11941
+ IMPORTANT: Extract only the PROJECT TYPE/CONCEPT. Ignore all conversational phrases:
11942
+ - Ignore: "I want", "I need", "I would like", "please", "can you", "build me", "create me", "make me"
11943
+ - Ignore: "a", "an", "the", "using", "with", "for me"
11944
+ - Focus ONLY on WHAT is being built, not how the user asked for it
11945
+
11946
+ Generate:
11947
+ 1. A URL-friendly slug (lowercase, hyphens, 2-4 words, max 30 chars)
11948
+ 2. A human-readable friendly name (Title Case, 2-5 words)
11949
+
11950
+ Examples:
11951
+ - "I want to build a todo app" → slug: "todo-app", friendlyName: "Todo App"
11952
+ - "I want a workflow automation tool" → slug: "workflow-automation", friendlyName: "Workflow Automation"
11953
+ - "Can you create an error monitoring dashboard for me" → slug: "error-monitoring", friendlyName: "Error Monitoring Dashboard"
11954
+ - "I need a chat app with real-time messaging please" → slug: "realtime-chat", friendlyName: "Realtime Chat"
11955
+
11956
+ Requirements:
11957
+ - Slug: lowercase, hyphens only, no special characters, max 30 chars
11958
+ - Friendly name: Title Case, readable, professional, 2-5 words
11959
+ - NEVER include words like "want", "need", "please", "build", "create", "make" in the output
11960
+ - Focus on the core product/application concept`;
11961
+ try {
11962
+ console.log('[project-analyzer] Generating project name...');
11963
+ const result = await generateStructuredOutput({
11964
+ model,
11965
+ schema: ProjectNamingSchema,
11966
+ prompt: namePrompt,
11967
+ });
11968
+ const { slug, friendlyName } = result.object;
11969
+ // Validate slug format
11970
+ if (slug.length < 2 || slug.length > 100 || !/^[a-z0-9-]+$/.test(slug)) {
11971
+ throw new Error('Generated slug is invalid format');
11972
+ }
11973
+ console.log(`[project-analyzer] Generated name: ${friendlyName} (${slug})`);
11974
+ return { slug, friendlyName };
11975
+ }
11976
+ catch (error) {
11977
+ console.error('[project-analyzer] Name generation failed, using fallback:', error);
11978
+ // Fallback: extract words from prompt
11979
+ const words = prompt
11980
+ .toLowerCase()
11981
+ .replace(/[^a-z0-9\s]/g, '')
11982
+ .split(/\s+/)
11983
+ .filter(w => w.length > 2)
11984
+ .filter(w => !['the', 'and', 'for', 'with', 'build', 'create', 'make', 'want', 'need', 'please', 'can', 'you', 'help', 'using'].includes(w))
11985
+ .slice(0, 4);
11986
+ const slug = words.length > 0 ? words.join('-') : 'new-project';
11987
+ const friendlyName = words.length > 0
11988
+ ? words.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
11989
+ : 'New Project';
11990
+ return { slug, friendlyName };
11991
+ }
11992
+ }
11993
+ /**
11994
+ * Generate project metadata (icon, description)
11995
+ */
11996
+ async function generateProjectMetadata(prompt, model) {
11997
+ const metadataPrompt = `Based on this project request, generate appropriate metadata:
11998
+
11999
+ User's request: "${prompt}"
12000
+
12001
+ Available icons: Folder, Code, Layout, Database, Zap, Globe, Lock, Users, ShoppingCart, Calendar, MessageSquare, FileText, Image, Music, Video, CheckCircle, Star
12002
+
12003
+ Generate:
12004
+ - icon: Most appropriate icon from the list above
12005
+ - description: 1-2 sentences describing what the project does
12006
+
12007
+ IMPORTANT: Focus on the actual functionality being requested.`;
12008
+ try {
12009
+ console.log('[project-analyzer] Generating project metadata...');
12010
+ const result = await generateStructuredOutput({
12011
+ model,
12012
+ schema: ProjectMetadataSchema.pick({ icon: true, description: true }).extend({
12013
+ slug: z.string().optional(),
12014
+ friendlyName: z.string().optional(),
12015
+ }),
12016
+ prompt: metadataPrompt,
12017
+ });
12018
+ console.log(`[project-analyzer] Generated icon: ${result.object.icon}`);
12019
+ return {
12020
+ icon: result.object.icon,
12021
+ description: result.object.description
12022
+ };
12023
+ }
12024
+ catch (error) {
12025
+ console.error('[project-analyzer] Metadata generation failed, using defaults:', error);
12026
+ return {
12027
+ icon: 'Code',
12028
+ description: prompt.substring(0, 150),
12029
+ };
12030
+ }
12031
+ }
12032
+ /**
12033
+ * Select template based on prompt (AI analysis)
12034
+ */
12035
+ async function selectTemplateWithAI(prompt, templates, model) {
12036
+ const templateContext = templates.map(t => `
12037
+ ## ${t.name} (ID: ${t.id})
12038
+ ${t.description}
12039
+ Keywords: ${t.selection.keywords.join(', ')}
12040
+ Use cases: ${t.selection.useCases.join('; ')}
12041
+ Tech: ${t.tech.framework} ${t.tech.version}, ${t.tech.language}
12042
+ `).join('\n---\n');
12043
+ const selectionPrompt = `Select the best template for this project:
12044
+
12045
+ User's request: "${prompt}"
12046
+
12047
+ Available templates:
12048
+ ${templateContext}
12049
+
12050
+ Selection guidelines:
12051
+ - react-vite: Simple SPAs, prototypes, basic UIs
12052
+ - nextjs-fullstack: Full-stack apps needing auth, database, API routes, SSR
12053
+ - astro-static: Blogs, documentation, landing pages
12054
+
12055
+ Return the template ID, your reasoning, and confidence score.
12056
+ VALID templateId values: ${templates.map(t => t.id).join(', ')}`;
12057
+ try {
12058
+ console.log('[project-analyzer] Analyzing prompt for template selection...');
12059
+ const result = await generateStructuredOutput({
12060
+ model,
12061
+ schema: TemplateAnalysisSchema,
12062
+ prompt: selectionPrompt,
12063
+ });
12064
+ const template = templates.find(t => t.id === result.object.templateId);
12065
+ if (!template) {
12066
+ throw new Error(`Template ${result.object.templateId} not found`);
12067
+ }
12068
+ console.log(`[project-analyzer] Selected template: ${template.name} (confidence: ${result.object.confidence})`);
12069
+ console.log(`[project-analyzer] Reasoning: ${result.object.reasoning}`);
12070
+ return {
12071
+ template,
12072
+ reasoning: result.object.reasoning,
12073
+ confidence: result.object.confidence,
12074
+ };
12075
+ }
12076
+ catch (error) {
12077
+ console.error('[project-analyzer] Template selection failed, using fallback:', error);
12078
+ // Fallback to keyword-based selection
12079
+ const promptLower = prompt.toLowerCase();
12080
+ let template = templates.find(t => t.id === 'react-vite'); // Default
12081
+ if (promptLower.includes('next') || promptLower.includes('full-stack') || promptLower.includes('database') || promptLower.includes('auth')) {
12082
+ template = templates.find(t => t.id === 'nextjs-fullstack') || template;
12083
+ }
12084
+ else if (promptLower.includes('blog') || promptLower.includes('landing') || promptLower.includes('static') || promptLower.includes('docs')) {
12085
+ template = templates.find(t => t.id === 'astro-static') || template;
12086
+ }
12087
+ return {
12088
+ template: template,
12089
+ reasoning: 'Fallback keyword-based selection',
12090
+ confidence: 0.5,
12091
+ };
12092
+ }
12093
+ }
12094
+ /**
12095
+ * Get template from framework tag (fast path - no AI)
12096
+ */
12097
+ function getTemplateFromTag(templates, tags) {
12098
+ const frameworkTag = tags.find(t => t.key === 'framework');
12099
+ if (!frameworkTag)
12100
+ return null;
12101
+ // Find template matching the framework tag
12102
+ const matchingTemplate = templates.find(t => t.tech.framework.toLowerCase() === frameworkTag.value.toLowerCase());
12103
+ if (matchingTemplate) {
12104
+ console.log(`[project-analyzer] Using template from framework tag: ${matchingTemplate.name}`);
12105
+ return matchingTemplate;
12106
+ }
12107
+ // Try to get template info from TAG_DEFINITIONS
12108
+ const frameworkDef = TAG_DEFINITIONS.find(d => d.key === 'framework');
12109
+ const frameworkOption = frameworkDef?.options?.find(o => o.value === frameworkTag.value);
12110
+ if (frameworkOption?.repository) {
12111
+ console.log(`[project-analyzer] Framework tag found but no matching template, using tag metadata`);
12112
+ // Return null - caller should use tag metadata directly
12113
+ return null;
12114
+ }
12115
+ return null;
12116
+ }
12117
+ /**
12118
+ * Main analysis function - orchestrates all AI calls
12119
+ *
12120
+ * Sends events via callback as analysis progresses:
12121
+ * - analysis-started
12122
+ * - project-metadata (with results)
12123
+ * - analysis-complete
12124
+ */
12125
+ async function analyzeProject(options, sendEvent, commandId) {
12126
+ const { prompt, agent, claudeModel, tags } = options;
12127
+ const model = claudeModel || 'claude-sonnet-4-5';
12128
+ console.log('[project-analyzer] Starting project analysis...');
12129
+ console.log(`[project-analyzer] Agent: ${agent}, Model: ${model}`);
12130
+ // Emit analysis started
12131
+ sendEvent({
12132
+ type: 'analysis-started',
12133
+ commandId,
12134
+ timestamp: new Date().toISOString(),
12135
+ });
12136
+ // Load templates
12137
+ const templates = await getAllTemplates();
12138
+ console.log(`[project-analyzer] Loaded ${templates.length} templates`);
12139
+ // Step 1: Template Selection
12140
+ let selectedTemplate = null;
12141
+ // Fast path: Check if framework tag is present
12142
+ if (tags && tags.length > 0) {
12143
+ const tagTemplate = getTemplateFromTag(templates, tags);
12144
+ if (tagTemplate) {
12145
+ selectedTemplate = tagTemplate;
12146
+ console.log('[project-analyzer] FAST PATH: Using template from framework tag');
12147
+ }
12148
+ else {
12149
+ // Check if we have tag metadata without matching template
12150
+ const frameworkTag = tags.find(t => t.key === 'framework');
12151
+ if (frameworkTag) {
12152
+ const frameworkDef = TAG_DEFINITIONS.find(d => d.key === 'framework');
12153
+ const frameworkOption = frameworkDef?.options?.find(o => o.value === frameworkTag.value);
12154
+ if (frameworkOption?.repository) {
12155
+ // Build synthetic template from tag
12156
+ selectedTemplate = {
12157
+ id: `${frameworkTag.value}-default`,
12158
+ name: frameworkOption.label,
12159
+ description: `Template for ${frameworkOption.label}`,
12160
+ repository: frameworkOption.repository,
12161
+ branch: frameworkOption.branch || 'main',
12162
+ selection: { keywords: [], useCases: [], examples: [] },
12163
+ tech: {
12164
+ framework: frameworkTag.value,
12165
+ version: 'latest',
12166
+ language: 'TypeScript',
12167
+ styling: 'Tailwind CSS',
12168
+ packageManager: 'pnpm',
12169
+ nodeVersion: '20',
12170
+ },
12171
+ setup: {
12172
+ defaultPort: 3000,
12173
+ installCommand: 'pnpm install',
12174
+ devCommand: 'pnpm dev',
12175
+ buildCommand: 'pnpm build',
12176
+ },
12177
+ ai: { systemPromptAddition: '', includedFeatures: [] },
12178
+ };
12179
+ console.log('[project-analyzer] FAST PATH: Built template from tag metadata');
12180
+ }
12181
+ }
12182
+ }
12183
+ }
12184
+ // Slow path: AI template selection (if not already selected via tag)
12185
+ if (!selectedTemplate) {
12186
+ console.log('[project-analyzer] Running AI template selection...');
12187
+ const selection = await selectTemplateWithAI(prompt, templates, model);
12188
+ selectedTemplate = selection.template;
12189
+ }
12190
+ // At this point selectedTemplate is guaranteed to be assigned
12191
+ const finalTemplate = selectedTemplate;
12192
+ // Step 2: Generate project name (parallel-capable but keeping sequential for simplicity)
12193
+ const { slug, friendlyName } = await generateProjectName(prompt, model);
12194
+ // Step 3: Generate metadata (icon, description)
12195
+ const { icon, description } = await generateProjectMetadata(prompt, model);
12196
+ // Build result
12197
+ const result = {
12198
+ slug,
12199
+ friendlyName,
12200
+ description,
12201
+ icon,
12202
+ template: {
12203
+ id: finalTemplate.id,
12204
+ name: finalTemplate.name,
12205
+ framework: finalTemplate.tech.framework,
12206
+ port: finalTemplate.setup.defaultPort,
12207
+ runCommand: finalTemplate.setup.devCommand,
12208
+ repository: finalTemplate.repository,
12209
+ branch: finalTemplate.branch,
12210
+ },
12211
+ };
12212
+ // Emit project metadata
12213
+ sendEvent({
12214
+ type: 'project-metadata',
12215
+ commandId,
12216
+ timestamp: new Date().toISOString(),
12217
+ payload: {
12218
+ path: '', // Not yet created
12219
+ projectType: result.template.framework,
12220
+ runCommand: result.template.runCommand,
12221
+ port: result.template.port,
12222
+ detectedFramework: result.template.framework,
12223
+ slug: result.slug,
12224
+ friendlyName: result.friendlyName,
12225
+ description: result.description,
12226
+ icon: result.icon,
12227
+ template: result.template,
12228
+ },
12229
+ });
12230
+ // Emit analysis complete
12231
+ sendEvent({
12232
+ type: 'analysis-complete',
12233
+ commandId,
12234
+ timestamp: new Date().toISOString(),
12235
+ });
12236
+ console.log('[project-analyzer] Analysis complete!');
12237
+ console.log(`[project-analyzer] Project: ${friendlyName} (${slug})`);
12238
+ console.log(`[project-analyzer] Template: ${finalTemplate.name}`);
12239
+ console.log(`[project-analyzer] Icon: ${icon}`);
12240
+ return result;
12241
+ }
12242
+
11546
12243
  // Silent mode for TUI
11547
12244
  let isSilentMode$1 = false;
11548
12245
  function setSilentMode(silent) {
@@ -12694,8 +13391,45 @@ async function startRunner(options = {}) {
12694
13391
  };
12695
13392
  }
12696
13393
  async function handleCommand(command) {
13394
+ // Handle analyze-project specially - no projectId yet
13395
+ if (command.type === 'analyze-project') {
13396
+ debugLog(`Received command: analyze-project`);
13397
+ debugLog(`Command ID: ${command.id}`);
13398
+ debugLog(`Timestamp: ${command.timestamp}`);
13399
+ // Send ack without projectId
13400
+ sendEvent({
13401
+ type: "ack",
13402
+ commandId: command.id,
13403
+ timestamp: new Date().toISOString(),
13404
+ message: `Command analyze-project accepted`,
13405
+ });
13406
+ // Handle the analyze-project command
13407
+ try {
13408
+ log('🔍 Starting project analysis...');
13409
+ await analyzeProject({
13410
+ prompt: command.payload.prompt,
13411
+ agent: command.payload.agent,
13412
+ claudeModel: command.payload.claudeModel,
13413
+ tags: command.payload.tags,
13414
+ }, sendEvent, command.id);
13415
+ log('✅ Project analysis complete');
13416
+ }
13417
+ catch (error) {
13418
+ log('❌ Project analysis failed:', error);
13419
+ sendEvent({
13420
+ type: 'error',
13421
+ commandId: command.id,
13422
+ timestamp: new Date().toISOString(),
13423
+ error: error instanceof Error ? error.message : 'Analysis failed',
13424
+ stack: error instanceof Error ? error.stack : undefined,
13425
+ });
13426
+ }
13427
+ return;
13428
+ }
13429
+ // All other commands have projectId
13430
+ const projectId = command.projectId;
12697
13431
  // Log command receipt - only verbose details in debug mode
12698
- debugLog(`Received command: ${command.type} for project: ${command.projectId}`);
13432
+ debugLog(`Received command: ${command.type} for project: ${projectId}`);
12699
13433
  debugLog(`Command ID: ${command.id}`);
12700
13434
  debugLog(`Timestamp: ${command.timestamp}`);
12701
13435
  // Log command-specific details (verbose only)
@@ -12715,7 +13449,7 @@ async function startRunner(options = {}) {
12715
13449
  }
12716
13450
  sendEvent({
12717
13451
  type: "ack",
12718
- ...buildEventBase(command.projectId, command.id),
13452
+ ...buildEventBase(projectId, command.id),
12719
13453
  message: `Command ${command.type} accepted`,
12720
13454
  });
12721
13455
  switch (command.type) {
@@ -14208,6 +14942,10 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
14208
14942
  const command = JSON.parse(String(data));
14209
14943
  // BUG FIX: Update lastCommandReceived timestamp
14210
14944
  lastCommandReceived = Date.now();
14945
+ // Get projectId - analyze-project commands don't have one yet
14946
+ const projectIdForTelemetry = command.type === 'analyze-project'
14947
+ ? 'pending-analysis'
14948
+ : command.projectId;
14211
14949
  // Continue trace from frontend - each build now starts its trace in the frontend
14212
14950
  // This creates a span within the continued trace for the runner's work
14213
14951
  if (command._sentry?.trace) {
@@ -14223,13 +14961,13 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
14223
14961
  attributes: {
14224
14962
  'command.type': command.type,
14225
14963
  'command.id': command.id,
14226
- 'project.id': command.projectId,
14964
+ 'project.id': projectIdForTelemetry,
14227
14965
  'trace.continued': true,
14228
14966
  },
14229
14967
  }, async (span) => {
14230
14968
  try {
14231
14969
  setTag("command_type", command.type);
14232
- setTag("project_id", command.projectId);
14970
+ setTag("project_id", projectIdForTelemetry);
14233
14971
  setTag("command_id", command.id);
14234
14972
  // Capture build metrics for start-build commands
14235
14973
  if (command.type === 'start-build' && command.payload) {
@@ -14267,13 +15005,13 @@ Write a brief, professional summary (1-3 sentences) describing what was accompli
14267
15005
  attributes: {
14268
15006
  'command.type': command.type,
14269
15007
  'command.id': command.id,
14270
- 'project.id': command.projectId,
15008
+ 'project.id': projectIdForTelemetry,
14271
15009
  'trace.continued': false,
14272
15010
  },
14273
15011
  }, async (span) => {
14274
15012
  try {
14275
15013
  setTag("command_type", command.type);
14276
- setTag("project_id", command.projectId);
15014
+ setTag("project_id", projectIdForTelemetry);
14277
15015
  setTag("command_id", command.id);
14278
15016
  // Capture build metrics for start-build commands
14279
15017
  if (command.type === 'start-build' && command.payload) {