assistant-ui 0.0.91 → 0.0.92

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,10 +1,10 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { downloadTemplate } from "giget";
4
- import { spawn } from "cross-spawn";
5
4
  import { sync as globSync } from "glob";
6
5
  import { detect } from "detect-package-manager";
7
6
  import { logger } from "./utils/logger";
7
+ import { runSpawn, SpawnExitError } from "./run-spawn";
8
8
 
9
9
  export type PackageManagerName = "npm" | "pnpm" | "yarn" | "bun";
10
10
 
@@ -27,6 +27,40 @@ export interface TransformOptions {
27
27
  packageManager: PackageManagerName;
28
28
  }
29
29
 
30
+ export type ProjectSource =
31
+ | {
32
+ kind: "github";
33
+ ref: string | undefined;
34
+ }
35
+ | {
36
+ kind: "local";
37
+ rootDir: string;
38
+ };
39
+
40
+ const LOCAL_PROJECT_ARTIFACT_DIRS: readonly string[] = [
41
+ "node_modules",
42
+ ".next",
43
+ "dist",
44
+ "build",
45
+ ];
46
+
47
+ const LOCAL_PROJECT_ARTIFACT_GLOB_IGNORES = LOCAL_PROJECT_ARTIFACT_DIRS.map(
48
+ (dir) => `**/${dir}/**`,
49
+ );
50
+
51
+ export function resolvePackageManager(opts: {
52
+ useNpm?: boolean;
53
+ usePnpm?: boolean;
54
+ useYarn?: boolean;
55
+ useBun?: boolean;
56
+ }): PackageManagerName | undefined {
57
+ if (opts.useNpm) return "npm";
58
+ if (opts.usePnpm) return "pnpm";
59
+ if (opts.useYarn) return "yarn";
60
+ if (opts.useBun) return "bun";
61
+ return undefined;
62
+ }
63
+
30
64
  export async function resolveLatestReleaseRef(): Promise<string | undefined> {
31
65
  try {
32
66
  const res = await fetch(
@@ -89,6 +123,47 @@ export async function downloadProject(
89
123
  }
90
124
  }
91
125
 
126
+ function shouldCopyLocalProjectPath(src: string, projectDir: string): boolean {
127
+ const relative = path.relative(projectDir, src);
128
+ if (!relative) return true;
129
+
130
+ const segments = relative.split(path.sep);
131
+ return !segments.some((segment) =>
132
+ LOCAL_PROJECT_ARTIFACT_DIRS.includes(segment),
133
+ );
134
+ }
135
+
136
+ export async function scaffoldProject(
137
+ repoPath: string,
138
+ destDir: string,
139
+ source: ProjectSource,
140
+ ): Promise<void> {
141
+ if (source.kind === "github") {
142
+ await downloadProject(repoPath, destDir, source.ref);
143
+ return;
144
+ }
145
+
146
+ const localProjectDir = path.resolve(source.rootDir, repoPath);
147
+ try {
148
+ fs.cpSync(localProjectDir, destDir, {
149
+ recursive: true,
150
+ force: true,
151
+ filter: (src) => shouldCopyLocalProjectPath(src, localProjectDir),
152
+ });
153
+ } catch (error) {
154
+ const code =
155
+ error instanceof Error
156
+ ? (error as NodeJS.ErrnoException).code
157
+ : undefined;
158
+ if (code === "ENOENT") {
159
+ throw new Error(
160
+ `Local project source does not exist: ${localProjectDir}`,
161
+ );
162
+ }
163
+ throw error;
164
+ }
165
+ }
166
+
92
167
  function detectFromUserAgent(): PackageManagerName | undefined {
93
168
  const ua = process.env.npm_config_user_agent;
94
169
  if (!ua) return undefined;
@@ -99,15 +174,15 @@ function detectFromUserAgent(): PackageManagerName | undefined {
99
174
  return undefined;
100
175
  }
101
176
 
102
- export async function resolvePackageManagerName(
103
- projectDir: string,
177
+ export async function resolvePackageManagerForCwd(
178
+ cwd: string,
104
179
  packageManager?: PackageManagerName,
105
180
  ): Promise<PackageManagerName> {
106
181
  if (packageManager) return packageManager;
107
182
  const fromAgent = detectFromUserAgent();
108
183
  if (fromAgent) return fromAgent;
109
184
  try {
110
- return await detect({ cwd: path.dirname(projectDir) });
185
+ return await detect({ cwd });
111
186
  } catch {
112
187
  return "npm";
113
188
  }
@@ -117,9 +192,8 @@ export async function transformProject(
117
192
  projectDir: string,
118
193
  opts: TransformOptions,
119
194
  ): Promise<void> {
120
- // 1. Transform package.json (always)
121
195
  logger.step("Transforming package.json...");
122
- await transformPackageJson(projectDir);
196
+ transformPackageJson(projectDir);
123
197
 
124
198
  let assistantUI: string[] | undefined;
125
199
  let shadcnUI: string[] | undefined;
@@ -127,20 +201,14 @@ export async function transformProject(
127
201
  if (!opts.hasLocalComponents) {
128
202
  logger.step("Transforming project files...");
129
203
 
130
- // 2–4. Transform tsconfig, CSS, and scan components in parallel
131
- const [, , components] = await Promise.all([
132
- transformTsConfig(projectDir),
133
- transformCssFiles(projectDir),
134
- scanRequiredComponents(projectDir),
135
- ]);
204
+ transformTsConfig(projectDir);
205
+ transformCssFiles(projectDir);
206
+
207
+ const components = scanRequiredComponents(projectDir);
136
208
  assistantUI = components.assistantUI;
137
209
  shadcnUI = components.shadcnUI;
138
-
139
- // 5. Remove workspace components (after scan completes — scan reads these files)
140
- await removeWorkspaceComponents(projectDir);
141
210
  }
142
211
 
143
- // 6. Install dependencies
144
212
  const pm = opts.packageManager;
145
213
  if (!opts.skipInstall) {
146
214
  logger.step("Installing dependencies...");
@@ -148,14 +216,12 @@ export async function transformProject(
148
216
  }
149
217
 
150
218
  if (!opts.hasLocalComponents && shadcnUI && assistantUI) {
151
- // 7. Install shadcn UI components
152
219
  const allShadcn = shadcnUI.includes("utils")
153
220
  ? shadcnUI
154
221
  : [...shadcnUI, "utils"];
155
222
  logger.step(`Installing shadcn UI components: ${allShadcn.join(", ")}...`);
156
223
  await installShadcnRegistry(projectDir, allShadcn, "shadcn components", pm);
157
224
 
158
- // 8. Install assistant-ui components
159
225
  if (assistantUI.length > 0) {
160
226
  const auiComponents = assistantUI.map((c) => `@assistant-ui/${c}`);
161
227
  logger.step(
@@ -166,11 +232,11 @@ export async function transformProject(
166
232
  }
167
233
  }
168
234
 
169
- async function transformPackageJson(projectDir: string): Promise<void> {
235
+ function transformPackageJson(projectDir: string): void {
170
236
  const pkgPath = path.join(projectDir, "package.json");
171
237
  const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
172
238
 
173
- // Remove @assistant-ui/react-ui (workspace-only package)
239
+ // Remove @assistant-ui/ui dependency
174
240
  if (pkg.dependencies?.["@assistant-ui/ui"]) {
175
241
  delete pkg.dependencies["@assistant-ui/ui"];
176
242
  }
@@ -199,7 +265,7 @@ async function transformPackageJson(projectDir: string): Promise<void> {
199
265
  fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
200
266
  }
201
267
 
202
- async function transformTsConfig(projectDir: string): Promise<void> {
268
+ function transformTsConfig(projectDir: string): void {
203
269
  const tsconfigPath = path.join(projectDir, "tsconfig.json");
204
270
 
205
271
  if (!fs.existsSync(tsconfigPath)) {
@@ -212,7 +278,9 @@ async function transformTsConfig(projectDir: string): Promise<void> {
212
278
  // Remove workspace paths
213
279
  if (tsconfig.compilerOptions?.paths) {
214
280
  delete tsconfig.compilerOptions.paths["@/components/assistant-ui/*"];
281
+ delete tsconfig.compilerOptions.paths["@/components/icons/*"];
215
282
  delete tsconfig.compilerOptions.paths["@/components/ui/*"];
283
+ delete tsconfig.compilerOptions.paths["@/hooks/*"];
216
284
  delete tsconfig.compilerOptions.paths["@/lib/utils"];
217
285
  delete tsconfig.compilerOptions.paths["@assistant-ui/ui/*"];
218
286
 
@@ -254,10 +322,10 @@ async function transformTsConfig(projectDir: string): Promise<void> {
254
322
  fs.writeFileSync(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}\n`);
255
323
  }
256
324
 
257
- async function transformCssFiles(projectDir: string): Promise<void> {
325
+ function transformCssFiles(projectDir: string): void {
258
326
  const cssFiles = globSync("**/*.css", {
259
327
  cwd: projectDir,
260
- ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**"],
328
+ ignore: LOCAL_PROJECT_ARTIFACT_GLOB_IGNORES,
261
329
  });
262
330
 
263
331
  for (const file of cssFiles) {
@@ -284,12 +352,14 @@ interface RequiredComponents {
284
352
  shadcnUI: string[];
285
353
  }
286
354
 
287
- async function scanRequiredComponents(
288
- projectDir: string,
289
- ): Promise<RequiredComponents> {
355
+ function stripImportExtension(component: string): string {
356
+ return component.replace(/\.[cm]?[tj]sx?$/, "");
357
+ }
358
+
359
+ function scanRequiredComponents(projectDir: string): RequiredComponents {
290
360
  const files = globSync("**/*.{ts,tsx}", {
291
361
  cwd: projectDir,
292
- ignore: ["**/node_modules/**", "**/dist/**", "**/.next/**"],
362
+ ignore: LOCAL_PROJECT_ARTIFACT_GLOB_IGNORES,
293
363
  });
294
364
 
295
365
  const assistantUIComponents = new Set<string>();
@@ -303,12 +373,12 @@ async function scanRequiredComponents(
303
373
  const assistantUIRegex =
304
374
  /from\s+["']@\/components\/assistant-ui\/([^"']+)["']/g;
305
375
  for (const match of content.matchAll(assistantUIRegex)) {
306
- assistantUIComponents.add(match[1]!);
376
+ assistantUIComponents.add(stripImportExtension(match[1]!));
307
377
  }
308
378
 
309
379
  const uiRegex = /from\s+["']@\/components\/ui\/([^"']+)["']/g;
310
380
  for (const match of content.matchAll(uiRegex)) {
311
- shadcnUIComponents.add(match[1]!);
381
+ shadcnUIComponents.add(stripImportExtension(match[1]!));
312
382
  }
313
383
  } catch {
314
384
  // Ignore files that cannot be read
@@ -321,35 +391,20 @@ async function scanRequiredComponents(
321
391
  };
322
392
  }
323
393
 
324
- async function removeWorkspaceComponents(projectDir: string): Promise<void> {
325
- const componentsDir = path.join(projectDir, "components", "assistant-ui");
326
- fs.rmSync(componentsDir, { recursive: true, force: true });
327
- }
328
-
329
394
  async function installDependencies(
330
395
  projectDir: string,
331
396
  pm: PackageManagerName,
332
397
  ): Promise<void> {
333
398
  const args = pm === "yarn" ? [] : ["install"];
334
-
335
- return new Promise((resolve, reject) => {
336
- const child = spawn(pm, args, {
337
- cwd: projectDir,
338
- stdio: "inherit",
339
- });
340
-
341
- child.on("error", (error) => {
342
- reject(new Error(`Failed to install dependencies: ${error.message}`));
343
- });
344
-
345
- child.on("close", (code) => {
346
- if (code !== 0) {
347
- reject(new Error(`${pm} install exited with code ${code}`));
348
- } else {
349
- resolve();
350
- }
351
- });
352
- });
399
+ try {
400
+ await runSpawn(pm, args, projectDir);
401
+ } catch (error) {
402
+ if (error instanceof SpawnExitError) {
403
+ throw new Error(`${pm} install exited with code ${error.code}`);
404
+ }
405
+ const message = error instanceof Error ? error.message : String(error);
406
+ throw new Error(`Failed to install dependencies: ${message}`);
407
+ }
353
408
  }
354
409
 
355
410
  async function installShadcnRegistry(
@@ -359,27 +414,21 @@ async function installShadcnRegistry(
359
414
  pm: PackageManagerName,
360
415
  ): Promise<void> {
361
416
  const [cmd, dlxArgs] = dlxCommand(pm);
362
- return new Promise((resolve, reject) => {
363
- const child = spawn(
364
- cmd,
365
- [...dlxArgs, "shadcn@latest", "add", ...components, "--yes"],
366
- {
367
- cwd: projectDir,
368
- stdio: "inherit",
369
- },
370
- );
417
+ // For npm, dlxArgs may already include `--yes` for npx auto-install.
418
+ // The trailing `--yes` is for shadcn's own confirmation prompt.
419
+ const addArgs = [...dlxArgs, "shadcn@latest", "add", ...components, "--yes"];
371
420
 
372
- child.on("error", (error) => {
373
- reject(new Error(`Failed to install ${label}: ${error.message}`));
374
- });
421
+ try {
422
+ await runSpawn(cmd, addArgs, projectDir);
423
+ } catch (error) {
424
+ if (error instanceof SpawnExitError) {
425
+ logger.warn(
426
+ `shadcn exited with code ${error.code}. Run the following to retry:\n ${cmd} ${addArgs.slice(0, -1).join(" ")}`,
427
+ );
428
+ return;
429
+ }
375
430
 
376
- child.on("close", (code) => {
377
- if (code !== 0) {
378
- logger.warn(
379
- `shadcn exited with code ${code}. Run the following to retry:\n ${cmd} ${[...dlxArgs, "shadcn@latest", "add", ...components].join(" ")}`,
380
- );
381
- }
382
- resolve();
383
- });
384
- });
431
+ const message = error instanceof Error ? error.message : String(error);
432
+ throw new Error(`Failed to install ${label}: ${message}`);
433
+ }
385
434
  }
@@ -0,0 +1,32 @@
1
+ import { spawn } from "cross-spawn";
2
+
3
+ export class SpawnExitError extends Error {
4
+ code: number;
5
+
6
+ constructor(code: number) {
7
+ super(`Process exited with code ${code}`);
8
+ this.code = code;
9
+ }
10
+ }
11
+
12
+ export function runSpawn(
13
+ command: string,
14
+ args: string[],
15
+ cwd?: string,
16
+ ): Promise<void> {
17
+ return new Promise((resolve, reject) => {
18
+ const child = spawn(command, args, {
19
+ stdio: "inherit",
20
+ cwd,
21
+ });
22
+
23
+ child.on("error", (error) => reject(error));
24
+ child.on("close", (code) => {
25
+ if (code !== 0) {
26
+ reject(new SpawnExitError(code || 1));
27
+ } else {
28
+ resolve();
29
+ }
30
+ });
31
+ });
32
+ }