@ripla/godd-mcp 1.0.6 → 1.0.7-canary.2
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/godd.cjs +194 -185
- package/dist/godd.js +191 -49
- package/dist/index.js +1 -1
- package/notes/api/.env.example +1 -4
- package/notes/api/README.md +4 -2
- package/notes/api/app/config.py +8 -5
- package/notes/api/app/routers/tree.py +3 -3
- package/notes/api/app/services/github.py +14 -13
- package/notes/api/app/services/github_app_token.py +10 -15
- package/notes/api/app/services/github_pr.py +14 -8
- package/notes/api/tests/test_files.py +2 -2
- package/notes/api/tests/test_github_app_token.py +6 -5
- package/notes/api/tests/test_github_service.py +30 -4
- package/notes/api/tests/test_pr_chain.py +25 -2
- package/notes/api/tests/test_tree.py +1 -1
- package/notes/app/e2e/pages/login-page.ts +6 -0
- package/notes/app/e2e/password-toggle.spec.ts +198 -0
- package/notes/app/src/components/PasswordInput.test.tsx +122 -0
- package/notes/app/src/components/PasswordInput.tsx +67 -0
- package/notes/app/src/pages/LoginPage.test.tsx +31 -1
- package/notes/app/src/pages/LoginPage.tsx +14 -8
- package/notes/app/src/pages/UserManagementPage.test.tsx +51 -0
- package/notes/app/src/pages/UserManagementPage.tsx +2 -2
- package/package.json +1 -1
- package/templates/github-actions/aws/deploy-notes.yml.hbs +31 -0
- package/templates/notes-compose.yml +0 -1
- package/templates/notes-docs/007_guides/setup_godd.md +46 -10
- package/templates/notes-docs/007_guides/setup_godd_notes.md +218 -16
- package/templates/notes-docs/007_guides/setup_godd_notes.md.styles.json +409 -0
- package/templates/notes-docs/007_guides/setup_infra.template.md +75 -22
- package/templates/terraform/aws/ecs.tf.hbs +9 -1
- package/templates/terraform/aws/provider.tf.hbs +1 -1
- package/templates/terraform/aws/secrets.tf.hbs +11 -3
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen, fireEvent } from "@testing-library/react";
|
|
3
|
+
import PasswordInput from "./PasswordInput";
|
|
4
|
+
|
|
5
|
+
describe("PasswordInput", () => {
|
|
6
|
+
it("REQ-UI-AC-13: 初期状態では伏字(type=password)で描画される", () => {
|
|
7
|
+
render(<PasswordInput id="pw" aria-label="パスワード" defaultValue="secret" />);
|
|
8
|
+
expect(screen.getByLabelText("パスワード").getAttribute("type")).toBe("password");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("REQ-UI-AC-14: 目玉アイコンのクリックで平文表示に切り替わり、再クリックで伏字に戻る", () => {
|
|
12
|
+
render(<PasswordInput id="pw" aria-label="パスワード" defaultValue="secret" />);
|
|
13
|
+
const input = screen.getByLabelText("パスワード");
|
|
14
|
+
|
|
15
|
+
fireEvent.click(screen.getByRole("button", { name: "パスワードを表示" }));
|
|
16
|
+
expect(input.getAttribute("type")).toBe("text");
|
|
17
|
+
|
|
18
|
+
fireEvent.click(screen.getByRole("button", { name: "パスワードを非表示" }));
|
|
19
|
+
expect(input.getAttribute("type")).toBe("password");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("REQ-UI-AC-17: トグルボタンは submit を誘発しない type=button である", () => {
|
|
23
|
+
render(<PasswordInput id="pw" aria-label="パスワード" />);
|
|
24
|
+
expect(screen.getByRole("button", { name: "パスワードを表示" }).getAttribute("type")).toBe(
|
|
25
|
+
"button",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("REQ-UI-AC-17: トグルボタンはキーボードフォーカスリングを持ち、hover と弁別できる", () => {
|
|
30
|
+
render(<PasswordInput id="pw" aria-label="パスワード" />);
|
|
31
|
+
const toggle = screen.getByRole("button", { name: "パスワードを表示" });
|
|
32
|
+
|
|
33
|
+
// outline-none で既定リングを消すため、代替の focus-visible リングが必須(WCAG 2.4.11 / REQ-UI-AC-11)
|
|
34
|
+
expect(toggle.className).toContain("focus-visible:ring-2");
|
|
35
|
+
expect(toggle.className).toContain("focus-visible:ring-blue-500/70");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("REQ-UI-AC-17: disabled はトグルボタンにも伝播し、無効時は表示切替できない", () => {
|
|
39
|
+
render(<PasswordInput id="pw" aria-label="パスワード" defaultValue="secret" disabled />);
|
|
40
|
+
const input = screen.getByLabelText("パスワード");
|
|
41
|
+
const toggle = screen.getByRole("button", { name: "パスワードを表示" });
|
|
42
|
+
|
|
43
|
+
expect((toggle as HTMLButtonElement).disabled).toBe(true);
|
|
44
|
+
fireEvent.click(toggle);
|
|
45
|
+
expect(input.getAttribute("type")).toBe("password");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("平文表示中の値がスペルチェック・自動修正に取り込まれないよう抑止する", () => {
|
|
49
|
+
render(<PasswordInput id="pw" aria-label="パスワード" />);
|
|
50
|
+
const input = screen.getByLabelText("パスワード");
|
|
51
|
+
expect(input.getAttribute("spellcheck")).toBe("false");
|
|
52
|
+
expect(input.getAttribute("autocapitalize")).toBe("off");
|
|
53
|
+
expect(input.getAttribute("autocorrect")).toBe("off");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("スペルチェック抑止は呼び出し側から上書きできない", () => {
|
|
57
|
+
render(
|
|
58
|
+
// @ts-expect-error セキュリティ上の抑止は Omit しているため渡せないことを型でも保証する
|
|
59
|
+
<PasswordInput id="pw" aria-label="パスワード" spellCheck autoCorrect="on" autoCapitalize="characters" />,
|
|
60
|
+
);
|
|
61
|
+
const input = screen.getByLabelText("パスワード");
|
|
62
|
+
expect(input.getAttribute("spellcheck")).toBe("false");
|
|
63
|
+
expect(input.getAttribute("autocorrect")).toBe("off");
|
|
64
|
+
expect(input.getAttribute("autocapitalize")).toBe("off");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("REQ-UI-AC-17: 状態は aria-label とライブリージョンで伝え、aria-pressed は併用しない", () => {
|
|
68
|
+
render(<PasswordInput id="pw" aria-label="パスワード" />);
|
|
69
|
+
const toggle = () => screen.getByRole("button", { name: /パスワードを(非)?表示/ });
|
|
70
|
+
|
|
71
|
+
// aria-label と併用すると「パスワードを非表示, 押されている」と二重に読み上げられ意味が反転する
|
|
72
|
+
expect(toggle().getAttribute("aria-pressed")).toBeNull();
|
|
73
|
+
|
|
74
|
+
const live = screen.getByRole("status");
|
|
75
|
+
expect(live.getAttribute("aria-live")).toBe("polite");
|
|
76
|
+
// sr-only が外れると文言が入力欄内に可視表示されてしまう
|
|
77
|
+
expect(live.className).toContain("sr-only");
|
|
78
|
+
// 初回描画では何も起きていないため通知しない
|
|
79
|
+
expect(live.textContent).toBe("");
|
|
80
|
+
|
|
81
|
+
fireEvent.click(toggle());
|
|
82
|
+
expect(live.textContent).toBe("パスワードを表示しました");
|
|
83
|
+
|
|
84
|
+
fireEvent.click(toggle());
|
|
85
|
+
expect(live.textContent).toBe("パスワードを非表示にしました");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("渡された className を引き継ぎ、アイコン分の inline-end パディングを付与する", () => {
|
|
89
|
+
render(
|
|
90
|
+
<PasswordInput
|
|
91
|
+
id="pw"
|
|
92
|
+
aria-label="パスワード"
|
|
93
|
+
className="custom-class"
|
|
94
|
+
autoComplete="new-password"
|
|
95
|
+
placeholder="入力してください"
|
|
96
|
+
/>,
|
|
97
|
+
);
|
|
98
|
+
const input = screen.getByLabelText("パスワード");
|
|
99
|
+
expect(input.className).toContain("custom-class");
|
|
100
|
+
// アイコンと入力文字の重なりを防ぐ契約(仕様書に明記)
|
|
101
|
+
expect(input.className).toContain("pe-10");
|
|
102
|
+
expect(input.getAttribute("autocomplete")).toBe("new-password");
|
|
103
|
+
expect(input.getAttribute("placeholder")).toBe("入力してください");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("呼び出し側は type を上書きできない", () => {
|
|
107
|
+
render(
|
|
108
|
+
// @ts-expect-error type は Omit しているため渡せないことを型でも保証する
|
|
109
|
+
<PasswordInput id="pw" aria-label="パスワード" type="text" />,
|
|
110
|
+
);
|
|
111
|
+
expect(screen.getByLabelText("パスワード").getAttribute("type")).toBe("password");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("onChange が呼び出される", () => {
|
|
115
|
+
let received = "";
|
|
116
|
+
render(
|
|
117
|
+
<PasswordInput id="pw" aria-label="パスワード" onChange={(e) => (received = e.target.value)} />,
|
|
118
|
+
);
|
|
119
|
+
fireEvent.change(screen.getByLabelText("パスワード"), { target: { value: "abc" } });
|
|
120
|
+
expect(received).toBe("abc");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import type { InputHTMLAttributes } from "react";
|
|
3
|
+
import { Eye, EyeOff } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 呼び出し側に指定させないプロパティ:
|
|
7
|
+
* - `type` … 表示状態から内部で決める
|
|
8
|
+
* - `spellCheck` / `autoCapitalize` / `autoCorrect` … 平文表示中の値が外部送信され得るため無効化を強制する
|
|
9
|
+
*
|
|
10
|
+
* `id` はラベルとの `htmlFor` 紐付けに必須なので必須プロパティに引き上げる。
|
|
11
|
+
*/
|
|
12
|
+
type PasswordInputProps = Omit<
|
|
13
|
+
InputHTMLAttributes<HTMLInputElement>,
|
|
14
|
+
"type" | "spellCheck" | "autoCapitalize" | "autoCorrect"
|
|
15
|
+
> & {
|
|
16
|
+
id: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 表示/非表示を目玉アイコンで切り替えられるパスワード入力欄。
|
|
21
|
+
* 見た目はページ側の className をそのまま引き継ぐ(アイコン分の inline-end パディングのみ付与)。
|
|
22
|
+
*/
|
|
23
|
+
export default function PasswordInput({ className = "", ...inputProps }: PasswordInputProps) {
|
|
24
|
+
const [visible, setVisible] = useState(false);
|
|
25
|
+
const [toggled, setToggled] = useState(false);
|
|
26
|
+
const { disabled } = inputProps;
|
|
27
|
+
|
|
28
|
+
const toggle = () => {
|
|
29
|
+
setVisible((v) => !v);
|
|
30
|
+
setToggled(true);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="relative">
|
|
35
|
+
{/* 平文表示中は通常の text input になる。ブラウザの拡張スペルチェックは入力内容を外部送信し得るため、
|
|
36
|
+
モバイルの自動修正辞書と併せてパスワードが取り込まれないよう抑止する。
|
|
37
|
+
`type` と同様、スプレッド後に置き型からも Omit して上書き不能にする。 */}
|
|
38
|
+
<input
|
|
39
|
+
{...inputProps}
|
|
40
|
+
type={visible ? "text" : "password"}
|
|
41
|
+
spellCheck={false}
|
|
42
|
+
autoCapitalize="off"
|
|
43
|
+
autoCorrect="off"
|
|
44
|
+
className={`${className} pe-10`}
|
|
45
|
+
/>
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={toggle}
|
|
49
|
+
disabled={disabled}
|
|
50
|
+
// 状態は「次に起きること」を示すラベルで伝える。aria-pressed を併用すると
|
|
51
|
+
// 「パスワードを非表示, 押されている」と二重に読み上げられ意味が反転するため使わない。
|
|
52
|
+
aria-label={visible ? "パスワードを非表示" : "パスワードを表示"}
|
|
53
|
+
// ring-inset 必須: ボタンは入力欄の角に接しており、呼び出し側の角丸(LoginPage は rounded-lg=8px、
|
|
54
|
+
// モーダルは rounded=4px)と異なる半径を持つ。外向きリングだと入力欄の丸角から角張って食み出す。
|
|
55
|
+
className="absolute inset-y-0 end-0 flex items-center px-3 rounded text-gray-400 hover:text-gray-200 disabled:opacity-40 disabled:cursor-not-allowed outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500/70"
|
|
56
|
+
>
|
|
57
|
+
{visible ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />}
|
|
58
|
+
</button>
|
|
59
|
+
{/* フォーカス中の要素のアクセシブル名を差し替えても再読み上げは SR/ブラウザ依存で不安定なため、
|
|
60
|
+
状態変化はライブリージョンでも通知する。初回描画では何も起きていないので空にする。
|
|
61
|
+
sr-only を外すと文言が入力欄内に可視表示されてしまうため必須。 */}
|
|
62
|
+
<span className="sr-only" role="status" aria-live="polite">
|
|
63
|
+
{toggled ? (visible ? "パスワードを表示しました" : "パスワードを非表示にしました") : ""}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import { screen } from "@testing-library/react";
|
|
2
|
+
import { screen, fireEvent } from "@testing-library/react";
|
|
3
3
|
import { mockAuthContext } from "@/test/mocks";
|
|
4
4
|
import { renderWithProviders } from "@/test/render";
|
|
5
5
|
import LoginPage from "./LoginPage";
|
|
@@ -14,4 +14,34 @@ describe("LoginPage", () => {
|
|
|
14
14
|
renderWithProviders(<LoginPage />);
|
|
15
15
|
expect(screen.getByText("GoDD Notes")).toBeTruthy();
|
|
16
16
|
});
|
|
17
|
+
|
|
18
|
+
it("REQ-UI-AC-13/14: パスワード欄は初期状態で伏字であり、目玉アイコンで表示/非表示を切り替えられる", () => {
|
|
19
|
+
renderWithProviders(<LoginPage />);
|
|
20
|
+
const password = screen.getByLabelText("パスワード");
|
|
21
|
+
expect(password.getAttribute("type")).toBe("password");
|
|
22
|
+
|
|
23
|
+
fireEvent.click(screen.getByRole("button", { name: "パスワードを表示" }));
|
|
24
|
+
expect(password.getAttribute("type")).toBe("text");
|
|
25
|
+
|
|
26
|
+
fireEvent.click(screen.getByRole("button", { name: "パスワードを非表示" }));
|
|
27
|
+
expect(password.getAttribute("type")).toBe("password");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("REQ-UI-AC-16: 表示切替後も入力値が保持され、既存の #login-password が維持される", () => {
|
|
31
|
+
renderWithProviders(<LoginPage />);
|
|
32
|
+
const password = screen.getByLabelText("パスワード");
|
|
33
|
+
expect(password.getAttribute("id")).toBe("login-password");
|
|
34
|
+
|
|
35
|
+
fireEvent.change(password, { target: { value: "secret123" } });
|
|
36
|
+
fireEvent.click(screen.getByRole("button", { name: "パスワードを表示" }));
|
|
37
|
+
expect((password as HTMLInputElement).value).toBe("secret123");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("パスワードマネージャ連携のため autoComplete が設定されている", () => {
|
|
41
|
+
renderWithProviders(<LoginPage />);
|
|
42
|
+
expect(screen.getByLabelText("ユーザー名").getAttribute("autocomplete")).toBe("username");
|
|
43
|
+
expect(screen.getByLabelText("パスワード").getAttribute("autocomplete")).toBe(
|
|
44
|
+
"current-password",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
17
47
|
});
|
|
@@ -6,6 +6,7 @@ import { resolvePostLoginPath } from "@/lib/postLoginRedirect";
|
|
|
6
6
|
import { useAuth } from "@/contexts/AuthContext";
|
|
7
7
|
import { useToast } from "@/contexts/ToastContext";
|
|
8
8
|
import { PageSkeleton } from "@/components/Skeleton";
|
|
9
|
+
import PasswordInput from "@/components/PasswordInput";
|
|
9
10
|
|
|
10
11
|
export default function LoginPage() {
|
|
11
12
|
const { login, loading, isAuthenticated } = useAuth();
|
|
@@ -60,8 +61,10 @@ export default function LoginPage() {
|
|
|
60
61
|
</div>
|
|
61
62
|
)}
|
|
62
63
|
|
|
63
|
-
<
|
|
64
|
-
<
|
|
64
|
+
<div className="mb-4">
|
|
65
|
+
<label htmlFor="login-username" className="block text-sm text-gray-400">
|
|
66
|
+
ユーザー名
|
|
67
|
+
</label>
|
|
65
68
|
<input
|
|
66
69
|
id="login-username"
|
|
67
70
|
type="text"
|
|
@@ -69,20 +72,23 @@ export default function LoginPage() {
|
|
|
69
72
|
onChange={(e) => setUsername(e.target.value)}
|
|
70
73
|
className="mt-1 block w-full rounded-lg bg-gray-700 border border-gray-600 px-3 py-2 text-gray-100 placeholder-gray-500 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
|
|
71
74
|
placeholder="admin"
|
|
75
|
+
autoComplete="username"
|
|
72
76
|
autoFocus
|
|
73
77
|
/>
|
|
74
|
-
</
|
|
78
|
+
</div>
|
|
75
79
|
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
80
|
+
<div className="mb-6">
|
|
81
|
+
<label htmlFor="login-password" className="block text-sm text-gray-400">
|
|
82
|
+
パスワード
|
|
83
|
+
</label>
|
|
84
|
+
<PasswordInput
|
|
79
85
|
id="login-password"
|
|
80
|
-
type="password"
|
|
81
86
|
value={password}
|
|
82
87
|
onChange={(e) => setPassword(e.target.value)}
|
|
83
88
|
className="mt-1 block w-full rounded-lg bg-gray-700 border border-gray-600 px-3 py-2 text-gray-100 placeholder-gray-500 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none"
|
|
89
|
+
autoComplete="current-password"
|
|
84
90
|
/>
|
|
85
|
-
</
|
|
91
|
+
</div>
|
|
86
92
|
|
|
87
93
|
<button
|
|
88
94
|
type="submit"
|
|
@@ -148,6 +148,57 @@ describe("UserManagementPage", () => {
|
|
|
148
148
|
);
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
+
it("REQ-UI-AC-15/16: 作成モーダルのパスワード欄は目玉アイコンで表示/非表示を切り替えられる", async () => {
|
|
152
|
+
renderPage();
|
|
153
|
+
await waitFor(() => expect(screen.getByText("+ ユーザー追加")).toBeTruthy());
|
|
154
|
+
|
|
155
|
+
fireEvent.click(screen.getByText("+ ユーザー追加"));
|
|
156
|
+
await waitFor(() => expect(screen.getByText("ユーザー作成")).toBeTruthy());
|
|
157
|
+
|
|
158
|
+
const password = screen.getByLabelText("パスワード");
|
|
159
|
+
expect(password.getAttribute("type")).toBe("password");
|
|
160
|
+
|
|
161
|
+
fireEvent.change(password, { target: { value: "password123" } });
|
|
162
|
+
fireEvent.click(screen.getByRole("button", { name: "パスワードを表示" }));
|
|
163
|
+
expect(password.getAttribute("type")).toBe("text");
|
|
164
|
+
expect((password as HTMLInputElement).value).toBe("password123");
|
|
165
|
+
|
|
166
|
+
fireEvent.click(screen.getByRole("button", { name: "パスワードを非表示" }));
|
|
167
|
+
expect(password.getAttribute("type")).toBe("password");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("REQ-UI-AC-15: 編集モーダルのパスワード欄でも表示/非表示を切り替えられる", async () => {
|
|
171
|
+
renderPage();
|
|
172
|
+
await waitFor(() => expect(screen.getAllByText("編集").length).toBeGreaterThan(0));
|
|
173
|
+
|
|
174
|
+
fireEvent.click(screen.getAllByText("編集")[0]);
|
|
175
|
+
await waitFor(() => expect(screen.getByText("ユーザー編集")).toBeTruthy());
|
|
176
|
+
|
|
177
|
+
// 編集モードはラベル文言が異なるため、作成モードのクエリは流用できない
|
|
178
|
+
const password = screen.getByLabelText("新しいパスワード(変更する場合のみ)");
|
|
179
|
+
expect(password.getAttribute("type")).toBe("password");
|
|
180
|
+
|
|
181
|
+
fireEvent.click(screen.getByRole("button", { name: "パスワードを表示" }));
|
|
182
|
+
expect(password.getAttribute("type")).toBe("text");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("REQ-UI-AC-13: モーダルを閉じて開き直すと伏字に戻る", async () => {
|
|
186
|
+
renderPage();
|
|
187
|
+
await waitFor(() => expect(screen.getByText("+ ユーザー追加")).toBeTruthy());
|
|
188
|
+
|
|
189
|
+
fireEvent.click(screen.getByText("+ ユーザー追加"));
|
|
190
|
+
await waitFor(() => expect(screen.getByText("ユーザー作成")).toBeTruthy());
|
|
191
|
+
fireEvent.click(screen.getByRole("button", { name: "パスワードを表示" }));
|
|
192
|
+
expect(screen.getByLabelText("パスワード").getAttribute("type")).toBe("text");
|
|
193
|
+
|
|
194
|
+
fireEvent.click(screen.getByLabelText("閉じる"));
|
|
195
|
+
await waitFor(() => expect(screen.queryByText("ユーザー作成")).toBeNull());
|
|
196
|
+
|
|
197
|
+
fireEvent.click(screen.getByText("+ ユーザー追加"));
|
|
198
|
+
await waitFor(() => expect(screen.getByText("ユーザー作成")).toBeTruthy());
|
|
199
|
+
expect(screen.getByLabelText("パスワード").getAttribute("type")).toBe("password");
|
|
200
|
+
});
|
|
201
|
+
|
|
151
202
|
it("shows validation error for short password on create", async () => {
|
|
152
203
|
renderPage();
|
|
153
204
|
await waitFor(() => expect(screen.getByText("+ ユーザー追加")).toBeTruthy());
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import type { UserDetail, UserCreateInput, UserUpdateInput } from "@/lib/api";
|
|
13
13
|
import { ArrowLeft, X } from "lucide-react";
|
|
14
14
|
import { ListSkeleton } from "@/components/Skeleton";
|
|
15
|
+
import PasswordInput from "@/components/PasswordInput";
|
|
15
16
|
|
|
16
17
|
type UserRole = "admin" | "editor" | "viewer";
|
|
17
18
|
|
|
@@ -200,9 +201,8 @@ function UserForm({
|
|
|
200
201
|
<label htmlFor="form-password" className="block text-xs font-medium text-gray-300 mb-1">
|
|
201
202
|
{mode === "create" ? "パスワード" : "新しいパスワード(変更する場合のみ)"}
|
|
202
203
|
</label>
|
|
203
|
-
<
|
|
204
|
+
<PasswordInput
|
|
204
205
|
id="form-password"
|
|
205
|
-
type="password"
|
|
206
206
|
value={password}
|
|
207
207
|
onChange={(e) => setPassword(e.target.value)}
|
|
208
208
|
className={inputClasses}
|
package/package.json
CHANGED
|
@@ -80,11 +80,42 @@ jobs:
|
|
|
80
80
|
cache-to: type=gha,mode=max
|
|
81
81
|
|
|
82
82
|
- name: Deploy to ECS
|
|
83
|
+
env:
|
|
84
|
+
IMAGE_URI: $\{{ steps.ecr-login.outputs.registry }}/$\{{ env.ECR_REPOSITORY }}:$\{{ github.sha }}
|
|
83
85
|
run: |
|
|
86
|
+
TASK_DEF=$(aws ecs describe-services \
|
|
87
|
+
--cluster $ECS_CLUSTER \
|
|
88
|
+
--services $ECS_SERVICE \
|
|
89
|
+
--query 'services[0].taskDefinition' \
|
|
90
|
+
--output text)
|
|
91
|
+
|
|
92
|
+
TASK_DEF_JSON=$(aws ecs describe-task-definition \
|
|
93
|
+
--task-definition "$TASK_DEF" \
|
|
94
|
+
--query 'taskDefinition')
|
|
95
|
+
|
|
96
|
+
# Strip legacy PAT injection (GITHUB_TOKEN) from every container (match ecs-task-def.ts).
|
|
97
|
+
NEW_TASK_DEF=$(echo "$TASK_DEF_JSON" | jq --arg IMAGE "$IMAGE_URI" '
|
|
98
|
+
.containerDefinitions[0].image = $IMAGE
|
|
99
|
+
| .containerDefinitions |= map(
|
|
100
|
+
if .secrets != null then
|
|
101
|
+
.secrets |= map(select(.name != "GITHUB_TOKEN"))
|
|
102
|
+
else .
|
|
103
|
+
end
|
|
104
|
+
)
|
|
105
|
+
| del(.taskDefinitionArn, .revision, .status, .requiresAttributes, .compatibilities, .registeredAt, .registeredBy)
|
|
106
|
+
')
|
|
107
|
+
|
|
108
|
+
NEW_TASK_DEF_ARN=$(aws ecs register-task-definition \
|
|
109
|
+
--cli-input-json "$NEW_TASK_DEF" \
|
|
110
|
+
--query 'taskDefinition.taskDefinitionArn' \
|
|
111
|
+
--output text)
|
|
112
|
+
|
|
84
113
|
aws ecs update-service \
|
|
85
114
|
--cluster $ECS_CLUSTER \
|
|
86
115
|
--service $ECS_SERVICE \
|
|
116
|
+
--task-definition "$NEW_TASK_DEF_ARN" \
|
|
87
117
|
--force-new-deployment
|
|
118
|
+
|
|
88
119
|
aws ecs wait services-stable \
|
|
89
120
|
--cluster $ECS_CLUSTER \
|
|
90
121
|
--services $ECS_SERVICE
|
|
@@ -114,31 +114,65 @@ Step 3 まで完了したら、利用する IDE に応じて MCP を登録しま
|
|
|
114
114
|
|
|
115
115
|
### Cursor IDE
|
|
116
116
|
|
|
117
|
+
`godd init`(Step 3)で `.cursor/mcp.json` が自動生成されます。**`claude mcp add` は不要**です。
|
|
118
|
+
|
|
117
119
|
1. Cursor IDE で対象プロジェクトを開く
|
|
118
120
|
2. `Settings → MCP` で `godd` サーバーが認識されていることを確認
|
|
119
121
|
3. 必要に応じてリロード(`Reload Window`)
|
|
120
122
|
|
|
121
|
-
>
|
|
123
|
+
> 内容を手動で編集する場合は、`env.GODD_CONFIG` がプロジェクトの `config.godd` を指していることを確認してください。
|
|
122
124
|
|
|
123
125
|
### Claude Code
|
|
124
126
|
|
|
125
127
|
Claude Code では `claude mcp` コマンドで MCP サーバーを登録します。
|
|
126
|
-
|
|
128
|
+
**実行順序(重要):** ① `godd init`(Step 3)→ ② `.env` をシェルに読み込む → ③ `claude mcp add`
|
|
129
|
+
|
|
130
|
+
`godd install` 済みであること。**プロジェクトルート** で以下を実行してください。
|
|
131
|
+
|
|
132
|
+
#### 1. 先に `godd init` を完了する(Step 3)
|
|
133
|
+
|
|
134
|
+
Step 3 をスキップしている場合は先に実行してください。`godd init` が `.env` と `config.godd` を生成します。
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
cd /path/to/your-project
|
|
138
|
+
godd init
|
|
139
|
+
```
|
|
127
140
|
|
|
128
|
-
####
|
|
141
|
+
#### 2. `.env` をシェル環境変数として読み込む
|
|
129
142
|
|
|
130
|
-
`godd init` はライセンスキー等をプロジェクトの `.env` に書き込みます。
|
|
131
143
|
MCP 登録時は **シェル環境変数として参照** し、コマンドラインやドキュメントに平文で書かないでください。
|
|
132
144
|
|
|
145
|
+
**macOS / Linux(bash / zsh):**
|
|
146
|
+
|
|
133
147
|
```bash
|
|
134
148
|
cd /path/to/your-project
|
|
135
149
|
|
|
136
|
-
#
|
|
137
|
-
set -a && source .env && set +a
|
|
138
|
-
|
|
150
|
+
# set -a が必須(変数を自動エクスポート)。bare の source .env だけでは不十分な場合がある
|
|
151
|
+
set -a && source .env && set +a
|
|
152
|
+
|
|
153
|
+
# 読み込み確認(値そのものは表示しない)
|
|
154
|
+
test -n "$GODD_LICENSE_KEY" && echo "GODD_LICENSE_KEY: loaded" || echo "GODD_LICENSE_KEY: EMPTY"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Windows (PowerShell):**
|
|
158
|
+
|
|
159
|
+
```powershell
|
|
160
|
+
cd C:\path\to\your-project
|
|
161
|
+
|
|
162
|
+
# .env の各 GODD_* キーを $env: に設定(例)
|
|
163
|
+
Get-Content .env | ForEach-Object {
|
|
164
|
+
if ($_ -match '^\s*([^#=]+)=(.*)$') {
|
|
165
|
+
$name = $matches[1].Trim()
|
|
166
|
+
$value = $matches[2].Trim().Trim('"')
|
|
167
|
+
if ($name -like 'GODD_*') { Set-Item -Path "env:$name" -Value $value }
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# 読み込み確認
|
|
172
|
+
if ($env:GODD_LICENSE_KEY) { "GODD_LICENSE_KEY: loaded" } else { "GODD_LICENSE_KEY: EMPTY" }
|
|
139
173
|
```
|
|
140
174
|
|
|
141
|
-
####
|
|
175
|
+
#### 3. MCP サーバーを登録する
|
|
142
176
|
|
|
143
177
|
```bash
|
|
144
178
|
claude mcp add godd -s local \
|
|
@@ -160,7 +194,7 @@ claude mcp add godd -s local \
|
|
|
160
194
|
|
|
161
195
|
> Windows (PowerShell) では `${PWD}` の代わりに `$PWD.Path`、環境変数展開は `"$env:GODD_LICENSE_KEY"` 形式を使用してください。
|
|
162
196
|
|
|
163
|
-
####
|
|
197
|
+
#### 4. Claude Code を再起動する
|
|
164
198
|
|
|
165
199
|
登録後、Claude Code を一度終了して再起動すると MCP 接続が反映されます。
|
|
166
200
|
|
|
@@ -238,7 +272,9 @@ docs/
|
|
|
238
272
|
|---|---|
|
|
239
273
|
| `godd: command not found` | ターミナルを再起動。それでも解決しない場合は PATH に `~/.godd/bin/`(Windows: `%LOCALAPPDATA%\GoDD\`)を追加 |
|
|
240
274
|
| Cursor が MCP を認識しない | `Settings → MCP` で `godd` が green になっているか確認。`.cursor/mcp.json` の `env.GODD_CONFIG` が正しい絶対パスか確認 |
|
|
241
|
-
| Claude Code が MCP を認識しない | `claude mcp list` で `godd` を確認。`GODD_CONFIG` が `config.godd` の絶対パスか確認。Claude Code を再起動 |
|
|
275
|
+
| Claude Code が MCP を認識しない | `claude mcp list` または `/mcp` で `godd` を確認。`GODD_CONFIG` が `config.godd` の絶対パスか確認。Claude Code を再起動 |
|
|
276
|
+
| `GODD_LICENSE_KEY` が空 | `.env` の読み込み方法を確認。macOS/Linux は `set -a && source .env && set +a`(bare の `source .env` だけでは不十分な場合あり)。PowerShell は各 `GODD_*` を `$env:` に設定 |
|
|
277
|
+
| MCP 登録後も接続できない | ターミナルで `godd server` が起動するか確認。失敗する場合は PATH またはライセンスキーを確認 |
|
|
242
278
|
| ライセンス検証エラー | Registry URL とライセンスキーが正しいか確認。`.env` の `GODD_LICENSE_KEY` または MCP 登録時の環境変数を確認。管理者にライセンス状態を問い合わせ |
|
|
243
279
|
| 「開発元不明」エラー (macOS) | `xattr -d com.apple.quarantine $(which godd)` を実行 |
|
|
244
280
|
|