@dypai-ai/mcp 1.6.16 → 1.6.17
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/package.json +1 -1
- package/src/generated/serverInstructions.js +3 -3
- package/src/index.js +1 -1
- package/src/lib/backendSnapshot.js +8 -0
- package/src/lib/effective-workflows-runner.js +6 -3
- package/src/toolProfiles.js +6 -5
- package/src/tools/frontend.js +42 -8
- package/src/tools/project-deploy-production.js +54 -31
- package/src/tools/project-push.js +0 -5
- package/src/tools/sync/diff.js +28 -130
- package/src/tools/sync/planner.js +20 -1
- package/src/tools/sync/pull.js +38 -15
- package/src/tools/sync/push.js +46 -10
- package/src/tools/sync/source-diff.js +217 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
2
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "fs/promises"
|
|
3
|
+
import { dirname, join } from "path"
|
|
4
|
+
|
|
5
|
+
const BACKEND_SOURCE_DIRS = ["flows", "automations", "endpoints", "migrations", "types", "lib"]
|
|
6
|
+
const BACKEND_SOURCE_EXACT = [
|
|
7
|
+
"schema.sql",
|
|
8
|
+
"realtime.yaml",
|
|
9
|
+
"credentials.yaml",
|
|
10
|
+
"node-catalog.json",
|
|
11
|
+
"capability-catalog.json",
|
|
12
|
+
"capability-brief.md",
|
|
13
|
+
"dypai.config.yaml",
|
|
14
|
+
]
|
|
15
|
+
const BACKEND_SOURCE_EXTS = new Set([
|
|
16
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts", ".cjs", ".cts",
|
|
17
|
+
".json", ".yaml", ".yml", ".sql", ".md", ".txt",
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
function extOfPath(path) {
|
|
21
|
+
const filename = path.split("/").pop() || path
|
|
22
|
+
return filename.includes(".") ? `.${filename.split(".").pop().toLowerCase()}` : ""
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isTextBackendPath(path) {
|
|
26
|
+
return BACKEND_SOURCE_EXTS.has(extOfPath(path))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizePath(path) {
|
|
30
|
+
return String(path || "").replace(/\\/g, "/").replace(/^\/+/, "")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isTrackedBackendSourcePath(path) {
|
|
34
|
+
const normalized = normalizePath(path)
|
|
35
|
+
if (!normalized.startsWith("dypai/")) return false
|
|
36
|
+
if (normalized.startsWith("dypai/.dypai/")) return false
|
|
37
|
+
const rel = normalized.slice("dypai/".length)
|
|
38
|
+
if (BACKEND_SOURCE_EXACT.includes(rel)) return true
|
|
39
|
+
return BACKEND_SOURCE_DIRS.some((dir) => rel.startsWith(`${dir}/`)) && isTextBackendPath(rel)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function hashContent(content) {
|
|
43
|
+
return createHash("sha256").update(content).digest("hex")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function addFile(files, absPath, relPath) {
|
|
47
|
+
if (!isTextBackendPath(relPath)) return
|
|
48
|
+
const content = await readFile(absPath, "utf8")
|
|
49
|
+
files.push({
|
|
50
|
+
path: `dypai/${relPath}`,
|
|
51
|
+
content,
|
|
52
|
+
sha256: hashContent(content),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function walkDir(files, absDir, relBase) {
|
|
57
|
+
let entries = []
|
|
58
|
+
try {
|
|
59
|
+
entries = await readdir(absDir, { withFileTypes: true })
|
|
60
|
+
} catch {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (entry.name.startsWith(".")) continue
|
|
65
|
+
const relPath = `${relBase}/${entry.name}`.replace(/^\/+/, "")
|
|
66
|
+
const absPath = join(absDir, entry.name)
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
await walkDir(files, absPath, relPath)
|
|
69
|
+
} else if (entry.isFile()) {
|
|
70
|
+
await addFile(files, absPath, relPath)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function collectBackendSourceFiles(rootDir) {
|
|
76
|
+
const files = []
|
|
77
|
+
|
|
78
|
+
for (const relPath of BACKEND_SOURCE_EXACT) {
|
|
79
|
+
const absPath = join(rootDir, relPath)
|
|
80
|
+
try {
|
|
81
|
+
const s = await stat(absPath)
|
|
82
|
+
if (s.isFile()) await addFile(files, absPath, relPath)
|
|
83
|
+
} catch {
|
|
84
|
+
// optional
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const dir of BACKEND_SOURCE_DIRS) {
|
|
89
|
+
await walkDir(files, join(rootDir, dir), dir)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
files.sort((a, b) => a.path.localeCompare(b.path))
|
|
93
|
+
return files
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeBaseline(raw) {
|
|
97
|
+
const parsed = JSON.parse(raw)
|
|
98
|
+
const files = parsed?.files
|
|
99
|
+
const result = new Map()
|
|
100
|
+
if (Array.isArray(files)) {
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
if (typeof file?.path === "string" && typeof file?.sha256 === "string" && isTrackedBackendSourcePath(file.path)) {
|
|
103
|
+
result.set(file.path, file.sha256)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} else if (files && typeof files === "object") {
|
|
107
|
+
for (const [path, value] of Object.entries(files)) {
|
|
108
|
+
if (!isTrackedBackendSourcePath(path)) continue
|
|
109
|
+
if (typeof value === "string") result.set(path, value)
|
|
110
|
+
else if (value && typeof value === "object" && typeof value.sha256 === "string") result.set(path, value.sha256)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function readBackendSourceBaseline(rootDir) {
|
|
117
|
+
try {
|
|
118
|
+
const raw = await readFile(join(rootDir, ".dypai", "backend-baseline.json"), "utf8")
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
baseline: normalizeBaseline(raw),
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.code === "ENOENT") {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
missing: true,
|
|
128
|
+
baseline: new Map(),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
error: error?.message || String(error),
|
|
134
|
+
baseline: new Map(),
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function diffBackendSource(rootDir) {
|
|
140
|
+
const currentFiles = await collectBackendSourceFiles(rootDir)
|
|
141
|
+
const current = new Map(currentFiles.map((file) => [file.path, file.sha256]))
|
|
142
|
+
const baselineResult = await readBackendSourceBaseline(rootDir)
|
|
143
|
+
|
|
144
|
+
if (!baselineResult.ok && !baselineResult.missing) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
reason: "baseline_parse_error",
|
|
148
|
+
error: baselineResult.error,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (baselineResult.missing) {
|
|
153
|
+
return {
|
|
154
|
+
ok: true,
|
|
155
|
+
baseline: false,
|
|
156
|
+
files: {
|
|
157
|
+
created: currentFiles.map((file) => file.path),
|
|
158
|
+
updated: [],
|
|
159
|
+
deleted: [],
|
|
160
|
+
},
|
|
161
|
+
summary: {
|
|
162
|
+
create: currentFiles.length,
|
|
163
|
+
update: 0,
|
|
164
|
+
delete: 0,
|
|
165
|
+
unchanged: 0,
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const baseline = baselineResult.baseline
|
|
171
|
+
const created = []
|
|
172
|
+
const updated = []
|
|
173
|
+
const deleted = []
|
|
174
|
+
const unchanged = []
|
|
175
|
+
|
|
176
|
+
for (const [path, hash] of current) {
|
|
177
|
+
if (!baseline.has(path)) created.push(path)
|
|
178
|
+
else if (baseline.get(path) !== hash) updated.push(path)
|
|
179
|
+
else unchanged.push(path)
|
|
180
|
+
}
|
|
181
|
+
for (const path of baseline.keys()) {
|
|
182
|
+
if (!current.has(path)) deleted.push(path)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
ok: true,
|
|
187
|
+
baseline: true,
|
|
188
|
+
files: {
|
|
189
|
+
created,
|
|
190
|
+
updated,
|
|
191
|
+
deleted,
|
|
192
|
+
},
|
|
193
|
+
summary: {
|
|
194
|
+
create: created.length,
|
|
195
|
+
update: updated.length,
|
|
196
|
+
delete: deleted.length,
|
|
197
|
+
unchanged: unchanged.length,
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function writeBackendSourceBaseline(rootDir, projectId = null) {
|
|
203
|
+
const files = await collectBackendSourceFiles(rootDir)
|
|
204
|
+
const payload = {
|
|
205
|
+
synced_at: new Date().toISOString(),
|
|
206
|
+
...(projectId ? { project_id: projectId } : {}),
|
|
207
|
+
files: files.map((file) => ({
|
|
208
|
+
path: file.path,
|
|
209
|
+
sha256: file.sha256,
|
|
210
|
+
})),
|
|
211
|
+
}
|
|
212
|
+
const target = join(rootDir, ".dypai", "backend-baseline.json")
|
|
213
|
+
await mkdir(dirname(target), { recursive: true })
|
|
214
|
+
await writeFile(target, JSON.stringify(payload, null, 2) + "\n", "utf8")
|
|
215
|
+
return payload
|
|
216
|
+
}
|
|
217
|
+
|