unframer 4.0.4 → 4.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.
- package/README.md +67 -0
- package/dist/cli-readonly-server-api.test.d.ts +2 -0
- package/dist/cli-readonly-server-api.test.d.ts.map +1 -0
- package/dist/cli-readonly-server-api.test.js +96 -0
- package/dist/cli-readonly-server-api.test.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +242 -64
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +18 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/{lib/config.js → config.js} +1 -0
- package/dist/config.js.map +1 -0
- package/dist/esbuild.d.ts +5 -1
- package/dist/esbuild.d.ts.map +1 -1
- package/dist/esbuild.js +2 -1
- package/dist/esbuild.js.map +1 -1
- package/dist/example-code.test.js +53 -53
- package/dist/example-code.test.js.map +1 -1
- package/dist/framer-client.server.d.ts +4 -0
- package/dist/framer-client.server.d.ts.map +1 -0
- package/dist/framer-client.server.js +45 -0
- package/dist/framer-client.server.js.map +1 -0
- package/dist/framer.js +9269 -6630
- package/dist/plugin-mcp-dist/lib/client-websocket.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/client-websocket.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/client-websocket.js +103 -0
- package/dist/plugin-mcp-dist/lib/client-websocket.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/cms.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/cms.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/cms.js +58 -0
- package/dist/plugin-mcp-dist/lib/cms.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/errors.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/errors.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/errors.js +48 -0
- package/dist/plugin-mcp-dist/lib/errors.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/framer-client.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/framer-client.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/framer-client.js +4 -0
- package/dist/plugin-mcp-dist/lib/framer-client.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.js +4 -0
- package/dist/plugin-mcp-dist/lib/framer-client.server.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/framer.d.ts +36 -0
- package/dist/plugin-mcp-dist/lib/framer.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/framer.js +1000 -0
- package/dist/plugin-mcp-dist/lib/framer.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/hooks.d.ts +6 -0
- package/dist/plugin-mcp-dist/lib/hooks.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/hooks.js +46 -0
- package/dist/plugin-mcp-dist/lib/hooks.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp-client.d.ts +141 -0
- package/dist/plugin-mcp-dist/lib/mcp-client.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp-client.js +40 -0
- package/dist/plugin-mcp-dist/lib/mcp-client.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts +378 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js +1807 -0
- package/dist/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp-tools.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/mcp-tools.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp-tools.js +5 -0
- package/dist/plugin-mcp-dist/lib/mcp-tools.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp-websocket.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/mcp-websocket.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp-websocket.js +88 -0
- package/dist/plugin-mcp-dist/lib/mcp-websocket.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp.test.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/mcp.test.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/mcp.test.js +1315 -0
- package/dist/plugin-mcp-dist/lib/mcp.test.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts +5 -0
- package/dist/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js +168 -0
- package/dist/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/react-export.d.ts +51 -0
- package/dist/plugin-mcp-dist/lib/react-export.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/react-export.js +340 -0
- package/dist/plugin-mcp-dist/lib/react-export.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/schema.d.ts +261 -0
- package/dist/plugin-mcp-dist/lib/schema.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/schema.js +871 -0
- package/dist/plugin-mcp-dist/lib/schema.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/store.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/store.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/store.js +8 -0
- package/dist/plugin-mcp-dist/lib/store.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/tree-utils.d.ts +6 -0
- package/dist/plugin-mcp-dist/lib/tree-utils.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/tree-utils.js +141 -0
- package/dist/plugin-mcp-dist/lib/tree-utils.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/types.d.ts +110 -0
- package/dist/plugin-mcp-dist/lib/types.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/types.js +188 -0
- package/dist/plugin-mcp-dist/lib/types.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/utils.d.ts +21 -0
- package/dist/plugin-mcp-dist/lib/utils.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/utils.js +53 -0
- package/dist/plugin-mcp-dist/lib/utils.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/websocket-server.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/websocket-server.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/websocket-server.js +88 -0
- package/dist/plugin-mcp-dist/lib/websocket-server.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/xml.d.ts +10 -0
- package/dist/plugin-mcp-dist/lib/xml.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/xml.js +395 -0
- package/dist/plugin-mcp-dist/lib/xml.js.map +1 -0
- package/dist/plugin-mcp-dist/lib/xml.test.d.ts +2 -0
- package/dist/plugin-mcp-dist/lib/xml.test.d.ts.map +1 -0
- package/dist/plugin-mcp-dist/lib/xml.test.js +1030 -0
- package/dist/plugin-mcp-dist/lib/xml.test.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +13 -3
- package/src/cli-readonly-server-api.test.ts +152 -0
- package/src/cli.ts +315 -69
- package/src/{lib/config.ts → config.ts} +11 -9
- package/src/esbuild.ts +2 -0
- package/src/example-code.test.ts +53 -53
- package/src/framer-chunks/chunk-2JNOE5PX.js +51 -0
- package/src/framer-chunks/chunk-76VXR6QG.js +91 -0
- package/src/framer-chunks/chunk-A2PMVMFI.js +91 -0
- package/src/framer-chunks/chunk-DBJCHRFG.js +105 -0
- package/src/framer-chunks/chunk-H7SXYDQJ.js +68 -0
- package/src/framer-chunks/chunk-OAKBJJLO.js +105 -0
- package/src/framer-chunks/chunk-VUHWYTYT.js +105 -0
- package/src/framer-client.server.ts +97 -0
- package/src/framer.js +9269 -6630
- package/src/plugin-mcp-dist/lib/client-websocket.d.ts +6 -0
- package/src/plugin-mcp-dist/lib/client-websocket.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/client-websocket.js +102 -0
- package/src/plugin-mcp-dist/lib/client-websocket.js.map +1 -0
- package/src/plugin-mcp-dist/lib/cms.d.ts +7 -0
- package/src/plugin-mcp-dist/lib/cms.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/cms.js +57 -0
- package/src/plugin-mcp-dist/lib/cms.js.map +1 -0
- package/src/plugin-mcp-dist/lib/errors.d.ts +5 -0
- package/src/plugin-mcp-dist/lib/errors.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/errors.js +47 -0
- package/src/plugin-mcp-dist/lib/errors.js.map +1 -0
- package/src/plugin-mcp-dist/lib/framer-client.d.ts +2 -0
- package/src/plugin-mcp-dist/lib/framer-client.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/framer-client.js +3 -0
- package/src/plugin-mcp-dist/lib/framer-client.js.map +1 -0
- package/src/plugin-mcp-dist/lib/framer-client.server.d.ts +2 -0
- package/src/plugin-mcp-dist/lib/framer-client.server.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/framer-client.server.js +3 -0
- package/src/plugin-mcp-dist/lib/framer-client.server.js.map +1 -0
- package/src/plugin-mcp-dist/lib/framer.d.ts +69 -0
- package/src/plugin-mcp-dist/lib/framer.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/framer.js +999 -0
- package/src/plugin-mcp-dist/lib/framer.js.map +1 -0
- package/src/plugin-mcp-dist/lib/hooks.d.ts +6 -0
- package/src/plugin-mcp-dist/lib/hooks.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/hooks.js +45 -0
- package/src/plugin-mcp-dist/lib/hooks.js.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp-client.d.ts +50 -0
- package/src/plugin-mcp-dist/lib/mcp-client.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp-client.js +39 -0
- package/src/plugin-mcp-dist/lib/mcp-client.js.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts +375 -0
- package/src/plugin-mcp-dist/lib/mcp-handlers.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp-handlers.js +1806 -0
- package/src/plugin-mcp-dist/lib/mcp-handlers.js.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp-tools.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp-tools.js +4 -0
- package/src/plugin-mcp-dist/lib/mcp-tools.js.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp-websocket.d.ts +21 -0
- package/src/plugin-mcp-dist/lib/mcp-websocket.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp-websocket.js +87 -0
- package/src/plugin-mcp-dist/lib/mcp-websocket.js.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp.test.d.ts +2 -0
- package/src/plugin-mcp-dist/lib/mcp.test.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/mcp.test.js +1314 -0
- package/src/plugin-mcp-dist/lib/mcp.test.js.map +1 -0
- package/src/plugin-mcp-dist/lib/plugin-websocket.d.ts +6 -0
- package/src/plugin-mcp-dist/lib/plugin-websocket.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/plugin-websocket.js +167 -0
- package/src/plugin-mcp-dist/lib/plugin-websocket.js.map +1 -0
- package/src/plugin-mcp-dist/lib/react-export.d.ts +101 -0
- package/src/plugin-mcp-dist/lib/react-export.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/react-export.js +339 -0
- package/src/plugin-mcp-dist/lib/react-export.js.map +1 -0
- package/src/plugin-mcp-dist/lib/schema.d.ts +358 -0
- package/src/plugin-mcp-dist/lib/schema.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/schema.js +870 -0
- package/src/plugin-mcp-dist/lib/schema.js.map +1 -0
- package/src/plugin-mcp-dist/lib/snapshots/add-frame-to-section.patch +1 -0
- package/src/plugin-mcp-dist/lib/snapshots/code-file-insert-info.md +1 -0
- package/src/plugin-mcp-dist/lib/snapshots/component-insert-info.md +1 -0
- package/src/plugin-mcp-dist/lib/snapshots/component.html +1 -0
- package/src/plugin-mcp-dist/lib/snapshots/create-nodes-with-layout.patch +1 -0
- package/src/plugin-mcp-dist/lib/snapshots/page.html +1 -0
- package/src/plugin-mcp-dist/lib/snapshots/project.html +1 -0
- package/src/plugin-mcp-dist/lib/snapshots/tools-schema.yaml +908 -0
- package/src/plugin-mcp-dist/lib/snapshots/tools.jsonc +906 -0
- package/src/plugin-mcp-dist/lib/store.d.ts +8 -0
- package/src/plugin-mcp-dist/lib/store.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/store.js +7 -0
- package/src/plugin-mcp-dist/lib/store.js.map +1 -0
- package/src/plugin-mcp-dist/lib/tree-utils.d.ts +18 -0
- package/src/plugin-mcp-dist/lib/tree-utils.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/tree-utils.js +140 -0
- package/src/plugin-mcp-dist/lib/tree-utils.js.map +1 -0
- package/src/plugin-mcp-dist/lib/types.d.ts +139 -0
- package/src/plugin-mcp-dist/lib/types.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/types.js +187 -0
- package/src/plugin-mcp-dist/lib/types.js.map +1 -0
- package/src/plugin-mcp-dist/lib/utils.d.ts +23 -0
- package/src/plugin-mcp-dist/lib/utils.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/utils.js +52 -0
- package/src/plugin-mcp-dist/lib/utils.js.map +1 -0
- package/src/plugin-mcp-dist/lib/websocket-server.d.ts +21 -0
- package/src/plugin-mcp-dist/lib/websocket-server.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/websocket-server.js +87 -0
- package/src/plugin-mcp-dist/lib/websocket-server.js.map +1 -0
- package/src/plugin-mcp-dist/lib/xml.d.ts +33 -0
- package/src/plugin-mcp-dist/lib/xml.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/xml.js +394 -0
- package/src/plugin-mcp-dist/lib/xml.js.map +1 -0
- package/src/plugin-mcp-dist/lib/xml.test.d.ts +2 -0
- package/src/plugin-mcp-dist/lib/xml.test.d.ts.map +1 -0
- package/src/plugin-mcp-dist/lib/xml.test.js +1029 -0
- package/src/plugin-mcp-dist/lib/xml.test.js.map +1 -0
- package/src/styles/framer.css +7 -7
- package/src/version.ts +1 -1
- package/dist/lib/config.d.ts +0 -17
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/mcp-to-cli.d.ts +0 -25
- package/dist/lib/mcp-to-cli.d.ts.map +0 -1
- package/dist/lib/mcp-to-cli.js +0 -176
- package/dist/lib/mcp-to-cli.js.map +0 -1
- package/src/lib/mcp-to-cli.ts +0 -229
package/README.md
CHANGED
|
@@ -314,3 +314,70 @@ Here is the below landing page Lighthouse score when using Astro:
|
|
|
314
314
|
## Example
|
|
315
315
|
|
|
316
316
|
Look at the [nextjs-app source code folder](./nextjs-app) for an example and [the deployed website here](https://unframer-nextjs-app.vercel.app/)
|
|
317
|
+
|
|
318
|
+
## MCP CLI Commands
|
|
319
|
+
|
|
320
|
+
Unframer can act as a command-line client for the [Framer MCP plugin](https://www.framer.com/marketplace/plugins/mcp/), allowing you to interact with your Framer project directly from the terminal.
|
|
321
|
+
|
|
322
|
+
### Setup
|
|
323
|
+
|
|
324
|
+
```sh
|
|
325
|
+
# Login with your MCP URL (get it from the Framer MCP plugin)
|
|
326
|
+
npx unframer mcp login
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
After login, all MCP commands become available. Run `npx unframer --help` to see the full list.
|
|
330
|
+
|
|
331
|
+
### Example Commands
|
|
332
|
+
|
|
333
|
+
```sh
|
|
334
|
+
# Get project structure as XML
|
|
335
|
+
npx unframer mcp getProjectXml
|
|
336
|
+
|
|
337
|
+
# Get a specific node's XML by ID
|
|
338
|
+
npx unframer mcp getNodeXml --nodeId "abc123"
|
|
339
|
+
|
|
340
|
+
# Update node text or attributes
|
|
341
|
+
npx unframer mcp updateXmlForNode --nodeId "abc123" --xml '<Text nodeId="abc123">New text</Text>'
|
|
342
|
+
|
|
343
|
+
# Search for fonts
|
|
344
|
+
npx unframer mcp searchFonts --query "Inter"
|
|
345
|
+
|
|
346
|
+
# Export React components
|
|
347
|
+
npx unframer mcp exportReactComponents
|
|
348
|
+
|
|
349
|
+
# Get published website URL
|
|
350
|
+
npx unframer mcp getProjectWebsiteUrl
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### CMS Operations
|
|
354
|
+
|
|
355
|
+
```sh
|
|
356
|
+
# List all CMS collections
|
|
357
|
+
npx unframer mcp getCMSCollections
|
|
358
|
+
|
|
359
|
+
# Get items from a collection
|
|
360
|
+
npx unframer mcp getCMSItems --collectionId "col123"
|
|
361
|
+
|
|
362
|
+
# Create or update a CMS item
|
|
363
|
+
npx unframer mcp upsertCMSItem --collectionId "col123" --slug "my-post" --fieldData '{"title": "Hello"}'
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Code Files
|
|
367
|
+
|
|
368
|
+
```sh
|
|
369
|
+
# Create a new code component
|
|
370
|
+
npx unframer mcp createCodeFile --name "MyComponent.tsx" --content "export default function MyComponent() { return <div>Hello</div> }"
|
|
371
|
+
|
|
372
|
+
# Read existing code file
|
|
373
|
+
npx unframer mcp readCodeFile --codeFileId "code123"
|
|
374
|
+
|
|
375
|
+
# Update code file content
|
|
376
|
+
npx unframer mcp updateCodeFile --codeFileId "code123" --content "// updated code"
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Run any command with `--help` for detailed options:
|
|
380
|
+
|
|
381
|
+
```sh
|
|
382
|
+
npx unframer mcp updateXmlForNode --help
|
|
383
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-readonly-server-api.test.d.ts","sourceRoot":"","sources":["../src/cli-readonly-server-api.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Read-only integration checks for server-api CLI commands and reconnect latency.
|
|
2
|
+
import childProcess from 'node:child_process';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import util from 'node:util';
|
|
5
|
+
import { expect, test } from 'vitest';
|
|
6
|
+
const execFile = util.promisify(childProcess.execFile);
|
|
7
|
+
const defaultProjectUrl = 'https://framer.com/projects/Framer-MCP-project-Designor-Framer-Template-copy--lfAw10qcrLpLLEznmZmo-irrP1?node=CpFAHygNJ';
|
|
8
|
+
const hasApiKey = Boolean(process.env.FRAMER_API_KEY);
|
|
9
|
+
function sleep({ ms }) {
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
setTimeout(() => {
|
|
12
|
+
resolve();
|
|
13
|
+
}, ms);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
async function runServerApiCliReadOnly({ args, timeoutMs = 180_000, }) {
|
|
17
|
+
const projectUrl = process.env.FRAMER_PROJECT_URL || defaultProjectUrl;
|
|
18
|
+
const startMs = performance.now();
|
|
19
|
+
const { stdout, stderr } = await execFile('pnpm', ['tsx', 'src/bin.ts', ...args, '--project', projectUrl], {
|
|
20
|
+
cwd: process.cwd(),
|
|
21
|
+
env: process.env,
|
|
22
|
+
timeout: timeoutMs,
|
|
23
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
24
|
+
});
|
|
25
|
+
return {
|
|
26
|
+
stdout,
|
|
27
|
+
stderr,
|
|
28
|
+
durationMs: performance.now() - startMs,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
async function runServerApiCliReadOnlyWithRetry({ args, timeoutMs = 180_000, attempt = 1, maxAttempts = 3, }) {
|
|
32
|
+
try {
|
|
33
|
+
return await runServerApiCliReadOnly({ args, timeoutMs });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
if (attempt >= maxAttempts) {
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
await sleep({ ms: 1_000 * attempt });
|
|
40
|
+
return runServerApiCliReadOnlyWithRetry({
|
|
41
|
+
args,
|
|
42
|
+
timeoutMs,
|
|
43
|
+
attempt: attempt + 1,
|
|
44
|
+
maxAttempts,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function extractJsonFromCliOutput({ output }) {
|
|
49
|
+
const jsonStart = output.indexOf('{');
|
|
50
|
+
if (jsonStart < 0) {
|
|
51
|
+
throw new Error(`Could not find JSON in CLI output:\n${output}`);
|
|
52
|
+
}
|
|
53
|
+
return JSON.parse(output.slice(jsonStart));
|
|
54
|
+
}
|
|
55
|
+
test.skipIf(!hasApiKey)('server-api read-only cli commands return expected output', async () => {
|
|
56
|
+
const websiteRun = await runServerApiCliReadOnly({
|
|
57
|
+
args: ['mcp', 'getProjectWebsiteUrl'],
|
|
58
|
+
});
|
|
59
|
+
const website = extractJsonFromCliOutput({ output: websiteRun.stdout });
|
|
60
|
+
expect(Object.keys(website).length).toBeGreaterThan(0);
|
|
61
|
+
const websiteJson = JSON.stringify(website);
|
|
62
|
+
expect(websiteJson.includes('https://')).toBe(true);
|
|
63
|
+
expect(websiteRun.stderr.includes('Running getProjectWebsiteUrl')).toBe(true);
|
|
64
|
+
const xmlRun = await runServerApiCliReadOnly({
|
|
65
|
+
args: ['mcp', 'getProjectXml'],
|
|
66
|
+
});
|
|
67
|
+
expect(xmlRun.stdout.includes('# Project structure:')).toBe(true);
|
|
68
|
+
expect(xmlRun.stdout.includes('<Project>')).toBe(true);
|
|
69
|
+
expect(xmlRun.stderr.includes('Running getProjectXml')).toBe(true);
|
|
70
|
+
const selectedNodesRun = await runServerApiCliReadOnly({
|
|
71
|
+
args: ['mcp', 'getSelectedNodesXml'],
|
|
72
|
+
});
|
|
73
|
+
expect(selectedNodesRun.stdout.includes('No nodes are currently selected.')).toBe(true);
|
|
74
|
+
const focusedNodeMatch = xmlRun.stdout.match(/currently focused [^`]* ID is: `([^`]+)`/);
|
|
75
|
+
const focusedNodeId = focusedNodeMatch?.[1];
|
|
76
|
+
expect(Boolean(focusedNodeId)).toBe(true);
|
|
77
|
+
const zoomRun = await runServerApiCliReadOnly({
|
|
78
|
+
args: ['mcp', 'zoomIntoView', '--nodeId', focusedNodeId || ''],
|
|
79
|
+
});
|
|
80
|
+
expect(zoomRun.stdout.includes('Zoomed into view for node')).toBe(true);
|
|
81
|
+
}, 180_000);
|
|
82
|
+
test.skipIf(!hasApiKey)('server-api reconnects cleanly for repeated read-only calls', async () => {
|
|
83
|
+
const firstRun = await runServerApiCliReadOnly({
|
|
84
|
+
args: ['mcp', 'getProjectWebsiteUrl'],
|
|
85
|
+
});
|
|
86
|
+
const secondRun = await runServerApiCliReadOnlyWithRetry({
|
|
87
|
+
args: ['mcp', 'getProjectWebsiteUrl'],
|
|
88
|
+
});
|
|
89
|
+
expect(firstRun.durationMs).toBeGreaterThan(0);
|
|
90
|
+
expect(secondRun.durationMs).toBeGreaterThan(0);
|
|
91
|
+
expect(firstRun.durationMs).toBeLessThan(120_000);
|
|
92
|
+
expect(secondRun.durationMs).toBeLessThan(120_000);
|
|
93
|
+
const reconnectDeltaMs = secondRun.durationMs - firstRun.durationMs;
|
|
94
|
+
console.info(`getProjectWebsiteUrl latency ms: first=${firstRun.durationMs.toFixed(0)}, second=${secondRun.durationMs.toFixed(0)}, delta=${reconnectDeltaMs.toFixed(0)}`);
|
|
95
|
+
}, 240_000);
|
|
96
|
+
//# sourceMappingURL=cli-readonly-server-api.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-readonly-server-api.test.js","sourceRoot":"","sources":["../src/cli-readonly-server-api.test.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import './sentry.js';
|
|
2
2
|
import { StyleToken } from './exporter.js';
|
|
3
3
|
import { BreakpointSizes } from './css.js';
|
|
4
|
-
export declare const cli: import("
|
|
4
|
+
export declare const cli: import("goke").Goke;
|
|
5
5
|
export type Config = {
|
|
6
6
|
jsx?: boolean;
|
|
7
7
|
components: {
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAMA,OAAO,aAAa,CAAA;AAGpB,OAAO,EAAU,UAAU,EAA8B,MAAM,eAAe,CAAA;AAU9E,OAAO,EAAE,eAAe,EAA0B,MAAM,UAAU,CAAA;AAsBlE,eAAO,MAAM,GAAG,qBAAmB,CAAA;AAkkBnC,MAAM,MAAM,MAAM,GAAG;IACjB,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,UAAU,EAAE;QACR,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KACzB,CAAA;IACD,oBAAoB,CAAC,EAAE;QACnB,SAAS,EAAE,MAAM,CAAA;QACjB,WAAW,EAAE,MAAM,CAAA;QACnB,aAAa,EAAE,MAAM,CAAA;QACrB,cAAc,EAAE,MAAM,CAAA;QACtB,KAAK,EAAE,MAAM,CAAA;KAChB,EAAE,CAAA;IACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC3B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,cAAc,CAAC,EAAE;QACb,SAAS,EAAE,MAAM,CAAA;QACjB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;QACrB,IAAI,EAAE,MAAM,CAAA;KACf,EAAE,CAAA;IAEH,OAAO,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,EAAE,EAAE,MAAM,CAAA;QACV,IAAI,EAAE,MAAM,CAAA;QACZ,IAAI,EAAE,MAAM,CAAA;KACf,EAAE,CAAA;IACH,WAAW,CAAC,EAAE,eAAe,CAAA;IAC7B,MAAM,CAAC,EAAE,UAAU,EAAE,CAAA;IACrB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,6BAA6B,EAAE,uBAAuB,EAAE,CAAA;IACxD,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAE/B,CAAA;AAED,KAAK,uBAAuB,GAAG;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,iBAAiB,EAAE,MAAM,CAAA;IACzB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;IAEjB,SAAS,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAsB,eAAe,CAAC,EAClC,SAAS,EACT,gBAAiC,EACjC,WAAmB,EACnB,KAAU,EACV,MAAwC,EAC3C;;;;;;CAAA;;;;GAyHA"}
|
package/dist/cli.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { setMaxListeners } from 'events';
|
|
2
3
|
import pkg from '../package.json' with { type: 'json' };
|
|
3
4
|
import pico from 'picocolors';
|
|
4
5
|
const { blue, bgBlue, green } = pico;
|
|
5
6
|
import { fetch } from 'undici';
|
|
6
7
|
import './sentry.js';
|
|
7
|
-
import { input } from '@inquirer/prompts';
|
|
8
|
+
import { input, select, password } from '@inquirer/prompts';
|
|
8
9
|
import { bundle, createExampleComponentCode } from './exporter.js';
|
|
9
10
|
import { createClient } from './generated/api-client.js';
|
|
10
11
|
import { generateStackblitzFiles } from './stackblitz.js';
|
|
11
|
-
import {
|
|
12
|
+
import { goke, wrapJsonSchema } from 'goke';
|
|
12
13
|
import { exec } from 'child_process';
|
|
13
14
|
import { promisify } from 'util';
|
|
14
15
|
import fs from 'fs';
|
|
@@ -18,27 +19,22 @@ import { componentNameToPath, dedent, isTruthy, logger, sleep, spinner, } from '
|
|
|
18
19
|
import { getPackageManager } from './package-manager.js';
|
|
19
20
|
import { notifyError } from './sentry.js';
|
|
20
21
|
import { dispatcher } from './undici-dispatcher.js';
|
|
21
|
-
import { loadConfig, saveConfig, getConfigPath } from './
|
|
22
|
-
import { addMcpCommands } from '
|
|
23
|
-
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
22
|
+
import { loadConfig, saveConfig, getConfigPath, } from './config.js';
|
|
23
|
+
import { addMcpCommands } from '@goke/mcp';
|
|
24
24
|
const configNames = ['unframer.config.json', 'unframer.json'];
|
|
25
|
-
export const cli =
|
|
25
|
+
export const cli = goke('unframer');
|
|
26
26
|
let defaultOutDir = 'framer';
|
|
27
27
|
cli.command('[projectId]', 'Run unframer with optional project ID')
|
|
28
|
-
.option('--outDir <dir>', 'Output directory'
|
|
29
|
-
.option('--external [package]', 'Make some package external, do not pass a package name to make all packages external'
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.option('--
|
|
33
|
-
.option('--
|
|
34
|
-
default: true,
|
|
35
|
-
})
|
|
36
|
-
.option('--debug', 'Enable debug logging', { default: false })
|
|
37
|
-
.option('--metafile', 'Generate meta.json file with build metadata', {
|
|
38
|
-
default: false,
|
|
39
|
-
})
|
|
28
|
+
.option('--outDir <dir>', 'Output directory')
|
|
29
|
+
.option('--external [package]', 'Make some package external, do not pass a package name to make all packages external')
|
|
30
|
+
.option('--watch', 'Watch for changes and rebuild')
|
|
31
|
+
.option('--jsx', 'Output jsx code instead of minified .js code')
|
|
32
|
+
.option('--debug', 'Enable debug logging')
|
|
33
|
+
.option('--metafile', 'Generate meta.json file with build metadata')
|
|
40
34
|
.action(async function main(projectId, options) {
|
|
41
|
-
const
|
|
35
|
+
const outDir = options.outDir || defaultOutDir;
|
|
36
|
+
const jsx = options.jsx ?? true;
|
|
37
|
+
const external_ = options.external ?? true;
|
|
42
38
|
const allExternal = external_ === true;
|
|
43
39
|
const externalPackages = Array.isArray(external_)
|
|
44
40
|
? external_.filter((x) => x.trim())
|
|
@@ -49,7 +45,6 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
|
|
|
49
45
|
if (options.debug) {
|
|
50
46
|
logger.debug = true;
|
|
51
47
|
}
|
|
52
|
-
const outDir = options.outDir;
|
|
53
48
|
const controller = new AbortController();
|
|
54
49
|
const signal = controller.signal;
|
|
55
50
|
const watch = options.watch;
|
|
@@ -60,7 +55,6 @@ cli.command('[projectId]', 'Run unframer with optional project ID')
|
|
|
60
55
|
outDir,
|
|
61
56
|
projectId,
|
|
62
57
|
});
|
|
63
|
-
let jsx = options.jsx;
|
|
64
58
|
const { rebuild, buildContext } = await bundle({
|
|
65
59
|
config: {
|
|
66
60
|
jsx,
|
|
@@ -149,16 +143,14 @@ function fixOldUnframerPath() {
|
|
|
149
143
|
const version = pkg.version;
|
|
150
144
|
cli.version(version).help();
|
|
151
145
|
cli.command('example-app <projectId>', 'Create an example app with Framer components')
|
|
152
|
-
.option('--outDir <dir>', 'Output directory'
|
|
153
|
-
default: 'example-unframer-app',
|
|
154
|
-
})
|
|
146
|
+
.option('--outDir <dir>', 'Output directory')
|
|
155
147
|
.action(async (projectId, options) => {
|
|
156
148
|
if (!projectId?.trim()) {
|
|
157
149
|
console.log(`unframer example-app requires a project id positional param`);
|
|
158
150
|
process.exit(1);
|
|
159
151
|
}
|
|
160
152
|
try {
|
|
161
|
-
const outDir = options.outDir;
|
|
153
|
+
const outDir = options.outDir || 'example-unframer-app';
|
|
162
154
|
console.log(`Creating example app in ${outDir}`);
|
|
163
155
|
// Create the output directory
|
|
164
156
|
const absoluteOutDir = path.resolve(process.cwd(), outDir);
|
|
@@ -257,52 +249,238 @@ cli.command('example-app <projectId>', 'Create an example app with Framer compon
|
|
|
257
249
|
throw error;
|
|
258
250
|
}
|
|
259
251
|
});
|
|
260
|
-
cli.command('mcp login [url]', '
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
let mcpUrl = url;
|
|
270
|
-
if (!mcpUrl) {
|
|
271
|
-
try {
|
|
272
|
-
mcpUrl = await input({ message: 'Paste MCP URL:' });
|
|
252
|
+
cli.command('mcp login [url]', 'Login to Framer MCP. Choose between plugin mode (requires Framer open) or server API mode (works headlessly with API key).').action(async (url) => {
|
|
253
|
+
try {
|
|
254
|
+
// If URL is passed directly, use plugin mode
|
|
255
|
+
if (url) {
|
|
256
|
+
saveConfig({ mode: 'plugin', mcpUrl: url });
|
|
257
|
+
console.log(`MCP URL saved to ${getConfigPath()}`);
|
|
258
|
+
console.log(`Run \`unframer --help\` to see all available MCP commands`);
|
|
259
|
+
return;
|
|
273
260
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
261
|
+
// Show mode selection dropdown
|
|
262
|
+
const mode = await select({
|
|
263
|
+
message: 'Select authentication mode:',
|
|
264
|
+
choices: [
|
|
265
|
+
{
|
|
266
|
+
value: 'plugin',
|
|
267
|
+
name: 'MCP Plugin (Recommended)',
|
|
268
|
+
description: 'Requires Framer to be open with MCP plugin running. Paste URL from plugin.',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
value: 'server-api',
|
|
272
|
+
name: 'Server API',
|
|
273
|
+
description: 'Works without Framer open. Requires API key from project settings.',
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
});
|
|
277
|
+
if (mode === 'plugin') {
|
|
278
|
+
const shortcut = process.platform === 'darwin' ? 'Cmd+K' : 'Ctrl+K';
|
|
279
|
+
console.log('\nTo get your MCP URL:');
|
|
280
|
+
console.log(' 1. Go to https://framer.com and open your project');
|
|
281
|
+
console.log(` 2. Press ${shortcut} and search for "MCP" plugin`);
|
|
282
|
+
console.log(' 3. Copy the URL shown in the plugin\n');
|
|
283
|
+
const mcpUrl = await input({ message: 'Paste MCP URL:' });
|
|
284
|
+
if (!mcpUrl) {
|
|
285
|
+
console.error('MCP URL is required');
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
saveConfig({ mode: 'plugin', mcpUrl });
|
|
289
|
+
console.log(`\nMCP URL saved to ${getConfigPath()}`);
|
|
290
|
+
console.log(`Run \`unframer --help\` to see all available MCP commands`);
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
// Server API mode
|
|
294
|
+
console.log('\nTo get your API key:');
|
|
295
|
+
console.log(' 1. Open your Framer project at https://framer.com/projects');
|
|
296
|
+
console.log(' 2. Go to Project Settings > API');
|
|
297
|
+
console.log(' 3. Generate or copy your API key\n');
|
|
298
|
+
const apiKey = await password({ message: 'Enter Framer API key:', mask: '*' });
|
|
299
|
+
if (!apiKey) {
|
|
300
|
+
console.error('API key is required');
|
|
301
|
+
process.exit(1);
|
|
302
|
+
}
|
|
303
|
+
console.log('\nTo get your project URL:');
|
|
304
|
+
console.log(' Copy the URL from your browser when viewing the project');
|
|
305
|
+
console.log(' Example: https://framer.com/projects/MyProject--abc123\n');
|
|
306
|
+
const projectUrl = await input({
|
|
307
|
+
message: 'Enter Framer project URL (optional, can use --project later):',
|
|
308
|
+
});
|
|
309
|
+
saveConfig({
|
|
310
|
+
mode: 'server-api',
|
|
311
|
+
framerApiKey: apiKey,
|
|
312
|
+
framerProjectUrl: projectUrl || undefined,
|
|
313
|
+
});
|
|
314
|
+
console.log(`\nServer API credentials saved to ${getConfigPath()}`);
|
|
315
|
+
console.log(`\nUsage:`);
|
|
316
|
+
if (projectUrl) {
|
|
317
|
+
console.log(` unframer mcp getProjectXml`);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
console.log(` unframer mcp getProjectXml --project "https://framer.com/projects/..."`);
|
|
278
321
|
}
|
|
279
|
-
|
|
322
|
+
console.log(`\nOr set FRAMER_PROJECT_URL environment variable`);
|
|
280
323
|
}
|
|
281
324
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
325
|
+
catch (error) {
|
|
326
|
+
if (error instanceof Error && error.name === 'ExitPromptError') {
|
|
327
|
+
process.exit(0);
|
|
328
|
+
}
|
|
329
|
+
throw error;
|
|
285
330
|
}
|
|
286
|
-
saveConfig({ mcpUrl });
|
|
287
|
-
console.log(`MCP URL saved to ${getConfigPath()}`);
|
|
288
331
|
});
|
|
289
|
-
// Add MCP tool commands
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
332
|
+
// Add MCP tool commands
|
|
333
|
+
const config = loadConfig();
|
|
334
|
+
const cliArgs = process.argv.slice(2);
|
|
335
|
+
const hasMcpCommand = cliArgs.includes('mcp');
|
|
336
|
+
const hasProjectOption = cliArgs.some((arg) => {
|
|
337
|
+
return arg === '--project' || arg.startsWith('--project=');
|
|
338
|
+
});
|
|
339
|
+
const shouldUseServerApiForProjectOption = hasMcpCommand && hasProjectOption;
|
|
340
|
+
const mcpMode = shouldUseServerApiForProjectOption
|
|
341
|
+
? 'server-api'
|
|
342
|
+
: config.mode || (config.mcpUrl ? 'plugin' : undefined);
|
|
343
|
+
if (mcpMode === 'server-api') {
|
|
344
|
+
// Server API mode - use framer-api directly
|
|
345
|
+
// Commands are registered via registerServerApiCommands below
|
|
346
|
+
await registerServerApiCommands();
|
|
347
|
+
}
|
|
348
|
+
else {
|
|
349
|
+
// Plugin mode - use MCP transport
|
|
350
|
+
await addMcpCommands({
|
|
351
|
+
cli,
|
|
352
|
+
commandPrefix: 'mcp',
|
|
353
|
+
getMcpTransport: async (sessionId) => {
|
|
354
|
+
const { StreamableHTTPClientTransport } = await import('@modelcontextprotocol/sdk/client/streamableHttp.js');
|
|
355
|
+
// UNFRAMER_MCP_URL env var overrides config file (contains full URL with auth)
|
|
356
|
+
const mcpUrl = process.env.UNFRAMER_MCP_URL || loadConfig().mcpUrl;
|
|
357
|
+
if (!mcpUrl) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const url = new URL(mcpUrl);
|
|
361
|
+
// Use /mcp endpoint for StreamableHTTP
|
|
362
|
+
if (url.pathname.endsWith('/sse')) {
|
|
363
|
+
url.pathname = url.pathname.replace(/\/sse$/, '/mcp');
|
|
364
|
+
}
|
|
365
|
+
return new StreamableHTTPClientTransport(url, { sessionId });
|
|
366
|
+
},
|
|
367
|
+
loadCache: () => {
|
|
368
|
+
return loadConfig().cachedMcpTools;
|
|
369
|
+
},
|
|
370
|
+
saveCache: (cache) => {
|
|
371
|
+
const configNow = loadConfig();
|
|
372
|
+
saveConfig({ ...configNow, cachedMcpTools: cache });
|
|
373
|
+
},
|
|
374
|
+
}).catch((e) => console.error(e));
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Register MCP commands for server-api mode using framer-api directly.
|
|
378
|
+
* This bypasses the MCP transport and calls handlers directly.
|
|
379
|
+
*/
|
|
380
|
+
async function registerServerApiCommands() {
|
|
381
|
+
// Dynamic import to avoid loading framer-api in plugin mode
|
|
382
|
+
const { connect } = await import('framer-api');
|
|
383
|
+
// Import tool definitions and handler from plugin-mcp
|
|
384
|
+
// Note: Run `pnpm --filter plugin-mcp build && pnpm --filter plugin-mcp gen-unframer` first
|
|
385
|
+
const { mcpTools, mcpToolHandler } = await import('./plugin-mcp-dist/lib/mcp-handlers.js');
|
|
386
|
+
if (!mcpTools || !mcpToolHandler) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
// Register a command for each MCP tool
|
|
390
|
+
for (const [toolName, toolDef] of Object.entries(mcpTools)) {
|
|
391
|
+
const cmd = cli.command(`mcp ${toolName}`, toolDef.description.split('\n')[0]);
|
|
392
|
+
cmd.option('--project <url>', 'Framer project URL. Uses server-api mode (framer-api headless). Works alongside plugin mode login, pass --project to switch to server-api for a single command. Also reads FRAMER_PROJECT_URL env var.');
|
|
393
|
+
// Add options based on tool input schema, using zod v4 native JSON Schema conversion
|
|
394
|
+
const inputSchema = toolDef.input;
|
|
395
|
+
if (inputSchema) {
|
|
396
|
+
const jsonSchema = z.toJSONSchema(inputSchema);
|
|
397
|
+
const properties = jsonSchema.properties || {};
|
|
398
|
+
const required = new Set(jsonSchema.required || []);
|
|
399
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
400
|
+
const isRequired = required.has(key);
|
|
401
|
+
const isBooleanType = prop.type === 'boolean';
|
|
402
|
+
const optionName = isBooleanType
|
|
403
|
+
? `--${key}`
|
|
404
|
+
: `--${key} <value>`;
|
|
405
|
+
const optionDescription = [
|
|
406
|
+
prop.description || key,
|
|
407
|
+
isRequired ? '(required)' : '',
|
|
408
|
+
]
|
|
409
|
+
.filter(Boolean)
|
|
410
|
+
.join(' ');
|
|
411
|
+
if (isBooleanType && prop.default === undefined) {
|
|
412
|
+
cmd.option(optionName, optionDescription);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
cmd.option(optionName, wrapJsonSchema({
|
|
416
|
+
...prop,
|
|
417
|
+
description: optionDescription,
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
302
420
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
421
|
+
cmd.action(async (options) => {
|
|
422
|
+
const projectOption = typeof options.project === 'string'
|
|
423
|
+
? options.project
|
|
424
|
+
: undefined;
|
|
425
|
+
const projectUrl = projectOption ||
|
|
426
|
+
process.env.FRAMER_PROJECT_URL ||
|
|
427
|
+
loadConfig().framerProjectUrl;
|
|
428
|
+
const apiKey = process.env.FRAMER_API_KEY || loadConfig().framerApiKey;
|
|
429
|
+
if (!projectUrl) {
|
|
430
|
+
console.error('Project URL required. Use --project option, FRAMER_PROJECT_URL env var, or set during login.');
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
if (!apiKey) {
|
|
434
|
+
console.error('API key required. Set FRAMER_API_KEY env var or run `unframer mcp login` first.');
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
// Remove CLI-specific options from input
|
|
438
|
+
const toolInput = Object.fromEntries(Object.entries(options).filter(([key]) => {
|
|
439
|
+
return key !== 'project';
|
|
440
|
+
}));
|
|
441
|
+
const globalWithFramer = globalThis;
|
|
442
|
+
let framerClient = undefined;
|
|
443
|
+
let actionError = undefined;
|
|
444
|
+
try {
|
|
445
|
+
spinner.start(`Connecting to Framer...`);
|
|
446
|
+
framerClient = await connect(projectUrl, apiKey);
|
|
447
|
+
// Set global framer for utility functions that use it
|
|
448
|
+
globalWithFramer.framer = framerClient;
|
|
449
|
+
spinner.start(`Running ${toolName}...`);
|
|
450
|
+
const result = await mcpToolHandler({
|
|
451
|
+
type: toolName,
|
|
452
|
+
input: toolInput,
|
|
453
|
+
});
|
|
454
|
+
spinner.stop('');
|
|
455
|
+
// Output result
|
|
456
|
+
if (typeof result === 'string') {
|
|
457
|
+
console.log(result);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
console.log(JSON.stringify(result, null, 2));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
actionError = error;
|
|
465
|
+
}
|
|
466
|
+
finally {
|
|
467
|
+
if (framerClient) {
|
|
468
|
+
try {
|
|
469
|
+
await framerClient.disconnect();
|
|
470
|
+
}
|
|
471
|
+
catch (disconnectError) {
|
|
472
|
+
logger.error('Failed disconnecting from Framer:', disconnectError);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
delete globalWithFramer.framer;
|
|
476
|
+
}
|
|
477
|
+
if (actionError) {
|
|
478
|
+
spinner.error(`Failed: ${actionError instanceof Error ? actionError.message : String(actionError)}`);
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
306
484
|
export async function configFromFetch({ projectId, externalPackages = [], allExternal = false, agent = '', outDir = undefined, }) {
|
|
307
485
|
logger.log(`Fetching config for project ${projectId}`);
|
|
308
486
|
const url = process.env.UNFRAMER_SERVER_URL;
|