@wwjd/pi-graphify 0.1.0 → 0.3.0

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/src/version.ts ADDED
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Graphify CLI version detection and capability matrix.
3
+ */
4
+
5
+ import { execFile } from "node:child_process";
6
+ import type { GraphifyCapabilities } from "./backends/types.js";
7
+
8
+ /** Minimum Graphify CLI version considered supported. */
9
+ export const MIN_SUPPORTED_GRAPHIFY = "2.100.0";
10
+
11
+ export interface GraphifyVersionInfo {
12
+ /** Detected version, or null if unknown. */
13
+ version: string | null;
14
+ /** Whether the detected version is at or above the minimum. */
15
+ isSupported: boolean;
16
+ /** Human-readable warnings about version/capability issues. */
17
+ warnings: string[];
18
+ }
19
+
20
+ /** Run a command to detect the Graphify version. */
21
+ export async function detectVersion(cli: string, cwd: string): Promise<GraphifyVersionInfo> {
22
+ const warnings: string[] = [];
23
+
24
+ let version: string | null = null;
25
+ for (const args of [["--version"], ["version"]]) {
26
+ const result = await runProbe(cli, args, cwd, 10_000);
27
+ if (result.exitCode === 0) {
28
+ version = parseVersion(result.stdout);
29
+ if (version) break;
30
+ }
31
+ }
32
+
33
+ if (!version) {
34
+ warnings.push("Could not detect Graphify version; entering best-effort mode.");
35
+ return { version: null, isSupported: false, warnings };
36
+ }
37
+
38
+ const isSupported = isAtLeast(version, MIN_SUPPORTED_GRAPHIFY);
39
+ if (!isSupported) {
40
+ warnings.push(
41
+ `Detected Graphify ${version} is below the supported minimum (${MIN_SUPPORTED_GRAPHIFY}). Some features may be disabled.`,
42
+ );
43
+ }
44
+
45
+ return { version, isSupported, warnings };
46
+ }
47
+
48
+ /** Best-effort parse a version string from CLI output. */
49
+ export function parseVersion(output: string): string | null {
50
+ // Match semver-like strings such as 2.100.0, 2.100.0-alpha, etc.
51
+ const match = output.match(/(\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?)/);
52
+ return match?.[1] ?? null;
53
+ }
54
+
55
+ /** Compare two semver strings. Returns true if `version` >= `minimum`. */
56
+ export function isAtLeast(version: string, minimum: string): boolean {
57
+ const v = version.split(".").map(Number);
58
+ const m = minimum.split(".").map(Number);
59
+ for (let i = 0; i < Math.max(v.length, m.length); i++) {
60
+ const a = v[i] ?? 0;
61
+ const b = m[i] ?? 0;
62
+ if (a > b) return true;
63
+ if (a < b) return false;
64
+ }
65
+ return true;
66
+ }
67
+
68
+ /** Build a capability matrix from version and runtime help probes. */
69
+ export async function buildCapabilities(
70
+ _cli: string,
71
+ _cwd: string,
72
+ versionInfo: GraphifyVersionInfo,
73
+ runHelp: (args: string[]) => Promise<{ stdout: string; exitCode: number }>,
74
+ ): Promise<GraphifyCapabilities> {
75
+ const caps: GraphifyCapabilities = {
76
+ build: false,
77
+ query: false,
78
+ path: false,
79
+ explain: false,
80
+ affected: false,
81
+ add: false,
82
+ watch: false,
83
+ reflect: false,
84
+ mcp: false,
85
+ status: false,
86
+ version: false,
87
+ directedGraph: false,
88
+ incrementalUpdate: false,
89
+ semanticExtraction: false,
90
+ };
91
+
92
+ // Without a version, we can only safely probe. With a supported version, we
93
+ // still probe because runtime presence is more reliable than version numbers.
94
+ const canProbe = versionInfo.version !== null;
95
+ if (!canProbe) {
96
+ // Best-effort mode: leave everything disabled and let the coordinator warn.
97
+ return caps;
98
+ }
99
+
100
+ // Probe each subcommand. A zero exit on `<cmd> --help` means the command exists.
101
+ const commands = [
102
+ "build",
103
+ "query",
104
+ "path",
105
+ "explain",
106
+ "affected",
107
+ "add",
108
+ "watch",
109
+ "reflect",
110
+ ] as const;
111
+
112
+ for (const cmd of commands) {
113
+ const result = await runHelp([cmd, "--help"]);
114
+ if (result.exitCode === 0) {
115
+ // Map command existence to capability. Some capabilities share a command.
116
+ if (cmd === "build") {
117
+ caps.build = true;
118
+ caps.status = true;
119
+ caps.incrementalUpdate = helpMentions(result.stdout, "--update");
120
+ caps.directedGraph = helpMentions(result.stdout, "--directed");
121
+ } else {
122
+ caps[cmd] = true;
123
+ }
124
+ }
125
+ }
126
+
127
+ // Version is always reported if we can run the binary.
128
+ caps.version = true;
129
+
130
+ // MCP support requires both the CLI and the ability to spawn --mcp.
131
+ const mcpHelp = await runHelp(["--mcp", "--help"]).catch(() => ({ stdout: "", exitCode: 1 }));
132
+ caps.mcp = mcpHelp.exitCode === 0 || helpMentions(mcpHelp.stdout, "--mcp");
133
+
134
+ // Semantic extraction is inferred from help on build or extract commands.
135
+ caps.semanticExtraction =
136
+ caps.build &&
137
+ helpMentions(
138
+ await runHelp(["build", "--help"])
139
+ .then((r) => r.stdout)
140
+ .catch(() => ""),
141
+ ["--semantic", "--extract"],
142
+ );
143
+
144
+ return caps;
145
+ }
146
+
147
+ function helpMentions(help: string, flags: string | string[]): boolean {
148
+ const terms = Array.isArray(flags) ? flags : [flags];
149
+ return terms.some((flag) => help.includes(flag));
150
+ }
151
+
152
+ /** Run a quick probe command. */
153
+ function runProbe(
154
+ cli: string,
155
+ args: string[],
156
+ cwd: string,
157
+ timeout: number,
158
+ ): Promise<{ stdout: string; exitCode: number; stderr: string }> {
159
+ return new Promise((resolve) => {
160
+ execFile(cli, args, { cwd, timeout }, (error, stdout, stderr) => {
161
+ if (error) {
162
+ resolve({
163
+ stdout: stdout.trim(),
164
+ stderr: stderr.trim(),
165
+ exitCode: typeof error.code === "number" ? error.code : 1,
166
+ });
167
+ return;
168
+ }
169
+ resolve({ stdout: stdout.trim(), stderr: stderr.trim(), exitCode: 0 });
170
+ });
171
+ });
172
+ }