@wwjd/pi-graphify 0.3.0 → 0.4.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.
@@ -0,0 +1,266 @@
1
+ /**
2
+ * Tests for the unified /graphify menu.
3
+ */
4
+
5
+ import assert from "node:assert";
6
+ import { describe, it } from "node:test";
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import { allCapabilitiesEnabled, createMockBackend } from "../backends/mock.js";
9
+ import { buildMenuItems, registerGraphifyMenuCommand } from "./menu.js";
10
+ import { queryCommand } from "./query.js";
11
+ import { graphifyCommands } from "./registry.js";
12
+ import { createTestCoordinator, getCoordinator } from "./test-helpers.js";
13
+ import type { CommandContext } from "./types.js";
14
+ import { versionCommand } from "./version.js";
15
+
16
+ interface Notification {
17
+ message: string;
18
+ type?: "info" | "warning" | "error";
19
+ }
20
+
21
+ interface MockUi {
22
+ notifications: Notification[];
23
+ selectCalls: Array<{ title: string; options: string[] }>;
24
+ inputCalls: Array<{ title: string; placeholder?: string }>;
25
+ customCalls: unknown[];
26
+ customResult: string | null;
27
+ selectResult: string | undefined;
28
+ inputResult: string | undefined;
29
+ }
30
+
31
+ interface MockContextResult {
32
+ ctx: CommandContext;
33
+ ui: MockUi;
34
+ }
35
+
36
+ function createMockContext(overrides: Partial<CommandContext> = {}): MockContextResult {
37
+ const ui: MockUi = {
38
+ notifications: [],
39
+ selectCalls: [],
40
+ inputCalls: [],
41
+ customCalls: [],
42
+ customResult: null,
43
+ selectResult: undefined,
44
+ inputResult: undefined,
45
+ };
46
+
47
+ const ctx: CommandContext = {
48
+ cwd: process.cwd(),
49
+ hasUI: true,
50
+ mode: "tui",
51
+ isProjectTrusted: () => true,
52
+ ui: {
53
+ notify: (message, type) => ui.notifications.push({ message, type }),
54
+ input: async (title, placeholder) => {
55
+ ui.inputCalls.push({ title, placeholder });
56
+ return ui.inputResult;
57
+ },
58
+ select: async (title, options) => {
59
+ ui.selectCalls.push({ title, options });
60
+ return ui.selectResult;
61
+ },
62
+ custom: async (factory) => {
63
+ ui.customCalls.push(factory);
64
+ return ui.customResult as never;
65
+ },
66
+ },
67
+ ...overrides,
68
+ };
69
+
70
+ return { ctx, ui };
71
+ }
72
+
73
+ interface CommandHandler {
74
+ description?: string;
75
+ handler: (args: string, ctx: unknown) => Promise<void>;
76
+ }
77
+
78
+ interface MockPi {
79
+ commands: Record<string, CommandHandler>;
80
+ registerCommand: (name: string, options: CommandHandler) => void;
81
+ }
82
+
83
+ function createMockPi(): MockPi {
84
+ const commands: MockPi["commands"] = {};
85
+ return {
86
+ commands,
87
+ registerCommand: (name, options) => {
88
+ commands[name] = options;
89
+ },
90
+ };
91
+ }
92
+
93
+ function invokeHandler(pi: MockPi, ctx: CommandContext): Promise<void> {
94
+ return pi.commands.graphify.handler(
95
+ "",
96
+ ctx as unknown as Parameters<CommandHandler["handler"]>[1],
97
+ );
98
+ }
99
+
100
+ describe("registerGraphifyMenuCommand", () => {
101
+ it("registers /graphify", () => {
102
+ const pi = createMockPi();
103
+ registerGraphifyMenuCommand(pi as unknown as ExtensionAPI, () => null);
104
+
105
+ assert.ok("graphify" in pi.commands);
106
+ assert.equal(pi.commands.graphify.description, "Open the Graphify command menu");
107
+ });
108
+ });
109
+
110
+ describe("buildMenuItems", () => {
111
+ it("maps commands to SelectItem values", () => {
112
+ const items = buildMenuItems(graphifyCommands);
113
+
114
+ assert.equal(items.length, graphifyCommands.length);
115
+ for (const command of graphifyCommands) {
116
+ assert.ok(items.some((item) => item.value === command.name && item.label === command.label));
117
+ }
118
+ });
119
+ });
120
+
121
+ describe("showGraphifyMenu TUI mode", () => {
122
+ it("calls ctx.ui.custom when commands are available", async () => {
123
+ const { ctx, ui } = createMockContext();
124
+ ui.customResult = "graphify-status";
125
+ const coordinator = createTestCoordinator();
126
+ const pi = createMockPi();
127
+ registerGraphifyMenuCommand(pi as unknown as ExtensionAPI, getCoordinator(coordinator));
128
+ await invokeHandler(pi, ctx);
129
+
130
+ assert.equal(ui.customCalls.length, 1);
131
+ assert.equal(ui.notifications.length, 1);
132
+ assert.ok(ui.notifications[0].message.includes("Graphify graph ready"));
133
+ });
134
+
135
+ it("renders the menu with the correct items", () => {
136
+ const { ctx, ui } = createMockContext();
137
+ const coordinator = createTestCoordinator();
138
+ const pi = createMockPi();
139
+ registerGraphifyMenuCommand(pi as unknown as ExtensionAPI, getCoordinator(coordinator));
140
+ invokeHandler(pi, ctx);
141
+
142
+ const factory = ui.customCalls[0] as (
143
+ tui: unknown,
144
+ theme: unknown,
145
+ kb: unknown,
146
+ done: (value: string | null) => void,
147
+ ) => { render: (width: number) => string[] };
148
+ const mockTheme = {
149
+ fg: (_color: string, text: string) => text,
150
+ bold: (text: string) => text,
151
+ };
152
+ const component = factory({ requestRender: () => {} }, mockTheme, {}, () => {});
153
+ const lines = component.render(80);
154
+
155
+ for (const command of graphifyCommands) {
156
+ assert.ok(
157
+ lines.some((line) => line.includes(command.label)),
158
+ `expected menu to contain ${command.label}`,
159
+ );
160
+ }
161
+ });
162
+
163
+ it("warns when no commands are available", async () => {
164
+ const { ctx, ui } = createMockContext();
165
+ const backend = createMockBackend({
166
+ capabilities: {
167
+ ...allCapabilitiesEnabled(),
168
+ build: false,
169
+ query: false,
170
+ path: false,
171
+ explain: false,
172
+ affected: false,
173
+ status: false,
174
+ version: false,
175
+ },
176
+ });
177
+ const coordinator = createTestCoordinator(backend);
178
+ const pi = createMockPi();
179
+ registerGraphifyMenuCommand(pi as unknown as ExtensionAPI, getCoordinator(coordinator));
180
+ await invokeHandler(pi, ctx);
181
+
182
+ assert.equal(ui.customCalls.length, 0);
183
+ assert.equal(ui.notifications[0].type, "warning");
184
+ assert.ok(ui.notifications[0].message.includes("No Graphify commands are available"));
185
+ });
186
+
187
+ it("cancels without executing when custom returns null", async () => {
188
+ const { ctx, ui } = createMockContext();
189
+ ui.customResult = null;
190
+ const coordinator = createTestCoordinator();
191
+ const pi = createMockPi();
192
+ registerGraphifyMenuCommand(pi as unknown as ExtensionAPI, getCoordinator(coordinator));
193
+ await invokeHandler(pi, ctx);
194
+
195
+ assert.equal(ui.customCalls.length, 1);
196
+ assert.equal(ui.notifications.length, 0);
197
+ });
198
+
199
+ it("prompts for input and executes arg commands", async () => {
200
+ const { ctx, ui } = createMockContext();
201
+ ui.customResult = "graphify-query";
202
+ ui.inputResult = "how does auth work?";
203
+ const coordinator = createTestCoordinator();
204
+ const pi = createMockPi();
205
+ registerGraphifyMenuCommand(pi as unknown as ExtensionAPI, getCoordinator(coordinator));
206
+ await invokeHandler(pi, ctx);
207
+
208
+ assert.equal(ui.inputCalls.length, 1);
209
+ assert.equal(ui.inputCalls[0].title, queryCommand.prompt);
210
+ assert.equal(ui.notifications[0].message, "ok: query");
211
+ });
212
+
213
+ it("cancels when input is cancelled", async () => {
214
+ const { ctx, ui } = createMockContext();
215
+ ui.customResult = "graphify-query";
216
+ ui.inputResult = undefined;
217
+ const coordinator = createTestCoordinator();
218
+ const pi = createMockPi();
219
+ registerGraphifyMenuCommand(pi as unknown as ExtensionAPI, getCoordinator(coordinator));
220
+ await invokeHandler(pi, ctx);
221
+
222
+ assert.equal(ui.inputCalls.length, 1);
223
+ assert.equal(ui.notifications.length, 0);
224
+ });
225
+ });
226
+
227
+ describe("showGraphifyMenu RPC mode", () => {
228
+ it("uses ctx.ui.select when in rpc mode", async () => {
229
+ const { ctx, ui } = createMockContext({ mode: "rpc" });
230
+ ui.selectResult = `${versionCommand.label}: ${versionCommand.description}`;
231
+ const coordinator = createTestCoordinator();
232
+ const pi = createMockPi();
233
+ registerGraphifyMenuCommand(pi as unknown as ExtensionAPI, getCoordinator(coordinator));
234
+ await invokeHandler(pi, ctx);
235
+
236
+ assert.equal(ui.selectCalls.length, 1);
237
+ assert.equal(ui.selectCalls[0].title, "Graphify");
238
+ assert.equal(ui.notifications[0].message, "2.100.0");
239
+ });
240
+
241
+ it("does not execute when select is cancelled", async () => {
242
+ const { ctx, ui } = createMockContext({ mode: "rpc" });
243
+ ui.selectResult = undefined;
244
+ const coordinator = createTestCoordinator();
245
+ const pi = createMockPi();
246
+ registerGraphifyMenuCommand(pi as unknown as ExtensionAPI, getCoordinator(coordinator));
247
+ await invokeHandler(pi, ctx);
248
+
249
+ assert.equal(ui.selectCalls.length, 1);
250
+ assert.equal(ui.notifications.length, 0);
251
+ });
252
+ });
253
+
254
+ describe("showGraphifyMenu non-UI mode", () => {
255
+ it("returns silently when hasUI is false", async () => {
256
+ const { ctx, ui } = createMockContext({ hasUI: false });
257
+ const coordinator = createTestCoordinator();
258
+ const pi = createMockPi();
259
+ registerGraphifyMenuCommand(pi as unknown as ExtensionAPI, getCoordinator(coordinator));
260
+ await invokeHandler(pi, ctx);
261
+
262
+ assert.equal(ui.customCalls.length, 0);
263
+ assert.equal(ui.selectCalls.length, 0);
264
+ assert.equal(ui.notifications.length, 0);
265
+ });
266
+ });
@@ -0,0 +1,150 @@
1
+ /**
2
+ * /graphify — unified command menu.
3
+ */
4
+
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import { DynamicBorder } from "@earendil-works/pi-coding-agent";
7
+ import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
8
+ import type { CoordinatorProvider } from "../coordinator.js";
9
+ import { getAvailableCommands, graphifyCommands } from "./registry.js";
10
+ import type { CommandContext, CommandDefinition } from "./types.js";
11
+ import { toCommandContext } from "./types.js";
12
+
13
+ const MAX_MENU_VISIBLE_ITEMS = 10;
14
+
15
+ export function registerGraphifyMenuCommand(
16
+ pi: ExtensionAPI,
17
+ getCoordinator: CoordinatorProvider,
18
+ ): void {
19
+ pi.registerCommand("graphify", {
20
+ description: "Open the Graphify command menu",
21
+ handler: async (_args, ctx) => {
22
+ const commandCtx = toCommandContext(ctx);
23
+ await showGraphifyMenu(commandCtx, getCoordinator);
24
+ },
25
+ });
26
+ }
27
+
28
+ async function showGraphifyMenu(
29
+ ctx: CommandContext,
30
+ getCoordinator: CoordinatorProvider,
31
+ ): Promise<void> {
32
+ if (!ctx.hasUI) return;
33
+
34
+ const coordinator = getCoordinator();
35
+ if (!coordinator) {
36
+ ctx.ui.notify("Graphify is not initialized.", "warning");
37
+ return;
38
+ }
39
+
40
+ const availableCommands = getAvailableCommands(coordinator, graphifyCommands);
41
+ if (availableCommands.length === 0) {
42
+ ctx.ui.notify("No Graphify commands are available for this installation.", "warning");
43
+ return;
44
+ }
45
+
46
+ const selectedName = await selectCommandName(ctx, availableCommands);
47
+ await runSelectedCommand(ctx, getCoordinator, availableCommands, selectedName);
48
+ }
49
+
50
+ async function selectCommandName(
51
+ ctx: CommandContext,
52
+ availableCommands: CommandDefinition[],
53
+ ): Promise<string | null | undefined> {
54
+ if (ctx.mode === "tui") return showTuiMenu(ctx, availableCommands);
55
+ if (ctx.mode === "rpc") return showRpcMenu(ctx, availableCommands);
56
+ return undefined;
57
+ }
58
+
59
+ async function runSelectedCommand(
60
+ ctx: CommandContext,
61
+ getCoordinator: CoordinatorProvider,
62
+ availableCommands: CommandDefinition[],
63
+ selectedName: string | null | undefined,
64
+ ): Promise<void> {
65
+ if (!selectedName) return;
66
+ await executeMenuChoice(ctx, getCoordinator, availableCommands, selectedName);
67
+ }
68
+
69
+ async function showTuiMenu(
70
+ ctx: CommandContext,
71
+ availableCommands: CommandDefinition[],
72
+ ): Promise<string | null> {
73
+ const items = buildMenuItems(availableCommands);
74
+
75
+ return ctx.ui.custom<string | null>(
76
+ (tui, theme, _kb, done) => {
77
+ const container = new Container();
78
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
79
+ container.addChild(new Text(theme.fg("accent", theme.bold(" Graphify "))));
80
+
81
+ const selectList = new SelectList(items, Math.min(items.length, MAX_MENU_VISIBLE_ITEMS), {
82
+ selectedPrefix: (text) => theme.fg("accent", text),
83
+ selectedText: (text) => theme.fg("accent", text),
84
+ description: (text) => theme.fg("muted", text),
85
+ scrollInfo: (text) => theme.fg("dim", text),
86
+ noMatch: (text) => theme.fg("warning", text),
87
+ });
88
+
89
+ selectList.onSelect = (item) => done(item.value);
90
+ selectList.onCancel = () => done(null);
91
+ container.addChild(selectList);
92
+ container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel")));
93
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
94
+
95
+ return {
96
+ render: (width) => container.render(width),
97
+ invalidate: () => container.invalidate(),
98
+ handleInput: (data) => {
99
+ selectList.handleInput(data);
100
+ tui.requestRender();
101
+ },
102
+ };
103
+ },
104
+ { overlay: true },
105
+ );
106
+ }
107
+
108
+ async function showRpcMenu(
109
+ ctx: CommandContext,
110
+ availableCommands: CommandDefinition[],
111
+ ): Promise<string | undefined> {
112
+ const choices = availableCommands.map((command) => ({
113
+ label: `${command.label}: ${command.description}`,
114
+ value: command.name,
115
+ }));
116
+ const labels = choices.map((c) => c.label);
117
+ const selectedLabel = await ctx.ui.select("Graphify", labels);
118
+ return choices.find((c) => c.label === selectedLabel)?.value;
119
+ }
120
+
121
+ export function buildMenuItems(availableCommands: CommandDefinition[]): SelectItem[] {
122
+ return availableCommands.map((command) => ({
123
+ value: command.name,
124
+ label: command.label,
125
+ description: command.description,
126
+ }));
127
+ }
128
+
129
+ async function executeMenuChoice(
130
+ ctx: CommandContext,
131
+ getCoordinator: CoordinatorProvider,
132
+ availableCommands: CommandDefinition[],
133
+ selectedName: string,
134
+ ): Promise<void> {
135
+ const selectedCommand = availableCommands.find((c) => c.name === selectedName);
136
+ if (!selectedCommand) return;
137
+
138
+ if (selectedCommand.argMode === "none") {
139
+ await selectedCommand.execute(ctx, getCoordinator, "");
140
+ return;
141
+ }
142
+
143
+ const input = await ctx.ui.input(
144
+ selectedCommand.prompt ?? selectedCommand.label,
145
+ selectedCommand.placeholder,
146
+ );
147
+ if (input === undefined) return;
148
+
149
+ await selectedCommand.execute(ctx, getCoordinator, input);
150
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Shared argument parsing utilities for Graphify slash commands.
3
+ */
4
+
5
+ const WHITESPACE = /\s+/;
6
+
7
+ /** Split an argument string on whitespace while respecting single and double quotes. */
8
+ export function splitArgs(args: string): string[] {
9
+ const tokens: string[] = [];
10
+ let current = "";
11
+ let quote: "'" | '"' | null = null;
12
+
13
+ function flushToken() {
14
+ if (current === "") return;
15
+ tokens.push(current);
16
+ current = "";
17
+ }
18
+
19
+ for (const char of args) {
20
+ if (quote && char === quote) {
21
+ quote = null;
22
+ continue;
23
+ }
24
+ if (quote) {
25
+ current += char;
26
+ continue;
27
+ }
28
+ if (char === "'" || char === '"') {
29
+ quote = char;
30
+ continue;
31
+ }
32
+ if (WHITESPACE.test(char)) {
33
+ flushToken();
34
+ continue;
35
+ }
36
+ current += char;
37
+ }
38
+
39
+ flushToken();
40
+ if (quote !== null) {
41
+ tokens.push(current);
42
+ }
43
+
44
+ return tokens;
45
+ }
46
+
47
+ export interface ParseBooleanFlagResult {
48
+ value: boolean | undefined;
49
+ rest: string[];
50
+ }
51
+
52
+ /**
53
+ * Detect `--flag` or `--flag=true|false` in a token list.
54
+ * Returns the flag value and the remaining tokens with the flag removed.
55
+ */
56
+ export function parseBooleanFlag(
57
+ tokens: string[],
58
+ flag: string,
59
+ supportsValue: boolean = false,
60
+ ): ParseBooleanFlagResult {
61
+ const prefix = `--${flag}`;
62
+ const valuePrefix = `${prefix}=`;
63
+ let value: boolean | undefined;
64
+ const rest: string[] = [];
65
+
66
+ for (const token of tokens) {
67
+ if (token === prefix) {
68
+ value = true;
69
+ continue;
70
+ }
71
+
72
+ if (supportsValue && token.startsWith(valuePrefix)) {
73
+ value = parseFlagValue(token.slice(valuePrefix.length), flag);
74
+ continue;
75
+ }
76
+
77
+ rest.push(token);
78
+ }
79
+
80
+ return { value, rest };
81
+ }
82
+
83
+ function parseFlagValue(raw: string, flag: string): boolean {
84
+ if (raw === "true") return true;
85
+ if (raw === "false") return false;
86
+ throw new FlagValueError(flag, raw);
87
+ }
88
+
89
+ class FlagValueError extends Error {
90
+ readonly flag: string;
91
+ readonly value: string;
92
+
93
+ constructor(flag: string, value: string) {
94
+ super(`Invalid value for --${flag}: ${value}`);
95
+ this.flag = flag;
96
+ this.value = value;
97
+ }
98
+ }
99
+
100
+ /** Build a usage message for a command. */
101
+ export function usageError(command: string, usage: string): Error {
102
+ return new Error(`Usage: /${command} ${usage}`);
103
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * /graphify-path — find the shortest path between two nodes in the graph.
3
+ */
4
+
5
+ import type { GraphifyCapabilities } from "../backends/types.js";
6
+ import { formatResultForCommand } from "./format.js";
7
+ import { notifyCommandError, requireCoordinatorWithCapability } from "./helpers.js";
8
+ import { splitArgs, usageError } from "./parse.js";
9
+ import type { CommandDefinition, CommandExecutor } from "./types.js";
10
+
11
+ const FEATURE: keyof GraphifyCapabilities = "path";
12
+ const PATH_USAGE = "<source> <target>";
13
+
14
+ const executePath: CommandExecutor = async (ctx, getCoordinator, args) => {
15
+ if (!ctx.hasUI) return;
16
+
17
+ const coordinator = requireCoordinatorWithCapability(
18
+ ctx,
19
+ getCoordinator,
20
+ FEATURE,
21
+ "The installed Graphify version does not support path queries.",
22
+ );
23
+ if (!coordinator) return;
24
+
25
+ try {
26
+ const [source, target] = parsePathArgs(args);
27
+ const result = await coordinator.path({ cwd: ctx.cwd, source, target });
28
+ ctx.ui.notify(formatResultForCommand(result), "info");
29
+ } catch (error) {
30
+ notifyCommandError(ctx, error);
31
+ }
32
+ };
33
+
34
+ export const pathCommand: CommandDefinition = {
35
+ name: "graphify-path",
36
+ label: "Find path",
37
+ description: "Find the shortest path between two nodes in the graph",
38
+ feature: FEATURE,
39
+ usage: PATH_USAGE,
40
+ argMode: "input",
41
+ prompt: "Path source and target",
42
+ placeholder: "src/index.ts src/utils.ts",
43
+ execute: executePath,
44
+ };
45
+
46
+ function parsePathArgs(args: string): [string, string] {
47
+ const tokens = splitArgs(args);
48
+ if (tokens.length !== 2) {
49
+ throw usageError("graphify-path", PATH_USAGE);
50
+ }
51
+ return [tokens[0], tokens[1]];
52
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * /graphify-query — ask a natural-language question against the graph.
3
+ */
4
+
5
+ import type { GraphifyCapabilities } from "../backends/types.js";
6
+ import { formatErrorForCommand, formatResultForCommand } from "./format.js";
7
+ import { requireCoordinatorWithCapability } from "./helpers.js";
8
+ import type { CommandDefinition, CommandExecutor } from "./types.js";
9
+
10
+ const FEATURE: keyof GraphifyCapabilities = "query";
11
+
12
+ const executeQuery: CommandExecutor = async (ctx, getCoordinator, args) => {
13
+ if (!ctx.hasUI) return;
14
+
15
+ const coordinator = requireCoordinatorWithCapability(
16
+ ctx,
17
+ getCoordinator,
18
+ FEATURE,
19
+ "The installed Graphify version does not support graph queries.",
20
+ );
21
+ if (!coordinator) return;
22
+
23
+ const question = args.trim();
24
+ if (question === "") {
25
+ ctx.ui.notify("Usage: /graphify-query <question>", "warning");
26
+ return;
27
+ }
28
+
29
+ try {
30
+ const result = await coordinator.query({ cwd: ctx.cwd, question });
31
+ ctx.ui.notify(formatResultForCommand(result), "info");
32
+ } catch (error) {
33
+ const { message, severity } = formatErrorForCommand(error);
34
+ ctx.ui.notify(message, severity);
35
+ }
36
+ };
37
+
38
+ export const queryCommand: CommandDefinition = {
39
+ name: "graphify-query",
40
+ label: "Query graph",
41
+ description: "Ask a natural-language question against the graph",
42
+ feature: FEATURE,
43
+ usage: "<question>",
44
+ argMode: "input",
45
+ prompt: "Question",
46
+ placeholder: "What does this project do?",
47
+ execute: executeQuery,
48
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Single source of truth for all Graphify slash commands.
3
+ */
4
+
5
+ import type { GraphifyCoordinator } from "../coordinator.js";
6
+ import { affectedCommand } from "./affected.js";
7
+ import { buildCommand } from "./build.js";
8
+ import { explainCommand } from "./explain.js";
9
+ import { pathCommand } from "./path.js";
10
+ import { queryCommand } from "./query.js";
11
+ import { statusCommand } from "./status.js";
12
+ import type { CommandDefinition } from "./types.js";
13
+ import { versionCommand } from "./version.js";
14
+
15
+ export const graphifyCommands: CommandDefinition[] = [
16
+ buildCommand,
17
+ queryCommand,
18
+ pathCommand,
19
+ explainCommand,
20
+ affectedCommand,
21
+ statusCommand,
22
+ versionCommand,
23
+ ];
24
+
25
+ export function getAvailableCommands(
26
+ coordinator: GraphifyCoordinator | null,
27
+ commands: CommandDefinition[] = graphifyCommands,
28
+ ): CommandDefinition[] {
29
+ if (!coordinator) return [];
30
+ return commands.filter((command) => coordinator.supports(command.feature));
31
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * /graphify-status — show graph status for the current project.
3
+ */
4
+
5
+ import type { GraphifyCapabilities } from "../backends/types.js";
6
+ import { formatErrorForCommand } from "./format.js";
7
+ import { requireCoordinator } from "./helpers.js";
8
+ import type { CommandDefinition, CommandExecutor } from "./types.js";
9
+
10
+ const FEATURE: keyof GraphifyCapabilities = "status";
11
+
12
+ const executeStatus: CommandExecutor = async (ctx, getCoordinator) => {
13
+ if (!ctx.hasUI) return;
14
+
15
+ const coordinator = requireCoordinator(ctx, getCoordinator);
16
+ if (!coordinator) return;
17
+
18
+ try {
19
+ const status = await coordinator.status({ cwd: ctx.cwd });
20
+ if (status.hasGraph && status.graphPath) {
21
+ ctx.ui.notify(`Graphify graph ready: ${status.graphPath}`, "info");
22
+ return;
23
+ }
24
+ ctx.ui.notify("No Graphify graph found. Build one with `graphify .`.", "warning");
25
+ } catch (error) {
26
+ const { message, severity } = formatErrorForCommand(error);
27
+ ctx.ui.notify(message, severity);
28
+ }
29
+ };
30
+
31
+ export const statusCommand: CommandDefinition = {
32
+ name: "graphify-status",
33
+ label: "Graph status",
34
+ description: "Show whether a Graphify knowledge graph exists for this project",
35
+ feature: FEATURE,
36
+ usage: "",
37
+ argMode: "none",
38
+ execute: executeStatus,
39
+ };