flutterflow-mcp 0.1.0

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.
Files changed (83) hide show
  1. package/README.md +124 -0
  2. package/build/api/flutterflow.d.ts +11 -0
  3. package/build/api/flutterflow.js +61 -0
  4. package/build/index.d.ts +2 -0
  5. package/build/index.js +54 -0
  6. package/build/prompts/dev-workflow.d.ts +2 -0
  7. package/build/prompts/dev-workflow.js +68 -0
  8. package/build/prompts/generate-page.d.ts +2 -0
  9. package/build/prompts/generate-page.js +36 -0
  10. package/build/prompts/inspect-project.d.ts +2 -0
  11. package/build/prompts/inspect-project.js +30 -0
  12. package/build/prompts/modify-component.d.ts +2 -0
  13. package/build/prompts/modify-component.js +39 -0
  14. package/build/resources/docs.d.ts +2 -0
  15. package/build/resources/docs.js +76 -0
  16. package/build/resources/projects.d.ts +3 -0
  17. package/build/resources/projects.js +60 -0
  18. package/build/tools/find-component-usages.d.ts +7 -0
  19. package/build/tools/find-component-usages.js +225 -0
  20. package/build/tools/find-page-navigations.d.ts +7 -0
  21. package/build/tools/find-page-navigations.js +228 -0
  22. package/build/tools/get-component-summary.d.ts +22 -0
  23. package/build/tools/get-component-summary.js +193 -0
  24. package/build/tools/get-page-by-name.d.ts +3 -0
  25. package/build/tools/get-page-by-name.js +56 -0
  26. package/build/tools/get-page-summary.d.ts +22 -0
  27. package/build/tools/get-page-summary.js +220 -0
  28. package/build/tools/get-yaml-docs.d.ts +6 -0
  29. package/build/tools/get-yaml-docs.js +217 -0
  30. package/build/tools/get-yaml.d.ts +3 -0
  31. package/build/tools/get-yaml.js +47 -0
  32. package/build/tools/list-files.d.ts +3 -0
  33. package/build/tools/list-files.js +30 -0
  34. package/build/tools/list-pages.d.ts +25 -0
  35. package/build/tools/list-pages.js +101 -0
  36. package/build/tools/list-projects.d.ts +3 -0
  37. package/build/tools/list-projects.js +19 -0
  38. package/build/tools/sync-project.d.ts +3 -0
  39. package/build/tools/sync-project.js +144 -0
  40. package/build/tools/update-yaml.d.ts +3 -0
  41. package/build/tools/update-yaml.js +24 -0
  42. package/build/tools/validate-yaml.d.ts +3 -0
  43. package/build/tools/validate-yaml.js +22 -0
  44. package/build/utils/cache.d.ts +48 -0
  45. package/build/utils/cache.js +162 -0
  46. package/build/utils/decode-yaml.d.ts +7 -0
  47. package/build/utils/decode-yaml.js +31 -0
  48. package/build/utils/page-summary/action-summarizer.d.ts +9 -0
  49. package/build/utils/page-summary/action-summarizer.js +291 -0
  50. package/build/utils/page-summary/formatter.d.ts +13 -0
  51. package/build/utils/page-summary/formatter.js +121 -0
  52. package/build/utils/page-summary/node-extractor.d.ts +17 -0
  53. package/build/utils/page-summary/node-extractor.js +207 -0
  54. package/build/utils/page-summary/tree-walker.d.ts +6 -0
  55. package/build/utils/page-summary/tree-walker.js +55 -0
  56. package/build/utils/page-summary/types.d.ts +56 -0
  57. package/build/utils/page-summary/types.js +4 -0
  58. package/build/utils/parse-folders.d.ts +9 -0
  59. package/build/utils/parse-folders.js +29 -0
  60. package/docs/ff-yaml/00-overview.md +137 -0
  61. package/docs/ff-yaml/01-project-files.md +513 -0
  62. package/docs/ff-yaml/02-pages.md +572 -0
  63. package/docs/ff-yaml/03-components.md +413 -0
  64. package/docs/ff-yaml/04-widgets/README.md +122 -0
  65. package/docs/ff-yaml/04-widgets/button.md +444 -0
  66. package/docs/ff-yaml/04-widgets/container.md +358 -0
  67. package/docs/ff-yaml/04-widgets/dropdown.md +579 -0
  68. package/docs/ff-yaml/04-widgets/form.md +256 -0
  69. package/docs/ff-yaml/04-widgets/image.md +276 -0
  70. package/docs/ff-yaml/04-widgets/layout.md +355 -0
  71. package/docs/ff-yaml/04-widgets/misc.md +553 -0
  72. package/docs/ff-yaml/04-widgets/text-field.md +326 -0
  73. package/docs/ff-yaml/04-widgets/text.md +302 -0
  74. package/docs/ff-yaml/05-actions.md +843 -0
  75. package/docs/ff-yaml/06-variables.md +834 -0
  76. package/docs/ff-yaml/07-data.md +591 -0
  77. package/docs/ff-yaml/08-custom-code.md +715 -0
  78. package/docs/ff-yaml/09-theming.md +592 -0
  79. package/docs/ff-yaml/10-editing-guide.md +454 -0
  80. package/docs/ff-yaml/README.md +105 -0
  81. package/package.json +55 -0
  82. package/skills/ff-widget-patterns.md +141 -0
  83. package/skills/ff-yaml-dev.md +58 -0
@@ -0,0 +1,101 @@
1
+ import { z } from "zod";
2
+ import { decodeProjectYamlResponse } from "../utils/decode-yaml.js";
3
+ import { parseFolderMapping } from "../utils/parse-folders.js";
4
+ import { cacheRead, cacheWrite } from "../utils/cache.js";
5
+ /**
6
+ * Extract page scaffold IDs from the partitioned file names list.
7
+ * Filters for top-level page files only (not sub-files like widget-tree-outline).
8
+ */
9
+ export function extractPageFileKeys(fileNames) {
10
+ const raw = fileNames;
11
+ const names = raw?.value?.file_names ?? raw?.value?.fileNames ?? [];
12
+ return names.filter((n) => /^page\/id-Scaffold_\w+$/.test(n));
13
+ }
14
+ /**
15
+ * Fetch a single YAML file and decode it. Returns null on failure.
16
+ */
17
+ export async function fetchOneFile(client, projectId, fileName) {
18
+ // Check cache first
19
+ const cached = await cacheRead(projectId, fileName);
20
+ if (cached) {
21
+ return { fileKey: fileName, content: cached };
22
+ }
23
+ try {
24
+ const raw = await client.getProjectYamls(projectId, fileName);
25
+ const decoded = decodeProjectYamlResponse(raw);
26
+ const entries = Object.entries(decoded);
27
+ if (entries.length > 0) {
28
+ // Write to cache on fetch
29
+ await cacheWrite(projectId, fileName, entries[0][1]);
30
+ return { fileKey: fileName, content: entries[0][1] };
31
+ }
32
+ return null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ /**
39
+ * Process items in batches to avoid API rate limits.
40
+ */
41
+ async function batchProcess(items, batchSize, fn) {
42
+ const results = [];
43
+ for (let i = 0; i < items.length; i += batchSize) {
44
+ const batch = items.slice(i, i + batchSize);
45
+ const batchResults = await Promise.allSettled(batch.map(fn));
46
+ results.push(...batchResults);
47
+ }
48
+ return results;
49
+ }
50
+ /**
51
+ * Extract a compact page index by fetching pages in batches.
52
+ */
53
+ export async function listPages(client, projectId) {
54
+ // Step 1: Get file list and folders in parallel
55
+ const [fileNamesRaw, foldersResult] = await Promise.all([
56
+ client.listPartitionedFileNames(projectId),
57
+ fetchOneFile(client, projectId, "folders"),
58
+ ]);
59
+ const pageFileKeys = extractPageFileKeys(fileNamesRaw);
60
+ const folderMap = foldersResult
61
+ ? parseFolderMapping(foldersResult.content)
62
+ : {};
63
+ // Step 2: Fetch pages in batches of 5 to avoid rate limits
64
+ const results = await batchProcess(pageFileKeys, 5, (fileKey) => fetchOneFile(client, projectId, fileKey));
65
+ const pages = [];
66
+ for (let i = 0; i < pageFileKeys.length; i++) {
67
+ const fileKey = pageFileKeys[i];
68
+ const scaffoldMatch = fileKey.match(/^page\/id-(Scaffold_\w+)$/);
69
+ if (!scaffoldMatch)
70
+ continue;
71
+ const scaffoldId = scaffoldMatch[1];
72
+ const result = results[i];
73
+ let name = "(error - could not fetch)";
74
+ if (result.status === "fulfilled" && result.value) {
75
+ const nameMatch = result.value.content.match(/^name:\s*(.+)$/m);
76
+ name = nameMatch ? nameMatch[1].trim() : "(unknown)";
77
+ }
78
+ const folder = folderMap[scaffoldId] || "(unmapped)";
79
+ pages.push({ scaffoldId, name, folder, fileKey });
80
+ }
81
+ // Sort by folder then name
82
+ pages.sort((a, b) => a.folder === b.folder
83
+ ? a.name.localeCompare(b.name)
84
+ : a.folder.localeCompare(b.folder));
85
+ return pages;
86
+ }
87
+ export function registerListPagesTool(server, client) {
88
+ server.tool("list_pages", "List all pages in a FlutterFlow project with human-readable names, scaffold IDs, and folder assignments. Use this FIRST to discover pages before fetching their YAML.", {
89
+ projectId: z.string().describe("The FlutterFlow project ID"),
90
+ }, async ({ projectId }) => {
91
+ const pages = await listPages(client, projectId);
92
+ return {
93
+ content: [
94
+ {
95
+ type: "text",
96
+ text: JSON.stringify(pages, null, 2),
97
+ },
98
+ ],
99
+ };
100
+ });
101
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { FlutterFlowClient } from "../api/flutterflow.js";
3
+ export declare function registerListProjectsTool(server: McpServer, client: FlutterFlowClient): void;
@@ -0,0 +1,19 @@
1
+ import { z } from "zod";
2
+ export function registerListProjectsTool(server, client) {
3
+ server.tool("list_projects", "List all FlutterFlow projects for the authenticated user", {
4
+ project_type: z
5
+ .string()
6
+ .optional()
7
+ .describe("Optional filter for project type"),
8
+ }, async ({ project_type }) => {
9
+ const result = await client.listProjects(project_type);
10
+ return {
11
+ content: [
12
+ {
13
+ type: "text",
14
+ text: JSON.stringify(result, null, 2),
15
+ },
16
+ ],
17
+ };
18
+ });
19
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { FlutterFlowClient } from "../api/flutterflow.js";
3
+ export declare function registerSyncProjectTool(server: McpServer, client: FlutterFlowClient): void;
@@ -0,0 +1,144 @@
1
+ import { z } from "zod";
2
+ import { decodeProjectYamlResponse } from "../utils/decode-yaml.js";
3
+ import { cacheWriteBulk, cacheWriteMeta, cacheWrite, cacheMeta, } from "../utils/cache.js";
4
+ import { fetchOneFile } from "./list-pages.js";
5
+ /**
6
+ * Extract top-level file keys from the listPartitionedFileNames response.
7
+ * Filters out deeply nested sub-files (widget nodes, etc.) that can't be
8
+ * fetched individually and would hammer the API with thousands of requests.
9
+ * Top-level keys have at most 2 path segments (e.g. "page/id-Scaffold_xxx").
10
+ */
11
+ function extractTopLevelFileKeys(fileNamesRaw) {
12
+ const raw = fileNamesRaw;
13
+ const allKeys = raw?.value?.file_names ?? raw?.value?.fileNames ?? [];
14
+ return allKeys.filter((key) => key.split("/").length <= 2);
15
+ }
16
+ /**
17
+ * Process items in batches to avoid API rate limits.
18
+ */
19
+ async function batchProcess(items, batchSize, fn) {
20
+ const results = [];
21
+ for (let i = 0; i < items.length; i += batchSize) {
22
+ const batch = items.slice(i, i + batchSize);
23
+ const batchResults = await Promise.allSettled(batch.map(fn));
24
+ results.push(...batchResults);
25
+ }
26
+ return results;
27
+ }
28
+ export function registerSyncProjectTool(server, client) {
29
+ server.tool("sync_project", "Sync an entire FlutterFlow project to the local cache. Downloads all YAML files (bulk or batched fallback) for fast offline reads. Use force=true to re-sync an already cached project.", {
30
+ projectId: z.string().describe("The FlutterFlow project ID"),
31
+ force: z
32
+ .boolean()
33
+ .optional()
34
+ .describe("Force re-sync even if cache already exists (default: false)"),
35
+ }, async ({ projectId, force }) => {
36
+ // Check existing cache unless force is set
37
+ if (!force) {
38
+ const existing = await cacheMeta(projectId);
39
+ if (existing) {
40
+ return {
41
+ content: [
42
+ {
43
+ type: "text",
44
+ text: JSON.stringify({
45
+ status: "already_cached",
46
+ message: "Project is already cached. Pass force=true to re-sync.",
47
+ cachedAt: existing.lastSyncedAt,
48
+ fileCount: existing.fileCount,
49
+ syncMethod: existing.syncMethod,
50
+ }, null, 2),
51
+ },
52
+ ],
53
+ };
54
+ }
55
+ }
56
+ // Primary path: bulk fetch entire project as one ZIP
57
+ try {
58
+ const raw = await client.getProjectYamls(projectId);
59
+ const decoded = decodeProjectYamlResponse(raw);
60
+ // ZIP entry names include .yaml extension, but cacheWrite adds it too.
61
+ // Strip .yaml from keys to avoid double extension (.yaml.yaml).
62
+ const normalized = {};
63
+ for (const [key, content] of Object.entries(decoded)) {
64
+ const cleanKey = key.endsWith(".yaml") ? key.slice(0, -".yaml".length) : key;
65
+ normalized[cleanKey] = content;
66
+ }
67
+ const syncedFiles = await cacheWriteBulk(projectId, normalized);
68
+ const meta = {
69
+ lastSyncedAt: new Date().toISOString(),
70
+ fileCount: syncedFiles,
71
+ syncMethod: "bulk",
72
+ };
73
+ await cacheWriteMeta(projectId, meta);
74
+ return {
75
+ content: [
76
+ {
77
+ type: "text",
78
+ text: JSON.stringify({
79
+ status: "synced",
80
+ syncedFiles,
81
+ failed: 0,
82
+ method: "bulk",
83
+ cachedAt: meta.lastSyncedAt,
84
+ }, null, 2),
85
+ },
86
+ ],
87
+ };
88
+ }
89
+ catch (err) {
90
+ // Bulk fetch failed — log and fall through to batched approach
91
+ console.error(`[sync_project] Bulk fetch failed, falling back to batched:`, err instanceof Error ? err.message : err);
92
+ }
93
+ // Fallback path: list all file keys, then batch-fetch 5 at a time
94
+ const fileNamesRaw = await client.listPartitionedFileNames(projectId);
95
+ const allKeys = extractTopLevelFileKeys(fileNamesRaw);
96
+ if (allKeys.length === 0) {
97
+ return {
98
+ content: [
99
+ {
100
+ type: "text",
101
+ text: JSON.stringify({
102
+ status: "error",
103
+ message: "No file keys returned by listPartitionedFileNames. " +
104
+ "Check the projectId.",
105
+ }, null, 2),
106
+ },
107
+ ],
108
+ };
109
+ }
110
+ let syncedFiles = 0;
111
+ let failed = 0;
112
+ const results = await batchProcess(allKeys, 5, (fileKey) => fetchOneFile(client, projectId, fileKey));
113
+ for (let i = 0; i < allKeys.length; i++) {
114
+ const result = results[i];
115
+ if (result.status === "fulfilled" && result.value) {
116
+ await cacheWrite(projectId, allKeys[i], result.value.content);
117
+ syncedFiles++;
118
+ }
119
+ else {
120
+ failed++;
121
+ }
122
+ }
123
+ const meta = {
124
+ lastSyncedAt: new Date().toISOString(),
125
+ fileCount: syncedFiles,
126
+ syncMethod: "batched",
127
+ };
128
+ await cacheWriteMeta(projectId, meta);
129
+ return {
130
+ content: [
131
+ {
132
+ type: "text",
133
+ text: JSON.stringify({
134
+ status: "synced",
135
+ syncedFiles,
136
+ failed,
137
+ method: "batched",
138
+ cachedAt: meta.lastSyncedAt,
139
+ }, null, 2),
140
+ },
141
+ ],
142
+ };
143
+ });
144
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { FlutterFlowClient } from "../api/flutterflow.js";
3
+ export declare function registerUpdateYamlTool(server: McpServer, client: FlutterFlowClient): void;
@@ -0,0 +1,24 @@
1
+ import { z } from "zod";
2
+ import { cacheWrite } from "../utils/cache.js";
3
+ export function registerUpdateYamlTool(server, client) {
4
+ server.tool("update_project_yaml", "Push YAML changes to a FlutterFlow project. IMPORTANT: Always call validate_yaml first to check for errors before updating.", {
5
+ projectId: z.string().describe("The FlutterFlow project ID"),
6
+ fileKeyToContent: z
7
+ .record(z.string(), z.string())
8
+ .describe("Map of file keys to YAML content. Pass each value as a normal multi-line YAML string."),
9
+ }, async ({ projectId, fileKeyToContent }) => {
10
+ const result = await client.updateProjectByYaml(projectId, fileKeyToContent);
11
+ // Update cache with the pushed content
12
+ for (const [fileKey, content] of Object.entries(fileKeyToContent)) {
13
+ await cacheWrite(projectId, fileKey, content);
14
+ }
15
+ return {
16
+ content: [
17
+ {
18
+ type: "text",
19
+ text: JSON.stringify(result, null, 2),
20
+ },
21
+ ],
22
+ };
23
+ });
24
+ }
@@ -0,0 +1,3 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { FlutterFlowClient } from "../api/flutterflow.js";
3
+ export declare function registerValidateYamlTool(server: McpServer, client: FlutterFlowClient): void;
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ export function registerValidateYamlTool(server, client) {
3
+ server.tool("validate_yaml", "Validate YAML content before pushing changes to a FlutterFlow project. Always call this before update_project_yaml.", {
4
+ projectId: z.string().describe("The FlutterFlow project ID"),
5
+ fileKey: z
6
+ .string()
7
+ .describe("The YAML file key (e.g. 'app-details', 'page/id-xxx')"),
8
+ fileContent: z
9
+ .string()
10
+ .describe("Pass YAML content as a normal multi-line string."),
11
+ }, async ({ projectId, fileKey, fileContent }) => {
12
+ const result = await client.validateProjectYaml(projectId, fileKey, fileContent);
13
+ return {
14
+ content: [
15
+ {
16
+ type: "text",
17
+ text: JSON.stringify(result, null, 2),
18
+ },
19
+ ],
20
+ };
21
+ });
22
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Metadata stored alongside the cached YAML files for a project.
3
+ */
4
+ export interface CacheMeta {
5
+ lastSyncedAt: string;
6
+ fileCount: number;
7
+ syncMethod: "bulk" | "batched";
8
+ }
9
+ /**
10
+ * Resolve the cache directory for a given project.
11
+ * Returns an absolute path: `<projectRoot>/.ff-cache/<projectId>/`
12
+ */
13
+ export declare function cacheDir(projectId: string): string;
14
+ /**
15
+ * Read a cached YAML string for a given file key.
16
+ * Returns `null` if the file does not exist.
17
+ */
18
+ export declare function cacheRead(projectId: string, fileKey: string): Promise<string | null>;
19
+ /**
20
+ * Write a single YAML string to the cache, creating directories as needed.
21
+ */
22
+ export declare function cacheWrite(projectId: string, fileKey: string, content: string): Promise<void>;
23
+ /**
24
+ * Delete a single cached file. No-op if the file does not exist.
25
+ */
26
+ export declare function cacheInvalidate(projectId: string, fileKey: string): Promise<void>;
27
+ /**
28
+ * Write multiple YAML entries to the cache in one call.
29
+ * Returns the number of entries written.
30
+ */
31
+ export declare function cacheWriteBulk(projectId: string, entries: Record<string, string>): Promise<number>;
32
+ /**
33
+ * Read the `_meta.json` file for a project cache.
34
+ * Returns `null` if it does not exist.
35
+ */
36
+ export declare function cacheMeta(projectId: string): Promise<CacheMeta | null>;
37
+ /**
38
+ * Write (or overwrite) the `_meta.json` file for a project cache.
39
+ */
40
+ export declare function cacheWriteMeta(projectId: string, meta: CacheMeta): Promise<void>;
41
+ /**
42
+ * Walk the cache directory for a project and return all cached file keys,
43
+ * optionally filtered by a prefix (e.g. `"page/"` to list only pages).
44
+ *
45
+ * File keys are reconstructed by stripping the `.yaml` extension and making
46
+ * the path relative to the project cache dir.
47
+ */
48
+ export declare function listCachedKeys(projectId: string, prefix?: string): Promise<string[]>;
@@ -0,0 +1,162 @@
1
+ import { mkdir, readFile, writeFile, unlink, readdir } from "node:fs/promises";
2
+ import { join, dirname, resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ // ---------------------------------------------------------------------------
5
+ // Project root resolution
6
+ // ---------------------------------------------------------------------------
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ /** Project root: two levels up from src/utils/ */
9
+ const PROJECT_ROOT = resolve(__dirname, "..", "..");
10
+ const CACHE_DIR_NAME = ".ff-cache";
11
+ const META_FILE = "_meta.json";
12
+ // ---------------------------------------------------------------------------
13
+ // Helpers
14
+ // ---------------------------------------------------------------------------
15
+ /**
16
+ * Resolve the cache directory for a given project.
17
+ * Returns an absolute path: `<projectRoot>/.ff-cache/<projectId>/`
18
+ */
19
+ export function cacheDir(projectId) {
20
+ return join(PROJECT_ROOT, CACHE_DIR_NAME, projectId);
21
+ }
22
+ /**
23
+ * Convert a FlutterFlow file key to a cache file path.
24
+ * `page/id-Scaffold_xxx` -> `.ff-cache/{pid}/page/id-Scaffold_xxx.yaml`
25
+ */
26
+ function keyToPath(projectId, fileKey) {
27
+ return join(cacheDir(projectId), `${fileKey}.yaml`);
28
+ }
29
+ /**
30
+ * Ensure a directory exists (recursive mkdir, no-op if already there).
31
+ */
32
+ async function ensureDir(dir) {
33
+ await mkdir(dir, { recursive: true });
34
+ }
35
+ // ---------------------------------------------------------------------------
36
+ // Public API
37
+ // ---------------------------------------------------------------------------
38
+ /**
39
+ * Read a cached YAML string for a given file key.
40
+ * Returns `null` if the file does not exist.
41
+ */
42
+ export async function cacheRead(projectId, fileKey) {
43
+ try {
44
+ const content = await readFile(keyToPath(projectId, fileKey), "utf-8");
45
+ return content;
46
+ }
47
+ catch (err) {
48
+ if (isNodeError(err) && err.code === "ENOENT") {
49
+ return null;
50
+ }
51
+ throw err;
52
+ }
53
+ }
54
+ /**
55
+ * Write a single YAML string to the cache, creating directories as needed.
56
+ */
57
+ export async function cacheWrite(projectId, fileKey, content) {
58
+ const filePath = keyToPath(projectId, fileKey);
59
+ await ensureDir(dirname(filePath));
60
+ await writeFile(filePath, content, "utf-8");
61
+ }
62
+ /**
63
+ * Delete a single cached file. No-op if the file does not exist.
64
+ */
65
+ export async function cacheInvalidate(projectId, fileKey) {
66
+ try {
67
+ await unlink(keyToPath(projectId, fileKey));
68
+ }
69
+ catch (err) {
70
+ if (isNodeError(err) && err.code === "ENOENT") {
71
+ return; // already gone — no-op
72
+ }
73
+ throw err;
74
+ }
75
+ }
76
+ /**
77
+ * Write multiple YAML entries to the cache in one call.
78
+ * Returns the number of entries written.
79
+ */
80
+ export async function cacheWriteBulk(projectId, entries) {
81
+ const keys = Object.keys(entries);
82
+ await Promise.all(keys.map((key) => cacheWrite(projectId, key, entries[key])));
83
+ return keys.length;
84
+ }
85
+ /**
86
+ * Read the `_meta.json` file for a project cache.
87
+ * Returns `null` if it does not exist.
88
+ */
89
+ export async function cacheMeta(projectId) {
90
+ const metaPath = join(cacheDir(projectId), META_FILE);
91
+ try {
92
+ const raw = await readFile(metaPath, "utf-8");
93
+ return JSON.parse(raw);
94
+ }
95
+ catch (err) {
96
+ if (isNodeError(err) && err.code === "ENOENT") {
97
+ return null;
98
+ }
99
+ throw err;
100
+ }
101
+ }
102
+ /**
103
+ * Write (or overwrite) the `_meta.json` file for a project cache.
104
+ */
105
+ export async function cacheWriteMeta(projectId, meta) {
106
+ const dir = cacheDir(projectId);
107
+ await ensureDir(dir);
108
+ await writeFile(join(dir, META_FILE), JSON.stringify(meta, null, 2), "utf-8");
109
+ }
110
+ /**
111
+ * Walk the cache directory for a project and return all cached file keys,
112
+ * optionally filtered by a prefix (e.g. `"page/"` to list only pages).
113
+ *
114
+ * File keys are reconstructed by stripping the `.yaml` extension and making
115
+ * the path relative to the project cache dir.
116
+ */
117
+ export async function listCachedKeys(projectId, prefix) {
118
+ const root = cacheDir(projectId);
119
+ const keys = [];
120
+ try {
121
+ await walkDir(root, root, keys, ".yaml");
122
+ }
123
+ catch (err) {
124
+ if (isNodeError(err) && err.code === "ENOENT") {
125
+ return []; // cache dir doesn't exist yet
126
+ }
127
+ throw err;
128
+ }
129
+ const yamlKeys = keys.map((k) => k.slice(0, -".yaml".length));
130
+ if (prefix) {
131
+ return yamlKeys.filter((k) => k.startsWith(prefix));
132
+ }
133
+ return yamlKeys;
134
+ }
135
+ // ---------------------------------------------------------------------------
136
+ // Internal helpers
137
+ // ---------------------------------------------------------------------------
138
+ /**
139
+ * Recursively walk a directory, collecting relative paths of files.
140
+ */
141
+ async function walkDir(base, dir, out, ext) {
142
+ const entries = await readdir(dir, { withFileTypes: true });
143
+ for (const entry of entries) {
144
+ const full = join(dir, entry.name);
145
+ if (entry.isDirectory()) {
146
+ await walkDir(base, full, out, ext);
147
+ }
148
+ else if (entry.isFile()) {
149
+ if (ext && !entry.name.endsWith(ext))
150
+ continue;
151
+ // Relative path from cache root, using forward slashes for consistency
152
+ const rel = full.slice(base.length + 1).split("\\").join("/");
153
+ out.push(rel);
154
+ }
155
+ }
156
+ }
157
+ /**
158
+ * Type guard for Node.js system errors (which carry a `code` property).
159
+ */
160
+ function isNodeError(err) {
161
+ return err instanceof Error && "code" in err;
162
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Decode the base64-encoded ZIP returned by the FlutterFlow projectYamls API
3
+ * into a map of filename → YAML text.
4
+ *
5
+ * The API response shape is: { value: { projectYamlBytes: "<base64>" } }
6
+ */
7
+ export declare function decodeProjectYamlResponse(apiResponse: unknown): Record<string, string>;
@@ -0,0 +1,31 @@
1
+ import AdmZip from "adm-zip";
2
+ /**
3
+ * Decode the base64-encoded ZIP returned by the FlutterFlow projectYamls API
4
+ * into a map of filename → YAML text.
5
+ *
6
+ * The API response shape is: { value: { projectYamlBytes: "<base64>" } }
7
+ */
8
+ export function decodeProjectYamlResponse(apiResponse) {
9
+ const resp = apiResponse;
10
+ // API docs use camelCase (projectYamlBytes), but handle snake_case fallback
11
+ const b64 = resp?.value?.projectYamlBytes ?? resp?.value?.project_yaml_bytes;
12
+ if (!b64) {
13
+ throw new Error("Unexpected API response: missing value.projectYamlBytes");
14
+ }
15
+ const zipBuffer = Buffer.from(b64, "base64");
16
+ const zip = new AdmZip(zipBuffer);
17
+ const entries = zip.getEntries();
18
+ const result = {};
19
+ for (const entry of entries) {
20
+ if (!entry.isDirectory) {
21
+ try {
22
+ result[entry.entryName] = entry.getData().toString("utf-8");
23
+ }
24
+ catch {
25
+ // Skip entries that fail to decompress (e.g. buffer overflow on large pages)
26
+ console.error(`[decode-yaml] Failed to decompress entry: ${entry.entryName}`);
27
+ }
28
+ }
29
+ }
30
+ return result;
31
+ }
@@ -0,0 +1,9 @@
1
+ import { TriggerSummary } from "./types.js";
2
+ /**
3
+ * Read all trigger_actions for a node and return summaries.
4
+ *
5
+ * @param projectId - FF project ID
6
+ * @param nodeFileKeyPrefix - Cache prefix for the node's trigger actions,
7
+ * e.g. "page/id-Scaffold_xxx/page-widget-tree-outline/node/id-Button_yyy"
8
+ */
9
+ export declare function summarizeTriggers(projectId: string, nodeFileKeyPrefix: string): Promise<TriggerSummary[]>;