@_davideast/stitch-mcp 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/chunk-22bymmh9.js +137 -0
  2. package/dist/chunk-22bymmh9.js.map +10 -0
  3. package/dist/chunk-387yyye2.js +248 -0
  4. package/dist/chunk-387yyye2.js.map +14 -0
  5. package/dist/chunk-3ff2k44g.js +19 -0
  6. package/dist/chunk-3ff2k44g.js.map +9 -0
  7. package/dist/chunk-46cay1zp.js +273 -0
  8. package/dist/chunk-46cay1zp.js.map +10 -0
  9. package/dist/chunk-48e1jpm8.js +167 -0
  10. package/dist/chunk-48e1jpm8.js.map +10 -0
  11. package/dist/chunk-4arzkk1s.js +69 -0
  12. package/dist/chunk-4arzkk1s.js.map +10 -0
  13. package/dist/chunk-4js7vw6h.js +415 -0
  14. package/dist/chunk-4js7vw6h.js.map +20 -0
  15. package/dist/chunk-4p1wfk3t.js +736 -0
  16. package/dist/chunk-4p1wfk3t.js.map +16 -0
  17. package/dist/chunk-4zg969tc.js +370 -0
  18. package/dist/chunk-4zg969tc.js.map +16 -0
  19. package/dist/chunk-5jjt7tgw.js +247 -0
  20. package/dist/chunk-5jjt7tgw.js.map +14 -0
  21. package/dist/chunk-5t2h8z2n.js +273 -0
  22. package/dist/chunk-5t2h8z2n.js.map +10 -0
  23. package/dist/chunk-5zy3et1m.js +759 -0
  24. package/dist/chunk-5zy3et1m.js.map +19 -0
  25. package/dist/chunk-6wvst7z8.js +125 -0
  26. package/dist/chunk-6wvst7z8.js.map +12 -0
  27. package/dist/chunk-8mm08arm.js +256 -0
  28. package/dist/chunk-8mm08arm.js.map +11 -0
  29. package/dist/chunk-8nv4wwv0.js +19 -0
  30. package/dist/chunk-8nv4wwv0.js.map +9 -0
  31. package/dist/chunk-9cjny9z2.js +19 -0
  32. package/dist/chunk-9cjny9z2.js.map +9 -0
  33. package/dist/chunk-9ggfw78s.js +19 -0
  34. package/dist/chunk-9ggfw78s.js.map +9 -0
  35. package/dist/chunk-9nyyn1hn.js +125 -0
  36. package/dist/chunk-9nyyn1hn.js.map +12 -0
  37. package/dist/chunk-9nzbvp6r.js +19 -0
  38. package/dist/chunk-9nzbvp6r.js.map +9 -0
  39. package/dist/chunk-a0gmbh8e.js +415 -0
  40. package/dist/chunk-a0gmbh8e.js.map +20 -0
  41. package/dist/chunk-acv998mp.js +941 -0
  42. package/dist/chunk-acv998mp.js.map +27 -0
  43. package/dist/chunk-cpjsvven.js +10 -0
  44. package/dist/chunk-cpjsvven.js.map +9 -0
  45. package/dist/chunk-dmrahbs1.js +31467 -0
  46. package/dist/chunk-dmrahbs1.js.map +245 -0
  47. package/dist/chunk-dts6851a.js +137 -0
  48. package/dist/chunk-dts6851a.js.map +10 -0
  49. package/dist/chunk-efd0rm6g.js +69 -0
  50. package/dist/chunk-efd0rm6g.js.map +10 -0
  51. package/dist/chunk-f1kjn6cd.js +16554 -0
  52. package/dist/chunk-f1kjn6cd.js.map +98 -0
  53. package/dist/chunk-f5f6ekgp.js +109 -0
  54. package/dist/chunk-f5f6ekgp.js.map +10 -0
  55. package/dist/chunk-fmewhfh3.js +137 -0
  56. package/dist/chunk-fmewhfh3.js.map +10 -0
  57. package/dist/chunk-fmhrn6cg.js +6382 -0
  58. package/dist/chunk-fmhrn6cg.js.map +109 -0
  59. package/dist/chunk-gh7dzfhb.js +256 -0
  60. package/dist/chunk-gh7dzfhb.js.map +11 -0
  61. package/dist/chunk-gwhtc0fe.js +370 -0
  62. package/dist/chunk-gwhtc0fe.js.map +16 -0
  63. package/dist/chunk-hc7sdwmn.js +449 -0
  64. package/dist/chunk-hc7sdwmn.js.map +17 -0
  65. package/dist/chunk-k4xwb3wp.js +44184 -0
  66. package/dist/chunk-k4xwb3wp.js.map +237 -0
  67. package/dist/chunk-k751yfm6.js +736 -0
  68. package/dist/chunk-k751yfm6.js.map +16 -0
  69. package/dist/chunk-kkc2tvar.js +19 -0
  70. package/dist/chunk-kkc2tvar.js.map +9 -0
  71. package/dist/chunk-kva47mgc.js +10 -0
  72. package/dist/chunk-kva47mgc.js.map +9 -0
  73. package/dist/chunk-nv2j020p.js +45210 -0
  74. package/dist/chunk-nv2j020p.js.map +258 -0
  75. package/dist/chunk-pdj9s41r.js +683 -0
  76. package/dist/chunk-pdj9s41r.js.map +17 -0
  77. package/dist/chunk-ppphsy4t.js +273 -0
  78. package/dist/chunk-ppphsy4t.js.map +10 -0
  79. package/dist/chunk-psmw9zpa.js +941 -0
  80. package/dist/chunk-psmw9zpa.js.map +27 -0
  81. package/dist/chunk-pz31v3ma.js +24 -0
  82. package/dist/chunk-pz31v3ma.js.map +9 -0
  83. package/dist/chunk-qbt0906e.js +1495 -0
  84. package/dist/chunk-qbt0906e.js.map +23 -0
  85. package/dist/chunk-rh3k09h7.js +19 -0
  86. package/dist/chunk-rh3k09h7.js.map +9 -0
  87. package/dist/chunk-saznae7w.js +10 -0
  88. package/dist/chunk-saznae7w.js.map +9 -0
  89. package/dist/chunk-sscqdg1j.js +69 -0
  90. package/dist/chunk-sscqdg1j.js.map +10 -0
  91. package/dist/chunk-stgj6y85.js +835 -0
  92. package/dist/chunk-stgj6y85.js.map +19 -0
  93. package/dist/chunk-t3tqmxyj.js +42 -0
  94. package/dist/chunk-t3tqmxyj.js.map +9 -0
  95. package/dist/chunk-v8ak35zd.js +94 -0
  96. package/dist/chunk-v8ak35zd.js.map +10 -0
  97. package/dist/chunk-w9acw256.js +167 -0
  98. package/dist/chunk-w9acw256.js.map +10 -0
  99. package/dist/chunk-wq60thjn.js +167 -0
  100. package/dist/chunk-wq60thjn.js.map +10 -0
  101. package/dist/chunk-wzkdeb8a.js +736 -0
  102. package/dist/chunk-wzkdeb8a.js.map +16 -0
  103. package/dist/chunk-xxv6j815.js +256 -0
  104. package/dist/chunk-xxv6j815.js.map +11 -0
  105. package/dist/chunk-yjnj35q8.js +1495 -0
  106. package/dist/chunk-yjnj35q8.js.map +23 -0
  107. package/dist/chunk-yvzzrczy.js +45206 -0
  108. package/dist/chunk-yvzzrczy.js.map +258 -0
  109. package/dist/chunk-ywax1akt.js +370 -0
  110. package/dist/chunk-ywax1akt.js.map +16 -0
  111. package/dist/chunk-zakq9pc0.js +4989 -0
  112. package/dist/chunk-zakq9pc0.js.map +65 -0
  113. package/dist/commands/doctor/command.js +1 -1
  114. package/dist/commands/doctor/steps/AdcProjectCheckStep.d.ts +8 -0
  115. package/dist/commands/init/command.js +1 -1
  116. package/dist/commands/logout/command.js +1 -1
  117. package/dist/commands/proxy/command.js +1 -1
  118. package/dist/commands/screens/command.js +2 -2
  119. package/dist/commands/serve/command.js +3 -3
  120. package/dist/commands/site/command.js +1 -1
  121. package/dist/commands/snapshot/command.js +1 -1
  122. package/dist/commands/tool/command.js +1 -1
  123. package/dist/commands/view/command.js +1 -1
  124. package/dist/index.js +4 -4
  125. package/dist/lib/server/AssetGateway.d.ts +12 -0
  126. package/package.json +1 -1
@@ -0,0 +1,1495 @@
1
+ import {
2
+ StitchHandler,
3
+ createSpinner
4
+ } from "./chunk-psmw9zpa.js";
5
+ import {
6
+ ConsoleUI,
7
+ GcloudHandler,
8
+ execCommand,
9
+ promptConfirm,
10
+ promptInput,
11
+ promptSelect
12
+ } from "./chunk-zakq9pc0.js";
13
+ import {
14
+ runSteps
15
+ } from "./chunk-f2hq6bfv.js";
16
+ import {
17
+ icons,
18
+ theme
19
+ } from "./chunk-kbtqrkwh.js";
20
+ import {
21
+ __require
22
+ } from "./chunk-9wyra8hs.js";
23
+
24
+ // src/services/project/handler.ts
25
+ class ProjectHandler {
26
+ gcloudService;
27
+ constructor(gcloudService) {
28
+ this.gcloudService = gcloudService;
29
+ }
30
+ async selectProject(input) {
31
+ try {
32
+ const projectsResult = await this.gcloudService.listProjects({
33
+ limit: input.limit,
34
+ sortBy: "~createTime"
35
+ });
36
+ if (!projectsResult.success) {
37
+ return {
38
+ success: false,
39
+ error: {
40
+ code: "SEARCH_FAILED",
41
+ message: "Failed to fetch projects",
42
+ suggestion: "Ensure you are authenticated and have access to GCP projects",
43
+ recoverable: true
44
+ }
45
+ };
46
+ }
47
+ const projects = projectsResult.data.projects;
48
+ if (projects.length === 0) {
49
+ return {
50
+ success: false,
51
+ error: {
52
+ code: "NO_PROJECTS_FOUND",
53
+ message: "No projects found in your account",
54
+ suggestion: "Create a project at https://console.cloud.google.com",
55
+ recoverable: false
56
+ }
57
+ };
58
+ }
59
+ const choices = [
60
+ ...input.allowSearch ? [{ name: theme.gray("\uD83D\uDD0D Search for a project..."), value: "__SEARCH__" }] : [],
61
+ ...projects.map((p) => ({
62
+ name: `${p.name} ${theme.gray(`(${p.projectId})`)}`,
63
+ value: p.projectId
64
+ }))
65
+ ];
66
+ const selected = await promptSelect("Select a project", choices);
67
+ if (selected === "__SEARCH__") {
68
+ return await this.searchAndSelect();
69
+ }
70
+ const selectedProject = projects.find((p) => p.projectId === selected);
71
+ if (!selectedProject) {
72
+ return {
73
+ success: false,
74
+ error: {
75
+ code: "SELECTION_CANCELLED",
76
+ message: "Project selection failed",
77
+ recoverable: true
78
+ }
79
+ };
80
+ }
81
+ return {
82
+ success: true,
83
+ data: {
84
+ projectId: selectedProject.projectId,
85
+ name: selectedProject.name
86
+ }
87
+ };
88
+ } catch (error) {
89
+ return {
90
+ success: false,
91
+ error: {
92
+ code: "UNKNOWN_ERROR",
93
+ message: error instanceof Error ? error.message : String(error),
94
+ recoverable: false
95
+ }
96
+ };
97
+ }
98
+ }
99
+ async getProjectDetails(input) {
100
+ try {
101
+ const projectResult = await this.gcloudService.listProjects({
102
+ filter: `projectId:${input.projectId}`,
103
+ limit: 1
104
+ });
105
+ if (!projectResult.success) {
106
+ return {
107
+ success: false,
108
+ error: {
109
+ code: "PROJECT_FETCH_FAILED",
110
+ message: `Failed to fetch project details: ${projectResult.error.message}`,
111
+ recoverable: true
112
+ }
113
+ };
114
+ }
115
+ if (projectResult.data.projects.length === 0) {
116
+ return {
117
+ success: false,
118
+ error: {
119
+ code: "PROJECT_NOT_FOUND",
120
+ message: `Project not found: ${input.projectId}`,
121
+ recoverable: true
122
+ }
123
+ };
124
+ }
125
+ const project = projectResult.data.projects[0];
126
+ return {
127
+ success: true,
128
+ data: {
129
+ projectId: project.projectId,
130
+ name: project.name
131
+ }
132
+ };
133
+ } catch (error) {
134
+ return {
135
+ success: false,
136
+ error: {
137
+ code: "UNKNOWN_ERROR",
138
+ message: error instanceof Error ? error.message : String(error),
139
+ recoverable: false
140
+ }
141
+ };
142
+ }
143
+ }
144
+ async searchAndSelect() {
145
+ try {
146
+ const query = await promptInput("Enter project name or ID to search (press Enter)");
147
+ if (!query.trim()) {
148
+ return {
149
+ success: false,
150
+ error: {
151
+ code: "SELECTION_CANCELLED",
152
+ message: "Search cancelled",
153
+ recoverable: true
154
+ }
155
+ };
156
+ }
157
+ const searchResult = await this.gcloudService.listProjects({
158
+ filter: `name:*${query}* OR projectId:*${query}*`,
159
+ limit: 5
160
+ });
161
+ if (!searchResult.success) {
162
+ return {
163
+ success: false,
164
+ error: {
165
+ code: "SEARCH_FAILED",
166
+ message: `Search failed: ${searchResult.error.message}`,
167
+ recoverable: true
168
+ }
169
+ };
170
+ }
171
+ const projects = searchResult.data.projects;
172
+ if (projects.length === 0) {
173
+ const useManual = await promptConfirm(`No projects found matching "${query}". Use "${query}" as project ID?`, false);
174
+ if (useManual) {
175
+ return {
176
+ success: true,
177
+ data: {
178
+ projectId: query,
179
+ name: query
180
+ }
181
+ };
182
+ }
183
+ return {
184
+ success: false,
185
+ error: {
186
+ code: "NO_PROJECTS_FOUND",
187
+ message: `No projects found matching "${query}"`,
188
+ suggestion: "Try a different search term or select from recent projects",
189
+ recoverable: true
190
+ }
191
+ };
192
+ }
193
+ const choices = projects.map((p) => ({
194
+ name: `${p.name} ${theme.gray(`(${p.projectId})`)}`,
195
+ value: p.projectId
196
+ }));
197
+ const selected = await promptSelect(`Search results for "${query}"`, choices);
198
+ const selectedProject = projects.find((p) => p.projectId === selected);
199
+ if (!selectedProject) {
200
+ return {
201
+ success: false,
202
+ error: {
203
+ code: "SELECTION_CANCELLED",
204
+ message: "Selection cancelled",
205
+ recoverable: true
206
+ }
207
+ };
208
+ }
209
+ return {
210
+ success: true,
211
+ data: {
212
+ projectId: selectedProject.projectId,
213
+ name: selectedProject.name
214
+ }
215
+ };
216
+ } catch (error) {
217
+ return {
218
+ success: false,
219
+ error: {
220
+ code: "UNKNOWN_ERROR",
221
+ message: error instanceof Error ? error.message : String(error),
222
+ recoverable: false
223
+ }
224
+ };
225
+ }
226
+ }
227
+ }
228
+
229
+ // src/services/mcp-config/handler.ts
230
+ class McpConfigHandler {
231
+ async generateConfig(input) {
232
+ try {
233
+ const config = input.transport === "http" ? this.generateHttpConfig(input) : this.generateStdioConfig(input);
234
+ const configString = config ? JSON.stringify(config, null, 2) : "";
235
+ const instructions = this.getInstructionsForClient(input.client, configString, input.transport, input.projectId, input.apiKey);
236
+ return {
237
+ success: true,
238
+ data: {
239
+ config: configString,
240
+ instructions
241
+ }
242
+ };
243
+ } catch (error) {
244
+ return {
245
+ success: false,
246
+ error: {
247
+ code: "CONFIG_GENERATION_FAILED",
248
+ message: error instanceof Error ? error.message : String(error),
249
+ recoverable: false
250
+ }
251
+ };
252
+ }
253
+ }
254
+ generateHttpConfig(input) {
255
+ switch (input.client) {
256
+ case "cursor":
257
+ return this.generateCursorConfig(input.projectId, input.apiKey);
258
+ case "antigravity":
259
+ return this.generateAntigravityConfig(input.projectId, input.apiKey);
260
+ case "vscode":
261
+ return this.generateVSCodeConfig(input.projectId, input.apiKey);
262
+ case "claude-code":
263
+ return this.generateClaudeCodeConfig();
264
+ case "gemini-cli":
265
+ return this.generateGeminiCliConfig();
266
+ case "codex":
267
+ return null;
268
+ case "opencode":
269
+ return this.generateOpencodeConfig(input.apiKey);
270
+ }
271
+ }
272
+ generateCursorConfig(projectId, apiKey) {
273
+ const headers = apiKey ? { "X-Goog-Api-Key": apiKey } : {
274
+ Authorization: "Bearer <YOUR_ACCESS_TOKEN>",
275
+ "X-Goog-User-Project": projectId
276
+ };
277
+ return {
278
+ mcpServers: {
279
+ stitch: {
280
+ url: "https://stitch.googleapis.com/mcp",
281
+ headers
282
+ }
283
+ }
284
+ };
285
+ }
286
+ generateAntigravityConfig(projectId, apiKey) {
287
+ const headers = apiKey ? { "X-Goog-Api-Key": apiKey } : {
288
+ Authorization: "Bearer <YOUR_ACCESS_TOKEN>",
289
+ "X-Goog-User-Project": projectId
290
+ };
291
+ return {
292
+ mcpServers: {
293
+ stitch: {
294
+ serverUrl: "https://stitch.googleapis.com/mcp",
295
+ headers
296
+ }
297
+ }
298
+ };
299
+ }
300
+ generateVSCodeConfig(projectId, apiKey) {
301
+ if (apiKey) {
302
+ return {
303
+ servers: {
304
+ stitch: {
305
+ type: "http",
306
+ url: "https://stitch.googleapis.com/mcp",
307
+ headers: {
308
+ Accept: "application/json",
309
+ "X-Goog-Api-Key": apiKey
310
+ }
311
+ }
312
+ }
313
+ };
314
+ }
315
+ return {
316
+ inputs: [
317
+ {
318
+ type: "promptString",
319
+ id: "stitch-access-token",
320
+ description: "Google Cloud Access Token (run: gcloud auth print-access-token)",
321
+ password: true
322
+ }
323
+ ],
324
+ servers: {
325
+ stitch: {
326
+ type: "http",
327
+ url: "https://stitch.googleapis.com/mcp",
328
+ headers: {
329
+ Authorization: "Bearer ${input:stitch-access-token}",
330
+ "X-Goog-User-Project": projectId
331
+ }
332
+ }
333
+ }
334
+ };
335
+ }
336
+ generateClaudeCodeConfig() {
337
+ return null;
338
+ }
339
+ generateGeminiCliConfig() {
340
+ return null;
341
+ }
342
+ generateOpencodeConfig(apiKey) {
343
+ const headers = apiKey ? { "X-Goog-Api-Key": apiKey } : {
344
+ Authorization: "Bearer $STITCH_ACCESS_TOKEN",
345
+ "X-Goog-User-Project": "$GOOGLE_CLOUD_PROJECT"
346
+ };
347
+ return {
348
+ $schema: "https://opencode.ai/config.json",
349
+ mcp: {
350
+ stitch: {
351
+ type: "remote",
352
+ url: "https://stitch.googleapis.com/mcp",
353
+ headers
354
+ }
355
+ }
356
+ };
357
+ }
358
+ generateStdioConfig(input) {
359
+ if (input.client === "claude-code" || input.client === "gemini-cli" || input.client === "codex") {
360
+ return null;
361
+ }
362
+ const env = {};
363
+ if (!input.apiKey) {
364
+ env.STITCH_PROJECT_ID = input.projectId;
365
+ } else {
366
+ env.STITCH_API_KEY = input.apiKey;
367
+ }
368
+ if (input.client === "vscode") {
369
+ return {
370
+ servers: {
371
+ stitch: {
372
+ type: "stdio",
373
+ command: "npx",
374
+ args: ["@_davideast/stitch-mcp", "proxy"],
375
+ env
376
+ }
377
+ }
378
+ };
379
+ }
380
+ if (input.client === "opencode") {
381
+ return {
382
+ $schema: "https://opencode.ai/config.json",
383
+ mcp: {
384
+ stitch: {
385
+ type: "local",
386
+ command: ["npx", "@_davideast/stitch-mcp", "proxy"],
387
+ environment: env
388
+ }
389
+ }
390
+ };
391
+ }
392
+ return {
393
+ mcpServers: {
394
+ stitch: {
395
+ command: "npx",
396
+ args: ["@_davideast/stitch-mcp", "proxy"],
397
+ env
398
+ }
399
+ }
400
+ };
401
+ }
402
+ getInstructionsForClient(client, config, transport, projectId, apiKey) {
403
+ const baseInstructions = `
404
+ ${theme.blue("MCP Configuration Generated")}
405
+
406
+ ${config}
407
+ `;
408
+ const transportNote = transport === "stdio" ? `
409
+ ${theme.yellow("Note:")} This uses the proxy server. Keep it running with:
410
+ npx @_davideast/stitch-mcp proxy
411
+ ` : "";
412
+ const tokenHint = transport === "http" && !apiKey ? `
413
+ ${theme.yellow("To get your access token, run:")}
414
+ ` + ` CLOUDSDK_CONFIG=~/.stitch-mcp/config ~/.stitch-mcp/google-cloud-sdk/bin/gcloud auth print-access-token
415
+ ` + `
416
+ ${theme.yellow("Important:")} Replace ${theme.blue("<YOUR_ACCESS_TOKEN>")} in the config with the token from the command above.
417
+ ` + `Access tokens expire after 1 hour. Consider using ${theme.blue("stdio")} transport for automatic refresh.
418
+ ` : "";
419
+ const vscodeTokenHint = transport === "http" && !apiKey ? `
420
+ ${theme.yellow("To get your access token, run:")}
421
+ ` + ` CLOUDSDK_CONFIG=~/.stitch-mcp/config ~/.stitch-mcp/google-cloud-sdk/bin/gcloud auth print-access-token
422
+ ` + `
423
+ ${theme.yellow("Important:")} When prompted, paste the token from the command above.
424
+ ` + `Access tokens expire after 1 hour. Consider using ${theme.blue("stdio")} transport for automatic refresh.
425
+ ` : "";
426
+ switch (client) {
427
+ case "antigravity":
428
+ if (transport === "stdio") {
429
+ return baseInstructions + transportNote + `
430
+ ${theme.green("Next Steps for Antigravity:")}
431
+ ` + `1. In the Agent Panel, click the three dots in the top right
432
+ ` + `2. Select "MCP Servers" → "Manage MCP Servers"
433
+ ` + `3. Select "View raw config" and add the above configuration
434
+ ` + `4. Restart Antigravity to load the configuration
435
+ `;
436
+ }
437
+ return baseInstructions + tokenHint + `
438
+ ${theme.green("Next Steps for Antigravity:")}
439
+ ` + `1. In the Agent Panel, click the three dots in the top right
440
+ ` + `2. Select "MCP Servers" → "Manage MCP Servers"
441
+ ` + `3. Select "View raw config" and add the above configuration
442
+ ` + `4. Restart Antigravity to load the configuration
443
+ `;
444
+ case "vscode":
445
+ if (transport === "stdio") {
446
+ return baseInstructions + `
447
+ ${theme.green("Next Steps for VSCode:")}
448
+ ` + `1. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)
449
+ ` + `2. Run "MCP: Open User Configuration" or "MCP: Open Workspace Folder Configuration"
450
+ ` + `3. Add the above configuration to the mcp.json file
451
+ ` + `4. VS Code will automatically start the proxy server when needed
452
+ `;
453
+ }
454
+ return baseInstructions + vscodeTokenHint + `
455
+ ${theme.green("Next Steps for VSCode:")}
456
+ ` + `1. Open the Command Palette (Ctrl+Shift+P or Cmd+Shift+P)
457
+ ` + `2. Run "MCP: Open User Configuration" or "MCP: Open Workspace Folder Configuration"
458
+ ` + `3. Add the above configuration to the mcp.json file
459
+ ` + (apiKey ? "" : `4. When prompted, paste the access token from the command above
460
+ `) + `5. Restart VS Code or run "MCP: List Servers" to start the server
461
+ `;
462
+ case "cursor":
463
+ if (transport === "stdio") {
464
+ return baseInstructions + transportNote + `
465
+ ${theme.green("Next Steps for Cursor:")}
466
+ ` + `1. Create a .cursor/mcp.json file in your project root
467
+ ` + `2. Add the above configuration to the file
468
+ ` + `3. Restart Cursor to load the configuration
469
+ `;
470
+ }
471
+ return baseInstructions + tokenHint + `
472
+ ${theme.green("Next Steps for Cursor:")}
473
+ ` + `1. Create a .cursor/mcp.json file in your project root
474
+ ` + `2. Add the above configuration to the file
475
+ ` + `3. Restart Cursor to load the configuration
476
+ `;
477
+ case "claude-code":
478
+ if (transport === "stdio") {
479
+ let envHint = "";
480
+ if (apiKey) {
481
+ envHint = `${theme.blue(` -e STITCH_API_KEY=${apiKey} \\`)}
482
+ `;
483
+ } else if (projectId) {
484
+ envHint = `${theme.blue(` -e STITCH_PROJECT_ID=${projectId} \\`)}
485
+ `;
486
+ }
487
+ return transportNote + `
488
+ ${theme.green("Setup Claude Code:")}
489
+
490
+ ` + `Run the following command to add the Stitch MCP server:
491
+
492
+ ` + `${theme.blue("claude mcp add stitch \\")}
493
+ ` + envHint + `${theme.blue(" -- npx @_davideast/stitch-mcp proxy")}`;
494
+ } else {
495
+ if (apiKey) {
496
+ return `
497
+ ${theme.green("Setup Claude Code:")}
498
+
499
+ ` + `Run the following command to add the Stitch MCP server:
500
+
501
+ ` + `${theme.blue("claude mcp add stitch \\")}
502
+ ` + `${theme.blue(" --transport http https://stitch.googleapis.com/mcp \\")}
503
+ ` + `${theme.blue(` --header "X-Goog-Api-Key: ${apiKey}" \\`)}
504
+ ` + `${theme.blue(" -s user")}
505
+
506
+ ` + `${theme.yellow("Note:")} -s user saves to $HOME/.claude.json, use -s project for ./.mcp.json
507
+ `;
508
+ }
509
+ return tokenHint + `
510
+ ${theme.green("Setup Claude Code:")}
511
+
512
+ ` + `Run the following command to add the Stitch MCP server:
513
+
514
+ ` + `${theme.blue("claude mcp add stitch \\")}
515
+ ` + `${theme.blue(" --transport http https://stitch.googleapis.com/mcp \\")}
516
+ ` + `${theme.blue(' --header "Authorization: Bearer <YOUR_ACCESS_TOKEN>" \\')}
517
+ ` + `${theme.blue(` --header "X-Goog-User-Project: ${projectId}" \\`)}
518
+ ` + `${theme.blue(" -s user")}
519
+
520
+ ` + `${theme.yellow("Note:")} -s user saves to $HOME/.claude.json, use -s project for ./.mcp.json
521
+ `;
522
+ }
523
+ case "gemini-cli":
524
+ return transportNote + `
525
+ ${theme.green("Setup Gemini CLI:")}
526
+
527
+ ` + `Install the Stitch extension for the Gemini CLI:
528
+
529
+ ` + `${theme.blue("gemini extensions install https://github.com/gemini-cli-extensions/stitch")}
530
+ `;
531
+ case "codex": {
532
+ const isHttp = transport === "http";
533
+ let configBlock;
534
+ if (isHttp) {
535
+ if (apiKey) {
536
+ configBlock = [
537
+ "[mcp_servers.stitch]",
538
+ 'url = "https://stitch.googleapis.com/mcp"',
539
+ "",
540
+ "[mcp_servers.stitch.env_http_headers]",
541
+ `X-Goog-Api-Key = "${apiKey}"`
542
+ ].join(`
543
+ `);
544
+ } else {
545
+ configBlock = [
546
+ "[mcp_servers.stitch]",
547
+ 'url = "https://stitch.googleapis.com/mcp"',
548
+ 'bearer_token_env_var = "STITCH_ACCESS_TOKEN"',
549
+ "",
550
+ "[mcp_servers.stitch.env_http_headers]",
551
+ 'X-Goog-User-Project = "GOOGLE_CLOUD_PROJECT"'
552
+ ].join(`
553
+ `);
554
+ }
555
+ } else {
556
+ if (apiKey) {
557
+ configBlock = [
558
+ "[mcp_servers.stitch]",
559
+ 'command = "npx"',
560
+ 'args = ["@_davideast/stitch-mcp", "proxy"]',
561
+ "",
562
+ "[mcp_servers.stitch.env]",
563
+ `STITCH_API_KEY = "${apiKey}"`
564
+ ].join(`
565
+ `);
566
+ } else {
567
+ configBlock = [
568
+ "[mcp_servers.stitch]",
569
+ 'command = "npx"',
570
+ 'args = ["@_davideast/stitch-mcp", "proxy"]',
571
+ "",
572
+ "[mcp_servers.stitch.env]",
573
+ `STITCH_PROJECT_ID = "${projectId}"`
574
+ ].join(`
575
+ `);
576
+ }
577
+ }
578
+ const note = isHttp && !apiKey ? `${theme.yellow("Note:")} Direct mode requires a valid access token in ${theme.blue("STITCH_ACCESS_TOKEN")} and a project id in ${theme.blue("GOOGLE_CLOUD_PROJECT")}.
579
+ ` : `${theme.yellow("Note:")} Proxy mode handles token refresh automatically.
580
+ `;
581
+ return `
582
+ ${theme.green("Setup Codex CLI:")}
583
+
584
+ ` + `Add this to ${theme.blue("~/.codex/config.toml")}:
585
+
586
+ ` + `${configBlock}
587
+
588
+ ` + note;
589
+ }
590
+ case "opencode": {
591
+ const fileName = transport === "http" ? "opencode.json" : "opencode.json";
592
+ return baseInstructions + transportNote + `
593
+ ${theme.green("Setup OpenCode:")}
594
+
595
+ ` + `1. Add the above configuration to ${theme.blue(fileName)} in your project root
596
+ ` + `2. If using HTTP transport, OpenCode will automatically handle OAuth when you first use the MCP server
597
+ ` + `3. If using STDIO transport, make sure the proxy server is running with:
598
+ ` + ` ${theme.blue("npx @_davideast/stitch-mcp proxy")}
599
+
600
+ ` + `${theme.gray("Note:")} You can now use Stitch tools by adding "use the stitch tool" to your prompts.
601
+ `;
602
+ }
603
+ default:
604
+ return baseInstructions + transportNote + `
605
+ ${theme.yellow("Add this configuration to your MCP client.")}
606
+ `;
607
+ }
608
+ }
609
+ }
610
+
611
+ // src/ui/checklist/handler.ts
612
+ var STATE_ICONS = {
613
+ PENDING: "○",
614
+ IN_PROGRESS: "▸",
615
+ COMPLETE: "✓",
616
+ SKIPPED: "−",
617
+ FAILED: "✗"
618
+ };
619
+
620
+ class ChecklistUIHandler {
621
+ config;
622
+ states = new Map;
623
+ lastOutputLines = 0;
624
+ initialize(config) {
625
+ this.config = config;
626
+ this.states.clear();
627
+ const initItem = (item) => {
628
+ this.states.set(item.id, { state: "PENDING" });
629
+ item.children?.forEach(initItem);
630
+ };
631
+ config.items.forEach(initItem);
632
+ }
633
+ updateItem(input) {
634
+ const current = this.states.get(input.itemId);
635
+ if (!current) {
636
+ return {
637
+ success: false,
638
+ error: {
639
+ code: "ITEM_NOT_FOUND",
640
+ message: `Item "${input.itemId}" not found`,
641
+ recoverable: false
642
+ }
643
+ };
644
+ }
645
+ const previousState = current.state;
646
+ this.states.set(input.itemId, {
647
+ state: input.state,
648
+ detail: input.detail,
649
+ reason: input.reason
650
+ });
651
+ return {
652
+ success: true,
653
+ data: {
654
+ itemId: input.itemId,
655
+ previousState,
656
+ newState: input.state
657
+ }
658
+ };
659
+ }
660
+ render() {
661
+ try {
662
+ const lines = [];
663
+ const { completed, total, percent } = this.getProgress();
664
+ lines.push(`\uD83E\uDDF5 ${this.config.title}`);
665
+ lines.push("");
666
+ this.config.items.forEach((item, idx) => {
667
+ const state = this.states.get(item.id);
668
+ const icon = STATE_ICONS[state.state];
669
+ const color = this.getStateColor(state.state);
670
+ let line = ` ${color(icon)} ${idx + 1}. ${item.label}`;
671
+ if (state.detail) {
672
+ line += ` ${theme.gray("·")} ${state.detail}`;
673
+ }
674
+ lines.push(line);
675
+ if (state.reason) {
676
+ lines.push(` └─ ${theme.gray(state.reason)}`);
677
+ }
678
+ });
679
+ if (this.config.showProgress) {
680
+ lines.push("");
681
+ const barWidth = 40;
682
+ const filled = Math.round(percent / 100 * barWidth);
683
+ const bar = "━".repeat(filled) + "─".repeat(barWidth - filled);
684
+ lines.push(` ${bar} ${percent}%`);
685
+ }
686
+ return {
687
+ success: true,
688
+ data: {
689
+ output: lines.join(`
690
+ `),
691
+ completedCount: completed,
692
+ totalCount: total,
693
+ percentComplete: percent
694
+ }
695
+ };
696
+ } catch (error) {
697
+ return {
698
+ success: false,
699
+ error: {
700
+ code: "RENDER_FAILED",
701
+ message: error instanceof Error ? error.message : String(error),
702
+ recoverable: false
703
+ }
704
+ };
705
+ }
706
+ }
707
+ print(options) {
708
+ if (options?.clearPrevious && this.lastOutputLines > 0) {
709
+ process.stdout.write(`\x1B[${this.lastOutputLines}A\x1B[0J`);
710
+ }
711
+ const result = this.render();
712
+ if (result.success) {
713
+ console.log(result.data.output);
714
+ this.lastOutputLines = result.data.output.split(`
715
+ `).length;
716
+ }
717
+ }
718
+ getProgress() {
719
+ const total = this.states.size;
720
+ let completed = 0;
721
+ this.states.forEach((state) => {
722
+ if (state.state === "COMPLETE" || state.state === "SKIPPED") {
723
+ completed++;
724
+ }
725
+ });
726
+ return {
727
+ completed,
728
+ total,
729
+ percent: Math.round(completed / total * 100)
730
+ };
731
+ }
732
+ isComplete() {
733
+ for (const state of this.states.values()) {
734
+ if (state.state === "PENDING" || state.state === "IN_PROGRESS") {
735
+ return false;
736
+ }
737
+ }
738
+ return true;
739
+ }
740
+ getStateColor(state) {
741
+ switch (state) {
742
+ case "COMPLETE":
743
+ return theme.green;
744
+ case "SKIPPED":
745
+ return theme.gray;
746
+ case "FAILED":
747
+ return theme.red;
748
+ case "IN_PROGRESS":
749
+ return theme.yellow;
750
+ default:
751
+ return theme.gray;
752
+ }
753
+ }
754
+ }
755
+
756
+ // src/commands/init/steps/ClientSelectionStep.ts
757
+ class ClientSelectionStep {
758
+ id = "mcp-client";
759
+ name = "Select MCP client";
760
+ async shouldRun(context) {
761
+ return true;
762
+ }
763
+ async run(context) {
764
+ if (context.input.client) {
765
+ try {
766
+ context.mcpClient = this.resolveMcpClient(context.input.client);
767
+ return {
768
+ success: true,
769
+ detail: context.mcpClient,
770
+ status: "SKIPPED",
771
+ reason: "Set via --client flag"
772
+ };
773
+ } catch (e) {
774
+ return {
775
+ success: false,
776
+ error: e instanceof Error ? e : new Error(String(e))
777
+ };
778
+ }
779
+ }
780
+ context.mcpClient = await context.ui.promptMcpClient();
781
+ return {
782
+ success: true,
783
+ detail: context.mcpClient
784
+ };
785
+ }
786
+ resolveMcpClient(input) {
787
+ const map = {
788
+ antigravity: "antigravity",
789
+ agy: "antigravity",
790
+ vscode: "vscode",
791
+ vsc: "vscode",
792
+ cursor: "cursor",
793
+ cur: "cursor",
794
+ "claude-code": "claude-code",
795
+ cc: "claude-code",
796
+ "gemini-cli": "gemini-cli",
797
+ gcli: "gemini-cli",
798
+ codex: "codex",
799
+ cdx: "codex",
800
+ opencode: "opencode",
801
+ opc: "opencode"
802
+ };
803
+ const normalized = input.trim().toLowerCase();
804
+ const client = map[normalized];
805
+ if (!client) {
806
+ throw new Error(`Invalid client '${input}'. Supported: antigravity (agy), vscode (vsc), cursor (cur), claude-code (cc), gemini-cli (gcli), codex (cdx), opencode (opc)`);
807
+ }
808
+ return client;
809
+ }
810
+ }
811
+
812
+ // src/commands/init/steps/AuthModeStep.ts
813
+ import fs from "node:fs";
814
+ import path from "node:path";
815
+
816
+ class AuthModeStep {
817
+ id = "authentication-mode";
818
+ name = "Select Authentication Mode";
819
+ async shouldRun(context) {
820
+ return true;
821
+ }
822
+ async run(context) {
823
+ const authMode = await context.ui.promptAuthMode();
824
+ context.authMode = authMode;
825
+ if (authMode === "apiKey") {
826
+ const storage = await context.ui.promptApiKeyStorage();
827
+ if (storage === "config") {
828
+ context.apiKey = await context.ui.promptApiKey();
829
+ } else if (storage === "skip") {
830
+ context.apiKey = "YOUR-API-KEY";
831
+ } else if (storage === ".env") {
832
+ const inputKey = await context.ui.promptApiKey();
833
+ context.apiKey = "YOUR-API-KEY";
834
+ const envPath = path.join(process.cwd(), ".env");
835
+ const envContent = `
836
+ STITCH_API_KEY=${inputKey}
837
+ `;
838
+ try {
839
+ await fs.promises.writeFile(envPath, envContent, { flag: "a", mode: 384 });
840
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
841
+ try {
842
+ const gitignoreContent = await fs.promises.readFile(gitignorePath, "utf8");
843
+ if (!gitignoreContent.includes(".env")) {
844
+ await fs.promises.appendFile(gitignorePath, `
845
+ .env
846
+ `);
847
+ }
848
+ } catch (err) {
849
+ if (err.code === "ENOENT") {
850
+ await fs.promises.writeFile(gitignorePath, `.env
851
+ `);
852
+ } else {
853
+ throw err;
854
+ }
855
+ }
856
+ } catch (e) {
857
+ context.ui.warn(`Warning: Failed to update .env or .gitignore: ${e instanceof Error ? e.message : String(e)}`);
858
+ }
859
+ }
860
+ return {
861
+ success: true,
862
+ detail: "API Key",
863
+ status: "COMPLETE"
864
+ };
865
+ }
866
+ return {
867
+ success: true,
868
+ detail: "OAuth",
869
+ status: "COMPLETE"
870
+ };
871
+ }
872
+ }
873
+
874
+ // src/commands/init/steps/GcloudInstallStep.ts
875
+ class GcloudInstallStep {
876
+ id = "gcloud-cli";
877
+ name = "Install Google Cloud CLI";
878
+ async shouldRun(context) {
879
+ return context.authMode !== "apiKey";
880
+ }
881
+ async run(context) {
882
+ if (context.authMode === "apiKey") {
883
+ return { success: true, status: "SKIPPED", reason: "Not required for API Key" };
884
+ }
885
+ const gcloudResult = await context.gcloudService.ensureInstalled({
886
+ minVersion: "400.0.0",
887
+ forceLocal: context.input.local
888
+ });
889
+ if (!gcloudResult.success) {
890
+ return {
891
+ success: false,
892
+ error: new Error(gcloudResult.error.message),
893
+ detail: gcloudResult.error.message
894
+ };
895
+ }
896
+ return {
897
+ success: true,
898
+ detail: `v${gcloudResult.data.version} (${gcloudResult.data.location})`
899
+ };
900
+ }
901
+ }
902
+
903
+ // src/platform/environment.ts
904
+ import fs2 from "node:fs";
905
+ function detectWSL() {
906
+ try {
907
+ const procVersion = fs2.readFileSync("/proc/version", "utf8").toLowerCase();
908
+ return procVersion.includes("microsoft") || procVersion.includes("wsl");
909
+ } catch {
910
+ return false;
911
+ }
912
+ }
913
+ function detectSSH() {
914
+ return Boolean(process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION);
915
+ }
916
+ function detectDocker() {
917
+ try {
918
+ return fs2.existsSync("/.dockerenv");
919
+ } catch {
920
+ return false;
921
+ }
922
+ }
923
+ function detectCloudShell() {
924
+ return Boolean(process.env.CLOUD_SHELL);
925
+ }
926
+ function detectDisplay() {
927
+ return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
928
+ }
929
+ function detectEnvironment() {
930
+ const isWSL = detectWSL();
931
+ const isSSH = detectSSH();
932
+ const isDocker = detectDocker();
933
+ const isCloudShell = detectCloudShell();
934
+ const hasDisplay = detectDisplay();
935
+ let needsNoBrowser = false;
936
+ let reason;
937
+ if (isWSL) {
938
+ needsNoBrowser = true;
939
+ reason = "WSL detected - browser redirect to localhost may not work";
940
+ } else if (isSSH && !hasDisplay) {
941
+ needsNoBrowser = true;
942
+ reason = "SSH session without display forwarding";
943
+ } else if (isDocker) {
944
+ needsNoBrowser = true;
945
+ reason = "Docker container detected";
946
+ } else if (isCloudShell) {
947
+ needsNoBrowser = true;
948
+ reason = "Cloud Shell detected";
949
+ } else if (!hasDisplay) {
950
+ needsNoBrowser = true;
951
+ reason = "No display detected (headless environment)";
952
+ }
953
+ return {
954
+ isWSL,
955
+ isSSH,
956
+ isDocker,
957
+ isCloudShell,
958
+ hasDisplay,
959
+ needsNoBrowser,
960
+ reason
961
+ };
962
+ }
963
+
964
+ // src/commands/init/steps/AuthStep.ts
965
+ import path2 from "node:path";
966
+ class AuthStep {
967
+ id = "authentication";
968
+ name = "Authenticate with Google";
969
+ async shouldRun(context) {
970
+ return context.authMode !== "apiKey";
971
+ }
972
+ async run(context) {
973
+ if (context.authMode === "apiKey") {
974
+ return { success: true, status: "SKIPPED", reason: "Using API Key" };
975
+ }
976
+ const env = detectEnvironment();
977
+ if (env.needsNoBrowser && env.reason) {
978
+ context.ui.warn(`
979
+ ⚠ ${env.reason}`);
980
+ context.ui.log(` If browser auth fails, copy the URL from terminal and open manually.
981
+ `);
982
+ }
983
+ const existingAccount = await context.gcloudService.getActiveAccount();
984
+ const hasADC = await context.gcloudService.hasADC();
985
+ if (existingAccount && hasADC) {
986
+ context.authAccount = existingAccount;
987
+ return {
988
+ success: true,
989
+ detail: existingAccount,
990
+ status: "SKIPPED",
991
+ reason: "Already authenticated"
992
+ };
993
+ }
994
+ const gcloudInfo = await context.gcloudService.ensureInstalled({
995
+ minVersion: "400.0.0",
996
+ forceLocal: context.input.local
997
+ });
998
+ if (!gcloudInfo.success)
999
+ return { success: false, error: new Error("Gcloud not found") };
1000
+ const isBundled = gcloudInfo.data.location === "bundled";
1001
+ const gcloudBinDir = path2.dirname(gcloudInfo.data.path);
1002
+ let configPrefix = "";
1003
+ if (isBundled) {
1004
+ const configPath = path2.dirname(gcloudBinDir) + "/../config";
1005
+ configPrefix = `CLOUDSDK_CONFIG="${configPath}"`;
1006
+ context.ui.warn(`
1007
+ Configure gcloud PATH
1008
+ `);
1009
+ context.ui.log(` Open a NEW terminal tab/window and run this command:
1010
+ `);
1011
+ context.ui.log(theme.cyan(` export PATH="${gcloudBinDir}:$PATH"
1012
+ `));
1013
+ try {
1014
+ const { default: clipboard } = await import("./chunk-kad4d9sf.js");
1015
+ await clipboard.write(`export PATH="${gcloudBinDir}:$PATH"`);
1016
+ context.ui.log(theme.gray(" (copied to clipboard)"));
1017
+ } catch {}
1018
+ await context.ui.promptConfirm("Press Enter when complete", true);
1019
+ }
1020
+ if (!existingAccount) {
1021
+ context.ui.warn(`
1022
+ Authenticate with Google Cloud
1023
+ `);
1024
+ context.ui.log(theme.cyan(` ${configPrefix} gcloud auth login
1025
+ `));
1026
+ await context.ui.promptConfirm("Press Enter when complete", true);
1027
+ }
1028
+ if (!hasADC) {
1029
+ context.ui.warn(`
1030
+ Authorize Application Default Credentials
1031
+ `);
1032
+ context.ui.log(theme.cyan(` ${configPrefix} gcloud auth application-default login
1033
+ `));
1034
+ await context.ui.promptConfirm("Press Enter when complete", true);
1035
+ }
1036
+ const verifyAccount = await context.gcloudService.getActiveAccount();
1037
+ if (!verifyAccount) {
1038
+ return {
1039
+ success: false,
1040
+ error: new Error("No authenticated account found after setup"),
1041
+ detail: "No account found",
1042
+ errorCode: "AUTH_FAILED"
1043
+ };
1044
+ }
1045
+ context.authAccount = verifyAccount;
1046
+ return {
1047
+ success: true,
1048
+ detail: verifyAccount
1049
+ };
1050
+ }
1051
+ }
1052
+
1053
+ // src/commands/init/steps/TransportStep.ts
1054
+ class TransportStep {
1055
+ id = "connection-method";
1056
+ name = "Choose connection method";
1057
+ async shouldRun(context) {
1058
+ return true;
1059
+ }
1060
+ async run(context) {
1061
+ if (context.input.transport) {
1062
+ context.transport = this.resolveTransport(context.input.transport);
1063
+ const transportLabel2 = context.transport === "http" ? "Direct" : "Proxy";
1064
+ return {
1065
+ success: true,
1066
+ detail: transportLabel2,
1067
+ status: "SKIPPED",
1068
+ reason: "Set via --transport flag"
1069
+ };
1070
+ }
1071
+ context.transport = await context.ui.promptTransportType(context.authMode);
1072
+ const transportLabel = context.transport === "http" ? "Direct" : "Proxy";
1073
+ return {
1074
+ success: true,
1075
+ detail: transportLabel
1076
+ };
1077
+ }
1078
+ resolveTransport(input) {
1079
+ const normalized = input.trim().toLowerCase();
1080
+ if (normalized === "http")
1081
+ return "http";
1082
+ if (normalized === "stdio")
1083
+ return "stdio";
1084
+ throw new Error(`Invalid transport '${input}'. Supported: http, stdio`);
1085
+ }
1086
+ }
1087
+
1088
+ // src/commands/init/steps/ProjectSelectStep.ts
1089
+ class ProjectSelectStep {
1090
+ id = "project-selection";
1091
+ name = "Select Google Cloud project";
1092
+ async shouldRun(context) {
1093
+ return context.authMode !== "apiKey";
1094
+ }
1095
+ async run(context) {
1096
+ if (context.authMode === "apiKey") {
1097
+ return { success: true, status: "SKIPPED", reason: "Not required for API Key" };
1098
+ }
1099
+ let projectResult = null;
1100
+ const activeProjectId = await context.gcloudService.getProjectId();
1101
+ if (activeProjectId) {
1102
+ const detailsResult = await context.projectService.getProjectDetails({ projectId: activeProjectId });
1103
+ if (detailsResult.success) {
1104
+ const useActive = context.input.defaults || context.input.autoVerify ? true : await context.ui.promptConfirm(`Use active project: ${detailsResult.data.name} (${detailsResult.data.projectId})?`, true);
1105
+ if (useActive) {
1106
+ projectResult = detailsResult;
1107
+ }
1108
+ }
1109
+ }
1110
+ if (!projectResult) {
1111
+ projectResult = await context.projectService.selectProject({
1112
+ allowSearch: true,
1113
+ limit: 5
1114
+ });
1115
+ }
1116
+ if (!projectResult.success) {
1117
+ const error = projectResult.error || { message: "Unknown error" };
1118
+ return { success: false, error: new Error(error.message) };
1119
+ }
1120
+ const setProjectResult = await context.gcloudService.setProject({
1121
+ projectId: projectResult.data.projectId
1122
+ });
1123
+ if (!setProjectResult.success) {
1124
+ const error = setProjectResult.error || { message: "Unknown error" };
1125
+ return { success: false, error: new Error(error.message) };
1126
+ }
1127
+ context.projectId = projectResult.data.projectId;
1128
+ return {
1129
+ success: true,
1130
+ detail: context.projectId
1131
+ };
1132
+ }
1133
+ }
1134
+
1135
+ // src/commands/init/steps/IamApiStep.ts
1136
+ class IamApiStep {
1137
+ id = "iam-and-api";
1138
+ name = "Configure IAM & enable API";
1139
+ async shouldRun(context) {
1140
+ return context.authMode !== "apiKey";
1141
+ }
1142
+ async run(context) {
1143
+ if (context.authMode === "apiKey") {
1144
+ return { success: true, status: "SKIPPED", reason: "Not required for API Key" };
1145
+ }
1146
+ if (!context.projectId || !context.authAccount) {
1147
+ return { success: false, error: new Error("Project ID or Auth Account missing") };
1148
+ }
1149
+ const hasIAMRole = await context.stitchService.checkIAMRole({
1150
+ projectId: context.projectId,
1151
+ userEmail: context.authAccount
1152
+ });
1153
+ if (!hasIAMRole) {
1154
+ const shouldConfigureIam = context.input.autoVerify || await context.ui.promptConfirm("Add the required IAM role to your account?", true);
1155
+ if (shouldConfigureIam) {
1156
+ await context.stitchService.configureIAM({
1157
+ projectId: context.projectId,
1158
+ userEmail: context.authAccount
1159
+ });
1160
+ }
1161
+ }
1162
+ await context.gcloudService.installBetaComponents();
1163
+ const isApiEnabled = await context.stitchService.checkAPIEnabled({
1164
+ projectId: context.projectId
1165
+ });
1166
+ if (!isApiEnabled) {
1167
+ await context.stitchService.enableAPI({
1168
+ projectId: context.projectId
1169
+ });
1170
+ }
1171
+ context.accessToken = await context.gcloudService.getAccessToken() || undefined;
1172
+ if (!context.accessToken) {
1173
+ return { success: false, error: new Error("Could not obtain access token") };
1174
+ }
1175
+ return { success: true, detail: "Ready" };
1176
+ }
1177
+ }
1178
+
1179
+ // src/commands/init/steps/ConfigStep.ts
1180
+ import fs3 from "node:fs";
1181
+ import os from "node:os";
1182
+ import path3 from "node:path";
1183
+
1184
+ class ConfigStep {
1185
+ id = "mcp-config";
1186
+ name = "Generate MCP configuration";
1187
+ async shouldRun(context) {
1188
+ return true;
1189
+ }
1190
+ async run(context) {
1191
+ if (context.mcpClient === "gemini-cli") {
1192
+ await this.setupGeminiExtension(context);
1193
+ }
1194
+ const configResult = await context.mcpConfigService.generateConfig({
1195
+ client: context.mcpClient,
1196
+ projectId: context.projectId || "ignored-project-id",
1197
+ accessToken: context.accessToken,
1198
+ transport: context.transport,
1199
+ authMode: context.authMode,
1200
+ apiKey: context.apiKey
1201
+ });
1202
+ if (!configResult.success) {
1203
+ const error = configResult.error || { message: "Unknown error" };
1204
+ return { success: false, error: new Error(error.message) };
1205
+ }
1206
+ context.instructions = configResult.data.instructions;
1207
+ context.finalConfig = configResult.data.config;
1208
+ return { success: true, detail: "Generated" };
1209
+ }
1210
+ async setupGeminiExtension(context) {
1211
+ const spinner = createSpinner();
1212
+ const extensionPath = path3.join(os.homedir(), ".gemini", "extensions", "Stitch", "gemini-extension.json");
1213
+ let isInstalled = false;
1214
+ try {
1215
+ await fs3.promises.access(extensionPath);
1216
+ isInstalled = true;
1217
+ } catch {
1218
+ isInstalled = false;
1219
+ }
1220
+ if (isInstalled) {
1221
+ spinner.succeed("Stitch extension is already installed");
1222
+ } else {
1223
+ context.ui.log(theme.gray(" > gemini extensions install https://github.com/gemini-cli-extensions/stitch"));
1224
+ const shouldInstall = await context.ui.promptConfirm("Run this command?", true);
1225
+ if (shouldInstall) {
1226
+ spinner.start("Installing Stitch extension...");
1227
+ const installResult = await execCommand(["gemini", "extensions", "install", "https://github.com/gemini-cli-extensions/stitch"]);
1228
+ if (!installResult.success) {
1229
+ spinner.fail("Failed to install Stitch extension");
1230
+ context.ui.log(theme.red(` Error: ${installResult.stderr || installResult.error}`));
1231
+ context.ui.log(theme.gray(" Attempting to configure existing extension..."));
1232
+ } else {
1233
+ spinner.succeed("Extension installed");
1234
+ }
1235
+ }
1236
+ }
1237
+ spinner.start("Configuring extension...");
1238
+ try {
1239
+ await fs3.promises.access(extensionPath);
1240
+ } catch {
1241
+ spinner.fail("Extension configuration file not found");
1242
+ context.ui.log(theme.gray(` Expected path: ${extensionPath}`));
1243
+ return;
1244
+ }
1245
+ try {
1246
+ const content = await fs3.promises.readFile(extensionPath, "utf8");
1247
+ const config = JSON.parse(content);
1248
+ if (!config.mcpServers?.stitch) {
1249
+ spinner.fail("Invalid extension configuration format detected");
1250
+ return;
1251
+ }
1252
+ if (context.transport === "stdio") {
1253
+ const env = {
1254
+ PATH: process.env.PATH || ""
1255
+ };
1256
+ if (context.apiKey) {
1257
+ env.STITCH_API_KEY = context.apiKey;
1258
+ } else {
1259
+ env.STITCH_PROJECT_ID = context.projectId;
1260
+ }
1261
+ config.mcpServers.stitch = {
1262
+ command: "npx",
1263
+ args: ["@_davideast/stitch-mcp", "proxy"],
1264
+ env
1265
+ };
1266
+ await fs3.promises.writeFile(extensionPath, JSON.stringify(config, null, 4));
1267
+ const successMsg = context.apiKey ? "Stitch extension configured for STDIO with API Key" : `Stitch extension configured for STDIO: Project ID set to ${theme.blue(context.projectId)}`;
1268
+ spinner.succeed(successMsg);
1269
+ } else {
1270
+ const existingHeaders = config.mcpServers.stitch.headers || {};
1271
+ if (context.apiKey) {
1272
+ config.mcpServers.stitch = {
1273
+ url: "https://stitch.googleapis.com/mcp",
1274
+ headers: {
1275
+ ...existingHeaders,
1276
+ "X-Goog-Api-Key": context.apiKey
1277
+ }
1278
+ };
1279
+ delete config.mcpServers.stitch.headers["Authorization"];
1280
+ delete config.mcpServers.stitch.headers["X-Goog-User-Project"];
1281
+ await fs3.promises.writeFile(extensionPath, JSON.stringify(config, null, 4));
1282
+ spinner.succeed(`Stitch extension configured for HTTP with API Key`);
1283
+ } else {
1284
+ config.mcpServers.stitch = {
1285
+ url: "https://stitch.googleapis.com/mcp",
1286
+ headers: {
1287
+ Authorization: "Bearer $STITCH_ACCESS_TOKEN",
1288
+ ...existingHeaders,
1289
+ "X-Goog-User-Project": context.projectId
1290
+ }
1291
+ };
1292
+ await fs3.promises.writeFile(extensionPath, JSON.stringify(config, null, 4));
1293
+ spinner.succeed(`Stitch extension configured for HTTP: Project ID set to ${theme.blue(context.projectId)}`);
1294
+ }
1295
+ }
1296
+ context.ui.log(theme.gray(` File: ${extensionPath}`));
1297
+ } catch (e) {
1298
+ spinner.fail("Failed to update extension configuration");
1299
+ context.ui.log(theme.red(` Error: ${e instanceof Error ? e.message : String(e)}`));
1300
+ }
1301
+ }
1302
+ }
1303
+
1304
+ // src/commands/init/steps/TestConnectionStep.ts
1305
+ class TestConnectionStep {
1306
+ id = "connection-test";
1307
+ name = "Test connection";
1308
+ async shouldRun(context) {
1309
+ return context.authMode === "oauth";
1310
+ }
1311
+ async run(context) {
1312
+ if (context.authMode !== "oauth") {
1313
+ return { success: true, status: "SKIPPED", reason: "Not supported for API Key yet" };
1314
+ }
1315
+ if (!context.accessToken) {
1316
+ return { success: false, status: "SKIPPED", reason: "No access token" };
1317
+ }
1318
+ const testResult = await context.stitchService.testConnection({
1319
+ projectId: context.projectId,
1320
+ accessToken: context.accessToken
1321
+ });
1322
+ if (!testResult.success) {
1323
+ const error = testResult.error || { message: "Unknown error", suggestion: "" };
1324
+ context.ui.log(theme.red(`
1325
+ ${icons.error} Error: ${error.message}`));
1326
+ context.ui.warn(` ${error.suggestion}`);
1327
+ return {
1328
+ success: false,
1329
+ detail: error.message,
1330
+ error: new Error(error.message)
1331
+ };
1332
+ }
1333
+ return { success: true, detail: `${testResult.data.statusCode} OK` };
1334
+ }
1335
+ }
1336
+
1337
+ // src/commands/init/handler.ts
1338
+ class InitHandler {
1339
+ gcloudService;
1340
+ mcpConfigService;
1341
+ projectService;
1342
+ stitchService;
1343
+ ui;
1344
+ checklist;
1345
+ steps;
1346
+ constructor(gcloudService, mcpConfigService, projectService, stitchService, ui) {
1347
+ this.gcloudService = gcloudService || new GcloudHandler;
1348
+ this.mcpConfigService = mcpConfigService || new McpConfigHandler;
1349
+ this.projectService = projectService || new ProjectHandler(this.gcloudService);
1350
+ this.stitchService = stitchService || new StitchHandler;
1351
+ this.checklist = new ChecklistUIHandler;
1352
+ this.ui = ui || new ConsoleUI;
1353
+ this.steps = [
1354
+ new ClientSelectionStep,
1355
+ new AuthModeStep,
1356
+ new GcloudInstallStep,
1357
+ new AuthStep,
1358
+ new TransportStep,
1359
+ new ProjectSelectStep,
1360
+ new IamApiStep,
1361
+ new ConfigStep,
1362
+ new TestConnectionStep
1363
+ ];
1364
+ }
1365
+ async execute(input) {
1366
+ this.checklist.initialize({
1367
+ title: "Stitch MCP Setup",
1368
+ items: this.steps.map((s) => ({ id: s.id, label: s.name })),
1369
+ showProgress: true,
1370
+ animationDelayMs: 100
1371
+ });
1372
+ if (!input.json)
1373
+ console.log(`
1374
+ ${theme.blue("\uD83E\uDDF5 Stitch MCP Setup")}
1375
+ `);
1376
+ const context = {
1377
+ input,
1378
+ ui: this.ui,
1379
+ gcloudService: this.gcloudService,
1380
+ mcpConfigService: this.mcpConfigService,
1381
+ projectService: this.projectService,
1382
+ stitchService: this.stitchService
1383
+ };
1384
+ try {
1385
+ const { stoppedAt } = await runSteps(this.steps, context, {
1386
+ onBeforeStep: (step) => {
1387
+ if (!input.json)
1388
+ this.updateStep(step.id, "IN_PROGRESS");
1389
+ },
1390
+ onAfterStep: (step, result2) => {
1391
+ if (!result2.success) {
1392
+ const message = result2.error?.message || result2.detail || "Failed";
1393
+ if (!input.json)
1394
+ this.updateStep(step.id, "FAILED", message);
1395
+ return true;
1396
+ }
1397
+ const status = result2.status || "COMPLETE";
1398
+ if (!input.json)
1399
+ this.updateStep(step.id, status, result2.detail, result2.reason);
1400
+ return false;
1401
+ },
1402
+ onSkippedStep: (step) => {
1403
+ if (!input.json)
1404
+ this.updateStep(step.id, "SKIPPED", "Not required");
1405
+ }
1406
+ });
1407
+ if (stoppedAt) {
1408
+ const message = stoppedAt.result.error?.message || stoppedAt.result.detail || "Failed";
1409
+ return {
1410
+ success: false,
1411
+ error: {
1412
+ code: stoppedAt.result.errorCode || "UNKNOWN_ERROR",
1413
+ message,
1414
+ recoverable: true
1415
+ }
1416
+ };
1417
+ }
1418
+ const result = {
1419
+ success: true,
1420
+ data: {
1421
+ projectId: context.projectId || "ignored",
1422
+ mcpConfig: context.finalConfig || "",
1423
+ instructions: context.instructions || ""
1424
+ }
1425
+ };
1426
+ if (input.json) {
1427
+ console.log(JSON.stringify(result, null, 2));
1428
+ return result;
1429
+ }
1430
+ const { percent } = this.checklist.getProgress();
1431
+ const barWidth = 40;
1432
+ const filled = Math.round(percent / 100 * barWidth);
1433
+ const bar = "━".repeat(filled) + "─".repeat(barWidth - filled);
1434
+ console.log(`
1435
+ ${bar} ${percent}%`);
1436
+ if (this.checklist.isComplete()) {
1437
+ console.log(`
1438
+ ${theme.green("\uD83C\uDF89 Setup complete!")}
1439
+ `);
1440
+ }
1441
+ if (context.instructions) {
1442
+ console.log(context.instructions);
1443
+ }
1444
+ return result;
1445
+ } catch (error) {
1446
+ return {
1447
+ success: false,
1448
+ error: {
1449
+ code: "UNKNOWN_ERROR",
1450
+ message: error instanceof Error ? error.message : String(error),
1451
+ recoverable: false
1452
+ }
1453
+ };
1454
+ }
1455
+ }
1456
+ updateStep(stepId, state, detail, reason) {
1457
+ this.checklist.updateItem({ itemId: stepId, state, detail, reason });
1458
+ if (state !== "IN_PROGRESS") {
1459
+ const step = this.steps.find((s) => s.id === stepId);
1460
+ this.printStepResult(stepId, step?.name || stepId, state, detail, reason);
1461
+ }
1462
+ }
1463
+ printStepResult(stepId, label, state, detail, reason) {
1464
+ const stepIndex = this.steps.findIndex((s) => s.id === stepId);
1465
+ const stepNum = stepIndex + 1;
1466
+ const icons2 = {
1467
+ PENDING: "○",
1468
+ IN_PROGRESS: "▸",
1469
+ COMPLETE: "✓",
1470
+ SKIPPED: "−",
1471
+ FAILED: "✗"
1472
+ };
1473
+ const icon = icons2[state];
1474
+ const colors = {
1475
+ PENDING: theme.gray,
1476
+ IN_PROGRESS: theme.yellow,
1477
+ COMPLETE: theme.green,
1478
+ SKIPPED: theme.gray,
1479
+ FAILED: theme.red
1480
+ };
1481
+ const color = colors[state];
1482
+ let line = ` ${color(icon)} ${stepNum}. ${label}`;
1483
+ if (detail) {
1484
+ line += ` ${theme.gray("·")} ${detail}`;
1485
+ }
1486
+ console.log(line);
1487
+ if (reason) {
1488
+ console.log(` └─ ${theme.gray(reason)}`);
1489
+ }
1490
+ }
1491
+ }
1492
+
1493
+ export { ProjectHandler, McpConfigHandler, InitHandler };
1494
+
1495
+ //# debugId=D9F76D12D96878B564756E2164756E21