@actuate-media/cli 0.10.0 → 0.11.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.
@@ -1,4 +1,4 @@
1
1
 
2
- > @actuate-media/cli@0.10.0 build /home/runner/work/actuatecms/actuatecms/packages/cli
2
+ > @actuate-media/cli@0.11.0 build /home/runner/work/actuatecms/actuatecms/packages/cli
3
3
  > tsc
4
4
 
@@ -1,38 +1,38 @@
1
1
 
2
- > @actuate-media/cli@0.10.0 test /home/runner/work/actuatecms/actuatecms/packages/cli
2
+ > @actuate-media/cli@0.11.0 test /home/runner/work/actuatecms/actuatecms/packages/cli
3
3
  > vitest run
4
4
 
5
5
 
6
6
   RUN  v4.1.8 /home/runner/work/actuatecms/actuatecms/packages/cli
7
7
 
8
- ✓ dist/__tests__/db-sync.test.js (10 tests) 324ms
9
- ✓ dist/__tests__/seed.test.js (10 tests) 241ms
10
- ✓ src/__tests__/db-sync.test.ts (10 tests) 300ms
11
- ✓ src/__tests__/seed.test.ts (10 tests) 141ms
12
- ✓ dist/__tests__/deployment-diagnostics.test.js (8 tests) 68ms
13
- ✓ src/__tests__/deployment-diagnostics.test.ts (8 tests) 58ms
8
+ ✓ dist/__tests__/db-sync.test.js (10 tests) 462ms
9
+ ✓ dist/__tests__/seed.test.js (10 tests) 201ms
10
+ ✓ src/__tests__/db-sync.test.ts (10 tests) 468ms
11
+ ✓ src/__tests__/seed.test.ts (10 tests) 194ms
12
+ ✓ dist/__tests__/deployment-diagnostics.test.js (8 tests) 52ms
13
+ ✓ src/__tests__/deployment-diagnostics.test.ts (8 tests) 59ms
14
14
  - Reading canonical Actuate schema...
15
+ ✔ Actuate CMS models added to schema.
16
+ - Running prisma generate...
15
17
  stdout | dist/__tests__/db-init.test.js > db:init (WS-D D5 — canonical schema, no stale fragment) > command > injects canonical models with @@map and no duplicate datasource/generator
16
18
  ✔ Prisma client generated.
17
19
  ℹ Run `npx prisma migrate dev --name actuate-cms` to create the migration.
18
20
 
19
- ✔ Actuate CMS models added to schema.
20
- - Running prisma generate...
21
+ - Reading canonical Actuate schema...
22
+ ✖ Could not locate @actuate-media/cms-core.
21
23
  stdout | dist/__tests__/db-init.test.js > db:init (WS-D D5 — canonical schema, no stale fragment) > command > fails clearly when cms-core cannot be located
22
24
  ℹ Install it first: `npm install @actuate-media/cms-core`.
23
- - Reading canonical Actuate schema...
24
25
 
25
- ✖ Could not locate @actuate-media/cms-core.
26
26
  - Reading canonical Actuate schema...
27
+ ✔ Actuate CMS models added to schema.
28
+ - Running prisma generate...
27
29
  stdout | dist/__tests__/db-init.test.js > db:init (WS-D D5 — canonical schema, no stale fragment) > command > is idempotent without --force
28
30
  ✔ Prisma client generated.
29
31
  ℹ Run `npx prisma migrate dev --name actuate-cms` to create the migration.
30
32
 
31
- ✔ Actuate CMS models added to schema.
32
- - Running prisma generate...
33
33
  - Reading canonical Actuate schema...
34
34
  ℹ Actuate CMS models already present in schema. Use --force to overwrite.
35
- ✓ dist/__tests__/db-init.test.js (4 tests) 285ms
35
+ ✓ dist/__tests__/db-init.test.js (4 tests) 365ms
36
36
  - Reading canonical Actuate schema...
37
37
  ✔ Actuate CMS models added to schema.
38
38
  - Running prisma generate...
@@ -41,8 +41,8 @@
41
41
  ℹ Run `npx prisma migrate dev --name actuate-cms` to create the migration.
42
42
 
43
43
  stdout | src/__tests__/db-init.test.ts > db:init (WS-D D5 — canonical schema, no stale fragment) > command > fails clearly when cms-core cannot be located
44
- - Reading canonical Actuate schema...
45
44
  ℹ Install it first: `npm install @actuate-media/cms-core`.
45
+ - Reading canonical Actuate schema...
46
46
 
47
47
  ✖ Could not locate @actuate-media/cms-core.
48
48
  - Reading canonical Actuate schema...
@@ -54,32 +54,290 @@
54
54
 
55
55
  - Reading canonical Actuate schema...
56
56
  ℹ Actuate CMS models already present in schema. Use --force to overwrite.
57
- ✓ src/__tests__/db-init.test.ts (4 tests) 289ms
58
- ✓ dist/__tests__/form-seed.test.js (10 tests) 57ms
59
- ✓ src/__tests__/form-seed.test.ts (10 tests) 59ms
60
- ✓ dist/__tests__/vercel-env-matrix.test.js (5 tests) 34ms
61
- ✓ src/__tests__/vercel-env-matrix.test.ts (5 tests) 43ms
57
+ ✓ src/__tests__/db-init.test.ts (4 tests) 391ms
58
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > creates both editor configs with placeholder key and git-ignores them
59
+ ✔ Created .cursor/mcp.json
60
+
61
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > creates both editor configs with placeholder key and git-ignores them
62
+ ✔ Created .vscode/mcp.json
63
+
64
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > creates both editor configs with placeholder key and git-ignores them
65
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
66
+
67
+ Next steps
68
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
69
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
70
+ 3. Restart Cursor / VS Code so the MCP server is picked up
71
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
72
+
73
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > applies --url and --key to both configs
74
+ ✔ Created .cursor/mcp.json
75
+
76
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > applies --url and --key to both configs
77
+ ✔ Created .vscode/mcp.json
78
+
79
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > applies --url and --key to both configs
80
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
81
+
82
+ Next steps
83
+ 1. Restart Cursor / VS Code so the MCP server is picked up
84
+
85
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > merges into an existing config without clobbering other servers
86
+ ✔ Updated .cursor/mcp.json
87
+
88
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > merges into an existing config without clobbering other servers
89
+ ✔ Created .vscode/mcp.json
90
+
91
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > merges into an existing config without clobbering other servers
92
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
93
+
94
+ Next steps
95
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
96
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
97
+ 3. Restart Cursor / VS Code so the MCP server is picked up
98
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
99
+
100
+ stderr | dist/__tests__/mcp-init.test.js > mcp:init > leaves an existing actuate entry alone unless --force is passed
101
+ ⚠ .cursor/mcp.json already has an "actuate" server — use --force to replace it.
102
+
103
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > leaves an existing actuate entry alone unless --force is passed
104
+ ✔ Created .vscode/mcp.json
105
+
106
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > leaves an existing actuate entry alone unless --force is passed
107
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
108
+
109
+ Next steps
110
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
111
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
112
+ 3. Restart Cursor / VS Code so the MCP server is picked up
113
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
114
+
115
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > leaves an existing actuate entry alone unless --force is passed
116
+ ✔ Updated .cursor/mcp.json
117
+
118
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > leaves an existing actuate entry alone unless --force is passed
119
+ ✔ Updated .vscode/mcp.json
120
+
121
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > leaves an existing actuate entry alone unless --force is passed
122
+ 
123
+ Next steps
124
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
125
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
126
+ 3. Restart Cursor / VS Code so the MCP server is picked up
127
+
128
+ stderr | dist/__tests__/mcp-init.test.js > mcp:init > fails on invalid JSON without --force, overwrites with --force
129
+ ✖ .cursor/mcp.json contains invalid JSON — fix it or re-run with --force.
130
+
131
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > fails on invalid JSON without --force, overwrites with --force
132
+ ✔ Created .vscode/mcp.json
133
+
134
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > fails on invalid JSON without --force, overwrites with --force
135
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
136
+
137
+ Next steps
138
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
139
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
140
+ 3. Restart Cursor / VS Code so the MCP server is picked up
141
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
142
+
143
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > fails on invalid JSON without --force, overwrites with --force
144
+ ✔ Updated .cursor/mcp.json
145
+
146
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > fails on invalid JSON without --force, overwrites with --force
147
+ ✔ Updated .vscode/mcp.json
148
+
149
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > fails on invalid JSON without --force, overwrites with --force
150
+ 
151
+ Next steps
152
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
153
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
154
+ 3. Restart Cursor / VS Code so the MCP server is picked up
155
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
156
+
157
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > does not duplicate gitignore entries on re-runs
158
+ ✔ Created .cursor/mcp.json
159
+
160
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > does not duplicate gitignore entries on re-runs
161
+ ✔ Created .vscode/mcp.json
162
+
163
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > does not duplicate gitignore entries on re-runs
164
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
165
+
166
+ Next steps
167
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
168
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
169
+ 3. Restart Cursor / VS Code so the MCP server is picked up
170
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
171
+
172
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > does not duplicate gitignore entries on re-runs
173
+ ✔ Updated .cursor/mcp.json
174
+
175
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > does not duplicate gitignore entries on re-runs
176
+ ✔ Updated .vscode/mcp.json
177
+
178
+ stdout | dist/__tests__/mcp-init.test.js > mcp:init > does not duplicate gitignore entries on re-runs
179
+ 
180
+ Next steps
181
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
182
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
183
+ 3. Restart Cursor / VS Code so the MCP server is picked up
184
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
185
+
186
+ ✓ dist/__tests__/mcp-init.test.js (6 tests) 514ms
187
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > creates both editor configs with placeholder key and git-ignores them
188
+ ✔ Created .cursor/mcp.json
189
+
190
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > creates both editor configs with placeholder key and git-ignores them
191
+ ✔ Created .vscode/mcp.json
192
+
193
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > creates both editor configs with placeholder key and git-ignores them
194
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
195
+
196
+ Next steps
197
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
198
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
199
+ 3. Restart Cursor / VS Code so the MCP server is picked up
200
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
201
+
202
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > applies --url and --key to both configs
203
+ ✔ Created .cursor/mcp.json
204
+
205
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > applies --url and --key to both configs
206
+ ✔ Created .vscode/mcp.json
207
+
208
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > applies --url and --key to both configs
209
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
210
+
211
+ Next steps
212
+ 1. Restart Cursor / VS Code so the MCP server is picked up
213
+
214
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > merges into an existing config without clobbering other servers
215
+ ✔ Updated .cursor/mcp.json
216
+
217
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > merges into an existing config without clobbering other servers
218
+ ✔ Created .vscode/mcp.json
219
+
220
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > merges into an existing config without clobbering other servers
221
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
222
+
223
+ Next steps
224
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
225
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
226
+ 3. Restart Cursor / VS Code so the MCP server is picked up
227
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
228
+
229
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > leaves an existing actuate entry alone unless --force is passed
230
+ ✔ Created .vscode/mcp.json
231
+
232
+ stderr | src/__tests__/mcp-init.test.ts > mcp:init > leaves an existing actuate entry alone unless --force is passed
233
+ ⚠ .cursor/mcp.json already has an "actuate" server — use --force to replace it.
234
+
235
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > leaves an existing actuate entry alone unless --force is passed
236
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
237
+
238
+ Next steps
239
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
240
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
241
+ 3. Restart Cursor / VS Code so the MCP server is picked up
242
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
243
+
244
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > leaves an existing actuate entry alone unless --force is passed
245
+ ✔ Updated .cursor/mcp.json
246
+
247
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > leaves an existing actuate entry alone unless --force is passed
248
+ ✔ Updated .vscode/mcp.json
249
+
250
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > leaves an existing actuate entry alone unless --force is passed
251
+ 
252
+ Next steps
253
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
254
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
255
+ 3. Restart Cursor / VS Code so the MCP server is picked up
256
+
257
+ stderr | src/__tests__/mcp-init.test.ts > mcp:init > fails on invalid JSON without --force, overwrites with --force
258
+ ✖ .cursor/mcp.json contains invalid JSON — fix it or re-run with --force.
259
+
260
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > fails on invalid JSON without --force, overwrites with --force
261
+ ✔ Created .vscode/mcp.json
262
+
263
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > fails on invalid JSON without --force, overwrites with --force
264
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
265
+
266
+ Next steps
267
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
268
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
269
+ 3. Restart Cursor / VS Code so the MCP server is picked up
270
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
271
+
272
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > fails on invalid JSON without --force, overwrites with --force
273
+ ✔ Updated .cursor/mcp.json
274
+
275
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > fails on invalid JSON without --force, overwrites with --force
276
+ ✔ Updated .vscode/mcp.json
277
+
278
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > fails on invalid JSON without --force, overwrites with --force
279
+ 
280
+ Next steps
281
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
282
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
283
+ 3. Restart Cursor / VS Code so the MCP server is picked up
284
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
285
+
286
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > does not duplicate gitignore entries on re-runs
287
+ ✔ Created .cursor/mcp.json
288
+
289
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > does not duplicate gitignore entries on re-runs
290
+ ✔ Created .vscode/mcp.json
291
+
292
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > does not duplicate gitignore entries on re-runs
293
+ ✔ Added to .gitignore: .cursor/mcp.json, .vscode/mcp.json
294
+
295
+ Next steps
296
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
297
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
298
+ 3. Restart Cursor / VS Code so the MCP server is picked up
299
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
300
+
301
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > does not duplicate gitignore entries on re-runs
302
+ ✔ Updated .cursor/mcp.json
303
+
304
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > does not duplicate gitignore entries on re-runs
305
+ ✔ Updated .vscode/mcp.json
306
+
307
+ stdout | src/__tests__/mcp-init.test.ts > mcp:init > does not duplicate gitignore entries on re-runs
308
+ 
309
+ Next steps
310
+ 1. Mint an API key in Admin → Settings → API Keys (act_sk_...)
311
+ 2. Replace act_sk_REPLACE_WITH_YOUR_KEY in both config files with the real key
312
+ 3. Restart Cursor / VS Code so the MCP server is picked up
313
+ Tip: pass --url https://your-site.com to target a deployed CMS instead.
314
+
315
+ ✓ src/__tests__/mcp-init.test.ts (6 tests) 366ms
316
+ ✓ dist/__tests__/form-seed.test.js (10 tests) 67ms
317
+ ✓ src/__tests__/form-seed.test.ts (10 tests) 61ms
318
+ ✓ dist/__tests__/vercel-env-matrix.test.js (5 tests) 49ms
319
+ ✓ src/__tests__/vercel-env-matrix.test.ts (5 tests) 69ms
62
320
  - Reading canonical Actuate schema...
63
321
  ✔ Actuate CMS models added to schema.
64
- - Running prisma generate...
65
322
  stdout | dist/__tests__/schema-fragment.test.js > db:init schema fragment > injects all deploy-critical Actuate models
66
323
  ✔ Prisma client generated.
67
324
  ℹ Run `npx prisma migrate dev --name actuate-cms` to create the migration.
68
325
 
69
- ✓ dist/__tests__/schema-fragment.test.js (1 test) 96ms
326
+ - Running prisma generate...
327
+ ✓ dist/__tests__/schema-fragment.test.js (1 test) 193ms
70
328
  - Reading canonical Actuate schema...
71
- stdout | src/__tests__/schema-fragment.test.ts > db:init schema fragment > injects all deploy-critical Actuate models
72
329
  ✔ Actuate CMS models added to schema.
330
+ - Running prisma generate...
331
+ stdout | src/__tests__/schema-fragment.test.ts > db:init schema fragment > injects all deploy-critical Actuate models
73
332
  ✔ Prisma client generated.
74
333
  ℹ Run `npx prisma migrate dev --name actuate-cms` to create the migration.
75
334
 
76
- - Running prisma generate...
77
- ✓ src/__tests__/schema-fragment.test.ts (1 test) 116ms
78
- ✓ dist/__tests__/init.test.js (2 tests) 54ms
79
- ✓ src/__tests__/init.test.ts (2 tests) 32ms
80
-
81
-  Test Files  16 passed (16)
82
-  Tests  100 passed (100)
83
-  Start at  15:53:52
84
-  Duration  23.79s (transform 2.27s, setup 0ms, import 6.86s, tests 2.20s, environment 2ms)
335
+ ✓ src/__tests__/schema-fragment.test.ts (1 test) 172ms
336
+ ✓ dist/__tests__/init.test.js (2 tests) 45ms
337
+ ✓ src/__tests__/init.test.ts (2 tests) 42ms
338
+
339
+  Test Files  18 passed (18)
340
+  Tests  112 passed (112)
341
+  Start at  03:11:38
342
+  Duration  30.42s (transform 2.66s, setup 0ms, import 7.77s, tests 3.77s, environment 12ms)
85
343
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @actuate-media/cli
2
2
 
3
+ ## 0.11.0
4
+
5
+ ### Minor Changes
6
+
7
+ - eb998e5: New `actuate mcp:init` command — retrofit the Actuate MCP server onto existing projects.
8
+
9
+ Writes `.cursor/mcp.json` (Cursor / Claude Desktop shape) and `.vscode/mcp.json` (VS Code shape) wiring `@actuate-media/mcp-server`, with `--url` / `--key` options. Merges into existing configs without clobbering other MCP servers; an existing `actuate` entry is only replaced with `--force`. Both paths are appended to `.gitignore` so a real `act_sk_...` key is never committed. New scaffolds already get these files from `create-actuate-cms` — this closes the gap for projects created before that.
10
+
3
11
  ## 0.10.0
4
12
 
5
13
  ### Minor Changes
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=mcp-init.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-init.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/mcp-init.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,100 @@
1
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { Command } from 'commander';
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
6
+ import { registerMcpCommand } from '../commands/mcp.js';
7
+ /**
8
+ * `actuate mcp:init` retrofits MCP editor configs onto existing projects.
9
+ * Pins the merge contract: other configured servers are never clobbered, an
10
+ * existing `actuate` entry needs `--force`, and both config paths end up
11
+ * git-ignored (they hold a real API key once configured).
12
+ */
13
+ let projectDir;
14
+ let originalCwd;
15
+ async function run(args = []) {
16
+ const program = new Command();
17
+ program.exitOverride();
18
+ registerMcpCommand(program);
19
+ await program.parseAsync(['mcp:init', ...args], { from: 'user' });
20
+ }
21
+ async function readJson(relPath) {
22
+ return JSON.parse(await readFile(path.join(projectDir, relPath), 'utf8'));
23
+ }
24
+ beforeEach(async () => {
25
+ originalCwd = process.cwd();
26
+ projectDir = await mkdtemp(path.join(tmpdir(), 'actuate-mcp-init-'));
27
+ process.chdir(projectDir);
28
+ });
29
+ afterEach(async () => {
30
+ process.chdir(originalCwd);
31
+ process.exitCode = 0;
32
+ await rm(projectDir, { recursive: true, force: true });
33
+ });
34
+ describe('mcp:init', () => {
35
+ it('creates both editor configs with placeholder key and git-ignores them', async () => {
36
+ await writeFile(path.join(projectDir, '.gitignore'), 'node_modules\n', 'utf8');
37
+ await run();
38
+ const cursor = await readJson('.cursor/mcp.json');
39
+ expect(cursor.mcpServers.actuate.command).toBe('npx');
40
+ expect(cursor.mcpServers.actuate.args).toEqual(['-y', '@actuate-media/mcp-server']);
41
+ expect(cursor.mcpServers.actuate.env.ACTUATE_BASE_URL).toBe('http://localhost:3000');
42
+ expect(cursor.mcpServers.actuate.env.ACTUATE_API_KEY).toBe('act_sk_REPLACE_WITH_YOUR_KEY');
43
+ const vscode = await readJson('.vscode/mcp.json');
44
+ expect(vscode.servers.actuate.type).toBe('stdio');
45
+ const gitignore = await readFile(path.join(projectDir, '.gitignore'), 'utf8');
46
+ expect(gitignore).toContain('node_modules');
47
+ expect(gitignore).toContain('.cursor/mcp.json');
48
+ expect(gitignore).toContain('.vscode/mcp.json');
49
+ });
50
+ it('applies --url and --key to both configs', async () => {
51
+ await run(['--url', 'https://example.com', '--key', 'act_sk_real123']);
52
+ const cursor = await readJson('.cursor/mcp.json');
53
+ expect(cursor.mcpServers.actuate.env).toEqual({
54
+ ACTUATE_BASE_URL: 'https://example.com',
55
+ ACTUATE_API_KEY: 'act_sk_real123',
56
+ });
57
+ const vscode = await readJson('.vscode/mcp.json');
58
+ expect(vscode.servers.actuate.env.ACTUATE_BASE_URL).toBe('https://example.com');
59
+ });
60
+ it('merges into an existing config without clobbering other servers', async () => {
61
+ await mkdir(path.join(projectDir, '.cursor'), { recursive: true });
62
+ await writeFile(path.join(projectDir, '.cursor', 'mcp.json'), JSON.stringify({ mcpServers: { sentry: { command: 'sentry-mcp' } } }), 'utf8');
63
+ await run();
64
+ const cursor = await readJson('.cursor/mcp.json');
65
+ expect(cursor.mcpServers.sentry).toEqual({ command: 'sentry-mcp' });
66
+ expect(cursor.mcpServers.actuate).toBeDefined();
67
+ });
68
+ it('leaves an existing actuate entry alone unless --force is passed', async () => {
69
+ await mkdir(path.join(projectDir, '.cursor'), { recursive: true });
70
+ const existing = {
71
+ mcpServers: { actuate: { command: 'custom', env: { ACTUATE_BASE_URL: 'https://keep.me' } } },
72
+ };
73
+ await writeFile(path.join(projectDir, '.cursor', 'mcp.json'), JSON.stringify(existing), 'utf8');
74
+ await run();
75
+ let cursor = await readJson('.cursor/mcp.json');
76
+ expect(cursor.mcpServers.actuate.command).toBe('custom');
77
+ await run(['--force', '--url', 'https://new.example.com']);
78
+ cursor = await readJson('.cursor/mcp.json');
79
+ expect(cursor.mcpServers.actuate.command).toBe('npx');
80
+ expect(cursor.mcpServers.actuate.env.ACTUATE_BASE_URL).toBe('https://new.example.com');
81
+ });
82
+ it('fails on invalid JSON without --force, overwrites with --force', async () => {
83
+ await mkdir(path.join(projectDir, '.cursor'), { recursive: true });
84
+ await writeFile(path.join(projectDir, '.cursor', 'mcp.json'), '{ not json', 'utf8');
85
+ await run();
86
+ expect(process.exitCode).toBe(1);
87
+ process.exitCode = 0;
88
+ await run(['--force']);
89
+ const cursor = await readJson('.cursor/mcp.json');
90
+ expect(cursor.mcpServers.actuate).toBeDefined();
91
+ });
92
+ it('does not duplicate gitignore entries on re-runs', async () => {
93
+ await run();
94
+ await run(['--force']);
95
+ const gitignore = await readFile(path.join(projectDir, '.gitignore'), 'utf8');
96
+ const count = gitignore.split('\n').filter((l) => l.trim() === '.cursor/mcp.json').length;
97
+ expect(count).toBe(1);
98
+ });
99
+ });
100
+ //# sourceMappingURL=mcp-init.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp-init.test.js","sourceRoot":"","sources":["../../src/__tests__/mcp-init.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAA;AAChC,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AAEpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAEvD;;;;;GAKG;AAEH,IAAI,UAAkB,CAAA;AACtB,IAAI,WAAmB,CAAA;AAEvB,KAAK,UAAU,GAAG,CAAC,OAAiB,EAAE;IACpC,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;IAC7B,OAAO,CAAC,YAAY,EAAE,CAAA;IACtB,kBAAkB,CAAC,OAAO,CAAC,CAAA;IAC3B,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;AACnE,CAAC;AAED,KAAK,UAAU,QAAQ,CAAC,OAAe;IACrC,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,CAAC,CAAA;AAC3E,CAAC;AAED,UAAU,CAAC,KAAK,IAAI,EAAE;IACpB,WAAW,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IAC3B,UAAU,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,mBAAmB,CAAC,CAAC,CAAA;IACpE,OAAO,CAAC,KAAK,CAAC,UAAU,CAAC,CAAA;AAC3B,CAAC,CAAC,CAAA;AAEF,SAAS,CAAC,KAAK,IAAI,EAAE;IACnB,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IAC1B,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;IACpB,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAA;AACxD,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,uEAAuE,EAAE,KAAK,IAAI,EAAE;QACrF,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,gBAAgB,EAAE,MAAM,CAAC,CAAA;QAC9E,MAAM,GAAG,EAAE,CAAA;QAEX,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,kBAAkB,CAAC,CAAA;QACjD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACrD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,EAAE,2BAA2B,CAAC,CAAC,CAAA;QACnF,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAA;QACpF,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC,IAAI,CAAC,8BAA8B,CAAC,CAAA;QAE1F,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,kBAAkB,CAAC,CAAA;QACjD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAEjD,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAA;QAC7E,MAAM,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAA;QAC3C,MAAM,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAA;QAC/C,MAAM,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAA;IACjD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,qBAAqB,EAAE,OAAO,EAAE,gBAAgB,CAAC,CAAC,CAAA;QAEtE,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,kBAAkB,CAAC,CAAA;QACjD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC;YAC5C,gBAAgB,EAAE,qBAAqB;YACvC,eAAe,EAAE,gBAAgB;SAClC,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,kBAAkB,CAAC,CAAA;QACjD,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAA;IACjF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAClE,MAAM,SAAS,CACb,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,CAAC,EAC5C,IAAI,CAAC,SAAS,CAAC,EAAE,UAAU,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC,EACrE,MAAM,CACP,CAAA;QACD,MAAM,GAAG,EAAE,CAAA;QAEX,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,kBAAkB,CAAC,CAAA;QACjD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC,CAAA;QACnE,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAA;IACjD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iEAAiE,EAAE,KAAK,IAAI,EAAE;QAC/E,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAClE,MAAM,QAAQ,GAAG;YACf,UAAU,EAAE,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,EAAE,EAAE;SAC7F,CAAA;QACD,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAA;QAE/F,MAAM,GAAG,EAAE,CAAA;QACX,IAAI,MAAM,GAAG,MAAM,QAAQ,CAAC,kBAAkB,CAAC,CAAA;QAC/C,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAExD,MAAM,GAAG,CAAC,CAAC,SAAS,EAAE,OAAO,EAAE,yBAAyB,CAAC,CAAC,CAAA;QAC1D,MAAM,GAAG,MAAM,QAAQ,CAAC,kBAAkB,CAAC,CAAA;QAC3C,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QACrD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,yBAAyB,CAAC,CAAA;IACxF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gEAAgE,EAAE,KAAK,IAAI,EAAE;QAC9E,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAClE,MAAM,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,UAAU,CAAC,EAAE,YAAY,EAAE,MAAM,CAAC,CAAA;QAEnF,MAAM,GAAG,EAAE,CAAA;QACX,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChC,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QAEpB,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;QACtB,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,kBAAkB,CAAC,CAAA;QACjD,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,WAAW,EAAE,CAAA;IACjD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,iDAAiD,EAAE,KAAK,IAAI,EAAE;QAC/D,MAAM,GAAG,EAAE,CAAA;QACX,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;QACtB,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,MAAM,CAAC,CAAA;QAC7E,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,KAAK,kBAAkB,CAAC,CAAC,MAAM,CAAA;QACzF,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACvB,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerMcpCommand(program: Command): void;
3
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAuHnC,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CA0DzD"}
@@ -0,0 +1,126 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import chalk from 'chalk';
5
+ import { logger } from '../utils/logger.js';
6
+ const PLACEHOLDER_KEY = 'act_sk_REPLACE_WITH_YOUR_KEY';
7
+ /** Cursor / Claude Desktop shape: top-level `mcpServers`. */
8
+ function cursorEntry(opts) {
9
+ return {
10
+ command: 'npx',
11
+ args: ['-y', '@actuate-media/mcp-server'],
12
+ env: { ACTUATE_BASE_URL: opts.url, ACTUATE_API_KEY: opts.key },
13
+ };
14
+ }
15
+ /** VS Code shape: top-level `servers`, each entry declares `type: "stdio"`. */
16
+ function vscodeEntry(opts) {
17
+ return { type: 'stdio', ...cursorEntry(opts) };
18
+ }
19
+ /**
20
+ * Create or merge one editor config. Existing files keep every other server
21
+ * entry; an existing `actuate` entry is only replaced with `--force`.
22
+ */
23
+ async function mergeConfigFile(root, target, force) {
24
+ const filePath = path.join(root, target.relPath);
25
+ let config = {};
26
+ let existed = false;
27
+ if (existsSync(filePath)) {
28
+ existed = true;
29
+ const raw = await readFile(filePath, 'utf8');
30
+ try {
31
+ const parsed = JSON.parse(raw);
32
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
33
+ config = parsed;
34
+ }
35
+ else if (!force) {
36
+ logger.error(`${target.relPath} is not a JSON object — fix it or re-run with --force.`);
37
+ return 'error';
38
+ }
39
+ }
40
+ catch {
41
+ if (!force) {
42
+ logger.error(`${target.relPath} contains invalid JSON — fix it or re-run with --force.`);
43
+ return 'error';
44
+ }
45
+ config = {};
46
+ }
47
+ }
48
+ const serversRaw = config[target.rootKey];
49
+ const servers = serversRaw && typeof serversRaw === 'object' && !Array.isArray(serversRaw)
50
+ ? serversRaw
51
+ : {};
52
+ if (servers.actuate && !force) {
53
+ logger.warn(`${target.relPath} already has an "actuate" server — use --force to replace it.`);
54
+ return 'skipped';
55
+ }
56
+ config[target.rootKey] = { ...servers, actuate: target.entry };
57
+ await mkdir(path.dirname(filePath), { recursive: true });
58
+ await writeFile(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8');
59
+ return existed ? 'updated' : 'created';
60
+ }
61
+ /**
62
+ * Make sure both MCP config paths are git-ignored. Appends only the missing
63
+ * entries; creates `.gitignore` if the project somehow has none.
64
+ */
65
+ async function ensureGitignored(root, relPaths) {
66
+ const gitignorePath = path.join(root, '.gitignore');
67
+ const existing = existsSync(gitignorePath) ? await readFile(gitignorePath, 'utf8') : '';
68
+ const lines = new Set(existing.split(/\r?\n/).map((l) => l.trim()));
69
+ const missing = relPaths.filter((p) => !lines.has(p));
70
+ if (missing.length === 0)
71
+ return [];
72
+ const block = (existing && !existing.endsWith('\n') ? '\n' : '') +
73
+ '\n# MCP configs hold a real API key once configured — never commit them\n' +
74
+ missing.join('\n') +
75
+ '\n';
76
+ await writeFile(gitignorePath, existing + block, 'utf8');
77
+ return missing;
78
+ }
79
+ export function registerMcpCommand(program) {
80
+ program
81
+ .command('mcp:init')
82
+ .description('Write .cursor/mcp.json + .vscode/mcp.json wiring the Actuate MCP server')
83
+ .option('--url <baseUrl>', 'Base URL of the running CMS', 'http://localhost:3000')
84
+ .option('--key <apiKey>', 'Actuate API key (act_sk_...)', PLACEHOLDER_KEY)
85
+ .option('--force', 'Replace an existing "actuate" server entry (or unparseable config)')
86
+ .action(async (opts) => {
87
+ const root = process.cwd();
88
+ if (opts.key !== PLACEHOLDER_KEY && !opts.key.startsWith('act_sk_')) {
89
+ logger.warn('The provided --key does not look like an Actuate API key (expected act_sk_ prefix).');
90
+ }
91
+ const targets = [
92
+ { relPath: '.cursor/mcp.json', rootKey: 'mcpServers', entry: cursorEntry(opts) },
93
+ { relPath: '.vscode/mcp.json', rootKey: 'servers', entry: vscodeEntry(opts) },
94
+ ];
95
+ let failed = false;
96
+ for (const target of targets) {
97
+ const result = await mergeConfigFile(root, target, Boolean(opts.force));
98
+ if (result === 'created')
99
+ logger.success(`Created ${target.relPath}`);
100
+ else if (result === 'updated')
101
+ logger.success(`Updated ${target.relPath}`);
102
+ else if (result === 'error')
103
+ failed = true;
104
+ }
105
+ const ignored = await ensureGitignored(root, targets.map((t) => t.relPath));
106
+ if (ignored.length > 0) {
107
+ logger.success(`Added to .gitignore: ${ignored.join(', ')}`);
108
+ }
109
+ console.log();
110
+ console.log(chalk.bold('Next steps'));
111
+ if (opts.key === PLACEHOLDER_KEY) {
112
+ console.log(` 1. Mint an API key in ${chalk.cyan('Admin → Settings → API Keys')} (act_sk_...)`);
113
+ console.log(` 2. Replace ${chalk.cyan(PLACEHOLDER_KEY)} in both config files with the real key`);
114
+ console.log(' 3. Restart Cursor / VS Code so the MCP server is picked up');
115
+ }
116
+ else {
117
+ console.log(' 1. Restart Cursor / VS Code so the MCP server is picked up');
118
+ }
119
+ if (opts.url === 'http://localhost:3000') {
120
+ console.log(chalk.dim(' Tip: pass --url https://your-site.com to target a deployed CMS instead.'));
121
+ }
122
+ if (failed)
123
+ process.exitCode = 1;
124
+ });
125
+ }
126
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../../src/commands/mcp.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { registerDbStatusCommand } from './commands/db-status.js';
14
14
  import { registerInitCommand } from './commands/init.js';
15
15
  import { registerDeployCheckCommand, registerDoctorCommand, registerVerifyCommand, } from './commands/doctor.js';
16
16
  import { registerVercelBlobLinkCommand } from './commands/vercel-blob-link.js';
17
+ import { registerMcpCommand } from './commands/mcp.js';
17
18
  const program = new Command();
18
19
  program
19
20
  .name('actuate')
@@ -35,5 +36,6 @@ registerDoctorCommand(program);
35
36
  registerDeployCheckCommand(program);
36
37
  registerVerifyCommand(program);
37
38
  registerVercelBlobLinkCommand(program);
39
+ registerMcpCommand(program);
38
40
  program.parse();
39
41
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAA;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;AAE9E,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;AAE7B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,6DAA6D,CAAC;KAC1E,OAAO,CAAC,OAAO,CAAC,CAAA;AAEnB,sBAAsB,CAAC,OAAO,CAAC,CAAA;AAC/B,8BAA8B,CAAC,OAAO,CAAC,CAAA;AACvC,uBAAuB,CAAC,OAAO,CAAC,CAAA;AAChC,mBAAmB,CAAC,OAAO,CAAC,CAAA;AAC5B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,sBAAsB,CAAC,OAAO,CAAC,CAAA;AAC/B,0BAA0B,CAAC,OAAO,CAAC,CAAA;AACnC,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,uBAAuB,CAAC,OAAO,CAAC,CAAA;AAChC,mBAAmB,CAAC,OAAO,CAAC,CAAA;AAC5B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,0BAA0B,CAAC,OAAO,CAAC,CAAA;AACnC,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,6BAA6B,CAAC,OAAO,CAAC,CAAA;AAEtC,OAAO,CAAC,KAAK,EAAE,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAA;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AAC9D,OAAO,EAAE,0BAA0B,EAAE,MAAM,4BAA4B,CAAA;AACvE,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAA;AAC7D,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,EACL,0BAA0B,EAC1B,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAA;AAC9E,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAA;AAEtD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAA;AAE7B,OAAO;KACJ,IAAI,CAAC,SAAS,CAAC;KACf,WAAW,CAAC,6DAA6D,CAAC;KAC1E,OAAO,CAAC,OAAO,CAAC,CAAA;AAEnB,sBAAsB,CAAC,OAAO,CAAC,CAAA;AAC/B,8BAA8B,CAAC,OAAO,CAAC,CAAA;AACvC,uBAAuB,CAAC,OAAO,CAAC,CAAA;AAChC,mBAAmB,CAAC,OAAO,CAAC,CAAA;AAC5B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,sBAAsB,CAAC,OAAO,CAAC,CAAA;AAC/B,0BAA0B,CAAC,OAAO,CAAC,CAAA;AACnC,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,uBAAuB,CAAC,OAAO,CAAC,CAAA;AAChC,mBAAmB,CAAC,OAAO,CAAC,CAAA;AAC5B,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,0BAA0B,CAAC,OAAO,CAAC,CAAA;AACnC,qBAAqB,CAAC,OAAO,CAAC,CAAA;AAC9B,6BAA6B,CAAC,OAAO,CAAC,CAAA;AACtC,kBAAkB,CAAC,OAAO,CAAC,CAAA;AAE3B,OAAO,CAAC,KAAK,EAAE,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@actuate-media/cli",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "CLI for Actuate CMS — migrations, codegen, imports, exports, and upgrades",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,7 +28,7 @@
28
28
  "devDependencies": {
29
29
  "typescript": "^5.7.0",
30
30
  "vitest": "^4.1.0",
31
- "@actuate-media/cms-core": "0.61.0"
31
+ "@actuate-media/cms-core": "0.63.0"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsc",
@@ -0,0 +1,125 @@
1
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
2
+ import { tmpdir } from 'node:os'
3
+ import path from 'node:path'
4
+ import { Command } from 'commander'
5
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
6
+
7
+ import { registerMcpCommand } from '../commands/mcp.js'
8
+
9
+ /**
10
+ * `actuate mcp:init` retrofits MCP editor configs onto existing projects.
11
+ * Pins the merge contract: other configured servers are never clobbered, an
12
+ * existing `actuate` entry needs `--force`, and both config paths end up
13
+ * git-ignored (they hold a real API key once configured).
14
+ */
15
+
16
+ let projectDir: string
17
+ let originalCwd: string
18
+
19
+ async function run(args: string[] = []): Promise<void> {
20
+ const program = new Command()
21
+ program.exitOverride()
22
+ registerMcpCommand(program)
23
+ await program.parseAsync(['mcp:init', ...args], { from: 'user' })
24
+ }
25
+
26
+ async function readJson(relPath: string): Promise<any> {
27
+ return JSON.parse(await readFile(path.join(projectDir, relPath), 'utf8'))
28
+ }
29
+
30
+ beforeEach(async () => {
31
+ originalCwd = process.cwd()
32
+ projectDir = await mkdtemp(path.join(tmpdir(), 'actuate-mcp-init-'))
33
+ process.chdir(projectDir)
34
+ })
35
+
36
+ afterEach(async () => {
37
+ process.chdir(originalCwd)
38
+ process.exitCode = 0
39
+ await rm(projectDir, { recursive: true, force: true })
40
+ })
41
+
42
+ describe('mcp:init', () => {
43
+ it('creates both editor configs with placeholder key and git-ignores them', async () => {
44
+ await writeFile(path.join(projectDir, '.gitignore'), 'node_modules\n', 'utf8')
45
+ await run()
46
+
47
+ const cursor = await readJson('.cursor/mcp.json')
48
+ expect(cursor.mcpServers.actuate.command).toBe('npx')
49
+ expect(cursor.mcpServers.actuate.args).toEqual(['-y', '@actuate-media/mcp-server'])
50
+ expect(cursor.mcpServers.actuate.env.ACTUATE_BASE_URL).toBe('http://localhost:3000')
51
+ expect(cursor.mcpServers.actuate.env.ACTUATE_API_KEY).toBe('act_sk_REPLACE_WITH_YOUR_KEY')
52
+
53
+ const vscode = await readJson('.vscode/mcp.json')
54
+ expect(vscode.servers.actuate.type).toBe('stdio')
55
+
56
+ const gitignore = await readFile(path.join(projectDir, '.gitignore'), 'utf8')
57
+ expect(gitignore).toContain('node_modules')
58
+ expect(gitignore).toContain('.cursor/mcp.json')
59
+ expect(gitignore).toContain('.vscode/mcp.json')
60
+ })
61
+
62
+ it('applies --url and --key to both configs', async () => {
63
+ await run(['--url', 'https://example.com', '--key', 'act_sk_real123'])
64
+
65
+ const cursor = await readJson('.cursor/mcp.json')
66
+ expect(cursor.mcpServers.actuate.env).toEqual({
67
+ ACTUATE_BASE_URL: 'https://example.com',
68
+ ACTUATE_API_KEY: 'act_sk_real123',
69
+ })
70
+ const vscode = await readJson('.vscode/mcp.json')
71
+ expect(vscode.servers.actuate.env.ACTUATE_BASE_URL).toBe('https://example.com')
72
+ })
73
+
74
+ it('merges into an existing config without clobbering other servers', async () => {
75
+ await mkdir(path.join(projectDir, '.cursor'), { recursive: true })
76
+ await writeFile(
77
+ path.join(projectDir, '.cursor', 'mcp.json'),
78
+ JSON.stringify({ mcpServers: { sentry: { command: 'sentry-mcp' } } }),
79
+ 'utf8',
80
+ )
81
+ await run()
82
+
83
+ const cursor = await readJson('.cursor/mcp.json')
84
+ expect(cursor.mcpServers.sentry).toEqual({ command: 'sentry-mcp' })
85
+ expect(cursor.mcpServers.actuate).toBeDefined()
86
+ })
87
+
88
+ it('leaves an existing actuate entry alone unless --force is passed', async () => {
89
+ await mkdir(path.join(projectDir, '.cursor'), { recursive: true })
90
+ const existing = {
91
+ mcpServers: { actuate: { command: 'custom', env: { ACTUATE_BASE_URL: 'https://keep.me' } } },
92
+ }
93
+ await writeFile(path.join(projectDir, '.cursor', 'mcp.json'), JSON.stringify(existing), 'utf8')
94
+
95
+ await run()
96
+ let cursor = await readJson('.cursor/mcp.json')
97
+ expect(cursor.mcpServers.actuate.command).toBe('custom')
98
+
99
+ await run(['--force', '--url', 'https://new.example.com'])
100
+ cursor = await readJson('.cursor/mcp.json')
101
+ expect(cursor.mcpServers.actuate.command).toBe('npx')
102
+ expect(cursor.mcpServers.actuate.env.ACTUATE_BASE_URL).toBe('https://new.example.com')
103
+ })
104
+
105
+ it('fails on invalid JSON without --force, overwrites with --force', async () => {
106
+ await mkdir(path.join(projectDir, '.cursor'), { recursive: true })
107
+ await writeFile(path.join(projectDir, '.cursor', 'mcp.json'), '{ not json', 'utf8')
108
+
109
+ await run()
110
+ expect(process.exitCode).toBe(1)
111
+ process.exitCode = 0
112
+
113
+ await run(['--force'])
114
+ const cursor = await readJson('.cursor/mcp.json')
115
+ expect(cursor.mcpServers.actuate).toBeDefined()
116
+ })
117
+
118
+ it('does not duplicate gitignore entries on re-runs', async () => {
119
+ await run()
120
+ await run(['--force'])
121
+ const gitignore = await readFile(path.join(projectDir, '.gitignore'), 'utf8')
122
+ const count = gitignore.split('\n').filter((l) => l.trim() === '.cursor/mcp.json').length
123
+ expect(count).toBe(1)
124
+ })
125
+ })
@@ -0,0 +1,181 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises'
3
+ import path from 'node:path'
4
+ import { Command } from 'commander'
5
+ import chalk from 'chalk'
6
+ import { logger } from '../utils/logger.js'
7
+
8
+ /**
9
+ * `actuate mcp:init` — retrofit the Actuate MCP server config onto an
10
+ * existing project.
11
+ *
12
+ * New scaffolds (`npm create actuate-cms`) emit `.cursor/mcp.json` and
13
+ * `.vscode/mcp.json` automatically; projects created before that feature (or
14
+ * set up by hand) have no easy path. This command writes both editor configs,
15
+ * merging into existing files so other MCP servers are preserved, and ensures
16
+ * both paths are git-ignored (a real `act_sk_...` key pasted into them must
17
+ * never be committed).
18
+ */
19
+
20
+ interface McpInitOptions {
21
+ url: string
22
+ key: string
23
+ force?: boolean
24
+ }
25
+
26
+ const PLACEHOLDER_KEY = 'act_sk_REPLACE_WITH_YOUR_KEY'
27
+
28
+ /** Cursor / Claude Desktop shape: top-level `mcpServers`. */
29
+ function cursorEntry(opts: McpInitOptions): Record<string, unknown> {
30
+ return {
31
+ command: 'npx',
32
+ args: ['-y', '@actuate-media/mcp-server'],
33
+ env: { ACTUATE_BASE_URL: opts.url, ACTUATE_API_KEY: opts.key },
34
+ }
35
+ }
36
+
37
+ /** VS Code shape: top-level `servers`, each entry declares `type: "stdio"`. */
38
+ function vscodeEntry(opts: McpInitOptions): Record<string, unknown> {
39
+ return { type: 'stdio', ...cursorEntry(opts) }
40
+ }
41
+
42
+ interface MergeTarget {
43
+ /** Path relative to the project root, also used in log lines. */
44
+ relPath: string
45
+ /** Top-level key the editor expects (`mcpServers` vs `servers`). */
46
+ rootKey: 'mcpServers' | 'servers'
47
+ entry: Record<string, unknown>
48
+ }
49
+
50
+ type MergeResult = 'created' | 'updated' | 'skipped' | 'error'
51
+
52
+ /**
53
+ * Create or merge one editor config. Existing files keep every other server
54
+ * entry; an existing `actuate` entry is only replaced with `--force`.
55
+ */
56
+ async function mergeConfigFile(
57
+ root: string,
58
+ target: MergeTarget,
59
+ force: boolean,
60
+ ): Promise<MergeResult> {
61
+ const filePath = path.join(root, target.relPath)
62
+
63
+ let config: Record<string, unknown> = {}
64
+ let existed = false
65
+ if (existsSync(filePath)) {
66
+ existed = true
67
+ const raw = await readFile(filePath, 'utf8')
68
+ try {
69
+ const parsed = JSON.parse(raw) as unknown
70
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
71
+ config = parsed as Record<string, unknown>
72
+ } else if (!force) {
73
+ logger.error(`${target.relPath} is not a JSON object — fix it or re-run with --force.`)
74
+ return 'error'
75
+ }
76
+ } catch {
77
+ if (!force) {
78
+ logger.error(`${target.relPath} contains invalid JSON — fix it or re-run with --force.`)
79
+ return 'error'
80
+ }
81
+ config = {}
82
+ }
83
+ }
84
+
85
+ const serversRaw = config[target.rootKey]
86
+ const servers =
87
+ serversRaw && typeof serversRaw === 'object' && !Array.isArray(serversRaw)
88
+ ? (serversRaw as Record<string, unknown>)
89
+ : {}
90
+
91
+ if (servers.actuate && !force) {
92
+ logger.warn(`${target.relPath} already has an "actuate" server — use --force to replace it.`)
93
+ return 'skipped'
94
+ }
95
+
96
+ config[target.rootKey] = { ...servers, actuate: target.entry }
97
+
98
+ await mkdir(path.dirname(filePath), { recursive: true })
99
+ await writeFile(filePath, JSON.stringify(config, null, 2) + '\n', 'utf8')
100
+ return existed ? 'updated' : 'created'
101
+ }
102
+
103
+ /**
104
+ * Make sure both MCP config paths are git-ignored. Appends only the missing
105
+ * entries; creates `.gitignore` if the project somehow has none.
106
+ */
107
+ async function ensureGitignored(root: string, relPaths: string[]): Promise<string[]> {
108
+ const gitignorePath = path.join(root, '.gitignore')
109
+ const existing = existsSync(gitignorePath) ? await readFile(gitignorePath, 'utf8') : ''
110
+ const lines = new Set(existing.split(/\r?\n/).map((l) => l.trim()))
111
+ const missing = relPaths.filter((p) => !lines.has(p))
112
+ if (missing.length === 0) return []
113
+
114
+ const block =
115
+ (existing && !existing.endsWith('\n') ? '\n' : '') +
116
+ '\n# MCP configs hold a real API key once configured — never commit them\n' +
117
+ missing.join('\n') +
118
+ '\n'
119
+ await writeFile(gitignorePath, existing + block, 'utf8')
120
+ return missing
121
+ }
122
+
123
+ export function registerMcpCommand(program: Command): void {
124
+ program
125
+ .command('mcp:init')
126
+ .description('Write .cursor/mcp.json + .vscode/mcp.json wiring the Actuate MCP server')
127
+ .option('--url <baseUrl>', 'Base URL of the running CMS', 'http://localhost:3000')
128
+ .option('--key <apiKey>', 'Actuate API key (act_sk_...)', PLACEHOLDER_KEY)
129
+ .option('--force', 'Replace an existing "actuate" server entry (or unparseable config)')
130
+ .action(async (opts: McpInitOptions) => {
131
+ const root = process.cwd()
132
+
133
+ if (opts.key !== PLACEHOLDER_KEY && !opts.key.startsWith('act_sk_')) {
134
+ logger.warn(
135
+ 'The provided --key does not look like an Actuate API key (expected act_sk_ prefix).',
136
+ )
137
+ }
138
+
139
+ const targets: MergeTarget[] = [
140
+ { relPath: '.cursor/mcp.json', rootKey: 'mcpServers', entry: cursorEntry(opts) },
141
+ { relPath: '.vscode/mcp.json', rootKey: 'servers', entry: vscodeEntry(opts) },
142
+ ]
143
+
144
+ let failed = false
145
+ for (const target of targets) {
146
+ const result = await mergeConfigFile(root, target, Boolean(opts.force))
147
+ if (result === 'created') logger.success(`Created ${target.relPath}`)
148
+ else if (result === 'updated') logger.success(`Updated ${target.relPath}`)
149
+ else if (result === 'error') failed = true
150
+ }
151
+
152
+ const ignored = await ensureGitignored(
153
+ root,
154
+ targets.map((t) => t.relPath),
155
+ )
156
+ if (ignored.length > 0) {
157
+ logger.success(`Added to .gitignore: ${ignored.join(', ')}`)
158
+ }
159
+
160
+ console.log()
161
+ console.log(chalk.bold('Next steps'))
162
+ if (opts.key === PLACEHOLDER_KEY) {
163
+ console.log(
164
+ ` 1. Mint an API key in ${chalk.cyan('Admin → Settings → API Keys')} (act_sk_...)`,
165
+ )
166
+ console.log(
167
+ ` 2. Replace ${chalk.cyan(PLACEHOLDER_KEY)} in both config files with the real key`,
168
+ )
169
+ console.log(' 3. Restart Cursor / VS Code so the MCP server is picked up')
170
+ } else {
171
+ console.log(' 1. Restart Cursor / VS Code so the MCP server is picked up')
172
+ }
173
+ if (opts.url === 'http://localhost:3000') {
174
+ console.log(
175
+ chalk.dim(' Tip: pass --url https://your-site.com to target a deployed CMS instead.'),
176
+ )
177
+ }
178
+
179
+ if (failed) process.exitCode = 1
180
+ })
181
+ }
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  registerVerifyCommand,
19
19
  } from './commands/doctor.js'
20
20
  import { registerVercelBlobLinkCommand } from './commands/vercel-blob-link.js'
21
+ import { registerMcpCommand } from './commands/mcp.js'
21
22
 
22
23
  const program = new Command()
23
24
 
@@ -42,5 +43,6 @@ registerDoctorCommand(program)
42
43
  registerDeployCheckCommand(program)
43
44
  registerVerifyCommand(program)
44
45
  registerVercelBlobLinkCommand(program)
46
+ registerMcpCommand(program)
45
47
 
46
48
  program.parse()