@dypai-ai/mcp 1.6.20 → 1.7.1
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 +82 -8
- package/src/tools/deploy.js +142 -4
- package/src/tools/project-deploy-production.js +9 -0
- package/src/tools/storage.js +437 -72
- package/src/tools/sync/generate-types.js +17 -1
- package/src/tools/sync/planner.js +40 -3
- package/src/tools/sync/push.js +22 -3
- package/src/tools/sync/test-endpoint.js +7 -6
- package/src/tools/sync/validate.js +74 -1
- package/src/tools/trace-summarize.js +109 -1
package/src/tools/deploy.js
CHANGED
|
@@ -55,6 +55,8 @@ const IGNORE_DIRS_ANYWHERE = new Set([
|
|
|
55
55
|
// under `dypai/` are filtered by path below.
|
|
56
56
|
const IGNORE_DIRS_AT_ROOT = new Set([])
|
|
57
57
|
|
|
58
|
+
const IGNORE_FILES = [".dypaiignore", ".gitignore"]
|
|
59
|
+
|
|
58
60
|
// ─── Accepted file extensions ───────────────────────────────────────────────
|
|
59
61
|
|
|
60
62
|
const CODE_EXTS = new Set([
|
|
@@ -116,6 +118,87 @@ function sha256(buf) {
|
|
|
116
118
|
return createHash("sha256").update(buf).digest("hex")
|
|
117
119
|
}
|
|
118
120
|
|
|
121
|
+
function escapeRegExp(value) {
|
|
122
|
+
return String(value).replace(/[|\\{}()[\]^$+?.]/g, "\\$&")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function globSegmentToRegExp(pattern) {
|
|
126
|
+
const body = String(pattern)
|
|
127
|
+
.split("*")
|
|
128
|
+
.map((part) => escapeRegExp(part))
|
|
129
|
+
.join(".*")
|
|
130
|
+
return new RegExp(`^${body}$`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function readIgnoreRules(sourceDirectory) {
|
|
134
|
+
const rules = []
|
|
135
|
+
for (const filename of IGNORE_FILES) {
|
|
136
|
+
const filePath = join(sourceDirectory, filename)
|
|
137
|
+
if (!existsSync(filePath)) continue
|
|
138
|
+
let lines = []
|
|
139
|
+
try {
|
|
140
|
+
lines = readFileSync(filePath, "utf-8").split(/\r?\n/)
|
|
141
|
+
} catch {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
for (const rawLine of lines) {
|
|
145
|
+
let pattern = rawLine.trim()
|
|
146
|
+
if (!pattern || pattern.startsWith("#")) continue
|
|
147
|
+
if (pattern.startsWith("!")) continue
|
|
148
|
+
pattern = pattern.replace(/\\/g, "/")
|
|
149
|
+
|
|
150
|
+
const directoryOnly = pattern.endsWith("/")
|
|
151
|
+
if (directoryOnly) pattern = pattern.replace(/\/+$/, "")
|
|
152
|
+
if (!pattern) continue
|
|
153
|
+
|
|
154
|
+
const anchored = pattern.startsWith("/")
|
|
155
|
+
if (anchored) pattern = pattern.replace(/^\/+/, "")
|
|
156
|
+
if (!pattern) continue
|
|
157
|
+
|
|
158
|
+
rules.push({
|
|
159
|
+
source: filename,
|
|
160
|
+
pattern: rawLine.trim(),
|
|
161
|
+
value: pattern,
|
|
162
|
+
anchored,
|
|
163
|
+
directoryOnly,
|
|
164
|
+
hasSlash: pattern.includes("/"),
|
|
165
|
+
re: globSegmentToRegExp(pattern),
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return rules
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function pathMatchesIgnoreRule(path, isDirectory, rule) {
|
|
173
|
+
const normalized = path.replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "")
|
|
174
|
+
if (!normalized) return false
|
|
175
|
+
if (rule.directoryOnly && !isDirectory) return false
|
|
176
|
+
|
|
177
|
+
if (!rule.hasSlash) {
|
|
178
|
+
const segments = normalized.split("/")
|
|
179
|
+
if (rule.directoryOnly) return segments.some((segment) => rule.re.test(segment))
|
|
180
|
+
return rule.re.test(segments[segments.length - 1] || "")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const value = rule.value.replace(/\/+$/, "")
|
|
184
|
+
const re = globSegmentToRegExp(value)
|
|
185
|
+
if (rule.anchored) {
|
|
186
|
+
return normalized === value || normalized.startsWith(`${value}/`) || re.test(normalized)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (
|
|
190
|
+
normalized === value
|
|
191
|
+
|| normalized.endsWith(`/${value}`)
|
|
192
|
+
|| normalized.startsWith(`${value}/`)
|
|
193
|
+
|| normalized.includes(`/${value}/`)
|
|
194
|
+
|| re.test(normalized)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function ignoredByRule(path, isDirectory, rules) {
|
|
199
|
+
return rules.find((rule) => pathMatchesIgnoreRule(path, isDirectory, rule)) || null
|
|
200
|
+
}
|
|
201
|
+
|
|
119
202
|
function isAllowedRootDypaiSourcePath(path) {
|
|
120
203
|
if (!path.startsWith("dypai/")) return true
|
|
121
204
|
if (
|
|
@@ -213,6 +296,18 @@ export function updateMediaManifest(sourceDirectory, localPath, entry) {
|
|
|
213
296
|
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n")
|
|
214
297
|
}
|
|
215
298
|
|
|
299
|
+
/** Merge many manifest entries in one read/write (batch uploads). */
|
|
300
|
+
export function writeMediaManifestBatch(sourceDirectory, entriesByLocalPath) {
|
|
301
|
+
const path = mediaManifestPath(sourceDirectory)
|
|
302
|
+
const dir = dirname(path)
|
|
303
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
304
|
+
const manifest = readMediaManifest(sourceDirectory)
|
|
305
|
+
for (const [localPath, entry] of Object.entries(entriesByLocalPath)) {
|
|
306
|
+
manifest[localPath] = entry
|
|
307
|
+
}
|
|
308
|
+
writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n")
|
|
309
|
+
}
|
|
310
|
+
|
|
216
311
|
export function removeFromMediaManifest(sourceDirectory, localPath) {
|
|
217
312
|
const path = mediaManifestPath(sourceDirectory)
|
|
218
313
|
if (!existsSync(path)) return
|
|
@@ -246,9 +341,11 @@ export function collectSource(dir) {
|
|
|
246
341
|
const skipped = []
|
|
247
342
|
const textByPath = new Map()
|
|
248
343
|
let total = 0
|
|
344
|
+
let bundleLimitExceeded = false
|
|
345
|
+
const ignoreRules = readIgnoreRules(dir)
|
|
249
346
|
|
|
250
347
|
function walk(d, rel) {
|
|
251
|
-
if (
|
|
348
|
+
if (bundleLimitExceeded) return
|
|
252
349
|
let entries
|
|
253
350
|
try { entries = readdirSync(d) } catch { return }
|
|
254
351
|
for (const entry of entries) {
|
|
@@ -267,6 +364,15 @@ export function collectSource(dir) {
|
|
|
267
364
|
if (stat.isDirectory()) {
|
|
268
365
|
if (entry.startsWith(".")) continue
|
|
269
366
|
const nextRel = rel ? `${rel}/${entry}` : entry
|
|
367
|
+
const ignored = ignoredByRule(nextRel, true, ignoreRules)
|
|
368
|
+
if (ignored) {
|
|
369
|
+
skipped.push({
|
|
370
|
+
local_path: `${nextRel}/`,
|
|
371
|
+
reason_code: "ignored_by_ignore_file",
|
|
372
|
+
reason_human: `Ignored by ${ignored.source}: ${ignored.pattern}`,
|
|
373
|
+
})
|
|
374
|
+
continue
|
|
375
|
+
}
|
|
270
376
|
if (isSkippedRootDypaiDirectory(nextRel)) continue
|
|
271
377
|
walk(full, nextRel)
|
|
272
378
|
continue
|
|
@@ -276,6 +382,15 @@ export function collectSource(dir) {
|
|
|
276
382
|
if (BLOCKED.has(entry)) continue
|
|
277
383
|
|
|
278
384
|
const path = rel ? `${rel}/${entry}` : entry
|
|
385
|
+
const ignored = ignoredByRule(path, false, ignoreRules)
|
|
386
|
+
if (ignored) {
|
|
387
|
+
skipped.push({
|
|
388
|
+
local_path: path,
|
|
389
|
+
reason_code: "ignored_by_ignore_file",
|
|
390
|
+
reason_human: `Ignored by ${ignored.source}: ${ignored.pattern}`,
|
|
391
|
+
})
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
279
394
|
if (!isAllowedRootDypaiSourcePath(path)) continue
|
|
280
395
|
try {
|
|
281
396
|
const stat = statSync(full)
|
|
@@ -296,10 +411,11 @@ export function collectSource(dir) {
|
|
|
296
411
|
|
|
297
412
|
const content = readFileSync(full)
|
|
298
413
|
if (total + content.length > MAX_SOURCE_SIZE) {
|
|
414
|
+
bundleLimitExceeded = true
|
|
299
415
|
skipped.push({
|
|
300
416
|
local_path: path, ext, size_bytes: stat.size, size_mb: formatMb(stat.size),
|
|
301
417
|
reason_code: "bundle_size_limit_reached",
|
|
302
|
-
reason_human: `Total upload size
|
|
418
|
+
reason_human: `Total upload size would exceed ${formatMb(MAX_SOURCE_SIZE)} MB. Add local-only folders to .dypaiignore/.gitignore or upload large assets with manage_storage.`,
|
|
303
419
|
})
|
|
304
420
|
return
|
|
305
421
|
}
|
|
@@ -316,7 +432,7 @@ export function collectSource(dir) {
|
|
|
316
432
|
}
|
|
317
433
|
|
|
318
434
|
walk(dir, "")
|
|
319
|
-
return { allFiles, total, skipped, textByPath }
|
|
435
|
+
return { allFiles, total, skipped, textByPath, bundleLimitExceeded }
|
|
320
436
|
}
|
|
321
437
|
|
|
322
438
|
// ─── Reference grep for assets_requiring_action ─────────────────────────────
|
|
@@ -476,12 +592,34 @@ export async function deployFromSource({
|
|
|
476
592
|
_lastSourceDirectory = resolve(sourceDirectory)
|
|
477
593
|
|
|
478
594
|
const framework = detectFramework(sourceDirectory)
|
|
479
|
-
const { allFiles, total, skipped, textByPath } = collectSource(sourceDirectory)
|
|
595
|
+
const { allFiles, total, skipped, textByPath, bundleLimitExceeded } = collectSource(sourceDirectory)
|
|
480
596
|
|
|
481
597
|
if (!allFiles.length) {
|
|
482
598
|
return { error: "No source files found." }
|
|
483
599
|
}
|
|
484
600
|
|
|
601
|
+
if (bundleLimitExceeded) {
|
|
602
|
+
const blockers = skipped
|
|
603
|
+
.filter((item) => item.reason_code === "bundle_size_limit_reached")
|
|
604
|
+
.slice(0, 10)
|
|
605
|
+
.map((item) => ({
|
|
606
|
+
local_path: item.local_path,
|
|
607
|
+
size_mb: item.size_mb,
|
|
608
|
+
reason: item.reason_human,
|
|
609
|
+
}))
|
|
610
|
+
return {
|
|
611
|
+
success: false,
|
|
612
|
+
error_code: "frontend_source_too_large",
|
|
613
|
+
error: `Frontend source exceeds the deploy bundle limit (${formatMb(MAX_SOURCE_SIZE)} MB). No deploy was sent.`,
|
|
614
|
+
bytes_collected_mb: formatMb(total),
|
|
615
|
+
files_collected: allFiles.length,
|
|
616
|
+
blockers,
|
|
617
|
+
advice:
|
|
618
|
+
"If these files are local-only, add them to .dypaiignore (for example: imagenes/). " +
|
|
619
|
+
"If the app needs them at runtime, upload them with manage_storage(upload_file/upload_files) and reference the returned public URLs.",
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
485
623
|
// ── Skip-if-identical check ─────────────────────────────────────────────
|
|
486
624
|
// One hash represents the whole project state. Same hash → nothing to do.
|
|
487
625
|
// Different hash (or no previous hash) → full deploy.
|
|
@@ -4,6 +4,7 @@ import YAML from "yaml"
|
|
|
4
4
|
import { deployFromSource } from "./deploy.js"
|
|
5
5
|
import { proxyToolCall } from "./proxy.js"
|
|
6
6
|
import { resolveOutDir } from "./sync/path-resolver.js"
|
|
7
|
+
import { refreshLocalStateSnapshot } from "./sync/planner.js"
|
|
7
8
|
|
|
8
9
|
function resolveWorkspace({ workspace_root, sourceDirectory, root_dir } = {}) {
|
|
9
10
|
if (workspace_root) {
|
|
@@ -172,6 +173,14 @@ export const dypaiDeployProductionTool = {
|
|
|
172
173
|
live_changed: false,
|
|
173
174
|
}
|
|
174
175
|
}
|
|
176
|
+
try {
|
|
177
|
+
await refreshLocalStateSnapshot({ projectId: targetProjectId, rootDir })
|
|
178
|
+
} catch (refreshError) {
|
|
179
|
+
backend = {
|
|
180
|
+
...backend,
|
|
181
|
+
state_baseline_refresh_warning: refreshError.message,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
175
184
|
}
|
|
176
185
|
}
|
|
177
186
|
|