@ripla/godd-mcp 1.0.2-canary.7 → 1.0.2-canary.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { createdFileToContent, createdFileToTreeEntry } from "./created-file";
|
|
3
|
+
|
|
4
|
+
describe("created-file helpers", () => {
|
|
5
|
+
it("作成APIの戻り値からファイルツリー項目を作る", () => {
|
|
6
|
+
const entry = createdFileToTreeEntry({
|
|
7
|
+
path: "docs/project/new-note.md",
|
|
8
|
+
sha: "abc123",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
expect(entry).toEqual({
|
|
12
|
+
name: "new-note.md",
|
|
13
|
+
path: "docs/project/new-note.md",
|
|
14
|
+
type: "file",
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it(".md 作成直後に表示できる空のファイル内容を作る", () => {
|
|
19
|
+
const content = createdFileToContent({
|
|
20
|
+
path: "docs/project/new-note.md",
|
|
21
|
+
sha: "abc123",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
expect(content).toEqual({
|
|
25
|
+
path: "docs/project/new-note.md",
|
|
26
|
+
sha: "abc123",
|
|
27
|
+
content: "",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FileContent, TreeEntry } from "@/types";
|
|
2
|
+
|
|
3
|
+
export type CreatedFile = {
|
|
4
|
+
path: string;
|
|
5
|
+
sha: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function createdFileToTreeEntry(file: CreatedFile): TreeEntry {
|
|
9
|
+
return {
|
|
10
|
+
name: file.path.split("/").pop() ?? file.path,
|
|
11
|
+
path: file.path,
|
|
12
|
+
type: "file",
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function createdFileToContent(file: CreatedFile): FileContent {
|
|
17
|
+
return {
|
|
18
|
+
path: file.path,
|
|
19
|
+
sha: file.sha,
|
|
20
|
+
content: "",
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -35,3 +35,42 @@ export function removeEntry(entries: TreeEntry[], targetPath: string): TreeEntry
|
|
|
35
35
|
};
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
|
|
39
|
+
export function moveEntry(
|
|
40
|
+
entries: TreeEntry[],
|
|
41
|
+
oldPath: string,
|
|
42
|
+
newParentDir: string,
|
|
43
|
+
): TreeEntry[] {
|
|
44
|
+
if (newParentDir === oldPath || newParentDir.startsWith(oldPath + "/")) {
|
|
45
|
+
return entries;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const entry = findEntry(entries, oldPath);
|
|
49
|
+
const targetParent = findEntry(entries, newParentDir);
|
|
50
|
+
if (!entry || targetParent?.type !== "dir") return entries;
|
|
51
|
+
|
|
52
|
+
const newPath = `${newParentDir}/${entry.name}`;
|
|
53
|
+
const moved = rebaseEntryPath(entry, oldPath, newPath);
|
|
54
|
+
return insertChildEntry(removeEntry(entries, oldPath), newParentDir, moved);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function findEntry(entries: TreeEntry[], targetPath: string): TreeEntry | null {
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.path === targetPath) return entry;
|
|
60
|
+
if (entry.children) {
|
|
61
|
+
const found = findEntry(entry.children, targetPath);
|
|
62
|
+
if (found) return found;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function rebaseEntryPath(entry: TreeEntry, oldPath: string, newPath: string): TreeEntry {
|
|
69
|
+
return {
|
|
70
|
+
...entry,
|
|
71
|
+
path: entry.path === oldPath
|
|
72
|
+
? newPath
|
|
73
|
+
: entry.path.replace(oldPath + "/", newPath + "/"),
|
|
74
|
+
children: entry.children?.map((child) => rebaseEntryPath(child, oldPath, newPath)),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import type { TreeEntry } from "@/types";
|
|
3
|
-
import { insertChildEntry, removeEntry } from "@/lib/tree-utils";
|
|
3
|
+
import { insertChildEntry, moveEntry, removeEntry } from "@/lib/tree-utils";
|
|
4
4
|
|
|
5
5
|
const baseTree: TreeEntry[] = [
|
|
6
6
|
{
|
|
@@ -110,3 +110,32 @@ describe("removeEntry", () => {
|
|
|
110
110
|
expect(result).toEqual(baseTree);
|
|
111
111
|
});
|
|
112
112
|
});
|
|
113
|
+
|
|
114
|
+
describe("moveEntry", () => {
|
|
115
|
+
it("moves a file to another folder and updates its path", () => {
|
|
116
|
+
const result = moveEntry(baseTree, "docs/index.md", "docs/project");
|
|
117
|
+
const docsChildren = result[0].children ?? [];
|
|
118
|
+
const project = docsChildren.find((entry) => entry.path === "docs/project");
|
|
119
|
+
|
|
120
|
+
expect(docsChildren.some((entry) => entry.path === "docs/index.md")).toBe(false);
|
|
121
|
+
expect(project?.children?.some((entry) => entry.path === "docs/project/index.md")).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("moves a folder and rebases child paths", () => {
|
|
125
|
+
const tree: TreeEntry[] = [
|
|
126
|
+
...baseTree,
|
|
127
|
+
{ name: "archive", path: "archive", type: "dir", children: [] },
|
|
128
|
+
];
|
|
129
|
+
const result = moveEntry(tree, "docs/project", "archive");
|
|
130
|
+
const archive = result.find((entry) => entry.path === "archive");
|
|
131
|
+
const moved = archive?.children?.find((entry) => entry.path === "archive/project");
|
|
132
|
+
|
|
133
|
+
expect(JSON.stringify(result)).not.toContain("docs/project/readme.md");
|
|
134
|
+
expect(moved?.children?.[0].path).toBe("archive/project/readme.md");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not move into itself", () => {
|
|
138
|
+
const result = moveEntry(baseTree, "docs/project", "docs/project");
|
|
139
|
+
expect(result).toEqual(baseTree);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -34,7 +34,8 @@ import BranchSelectorModal from "@/components/BranchSelectorModal";
|
|
|
34
34
|
import IssueList from "@/components/IssueList";
|
|
35
35
|
import type { TreeEntry, FileContent, ApiState, FileStyles } from "@/types";
|
|
36
36
|
import { getFileExt } from "@/types";
|
|
37
|
-
import { insertChildEntry, removeEntry } from "@/lib/tree-utils";
|
|
37
|
+
import { insertChildEntry, moveEntry, removeEntry } from "@/lib/tree-utils";
|
|
38
|
+
import { createdFileToContent, createdFileToTreeEntry } from "@/lib/created-file";
|
|
38
39
|
|
|
39
40
|
const MarkdownEditor = lazy(() => import("@/components/MarkdownEditor"));
|
|
40
41
|
const SpreadsheetEditor = lazy(() => import("@/components/SpreadsheetEditor"));
|
|
@@ -394,19 +395,36 @@ export default function MainPage() {
|
|
|
394
395
|
const handleCreateFile = useCallback(
|
|
395
396
|
async (fileName: string) => {
|
|
396
397
|
if (!dialog || dialog.type !== "create") return;
|
|
398
|
+
const unsaved = autoSave.status === "unsaved" || autoSave.status === "saving";
|
|
399
|
+
if (unsaved && !window.confirm("未保存の変更があります。ファイルを切り替えてもよろしいですか?")) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
397
402
|
const parentPath = dialog.entry.path;
|
|
398
403
|
const fullPath = `${parentPath}/${fileName}`;
|
|
399
404
|
setDialog(null);
|
|
400
405
|
try {
|
|
401
|
-
await createFileApi(fullPath);
|
|
406
|
+
const created = await createFileApi(fullPath);
|
|
402
407
|
showToast(`${fileName} を作成しました`, "success");
|
|
403
|
-
|
|
404
|
-
|
|
408
|
+
fetchAbortRef.current?.abort();
|
|
409
|
+
autoSave.reset();
|
|
410
|
+
pendingStylesRef.current = null;
|
|
411
|
+
setSelectedIssue(null);
|
|
412
|
+
setSelectedFile(created.path);
|
|
413
|
+
setActiveDir(parentPath);
|
|
414
|
+
setSearchParams({ file: created.path }, { replace: true });
|
|
415
|
+
setFileLoading(false);
|
|
416
|
+
setEditing(false);
|
|
417
|
+
setShowOriginal(false);
|
|
418
|
+
setEditedContent(null);
|
|
419
|
+
setFileContent(createdFileToContent(created));
|
|
420
|
+
setTree((prev) => insertChildEntry(prev, parentPath, createdFileToTreeEntry(created)));
|
|
421
|
+
if (treeSyncTimerRef.current) clearTimeout(treeSyncTimerRef.current);
|
|
422
|
+
treeSyncTimerRef.current = setTimeout(() => loadTree(), 1500);
|
|
405
423
|
} catch (e) {
|
|
406
424
|
showToast(`作成に失敗しました: ${getErrorMessage(e)}`, "error");
|
|
407
425
|
}
|
|
408
426
|
},
|
|
409
|
-
[dialog, showToast,
|
|
427
|
+
[dialog, showToast, autoSave, setSearchParams, loadTree],
|
|
410
428
|
);
|
|
411
429
|
|
|
412
430
|
const handleCreateFolder = useCallback(
|
|
@@ -529,7 +547,9 @@ export default function MainPage() {
|
|
|
529
547
|
handleFileSelect(newPath);
|
|
530
548
|
}
|
|
531
549
|
}
|
|
532
|
-
|
|
550
|
+
setTree((prev) => moveEntry(prev, oldPath, newParentDir));
|
|
551
|
+
if (treeSyncTimerRef.current) clearTimeout(treeSyncTimerRef.current);
|
|
552
|
+
treeSyncTimerRef.current = setTimeout(() => loadTree(), 1500);
|
|
533
553
|
} catch (e) {
|
|
534
554
|
showToast(`移動に失敗しました: ${getErrorMessage(e)}`, "error");
|
|
535
555
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ripla/godd-mcp",
|
|
3
|
-
"version": "1.0.2-canary.
|
|
3
|
+
"version": "1.0.2-canary.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "GoDD (Governance-orchestrated Driven Development) MCP Server - Encrypted prompt distribution via Model Context Protocol",
|
|
6
6
|
"main": "dist/index.js",
|