-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathServeCommand.java
More file actions
139 lines (118 loc) · 5.43 KB
/
Copy pathServeCommand.java
File metadata and controls
139 lines (118 loc) · 5.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
package io.github.randomcodespace.iq.cli;
import io.github.randomcodespace.iq.config.CodeIqConfig;
import io.github.randomcodespace.iq.graph.GraphStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.LivenessState;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import java.nio.file.Path;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.concurrent.Callable;
/**
* Start the web UI + REST API + MCP server.
* <p>
* This command expects Neo4j to already be populated by {@code enrich}.
* It simply starts the Spring Boot web server and blocks until shutdown.
* <p>
* Pipeline: {@code index} → {@code enrich} → {@code serve}
*/
@Component
@Command(name = "serve", mixinStandardHelpOptions = true,
description = "Start web UI + REST API + MCP server")
public class ServeCommand implements Callable<Integer> {
private static final Logger log = LoggerFactory.getLogger(ServeCommand.class);
@Parameters(index = "0", defaultValue = ".", description = "Path to analyzed codebase")
private Path path;
@Option(names = {"--port", "-p"}, defaultValue = "8080", description = "Server port")
private int port;
@Option(names = {"--host"}, defaultValue = "0.0.0.0", description = "Bind address")
private String host;
@Option(names = {"--graph"}, description = "Path to shared graph directory (overrides default)")
private Path graphPath;
@Option(names = {"--no-ui"}, defaultValue = "false",
description = "Disable the web UI (React SPA). API and MCP endpoints remain active.")
private boolean noUi;
@Option(names = {"--read-only"}, defaultValue = "false",
description = "Read-only mode: no lock files, no writes. For read-only filesystems (AKS/K8s).")
private boolean readOnly;
@Autowired
private CodeIqConfig config;
@Autowired(required = false)
private GraphStore graphStore;
@Autowired
private ApplicationEventPublisher events;
@Override
public Integer call() {
Path root = path.toAbsolutePath().normalize();
config.setRootPath(root.toString());
if (readOnly) {
config.setReadOnly(true);
}
NumberFormat nf = NumberFormat.getIntegerInstance(Locale.US);
// Report Neo4j graph status
if (graphStore != null) {
try {
long nodeCount = graphStore.count();
long edgeCount = graphStore.countEdges();
if (nodeCount > 0) {
CliOutput.success("Neo4j graph: " + nf.format(nodeCount) + " nodes, "
+ nf.format(edgeCount) + " edges");
} else {
CliOutput.warn("Neo4j graph is empty. Run 'code-iq enrich " + root + "' to populate.");
}
} catch (Exception e) {
log.debug("Could not check Neo4j state", e);
CliOutput.warn("Could not read Neo4j graph. Run 'code-iq enrich' first.");
}
}
CliOutput.step("[OK]", "@|bold,green Server started|@");
System.out.println();
if (noUi) {
CliOutput.info(" Web UI: disabled (API and MCP active at :" + port + ")");
} else {
CliOutput.info(" Web UI: http://" + host + ":" + port + " (React SPA)");
}
CliOutput.info(" REST API: http://" + host + ":" + port + "/api");
CliOutput.info(" MCP: http://" + host + ":" + port + "/mcp");
CliOutput.info(" Health: http://" + host + ":" + port + "/actuator/health");
CliOutput.info(" Codebase: " + root);
System.out.println();
CliOutput.info("Press Ctrl+C to stop.");
// Publish availability transitions so /actuator/health reports UP (200).
// This Callable is invoked from a CommandLineRunner that blocks forever
// (the Thread.join below), so Spring's ApplicationReadyEvent — which
// normally drives ReadinessState to ACCEPTING_TRAFFIC — never fires.
// Without this, /actuator/health stays OUT_OF_SERVICE (503) even though
// the server is accepting and serving traffic. See also: known gap on
// GraphBootstrapper's @EventListener(ApplicationReadyEvent.class) which
// is dead for the same reason — out of scope for this fix.
markReady();
try {
Thread.currentThread().join();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return 0;
}
/**
* Flip availability state to live + accepting traffic. Extracted for
* testability — callers can verify the right events are published.
*/
void markReady() {
AvailabilityChangeEvent.publish(events, this, LivenessState.CORRECT);
AvailabilityChangeEvent.publish(events, this, ReadinessState.ACCEPTING_TRAFFIC);
}
public Path getPath() { return path; }
public int getPort() { return port; }
public String getHost() { return host; }
public Path getGraphPath() { return graphPath; }
public boolean isNoUi() { return noUi; }
}