specifian 0.1.0-beta.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 +314 -0
- package/dist/chunk-774NFJLC.js +267 -0
- package/dist/chunk-JY5QLNZ4.js +127 -0
- package/dist/cli/index.js +1757 -0
- package/dist/client/assets/Assistant-Bold-gm-uSS1B.woff2 +0 -0
- package/dist/client/assets/Assistant-Medium-DrcxCXg3.woff2 +0 -0
- package/dist/client/assets/Assistant-Regular-DVxZuzxb.woff2 +0 -0
- package/dist/client/assets/Assistant-SemiBold-SCI4bEL9.woff2 +0 -0
- package/dist/client/assets/ar-SA-G6X2FPQ2-Dd0X68bc.js +10 -0
- package/dist/client/assets/arc-OeTCl9Oy.js +1 -0
- package/dist/client/assets/architecture-7EHR7CIX-CaxcM-P5.js +1 -0
- package/dist/client/assets/architectureDiagram-3BPJPVTR-BIjzr_cP.js +36 -0
- package/dist/client/assets/array-BifhSqXX.js +1 -0
- package/dist/client/assets/az-AZ-76LH7QW2-CikYmJ_f.js +1 -0
- package/dist/client/assets/bg-BG-XCXSNQG7-3zBzA4I3.js +5 -0
- package/dist/client/assets/blockDiagram-GPEHLZMM-K767VD5J.js +132 -0
- package/dist/client/assets/bn-BD-2XOGV67Q-F99bvhop.js +5 -0
- package/dist/client/assets/c4Diagram-AAUBKEIU-Su5ecvGw.js +10 -0
- package/dist/client/assets/ca-ES-6MX7JW3Y-tT1B4cci.js +8 -0
- package/dist/client/assets/channel-B5WJ2uRW.js +1 -0
- package/dist/client/assets/chunk-2J33WTMH-CRsWeOs1.js +1 -0
- package/dist/client/assets/chunk-3OPIFGDE-Ba3bqwD7.js +62 -0
- package/dist/client/assets/chunk-4BX2VUAB-G5rryZd4.js +1 -0
- package/dist/client/assets/chunk-55IACEB6-pY-b07PU.js +1 -0
- package/dist/client/assets/chunk-5ZQYHXKU-txrrIoic.js +2 -0
- package/dist/client/assets/chunk-6U3AYISY-C6-Mtja1.js +12 -0
- package/dist/client/assets/chunk-727SXJPM-B4WReBxI.js +206 -0
- package/dist/client/assets/chunk-AQP2D5EJ-C8Hs2uxD.js +231 -0
- package/dist/client/assets/chunk-BSJP7CBP-Dc0utuAA.js +1 -0
- package/dist/client/assets/chunk-CSCIHK7Q-D5y67sbU.js +124 -0
- package/dist/client/assets/chunk-EIO257PC-Bx5t8kU4.js +22 -0
- package/dist/client/assets/chunk-FMBD7UC4-RPQDsV4e.js +15 -0
- package/dist/client/assets/chunk-K2UTITRG-3wps4yb2.js +35 -0
- package/dist/client/assets/chunk-KSCS5N6A-CUZckNfe.js +10 -0
- package/dist/client/assets/chunk-L5ZTLDWV-CKWh4nJc.js +1 -0
- package/dist/client/assets/chunk-ND2GUHAM-CRxqa1cH.js +1 -0
- package/dist/client/assets/chunk-NNHCCRGN-DlpIbxXb.js +159 -0
- package/dist/client/assets/chunk-NZK2D7GU-DzyoezBz.js +1 -0
- package/dist/client/assets/chunk-O5CBEL6O-DW5LwnUN.js +70 -0
- package/dist/client/assets/chunk-QZHKN3VN-D8ZpkZ2T.js +1 -0
- package/dist/client/assets/chunk-SRAX5OIU-CiRgnkYo.js +1 -0
- package/dist/client/assets/chunk-Z3N5DIM6-D9TMVU7m.js +1 -0
- package/dist/client/assets/chunk-ZUYEQ4TG-BzFOpW--.js +7 -0
- package/dist/client/assets/chunk-aKtaBQYM.js +1 -0
- package/dist/client/assets/classDiagram-4FO5ZUOK-Bu-eJOPP.js +1 -0
- package/dist/client/assets/classDiagram-v2-Q7XG4LA2-Bu-eJOPP.js +1 -0
- package/dist/client/assets/cose-bilkent-S5V4N54A-CebQ3rSv.js +1 -0
- package/dist/client/assets/cs-CZ-2BRQDIVT-CfiY7B0I.js +11 -0
- package/dist/client/assets/cytoscape.esm-h6BdjjI9.js +321 -0
- package/dist/client/assets/da-DK-5WZEPLOC-_se7cnqh.js +5 -0
- package/dist/client/assets/dagre-BM42HDAG-Dh4U_FZ3.js +4 -0
- package/dist/client/assets/dagre-Bx709z4p.js +1 -0
- package/dist/client/assets/de-DE-XR44H4JA-8OXIdaPv.js +8 -0
- package/dist/client/assets/defaultLocale-C8Fc0cco.js +1 -0
- package/dist/client/assets/diagram-2AECGRRQ-x13SPAsu.js +43 -0
- package/dist/client/assets/diagram-5GNKFQAL-Dd2ojJwV.js +10 -0
- package/dist/client/assets/diagram-KO2AKTUF-B8fPg745.js +3 -0
- package/dist/client/assets/diagram-LMA3HP47-BCOXIOSa.js +24 -0
- package/dist/client/assets/diagram-OG6HWLK6-koOG8baV.js +24 -0
- package/dist/client/assets/directory-open-01563666-D4xXyWb_.js +1 -0
- package/dist/client/assets/directory-open-4ed118d0-1i309Asm.js +1 -0
- package/dist/client/assets/dist-CkHWSq-9.js +19 -0
- package/dist/client/assets/dist-DbpddOpB.js +1 -0
- package/dist/client/assets/el-GR-BZB4AONW-DKyQ9Zbj.js +10 -0
- package/dist/client/assets/en-B4ZKOASM-C0nvDD-u.js +1 -0
- package/dist/client/assets/erDiagram-TEJ5UH35-BEkaZffl.js +85 -0
- package/dist/client/assets/es-ES-U4NZUMDT-BP9WJyyE.js +9 -0
- package/dist/client/assets/esm-BBZsBR2Q.js +135 -0
- package/dist/client/assets/eu-ES-A7QVB2H4-FF8Yo9ZO.js +11 -0
- package/dist/client/assets/eventmodeling-FCH6USID-BMNHUh4b.js +1 -0
- package/dist/client/assets/fa-IR-HGAKTJCU-9qIByi7w.js +8 -0
- package/dist/client/assets/fi-FI-Z5N7JZ37-PJ5NZtDN.js +6 -0
- package/dist/client/assets/file-open-002ab408-BHUWm0Sh.js +1 -0
- package/dist/client/assets/file-open-7c801643-DZtJT5zp.js +1 -0
- package/dist/client/assets/file-save-3189631c-CO9S4HFW.js +1 -0
- package/dist/client/assets/file-save-745eba88-Bwdfz6OZ.js +1 -0
- package/dist/client/assets/flowDiagram-I6XJVG4X-DyWXWaQw.js +162 -0
- package/dist/client/assets/fr-FR-RHASNOE6-jpsFHqOx.js +9 -0
- package/dist/client/assets/ganttDiagram-6RSMTGT7-ZyvknnRG.js +292 -0
- package/dist/client/assets/gitGraph-WXDBUCRP-Q6znYb8F.js +1 -0
- package/dist/client/assets/gitGraphDiagram-PVQCEYII-BduTvbdg.js +106 -0
- package/dist/client/assets/gl-ES-HMX3MZ6V-BJfquIk1.js +10 -0
- package/dist/client/assets/graphlib-B8gBHxth.js +1 -0
- package/dist/client/assets/he-IL-6SHJWFNN-BcKH2buH.js +10 -0
- package/dist/client/assets/hi-IN-IWLTKZ5I-CZ6xC-p-.js +4 -0
- package/dist/client/assets/hu-HU-A5ZG7DT2-Qdmeb1R8.js +7 -0
- package/dist/client/assets/id-ID-SAP4L64H-BF7hTexH.js +10 -0
- package/dist/client/assets/image-GAAHSSAO-614nODhe.js +1 -0
- package/dist/client/assets/image-blob-reduce.esm-NzmDxm1v.js +2 -0
- package/dist/client/assets/index-BM1wVbBu.css +1 -0
- package/dist/client/assets/index-CJS5C-21.js +236 -0
- package/dist/client/assets/info-J43DQDTF-B0NWKxg9.js +1 -0
- package/dist/client/assets/infoDiagram-5YYISTIA-S14LSWq2.js +2 -0
- package/dist/client/assets/init-D6jRqBbL.js +1 -0
- package/dist/client/assets/ishikawaDiagram-YF4QCWOH-mBgTlJYa.js +70 -0
- package/dist/client/assets/it-IT-JPQ66NNP-Cpwr4nh4.js +11 -0
- package/dist/client/assets/ja-JP-DBVTYXUO-aRcxqHgI.js +8 -0
- package/dist/client/assets/journeyDiagram-JHISSGLW-IqSYFtBk.js +139 -0
- package/dist/client/assets/kaa-6HZHGXH3-C3lGQNoX.js +1 -0
- package/dist/client/assets/kab-KAB-ZGHBKWFO-BTzCFBfa.js +8 -0
- package/dist/client/assets/kanban-definition-UN3LZRKU-56oDzy8w.js +89 -0
- package/dist/client/assets/katex-Vhh-h91d.js +257 -0
- package/dist/client/assets/kk-KZ-P5N5QNE5-BN2QE2qN.js +1 -0
- package/dist/client/assets/km-KH-HSX4SM5Z-ntycs-3y.js +11 -0
- package/dist/client/assets/ko-KR-MTYHY66A-z81zH0yV.js +9 -0
- package/dist/client/assets/ku-TR-6OUDTVRD-B3Hewbuw.js +9 -0
- package/dist/client/assets/line-Dw_ht09V.js +1 -0
- package/dist/client/assets/linear-CyOYhOcs.js +1 -0
- package/dist/client/assets/lt-LT-XHIRWOB4-A6GRBBpG.js +3 -0
- package/dist/client/assets/lv-LV-5QDEKY6T-JE4Th9NB.js +7 -0
- package/dist/client/assets/mermaid-parser.core-BLpZ1Xi3.js +4 -0
- package/dist/client/assets/mermaid.core-De7CyoQD.js +40 -0
- package/dist/client/assets/mindmap-definition-RKZ34NQL-uGJycOYz.js +96 -0
- package/dist/client/assets/mr-IN-CRQNXWMA-B9eQuk1Z.js +13 -0
- package/dist/client/assets/my-MM-5M5IBNSE-CHlvzYOu.js +1 -0
- package/dist/client/assets/nb-NO-T6EIAALU-FdyMoR8J.js +10 -0
- package/dist/client/assets/nl-NL-IS3SIHDZ-CcM_lR89.js +8 -0
- package/dist/client/assets/nn-NO-6E72VCQL-NsOr1ixK.js +8 -0
- package/dist/client/assets/oc-FR-POXYY2M6-ClC5X0pl.js +8 -0
- package/dist/client/assets/ordinal-hYBb2elL.js +1 -0
- package/dist/client/assets/pa-IN-N4M65BXN-LWsiX5S8.js +4 -0
- package/dist/client/assets/packet-YPE3B663-CJHLPqbQ.js +1 -0
- package/dist/client/assets/path-BWPyau1x.js +1 -0
- package/dist/client/assets/percentages-BXMCSKIN-V9nyH_gb.js +1 -0
- package/dist/client/assets/pica-DjQMjoX_.js +2 -0
- package/dist/client/assets/pie-LRSECV5Y-BUHMb4Eu.js +1 -0
- package/dist/client/assets/pieDiagram-4H26LBE5-GAPDiBah.js +30 -0
- package/dist/client/assets/pl-PL-T2D74RX3-Da6GvME-.js +9 -0
- package/dist/client/assets/prod-B3A6UDL8.js +149 -0
- package/dist/client/assets/prod-DtRVLazd.css +1 -0
- package/dist/client/assets/pt-BR-5N22H2LF-DfjURw9w.js +9 -0
- package/dist/client/assets/pt-PT-UZXXM6DQ-DwBqhsuh.js +9 -0
- package/dist/client/assets/quadrantDiagram-W4KKPZXB-BRcZqGCz.js +7 -0
- package/dist/client/assets/radar-GUYGQ44K-D2D556tt.js +1 -0
- package/dist/client/assets/requirementDiagram-4Y6WPE33-CX1Co4la.js +84 -0
- package/dist/client/assets/ro-RO-JPDTUUEW-CG0iQOM3.js +11 -0
- package/dist/client/assets/rough.esm-CSKSodPl.js +1 -0
- package/dist/client/assets/roundRect-D01gJrlt.js +1 -0
- package/dist/client/assets/ru-RU-B4JR7IUQ-Dg3BF3G-.js +9 -0
- package/dist/client/assets/sankeyDiagram-5OEKKPKP-D9ysMket.js +40 -0
- package/dist/client/assets/sequenceDiagram-3UESZ5HK-0g_Spb0h.js +162 -0
- package/dist/client/assets/si-LK-N5RQ5JYF-CpzQXp0R.js +1 -0
- package/dist/client/assets/sk-SK-C5VTKIMK-C6iDRiUH.js +6 -0
- package/dist/client/assets/sl-SI-NN7IZMDC-CpsmjUH1.js +6 -0
- package/dist/client/assets/src-BViOx7y3.js +1 -0
- package/dist/client/assets/stateDiagram-AJRCARHV-r4AmMQTe.js +1 -0
- package/dist/client/assets/stateDiagram-v2-BHNVJYJU-Ck2vn4J9.js +1 -0
- package/dist/client/assets/subset-shared.chunk-CI4ywhrU.js +1 -0
- package/dist/client/assets/subset-worker.chunk-7sWn7Ze3.js +1 -0
- package/dist/client/assets/sv-SE-XGPEYMSR-B5E6Hc1s.js +10 -0
- package/dist/client/assets/ta-IN-2NMHFXQM-DNdbg2NX.js +9 -0
- package/dist/client/assets/th-TH-HPSO5L25-CwCFFmIs.js +2 -0
- package/dist/client/assets/timeline-definition-PNZ67QCA-BJtGyu1Y.js +120 -0
- package/dist/client/assets/tr-TR-DEFEU3FU-D-USnzJt.js +7 -0
- package/dist/client/assets/treeView-BLDUP644-CpwJjSVv.js +1 -0
- package/dist/client/assets/treemap-LRROVOQU-DKQuF8GY.js +1 -0
- package/dist/client/assets/uk-UA-QMV73CPH-BGFyYrZm.js +6 -0
- package/dist/client/assets/vennDiagram-CIIHVFJN-BJ7Omwmb.js +34 -0
- package/dist/client/assets/vi-VN-M7AON7JQ-BEVYsUmB.js +5 -0
- package/dist/client/assets/wardley-L42UT6IY-B6p__8sD.js +1 -0
- package/dist/client/assets/wardleyDiagram-YWT4CUSO-BZNv-WBl.js +78 -0
- package/dist/client/assets/xychartDiagram-2RQKCTM6-DL9nFgTt.js +7 -0
- package/dist/client/assets/zh-CN-LNUGB5OW-Ya4bFRUj.js +10 -0
- package/dist/client/assets/zh-HK-E62DVLB3-B_9mh-qR.js +1 -0
- package/dist/client/assets/zh-TW-RAJ6MFWO-EeeAfw19.js +9 -0
- package/dist/client/index.html +14 -0
- package/dist/lintCore-E4ZG4DFJ.js +8 -0
- package/dist/store-WLSSJHFW.js +13 -0
- package/examples/specs/_.mdx +51 -0
- package/examples/specs/_components/ColumnCount.tsx +78 -0
- package/examples/specs/_components/StatusBadge.tsx +42 -0
- package/examples/specs/_generators/typescript-interface.md +16 -0
- package/examples/specs/api/_.mdx +10 -0
- package/examples/specs/api/_schema.json +77 -0
- package/examples/specs/api/_template.mdx +39 -0
- package/examples/specs/api/users-api.mdx +76 -0
- package/examples/specs/architecture/_.mdx +10 -0
- package/examples/specs/architecture/overview.mdx +45 -0
- package/examples/specs/screens/_.mdx +10 -0
- package/examples/specs/screens/_template.mdx +29 -0
- package/examples/specs/screens/login.excalidraw +282 -0
- package/examples/specs/screens/login.mdx +37 -0
- package/examples/specs/tables/_.mdx +10 -0
- package/examples/specs/tables/_schema.json +82 -0
- package/examples/specs/tables/_template.mdx +19 -0
- package/examples/specs/tables/posts.mdx +60 -0
- package/examples/specs/tables/users.mdx +34 -0
- package/package.json +88 -0
|
@@ -0,0 +1,1757 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
lintContent,
|
|
4
|
+
loadCategorySchema,
|
|
5
|
+
validateSpecs
|
|
6
|
+
} from "../chunk-774NFJLC.js";
|
|
7
|
+
import {
|
|
8
|
+
WIKILINK_PATTERN,
|
|
9
|
+
guardPath,
|
|
10
|
+
loadSpec,
|
|
11
|
+
loadSpecs,
|
|
12
|
+
parseSpecId,
|
|
13
|
+
resolveSpecPathAny,
|
|
14
|
+
toSpecId
|
|
15
|
+
} from "../chunk-JY5QLNZ4.js";
|
|
16
|
+
|
|
17
|
+
// src/cli/index.ts
|
|
18
|
+
import fs11 from "fs";
|
|
19
|
+
import path13 from "path";
|
|
20
|
+
import { program } from "commander";
|
|
21
|
+
|
|
22
|
+
// src/server/app.ts
|
|
23
|
+
import http from "http";
|
|
24
|
+
import fs8 from "fs";
|
|
25
|
+
import path10 from "path";
|
|
26
|
+
import { fileURLToPath } from "url";
|
|
27
|
+
import express from "express";
|
|
28
|
+
import { WebSocketServer } from "ws";
|
|
29
|
+
|
|
30
|
+
// src/server/routes/specs.ts
|
|
31
|
+
import { Router } from "express";
|
|
32
|
+
import fs2 from "fs/promises";
|
|
33
|
+
import path2 from "path";
|
|
34
|
+
import matter from "gray-matter";
|
|
35
|
+
|
|
36
|
+
// src/server/specOps.ts
|
|
37
|
+
import fs from "fs/promises";
|
|
38
|
+
import path from "path";
|
|
39
|
+
var SpecOpsError = class extends Error {
|
|
40
|
+
constructor(status, message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.status = status;
|
|
43
|
+
this.name = "SpecOpsError";
|
|
44
|
+
}
|
|
45
|
+
status;
|
|
46
|
+
};
|
|
47
|
+
function resolveSpecPath(specsDir, category, slug) {
|
|
48
|
+
const parts = category === "" ? [] : category.split("/");
|
|
49
|
+
const filePath = path.join(specsDir, ...parts, `${slug}.mdx`);
|
|
50
|
+
if (!guardPath(specsDir, filePath)) {
|
|
51
|
+
throw new SpecOpsError(400, "\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093");
|
|
52
|
+
}
|
|
53
|
+
return filePath;
|
|
54
|
+
}
|
|
55
|
+
function buildProtectedRanges(src) {
|
|
56
|
+
const ranges = [];
|
|
57
|
+
const fenceRe = /(`{3,}|~{3,})[^\n]*\n[\s\S]*?\1/g;
|
|
58
|
+
let m;
|
|
59
|
+
while ((m = fenceRe.exec(src)) !== null) {
|
|
60
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
61
|
+
}
|
|
62
|
+
const inlineRe = /`[^`\n]*`/g;
|
|
63
|
+
while ((m = inlineRe.exec(src)) !== null) {
|
|
64
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
65
|
+
}
|
|
66
|
+
return ranges;
|
|
67
|
+
}
|
|
68
|
+
function isProtected(pos, ranges) {
|
|
69
|
+
for (const [s, e] of ranges) {
|
|
70
|
+
if (pos >= s && pos < e) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
function rewriteLinks(src, from, to) {
|
|
75
|
+
if (!src.includes(`[[${from}]]`) && !src.includes(`[[${from}|`)) {
|
|
76
|
+
return src;
|
|
77
|
+
}
|
|
78
|
+
const protected_ = buildProtectedRanges(src);
|
|
79
|
+
const re = new RegExp(WIKILINK_PATTERN.source, WIKILINK_PATTERN.flags);
|
|
80
|
+
let result = "";
|
|
81
|
+
let lastIndex = 0;
|
|
82
|
+
let match;
|
|
83
|
+
while ((match = re.exec(src)) !== null) {
|
|
84
|
+
const target = match[1].trim();
|
|
85
|
+
if (target !== from) continue;
|
|
86
|
+
if (isProtected(match.index, protected_)) continue;
|
|
87
|
+
result += src.slice(lastIndex, match.index);
|
|
88
|
+
const label = match[2];
|
|
89
|
+
if (label !== void 0) {
|
|
90
|
+
result += `[[${to}|${label}]]`;
|
|
91
|
+
} else {
|
|
92
|
+
result += `[[${to}]]`;
|
|
93
|
+
}
|
|
94
|
+
lastIndex = match.index + match[0].length;
|
|
95
|
+
}
|
|
96
|
+
result += src.slice(lastIndex);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
async function findRefs(specsDir, id) {
|
|
100
|
+
const specs = await loadSpecs(specsDir);
|
|
101
|
+
const refs = specs.filter((s) => s.id !== id && s.links.includes(id)).map((s) => s.id).sort();
|
|
102
|
+
return refs;
|
|
103
|
+
}
|
|
104
|
+
async function renameSpec(specsDir, from, to) {
|
|
105
|
+
const parsedFrom = parseSpecId(from);
|
|
106
|
+
if (!parsedFrom) {
|
|
107
|
+
throw new SpecOpsError(400, `\u7121\u52B9\u306A\u30B9\u30DA\u30C3\u30AF ID: ${from}`);
|
|
108
|
+
}
|
|
109
|
+
const parsedTo = parseSpecId(to);
|
|
110
|
+
if (!parsedTo) {
|
|
111
|
+
throw new SpecOpsError(400, `\u7121\u52B9\u306A\u30B9\u30DA\u30C3\u30AF ID: ${to}`);
|
|
112
|
+
}
|
|
113
|
+
if (parsedFrom.slug === "_" || parsedFrom.slug === "_template") {
|
|
114
|
+
throw new SpecOpsError(400, "\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9/\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306F\u30EA\u30CD\u30FC\u30E0\u3067\u304D\u307E\u305B\u3093");
|
|
115
|
+
}
|
|
116
|
+
if (parsedTo.slug === "_" || parsedTo.slug === "_template") {
|
|
117
|
+
throw new SpecOpsError(400, "\u30EA\u30CD\u30FC\u30E0\u5148\u306E slug \u306B\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9/\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093");
|
|
118
|
+
}
|
|
119
|
+
const fromPath = resolveSpecPath(specsDir, parsedFrom.category, parsedFrom.slug);
|
|
120
|
+
const toPath = resolveSpecPath(specsDir, parsedTo.category, parsedTo.slug);
|
|
121
|
+
const specs = await loadSpecs(specsDir);
|
|
122
|
+
const fromMeta = specs.find((s) => s.id === from);
|
|
123
|
+
if (!fromMeta) {
|
|
124
|
+
throw new SpecOpsError(404, `\u30B9\u30DA\u30C3\u30AF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${from}`);
|
|
125
|
+
}
|
|
126
|
+
const toExists = specs.some((s) => s.id === to);
|
|
127
|
+
if (toExists) {
|
|
128
|
+
throw new SpecOpsError(409, `\u30EA\u30CD\u30FC\u30E0\u5148\u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059: ${to}`);
|
|
129
|
+
}
|
|
130
|
+
const toCategoryDir = path.dirname(toPath);
|
|
131
|
+
try {
|
|
132
|
+
await fs.access(toCategoryDir);
|
|
133
|
+
} catch {
|
|
134
|
+
const relCategoryDir = path.relative(specsDir, toCategoryDir).split(path.sep).join("/");
|
|
135
|
+
throw new SpecOpsError(400, `\u30EA\u30CD\u30FC\u30E0\u5148\u306E\u30AB\u30C6\u30B4\u30EA\u30FC\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${relCategoryDir}`);
|
|
136
|
+
}
|
|
137
|
+
await fs.rename(fromPath, toPath);
|
|
138
|
+
const specsAfter = await loadSpecs(specsDir);
|
|
139
|
+
const rewrittenIds = [];
|
|
140
|
+
await Promise.all(
|
|
141
|
+
specsAfter.map(async (spec) => {
|
|
142
|
+
const parts = spec.category === "" ? [] : spec.category.split("/");
|
|
143
|
+
const filePath = path.join(specsDir, ...parts, `${spec.slug}.mdx`);
|
|
144
|
+
let content;
|
|
145
|
+
try {
|
|
146
|
+
content = await fs.readFile(filePath, "utf-8");
|
|
147
|
+
} catch {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const rewritten = rewriteLinks(content, from, to);
|
|
151
|
+
if (rewritten === content) return;
|
|
152
|
+
await fs.writeFile(filePath, rewritten, "utf-8");
|
|
153
|
+
rewrittenIds.push(spec.id);
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
rewrittenIds.sort();
|
|
157
|
+
const { loadSpec: loadSpec2 } = await import("../store-WLSSJHFW.js");
|
|
158
|
+
const result = await loadSpec2(specsDir, toPath);
|
|
159
|
+
if (!result) {
|
|
160
|
+
throw new SpecOpsError(500, "\u30EA\u30CD\u30FC\u30E0\u5F8C\u306E\u30B9\u30DA\u30C3\u30AF\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
|
|
161
|
+
}
|
|
162
|
+
return { meta: result.meta, rewrittenFiles: rewrittenIds };
|
|
163
|
+
}
|
|
164
|
+
async function deleteSpec(specsDir, id) {
|
|
165
|
+
const parsed = parseSpecId(id);
|
|
166
|
+
if (!parsed) {
|
|
167
|
+
throw new SpecOpsError(400, `\u7121\u52B9\u306A\u30B9\u30DA\u30C3\u30AF ID: ${id}`);
|
|
168
|
+
}
|
|
169
|
+
const filePath = resolveSpecPath(specsDir, parsed.category, parsed.slug);
|
|
170
|
+
const specs = await loadSpecs(specsDir);
|
|
171
|
+
const exists = specs.some((s) => s.id === id);
|
|
172
|
+
if (!exists) {
|
|
173
|
+
throw new SpecOpsError(404, `\u30B9\u30DA\u30C3\u30AF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${id}`);
|
|
174
|
+
}
|
|
175
|
+
await fs.unlink(filePath);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/server/routes/specs.ts
|
|
179
|
+
function specsRouter(specsDir) {
|
|
180
|
+
const router = Router();
|
|
181
|
+
router.get("/", async (_req, res) => {
|
|
182
|
+
try {
|
|
183
|
+
const specs = await loadSpecs(specsDir);
|
|
184
|
+
const filtered = specs.filter((s) => s.slug !== "_template");
|
|
185
|
+
res.json(filtered);
|
|
186
|
+
} catch (err) {
|
|
187
|
+
res.status(500).json({ error: String(err) });
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
router.post("/", async (req, res) => {
|
|
191
|
+
const body = req.body;
|
|
192
|
+
if (!body.slug) {
|
|
193
|
+
res.status(400).json({ error: "slug \u306F\u5FC5\u9808\u3067\u3059" });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const category = body.category ?? "";
|
|
197
|
+
const slug = body.slug;
|
|
198
|
+
const title = body.title ?? slug;
|
|
199
|
+
const categoryParts = category === "" ? [] : category.split("/");
|
|
200
|
+
const newFilePath = path2.join(specsDir, ...categoryParts, `${slug}.mdx`);
|
|
201
|
+
if (!guardPath(specsDir, newFilePath)) {
|
|
202
|
+
res.status(400).json({ error: "\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u304C\u691C\u51FA\u3055\u308C\u307E\u3057\u305F" });
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
await fs2.access(newFilePath);
|
|
207
|
+
res.status(409).json({ error: `"${slug}" \u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059` });
|
|
208
|
+
return;
|
|
209
|
+
} catch {
|
|
210
|
+
}
|
|
211
|
+
let content;
|
|
212
|
+
if (slug === "_") {
|
|
213
|
+
const indexTitle = body.title ?? (category || "\u30DB\u30FC\u30E0");
|
|
214
|
+
content = `---
|
|
215
|
+
title: ${indexTitle}
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
# ${indexTitle}
|
|
219
|
+
|
|
220
|
+
<SpecList />
|
|
221
|
+
`;
|
|
222
|
+
} else {
|
|
223
|
+
const templatePath = path2.join(specsDir, ...categoryParts, "_template.mdx");
|
|
224
|
+
try {
|
|
225
|
+
await fs2.access(templatePath);
|
|
226
|
+
const templateContent = await fs2.readFile(templatePath, "utf-8");
|
|
227
|
+
const parsed = matter(templateContent);
|
|
228
|
+
parsed.data["title"] = title;
|
|
229
|
+
content = matter.stringify(parsed.content, parsed.data);
|
|
230
|
+
} catch {
|
|
231
|
+
content = `---
|
|
232
|
+
title: ${title}
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
# ${title}
|
|
236
|
+
`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
await fs2.mkdir(path2.dirname(newFilePath), { recursive: true });
|
|
241
|
+
await fs2.writeFile(newFilePath, content, "utf-8");
|
|
242
|
+
const result = await loadSpec(specsDir, newFilePath);
|
|
243
|
+
if (!result) {
|
|
244
|
+
res.status(500).json({ error: "\u4F5C\u6210\u5F8C\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F" });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
res.status(201).json({ meta: result.meta });
|
|
248
|
+
} catch (err) {
|
|
249
|
+
res.status(500).json({ error: String(err) });
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
router.get("/*", async (req, res) => {
|
|
253
|
+
const paramPath = req.params[0] ?? "";
|
|
254
|
+
if (!paramPath) {
|
|
255
|
+
res.status(400).json({ error: "\u30D1\u30B9\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093" });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const segments = paramPath.split("/").filter(Boolean);
|
|
259
|
+
if (segments.length === 0) {
|
|
260
|
+
res.status(400).json({ error: "\u30D1\u30B9\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093" });
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const slug = segments[segments.length - 1];
|
|
264
|
+
const category = segments.length > 1 ? segments.slice(0, -1).join("/") : "";
|
|
265
|
+
const candidates = resolveSpecPathAny(specsDir, category, slug);
|
|
266
|
+
for (const candidate of candidates) {
|
|
267
|
+
if (!guardPath(specsDir, candidate)) {
|
|
268
|
+
res.status(400).json({ error: "\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u304C\u691C\u51FA\u3055\u308C\u307E\u3057\u305F" });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const result = await loadSpec(specsDir, candidate);
|
|
272
|
+
if (result) {
|
|
273
|
+
res.json(result);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
res.status(404).json({ error: `\u30B9\u30DA\u30C3\u30AF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${paramPath}` });
|
|
278
|
+
});
|
|
279
|
+
router.put("/*", async (req, res) => {
|
|
280
|
+
const paramPath = req.params[0] ?? "";
|
|
281
|
+
if (!paramPath) {
|
|
282
|
+
res.status(400).json({ error: "\u30D1\u30B9\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093" });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const body = req.body;
|
|
286
|
+
if (typeof body.content !== "string") {
|
|
287
|
+
res.status(400).json({ error: "content \u306F\u5FC5\u9808\u3067\u3059" });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const segments = paramPath.split("/").filter(Boolean);
|
|
291
|
+
if (segments.length === 0) {
|
|
292
|
+
res.status(400).json({ error: "\u30D1\u30B9\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093" });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const slug = segments[segments.length - 1];
|
|
296
|
+
const category = segments.length > 1 ? segments.slice(0, -1).join("/") : "";
|
|
297
|
+
const candidates = resolveSpecPathAny(specsDir, category, slug);
|
|
298
|
+
let foundCandidate = null;
|
|
299
|
+
for (const candidate of candidates) {
|
|
300
|
+
if (!guardPath(specsDir, candidate)) {
|
|
301
|
+
res.status(400).json({ error: "\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u304C\u691C\u51FA\u3055\u308C\u307E\u3057\u305F" });
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
try {
|
|
305
|
+
await fs2.access(candidate);
|
|
306
|
+
foundCandidate = candidate;
|
|
307
|
+
break;
|
|
308
|
+
} catch {
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
if (!foundCandidate) {
|
|
312
|
+
res.status(404).json({ error: `\u30B9\u30DA\u30C3\u30AF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${paramPath}` });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
await fs2.writeFile(foundCandidate, body.content, "utf-8");
|
|
317
|
+
const result = await loadSpec(specsDir, foundCandidate);
|
|
318
|
+
if (!result) {
|
|
319
|
+
res.status(500).json({ error: "\u4FDD\u5B58\u5F8C\u306E\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F" });
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
let issues = [];
|
|
323
|
+
try {
|
|
324
|
+
const { lintContent: lintContent2 } = await import("../lintCore-E4ZG4DFJ.js");
|
|
325
|
+
issues = await lintContent2(specsDir, {
|
|
326
|
+
content: body.content,
|
|
327
|
+
category,
|
|
328
|
+
slug
|
|
329
|
+
});
|
|
330
|
+
} catch (lintErr) {
|
|
331
|
+
console.error("[specs] lintContent \u5931\u6557 (\u7121\u8996):", lintErr);
|
|
332
|
+
}
|
|
333
|
+
res.json({ meta: result.meta, issues });
|
|
334
|
+
} catch (err) {
|
|
335
|
+
res.status(500).json({ error: String(err) });
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
router.delete("/*", async (req, res) => {
|
|
339
|
+
const paramPath = req.params[0] ?? "";
|
|
340
|
+
if (!paramPath) {
|
|
341
|
+
res.status(400).json({ error: "\u30D1\u30B9\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093" });
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const segments = paramPath.split("/").filter(Boolean);
|
|
345
|
+
if (segments.length === 0) {
|
|
346
|
+
res.status(400).json({ error: "\u30D1\u30B9\u304C\u6307\u5B9A\u3055\u308C\u3066\u3044\u307E\u305B\u3093" });
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const slug = segments[segments.length - 1];
|
|
350
|
+
const category = segments.length > 1 ? segments.slice(0, -1).join("/") : "";
|
|
351
|
+
const id = toSpecId(category, slug);
|
|
352
|
+
try {
|
|
353
|
+
await deleteSpec(specsDir, id);
|
|
354
|
+
res.json({ ok: true });
|
|
355
|
+
} catch (err) {
|
|
356
|
+
if (err instanceof SpecOpsError) {
|
|
357
|
+
res.status(err.status).json({ error: err.message });
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
res.status(500).json({ error: String(err) });
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
return router;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// src/server/routes/categories.ts
|
|
367
|
+
import { Router as Router2 } from "express";
|
|
368
|
+
import fs3 from "fs/promises";
|
|
369
|
+
import path3 from "path";
|
|
370
|
+
var DEFAULT_INDEX_CONTENT = (categoryPath) => `---
|
|
371
|
+
title: ${categoryPath}
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
# ${categoryPath}
|
|
375
|
+
|
|
376
|
+
<SpecList />
|
|
377
|
+
`;
|
|
378
|
+
var DEFAULT_TEMPLATE_CONTENT = (categoryPath) => `---
|
|
379
|
+
title: \u65B0\u3057\u3044\u30B9\u30DA\u30C3\u30AF
|
|
380
|
+
description: ''
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
# \u65B0\u3057\u3044\u30B9\u30DA\u30C3\u30AF
|
|
384
|
+
|
|
385
|
+
\u30B9\u30DA\u30C3\u30AF\u306E\u8AAC\u660E\u3092\u3053\u3053\u306B\u8A18\u8FF0\u3057\u307E\u3059\u3002
|
|
386
|
+
`;
|
|
387
|
+
function categoriesRouter(specsDir) {
|
|
388
|
+
const router = Router2();
|
|
389
|
+
router.post("/", async (req, res) => {
|
|
390
|
+
const body = req.body;
|
|
391
|
+
if (!body.path) {
|
|
392
|
+
res.status(400).json({ error: "path \u306F\u5FC5\u9808\u3067\u3059" });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
const categoryPath = body.path.replace(/\\/g, "/").replace(/^\/|\/$/g, "");
|
|
396
|
+
const parts = categoryPath.split("/");
|
|
397
|
+
const newDir = path3.join(specsDir, ...parts);
|
|
398
|
+
if (!guardPath(specsDir, newDir)) {
|
|
399
|
+
res.status(400).json({ error: "\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u304C\u691C\u51FA\u3055\u308C\u307E\u3057\u305F" });
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
await fs3.access(newDir);
|
|
404
|
+
res.status(409).json({ error: `\u30AB\u30C6\u30B4\u30EA\u30FC "${categoryPath}" \u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059` });
|
|
405
|
+
return;
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
await fs3.mkdir(newDir, { recursive: true });
|
|
410
|
+
const indexPath = path3.join(newDir, "_.mdx");
|
|
411
|
+
await fs3.writeFile(indexPath, DEFAULT_INDEX_CONTENT(categoryPath), "utf-8");
|
|
412
|
+
const templatePath = path3.join(newDir, "_template.mdx");
|
|
413
|
+
await fs3.writeFile(templatePath, DEFAULT_TEMPLATE_CONTENT(categoryPath), "utf-8");
|
|
414
|
+
res.status(201).json({ path: categoryPath });
|
|
415
|
+
} catch (err) {
|
|
416
|
+
res.status(500).json({ error: String(err) });
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
return router;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/server/routes/data.ts
|
|
423
|
+
import { Router as Router3 } from "express";
|
|
424
|
+
function dataRouter(specsDir) {
|
|
425
|
+
const router = Router3();
|
|
426
|
+
router.get("/", async (_req, res) => {
|
|
427
|
+
try {
|
|
428
|
+
const specs = await loadSpecs(specsDir);
|
|
429
|
+
const filtered = specs.filter((s) => s.slug !== "_template");
|
|
430
|
+
const result = {};
|
|
431
|
+
for (const spec of filtered) {
|
|
432
|
+
const cat = spec.category;
|
|
433
|
+
if (!result[cat]) result[cat] = {};
|
|
434
|
+
result[cat][spec.slug] = spec.data;
|
|
435
|
+
}
|
|
436
|
+
res.json(result);
|
|
437
|
+
} catch (err) {
|
|
438
|
+
res.status(500).json({ error: String(err) });
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
router.get("/*", async (req, res) => {
|
|
442
|
+
const categoryPath = (req.params[0] ?? "").split("/").filter(Boolean).join("/");
|
|
443
|
+
try {
|
|
444
|
+
const specs = await loadSpecs(specsDir);
|
|
445
|
+
const filtered = specs.filter(
|
|
446
|
+
(s) => s.category === categoryPath && s.slug !== "_template"
|
|
447
|
+
);
|
|
448
|
+
const result = {};
|
|
449
|
+
for (const spec of filtered) {
|
|
450
|
+
result[spec.slug] = spec.data;
|
|
451
|
+
}
|
|
452
|
+
res.json(result);
|
|
453
|
+
} catch (err) {
|
|
454
|
+
res.status(500).json({ error: String(err) });
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
return router;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// src/server/routes/graph.ts
|
|
461
|
+
import { Router as Router4 } from "express";
|
|
462
|
+
function graphRouter(specsDir) {
|
|
463
|
+
const router = Router4();
|
|
464
|
+
router.get("/", async (_req, res) => {
|
|
465
|
+
try {
|
|
466
|
+
const specs = await loadSpecs(specsDir);
|
|
467
|
+
const filtered = specs.filter((s) => s.slug !== "_template");
|
|
468
|
+
const existingIds = new Set(filtered.map((s) => s.id));
|
|
469
|
+
const nodes = filtered.map((s) => ({
|
|
470
|
+
id: s.id,
|
|
471
|
+
title: s.title,
|
|
472
|
+
category: s.category
|
|
473
|
+
}));
|
|
474
|
+
const edges = [];
|
|
475
|
+
const missingIds = /* @__PURE__ */ new Set();
|
|
476
|
+
for (const spec of filtered) {
|
|
477
|
+
for (const linkTarget of spec.links) {
|
|
478
|
+
edges.push({ source: spec.id, target: linkTarget });
|
|
479
|
+
if (!existingIds.has(linkTarget)) {
|
|
480
|
+
missingIds.add(linkTarget);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
for (const missingId of missingIds) {
|
|
485
|
+
const lastColon = missingId.lastIndexOf(":");
|
|
486
|
+
const title = lastColon >= 0 ? missingId.slice(lastColon + 1) : missingId;
|
|
487
|
+
const category = lastColon >= 0 ? missingId.slice(0, lastColon) : "";
|
|
488
|
+
nodes.push({ id: missingId, title, category, missing: true });
|
|
489
|
+
}
|
|
490
|
+
const graph = { nodes, edges };
|
|
491
|
+
res.json(graph);
|
|
492
|
+
} catch (err) {
|
|
493
|
+
res.status(500).json({ error: String(err) });
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
return router;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/server/routes/generators.ts
|
|
500
|
+
import { Router as Router5 } from "express";
|
|
501
|
+
|
|
502
|
+
// src/server/generate.ts
|
|
503
|
+
import fs4 from "fs/promises";
|
|
504
|
+
import path4 from "path";
|
|
505
|
+
import matter2 from "gray-matter";
|
|
506
|
+
import { render, createContext, defineHelper } from "@scaffdog/engine";
|
|
507
|
+
function normalizePath(p) {
|
|
508
|
+
return p.split(path4.sep).join("/");
|
|
509
|
+
}
|
|
510
|
+
function specMetaToVariable(spec) {
|
|
511
|
+
return {
|
|
512
|
+
id: spec.id,
|
|
513
|
+
category: spec.category,
|
|
514
|
+
slug: spec.slug,
|
|
515
|
+
path: spec.path,
|
|
516
|
+
title: spec.title,
|
|
517
|
+
description: spec.description ?? "",
|
|
518
|
+
data: spec.data,
|
|
519
|
+
links: spec.links,
|
|
520
|
+
isIndex: spec.isIndex
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function specsToVariable(specs) {
|
|
524
|
+
return specs.map(specMetaToVariable);
|
|
525
|
+
}
|
|
526
|
+
function buildHelpers() {
|
|
527
|
+
const helpers = /* @__PURE__ */ new Map();
|
|
528
|
+
defineHelper(helpers, "pascal", (_ctx, value) => {
|
|
529
|
+
if (typeof value !== "string") return String(value);
|
|
530
|
+
return value.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (c) => c.toUpperCase());
|
|
531
|
+
});
|
|
532
|
+
defineHelper(helpers, "camel", (_ctx, value) => {
|
|
533
|
+
if (typeof value !== "string") return String(value);
|
|
534
|
+
const pascal = value.replace(/[-_\s]+(.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (c) => c.toUpperCase());
|
|
535
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
536
|
+
});
|
|
537
|
+
defineHelper(helpers, "upper", (_ctx, value) => {
|
|
538
|
+
if (typeof value !== "string") return String(value);
|
|
539
|
+
return value.toUpperCase();
|
|
540
|
+
});
|
|
541
|
+
defineHelper(helpers, "lower", (_ctx, value) => {
|
|
542
|
+
if (typeof value !== "string") return String(value);
|
|
543
|
+
return value.toLowerCase();
|
|
544
|
+
});
|
|
545
|
+
defineHelper(helpers, "snake", (_ctx, value) => {
|
|
546
|
+
if (typeof value !== "string") return String(value);
|
|
547
|
+
return value.replace(/([A-Z])/g, "_$1").replace(/[-\s]+/g, "_").replace(/^_/, "").toLowerCase();
|
|
548
|
+
});
|
|
549
|
+
defineHelper(helpers, "optionalMark", (_ctx, value) => {
|
|
550
|
+
if (value && typeof value === "object" && value.nullable === true) {
|
|
551
|
+
return "?";
|
|
552
|
+
}
|
|
553
|
+
return "";
|
|
554
|
+
});
|
|
555
|
+
defineHelper(helpers, "tsType", (_ctx, value) => {
|
|
556
|
+
if (typeof value !== "string") return "unknown";
|
|
557
|
+
const v = value.toLowerCase().trim();
|
|
558
|
+
if (v.startsWith("bigint") || v.startsWith("int") || v.startsWith("smallint") || v.startsWith("tinyint") || v.startsWith("numeric") || v.startsWith("decimal") || v.startsWith("float") || v.startsWith("double") || v.startsWith("real")) return "number";
|
|
559
|
+
if (v.startsWith("bool")) return "boolean";
|
|
560
|
+
if (v.startsWith("json") || v.startsWith("jsonb")) return "Record<string, unknown>";
|
|
561
|
+
if (v.startsWith("timestamp") || v.startsWith("date") || v.startsWith("time")) return "string";
|
|
562
|
+
return "string";
|
|
563
|
+
});
|
|
564
|
+
return helpers;
|
|
565
|
+
}
|
|
566
|
+
function parseGeneratorDoc(content) {
|
|
567
|
+
const parsed = matter2(content);
|
|
568
|
+
const body = parsed.content;
|
|
569
|
+
const sections = [];
|
|
570
|
+
const headingPattern = /^#\s+`([^`]+)`\s*$/gm;
|
|
571
|
+
const marks = [];
|
|
572
|
+
let m;
|
|
573
|
+
while ((m = headingPattern.exec(body)) !== null) {
|
|
574
|
+
marks.push({ filename: m[1].trim(), headStart: m.index, bodyStart: m.index + m[0].length });
|
|
575
|
+
}
|
|
576
|
+
marks.forEach((mark, i) => {
|
|
577
|
+
const end = i + 1 < marks.length ? marks[i + 1].headStart : body.length;
|
|
578
|
+
const segment = body.slice(mark.bodyStart, end);
|
|
579
|
+
const fenceMatch = /^```[^\n]*\n([\s\S]*?)^```/m.exec(segment);
|
|
580
|
+
if (fenceMatch) {
|
|
581
|
+
sections.push({ filename: mark.filename, template: fenceMatch[1] });
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
return sections;
|
|
585
|
+
}
|
|
586
|
+
async function listGenerators(specsDir) {
|
|
587
|
+
const generatorsDir = path4.join(specsDir, "_generators");
|
|
588
|
+
try {
|
|
589
|
+
const entries = await fs4.readdir(generatorsDir, { withFileTypes: true });
|
|
590
|
+
return entries.filter((e) => e.isFile() && /\.md$/.test(e.name)).map((e) => e.name.replace(/\.md$/, ""));
|
|
591
|
+
} catch {
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async function runGenerator(specsDir, generator, allSpecs, specId, outDir) {
|
|
596
|
+
const generatorPath = path4.join(specsDir, "_generators", `${generator}.md`);
|
|
597
|
+
let generatorContent;
|
|
598
|
+
try {
|
|
599
|
+
generatorContent = await fs4.readFile(generatorPath, "utf-8");
|
|
600
|
+
} catch {
|
|
601
|
+
throw new Error(`\u30B8\u30A7\u30CD\u30EC\u30FC\u30BF\u30FC "${generator}" \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${generatorPath}`);
|
|
602
|
+
}
|
|
603
|
+
const sections = parseGeneratorDoc(generatorContent);
|
|
604
|
+
if (sections.length === 0) {
|
|
605
|
+
throw new Error(`\u30B8\u30A7\u30CD\u30EC\u30FC\u30BF\u30FC "${generator}" \u306B\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u30BB\u30AF\u30B7\u30E7\u30F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093`);
|
|
606
|
+
}
|
|
607
|
+
const targetSpecs = specId != null ? allSpecs.filter((s) => s.id === specId) : allSpecs.filter((s) => !s.isIndex && s.slug !== "_template");
|
|
608
|
+
if (specId != null && targetSpecs.length === 0) {
|
|
609
|
+
throw new Error(`\u30B9\u30DA\u30C3\u30AF "${specId}" \u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093`);
|
|
610
|
+
}
|
|
611
|
+
const helpers = buildHelpers();
|
|
612
|
+
const allSpecsVar = specsToVariable(allSpecs);
|
|
613
|
+
const files = [];
|
|
614
|
+
for (const spec of targetSpecs) {
|
|
615
|
+
const specVar = specMetaToVariable(spec);
|
|
616
|
+
const variables = /* @__PURE__ */ new Map([
|
|
617
|
+
["spec", specVar],
|
|
618
|
+
["specs", allSpecsVar]
|
|
619
|
+
]);
|
|
620
|
+
const context = createContext({ variables, helpers });
|
|
621
|
+
for (const section of sections) {
|
|
622
|
+
const renderedFilename = render(section.filename, context);
|
|
623
|
+
const renderedContent = render(section.template, context);
|
|
624
|
+
const filePath = normalizePath(renderedFilename);
|
|
625
|
+
if (outDir != null) {
|
|
626
|
+
const absOut = path4.resolve(outDir, renderedFilename);
|
|
627
|
+
await fs4.mkdir(path4.dirname(absOut), { recursive: true });
|
|
628
|
+
await fs4.writeFile(absOut, renderedContent, "utf-8");
|
|
629
|
+
}
|
|
630
|
+
files.push({ path: filePath, content: renderedContent });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return files;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// src/server/routes/generators.ts
|
|
637
|
+
function generatorsRouter(specsDir) {
|
|
638
|
+
const router = Router5();
|
|
639
|
+
router.get("/", async (_req, res) => {
|
|
640
|
+
try {
|
|
641
|
+
const names = await listGenerators(specsDir);
|
|
642
|
+
res.json(names);
|
|
643
|
+
} catch (err) {
|
|
644
|
+
res.status(500).json({ error: String(err) });
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
return router;
|
|
648
|
+
}
|
|
649
|
+
function generateRouter(specsDir) {
|
|
650
|
+
const router = Router5();
|
|
651
|
+
router.post("/", async (req, res) => {
|
|
652
|
+
const body = req.body;
|
|
653
|
+
if (!body.generator) {
|
|
654
|
+
res.status(400).json({ error: "generator \u306F\u5FC5\u9808\u3067\u3059" });
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
const allSpecs = await loadSpecs(specsDir);
|
|
659
|
+
const files = await runGenerator(
|
|
660
|
+
specsDir,
|
|
661
|
+
body.generator,
|
|
662
|
+
allSpecs,
|
|
663
|
+
body.specId,
|
|
664
|
+
body.out
|
|
665
|
+
);
|
|
666
|
+
res.json({ files });
|
|
667
|
+
} catch (err) {
|
|
668
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
669
|
+
res.status(400).json({ error: message });
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
return router;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/server/routes/search.ts
|
|
676
|
+
import { Router as Router6 } from "express";
|
|
677
|
+
|
|
678
|
+
// src/server/searchCore.ts
|
|
679
|
+
import fs5 from "fs/promises";
|
|
680
|
+
import path5 from "path";
|
|
681
|
+
import matter3 from "gray-matter";
|
|
682
|
+
var FIELD_RANK = {
|
|
683
|
+
title: 0,
|
|
684
|
+
description: 1,
|
|
685
|
+
data: 2,
|
|
686
|
+
body: 3
|
|
687
|
+
};
|
|
688
|
+
var SNIPPET_CONTEXT = 40;
|
|
689
|
+
function makeSnippet(text, query) {
|
|
690
|
+
const normalized = text.replace(/\s+/g, " ").trim();
|
|
691
|
+
const lower = normalized.toLowerCase();
|
|
692
|
+
const lowerQ = query.toLowerCase();
|
|
693
|
+
const idx = lower.indexOf(lowerQ);
|
|
694
|
+
if (idx === -1) return normalized.slice(0, SNIPPET_CONTEXT * 2);
|
|
695
|
+
const start = Math.max(0, idx - SNIPPET_CONTEXT);
|
|
696
|
+
const end = Math.min(normalized.length, idx + lowerQ.length + SNIPPET_CONTEXT);
|
|
697
|
+
const prefix = start > 0 ? "\u2026" : "";
|
|
698
|
+
const suffix = end < normalized.length ? "\u2026" : "";
|
|
699
|
+
return prefix + normalized.slice(start, end) + suffix;
|
|
700
|
+
}
|
|
701
|
+
function firstBodyLine(body) {
|
|
702
|
+
for (const line of body.split("\n")) {
|
|
703
|
+
const trimmed = line.trim();
|
|
704
|
+
if (trimmed) return trimmed.slice(0, SNIPPET_CONTEXT * 2);
|
|
705
|
+
}
|
|
706
|
+
return "";
|
|
707
|
+
}
|
|
708
|
+
async function searchSpecs(specsDir, q, limit) {
|
|
709
|
+
if (!q) return [];
|
|
710
|
+
const allSpecs = await loadSpecs(specsDir);
|
|
711
|
+
const specs = allSpecs.filter((s) => s.slug !== "_template");
|
|
712
|
+
const lowerQ = q.toLowerCase();
|
|
713
|
+
const results = [];
|
|
714
|
+
for (const spec of specs) {
|
|
715
|
+
if (spec.title.toLowerCase().includes(lowerQ)) {
|
|
716
|
+
let snippet = "";
|
|
717
|
+
if (spec.description) {
|
|
718
|
+
snippet = makeSnippet(spec.description, q);
|
|
719
|
+
} else {
|
|
720
|
+
const absPath2 = path5.join(specsDir, ...spec.path.split("/"));
|
|
721
|
+
try {
|
|
722
|
+
const raw = await fs5.readFile(absPath2, "utf-8");
|
|
723
|
+
const parsed = matter3(raw);
|
|
724
|
+
snippet = firstBodyLine(parsed.content);
|
|
725
|
+
} catch {
|
|
726
|
+
snippet = spec.title;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
results.push({
|
|
730
|
+
id: spec.id,
|
|
731
|
+
title: spec.title,
|
|
732
|
+
category: spec.category,
|
|
733
|
+
slug: spec.slug,
|
|
734
|
+
snippet,
|
|
735
|
+
field: "title"
|
|
736
|
+
});
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
if (spec.description && spec.description.toLowerCase().includes(lowerQ)) {
|
|
740
|
+
results.push({
|
|
741
|
+
id: spec.id,
|
|
742
|
+
title: spec.title,
|
|
743
|
+
category: spec.category,
|
|
744
|
+
slug: spec.slug,
|
|
745
|
+
snippet: makeSnippet(spec.description, q),
|
|
746
|
+
field: "description"
|
|
747
|
+
});
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
const dataStr = JSON.stringify(spec.data);
|
|
751
|
+
if (dataStr.toLowerCase().includes(lowerQ)) {
|
|
752
|
+
results.push({
|
|
753
|
+
id: spec.id,
|
|
754
|
+
title: spec.title,
|
|
755
|
+
category: spec.category,
|
|
756
|
+
slug: spec.slug,
|
|
757
|
+
snippet: makeSnippet(dataStr, q),
|
|
758
|
+
field: "data"
|
|
759
|
+
});
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const absPath = path5.join(specsDir, ...spec.path.split("/"));
|
|
763
|
+
try {
|
|
764
|
+
const raw = await fs5.readFile(absPath, "utf-8");
|
|
765
|
+
const parsed = matter3(raw);
|
|
766
|
+
const body = parsed.content;
|
|
767
|
+
if (body.toLowerCase().includes(lowerQ)) {
|
|
768
|
+
results.push({
|
|
769
|
+
id: spec.id,
|
|
770
|
+
title: spec.title,
|
|
771
|
+
category: spec.category,
|
|
772
|
+
slug: spec.slug,
|
|
773
|
+
snippet: makeSnippet(body, q),
|
|
774
|
+
field: "body"
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
} catch {
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
results.sort((a, b) => {
|
|
781
|
+
const rankDiff = FIELD_RANK[a.field] - FIELD_RANK[b.field];
|
|
782
|
+
if (rankDiff !== 0) return rankDiff;
|
|
783
|
+
return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
|
|
784
|
+
});
|
|
785
|
+
return results.slice(0, limit);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/server/routes/search.ts
|
|
789
|
+
function searchRouter(specsDir) {
|
|
790
|
+
const router = Router6();
|
|
791
|
+
router.get("/", async (req, res) => {
|
|
792
|
+
try {
|
|
793
|
+
const rawQ = typeof req.query["q"] === "string" ? req.query["q"] : "";
|
|
794
|
+
const q = rawQ.trim();
|
|
795
|
+
if (!q) {
|
|
796
|
+
res.json([]);
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const rawLimit = parseInt(
|
|
800
|
+
typeof req.query["limit"] === "string" ? req.query["limit"] : "20",
|
|
801
|
+
10
|
|
802
|
+
);
|
|
803
|
+
const limit = isNaN(rawLimit) ? 20 : Math.min(100, Math.max(1, rawLimit));
|
|
804
|
+
const results = await searchSpecs(specsDir, q, limit);
|
|
805
|
+
res.json(results);
|
|
806
|
+
} catch (err) {
|
|
807
|
+
res.status(500).json({ error: String(err) });
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
return router;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// src/server/routes/components.ts
|
|
814
|
+
import { Router as Router7 } from "express";
|
|
815
|
+
import fs6 from "fs/promises";
|
|
816
|
+
import path6 from "path";
|
|
817
|
+
function normalizePath2(p) {
|
|
818
|
+
return p.split(path6.sep).join("/");
|
|
819
|
+
}
|
|
820
|
+
function componentsRouter(specsDir) {
|
|
821
|
+
const router = Router7();
|
|
822
|
+
router.get("/", async (_req, res) => {
|
|
823
|
+
const dir = path6.join(specsDir, "_components");
|
|
824
|
+
try {
|
|
825
|
+
const entries = await fs6.readdir(dir, { withFileTypes: true }).catch((err) => {
|
|
826
|
+
if (err.code === "ENOENT") return null;
|
|
827
|
+
throw err;
|
|
828
|
+
});
|
|
829
|
+
if (entries === null) {
|
|
830
|
+
res.json([]);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const files = [];
|
|
834
|
+
for (const entry of entries) {
|
|
835
|
+
if (!entry.isFile()) continue;
|
|
836
|
+
if (!/\.(tsx|jsx)$/.test(entry.name)) continue;
|
|
837
|
+
const abs = path6.join(dir, entry.name);
|
|
838
|
+
const source = await fs6.readFile(abs, "utf-8");
|
|
839
|
+
files.push({
|
|
840
|
+
path: normalizePath2(path6.relative(specsDir, abs)),
|
|
841
|
+
source
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
files.sort((a, b) => a.path.localeCompare(b.path));
|
|
845
|
+
res.json(files);
|
|
846
|
+
} catch (err) {
|
|
847
|
+
res.status(500).json({ error: String(err) });
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
return router;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// src/server/routes/validation.ts
|
|
854
|
+
import { Router as Router8 } from "express";
|
|
855
|
+
function validationRouter(specsDir) {
|
|
856
|
+
const router = Router8();
|
|
857
|
+
router.get("/", async (_req, res) => {
|
|
858
|
+
try {
|
|
859
|
+
const report = await validateSpecs(specsDir);
|
|
860
|
+
res.json(report);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
res.status(500).json({ error: String(err) });
|
|
863
|
+
}
|
|
864
|
+
});
|
|
865
|
+
return router;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/server/routes/schema.ts
|
|
869
|
+
import path7 from "path";
|
|
870
|
+
import { Router as Router9 } from "express";
|
|
871
|
+
function schemaRouter(specsDir) {
|
|
872
|
+
const router = Router9();
|
|
873
|
+
router.get(/^(\/.*)?$/, async (req, res) => {
|
|
874
|
+
const rawPath = req.path;
|
|
875
|
+
let decoded;
|
|
876
|
+
try {
|
|
877
|
+
decoded = decodeURIComponent(rawPath);
|
|
878
|
+
} catch {
|
|
879
|
+
res.status(400).json({ error: "URL\u306E\u30C7\u30B3\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F" });
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const category = decoded.replace(/^\//, "");
|
|
883
|
+
const resolvedSpecsDir = path7.resolve(specsDir);
|
|
884
|
+
const segments = category === "" ? [] : category.split("/");
|
|
885
|
+
const resolvedTarget = path7.resolve(specsDir, ...segments);
|
|
886
|
+
if (!resolvedTarget.startsWith(resolvedSpecsDir + path7.sep) && resolvedTarget !== resolvedSpecsDir) {
|
|
887
|
+
res.status(400).json({ error: "\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093" });
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const result = await loadCategorySchema(specsDir, category);
|
|
891
|
+
if (result.error !== void 0) {
|
|
892
|
+
res.status(500).json({ error: result.error });
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const body = { schema: result.schema };
|
|
896
|
+
res.json(body);
|
|
897
|
+
});
|
|
898
|
+
return router;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/server/routes/drawings.ts
|
|
902
|
+
import path8 from "path";
|
|
903
|
+
import fs7 from "fs/promises";
|
|
904
|
+
import { Router as Router10, json } from "express";
|
|
905
|
+
function normalizePath3(p) {
|
|
906
|
+
return p.split(path8.sep).join("/");
|
|
907
|
+
}
|
|
908
|
+
function resolveGuarded(specsDir, relPath) {
|
|
909
|
+
const resolvedSpecsDir = path8.resolve(specsDir);
|
|
910
|
+
const resolvedTarget = path8.resolve(specsDir, ...relPath.split("/"));
|
|
911
|
+
if (resolvedTarget !== resolvedSpecsDir && !resolvedTarget.startsWith(resolvedSpecsDir + path8.sep)) {
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
914
|
+
return resolvedTarget;
|
|
915
|
+
}
|
|
916
|
+
async function scanExcalidraw(specsDir, dir) {
|
|
917
|
+
let entries;
|
|
918
|
+
try {
|
|
919
|
+
entries = await fs7.readdir(dir, { withFileTypes: true });
|
|
920
|
+
} catch {
|
|
921
|
+
return [];
|
|
922
|
+
}
|
|
923
|
+
const results = [];
|
|
924
|
+
for (const entry of entries) {
|
|
925
|
+
if (entry.name.startsWith(".")) continue;
|
|
926
|
+
if (entry.name === "node_modules") continue;
|
|
927
|
+
const abs = path8.join(dir, entry.name);
|
|
928
|
+
if (entry.isDirectory()) {
|
|
929
|
+
const children = await scanExcalidraw(specsDir, abs);
|
|
930
|
+
results.push(...children);
|
|
931
|
+
} else if (entry.isFile() && entry.name.endsWith(".excalidraw")) {
|
|
932
|
+
results.push({
|
|
933
|
+
path: normalizePath3(path8.relative(specsDir, abs))
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return results;
|
|
938
|
+
}
|
|
939
|
+
function drawingsRouter(specsDir) {
|
|
940
|
+
const router = Router10();
|
|
941
|
+
router.use(json({ limit: "20mb" }));
|
|
942
|
+
router.get("/", async (_req, res) => {
|
|
943
|
+
try {
|
|
944
|
+
const drawings = await scanExcalidraw(specsDir, specsDir);
|
|
945
|
+
drawings.sort((a, b) => a.path.localeCompare(b.path));
|
|
946
|
+
res.json(drawings);
|
|
947
|
+
} catch (err) {
|
|
948
|
+
res.status(500).json({ error: String(err) });
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
router.get(/^\/.+$/, async (req, res) => {
|
|
952
|
+
let decoded;
|
|
953
|
+
try {
|
|
954
|
+
decoded = decodeURIComponent(req.path);
|
|
955
|
+
} catch {
|
|
956
|
+
res.status(400).json({ error: "URL\u306E\u30C7\u30B3\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F" });
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
const relPath = decoded.replace(/^\//, "");
|
|
960
|
+
if (!relPath) {
|
|
961
|
+
res.status(400).json({ error: "\u30D1\u30B9\u304C\u7A7A\u3067\u3059" });
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
const resolved = resolveGuarded(specsDir, relPath);
|
|
965
|
+
if (resolved === null) {
|
|
966
|
+
res.status(400).json({ error: "\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093" });
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
const filePath = resolved + ".excalidraw";
|
|
970
|
+
let raw;
|
|
971
|
+
try {
|
|
972
|
+
raw = await fs7.readFile(filePath, "utf-8");
|
|
973
|
+
} catch (err) {
|
|
974
|
+
const e = err;
|
|
975
|
+
if (e.code === "ENOENT") {
|
|
976
|
+
res.status(404).json({ error: "\u56F3\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093" });
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
res.status(500).json({ error: String(err) });
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
res.type("application/json").send(raw);
|
|
983
|
+
});
|
|
984
|
+
router.put(/^\/.+$/, async (req, res) => {
|
|
985
|
+
let decoded;
|
|
986
|
+
try {
|
|
987
|
+
decoded = decodeURIComponent(req.path);
|
|
988
|
+
} catch {
|
|
989
|
+
res.status(400).json({ error: "URL\u306E\u30C7\u30B3\u30FC\u30C9\u306B\u5931\u6557\u3057\u307E\u3057\u305F" });
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
const relPath = decoded.replace(/^\//, "");
|
|
993
|
+
if (!relPath) {
|
|
994
|
+
res.status(400).json({ error: "\u30D1\u30B9\u304C\u7A7A\u3067\u3059" });
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
997
|
+
const resolved = resolveGuarded(specsDir, relPath);
|
|
998
|
+
if (resolved === null) {
|
|
999
|
+
res.status(400).json({ error: "\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093" });
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
const body = req.body;
|
|
1003
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
1004
|
+
res.status(400).json({ error: "\u30EA\u30AF\u30A8\u30B9\u30C8\u30DC\u30C7\u30A3\u306F JSON \u30AA\u30D6\u30B8\u30A7\u30AF\u30C8\u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059" });
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const filePath = resolved + ".excalidraw";
|
|
1008
|
+
const parentDir = path8.dirname(filePath);
|
|
1009
|
+
try {
|
|
1010
|
+
await fs7.access(parentDir);
|
|
1011
|
+
} catch {
|
|
1012
|
+
res.status(400).json({ error: `\u89AA\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${normalizePath3(path8.relative(specsDir, parentDir))}` });
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
try {
|
|
1016
|
+
const content = JSON.stringify(body, null, 2) + "\n";
|
|
1017
|
+
await fs7.writeFile(filePath, content, { encoding: "utf-8" });
|
|
1018
|
+
} catch (err) {
|
|
1019
|
+
res.status(500).json({ error: String(err) });
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
const meta = {
|
|
1023
|
+
path: normalizePath3(path8.relative(specsDir, filePath))
|
|
1024
|
+
};
|
|
1025
|
+
res.json(meta);
|
|
1026
|
+
});
|
|
1027
|
+
return router;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/server/routes/lint.ts
|
|
1031
|
+
import { Router as Router11 } from "express";
|
|
1032
|
+
function lintRouter(specsDir) {
|
|
1033
|
+
const router = Router11();
|
|
1034
|
+
router.post("/", async (req, res) => {
|
|
1035
|
+
try {
|
|
1036
|
+
const body = req.body;
|
|
1037
|
+
if (typeof body.content !== "string") {
|
|
1038
|
+
res.status(400).json({ error: "content \u306F string \u3067\u3042\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059" });
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
const issues = await lintContent(specsDir, {
|
|
1042
|
+
content: body.content,
|
|
1043
|
+
category: typeof body.category === "string" ? body.category : void 0,
|
|
1044
|
+
slug: typeof body.slug === "string" ? body.slug : void 0
|
|
1045
|
+
});
|
|
1046
|
+
res.json({ issues });
|
|
1047
|
+
} catch (err) {
|
|
1048
|
+
res.status(500).json({ error: String(err) });
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
return router;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// src/server/routes/rename.ts
|
|
1055
|
+
import { Router as Router12 } from "express";
|
|
1056
|
+
function renameRouter(specsDir) {
|
|
1057
|
+
const router = Router12();
|
|
1058
|
+
router.post("/", async (req, res) => {
|
|
1059
|
+
const body = req.body;
|
|
1060
|
+
if (typeof body.from !== "string" || !body.from) {
|
|
1061
|
+
res.status(400).json({ error: "from \u306F\u5FC5\u9808\u3067\u3059" });
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
if (typeof body.to !== "string" || !body.to) {
|
|
1065
|
+
res.status(400).json({ error: "to \u306F\u5FC5\u9808\u3067\u3059" });
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
try {
|
|
1069
|
+
const result = await renameSpec(specsDir, body.from, body.to);
|
|
1070
|
+
const response = {
|
|
1071
|
+
meta: result.meta,
|
|
1072
|
+
rewrittenFiles: result.rewrittenFiles
|
|
1073
|
+
};
|
|
1074
|
+
res.json(response);
|
|
1075
|
+
} catch (err) {
|
|
1076
|
+
if (err instanceof SpecOpsError) {
|
|
1077
|
+
res.status(err.status).json({ error: err.message });
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
res.status(500).json({ error: String(err) });
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
return router;
|
|
1084
|
+
}
|
|
1085
|
+
function refsRouter(specsDir) {
|
|
1086
|
+
const router = Router12();
|
|
1087
|
+
router.get("/", async (req, res) => {
|
|
1088
|
+
const id = req.query["id"];
|
|
1089
|
+
if (typeof id !== "string" || !id) {
|
|
1090
|
+
res.status(400).json({ error: "id \u30AF\u30A8\u30EA\u30D1\u30E9\u30E1\u30FC\u30BF\u30FC\u306F\u5FC5\u9808\u3067\u3059" });
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
const refs = await findRefs(specsDir, id);
|
|
1095
|
+
const response = { refs };
|
|
1096
|
+
res.json(response);
|
|
1097
|
+
} catch (err) {
|
|
1098
|
+
if (err instanceof SpecOpsError) {
|
|
1099
|
+
res.status(err.status).json({ error: err.message });
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
res.status(500).json({ error: String(err) });
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
return router;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/server/watcher.ts
|
|
1109
|
+
import chokidar from "chokidar";
|
|
1110
|
+
import path9 from "path";
|
|
1111
|
+
function normalizePath4(p) {
|
|
1112
|
+
return p.split(path9.sep).join("/");
|
|
1113
|
+
}
|
|
1114
|
+
function pathToSpecId(specsDir, filePath) {
|
|
1115
|
+
const rel = normalizePath4(path9.relative(specsDir, filePath));
|
|
1116
|
+
if (rel.startsWith("_generators/")) return null;
|
|
1117
|
+
if (!/\.mdx?$/.test(rel)) return null;
|
|
1118
|
+
const parts = rel.split("/");
|
|
1119
|
+
const filename = parts[parts.length - 1];
|
|
1120
|
+
const slug = filename.replace(/\.mdx?$/, "");
|
|
1121
|
+
const category = parts.length > 1 ? parts.slice(0, -1).join("/") : "";
|
|
1122
|
+
return category === "" ? slug : `${category}:${slug}`;
|
|
1123
|
+
}
|
|
1124
|
+
function broadcast(wss, message) {
|
|
1125
|
+
const data = JSON.stringify(message);
|
|
1126
|
+
wss.clients.forEach((client) => {
|
|
1127
|
+
if (client.readyState === 1) {
|
|
1128
|
+
client.send(data);
|
|
1129
|
+
}
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
function startWatcher(specsDir, wss) {
|
|
1133
|
+
const watcher = chokidar.watch(specsDir, {
|
|
1134
|
+
ignoreInitial: true,
|
|
1135
|
+
ignored: /(^|[/\\])\../,
|
|
1136
|
+
awaitWriteFinish: {
|
|
1137
|
+
stabilityThreshold: 150,
|
|
1138
|
+
pollInterval: 50
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
function handler(event) {
|
|
1142
|
+
return (filePath) => {
|
|
1143
|
+
const rel = normalizePath4(path9.relative(specsDir, filePath));
|
|
1144
|
+
const isUserComponent = rel.startsWith("_components/") && /\.(tsx|jsx)$/.test(rel);
|
|
1145
|
+
const isExcalidraw = rel.endsWith(".excalidraw");
|
|
1146
|
+
if (!/\.mdx?$/.test(rel) && !isUserComponent && !isExcalidraw) return;
|
|
1147
|
+
const specId = isUserComponent || isExcalidraw ? null : pathToSpecId(specsDir, filePath);
|
|
1148
|
+
const msg = {
|
|
1149
|
+
type: "fs",
|
|
1150
|
+
event,
|
|
1151
|
+
specId,
|
|
1152
|
+
path: rel
|
|
1153
|
+
};
|
|
1154
|
+
broadcast(wss, msg);
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
watcher.on("add", handler("add"));
|
|
1158
|
+
watcher.on("change", handler("change"));
|
|
1159
|
+
watcher.on("unlink", handler("unlink"));
|
|
1160
|
+
return () => {
|
|
1161
|
+
watcher.close();
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/server/app.ts
|
|
1166
|
+
function createServer(options) {
|
|
1167
|
+
const { specsDir } = options;
|
|
1168
|
+
const app = express();
|
|
1169
|
+
app.use(express.json());
|
|
1170
|
+
app.use("/api/specs", specsRouter(specsDir));
|
|
1171
|
+
app.use("/api/categories", categoriesRouter(specsDir));
|
|
1172
|
+
app.use("/api/data", dataRouter(specsDir));
|
|
1173
|
+
app.use("/api/graph", graphRouter(specsDir));
|
|
1174
|
+
app.use("/api/generators", generatorsRouter(specsDir));
|
|
1175
|
+
app.use("/api/generate", generateRouter(specsDir));
|
|
1176
|
+
app.use("/api/search", searchRouter(specsDir));
|
|
1177
|
+
app.use("/api/components", componentsRouter(specsDir));
|
|
1178
|
+
app.use("/api/validation", validationRouter(specsDir));
|
|
1179
|
+
app.use("/api/schema", schemaRouter(specsDir));
|
|
1180
|
+
app.use("/api/drawings", drawingsRouter(specsDir));
|
|
1181
|
+
app.use("/api/lint", lintRouter(specsDir));
|
|
1182
|
+
app.use("/api/rename", renameRouter(specsDir));
|
|
1183
|
+
app.use("/api/refs", refsRouter(specsDir));
|
|
1184
|
+
const clientDir = fileURLToPath(new URL("../client", import.meta.url));
|
|
1185
|
+
const indexHtml = path10.join(clientDir, "index.html");
|
|
1186
|
+
const clientExists = fs8.existsSync(clientDir);
|
|
1187
|
+
if (clientExists) {
|
|
1188
|
+
app.use(express.static(clientDir));
|
|
1189
|
+
}
|
|
1190
|
+
app.get(/^(?!\/api|\/ws).*/, (_req, res) => {
|
|
1191
|
+
if (fs8.existsSync(indexHtml)) {
|
|
1192
|
+
res.sendFile(indexHtml);
|
|
1193
|
+
} else {
|
|
1194
|
+
res.status(503).send(
|
|
1195
|
+
"\u30AF\u30E9\u30A4\u30A2\u30F3\u30C8\u306E\u30D3\u30EB\u30C9\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3002npm run build \u3092\u5B9F\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
const server = http.createServer(app);
|
|
1200
|
+
const wss = new WebSocketServer({ server, path: "/ws" });
|
|
1201
|
+
const stopWatcher = startWatcher(specsDir, wss);
|
|
1202
|
+
const close = () => {
|
|
1203
|
+
stopWatcher();
|
|
1204
|
+
wss.close();
|
|
1205
|
+
server.close();
|
|
1206
|
+
};
|
|
1207
|
+
return { server, close };
|
|
1208
|
+
}
|
|
1209
|
+
function startServer(options) {
|
|
1210
|
+
return new Promise((resolve, reject) => {
|
|
1211
|
+
let srv;
|
|
1212
|
+
try {
|
|
1213
|
+
srv = createServer(options);
|
|
1214
|
+
} catch (err) {
|
|
1215
|
+
reject(err);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
srv.server.listen(options.port, () => {
|
|
1219
|
+
resolve(srv);
|
|
1220
|
+
});
|
|
1221
|
+
srv.server.on("error", reject);
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// src/server/init.ts
|
|
1226
|
+
import fs9 from "fs/promises";
|
|
1227
|
+
import path11 from "path";
|
|
1228
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1229
|
+
async function copyDir(src, dest) {
|
|
1230
|
+
await fs9.mkdir(dest, { recursive: true });
|
|
1231
|
+
const entries = await fs9.readdir(src, { withFileTypes: true });
|
|
1232
|
+
for (const entry of entries) {
|
|
1233
|
+
const srcPath = path11.join(src, entry.name);
|
|
1234
|
+
const destPath = path11.join(dest, entry.name);
|
|
1235
|
+
if (entry.isDirectory()) {
|
|
1236
|
+
await copyDir(srcPath, destPath);
|
|
1237
|
+
} else if (entry.isFile()) {
|
|
1238
|
+
await fs9.copyFile(srcPath, destPath);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
async function isDirEmpty(dir) {
|
|
1243
|
+
try {
|
|
1244
|
+
const entries = await fs9.readdir(dir);
|
|
1245
|
+
return entries.length === 0;
|
|
1246
|
+
} catch {
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
async function dirExists(dir) {
|
|
1251
|
+
try {
|
|
1252
|
+
const stat = await fs9.stat(dir);
|
|
1253
|
+
return stat.isDirectory();
|
|
1254
|
+
} catch {
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
async function initSpecs(targetDir) {
|
|
1259
|
+
if (await dirExists(targetDir)) {
|
|
1260
|
+
const empty = await isDirEmpty(targetDir);
|
|
1261
|
+
if (!empty) {
|
|
1262
|
+
throw new Error(
|
|
1263
|
+
`"${targetDir}" \u306F\u65E2\u306B\u5B58\u5728\u3057\u3001\u7A7A\u3067\u306F\u3042\u308A\u307E\u305B\u3093\u3002
|
|
1264
|
+
\u5225\u306E\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u3092\u6307\u5B9A\u3059\u308B\u304B\u3001\u624B\u52D5\u3067\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const examplesDir = fileURLToPath2(new URL("../../examples/specs", import.meta.url));
|
|
1269
|
+
try {
|
|
1270
|
+
await fs9.access(examplesDir);
|
|
1271
|
+
} catch {
|
|
1272
|
+
throw new Error(
|
|
1273
|
+
`\u30B5\u30F3\u30D7\u30EB\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${examplesDir}
|
|
1274
|
+
\u30D3\u30EB\u30C9\u304C\u5B8C\u4E86\u3057\u3066\u3044\u308B\u304B\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
await copyDir(examplesDir, targetDir);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// src/server/mcp.ts
|
|
1281
|
+
import fs10 from "fs/promises";
|
|
1282
|
+
import path12 from "path";
|
|
1283
|
+
import matter4 from "gray-matter";
|
|
1284
|
+
import { z } from "zod";
|
|
1285
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1286
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1287
|
+
function ok(result) {
|
|
1288
|
+
return {
|
|
1289
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
function fail(message) {
|
|
1293
|
+
return {
|
|
1294
|
+
content: [{ type: "text", text: message }],
|
|
1295
|
+
isError: true
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
function errMessage(err) {
|
|
1299
|
+
if (err instanceof SpecOpsError) return err.message;
|
|
1300
|
+
if (err instanceof Error) return err.message;
|
|
1301
|
+
return String(err);
|
|
1302
|
+
}
|
|
1303
|
+
function resolveMdxPath(specsDir, category, slug) {
|
|
1304
|
+
const parts = category === "" ? [] : category.split("/");
|
|
1305
|
+
const target = path12.join(specsDir, ...parts, `${slug}.mdx`);
|
|
1306
|
+
if (!guardPath(specsDir, target)) return null;
|
|
1307
|
+
return target;
|
|
1308
|
+
}
|
|
1309
|
+
async function findMeta(specsDir, id) {
|
|
1310
|
+
const specs = await loadSpecs(specsDir);
|
|
1311
|
+
return specs.find((s) => s.id === id) ?? null;
|
|
1312
|
+
}
|
|
1313
|
+
async function startMcpServer(specsDir) {
|
|
1314
|
+
const server = new McpServer({ name: "specifian", version: "0.1.0" });
|
|
1315
|
+
server.registerTool(
|
|
1316
|
+
"list_specs",
|
|
1317
|
+
{
|
|
1318
|
+
description: 'specs \u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u5185\u306E\u5168\u30B9\u30DA\u30C3\u30AF\u306E\u30E1\u30BF\u60C5\u5831 (SpecMeta[]) \u3092\u8FD4\u3057\u307E\u3059\u3002\u5404\u8981\u7D20\u306B\u306F id ("category:slug" \u5F62\u5F0F)\u3001category\u3001slug\u3001path\u3001title\u3001description\u3001front-matter \u5168\u4F53 (data)\u3001\u672C\u6587\u4E2D\u306E wiki \u30EA\u30F3\u30AF\u5148 ID (links)\u3001isIndex \u304C\u542B\u307E\u308C\u307E\u3059\u3002_template \u306F\u9664\u5916\u3055\u308C\u307E\u3059\u3002',
|
|
1319
|
+
inputSchema: {}
|
|
1320
|
+
},
|
|
1321
|
+
async () => {
|
|
1322
|
+
try {
|
|
1323
|
+
const specs = await loadSpecs(specsDir);
|
|
1324
|
+
return ok(specs.filter((s) => s.slug !== "_template"));
|
|
1325
|
+
} catch (err) {
|
|
1326
|
+
return fail(errMessage(err));
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
);
|
|
1330
|
+
server.registerTool(
|
|
1331
|
+
"read_spec",
|
|
1332
|
+
{
|
|
1333
|
+
description: '\u6307\u5B9A\u3057\u305F\u30B9\u30DA\u30C3\u30AF\u306E meta \u3068 content \u3092\u8FD4\u3057\u307E\u3059\u3002id \u306F "category:slug" \u5F62\u5F0F (\u4F8B: "tables:users"\u3001\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u306F "tables:_")\u3002content \u306F front-matter \u3092\u542B\u3080\u751F\u306E MDX \u5168\u6587\u3067\u3059\u3002\u5B58\u5728\u3057\u306A\u3044\u5834\u5408\u306F\u30A8\u30E9\u30FC\u306B\u306A\u308A\u307E\u3059\u3002',
|
|
1334
|
+
inputSchema: {
|
|
1335
|
+
id: z.string().describe('\u30B9\u30DA\u30C3\u30AF ID\u3002"category:slug" \u5F62\u5F0F (\u4F8B: "tables:users")')
|
|
1336
|
+
}
|
|
1337
|
+
},
|
|
1338
|
+
async ({ id }) => {
|
|
1339
|
+
const parsed = parseSpecId(id);
|
|
1340
|
+
if (!parsed) return fail(`\u7121\u52B9\u306A\u30B9\u30DA\u30C3\u30AF ID: ${id}`);
|
|
1341
|
+
const target = resolveMdxPath(specsDir, parsed.category, parsed.slug);
|
|
1342
|
+
if (!target) return fail("\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093");
|
|
1343
|
+
const result = await loadSpec(specsDir, target);
|
|
1344
|
+
if (!result) return fail(`\u30B9\u30DA\u30C3\u30AF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${id}`);
|
|
1345
|
+
return ok(result);
|
|
1346
|
+
}
|
|
1347
|
+
);
|
|
1348
|
+
server.registerTool(
|
|
1349
|
+
"write_spec",
|
|
1350
|
+
{
|
|
1351
|
+
description: '\u65E2\u5B58\u30B9\u30DA\u30C3\u30AF\u306E\u5185\u5BB9\u3092\u4E0A\u66F8\u304D\u4FDD\u5B58\u3057\u307E\u3059\u3002id \u306F "category:slug" \u5F62\u5F0F\u3002content \u306F front-matter \u3092\u542B\u3080 MDX \u5168\u6587\u3002\u5BFE\u8C61\u304C\u5B58\u5728\u3057\u306A\u3044\u5834\u5408\u306F\u30A8\u30E9\u30FC\u306B\u306A\u308A\u307E\u3059 (\u65B0\u898F\u4F5C\u6210\u306F create_spec \u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044)\u3002\u4FDD\u5B58\u5F8C\u306B lint \u3092\u5B9F\u884C\u3057\u3001{ meta, issues } \u3092\u8FD4\u3057\u307E\u3059 (issues \u306F\u60C5\u5831\u63D0\u4F9B\u3067\u3042\u308A\u3001\u4FDD\u5B58\u81EA\u4F53\u306F\u5E38\u306B\u5B9F\u884C\u3055\u308C\u307E\u3059)\u3002',
|
|
1352
|
+
inputSchema: {
|
|
1353
|
+
id: z.string().describe('\u4E0A\u66F8\u304D\u3059\u308B\u65E2\u5B58\u30B9\u30DA\u30C3\u30AF\u306E ID\u3002"category:slug" \u5F62\u5F0F'),
|
|
1354
|
+
content: z.string().describe("front-matter \u3092\u542B\u3080 MDX \u5168\u6587 (\u30D5\u30A1\u30A4\u30EB\u5168\u4F53\u3092\u7F6E\u304D\u63DB\u3048\u307E\u3059)")
|
|
1355
|
+
}
|
|
1356
|
+
},
|
|
1357
|
+
async ({ id, content }) => {
|
|
1358
|
+
const parsed = parseSpecId(id);
|
|
1359
|
+
if (!parsed) return fail(`\u7121\u52B9\u306A\u30B9\u30DA\u30C3\u30AF ID: ${id}`);
|
|
1360
|
+
const target = resolveMdxPath(specsDir, parsed.category, parsed.slug);
|
|
1361
|
+
if (!target) return fail("\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093");
|
|
1362
|
+
try {
|
|
1363
|
+
await fs10.access(target);
|
|
1364
|
+
} catch {
|
|
1365
|
+
return fail(
|
|
1366
|
+
`\u30B9\u30DA\u30C3\u30AF\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${id}\u3002\u65B0\u898F\u4F5C\u6210\u306F create_spec \u3092\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044\u3002`
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1369
|
+
try {
|
|
1370
|
+
await fs10.writeFile(target, content, "utf-8");
|
|
1371
|
+
} catch (err) {
|
|
1372
|
+
return fail(`\u4FDD\u5B58\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errMessage(err)}`);
|
|
1373
|
+
}
|
|
1374
|
+
const meta = await findMeta(specsDir, id);
|
|
1375
|
+
let issues;
|
|
1376
|
+
try {
|
|
1377
|
+
issues = await lintContent(specsDir, {
|
|
1378
|
+
content,
|
|
1379
|
+
category: parsed.category,
|
|
1380
|
+
slug: parsed.slug
|
|
1381
|
+
});
|
|
1382
|
+
} catch (lintErr) {
|
|
1383
|
+
console.error("[mcp] lintContent \u5931\u6557 (\u7121\u8996):", lintErr);
|
|
1384
|
+
issues = [];
|
|
1385
|
+
}
|
|
1386
|
+
return ok({ meta, issues });
|
|
1387
|
+
}
|
|
1388
|
+
);
|
|
1389
|
+
server.registerTool(
|
|
1390
|
+
"create_spec",
|
|
1391
|
+
{
|
|
1392
|
+
description: '\u65B0\u898F\u30B9\u30DA\u30C3\u30AF\u3092\u4F5C\u6210\u3057\u307E\u3059 (POST /api/specs \u3068\u540C\u3058\u610F\u5473\u8AD6)\u3002category \u306F\u30AB\u30C6\u30B4\u30EA\u30FC\u30D1\u30B9 ("/" \u533A\u5207\u308A\u3001\u4F8B: "tables" \u3084 "api/v1")\u3001slug \u306F\u30D5\u30A1\u30A4\u30EB\u540D (\u62E1\u5F35\u5B50\u306A\u3057)\u3002\u30AB\u30C6\u30B4\u30EA\u30FC\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u306F\u4E8B\u524D\u306B\u5B58\u5728\u3057\u3066\u3044\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002\u540C\u540D\u30B9\u30DA\u30C3\u30AF\u304C\u65E2\u306B\u5B58\u5728\u3059\u308B\u5834\u5408\u306F\u30A8\u30E9\u30FC\u306B\u306A\u308A\u307E\u3059\u3002\u30AB\u30C6\u30B4\u30EA\u30FC\u5185\u306B _template.mdx \u304C\u3042\u308C\u3070\u305D\u308C\u3092\u30B3\u30D4\u30FC\u3057 front-matter \u306E title \u3092\u7F6E\u304D\u63DB\u3048\u307E\u3059\u3002\u7121\u3051\u308C\u3070\u6700\u5C0F\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8 (title + \u898B\u51FA\u3057) \u3092\u751F\u6210\u3057\u307E\u3059\u3002\u623B\u308A\u5024\u306F { meta }\u3002',
|
|
1393
|
+
inputSchema: {
|
|
1394
|
+
category: z.string().describe('\u30AB\u30C6\u30B4\u30EA\u30FC\u30D1\u30B9 ("/" \u533A\u5207\u308A\u3002\u30EB\u30FC\u30C8\u76F4\u4E0B\u306F\u7A7A\u6587\u5B57)\u3002\u4F8B: "tables"'),
|
|
1395
|
+
slug: z.string().describe('\u30B9\u30DA\u30C3\u30AF\u306E\u30D5\u30A1\u30A4\u30EB\u540D (\u62E1\u5F35\u5B50\u306A\u3057)\u3002\u4F8B: "users"'),
|
|
1396
|
+
title: z.string().optional().describe("front-matter \u306E title\u3002\u7701\u7565\u6642\u306F slug \u304C\u4F7F\u308F\u308C\u307E\u3059")
|
|
1397
|
+
}
|
|
1398
|
+
},
|
|
1399
|
+
async ({ category, slug, title }) => {
|
|
1400
|
+
if (!slug) return fail("slug \u306F\u5FC5\u9808\u3067\u3059");
|
|
1401
|
+
const cat = category ?? "";
|
|
1402
|
+
const effectiveTitle = title ?? slug;
|
|
1403
|
+
const target = resolveMdxPath(specsDir, cat, slug);
|
|
1404
|
+
if (!target) return fail("\u30D1\u30B9\u30C8\u30E9\u30D0\u30FC\u30B5\u30EB\u306F\u8A31\u53EF\u3055\u308C\u3066\u3044\u307E\u305B\u3093");
|
|
1405
|
+
const categoryParts = cat === "" ? [] : cat.split("/");
|
|
1406
|
+
const categoryDir = path12.join(specsDir, ...categoryParts);
|
|
1407
|
+
try {
|
|
1408
|
+
const stat = await fs10.stat(categoryDir);
|
|
1409
|
+
if (!stat.isDirectory()) {
|
|
1410
|
+
return fail(`\u30AB\u30C6\u30B4\u30EA\u30FC\u304C\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u3067\u306F\u3042\u308A\u307E\u305B\u3093: ${cat}`);
|
|
1411
|
+
}
|
|
1412
|
+
} catch {
|
|
1413
|
+
return fail(`\u30AB\u30C6\u30B4\u30EA\u30FC\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u304C\u5B58\u5728\u3057\u307E\u305B\u3093: ${cat || "(\u30EB\u30FC\u30C8)"}`);
|
|
1414
|
+
}
|
|
1415
|
+
try {
|
|
1416
|
+
await fs10.access(target);
|
|
1417
|
+
return fail(`"${slug}" \u306F\u65E2\u306B\u5B58\u5728\u3057\u307E\u3059`);
|
|
1418
|
+
} catch {
|
|
1419
|
+
}
|
|
1420
|
+
let content;
|
|
1421
|
+
const templatePath = path12.join(categoryDir, "_template.mdx");
|
|
1422
|
+
try {
|
|
1423
|
+
const templateContent = await fs10.readFile(templatePath, "utf-8");
|
|
1424
|
+
const parsed = matter4(templateContent);
|
|
1425
|
+
parsed.data["title"] = effectiveTitle;
|
|
1426
|
+
content = matter4.stringify(parsed.content, parsed.data);
|
|
1427
|
+
} catch {
|
|
1428
|
+
content = `---
|
|
1429
|
+
title: ${effectiveTitle}
|
|
1430
|
+
---
|
|
1431
|
+
|
|
1432
|
+
# ${effectiveTitle}
|
|
1433
|
+
`;
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
await fs10.writeFile(target, content, "utf-8");
|
|
1437
|
+
} catch (err) {
|
|
1438
|
+
return fail(`\u4F5C\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F: ${errMessage(err)}`);
|
|
1439
|
+
}
|
|
1440
|
+
const id = toSpecId(cat, slug);
|
|
1441
|
+
const meta = await findMeta(specsDir, id);
|
|
1442
|
+
if (!meta) return fail("\u4F5C\u6210\u5F8C\u306E\u30B9\u30DA\u30C3\u30AF\u8AAD\u307F\u8FBC\u307F\u306B\u5931\u6557\u3057\u307E\u3057\u305F");
|
|
1443
|
+
return ok({ meta });
|
|
1444
|
+
}
|
|
1445
|
+
);
|
|
1446
|
+
server.registerTool(
|
|
1447
|
+
"rename_spec",
|
|
1448
|
+
{
|
|
1449
|
+
description: '\u30B9\u30DA\u30C3\u30AF\u306E\u30D5\u30A1\u30A4\u30EB\u540D\u3092\u5909\u66F4\u3057\u3001\u4ED6\u306E\u5168\u30B9\u30DA\u30C3\u30AF\u306E wiki \u30EA\u30F3\u30AF [[from]] / [[from|\u30E9\u30D9\u30EB]] \u3092\u4E00\u62EC\u3067 [[to]] \u3078\u66F8\u304D\u63DB\u3048\u307E\u3059 (\u30B3\u30FC\u30C9\u30D5\u30A7\u30F3\u30B9\u30FB\u30A4\u30F3\u30E9\u30A4\u30F3\u30B3\u30FC\u30C9\u5185\u306F\u5909\u66F4\u3057\u307E\u305B\u3093)\u3002from / to \u306F\u3068\u3082\u306B "category:slug" \u5F62\u5F0F\u306E\u30B9\u30DA\u30C3\u30AF ID\u3002to \u306E\u30AB\u30C6\u30B4\u30EA\u30FC\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u306F\u5B58\u5728\u3057\u3066\u3044\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9 (slug "_") / \u30C6\u30F3\u30D7\u30EC\u30FC\u30C8 (slug "_template") \u306F\u30EA\u30CD\u30FC\u30E0\u3067\u304D\u307E\u305B\u3093\u3002\u623B\u308A\u5024\u306F { meta, rewrittenFiles } (rewrittenFiles \u306F\u5B9F\u969B\u306B\u30EA\u30F3\u30AF\u3092\u66F8\u304D\u63DB\u3048\u305F\u30B9\u30DA\u30C3\u30AF ID \u4E00\u89A7)\u3002',
|
|
1450
|
+
inputSchema: {
|
|
1451
|
+
from: z.string().describe('\u5909\u66F4\u5143\u30B9\u30DA\u30C3\u30AF ID\u3002"category:slug" \u5F62\u5F0F'),
|
|
1452
|
+
to: z.string().describe('\u5909\u66F4\u5148\u30B9\u30DA\u30C3\u30AF ID\u3002"category:slug" \u5F62\u5F0F')
|
|
1453
|
+
}
|
|
1454
|
+
},
|
|
1455
|
+
async ({ from, to }) => {
|
|
1456
|
+
try {
|
|
1457
|
+
const result = await renameSpec(specsDir, from, to);
|
|
1458
|
+
return ok(result);
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
return fail(errMessage(err));
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
);
|
|
1464
|
+
server.registerTool(
|
|
1465
|
+
"delete_spec",
|
|
1466
|
+
{
|
|
1467
|
+
description: '\u30B9\u30DA\u30C3\u30AF\u3092\u524A\u9664\u3057\u307E\u3059\u3002id \u306F "category:slug" \u5F62\u5F0F\u3002\u524A\u9664\u524D\u306B\u53C2\u7167\u5143 (\u3053\u306E ID \u3092 wiki \u30EA\u30F3\u30AF\u3057\u3066\u3044\u308B\u30B9\u30DA\u30C3\u30AF) \u3092\u8ABF\u3079\u3001\u623B\u308A\u5024 { ok: true, brokenRefs } \u3067\u300C\u58CA\u308C\u305F\u53C2\u7167\u300D\u3092\u901A\u77E5\u3057\u307E\u3059\u3002brokenRefs \u304C\u975E\u7A7A\u306E\u5834\u5408\u3001\u305D\u308C\u3089\u306E\u30B9\u30DA\u30C3\u30AF\u306F\u672A\u89E3\u6C7A\u30EA\u30F3\u30AF\u3092\u62B1\u3048\u308B\u3053\u3068\u306B\u306A\u308A\u307E\u3059\u3002\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9 (slug "_") \u3084\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3082\u524A\u9664\u53EF\u80FD\u3067\u3059\u3002',
|
|
1468
|
+
inputSchema: {
|
|
1469
|
+
id: z.string().describe('\u524A\u9664\u3059\u308B\u30B9\u30DA\u30C3\u30AF ID\u3002"category:slug" \u5F62\u5F0F')
|
|
1470
|
+
}
|
|
1471
|
+
},
|
|
1472
|
+
async ({ id }) => {
|
|
1473
|
+
try {
|
|
1474
|
+
const refs = await findRefs(specsDir, id);
|
|
1475
|
+
await deleteSpec(specsDir, id);
|
|
1476
|
+
return ok({ ok: true, brokenRefs: refs });
|
|
1477
|
+
} catch (err) {
|
|
1478
|
+
return fail(errMessage(err));
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
);
|
|
1482
|
+
server.registerTool(
|
|
1483
|
+
"get_refs",
|
|
1484
|
+
{
|
|
1485
|
+
description: '\u6307\u5B9A ID \u3092 wiki \u30EA\u30F3\u30AF\u3067\u53C2\u7167\u3057\u3066\u3044\u308B\u30B9\u30DA\u30C3\u30AF ID \u4E00\u89A7 ({ refs }) \u3092\u8FD4\u3057\u307E\u3059\u3002id \u306F "category:slug" \u5F62\u5F0F\u3002\u30D0\u30C3\u30AF\u30EA\u30F3\u30AF\u78BA\u8A8D\u3084\u524A\u9664\u5F71\u97FF\u7BC4\u56F2\u306E\u8ABF\u67FB\u306B\u4F7F\u3044\u307E\u3059\u3002',
|
|
1486
|
+
inputSchema: {
|
|
1487
|
+
id: z.string().describe('\u53C2\u7167\u5143\u3092\u8ABF\u3079\u305F\u3044\u30B9\u30DA\u30C3\u30AF ID\u3002"category:slug" \u5F62\u5F0F')
|
|
1488
|
+
}
|
|
1489
|
+
},
|
|
1490
|
+
async ({ id }) => {
|
|
1491
|
+
try {
|
|
1492
|
+
const refs = await findRefs(specsDir, id);
|
|
1493
|
+
return ok({ refs });
|
|
1494
|
+
} catch (err) {
|
|
1495
|
+
return fail(errMessage(err));
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
);
|
|
1499
|
+
server.registerTool(
|
|
1500
|
+
"search",
|
|
1501
|
+
{
|
|
1502
|
+
description: "\u30B9\u30DA\u30C3\u30AF\u3092\u5168\u6587\u691C\u7D22\u3057\u307E\u3059\u3002title > description > front-matter (data) > \u672C\u6587 \u306E\u512A\u5148\u9806\u3067\u30DE\u30C3\u30C1\u3057\u3001\u5404\u30D2\u30C3\u30C8\u306B snippet (\u524D\u5F8C\u306E\u6587\u8108\u4ED8\u304D\u629C\u7C8B) \u3068 field (\u30DE\u30C3\u30C1\u7B87\u6240) \u304C\u4ED8\u304D\u307E\u3059\u3002_template \u306F\u691C\u7D22\u5BFE\u8C61\u5916\u3002\u623B\u308A\u5024\u306F SearchResult[]\u3002",
|
|
1503
|
+
inputSchema: {
|
|
1504
|
+
query: z.string().describe("\u691C\u7D22\u30AF\u30A8\u30EA\u30FC\u6587\u5B57\u5217 (\u5927\u6587\u5B57\u5C0F\u6587\u5B57\u3092\u533A\u5225\u3057\u307E\u305B\u3093)"),
|
|
1505
|
+
limit: z.number().int().positive().optional().describe("\u6700\u5927\u8FD4\u5374\u4EF6\u6570\u3002\u7701\u7565\u6642\u306F 20")
|
|
1506
|
+
}
|
|
1507
|
+
},
|
|
1508
|
+
async ({ query, limit }) => {
|
|
1509
|
+
try {
|
|
1510
|
+
const results = await searchSpecs(specsDir, query, limit ?? 20);
|
|
1511
|
+
return ok(results);
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
return fail(errMessage(err));
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
);
|
|
1517
|
+
server.registerTool(
|
|
1518
|
+
"get_data",
|
|
1519
|
+
{
|
|
1520
|
+
description: "\u30B9\u30DA\u30C3\u30AF\u306E front-matter (data) \u3092\u307E\u3068\u3081\u3066\u8FD4\u3057\u307E\u3059\u3002category \u3092\u7701\u7565\u3057\u305F\u5834\u5408\u306F { category: { slug: data } } \u306E\u30CD\u30B9\u30C8\u69CB\u9020 (\u5168\u30AB\u30C6\u30B4\u30EA\u30FC)\u3002category \u3092\u6307\u5B9A\u3057\u305F\u5834\u5408\u306F\u305D\u306E\u30AB\u30C6\u30B4\u30EA\u30FC\u306E { slug: data } \u306E\u307F\u3002_template \u306F\u9664\u5916\u3055\u308C\u307E\u3059\u3002\u30C6\u30FC\u30D6\u30EB\u5B9A\u7FA9\u4E00\u89A7\u306E\u53D6\u5F97\u306A\u3069\u306B\u4FBF\u5229\u3067\u3059\u3002",
|
|
1521
|
+
inputSchema: {
|
|
1522
|
+
category: z.string().optional().describe('\u30AB\u30C6\u30B4\u30EA\u30FC\u30D1\u30B9 ("/" \u533A\u5207\u308A)\u3002\u7701\u7565\u6642\u306F\u5168\u30AB\u30C6\u30B4\u30EA\u30FC')
|
|
1523
|
+
}
|
|
1524
|
+
},
|
|
1525
|
+
async ({ category }) => {
|
|
1526
|
+
try {
|
|
1527
|
+
const specs = await loadSpecs(specsDir);
|
|
1528
|
+
const filtered = specs.filter((s) => s.slug !== "_template");
|
|
1529
|
+
if (category === void 0) {
|
|
1530
|
+
const result2 = {};
|
|
1531
|
+
for (const spec of filtered) {
|
|
1532
|
+
const cat2 = spec.category;
|
|
1533
|
+
if (!result2[cat2]) result2[cat2] = {};
|
|
1534
|
+
result2[cat2][spec.slug] = spec.data;
|
|
1535
|
+
}
|
|
1536
|
+
return ok(result2);
|
|
1537
|
+
}
|
|
1538
|
+
const cat = category.split("/").filter(Boolean).join("/");
|
|
1539
|
+
const result = {};
|
|
1540
|
+
for (const spec of filtered) {
|
|
1541
|
+
if (spec.category === cat) result[spec.slug] = spec.data;
|
|
1542
|
+
}
|
|
1543
|
+
return ok(result);
|
|
1544
|
+
} catch (err) {
|
|
1545
|
+
return fail(errMessage(err));
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
);
|
|
1549
|
+
server.registerTool(
|
|
1550
|
+
"validate",
|
|
1551
|
+
{
|
|
1552
|
+
description: "_schema.json \u3092\u6301\u3064\u30AB\u30C6\u30B4\u30EA\u30FC\u306B\u3064\u3044\u3066\u3001\u5168\u30B9\u30DA\u30C3\u30AF\u306E front-matter \u3092 JSON Schema (ajv) \u3067\u691C\u8A3C\u3057\u3001\u9055\u53CD\u4E00\u89A7 (ValidationReport { issues }) \u3092\u8FD4\u3057\u307E\u3059\u3002issues \u304C\u7A7A\u306A\u3089\u5168\u4EF6\u9069\u5408\u3067\u3059\u3002",
|
|
1553
|
+
inputSchema: {}
|
|
1554
|
+
},
|
|
1555
|
+
async () => {
|
|
1556
|
+
try {
|
|
1557
|
+
const report = await validateSpecs(specsDir);
|
|
1558
|
+
return ok(report);
|
|
1559
|
+
} catch (err) {
|
|
1560
|
+
return fail(errMessage(err));
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
);
|
|
1564
|
+
server.registerTool(
|
|
1565
|
+
"lint",
|
|
1566
|
+
{
|
|
1567
|
+
description: "\u4FDD\u5B58\u305B\u305A\u306B MDX \u5168\u6587\u3092\u691C\u8A3C\u3057\u3001issues (LintIssue[]) \u3092\u8FD4\u3057\u307E\u3059\u3002\u30C1\u30A7\u30C3\u30AF\u5185\u5BB9: YAML \u30D1\u30FC\u30B9 / MDX \u69CB\u6587 / wiki \u30EA\u30F3\u30AF\u89E3\u6C7A / (category \u6307\u5B9A\u6642) \u30B9\u30AD\u30FC\u30DE\u691C\u8A3C\u3002content \u306F front-matter \u3092\u542B\u3080 MDX \u5168\u6587\u3002category/slug \u3092\u6E21\u3059\u3068\u30B9\u30AD\u30FC\u30DE\u691C\u8A3C\u3082\u884C\u308F\u308C\u307E\u3059\u3002\u4FDD\u5B58\u524D\u306E\u4E8B\u524D\u30C1\u30A7\u30C3\u30AF\u306B\u4F7F\u3063\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
1568
|
+
inputSchema: {
|
|
1569
|
+
content: z.string().describe("front-matter \u3092\u542B\u3080 MDX \u5168\u6587"),
|
|
1570
|
+
category: z.string().optional().describe("\u30B9\u30AD\u30FC\u30DE\u691C\u8A3C\u306B\u4F7F\u3046\u30AB\u30C6\u30B4\u30EA\u30FC\u3002\u7701\u7565\u6642\u306F\u30B9\u30AD\u30FC\u30DE\u691C\u8A3C\u3092\u30B9\u30AD\u30C3\u30D7"),
|
|
1571
|
+
slug: z.string().optional().describe('\u5BFE\u8C61 slug\u3002"_" / "_template" \u306F\u30B9\u30AD\u30FC\u30DE\u691C\u8A3C\u5BFE\u8C61\u5916\u306B\u306A\u308A\u307E\u3059')
|
|
1572
|
+
}
|
|
1573
|
+
},
|
|
1574
|
+
async ({ content, category, slug }) => {
|
|
1575
|
+
try {
|
|
1576
|
+
const issues = await lintContent(specsDir, { content, category, slug });
|
|
1577
|
+
return ok({ issues });
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
return fail(errMessage(err));
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
);
|
|
1583
|
+
server.registerTool(
|
|
1584
|
+
"list_generators",
|
|
1585
|
+
{
|
|
1586
|
+
description: "specs/_generators/ \u914D\u4E0B\u306E\u30B3\u30FC\u30C9\u30B8\u30A7\u30CD\u30EC\u30FC\u30BF\u30FC\u540D\u4E00\u89A7 (string[]) \u3092\u8FD4\u3057\u307E\u3059\u3002generate \u30C4\u30FC\u30EB\u306E generator \u5F15\u6570\u306B\u4F7F\u3048\u307E\u3059\u3002",
|
|
1587
|
+
inputSchema: {}
|
|
1588
|
+
},
|
|
1589
|
+
async () => {
|
|
1590
|
+
try {
|
|
1591
|
+
const generators = await listGenerators(specsDir);
|
|
1592
|
+
return ok(generators);
|
|
1593
|
+
} catch (err) {
|
|
1594
|
+
return fail(errMessage(err));
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
);
|
|
1598
|
+
server.registerTool(
|
|
1599
|
+
"generate",
|
|
1600
|
+
{
|
|
1601
|
+
description: '\u30B3\u30FC\u30C9\u30B8\u30A7\u30CD\u30EC\u30FC\u30BF\u30FC\u3092\u5B9F\u884C\u3057\u307E\u3059\u3002generator \u306F list_generators \u3067\u5F97\u3089\u308C\u308B\u540D\u524D\u3002specId ("category:slug") \u3092\u6307\u5B9A\u3059\u308B\u3068\u305D\u306E\u30B9\u30DA\u30C3\u30AF\u306E\u307F\u3001\u7701\u7565\u6642\u306F\u5168\u30B9\u30DA\u30C3\u30AF\u304C\u5BFE\u8C61\u3002out \u3092\u6307\u5B9A\u3059\u308B\u3068\u30D7\u30ED\u30BB\u30B9\u306E cwd \u76F8\u5BFE\u3067\u30D5\u30A1\u30A4\u30EB\u3092\u30C7\u30A3\u30B9\u30AF\u306B\u66F8\u304D\u51FA\u3057\u307E\u3059 (\u7701\u7565\u6642\u306F\u66F8\u304D\u8FBC\u307E\u305A\u30EC\u30B9\u30DD\u30F3\u30B9\u306E\u307F)\u3002\u623B\u308A\u5024\u306F { files: { path, content }[] }\u3002',
|
|
1602
|
+
inputSchema: {
|
|
1603
|
+
generator: z.string().describe("\u30B8\u30A7\u30CD\u30EC\u30FC\u30BF\u30FC\u540D (specs/_generators/<name>.md)"),
|
|
1604
|
+
specId: z.string().optional().describe('\u5BFE\u8C61\u30B9\u30DA\u30C3\u30AF ID\u3002"category:slug" \u5F62\u5F0F\u3002\u7701\u7565\u6642\u306F\u5168\u30B9\u30DA\u30C3\u30AF'),
|
|
1605
|
+
out: z.string().optional().describe("\u51FA\u529B\u5148\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC (cwd \u76F8\u5BFE)\u3002\u7701\u7565\u6642\u306F\u30C7\u30A3\u30B9\u30AF\u306B\u66F8\u304D\u8FBC\u307F\u307E\u305B\u3093")
|
|
1606
|
+
}
|
|
1607
|
+
},
|
|
1608
|
+
async ({ generator, specId, out }) => {
|
|
1609
|
+
try {
|
|
1610
|
+
const allSpecs = await loadSpecs(specsDir);
|
|
1611
|
+
const files = await runGenerator(
|
|
1612
|
+
specsDir,
|
|
1613
|
+
generator,
|
|
1614
|
+
allSpecs,
|
|
1615
|
+
specId,
|
|
1616
|
+
out
|
|
1617
|
+
);
|
|
1618
|
+
return ok({ files });
|
|
1619
|
+
} catch (err) {
|
|
1620
|
+
return fail(errMessage(err));
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
);
|
|
1624
|
+
const transport = new StdioServerTransport();
|
|
1625
|
+
await server.connect(transport);
|
|
1626
|
+
await new Promise((resolve) => {
|
|
1627
|
+
transport.onclose = () => resolve();
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
// src/cli/index.ts
|
|
1632
|
+
var VERSION = "0.1.0";
|
|
1633
|
+
program.name("specifian").version(VERSION).description("Storybook \u30E9\u30A4\u30AF\u306A MDX \u30B9\u30DA\u30C3\u30AF\u7BA1\u7406\u30C4\u30FC\u30EB");
|
|
1634
|
+
program.command("serve", { isDefault: true }).description("\u30B9\u30DA\u30C3\u30AF\u30B5\u30FC\u30D0\u30FC\u3092\u8D77\u52D5\u3057\u307E\u3059").option("--dir <specsDir>", "\u30B9\u30DA\u30C3\u30AF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC", "./specs").option("--port <port>", "\u30DD\u30FC\u30C8\u756A\u53F7", "4400").option("--open", "\u30D6\u30E9\u30A6\u30B6\u30FC\u3092\u81EA\u52D5\u7684\u306B\u958B\u304F", false).action(async (opts) => {
|
|
1635
|
+
const specsDir = path13.resolve(opts.dir);
|
|
1636
|
+
const port = parseInt(opts.port, 10);
|
|
1637
|
+
if (!fs11.existsSync(specsDir)) {
|
|
1638
|
+
console.error(
|
|
1639
|
+
`\u30A8\u30E9\u30FC: \u30B9\u30DA\u30C3\u30AF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${specsDir}`
|
|
1640
|
+
);
|
|
1641
|
+
console.error("\u30D2\u30F3\u30C8: specifian init --dir <\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC> \u3067\u521D\u671F\u5316\u3067\u304D\u307E\u3059\u3002");
|
|
1642
|
+
process.exit(1);
|
|
1643
|
+
}
|
|
1644
|
+
console.log(`
|
|
1645
|
+
\u{1F5D2} specifian v${VERSION}`);
|
|
1646
|
+
console.log(`\u{1F4C2} \u30B9\u30DA\u30C3\u30AF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC: ${specsDir}`);
|
|
1647
|
+
console.log(`\u{1F680} \u30B5\u30FC\u30D0\u30FC\u3092\u8D77\u52D5\u3057\u3066\u3044\u307E\u3059... http://localhost:${port}
|
|
1648
|
+
`);
|
|
1649
|
+
try {
|
|
1650
|
+
await startServer({ specsDir, port });
|
|
1651
|
+
console.log(`\u2705 \u30B5\u30FC\u30D0\u30FC\u8D77\u52D5\u5B8C\u4E86: http://localhost:${port}`);
|
|
1652
|
+
if (opts.open) {
|
|
1653
|
+
const { default: open } = await import("open");
|
|
1654
|
+
await open(`http://localhost:${port}`);
|
|
1655
|
+
}
|
|
1656
|
+
} catch (err) {
|
|
1657
|
+
console.error("\u30B5\u30FC\u30D0\u30FC\u306E\u8D77\u52D5\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", err);
|
|
1658
|
+
process.exit(1);
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
program.command("init").description("\u30B5\u30F3\u30D7\u30EB\u30B9\u30DA\u30C3\u30AF\u3092\u4F7F\u3063\u3066\u521D\u671F\u5316\u3057\u307E\u3059").option("--dir <specsDir>", "\u521D\u671F\u5316\u5148\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC", "./specs").action(async (opts) => {
|
|
1662
|
+
const targetDir = path13.resolve(opts.dir);
|
|
1663
|
+
console.log(`\u{1F4C2} \u521D\u671F\u5316\u5148: ${targetDir}`);
|
|
1664
|
+
try {
|
|
1665
|
+
await initSpecs(targetDir);
|
|
1666
|
+
console.log(`\u2705 \u521D\u671F\u5316\u5B8C\u4E86: ${targetDir}`);
|
|
1667
|
+
console.log("\u6B21\u306E\u30B3\u30DE\u30F3\u30C9\u3067\u30B5\u30FC\u30D0\u30FC\u3092\u8D77\u52D5\u3067\u304D\u307E\u3059:");
|
|
1668
|
+
console.log(` specifian serve --dir ${opts.dir}`);
|
|
1669
|
+
} catch (err) {
|
|
1670
|
+
console.error("\u521D\u671F\u5316\u306B\u5931\u6557\u3057\u307E\u3057\u305F:", err instanceof Error ? err.message : err);
|
|
1671
|
+
process.exit(1);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
program.command("generate <generator>").description("\u30B3\u30FC\u30C9\u751F\u6210\u30C6\u30F3\u30D7\u30EC\u30FC\u30C8\u3092\u5B9F\u884C\u3057\u307E\u3059").option("--dir <specsDir>", "\u30B9\u30DA\u30C3\u30AF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC", "./specs").option("--spec <specId>", "\u5BFE\u8C61\u30B9\u30DA\u30C3\u30AF ID (\u7701\u7565\u6642\u306F\u5168\u30B9\u30DA\u30C3\u30AF)").option("--out <outDir>", "\u51FA\u529B\u5148\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC", ".").action(
|
|
1675
|
+
async (generator, opts) => {
|
|
1676
|
+
const specsDir = path13.resolve(opts.dir);
|
|
1677
|
+
if (!fs11.existsSync(specsDir)) {
|
|
1678
|
+
console.error(
|
|
1679
|
+
`\u30A8\u30E9\u30FC: \u30B9\u30DA\u30C3\u30AF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${specsDir}`
|
|
1680
|
+
);
|
|
1681
|
+
process.exit(1);
|
|
1682
|
+
}
|
|
1683
|
+
try {
|
|
1684
|
+
const allSpecs = await loadSpecs(specsDir);
|
|
1685
|
+
const files = await runGenerator(
|
|
1686
|
+
specsDir,
|
|
1687
|
+
generator,
|
|
1688
|
+
allSpecs,
|
|
1689
|
+
opts.spec,
|
|
1690
|
+
opts.out
|
|
1691
|
+
);
|
|
1692
|
+
if (files.length === 0) {
|
|
1693
|
+
console.log("\u751F\u6210\u3055\u308C\u305F\u30D5\u30A1\u30A4\u30EB\u306F\u3042\u308A\u307E\u305B\u3093\u3002");
|
|
1694
|
+
} else {
|
|
1695
|
+
console.log(`\u2705 ${files.length} \u4EF6\u306E\u30D5\u30A1\u30A4\u30EB\u3092\u751F\u6210\u3057\u307E\u3057\u305F:`);
|
|
1696
|
+
for (const f of files) {
|
|
1697
|
+
console.log(` ${f.path}`);
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
} catch (err) {
|
|
1701
|
+
console.error(
|
|
1702
|
+
"\u30B3\u30FC\u30C9\u751F\u6210\u306B\u5931\u6557\u3057\u307E\u3057\u305F:",
|
|
1703
|
+
err instanceof Error ? err.message : err
|
|
1704
|
+
);
|
|
1705
|
+
process.exit(1);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
);
|
|
1709
|
+
program.command("validate").description("front-matter \u3092\u30B9\u30AD\u30FC\u30DE\u306B\u5BFE\u3057\u3066\u30D0\u30EA\u30C7\u30FC\u30B7\u30E7\u30F3\u3057\u307E\u3059").option("--dir <specsDir>", "\u30B9\u30DA\u30C3\u30AF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC", "./specs").action(async (opts) => {
|
|
1710
|
+
const specsDir = path13.resolve(opts.dir);
|
|
1711
|
+
if (!fs11.existsSync(specsDir)) {
|
|
1712
|
+
console.error(
|
|
1713
|
+
`\u30A8\u30E9\u30FC: \u30B9\u30DA\u30C3\u30AF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${specsDir}`
|
|
1714
|
+
);
|
|
1715
|
+
process.exit(1);
|
|
1716
|
+
}
|
|
1717
|
+
try {
|
|
1718
|
+
const report = await validateSpecs(specsDir);
|
|
1719
|
+
if (report.issues.length === 0) {
|
|
1720
|
+
console.log("\u2705 front-matter \u306F\u3059\u3079\u3066\u306E\u30B9\u30AD\u30FC\u30DE\u306B\u9069\u5408\u3057\u3066\u3044\u307E\u3059");
|
|
1721
|
+
process.exit(0);
|
|
1722
|
+
} else {
|
|
1723
|
+
console.error(`\u274C \u30B9\u30AD\u30FC\u30DE\u9055\u53CD\u304C ${report.issues.length} \u4EF6\u898B\u3064\u304B\u308A\u307E\u3057\u305F:`);
|
|
1724
|
+
for (const issue of report.issues) {
|
|
1725
|
+
console.error(` ${issue.specId} ${issue.path}: ${issue.message}`);
|
|
1726
|
+
}
|
|
1727
|
+
process.exit(1);
|
|
1728
|
+
}
|
|
1729
|
+
} catch (err) {
|
|
1730
|
+
console.error(
|
|
1731
|
+
"\u30D0\u30EA\u30C7\u30FC\u30B7\u30E7\u30F3\u306B\u5931\u6557\u3057\u307E\u3057\u305F:",
|
|
1732
|
+
err instanceof Error ? err.message : err
|
|
1733
|
+
);
|
|
1734
|
+
process.exit(1);
|
|
1735
|
+
}
|
|
1736
|
+
});
|
|
1737
|
+
program.command("mcp").description("MCP \u30B5\u30FC\u30D0\u30FC (stdio) \u3092\u8D77\u52D5\u3057\u307E\u3059\u3002AI \u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u304C\u30B9\u30DA\u30C3\u30AF\u3092\u8AAD\u307F\u66F8\u304D\u3067\u304D\u307E\u3059").option("--dir <specsDir>", "\u30B9\u30DA\u30C3\u30AF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC", "./specs").action(async (opts) => {
|
|
1738
|
+
const specsDir = path13.resolve(opts.dir);
|
|
1739
|
+
if (!fs11.existsSync(specsDir)) {
|
|
1740
|
+
console.error(
|
|
1741
|
+
`\u30A8\u30E9\u30FC: \u30B9\u30DA\u30C3\u30AF\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093: ${specsDir}`
|
|
1742
|
+
);
|
|
1743
|
+
console.error("\u30D2\u30F3\u30C8: specifian init --dir <\u30C7\u30A3\u30EC\u30AF\u30C8\u30EA\u30FC> \u3067\u521D\u671F\u5316\u3067\u304D\u307E\u3059\u3002");
|
|
1744
|
+
process.exit(1);
|
|
1745
|
+
}
|
|
1746
|
+
console.error(`specifian MCP server (stdio) \u2014 specs: ${specsDir}`);
|
|
1747
|
+
try {
|
|
1748
|
+
await startMcpServer(specsDir);
|
|
1749
|
+
} catch (err) {
|
|
1750
|
+
console.error(
|
|
1751
|
+
"MCP \u30B5\u30FC\u30D0\u30FC\u306E\u8D77\u52D5\u306B\u5931\u6557\u3057\u307E\u3057\u305F:",
|
|
1752
|
+
err instanceof Error ? err.message : err
|
|
1753
|
+
);
|
|
1754
|
+
process.exit(1);
|
|
1755
|
+
}
|
|
1756
|
+
});
|
|
1757
|
+
program.parse(process.argv);
|