swallowkit 1.0.0-beta.55 → 1.0.0-beta.56
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/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +154 -157
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/init.d.ts +5 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +178 -25
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +7 -2
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +111 -123
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +127 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +1 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +8 -4
- package/dist/core/config.js.map +1 -1
- package/dist/core/operations/auth-operations.d.ts +57 -0
- package/dist/core/operations/auth-operations.d.ts.map +1 -0
- package/dist/core/operations/auth-operations.js +269 -0
- package/dist/core/operations/auth-operations.js.map +1 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +23 -13
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/file-session.d.ts +53 -0
- package/dist/core/operations/file-session.d.ts.map +1 -0
- package/dist/core/operations/file-session.js +195 -0
- package/dist/core/operations/file-session.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +52 -0
- package/dist/core/operations/provision-operations.d.ts.map +1 -0
- package/dist/core/operations/provision-operations.js +211 -0
- package/dist/core/operations/provision-operations.js.map +1 -0
- package/dist/core/operations/runtime.d.ts +5 -0
- package/dist/core/operations/runtime.d.ts.map +1 -1
- package/dist/core/operations/runtime.js +23 -0
- package/dist/core/operations/runtime.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +21 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -0
- package/dist/core/operations/scaffold-apply.js +57 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js +1 -13
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +43 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -0
- package/dist/core/operations/scaffold-plan.js +195 -0
- package/dist/core/operations/scaffold-plan.js.map +1 -0
- package/dist/core/project/artifacts.d.ts +58 -0
- package/dist/core/project/artifacts.d.ts.map +1 -0
- package/dist/core/project/artifacts.js +179 -0
- package/dist/core/project/artifacts.js.map +1 -0
- package/dist/core/project/boundaries.d.ts +50 -0
- package/dist/core/project/boundaries.d.ts.map +1 -0
- package/dist/core/project/boundaries.js +117 -0
- package/dist/core/project/boundaries.js.map +1 -0
- package/dist/core/project/drift.d.ts +29 -0
- package/dist/core/project/drift.d.ts.map +1 -0
- package/dist/core/project/drift.js +188 -0
- package/dist/core/project/drift.js.map +1 -0
- package/dist/core/project/infra.d.ts +49 -0
- package/dist/core/project/infra.d.ts.map +1 -0
- package/dist/core/project/infra.js +167 -0
- package/dist/core/project/infra.js.map +1 -0
- package/dist/core/project/state.d.ts +15 -0
- package/dist/core/project/state.d.ts.map +1 -0
- package/dist/core/project/state.js +106 -0
- package/dist/core/project/state.js.map +1 -0
- package/dist/core/project/workflows.d.ts +43 -0
- package/dist/core/project/workflows.d.ts.map +1 -0
- package/dist/core/project/workflows.js +255 -0
- package/dist/core/project/workflows.js.map +1 -0
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +24 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/functions-generator.js +9 -9
- package/dist/core/scaffold/nextjs-generator.js +6 -6
- package/dist/core/scaffold/ui-generator.js +19 -19
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +62 -0
- package/dist/core/verify/index.d.ts.map +1 -0
- package/dist/core/verify/index.js +343 -0
- package/dist/core/verify/index.js.map +1 -0
- package/dist/machine/contracts.d.ts +8 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +4 -1
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +9 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +350 -16
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +202 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/types/index.d.ts +14 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/functions-generator.test.ts.snap +17 -17
- package/src/__tests__/__snapshots__/nextjs-generator.test.ts.snap +6 -6
- package/src/__tests__/__snapshots__/ui-generator.test.ts.snap +11 -11
- package/src/__tests__/agent-loop-phase34.test.ts +449 -0
- package/src/__tests__/file-session.test.ts +98 -0
- package/src/__tests__/init.test.ts +50 -0
- package/src/__tests__/mcp.test.ts +113 -5
- package/src/__tests__/plan-apply.test.ts +294 -0
- package/src/cli/commands/add-auth.ts +164 -164
- package/src/cli/commands/init.ts +186 -24
- package/src/cli/commands/scaffold.ts +128 -129
- package/src/cli/index.ts +148 -2
- package/src/core/config.ts +5 -4
- package/src/core/operations/auth-operations.ts +353 -0
- package/src/core/operations/create-model.ts +25 -13
- package/src/core/operations/file-session.ts +210 -0
- package/src/core/operations/provision-operations.ts +297 -0
- package/src/core/operations/runtime.ts +23 -0
- package/src/core/operations/scaffold-apply.ts +106 -0
- package/src/core/operations/scaffold-machine.ts +2 -16
- package/src/core/operations/scaffold-plan.ts +224 -0
- package/src/core/project/artifacts.ts +205 -0
- package/src/core/project/boundaries.ts +170 -0
- package/src/core/project/drift.ts +202 -0
- package/src/core/project/infra.ts +196 -0
- package/src/core/project/state.ts +77 -0
- package/src/core/project/workflows.ts +249 -0
- package/src/core/scaffold/auth-generator.ts +24 -27
- package/src/core/scaffold/functions-generator.ts +9 -9
- package/src/core/scaffold/nextjs-generator.ts +6 -6
- package/src/core/scaffold/ui-generator.ts +19 -19
- package/src/core/verify/index.ts +392 -0
- package/src/machine/contracts.ts +16 -0
- package/src/machine/errors.ts +12 -1
- package/src/machine/index.ts +444 -15
- package/src/mcp/index.ts +198 -2
- package/src/types/index.ts +17 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow Docs & Agent Skills
|
|
3
|
+
*
|
|
4
|
+
* 同一のランブック コンテンツから 2 つの表面を生成する:
|
|
5
|
+
* - `.swallowkit/workflows/*.md` — Coding Agent 非依存の Markdown ランブック
|
|
6
|
+
* (AGENTS.md / CLAUDE.md / copilot-instructions.md から参照される)
|
|
7
|
+
* - `.github/skills/<name>/SKILL.md` — Agent Skills (agentskills.io open standard)。
|
|
8
|
+
* GitHub Copilot (VS Code / CLI / cloud agent) 等が name/description で自動発見し、
|
|
9
|
+
* タスクに関連するときだけ本文をロードする (progressive disclosure)。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
|
|
15
|
+
export interface WorkflowDoc {
|
|
16
|
+
/** `.swallowkit/workflows/` 配下のファイル名 */
|
|
17
|
+
fileName: string;
|
|
18
|
+
title: string;
|
|
19
|
+
content: string;
|
|
20
|
+
/**
|
|
21
|
+
* Agent Skills のスキル名 (ディレクトリ名と一致必須)。
|
|
22
|
+
* agentskills.io 仕様: 小文字英数字とハイフンのみ・64 文字以内。
|
|
23
|
+
* undefined の場合はスキルとして出力しない (README など)。
|
|
24
|
+
*/
|
|
25
|
+
skillName?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Agent Skills の description (最大 1024 文字)。
|
|
28
|
+
* 「何をするか」と「いつ使うか」の両方を書く。
|
|
29
|
+
*/
|
|
30
|
+
skillDescription?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function buildWorkflowDocs(runCmd: string): WorkflowDoc[] {
|
|
34
|
+
const addModel: WorkflowDoc = {
|
|
35
|
+
fileName: "add-model.md",
|
|
36
|
+
title: "Add a new model (autonomous loop)",
|
|
37
|
+
skillName: "swallowkit-add-model",
|
|
38
|
+
skillDescription:
|
|
39
|
+
"Add a new data model to a SwallowKit project and deterministically derive CRUD Functions, BFF routes, UI pages, and Cosmos DB infra from it using the plan/apply/verify loop. Use when asked to add a model, entity, resource, table, or a new CRUD feature.",
|
|
40
|
+
content: `# Workflow: Add a new model
|
|
41
|
+
|
|
42
|
+
Goal: add a data model and derive CRUD/BFF/UI/infra deterministically.
|
|
43
|
+
|
|
44
|
+
## Responsibility boundary
|
|
45
|
+
|
|
46
|
+
- **You author freely**: the model schema (\`shared/models/<name>.ts\`) and any business logic.
|
|
47
|
+
- **SwallowKit generates deterministically**: Functions CRUD, BFF routes, UI pages, OpenAPI, Bicep containers.
|
|
48
|
+
- Query the full contract: \`${runCmd} swallowkit machine inspect boundaries\`
|
|
49
|
+
|
|
50
|
+
## Steps
|
|
51
|
+
|
|
52
|
+
1. Create the model file (deterministic template):
|
|
53
|
+
\`${runCmd} swallowkit machine generate model <name> --overwrite never\`
|
|
54
|
+
2. Edit \`shared/models/<name>.ts\` — add domain fields (free-form authoring).
|
|
55
|
+
3. Compute the change plan (writes nothing):
|
|
56
|
+
\`${runCmd} swallowkit machine plan scaffold <name>\`
|
|
57
|
+
- \`status: "requires-human"\` → review \`data.conflicts\`; only proceed with \`--approve\` if overwriting is intended.
|
|
58
|
+
4. Apply:
|
|
59
|
+
\`${runCmd} swallowkit machine apply scaffold --plan <planId>\`
|
|
60
|
+
5. Verify:
|
|
61
|
+
\`${runCmd} swallowkit machine verify project\`
|
|
62
|
+
- \`summary.done: false\` → \`${runCmd} swallowkit machine explain failure\` and repair, then re-verify.
|
|
63
|
+
6. If \`dev-seeds/\` exists, update seed JSON for the new model.
|
|
64
|
+
|
|
65
|
+
## Terminal states
|
|
66
|
+
|
|
67
|
+
| status | meaning | next |
|
|
68
|
+
|--------|---------|------|
|
|
69
|
+
| complete | operation finished | continue |
|
|
70
|
+
| in-progress | more work remains (e.g. failing checks) | follow nextActions |
|
|
71
|
+
| blocked | prerequisite missing (e.g. stale plan) | re-plan |
|
|
72
|
+
| requires-human | approval needed | ask the human, or pass --approve when instructed |
|
|
73
|
+
| failed | operation failed | inspect error.details |
|
|
74
|
+
`,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const modifyModel: WorkflowDoc = {
|
|
78
|
+
fileName: "modify-model.md",
|
|
79
|
+
title: "Modify an existing model schema",
|
|
80
|
+
skillName: "swallowkit-modify-model",
|
|
81
|
+
skillDescription:
|
|
82
|
+
"Change an existing Zod model schema in a SwallowKit project and re-align every generated artifact (Functions, BFF, UI, infra) via drift inspection and plan/apply regeneration. Use when asked to add/remove/rename model fields, change validation rules, or when inspect drift reports schema-drift.",
|
|
83
|
+
content: `# Workflow: Modify an existing model
|
|
84
|
+
|
|
85
|
+
Goal: change a schema and re-align every generated artifact.
|
|
86
|
+
|
|
87
|
+
## Steps
|
|
88
|
+
|
|
89
|
+
1. Edit \`shared/models/<name>.ts\` (free-form authoring).
|
|
90
|
+
2. Check what drifted:
|
|
91
|
+
\`${runCmd} swallowkit machine inspect drift\`
|
|
92
|
+
- \`schema-drift\` findings list the models whose artifacts are stale.
|
|
93
|
+
3. Plan the regeneration:
|
|
94
|
+
\`${runCmd} swallowkit machine plan scaffold <name>\`
|
|
95
|
+
- Conflicts mean generated files were hand-edited after generation. Decide: keep the hand-edit (move the logic to an ai-authored file first) or overwrite with \`--approve\`.
|
|
96
|
+
4. Apply: \`${runCmd} swallowkit machine apply scaffold --plan <planId>\`
|
|
97
|
+
5. Verify: \`${runCmd} swallowkit machine verify project\`
|
|
98
|
+
|
|
99
|
+
## Guardrail
|
|
100
|
+
|
|
101
|
+
Never hand-edit managed artifacts to implement schema changes — always change the model
|
|
102
|
+
and regenerate. Hand-edits are detected as drift and block unattended overwrites.
|
|
103
|
+
`,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const verifyRepair: WorkflowDoc = {
|
|
107
|
+
fileName: "verify-and-repair.md",
|
|
108
|
+
title: "Verify and repair loop",
|
|
109
|
+
skillName: "swallowkit-verify-repair",
|
|
110
|
+
skillDescription:
|
|
111
|
+
"Converge a SwallowKit project to a verified state: run structure/drift/typecheck (and build/lint/test) checks, read evidence with explain failure, repair, and repeat. Use after any code or schema change, when verification fails, when the build/lint/tests are broken, or before finishing a task.",
|
|
112
|
+
content: `# Workflow: Verify and repair
|
|
113
|
+
|
|
114
|
+
Goal: converge the project to a verified state after any change.
|
|
115
|
+
|
|
116
|
+
## Loop
|
|
117
|
+
|
|
118
|
+
1. \`${runCmd} swallowkit machine verify project\`
|
|
119
|
+
- default checks: structure, drift, typecheck (+ project-specific checks from swallowkit.config \`verify.checks\`)
|
|
120
|
+
- opt-in heavier checks: \`--checks structure,drift,typecheck,build,lint,test\`
|
|
121
|
+
2. If \`summary.done\` is false:
|
|
122
|
+
- \`${runCmd} swallowkit machine explain failure\` → evidence (logTail, findings, violations) and suggestedActions per check.
|
|
123
|
+
- Repair:
|
|
124
|
+
- \`drift\` failures → follow each finding's \`repairAction\` (usually re-run plan/apply scaffold).
|
|
125
|
+
- \`structure\` failures → follow \`suggestedFix\` per violation.
|
|
126
|
+
- \`typecheck\`/\`build\`/\`lint\`/\`test\` failures → fix the code in ai-authored files.
|
|
127
|
+
3. Repeat until \`summary.done: true\`.
|
|
128
|
+
|
|
129
|
+
## Exit criteria
|
|
130
|
+
|
|
131
|
+
Stop and ask a human when:
|
|
132
|
+
- the same check fails 3 times in a row with the same evidence, or
|
|
133
|
+
- a repair would require overwriting hand-edited files (\`requires-human\`).
|
|
134
|
+
`,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const provision: WorkflowDoc = {
|
|
138
|
+
fileName: "provision.md",
|
|
139
|
+
title: "Provision Azure resources (plan/apply with approval gate)",
|
|
140
|
+
skillName: "swallowkit-provision",
|
|
141
|
+
skillDescription:
|
|
142
|
+
"Provision Azure resources (Static Web Apps, Functions, Cosmos DB) for a SwallowKit project using the two-phase plan/apply flow with a mandatory human approval gate. Use when asked to deploy infrastructure, provision Azure, create resource groups, or run Bicep deployments.",
|
|
143
|
+
content: `# Workflow: Provision Azure resources
|
|
144
|
+
|
|
145
|
+
Goal: deploy the Bicep infrastructure with an explicit human approval gate.
|
|
146
|
+
|
|
147
|
+
## Rules
|
|
148
|
+
|
|
149
|
+
- Provisioning **always** requires human approval — \`plan provision\` returns
|
|
150
|
+
\`status: "requires-human"\` and \`requiresApproval: true\` by design.
|
|
151
|
+
- Never pass \`--approve\` to \`apply provision\` unless a human explicitly
|
|
152
|
+
instructed you to provision in this session.
|
|
153
|
+
- Inspect infra locally first — no \`az\` calls are needed for inspection.
|
|
154
|
+
|
|
155
|
+
## Steps
|
|
156
|
+
|
|
157
|
+
1. Inspect the infrastructure definition (pure local Bicep analysis):
|
|
158
|
+
\`${runCmd} swallowkit machine inspect infra\`
|
|
159
|
+
- review parameters, modules, outputs, and container wiring warnings.
|
|
160
|
+
2. Compute the provisioning plan (no resources are created):
|
|
161
|
+
\`${runCmd} swallowkit machine plan provision -g <resource-group> --location <region>\`
|
|
162
|
+
- optional: \`--what-if\` to include an \`az deployment ... what-if\` preview.
|
|
163
|
+
- the plan lists the exact \`az\` commands that would run.
|
|
164
|
+
3. Present the plan to the human and wait for explicit approval.
|
|
165
|
+
4. Only after approval:
|
|
166
|
+
\`${runCmd} swallowkit machine apply provision --plan <planId> --approve\`
|
|
167
|
+
5. Verify the deployment outputs and report them.
|
|
168
|
+
|
|
169
|
+
## Terminal states
|
|
170
|
+
|
|
171
|
+
\`apply provision\` without \`--approve\` fails with \`approval-required\` —
|
|
172
|
+
this is the guardrail working as intended, not an error to work around.
|
|
173
|
+
`,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const index: WorkflowDoc = {
|
|
177
|
+
fileName: "README.md",
|
|
178
|
+
title: "SwallowKit agent workflows",
|
|
179
|
+
content: `# SwallowKit Agent Workflows
|
|
180
|
+
|
|
181
|
+
Agent-agnostic runbooks for autonomous development loops. Machine-readable
|
|
182
|
+
interfaces (\`swallowkit machine ...\` / MCP \`swallowkit_*\` tools) return a
|
|
183
|
+
\`status\` terminal state and \`nextActions\`, so agents can chain steps without
|
|
184
|
+
parsing human-oriented output.
|
|
185
|
+
|
|
186
|
+
These runbooks are also installed as Agent Skills (agentskills.io format)
|
|
187
|
+
under \`.github/skills/\` so skills-aware agents load them automatically.
|
|
188
|
+
|
|
189
|
+
| Workflow | File | Skill |
|
|
190
|
+
|----------|------|-------|
|
|
191
|
+
| Add a new model | [add-model.md](./add-model.md) | \`swallowkit-add-model\` |
|
|
192
|
+
| Modify an existing model | [modify-model.md](./modify-model.md) | \`swallowkit-modify-model\` |
|
|
193
|
+
| Verify and repair | [verify-and-repair.md](./verify-and-repair.md) | \`swallowkit-verify-repair\` |
|
|
194
|
+
| Provision Azure resources | [provision.md](./provision.md) | \`swallowkit-provision\` |
|
|
195
|
+
|
|
196
|
+
## Responsibility boundary (summary)
|
|
197
|
+
|
|
198
|
+
- **ai-authored** (write freely): \`shared/models/\`, business logic, custom pages/components, tests.
|
|
199
|
+
- **deterministic** (never hand-edit): artifacts generated by SwallowKit (see \`.swallowkit/artifacts.json\`) and \`.swallowkit/\` metadata. Change models/config and regenerate via plan/apply.
|
|
200
|
+
- **shared** (edit outside markers): \`infra/main.bicep\`, \`functions/function_app.py\`, \`shared/index.ts\`.
|
|
201
|
+
|
|
202
|
+
Full machine-readable contract: \`${runCmd} swallowkit machine inspect boundaries\`
|
|
203
|
+
`,
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
return [index, addModel, modifyModel, verifyRepair, provision];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const WORKFLOWS_DIR = path.join(".swallowkit", "workflows");
|
|
210
|
+
export const SKILLS_DIR = path.join(".github", "skills");
|
|
211
|
+
|
|
212
|
+
/** `.swallowkit/workflows/*.md` を書き出し、書き込んだ相対パスを返す。 */
|
|
213
|
+
export function writeWorkflowDocs(projectDir: string, runCmd: string): string[] {
|
|
214
|
+
const dir = path.join(projectDir, WORKFLOWS_DIR);
|
|
215
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
216
|
+
const written: string[] = [];
|
|
217
|
+
for (const doc of buildWorkflowDocs(runCmd)) {
|
|
218
|
+
fs.writeFileSync(path.join(dir, doc.fileName), doc.content, "utf-8");
|
|
219
|
+
written.push(path.posix.join(".swallowkit", "workflows", doc.fileName));
|
|
220
|
+
}
|
|
221
|
+
return written;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Agent Skills (agentskills.io) の SKILL.md コンテンツを組み立てる。 */
|
|
225
|
+
export function buildAgentSkills(runCmd: string): Array<{ skillName: string; content: string }> {
|
|
226
|
+
return buildWorkflowDocs(runCmd)
|
|
227
|
+
.filter((doc): doc is WorkflowDoc & { skillName: string; skillDescription: string } =>
|
|
228
|
+
Boolean(doc.skillName && doc.skillDescription)
|
|
229
|
+
)
|
|
230
|
+
.map((doc) => ({
|
|
231
|
+
skillName: doc.skillName,
|
|
232
|
+
content: `---\nname: ${doc.skillName}\ndescription: ${doc.skillDescription}\nmetadata:\n generator: swallowkit\n---\n\n${doc.content}`,
|
|
233
|
+
}));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* `.github/skills/<name>/SKILL.md` を書き出し、書き込んだ相対パスを返す。
|
|
238
|
+
* agentskills.io 仕様: ディレクトリ名は frontmatter の name と一致必須。
|
|
239
|
+
*/
|
|
240
|
+
export function writeAgentSkills(projectDir: string, runCmd: string): string[] {
|
|
241
|
+
const written: string[] = [];
|
|
242
|
+
for (const skill of buildAgentSkills(runCmd)) {
|
|
243
|
+
const dir = path.join(projectDir, SKILLS_DIR, skill.skillName);
|
|
244
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
245
|
+
fs.writeFileSync(path.join(dir, "SKILL.md"), skill.content, "utf-8");
|
|
246
|
+
written.push(path.posix.join(".github", "skills", skill.skillName, "SKILL.md"));
|
|
247
|
+
}
|
|
248
|
+
return written;
|
|
249
|
+
}
|
|
@@ -20,6 +20,7 @@ export function generateNamedAuthRouterTS(config: NormalizedAuthConfig): string
|
|
|
20
20
|
})).join("\n");
|
|
21
21
|
const verifiers = external.map(([name]) => { const id = safeIdentifier(name); return `${JSON.stringify(name)}: async (token: string) => { try { return await verify_${id}(token); } catch (e) { if (e instanceof Error_${id}) throw new AuthError(e.status, e.message); throw e; } }`; }).join(",\n ");
|
|
22
22
|
return `import type { HttpRequest, HttpResponseInit } from '@azure/functions';
|
|
23
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- generated multi-scheme router works with dynamically shaped principals */
|
|
23
24
|
${imports}
|
|
24
25
|
export type AuthPrincipal = { subject: string; scheme: string; issuer: string; roles: string[]; claims: Record<string, unknown>; /** @deprecated */ userId?: string; /** @deprecated */ userDetails?: string };
|
|
25
26
|
export class AuthError extends Error { constructor(public status: 401 | 403 | 503, message: string) { super(message); } }
|
|
@@ -283,6 +284,7 @@ export function generateAuthFunctionsTS(
|
|
|
283
284
|
'SELECT * FROM ${config.userTable} WHERE ${config.loginIdColumn} = ?',
|
|
284
285
|
[body.loginId]
|
|
285
286
|
);
|
|
287
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- mysql2 rows are dynamically shaped
|
|
286
288
|
const users = rows as any[];`
|
|
287
289
|
: provider === "postgres"
|
|
288
290
|
? `const result = await ${connVar}.query(
|
|
@@ -360,7 +362,7 @@ app.http('auth-login', {
|
|
|
360
362
|
} finally {
|
|
361
363
|
${cleanup}
|
|
362
364
|
}
|
|
363
|
-
} catch (error
|
|
365
|
+
} catch (error) {
|
|
364
366
|
context.error('Login error:', error);
|
|
365
367
|
return { status: 500, jsonBody: { error: 'Login failed' } };
|
|
366
368
|
}
|
|
@@ -998,7 +1000,7 @@ export async function POST(request: NextRequest) {
|
|
|
998
1000
|
maxAge: 86400,
|
|
999
1001
|
});
|
|
1000
1002
|
return response;
|
|
1001
|
-
} catch (error
|
|
1003
|
+
} catch (error) {
|
|
1002
1004
|
console.error('[BFF] Login error:', error);
|
|
1003
1005
|
return NextResponse.json({ error: 'Login failed' }, { status: 500 });
|
|
1004
1006
|
}
|
|
@@ -1043,7 +1045,7 @@ export async function GET() {
|
|
|
1043
1045
|
|
|
1044
1046
|
const data = await result.json();
|
|
1045
1047
|
return NextResponse.json(data);
|
|
1046
|
-
} catch (error
|
|
1048
|
+
} catch (error) {
|
|
1047
1049
|
console.error('[BFF] Auth/me error:', error);
|
|
1048
1050
|
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
|
|
1049
1051
|
}
|
|
@@ -1212,7 +1214,7 @@ export default function LoginPage() {
|
|
|
1212
1214
|
export function generateAuthContext(): string {
|
|
1213
1215
|
return `'use client';
|
|
1214
1216
|
|
|
1215
|
-
import { createContext, useContext, useEffect, useState,
|
|
1217
|
+
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
|
1216
1218
|
import { useRouter } from 'next/navigation';
|
|
1217
1219
|
|
|
1218
1220
|
interface AuthUser {
|
|
@@ -1239,26 +1241,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|
|
1239
1241
|
const [loading, setLoading] = useState(true);
|
|
1240
1242
|
const router = useRouter();
|
|
1241
1243
|
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1244
|
+
useEffect(() => {
|
|
1245
|
+
let cancelled = false;
|
|
1246
|
+
fetch('/api/auth/me')
|
|
1247
|
+
.then((res) => (res.ok ? res.json() : null))
|
|
1248
|
+
.catch(() => null)
|
|
1249
|
+
.then((data) => {
|
|
1250
|
+
if (cancelled) return;
|
|
1247
1251
|
setUser(data);
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
} finally {
|
|
1254
|
-
setLoading(false);
|
|
1255
|
-
}
|
|
1252
|
+
setLoading(false);
|
|
1253
|
+
});
|
|
1254
|
+
return () => {
|
|
1255
|
+
cancelled = true;
|
|
1256
|
+
};
|
|
1256
1257
|
}, []);
|
|
1257
1258
|
|
|
1258
|
-
useEffect(() => {
|
|
1259
|
-
fetchUser();
|
|
1260
|
-
}, [fetchUser]);
|
|
1261
|
-
|
|
1262
1259
|
const login = async (loginId: string, password: string) => {
|
|
1263
1260
|
try {
|
|
1264
1261
|
const res = await fetch('/api/auth/login', {
|
|
@@ -1311,7 +1308,7 @@ export function useAuth() {
|
|
|
1311
1308
|
// ============================================================
|
|
1312
1309
|
|
|
1313
1310
|
export function generateBFFCallFunctionWithAuth(): string {
|
|
1314
|
-
return `import {
|
|
1311
|
+
return `import { NextResponse } from 'next/server';
|
|
1315
1312
|
import { headers } from 'next/headers';
|
|
1316
1313
|
import { z } from 'zod/v4';
|
|
1317
1314
|
|
|
@@ -1335,13 +1332,13 @@ function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string
|
|
|
1335
1332
|
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
1336
1333
|
}
|
|
1337
1334
|
|
|
1338
|
-
interface CallFunctionConfig<TInput =
|
|
1335
|
+
interface CallFunctionConfig<TInput = unknown, TOutput = unknown> {
|
|
1339
1336
|
/** HTTP メソッド */
|
|
1340
1337
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
1341
1338
|
/** Azure Functions のパス (例: '/api/todo', '/api/todo/123') */
|
|
1342
1339
|
path: string;
|
|
1343
1340
|
/** リクエストボディ (POST/PUT 用) */
|
|
1344
|
-
body?:
|
|
1341
|
+
body?: unknown;
|
|
1345
1342
|
/** 入力バリデーション用 Zod スキーマ (省略時はバリデーションなし) */
|
|
1346
1343
|
inputSchema?: z.ZodSchema<TInput>;
|
|
1347
1344
|
/** 出力バリデーション用 Zod スキーマ (省略時はそのまま返す) */
|
|
@@ -1350,7 +1347,7 @@ interface CallFunctionConfig<TInput = any, TOutput = any> {
|
|
|
1350
1347
|
successStatus?: number;
|
|
1351
1348
|
}
|
|
1352
1349
|
|
|
1353
|
-
export async function callFunction<TInput =
|
|
1350
|
+
export async function callFunction<TInput = unknown, TOutput = unknown>(
|
|
1354
1351
|
config: CallFunctionConfig<TInput, TOutput>
|
|
1355
1352
|
): Promise<NextResponse> {
|
|
1356
1353
|
const { method, path, body, inputSchema, responseSchema, successStatus = 200 } = config;
|
|
@@ -1428,10 +1425,10 @@ export async function callFunction<TInput = any, TOutput = any>(
|
|
|
1428
1425
|
}
|
|
1429
1426
|
|
|
1430
1427
|
return NextResponse.json(data, { status: successStatus });
|
|
1431
|
-
} catch (error
|
|
1428
|
+
} catch (error) {
|
|
1432
1429
|
console.error(\`[BFF] Error:\`, error);
|
|
1433
1430
|
return NextResponse.json(
|
|
1434
|
-
{ error: error.message
|
|
1431
|
+
{ error: error instanceof Error ? error.message : 'Internal server error' },
|
|
1435
1432
|
{ status: 500 }
|
|
1436
1433
|
);
|
|
1437
1434
|
}
|
|
@@ -83,7 +83,7 @@ app.http('${modelCamel}-get-all', {
|
|
|
83
83
|
],
|
|
84
84
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
85
85
|
try {${readGuard}
|
|
86
|
-
const documents = context.extraInputs.get('cosmosInput') as
|
|
86
|
+
const documents = context.extraInputs.get('cosmosInput') as unknown[];
|
|
87
87
|
|
|
88
88
|
if (!documents || !Array.isArray(documents)) {
|
|
89
89
|
return { status: 200, jsonBody: [] };
|
|
@@ -119,7 +119,7 @@ app.http('${modelCamel}-create', {
|
|
|
119
119
|
],
|
|
120
120
|
handler: async (request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> => {
|
|
121
121
|
try {${writeGuard}
|
|
122
|
-
const body = await request.json() as
|
|
122
|
+
const body = await request.json() as Record<string, unknown>;
|
|
123
123
|
|
|
124
124
|
const { id, createdAt, updatedAt, ...userData } = body;
|
|
125
125
|
const now = new Date().toISOString();
|
|
@@ -258,12 +258,12 @@ app.http('${modelCamel}-update', {
|
|
|
258
258
|
return { status: 400, jsonBody: { error: 'ID is required' } };
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
const existingDocument = context.extraInputs.get('cosmosInput') as
|
|
261
|
+
const existingDocument = context.extraInputs.get('cosmosInput') as Record<string, unknown> | undefined;
|
|
262
262
|
if (!existingDocument) {
|
|
263
263
|
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
const body = await request.json() as
|
|
266
|
+
const body = await request.json() as Record<string, unknown>;
|
|
267
267
|
const { createdAt, updatedAt, ...userData } = body;
|
|
268
268
|
|
|
269
269
|
const dataWithManagedFields = {
|
|
@@ -315,7 +315,7 @@ app.http('${modelCamel}-update', {
|
|
|
315
315
|
}
|
|
316
316
|
|
|
317
317
|
const existingDocument = resources[0];
|
|
318
|
-
const body = await request.json() as
|
|
318
|
+
const body = await request.json() as Record<string, unknown>;
|
|
319
319
|
const { createdAt, updatedAt, ...userData } = body;
|
|
320
320
|
|
|
321
321
|
const dataWithManagedFields = {
|
|
@@ -364,8 +364,8 @@ app.http('${modelCamel}-delete', {
|
|
|
364
364
|
context.log(\`Deleted item \${id} from \${containerName}\`);
|
|
365
365
|
|
|
366
366
|
return { status: 204 };
|
|
367
|
-
} catch (error
|
|
368
|
-
if (error.code === 404) {
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if ((error as { code?: number }).code === 404) {
|
|
369
369
|
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
370
370
|
}
|
|
371
371
|
${authCatchBlock} context.error(\`Error deleting item from \${containerName}:\`, error);
|
|
@@ -405,8 +405,8 @@ app.http('${modelCamel}-delete', {
|
|
|
405
405
|
context.log(\`Deleted item \${id} from \${containerName}\`);
|
|
406
406
|
|
|
407
407
|
return { status: 204 };
|
|
408
|
-
} catch (error
|
|
409
|
-
if (error.code === 404) {
|
|
408
|
+
} catch (error) {
|
|
409
|
+
if ((error as { code?: number }).code === 404) {
|
|
410
410
|
return { status: 404, jsonBody: { error: 'Item not found' } };
|
|
411
411
|
}
|
|
412
412
|
${authCatchBlock} context.error(\`Error deleting item from \${containerName}:\`, error);
|
|
@@ -10,7 +10,7 @@ import { ModelInfo, toCamelCase } from "./model-parser";
|
|
|
10
10
|
* BFF callFunction ヘルパー (lib/api/call-function.ts) のコードを生成
|
|
11
11
|
*/
|
|
12
12
|
export function generateBFFCallFunction(): string {
|
|
13
|
-
return `import {
|
|
13
|
+
return `import { NextResponse } from 'next/server';
|
|
14
14
|
import { z } from 'zod/v4';
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -43,13 +43,13 @@ function getFunctionsKeyHeaders(functionsBaseUrl: string): Record<string, string
|
|
|
43
43
|
throw new Error('BACKEND_FUNCTIONS_KEY is required for non-local Functions calls');
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
interface CallFunctionConfig<TInput =
|
|
46
|
+
interface CallFunctionConfig<TInput = unknown, TOutput = unknown> {
|
|
47
47
|
/** HTTP メソッド */
|
|
48
48
|
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
49
49
|
/** Azure Functions のパス (例: '/api/todo', '/api/todo/123') */
|
|
50
50
|
path: string;
|
|
51
51
|
/** リクエストボディ (POST/PUT 用) */
|
|
52
|
-
body?:
|
|
52
|
+
body?: unknown;
|
|
53
53
|
/** 入力バリデーション用 Zod スキーマ (省略時はバリデーションなし) */
|
|
54
54
|
inputSchema?: z.ZodSchema<TInput>;
|
|
55
55
|
/** 出力バリデーション用 Zod スキーマ (省略時はそのまま返す) */
|
|
@@ -58,7 +58,7 @@ interface CallFunctionConfig<TInput = any, TOutput = any> {
|
|
|
58
58
|
successStatus?: number;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
export async function callFunction<TInput =
|
|
61
|
+
export async function callFunction<TInput = unknown, TOutput = unknown>(
|
|
62
62
|
config: CallFunctionConfig<TInput, TOutput>
|
|
63
63
|
): Promise<NextResponse> {
|
|
64
64
|
const { method, path, body, inputSchema, responseSchema, successStatus = 200 } = config;
|
|
@@ -121,10 +121,10 @@ export async function callFunction<TInput = any, TOutput = any>(
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
return NextResponse.json(data, { status: successStatus });
|
|
124
|
-
} catch (error
|
|
124
|
+
} catch (error) {
|
|
125
125
|
console.error(\`[BFF] Error:\`, error);
|
|
126
126
|
return NextResponse.json(
|
|
127
|
-
{ error: error.message
|
|
127
|
+
{ error: error instanceof Error ? error.message : 'Internal server error' },
|
|
128
128
|
{ status: 500 }
|
|
129
129
|
);
|
|
130
130
|
}
|
|
@@ -71,7 +71,7 @@ export function generateListPage(model: ModelInfo, sharedPackageName: string, au
|
|
|
71
71
|
const refModelCamel = toCamelCase(refModel);
|
|
72
72
|
return ` fetch('/api/${refModelCamel}')
|
|
73
73
|
.then(res => res.json())
|
|
74
|
-
.then((data:
|
|
74
|
+
.then((data: Array<{ id: string; name?: string; title?: string }>) => {
|
|
75
75
|
const map: Record<string, string> = {};
|
|
76
76
|
data.forEach(item => {
|
|
77
77
|
// name または title フィールドを表示用文字列として使用
|
|
@@ -138,8 +138,8 @@ ${hasForeignKeys ? foreignKeyFetches : ''}
|
|
|
138
138
|
if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
|
|
139
139
|
|
|
140
140
|
set${modelName}s(${modelCamel}s.filter((item) => item.id !== id));
|
|
141
|
-
} catch (err
|
|
142
|
-
alert(\`Error: \${err.message}\`);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
alert(\`Error: \${err instanceof Error ? err.message : String(err)}\`);
|
|
143
143
|
}
|
|
144
144
|
};
|
|
145
145
|
|
|
@@ -216,7 +216,7 @@ ${displayFields.map(f => {
|
|
|
216
216
|
const displayField = f.nestedDisplayField || 'name';
|
|
217
217
|
if (f.isArray) {
|
|
218
218
|
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
219
|
-
{Array.isArray(item.${f.name}) ? item.${f.name}.map((ref:
|
|
219
|
+
{Array.isArray(item.${f.name}) ? item.${f.name}.map((ref: unknown) => (ref as { ${displayField}?: string } | null)?.${displayField} || '-').join(', ') : '-'}
|
|
220
220
|
</td>`;
|
|
221
221
|
} else {
|
|
222
222
|
return ` <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
|
@@ -317,7 +317,7 @@ export function generateDetailPage(model: ModelInfo, sharedPackageName: string,
|
|
|
317
317
|
const refModelCamel = toCamelCase(refModel);
|
|
318
318
|
return ` fetch('/api/${refModelCamel}')
|
|
319
319
|
.then(res => res.json())
|
|
320
|
-
.then((data:
|
|
320
|
+
.then((data: Array<{ id: string; name?: string; title?: string }>) => {
|
|
321
321
|
const map: Record<string, string> = {};
|
|
322
322
|
data.forEach(item => {
|
|
323
323
|
map[item.id] = item.name || item.title || item.id;
|
|
@@ -389,8 +389,8 @@ ${hasForeignKeys ? foreignKeyFetches : ''}
|
|
|
389
389
|
if (!res.ok) throw new Error('Failed to delete ${modelCamel}');
|
|
390
390
|
|
|
391
391
|
router.push('/${modelKebab}');
|
|
392
|
-
} catch (err
|
|
393
|
-
alert(\`Error: \${err.message}\`);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
alert(\`Error: \${err instanceof Error ? err.message : String(err)}\`);
|
|
394
394
|
}
|
|
395
395
|
};
|
|
396
396
|
|
|
@@ -455,7 +455,7 @@ ${model.fields.map(f => {
|
|
|
455
455
|
if (f.isArray) {
|
|
456
456
|
return ` <div>
|
|
457
457
|
<dt className="text-sm font-medium text-gray-500 dark:text-gray-400">${label}</dt>
|
|
458
|
-
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{Array.isArray(${modelCamel}.${f.name}) ? ${modelCamel}.${f.name}.map((ref:
|
|
458
|
+
<dd className="mt-1 text-sm text-gray-900 dark:text-gray-100">{Array.isArray(${modelCamel}.${f.name}) ? ${modelCamel}.${f.name}.map((ref: unknown) => (ref as { ${displayField}?: string } | null)?.${displayField} || '-').join(', ') : '-'}</dd>
|
|
459
459
|
</div>`;
|
|
460
460
|
} else {
|
|
461
461
|
return ` <div>
|
|
@@ -547,7 +547,7 @@ export function generateFormComponent(model: ModelInfo, sharedPackageName: strin
|
|
|
547
547
|
return ` // ${refModelPascal} の一覧を取得
|
|
548
548
|
fetch('/api/${refModelCamel}')
|
|
549
549
|
.then(res => res.json())
|
|
550
|
-
.then(data => set${refModelPascal}Options(data.map((item:
|
|
550
|
+
.then(data => set${refModelPascal}Options(data.map((item: Record<string, string>) => ({ id: item.id, name: item.${displayField} || item.name || item.title || item.id }))))
|
|
551
551
|
.catch(err => console.error('Failed to load ${refModelPascal} options:', err));`;
|
|
552
552
|
}).join('\n');
|
|
553
553
|
|
|
@@ -581,7 +581,7 @@ ${formFields.map(f => {
|
|
|
581
581
|
// ネストスキーマの場合は参照ID(単一: string, 配列: string[])を管理
|
|
582
582
|
if (f.isNestedSchema) {
|
|
583
583
|
if (f.isArray) {
|
|
584
|
-
return ` ${f.name}Ids: initialData?.${f.name} ? (Array.isArray(initialData.${f.name}) ? initialData.${f.name}.map((item:
|
|
584
|
+
return ` ${f.name}Ids: initialData?.${f.name} ? (Array.isArray(initialData.${f.name}) ? initialData.${f.name}.map((item: { id: string }) => item.id) : []) : [] as string[],`;
|
|
585
585
|
} else {
|
|
586
586
|
return ` ${f.name}Id: initialData?.${f.name}?.id ?? '',`;
|
|
587
587
|
}
|
|
@@ -615,7 +615,7 @@ ${needsUseEffect ? `
|
|
|
615
615
|
${hasForignKeys ? foreignKeyFields.map(f => ` // ${f.referencedModel} の一覧を取得
|
|
616
616
|
fetch('/api/${toKebabCase(f.referencedModel!)}')
|
|
617
617
|
.then(res => res.json())
|
|
618
|
-
.then(data => set${f.referencedModel}Options(data.map((item:
|
|
618
|
+
.then(data => set${f.referencedModel}Options(data.map((item: Record<string, string>) => ({ id: item.id, name: item.name || item.title || item.id }))))
|
|
619
619
|
.catch(err => console.error('Failed to load ${f.referencedModel} options:', err));`).join('\n') : ''}
|
|
620
620
|
${hasNestedSchemas ? nestedSchemaFetches : ''}
|
|
621
621
|
}, []);
|
|
@@ -627,7 +627,7 @@ ${hasNestedSchemas ? nestedSchemaFetches : ''}
|
|
|
627
627
|
|
|
628
628
|
try {
|
|
629
629
|
// Array フィールドをカンマ区切りから配列に変換
|
|
630
|
-
const submitData:
|
|
630
|
+
const submitData: Record<string, unknown> = { ...formData };
|
|
631
631
|
${formFields.filter(f => f.isArray && !f.isNestedSchema).map(f => ` if (typeof submitData.${f.name} === 'string') {
|
|
632
632
|
submitData.${f.name} = submitData.${f.name}.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0);
|
|
633
633
|
}`).join('\n')}
|
|
@@ -651,7 +651,7 @@ ${nestedSchemaFields.map(f => {
|
|
|
651
651
|
if (f.isArray) {
|
|
652
652
|
return ` // ${refModelPascal} 配列参照の変換
|
|
653
653
|
if (submitData.${f.name}Ids) {
|
|
654
|
-
submitData.${f.name} = submitData.${f.name}Ids
|
|
654
|
+
submitData.${f.name} = (submitData.${f.name}Ids as string[])
|
|
655
655
|
.map((refId: string) => ${refModelCamel}Options.find(opt => opt.id === refId))
|
|
656
656
|
.filter(Boolean);
|
|
657
657
|
delete submitData.${f.name}Ids;
|
|
@@ -688,17 +688,17 @@ ${nestedSchemaFields.map(f => {
|
|
|
688
688
|
}
|
|
689
689
|
|
|
690
690
|
router.push('/${toKebabCase(modelName)}');
|
|
691
|
-
} catch (err
|
|
692
|
-
if (err.
|
|
691
|
+
} catch (err) {
|
|
692
|
+
if (err instanceof z.ZodError) {
|
|
693
693
|
// Zod validation errors
|
|
694
694
|
const fieldErrors: Record<string, string> = {};
|
|
695
|
-
err.issues.forEach((
|
|
696
|
-
const field =
|
|
697
|
-
fieldErrors[field] =
|
|
695
|
+
err.issues.forEach((issue) => {
|
|
696
|
+
const field = String(issue.path[0]);
|
|
697
|
+
fieldErrors[field] = issue.message;
|
|
698
698
|
});
|
|
699
699
|
setErrors(fieldErrors);
|
|
700
700
|
} else {
|
|
701
|
-
alert(\`Error: \${err.message}\`);
|
|
701
|
+
alert(\`Error: \${err instanceof Error ? err.message : String(err)}\`);
|
|
702
702
|
}
|
|
703
703
|
setLoading(false);
|
|
704
704
|
}
|