@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
package/src/tools/sync/diff.js
CHANGED
|
@@ -1,26 +1,13 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dypai_diff — preview what dypai_push would change. Read-only, safe.
|
|
3
|
-
*
|
|
4
|
-
* The picture has THREE layers, not two:
|
|
5
|
-
* - LOCAL : what's on disk in dypai/
|
|
6
|
-
* - DRAFT : what's already been staged via prior pushes (system.config_drafts)
|
|
7
|
-
* - LIVE : what the engine actually serves at <project_id>.dypai.dev
|
|
8
|
-
*
|
|
9
|
-
* The diff body reports LOCAL → EFFECTIVE_REMOTE (live + pending drafts) —
|
|
10
|
-
* what `dypai_push` will actually queue. We additionally surface the DRAFT
|
|
11
|
-
* layer metadata so the agent can spot overlap and publish/discard decisions.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
1
|
import { resolve as resolvePath } from "path"
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
2
|
+
import { readLocalConfig } from "./planner.js"
|
|
3
|
+
import { diffBackendSource } from "./source-diff.js"
|
|
17
4
|
|
|
18
5
|
export const dypaiDiffTool = {
|
|
19
6
|
name: "dypai_diff",
|
|
20
7
|
description:
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
8
|
+
"Fast source diff for the local ./dypai/ folder. " +
|
|
9
|
+
"It does not compile, call remote services, or stage changes. " +
|
|
10
|
+
"Use dypai_push to compile, validate, save backend drafts, and regenerate types.",
|
|
24
11
|
inputSchema: {
|
|
25
12
|
type: "object",
|
|
26
13
|
properties: {
|
|
@@ -35,138 +22,49 @@ export const dypaiDiffTool = {
|
|
|
35
22
|
},
|
|
36
23
|
delete_orphans: {
|
|
37
24
|
type: "boolean",
|
|
38
|
-
description: "
|
|
25
|
+
description: "Ignored by dypai_diff. Deletions are only applied by dypai_push when explicitly requested.",
|
|
39
26
|
default: false,
|
|
40
27
|
},
|
|
41
28
|
},
|
|
42
29
|
},
|
|
43
30
|
|
|
44
|
-
async execute({ project_id, root_dir = "./dypai"
|
|
31
|
+
async execute({ project_id, root_dir = "./dypai" } = {}) {
|
|
45
32
|
const rootDir = resolvePath(process.cwd(), root_dir)
|
|
46
33
|
|
|
47
34
|
const config = await readLocalConfig(rootDir)
|
|
48
35
|
const targetProjectId = project_id || config?.project_id || null
|
|
36
|
+
const diff = await diffBackendSource(rootDir)
|
|
49
37
|
|
|
50
|
-
|
|
51
|
-
readLocalEffectiveState(rootDir, targetProjectId),
|
|
52
|
-
fetchRemoteState(targetProjectId),
|
|
53
|
-
readLocalStateSnapshot(rootDir),
|
|
54
|
-
// Pending drafts. Cheap on dev (always 0); on prod surfaces what's
|
|
55
|
-
// already staged so the agent can reason about overlap with the local
|
|
56
|
-
// change set. Old engines without the drafts API silently fall back.
|
|
57
|
-
proxyToolCall("manage_drafts", targetProjectId
|
|
58
|
-
? { project_id: targetProjectId, operation: "list" }
|
|
59
|
-
: { operation: "list" }
|
|
60
|
-
).catch(() => ({ total: 0, drafts: [], _unavailable: true })),
|
|
61
|
-
])
|
|
62
|
-
|
|
63
|
-
if (local.errors.length) {
|
|
64
|
-
return {
|
|
65
|
-
success: false,
|
|
66
|
-
phase: "read_local",
|
|
67
|
-
errors: local.errors,
|
|
68
|
-
hint: "Fix the YAML/file errors before running diff again.",
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!remote || !remote.mapsCtx) {
|
|
38
|
+
if (!diff.ok) {
|
|
73
39
|
return {
|
|
74
40
|
success: false,
|
|
75
|
-
phase: "
|
|
76
|
-
|
|
41
|
+
phase: "source_diff",
|
|
42
|
+
reason: diff.reason,
|
|
43
|
+
error: diff.error,
|
|
44
|
+
hint: "Fix the local dypai/.dypai/backend-baseline.json or run dypai_pull/sync again.",
|
|
77
45
|
}
|
|
78
46
|
}
|
|
79
47
|
|
|
80
|
-
const
|
|
81
|
-
const plan = computePlan(local, effectiveRemote, remote.mapsCtx, {
|
|
82
|
-
deleteOrphans: delete_orphans,
|
|
83
|
-
stateSnapshot,
|
|
84
|
-
liveRemote: remote,
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
const groupChanges = (plan.groups?.create?.length || 0) + (plan.groups?.delete?.length || 0)
|
|
88
|
-
const totalChanges = plan.create.length + plan.update.length + plan.delete.length + groupChanges
|
|
89
|
-
const hasConflicts = (plan.warnings || []).some(w => w.type === "remote_changed_since_pull")
|
|
90
|
-
|
|
91
|
-
// ─── Drafts overlay ───────────────────────────────────────────────────
|
|
92
|
-
// Build a `pending_drafts` view that tells the agent what's already
|
|
93
|
-
// staged AND highlights the overlap with this diff's local-change set.
|
|
94
|
-
// The overlap is the agent-relevant signal: same endpoint touched on
|
|
95
|
-
// BOTH sides means pushing now will overwrite the existing draft.
|
|
96
|
-
const draftItems = Array.isArray(draftsResult?.drafts) ? draftsResult.drafts : []
|
|
97
|
-
const draftCount = draftsResult?.total || 0
|
|
98
|
-
|
|
99
|
-
const localChangeNames = new Set([
|
|
100
|
-
...plan.create.map(p => p.name),
|
|
101
|
-
...plan.update.map(p => p.name),
|
|
102
|
-
...plan.delete.map(p => p.name),
|
|
103
|
-
])
|
|
104
|
-
|
|
105
|
-
const overlap = draftItems
|
|
106
|
-
.filter(d => d.resource_type === "endpoint" && localChangeNames.has(d.resource_name))
|
|
107
|
-
.map(d => ({
|
|
108
|
-
endpoint: d.resource_name,
|
|
109
|
-
existing_draft_op: d.op,
|
|
110
|
-
local_change_op: plan.create.find(p => p.name === d.resource_name) ? "create"
|
|
111
|
-
: plan.update.find(p => p.name === d.resource_name) ? "update"
|
|
112
|
-
: "delete",
|
|
113
|
-
}))
|
|
114
|
-
|
|
115
|
-
const pendingDrafts = draftCount > 0 ? {
|
|
116
|
-
total: draftCount,
|
|
117
|
-
counts_by_type: draftsResult?.counts_by_type || {},
|
|
118
|
-
items: draftItems
|
|
119
|
-
.map(d => ({
|
|
120
|
-
resource_type: d.resource_type,
|
|
121
|
-
resource_name: d.resource_name,
|
|
122
|
-
op: d.op,
|
|
123
|
-
}))
|
|
124
|
-
.sort((a, b) => (a.resource_name || "").localeCompare(b.resource_name || "")),
|
|
125
|
-
// Endpoints that are BOTH staged AND modified locally — pushing again
|
|
126
|
-
// replaces the existing draft. Empty array = no overlap (clean signal).
|
|
127
|
-
overlap_with_local: overlap,
|
|
128
|
-
} : null
|
|
129
|
-
|
|
130
|
-
// ─── Hint priority ────────────────────────────────────────────────────
|
|
131
|
-
// 1. Conflicts and missing creds always win (block push).
|
|
132
|
-
// 2. Overlap is next: explicitly tell the agent that re-push overwrites.
|
|
133
|
-
// 3. Pending drafts (no overlap): suggest publishing or discarding before
|
|
134
|
-
// stacking more on top.
|
|
135
|
-
const hint = hasConflicts
|
|
136
|
-
? "Remote changed since your last pull — dypai_pull first, or dypai_push will be blocked."
|
|
137
|
-
: (plan.warnings || []).some(w => w.type === "missing_credential")
|
|
138
|
-
? "Some YAMLs reference credentials missing remotely. Create them in the dashboard before pushing."
|
|
139
|
-
: overlap.length > 0
|
|
140
|
-
? `${overlap.length} endpoint(s) have BOTH a pending draft AND a local change — dypai_push will REPLACE the existing draft with the new version. Review pending_drafts.overlap_with_local before pushing.`
|
|
141
|
-
: totalChanges === 0 && draftCount > 0
|
|
142
|
-
? `${draftCount} pending backend change(s) — local matches what's already saved. Test with dypai_test_endpoint(mode:'draft'), then use dypai_deploy_production(confirm:true) when the user approves going live.`
|
|
143
|
-
: draftCount > 0 && totalChanges > 0
|
|
144
|
-
? `${draftCount} backend change(s) already pending; this diff would save ${totalChanges} more. Keep testing in preview, or use dypai_deploy_production(confirm:true) after explicit approval.`
|
|
145
|
-
: draftCount > 0
|
|
146
|
-
? `${draftCount} pending backend change(s) — no local changes to push. Test with dypai_test_endpoint(mode:'draft'), then use dypai_deploy_production(confirm:true) after explicit approval.`
|
|
147
|
-
: undefined
|
|
48
|
+
const totalChanges = diff.summary.create + diff.summary.update + diff.summary.delete
|
|
148
49
|
|
|
149
50
|
return {
|
|
150
51
|
success: true,
|
|
52
|
+
mode: "source",
|
|
53
|
+
project_id: targetProjectId,
|
|
54
|
+
baseline: diff.baseline,
|
|
151
55
|
summary: {
|
|
152
|
-
create:
|
|
153
|
-
update:
|
|
154
|
-
delete:
|
|
155
|
-
unchanged:
|
|
156
|
-
orphans_ignored: plan.orphansIgnored?.length || 0,
|
|
157
|
-
groups_create: plan.groups?.create?.length || 0,
|
|
158
|
-
groups_delete: plan.groups?.delete?.length || 0,
|
|
159
|
-
groups_unchanged: plan.groups?.unchanged?.length || 0,
|
|
160
|
-
warnings: plan.warnings?.length || 0,
|
|
161
|
-
pending_drafts: draftCount,
|
|
162
|
-
drafts_overlap_with_local: overlap.length,
|
|
56
|
+
create: diff.summary.create,
|
|
57
|
+
update: diff.summary.update,
|
|
58
|
+
delete: diff.summary.delete,
|
|
59
|
+
unchanged: diff.summary.unchanged,
|
|
163
60
|
},
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
61
|
+
source_changes: totalChanges > 0 ? diff.files : undefined,
|
|
62
|
+
requires_push: totalChanges > 0,
|
|
63
|
+
requires_compile: totalChanges > 0,
|
|
64
|
+
pending_drafts: null,
|
|
65
|
+
hint: totalChanges > 0
|
|
66
|
+
? "Local backend source changed. Run dypai_push to compile, validate, save preview drafts, and regenerate types."
|
|
67
|
+
: "No local backend source changes. Nothing to push.",
|
|
170
68
|
}
|
|
171
69
|
},
|
|
172
70
|
}
|
|
@@ -40,6 +40,15 @@ function parseMaybeJson(v) {
|
|
|
40
40
|
try { return JSON.parse(v) } catch { return v }
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
function isBackendSourcePath(path) {
|
|
44
|
+
if (typeof path !== "string") return false
|
|
45
|
+
return (
|
|
46
|
+
(path.startsWith("dypai/flows/") && /\.flow\.ts$/i.test(path)) ||
|
|
47
|
+
(path.startsWith("dypai/automations/") && /\.automation\.ts$/i.test(path)) ||
|
|
48
|
+
(path.startsWith("dypai/endpoints/") && /\.(ya?ml)$/i.test(path))
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
43
52
|
function hydrateRow(row) {
|
|
44
53
|
return {
|
|
45
54
|
...row,
|
|
@@ -453,6 +462,7 @@ export async function readLocalEffectiveState(rootDir, projectId = null, deps =
|
|
|
453
462
|
|
|
454
463
|
const payloads = Array.isArray(bulkResult.data?.payloads) ? bulkResult.data.payloads : []
|
|
455
464
|
const diagnostics = Array.isArray(bulkResult.data?.diagnostics) ? bulkResult.data.diagnostics : []
|
|
465
|
+
const rejected = Array.isArray(bulkResult.data?.rejected) ? bulkResult.data.rejected : []
|
|
456
466
|
const byName = {}
|
|
457
467
|
const shadowed = []
|
|
458
468
|
const flowNames = new Set()
|
|
@@ -484,12 +494,21 @@ export async function readLocalEffectiveState(rootDir, projectId = null, deps =
|
|
|
484
494
|
})
|
|
485
495
|
}
|
|
486
496
|
|
|
497
|
+
for (const item of rejected) {
|
|
498
|
+
if (!isBackendSourcePath(item?.path)) continue
|
|
499
|
+
yamlState.errors.push({
|
|
500
|
+
file: item.path,
|
|
501
|
+
error: `Backend compiler rejected source file: ${item.reason || "rejected"}`,
|
|
502
|
+
rule: "backend_compiler_rejected_source",
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
|
|
487
506
|
for (const [name, entry] of Object.entries(yamlState.byName)) {
|
|
488
507
|
if (flowNames.has(name)) continue
|
|
489
508
|
byName[name] = { ...entry, source: "legacy-yaml" }
|
|
490
509
|
}
|
|
491
510
|
|
|
492
|
-
return { byName, errors: yamlState.errors, shadowed, runnerWarning: null }
|
|
511
|
+
return { byName, errors: yamlState.errors, shadowed, runnerWarning: null, automations: bulkResult.data?.automations ?? null }
|
|
493
512
|
}
|
|
494
513
|
|
|
495
514
|
// ─── Normalization + diff ──────────────────────────────────────────────────
|
package/src/tools/sync/pull.js
CHANGED
|
@@ -84,7 +84,7 @@ function suspiciousPathWarning(resolvedPath, source) {
|
|
|
84
84
|
|
|
85
85
|
// Subfolders that are always created so the layout is predictable. An agent
|
|
86
86
|
// never has to check "does this folder exist?" before writing a new SQL/prompt/JS file.
|
|
87
|
-
const CANONICAL_SUBDIRS = ["endpoints", "flows", "sql", "prompts", "code", "migrations"]
|
|
87
|
+
const CANONICAL_SUBDIRS = ["endpoints", "flows", "automations", "sql", "prompts", "code", "migrations"]
|
|
88
88
|
|
|
89
89
|
const README_CONTENT = `# dypai/
|
|
90
90
|
|
|
@@ -97,6 +97,8 @@ Declarative snapshot of your DYPAI project's backend.
|
|
|
97
97
|
Subfolders represent endpoint groups, e.g. \`endpoints/Admin/foo.yaml\` → group "Admin".
|
|
98
98
|
- \`flows/\` — TypeScript flow definitions (\`*.flow.ts\`). **Authoritative for new work.**
|
|
99
99
|
When a flow exists for an endpoint name, it wins over legacy YAML at validate/push/test time.
|
|
100
|
+
- \`automations/\` — TypeScript automation definitions (\`*.automation.ts\`) for scheduled/webhook business processes.
|
|
101
|
+
Automations compile to normal backend workflows, but also declare setup requirements, notifications, and history metadata for the organization Automations page.
|
|
100
102
|
- \`capability-catalog.json\` — grouped capability catalog (synced by \`dypai_pull\`).
|
|
101
103
|
- \`capability-brief.md\` — compact capability index for agents.
|
|
102
104
|
- \`node-catalog.json\` — every node_type with input/output schemas.
|
|
@@ -108,7 +110,7 @@ Declarative snapshot of your DYPAI project's backend.
|
|
|
108
110
|
## Workflow
|
|
109
111
|
|
|
110
112
|
1. \`dypai_pull\` to snapshot the remote state into this folder
|
|
111
|
-
2. Edit \`flows/*.flow.ts\`
|
|
113
|
+
2. Edit \`flows/*.flow.ts\` for callable endpoints, \`automations/*.automation.ts\` for scheduled/webhook processes, or legacy YAML, SQL, prompts, code
|
|
112
114
|
3. \`dypai_validate\` / \`dypai_test_endpoint\` before push
|
|
113
115
|
4. \`dypai_diff\` to preview changes
|
|
114
116
|
5. \`dypai_push\` to apply to the remote
|
|
@@ -523,17 +525,36 @@ function isFlowTsSource(workflowSource) {
|
|
|
523
525
|
return workflowSource?.kind === "flow-ts"
|
|
524
526
|
}
|
|
525
527
|
|
|
528
|
+
function isAutomationTsSource(workflowSource) {
|
|
529
|
+
return workflowSource?.kind === "automation-ts"
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function isEditableTsSource(workflowSource) {
|
|
533
|
+
return isFlowTsSource(workflowSource) || isAutomationTsSource(workflowSource)
|
|
534
|
+
}
|
|
535
|
+
|
|
526
536
|
function isFlowSourcePath(path) {
|
|
527
537
|
return typeof path === "string" && path.startsWith("dypai/flows/") && /\.flow\.ts$/i.test(path)
|
|
528
538
|
}
|
|
529
539
|
|
|
530
|
-
|
|
540
|
+
function isAutomationSourcePath(path) {
|
|
541
|
+
return typeof path === "string" && path.startsWith("dypai/automations/") && /\.automation\.ts$/i.test(path)
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function normalizeEditableSourceFile(workflowSource, endpointName) {
|
|
531
545
|
const raw = typeof workflowSource?.file === "string" ? workflowSource.file.trim().replace(/\\/g, "/") : ""
|
|
532
546
|
if (isFlowSourcePath(raw)) return raw
|
|
547
|
+
if (isAutomationSourcePath(raw)) return raw
|
|
533
548
|
if (raw.startsWith("flows/") && /\.flow\.ts$/i.test(raw)) return `dypai/${raw}`
|
|
549
|
+
if (raw.startsWith("automations/") && /\.automation\.ts$/i.test(raw)) return `dypai/${raw}`
|
|
550
|
+
if (isAutomationTsSource(workflowSource)) return `dypai/automations/${endpointName}.automation.ts`
|
|
534
551
|
return `dypai/flows/${endpointName}.flow.ts`
|
|
535
552
|
}
|
|
536
553
|
|
|
554
|
+
export function normalizeFlowSourceFile(workflowSource, endpointName) {
|
|
555
|
+
return normalizeEditableSourceFile(workflowSource, endpointName)
|
|
556
|
+
}
|
|
557
|
+
|
|
537
558
|
function flowSourceRelPath(sourceFile) {
|
|
538
559
|
return sourceFile.replace(/^dypai\//, "")
|
|
539
560
|
}
|
|
@@ -553,7 +574,7 @@ export function buildPersistedFlowSourceMap(sourcePayload) {
|
|
|
553
574
|
const byPath = new Map()
|
|
554
575
|
for (const entry of files) {
|
|
555
576
|
const path = typeof entry?.path === "string" ? entry.path.trim().replace(/\\/g, "/") : ""
|
|
556
|
-
if (!isFlowSourcePath(path)) continue
|
|
577
|
+
if (!isFlowSourcePath(path) && !isAutomationSourcePath(path)) continue
|
|
557
578
|
const decoded = decodeSourceFileContent(entry)
|
|
558
579
|
if (decoded == null) continue
|
|
559
580
|
byPath.set(path, decoded)
|
|
@@ -607,13 +628,15 @@ async function removeEndpointYamlFiles(outDir, name, groupName = null) {
|
|
|
607
628
|
function renderMissingFlowStub(row, sourceFile, sourceFetch) {
|
|
608
629
|
const refLine = sourceFetch?.ref ? `# Checked Git source ref: ${sourceFetch.ref}\n` : ""
|
|
609
630
|
const errorLine = sourceFetch?.error ? `# Source fetch error: ${sourceFetch.error}\n` : ""
|
|
631
|
+
const sourceKind = sourceFile.endsWith(".automation.ts") ? "AUTOMATION" : "FLOW"
|
|
632
|
+
const extension = sourceFile.endsWith(".automation.ts") ? ".automation.ts" : ".flow.ts"
|
|
610
633
|
return (
|
|
611
|
-
`# MISSING AUTHORITATIVE
|
|
612
|
-
`# Remote endpoint '${row.name}' is marked as
|
|
634
|
+
`# MISSING AUTHORITATIVE ${sourceKind} SOURCE\n` +
|
|
635
|
+
`# Remote endpoint '${row.name}' is marked as ${sourceKind.toLowerCase()}-authored, but dypai_pull could not find:\n` +
|
|
613
636
|
`# ${sourceFile}\n` +
|
|
614
637
|
refLine +
|
|
615
638
|
errorLine +
|
|
616
|
-
`# Do not edit this YAML stub. Restore or sync the
|
|
639
|
+
`# Do not edit this YAML stub. Restore or sync the ${extension} source, then rerun dypai_pull.\n\n` +
|
|
617
640
|
`# name: ${row.name}\n` +
|
|
618
641
|
`# method: ${row.method || "POST"}\n`
|
|
619
642
|
)
|
|
@@ -901,9 +924,9 @@ export const dypaiPullTool = {
|
|
|
901
924
|
row.workflow_source = parseMaybeJson(row.workflow_source)
|
|
902
925
|
try {
|
|
903
926
|
const workflowSource = row.workflow_source
|
|
904
|
-
if (
|
|
927
|
+
if (isEditableTsSource(workflowSource)) {
|
|
905
928
|
const groupName = row.group_id ? (mapsCtx.groupIdToName[row.group_id] || null) : null
|
|
906
|
-
const sourceFile =
|
|
929
|
+
const sourceFile = normalizeEditableSourceFile(workflowSource, row.name)
|
|
907
930
|
const relFlowPath = flowSourceRelPath(sourceFile)
|
|
908
931
|
const flowContent = persistedFlowSources.byPath.get(sourceFile)
|
|
909
932
|
// Drop stale YAML/stubs from a previous pull. Any YAML next to a real
|
|
@@ -945,8 +968,8 @@ export const dypaiPullTool = {
|
|
|
945
968
|
errors.push({
|
|
946
969
|
endpoint: row.name,
|
|
947
970
|
error:
|
|
948
|
-
`Endpoint is
|
|
949
|
-
`Restore/sync the
|
|
971
|
+
`Endpoint is source-authored but ${sourceFile} was not found in persisted project source. ` +
|
|
972
|
+
`Restore/sync the editable TypeScript source, then rerun dypai_pull.`,
|
|
950
973
|
})
|
|
951
974
|
continue
|
|
952
975
|
}
|
|
@@ -1129,11 +1152,11 @@ export const dypaiPullTool = {
|
|
|
1129
1152
|
next_steps: pendingDrafts
|
|
1130
1153
|
? [
|
|
1131
1154
|
`${draftsTotal} pending draft(s). Review with manage_drafts(operation:'list'), verify with dypai_test_endpoint(mode:'draft'), then publish or discard.`,
|
|
1132
|
-
"After resolving drafts, edit Flow in dypai/flows/
|
|
1155
|
+
"After resolving drafts, edit Flow in dypai/flows/, Automations in dypai/automations/, or legacy YAML in dypai/endpoints/, then dypai_diff → dypai_push to stage more.",
|
|
1133
1156
|
]
|
|
1134
1157
|
: (endpoints || []).length === 0
|
|
1135
|
-
? ["Empty project. Create tables via execute_sql, then write dypai/flows/<name>.flow.ts
|
|
1136
|
-
: ["Read dypai/schema.sql before writing queries.", "Edit Flow in dypai/flows/
|
|
1158
|
+
? ["Empty project. Create tables via execute_sql, then write dypai/flows/<name>.flow.ts for callable endpoints or dypai/automations/<name>.automation.ts for scheduled/webhook processes, then dypai_push."]
|
|
1159
|
+
: ["Read dypai/schema.sql before writing queries.", "Edit Flow in dypai/flows/, Automations in dypai/automations/, or legacy YAML in dypai/endpoints/, then dypai_diff → dypai_push."],
|
|
1137
1160
|
}
|
|
1138
1161
|
|
|
1139
1162
|
return {
|
|
@@ -1155,7 +1178,7 @@ export const dypaiPullTool = {
|
|
|
1155
1178
|
: draftsTotal > 0
|
|
1156
1179
|
? `${draftsTotal} pending draft(s) on this project — see overview.pending_drafts and decide publish vs discard before pushing more.`
|
|
1157
1180
|
: endpoints.length === 0
|
|
1158
|
-
? "Empty project. Create tables with execute_sql, then write flows/<name>.flow.ts and dypai_push."
|
|
1181
|
+
? "Empty project. Create tables with execute_sql, then write flows/<name>.flow.ts or automations/<name>.automation.ts and dypai_push."
|
|
1159
1182
|
: undefined,
|
|
1160
1183
|
}
|
|
1161
1184
|
},
|
package/src/tools/sync/push.js
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./planner.js"
|
|
26
26
|
import { runValidation } from "./validate.js"
|
|
27
27
|
import { runGenerateEndpointTypes } from "./generate-types.js"
|
|
28
|
+
import { writeBackendSourceBaseline } from "./source-diff.js"
|
|
28
29
|
// Codegen removed from v1 — see pull.js note.
|
|
29
30
|
// import { regenerateTypes } from "../codegen.js"
|
|
30
31
|
|
|
@@ -34,7 +35,7 @@ const VALID_RESPONSE_CARDINALITY = new Set(["single", "many", "zero_or_one"])
|
|
|
34
35
|
const PUSH_CONCURRENCY = Math.max(1, Number(process.env.DYPAI_PUSH_CONCURRENCY || 1))
|
|
35
36
|
const PUSH_RETRY_ATTEMPTS = Math.max(1, Number(process.env.DYPAI_PUSH_RETRY_ATTEMPTS || 3))
|
|
36
37
|
const PUSH_RETRY_DELAY_MS = Math.max(100, Number(process.env.DYPAI_PUSH_RETRY_DELAY_MS || 500))
|
|
37
|
-
const BACKEND_SOURCE_CHECKPOINT_DIRS = ["flows", "endpoints", "migrations", "types", "lib"]
|
|
38
|
+
const BACKEND_SOURCE_CHECKPOINT_DIRS = ["flows", "automations", "endpoints", "migrations", "types", "lib"]
|
|
38
39
|
const BACKEND_SOURCE_CHECKPOINT_EXACT = ["schema.sql", "realtime.yaml"]
|
|
39
40
|
const BACKEND_SOURCE_CHECKPOINT_EXTS = new Set([
|
|
40
41
|
".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts", ".cjs", ".cts",
|
|
@@ -136,6 +137,8 @@ function normalizeWorkflowSourceFile(row) {
|
|
|
136
137
|
const raw = typeof source?.file === "string" ? source.file.trim().replace(/\\/g, "/") : ""
|
|
137
138
|
if (raw.startsWith("dypai/flows/") && raw.endsWith(".flow.ts")) return raw
|
|
138
139
|
if (raw.startsWith("flows/") && raw.endsWith(".flow.ts")) return `dypai/${raw}`
|
|
140
|
+
if (raw.startsWith("dypai/automations/") && raw.endsWith(".automation.ts")) return raw
|
|
141
|
+
if (raw.startsWith("automations/") && raw.endsWith(".automation.ts")) return `dypai/${raw}`
|
|
139
142
|
return null
|
|
140
143
|
}
|
|
141
144
|
|
|
@@ -195,6 +198,29 @@ async function checkpointBackendSource(projectId, rootDir, plan, remote) {
|
|
|
195
198
|
}
|
|
196
199
|
}
|
|
197
200
|
|
|
201
|
+
async function refreshBackendSourceBaseline(projectId, rootDir, sourceCheckpoint) {
|
|
202
|
+
if (sourceCheckpoint?.ok === false && sourceCheckpoint?.skipped !== true) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
skipped: true,
|
|
206
|
+
reason: "source_checkpoint_failed",
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const baseline = await writeBackendSourceBaseline(rootDir, projectId)
|
|
211
|
+
return {
|
|
212
|
+
ok: true,
|
|
213
|
+
files: Array.isArray(baseline.files) ? baseline.files.length : 0,
|
|
214
|
+
path: "dypai/.dypai/backend-baseline.json",
|
|
215
|
+
}
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
error: error?.message || String(error),
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
198
224
|
/**
|
|
199
225
|
* Run an async worker over an array with bounded concurrency. Workers never
|
|
200
226
|
* throw (they handle their own errors via the push errors[] accumulator),
|
|
@@ -275,7 +301,7 @@ function assertMutationOK(result, op, name) {
|
|
|
275
301
|
* Returns true when the API staged the change as a draft (default behavior)
|
|
276
302
|
* instead of applying it directly to live. Both the draft branch and the
|
|
277
303
|
* direct-write branch are valid successful outcomes — drafts just need to
|
|
278
|
-
* be reported back to the user so they know to
|
|
304
|
+
* be reported back to the user so they know to publish the drafts explicitly.
|
|
279
305
|
*/
|
|
280
306
|
function isDraftResponse(result) {
|
|
281
307
|
return result && typeof result === "object" && result.applied_to === "draft"
|
|
@@ -439,7 +465,7 @@ export const dypaiPushTool = {
|
|
|
439
465
|
},
|
|
440
466
|
dry_run: {
|
|
441
467
|
type: "boolean",
|
|
442
|
-
description: "If true, compute the plan
|
|
468
|
+
description: "If true, compile and compute the backend plan without applying or regenerating types. For a fast source-only preview use dypai_diff.",
|
|
443
469
|
default: false,
|
|
444
470
|
},
|
|
445
471
|
force: {
|
|
@@ -499,11 +525,13 @@ export const dypaiPushTool = {
|
|
|
499
525
|
}
|
|
500
526
|
}
|
|
501
527
|
|
|
502
|
-
let typesGenerated = null
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
528
|
+
let typesGenerated = dry_run ? { ok: true, skipped: true, reason: "dry_run" } : null
|
|
529
|
+
if (!dry_run) {
|
|
530
|
+
try {
|
|
531
|
+
typesGenerated = await runGenerateEndpointTypes(rootDir, targetProjectId)
|
|
532
|
+
} catch (e) {
|
|
533
|
+
typesGenerated = { ok: false, error: e.message }
|
|
534
|
+
}
|
|
507
535
|
}
|
|
508
536
|
|
|
509
537
|
let local
|
|
@@ -594,6 +622,9 @@ export const dypaiPushTool = {
|
|
|
594
622
|
? null
|
|
595
623
|
: await checkpointBackendSource(targetProjectId, rootDir, plan, remote)
|
|
596
624
|
const sourceCheckpointFailed = sourceCheckpoint?.ok === false && sourceCheckpoint?.skipped !== true
|
|
625
|
+
const sourceBaseline = !dry_run && !sourceCheckpointFailed
|
|
626
|
+
? await refreshBackendSourceBaseline(targetProjectId, rootDir, sourceCheckpoint)
|
|
627
|
+
: undefined
|
|
597
628
|
return {
|
|
598
629
|
success: !sourceCheckpointFailed,
|
|
599
630
|
applied: false,
|
|
@@ -602,6 +633,7 @@ export const dypaiPushTool = {
|
|
|
602
633
|
plan,
|
|
603
634
|
types: typesGenerated,
|
|
604
635
|
source_checkpoint: sourceCheckpoint || undefined,
|
|
636
|
+
source_baseline: sourceBaseline,
|
|
605
637
|
hint: sourceCheckpointFailed
|
|
606
638
|
? "Backend runtime was unchanged, but DYPAI could not save the local dypai/ source to the Studio branch. Retry dypai_push once the GitHub/source checkpoint issue is resolved."
|
|
607
639
|
: undefined,
|
|
@@ -725,7 +757,7 @@ export const dypaiPushTool = {
|
|
|
725
757
|
]
|
|
726
758
|
|
|
727
759
|
// How many ops landed in draft state. When `draftCount > 0` the user
|
|
728
|
-
// must
|
|
760
|
+
// must publish drafts explicitly to expose the changes
|
|
729
761
|
// on live; tests against live won't see them yet. Realtime drafts are
|
|
730
762
|
// also counted — the API returns `applied_to: "draft"` and a
|
|
731
763
|
// `drafts_queued` array when realtime policies were staged.
|
|
@@ -752,6 +784,9 @@ export const dypaiPushTool = {
|
|
|
752
784
|
reason: "push_had_errors",
|
|
753
785
|
}
|
|
754
786
|
const sourceCheckpointFailed = sourceCheckpoint?.ok === false && sourceCheckpoint?.skipped !== true
|
|
787
|
+
const sourceBaseline = errors.length === 0 && !sourceCheckpointFailed
|
|
788
|
+
? await refreshBackendSourceBaseline(targetProjectId, rootDir, sourceCheckpoint)
|
|
789
|
+
: undefined
|
|
755
790
|
|
|
756
791
|
return {
|
|
757
792
|
success: errors.length === 0 && !sourceCheckpointFailed,
|
|
@@ -786,6 +821,7 @@ export const dypaiPushTool = {
|
|
|
786
821
|
errors: errors.length ? errors : undefined,
|
|
787
822
|
types: typesGenerated,
|
|
788
823
|
source_checkpoint: sourceCheckpoint,
|
|
824
|
+
source_baseline: sourceBaseline,
|
|
789
825
|
// Only one next_step — and only when it's non-obvious. Drafts win
|
|
790
826
|
// over the test suggestion because tests against live won't see
|
|
791
827
|
// the change until the drafts are promoted.
|
|
@@ -794,7 +830,7 @@ export const dypaiPushTool = {
|
|
|
794
830
|
? `${endpointTotal} endpoint(s) saved, ${failedTotal} failed. Fix failed endpoints and push again. Failed: ${errors.slice(0, 3).map((item) => item.endpoint || item.group || item.op).join(", ")}${errors.length > 3 ? "…" : ""}`
|
|
795
831
|
: "Fix the offending YAMLs and push again."
|
|
796
832
|
: draftCount > 0
|
|
797
|
-
? `${draftCount} backend change(s) saved
|
|
833
|
+
? `${draftCount} backend change(s) saved as drafts — they're not live yet. Review with manage_drafts(operation:'list'), verify with dypai_test_endpoint(mode:'draft', endpoint:'<name>') when useful, then publish with manage_drafts(operation:'publish', confirm:true) or dypai_deploy_production(target:'backend', confirm:true) ONLY after explicit approval.`
|
|
798
834
|
: changedNames.length
|
|
799
835
|
? `Test changed endpoint(s) with dypai_test_endpoint: ${changedNames.slice(0, 3).join(", ")}${changedNames.length > 3 ? "…" : ""}`
|
|
800
836
|
: undefined,
|