Skip to content

Commit b1d770a

Browse files
authored
Added option to add CLAUDE.md file to projects (#233)
1 parent 2d35c09 commit b1d770a

File tree

5 files changed

+233
-2
lines changed

5 files changed

+233
-2
lines changed
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package ca.weblite.jdeploy.claude;
2+
3+
import org.apache.commons.io.FileUtils;
4+
import org.apache.http.client.methods.CloseableHttpResponse;
5+
import org.apache.http.client.methods.HttpGet;
6+
import org.apache.http.impl.client.CloseableHttpClient;
7+
import org.apache.http.impl.client.HttpClients;
8+
import org.apache.http.util.EntityUtils;
9+
10+
import javax.inject.Singleton;
11+
import java.io.File;
12+
import java.io.IOException;
13+
import java.nio.charset.StandardCharsets;
14+
15+
/**
16+
* A service that setups up the CLAUDE.md file for a proejct.
17+
* It will download the latest version of the CLAUDE.md file from
18+
* https://github.com/shannah/jdeploy-claude/blob/main/CLAUDE.md, and append it to the project's
19+
* CLAUDE.md file if it exists, or create a new CLAUDE.md file if it does not exist.
20+
*/
21+
@Singleton
22+
public class SetupClaudeService {
23+
24+
private static final String CLAUDE_MD_URL = "https://raw.githubusercontent.com/shannah/jdeploy-claude/main/CLAUDE.md";
25+
26+
public void setup(File projectDirectory) throws IOException {
27+
if (projectDirectory == null || !projectDirectory.exists() || !projectDirectory.isDirectory()) {
28+
throw new IllegalArgumentException("Project directory must be a valid existing directory");
29+
}
30+
31+
File claudeFile = new File(projectDirectory, "CLAUDE.md");
32+
String claudeContent = downloadClaudeTemplate();
33+
34+
if (claudeFile.exists()) {
35+
String existingContent = FileUtils.readFileToString(claudeFile, StandardCharsets.UTF_8);
36+
if (!hasJDeploySection(existingContent)) {
37+
String combinedContent = existingContent + "\n\n" + claudeContent;
38+
FileUtils.writeStringToFile(claudeFile, combinedContent, StandardCharsets.UTF_8);
39+
}
40+
} else {
41+
FileUtils.writeStringToFile(claudeFile, claudeContent, StandardCharsets.UTF_8);
42+
}
43+
}
44+
45+
private boolean hasJDeploySection(String content) {
46+
String lowerContent = content.toLowerCase();
47+
return lowerContent.contains("Claude Instructions for jDeploy Setup".toLowerCase());
48+
}
49+
50+
private String downloadClaudeTemplate() throws IOException {
51+
CloseableHttpClient httpClient = HttpClients.createDefault();
52+
53+
try {
54+
HttpGet getRequest = new HttpGet(CLAUDE_MD_URL);
55+
getRequest.setHeader("Accept", "text/plain");
56+
57+
try (CloseableHttpResponse response = httpClient.execute(getRequest)) {
58+
int statusCode = response.getStatusLine().getStatusCode();
59+
if (statusCode == 200) {
60+
return EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
61+
} else {
62+
throw new IOException("Failed to download CLAUDE.md template. HTTP status: " + statusCode);
63+
}
64+
}
65+
} finally {
66+
httpClient.close();
67+
}
68+
}
69+
}

cli/src/main/java/ca/weblite/jdeploy/gui/JDeployProjectEditor.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import ca.weblite.jdeploy.publishing.PublishingContext;
2727
import ca.weblite.jdeploy.publishing.github.GitHubPublishDriver;
2828
import ca.weblite.jdeploy.services.*;
29+
import ca.weblite.jdeploy.claude.SetupClaudeService;
2930
import ca.weblite.jdeploy.downloadPage.DownloadPageSettings;
3031
import ca.weblite.jdeploy.downloadPage.DownloadPageSettingsService;
3132
import ca.weblite.jdeploy.downloadPage.swing.DownloadPageSettingsPanel;
@@ -1761,8 +1762,18 @@ protected Object doInBackground() throws Exception {
17611762
verifyHomepage.addActionListener(evt->{
17621763
handleVerifyHomepage();
17631764
});
1765+
1766+
JMenuItem setupClaude = new JMenuItem("Setup Claude AI Assistant");
1767+
setupClaude.setToolTipText(
1768+
"Setup Claude AI assistant for this project by adding jDeploy-specific instructions to CLAUDE.md"
1769+
);
1770+
setupClaude.addActionListener(evt->{
1771+
handleSetupClaude();
1772+
});
1773+
17641774
file.addSeparator();
17651775
file.add(verifyHomepage);
1776+
file.add(setupClaude);
17661777

17671778
if (context.shouldDisplayExitMenu()) {
17681779
file.addSeparator();
@@ -1820,6 +1831,34 @@ private void handleVerifyHomepage() {
18201831
VerifyWebsiteController verifyController = new VerifyWebsiteController(frame, app);
18211832
EventQueue.invokeLater(verifyController);
18221833
}
1834+
1835+
private void handleSetupClaude() {
1836+
SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>() {
1837+
@Override
1838+
protected Void doInBackground() throws Exception {
1839+
SetupClaudeService service = new SetupClaudeService();
1840+
File projectDirectory = packageJSONFile.getAbsoluteFile().getParentFile();
1841+
service.setup(projectDirectory);
1842+
return null;
1843+
}
1844+
1845+
@Override
1846+
protected void done() {
1847+
try {
1848+
get();
1849+
JOptionPane.showMessageDialog(
1850+
frame,
1851+
"Claude AI assistant has been successfully set up for this project.\nCLAUDE.md file has been created/updated with jDeploy-specific instructions.",
1852+
"Claude Setup Complete",
1853+
JOptionPane.INFORMATION_MESSAGE
1854+
);
1855+
} catch (Exception ex) {
1856+
showError("Failed to setup Claude AI assistant: " + ex.getMessage(), ex);
1857+
}
1858+
}
1859+
};
1860+
worker.execute();
1861+
}
18231862

18241863
private void generateGithubWorkflow() {
18251864
final File projectDirectory = packageJSONFile.getAbsoluteFile().getParentFile();

cli/src/main/java/ca/weblite/jdeploy/services/ProjectInitializer.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ca.weblite.jdeploy.services;
22

3+
import ca.weblite.jdeploy.claude.SetupClaudeService;
34
import com.codename1.io.JSONParser;
45
import com.codename1.processing.Result;
56
import org.apache.commons.io.FileUtils;
@@ -18,10 +19,15 @@
1819
public class ProjectInitializer {
1920

2021
private final ProjectJarFinder projectJarFinder;
22+
private final SetupClaudeService setupClaudeService;
2123

2224
@Inject
23-
public ProjectInitializer(ProjectJarFinder projectJarFinder) {
25+
public ProjectInitializer(
26+
ProjectJarFinder projectJarFinder,
27+
SetupClaudeService setupClaudeService
28+
) {
2429
this.projectJarFinder = projectJarFinder;
30+
this.setupClaudeService = setupClaudeService;
2531
}
2632
public static class Request {
2733
public final String projectDirectory;
@@ -255,6 +261,9 @@ private Response init(
255261
}
256262
}
257263
}
264+
265+
this.setupClaudeService.setup(directory);
266+
258267
}
259268

260269
return new Response(
@@ -332,6 +341,9 @@ private Response updatePackageJson(
332341

333342
final GithubWorkflowGenerator githubWorkflowGenerator = new GithubWorkflowGenerator(directory);
334343
boolean githubWorkflowExists = githubWorkflowGenerator.getGithubWorkflowFile().exists();
344+
345+
setupClaudeService.setup(directory);
346+
335347
return new Response(
336348
jsonStr,
337349
directory.getAbsolutePath(),
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
package ca.weblite.jdeploy.claude;
2+
3+
import org.apache.commons.io.FileUtils;
4+
import org.junit.jupiter.api.Test;
5+
import org.junit.jupiter.api.io.TempDir;
6+
7+
import java.io.File;
8+
import java.io.IOException;
9+
import java.nio.charset.StandardCharsets;
10+
11+
import static org.junit.jupiter.api.Assertions.*;
12+
13+
class SetupClaudeServiceTest {
14+
15+
@Test
16+
void testSetup_createNewClaudeFile(@TempDir File tempDir) throws IOException {
17+
SetupClaudeService service = new SetupClaudeService();
18+
19+
service.setup(tempDir);
20+
21+
File claudeFile = new File(tempDir, "CLAUDE.md");
22+
assertTrue(claudeFile.exists(), "CLAUDE.md file should be created");
23+
24+
String content = FileUtils.readFileToString(claudeFile, StandardCharsets.UTF_8);
25+
assertFalse(content.isEmpty(), "CLAUDE.md content should not be empty");
26+
assertTrue(content.contains("jDeploy"), "Content should contain jDeploy information");
27+
}
28+
29+
@Test
30+
void testSetup_appendToExistingClaudeFile(@TempDir File tempDir) throws IOException {
31+
SetupClaudeService service = new SetupClaudeService();
32+
33+
File claudeFile = new File(tempDir, "CLAUDE.md");
34+
String existingContent = "# My Project\n\nThis is my existing project documentation.\n";
35+
FileUtils.writeStringToFile(claudeFile, existingContent, StandardCharsets.UTF_8);
36+
37+
service.setup(tempDir);
38+
39+
String finalContent = FileUtils.readFileToString(claudeFile, StandardCharsets.UTF_8);
40+
assertTrue(finalContent.startsWith(existingContent), "Original content should be preserved");
41+
assertTrue(finalContent.contains("jDeploy"), "New content should be appended");
42+
assertTrue(finalContent.length() > existingContent.length(), "File should be longer after appending");
43+
}
44+
45+
@Test
46+
void testSetup_noDuplicateContent(@TempDir File tempDir) throws IOException {
47+
SetupClaudeService service = new SetupClaudeService();
48+
49+
service.setup(tempDir);
50+
51+
File claudeFile = new File(tempDir, "CLAUDE.md");
52+
String firstContent = FileUtils.readFileToString(claudeFile, StandardCharsets.UTF_8);
53+
54+
service.setup(tempDir);
55+
56+
String secondContent = FileUtils.readFileToString(claudeFile, StandardCharsets.UTF_8);
57+
assertEquals(firstContent, secondContent, "Content should not be duplicated on subsequent runs");
58+
}
59+
60+
@Test
61+
void testSetup_existingJDeploySection(@TempDir File tempDir) throws IOException {
62+
SetupClaudeService service = new SetupClaudeService();
63+
64+
File claudeFile = new File(tempDir, "CLAUDE.md");
65+
String existingContent = "# My Project\n\nThis is my project.\n\n# Claude Instructions for jDeploy Setup\n\nThis project is already configured for jDeploy.\n";
66+
FileUtils.writeStringToFile(claudeFile, existingContent, StandardCharsets.UTF_8);
67+
68+
service.setup(tempDir);
69+
70+
String finalContent = FileUtils.readFileToString(claudeFile, StandardCharsets.UTF_8);
71+
assertEquals(existingContent, finalContent, "Content should not be modified when jDeploy section already exists");
72+
}
73+
74+
@Test
75+
void testSetup_invalidProjectDirectory() {
76+
SetupClaudeService service = new SetupClaudeService();
77+
78+
assertThrows(IllegalArgumentException.class, () -> {
79+
service.setup(null);
80+
}, "Should throw exception for null directory");
81+
82+
assertThrows(IllegalArgumentException.class, () -> {
83+
service.setup(new File("/non/existent/directory"));
84+
}, "Should throw exception for non-existent directory");
85+
86+
File tempFile;
87+
try {
88+
tempFile = File.createTempFile("test", ".txt");
89+
tempFile.deleteOnExit();
90+
} catch (IOException e) {
91+
throw new RuntimeException(e);
92+
}
93+
94+
assertThrows(IllegalArgumentException.class, () -> {
95+
service.setup(tempFile);
96+
}, "Should throw exception for file instead of directory");
97+
}
98+
99+
@Test
100+
void testSetup_networkFailureHandling(@TempDir File tempDir) {
101+
SetupClaudeService service = new SetupClaudeService();
102+
103+
assertDoesNotThrow(() -> {
104+
service.setup(tempDir);
105+
}, "Service should handle network issues gracefully");
106+
}
107+
}

cli/src/test/java/ca/weblite/jdeploy/services/ProjectInitializerTest.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package ca.weblite.jdeploy.services;
22

3+
import ca.weblite.jdeploy.claude.SetupClaudeService;
34
import org.apache.commons.io.FileUtils;
45
import org.json.JSONObject;
56
import org.junit.jupiter.api.*;
@@ -21,11 +22,14 @@ class ProjectInitializerTest {
2122
@Mock
2223
private ProjectJarFinder mockProjectJarFinder;
2324

25+
@Mock
26+
SetupClaudeService mockSetupClaudeService;
27+
2428
private ProjectInitializer initializer;
2529

2630
@BeforeEach
2731
void setUp() {
28-
initializer = new ProjectInitializer(mockProjectJarFinder);
32+
initializer = new ProjectInitializer(mockProjectJarFinder, mockSetupClaudeService);
2933
}
3034

3135
/**

0 commit comments

Comments
 (0)