codacus commited on
Commit
6c1ff87
Β·
2 Parent(s): dc2710c 6921c15

Merge branch 'main' into git-import-from-url

Browse files
README.md CHANGED
@@ -41,6 +41,7 @@ https://thinktank.ottomator.ai
41
  - βœ… Mobile friendly (@qwikode)
42
  - βœ… Better prompt enhancing (@SujalXplores)
43
  - βœ… Attach images to prompts (@atrokhym)
 
44
  - ⬜ **HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs)
45
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
46
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
 
41
  - βœ… Mobile friendly (@qwikode)
42
  - βœ… Better prompt enhancing (@SujalXplores)
43
  - βœ… Attach images to prompts (@atrokhym)
44
+ - βœ… Detect package.json and commands to auto install and run preview for folder and git import (@wonderwhy-er)
45
  - ⬜ **HIGH PRIORITY** - Prevent Bolt from rewriting files as often (file locking and diffs)
46
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
47
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
app/commit.json CHANGED
@@ -1 +1 @@
1
- { "commit": "95e38e020cc8a4d865172187fc25c94b39806275" }
 
1
+ { "commit": "67f63aaf31f406379daa97708d6a1a9f8ac41d43" }
app/components/chat/GitCloneButton.tsx CHANGED
@@ -2,6 +2,8 @@ import ignore from 'ignore';
2
  import { useGit } from '~/lib/hooks/useGit';
3
  import type { Message } from 'ai';
4
  import WithTooltip from '~/components/ui/Tooltip';
 
 
5
 
6
  const IGNORE_PATTERNS = [
7
  'node_modules/**',
@@ -28,7 +30,6 @@ const IGNORE_PATTERNS = [
28
  ];
29
 
30
  const ig = ignore().add(IGNORE_PATTERNS);
31
- const generateId = () => Math.random().toString(36).substring(2, 15);
32
 
33
  interface GitCloneButtonProps {
34
  className?: string;
@@ -52,36 +53,47 @@ export default function GitCloneButton({ importChat }: GitCloneButtonProps) {
52
  console.log(filePaths);
53
 
54
  const textDecoder = new TextDecoder('utf-8');
55
- const message: Message = {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  role: 'assistant',
57
  content: `Cloning the repo ${repoUrl} into ${workdir}
58
- <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled" >
59
- ${filePaths
60
- .map((filePath) => {
61
- const { data: content, encoding } = data[filePath];
62
-
63
- if (encoding === 'utf8') {
64
- return `<boltAction type="file" filePath="${filePath}">
65
- ${content}
66
- </boltAction>`;
67
- } else if (content instanceof Uint8Array) {
68
- return `<boltAction type="file" filePath="${filePath}">
69
- ${textDecoder.decode(content)}
70
- </boltAction>`;
71
- } else {
72
- return '';
73
- }
74
- })
75
- .join('\n')}
76
- </boltArtifact>`,
77
  id: generateId(),
78
  createdAt: new Date(),
79
  };
80
- console.log(JSON.stringify(message));
81
 
82
- importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, [message]);
 
 
 
 
83
 
84
- // console.log(files);
85
  }
86
  }
87
  };
 
2
  import { useGit } from '~/lib/hooks/useGit';
3
  import type { Message } from 'ai';
4
  import WithTooltip from '~/components/ui/Tooltip';
5
+ import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands';
6
+ import { generateId } from '~/utils/fileUtils';
7
 
8
  const IGNORE_PATTERNS = [
9
  'node_modules/**',
 
30
  ];
31
 
32
  const ig = ignore().add(IGNORE_PATTERNS);
 
33
 
34
  interface GitCloneButtonProps {
35
  className?: string;
 
53
  console.log(filePaths);
54
 
55
  const textDecoder = new TextDecoder('utf-8');
56
+
57
+ // Convert files to common format for command detection
58
+ const fileContents = filePaths
59
+ .map((filePath) => {
60
+ const { data: content, encoding } = data[filePath];
61
+ return {
62
+ path: filePath,
63
+ content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '',
64
+ };
65
+ })
66
+ .filter((f) => f.content);
67
+
68
+ // Detect and create commands message
69
+ const commands = await detectProjectCommands(fileContents);
70
+ const commandsMessage = createCommandsMessage(commands);
71
+
72
+ // Create files message
73
+ const filesMessage: Message = {
74
  role: 'assistant',
75
  content: `Cloning the repo ${repoUrl} into ${workdir}
76
+ <boltArtifact id="imported-files" title="Git Cloned Files" type="bundled">
77
+ ${fileContents
78
+ .map(
79
+ (file) =>
80
+ `<boltAction type="file" filePath="${file.path}">
81
+ ${file.content}
82
+ </boltAction>`,
83
+ )
84
+ .join('\n')}
85
+ </boltArtifact>`,
 
 
 
 
 
 
 
 
 
86
  id: generateId(),
87
  createdAt: new Date(),
88
  };
 
89
 
90
+ const messages = [filesMessage];
91
+
92
+ if (commandsMessage) {
93
+ messages.push(commandsMessage);
94
+ }
95
 
96
+ await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages);
97
  }
98
  }
99
  };
app/components/chat/ImportFolderButton.tsx CHANGED
@@ -1,102 +1,75 @@
1
- import React from 'react';
2
  import type { Message } from 'ai';
3
  import { toast } from 'react-toastify';
4
- import ignore from 'ignore';
 
5
 
6
  interface ImportFolderButtonProps {
7
  className?: string;
8
  importChat?: (description: string, messages: Message[]) => Promise<void>;
9
  }
10
 
11
- // Common patterns to ignore, similar to .gitignore
12
- const IGNORE_PATTERNS = [
13
- 'node_modules/**',
14
- '.git/**',
15
- 'dist/**',
16
- 'build/**',
17
- '.next/**',
18
- 'coverage/**',
19
- '.cache/**',
20
- '.vscode/**',
21
- '.idea/**',
22
- '**/*.log',
23
- '**/.DS_Store',
24
- '**/npm-debug.log*',
25
- '**/yarn-debug.log*',
26
- '**/yarn-error.log*',
27
- ];
28
-
29
- const ig = ignore().add(IGNORE_PATTERNS);
30
- const generateId = () => Math.random().toString(36).substring(2, 15);
31
-
32
- const isBinaryFile = async (file: File): Promise<boolean> => {
33
- const chunkSize = 1024; // Read the first 1 KB of the file
34
- const buffer = new Uint8Array(await file.slice(0, chunkSize).arrayBuffer());
35
-
36
- for (let i = 0; i < buffer.length; i++) {
37
- const byte = buffer[i];
38
-
39
- if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {
40
- return true; // Found a binary character
41
- }
42
- }
43
 
44
- return false;
45
- };
46
 
47
- export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ className, importChat }) => {
48
- const shouldIncludeFile = (path: string): boolean => {
49
- return !ig.ignores(path);
50
- };
 
 
51
 
52
- const createChatFromFolder = async (files: File[], binaryFiles: string[]) => {
53
- const fileArtifacts = await Promise.all(
54
- files.map(async (file) => {
55
- return new Promise<string>((resolve, reject) => {
56
- const reader = new FileReader();
57
-
58
- reader.onload = () => {
59
- const content = reader.result as string;
60
- const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
61
- resolve(
62
- `<boltAction type="file" filePath="${relativePath}">
63
- ${content}
64
- </boltAction>`,
65
- );
66
- };
67
- reader.onerror = reject;
68
- reader.readAsText(file);
69
- });
70
- }),
71
- );
72
-
73
- const binaryFilesMessage =
74
- binaryFiles.length > 0
75
- ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
76
- : '';
77
-
78
- const message: Message = {
79
- role: 'assistant',
80
- content: `I'll help you set up these files.${binaryFilesMessage}
81
-
82
- <boltArtifact id="imported-files" title="Imported Files" type="bundled">
83
- ${fileArtifacts.join('\n\n')}
84
- </boltArtifact>`,
85
- id: generateId(),
86
- createdAt: new Date(),
87
- };
88
-
89
- const userMessage: Message = {
90
- role: 'user',
91
- id: generateId(),
92
- content: 'Import my files',
93
- createdAt: new Date(),
94
- };
95
-
96
- const description = `Folder Import: ${files[0].webkitRelativePath.split('/')[0]}`;
97
-
98
- if (importChat) {
99
- await importChat(description, [userMessage, message]);
100
  }
101
  };
102
 
@@ -108,46 +81,8 @@ ${fileArtifacts.join('\n\n')}
108
  className="hidden"
109
  webkitdirectory=""
110
  directory=""
111
- onChange={async (e) => {
112
- const allFiles = Array.from(e.target.files || []);
113
- const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));
114
-
115
- if (filteredFiles.length === 0) {
116
- toast.error('No files found in the selected folder');
117
- return;
118
- }
119
-
120
- try {
121
- const fileChecks = await Promise.all(
122
- filteredFiles.map(async (file) => ({
123
- file,
124
- isBinary: await isBinaryFile(file),
125
- })),
126
- );
127
-
128
- const textFiles = fileChecks.filter((f) => !f.isBinary).map((f) => f.file);
129
- const binaryFilePaths = fileChecks
130
- .filter((f) => f.isBinary)
131
- .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));
132
-
133
- if (textFiles.length === 0) {
134
- toast.error('No text files found in the selected folder');
135
- return;
136
- }
137
-
138
- if (binaryFilePaths.length > 0) {
139
- toast.info(`Skipping ${binaryFilePaths.length} binary files`);
140
- }
141
-
142
- await createChatFromFolder(textFiles, binaryFilePaths);
143
- } catch (error) {
144
- console.error('Failed to import folder:', error);
145
- toast.error('Failed to import folder');
146
- }
147
-
148
- e.target.value = ''; // Reset file input
149
- }}
150
- {...({} as any)} // if removed webkitdirectory will throw errors as unknow attribute
151
  />
152
  <button
153
  onClick={() => {
@@ -155,9 +90,10 @@ ${fileArtifacts.join('\n\n')}
155
  input?.click();
156
  }}
157
  className={className}
 
158
  >
159
  <div className="i-ph:upload-simple" />
160
- Import Folder
161
  </button>
162
  </>
163
  );
 
1
+ import React, { useState } from 'react';
2
  import type { Message } from 'ai';
3
  import { toast } from 'react-toastify';
4
+ import { MAX_FILES, isBinaryFile, shouldIncludeFile } from '~/utils/fileUtils';
5
+ import { createChatFromFolder } from '~/utils/folderImport';
6
 
7
  interface ImportFolderButtonProps {
8
  className?: string;
9
  importChat?: (description: string, messages: Message[]) => Promise<void>;
10
  }
11
 
12
+ export const ImportFolderButton: React.FC<ImportFolderButtonProps> = ({ className, importChat }) => {
13
+ const [isLoading, setIsLoading] = useState(false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
 
15
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
16
+ const allFiles = Array.from(e.target.files || []);
17
 
18
+ if (allFiles.length > MAX_FILES) {
19
+ toast.error(
20
+ `This folder contains ${allFiles.length.toLocaleString()} files. This product is not yet optimized for very large projects. Please select a folder with fewer than ${MAX_FILES.toLocaleString()} files.`,
21
+ );
22
+ return;
23
+ }
24
 
25
+ const folderName = allFiles[0]?.webkitRelativePath.split('/')[0] || 'Unknown Folder';
26
+ setIsLoading(true);
27
+
28
+ const loadingToast = toast.loading(`Importing ${folderName}...`);
29
+
30
+ try {
31
+ const filteredFiles = allFiles.filter((file) => shouldIncludeFile(file.webkitRelativePath));
32
+
33
+ if (filteredFiles.length === 0) {
34
+ toast.error('No files found in the selected folder');
35
+ return;
36
+ }
37
+
38
+ const fileChecks = await Promise.all(
39
+ filteredFiles.map(async (file) => ({
40
+ file,
41
+ isBinary: await isBinaryFile(file),
42
+ })),
43
+ );
44
+
45
+ const textFiles = fileChecks.filter((f) => !f.isBinary).map((f) => f.file);
46
+ const binaryFilePaths = fileChecks
47
+ .filter((f) => f.isBinary)
48
+ .map((f) => f.file.webkitRelativePath.split('/').slice(1).join('/'));
49
+
50
+ if (textFiles.length === 0) {
51
+ toast.error('No text files found in the selected folder');
52
+ return;
53
+ }
54
+
55
+ if (binaryFilePaths.length > 0) {
56
+ toast.info(`Skipping ${binaryFilePaths.length} binary files`);
57
+ }
58
+
59
+ const messages = await createChatFromFolder(textFiles, binaryFilePaths, folderName);
60
+
61
+ if (importChat) {
62
+ await importChat(folderName, [...messages]);
63
+ }
64
+
65
+ toast.success('Folder imported successfully');
66
+ } catch (error) {
67
+ console.error('Failed to import folder:', error);
68
+ toast.error('Failed to import folder');
69
+ } finally {
70
+ setIsLoading(false);
71
+ toast.dismiss(loadingToast);
72
+ e.target.value = ''; // Reset file input
73
  }
74
  };
75
 
 
81
  className="hidden"
82
  webkitdirectory=""
83
  directory=""
84
+ onChange={handleFileChange}
85
+ {...({} as any)}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  />
87
  <button
88
  onClick={() => {
 
90
  input?.click();
91
  }}
92
  className={className}
93
+ disabled={isLoading}
94
  >
95
  <div className="i-ph:upload-simple" />
96
+ {isLoading ? 'Importing...' : 'Import Folder'}
97
  </button>
98
  </>
99
  );
app/components/chat/SendButton.client.tsx CHANGED
@@ -23,6 +23,7 @@ export const SendButton = ({ show, isStreaming, disabled, onClick }: SendButtonP
23
  disabled={disabled}
24
  onClick={(event) => {
25
  event.preventDefault();
 
26
  if (!disabled) {
27
  onClick?.(event);
28
  }
 
23
  disabled={disabled}
24
  onClick={(event) => {
25
  event.preventDefault();
26
+
27
  if (!disabled) {
28
  onClick?.(event);
29
  }
app/utils/fileUtils.ts ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ignore from 'ignore';
2
+
3
+ // Common patterns to ignore, similar to .gitignore
4
+ export const IGNORE_PATTERNS = [
5
+ 'node_modules/**',
6
+ '.git/**',
7
+ 'dist/**',
8
+ 'build/**',
9
+ '.next/**',
10
+ 'coverage/**',
11
+ '.cache/**',
12
+ '.vscode/**',
13
+ '.idea/**',
14
+ '**/*.log',
15
+ '**/.DS_Store',
16
+ '**/npm-debug.log*',
17
+ '**/yarn-debug.log*',
18
+ '**/yarn-error.log*',
19
+ ];
20
+
21
+ export const MAX_FILES = 1000;
22
+ export const ig = ignore().add(IGNORE_PATTERNS);
23
+
24
+ export const generateId = () => Math.random().toString(36).substring(2, 15);
25
+
26
+ export const isBinaryFile = async (file: File): Promise<boolean> => {
27
+ const chunkSize = 1024;
28
+ const buffer = new Uint8Array(await file.slice(0, chunkSize).arrayBuffer());
29
+
30
+ for (let i = 0; i < buffer.length; i++) {
31
+ const byte = buffer[i];
32
+
33
+ if (byte === 0 || (byte < 32 && byte !== 9 && byte !== 10 && byte !== 13)) {
34
+ return true;
35
+ }
36
+ }
37
+
38
+ return false;
39
+ };
40
+
41
+ export const shouldIncludeFile = (path: string): boolean => {
42
+ return !ig.ignores(path);
43
+ };
44
+
45
+ const readPackageJson = async (files: File[]): Promise<{ scripts?: Record<string, string> } | null> => {
46
+ const packageJsonFile = files.find((f) => f.webkitRelativePath.endsWith('package.json'));
47
+
48
+ if (!packageJsonFile) {
49
+ return null;
50
+ }
51
+
52
+ try {
53
+ const content = await new Promise<string>((resolve, reject) => {
54
+ const reader = new FileReader();
55
+ reader.onload = () => resolve(reader.result as string);
56
+ reader.onerror = reject;
57
+ reader.readAsText(packageJsonFile);
58
+ });
59
+
60
+ return JSON.parse(content);
61
+ } catch (error) {
62
+ console.error('Error reading package.json:', error);
63
+ return null;
64
+ }
65
+ };
66
+
67
+ export const detectProjectType = async (
68
+ files: File[],
69
+ ): Promise<{ type: string; setupCommand: string; followupMessage: string }> => {
70
+ const hasFile = (name: string) => files.some((f) => f.webkitRelativePath.endsWith(name));
71
+
72
+ if (hasFile('package.json')) {
73
+ const packageJson = await readPackageJson(files);
74
+ const scripts = packageJson?.scripts || {};
75
+
76
+ // Check for preferred commands in priority order
77
+ const preferredCommands = ['dev', 'start', 'preview'];
78
+ const availableCommand = preferredCommands.find((cmd) => scripts[cmd]);
79
+
80
+ if (availableCommand) {
81
+ return {
82
+ type: 'Node.js',
83
+ setupCommand: `npm install && npm run ${availableCommand}`,
84
+ followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`,
85
+ };
86
+ }
87
+
88
+ return {
89
+ type: 'Node.js',
90
+ setupCommand: 'npm install',
91
+ followupMessage:
92
+ 'Would you like me to inspect package.json to determine the available scripts for running this project?',
93
+ };
94
+ }
95
+
96
+ if (hasFile('index.html')) {
97
+ return {
98
+ type: 'Static',
99
+ setupCommand: 'npx --yes serve',
100
+ followupMessage: '',
101
+ };
102
+ }
103
+
104
+ return { type: '', setupCommand: '', followupMessage: '' };
105
+ };
app/utils/folderImport.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from 'ai';
2
+ import { generateId } from './fileUtils';
3
+ import { detectProjectCommands, createCommandsMessage } from './projectCommands';
4
+
5
+ export const createChatFromFolder = async (
6
+ files: File[],
7
+ binaryFiles: string[],
8
+ folderName: string,
9
+ ): Promise<Message[]> => {
10
+ const fileArtifacts = await Promise.all(
11
+ files.map(async (file) => {
12
+ return new Promise<{ content: string; path: string }>((resolve, reject) => {
13
+ const reader = new FileReader();
14
+
15
+ reader.onload = () => {
16
+ const content = reader.result as string;
17
+ const relativePath = file.webkitRelativePath.split('/').slice(1).join('/');
18
+ resolve({
19
+ content,
20
+ path: relativePath,
21
+ });
22
+ };
23
+ reader.onerror = reject;
24
+ reader.readAsText(file);
25
+ });
26
+ }),
27
+ );
28
+
29
+ const commands = await detectProjectCommands(fileArtifacts);
30
+ const commandsMessage = createCommandsMessage(commands);
31
+
32
+ const binaryFilesMessage =
33
+ binaryFiles.length > 0
34
+ ? `\n\nSkipped ${binaryFiles.length} binary files:\n${binaryFiles.map((f) => `- ${f}`).join('\n')}`
35
+ : '';
36
+
37
+ const filesMessage: Message = {
38
+ role: 'assistant',
39
+ content: `I've imported the contents of the "${folderName}" folder.${binaryFilesMessage}
40
+
41
+ <boltArtifact id="imported-files" title="Imported Files">
42
+ ${fileArtifacts
43
+ .map(
44
+ (file) => `<boltAction type="file" filePath="${file.path}">
45
+ ${file.content}
46
+ </boltAction>`,
47
+ )
48
+ .join('\n\n')}
49
+ </boltArtifact>`,
50
+ id: generateId(),
51
+ createdAt: new Date(),
52
+ };
53
+
54
+ const userMessage: Message = {
55
+ role: 'user',
56
+ id: generateId(),
57
+ content: `Import the "${folderName}" folder`,
58
+ createdAt: new Date(),
59
+ };
60
+
61
+ const messages = [userMessage, filesMessage];
62
+
63
+ if (commandsMessage) {
64
+ messages.push(commandsMessage);
65
+ }
66
+
67
+ return messages;
68
+ };
app/utils/projectCommands.ts ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Message } from 'ai';
2
+ import { generateId } from './fileUtils';
3
+
4
+ export interface ProjectCommands {
5
+ type: string;
6
+ setupCommand: string;
7
+ followupMessage: string;
8
+ }
9
+
10
+ interface FileContent {
11
+ content: string;
12
+ path: string;
13
+ }
14
+
15
+ export async function detectProjectCommands(files: FileContent[]): Promise<ProjectCommands> {
16
+ const hasFile = (name: string) => files.some((f) => f.path.endsWith(name));
17
+
18
+ if (hasFile('package.json')) {
19
+ const packageJsonFile = files.find((f) => f.path.endsWith('package.json'));
20
+
21
+ if (!packageJsonFile) {
22
+ return { type: '', setupCommand: '', followupMessage: '' };
23
+ }
24
+
25
+ try {
26
+ const packageJson = JSON.parse(packageJsonFile.content);
27
+ const scripts = packageJson?.scripts || {};
28
+
29
+ // Check for preferred commands in priority order
30
+ const preferredCommands = ['dev', 'start', 'preview'];
31
+ const availableCommand = preferredCommands.find((cmd) => scripts[cmd]);
32
+
33
+ if (availableCommand) {
34
+ return {
35
+ type: 'Node.js',
36
+ setupCommand: `npm install && npm run ${availableCommand}`,
37
+ followupMessage: `Found "${availableCommand}" script in package.json. Running "npm run ${availableCommand}" after installation.`,
38
+ };
39
+ }
40
+
41
+ return {
42
+ type: 'Node.js',
43
+ setupCommand: 'npm install',
44
+ followupMessage:
45
+ 'Would you like me to inspect package.json to determine the available scripts for running this project?',
46
+ };
47
+ } catch (error) {
48
+ console.error('Error parsing package.json:', error);
49
+ return { type: '', setupCommand: '', followupMessage: '' };
50
+ }
51
+ }
52
+
53
+ if (hasFile('index.html')) {
54
+ return {
55
+ type: 'Static',
56
+ setupCommand: 'npx --yes serve',
57
+ followupMessage: '',
58
+ };
59
+ }
60
+
61
+ return { type: '', setupCommand: '', followupMessage: '' };
62
+ }
63
+
64
+ export function createCommandsMessage(commands: ProjectCommands): Message | null {
65
+ if (!commands.setupCommand) {
66
+ return null;
67
+ }
68
+
69
+ return {
70
+ role: 'assistant',
71
+ content: `
72
+ <boltArtifact id="project-setup" title="Project Setup">
73
+ <boltAction type="shell">
74
+ ${commands.setupCommand}
75
+ </boltAction>
76
+ </boltArtifact>${commands.followupMessage ? `\n\n${commands.followupMessage}` : ''}`,
77
+ id: generateId(),
78
+ createdAt: new Date(),
79
+ };
80
+ }