gitnexushub 0.2.1
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/dist/api.d.ts +64 -0
- package/dist/api.js +55 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.js +31 -0
- package/dist/content.d.ts +20 -0
- package/dist/content.js +137 -0
- package/dist/context.d.ts +11 -0
- package/dist/context.js +50 -0
- package/dist/editors/claude-code.d.ts +8 -0
- package/dist/editors/claude-code.js +129 -0
- package/dist/editors/cursor.d.ts +8 -0
- package/dist/editors/cursor.js +83 -0
- package/dist/editors/detect.d.ts +7 -0
- package/dist/editors/detect.js +42 -0
- package/dist/editors/opencode.d.ts +8 -0
- package/dist/editors/opencode.js +83 -0
- package/dist/editors/types.d.ts +19 -0
- package/dist/editors/types.js +4 -0
- package/dist/editors/windsurf.d.ts +8 -0
- package/dist/editors/windsurf.js +83 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +397 -0
- package/dist/project.d.ts +21 -0
- package/dist/project.js +48 -0
- package/dist/utils.d.ts +23 -0
- package/dist/utils.js +90 -0
- package/package.json +53 -0
- package/skills/gitnexus-debugging.md +89 -0
- package/skills/gitnexus-exploring.md +78 -0
- package/skills/gitnexus-guide.md +64 -0
- package/skills/gitnexus-impact-analysis.md +97 -0
- package/skills/gitnexus-pr-review.md +163 -0
- package/skills/gitnexus-refactoring.md +121 -0
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub API Client
|
|
3
|
+
*
|
|
4
|
+
* Fetch-based, zero-dependency API client for GitNexus Hub.
|
|
5
|
+
*/
|
|
6
|
+
export interface UserProfile {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
email: string;
|
|
10
|
+
}
|
|
11
|
+
export interface HubRepo {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
fullName: string;
|
|
15
|
+
status: string;
|
|
16
|
+
stats?: {
|
|
17
|
+
nodes?: number;
|
|
18
|
+
edges?: number;
|
|
19
|
+
processes?: number;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface ConnectContext {
|
|
23
|
+
claudeMd: string;
|
|
24
|
+
agentsMd: string;
|
|
25
|
+
skills: Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
content: string;
|
|
28
|
+
}>;
|
|
29
|
+
}
|
|
30
|
+
export interface IndexResult {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
fullName: string;
|
|
34
|
+
status: string;
|
|
35
|
+
}
|
|
36
|
+
export interface RepoDetail {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
fullName: string;
|
|
40
|
+
status: string;
|
|
41
|
+
stats?: {
|
|
42
|
+
nodes?: number;
|
|
43
|
+
edges?: number;
|
|
44
|
+
processes?: number;
|
|
45
|
+
};
|
|
46
|
+
error?: string;
|
|
47
|
+
job?: {
|
|
48
|
+
status: string;
|
|
49
|
+
progress: number;
|
|
50
|
+
phase: string;
|
|
51
|
+
} | null;
|
|
52
|
+
}
|
|
53
|
+
export declare class HubAPI {
|
|
54
|
+
private hubUrl;
|
|
55
|
+
private token;
|
|
56
|
+
constructor(hubUrl: string, token: string);
|
|
57
|
+
private request;
|
|
58
|
+
private post;
|
|
59
|
+
getMe(): Promise<UserProfile>;
|
|
60
|
+
listRepos(): Promise<HubRepo[]>;
|
|
61
|
+
getConnectContext(repoFullName: string): Promise<ConnectContext>;
|
|
62
|
+
indexRepo(fullName: string): Promise<IndexResult>;
|
|
63
|
+
getRepo(repoId: string): Promise<RepoDetail>;
|
|
64
|
+
}
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hub API Client
|
|
3
|
+
*
|
|
4
|
+
* Fetch-based, zero-dependency API client for GitNexus Hub.
|
|
5
|
+
*/
|
|
6
|
+
export class HubAPI {
|
|
7
|
+
hubUrl;
|
|
8
|
+
token;
|
|
9
|
+
constructor(hubUrl, token) {
|
|
10
|
+
this.hubUrl = hubUrl;
|
|
11
|
+
this.token = token;
|
|
12
|
+
}
|
|
13
|
+
async request(path) {
|
|
14
|
+
const url = `${this.hubUrl}${path}`;
|
|
15
|
+
const res = await fetch(url, {
|
|
16
|
+
headers: { Authorization: `Bearer ${this.token}` },
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const body = await res.json().catch(() => ({ error: res.statusText }));
|
|
20
|
+
throw new Error(body.error || `HTTP ${res.status}`);
|
|
21
|
+
}
|
|
22
|
+
return res.json();
|
|
23
|
+
}
|
|
24
|
+
async post(path, body) {
|
|
25
|
+
const url = `${this.hubUrl}${path}`;
|
|
26
|
+
const res = await fetch(url, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${this.token}`,
|
|
30
|
+
'Content-Type': 'application/json',
|
|
31
|
+
},
|
|
32
|
+
body: JSON.stringify(body),
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const data = await res.json().catch(() => ({ error: res.statusText }));
|
|
36
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
37
|
+
}
|
|
38
|
+
return res.json();
|
|
39
|
+
}
|
|
40
|
+
async getMe() {
|
|
41
|
+
return this.request('/auth/me');
|
|
42
|
+
}
|
|
43
|
+
async listRepos() {
|
|
44
|
+
return this.request('/api/repos');
|
|
45
|
+
}
|
|
46
|
+
async getConnectContext(repoFullName) {
|
|
47
|
+
return this.request(`/api/connect/context?repo=${encodeURIComponent(repoFullName)}`);
|
|
48
|
+
}
|
|
49
|
+
async indexRepo(fullName) {
|
|
50
|
+
return this.post('/api/repos/public', { fullName });
|
|
51
|
+
}
|
|
52
|
+
async getRepo(repoId) {
|
|
53
|
+
return this.request(`/api/repos/${repoId}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Manager
|
|
3
|
+
*
|
|
4
|
+
* Persists Hub token and URL in ~/.gitnexus/config.json.
|
|
5
|
+
*/
|
|
6
|
+
interface ConnectConfig {
|
|
7
|
+
hubToken?: string;
|
|
8
|
+
hubUrl?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function loadConfig(): Promise<ConnectConfig>;
|
|
11
|
+
export declare function saveConfig(updates: Partial<ConnectConfig>): Promise<void>;
|
|
12
|
+
export declare function clearConfig(): Promise<void>;
|
|
13
|
+
export declare function getConfigPath(): string;
|
|
14
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Manager
|
|
3
|
+
*
|
|
4
|
+
* Persists Hub token and URL in ~/.gitnexus/config.json.
|
|
5
|
+
*/
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { readJsonFile, writeJsonFile } from './utils.js';
|
|
9
|
+
const CONFIG_DIR = path.join(os.homedir(), '.gitnexus');
|
|
10
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
|
|
11
|
+
export async function loadConfig() {
|
|
12
|
+
const data = await readJsonFile(CONFIG_PATH);
|
|
13
|
+
return data || {};
|
|
14
|
+
}
|
|
15
|
+
export async function saveConfig(updates) {
|
|
16
|
+
const existing = await loadConfig();
|
|
17
|
+
const merged = { ...existing, ...updates };
|
|
18
|
+
await writeJsonFile(CONFIG_PATH, merged);
|
|
19
|
+
}
|
|
20
|
+
export async function clearConfig() {
|
|
21
|
+
try {
|
|
22
|
+
const fs = await import('fs/promises');
|
|
23
|
+
await fs.rm(CONFIG_DIR, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Already gone
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function getConfigPath() {
|
|
30
|
+
return CONFIG_PATH;
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates CLAUDE.md, AGENTS.md content and loads bundled skills.
|
|
5
|
+
* Self-contained — no Hub API dependency for content generation.
|
|
6
|
+
*/
|
|
7
|
+
import type { ConnectContext } from './api.js';
|
|
8
|
+
/** Skills to include for Hub users (excludes gitnexus-cli) */
|
|
9
|
+
export declare const HUB_SKILLS: string[];
|
|
10
|
+
interface RepoStats {
|
|
11
|
+
nodes?: number;
|
|
12
|
+
edges?: number;
|
|
13
|
+
processes?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generate full ConnectContext from repo stats + bundled skills.
|
|
17
|
+
* No Hub API round-trip needed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function generateConnectContext(repoFullName: string, stats: RepoStats): Promise<ConnectContext>;
|
|
20
|
+
export {};
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Generator
|
|
3
|
+
*
|
|
4
|
+
* Generates CLAUDE.md, AGENTS.md content and loads bundled skills.
|
|
5
|
+
* Self-contained — no Hub API dependency for content generation.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const GITNEXUS_START_MARKER = '<!-- gitnexus:start -->';
|
|
13
|
+
const GITNEXUS_END_MARKER = '<!-- gitnexus:end -->';
|
|
14
|
+
/** Skills to include for Hub users (excludes gitnexus-cli) */
|
|
15
|
+
export const HUB_SKILLS = [
|
|
16
|
+
'gitnexus-exploring',
|
|
17
|
+
'gitnexus-debugging',
|
|
18
|
+
'gitnexus-impact-analysis',
|
|
19
|
+
'gitnexus-refactoring',
|
|
20
|
+
'gitnexus-guide',
|
|
21
|
+
'gitnexus-pr-review',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Generate Hub-flavored CLAUDE.md / AGENTS.md content.
|
|
25
|
+
* No detect_changes or rename references (not available via Hub MCP).
|
|
26
|
+
*/
|
|
27
|
+
function generateHubContent(projectName, stats) {
|
|
28
|
+
return `${GITNEXUS_START_MARKER}
|
|
29
|
+
# GitNexus — Code Intelligence
|
|
30
|
+
|
|
31
|
+
This project is indexed by GitNexus as **${projectName}** (${stats.nodes || 0} symbols, ${stats.edges || 0} relationships, ${stats.processes || 0} execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely.
|
|
32
|
+
|
|
33
|
+
> Re-indexing is managed from the GitNexus Hub dashboard.
|
|
34
|
+
|
|
35
|
+
## Always Do
|
|
36
|
+
|
|
37
|
+
- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run \`gitnexus_impact({target: "symbolName", direction: "upstream"})\` and report the blast radius (direct callers, affected processes, risk level) to the user.
|
|
38
|
+
- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
|
|
39
|
+
- When exploring unfamiliar code, use \`gitnexus_query({query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
|
|
40
|
+
- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use \`gitnexus_context({name: "symbolName"})\`.
|
|
41
|
+
|
|
42
|
+
## When Debugging
|
|
43
|
+
|
|
44
|
+
1. \`gitnexus_query({query: "<error or symptom>"})\` — find execution flows related to the issue
|
|
45
|
+
2. \`gitnexus_context({name: "<suspect function>"})\` — see all callers, callees, and process participation
|
|
46
|
+
3. \`READ gitnexus://repo/${projectName}/process/{processName}\` — trace the full execution flow step by step
|
|
47
|
+
|
|
48
|
+
## When Refactoring
|
|
49
|
+
|
|
50
|
+
- **Extracting/Splitting**: MUST run \`gitnexus_context({name: "target"})\` to see all incoming/outgoing refs, then \`gitnexus_impact({target: "target", direction: "upstream"})\` to find all external callers before moving code.
|
|
51
|
+
|
|
52
|
+
## Never Do
|
|
53
|
+
|
|
54
|
+
- NEVER edit a function, class, or method without first running \`gitnexus_impact\` on it.
|
|
55
|
+
- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis.
|
|
56
|
+
|
|
57
|
+
## Tools Quick Reference
|
|
58
|
+
|
|
59
|
+
| Tool | When to use | Command |
|
|
60
|
+
|------|-------------|---------|
|
|
61
|
+
| \`query\` | Find code by concept | \`gitnexus_query({query: "auth validation"})\` |
|
|
62
|
+
| \`context\` | 360-degree view of one symbol | \`gitnexus_context({name: "validateUser"})\` |
|
|
63
|
+
| \`impact\` | Blast radius before editing | \`gitnexus_impact({target: "X", direction: "upstream"})\` |
|
|
64
|
+
| \`cypher\` | Custom graph queries | \`gitnexus_cypher({query: "MATCH ..."})\` |
|
|
65
|
+
|
|
66
|
+
## Impact Risk Levels
|
|
67
|
+
|
|
68
|
+
| Depth | Meaning | Action |
|
|
69
|
+
|-------|---------|--------|
|
|
70
|
+
| d=1 | WILL BREAK — direct callers/importers | MUST update these |
|
|
71
|
+
| d=2 | LIKELY AFFECTED — indirect deps | Should test |
|
|
72
|
+
| d=3 | MAY NEED TESTING — transitive | Test if critical path |
|
|
73
|
+
|
|
74
|
+
## Resources
|
|
75
|
+
|
|
76
|
+
| Resource | Use for |
|
|
77
|
+
|----------|---------|
|
|
78
|
+
| \`gitnexus://repo/${projectName}/context\` | Codebase overview, check index freshness |
|
|
79
|
+
| \`gitnexus://repo/${projectName}/clusters\` | All functional areas |
|
|
80
|
+
| \`gitnexus://repo/${projectName}/processes\` | All execution flows |
|
|
81
|
+
| \`gitnexus://repo/${projectName}/process/{name}\` | Step-by-step execution trace |
|
|
82
|
+
|
|
83
|
+
## Self-Check Before Finishing
|
|
84
|
+
|
|
85
|
+
Before completing any code modification task, verify:
|
|
86
|
+
1. \`gitnexus_impact\` was run for all modified symbols
|
|
87
|
+
2. No HIGH/CRITICAL risk warnings were ignored
|
|
88
|
+
3. All d=1 (WILL BREAK) dependents were updated
|
|
89
|
+
|
|
90
|
+
${GITNEXUS_END_MARKER}`;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Replace CLI references with Hub dashboard instructions.
|
|
94
|
+
*/
|
|
95
|
+
function adaptSkillForHub(content) {
|
|
96
|
+
return content
|
|
97
|
+
.replace(/run `npx gitnexus analyze` in terminal\.?/g, 'trigger re-indexing from the Hub dashboard.')
|
|
98
|
+
.replace(/run `npx gitnexus analyze` in the terminal first\.?/g, 'trigger re-indexing from the Hub dashboard.')
|
|
99
|
+
.replace(/> If "Index is stale" → run `npx gitnexus analyze` in terminal\.?/g, '> If "Index is stale" → trigger re-indexing from the Hub dashboard.')
|
|
100
|
+
.replace(/> If step 2 says "Index is stale" → run `npx gitnexus analyze` in terminal\.?/g, '> If step 2 says "Index is stale" → trigger re-indexing from the Hub dashboard.')
|
|
101
|
+
.replace(/> If step 1 warns the index is stale, run `npx gitnexus analyze` in the terminal first\.?/g, '> If step 1 warns the index is stale, trigger re-indexing from the Hub dashboard.')
|
|
102
|
+
.replace(/> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first\.?/g, '> Re-indexing is managed from the GitNexus Hub dashboard.');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Load bundled skills from the package's own skills/ directory.
|
|
106
|
+
* Skills ship with the npm package — no Hub API needed.
|
|
107
|
+
*/
|
|
108
|
+
async function loadBundledSkills() {
|
|
109
|
+
// Skills are at ../skills/ relative to dist/ (or ../../skills/ relative to src/)
|
|
110
|
+
const skillsDir = path.join(__dirname, '..', 'skills');
|
|
111
|
+
const skills = [];
|
|
112
|
+
for (const skillName of HUB_SKILLS) {
|
|
113
|
+
try {
|
|
114
|
+
const skillPath = path.join(skillsDir, `${skillName}.md`);
|
|
115
|
+
const raw = await fs.readFile(skillPath, 'utf-8');
|
|
116
|
+
skills.push({ name: skillName, content: adaptSkillForHub(raw) });
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Skill file not found — skip
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return skills;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Generate full ConnectContext from repo stats + bundled skills.
|
|
126
|
+
* No Hub API round-trip needed.
|
|
127
|
+
*/
|
|
128
|
+
export async function generateConnectContext(repoFullName, stats) {
|
|
129
|
+
const projectName = repoFullName.split('/').pop() || repoFullName;
|
|
130
|
+
const content = generateHubContent(projectName, stats);
|
|
131
|
+
const skills = await loadBundledSkills();
|
|
132
|
+
return {
|
|
133
|
+
claudeMd: content,
|
|
134
|
+
agentsMd: content,
|
|
135
|
+
skills,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Context Writer
|
|
3
|
+
*
|
|
4
|
+
* Writes CLAUDE.md, AGENTS.md, and skills into the current project.
|
|
5
|
+
*/
|
|
6
|
+
import type { ConnectContext } from './api.js';
|
|
7
|
+
export interface ContextResult {
|
|
8
|
+
files: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function writeProjectContext(projectDir: string, ctx: ConnectContext): Promise<ContextResult>;
|
|
11
|
+
export declare function removeProjectContext(projectDir: string): Promise<string[]>;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Context Writer
|
|
3
|
+
*
|
|
4
|
+
* Writes CLAUDE.md, AGENTS.md, and skills into the current project.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { upsertMarkedSection, removeMarkedSection } from './utils.js';
|
|
9
|
+
export async function writeProjectContext(projectDir, ctx) {
|
|
10
|
+
const files = [];
|
|
11
|
+
// Write CLAUDE.md
|
|
12
|
+
const claudePath = path.join(projectDir, 'CLAUDE.md');
|
|
13
|
+
const claudeResult = await upsertMarkedSection(claudePath, ctx.claudeMd);
|
|
14
|
+
files.push(`CLAUDE.md (${claudeResult})`);
|
|
15
|
+
// Write AGENTS.md
|
|
16
|
+
const agentsPath = path.join(projectDir, 'AGENTS.md');
|
|
17
|
+
const agentsResult = await upsertMarkedSection(agentsPath, ctx.agentsMd);
|
|
18
|
+
files.push(`AGENTS.md (${agentsResult})`);
|
|
19
|
+
// Write skills to .claude/skills/gitnexus/
|
|
20
|
+
if (ctx.skills.length > 0) {
|
|
21
|
+
const skillsDir = path.join(projectDir, '.claude', 'skills', 'gitnexus');
|
|
22
|
+
for (const skill of ctx.skills) {
|
|
23
|
+
const skillDir = path.join(skillsDir, skill.name);
|
|
24
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
25
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skill.content, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
files.push(`.claude/skills/gitnexus/ (${ctx.skills.length} skills)`);
|
|
28
|
+
}
|
|
29
|
+
return { files };
|
|
30
|
+
}
|
|
31
|
+
export async function removeProjectContext(projectDir) {
|
|
32
|
+
const removed = [];
|
|
33
|
+
// Remove marked sections from CLAUDE.md and AGENTS.md
|
|
34
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
35
|
+
const filePath = path.join(projectDir, name);
|
|
36
|
+
if (await removeMarkedSection(filePath)) {
|
|
37
|
+
removed.push(name);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Remove .claude/skills/gitnexus/ directory
|
|
41
|
+
const skillsDir = path.join(projectDir, '.claude', 'skills', 'gitnexus');
|
|
42
|
+
try {
|
|
43
|
+
await fs.rm(skillsDir, { recursive: true, force: true });
|
|
44
|
+
removed.push('.claude/skills/gitnexus/');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Already gone
|
|
48
|
+
}
|
|
49
|
+
return removed;
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Editor Setup
|
|
3
|
+
*
|
|
4
|
+
* Runs `claude mcp add` CLI command, or falls back to writing ~/.claude.json.
|
|
5
|
+
* Skills are installed to ~/.claude/skills/.
|
|
6
|
+
*/
|
|
7
|
+
import type { EditorConfig } from './types.js';
|
|
8
|
+
export declare const claudeCodeEditor: EditorConfig;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Editor Setup
|
|
3
|
+
*
|
|
4
|
+
* Runs `claude mcp add` CLI command, or falls back to writing ~/.claude.json.
|
|
5
|
+
* Skills are installed to ~/.claude/skills/.
|
|
6
|
+
*/
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
import { readJsonFile, writeJsonFile } from '../utils.js';
|
|
12
|
+
import { HUB_SKILLS } from '../content.js';
|
|
13
|
+
const GITNEXUS_SKILL_NAMES = HUB_SKILLS;
|
|
14
|
+
function isClaudeOnPath() {
|
|
15
|
+
try {
|
|
16
|
+
execSync('claude --version', { stdio: 'pipe' });
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function configure(hubUrl, token) {
|
|
24
|
+
const mcpUrl = `${hubUrl}/mcp`;
|
|
25
|
+
const headerValue = `Authorization: Bearer ${token}`;
|
|
26
|
+
// Try `claude mcp add` first
|
|
27
|
+
if (isClaudeOnPath()) {
|
|
28
|
+
try {
|
|
29
|
+
const cmd = `claude mcp add gitnexus --transport streamable-http --url "${mcpUrl}" --header "${headerValue}"`;
|
|
30
|
+
if (process.platform === 'win32') {
|
|
31
|
+
execSync(`cmd /c ${cmd}`, { stdio: 'pipe' });
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
35
|
+
}
|
|
36
|
+
return { success: true, message: 'MCP configured via `claude mcp add`' };
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Fall through to file-based config
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Fallback: write ~/.claude.json
|
|
43
|
+
try {
|
|
44
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
45
|
+
const existing = (await readJsonFile(claudeJsonPath)) || {};
|
|
46
|
+
if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
|
|
47
|
+
existing.mcpServers = {};
|
|
48
|
+
}
|
|
49
|
+
existing.mcpServers.gitnexus = {
|
|
50
|
+
type: 'streamable-http',
|
|
51
|
+
url: mcpUrl,
|
|
52
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
53
|
+
};
|
|
54
|
+
await writeJsonFile(claudeJsonPath, existing);
|
|
55
|
+
return { success: true, message: 'MCP configured in ~/.claude.json' };
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return { success: false, message: `Failed: ${err.message}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
async function installSkills(skills) {
|
|
62
|
+
const skillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
63
|
+
let installed = 0;
|
|
64
|
+
for (const skill of skills) {
|
|
65
|
+
try {
|
|
66
|
+
const skillDir = path.join(skillsDir, skill.name);
|
|
67
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
68
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skill.content, 'utf-8');
|
|
69
|
+
installed++;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Skip on error
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return installed;
|
|
76
|
+
}
|
|
77
|
+
async function unconfigure() {
|
|
78
|
+
// Try `claude mcp remove` first
|
|
79
|
+
if (isClaudeOnPath()) {
|
|
80
|
+
try {
|
|
81
|
+
const cmd = 'claude mcp remove gitnexus';
|
|
82
|
+
if (process.platform === 'win32') {
|
|
83
|
+
execSync(`cmd /c ${cmd}`, { stdio: 'pipe' });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
87
|
+
}
|
|
88
|
+
return { success: true, message: 'MCP removed via `claude mcp remove`' };
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Fall through to file-based removal
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Fallback: remove from ~/.claude.json
|
|
95
|
+
try {
|
|
96
|
+
const claudeJsonPath = path.join(os.homedir(), '.claude.json');
|
|
97
|
+
const existing = await readJsonFile(claudeJsonPath);
|
|
98
|
+
if (existing?.mcpServers?.gitnexus) {
|
|
99
|
+
delete existing.mcpServers.gitnexus;
|
|
100
|
+
await writeJsonFile(claudeJsonPath, existing);
|
|
101
|
+
}
|
|
102
|
+
return { success: true, message: 'MCP removed from ~/.claude.json' };
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
return { success: false, message: `Failed: ${err.message}` };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function removeSkills() {
|
|
109
|
+
const skillsDir = path.join(os.homedir(), '.claude', 'skills');
|
|
110
|
+
let removed = 0;
|
|
111
|
+
for (const name of GITNEXUS_SKILL_NAMES) {
|
|
112
|
+
try {
|
|
113
|
+
await fs.rm(path.join(skillsDir, name), { recursive: true, force: true });
|
|
114
|
+
removed++;
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Already gone
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return removed;
|
|
121
|
+
}
|
|
122
|
+
export const claudeCodeEditor = {
|
|
123
|
+
id: 'claude-code',
|
|
124
|
+
name: 'Claude Code',
|
|
125
|
+
configure,
|
|
126
|
+
unconfigure,
|
|
127
|
+
installSkills,
|
|
128
|
+
removeSkills,
|
|
129
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cursor Editor Setup
|
|
3
|
+
*
|
|
4
|
+
* Writes MCP config to ~/.cursor/mcp.json
|
|
5
|
+
* Installs skills to ~/.cursor/skills/
|
|
6
|
+
*/
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import fs from 'fs/promises';
|
|
10
|
+
import { readJsonFile, writeJsonFile } from '../utils.js';
|
|
11
|
+
import { HUB_SKILLS } from '../content.js';
|
|
12
|
+
function getMcpConfig(hubUrl, token) {
|
|
13
|
+
return {
|
|
14
|
+
type: 'streamable-http',
|
|
15
|
+
url: `${hubUrl}/mcp`,
|
|
16
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function configure(hubUrl, token) {
|
|
20
|
+
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
21
|
+
try {
|
|
22
|
+
const existing = (await readJsonFile(mcpPath)) || {};
|
|
23
|
+
if (!existing.mcpServers || typeof existing.mcpServers !== 'object') {
|
|
24
|
+
existing.mcpServers = {};
|
|
25
|
+
}
|
|
26
|
+
existing.mcpServers.gitnexus = getMcpConfig(hubUrl, token);
|
|
27
|
+
await writeJsonFile(mcpPath, existing);
|
|
28
|
+
return { success: true, message: 'MCP configured in ~/.cursor/mcp.json' };
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
return { success: false, message: `Failed: ${err.message}` };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function installSkills(skills) {
|
|
35
|
+
const skillsDir = path.join(os.homedir(), '.cursor', 'skills');
|
|
36
|
+
let installed = 0;
|
|
37
|
+
for (const skill of skills) {
|
|
38
|
+
try {
|
|
39
|
+
const skillDir = path.join(skillsDir, skill.name);
|
|
40
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
41
|
+
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skill.content, 'utf-8');
|
|
42
|
+
installed++;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Skip on error
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return installed;
|
|
49
|
+
}
|
|
50
|
+
async function unconfigure() {
|
|
51
|
+
const mcpPath = path.join(os.homedir(), '.cursor', 'mcp.json');
|
|
52
|
+
try {
|
|
53
|
+
const existing = await readJsonFile(mcpPath);
|
|
54
|
+
if (existing?.mcpServers?.gitnexus) {
|
|
55
|
+
delete existing.mcpServers.gitnexus;
|
|
56
|
+
await writeJsonFile(mcpPath, existing);
|
|
57
|
+
}
|
|
58
|
+
return { success: true, message: 'MCP removed from ~/.cursor/mcp.json' };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return { success: false, message: `Failed: ${err.message}` };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async function removeSkills() {
|
|
65
|
+
const skillsDir = path.join(os.homedir(), '.cursor', 'skills');
|
|
66
|
+
let removed = 0;
|
|
67
|
+
for (const name of HUB_SKILLS) {
|
|
68
|
+
try {
|
|
69
|
+
await fs.rm(path.join(skillsDir, name), { recursive: true, force: true });
|
|
70
|
+
removed++;
|
|
71
|
+
}
|
|
72
|
+
catch { /* already gone */ }
|
|
73
|
+
}
|
|
74
|
+
return removed;
|
|
75
|
+
}
|
|
76
|
+
export const cursorEditor = {
|
|
77
|
+
id: 'cursor',
|
|
78
|
+
name: 'Cursor',
|
|
79
|
+
configure,
|
|
80
|
+
unconfigure,
|
|
81
|
+
installSkills,
|
|
82
|
+
removeSkills,
|
|
83
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor Auto-Detection
|
|
3
|
+
*
|
|
4
|
+
* Probes well-known directories to detect which editors are installed.
|
|
5
|
+
*/
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { dirExists } from '../utils.js';
|
|
9
|
+
const PROBES = [
|
|
10
|
+
{
|
|
11
|
+
id: 'cursor',
|
|
12
|
+
name: 'Cursor',
|
|
13
|
+
dirs: [path.join(os.homedir(), '.cursor')],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: 'claude-code',
|
|
17
|
+
name: 'Claude Code',
|
|
18
|
+
dirs: [path.join(os.homedir(), '.claude')],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: 'windsurf',
|
|
22
|
+
name: 'Windsurf',
|
|
23
|
+
dirs: [path.join(os.homedir(), '.codeium', 'windsurf')],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'opencode',
|
|
27
|
+
name: 'OpenCode',
|
|
28
|
+
dirs: [path.join(os.homedir(), '.config', 'opencode')],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
export async function detectInstalledEditors() {
|
|
32
|
+
const found = [];
|
|
33
|
+
for (const probe of PROBES) {
|
|
34
|
+
for (const dir of probe.dirs) {
|
|
35
|
+
if (await dirExists(dir)) {
|
|
36
|
+
found.push(probe.id);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return found;
|
|
42
|
+
}
|