StignoreWorkspaceMojo.java
package network.ike.plugin.ws;
import network.ike.workspace.Subproject;
import network.ike.workspace.WorkspaceGraph;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* Generate Syncthing {@code .stignore} files for the workspace.
*
* <p>Syncthing should sync source files across a developer's own machines
* but must NOT sync build artifacts or git metadata (those are
* machine-specific). This goal generates two files:
*
* <ul>
* <li><b>Workspace-level {@code .stignore}</b> — ignores
* {@code target/}, {@code .git/}, IDE files, and OS metadata
* within each subproject directory.</li>
* <li><b>Per-subproject {@code .stignore}</b> — same patterns, placed
* in each cloned subproject for standalone Syncthing folders.</li>
* </ul>
*
* <p>The generated file is deterministic (sorted, no timestamps) so
* running the goal twice produces identical output — no Syncthing
* churn.
*
* <pre>{@code mvn ike:stignore}</pre>
*/
@Mojo(name = "stignore", projectRequired = false, aggregator = true)
public class StignoreWorkspaceMojo extends AbstractWorkspaceMojo {
/** Standard patterns that should never be synced. */
static final List<String> COMMON_IGNORES = List.of(
"// IKE Workspace .stignore",
"// Generated by: mvn ike:stignore",
"// Re-run after adding new components to workspace.yaml",
"",
"// ── Build artifacts ────────────────────────────────────",
"**/target",
"",
"// ── Git metadata (each machine has its own clone) ─────",
"**/.git",
"",
"// ── IDE project files ─────────────────────────────────",
"**/.idea",
"**/.settings",
"**/.project",
"**/.classpath",
"**/*.iml",
"**/.vscode",
"",
"// ── OS metadata ───────────────────────────────────────",
".DS_Store",
"**/.DS_Store",
"Thumbs.db",
"**/Thumbs.db",
"",
"// ── Maven local repo + wrapper downloads ──────────────",
"**/.mvn/local-repo",
"**/.mvn/wrapper/maven-wrapper.jar",
"",
"// ── Claude Code worktrees ─────────────────────────────",
"**/.claude/worktrees",
"",
"// ── Node / frontend build artifacts ──────────────────",
"**/node_modules"
);
/** Creates this goal instance. */
public StignoreWorkspaceMojo() {}
@Override
protected WorkspaceReportSpec runGoal() throws MojoException {
WorkspaceGraph graph = loadGraph();
File root = workspaceRoot();
getLog().info("");
getLog().info(header("Generate .stignore"));
getLog().info("══════════════════════════════════════════════════════════════");
// Build the workspace-level .stignore
List<String> lines = new ArrayList<>(workspaceIgnorePatterns());
// Write workspace .stignore
Path workspaceStignore = root.toPath().resolve(".stignore");
writeStignore(workspaceStignore, lines);
getLog().info(Ansi.green(" ✓ ") + workspaceStignore);
// Write per-subproject .stignore for components that are cloned
int perComponent = 0;
for (Subproject subproject : graph.manifest().subprojects().values()) {
File dir = new File(root, subproject.name());
if (dir.exists()) {
Path componentStignore = dir.toPath().resolve(".stignore");
writeStignore(componentStignore, COMMON_IGNORES);
perComponent++;
getLog().info(Ansi.green(" ✓ ") + subproject.name() + "/.stignore");
}
}
getLog().info("");
getLog().info(" Generated: 1 workspace + " + perComponent
+ " subproject .stignore files");
getLog().info("");
return new WorkspaceReportSpec(WsGoal.STIGNORE, "Generated **1** workspace + **"
+ perComponent + "** subproject .stignore files.\n");
}
private void writeStignore(Path path, List<String> lines)
throws MojoException {
try {
Files.writeString(path,
buildStignoreContent(lines),
StandardCharsets.UTF_8);
} catch (IOException e) {
throw new MojoException(
"Failed to write " + path, e);
}
}
// ── Content generation (pure, static, testable) ──────────────────
/**
* Return the standard list of ignore patterns common to all
* IKE workspace subprojects.
*
* @return unmodifiable list of patterns (includes comments and blanks)
*/
public static List<String> commonIgnorePatterns() {
return COMMON_IGNORES;
}
/**
* Build the workspace-level .stignore content, which includes the
* common patterns plus workspace-specific entries.
*
* @return workspace .stignore content lines
*/
public static List<String> workspaceIgnorePatterns() {
List<String> lines = new ArrayList<>(COMMON_IGNORES);
lines.add("");
lines.add("// ── Workspace checkpoint files (git-tracked) ──────────");
lines.add("checkpoints");
return List.copyOf(lines);
}
/**
* Join a list of patterns into .stignore file content.
*
* <p>Each pattern becomes a line, terminated by a trailing newline.
*
* @param patterns the lines to include
* @return file content ready to write
*/
public static String buildStignoreContent(List<String> patterns) {
return String.join("\n", patterns) + "\n";
}
}