assistant-ui 0.0.81 → 0.0.83
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 +1 -1
- package/bin/assistant-ui.js +2 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.d.ts.map +1 -0
- package/dist/commands/agent.js +34 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/create.d.ts +13 -45
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +389 -101
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/create-project.d.ts +12 -0
- package/dist/lib/create-project.d.ts.map +1 -0
- package/dist/lib/create-project.js +273 -0
- package/dist/lib/create-project.js.map +1 -0
- package/package.json +8 -4
- package/plugin/.claude-plugin/plugin.json +8 -0
- package/plugin/skills/assistant-ui/SKILL.md +167 -0
- package/src/commands/agent.ts +36 -0
- package/src/commands/create.ts +452 -129
- package/src/commands/init.ts +2 -2
- package/src/index.ts +2 -0
- package/src/lib/create-project.ts +367 -0
- package/dist/lib/create-from-example.d.ts +0 -9
- package/dist/lib/create-from-example.d.ts.map +0 -1
- package/dist/lib/create-from-example.js +0 -325
- package/dist/lib/create-from-example.js.map +0 -1
- package/src/lib/create-from-example.ts +0 -426
package/src/commands/create.ts
CHANGED
|
@@ -1,88 +1,313 @@
|
|
|
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 {
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
26
|
+
export const PROJECT_METADATA: ProjectMetadata[] = [
|
|
27
|
+
// Templates
|
|
28
|
+
{
|
|
29
|
+
name: "default",
|
|
12
30
|
label: "Default",
|
|
13
|
-
|
|
31
|
+
description: "Default template with Vercel AI SDK",
|
|
32
|
+
category: "template",
|
|
33
|
+
path: "templates/default",
|
|
34
|
+
hasLocalComponents: true,
|
|
14
35
|
},
|
|
15
|
-
|
|
16
|
-
|
|
36
|
+
{
|
|
37
|
+
name: "minimal",
|
|
17
38
|
label: "Minimal",
|
|
18
|
-
|
|
39
|
+
description: "Bare-bones starting point",
|
|
40
|
+
category: "template",
|
|
41
|
+
path: "templates/minimal",
|
|
42
|
+
hasLocalComponents: true,
|
|
19
43
|
},
|
|
20
|
-
|
|
21
|
-
|
|
44
|
+
{
|
|
45
|
+
name: "cloud",
|
|
22
46
|
label: "Cloud",
|
|
23
|
-
|
|
47
|
+
description: "Cloud-backed persistence starter",
|
|
48
|
+
category: "template",
|
|
49
|
+
path: "templates/cloud",
|
|
50
|
+
hasLocalComponents: true,
|
|
24
51
|
},
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
{
|
|
53
|
+
name: "cloud-clerk",
|
|
27
54
|
label: "Cloud + Clerk",
|
|
28
|
-
|
|
55
|
+
description: "Cloud-backed starter with Clerk auth",
|
|
56
|
+
category: "template",
|
|
57
|
+
path: "templates/cloud-clerk",
|
|
58
|
+
hasLocalComponents: true,
|
|
29
59
|
},
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
{
|
|
61
|
+
name: "langgraph",
|
|
32
62
|
label: "LangGraph",
|
|
33
|
-
|
|
63
|
+
description: "LangGraph starter template",
|
|
64
|
+
category: "template",
|
|
65
|
+
path: "templates/langgraph",
|
|
66
|
+
hasLocalComponents: true,
|
|
34
67
|
},
|
|
35
|
-
|
|
36
|
-
|
|
68
|
+
{
|
|
69
|
+
name: "mcp",
|
|
37
70
|
label: "MCP",
|
|
38
|
-
|
|
71
|
+
description: "MCP starter template",
|
|
72
|
+
category: "template",
|
|
73
|
+
path: "templates/mcp",
|
|
74
|
+
hasLocalComponents: true,
|
|
39
75
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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-expo",
|
|
143
|
+
label: "Expo",
|
|
144
|
+
description: "Expo / React Native",
|
|
145
|
+
category: "example",
|
|
146
|
+
path: "examples/with-expo",
|
|
147
|
+
hasLocalComponents: true,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: "with-external-store",
|
|
151
|
+
label: "External Store",
|
|
152
|
+
description: "Custom message store",
|
|
153
|
+
category: "example",
|
|
154
|
+
path: "examples/with-external-store",
|
|
155
|
+
hasLocalComponents: false,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
name: "with-ffmpeg",
|
|
159
|
+
label: "FFmpeg",
|
|
160
|
+
description: "File processing",
|
|
161
|
+
category: "example",
|
|
162
|
+
path: "examples/with-ffmpeg",
|
|
163
|
+
hasLocalComponents: false,
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "with-langgraph",
|
|
167
|
+
label: "LangGraph Example",
|
|
168
|
+
description: "LangGraph integration",
|
|
169
|
+
category: "example",
|
|
170
|
+
path: "examples/with-langgraph",
|
|
171
|
+
hasLocalComponents: false,
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "with-parent-id-grouping",
|
|
175
|
+
label: "Parent ID Grouping",
|
|
176
|
+
description: "Message grouping strategy",
|
|
177
|
+
category: "example",
|
|
178
|
+
path: "examples/with-parent-id-grouping",
|
|
179
|
+
hasLocalComponents: false,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "with-react-hook-form",
|
|
183
|
+
label: "React Hook Form",
|
|
184
|
+
description: "Form integration",
|
|
185
|
+
category: "example",
|
|
186
|
+
path: "examples/with-react-hook-form",
|
|
187
|
+
hasLocalComponents: false,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "with-react-ink",
|
|
191
|
+
label: "React Ink",
|
|
192
|
+
description: "Terminal UI chat",
|
|
193
|
+
category: "example",
|
|
194
|
+
path: "examples/with-react-ink",
|
|
195
|
+
hasLocalComponents: true,
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "with-react-router",
|
|
199
|
+
label: "React Router",
|
|
200
|
+
description: "React Router v7 + Vite",
|
|
201
|
+
category: "example",
|
|
202
|
+
path: "examples/with-react-router",
|
|
203
|
+
hasLocalComponents: false,
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
name: "with-tanstack",
|
|
207
|
+
label: "TanStack",
|
|
208
|
+
description: "TanStack/React Router + Vite",
|
|
209
|
+
category: "example",
|
|
210
|
+
path: "examples/with-tanstack",
|
|
211
|
+
hasLocalComponents: false,
|
|
212
|
+
},
|
|
213
|
+
];
|
|
214
|
+
|
|
215
|
+
// Examples that exist in the monorepo but are intentionally excluded from the CLI:
|
|
216
|
+
//
|
|
217
|
+
// - waterfall: Still in development, not ready for production.
|
|
218
|
+
// - with-cloud-standalone: For cloud without assistant-ui — not for the
|
|
219
|
+
// assistant-ui CLI.
|
|
220
|
+
// - with-store: In development, not ready for public use of the tap store.
|
|
221
|
+
// - with-tap-runtime: In development, not ready for public use of the tap
|
|
222
|
+
// store.
|
|
223
|
+
|
|
224
|
+
const templateNames = PROJECT_METADATA.filter(
|
|
225
|
+
(m) => m.category === "template",
|
|
226
|
+
).map((m) => m.name);
|
|
227
|
+
|
|
228
|
+
const exampleNames = PROJECT_METADATA.filter(
|
|
229
|
+
(m) => m.category === "example",
|
|
230
|
+
).map((m) => m.name);
|
|
231
|
+
|
|
232
|
+
export async function resolveProject(params: {
|
|
56
233
|
template?: string;
|
|
234
|
+
example?: string;
|
|
57
235
|
stdinIsTTY?: boolean;
|
|
58
236
|
select?: typeof p.select;
|
|
59
237
|
isCancel?: typeof p.isCancel;
|
|
60
|
-
}): Promise<
|
|
238
|
+
}): Promise<ProjectMetadata | null> {
|
|
61
239
|
const {
|
|
62
240
|
template,
|
|
241
|
+
example,
|
|
63
242
|
stdinIsTTY = process.stdin.isTTY,
|
|
64
243
|
select = p.select,
|
|
65
244
|
isCancel = p.isCancel,
|
|
66
245
|
} = params;
|
|
67
246
|
|
|
68
247
|
if (template) {
|
|
69
|
-
|
|
248
|
+
const meta = PROJECT_METADATA.find(
|
|
249
|
+
(m) => m.name === template && m.category === "template",
|
|
250
|
+
);
|
|
251
|
+
if (!meta) {
|
|
252
|
+
logger.error(`Unknown template: ${template}`);
|
|
253
|
+
logger.info(`Available templates: ${templateNames.join(", ")}`);
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
return meta;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (example) {
|
|
260
|
+
const meta = PROJECT_METADATA.find(
|
|
261
|
+
(m) => m.name === example && m.category === "example",
|
|
262
|
+
);
|
|
263
|
+
if (!meta) {
|
|
264
|
+
logger.error(`Unknown example: ${example}`);
|
|
265
|
+
logger.info(`Available examples: ${exampleNames.join(", ")}`);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
}
|
|
268
|
+
return meta;
|
|
70
269
|
}
|
|
71
270
|
|
|
72
271
|
if (!stdinIsTTY) {
|
|
73
|
-
return "default"
|
|
272
|
+
return PROJECT_METADATA.find((m) => m.name === "default")!;
|
|
74
273
|
}
|
|
75
274
|
|
|
76
275
|
const selected = await select({
|
|
77
|
-
message: "Select a
|
|
78
|
-
options:
|
|
276
|
+
message: "Select a project to scaffold:",
|
|
277
|
+
options: [
|
|
278
|
+
{
|
|
279
|
+
value: "_separator",
|
|
280
|
+
label: "────── Starter Templates ──────",
|
|
281
|
+
disabled: true,
|
|
282
|
+
},
|
|
283
|
+
...PROJECT_METADATA.filter((m) => m.category === "template").map((m) => ({
|
|
284
|
+
value: m.name,
|
|
285
|
+
label: m.label,
|
|
286
|
+
...(m.description ? { hint: m.description } : {}),
|
|
287
|
+
})),
|
|
288
|
+
{
|
|
289
|
+
value: "_separator",
|
|
290
|
+
label: "────── Feature Examples ──────",
|
|
291
|
+
disabled: true,
|
|
292
|
+
},
|
|
293
|
+
...PROJECT_METADATA.filter((m) => m.category === "example").map((m) => ({
|
|
294
|
+
value: m.name,
|
|
295
|
+
label: m.label,
|
|
296
|
+
...(m.description ? { hint: m.description } : {}),
|
|
297
|
+
})),
|
|
298
|
+
],
|
|
79
299
|
});
|
|
80
300
|
|
|
81
301
|
if (isCancel(selected)) {
|
|
82
302
|
return null;
|
|
83
303
|
}
|
|
84
304
|
|
|
85
|
-
|
|
305
|
+
const meta = PROJECT_METADATA.find((m) => m.name === selected);
|
|
306
|
+
if (!meta) {
|
|
307
|
+
logger.error(`Unknown selection: ${String(selected)}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
return meta;
|
|
86
311
|
}
|
|
87
312
|
|
|
88
313
|
class SpawnExitError extends Error {
|
|
@@ -116,37 +341,6 @@ async function runSpawn(
|
|
|
116
341
|
});
|
|
117
342
|
}
|
|
118
343
|
|
|
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
344
|
export function resolveCreateProjectDirectory(params: {
|
|
151
345
|
projectDirectory?: string;
|
|
152
346
|
stdinIsTTY?: boolean;
|
|
@@ -158,8 +352,27 @@ export function resolveCreateProjectDirectory(params: {
|
|
|
158
352
|
return undefined;
|
|
159
353
|
}
|
|
160
354
|
|
|
161
|
-
function
|
|
162
|
-
|
|
355
|
+
function resolvePackageManager(opts: {
|
|
356
|
+
useNpm?: boolean;
|
|
357
|
+
usePnpm?: boolean;
|
|
358
|
+
useYarn?: boolean;
|
|
359
|
+
useBun?: boolean;
|
|
360
|
+
}): PackageManagerName | undefined {
|
|
361
|
+
if (opts.useNpm) return "npm";
|
|
362
|
+
if (opts.usePnpm) return "pnpm";
|
|
363
|
+
if (opts.useYarn) return "yarn";
|
|
364
|
+
if (opts.useBun) return "bun";
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const PLAYGROUND_PRESET_BASE_URL =
|
|
369
|
+
"https://www.assistant-ui.com/playground/init";
|
|
370
|
+
|
|
371
|
+
export function resolvePresetUrl(preset: string): string {
|
|
372
|
+
if (preset.startsWith("http://") || preset.startsWith("https://")) {
|
|
373
|
+
return preset;
|
|
374
|
+
}
|
|
375
|
+
return `${PLAYGROUND_PRESET_BASE_URL}?preset=${encodeURIComponent(preset)}`;
|
|
163
376
|
}
|
|
164
377
|
|
|
165
378
|
export const create = new Command()
|
|
@@ -173,100 +386,210 @@ export const create = new Command()
|
|
|
173
386
|
)
|
|
174
387
|
.option(
|
|
175
388
|
"-e, --example <example>",
|
|
176
|
-
|
|
389
|
+
`create from an example (${exampleNames.join(", ")})`,
|
|
177
390
|
)
|
|
178
391
|
.option(
|
|
179
|
-
"-p, --preset <url>",
|
|
180
|
-
"preset
|
|
392
|
+
"-p, --preset <name-or-url>",
|
|
393
|
+
"preset name or URL (e.g., chatgpt or https://www.assistant-ui.com/playground/init?preset=chatgpt)",
|
|
181
394
|
)
|
|
182
395
|
.option("--use-npm", "explicitly use npm")
|
|
183
396
|
.option("--use-pnpm", "explicitly use pnpm")
|
|
184
397
|
.option("--use-yarn", "explicitly use yarn")
|
|
185
398
|
.option("--use-bun", "explicitly use bun")
|
|
399
|
+
.option("--native", "create an Expo / React Native project")
|
|
186
400
|
.option("--skip-install", "skip installing packages")
|
|
187
401
|
.action(async (projectDirectory, opts) => {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
402
|
+
if (opts.native) {
|
|
403
|
+
opts.example = "with-expo";
|
|
404
|
+
}
|
|
191
405
|
|
|
192
406
|
if (opts.example && opts.preset) {
|
|
193
407
|
logger.error("Cannot use --preset with --example.");
|
|
194
408
|
process.exit(1);
|
|
195
409
|
}
|
|
196
410
|
|
|
197
|
-
if (opts.
|
|
198
|
-
logger.error("
|
|
411
|
+
if (opts.template && opts.example) {
|
|
412
|
+
logger.error("Cannot use both --template and --example.");
|
|
199
413
|
process.exit(1);
|
|
200
414
|
}
|
|
201
415
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
416
|
+
// Start release ref resolution early (runs during user prompts)
|
|
417
|
+
const refPromise = resolveLatestReleaseRef();
|
|
418
|
+
|
|
419
|
+
// 1. Resolve project directory
|
|
420
|
+
let resolvedProjectDirectory = resolveCreateProjectDirectory({
|
|
421
|
+
projectDirectory,
|
|
422
|
+
});
|
|
208
423
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
424
|
+
if (!resolvedProjectDirectory) {
|
|
425
|
+
const result = await p.text({
|
|
426
|
+
message: "Project name:",
|
|
427
|
+
placeholder: "my-aui-app",
|
|
428
|
+
defaultValue: "my-aui-app",
|
|
429
|
+
validate: (value?: string) => {
|
|
430
|
+
const name = (value ?? "").trim();
|
|
431
|
+
if (!name) return "Project name cannot be empty";
|
|
432
|
+
if (name === "." || name === "..")
|
|
433
|
+
return "Project name cannot be . or ..";
|
|
434
|
+
if (name.includes("/") || name.includes("\\"))
|
|
435
|
+
return "Project name cannot contain path separators";
|
|
436
|
+
return undefined;
|
|
437
|
+
},
|
|
215
438
|
});
|
|
216
|
-
|
|
439
|
+
|
|
440
|
+
if (p.isCancel(result)) {
|
|
441
|
+
p.cancel("Project creation cancelled.");
|
|
442
|
+
process.exit(0);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
resolvedProjectDirectory = result;
|
|
217
446
|
}
|
|
218
447
|
|
|
219
|
-
//
|
|
220
|
-
const
|
|
448
|
+
// Check directory
|
|
449
|
+
const absoluteProjectDir = path.resolve(resolvedProjectDirectory);
|
|
450
|
+
try {
|
|
451
|
+
const files = fs.readdirSync(absoluteProjectDir);
|
|
452
|
+
if (files.length > 0) {
|
|
453
|
+
logger.error(
|
|
454
|
+
`Directory ${resolvedProjectDirectory} already exists and is not empty`,
|
|
455
|
+
);
|
|
456
|
+
process.exit(1);
|
|
457
|
+
}
|
|
458
|
+
} catch (err: any) {
|
|
459
|
+
if (err.code === "ENOENT") {
|
|
460
|
+
// Directory doesn't exist — good, proceed
|
|
461
|
+
} else if (err.code === "ENOTDIR") {
|
|
462
|
+
logger.error(
|
|
463
|
+
`${resolvedProjectDirectory} already exists and is not a directory`,
|
|
464
|
+
);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
} else {
|
|
467
|
+
logger.error(
|
|
468
|
+
`Cannot access ${resolvedProjectDirectory}: ${err.message}`,
|
|
469
|
+
);
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 2. Resolve scaffold target
|
|
475
|
+
const project = await resolveProject({
|
|
221
476
|
template: opts.template,
|
|
477
|
+
example: opts.example,
|
|
222
478
|
});
|
|
223
|
-
if (!
|
|
479
|
+
if (!project) {
|
|
224
480
|
p.cancel("Project creation cancelled.");
|
|
225
481
|
process.exit(0);
|
|
226
482
|
}
|
|
227
483
|
|
|
228
|
-
|
|
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}`);
|
|
484
|
+
logger.info(`Creating project from ${project.category}: ${project.label}`);
|
|
237
485
|
logger.break();
|
|
238
486
|
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
});
|
|
487
|
+
const pm = await resolvePackageManagerName(
|
|
488
|
+
absoluteProjectDir,
|
|
489
|
+
resolvePackageManager(opts),
|
|
490
|
+
);
|
|
491
|
+
|
|
492
|
+
// Clean up partial project directory on unexpected exit (e.g. Ctrl+C)
|
|
493
|
+
const cleanupOnExit = () => {
|
|
494
|
+
fs.rmSync(absoluteProjectDir, { recursive: true, force: true });
|
|
495
|
+
};
|
|
496
|
+
process.once("exit", cleanupOnExit);
|
|
250
497
|
|
|
251
498
|
try {
|
|
252
|
-
|
|
499
|
+
// 3. Resolve latest release ref (started before prompts)
|
|
500
|
+
logger.step("Resolving latest release...");
|
|
501
|
+
const ref = await refPromise;
|
|
502
|
+
if (!ref) {
|
|
503
|
+
logger.warn("Could not resolve latest release, downloading from HEAD");
|
|
504
|
+
}
|
|
253
505
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
506
|
+
// 4. Download project
|
|
507
|
+
logger.step("Downloading project...");
|
|
508
|
+
try {
|
|
509
|
+
await downloadProject(project.path, absoluteProjectDir, ref);
|
|
510
|
+
|
|
511
|
+
// If the template didn't exist at the release tag, retry from HEAD
|
|
512
|
+
if (
|
|
513
|
+
ref &&
|
|
514
|
+
!fs.existsSync(path.join(absoluteProjectDir, "package.json"))
|
|
515
|
+
) {
|
|
516
|
+
fs.rmSync(absoluteProjectDir, { recursive: true, force: true });
|
|
517
|
+
logger.warn(
|
|
518
|
+
"Template not found at release tag, downloading from HEAD",
|
|
519
|
+
);
|
|
520
|
+
await downloadProject(project.path, absoluteProjectDir);
|
|
258
521
|
}
|
|
522
|
+
|
|
523
|
+
// 5. Run transform pipeline
|
|
524
|
+
await transformProject(absoluteProjectDir, {
|
|
525
|
+
hasLocalComponents: project.hasLocalComponents,
|
|
526
|
+
skipInstall: opts.skipInstall,
|
|
527
|
+
packageManager: pm,
|
|
528
|
+
});
|
|
529
|
+
} catch (err) {
|
|
530
|
+
// Clean up partially created project directory
|
|
531
|
+
fs.rmSync(absoluteProjectDir, { recursive: true, force: true });
|
|
532
|
+
throw err;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// 6. Apply preset if provided
|
|
536
|
+
if (opts.preset) {
|
|
537
|
+
const presetUrl = resolvePresetUrl(opts.preset);
|
|
259
538
|
logger.info("Applying preset configuration...");
|
|
260
539
|
logger.break();
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
540
|
+
const [dlxCmd, dlxArgs] = dlxCommand(pm);
|
|
541
|
+
try {
|
|
542
|
+
await runSpawn(
|
|
543
|
+
dlxCmd,
|
|
544
|
+
[
|
|
545
|
+
...dlxArgs,
|
|
546
|
+
"shadcn@latest",
|
|
547
|
+
"add",
|
|
548
|
+
"--yes",
|
|
549
|
+
"--overwrite",
|
|
550
|
+
presetUrl,
|
|
551
|
+
],
|
|
552
|
+
absoluteProjectDir,
|
|
553
|
+
);
|
|
554
|
+
} catch {
|
|
555
|
+
logger.warn(
|
|
556
|
+
`Preset application failed. You can retry manually with:\n ${dlxCmd} ${[...dlxArgs, "shadcn@latest", "add", presetUrl].join(" ")}`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
266
559
|
}
|
|
267
560
|
|
|
561
|
+
process.removeListener("exit", cleanupOnExit);
|
|
562
|
+
|
|
268
563
|
logger.break();
|
|
269
564
|
logger.success("Project created successfully!");
|
|
565
|
+
logger.break();
|
|
566
|
+
const runCmd = pm === "npm" ? "npm run" : pm;
|
|
567
|
+
let devScript = "dev";
|
|
568
|
+
let envFile = ".env.local";
|
|
569
|
+
try {
|
|
570
|
+
const scaffoldedPkg = JSON.parse(
|
|
571
|
+
fs.readFileSync(
|
|
572
|
+
path.join(absoluteProjectDir, "package.json"),
|
|
573
|
+
"utf-8",
|
|
574
|
+
),
|
|
575
|
+
);
|
|
576
|
+
devScript = scaffoldedPkg.scripts?.dev
|
|
577
|
+
? "dev"
|
|
578
|
+
: scaffoldedPkg.scripts?.start
|
|
579
|
+
? "start"
|
|
580
|
+
: "dev";
|
|
581
|
+
envFile = scaffoldedPkg.dependencies?.next ? ".env.local" : ".env";
|
|
582
|
+
} catch {
|
|
583
|
+
// Fall back to defaults if package.json cannot be read
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
logger.info("Next steps:");
|
|
587
|
+
logger.info(` cd ${resolvedProjectDirectory}`);
|
|
588
|
+
if (opts.skipInstall) {
|
|
589
|
+
logger.info(` ${pm} install`);
|
|
590
|
+
}
|
|
591
|
+
logger.info(` # Set up your environment variables in ${envFile}`);
|
|
592
|
+
logger.info(` ${runCmd} ${devScript}`);
|
|
270
593
|
} catch (error) {
|
|
271
594
|
if (error instanceof SpawnExitError) {
|
|
272
595
|
logger.error(`Project creation failed with code ${error.code}`);
|