mcp-app-studio 0.3.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/{bridge-BOSEqpaS.d.ts → bridge-BXW_-p2R.d.ts} +5 -0
- package/dist/{chunk-QO43ZGJI.js → chunk-2OPSDEPI.js} +20 -11
- package/dist/{chunk-L2RRNF7V.js → chunk-EPZCYA26.js} +24 -2
- package/dist/cli/index.js +426 -109
- package/dist/core/index.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +62 -12
- package/dist/platforms/chatgpt/index.d.ts +1 -0
- package/dist/platforms/chatgpt/index.js +24 -6
- package/dist/platforms/mcp/index.d.ts +3 -3
- package/dist/platforms/mcp/index.js +47 -14
- package/package.json +25 -26
package/dist/cli/index.js
CHANGED
|
@@ -3,22 +3,48 @@ import fs2 from "fs";
|
|
|
3
3
|
import { createWriteStream } from "fs";
|
|
4
4
|
import { Readable } from "stream";
|
|
5
5
|
import { pipeline } from "stream/promises";
|
|
6
|
-
import
|
|
6
|
+
import path3 from "path";
|
|
7
7
|
import os from "os";
|
|
8
8
|
import { fileURLToPath } from "url";
|
|
9
9
|
import * as p from "@clack/prompts";
|
|
10
10
|
import pc from "picocolors";
|
|
11
11
|
import { extract } from "tar";
|
|
12
12
|
|
|
13
|
+
// src/cli/template-utils.ts
|
|
14
|
+
import path from "path";
|
|
15
|
+
function getGithubArchiveTarballUrl(repo, ref) {
|
|
16
|
+
return `https://github.com/${repo}/archive/${ref}.tar.gz`;
|
|
17
|
+
}
|
|
18
|
+
function isTarLinkEntry(entry) {
|
|
19
|
+
if (!entry || typeof entry !== "object") return false;
|
|
20
|
+
if (!("type" in entry)) return false;
|
|
21
|
+
const t = entry.type;
|
|
22
|
+
return t === "SymbolicLink" || t === "Link";
|
|
23
|
+
}
|
|
24
|
+
function assertSafeTarEntryPath(rootDir, entryPath) {
|
|
25
|
+
if (path.isAbsolute(entryPath)) {
|
|
26
|
+
throw new Error(`Template tarball contains an absolute path: ${entryPath}`);
|
|
27
|
+
}
|
|
28
|
+
const resolved = path.resolve(rootDir, entryPath);
|
|
29
|
+
if (resolved !== rootDir && !resolved.startsWith(`${rootDir}${path.sep}`)) {
|
|
30
|
+
throw new Error(`Template tarball contains an unsafe path: ${entryPath}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function filterTemplateTarEntry(rootDir, entryPath, entry) {
|
|
34
|
+
if (isTarLinkEntry(entry)) return false;
|
|
35
|
+
assertSafeTarEntryPath(rootDir, entryPath);
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
// src/cli/utils.ts
|
|
14
40
|
import fs from "fs";
|
|
15
|
-
import
|
|
41
|
+
import path2 from "path";
|
|
16
42
|
function isValidProjectPath(name) {
|
|
17
43
|
if (!name || name.trim() === "") {
|
|
18
44
|
return { valid: false, error: "Project name is required" };
|
|
19
45
|
}
|
|
20
46
|
const trimmed = name.trim();
|
|
21
|
-
if (
|
|
47
|
+
if (path2.isAbsolute(trimmed)) {
|
|
22
48
|
return {
|
|
23
49
|
valid: false,
|
|
24
50
|
error: "Absolute paths are not allowed. Use a relative path or project name."
|
|
@@ -31,9 +57,9 @@ function isValidProjectPath(name) {
|
|
|
31
57
|
};
|
|
32
58
|
}
|
|
33
59
|
const cwd = process.cwd();
|
|
34
|
-
const resolved =
|
|
35
|
-
const relative =
|
|
36
|
-
if (relative.startsWith("..") ||
|
|
60
|
+
const resolved = path2.resolve(cwd, trimmed);
|
|
61
|
+
const relative = path2.relative(cwd, resolved);
|
|
62
|
+
if (relative.startsWith("..") || path2.isAbsolute(relative)) {
|
|
37
63
|
return {
|
|
38
64
|
valid: false,
|
|
39
65
|
error: "Project must be created within current directory."
|
|
@@ -58,11 +84,16 @@ function emptyDir(dir) {
|
|
|
58
84
|
if (!fs.existsSync(dir)) return;
|
|
59
85
|
for (const file of fs.readdirSync(dir)) {
|
|
60
86
|
if (file === ".git") continue;
|
|
61
|
-
fs.rmSync(
|
|
87
|
+
fs.rmSync(path2.join(dir, file), { recursive: true, force: true });
|
|
62
88
|
}
|
|
63
89
|
}
|
|
64
|
-
|
|
65
|
-
|
|
90
|
+
var DEPENDENCY_OVERRIDES = {
|
|
91
|
+
"@assistant-ui/react": "^0.12.3",
|
|
92
|
+
"@assistant-ui/react-ai-sdk": "^1.3.3",
|
|
93
|
+
"@assistant-ui/react-markdown": "^0.12.1"
|
|
94
|
+
};
|
|
95
|
+
function updatePackageJson(dir, name, description, opts) {
|
|
96
|
+
const pkgPath = path2.join(dir, "package.json");
|
|
66
97
|
if (!fs.existsSync(pkgPath)) return;
|
|
67
98
|
try {
|
|
68
99
|
const content = fs.readFileSync(pkgPath, "utf-8");
|
|
@@ -73,6 +104,16 @@ function updatePackageJson(dir, name, description) {
|
|
|
73
104
|
if (description) {
|
|
74
105
|
pkg["description"] = description;
|
|
75
106
|
}
|
|
107
|
+
const deps = pkg["dependencies"] ?? {};
|
|
108
|
+
if (opts?.mcpAppStudioVersion && opts.mcpAppStudioVersion !== "0.0.0") {
|
|
109
|
+
deps["mcp-app-studio"] = `^${opts.mcpAppStudioVersion}`;
|
|
110
|
+
}
|
|
111
|
+
for (const [dep, version] of Object.entries(DEPENDENCY_OVERRIDES)) {
|
|
112
|
+
if (deps[dep]) {
|
|
113
|
+
deps[dep] = version;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
pkg["dependencies"] = deps;
|
|
76
117
|
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
77
118
|
`);
|
|
78
119
|
} catch (error) {
|
|
@@ -90,9 +131,11 @@ function detectPackageManager() {
|
|
|
90
131
|
}
|
|
91
132
|
|
|
92
133
|
// src/cli/index.ts
|
|
93
|
-
var __dirname =
|
|
94
|
-
var TEMPLATE_REPO = "assistant-ui/mcp-app-studio-starter";
|
|
95
|
-
var
|
|
134
|
+
var __dirname = path3.dirname(fileURLToPath(import.meta.url));
|
|
135
|
+
var TEMPLATE_REPO = process.env["MCP_APP_STUDIO_TEMPLATE_REPO"] ?? "assistant-ui/mcp-app-studio-starter";
|
|
136
|
+
var TEMPLATE_REF = process.env["MCP_APP_STUDIO_TEMPLATE_REF"] ?? process.env["MCP_APP_STUDIO_TEMPLATE_BRANCH"] ?? "main";
|
|
137
|
+
var DOCS_URL = "https://github.com/assistant-ui/assistant-ui/tree/main/packages/mcp-app-studio";
|
|
138
|
+
var EXAMPLES_URL = "https://github.com/assistant-ui/mcp-app-studio-starter";
|
|
96
139
|
var REQUIRED_NODE_VERSION = { major: 20, minor: 9, patch: 0 };
|
|
97
140
|
function parseNodeVersion(version) {
|
|
98
141
|
const [majorRaw, minorRaw, patchRaw] = version.split(".");
|
|
@@ -122,7 +165,7 @@ function ensureSupportedNodeVersion() {
|
|
|
122
165
|
}
|
|
123
166
|
function getVersion() {
|
|
124
167
|
try {
|
|
125
|
-
const pkgPath =
|
|
168
|
+
const pkgPath = path3.resolve(__dirname, "../package.json");
|
|
126
169
|
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
127
170
|
return pkg.version || "0.0.0";
|
|
128
171
|
} catch {
|
|
@@ -142,17 +185,22 @@ ${pc.bold("Usage:")}
|
|
|
142
185
|
npx mcp-app-studio [project-name] [options]
|
|
143
186
|
|
|
144
187
|
${pc.bold("Options:")}
|
|
145
|
-
--help, -h
|
|
146
|
-
--version, -v
|
|
188
|
+
--help, -h Show this help message
|
|
189
|
+
--version, -v Show version number
|
|
190
|
+
-y, --yes Non-interactive mode (use defaults or flag values)
|
|
191
|
+
--template <name> Template to use: minimal, poi-map (default: minimal)
|
|
192
|
+
--include-server Include MCP server (default: true)
|
|
193
|
+
--no-server Do not include MCP server
|
|
194
|
+
--description <text> App description
|
|
147
195
|
|
|
148
196
|
${pc.bold("Examples:")}
|
|
149
197
|
npx mcp-app-studio my-app
|
|
150
198
|
npx mcp-app-studio . ${pc.dim("# Use current directory")}
|
|
151
|
-
npx mcp-app-studio
|
|
199
|
+
npx mcp-app-studio my-app -y --template poi-map --include-server
|
|
152
200
|
|
|
153
201
|
${pc.bold("Learn more:")}
|
|
154
|
-
Documentation:
|
|
155
|
-
Examples:
|
|
202
|
+
Documentation: ${DOCS_URL}
|
|
203
|
+
Examples: ${EXAMPLES_URL}
|
|
156
204
|
`);
|
|
157
205
|
}
|
|
158
206
|
function quotePath(p2) {
|
|
@@ -175,6 +223,80 @@ var TEMPLATE_EXPORT_CONFIG = {
|
|
|
175
223
|
exportName: "POIMapSDK"
|
|
176
224
|
}
|
|
177
225
|
};
|
|
226
|
+
var TEMPLATE_DEFAULT_COMPONENT = {
|
|
227
|
+
minimal: "welcome",
|
|
228
|
+
"poi-map": "poi-map"
|
|
229
|
+
};
|
|
230
|
+
function writeStudioConfig(targetDir, template, appName) {
|
|
231
|
+
const exportConfig = TEMPLATE_EXPORT_CONFIG[template];
|
|
232
|
+
const config = {
|
|
233
|
+
widget: {
|
|
234
|
+
entryPoint: exportConfig.entryPoint,
|
|
235
|
+
exportName: exportConfig.exportName,
|
|
236
|
+
name: appName
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
fs2.writeFileSync(
|
|
240
|
+
path3.join(targetDir, "mcp-app-studio.config.json"),
|
|
241
|
+
`${JSON.stringify(config, null, 2)}
|
|
242
|
+
`
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
function updateServerPackageName(targetDir, projectPackageName) {
|
|
246
|
+
const serverPkgPath = path3.join(targetDir, "server", "package.json");
|
|
247
|
+
if (!fs2.existsSync(serverPkgPath)) return;
|
|
248
|
+
const raw = fs2.readFileSync(serverPkgPath, "utf-8");
|
|
249
|
+
const pkg = JSON.parse(raw);
|
|
250
|
+
const baseName = projectPackageName.includes("/") ? projectPackageName.split("/").pop() : projectPackageName;
|
|
251
|
+
if (!baseName) return;
|
|
252
|
+
pkg["name"] = `${baseName}-mcp-server`;
|
|
253
|
+
fs2.writeFileSync(serverPkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
254
|
+
`, "utf-8");
|
|
255
|
+
}
|
|
256
|
+
function ensureServerPostinstall(targetDir) {
|
|
257
|
+
const pkgPath = path3.join(targetDir, "package.json");
|
|
258
|
+
const serverPkgPath = path3.join(targetDir, "server", "package.json");
|
|
259
|
+
if (!fs2.existsSync(pkgPath) || !fs2.existsSync(serverPkgPath)) return;
|
|
260
|
+
const raw = fs2.readFileSync(pkgPath, "utf-8");
|
|
261
|
+
const pkg = JSON.parse(raw);
|
|
262
|
+
const scripts = pkg["scripts"] ?? {};
|
|
263
|
+
if (scripts["postinstall"]) return;
|
|
264
|
+
const scriptPath = path3.join(
|
|
265
|
+
targetDir,
|
|
266
|
+
"scripts",
|
|
267
|
+
"mcp-app-studio-postinstall.cjs"
|
|
268
|
+
);
|
|
269
|
+
fs2.mkdirSync(path3.dirname(scriptPath), { recursive: true });
|
|
270
|
+
fs2.writeFileSync(
|
|
271
|
+
scriptPath,
|
|
272
|
+
`/* eslint-disable */
|
|
273
|
+
const { spawnSync } = require("node:child_process");
|
|
274
|
+
const { existsSync } = require("node:fs");
|
|
275
|
+
const { join } = require("node:path");
|
|
276
|
+
|
|
277
|
+
const ROOT = process.cwd();
|
|
278
|
+
const SERVER_DIR = join(ROOT, "server");
|
|
279
|
+
|
|
280
|
+
if (!existsSync(join(SERVER_DIR, "package.json"))) process.exit(0);
|
|
281
|
+
if (existsSync(join(SERVER_DIR, "node_modules")) || existsSync(join(SERVER_DIR, ".pnp.cjs"))) process.exit(0);
|
|
282
|
+
|
|
283
|
+
const ua = process.env.npm_config_user_agent || "";
|
|
284
|
+
let pm = "npm";
|
|
285
|
+
if (ua.includes("pnpm")) pm = "pnpm";
|
|
286
|
+
else if (ua.includes("yarn")) pm = "yarn";
|
|
287
|
+
else if (ua.includes("bun")) pm = "bun";
|
|
288
|
+
|
|
289
|
+
console.log("\\n\\x1b[2mInstalling server dependencies (" + pm + ")...\\x1b[0m\\n");
|
|
290
|
+
const result = spawnSync(pm, ["install"], { cwd: SERVER_DIR, stdio: "inherit", shell: process.platform === "win32" });
|
|
291
|
+
process.exit(result.status == null ? 1 : result.status);
|
|
292
|
+
`,
|
|
293
|
+
"utf-8"
|
|
294
|
+
);
|
|
295
|
+
scripts["postinstall"] = "node scripts/mcp-app-studio-postinstall.cjs";
|
|
296
|
+
pkg["scripts"] = scripts;
|
|
297
|
+
fs2.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
298
|
+
`, "utf-8");
|
|
299
|
+
}
|
|
178
300
|
function generateComponentRegistry(components) {
|
|
179
301
|
const imports = [];
|
|
180
302
|
const entries = [];
|
|
@@ -265,6 +387,9 @@ export const workbenchComponents: WorkbenchComponentEntry[] = [
|
|
|
265
387
|
${entries.join(",\n")}
|
|
266
388
|
];
|
|
267
389
|
|
|
390
|
+
// The main app component (first in the list)
|
|
391
|
+
export const appComponent = workbenchComponents[0]!;
|
|
392
|
+
|
|
268
393
|
export function getComponent(id: string): WorkbenchComponentEntry | undefined {
|
|
269
394
|
return workbenchComponents.find((c) => c.id === id);
|
|
270
395
|
}
|
|
@@ -297,8 +422,10 @@ function generateExamplesIndex(components) {
|
|
|
297
422
|
` : "// No examples\n";
|
|
298
423
|
}
|
|
299
424
|
function updateExportScriptDefaults(targetDir, entryPoint, exportName) {
|
|
300
|
-
const exportScriptPath =
|
|
425
|
+
const exportScriptPath = path3.join(targetDir, "scripts/export.ts");
|
|
426
|
+
if (!fs2.existsSync(exportScriptPath)) return;
|
|
301
427
|
let content = fs2.readFileSync(exportScriptPath, "utf-8");
|
|
428
|
+
if (content.includes("mcp-app-studio.config.json")) return;
|
|
302
429
|
content = content.replace(
|
|
303
430
|
/entryPoint: "lib\/workbench\/wrappers\/[^"]+"/,
|
|
304
431
|
`entryPoint: "${entryPoint}"`
|
|
@@ -318,40 +445,77 @@ function updateExportScriptDefaults(targetDir, entryPoint, exportName) {
|
|
|
318
445
|
);
|
|
319
446
|
fs2.writeFileSync(exportScriptPath, content);
|
|
320
447
|
}
|
|
448
|
+
function generateWorkbenchIndexExport(components) {
|
|
449
|
+
const exports = [];
|
|
450
|
+
if (components.includes("welcome")) {
|
|
451
|
+
exports.push("WelcomeCardSDK");
|
|
452
|
+
}
|
|
453
|
+
if (components.includes("poi-map")) {
|
|
454
|
+
exports.push("POIMapSDK");
|
|
455
|
+
}
|
|
456
|
+
return exports.length > 0 ? `export { ${exports.join(", ")} } from "./wrappers";` : "// No SDK exports";
|
|
457
|
+
}
|
|
458
|
+
function updateWorkbenchIndex(targetDir, components) {
|
|
459
|
+
const indexPath = path3.join(targetDir, "lib/workbench/index.ts");
|
|
460
|
+
let content = fs2.readFileSync(indexPath, "utf-8");
|
|
461
|
+
const wrappersExportRegex = /export \{[^}]*\} from "\.\/wrappers";/;
|
|
462
|
+
const newExport = generateWorkbenchIndexExport(components);
|
|
463
|
+
if (wrappersExportRegex.test(content)) {
|
|
464
|
+
content = content.replace(wrappersExportRegex, newExport);
|
|
465
|
+
} else {
|
|
466
|
+
content = `${content.trimEnd()}
|
|
467
|
+
|
|
468
|
+
${newExport}
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
fs2.writeFileSync(indexPath, content);
|
|
472
|
+
}
|
|
473
|
+
function updateWorkbenchStoreDefault(targetDir, defaultComponent) {
|
|
474
|
+
const storePath = path3.join(targetDir, "lib/workbench/store.ts");
|
|
475
|
+
let content = fs2.readFileSync(storePath, "utf-8");
|
|
476
|
+
content = content.replace(
|
|
477
|
+
/selectedComponent: "[^"]+"/,
|
|
478
|
+
`selectedComponent: "${defaultComponent}"`
|
|
479
|
+
);
|
|
480
|
+
fs2.writeFileSync(storePath, content);
|
|
481
|
+
}
|
|
321
482
|
function applyTemplate(targetDir, template) {
|
|
322
483
|
const components = TEMPLATE_COMPONENTS[template];
|
|
323
|
-
const registryPath =
|
|
484
|
+
const registryPath = path3.join(
|
|
324
485
|
targetDir,
|
|
325
486
|
"lib/workbench/component-registry.tsx"
|
|
326
487
|
);
|
|
327
488
|
fs2.writeFileSync(registryPath, generateComponentRegistry(components));
|
|
328
|
-
const wrappersIndexPath =
|
|
489
|
+
const wrappersIndexPath = path3.join(
|
|
329
490
|
targetDir,
|
|
330
491
|
"lib/workbench/wrappers/index.ts"
|
|
331
492
|
);
|
|
332
493
|
fs2.writeFileSync(wrappersIndexPath, generateWrappersIndex(components));
|
|
333
|
-
const examplesIndexPath =
|
|
494
|
+
const examplesIndexPath = path3.join(
|
|
334
495
|
targetDir,
|
|
335
496
|
"components/examples/index.ts"
|
|
336
497
|
);
|
|
337
498
|
fs2.writeFileSync(examplesIndexPath, generateExamplesIndex(components));
|
|
338
|
-
|
|
499
|
+
updateWorkbenchIndex(targetDir, components);
|
|
500
|
+
const defaultComponent = TEMPLATE_DEFAULT_COMPONENT[template];
|
|
501
|
+
updateWorkbenchStoreDefault(targetDir, defaultComponent);
|
|
502
|
+
const examplesDir = path3.join(targetDir, "components/examples");
|
|
339
503
|
if (!components.includes("welcome")) {
|
|
340
|
-
fs2.rmSync(
|
|
504
|
+
fs2.rmSync(path3.join(examplesDir, "welcome-card"), {
|
|
341
505
|
recursive: true,
|
|
342
506
|
force: true
|
|
343
507
|
});
|
|
344
508
|
fs2.rmSync(
|
|
345
|
-
|
|
509
|
+
path3.join(targetDir, "lib/workbench/wrappers/welcome-card-sdk.tsx"),
|
|
346
510
|
{ force: true }
|
|
347
511
|
);
|
|
348
512
|
}
|
|
349
513
|
if (!components.includes("poi-map")) {
|
|
350
|
-
fs2.rmSync(
|
|
514
|
+
fs2.rmSync(path3.join(examplesDir, "poi-map"), {
|
|
351
515
|
recursive: true,
|
|
352
516
|
force: true
|
|
353
517
|
});
|
|
354
|
-
fs2.rmSync(
|
|
518
|
+
fs2.rmSync(path3.join(targetDir, "lib/workbench/wrappers/poi-map-sdk.tsx"), {
|
|
355
519
|
force: true
|
|
356
520
|
});
|
|
357
521
|
}
|
|
@@ -362,16 +526,61 @@ function applyTemplate(targetDir, template) {
|
|
|
362
526
|
exportConfig.exportName
|
|
363
527
|
);
|
|
364
528
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
529
|
+
var REQUIRED_TEMPLATE_PATHS = [
|
|
530
|
+
"package.json",
|
|
531
|
+
"scripts/dev.ts",
|
|
532
|
+
"scripts/export.ts",
|
|
533
|
+
"lib/workbench/component-registry.tsx",
|
|
534
|
+
"lib/workbench/wrappers/index.ts",
|
|
535
|
+
"components/examples/index.ts",
|
|
536
|
+
"lib/workbench/index.ts",
|
|
537
|
+
"lib/workbench/store.ts"
|
|
538
|
+
];
|
|
539
|
+
function validateTemplateDir(templateDir) {
|
|
540
|
+
const missing = REQUIRED_TEMPLATE_PATHS.filter(
|
|
541
|
+
(p2) => !fs2.existsSync(path3.join(templateDir, p2))
|
|
542
|
+
);
|
|
543
|
+
if (missing.length > 0) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`Template validation failed. Missing expected files:
|
|
546
|
+
${missing.map((p2) => `- ${p2}`).join(
|
|
547
|
+
"\n"
|
|
548
|
+
)}
|
|
549
|
+
|
|
550
|
+
This may indicate the starter template has changed. Try updating mcp-app-studio or set MCP_APP_STUDIO_TEMPLATE_REPO / MCP_APP_STUDIO_TEMPLATE_REF to a compatible template.`
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function copyDirContents(srcDir, destDir) {
|
|
555
|
+
fs2.mkdirSync(destDir, { recursive: true });
|
|
556
|
+
const entries = fs2.readdirSync(srcDir, { withFileTypes: true });
|
|
557
|
+
for (const entry of entries) {
|
|
558
|
+
if (entry.name === ".git") continue;
|
|
559
|
+
const srcPath = path3.join(srcDir, entry.name);
|
|
560
|
+
const destPath = path3.join(destDir, entry.name);
|
|
561
|
+
if (entry.isDirectory()) {
|
|
562
|
+
copyDirContents(srcPath, destPath);
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (entry.isFile()) {
|
|
566
|
+
fs2.mkdirSync(path3.dirname(destPath), { recursive: true });
|
|
567
|
+
fs2.copyFileSync(srcPath, destPath);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async function downloadTemplateToTemp() {
|
|
572
|
+
const tarballUrl = getGithubArchiveTarballUrl(TEMPLATE_REPO, TEMPLATE_REF);
|
|
573
|
+
const tempDir = fs2.mkdtempSync(
|
|
574
|
+
path3.join(os.tmpdir(), "mcp-app-studio-template-")
|
|
575
|
+
);
|
|
576
|
+
const tarballPath = path3.join(tempDir, "template.tar.gz");
|
|
577
|
+
const extractDir = path3.join(tempDir, "extract");
|
|
369
578
|
try {
|
|
370
|
-
fs2.mkdirSync(
|
|
579
|
+
fs2.mkdirSync(extractDir, { recursive: true });
|
|
371
580
|
const response = await fetch(tarballUrl);
|
|
372
581
|
if (!response.ok) {
|
|
373
582
|
throw new Error(
|
|
374
|
-
`Failed to download template: ${response.status} ${response.statusText}`
|
|
583
|
+
`Failed to download template from ${tarballUrl}: ${response.status} ${response.statusText}`
|
|
375
584
|
);
|
|
376
585
|
}
|
|
377
586
|
const fileStream = createWriteStream(tarballPath);
|
|
@@ -383,93 +592,179 @@ async function downloadTemplate(targetDir) {
|
|
|
383
592
|
body
|
|
384
593
|
);
|
|
385
594
|
await pipeline(nodeStream, fileStream);
|
|
386
|
-
fs2.mkdirSync(targetDir, { recursive: true });
|
|
387
595
|
await extract({
|
|
388
596
|
file: tarballPath,
|
|
389
|
-
cwd:
|
|
390
|
-
strip:
|
|
391
|
-
|
|
597
|
+
cwd: extractDir,
|
|
598
|
+
strip: 0,
|
|
599
|
+
filter: (entryPath, entry) => filterTemplateTarEntry(extractDir, entryPath, entry)
|
|
392
600
|
});
|
|
393
|
-
|
|
394
|
-
|
|
601
|
+
const topLevelDirs = fs2.readdirSync(extractDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
602
|
+
if (topLevelDirs.length !== 1) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`Unexpected template archive layout. Expected a single top-level directory, found ${topLevelDirs.length}.`
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
const topLevelDir = topLevelDirs[0];
|
|
608
|
+
if (!topLevelDir) {
|
|
609
|
+
throw new Error(
|
|
610
|
+
"Unexpected template archive layout. No top-level directory found."
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
const templateDir = path3.join(extractDir, topLevelDir.name);
|
|
614
|
+
validateTemplateDir(templateDir);
|
|
615
|
+
return { tempDir, templateDir };
|
|
616
|
+
} catch (err) {
|
|
617
|
+
try {
|
|
618
|
+
fs2.rmSync(tempDir, { recursive: true, force: true });
|
|
619
|
+
} catch {
|
|
620
|
+
}
|
|
621
|
+
throw err;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function parseArgs(args) {
|
|
625
|
+
const parsed = {
|
|
626
|
+
yes: false
|
|
627
|
+
};
|
|
628
|
+
for (let i = 0; i < args.length; i++) {
|
|
629
|
+
const arg = args[i];
|
|
630
|
+
const next = args[i + 1];
|
|
631
|
+
switch (arg) {
|
|
632
|
+
case "-y":
|
|
633
|
+
case "--yes":
|
|
634
|
+
parsed.yes = true;
|
|
635
|
+
break;
|
|
636
|
+
case "--template":
|
|
637
|
+
if (next && (next === "minimal" || next === "poi-map")) {
|
|
638
|
+
parsed.template = next;
|
|
639
|
+
i++;
|
|
640
|
+
}
|
|
641
|
+
break;
|
|
642
|
+
case "--include-server":
|
|
643
|
+
parsed.includeServer = true;
|
|
644
|
+
break;
|
|
645
|
+
case "--no-server":
|
|
646
|
+
parsed.includeServer = false;
|
|
647
|
+
break;
|
|
648
|
+
case "--description":
|
|
649
|
+
if (next && !next.startsWith("-")) {
|
|
650
|
+
parsed.description = next;
|
|
651
|
+
i++;
|
|
652
|
+
}
|
|
653
|
+
break;
|
|
654
|
+
default:
|
|
655
|
+
if (arg && !arg.startsWith("-") && !parsed.projectName) {
|
|
656
|
+
parsed.projectName = arg;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
395
659
|
}
|
|
660
|
+
return parsed;
|
|
396
661
|
}
|
|
397
662
|
async function main() {
|
|
398
663
|
ensureSupportedNodeVersion();
|
|
399
|
-
const
|
|
400
|
-
if (
|
|
664
|
+
const rawArgs = process.argv.slice(2);
|
|
665
|
+
if (rawArgs.includes("--help") || rawArgs.includes("-h")) {
|
|
401
666
|
showHelp();
|
|
402
667
|
process.exit(0);
|
|
403
668
|
}
|
|
404
|
-
if (
|
|
669
|
+
if (rawArgs.includes("--version") || rawArgs.includes("-v")) {
|
|
405
670
|
console.log(getVersion());
|
|
406
671
|
process.exit(0);
|
|
407
672
|
}
|
|
408
|
-
const
|
|
673
|
+
const parsedArgs = parseArgs(rawArgs);
|
|
674
|
+
const nonInteractive = parsedArgs.yes;
|
|
409
675
|
p.intro(pc.bgCyan(pc.black(" mcp-app-studio ")));
|
|
410
|
-
if (
|
|
411
|
-
const pathCheck = isValidProjectPath(
|
|
676
|
+
if (parsedArgs.projectName) {
|
|
677
|
+
const pathCheck = isValidProjectPath(parsedArgs.projectName);
|
|
412
678
|
if (!pathCheck.valid) {
|
|
413
679
|
p.log.error(pathCheck.error ?? "Invalid project path");
|
|
414
680
|
process.exit(1);
|
|
415
681
|
}
|
|
416
682
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
683
|
+
let projectName;
|
|
684
|
+
if (parsedArgs.projectName) {
|
|
685
|
+
projectName = parsedArgs.projectName;
|
|
686
|
+
} else if (nonInteractive) {
|
|
687
|
+
p.log.error("Project name is required in non-interactive mode");
|
|
688
|
+
process.exit(1);
|
|
689
|
+
} else {
|
|
690
|
+
projectName = await p.text({
|
|
691
|
+
message: "Project name:",
|
|
692
|
+
placeholder: "my-chatgpt-app",
|
|
693
|
+
validate: (value) => {
|
|
694
|
+
if (!value) return "Project name is required";
|
|
695
|
+
const pathCheck = isValidProjectPath(value);
|
|
696
|
+
if (!pathCheck.valid) return pathCheck.error;
|
|
697
|
+
if (!isValidPackageName(toValidPackageName(value))) {
|
|
698
|
+
return "Invalid project name";
|
|
699
|
+
}
|
|
700
|
+
return void 0;
|
|
426
701
|
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
});
|
|
702
|
+
});
|
|
703
|
+
}
|
|
430
704
|
if (p.isCancel(projectName)) {
|
|
431
705
|
p.cancel("Operation cancelled.");
|
|
432
706
|
process.exit(0);
|
|
433
707
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
708
|
+
let description;
|
|
709
|
+
if (parsedArgs.description !== void 0) {
|
|
710
|
+
description = parsedArgs.description;
|
|
711
|
+
} else if (nonInteractive) {
|
|
712
|
+
description = "";
|
|
713
|
+
} else {
|
|
714
|
+
description = await p.text({
|
|
715
|
+
message: "App description:",
|
|
716
|
+
placeholder: "A ChatGPT app that helps users...",
|
|
717
|
+
initialValue: ""
|
|
718
|
+
});
|
|
719
|
+
}
|
|
439
720
|
if (p.isCancel(description)) {
|
|
440
721
|
p.cancel("Operation cancelled.");
|
|
441
722
|
process.exit(0);
|
|
442
723
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
724
|
+
let template;
|
|
725
|
+
if (parsedArgs.template !== void 0) {
|
|
726
|
+
template = parsedArgs.template;
|
|
727
|
+
} else if (nonInteractive) {
|
|
728
|
+
template = "minimal";
|
|
729
|
+
} else {
|
|
730
|
+
template = await p.select({
|
|
731
|
+
message: "Choose a starter template:",
|
|
732
|
+
options: [
|
|
733
|
+
{
|
|
734
|
+
value: "minimal",
|
|
735
|
+
label: "Minimal",
|
|
736
|
+
hint: "Simple welcome card - perfect starting point"
|
|
737
|
+
},
|
|
738
|
+
{
|
|
739
|
+
value: "poi-map",
|
|
740
|
+
label: "Locations App",
|
|
741
|
+
hint: "Interactive map demo with full SDK features"
|
|
742
|
+
}
|
|
743
|
+
],
|
|
744
|
+
initialValue: "minimal"
|
|
745
|
+
});
|
|
746
|
+
}
|
|
459
747
|
if (p.isCancel(template)) {
|
|
460
748
|
p.cancel("Operation cancelled.");
|
|
461
749
|
process.exit(0);
|
|
462
750
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
})
|
|
751
|
+
let includeServer;
|
|
752
|
+
if (parsedArgs.includeServer !== void 0) {
|
|
753
|
+
includeServer = parsedArgs.includeServer;
|
|
754
|
+
} else if (nonInteractive) {
|
|
755
|
+
includeServer = true;
|
|
756
|
+
} else {
|
|
757
|
+
includeServer = await p.confirm({
|
|
758
|
+
message: "Include MCP server?",
|
|
759
|
+
initialValue: true
|
|
760
|
+
});
|
|
761
|
+
}
|
|
467
762
|
if (p.isCancel(includeServer)) {
|
|
468
763
|
p.cancel("Operation cancelled.");
|
|
469
764
|
process.exit(0);
|
|
470
765
|
}
|
|
471
|
-
const targetDir =
|
|
472
|
-
const packageName = projectName === "." ? toValidPackageName(
|
|
766
|
+
const targetDir = path3.resolve(process.cwd(), projectName);
|
|
767
|
+
const packageName = projectName === "." ? toValidPackageName(path3.basename(targetDir)) : toValidPackageName(projectName);
|
|
473
768
|
const config = {
|
|
474
769
|
name: projectName,
|
|
475
770
|
packageName,
|
|
@@ -477,21 +772,28 @@ async function main() {
|
|
|
477
772
|
template,
|
|
478
773
|
includeServer
|
|
479
774
|
};
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
p.
|
|
487
|
-
|
|
775
|
+
const targetNotEmpty = !isEmpty(targetDir);
|
|
776
|
+
let shouldOverwrite = false;
|
|
777
|
+
if (targetNotEmpty) {
|
|
778
|
+
if (nonInteractive) {
|
|
779
|
+
shouldOverwrite = true;
|
|
780
|
+
} else {
|
|
781
|
+
const overwrite = await p.confirm({
|
|
782
|
+
message: `Directory "${projectName}" is not empty. Remove existing files?`,
|
|
783
|
+
initialValue: false
|
|
784
|
+
});
|
|
785
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
786
|
+
p.cancel("Operation cancelled.");
|
|
787
|
+
process.exit(0);
|
|
788
|
+
}
|
|
789
|
+
shouldOverwrite = true;
|
|
488
790
|
}
|
|
489
|
-
emptyDir(targetDir);
|
|
490
791
|
}
|
|
491
792
|
const s = p.spinner();
|
|
492
793
|
s.start("Downloading template...");
|
|
794
|
+
let downloaded;
|
|
493
795
|
try {
|
|
494
|
-
await
|
|
796
|
+
downloaded = await downloadTemplateToTemp();
|
|
495
797
|
} catch (error) {
|
|
496
798
|
s.stop("Download failed");
|
|
497
799
|
p.log.error(
|
|
@@ -500,11 +802,35 @@ async function main() {
|
|
|
500
802
|
process.exit(1);
|
|
501
803
|
}
|
|
502
804
|
s.message("Creating project...");
|
|
503
|
-
|
|
805
|
+
try {
|
|
806
|
+
if (!downloaded) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
"Internal error: template download did not return a result"
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
if (shouldOverwrite) {
|
|
812
|
+
emptyDir(targetDir);
|
|
813
|
+
}
|
|
814
|
+
fs2.mkdirSync(targetDir, { recursive: true });
|
|
815
|
+
copyDirContents(downloaded.templateDir, targetDir);
|
|
816
|
+
} finally {
|
|
817
|
+
if (downloaded?.tempDir) {
|
|
818
|
+
fs2.rmSync(downloaded.tempDir, { recursive: true, force: true });
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
updatePackageJson(targetDir, config.packageName, config.description, {
|
|
822
|
+
mcpAppStudioVersion: getVersion()
|
|
823
|
+
});
|
|
824
|
+
if (config.includeServer) {
|
|
825
|
+
ensureServerPostinstall(targetDir);
|
|
826
|
+
}
|
|
504
827
|
s.message("Applying template...");
|
|
505
828
|
applyTemplate(targetDir, config.template);
|
|
506
|
-
|
|
507
|
-
|
|
829
|
+
writeStudioConfig(targetDir, config.template, path3.basename(targetDir));
|
|
830
|
+
if (config.includeServer) {
|
|
831
|
+
updateServerPackageName(targetDir, config.packageName);
|
|
832
|
+
} else {
|
|
833
|
+
fs2.rmSync(path3.join(targetDir, "server"), { recursive: true, force: true });
|
|
508
834
|
}
|
|
509
835
|
s.stop("Project created!");
|
|
510
836
|
const pm = detectPackageManager();
|
|
@@ -512,12 +838,7 @@ async function main() {
|
|
|
512
838
|
const runCmd = pm === "npm" ? "npm run" : pm === "bun" ? "bun run" : pm;
|
|
513
839
|
const devCmd = `${runCmd} dev`;
|
|
514
840
|
const quotedName = quotePath(projectName);
|
|
515
|
-
const nextSteps = [`cd ${quotedName}`, installCmd];
|
|
516
|
-
if (config.includeServer) {
|
|
517
|
-
nextSteps.push(`cd server && ${installCmd}`);
|
|
518
|
-
nextSteps.push("cd ..");
|
|
519
|
-
}
|
|
520
|
-
nextSteps.push(devCmd);
|
|
841
|
+
const nextSteps = [`cd ${quotedName}`, installCmd, devCmd];
|
|
521
842
|
if (config.includeServer) {
|
|
522
843
|
nextSteps.push("");
|
|
523
844
|
nextSteps.push(pc.dim("# This starts both Next.js and MCP server"));
|
|
@@ -559,12 +880,8 @@ async function main() {
|
|
|
559
880
|
);
|
|
560
881
|
p.log.message("");
|
|
561
882
|
p.log.step(pc.bold("Learn more:"));
|
|
562
|
-
p.log.message(
|
|
563
|
-
|
|
564
|
-
);
|
|
565
|
-
p.log.message(
|
|
566
|
-
` ${pc.dim("\u2022")} Examples: ${pc.cyan("https://github.com/assistant-ui/mcp-app-studio-starter")}`
|
|
567
|
-
);
|
|
883
|
+
p.log.message(` ${pc.dim("\u2022")} SDK Docs: ${pc.cyan(DOCS_URL)}`);
|
|
884
|
+
p.log.message(` ${pc.dim("\u2022")} Examples: ${pc.cyan(EXAMPLES_URL)}`);
|
|
568
885
|
if (config.includeServer) {
|
|
569
886
|
p.log.message(
|
|
570
887
|
` ${pc.dim("\u2022")} MCP Guide: ${pc.cyan("https://modelcontextprotocol.io/quickstart")}`
|