@dypai-ai/mcp 1.4.2 → 1.4.5

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/src/tools/sync.js CHANGED
@@ -12,11 +12,61 @@
12
12
  * node_modules, .vscode, etc.) are preserved.
13
13
  */
14
14
 
15
- import { writeFileSync, mkdirSync, existsSync } from "fs"
15
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs"
16
16
  import { join, dirname } from "path"
17
17
  import { createHash } from "crypto"
18
18
  import { api } from "../api.js"
19
19
 
20
+ // Engine hostname suffix. Production hosts of the form `<project_id>.dypai.dev`
21
+ // serve LIVE traffic; `dev-<project_id>.dypai.dev` serves the LAYER 2.5 draft
22
+ // overlay (drafts when present, live as fallback). Override only for local
23
+ // engine development against `*.localhost`.
24
+ const DEFAULT_ENGINE_BASE = "dypai.dev"
25
+
26
+ // Detect the frontend framework by inspecting `package.json` so we know which
27
+ // env-var prefix the bundler will inject at build time. We only need to
28
+ // distinguish Next.js from everything else (Vite/React/Astro/SvelteKit/etc.
29
+ // all consume `VITE_*` style or accept it as fallback). Returns "next" |
30
+ // "vite" | "unknown" — when "unknown" we write BOTH prefixes (cheap, harmless).
31
+ function detectFramework(targetDirectory) {
32
+ try {
33
+ const pkgPath = join(targetDirectory, "package.json")
34
+ if (!existsSync(pkgPath)) return "unknown"
35
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"))
36
+ const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) }
37
+ if (deps.next) return "next"
38
+ if (deps.vite || deps["@vitejs/plugin-react"]) return "vite"
39
+ return "unknown"
40
+ } catch {
41
+ return "unknown"
42
+ }
43
+ }
44
+
45
+ // Build the contents of `.env.local`. We point at the DRAFT OVERLAY host
46
+ // (`dev-<project_id>.<base>`), not at live, so an unpublished `dypai_push`
47
+ // is visible end-to-end from the local frontend. On production deploy the
48
+ // platform PATCHes `<PREFIX>DYPAI_URL` to the LIVE host as a CF Pages build
49
+ // env var, so this file is only ever consumed by `vite dev` / `next dev`.
50
+ function buildEnvLocalContents(project_id, framework, engineBase) {
51
+ const draftUrl = `https://dev-${project_id}.${engineBase}`
52
+ const lines = [
53
+ `# DYPAI — generated by manage_frontend(sync). Safe to edit; do NOT commit.`,
54
+ `# Points at the LAYER 2.5 draft overlay so unpublished \`dypai_push\` changes`,
55
+ `# are picked up by the local dev server. Production builds receive the LIVE`,
56
+ `# URL automatically as a CF Pages build env var (no action needed).`,
57
+ ``,
58
+ ]
59
+ if (framework === "next" || framework === "unknown") {
60
+ lines.push(`NEXT_PUBLIC_DYPAI_URL=${draftUrl}`)
61
+ lines.push(`NEXT_PUBLIC_PROJECT_ID=${project_id}`)
62
+ }
63
+ if (framework === "vite" || framework === "unknown") {
64
+ lines.push(`VITE_DYPAI_URL=${draftUrl}`)
65
+ lines.push(`VITE_PROJECT_ID=${project_id}`)
66
+ }
67
+ return lines.join("\n") + "\n"
68
+ }
69
+
20
70
  export async function syncFromRemote({ project_id, targetDirectory, overwrite = false }) {
21
71
  if (!project_id) {
22
72
  return { success: false, error: "project_id is required." }
@@ -92,22 +142,33 @@ export async function syncFromRemote({ project_id, targetDirectory, overwrite =
92
142
  }
93
143
  }
94
144
 
95
- // Seed the deploy manifest with the synced files' hashes. The next deploy
96
- // sees the full project already matches the remote and only uploads what
97
- // the agent actually edits avoids a pointless 30 MB "full deploy" right
98
- // after a sync when nothing has changed yet.
145
+ // Seed the last-deploy hash so the next deploy can skip if nothing
146
+ // changed. We compute the same project-wide hash deploy.js produces
147
+ // (sorted path+file-hash pairs). If the user runs `manage_frontend(deploy)`
148
+ // right after a sync and hasn't edited anything, it returns "no_changes"
149
+ // instantly without re-uploading 30 MB over the wire.
99
150
  try {
100
151
  const dypaiBase = existsSync(join(targetDirectory, "dypai"))
101
152
  ? join(targetDirectory, "dypai", ".dypai")
102
153
  : join(targetDirectory, ".dypai")
103
154
  mkdirSync(dypaiBase, { recursive: true })
155
+
156
+ // Same algorithm as deploy.js:computeProjectHash — sort by path, join
157
+ // path + hash with NUL, SHA-256 the concatenation. Keep in sync.
158
+ const entries = Object.entries(hashMap).sort(([a], [b]) => a.localeCompare(b))
159
+ const h = createHash("sha256")
160
+ for (const [path, fileHash] of entries) {
161
+ h.update(path); h.update("\0"); h.update(fileHash); h.update("\0")
162
+ }
163
+ const projectHash = h.digest("hex")
164
+
104
165
  writeFileSync(
105
- join(dypaiBase, "deploy-manifest.json"),
106
- JSON.stringify(hashMap, null, 2) + "\n",
166
+ join(dypaiBase, "last-deploy.json"),
167
+ JSON.stringify({ hash: projectHash, at: new Date().toISOString() }, null, 2) + "\n",
107
168
  )
108
169
  } catch (e) {
109
170
  // Non-fatal: the first deploy will just be a full deploy.
110
- failures.push({ path: ".dypai/deploy-manifest.json", reason: `Manifest seed failed: ${e.message}` })
171
+ failures.push({ path: ".dypai/last-deploy.json", reason: `Hash seed failed: ${e.message}` })
111
172
  }
112
173
 
113
174
  // Structured next_steps so the agent can act without re-prompting the LLM.
@@ -121,19 +182,41 @@ export async function syncFromRemote({ project_id, targetDirectory, overwrite =
121
182
  )
122
183
  }
123
184
 
124
- // .env is gitignored → the source API does NOT return it → the frontend won't
125
- // know the engine URL. If it's missing after sync, tell the agent to create it.
126
- // Engine URL convention: https://<project_id>.dypai.app (override with DYPAI_ENGINE_BASE).
127
- const envMissing = !existsSync(join(targetDirectory, ".env"))
128
- if (envMissing) {
129
- const engineBase = process.env.DYPAI_ENGINE_BASE || "dypai.app"
130
- const engineUrl = `https://${project_id}.${engineBase}`
131
- next_steps.push(
132
- `Create .env in ${targetDirectory} with \`VITE_DYPAI_URL=${engineUrl}\` (and \`VITE_PROJECT_ID=${project_id}\`). ` +
133
- `Use NEXT_PUBLIC_DYPAI_URL instead of VITE_DYPAI_URL for Next.js projects. ` +
134
- `The frontend SDK reads this to reach the engine — without .env, API calls will fail with network errors.`
135
- )
185
+ // `.env` / `.env.local` are gitignored → the source API does NOT return them
186
+ // the frontend wouldn't know which engine URL to hit. We auto-write
187
+ // `.env.local` with the LAYER 2.5 draft-overlay URL (`dev-<project_id>.<base>`)
188
+ // so the local dev server picks up unpublished `dypai_push` changes
189
+ // immediately. Production builds receive the LIVE URL via CF Pages build
190
+ // env vars set by the control plane (api/services/frontend/pages_service.py).
191
+ //
192
+ // We only WRITE if `.env.local` (and the legacy `.env`) don't exist —
193
+ // never clobber a user-authored config.
194
+ const engineBase = process.env.DYPAI_ENGINE_BASE || DEFAULT_ENGINE_BASE
195
+ const envLocalPath = join(targetDirectory, ".env.local")
196
+ const envPath = join(targetDirectory, ".env")
197
+ const hasUserEnv = existsSync(envLocalPath) || existsSync(envPath)
198
+ let envWritten = false
199
+ if (!hasUserEnv) {
200
+ try {
201
+ const framework = detectFramework(targetDirectory)
202
+ writeFileSync(envLocalPath, buildEnvLocalContents(project_id, framework, engineBase))
203
+ envWritten = true
204
+ next_steps.push(
205
+ `Wrote \`.env.local\` pointing at the draft-overlay host \`https://dev-${project_id}.${engineBase}\`. ` +
206
+ `Local dev (\`vite dev\` / \`next dev\`) will see unpublished \`dypai_push\` changes immediately. ` +
207
+ `Production builds get the LIVE URL injected automatically — no manual switch needed.`
208
+ )
209
+ } catch (e) {
210
+ failures.push({ path: ".env.local", reason: `Could not write env file: ${e.message}` })
211
+ next_steps.push(
212
+ `Could not auto-write .env.local (${e.message}). Create it manually with ` +
213
+ `\`VITE_DYPAI_URL=https://dev-${project_id}.${engineBase}\` (or \`NEXT_PUBLIC_DYPAI_URL=...\` for Next.js).`
214
+ )
215
+ }
136
216
  }
217
+ // Surface env_file_missing as before (back-compat for older agents that
218
+ // gate on it). Treats `.env.local` as satisfying the requirement.
219
+ const envMissing = !hasUserEnv && !envWritten
137
220
 
138
221
  return {
139
222
  success: written > 0,
@@ -1,94 +0,0 @@
1
- /**
2
- * Frontend status + build status tools.
3
- */
4
-
5
- import { api } from "../api.js"
6
-
7
- export const listDeploymentsTool = {
8
- name: "list_deployments",
9
- description: `List deployment history for the project's frontend. Shows recent deploys with status, commit, duration, and URL.`,
10
-
11
- inputSchema: {
12
- type: "object",
13
- properties: {
14
- project_id: { type: "string", description: "Project UUID." },
15
- limit: { type: "number", description: "Max deployments to return (default 10, max 20)." },
16
- },
17
- required: ["project_id"],
18
- },
19
-
20
- async execute({ project_id, limit }) {
21
- try {
22
- return await api.get(`/api/engine/${project_id}/frontend/deployments?limit=${limit || 10}`)
23
- } catch (e) {
24
- return { error: e.message }
25
- }
26
- },
27
- }
28
-
29
- export const getDeploymentLogsTool = {
30
- name: "get_deployment_logs",
31
- description: `Get build logs for a specific deployment. Use list_deployments first to get the deployment ID. Useful for debugging failed builds.`,
32
-
33
- inputSchema: {
34
- type: "object",
35
- properties: {
36
- project_id: { type: "string", description: "Project UUID." },
37
- deployment_id: { type: "string", description: "Deployment UUID from list_deployments." },
38
- },
39
- required: ["project_id", "deployment_id"],
40
- },
41
-
42
- async execute({ project_id, deployment_id }) {
43
- try {
44
- return await api.get(`/api/engine/${project_id}/frontend/deployments/${deployment_id}/logs`)
45
- } catch (e) {
46
- return { error: e.message }
47
- }
48
- },
49
- }
50
-
51
- export const frontendStatusTool = {
52
- name: "get_frontend_status",
53
- description: "Get the current frontend deployment status — URL, status, last deploy time, size.",
54
-
55
- inputSchema: {
56
- type: "object",
57
- properties: {
58
- project_id: { type: "string", description: "Project UUID." },
59
- },
60
- required: ["project_id"],
61
- },
62
-
63
- async execute({ project_id }) {
64
- try {
65
- return await api.get(`/api/engine/${project_id}/frontend`)
66
- } catch (e) {
67
- return { error: e.message }
68
- }
69
- },
70
- }
71
-
72
- export const buildStatusTool = {
73
- name: "get_build_status",
74
- description: `Get the latest build status from Cloudflare Pages.
75
-
76
- Returns: status (queued/building/success/failure), current stage, progress %, URL.
77
- Useful to check if a deploy has finished building.`,
78
-
79
- inputSchema: {
80
- type: "object",
81
- properties: {
82
- project_id: { type: "string", description: "Project UUID." },
83
- },
84
- required: ["project_id"],
85
- },
86
-
87
- async execute({ project_id }) {
88
- try {
89
- return await api.get(`/api/engine/${project_id}/frontend/build-status`)
90
- } catch (e) {
91
- return { error: e.message }
92
- }
93
- },
94
- }