@dypai-ai/mcp 1.7.2 → 1.7.4
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/README.md +150 -0
- package/package.json +1 -1
- package/src/generated/serverInstructions.js +3 -3
- package/src/index.js +10 -15
- package/src/lib/cloudBackendCompiler.js +4 -1
- package/src/searchDocsFilter.js +19 -8
- package/src/toolProfiles.js +11 -8
- package/src/tools/project-deploy-production.js +7 -8
- package/src/tools/project-push.js +17 -3
- package/src/tools/proxy.js +16 -6
- package/src/tools/sync/endpoint-names.js +29 -0
- package/src/tools/sync/generate-types.js +236 -48
- package/src/tools/sync/planner.js +81 -29
- package/src/tools/sync/pull.js +49 -6
- package/src/tools/sync/push.js +269 -41
package/src/tools/sync/push.js
CHANGED
|
@@ -81,14 +81,16 @@ function isCheckpointTextPath(path) {
|
|
|
81
81
|
return BACKEND_SOURCE_CHECKPOINT_EXTS.has(extOfPath(path))
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
async function collectBackendSourceCheckpointFiles(rootDir) {
|
|
84
|
+
async function collectBackendSourceCheckpointFiles(rootDir, allowedPaths = null) {
|
|
85
85
|
const files = []
|
|
86
86
|
|
|
87
87
|
async function addFile(absPath, relPath) {
|
|
88
|
+
const sourcePath = `dypai/${relPath}`
|
|
89
|
+
if (allowedPaths && !allowedPaths.has(sourcePath)) return
|
|
88
90
|
if (!isCheckpointTextPath(relPath)) return
|
|
89
91
|
const content = await readFile(absPath, "utf8")
|
|
90
92
|
files.push({
|
|
91
|
-
path:
|
|
93
|
+
path: sourcePath,
|
|
92
94
|
content: Buffer.from(content, "utf8").toString("base64"),
|
|
93
95
|
})
|
|
94
96
|
}
|
|
@@ -165,7 +167,7 @@ function deletedBackendSourcePathsFromPlan(plan, remote) {
|
|
|
165
167
|
return [...deleted].sort()
|
|
166
168
|
}
|
|
167
169
|
|
|
168
|
-
async function checkpointBackendSource(projectId, rootDir, plan, remote) {
|
|
170
|
+
async function checkpointBackendSource(projectId, rootDir, plan, remote, allowedPaths = null) {
|
|
169
171
|
if (!projectId) {
|
|
170
172
|
return {
|
|
171
173
|
ok: false,
|
|
@@ -173,7 +175,7 @@ async function checkpointBackendSource(projectId, rootDir, plan, remote) {
|
|
|
173
175
|
reason: "project_id_missing",
|
|
174
176
|
}
|
|
175
177
|
}
|
|
176
|
-
const files = await collectBackendSourceCheckpointFiles(rootDir)
|
|
178
|
+
const files = await collectBackendSourceCheckpointFiles(rootDir, allowedPaths)
|
|
177
179
|
const deleted_paths = deletedBackendSourcePathsFromPlan(plan, remote)
|
|
178
180
|
if (!files.length && !deleted_paths.length) {
|
|
179
181
|
return {
|
|
@@ -264,6 +266,85 @@ function endpointPayload(row) {
|
|
|
264
266
|
return p
|
|
265
267
|
}
|
|
266
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Splits the "error" severity diagnostics from a `dypai_validate` run into
|
|
271
|
+
* project-level errors (no `.endpoint` — can't be scoped, must block the
|
|
272
|
+
* whole push) vs. per-endpoint errors (can be isolated so the rest of the
|
|
273
|
+
* push still goes through). Returns everything the caller needs to decide
|
|
274
|
+
* whether to block or to do a partial push.
|
|
275
|
+
*/
|
|
276
|
+
function partitionValidationErrors(diagnostics) {
|
|
277
|
+
const errorDiags = (diagnostics || []).filter((d) => d.severity === "error")
|
|
278
|
+
const projectLevel = errorDiags.filter((d) => !d.endpoint)
|
|
279
|
+
const erroringEndpoints = new Set(errorDiags.map((d) => d.endpoint).filter(Boolean))
|
|
280
|
+
return { errorDiags, projectLevel, erroringEndpoints }
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Partial push: drops any `plan.create`/`plan.update` item whose endpoint
|
|
285
|
+
* name is in `erroringEndpoints` (mutates `plan` in place, matching the
|
|
286
|
+
* previous inline behavior) so it keeps its live version instead of being
|
|
287
|
+
* pushed. `plan.delete` is left untouched — a broken local flow shouldn't
|
|
288
|
+
* block removing an endpoint. Returns the `skipped_endpoints` summary (with
|
|
289
|
+
* up to 5 errors per endpoint drawn from `skippedDiagnostics`), or
|
|
290
|
+
* `undefined` when nothing was dropped.
|
|
291
|
+
*/
|
|
292
|
+
function dropErroringFromPlan(plan, erroringEndpoints, skippedDiagnostics) {
|
|
293
|
+
const skippedNames = []
|
|
294
|
+
if (erroringEndpoints.size) {
|
|
295
|
+
const drop = (arr) => (arr || []).filter((item) => {
|
|
296
|
+
if (erroringEndpoints.has(item.name)) { skippedNames.push(item.name); return false }
|
|
297
|
+
return true
|
|
298
|
+
})
|
|
299
|
+
plan.create = drop(plan.create)
|
|
300
|
+
plan.update = drop(plan.update)
|
|
301
|
+
}
|
|
302
|
+
return skippedNames.length
|
|
303
|
+
? [...new Set(skippedNames)].map((name) => ({
|
|
304
|
+
endpoint: name,
|
|
305
|
+
errors: skippedDiagnostics
|
|
306
|
+
.filter((d) => d.endpoint === name)
|
|
307
|
+
.slice(0, 5)
|
|
308
|
+
.map((d) => ({ rule: d.rule, message: d.message, file: d.file, loc: d.loc })),
|
|
309
|
+
}))
|
|
310
|
+
: undefined
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function skippedEndpointsFromDiagnostics(diagnostics) {
|
|
314
|
+
const names = [...new Set(
|
|
315
|
+
(diagnostics || [])
|
|
316
|
+
.filter((item) => item?.severity === "error" && item.endpoint)
|
|
317
|
+
.map((item) => item.endpoint),
|
|
318
|
+
)]
|
|
319
|
+
return names.length
|
|
320
|
+
? names.map((name) => ({
|
|
321
|
+
endpoint: name,
|
|
322
|
+
errors: diagnostics
|
|
323
|
+
.filter((item) => item.endpoint === name && item.severity === "error")
|
|
324
|
+
.slice(0, 5)
|
|
325
|
+
.map((item) => ({
|
|
326
|
+
rule: item.rule,
|
|
327
|
+
message: item.message,
|
|
328
|
+
file: item.file,
|
|
329
|
+
loc: item.loc,
|
|
330
|
+
})),
|
|
331
|
+
}))
|
|
332
|
+
: undefined
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function mergeSkippedEndpointSummaries(...groups) {
|
|
336
|
+
const merged = new Map()
|
|
337
|
+
for (const group of groups) {
|
|
338
|
+
for (const item of group || []) {
|
|
339
|
+
const current = merged.get(item.endpoint)
|
|
340
|
+
merged.set(item.endpoint, current
|
|
341
|
+
? { ...current, errors: [...(current.errors || []), ...(item.errors || [])].slice(0, 5) }
|
|
342
|
+
: item)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return merged.size ? [...merged.values()] : undefined
|
|
346
|
+
}
|
|
347
|
+
|
|
267
348
|
function splitBlockingConflicts(plan, conflicts) {
|
|
268
349
|
const touched = new Set([
|
|
269
350
|
...(plan.update || []).map((item) => item.name),
|
|
@@ -278,6 +359,68 @@ function splitBlockingConflicts(plan, conflicts) {
|
|
|
278
359
|
return { blocking, non_blocking }
|
|
279
360
|
}
|
|
280
361
|
|
|
362
|
+
function normalizeEndpointSelection(value) {
|
|
363
|
+
if (value == null) return null
|
|
364
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
365
|
+
throw new Error("only_endpoints must be a non-empty array of endpoint slugs")
|
|
366
|
+
}
|
|
367
|
+
const names = [...new Set(value.map((item) => typeof item === "string" ? item.trim() : ""))]
|
|
368
|
+
const invalid = names.filter((name) => !/^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/.test(name))
|
|
369
|
+
if (invalid.length) {
|
|
370
|
+
throw new Error(`only_endpoints contains invalid endpoint slug(s): ${invalid.join(", ")}`)
|
|
371
|
+
}
|
|
372
|
+
return names.sort()
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function scopePlanToEndpoints(plan, selectedNames, local) {
|
|
376
|
+
if (!selectedNames) return plan
|
|
377
|
+
const selected = new Set(selectedNames)
|
|
378
|
+
const keepItem = (item) => selected.has(typeof item === "string" ? item : item?.name)
|
|
379
|
+
|
|
380
|
+
plan.create = (plan.create || []).filter(keepItem)
|
|
381
|
+
plan.update = (plan.update || []).filter(keepItem)
|
|
382
|
+
plan.delete = (plan.delete || []).filter(keepItem)
|
|
383
|
+
plan.unchanged = (plan.unchanged || []).filter(keepItem)
|
|
384
|
+
plan.orphansIgnored = (plan.orphansIgnored || []).filter((name) => selected.has(name))
|
|
385
|
+
|
|
386
|
+
const selectedGroups = new Set()
|
|
387
|
+
for (const name of selected) {
|
|
388
|
+
const entry = local?.byName?.[name]
|
|
389
|
+
const group = entry?.group || entry?.doc?.group || entry?.pushPayload?.group_name
|
|
390
|
+
if (group) selectedGroups.add(group)
|
|
391
|
+
}
|
|
392
|
+
plan.groups = {
|
|
393
|
+
create: (plan.groups?.create || []).filter((name) => selectedGroups.has(name)),
|
|
394
|
+
// Endpoint-scoped pushes never delete groups: group deletion is a
|
|
395
|
+
// project-wide reconciliation and could affect unselected endpoints.
|
|
396
|
+
delete: [],
|
|
397
|
+
unchanged: (plan.groups?.unchanged || []).filter((name) => selectedGroups.has(name)),
|
|
398
|
+
}
|
|
399
|
+
plan.selection = { only_endpoints: selectedNames }
|
|
400
|
+
return plan
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function backendSourcePathsForEndpointSelection(local, selectedNames) {
|
|
404
|
+
if (!selectedNames) return null
|
|
405
|
+
const paths = new Set()
|
|
406
|
+
for (const name of selectedNames) {
|
|
407
|
+
const entry = local?.byName?.[name]
|
|
408
|
+
if (!entry) continue
|
|
409
|
+
if (entry.source === "flow") {
|
|
410
|
+
const raw = entry.pushPayload?.workflow_source?.file || entry.file
|
|
411
|
+
if (typeof raw === "string" && raw) {
|
|
412
|
+
paths.add(raw.startsWith("dypai/") ? raw : `dypai/${raw}`)
|
|
413
|
+
}
|
|
414
|
+
continue
|
|
415
|
+
}
|
|
416
|
+
if (entry.file) paths.add(`dypai/endpoints/${entry.file}`)
|
|
417
|
+
for (const refPath of Object.keys(entry.fileMap || {})) {
|
|
418
|
+
paths.add(refPath.startsWith("dypai/") ? refPath : `dypai/${refPath}`)
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return paths
|
|
422
|
+
}
|
|
423
|
+
|
|
281
424
|
/**
|
|
282
425
|
* Treat a remote tool response as a definite success only when it has at
|
|
283
426
|
* least one of the markers we expect from a real mutation. Anything else
|
|
@@ -329,20 +472,36 @@ async function applyCreate(canonicalRow, projectId) {
|
|
|
329
472
|
return assertMutationOK(res, "create", canonicalRow.name)
|
|
330
473
|
}
|
|
331
474
|
|
|
332
|
-
|
|
475
|
+
function endpointUpdatesForRemote(canonicalRow, expectedRevision = null) {
|
|
476
|
+
const updates = endpointPayload(canonicalRow)
|
|
477
|
+
if (expectedRevision != null) updates.expected_revision = Number(expectedRevision)
|
|
478
|
+
return updates
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
async function applyUpdate(canonicalRow, endpointId, projectId, expectedRevision = null) {
|
|
482
|
+
const updates = endpointUpdatesForRemote(canonicalRow, expectedRevision)
|
|
333
483
|
const res = await proxyToolCallWithRetry("update_endpoint", {
|
|
334
484
|
...(projectId ? { project_id: projectId } : {}),
|
|
335
485
|
endpoint_id: endpointId,
|
|
336
|
-
updates
|
|
486
|
+
updates,
|
|
337
487
|
})
|
|
338
488
|
return assertMutationOK(res, "update", canonicalRow.name)
|
|
339
489
|
}
|
|
340
490
|
|
|
341
|
-
|
|
342
|
-
|
|
491
|
+
function endpointDeleteArgs(endpointId, projectId, expectedRevision = null) {
|
|
492
|
+
return {
|
|
343
493
|
...(projectId ? { project_id: projectId } : {}),
|
|
344
494
|
endpoint_id: endpointId,
|
|
345
|
-
|
|
495
|
+
...(expectedRevision != null ? { expected_revision: Number(expectedRevision) } : {}),
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
async function applyDelete(endpointId, projectId, expectedRevision = null) {
|
|
500
|
+
const res = await proxyToolCallWithRetry("delete_endpoint", endpointDeleteArgs(
|
|
501
|
+
endpointId,
|
|
502
|
+
projectId,
|
|
503
|
+
expectedRevision,
|
|
504
|
+
))
|
|
346
505
|
return assertMutationOK(res, "delete", endpointId)
|
|
347
506
|
}
|
|
348
507
|
|
|
@@ -484,6 +643,7 @@ export const dypaiPushTool = {
|
|
|
484
643
|
"ALWAYS run dypai_diff first to preview what will be staged. " +
|
|
485
644
|
"The public dypai_push wrapper also saves frontend source to Studio. " +
|
|
486
645
|
"By default, endpoints in remote but missing locally are kept (safe). Pass delete_orphans: true to stage their deletion as a draft as well. " +
|
|
646
|
+
"For endpoint-scoped work, pass only_endpoints: ['slug-a', 'slug-b']; unrelated endpoints, groups, realtime policies, automations, and source files are left untouched. " +
|
|
487
647
|
"PARTIAL PUSH: an endpoint whose flow has errors is SKIPPED (it keeps its live version) and the rest are pushed — one broken endpoint no longer blocks the whole push. Skipped ones are listed in `skipped_endpoints` (with their errors) and `partial_success` is set. A project-level error still blocks. Use skip_validation:true to push everything, errors included.",
|
|
488
648
|
inputSchema: {
|
|
489
649
|
type: "object",
|
|
@@ -517,11 +677,29 @@ export const dypaiPushTool = {
|
|
|
517
677
|
description: "Skip the dypai_validate pre-flight check and push EVERY endpoint, including ones with errors (no per-endpoint skipping). By default the push already applies the valid endpoints and only skips the erroring ones — so use this only when you know the validator is wrong. Default: false.",
|
|
518
678
|
default: false,
|
|
519
679
|
},
|
|
680
|
+
only_endpoints: {
|
|
681
|
+
type: "array",
|
|
682
|
+
minItems: 1,
|
|
683
|
+
uniqueItems: true,
|
|
684
|
+
items: { type: "string" },
|
|
685
|
+
description: "Optional explicit endpoint allowlist. Only these endpoint slugs may be created, updated, or deleted; project-wide realtime/automation reconciliation is skipped.",
|
|
686
|
+
},
|
|
520
687
|
},
|
|
521
688
|
},
|
|
522
689
|
|
|
523
|
-
async execute({ project_id, root_dir = "./dypai", delete_orphans = false, dry_run = false, force = false, skip_validation = false } = {}) {
|
|
690
|
+
async execute({ project_id, root_dir = "./dypai", delete_orphans = false, dry_run = false, force = false, skip_validation = false, only_endpoints = null } = {}) {
|
|
524
691
|
const rootDir = resolvePath(process.cwd(), root_dir)
|
|
692
|
+
let selectedNames
|
|
693
|
+
try {
|
|
694
|
+
selectedNames = normalizeEndpointSelection(only_endpoints)
|
|
695
|
+
} catch (error) {
|
|
696
|
+
return {
|
|
697
|
+
success: false,
|
|
698
|
+
applied: false,
|
|
699
|
+
reason: "invalid_endpoint_selection",
|
|
700
|
+
error: error.message,
|
|
701
|
+
}
|
|
702
|
+
}
|
|
525
703
|
|
|
526
704
|
// Resolve the target project via dypai.config.yaml if the caller didn't
|
|
527
705
|
// pass an explicit project_id. If both are provided and disagree, refuse —
|
|
@@ -541,8 +719,7 @@ export const dypaiPushTool = {
|
|
|
541
719
|
try {
|
|
542
720
|
const validation = await runValidation(rootDir, project_id || configProjectId)
|
|
543
721
|
if (!validation.success) {
|
|
544
|
-
const errorDiags = (validation.diagnostics
|
|
545
|
-
const projectLevel = errorDiags.filter(d => !d.endpoint)
|
|
722
|
+
const { errorDiags, projectLevel, erroringEndpoints: partitioned } = partitionValidationErrors(validation.diagnostics)
|
|
546
723
|
if (projectLevel.length) {
|
|
547
724
|
return {
|
|
548
725
|
success: false,
|
|
@@ -554,7 +731,7 @@ export const dypaiPushTool = {
|
|
|
554
731
|
}
|
|
555
732
|
}
|
|
556
733
|
skippedDiagnostics = errorDiags
|
|
557
|
-
erroringEndpoints =
|
|
734
|
+
erroringEndpoints = partitioned
|
|
558
735
|
}
|
|
559
736
|
} catch (e) {
|
|
560
737
|
// Don't block the push on a validator crash — just warn
|
|
@@ -599,6 +776,20 @@ export const dypaiPushTool = {
|
|
|
599
776
|
error: e.message,
|
|
600
777
|
}
|
|
601
778
|
}
|
|
779
|
+
const compilerValidation = partitionValidationErrors(local.compilerDiagnostics)
|
|
780
|
+
if (compilerValidation.projectLevel.length) {
|
|
781
|
+
return {
|
|
782
|
+
success: false,
|
|
783
|
+
applied: false,
|
|
784
|
+
reason: "compiler_validation_failed",
|
|
785
|
+
diagnostics: compilerValidation.projectLevel.slice(0, 20),
|
|
786
|
+
hint: "The compiler reported project-level errors that cannot be isolated to one endpoint.",
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
for (const endpoint of compilerValidation.erroringEndpoints) {
|
|
790
|
+
erroringEndpoints.add(endpoint)
|
|
791
|
+
}
|
|
792
|
+
skippedDiagnostics.push(...compilerValidation.errorDiags)
|
|
602
793
|
try {
|
|
603
794
|
remote = await fetchRemoteState(targetProjectId)
|
|
604
795
|
} catch (e) {
|
|
@@ -653,29 +844,32 @@ export const dypaiPushTool = {
|
|
|
653
844
|
stateSnapshot,
|
|
654
845
|
liveRemote: remote,
|
|
655
846
|
})
|
|
847
|
+
if (selectedNames) {
|
|
848
|
+
const unavailable = selectedNames.filter((name) => {
|
|
849
|
+
if (local.byName[name]) return false
|
|
850
|
+
return !(delete_orphans && effectiveRemote.byName?.[name])
|
|
851
|
+
})
|
|
852
|
+
if (unavailable.length) {
|
|
853
|
+
return {
|
|
854
|
+
success: false,
|
|
855
|
+
applied: false,
|
|
856
|
+
reason: "endpoint_selection_not_found",
|
|
857
|
+
unavailable_endpoints: unavailable,
|
|
858
|
+
hint: "Each only_endpoints slug must exist locally. A remote-only slug is accepted only with delete_orphans:true for an explicit deletion.",
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
scopePlanToEndpoints(plan, selectedNames, local)
|
|
862
|
+
}
|
|
863
|
+
const selectedSourcePaths = backendSourcePathsForEndpointSelection(local, selectedNames)
|
|
656
864
|
|
|
657
865
|
// Partial push: drop endpoints that failed validation from create/update so
|
|
658
866
|
// they aren't pushed (they keep their live version). Deletes stay — a broken
|
|
659
867
|
// local flow shouldn't block removing endpoints. skip_validation leaves
|
|
660
868
|
// erroringEndpoints empty, so nothing is dropped.
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
return true
|
|
666
|
-
})
|
|
667
|
-
plan.create = drop(plan.create)
|
|
668
|
-
plan.update = drop(plan.update)
|
|
669
|
-
}
|
|
670
|
-
const skipped_endpoints = skippedNames.length
|
|
671
|
-
? [...new Set(skippedNames)].map((name) => ({
|
|
672
|
-
endpoint: name,
|
|
673
|
-
errors: skippedDiagnostics
|
|
674
|
-
.filter((d) => d.endpoint === name)
|
|
675
|
-
.slice(0, 5)
|
|
676
|
-
.map((d) => ({ rule: d.rule, message: d.message, file: d.file, loc: d.loc })),
|
|
677
|
-
}))
|
|
678
|
-
: undefined
|
|
869
|
+
const skipped_endpoints = mergeSkippedEndpointSummaries(
|
|
870
|
+
dropErroringFromPlan(plan, erroringEndpoints, skippedDiagnostics),
|
|
871
|
+
skippedEndpointsFromDiagnostics(compilerValidation.errorDiags),
|
|
872
|
+
)
|
|
679
873
|
|
|
680
874
|
const groupChanges = (plan.groups?.create?.length || 0) + (plan.groups?.delete?.length || 0)
|
|
681
875
|
const totalChanges = plan.create.length + plan.update.length + plan.delete.length + groupChanges
|
|
@@ -688,8 +882,13 @@ export const dypaiPushTool = {
|
|
|
688
882
|
success: false,
|
|
689
883
|
applied: false,
|
|
690
884
|
reason: "conflicts_detected",
|
|
885
|
+
summary: summaryFromPlan(plan),
|
|
691
886
|
conflicts: conflictSplit.blocking,
|
|
692
887
|
non_blocking_conflicts: conflictSplit.non_blocking.length ? conflictSplit.non_blocking : undefined,
|
|
888
|
+
skipped_endpoints,
|
|
889
|
+
compiler_diagnostics: local.compilerDiagnostics?.length
|
|
890
|
+
? local.compilerDiagnostics.slice(0, 50)
|
|
891
|
+
: undefined,
|
|
693
892
|
hint: "Remote changes overlap endpoints this push would update/delete. Run dypai_pull to refresh local state, resolve the overlap, then push again. To override, pass force=true.",
|
|
694
893
|
}
|
|
695
894
|
}
|
|
@@ -697,11 +896,13 @@ export const dypaiPushTool = {
|
|
|
697
896
|
if (dry_run || totalChanges === 0) {
|
|
698
897
|
const sourceCheckpoint = dry_run
|
|
699
898
|
? null
|
|
700
|
-
: await checkpointBackendSource(targetProjectId, rootDir, plan, remote)
|
|
899
|
+
: await checkpointBackendSource(targetProjectId, rootDir, plan, remote, selectedSourcePaths)
|
|
701
900
|
const sourceCheckpointFailed = sourceCheckpoint?.ok === false && sourceCheckpoint?.skipped !== true
|
|
702
901
|
const automationSync = dry_run
|
|
703
902
|
? { skipped: true, reason: "dry_run" }
|
|
704
|
-
:
|
|
903
|
+
: selectedNames
|
|
904
|
+
? { skipped: true, reason: "endpoint_scoped_push" }
|
|
905
|
+
: await syncAutomationMetadata(targetProjectId, local.automations)
|
|
705
906
|
const automationSyncHasFailed = automationSyncFailed(automationSync)
|
|
706
907
|
const sourceBaseline = !dry_run && !sourceCheckpointFailed
|
|
707
908
|
? await refreshBackendSourceBaseline(targetProjectId, rootDir, sourceCheckpoint)
|
|
@@ -717,12 +918,16 @@ export const dypaiPushTool = {
|
|
|
717
918
|
: totalChanges === 0 ? "no_changes" : "dry_run",
|
|
718
919
|
summary: summaryFromPlan(plan),
|
|
719
920
|
skipped_endpoints,
|
|
921
|
+
compiler_diagnostics: local.compilerDiagnostics?.length
|
|
922
|
+
? local.compilerDiagnostics.slice(0, 50)
|
|
923
|
+
: undefined,
|
|
720
924
|
plan,
|
|
721
925
|
non_blocking_conflicts: conflictSplit.non_blocking.length ? conflictSplit.non_blocking : undefined,
|
|
722
926
|
types: typesGenerated,
|
|
723
927
|
automation_sync: automationSync,
|
|
724
928
|
source_checkpoint: sourceCheckpoint || undefined,
|
|
725
929
|
source_baseline: sourceBaseline,
|
|
930
|
+
selection: selectedNames ? { only_endpoints: selectedNames } : undefined,
|
|
726
931
|
hint: automationSyncHasFailed
|
|
727
932
|
? "Backend runtime/source was unchanged, but DYPAI could not sync automation setup metadata. Retry after the API is updated/restarted; file requirements may be missing until this succeeds."
|
|
728
933
|
: sourceCheckpointFailed
|
|
@@ -787,7 +992,7 @@ export const dypaiPushTool = {
|
|
|
787
992
|
const canonical = localToCanonical(local.byName[item.name], remote.mapsCtx)
|
|
788
993
|
const endpointId = remote.byName[item.name]?.id
|
|
789
994
|
const res = endpointId
|
|
790
|
-
? await applyUpdate(canonical, endpointId, targetProjectId)
|
|
995
|
+
? await applyUpdate(canonical, endpointId, targetProjectId, remote.byName[item.name]?.revision)
|
|
791
996
|
: await applyCreate(canonical, targetProjectId)
|
|
792
997
|
applied.updated.push({ name: item.name, changed_fields: item.changedFields })
|
|
793
998
|
if (isDraftResponse(res)) applied.updated_drafts.push(item.name)
|
|
@@ -805,7 +1010,11 @@ export const dypaiPushTool = {
|
|
|
805
1010
|
try {
|
|
806
1011
|
const endpointId = remote.byName[item.name]?.id
|
|
807
1012
|
if (!endpointId) throw new Error("endpoint_id missing from remote state")
|
|
808
|
-
const res = await applyDelete(
|
|
1013
|
+
const res = await applyDelete(
|
|
1014
|
+
endpointId,
|
|
1015
|
+
targetProjectId,
|
|
1016
|
+
remote.byName[item.name]?.revision,
|
|
1017
|
+
)
|
|
809
1018
|
applied.deleted.push(item.name)
|
|
810
1019
|
if (isDraftResponse(res)) applied.deleted_drafts.push(item.name)
|
|
811
1020
|
endpoint_results.push(endpointResult("delete", item.name, "applied", {
|
|
@@ -834,10 +1043,14 @@ export const dypaiPushTool = {
|
|
|
834
1043
|
|
|
835
1044
|
// Also reconcile realtime policies if realtime.yaml exists
|
|
836
1045
|
let realtime = null
|
|
837
|
-
|
|
838
|
-
realtime =
|
|
839
|
-
}
|
|
840
|
-
|
|
1046
|
+
if (selectedNames) {
|
|
1047
|
+
realtime = { skipped: true, reason: "endpoint_scoped_push" }
|
|
1048
|
+
} else {
|
|
1049
|
+
try {
|
|
1050
|
+
realtime = await syncRealtimePolicies(targetProjectId, rootDir, false)
|
|
1051
|
+
} catch (e) {
|
|
1052
|
+
errors.push({ op: "realtime", error: e.message })
|
|
1053
|
+
}
|
|
841
1054
|
}
|
|
842
1055
|
|
|
843
1056
|
// Names of endpoints that actually changed this run — drives the follow-up
|
|
@@ -868,7 +1081,7 @@ export const dypaiPushTool = {
|
|
|
868
1081
|
const isDraftMode = draftCount > 0
|
|
869
1082
|
&& draftCount === endpointTotal + realtimeTotal
|
|
870
1083
|
const sourceCheckpoint = errors.length === 0
|
|
871
|
-
? await checkpointBackendSource(targetProjectId, rootDir, plan, remote)
|
|
1084
|
+
? await checkpointBackendSource(targetProjectId, rootDir, plan, remote, selectedSourcePaths)
|
|
872
1085
|
: {
|
|
873
1086
|
ok: true,
|
|
874
1087
|
skipped: true,
|
|
@@ -876,7 +1089,9 @@ export const dypaiPushTool = {
|
|
|
876
1089
|
}
|
|
877
1090
|
const sourceCheckpointFailed = sourceCheckpoint?.ok === false && sourceCheckpoint?.skipped !== true
|
|
878
1091
|
const automationSync = errors.length === 0
|
|
879
|
-
?
|
|
1092
|
+
? selectedNames
|
|
1093
|
+
? { skipped: true, reason: "endpoint_scoped_push" }
|
|
1094
|
+
: await syncAutomationMetadata(targetProjectId, local.automations)
|
|
880
1095
|
: { skipped: true, reason: "push_had_errors" }
|
|
881
1096
|
const automationSyncHasFailed = automationSyncFailed(automationSync)
|
|
882
1097
|
const sourceBaseline = errors.length === 0 && !sourceCheckpointFailed
|
|
@@ -918,12 +1133,16 @@ export const dypaiPushTool = {
|
|
|
918
1133
|
details: applied,
|
|
919
1134
|
endpoint_results,
|
|
920
1135
|
skipped_endpoints,
|
|
1136
|
+
compiler_diagnostics: local.compilerDiagnostics?.length
|
|
1137
|
+
? local.compilerDiagnostics.slice(0, 50)
|
|
1138
|
+
: undefined,
|
|
921
1139
|
non_blocking_conflicts: conflictSplit.non_blocking.length ? conflictSplit.non_blocking : undefined,
|
|
922
1140
|
errors: errors.length ? errors : undefined,
|
|
923
1141
|
types: typesGenerated,
|
|
924
1142
|
automation_sync: automationSync,
|
|
925
1143
|
source_checkpoint: sourceCheckpoint,
|
|
926
1144
|
source_baseline: sourceBaseline,
|
|
1145
|
+
selection: selectedNames ? { only_endpoints: selectedNames } : undefined,
|
|
927
1146
|
// Only one next_step — and only when it's non-obvious. Drafts win
|
|
928
1147
|
// over the test suggestion because tests against live won't see
|
|
929
1148
|
// the change until the drafts are promoted.
|
|
@@ -967,9 +1186,18 @@ function summaryFromPlan(plan) {
|
|
|
967
1186
|
export const __testing = {
|
|
968
1187
|
normalizeResponseCardinality,
|
|
969
1188
|
endpointPayload,
|
|
1189
|
+
endpointUpdatesForRemote,
|
|
1190
|
+
endpointDeleteArgs,
|
|
970
1191
|
splitBlockingConflicts,
|
|
971
1192
|
collectBackendSourceCheckpointFiles,
|
|
972
1193
|
deletedBackendSourcePathsFromPlan,
|
|
973
1194
|
syncAutomationMetadata,
|
|
1195
|
+
partitionValidationErrors,
|
|
1196
|
+
dropErroringFromPlan,
|
|
1197
|
+
skippedEndpointsFromDiagnostics,
|
|
1198
|
+
mergeSkippedEndpointSummaries,
|
|
1199
|
+
normalizeEndpointSelection,
|
|
1200
|
+
scopePlanToEndpoints,
|
|
1201
|
+
backendSourcePathsForEndpointSelection,
|
|
974
1202
|
PUSH_CONCURRENCY,
|
|
975
1203
|
}
|