godspeed-agent 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 +99 -0
- package/bin/godspeed +2 -0
- package/package.json +32 -0
- package/src/cli.ts +25 -0
- package/src/commands/agent.ts +81 -0
- package/src/commands/chat.ts +65 -0
- package/src/commands/models.ts +49 -0
- package/src/commands/providers.ts +83 -0
- package/src/commands/tools.ts +40 -0
- package/src/commands/tui.tsx +24 -0
- package/src/lib/agent.ts +183 -0
- package/src/lib/agentMemory.ts +69 -0
- package/src/lib/config.ts +55 -0
- package/src/lib/history.ts +50 -0
- package/src/lib/llm.ts +86 -0
- package/src/lib/providers.ts +45 -0
- package/src/providers/gemini.ts +91 -0
- package/src/providers/index.ts +24 -0
- package/src/providers/ollama.ts +84 -0
- package/src/providers/openrouter.ts +97 -0
- package/src/providers/types.ts +18 -0
- package/src/tools/labels.ts +38 -0
- package/src/tools/metadata.ts +102 -0
- package/src/tools/prompt.ts +37 -0
- package/src/tools/registry.ts +63 -0
- package/src/tools/tools.ts +179 -0
- package/src/tools/workspace.ts +22 -0
- package/src/tui/App.tsx +377 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { LLMProvider } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const openrouterProvider: LLMProvider = {
|
|
4
|
+
name: "openrouter",
|
|
5
|
+
|
|
6
|
+
async generate({ apiKey, model, messages }) {
|
|
7
|
+
const res = await fetch(
|
|
8
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
9
|
+
{
|
|
10
|
+
method: "POST",
|
|
11
|
+
headers: {
|
|
12
|
+
Authorization: `Bearer ${apiKey}`,
|
|
13
|
+
"Content-Type": "application/json",
|
|
14
|
+
},
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
model,
|
|
17
|
+
messages: messages.map((m) => ({
|
|
18
|
+
role: m.role === "model" ? "assistant" : "user",
|
|
19
|
+
content: m.text,
|
|
20
|
+
})),
|
|
21
|
+
}),
|
|
22
|
+
},
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (!res.ok) {
|
|
26
|
+
throw new Error(await res.text());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const data: any = await res.json();
|
|
30
|
+
|
|
31
|
+
return data.choices?.[0]?.message?.content ?? "";
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
async stream({ apiKey, model, prompt, onToken }) {
|
|
35
|
+
const res = await fetch(
|
|
36
|
+
"https://openrouter.ai/api/v1/chat/completions",
|
|
37
|
+
{
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: {
|
|
40
|
+
Authorization: `Bearer ${apiKey}`,
|
|
41
|
+
"Content-Type": "application/json",
|
|
42
|
+
},
|
|
43
|
+
body: JSON.stringify({
|
|
44
|
+
model,
|
|
45
|
+
stream: true,
|
|
46
|
+
messages: [
|
|
47
|
+
{
|
|
48
|
+
role: "user",
|
|
49
|
+
content: prompt,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
}),
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (!res.ok || !res.body) {
|
|
57
|
+
throw new Error(await res.text());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const reader = res.body.getReader();
|
|
61
|
+
const decoder = new TextDecoder();
|
|
62
|
+
|
|
63
|
+
let buffer = "";
|
|
64
|
+
|
|
65
|
+
while (true) {
|
|
66
|
+
const { done, value } = await reader.read();
|
|
67
|
+
|
|
68
|
+
if (done) break;
|
|
69
|
+
|
|
70
|
+
buffer += decoder.decode(value, { stream: true });
|
|
71
|
+
|
|
72
|
+
const lines = buffer.split("\n");
|
|
73
|
+
buffer = lines.pop() ?? "";
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
|
|
78
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
79
|
+
|
|
80
|
+
const jsonText = trimmed.replace("data: ", "");
|
|
81
|
+
|
|
82
|
+
if (jsonText === "[DONE]") continue;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const data = JSON.parse(jsonText);
|
|
86
|
+
|
|
87
|
+
const token =
|
|
88
|
+
data.choices?.[0]?.delta?.content;
|
|
89
|
+
|
|
90
|
+
if (token) {
|
|
91
|
+
onToken(token);
|
|
92
|
+
}
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ChatMessage } from "../lib/llm.js";
|
|
2
|
+
|
|
3
|
+
export type LLMProvider = {
|
|
4
|
+
name: string;
|
|
5
|
+
|
|
6
|
+
generate: (params: {
|
|
7
|
+
apiKey: string;
|
|
8
|
+
model: string;
|
|
9
|
+
messages: ChatMessage[];
|
|
10
|
+
}) => Promise<string>;
|
|
11
|
+
|
|
12
|
+
stream?: (params: {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
model: string;
|
|
15
|
+
prompt: string;
|
|
16
|
+
onToken: (token: string) => void;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ToolAction } from "./registry.js";
|
|
2
|
+
|
|
3
|
+
export function getToolLabel(action: ToolAction) {
|
|
4
|
+
switch (action.action) {
|
|
5
|
+
case "WRITE_FILE":
|
|
6
|
+
return `WRITE_FILE ${action.path}`;
|
|
7
|
+
|
|
8
|
+
case "EDIT_FILE":
|
|
9
|
+
return `EDIT_FILE ${action.path}`;
|
|
10
|
+
|
|
11
|
+
case "DELETE_FILE":
|
|
12
|
+
return `DELETE_FILE ${action.path}`;
|
|
13
|
+
|
|
14
|
+
case "CREATE_DIRECTORY":
|
|
15
|
+
return `CREATE_DIRECTORY ${action.path}`;
|
|
16
|
+
|
|
17
|
+
case "MOVE_FILE":
|
|
18
|
+
return `MOVE_FILE ${action.from} -> ${action.to}`;
|
|
19
|
+
|
|
20
|
+
case "COPY_FILE":
|
|
21
|
+
return `COPY_FILE ${action.from} -> ${action.to}`;
|
|
22
|
+
|
|
23
|
+
case "RUN_COMMAND":
|
|
24
|
+
return `RUN_COMMAND ${action.command}`;
|
|
25
|
+
|
|
26
|
+
case "READ_FILE":
|
|
27
|
+
return `READ_FILE ${action.path}`;
|
|
28
|
+
|
|
29
|
+
case "LIST_FILES":
|
|
30
|
+
return `LIST_FILES ${action.path ?? "."}`;
|
|
31
|
+
|
|
32
|
+
case "GLOB_FILES":
|
|
33
|
+
return `GLOB_FILES ${action.pattern}`;
|
|
34
|
+
|
|
35
|
+
case "SEARCH_FILES":
|
|
36
|
+
return `SEARCH_FILES ${action.query}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export const TOOL_DEFINITIONS = [
|
|
2
|
+
{
|
|
3
|
+
name: "LIST_FILES",
|
|
4
|
+
description: "List files in a directory",
|
|
5
|
+
example: {
|
|
6
|
+
action: "LIST_FILES",
|
|
7
|
+
path: ".",
|
|
8
|
+
},
|
|
9
|
+
},
|
|
10
|
+
|
|
11
|
+
{
|
|
12
|
+
name: "READ_FILE",
|
|
13
|
+
description: "Read file contents",
|
|
14
|
+
example: {
|
|
15
|
+
action: "READ_FILE",
|
|
16
|
+
path: "package.json",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
{
|
|
21
|
+
name: "WRITE_FILE",
|
|
22
|
+
description: "Create or overwrite a file",
|
|
23
|
+
example: {
|
|
24
|
+
action: "WRITE_FILE",
|
|
25
|
+
path: "hello.txt",
|
|
26
|
+
content: "hello world",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
name: "RUN_COMMAND",
|
|
32
|
+
description: "Run safe terminal commands",
|
|
33
|
+
example: {
|
|
34
|
+
action: "RUN_COMMAND",
|
|
35
|
+
command: "bun test",
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
{
|
|
40
|
+
name: "GLOB_FILES",
|
|
41
|
+
description: "Find files by glob pattern",
|
|
42
|
+
example: {
|
|
43
|
+
action: "GLOB_FILES",
|
|
44
|
+
pattern: "src/**/*.ts",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
{
|
|
49
|
+
name: "SEARCH_FILES",
|
|
50
|
+
description: "Search text inside project files",
|
|
51
|
+
example: {
|
|
52
|
+
action: "SEARCH_FILES",
|
|
53
|
+
query: "askLLMWithMessages",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "EDIT_FILE",
|
|
58
|
+
description: "Edit a file by replacing exact text with new text",
|
|
59
|
+
example: {
|
|
60
|
+
action: "EDIT_FILE",
|
|
61
|
+
path: "src/index.ts",
|
|
62
|
+
find: "console.log('hello');",
|
|
63
|
+
replace: "console.log('hello world');",
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "DELETE_FILE",
|
|
68
|
+
description: "Delete a file inside the workspace",
|
|
69
|
+
example: {
|
|
70
|
+
action: "DELETE_FILE",
|
|
71
|
+
path: "hello.txt",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "CREATE_DIRECTORY",
|
|
76
|
+
description: "Create a directory inside the workspace",
|
|
77
|
+
example: {
|
|
78
|
+
action: "CREATE_DIRECTORY",
|
|
79
|
+
path: "src/components",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
{
|
|
84
|
+
name: "MOVE_FILE",
|
|
85
|
+
description: "Move or rename a file inside the workspace",
|
|
86
|
+
example: {
|
|
87
|
+
action: "MOVE_FILE",
|
|
88
|
+
from: "old.txt",
|
|
89
|
+
to: "new.txt",
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
{
|
|
94
|
+
name: "COPY_FILE",
|
|
95
|
+
description: "Copy a file inside the workspace",
|
|
96
|
+
example: {
|
|
97
|
+
action: "COPY_FILE",
|
|
98
|
+
from: "hello.txt",
|
|
99
|
+
to: "backup/hello.txt",
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { TOOL_DEFINITIONS } from "./metadata.js";
|
|
2
|
+
|
|
3
|
+
export function buildSystemPrompt() {
|
|
4
|
+
const tools = TOOL_DEFINITIONS.map(
|
|
5
|
+
(tool) =>
|
|
6
|
+
`
|
|
7
|
+
Tool: ${tool.name}
|
|
8
|
+
Description: ${tool.description}
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
${JSON.stringify(tool.example, null, 2)}
|
|
12
|
+
`
|
|
13
|
+
).join("\n");
|
|
14
|
+
|
|
15
|
+
return `
|
|
16
|
+
You are a coding agent.
|
|
17
|
+
|
|
18
|
+
You can ONLY respond with valid JSON.
|
|
19
|
+
|
|
20
|
+
Available tools:
|
|
21
|
+
|
|
22
|
+
${tools}
|
|
23
|
+
|
|
24
|
+
Final answer:
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
"action": "FINAL",
|
|
28
|
+
"answer": "your answer"
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
Rules:
|
|
32
|
+
- Return ONLY JSON.
|
|
33
|
+
- Never use markdown.
|
|
34
|
+
- Use tools when necessary.
|
|
35
|
+
- Use FINAL only when the task is complete.
|
|
36
|
+
`;
|
|
37
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listFiles,
|
|
3
|
+
readFile,
|
|
4
|
+
writeFile,
|
|
5
|
+
runCommand,
|
|
6
|
+
globFiles,
|
|
7
|
+
searchFiles,
|
|
8
|
+
editFile,
|
|
9
|
+
deleteFile,
|
|
10
|
+
createDirectory,
|
|
11
|
+
moveFile,
|
|
12
|
+
copyFile,
|
|
13
|
+
} from "./tools.js";
|
|
14
|
+
|
|
15
|
+
export type ToolAction =
|
|
16
|
+
| { action: "LIST_FILES"; path?: string }
|
|
17
|
+
| { action: "READ_FILE"; path: string }
|
|
18
|
+
| { action: "WRITE_FILE"; path: string; content: string }
|
|
19
|
+
| { action: "DELETE_FILE"; path: string }
|
|
20
|
+
| { action: "EDIT_FILE"; path: string; find: string; replace: string }
|
|
21
|
+
| { action: "CREATE_DIRECTORY"; path: string }
|
|
22
|
+
| { action: "MOVE_FILE"; from: string; to: string }
|
|
23
|
+
| { action: "COPY_FILE"; from: string; to: string }
|
|
24
|
+
| { action: "RUN_COMMAND"; command: string }
|
|
25
|
+
| { action: "GLOB_FILES"; pattern: string }
|
|
26
|
+
| { action: "SEARCH_FILES"; query: string };
|
|
27
|
+
|
|
28
|
+
export async function executeTool(action: ToolAction) {
|
|
29
|
+
switch (action.action) {
|
|
30
|
+
case "LIST_FILES":
|
|
31
|
+
return listFiles(action.path ?? ".");
|
|
32
|
+
|
|
33
|
+
case "READ_FILE":
|
|
34
|
+
return readFile(action.path);
|
|
35
|
+
|
|
36
|
+
case "WRITE_FILE":
|
|
37
|
+
return writeFile(action.path, action.content);
|
|
38
|
+
|
|
39
|
+
case "DELETE_FILE":
|
|
40
|
+
return deleteFile(action.path);
|
|
41
|
+
|
|
42
|
+
case "EDIT_FILE":
|
|
43
|
+
return editFile(action.path, action.find, action.replace);
|
|
44
|
+
|
|
45
|
+
case "CREATE_DIRECTORY":
|
|
46
|
+
return createDirectory(action.path);
|
|
47
|
+
|
|
48
|
+
case "MOVE_FILE":
|
|
49
|
+
return moveFile(action.from, action.to);
|
|
50
|
+
|
|
51
|
+
case "COPY_FILE":
|
|
52
|
+
return copyFile(action.from, action.to);
|
|
53
|
+
|
|
54
|
+
case "RUN_COMMAND":
|
|
55
|
+
return runCommand(action.command);
|
|
56
|
+
|
|
57
|
+
case "GLOB_FILES":
|
|
58
|
+
return await globFiles(action.pattern);
|
|
59
|
+
|
|
60
|
+
case "SEARCH_FILES":
|
|
61
|
+
return await searchFiles(action.query);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { getWorkspace, resolveWorkspacePath } from "./workspace.js";
|
|
5
|
+
import fg from "fast-glob";
|
|
6
|
+
|
|
7
|
+
export function listFiles(dir = ".") {
|
|
8
|
+
const fullPath = resolveWorkspacePath(dir);
|
|
9
|
+
return fs.readdirSync(fullPath).join("\n");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function readFile(filePath: string) {
|
|
13
|
+
const fullPath = resolveWorkspacePath(filePath);
|
|
14
|
+
return fs.readFileSync(fullPath, "utf-8");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function writeFile(filePath: string, content: string) {
|
|
18
|
+
const fullPath = resolveWorkspacePath(filePath);
|
|
19
|
+
|
|
20
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
21
|
+
fs.writeFileSync(fullPath, content, "utf-8");
|
|
22
|
+
|
|
23
|
+
return `File written: ${filePath}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function runCommand(command: string) {
|
|
27
|
+
const allowedCommands = [
|
|
28
|
+
"ls",
|
|
29
|
+
"pwd",
|
|
30
|
+
"cat",
|
|
31
|
+
"echo",
|
|
32
|
+
"mkdir",
|
|
33
|
+
"touch",
|
|
34
|
+
"bun",
|
|
35
|
+
"node",
|
|
36
|
+
"npm",
|
|
37
|
+
"git",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
const first = command.trim().split(/\s+/)[0];
|
|
42
|
+
|
|
43
|
+
if (!first) {
|
|
44
|
+
throw new Error("Empty command");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!allowedCommands.includes(first)) {
|
|
48
|
+
throw new Error(`Blocked command: ${first}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!allowedCommands.includes(first)) {
|
|
52
|
+
throw new Error(`Blocked command: ${first}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return execSync(command, {
|
|
56
|
+
cwd: getWorkspace(),
|
|
57
|
+
encoding: "utf-8",
|
|
58
|
+
timeout: 10_000,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export async function globFiles(pattern: string) {
|
|
62
|
+
const files = await fg(pattern, {
|
|
63
|
+
cwd: getWorkspace(),
|
|
64
|
+
onlyFiles: true,
|
|
65
|
+
dot: true,
|
|
66
|
+
ignore: ["node_modules/**", ".git/**", "dist/**"],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return files.join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function searchFiles(query: string) {
|
|
73
|
+
const files = await fg("**/*", {
|
|
74
|
+
cwd: getWorkspace(),
|
|
75
|
+
onlyFiles: true,
|
|
76
|
+
dot: true,
|
|
77
|
+
ignore: ["node_modules/**", ".git/**", "dist/**", "bun.lock"],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const matches: string[] = [];
|
|
81
|
+
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const fullPath = resolveWorkspacePath(file);
|
|
84
|
+
|
|
85
|
+
let content = "";
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
content = fs.readFileSync(fullPath, "utf-8");
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const lines = content.split("\n");
|
|
94
|
+
|
|
95
|
+
lines.forEach((line, index) => {
|
|
96
|
+
if (line.toLowerCase().includes(query.toLowerCase())) {
|
|
97
|
+
matches.push(`${file}:${index + 1}: ${line.trim()}`);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return matches.length > 0
|
|
103
|
+
? matches.join("\n")
|
|
104
|
+
: `No matches found for: ${query}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
export function editFile(filePath: string, find: string, replace: string) {
|
|
111
|
+
const fullPath = resolveWorkspacePath(filePath);
|
|
112
|
+
|
|
113
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
114
|
+
|
|
115
|
+
if (!content.includes(find)) {
|
|
116
|
+
throw new Error(`Text not found in file: ${filePath}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const updated = content.replace(find, replace);
|
|
120
|
+
|
|
121
|
+
fs.writeFileSync(fullPath, updated, "utf-8");
|
|
122
|
+
|
|
123
|
+
return `Edited file: ${filePath}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function deleteFile(filePath: string) {
|
|
127
|
+
const fullPath = resolveWorkspacePath(filePath);
|
|
128
|
+
|
|
129
|
+
if (!fs.existsSync(fullPath)) {
|
|
130
|
+
throw new Error(`File not found: ${filePath}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const stat = fs.statSync(fullPath);
|
|
134
|
+
|
|
135
|
+
if (!stat.isFile()) {
|
|
136
|
+
throw new Error(`Not a file: ${filePath}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fs.unlinkSync(fullPath);
|
|
140
|
+
|
|
141
|
+
return `Deleted file: ${filePath}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
export function createDirectory(dirPath: string) {
|
|
146
|
+
const fullPath = resolveWorkspacePath(dirPath);
|
|
147
|
+
|
|
148
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
149
|
+
|
|
150
|
+
return `Directory created: ${dirPath}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function moveFile(from: string, to: string) {
|
|
154
|
+
const fromPath = resolveWorkspacePath(from);
|
|
155
|
+
const toPath = resolveWorkspacePath(to);
|
|
156
|
+
|
|
157
|
+
if (!fs.existsSync(fromPath)) {
|
|
158
|
+
throw new Error(`Source not found: ${from}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fs.mkdirSync(path.dirname(toPath), { recursive: true });
|
|
162
|
+
fs.renameSync(fromPath, toPath);
|
|
163
|
+
|
|
164
|
+
return `Moved: ${from} -> ${to}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function copyFile(from: string, to: string) {
|
|
168
|
+
const fromPath = resolveWorkspacePath(from);
|
|
169
|
+
const toPath = resolveWorkspacePath(to);
|
|
170
|
+
|
|
171
|
+
if (!fs.existsSync(fromPath)) {
|
|
172
|
+
throw new Error(`Source not found: ${from}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fs.mkdirSync(path.dirname(toPath), { recursive: true });
|
|
176
|
+
fs.copyFileSync(fromPath, toPath);
|
|
177
|
+
|
|
178
|
+
return `Copied: ${from} -> ${to}`;
|
|
179
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
let WORKSPACE = process.cwd();
|
|
4
|
+
|
|
5
|
+
export function setWorkspace(workspacePath: string) {
|
|
6
|
+
WORKSPACE = path.resolve(workspacePath);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getWorkspace() {
|
|
10
|
+
return WORKSPACE;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveWorkspacePath(filePath: string) {
|
|
14
|
+
const fullPath = path.resolve(WORKSPACE, filePath);
|
|
15
|
+
const relative = path.relative(WORKSPACE, fullPath);
|
|
16
|
+
|
|
17
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
18
|
+
throw new Error("Blocked: path outside workspace");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return fullPath;
|
|
22
|
+
}
|