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/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 path2 from "path";
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 path from "path";
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 (path.isAbsolute(trimmed)) {
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 = path.resolve(cwd, trimmed);
35
- const relative = path.relative(cwd, resolved);
36
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
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(path.join(dir, file), { recursive: true, force: true });
87
+ fs.rmSync(path2.join(dir, file), { recursive: true, force: true });
62
88
  }
63
89
  }
64
- function updatePackageJson(dir, name, description) {
65
- const pkgPath = path.join(dir, "package.json");
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 = path2.dirname(fileURLToPath(import.meta.url));
94
- var TEMPLATE_REPO = "assistant-ui/mcp-app-studio-starter";
95
- var TEMPLATE_BRANCH = "main";
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 = path2.resolve(__dirname, "../package.json");
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 Show this help message
146
- --version, -v Show version number
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: https://github.com/assistant-ui/mcp-app-studio
155
- Examples: https://github.com/assistant-ui/mcp-app-studio-starter
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 = path2.join(targetDir, "scripts/export.ts");
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 = path2.join(
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 = path2.join(
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 = path2.join(
494
+ const examplesIndexPath = path3.join(
334
495
  targetDir,
335
496
  "components/examples/index.ts"
336
497
  );
337
498
  fs2.writeFileSync(examplesIndexPath, generateExamplesIndex(components));
338
- const examplesDir = path2.join(targetDir, "components/examples");
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(path2.join(examplesDir, "welcome-card"), {
504
+ fs2.rmSync(path3.join(examplesDir, "welcome-card"), {
341
505
  recursive: true,
342
506
  force: true
343
507
  });
344
508
  fs2.rmSync(
345
- path2.join(targetDir, "lib/workbench/wrappers/welcome-card-sdk.tsx"),
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(path2.join(examplesDir, "poi-map"), {
514
+ fs2.rmSync(path3.join(examplesDir, "poi-map"), {
351
515
  recursive: true,
352
516
  force: true
353
517
  });
354
- fs2.rmSync(path2.join(targetDir, "lib/workbench/wrappers/poi-map-sdk.tsx"), {
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
- async function downloadTemplate(targetDir) {
366
- const tarballUrl = `https://github.com/${TEMPLATE_REPO}/archive/refs/heads/${TEMPLATE_BRANCH}.tar.gz`;
367
- const tempDir = path2.join(os.tmpdir(), `mcp-app-studio-${Date.now()}`);
368
- const tarballPath = path2.join(tempDir, "template.tar.gz");
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(tempDir, { recursive: true });
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: targetDir,
390
- strip: 1
391
- // Remove the top-level directory (e.g., mcp-app-studio-starter-main/)
597
+ cwd: extractDir,
598
+ strip: 0,
599
+ filter: (entryPath, entry) => filterTemplateTarEntry(extractDir, entryPath, entry)
392
600
  });
393
- } finally {
394
- fs2.rmSync(tempDir, { recursive: true, force: true });
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 args = process.argv.slice(2);
400
- if (args.includes("--help") || args.includes("-h")) {
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 (args.includes("--version") || args.includes("-v")) {
669
+ if (rawArgs.includes("--version") || rawArgs.includes("-v")) {
405
670
  console.log(getVersion());
406
671
  process.exit(0);
407
672
  }
408
- const argProjectName = args.find((arg) => !arg.startsWith("-"));
673
+ const parsedArgs = parseArgs(rawArgs);
674
+ const nonInteractive = parsedArgs.yes;
409
675
  p.intro(pc.bgCyan(pc.black(" mcp-app-studio ")));
410
- if (argProjectName) {
411
- const pathCheck = isValidProjectPath(argProjectName);
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
- const projectName = argProjectName ? argProjectName : await p.text({
418
- message: "Project name:",
419
- placeholder: "my-chatgpt-app",
420
- validate: (value) => {
421
- if (!value) return "Project name is required";
422
- const pathCheck = isValidProjectPath(value);
423
- if (!pathCheck.valid) return pathCheck.error;
424
- if (!isValidPackageName(toValidPackageName(value))) {
425
- return "Invalid project name";
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
- return void 0;
428
- }
429
- });
702
+ });
703
+ }
430
704
  if (p.isCancel(projectName)) {
431
705
  p.cancel("Operation cancelled.");
432
706
  process.exit(0);
433
707
  }
434
- const description = await p.text({
435
- message: "App description:",
436
- placeholder: "A ChatGPT app that helps users...",
437
- initialValue: ""
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
- const template = await p.select({
444
- message: "Choose a starter template:",
445
- options: [
446
- {
447
- value: "minimal",
448
- label: "Minimal",
449
- hint: "Simple welcome card - perfect starting point"
450
- },
451
- {
452
- value: "poi-map",
453
- label: "Locations App",
454
- hint: "Interactive map demo with full SDK features"
455
- }
456
- ],
457
- initialValue: "minimal"
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
- const includeServer = await p.confirm({
464
- message: "Include MCP server?",
465
- initialValue: true
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 = path2.resolve(process.cwd(), projectName);
472
- const packageName = projectName === "." ? toValidPackageName(path2.basename(targetDir)) : toValidPackageName(projectName);
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
- if (!isEmpty(targetDir)) {
481
- const overwrite = await p.confirm({
482
- message: `Directory "${projectName}" is not empty. Remove existing files?`,
483
- initialValue: false
484
- });
485
- if (p.isCancel(overwrite) || !overwrite) {
486
- p.cancel("Operation cancelled.");
487
- process.exit(0);
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 downloadTemplate(targetDir);
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
- updatePackageJson(targetDir, config.packageName, config.description);
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
- if (!config.includeServer) {
507
- fs2.rmSync(path2.join(targetDir, "server"), { recursive: true, force: true });
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
- ` ${pc.dim("\u2022")} SDK Docs: ${pc.cyan("https://github.com/assistant-ui/mcp-app-studio#sdk")}`
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")}`