assistant-ui 0.0.112 → 0.0.114

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.
Files changed (91) hide show
  1. package/dist/codemods/v0-12/assistant-api-to-aui.d.ts.map +1 -1
  2. package/dist/codemods/v0-12/assistant-api-to-aui.js +87 -62
  3. package/dist/codemods/v0-12/assistant-api-to-aui.js.map +1 -1
  4. package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -1
  5. package/dist/codemods/v0-12/primitive-if-to-aui-if.js +31 -45
  6. package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -1
  7. package/dist/commands/add.d.ts +1 -1
  8. package/dist/commands/add.d.ts.map +1 -1
  9. package/dist/commands/add.js +4 -2
  10. package/dist/commands/add.js.map +1 -1
  11. package/dist/commands/create.d.ts.map +1 -1
  12. package/dist/commands/create.js +33 -10
  13. package/dist/commands/create.js.map +1 -1
  14. package/dist/commands/info.d.ts.map +1 -1
  15. package/dist/commands/info.js +2 -1
  16. package/dist/commands/info.js.map +1 -1
  17. package/dist/commands/init.d.ts.map +1 -1
  18. package/dist/commands/init.js +4 -2
  19. package/dist/commands/init.js.map +1 -1
  20. package/dist/commands/mcp.d.ts.map +1 -1
  21. package/dist/commands/mcp.js +52 -40
  22. package/dist/commands/mcp.js.map +1 -1
  23. package/dist/commands/update.js +6 -7
  24. package/dist/commands/update.js.map +1 -1
  25. package/dist/commands/upgrade.d.ts.map +1 -1
  26. package/dist/commands/upgrade.js +5 -2
  27. package/dist/commands/upgrade.js.map +1 -1
  28. package/dist/index.js +2 -6
  29. package/dist/index.js.map +1 -1
  30. package/dist/lib/agent-skill.d.ts +1 -1
  31. package/dist/lib/agent-skill.d.ts.map +1 -1
  32. package/dist/lib/agent-skill.js.map +1 -1
  33. package/dist/lib/create-project.d.ts +11 -3
  34. package/dist/lib/create-project.d.ts.map +1 -1
  35. package/dist/lib/create-project.js +127 -43
  36. package/dist/lib/create-project.js.map +1 -1
  37. package/dist/lib/handle-cli-error.d.ts +5 -0
  38. package/dist/lib/handle-cli-error.d.ts.map +1 -0
  39. package/dist/lib/handle-cli-error.js +17 -0
  40. package/dist/lib/handle-cli-error.js.map +1 -0
  41. package/dist/lib/install-ai-sdk-lib.js +8 -1
  42. package/dist/lib/install-ai-sdk-lib.js.map +1 -1
  43. package/dist/lib/install-edge-lib.js +4 -8
  44. package/dist/lib/install-edge-lib.js.map +1 -1
  45. package/dist/lib/run-spawn.d.ts +16 -2
  46. package/dist/lib/run-spawn.d.ts.map +1 -1
  47. package/dist/lib/run-spawn.js +87 -9
  48. package/dist/lib/run-spawn.js.map +1 -1
  49. package/dist/lib/transform.d.ts +1 -1
  50. package/dist/lib/transform.d.ts.map +1 -1
  51. package/dist/lib/transform.js +12 -25
  52. package/dist/lib/transform.js.map +1 -1
  53. package/dist/lib/upgrade.js +20 -16
  54. package/dist/lib/upgrade.js.map +1 -1
  55. package/dist/lib/utils/file-scanner.d.ts +9 -1
  56. package/dist/lib/utils/file-scanner.d.ts.map +1 -1
  57. package/dist/lib/utils/file-scanner.js +22 -20
  58. package/dist/lib/utils/file-scanner.js.map +1 -1
  59. package/dist/lib/utils/package-installer.js +1 -1
  60. package/dist/lib/utils/package-manager.d.ts +3 -1
  61. package/dist/lib/utils/package-manager.d.ts.map +1 -1
  62. package/dist/lib/utils/package-manager.js +22 -10
  63. package/dist/lib/utils/package-manager.js.map +1 -1
  64. package/package.json +7 -7
  65. package/plugin/skills/assistant-ui/SKILL.md +4 -4
  66. package/src/codemods/v0-12/__tests__/assistant-api-to-aui.test.ts +370 -0
  67. package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +100 -0
  68. package/src/codemods/v0-12/assistant-api-to-aui.ts +227 -134
  69. package/src/codemods/v0-12/primitive-if-to-aui-if.ts +44 -72
  70. package/src/commands/add.ts +5 -5
  71. package/src/commands/create.ts +41 -7
  72. package/src/commands/info.ts +1 -0
  73. package/src/commands/init.ts +4 -6
  74. package/src/commands/mcp.ts +57 -41
  75. package/src/commands/update.ts +6 -8
  76. package/src/commands/upgrade.ts +5 -2
  77. package/src/index.ts +2 -7
  78. package/src/lib/agent-skill.ts +2 -1
  79. package/src/lib/create-project.test.ts +175 -0
  80. package/src/lib/create-project.ts +189 -59
  81. package/src/lib/handle-cli-error.test.ts +67 -0
  82. package/src/lib/handle-cli-error.ts +17 -0
  83. package/src/lib/install-ai-sdk-lib.ts +13 -1
  84. package/src/lib/install-edge-lib.ts +7 -8
  85. package/src/lib/run-spawn.test.ts +148 -0
  86. package/src/lib/run-spawn.ts +121 -11
  87. package/src/lib/transform.test.ts +87 -0
  88. package/src/lib/transform.ts +21 -37
  89. package/src/lib/upgrade.ts +23 -19
  90. package/src/lib/utils/file-scanner.ts +26 -20
  91. package/src/lib/utils/package-manager.ts +29 -12
@@ -0,0 +1,175 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { reconcileAssistantUIImportLayout } from "./create-project";
6
+
7
+ describe("reconcileAssistantUIImportLayout", () => {
8
+ let projectDir: string;
9
+
10
+ beforeEach(() => {
11
+ projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "aui-cli-test-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(projectDir, { recursive: true, force: true });
16
+ });
17
+
18
+ const write = (file: string, content: string) => {
19
+ const fullPath = path.join(projectDir, file);
20
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
21
+ fs.writeFileSync(fullPath, content);
22
+ };
23
+
24
+ const read = (file: string) =>
25
+ fs.readFileSync(path.join(projectDir, file), "utf8");
26
+
27
+ it("rewrites a legacy import when only the elements layout exists", async () => {
28
+ write(
29
+ "app/page.tsx",
30
+ 'import { Thread } from "@/components/assistant-ui/thread";\n' +
31
+ 'import { ThreadList } from "@/components/assistant-ui/thread-list";\n',
32
+ );
33
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
34
+ write("components/assistant-ui/elements/thread-list.aui.tsx", "export {};");
35
+
36
+ await reconcileAssistantUIImportLayout(projectDir);
37
+
38
+ expect(read("app/page.tsx")).toBe(
39
+ 'import { Thread } from "@/components/assistant-ui/elements/thread.aui";\n' +
40
+ 'import { ThreadList } from "@/components/assistant-ui/elements/thread-list.aui";\n',
41
+ );
42
+ });
43
+
44
+ it("rewrites module declarations without changing import-like source text", async () => {
45
+ write(
46
+ "app/page.tsx",
47
+ 'import { Thread } from "@/components/assistant-ui/thread";\n' +
48
+ 'export { ThreadList } from "@/components/assistant-ui/thread-list";\n' +
49
+ "const example = 'from \"@/components/assistant-ui/thread\"';\n" +
50
+ '// from "@/components/assistant-ui/thread-list"\n',
51
+ );
52
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
53
+ write("components/assistant-ui/elements/thread-list.aui.tsx", "export {};");
54
+
55
+ await reconcileAssistantUIImportLayout(projectDir);
56
+
57
+ expect(read("app/page.tsx")).toBe(
58
+ 'import { Thread } from "@/components/assistant-ui/elements/thread.aui";\n' +
59
+ 'export { ThreadList } from "@/components/assistant-ui/elements/thread-list.aui";\n' +
60
+ "const example = 'from \"@/components/assistant-ui/thread\"';\n" +
61
+ '// from "@/components/assistant-ui/thread-list"\n',
62
+ );
63
+ });
64
+
65
+ it("rewrites a legacy import to a bare elements file without the .aui segment", async () => {
66
+ write(
67
+ "app/MyThread.tsx",
68
+ 'import { MarkdownText } from "@/components/assistant-ui/markdown-text";\n' +
69
+ 'import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";\n',
70
+ );
71
+ write("components/assistant-ui/elements/markdown-text.tsx", "export {};");
72
+ write(
73
+ "components/assistant-ui/elements/tooltip-icon-button.tsx",
74
+ "export {};",
75
+ );
76
+
77
+ await reconcileAssistantUIImportLayout(projectDir);
78
+
79
+ expect(read("app/MyThread.tsx")).toBe(
80
+ 'import { MarkdownText } from "@/components/assistant-ui/elements/markdown-text";\n' +
81
+ 'import { TooltipIconButton } from "@/components/assistant-ui/elements/tooltip-icon-button";\n',
82
+ );
83
+ });
84
+
85
+ it("prefers the .aui variant when both variants share a basename", async () => {
86
+ write(
87
+ "app/page.tsx",
88
+ 'import { Reasoning } from "@/components/assistant-ui/reasoning";\n',
89
+ );
90
+ write("components/assistant-ui/elements/reasoning.tsx", "export {};");
91
+ write("components/assistant-ui/elements/reasoning.aui.tsx", "export {};");
92
+
93
+ await reconcileAssistantUIImportLayout(projectDir);
94
+
95
+ expect(read("app/page.tsx")).toBe(
96
+ 'import { Reasoning } from "@/components/assistant-ui/elements/reasoning.aui";\n',
97
+ );
98
+ });
99
+
100
+ it("supports the src/ project layout", async () => {
101
+ write(
102
+ "src/routes/index.tsx",
103
+ 'import { Thread } from "@/components/assistant-ui/thread";\n',
104
+ );
105
+ write("src/components/assistant-ui/elements/thread.aui.tsx", "export {};");
106
+
107
+ await reconcileAssistantUIImportLayout(projectDir);
108
+
109
+ expect(read("src/routes/index.tsx")).toBe(
110
+ 'import { Thread } from "@/components/assistant-ui/elements/thread.aui";\n',
111
+ );
112
+ });
113
+
114
+ it("leaves imports alone when the legacy file exists", async () => {
115
+ const source =
116
+ 'import { Thread } from "@/components/assistant-ui/thread";\n';
117
+ write("app/page.tsx", source);
118
+ write("components/assistant-ui/thread.tsx", "export {};");
119
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
120
+
121
+ await reconcileAssistantUIImportLayout(projectDir);
122
+
123
+ expect(read("app/page.tsx")).toBe(source);
124
+ });
125
+
126
+ it("leaves imports alone when neither layout has the file", async () => {
127
+ const source =
128
+ 'import { Custom } from "@/components/assistant-ui/custom-part";\n';
129
+ write("app/page.tsx", source);
130
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
131
+
132
+ await reconcileAssistantUIImportLayout(projectDir);
133
+
134
+ expect(read("app/page.tsx")).toBe(source);
135
+ });
136
+
137
+ it("leaves elements imports untouched", async () => {
138
+ const source =
139
+ 'import { Thread } from "@/components/assistant-ui/elements/thread.aui";\n';
140
+ write("app/page.tsx", source);
141
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
142
+
143
+ await reconcileAssistantUIImportLayout(projectDir);
144
+
145
+ expect(read("app/page.tsx")).toBe(source);
146
+ });
147
+
148
+ it("parses TypeScript files with generic arrow functions", async () => {
149
+ write(
150
+ "app/helpers.ts",
151
+ 'import { Thread } from "@/components/assistant-ui/thread";\n' +
152
+ "export const identity = <T>(value: T) => value;\n",
153
+ );
154
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
155
+
156
+ await reconcileAssistantUIImportLayout(projectDir);
157
+
158
+ expect(read("app/helpers.ts")).toBe(
159
+ 'import { Thread } from "@/components/assistant-ui/elements/thread.aui";\n' +
160
+ "export const identity = <T>(value: T) => value;\n",
161
+ );
162
+ });
163
+
164
+ it("leaves unparseable source files untouched", async () => {
165
+ const source =
166
+ 'import { Thread } from "@/components/assistant-ui/thread";\nconst broken = ;\n';
167
+ write("app/broken.ts", source);
168
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
169
+
170
+ await expect(
171
+ reconcileAssistantUIImportLayout(projectDir),
172
+ ).resolves.toBeUndefined();
173
+ expect(read("app/broken.ts")).toBe(source);
174
+ });
175
+ });
@@ -1,17 +1,15 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { downloadTemplate } from "giget";
4
- import { sync as globSync } from "glob";
5
- import { detect } from "detect-package-manager";
6
4
  import {
7
5
  parse as parseJsonc,
8
6
  printParseErrorCode,
9
7
  type ParseError,
10
8
  } from "jsonc-parser";
11
9
  import { logger } from "./utils/logger";
12
- import { runSpawn, SpawnExitError } from "./run-spawn";
13
-
14
- export type PackageManagerName = "npm" | "pnpm" | "yarn" | "bun";
10
+ import { runSpawn, SpawnExitError, SpawnSignalError } from "./run-spawn";
11
+ import { type PackageManagerName } from "./utils/package-manager";
12
+ import { readProjectFiles } from "./utils/file-scanner";
15
13
 
16
14
  export function dlxCommand(pm: PackageManagerName): [string, string[]] {
17
15
  switch (pm) {
@@ -186,30 +184,6 @@ export async function scaffoldProject(
186
184
  }
187
185
  }
188
186
 
189
- function detectFromUserAgent(): PackageManagerName | undefined {
190
- const ua = process.env.npm_config_user_agent;
191
- if (!ua) return undefined;
192
- if (ua.startsWith("bun/")) return "bun";
193
- if (ua.startsWith("pnpm/")) return "pnpm";
194
- if (ua.startsWith("yarn/")) return "yarn";
195
- if (ua.startsWith("npm/")) return "npm";
196
- return undefined;
197
- }
198
-
199
- export async function resolvePackageManagerForCwd(
200
- cwd: string,
201
- packageManager?: PackageManagerName,
202
- ): Promise<PackageManagerName> {
203
- if (packageManager) return packageManager;
204
- const fromAgent = detectFromUserAgent();
205
- if (fromAgent) return fromAgent;
206
- try {
207
- return await detect({ cwd });
208
- } catch {
209
- return "npm";
210
- }
211
- }
212
-
213
187
  export interface TransformResult {
214
188
  registryInstallFailure?: { retryCommand: string };
215
189
  }
@@ -246,11 +220,8 @@ export async function transformProject(
246
220
  shadcnUI &&
247
221
  assistantUI
248
222
  ) {
249
- const allShadcn = shadcnUI.includes("utils")
250
- ? shadcnUI
251
- : [...shadcnUI, "utils"];
252
223
  const auiComponents = assistantUI.map((c) => `@assistant-ui/${c}`);
253
- const components = [...allShadcn, ...auiComponents];
224
+ const components = ["@assistant-ui/utils", ...shadcnUI, ...auiComponents];
254
225
  logger.step(`Installing components: ${components.join(", ")}...`);
255
226
  const failure = await installShadcnRegistry(
256
227
  projectDir,
@@ -259,6 +230,7 @@ export async function transformProject(
259
230
  pm,
260
231
  );
261
232
  if (failure) return { registryInstallFailure: failure };
233
+ await reconcileAssistantUIImportLayout(projectDir);
262
234
  }
263
235
  return {};
264
236
  }
@@ -383,16 +355,11 @@ function transformTsConfig(projectDir: string): void {
383
355
  }
384
356
 
385
357
  function transformCssFiles(projectDir: string): void {
386
- const cssFiles = globSync("**/*.css", {
358
+ for (const { fullPath, content } of readProjectFiles("**/*.css", {
387
359
  cwd: projectDir,
388
360
  ignore: LOCAL_PROJECT_ARTIFACT_GLOB_IGNORES,
389
- });
390
-
391
- for (const file of cssFiles) {
392
- const fullPath = path.join(projectDir, file);
361
+ })) {
393
362
  try {
394
- const content = fs.readFileSync(fullPath, "utf-8");
395
-
396
363
  const newContent = content.replace(
397
364
  /@source\s+["'][^"']*packages\/ui\/src[^"']*["'];\s*\n?/g,
398
365
  "",
@@ -402,7 +369,7 @@ function transformCssFiles(projectDir: string): void {
402
369
  fs.writeFileSync(fullPath, newContent);
403
370
  }
404
371
  } catch {
405
- // Ignore files that cannot be read/written
372
+ continue;
406
373
  }
407
374
  }
408
375
  }
@@ -416,32 +383,189 @@ function stripImportExtension(component: string): string {
416
383
  return component.replace(/\.[cm]?[tj]sx?$/, "");
417
384
  }
418
385
 
419
- function scanRequiredComponents(projectDir: string): RequiredComponents {
420
- const files = globSync("**/*.{ts,tsx}", {
386
+ const ASSISTANT_UI_OWNED_UI = new Set([
387
+ "accordion",
388
+ "badge",
389
+ "diff-viewer",
390
+ "direction",
391
+ "dot-matrix",
392
+ "number-roll",
393
+ "select",
394
+ "tabs",
395
+ ]);
396
+
397
+ const BARE_ELEMENT_ITEMS = new Set([
398
+ "file",
399
+ "generative-ui",
400
+ "heat-graph",
401
+ "image",
402
+ "logos",
403
+ "markdown-text",
404
+ "syntax-highlighter",
405
+ "tooltip-icon-button",
406
+ ]);
407
+
408
+ function toAssistantUIItem(specifier: string): string | null {
409
+ let name = stripImportExtension(specifier);
410
+ const inElements = name.startsWith("elements/");
411
+ if (inElements) {
412
+ name = name.slice("elements/".length);
413
+ } else if (name.includes("/")) {
414
+ return null;
415
+ }
416
+ if (name.endsWith(".aui")) {
417
+ return name.slice(0, -".aui".length);
418
+ }
419
+ return inElements && !BARE_ELEMENT_ITEMS.has(name)
420
+ ? `elements-${name}`
421
+ : name;
422
+ }
423
+
424
+ /**
425
+ * Example snapshots are downloaded at a release tag while the shadcn registry
426
+ * is live, so a snapshot may import components at the legacy flat path
427
+ * (`@/components/assistant-ui/<name>`) after the registry has moved the file
428
+ * to `components/assistant-ui/elements/<name>.aui.tsx`. Resolve each legacy
429
+ * specifier against the files the registry actually installed and rewrite it
430
+ * only when the legacy path is absent and the elements layout has it.
431
+ */
432
+ export async function reconcileAssistantUIImportLayout(
433
+ projectDir: string,
434
+ ): Promise<void> {
435
+ const componentRoots = ["components", "src/components"]
436
+ .map((dir) => path.join(projectDir, dir, "assistant-ui"))
437
+ .filter((dir) => fs.existsSync(dir));
438
+ if (componentRoots.length === 0) return;
439
+
440
+ const resolvesAtLegacyPath = (name: string) =>
441
+ componentRoots.some((root) =>
442
+ [".tsx", ".ts", "/index.tsx", "/index.ts"].some((suffix) =>
443
+ fs.existsSync(path.join(root, `${name}${suffix}`)),
444
+ ),
445
+ );
446
+
447
+ // Index the installed tree by import name so the rewrite follows whatever
448
+ // layout the registry delivered — some items install as
449
+ // elements/<name>.aui.tsx, others as elements/<name>.tsx, and a future
450
+ // layout move should not require new knowledge here.
451
+ const installedByName = new Map<string, string>();
452
+ for (const root of componentRoots) {
453
+ for (const { file } of readProjectFiles("**/*.{ts,tsx}", { cwd: root })) {
454
+ const normalized = file.split(path.sep).join("/");
455
+ if (!normalized.includes("/")) continue;
456
+ const specifier = normalized.replace(/\.[cm]?[tj]sx?$/, "");
457
+ const name = path.posix.basename(specifier).replace(/\.aui$/, "");
458
+ // A flat legacy import maps to the registry's `<name>` item, which is
459
+ // the `.aui` file; a colliding bare file with the same basename belongs
460
+ // to the distinct `elements-<name>` item, so the `.aui` variant wins.
461
+ const existing = installedByName.get(name);
462
+ if (
463
+ existing === undefined ||
464
+ (!existing.endsWith(".aui") && specifier.endsWith(".aui"))
465
+ ) {
466
+ installedByName.set(name, specifier);
467
+ }
468
+ }
469
+ }
470
+ if (installedByName.size === 0) return;
471
+
472
+ const { default: jscodeshift } = await import("jscodeshift");
473
+ const parsers = {
474
+ ts: jscodeshift.withParser("ts"),
475
+ tsx: jscodeshift.withParser("tsx"),
476
+ };
477
+
478
+ for (const { fullPath, content } of readProjectFiles("**/*.{ts,tsx}", {
421
479
  cwd: projectDir,
422
480
  ignore: LOCAL_PROJECT_ARTIFACT_GLOB_IGNORES,
423
- });
481
+ })) {
482
+ if (!content.includes("@/components/assistant-ui/")) continue;
483
+
484
+ const replacements: Array<{ start: number; end: number; value: string }> =
485
+ [];
486
+ const collectReplacement = (source: {
487
+ value?: unknown;
488
+ start?: number | null;
489
+ end?: number | null;
490
+ }) => {
491
+ if (
492
+ typeof source.value !== "string" ||
493
+ source.start == null ||
494
+ source.end == null
495
+ ) {
496
+ return;
497
+ }
424
498
 
425
- const assistantUIComponents = new Set<string>();
426
- const shadcnUIComponents = new Set<string>();
499
+ const prefix = "@/components/assistant-ui/";
500
+ if (!source.value.startsWith(prefix)) return;
501
+ const specifier = source.value.slice(prefix.length);
502
+ if (specifier.includes("/")) return;
503
+
504
+ const name = stripImportExtension(specifier);
505
+ const installed = installedByName.get(name);
506
+ if (resolvesAtLegacyPath(name) || installed === undefined) return;
507
+
508
+ const raw = content.slice(source.start, source.end);
509
+ const quote = raw[0];
510
+ if ((quote !== '"' && quote !== "'") || raw.at(-1) !== quote) return;
511
+ replacements.push({
512
+ start: source.start,
513
+ end: source.end,
514
+ value: `${quote}@/components/assistant-ui/${installed}${quote}`,
515
+ });
516
+ };
427
517
 
428
- for (const file of files) {
429
- const fullPath = path.join(projectDir, file);
518
+ const j = fullPath.endsWith(".tsx") ? parsers.tsx : parsers.ts;
519
+ let root;
430
520
  try {
431
- const content = fs.readFileSync(fullPath, "utf-8");
521
+ root = j(content);
522
+ } catch {
523
+ continue;
524
+ }
525
+ root
526
+ .find(j.ImportDeclaration)
527
+ .forEach(({ node }) => collectReplacement(node.source));
528
+ root
529
+ .find(j.ExportNamedDeclaration)
530
+ .forEach(({ node }) => node.source && collectReplacement(node.source));
531
+ root
532
+ .find(j.ExportAllDeclaration)
533
+ .forEach(({ node }) => collectReplacement(node.source));
534
+
535
+ let next = content;
536
+ for (const replacement of replacements.sort((a, b) => b.start - a.start)) {
537
+ next =
538
+ next.slice(0, replacement.start) +
539
+ replacement.value +
540
+ next.slice(replacement.end);
541
+ }
542
+ if (next !== content) fs.writeFileSync(fullPath, next);
543
+ }
544
+ }
432
545
 
433
- const assistantUIRegex =
434
- /from\s+["']@\/components\/assistant-ui\/([^"']+)["']/g;
435
- for (const match of content.matchAll(assistantUIRegex)) {
436
- assistantUIComponents.add(stripImportExtension(match[1]!));
437
- }
546
+ function scanRequiredComponents(projectDir: string): RequiredComponents {
547
+ const assistantUIComponents = new Set<string>();
548
+ const shadcnUIComponents = new Set<string>();
438
549
 
439
- const uiRegex = /from\s+["']@\/components\/ui\/([^"']+)["']/g;
440
- for (const match of content.matchAll(uiRegex)) {
441
- shadcnUIComponents.add(stripImportExtension(match[1]!));
550
+ for (const { content } of readProjectFiles("**/*.{ts,tsx}", {
551
+ cwd: projectDir,
552
+ ignore: LOCAL_PROJECT_ARTIFACT_GLOB_IGNORES,
553
+ })) {
554
+ const assistantUIRegex =
555
+ /from\s+["']@\/components\/assistant-ui\/([^"']+)["']/g;
556
+ for (const match of content.matchAll(assistantUIRegex)) {
557
+ const item = toAssistantUIItem(match[1]!);
558
+ if (item) assistantUIComponents.add(item);
559
+ }
560
+
561
+ const uiRegex = /from\s+["']@\/components\/ui\/([^"']+)["']/g;
562
+ for (const match of content.matchAll(uiRegex)) {
563
+ const name = stripImportExtension(match[1]!);
564
+ if (ASSISTANT_UI_OWNED_UI.has(name)) {
565
+ assistantUIComponents.add(name);
566
+ } else {
567
+ shadcnUIComponents.add(name);
442
568
  }
443
- } catch {
444
- // Ignore files that cannot be read
445
569
  }
446
570
  }
447
571
 
@@ -459,6 +583,9 @@ async function installDependencies(
459
583
  try {
460
584
  await runSpawn(pm, args, projectDir);
461
585
  } catch (error) {
586
+ if (error instanceof SpawnSignalError) {
587
+ throw error;
588
+ }
462
589
  if (error instanceof SpawnExitError) {
463
590
  throw new Error(`${pm} install exited with code ${error.code}`);
464
591
  }
@@ -483,6 +610,9 @@ async function installShadcnRegistry(
483
610
  await runSpawn(cmd, addArgs, projectDir);
484
611
  return undefined;
485
612
  } catch (error) {
613
+ if (error instanceof SpawnSignalError) {
614
+ throw error;
615
+ }
486
616
  if (error instanceof SpawnExitError) {
487
617
  logger.warn(`shadcn exited with code ${error.code}.`);
488
618
  return { retryCommand: `${cmd} ${retryArgs.join(" ")}` };
@@ -0,0 +1,67 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { handleCliError } from "./handle-cli-error";
4
+ import { SpawnExitError, SpawnSignalError } from "./run-spawn";
5
+
6
+ describe("handleCliError", () => {
7
+ afterEach(() => {
8
+ vi.restoreAllMocks();
9
+ process.exitCode = undefined;
10
+ });
11
+
12
+ it("exits by the forwarded signal's status and re-raises it", () => {
13
+ const kill = vi.spyOn(process, "kill").mockReturnValue(true);
14
+
15
+ handleCliError(new SpawnSignalError("SIGINT", true));
16
+
17
+ expect(process.exitCode).toBe(130);
18
+ expect(kill).toHaveBeenCalledWith(process.pid, "SIGINT");
19
+ });
20
+
21
+ it("maps SIGTERM to 143", () => {
22
+ const kill = vi.spyOn(process, "kill").mockReturnValue(true);
23
+
24
+ handleCliError(new SpawnSignalError("SIGTERM", true));
25
+
26
+ expect(process.exitCode).toBe(143);
27
+ expect(kill).toHaveBeenCalledWith(process.pid, "SIGTERM");
28
+ });
29
+
30
+ it("reports a signal the child raised on itself without re-raising it", () => {
31
+ const consoleError = vi
32
+ .spyOn(console, "error")
33
+ .mockImplementation(() => {});
34
+ const kill = vi.spyOn(process, "kill").mockReturnValue(true);
35
+
36
+ handleCliError(new SpawnSignalError("SIGKILL", false));
37
+
38
+ expect(process.exitCode).toBe(137);
39
+ expect(kill).not.toHaveBeenCalled();
40
+ expect(consoleError).toHaveBeenCalledWith("Process terminated by SIGKILL");
41
+ });
42
+
43
+ it("stays silent on a signal the user sent", () => {
44
+ const consoleError = vi
45
+ .spyOn(console, "error")
46
+ .mockImplementation(() => {});
47
+ vi.spyOn(process, "kill").mockReturnValue(true);
48
+
49
+ handleCliError(new SpawnSignalError("SIGINT", true));
50
+
51
+ expect(consoleError).not.toHaveBeenCalled();
52
+ });
53
+
54
+ it("reports any other failure as exit 1", () => {
55
+ const consoleError = vi
56
+ .spyOn(console, "error")
57
+ .mockImplementation(() => {});
58
+ const kill = vi.spyOn(process, "kill").mockReturnValue(true);
59
+ const error = new SpawnExitError(7);
60
+
61
+ handleCliError(error);
62
+
63
+ expect(process.exitCode).toBe(1);
64
+ expect(kill).not.toHaveBeenCalled();
65
+ expect(consoleError).toHaveBeenCalledWith(error);
66
+ });
67
+ });
@@ -0,0 +1,17 @@
1
+ import { constants } from "node:os";
2
+ import { SpawnSignalError } from "./run-spawn";
3
+
4
+ export function handleCliError(error: unknown): void {
5
+ if (error instanceof SpawnSignalError) {
6
+ process.exitCode = 128 + (constants.signals[error.signal] ?? 0);
7
+ if (error.forwarded) {
8
+ process.kill(process.pid, error.signal);
9
+ } else {
10
+ console.error(error.message);
11
+ }
12
+ return;
13
+ }
14
+
15
+ console.error(error);
16
+ process.exitCode = 1;
17
+ }
@@ -1,6 +1,18 @@
1
1
  import { installPackageIfNeeded } from "./utils/package-installer";
2
2
 
3
3
  export default async function installAiSdkLib(): Promise<void> {
4
+ await installPackageIfNeeded({
5
+ packageName: "@assistant-ui/ai-sdk",
6
+ importPatterns: ["@assistant-ui/ai-sdk"],
7
+ promptMessage:
8
+ "AI SDK imports were added but @assistant-ui/ai-sdk is not installed. Do you want to install it? (Y/n) ",
9
+ skipMessage:
10
+ "@assistant-ui/ai-sdk is already installed. Skipping installation.",
11
+ notFoundMessage: "No AI SDK imports found; skipping installation.",
12
+ });
13
+
14
+ // The previous name is a separate package, so an import of it needs that
15
+ // package installed; the neutral one would not make it resolvable.
4
16
  await installPackageIfNeeded({
5
17
  packageName: "@assistant-ui/react-ai-sdk",
6
18
  importPatterns: ["@assistant-ui/react-ai-sdk"],
@@ -8,6 +20,6 @@ export default async function installAiSdkLib(): Promise<void> {
8
20
  "AI SDK imports were added but @assistant-ui/react-ai-sdk is not installed. Do you want to install it? (Y/n) ",
9
21
  skipMessage:
10
22
  "@assistant-ui/react-ai-sdk is already installed. Skipping installation.",
11
- notFoundMessage: "No AI SDK imports found; skipping installation.",
23
+ notFoundMessage: "No legacy AI SDK imports found; skipping installation.",
12
24
  });
13
25
  }
@@ -1,17 +1,16 @@
1
1
  import { installPackageIfNeeded } from "./utils/package-installer";
2
2
 
3
3
  export default async function installEdgeLib(): Promise<void> {
4
+ // Only the retired edge specifier is unambiguous here. `useChatRuntime` and
5
+ // the AI SDK package specifiers are shared by both AI SDK packages, so
6
+ // installAiSdkLib resolves those by the specifier the file actually imports.
4
7
  await installPackageIfNeeded({
5
- packageName: "@assistant-ui/react-ai-sdk",
6
- importPatterns: [
7
- "@assistant-ui/react-edge",
8
- "@assistant-ui/react-ai-sdk",
9
- "useChatRuntime",
10
- ],
8
+ packageName: "@assistant-ui/ai-sdk",
9
+ importPatterns: ["@assistant-ui/react-edge"],
11
10
  promptMessage:
12
- "Edge Runtime imports were detected but @assistant-ui/react-ai-sdk is not installed. Do you want to install it? (Y/n) ",
11
+ "Edge Runtime imports were detected but @assistant-ui/ai-sdk is not installed. Do you want to install it? (Y/n) ",
13
12
  skipMessage:
14
- "@assistant-ui/react-ai-sdk is already installed. Skipping installation.",
13
+ "@assistant-ui/ai-sdk is already installed. Skipping installation.",
15
14
  notFoundMessage: "No Edge Runtime imports found; skipping installation.",
16
15
  });
17
16
  }