assistant-ui 0.0.78 → 0.0.80

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.
@@ -0,0 +1,355 @@
1
+ import { createTransformer } from "../utils/createTransformer";
2
+
3
+ type ConditionFragment = {
4
+ expression: string;
5
+ negated: boolean;
6
+ };
7
+
8
+ // Map ThreadPrimitive.If props to condition expressions
9
+ const threadPropMap: Record<
10
+ string,
11
+ (value: unknown) => ConditionFragment | null
12
+ > = {
13
+ empty: (v) => ({
14
+ expression: "s.thread.isEmpty",
15
+ negated: v === false,
16
+ }),
17
+ running: (v) => ({
18
+ expression: "s.thread.isRunning",
19
+ negated: v === false,
20
+ }),
21
+ disabled: (v) => ({
22
+ expression: "s.thread.isDisabled",
23
+ negated: v === false,
24
+ }),
25
+ };
26
+
27
+ // Map MessagePrimitive.If props to condition expressions
28
+ const messagePropMap: Record<
29
+ string,
30
+ (value: unknown) => ConditionFragment | null
31
+ > = {
32
+ user: () => ({ expression: 's.message.role === "user"', negated: false }),
33
+ assistant: () => ({
34
+ expression: 's.message.role === "assistant"',
35
+ negated: false,
36
+ }),
37
+ system: () => ({
38
+ expression: 's.message.role === "system"',
39
+ negated: false,
40
+ }),
41
+ hasBranches: () => ({
42
+ expression: "s.message.branchCount >= 2",
43
+ negated: false,
44
+ }),
45
+ copied: (v) => ({
46
+ expression: "s.message.isCopied",
47
+ negated: v === false,
48
+ }),
49
+ last: (v) => ({
50
+ expression: "s.message.isLast",
51
+ negated: v === false,
52
+ }),
53
+ lastOrHover: () => ({
54
+ expression: "s.message.isHovering || s.message.isLast",
55
+ negated: false,
56
+ }),
57
+ speaking: (v) => ({
58
+ expression: "s.message.speech != null",
59
+ negated: v === false,
60
+ }),
61
+ hasAttachments: (v) =>
62
+ v === true
63
+ ? {
64
+ expression:
65
+ 's.message.role === "user" && !!s.message.attachments?.length',
66
+ negated: false,
67
+ }
68
+ : {
69
+ expression:
70
+ 's.message.role !== "user" || !s.message.attachments?.length',
71
+ negated: false,
72
+ },
73
+ hasContent: (v) => ({
74
+ expression: "s.message.parts.length > 0",
75
+ negated: v === false,
76
+ }),
77
+ submittedFeedback: (v) => {
78
+ if (v === null) {
79
+ return {
80
+ expression:
81
+ "(s.message.metadata.submittedFeedback?.type ?? null) === null",
82
+ negated: false,
83
+ };
84
+ }
85
+ return {
86
+ expression: `s.message.metadata.submittedFeedback?.type === "${v}"`,
87
+ negated: false,
88
+ };
89
+ },
90
+ };
91
+
92
+ // Map ComposerPrimitive.If props to condition expressions
93
+ const composerPropMap: Record<
94
+ string,
95
+ (value: unknown) => ConditionFragment | null
96
+ > = {
97
+ editing: (v) => ({
98
+ expression: "s.composer.isEditing",
99
+ negated: v === false,
100
+ }),
101
+ dictation: (v) => ({
102
+ expression: "s.composer.dictation != null",
103
+ negated: v === false,
104
+ }),
105
+ };
106
+
107
+ const primitiveMap: Record<
108
+ string,
109
+ Record<string, (value: unknown) => ConditionFragment | null>
110
+ > = {
111
+ ThreadPrimitive: threadPropMap,
112
+ MessagePrimitive: messagePropMap,
113
+ ComposerPrimitive: composerPropMap,
114
+ };
115
+
116
+ // Map of XPrimitive.Component → fixed condition (no props needed)
117
+ const fixedConditionMap: Record<string, Record<string, string>> = {
118
+ ThreadPrimitive: {
119
+ Empty: "s.thread.isEmpty",
120
+ },
121
+ };
122
+
123
+ /**
124
+ * Extract the value of a JSX attribute.
125
+ * - Boolean prop (no value): `<X.If user>` → `true`
126
+ * - `{true}` / `{false}`: → `true` / `false`
127
+ * - `{"positive"}`: → `"positive"`
128
+ * - `{null}`: → `null`
129
+ */
130
+ const getAttrValue = (j: any, attr: any): unknown => {
131
+ // Boolean attribute (no value), e.g. `<X.If user>`
132
+ if (attr.value === null || attr.value === undefined) {
133
+ return true;
134
+ }
135
+
136
+ // JSX expression container: `{true}`, `{false}`, `{"positive"}`, `{null}`
137
+ if (j.JSXExpressionContainer.check(attr.value)) {
138
+ const expr = attr.value.expression;
139
+ if (j.BooleanLiteral.check(expr)) return expr.value;
140
+ if (j.Literal.check(expr)) {
141
+ if (expr.value === null) return null;
142
+ return expr.value;
143
+ }
144
+ if (j.NullLiteral.check(expr)) return null;
145
+ if (j.Identifier.check(expr) && expr.name === "undefined") return undefined;
146
+ }
147
+
148
+ // String literal
149
+ if (j.StringLiteral.check(attr.value) || j.Literal.check(attr.value)) {
150
+ return attr.value.value;
151
+ }
152
+
153
+ return undefined;
154
+ };
155
+
156
+ const buildConditionString = (fragments: ConditionFragment[]): string => {
157
+ const parts = fragments.map((f) =>
158
+ f.negated ? `!${f.expression}` : f.expression,
159
+ );
160
+ if (parts.length === 1) return parts[0]!;
161
+ return parts.join(" && ");
162
+ };
163
+
164
+ const migratePrimitiveIfToAuiIf = createTransformer(
165
+ ({ j, root, markAsChanged }) => {
166
+ let needsAuiIfImport = false;
167
+
168
+ // Track which primitive namespaces are imported
169
+ const importedPrimitives = new Set<string>();
170
+ root.find(j.ImportDeclaration).forEach((path: any) => {
171
+ const source = path.value.source.value;
172
+ if (typeof source === "string" && source.startsWith("@assistant-ui/")) {
173
+ path.value.specifiers?.forEach((specifier: any) => {
174
+ if (j.ImportSpecifier.check(specifier)) {
175
+ const name = String(
176
+ specifier.local?.name ?? specifier.imported.name,
177
+ );
178
+ if (primitiveMap[name] || fixedConditionMap[name]) {
179
+ importedPrimitives.add(name);
180
+ }
181
+ }
182
+ });
183
+ }
184
+ });
185
+
186
+ if (importedPrimitives.size === 0) return;
187
+
188
+ // Process fixed-condition components: <ThreadPrimitive.Empty> → <AuiIf condition={...}>
189
+ root.find(j.JSXOpeningElement).forEach((path: any) => {
190
+ const name = path.value.name;
191
+ if (!j.JSXMemberExpression.check(name)) return;
192
+ if (!j.JSXIdentifier.check(name.object)) return;
193
+ if (!j.JSXIdentifier.check(name.property)) return;
194
+
195
+ const primitiveName = name.object.name as string;
196
+ const propertyName = name.property.name as string;
197
+ const fixedMap = fixedConditionMap[primitiveName];
198
+ if (!fixedMap) return;
199
+ const conditionBody = fixedMap[propertyName];
200
+ if (!conditionBody) return;
201
+ if (!importedPrimitives.has(primitiveName)) return;
202
+
203
+ // Only transform if there are no props (other than children, which are implicit)
204
+ const attrs: any[] = path.value.attributes || [];
205
+ if (attrs.length > 0) return;
206
+
207
+ const arrowFnAst = j(`(s) => ${conditionBody}`)
208
+ .find(j.ArrowFunctionExpression)
209
+ .paths()[0]!.value;
210
+
211
+ path.value.name = j.jsxIdentifier("AuiIf");
212
+ path.value.attributes = [
213
+ j.jsxAttribute(
214
+ j.jsxIdentifier("condition"),
215
+ j.jsxExpressionContainer(arrowFnAst),
216
+ ),
217
+ ];
218
+
219
+ needsAuiIfImport = true;
220
+ markAsChanged();
221
+ });
222
+
223
+ // Update closing elements for fixed-condition components
224
+ root.find(j.JSXClosingElement).forEach((path: any) => {
225
+ const name = path.value.name;
226
+ if (!j.JSXMemberExpression.check(name)) return;
227
+ if (!j.JSXIdentifier.check(name.object)) return;
228
+ if (!j.JSXIdentifier.check(name.property)) return;
229
+
230
+ const primitiveName = name.object.name as string;
231
+ const propertyName = name.property.name as string;
232
+ const fixedMap = fixedConditionMap[primitiveName];
233
+ if (!fixedMap || !fixedMap[propertyName]) return;
234
+ if (!importedPrimitives.has(primitiveName)) return;
235
+
236
+ path.value.name = j.jsxIdentifier("AuiIf");
237
+ markAsChanged();
238
+ });
239
+
240
+ // Process JSX elements: <ThreadPrimitive.If ...> → <AuiIf condition={...}>
241
+ root.find(j.JSXOpeningElement).forEach((path: any) => {
242
+ const name = path.value.name;
243
+
244
+ // Check for `<XPrimitive.If ...>`
245
+ if (!j.JSXMemberExpression.check(name)) return;
246
+ if (!j.JSXIdentifier.check(name.object)) return;
247
+ if (!j.JSXIdentifier.check(name.property)) return;
248
+ if (name.property.name !== "If") return;
249
+
250
+ const primitiveName = name.object.name;
251
+ const propMap = primitiveMap[primitiveName];
252
+ if (!propMap) return;
253
+ if (!importedPrimitives.has(primitiveName)) return;
254
+
255
+ // Extract props
256
+ const attrs: any[] = path.value.attributes || [];
257
+ const fragments: ConditionFragment[] = [];
258
+ let hasUnknownProp = false;
259
+
260
+ for (const attr of attrs) {
261
+ if (!j.JSXAttribute.check(attr)) {
262
+ // JSX spread attributes — can't migrate
263
+ hasUnknownProp = true;
264
+ continue;
265
+ }
266
+ const propName =
267
+ typeof attr.name.name === "string" ? attr.name.name : null;
268
+ if (!propName) continue;
269
+
270
+ const mapper = propMap[propName];
271
+ if (!mapper) {
272
+ hasUnknownProp = true;
273
+ continue;
274
+ }
275
+
276
+ const value = getAttrValue(j, attr);
277
+ const fragment = mapper(value);
278
+ if (fragment) {
279
+ fragments.push(fragment);
280
+ }
281
+ }
282
+
283
+ // If we couldn't map all props, skip this element
284
+ if (hasUnknownProp || fragments.length === 0) return;
285
+
286
+ const conditionBody = buildConditionString(fragments);
287
+
288
+ // Parse the arrow function as an expression to get a proper AST node
289
+ const arrowFnAst = j(`(s) => ${conditionBody}`)
290
+ .find(j.ArrowFunctionExpression)
291
+ .paths()[0]!.value;
292
+
293
+ // Replace <XPrimitive.If ...> with <AuiIf condition={...}>
294
+ path.value.name = j.jsxIdentifier("AuiIf");
295
+
296
+ // Replace all attributes with a single condition prop
297
+ path.value.attributes = [
298
+ j.jsxAttribute(
299
+ j.jsxIdentifier("condition"),
300
+ j.jsxExpressionContainer(arrowFnAst),
301
+ ),
302
+ ];
303
+
304
+ needsAuiIfImport = true;
305
+ markAsChanged();
306
+ });
307
+
308
+ // Update closing elements to match
309
+ root.find(j.JSXClosingElement).forEach((path: any) => {
310
+ const name = path.value.name;
311
+ if (!j.JSXMemberExpression.check(name)) return;
312
+ if (!j.JSXIdentifier.check(name.object)) return;
313
+ if (!j.JSXIdentifier.check(name.property)) return;
314
+ if (name.property.name !== "If") return;
315
+
316
+ const primitiveName = name.object.name;
317
+ if (!primitiveMap[primitiveName]) return;
318
+ if (!importedPrimitives.has(primitiveName)) return;
319
+
320
+ path.value.name = j.jsxIdentifier("AuiIf");
321
+ markAsChanged();
322
+ });
323
+
324
+ // Add AuiIf import if needed
325
+ if (needsAuiIfImport) {
326
+ let hasAuiIfImport = false;
327
+ let assistantUiImport: any = null;
328
+
329
+ root.find(j.ImportDeclaration).forEach((path: any) => {
330
+ const source = path.value.source.value;
331
+ if (typeof source === "string" && source.startsWith("@assistant-ui/")) {
332
+ assistantUiImport = path;
333
+ path.value.specifiers?.forEach((specifier: any) => {
334
+ if (
335
+ j.ImportSpecifier.check(specifier) &&
336
+ (specifier.imported.name === "AuiIf" ||
337
+ specifier.local?.name === "AuiIf")
338
+ ) {
339
+ hasAuiIfImport = true;
340
+ }
341
+ });
342
+ }
343
+ });
344
+
345
+ if (!hasAuiIfImport && assistantUiImport) {
346
+ assistantUiImport.value.specifiers.push(
347
+ j.importSpecifier(j.identifier("AuiIf")),
348
+ );
349
+ markAsChanged();
350
+ }
351
+ }
352
+ },
353
+ );
354
+
355
+ export default migratePrimitiveIfToAuiIf;
@@ -1,19 +1,167 @@
1
1
  import { Command } from "commander";
2
2
  import chalk from "chalk";
3
3
  import { spawn } from "cross-spawn";
4
+ import path from "node:path";
5
+ import * as p from "@clack/prompts";
4
6
  import { logger } from "../lib/utils/logger";
5
7
  import { createFromExample } from "../lib/create-from-example";
6
8
 
7
9
  // Keep in sync with packages/create-assistant-ui/src/index.ts
8
10
  const templates = {
9
- default: "https://github.com/assistant-ui/assistant-ui-starter",
10
- minimal: "https://github.com/assistant-ui/assistant-ui-starter-minimal",
11
- cloud: "https://github.com/assistant-ui/assistant-ui-starter-cloud",
12
- langgraph: "https://github.com/assistant-ui/assistant-ui-starter-langgraph",
13
- mcp: "https://github.com/assistant-ui/assistant-ui-starter-mcp",
14
- };
11
+ default: {
12
+ url: "https://github.com/assistant-ui/assistant-ui-starter",
13
+ label: "Default",
14
+ hint: "Default template with Vercel AI SDK",
15
+ },
16
+ minimal: {
17
+ url: "https://github.com/assistant-ui/assistant-ui-starter-minimal",
18
+ label: "Minimal",
19
+ hint: "Bare-bones starting point",
20
+ },
21
+ cloud: {
22
+ url: "https://github.com/assistant-ui/assistant-cloud-starter",
23
+ label: "Cloud",
24
+ hint: "Cloud-backed persistence starter",
25
+ },
26
+ "cloud-clerk": {
27
+ url: "https://github.com/assistant-ui/assistant-ui-starter-cloud-clerk",
28
+ label: "Cloud + Clerk",
29
+ hint: "Cloud-backed starter with Clerk auth",
30
+ },
31
+ langgraph: {
32
+ url: "https://github.com/assistant-ui/assistant-ui-starter-langgraph",
33
+ label: "LangGraph",
34
+ hint: "LangGraph starter template",
35
+ },
36
+ mcp: {
37
+ url: "https://github.com/assistant-ui/assistant-ui-starter-mcp",
38
+ label: "MCP",
39
+ hint: "MCP starter template",
40
+ },
41
+ } as const;
15
42
 
16
- const templateNames = Object.keys(templates);
43
+ type TemplateName = keyof typeof templates;
44
+ const templateNames = Object.keys(templates) as TemplateName[];
45
+
46
+ const templatePickerOptions: Array<{
47
+ value: TemplateName;
48
+ label: string;
49
+ hint: string;
50
+ }> = templateNames.map((name) => ({
51
+ value: name,
52
+ label: templates[name].label,
53
+ hint: templates[name].hint,
54
+ }));
55
+
56
+ export async function resolveCreateTemplateName(params: {
57
+ template?: string;
58
+ stdinIsTTY?: boolean;
59
+ select?: typeof p.select;
60
+ isCancel?: typeof p.isCancel;
61
+ }): Promise<TemplateName | null> {
62
+ const {
63
+ template,
64
+ stdinIsTTY = process.stdin.isTTY,
65
+ select = p.select,
66
+ isCancel = p.isCancel,
67
+ } = params;
68
+
69
+ if (template) {
70
+ return template as TemplateName;
71
+ }
72
+
73
+ if (!stdinIsTTY) {
74
+ return "default";
75
+ }
76
+
77
+ const selected = await select({
78
+ message: "Select a template:",
79
+ options: templatePickerOptions,
80
+ });
81
+
82
+ if (isCancel(selected)) {
83
+ return null;
84
+ }
85
+
86
+ return selected as TemplateName;
87
+ }
88
+
89
+ class SpawnExitError extends Error {
90
+ code: number;
91
+
92
+ constructor(code: number) {
93
+ super(`Process exited with code ${code}`);
94
+ this.code = code;
95
+ }
96
+ }
97
+
98
+ async function runSpawn(
99
+ command: string,
100
+ args: string[],
101
+ cwd?: string,
102
+ ): Promise<void> {
103
+ return new Promise((resolve, reject) => {
104
+ const child = spawn(command, args, {
105
+ stdio: "inherit",
106
+ cwd,
107
+ });
108
+
109
+ child.on("error", (error) => reject(error));
110
+ child.on("close", (code) => {
111
+ if (code !== 0) {
112
+ reject(new SpawnExitError(code || 1));
113
+ } else {
114
+ resolve();
115
+ }
116
+ });
117
+ });
118
+ }
119
+
120
+ export function buildCreateNextAppArgs(params: {
121
+ projectDirectory?: string;
122
+ useNpm?: boolean;
123
+ usePnpm?: boolean;
124
+ useYarn?: boolean;
125
+ useBun?: boolean;
126
+ skipInstall?: boolean;
127
+ templateUrl: string;
128
+ }): string[] {
129
+ const {
130
+ projectDirectory,
131
+ useNpm,
132
+ usePnpm,
133
+ useYarn,
134
+ useBun,
135
+ skipInstall,
136
+ templateUrl,
137
+ } = params;
138
+
139
+ const args = ["create-next-app@latest"];
140
+ if (projectDirectory) args.push(projectDirectory);
141
+ if (useNpm) args.push("--use-npm");
142
+ if (usePnpm) args.push("--use-pnpm");
143
+ if (useYarn) args.push("--use-yarn");
144
+ if (useBun) args.push("--use-bun");
145
+ if (skipInstall) args.push("--skip-install");
146
+
147
+ args.push("-e", templateUrl);
148
+ return args;
149
+ }
150
+
151
+ export function resolveCreateProjectDirectory(params: {
152
+ projectDirectory?: string;
153
+ stdinIsTTY?: boolean;
154
+ }): string | undefined {
155
+ const { projectDirectory, stdinIsTTY = process.stdin.isTTY } = params;
156
+
157
+ if (projectDirectory) return projectDirectory;
158
+ if (!stdinIsTTY) return "my-aui-app";
159
+ return undefined;
160
+ }
161
+
162
+ function buildPresetAddArgs(presetUrl: string): string[] {
163
+ return ["shadcn@latest", "add", "--yes", presetUrl];
164
+ }
17
165
 
18
166
  export const create = new Command()
19
167
  .name("create")
@@ -28,20 +176,38 @@ export const create = new Command()
28
176
  "-e, --example <example>",
29
177
  "create from an example (e.g., with-langgraph, with-ai-sdk-v6)",
30
178
  )
179
+ .option(
180
+ "-p, --preset <url>",
181
+ "preset URL from playground (e.g., https://www.assistant-ui.com/playground/init?preset=chatgpt)",
182
+ )
31
183
  .option("--use-npm", "explicitly use npm")
32
184
  .option("--use-pnpm", "explicitly use pnpm")
33
185
  .option("--use-yarn", "explicitly use yarn")
34
186
  .option("--use-bun", "explicitly use bun")
35
187
  .option("--skip-install", "skip installing packages")
36
188
  .action(async (projectDirectory, opts) => {
189
+ const resolvedProjectDirectory = resolveCreateProjectDirectory({
190
+ projectDirectory,
191
+ });
192
+
193
+ if (opts.example && opts.preset) {
194
+ logger.error("Cannot use --preset with --example.");
195
+ process.exit(1);
196
+ }
197
+
198
+ if (opts.preset && !resolvedProjectDirectory) {
199
+ logger.error("Project directory is required when using --preset.");
200
+ process.exit(1);
201
+ }
202
+
37
203
  // Handle --example option
38
204
  if (opts.example) {
39
- if (!projectDirectory) {
205
+ if (!resolvedProjectDirectory) {
40
206
  logger.error("Project directory is required when using --example");
41
207
  process.exit(1);
42
208
  }
43
209
 
44
- await createFromExample(projectDirectory, opts.example, {
210
+ await createFromExample(resolvedProjectDirectory, opts.example, {
45
211
  skipInstall: opts.skipInstall,
46
212
  useNpm: opts.useNpm,
47
213
  usePnpm: opts.usePnpm,
@@ -52,8 +218,15 @@ export const create = new Command()
52
218
  }
53
219
 
54
220
  // Handle --template option
55
- const templateName = (opts.template as keyof typeof templates) ?? "default";
56
- const templateUrl = templates[templateName];
221
+ const templateName = await resolveCreateTemplateName({
222
+ template: opts.template,
223
+ });
224
+ if (!templateName) {
225
+ p.cancel("Project creation cancelled.");
226
+ process.exit(0);
227
+ }
228
+
229
+ const templateUrl = templates[templateName]?.url;
57
230
 
58
231
  if (!templateUrl) {
59
232
  logger.error(`Unknown template: ${opts.template}`);
@@ -64,35 +237,44 @@ export const create = new Command()
64
237
  logger.info(`Creating project with template: ${templateName}`);
65
238
  logger.break();
66
239
 
67
- const filteredArgs = process.argv.slice(3).filter((arg, index, arr) => {
68
- return !(
69
- arg === "-t" ||
70
- arg === "--template" ||
71
- arr[index - 1] === "-t" ||
72
- arr[index - 1] === "--template"
73
- );
240
+ const createNextAppArgs = buildCreateNextAppArgs({
241
+ ...(resolvedProjectDirectory
242
+ ? { projectDirectory: resolvedProjectDirectory }
243
+ : {}),
244
+ ...(opts.useNpm ? { useNpm: true } : {}),
245
+ ...(opts.usePnpm ? { usePnpm: true } : {}),
246
+ ...(opts.useYarn ? { useYarn: true } : {}),
247
+ ...(opts.useBun ? { useBun: true } : {}),
248
+ ...(opts.skipInstall ? { skipInstall: true } : {}),
249
+ templateUrl,
74
250
  });
75
251
 
76
- const child = spawn(
77
- "npx",
78
- [`create-next-app@latest`, ...filteredArgs, "-e", templateUrl],
79
- {
80
- stdio: "inherit",
81
- },
82
- );
83
-
84
- child.on("error", (error) => {
85
- logger.error(`Failed to create project: ${error.message}`);
86
- process.exit(1);
87
- });
252
+ try {
253
+ await runSpawn("npx", createNextAppArgs);
88
254
 
89
- child.on("close", (code) => {
90
- if (code !== 0) {
91
- logger.error(`Project creation failed with code ${code}`);
92
- process.exit(code || 1);
93
- } else {
255
+ if (opts.preset) {
256
+ if (!resolvedProjectDirectory) {
257
+ logger.error("Project directory is required when using --preset.");
258
+ process.exit(1);
259
+ }
260
+ logger.info("Applying preset configuration...");
94
261
  logger.break();
95
- logger.success("Project created successfully!");
262
+ await runSpawn(
263
+ "npx",
264
+ buildPresetAddArgs(opts.preset),
265
+ path.resolve(process.cwd(), resolvedProjectDirectory),
266
+ );
96
267
  }
97
- });
268
+
269
+ logger.break();
270
+ logger.success("Project created successfully!");
271
+ } catch (error) {
272
+ if (error instanceof SpawnExitError) {
273
+ logger.error(`Project creation failed with code ${error.code}`);
274
+ process.exit(error.code);
275
+ }
276
+ const message = error instanceof Error ? error.message : String(error);
277
+ logger.error(`Failed to create project: ${message}`);
278
+ process.exit(1);
279
+ }
98
280
  });