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.
- package/README.md +16 -3
- package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts +3 -0
- package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -0
- package/dist/codemods/v0-12/primitive-if-to-aui-if.js +308 -0
- package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -0
- package/dist/commands/create.d.ts +54 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +160 -29
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +103 -76
- package/dist/commands/init.js.map +1 -1
- package/dist/lib/create-from-example.d.ts.map +1 -1
- package/dist/lib/create-from-example.js +9 -3
- package/dist/lib/create-from-example.js.map +1 -1
- package/dist/lib/upgrade.d.ts.map +1 -1
- package/dist/lib/upgrade.js +1 -0
- package/dist/lib/upgrade.js.map +1 -1
- package/package.json +4 -4
- package/src/codemods/v0-12/__tests__/assistant-api-to-aui.test.ts +10 -10
- package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +839 -0
- package/src/codemods/v0-12/primitive-if-to-aui-if.ts +355 -0
- package/src/commands/create.ts +219 -37
- package/src/commands/init.ts +130 -88
- package/src/lib/create-from-example.ts +9 -3
- package/src/lib/upgrade.ts +1 -0
|
@@ -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;
|
package/src/commands/create.ts
CHANGED
|
@@ -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:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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 = (
|
|
56
|
-
|
|
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
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
});
|