@aigencydev/cli 0.3.9 → 0.5.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/README.md +240 -168
- package/dist/agent/chunker.js +18 -4
- package/dist/agent/stream-guards.js +23 -0
- package/dist/cli.js +9 -0
- package/dist/commands/chat.js +59 -2
- package/dist/commands/mcp-server.js +4 -0
- package/dist/config/defaults.js +14 -0
- package/dist/mcp/client-manager.js +156 -0
- package/dist/mcp/server.js +46 -0
- package/dist/mcp/types.js +1 -0
- package/dist/tools/check-dependencies.js +149 -0
- package/dist/tools/components.js +76 -0
- package/dist/tools/docs-lookup.js +125 -0
- package/dist/tools/metadata.js +103 -0
- package/dist/tools/registry.js +9 -0
- package/dist/tools/write-file.js +16 -0
- package/package.json +2 -1
package/dist/commands/chat.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ensureProjectDataDirs } from "../config/bootstrap.js";
|
|
|
10
10
|
import { fetchMe } from "../api-client/auth.js";
|
|
11
11
|
import { App } from "../ui/App.js";
|
|
12
12
|
import { log } from "../utils/logger.js";
|
|
13
|
+
import { findLatestSession, loadChatHistoryFromSession, } from "../agent/session-store.js";
|
|
13
14
|
async function findProjectRoot(cwd) {
|
|
14
15
|
const markers = [
|
|
15
16
|
".git",
|
|
@@ -73,15 +74,71 @@ export async function runChatCommand(options = {}) {
|
|
|
73
74
|
const priorInsights = [instructions.combinedText, memory.summary]
|
|
74
75
|
.filter((s) => s && s.trim())
|
|
75
76
|
.join("\n\n---\n\n");
|
|
77
|
+
let restoredHistory = [];
|
|
78
|
+
let restoredSessionId;
|
|
79
|
+
let continuePrompt;
|
|
80
|
+
if (options.continueLastSession || options.resumeSessionId) {
|
|
81
|
+
try {
|
|
82
|
+
let targetSessionId;
|
|
83
|
+
if (options.resumeSessionId) {
|
|
84
|
+
targetSessionId = options.resumeSessionId;
|
|
85
|
+
log.info(`Session restore: ${targetSessionId}`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const latest = await findLatestSession(projectRoot);
|
|
89
|
+
if (latest) {
|
|
90
|
+
targetSessionId = latest.sessionId;
|
|
91
|
+
log.info(`Son session bulundu: ${targetSessionId}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (targetSessionId) {
|
|
95
|
+
const history = await loadChatHistoryFromSession(targetSessionId, projectRoot);
|
|
96
|
+
if (history.length > 0) {
|
|
97
|
+
restoredHistory = history;
|
|
98
|
+
restoredSessionId = targetSessionId;
|
|
99
|
+
if (!options.prompt) {
|
|
100
|
+
continuePrompt =
|
|
101
|
+
"Kaldığım yerden devam ediyorum. Önce read_tasks ile mevcut plana bak, " +
|
|
102
|
+
"pending ve in_progress task'leri sıraya göre tamamla. " +
|
|
103
|
+
"Tüm task'lar completed olana kadar durma.";
|
|
104
|
+
}
|
|
105
|
+
log.info(`${history.length} mesaj restore edildi`);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
log.warn("Session bulundu ama boş — yeni oturum başlatılıyor");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
log.warn("Devam edilecek session bulunamadı — yeni oturum başlatılıyor");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
log.warn(`Session restore hatası: ${err.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
let finalInsights = priorInsights;
|
|
120
|
+
if (restoredHistory.length > 0) {
|
|
121
|
+
const historyBlock = restoredHistory
|
|
122
|
+
.map((m) => `[${m.role === "user" ? "Kullanıcı" : "Asistan"}]: ${m.content.slice(0, 500)}`)
|
|
123
|
+
.slice(-20)
|
|
124
|
+
.join("\n\n");
|
|
125
|
+
finalInsights = [
|
|
126
|
+
priorInsights,
|
|
127
|
+
`## ÖNCEKİ OTURUM (${restoredSessionId || "bilinmiyor"})\nAşağıda önceki oturumdan devam edilen konuşma özeti var:\n\n${historyBlock}`,
|
|
128
|
+
]
|
|
129
|
+
.filter((s) => s && s.trim())
|
|
130
|
+
.join("\n\n---\n\n");
|
|
131
|
+
log.info(`Session history priorInsights'a enjekte edildi (${restoredHistory.length} mesaj)`);
|
|
132
|
+
}
|
|
76
133
|
const { waitUntilExit } = render(React.createElement(App, {
|
|
77
134
|
session,
|
|
78
135
|
initialMode: mode,
|
|
79
136
|
initialModel: model,
|
|
80
137
|
cwd,
|
|
81
138
|
projectRoot,
|
|
82
|
-
initialPrompt: options.prompt,
|
|
139
|
+
initialPrompt: continuePrompt || options.prompt,
|
|
83
140
|
settings,
|
|
84
|
-
memorySummary:
|
|
141
|
+
memorySummary: finalInsights,
|
|
85
142
|
}), {
|
|
86
143
|
exitOnCtrlC: false,
|
|
87
144
|
});
|
package/dist/config/defaults.js
CHANGED
|
@@ -39,5 +39,19 @@ export const SettingsSchema = z.object({
|
|
|
39
39
|
theme: z.enum(["dark", "auto"]).default("auto"),
|
|
40
40
|
})
|
|
41
41
|
.default({ theme: "auto" }),
|
|
42
|
+
mcp: z
|
|
43
|
+
.object({
|
|
44
|
+
servers: z
|
|
45
|
+
.array(z.object({
|
|
46
|
+
name: z.string().regex(/^[a-z0-9_-]+$/),
|
|
47
|
+
command: z.string(),
|
|
48
|
+
args: z.array(z.string()).default([]),
|
|
49
|
+
env: z.record(z.string()).default({}),
|
|
50
|
+
autoStart: z.boolean().default(true),
|
|
51
|
+
enabled: z.boolean().default(true),
|
|
52
|
+
}))
|
|
53
|
+
.default([]),
|
|
54
|
+
})
|
|
55
|
+
.default({ servers: [] }),
|
|
42
56
|
});
|
|
43
57
|
export const DEFAULT_SETTINGS = SettingsSchema.parse({});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { log } from "../utils/logger.js";
|
|
4
|
+
export class McpClientManager {
|
|
5
|
+
servers = new Map();
|
|
6
|
+
async startAll(configs) {
|
|
7
|
+
const eligible = configs.filter((c) => c.enabled && c.autoStart);
|
|
8
|
+
if (eligible.length === 0)
|
|
9
|
+
return;
|
|
10
|
+
log.info(`MCP: ${eligible.length} server başlatılıyor...`);
|
|
11
|
+
const results = await Promise.allSettled(eligible.map((c) => this.startServer(c)));
|
|
12
|
+
let ready = 0;
|
|
13
|
+
for (const r of results) {
|
|
14
|
+
if (r.status === "fulfilled")
|
|
15
|
+
ready++;
|
|
16
|
+
}
|
|
17
|
+
log.info(`MCP: ${ready}/${eligible.length} server hazır`);
|
|
18
|
+
}
|
|
19
|
+
async startServer(config) {
|
|
20
|
+
if (this.servers.has(config.name)) {
|
|
21
|
+
log.warn(`MCP: ${config.name} zaten çalışıyor, atlanıyor`);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const instance = {
|
|
25
|
+
name: config.name,
|
|
26
|
+
config,
|
|
27
|
+
client: null,
|
|
28
|
+
transport: null,
|
|
29
|
+
tools: [],
|
|
30
|
+
status: "starting",
|
|
31
|
+
};
|
|
32
|
+
this.servers.set(config.name, instance);
|
|
33
|
+
try {
|
|
34
|
+
const transport = new StdioClientTransport({
|
|
35
|
+
command: config.command,
|
|
36
|
+
args: config.args,
|
|
37
|
+
env: { ...process.env, ...config.env },
|
|
38
|
+
});
|
|
39
|
+
const client = new Client({ name: "aigency-cli", version: "1.0.0" }, { capabilities: {} });
|
|
40
|
+
instance.transport = transport;
|
|
41
|
+
instance.client = client;
|
|
42
|
+
await client.connect(transport);
|
|
43
|
+
const toolsResult = await client.listTools();
|
|
44
|
+
instance.tools = (toolsResult.tools || []).map((t) => ({
|
|
45
|
+
qualifiedName: `mcp_${config.name}_${t.name}`,
|
|
46
|
+
originalName: t.name,
|
|
47
|
+
serverName: config.name,
|
|
48
|
+
description: t.description || "",
|
|
49
|
+
inputSchema: t.inputSchema || { type: "object", properties: {} },
|
|
50
|
+
}));
|
|
51
|
+
instance.status = "ready";
|
|
52
|
+
log.info(`MCP: ${config.name} hazır (${instance.tools.length} tool)`);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
instance.status = "error";
|
|
56
|
+
instance.error = err.message;
|
|
57
|
+
log.error(`MCP: ${config.name} başlatılamadı: ${instance.error}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async stopAll() {
|
|
61
|
+
const names = [...this.servers.keys()];
|
|
62
|
+
await Promise.allSettled(names.map((n) => this.stopServer(n)));
|
|
63
|
+
this.servers.clear();
|
|
64
|
+
}
|
|
65
|
+
async stopServer(name) {
|
|
66
|
+
const instance = this.servers.get(name);
|
|
67
|
+
if (!instance)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
await instance.client?.close();
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
}
|
|
74
|
+
instance.status = "stopped";
|
|
75
|
+
this.servers.delete(name);
|
|
76
|
+
log.debug(`MCP: ${name} durduruldu`);
|
|
77
|
+
}
|
|
78
|
+
getAllTools() {
|
|
79
|
+
const tools = [];
|
|
80
|
+
for (const instance of this.servers.values()) {
|
|
81
|
+
if (instance.status === "ready") {
|
|
82
|
+
tools.push(...instance.tools);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return tools;
|
|
86
|
+
}
|
|
87
|
+
toLlmTools() {
|
|
88
|
+
return this.getAllTools().map((t) => ({
|
|
89
|
+
type: "function",
|
|
90
|
+
function: {
|
|
91
|
+
name: t.qualifiedName,
|
|
92
|
+
description: `[MCP: ${t.serverName}] ${t.description}`,
|
|
93
|
+
parameters: t.inputSchema,
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
async callTool(qualifiedName, params) {
|
|
98
|
+
const start = Date.now();
|
|
99
|
+
const match = qualifiedName.match(/^mcp_([^_]+)_(.+)$/);
|
|
100
|
+
if (!match) {
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
output: "",
|
|
104
|
+
error: `Geçersiz MCP tool adı: ${qualifiedName}`,
|
|
105
|
+
duration: Date.now() - start,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const serverName = match[1];
|
|
109
|
+
const toolName = match[2];
|
|
110
|
+
const instance = this.servers.get(serverName);
|
|
111
|
+
if (!instance || instance.status !== "ready") {
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
output: "",
|
|
115
|
+
error: `MCP server '${serverName}' hazır değil`,
|
|
116
|
+
duration: Date.now() - start,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const result = await instance.client.callTool({
|
|
121
|
+
name: toolName,
|
|
122
|
+
arguments: params,
|
|
123
|
+
});
|
|
124
|
+
const textContent = result.content
|
|
125
|
+
?.filter((c) => c.type === "text")
|
|
126
|
+
?.map((c) => c.text)
|
|
127
|
+
?.join("\n") || "";
|
|
128
|
+
return {
|
|
129
|
+
success: !result.isError,
|
|
130
|
+
output: textContent,
|
|
131
|
+
error: result.isError ? textContent.slice(0, 200) : undefined,
|
|
132
|
+
metadata: { mcpServer: serverName, mcpTool: toolName },
|
|
133
|
+
duration: Date.now() - start,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return {
|
|
138
|
+
success: false,
|
|
139
|
+
output: "",
|
|
140
|
+
error: `MCP çağrı hatası: ${err.message.slice(0, 150)}`,
|
|
141
|
+
duration: Date.now() - start,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
getStatuses() {
|
|
146
|
+
return [...this.servers.values()].map((s) => ({
|
|
147
|
+
name: s.name,
|
|
148
|
+
status: s.status,
|
|
149
|
+
toolCount: s.tools.length,
|
|
150
|
+
error: s.error,
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
hasTools() {
|
|
154
|
+
return this.getAllTools().length > 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { ListToolsRequestSchema, CallToolRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { dispatchLocalTool } from "../tools/registry.js";
|
|
5
|
+
import { EXPOSED_TOOL_SCHEMAS } from "../tools/metadata.js";
|
|
6
|
+
const CLI_VERSION = "0.4.0";
|
|
7
|
+
export async function startMcpServer() {
|
|
8
|
+
const server = new Server({ name: "aigency-cli", version: CLI_VERSION }, { capabilities: { tools: {} } });
|
|
9
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
10
|
+
tools: EXPOSED_TOOL_SCHEMAS.map((t) => ({
|
|
11
|
+
name: t.name,
|
|
12
|
+
description: t.description,
|
|
13
|
+
inputSchema: t.parameters,
|
|
14
|
+
})),
|
|
15
|
+
}));
|
|
16
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
17
|
+
const { name, arguments: args } = request.params;
|
|
18
|
+
const isExposed = EXPOSED_TOOL_SCHEMAS.some((t) => t.name === name);
|
|
19
|
+
if (!isExposed) {
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: `Tool '${name}' MCP üzerinden erişilemez` }],
|
|
22
|
+
isError: true,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const ctx = {
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
projectRoot: process.cwd(),
|
|
28
|
+
mode: "accept_edits",
|
|
29
|
+
};
|
|
30
|
+
try {
|
|
31
|
+
const result = await dispatchLocalTool(name, (args || {}), ctx);
|
|
32
|
+
return {
|
|
33
|
+
content: [{ type: "text", text: result.output || result.error || "(boş)" }],
|
|
34
|
+
isError: !result.success,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: `Hata: ${err.message.slice(0, 200)}` }],
|
|
40
|
+
isError: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const transport = new StdioServerTransport();
|
|
45
|
+
await server.connect(transport);
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
const SYSTEM_DEPS = [
|
|
6
|
+
{ name: "node", command: "node", required: true, versionFlag: "--version" },
|
|
7
|
+
{ name: "npm", command: "npm", required: true, versionFlag: "--version" },
|
|
8
|
+
{ name: "git", command: "git", required: false, versionFlag: "--version" },
|
|
9
|
+
{ name: "python3", command: os.platform() === "win32" ? "python" : "python3", required: false, versionFlag: "--version" },
|
|
10
|
+
{ name: "docker", command: "docker", required: false, versionFlag: "--version" },
|
|
11
|
+
];
|
|
12
|
+
function getInstallHint(name, platform) {
|
|
13
|
+
const hints = {
|
|
14
|
+
node: {
|
|
15
|
+
win32: "choco install nodejs-lts veya https://nodejs.org",
|
|
16
|
+
darwin: "brew install node",
|
|
17
|
+
linux: "sudo apt install nodejs npm veya https://nodejs.org",
|
|
18
|
+
},
|
|
19
|
+
npm: {
|
|
20
|
+
win32: "Node.js ile birlikte gelir — node kur",
|
|
21
|
+
darwin: "Node.js ile birlikte gelir — brew install node",
|
|
22
|
+
linux: "Node.js ile birlikte gelir — sudo apt install nodejs npm",
|
|
23
|
+
},
|
|
24
|
+
git: {
|
|
25
|
+
win32: "choco install git veya https://git-scm.com",
|
|
26
|
+
darwin: "xcode-select --install veya brew install git",
|
|
27
|
+
linux: "sudo apt install git",
|
|
28
|
+
},
|
|
29
|
+
python3: {
|
|
30
|
+
win32: "choco install python veya https://python.org",
|
|
31
|
+
darwin: "brew install python3",
|
|
32
|
+
linux: "sudo apt install python3",
|
|
33
|
+
},
|
|
34
|
+
docker: {
|
|
35
|
+
win32: "https://docs.docker.com/desktop/install/windows-install/",
|
|
36
|
+
darwin: "brew install --cask docker",
|
|
37
|
+
linux: "sudo apt install docker.io docker-compose",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
return hints[name]?.[platform] || `${name} kurulumu için dökümantasyona bakın`;
|
|
41
|
+
}
|
|
42
|
+
function checkCommand(command, versionFlag) {
|
|
43
|
+
try {
|
|
44
|
+
const result = execSync(`${command} ${versionFlag}`, {
|
|
45
|
+
timeout: 5000,
|
|
46
|
+
encoding: "utf8",
|
|
47
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
48
|
+
});
|
|
49
|
+
return result.trim().split("\n")[0] || "kurulu";
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export const checkDependenciesHandler = async (params, ctx) => {
|
|
56
|
+
const start = Date.now();
|
|
57
|
+
const projectPath = (params.path || params.projectPath || ".");
|
|
58
|
+
const resolved = path.resolve(ctx.cwd, projectPath);
|
|
59
|
+
const platform = os.platform();
|
|
60
|
+
const results = [];
|
|
61
|
+
const missing = [];
|
|
62
|
+
const warnings = [];
|
|
63
|
+
results.push("## Sistem Bağımlılıkları\n");
|
|
64
|
+
results.push(`Platform: ${platform} (${os.arch()})`);
|
|
65
|
+
results.push(`Node sürümü gereksinimi: >= 18.18.0\n`);
|
|
66
|
+
for (const dep of SYSTEM_DEPS) {
|
|
67
|
+
const version = checkCommand(dep.command, dep.versionFlag);
|
|
68
|
+
if (version) {
|
|
69
|
+
results.push(`✓ ${dep.name}: ${version}`);
|
|
70
|
+
if (dep.name === "node") {
|
|
71
|
+
const match = version.match(/v?(\d+)\.(\d+)/);
|
|
72
|
+
if (match) {
|
|
73
|
+
const major = parseInt(match[1], 10);
|
|
74
|
+
const minor = parseInt(match[2], 10);
|
|
75
|
+
if (major < 18 || (major === 18 && minor < 18)) {
|
|
76
|
+
warnings.push(`Node.js ${version} çok eski — 18.18+ gerekli`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
const status = dep.required ? "✗ (ZORUNLU)" : "✗ (opsiyonel)";
|
|
83
|
+
results.push(`${status} ${dep.name}: kurulu değil`);
|
|
84
|
+
results.push(` → ${getInstallHint(dep.name, platform)}`);
|
|
85
|
+
if (dep.required)
|
|
86
|
+
missing.push(dep.name);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
results.push("\n## Proje Bağımlılıkları\n");
|
|
90
|
+
const pkgPath = path.join(resolved, "package.json");
|
|
91
|
+
try {
|
|
92
|
+
const pkgContent = await fs.readFile(pkgPath, "utf8");
|
|
93
|
+
const pkg = JSON.parse(pkgContent);
|
|
94
|
+
results.push(`Proje: ${pkg.name || "(isimsiz)"} v${pkg.version || "0.0.0"}`);
|
|
95
|
+
const requiredScripts = ["dev", "build"];
|
|
96
|
+
const scripts = pkg.scripts || {};
|
|
97
|
+
for (const s of requiredScripts) {
|
|
98
|
+
if (scripts[s]) {
|
|
99
|
+
results.push(`✓ scripts.${s}: ${scripts[s]}`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
warnings.push(`scripts.${s} eksik`);
|
|
103
|
+
results.push(`✗ scripts.${s}: tanımlı değil`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await fs.access(path.join(resolved, "node_modules"));
|
|
108
|
+
results.push("✓ node_modules: mevcut");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
missing.push("node_modules");
|
|
112
|
+
results.push("✗ node_modules: eksik → npm install çalıştır");
|
|
113
|
+
}
|
|
114
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
115
|
+
const keyDeps = ["next", "react", "typescript", "tailwindcss"];
|
|
116
|
+
for (const dep of keyDeps) {
|
|
117
|
+
if (allDeps[dep]) {
|
|
118
|
+
results.push(`✓ ${dep}: ${allDeps[dep]}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
results.push("package.json bulunamadı — henüz proje oluşturulmamış olabilir");
|
|
124
|
+
}
|
|
125
|
+
results.push("\n## Özet\n");
|
|
126
|
+
if (missing.length === 0 && warnings.length === 0) {
|
|
127
|
+
results.push("✓ Tüm bağımlılıklar hazır — projeye başlanabilir.");
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
if (missing.length > 0) {
|
|
131
|
+
results.push(`✗ Eksik (zorunlu): ${missing.join(", ")}`);
|
|
132
|
+
}
|
|
133
|
+
if (warnings.length > 0) {
|
|
134
|
+
results.push(`⚠ Uyarılar: ${warnings.join("; ")}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
success: missing.length === 0,
|
|
139
|
+
output: results.join("\n"),
|
|
140
|
+
metadata: {
|
|
141
|
+
platform,
|
|
142
|
+
arch: os.arch(),
|
|
143
|
+
missing,
|
|
144
|
+
warnings,
|
|
145
|
+
hasMissing: missing.length > 0,
|
|
146
|
+
},
|
|
147
|
+
duration: Date.now() - start,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const ALL_SHADCN_COMPONENTS = [
|
|
4
|
+
"accordion", "alert", "alert-dialog", "aspect-ratio", "avatar",
|
|
5
|
+
"badge", "breadcrumb", "button", "calendar", "card", "carousel",
|
|
6
|
+
"chart", "checkbox", "collapsible", "combobox", "command",
|
|
7
|
+
"context-menu", "data-table", "date-picker", "dialog", "drawer",
|
|
8
|
+
"dropdown-menu", "form", "hover-card", "input", "input-otp",
|
|
9
|
+
"label", "menubar", "navigation-menu", "pagination", "popover",
|
|
10
|
+
"progress", "radio-group", "resizable", "scroll-area", "select",
|
|
11
|
+
"separator", "sheet", "sidebar", "skeleton", "slider", "sonner",
|
|
12
|
+
"switch", "table", "tabs", "textarea", "toast", "toggle",
|
|
13
|
+
"toggle-group", "tooltip",
|
|
14
|
+
];
|
|
15
|
+
async function scanComponents(projectRoot) {
|
|
16
|
+
const componentsDir = path.join(projectRoot, "components", "ui");
|
|
17
|
+
const configPath = path.join(projectRoot, "components.json");
|
|
18
|
+
let hasConfig = false;
|
|
19
|
+
try {
|
|
20
|
+
await fs.access(configPath);
|
|
21
|
+
hasConfig = true;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
}
|
|
25
|
+
const installed = [];
|
|
26
|
+
try {
|
|
27
|
+
const files = await fs.readdir(componentsDir);
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
const name = file.replace(/\.(tsx|ts|jsx|js)$/, "");
|
|
30
|
+
if (name && !name.startsWith(".")) {
|
|
31
|
+
installed.push(name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
}
|
|
37
|
+
const installedSet = new Set(installed);
|
|
38
|
+
const available = ALL_SHADCN_COMPONENTS.filter((c) => !installedSet.has(c));
|
|
39
|
+
return { installed, available, hasConfig, componentsDir };
|
|
40
|
+
}
|
|
41
|
+
export const listComponentsHandler = async (params, ctx) => {
|
|
42
|
+
const start = Date.now();
|
|
43
|
+
const projectPath = (params.path || params.projectPath || ".");
|
|
44
|
+
const resolved = path.resolve(ctx.cwd, projectPath);
|
|
45
|
+
const result = await scanComponents(resolved);
|
|
46
|
+
const lines = [];
|
|
47
|
+
lines.push("## shadcn/ui Component Durumu\n");
|
|
48
|
+
if (!result.hasConfig) {
|
|
49
|
+
lines.push("⚠ components.json bulunamadı — shadcn/ui henüz init edilmemiş olabilir.");
|
|
50
|
+
lines.push("Kurulum: `npx --yes shadcn@latest init --yes --defaults`\n");
|
|
51
|
+
}
|
|
52
|
+
if (result.installed.length > 0) {
|
|
53
|
+
lines.push(`### Kurulu (${result.installed.length})`);
|
|
54
|
+
lines.push(result.installed.map((c) => `✓ ${c}`).join("\n"));
|
|
55
|
+
lines.push("");
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
lines.push("### Kurulu: (yok)\n");
|
|
59
|
+
}
|
|
60
|
+
if (result.available.length > 0) {
|
|
61
|
+
lines.push(`### Kurulabilir (${result.available.length})`);
|
|
62
|
+
lines.push(result.available.map((c) => ` ${c}`).join("\n"));
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push("Kurulum: `npx --yes shadcn@latest add <component> -y`");
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
success: true,
|
|
68
|
+
output: lines.join("\n"),
|
|
69
|
+
metadata: {
|
|
70
|
+
installed: result.installed,
|
|
71
|
+
available: result.available,
|
|
72
|
+
hasConfig: result.hasConfig,
|
|
73
|
+
},
|
|
74
|
+
duration: Date.now() - start,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { log } from "../utils/logger.js";
|
|
2
|
+
const DOCS_URLS = {
|
|
3
|
+
"next.js": "https://nextjs.org/docs",
|
|
4
|
+
nextjs: "https://nextjs.org/docs",
|
|
5
|
+
next: "https://nextjs.org/docs",
|
|
6
|
+
react: "https://react.dev/reference",
|
|
7
|
+
tailwind: "https://tailwindcss.com/docs",
|
|
8
|
+
tailwindcss: "https://tailwindcss.com/docs",
|
|
9
|
+
prisma: "https://www.prisma.io/docs",
|
|
10
|
+
shadcn: "https://ui.shadcn.com/docs",
|
|
11
|
+
"shadcn/ui": "https://ui.shadcn.com/docs",
|
|
12
|
+
typescript: "https://www.typescriptlang.org/docs/handbook",
|
|
13
|
+
ts: "https://www.typescriptlang.org/docs/handbook",
|
|
14
|
+
node: "https://nodejs.org/docs/latest/api",
|
|
15
|
+
nodejs: "https://nodejs.org/docs/latest/api",
|
|
16
|
+
vite: "https://vitejs.dev/guide",
|
|
17
|
+
zod: "https://zod.dev",
|
|
18
|
+
"framer-motion": "https://www.framer.com/motion",
|
|
19
|
+
framer: "https://www.framer.com/motion",
|
|
20
|
+
express: "https://expressjs.com/en/5x/api.html",
|
|
21
|
+
"ink": "https://github.com/vadimdemedes/ink",
|
|
22
|
+
};
|
|
23
|
+
export function listKnownFrameworks() {
|
|
24
|
+
return [...new Set(Object.values(DOCS_URLS))].map((url) => {
|
|
25
|
+
const match = Object.entries(DOCS_URLS).find(([, v]) => v === url);
|
|
26
|
+
return match?.[0] || url;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export function buildDocsUrl(framework, topic) {
|
|
30
|
+
const key = framework.toLowerCase().trim();
|
|
31
|
+
const base = DOCS_URLS[key];
|
|
32
|
+
if (!base)
|
|
33
|
+
return null;
|
|
34
|
+
if (!topic)
|
|
35
|
+
return base;
|
|
36
|
+
const slug = topic
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.trim()
|
|
39
|
+
.replace(/\s+/g, "-")
|
|
40
|
+
.replace(/[^a-z0-9\-/]/g, "");
|
|
41
|
+
return `${base}/${slug}`;
|
|
42
|
+
}
|
|
43
|
+
function extractText(html) {
|
|
44
|
+
return html
|
|
45
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
46
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
47
|
+
.replace(/<nav[\s\S]*?<\/nav>/gi, "")
|
|
48
|
+
.replace(/<footer[\s\S]*?<\/footer>/gi, "")
|
|
49
|
+
.replace(/<[^>]+>/g, " ")
|
|
50
|
+
.replace(/&/g, "&")
|
|
51
|
+
.replace(/</g, "<")
|
|
52
|
+
.replace(/>/g, ">")
|
|
53
|
+
.replace(/"/g, '"')
|
|
54
|
+
.replace(/'/g, "'")
|
|
55
|
+
.replace(/ /g, " ")
|
|
56
|
+
.replace(/\s+/g, " ")
|
|
57
|
+
.trim();
|
|
58
|
+
}
|
|
59
|
+
async function fetchDocsContent(url, maxChars = 15_000) {
|
|
60
|
+
const controller = new AbortController();
|
|
61
|
+
const timeout = setTimeout(() => controller.abort(), 15_000);
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(url, {
|
|
64
|
+
headers: {
|
|
65
|
+
"User-Agent": "AIGENCY-CLI/1.0 (docs-lookup)",
|
|
66
|
+
Accept: "text/html,application/xhtml+xml",
|
|
67
|
+
},
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
});
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
return `HTTP ${res.status} — ${url} erişilemedi`;
|
|
72
|
+
}
|
|
73
|
+
const html = await res.text();
|
|
74
|
+
const text = extractText(html);
|
|
75
|
+
return text.slice(0, maxChars);
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
if (err.name === "AbortError") {
|
|
79
|
+
return `Zaman aşımı — ${url} 15 saniyede yanıt vermedi`;
|
|
80
|
+
}
|
|
81
|
+
return `Hata: ${err.message.slice(0, 100)}`;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export const docsLookupHandler = async (params) => {
|
|
88
|
+
const start = Date.now();
|
|
89
|
+
const framework = String(params.framework || "").trim();
|
|
90
|
+
const topic = params.topic ? String(params.topic).trim() : undefined;
|
|
91
|
+
const directUrl = params.url ? String(params.url).trim() : undefined;
|
|
92
|
+
if (!framework && !directUrl) {
|
|
93
|
+
return {
|
|
94
|
+
success: false,
|
|
95
|
+
output: "",
|
|
96
|
+
error: "framework veya url parametresi gerekli",
|
|
97
|
+
duration: Date.now() - start,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
let url;
|
|
101
|
+
if (directUrl) {
|
|
102
|
+
url = directUrl;
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
const built = buildDocsUrl(framework, topic);
|
|
106
|
+
if (!built) {
|
|
107
|
+
const known = Object.keys(DOCS_URLS).slice(0, 10).join(", ");
|
|
108
|
+
return {
|
|
109
|
+
success: false,
|
|
110
|
+
output: "",
|
|
111
|
+
error: `'${framework}' için docs URL bulunamadı. Bilinenler: ${known}`,
|
|
112
|
+
duration: Date.now() - start,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
url = built;
|
|
116
|
+
}
|
|
117
|
+
log.debug(`docs_lookup: ${url}`);
|
|
118
|
+
const content = await fetchDocsContent(url);
|
|
119
|
+
return {
|
|
120
|
+
success: true,
|
|
121
|
+
output: `## Dokümantasyon: ${framework || url}\nKaynak: ${url}\n\n${content}`,
|
|
122
|
+
metadata: { url, framework, topic },
|
|
123
|
+
duration: Date.now() - start,
|
|
124
|
+
};
|
|
125
|
+
};
|