whygraph 0.1.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/LICENSE +21 -0
- package/README.md +205 -0
- package/dist/cli/commands/config.d.ts +14 -0
- package/dist/cli/commands/config.js +123 -0
- package/dist/cli/commands/down.d.ts +9 -0
- package/dist/cli/commands/down.js +46 -0
- package/dist/cli/commands/init.d.ts +17 -0
- package/dist/cli/commands/init.js +144 -0
- package/dist/cli/commands/issues.d.ts +10 -0
- package/dist/cli/commands/issues.js +376 -0
- package/dist/cli/commands/mcp.d.ts +2 -0
- package/dist/cli/commands/mcp.js +9 -0
- package/dist/cli/commands/restart.d.ts +11 -0
- package/dist/cli/commands/restart.js +43 -0
- package/dist/cli/commands/serve.d.ts +14 -0
- package/dist/cli/commands/serve.js +132 -0
- package/dist/cli/commands/server-utils.d.ts +6 -0
- package/dist/cli/commands/server-utils.js +94 -0
- package/dist/cli/commands/status.d.ts +11 -0
- package/dist/cli/commands/status.js +97 -0
- package/dist/cli/commands/up.d.ts +13 -0
- package/dist/cli/commands/up.js +62 -0
- package/dist/cli/commands/validate.d.ts +14 -0
- package/dist/cli/commands/validate.js +88 -0
- package/dist/cli/commands/viz.d.ts +7 -0
- package/dist/cli/commands/viz.js +97 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +33 -0
- package/dist/entity/id.d.ts +8 -0
- package/dist/entity/id.js +48 -0
- package/dist/entity/issues.d.ts +12 -0
- package/dist/entity/issues.js +68 -0
- package/dist/entity/parser.d.ts +6 -0
- package/dist/entity/parser.js +166 -0
- package/dist/entity/types.d.ts +54 -0
- package/dist/entity/types.js +21 -0
- package/dist/entity/validate.d.ts +12 -0
- package/dist/entity/validate.js +136 -0
- package/dist/entity/writer.d.ts +16 -0
- package/dist/entity/writer.js +142 -0
- package/dist/frontend/assets/index-ByZzPwVe.css +1 -0
- package/dist/frontend/assets/index-F9dxfzD_.js +170 -0
- package/dist/frontend/index.html +14 -0
- package/dist/graph/cascade.d.ts +10 -0
- package/dist/graph/cascade.js +49 -0
- package/dist/graph/decisions.d.ts +11 -0
- package/dist/graph/decisions.js +27 -0
- package/dist/graph/gaps.d.ts +10 -0
- package/dist/graph/gaps.js +58 -0
- package/dist/graph/nodes.d.ts +20 -0
- package/dist/graph/nodes.js +33 -0
- package/dist/graph/projection.d.ts +6 -0
- package/dist/graph/projection.js +44 -0
- package/dist/graph/query.d.ts +15 -0
- package/dist/graph/query.js +82 -0
- package/dist/graph/search.d.ts +2 -0
- package/dist/graph/search.js +23 -0
- package/dist/graph/supersede.d.ts +7 -0
- package/dist/graph/supersede.js +48 -0
- package/dist/graph/temporal.d.ts +13 -0
- package/dist/graph/temporal.js +28 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.js +10 -0
- package/dist/mcp/server.d.ts +3 -0
- package/dist/mcp/server.js +340 -0
- package/dist/onboarding/interview.d.ts +22 -0
- package/dist/onboarding/interview.js +92 -0
- package/dist/onboarding/scan.d.ts +17 -0
- package/dist/onboarding/scan.js +106 -0
- package/dist/platform/rules.d.ts +8 -0
- package/dist/platform/rules.js +229 -0
- package/dist/server/core.d.ts +26 -0
- package/dist/server/core.js +111 -0
- package/dist/server/derived.d.ts +8 -0
- package/dist/server/derived.js +13 -0
- package/dist/server/etag.d.ts +9 -0
- package/dist/server/etag.js +25 -0
- package/dist/server/http.d.ts +13 -0
- package/dist/server/http.js +131 -0
- package/dist/server/pubsub.d.ts +12 -0
- package/dist/server/pubsub.js +19 -0
- package/dist/server/schema.d.ts +2 -0
- package/dist/server/schema.js +362 -0
- package/dist/server/stale-refs.d.ts +7 -0
- package/dist/server/stale-refs.js +23 -0
- package/dist/server/watcher.d.ts +21 -0
- package/dist/server/watcher.js +98 -0
- package/dist/server/worktree-watcher.d.ts +20 -0
- package/dist/server/worktree-watcher.js +79 -0
- package/dist/server/worktree.d.ts +22 -0
- package/dist/server/worktree.js +84 -0
- package/package.json +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Geovanie Ruiz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Whygraph
|
|
2
|
+
|
|
3
|
+
**The graph of why. So your agent knows before it touches anything.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/whygraph)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
[]()
|
|
9
|
+
|
|
10
|
+
> Whygraph captures the architectural decisions behind your codebase — the tradeoffs accepted, the alternatives rejected, the patterns you deliberately chose — and makes them queryable by AI agents before they write a single line.
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## The Problem
|
|
17
|
+
|
|
18
|
+
You're vibe-coding. The agent is fast. Features ship in minutes. Then three sessions later, it confidently rebuilds something you already tried and abandoned. It re-introduces the pattern you explicitly rejected. It optimizes for the local signal — passing tests, satisfying the prompt — while losing the thread of why the architecture is shaped the way it is.
|
|
19
|
+
|
|
20
|
+
This isn't a hallucination problem. **It's a memory problem.** The agent isn't wrong. It just doesn't know the why.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install --save-dev whygraph
|
|
28
|
+
npx whygraph init
|
|
29
|
+
npx whygraph up
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`init` walks you through two prompts — app name and environment — then registers the MCP server and writes agent instructions to `CLAUDE.md` (Claude Code) or `AGENTS.md` (Cursor, Copilot, other).
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
Initialized whygraph in .whygraph/
|
|
36
|
+
App node: wg-ab12
|
|
37
|
+
Config: /your/project/.whygraph/config.yaml
|
|
38
|
+
MCP: registered
|
|
39
|
+
|
|
40
|
+
Run 'whygraph up' to start the server.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
`up` starts the server in the background:
|
|
44
|
+
|
|
45
|
+
```text
|
|
46
|
+
whygraph server running at http://localhost:4777 (pid 12345)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
From here, agents capture decisions automatically as they work. Run `whygraph viz` to open the graph visualization.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## What Agents See
|
|
54
|
+
|
|
55
|
+
When an agent is about to modify `src/auth/session.ts`, it calls `whygraph_context` and gets back the decisions that shaped that code:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"nodes": [
|
|
60
|
+
{ "id": "wg-sess", "label": "Component", "name": "Session Management" }
|
|
61
|
+
],
|
|
62
|
+
"decisions": [
|
|
63
|
+
{
|
|
64
|
+
"id": "wg-d001",
|
|
65
|
+
"title": "JWT over server-side sessions",
|
|
66
|
+
"status": "active",
|
|
67
|
+
"context": "Needed stateless auth to support horizontal scaling...",
|
|
68
|
+
"decision": "Use short-lived JWTs with silent refresh via interceptor.",
|
|
69
|
+
"tradeoffs": "Gained: stateless, horizontally scalable. Lost: cannot invalidate tokens server-side.",
|
|
70
|
+
"alternatives": "Server-side sessions — rejected: requires sticky sessions or shared store."
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The agent knows what was decided, why, what was traded away, and what was rejected — before it touches anything.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## MCP Tools
|
|
81
|
+
|
|
82
|
+
Whygraph exposes six MCP tools for agent integration. Read tools are always available. Write tools require strict mode (`whygraph config --mcp-mode strict`).
|
|
83
|
+
|
|
84
|
+
| Tool | Mode | Description |
|
|
85
|
+
| --------------------------------- | ----- | ------------------------------------------------------- |
|
|
86
|
+
| `whygraph_context(file, symbol?)` | read | Get decisions affecting the code you're about to modify |
|
|
87
|
+
| `whygraph_get_decisions(filters)` | read | Query decisions by status, tags, or date range |
|
|
88
|
+
| `whygraph_get_gaps(limit?)` | read | Find structural nodes with no recorded decisions |
|
|
89
|
+
| `whygraph_list_nodes(filters)` | read | List app, feature, and component nodes |
|
|
90
|
+
| `whygraph_create_decision(...)` | write | Create a decision with full validation |
|
|
91
|
+
| `whygraph_create_node(...)` | write | Create a structural node |
|
|
92
|
+
|
|
93
|
+
In default mode, write tools are not registered. Agents write decision files directly to `.whygraph/graph/` as markdown. The file watcher picks them up, validates, and flags issues as JSON sidecars that the `whygraph issues` command can resolve.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Platform Integration
|
|
98
|
+
|
|
99
|
+
Whygraph works with any AI development environment.
|
|
100
|
+
|
|
101
|
+
| Platform | Agent Instructions | MCP Registration |
|
|
102
|
+
| ----------- | ------------------ | --------------------------------------- |
|
|
103
|
+
| Claude Code | `CLAUDE.md` | Auto-registered via `claude mcp add` |
|
|
104
|
+
| Cursor | `AGENTS.md` | `.cursor/mcp.json` (written by `init`) |
|
|
105
|
+
| Copilot | `AGENTS.md` | `.vscode/mcp.json` (written by `init`) |
|
|
106
|
+
| Other | `AGENTS.md` | `MCP_SETUP.md` with manual instructions |
|
|
107
|
+
|
|
108
|
+
**Claude Code** gets the deepest integration: MCP auto-registration, strict mode write tools, and instructions baked directly into `CLAUDE.md`.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## The Graph
|
|
113
|
+
|
|
114
|
+
<!-- GIF placeholder — coming soon -->
|
|
115
|
+
|
|
116
|
+
Whygraph models your application as a hierarchy of nodes with decisions attached to the nodes they affect.
|
|
117
|
+
|
|
118
|
+
```text
|
|
119
|
+
App: MyProject
|
|
120
|
+
├── Feature: Auth
|
|
121
|
+
│ ├── Component: Session Management
|
|
122
|
+
│ │ └── ◆ JWT over sessions (active)
|
|
123
|
+
│ └── Component: Token Refresh
|
|
124
|
+
│ └── ◆ Silent refresh via interceptor (active)
|
|
125
|
+
└── Feature: API Layer
|
|
126
|
+
├── Component: Rate Limiting
|
|
127
|
+
│ └── ◆ Redis-backed sliding window (active)
|
|
128
|
+
└── ◆ REST over tRPC (active)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Node types:** `App` → `Feature` → `Component`, with `Decision` nodes attached via `AFFECTS` edges.
|
|
132
|
+
|
|
133
|
+
**Decision fields:** `title`, `date`, `context`, `decision`, `tradeoffs`, `alternatives`, `status`, `affects`, `tags`
|
|
134
|
+
|
|
135
|
+
**Tags** (fixed taxonomy): `arch`, `data`, `security`, `performance`, `integration`, `infra`, `ux`
|
|
136
|
+
|
|
137
|
+
Decisions are never deleted. A superseded decision stays in the graph with a `SUPERSEDES` edge explaining how the architecture evolved.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## CLI
|
|
142
|
+
|
|
143
|
+
| Command | Description |
|
|
144
|
+
| ------------------------------ | --------------------------------------------------------------------------- |
|
|
145
|
+
| `whygraph init` | Set up whygraph: choose environment, register MCP, write agent instructions |
|
|
146
|
+
| `whygraph up` | Start the server in the background |
|
|
147
|
+
| `whygraph down` | Stop the server |
|
|
148
|
+
| `whygraph restart` | Stop and restart the server |
|
|
149
|
+
| `whygraph serve` | Start the server in the foreground |
|
|
150
|
+
| `whygraph status` | Check server status and entity counts |
|
|
151
|
+
| `whygraph viz` | Open the graph visualization in a browser |
|
|
152
|
+
| `whygraph issues` | List and interactively resolve validation issues |
|
|
153
|
+
| `whygraph validate` | Validate all entities and cross-references |
|
|
154
|
+
| `whygraph config [--flag val]` | View or modify configuration |
|
|
155
|
+
| `whygraph mcp` | Start the MCP stdio server |
|
|
156
|
+
|
|
157
|
+
All commands accept `--json` for programmatic output.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Philosophy
|
|
162
|
+
|
|
163
|
+
**The why is not in the code.** You can read a codebase and understand what it does. You cannot read it and understand what was tried before, what was rejected, and what trade-offs were accepted.
|
|
164
|
+
|
|
165
|
+
**Decisions, not documentation.** Structured records — context, choice, tradeoffs, alternatives — not prose. Structure is what makes decisions queryable by agents.
|
|
166
|
+
|
|
167
|
+
**Append-only.** Superseded decisions stay in the graph. They explain the path, not just the destination.
|
|
168
|
+
|
|
169
|
+
**Repo-native.** The graph lives in `.whygraph/`, versioned with your code. No external service. No account required.
|
|
170
|
+
|
|
171
|
+
**Agent-first.** The MCP tools and agent instructions are the primary interface. The CLI and visualization exist so humans can inspect what agents will read.
|
|
172
|
+
|
|
173
|
+
**Never lose data.** Entity files are always written, even if validation fails. Issues are tracked in sidecars, not by rejecting writes.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Limitations
|
|
178
|
+
|
|
179
|
+
- The server must be running for MCP read tools and live visualization to work. In default mode, agents can write decision files directly to `.whygraph/graph/` without the server — they'll be picked up when it starts.
|
|
180
|
+
- Multi-agent worktree support detects divergence but does not auto-merge. Conflict resolution follows standard git workflow.
|
|
181
|
+
- The fixed tag taxonomy (`arch`, `data`, `security`, `performance`, `integration`, `infra`, `ux`) is intentional — it keeps decisions queryable. Custom tags are not supported in v1.
|
|
182
|
+
- No cloud sync or hosted option. The graph is local to your repo.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Multi-Agent / Worktree Support
|
|
187
|
+
|
|
188
|
+
Whygraph runs one server per repo and watches all git worktrees. When agents work in parallel:
|
|
189
|
+
|
|
190
|
+
- Each worktree's `.whygraph/graph/` is watched independently
|
|
191
|
+
- ETag-based dirty tracking detects divergence from the main graph
|
|
192
|
+
- Entity IDs use NanoIDs to prevent collisions across concurrent agents
|
|
193
|
+
- Conflict resolution happens at git merge time
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Acknowledgments
|
|
198
|
+
|
|
199
|
+
- **[Matt Pocock](https://www.youtube.com/@mattpocockuk)** — the development workflow used to build this project (TDD, PRD-first, simplify) is based on his [5 Skills for Claude Code](https://www.youtube.com/watch?v=EJyuu6zlQCg)
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT © [Geovanie Ruiz](https://github.com/geovanie-ruiz)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { WhygraphConfig } from "../../entity/types.js";
|
|
3
|
+
export interface ConfigUpdates {
|
|
4
|
+
environment?: string;
|
|
5
|
+
mcpMode?: string;
|
|
6
|
+
serverPort?: number;
|
|
7
|
+
tags?: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ConfigResult {
|
|
10
|
+
config: WhygraphConfig;
|
|
11
|
+
updated: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function runConfig(whygraphDir: string, updates?: ConfigUpdates): ConfigResult;
|
|
14
|
+
export declare function registerConfigCommand(program: Command): void;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { DECISION_TAGS } from "../../entity/types.js";
|
|
5
|
+
// ============================================================
|
|
6
|
+
// Validation
|
|
7
|
+
// ============================================================
|
|
8
|
+
const VALID_ENVIRONMENTS = [
|
|
9
|
+
"claude-code",
|
|
10
|
+
"cursor",
|
|
11
|
+
"copilot",
|
|
12
|
+
"other",
|
|
13
|
+
];
|
|
14
|
+
const VALID_MCP_MODES = ["default", "strict"];
|
|
15
|
+
function validateUpdates(updates) {
|
|
16
|
+
if (updates.environment !== undefined &&
|
|
17
|
+
!VALID_ENVIRONMENTS.includes(updates.environment)) {
|
|
18
|
+
throw new Error(`Invalid environment "${updates.environment}". Must be one of: ${VALID_ENVIRONMENTS.join(", ")}`);
|
|
19
|
+
}
|
|
20
|
+
if (updates.mcpMode !== undefined &&
|
|
21
|
+
!VALID_MCP_MODES.includes(updates.mcpMode)) {
|
|
22
|
+
throw new Error(`Invalid mcpMode "${updates.mcpMode}". Must be one of: ${VALID_MCP_MODES.join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
if (updates.serverPort !== undefined) {
|
|
25
|
+
if (typeof updates.serverPort !== "number" || isNaN(updates.serverPort)) {
|
|
26
|
+
throw new Error(`Invalid serverPort "${updates.serverPort}". Must be a number.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (updates.tags !== undefined) {
|
|
30
|
+
for (const tag of updates.tags) {
|
|
31
|
+
if (!DECISION_TAGS.includes(tag)) {
|
|
32
|
+
throw new Error(`Invalid tag "${tag}". Must be one of: ${DECISION_TAGS.join(", ")}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ============================================================
|
|
38
|
+
// Core Logic
|
|
39
|
+
// ============================================================
|
|
40
|
+
export function runConfig(whygraphDir, updates) {
|
|
41
|
+
const configPath = join(whygraphDir, ".whygraph", "config.yaml");
|
|
42
|
+
if (!existsSync(configPath)) {
|
|
43
|
+
throw new Error(`.whygraph/ not found in ${whygraphDir}. Run "whygraph init" first.`);
|
|
44
|
+
}
|
|
45
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
46
|
+
const config = yaml.load(raw);
|
|
47
|
+
if (!updates || Object.keys(updates).length === 0) {
|
|
48
|
+
return { config, updated: false };
|
|
49
|
+
}
|
|
50
|
+
validateUpdates(updates);
|
|
51
|
+
if (updates.environment !== undefined) {
|
|
52
|
+
config.environment = updates.environment;
|
|
53
|
+
}
|
|
54
|
+
if (updates.mcpMode !== undefined) {
|
|
55
|
+
config.mcpMode = updates.mcpMode;
|
|
56
|
+
}
|
|
57
|
+
if (updates.serverPort !== undefined) {
|
|
58
|
+
config.serverPort = updates.serverPort;
|
|
59
|
+
}
|
|
60
|
+
if (updates.tags !== undefined) {
|
|
61
|
+
config.tags = updates.tags;
|
|
62
|
+
}
|
|
63
|
+
const configYaml = yaml.dump(config, { lineWidth: -1 });
|
|
64
|
+
writeFileSync(configPath, configYaml, "utf-8");
|
|
65
|
+
return { config, updated: true };
|
|
66
|
+
}
|
|
67
|
+
// ============================================================
|
|
68
|
+
// CLI Wiring
|
|
69
|
+
// ============================================================
|
|
70
|
+
function formatConfig(config) {
|
|
71
|
+
const lines = [
|
|
72
|
+
`appName: ${config.appName}`,
|
|
73
|
+
`environment: ${config.environment}`,
|
|
74
|
+
`prefix: ${config.prefix}`,
|
|
75
|
+
`idLength: ${config.idLength}`,
|
|
76
|
+
`tags: ${config.tags.join(", ")}`,
|
|
77
|
+
`mcpMode: ${config.mcpMode}`,
|
|
78
|
+
`serverPort: ${config.serverPort}`,
|
|
79
|
+
];
|
|
80
|
+
return lines.join("\n");
|
|
81
|
+
}
|
|
82
|
+
export function registerConfigCommand(program) {
|
|
83
|
+
program
|
|
84
|
+
.command("config")
|
|
85
|
+
.description("View or update whygraph configuration")
|
|
86
|
+
.option("--environment <env>", "Update environment")
|
|
87
|
+
.option("--mcp-mode <mode>", "Update MCP mode (default | strict)")
|
|
88
|
+
.option("--server-port <port>", "Update server port")
|
|
89
|
+
.option("--json", "Output results as JSON")
|
|
90
|
+
.action((opts) => {
|
|
91
|
+
try {
|
|
92
|
+
const updates = {};
|
|
93
|
+
if (opts.environment !== undefined)
|
|
94
|
+
updates.environment = opts.environment;
|
|
95
|
+
if (opts.mcpMode !== undefined)
|
|
96
|
+
updates.mcpMode = opts.mcpMode;
|
|
97
|
+
if (opts.serverPort !== undefined)
|
|
98
|
+
updates.serverPort = Number(opts.serverPort);
|
|
99
|
+
const hasUpdates = Object.keys(updates).length > 0;
|
|
100
|
+
const result = runConfig(process.cwd(), hasUpdates ? updates : undefined);
|
|
101
|
+
if (opts.json) {
|
|
102
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (result.updated) {
|
|
106
|
+
process.stdout.write("Configuration updated.\n\n");
|
|
107
|
+
}
|
|
108
|
+
process.stdout.write(formatConfig(result.config) + "\n");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
/* v8 ignore next 1 */
|
|
113
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
114
|
+
if (opts.json) {
|
|
115
|
+
process.stdout.write(JSON.stringify({ error: message }, null, 2) + "\n");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
119
|
+
}
|
|
120
|
+
process.exitCode = 1;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
export declare function runDown(targetDir: string, options?: {
|
|
3
|
+
port?: number;
|
|
4
|
+
json?: boolean;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
stopped: boolean;
|
|
7
|
+
port: number;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function registerDownCommand(program: Command): void;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { findWhygraphDir, getConfiguredPort, killProcessOnPort, waitForPortFree, } from "./server-utils.js";
|
|
2
|
+
export async function runDown(targetDir, options = {}) {
|
|
3
|
+
const projectDir = findWhygraphDir(targetDir);
|
|
4
|
+
if (!projectDir) {
|
|
5
|
+
throw new Error(`.whygraph/ not found. Run "whygraph init" first.`);
|
|
6
|
+
}
|
|
7
|
+
const port = options.port ?? getConfiguredPort(projectDir);
|
|
8
|
+
const killed = killProcessOnPort(port);
|
|
9
|
+
if (killed) {
|
|
10
|
+
await waitForPortFree(port);
|
|
11
|
+
}
|
|
12
|
+
return { stopped: killed, port };
|
|
13
|
+
}
|
|
14
|
+
export function registerDownCommand(program) {
|
|
15
|
+
program
|
|
16
|
+
.command("down")
|
|
17
|
+
.description("Stop the whygraph server")
|
|
18
|
+
.option("--port <number>", "Port number")
|
|
19
|
+
.option("--json", "Output results as JSON")
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
try {
|
|
22
|
+
const port = opts.port ? parseInt(opts.port, 10) : undefined;
|
|
23
|
+
const result = await runDown(process.cwd(), { port, json: opts.json });
|
|
24
|
+
if (opts.json) {
|
|
25
|
+
process.stdout.write(JSON.stringify(result) + "\n");
|
|
26
|
+
}
|
|
27
|
+
else if (result.stopped) {
|
|
28
|
+
process.stdout.write(`whygraph server stopped (port ${result.port})\n`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
process.stdout.write(`No server running on port ${result.port}\n`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
/* v8 ignore next 1 */
|
|
36
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
37
|
+
if (opts.json) {
|
|
38
|
+
process.stdout.write(JSON.stringify({ error: message }) + "\n");
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
42
|
+
}
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { Environment } from "../../entity/types.js";
|
|
3
|
+
export interface InitOptions {
|
|
4
|
+
appName?: string;
|
|
5
|
+
environment?: Environment;
|
|
6
|
+
json?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface InitResult {
|
|
9
|
+
configPath: string;
|
|
10
|
+
appNodePath: string;
|
|
11
|
+
appId: string;
|
|
12
|
+
platformRulesPath: string;
|
|
13
|
+
mcpRegistered: boolean;
|
|
14
|
+
mcpSetupPath?: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function runInit(targetDir: string, options: InitOptions): Promise<InitResult>;
|
|
17
|
+
export declare function registerInitCommand(program: Command): void;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import prompts from "prompts";
|
|
5
|
+
import { generateId } from "../../entity/id.js";
|
|
6
|
+
import { writeEntity } from "../../entity/writer.js";
|
|
7
|
+
import { DECISION_TAGS } from "../../entity/types.js";
|
|
8
|
+
import { writePlatformRules } from "../../platform/rules.js";
|
|
9
|
+
// ============================================================
|
|
10
|
+
// Core Logic
|
|
11
|
+
// ============================================================
|
|
12
|
+
export async function runInit(targetDir, options) {
|
|
13
|
+
const whygraphDir = join(targetDir, ".whygraph");
|
|
14
|
+
if (existsSync(whygraphDir)) {
|
|
15
|
+
throw new Error(`.whygraph/ already exists in ${targetDir}. Aborting init.`);
|
|
16
|
+
}
|
|
17
|
+
// Resolve app name and environment from options or prompts
|
|
18
|
+
let appName = options.appName;
|
|
19
|
+
let environment = options.environment;
|
|
20
|
+
if (!appName || !environment) {
|
|
21
|
+
const answers = await prompts([
|
|
22
|
+
...(appName
|
|
23
|
+
? []
|
|
24
|
+
: [
|
|
25
|
+
{
|
|
26
|
+
type: "text",
|
|
27
|
+
name: "appName",
|
|
28
|
+
message: "App name:",
|
|
29
|
+
},
|
|
30
|
+
]),
|
|
31
|
+
/* v8 ignore start */
|
|
32
|
+
...(environment
|
|
33
|
+
? []
|
|
34
|
+
: [
|
|
35
|
+
{
|
|
36
|
+
type: "select",
|
|
37
|
+
name: "environment",
|
|
38
|
+
message: "Environment:",
|
|
39
|
+
choices: [
|
|
40
|
+
{ title: "Claude Code", value: "claude-code" },
|
|
41
|
+
{ title: "Cursor", value: "cursor" },
|
|
42
|
+
{ title: "Copilot", value: "copilot" },
|
|
43
|
+
{ title: "Other", value: "other" },
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
]),
|
|
47
|
+
/* v8 ignore stop */
|
|
48
|
+
], { onCancel: () => { throw new Error("Init cancelled by user."); } });
|
|
49
|
+
if (!appName)
|
|
50
|
+
appName = answers.appName;
|
|
51
|
+
/* v8 ignore next 1 */
|
|
52
|
+
if (!environment)
|
|
53
|
+
environment = answers.environment;
|
|
54
|
+
}
|
|
55
|
+
if (!appName) {
|
|
56
|
+
throw new Error("App name is required.");
|
|
57
|
+
}
|
|
58
|
+
if (!environment) {
|
|
59
|
+
throw new Error("Environment is required.");
|
|
60
|
+
}
|
|
61
|
+
// Create directory structure
|
|
62
|
+
const graphDir = join(whygraphDir, "graph");
|
|
63
|
+
mkdirSync(graphDir, { recursive: true });
|
|
64
|
+
// Build config
|
|
65
|
+
const config = {
|
|
66
|
+
appName,
|
|
67
|
+
environment,
|
|
68
|
+
prefix: "wg-",
|
|
69
|
+
idLength: 4,
|
|
70
|
+
tags: [...DECISION_TAGS],
|
|
71
|
+
mcpMode: "default",
|
|
72
|
+
serverPort: 4777,
|
|
73
|
+
};
|
|
74
|
+
// Write config.yaml
|
|
75
|
+
const configPath = join(whygraphDir, "config.yaml");
|
|
76
|
+
const configYaml = yaml.dump(config, { lineWidth: -1 });
|
|
77
|
+
writeFileSync(configPath, configYaml, "utf-8");
|
|
78
|
+
// Create App node
|
|
79
|
+
const now = new Date().toISOString();
|
|
80
|
+
const appId = generateId({ prefix: config.prefix, length: config.idLength });
|
|
81
|
+
const appNode = {
|
|
82
|
+
id: appId,
|
|
83
|
+
label: "App",
|
|
84
|
+
name: appName,
|
|
85
|
+
status: "active",
|
|
86
|
+
created_at: now,
|
|
87
|
+
updated_at: now,
|
|
88
|
+
};
|
|
89
|
+
const { filePath: appNodePath } = writeEntity(graphDir, appNode);
|
|
90
|
+
// Write platform-specific rules and register MCP server
|
|
91
|
+
const platformResult = writePlatformRules(targetDir, environment, "", config);
|
|
92
|
+
return {
|
|
93
|
+
configPath,
|
|
94
|
+
appNodePath,
|
|
95
|
+
appId,
|
|
96
|
+
platformRulesPath: platformResult.filePath,
|
|
97
|
+
mcpRegistered: platformResult.mcpRegistered,
|
|
98
|
+
mcpSetupPath: platformResult.mcpSetupPath,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// ============================================================
|
|
102
|
+
// CLI Wiring
|
|
103
|
+
// ============================================================
|
|
104
|
+
export function registerInitCommand(program) {
|
|
105
|
+
program
|
|
106
|
+
.command("init")
|
|
107
|
+
.description("Initialize a new whygraph project in the current directory")
|
|
108
|
+
.option("--app-name <name>", "Application name (skips prompt)")
|
|
109
|
+
.option("--environment <env>", "Environment: claude-code, cursor, copilot, other (skips prompt)")
|
|
110
|
+
.option("--json", "Output results as JSON")
|
|
111
|
+
.action(async (opts) => {
|
|
112
|
+
try {
|
|
113
|
+
const result = await runInit(process.cwd(), {
|
|
114
|
+
appName: opts.appName,
|
|
115
|
+
environment: opts.environment,
|
|
116
|
+
json: opts.json,
|
|
117
|
+
});
|
|
118
|
+
if (opts.json) {
|
|
119
|
+
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const mcpLine = result.mcpRegistered
|
|
123
|
+
? ` MCP: registered\n`
|
|
124
|
+
: ` MCP: setup failed — see ${result.mcpSetupPath} for manual instructions\n`;
|
|
125
|
+
process.stdout.write(`Initialized whygraph in .whygraph/\n` +
|
|
126
|
+
` App node: ${result.appId}\n` +
|
|
127
|
+
` Config: ${result.configPath}\n` +
|
|
128
|
+
mcpLine +
|
|
129
|
+
`\nRun 'whygraph up' to start the server.\n`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
/* v8 ignore next 1 */
|
|
134
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
135
|
+
if (opts.json) {
|
|
136
|
+
process.stdout.write(JSON.stringify({ error: message }, null, 2) + "\n");
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
process.stderr.write(`Error: ${message}\n`);
|
|
140
|
+
}
|
|
141
|
+
process.exitCode = 1;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { EntityIssue } from "../../entity/issues.js";
|
|
3
|
+
export interface IssuesResult {
|
|
4
|
+
agentNeeded: number;
|
|
5
|
+
cliResolvable: number;
|
|
6
|
+
total: number;
|
|
7
|
+
issues: EntityIssue[];
|
|
8
|
+
}
|
|
9
|
+
export declare function runIssues(targetDir: string): IssuesResult;
|
|
10
|
+
export declare function registerIssuesCommand(program: Command): void;
|