assistant-ui 0.0.81 → 0.0.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,88 +1,288 @@
1
1
  import { Command } from "commander";
2
2
  import chalk from "chalk";
3
3
  import { spawn } from "cross-spawn";
4
+ import fs from "node:fs";
4
5
  import path from "node:path";
5
6
  import * as p from "@clack/prompts";
6
7
  import { logger } from "../lib/utils/logger";
7
- import { createFromExample } from "../lib/create-from-example";
8
+ import {
9
+ dlxCommand,
10
+ downloadProject,
11
+ resolveLatestReleaseRef,
12
+ resolvePackageManagerName,
13
+ transformProject,
14
+ type PackageManagerName,
15
+ } from "../lib/create-project";
16
+
17
+ export interface ProjectMetadata {
18
+ name: string;
19
+ label: string;
20
+ description?: string;
21
+ category: "template" | "example";
22
+ path: string;
23
+ hasLocalComponents: boolean;
24
+ }
8
25
 
9
- const templates = {
10
- default: {
11
- url: "https://github.com/assistant-ui/assistant-ui-starter",
26
+ export const PROJECT_METADATA: ProjectMetadata[] = [
27
+ // Templates
28
+ {
29
+ name: "default",
12
30
  label: "Default",
13
- hint: "Default template with Vercel AI SDK",
31
+ description: "Default template with Vercel AI SDK",
32
+ category: "template",
33
+ path: "templates/default",
34
+ hasLocalComponents: true,
14
35
  },
15
- minimal: {
16
- url: "https://github.com/assistant-ui/assistant-ui-starter-minimal",
36
+ {
37
+ name: "minimal",
17
38
  label: "Minimal",
18
- hint: "Bare-bones starting point",
39
+ description: "Bare-bones starting point",
40
+ category: "template",
41
+ path: "templates/minimal",
42
+ hasLocalComponents: true,
19
43
  },
20
- cloud: {
21
- url: "https://github.com/assistant-ui/assistant-ui-starter-cloud",
44
+ {
45
+ name: "cloud",
22
46
  label: "Cloud",
23
- hint: "Cloud-backed persistence starter",
47
+ description: "Cloud-backed persistence starter",
48
+ category: "template",
49
+ path: "templates/cloud",
50
+ hasLocalComponents: true,
24
51
  },
25
- "cloud-clerk": {
26
- url: "https://github.com/assistant-ui/assistant-ui-starter-cloud-clerk",
52
+ {
53
+ name: "cloud-clerk",
27
54
  label: "Cloud + Clerk",
28
- hint: "Cloud-backed starter with Clerk auth",
55
+ description: "Cloud-backed starter with Clerk auth",
56
+ category: "template",
57
+ path: "templates/cloud-clerk",
58
+ hasLocalComponents: true,
29
59
  },
30
- langgraph: {
31
- url: "https://github.com/assistant-ui/assistant-ui-starter-langgraph",
60
+ {
61
+ name: "langgraph",
32
62
  label: "LangGraph",
33
- hint: "LangGraph starter template",
63
+ description: "LangGraph starter template",
64
+ category: "template",
65
+ path: "templates/langgraph",
66
+ hasLocalComponents: true,
34
67
  },
35
- mcp: {
36
- url: "https://github.com/assistant-ui/assistant-ui-starter-mcp",
68
+ {
69
+ name: "mcp",
37
70
  label: "MCP",
38
- hint: "MCP starter template",
71
+ description: "MCP starter template",
72
+ category: "template",
73
+ path: "templates/mcp",
74
+ hasLocalComponents: true,
75
+ },
76
+ // Examples
77
+ {
78
+ name: "with-ag-ui",
79
+ label: "AG-UI",
80
+ description: "AG-UI protocol integration",
81
+ category: "example",
82
+ path: "examples/with-ag-ui",
83
+ hasLocalComponents: false,
84
+ },
85
+ {
86
+ name: "with-ai-sdk-v6",
87
+ label: "AI SDK v6",
88
+ description: "Vercel AI SDK v6",
89
+ category: "example",
90
+ path: "examples/with-ai-sdk-v6",
91
+ hasLocalComponents: false,
92
+ },
93
+ {
94
+ name: "with-artifacts",
95
+ label: "Artifacts",
96
+ description: "Artifact rendering",
97
+ category: "example",
98
+ path: "examples/with-artifacts",
99
+ hasLocalComponents: false,
100
+ },
101
+ {
102
+ name: "with-assistant-transport",
103
+ label: "Assistant Transport",
104
+ description: "Assistant transport protocol",
105
+ category: "example",
106
+ path: "examples/with-assistant-transport",
107
+ hasLocalComponents: false,
108
+ },
109
+ {
110
+ name: "with-chain-of-thought",
111
+ label: "Chain of Thought",
112
+ description: "Chain-of-thought rendering",
113
+ category: "example",
114
+ path: "examples/with-chain-of-thought",
115
+ hasLocalComponents: false,
116
+ },
117
+ {
118
+ name: "with-cloud",
119
+ label: "Cloud Example",
120
+ description: "Cloud integration example",
121
+ category: "example",
122
+ path: "examples/with-cloud",
123
+ hasLocalComponents: false,
124
+ },
125
+ {
126
+ name: "with-custom-thread-list",
127
+ label: "Custom Thread List",
128
+ description: "Custom thread list UI",
129
+ category: "example",
130
+ path: "examples/with-custom-thread-list",
131
+ hasLocalComponents: false,
132
+ },
133
+ {
134
+ name: "with-elevenlabs-scribe",
135
+ label: "ElevenLabs Scribe",
136
+ description: "Audio/speech integration",
137
+ category: "example",
138
+ path: "examples/with-elevenlabs-scribe",
139
+ hasLocalComponents: false,
140
+ },
141
+ {
142
+ name: "with-external-store",
143
+ label: "External Store",
144
+ description: "Custom message store",
145
+ category: "example",
146
+ path: "examples/with-external-store",
147
+ hasLocalComponents: false,
39
148
  },
40
- } as const;
149
+ {
150
+ name: "with-ffmpeg",
151
+ label: "FFmpeg",
152
+ description: "File processing",
153
+ category: "example",
154
+ path: "examples/with-ffmpeg",
155
+ hasLocalComponents: false,
156
+ },
157
+ {
158
+ name: "with-langgraph",
159
+ label: "LangGraph Example",
160
+ description: "LangGraph integration",
161
+ category: "example",
162
+ path: "examples/with-langgraph",
163
+ hasLocalComponents: false,
164
+ },
165
+ {
166
+ name: "with-parent-id-grouping",
167
+ label: "Parent ID Grouping",
168
+ description: "Message grouping strategy",
169
+ category: "example",
170
+ path: "examples/with-parent-id-grouping",
171
+ hasLocalComponents: false,
172
+ },
173
+ {
174
+ name: "with-react-hook-form",
175
+ label: "React Hook Form",
176
+ description: "Form integration",
177
+ category: "example",
178
+ path: "examples/with-react-hook-form",
179
+ hasLocalComponents: false,
180
+ },
181
+ {
182
+ name: "with-react-router",
183
+ label: "React Router",
184
+ description: "React Router v7 + Vite",
185
+ category: "example",
186
+ path: "examples/with-react-router",
187
+ hasLocalComponents: false,
188
+ },
189
+ {
190
+ name: "with-tanstack",
191
+ label: "TanStack",
192
+ description: "TanStack/React Router + Vite",
193
+ category: "example",
194
+ path: "examples/with-tanstack",
195
+ hasLocalComponents: false,
196
+ },
197
+ ];
41
198
 
42
- type TemplateName = keyof typeof templates;
43
- const templateNames = Object.keys(templates) as TemplateName[];
199
+ const templateNames = PROJECT_METADATA.filter(
200
+ (m) => m.category === "template",
201
+ ).map((m) => m.name);
44
202
 
45
- const templatePickerOptions: Array<{
46
- value: TemplateName;
47
- label: string;
48
- hint: string;
49
- }> = templateNames.map((name) => ({
50
- value: name,
51
- label: templates[name].label,
52
- hint: templates[name].hint,
53
- }));
54
-
55
- export async function resolveCreateTemplateName(params: {
203
+ const exampleNames = PROJECT_METADATA.filter(
204
+ (m) => m.category === "example",
205
+ ).map((m) => m.name);
206
+
207
+ export async function resolveProject(params: {
56
208
  template?: string;
209
+ example?: string;
57
210
  stdinIsTTY?: boolean;
58
211
  select?: typeof p.select;
59
212
  isCancel?: typeof p.isCancel;
60
- }): Promise<TemplateName | null> {
213
+ }): Promise<ProjectMetadata | null> {
61
214
  const {
62
215
  template,
216
+ example,
63
217
  stdinIsTTY = process.stdin.isTTY,
64
218
  select = p.select,
65
219
  isCancel = p.isCancel,
66
220
  } = params;
67
221
 
68
222
  if (template) {
69
- return template as TemplateName;
223
+ const meta = PROJECT_METADATA.find(
224
+ (m) => m.name === template && m.category === "template",
225
+ );
226
+ if (!meta) {
227
+ logger.error(`Unknown template: ${template}`);
228
+ logger.info(`Available templates: ${templateNames.join(", ")}`);
229
+ process.exit(1);
230
+ }
231
+ return meta;
232
+ }
233
+
234
+ if (example) {
235
+ const meta = PROJECT_METADATA.find(
236
+ (m) => m.name === example && m.category === "example",
237
+ );
238
+ if (!meta) {
239
+ logger.error(`Unknown example: ${example}`);
240
+ logger.info(`Available examples: ${exampleNames.join(", ")}`);
241
+ process.exit(1);
242
+ }
243
+ return meta;
70
244
  }
71
245
 
72
246
  if (!stdinIsTTY) {
73
- return "default";
247
+ return PROJECT_METADATA.find((m) => m.name === "default")!;
74
248
  }
75
249
 
76
250
  const selected = await select({
77
- message: "Select a template:",
78
- options: templatePickerOptions,
251
+ message: "Select a project to scaffold:",
252
+ options: [
253
+ {
254
+ value: "_separator",
255
+ label: "────── Starter Templates ──────",
256
+ disabled: true,
257
+ },
258
+ ...PROJECT_METADATA.filter((m) => m.category === "template").map((m) => ({
259
+ value: m.name,
260
+ label: m.label,
261
+ ...(m.description ? { hint: m.description } : {}),
262
+ })),
263
+ {
264
+ value: "_separator",
265
+ label: "────── Feature Examples ──────",
266
+ disabled: true,
267
+ },
268
+ ...PROJECT_METADATA.filter((m) => m.category === "example").map((m) => ({
269
+ value: m.name,
270
+ label: m.label,
271
+ ...(m.description ? { hint: m.description } : {}),
272
+ })),
273
+ ],
79
274
  });
80
275
 
81
276
  if (isCancel(selected)) {
82
277
  return null;
83
278
  }
84
279
 
85
- return selected as TemplateName;
280
+ const meta = PROJECT_METADATA.find((m) => m.name === selected);
281
+ if (!meta) {
282
+ logger.error(`Unknown selection: ${String(selected)}`);
283
+ process.exit(1);
284
+ }
285
+ return meta;
86
286
  }
87
287
 
88
288
  class SpawnExitError extends Error {
@@ -116,37 +316,6 @@ async function runSpawn(
116
316
  });
117
317
  }
118
318
 
119
- export function buildCreateNextAppArgs(params: {
120
- projectDirectory?: string;
121
- useNpm?: boolean;
122
- usePnpm?: boolean;
123
- useYarn?: boolean;
124
- useBun?: boolean;
125
- skipInstall?: boolean;
126
- templateUrl: string;
127
- }): string[] {
128
- const {
129
- projectDirectory,
130
- useNpm,
131
- usePnpm,
132
- useYarn,
133
- useBun,
134
- skipInstall,
135
- templateUrl,
136
- } = params;
137
-
138
- const args = ["create-next-app@latest"];
139
- if (projectDirectory) args.push(projectDirectory);
140
- if (useNpm) args.push("--use-npm");
141
- if (usePnpm) args.push("--use-pnpm");
142
- if (useYarn) args.push("--use-yarn");
143
- if (useBun) args.push("--use-bun");
144
- if (skipInstall) args.push("--skip-install");
145
-
146
- args.push("-e", templateUrl);
147
- return args;
148
- }
149
-
150
319
  export function resolveCreateProjectDirectory(params: {
151
320
  projectDirectory?: string;
152
321
  stdinIsTTY?: boolean;
@@ -158,8 +327,27 @@ export function resolveCreateProjectDirectory(params: {
158
327
  return undefined;
159
328
  }
160
329
 
161
- function buildPresetAddArgs(presetUrl: string): string[] {
162
- return ["shadcn@latest", "add", "--yes", presetUrl];
330
+ function resolvePackageManager(opts: {
331
+ useNpm?: boolean;
332
+ usePnpm?: boolean;
333
+ useYarn?: boolean;
334
+ useBun?: boolean;
335
+ }): PackageManagerName | undefined {
336
+ if (opts.useNpm) return "npm";
337
+ if (opts.usePnpm) return "pnpm";
338
+ if (opts.useYarn) return "yarn";
339
+ if (opts.useBun) return "bun";
340
+ return undefined;
341
+ }
342
+
343
+ const PLAYGROUND_PRESET_BASE_URL =
344
+ "https://www.assistant-ui.com/playground/init";
345
+
346
+ export function resolvePresetUrl(preset: string): string {
347
+ if (preset.startsWith("http://") || preset.startsWith("https://")) {
348
+ return preset;
349
+ }
350
+ return `${PLAYGROUND_PRESET_BASE_URL}?preset=${encodeURIComponent(preset)}`;
163
351
  }
164
352
 
165
353
  export const create = new Command()
@@ -173,11 +361,11 @@ export const create = new Command()
173
361
  )
174
362
  .option(
175
363
  "-e, --example <example>",
176
- "create from an example (e.g., with-langgraph, with-ai-sdk-v6)",
364
+ `create from an example (${exampleNames.join(", ")})`,
177
365
  )
178
366
  .option(
179
- "-p, --preset <url>",
180
- "preset URL from playground (e.g., https://www.assistant-ui.com/playground/init?preset=chatgpt)",
367
+ "-p, --preset <name-or-url>",
368
+ "preset name or URL (e.g., chatgpt or https://www.assistant-ui.com/playground/init?preset=chatgpt)",
181
369
  )
182
370
  .option("--use-npm", "explicitly use npm")
183
371
  .option("--use-pnpm", "explicitly use pnpm")
@@ -185,88 +373,174 @@ export const create = new Command()
185
373
  .option("--use-bun", "explicitly use bun")
186
374
  .option("--skip-install", "skip installing packages")
187
375
  .action(async (projectDirectory, opts) => {
188
- const resolvedProjectDirectory = resolveCreateProjectDirectory({
189
- projectDirectory,
190
- });
191
-
192
376
  if (opts.example && opts.preset) {
193
377
  logger.error("Cannot use --preset with --example.");
194
378
  process.exit(1);
195
379
  }
196
380
 
197
- if (opts.preset && !resolvedProjectDirectory) {
198
- logger.error("Project directory is required when using --preset.");
381
+ if (opts.template && opts.example) {
382
+ logger.error("Cannot use both --template and --example.");
199
383
  process.exit(1);
200
384
  }
201
385
 
202
- // Handle --example option
203
- if (opts.example) {
204
- if (!resolvedProjectDirectory) {
205
- logger.error("Project directory is required when using --example");
206
- process.exit(1);
207
- }
386
+ // Start release ref resolution early (runs during user prompts)
387
+ const refPromise = resolveLatestReleaseRef();
388
+
389
+ // 1. Resolve project directory
390
+ let resolvedProjectDirectory = resolveCreateProjectDirectory({
391
+ projectDirectory,
392
+ });
208
393
 
209
- await createFromExample(resolvedProjectDirectory, opts.example, {
210
- skipInstall: opts.skipInstall,
211
- useNpm: opts.useNpm,
212
- usePnpm: opts.usePnpm,
213
- useYarn: opts.useYarn,
214
- useBun: opts.useBun,
394
+ if (!resolvedProjectDirectory) {
395
+ const result = await p.text({
396
+ message: "Project name:",
397
+ placeholder: "my-aui-app",
398
+ defaultValue: "my-aui-app",
399
+ validate: (value?: string) => {
400
+ const name = (value ?? "").trim();
401
+ if (!name) return "Project name cannot be empty";
402
+ if (name === "." || name === "..")
403
+ return "Project name cannot be . or ..";
404
+ if (name.includes("/") || name.includes("\\"))
405
+ return "Project name cannot contain path separators";
406
+ return undefined;
407
+ },
215
408
  });
216
- return;
409
+
410
+ if (p.isCancel(result)) {
411
+ p.cancel("Project creation cancelled.");
412
+ process.exit(0);
413
+ }
414
+
415
+ resolvedProjectDirectory = result;
416
+ }
417
+
418
+ // Check directory
419
+ const absoluteProjectDir = path.resolve(resolvedProjectDirectory);
420
+ try {
421
+ const files = fs.readdirSync(absoluteProjectDir);
422
+ if (files.length > 0) {
423
+ logger.error(
424
+ `Directory ${resolvedProjectDirectory} already exists and is not empty`,
425
+ );
426
+ process.exit(1);
427
+ }
428
+ } catch (err: any) {
429
+ if (err.code === "ENOENT") {
430
+ // Directory doesn't exist — good, proceed
431
+ } else if (err.code === "ENOTDIR") {
432
+ logger.error(
433
+ `${resolvedProjectDirectory} already exists and is not a directory`,
434
+ );
435
+ process.exit(1);
436
+ } else {
437
+ logger.error(
438
+ `Cannot access ${resolvedProjectDirectory}: ${err.message}`,
439
+ );
440
+ process.exit(1);
441
+ }
217
442
  }
218
443
 
219
- // Handle --template option
220
- const templateName = await resolveCreateTemplateName({
444
+ // 2. Resolve scaffold target
445
+ const project = await resolveProject({
221
446
  template: opts.template,
447
+ example: opts.example,
222
448
  });
223
- if (!templateName) {
449
+ if (!project) {
224
450
  p.cancel("Project creation cancelled.");
225
451
  process.exit(0);
226
452
  }
227
453
 
228
- const templateUrl = templates[templateName]?.url;
229
-
230
- if (!templateUrl) {
231
- logger.error(`Unknown template: ${opts.template}`);
232
- logger.info(`Available templates: ${templateNames.join(", ")}`);
233
- process.exit(1);
234
- }
235
-
236
- logger.info(`Creating project with template: ${templateName}`);
454
+ logger.info(`Creating project from ${project.category}: ${project.label}`);
237
455
  logger.break();
238
456
 
239
- const createNextAppArgs = buildCreateNextAppArgs({
240
- ...(resolvedProjectDirectory
241
- ? { projectDirectory: resolvedProjectDirectory }
242
- : {}),
243
- ...(opts.useNpm ? { useNpm: true } : {}),
244
- ...(opts.usePnpm ? { usePnpm: true } : {}),
245
- ...(opts.useYarn ? { useYarn: true } : {}),
246
- ...(opts.useBun ? { useBun: true } : {}),
247
- ...(opts.skipInstall ? { skipInstall: true } : {}),
248
- templateUrl,
249
- });
457
+ const pm = await resolvePackageManagerName(
458
+ absoluteProjectDir,
459
+ resolvePackageManager(opts),
460
+ );
461
+
462
+ // Clean up partial project directory on unexpected exit (e.g. Ctrl+C)
463
+ const cleanupOnExit = () => {
464
+ fs.rmSync(absoluteProjectDir, { recursive: true, force: true });
465
+ };
466
+ process.once("exit", cleanupOnExit);
250
467
 
251
468
  try {
252
- await runSpawn("npx", createNextAppArgs);
469
+ // 3. Resolve latest release ref (started before prompts)
470
+ logger.step("Resolving latest release...");
471
+ const ref = await refPromise;
472
+ if (!ref) {
473
+ logger.warn("Could not resolve latest release, downloading from HEAD");
474
+ }
253
475
 
254
- if (opts.preset) {
255
- if (!resolvedProjectDirectory) {
256
- logger.error("Project directory is required when using --preset.");
257
- process.exit(1);
476
+ // 4. Download project
477
+ logger.step("Downloading project...");
478
+ try {
479
+ await downloadProject(project.path, absoluteProjectDir, ref);
480
+
481
+ // If the template didn't exist at the release tag, retry from HEAD
482
+ if (
483
+ ref &&
484
+ !fs.existsSync(path.join(absoluteProjectDir, "package.json"))
485
+ ) {
486
+ fs.rmSync(absoluteProjectDir, { recursive: true, force: true });
487
+ logger.warn(
488
+ "Template not found at release tag, downloading from HEAD",
489
+ );
490
+ await downloadProject(project.path, absoluteProjectDir);
258
491
  }
492
+
493
+ // 5. Run transform pipeline
494
+ await transformProject(absoluteProjectDir, {
495
+ hasLocalComponents: project.hasLocalComponents,
496
+ skipInstall: opts.skipInstall,
497
+ packageManager: pm,
498
+ });
499
+ } catch (err) {
500
+ // Clean up partially created project directory
501
+ fs.rmSync(absoluteProjectDir, { recursive: true, force: true });
502
+ throw err;
503
+ }
504
+
505
+ // 6. Apply preset if provided
506
+ if (opts.preset) {
507
+ const presetUrl = resolvePresetUrl(opts.preset);
259
508
  logger.info("Applying preset configuration...");
260
509
  logger.break();
261
- await runSpawn(
262
- "npx",
263
- buildPresetAddArgs(opts.preset),
264
- path.resolve(process.cwd(), resolvedProjectDirectory),
265
- );
510
+ const [dlxCmd, dlxArgs] = dlxCommand(pm);
511
+ try {
512
+ await runSpawn(
513
+ dlxCmd,
514
+ [
515
+ ...dlxArgs,
516
+ "shadcn@latest",
517
+ "add",
518
+ "--yes",
519
+ "--overwrite",
520
+ presetUrl,
521
+ ],
522
+ absoluteProjectDir,
523
+ );
524
+ } catch {
525
+ logger.warn(
526
+ `Preset application failed. You can retry manually with:\n ${dlxCmd} ${[...dlxArgs, "shadcn@latest", "add", presetUrl].join(" ")}`,
527
+ );
528
+ }
266
529
  }
267
530
 
531
+ process.removeListener("exit", cleanupOnExit);
532
+
268
533
  logger.break();
269
534
  logger.success("Project created successfully!");
535
+ logger.break();
536
+ const runCmd = pm === "npm" ? "npm run" : pm;
537
+ logger.info("Next steps:");
538
+ logger.info(` cd ${resolvedProjectDirectory}`);
539
+ if (opts.skipInstall) {
540
+ logger.info(` ${pm} install`);
541
+ }
542
+ logger.info(" # Set up your environment variables in .env.local");
543
+ logger.info(` ${runCmd} dev`);
270
544
  } catch (error) {
271
545
  if (error instanceof SpawnExitError) {
272
546
  logger.error(`Project creation failed with code ${error.code}`);
@@ -89,8 +89,8 @@ export const init = new Command()
89
89
  )
90
90
  .addOption(
91
91
  new Option(
92
- "-p, --preset <url>",
93
- "preset URL from playground (forwarded to 'assistant-ui create')",
92
+ "-p, --preset <name-or-url>",
93
+ "preset name or URL (forwarded to 'assistant-ui create')",
94
94
  ).hideHelp(),
95
95
  )
96
96
  .option("--use-npm", "explicitly use npm")
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ import { codemodCommand, upgradeCommand } from "./commands/upgrade";
7
7
  import { init } from "./commands/init";
8
8
  import { update } from "./commands/update";
9
9
  import { mcp } from "./commands/mcp";
10
+ import { agent } from "./commands/agent";
10
11
 
11
12
  process.on("SIGINT", () => process.exit(0));
12
13
  process.on("SIGTERM", () => process.exit(0));
@@ -23,6 +24,7 @@ function main() {
23
24
  program.addCommand(codemodCommand);
24
25
  program.addCommand(upgradeCommand);
25
26
  program.addCommand(update);
27
+ program.addCommand(agent);
26
28
 
27
29
  program.parse();
28
30
  }