assistant-ui 0.0.113 → 0.0.115

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 (37) hide show
  1. package/dist/codemods/v0-12/assistant-api-to-aui.js +1 -0
  2. package/dist/codemods/v0-12/assistant-api-to-aui.js.map +1 -1
  3. package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -1
  4. package/dist/codemods/v0-12/primitive-if-to-aui-if.js +7 -5
  5. package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -1
  6. package/dist/commands/add.d.ts +0 -1
  7. package/dist/commands/add.d.ts.map +1 -1
  8. package/dist/commands/add.js +0 -2
  9. package/dist/commands/add.js.map +1 -1
  10. package/dist/commands/create.d.ts +9 -1
  11. package/dist/commands/create.d.ts.map +1 -1
  12. package/dist/commands/create.js +20 -6
  13. package/dist/commands/create.js.map +1 -1
  14. package/dist/commands/init.js +1 -1
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/mcp.d.ts.map +1 -1
  17. package/dist/commands/mcp.js +31 -16
  18. package/dist/commands/mcp.js.map +1 -1
  19. package/dist/lib/create-project.d.ts +10 -1
  20. package/dist/lib/create-project.d.ts.map +1 -1
  21. package/dist/lib/create-project.js +114 -5
  22. package/dist/lib/create-project.js.map +1 -1
  23. package/dist/lib/transform.js +1 -1
  24. package/dist/lib/transform.js.map +1 -1
  25. package/package.json +4 -4
  26. package/plugin/skills/assistant-ui/SKILL.md +1 -1
  27. package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +48 -1
  28. package/src/codemods/v0-12/assistant-api-to-aui.ts +4 -1
  29. package/src/codemods/v0-12/primitive-if-to-aui-if.ts +18 -7
  30. package/src/commands/add.ts +0 -3
  31. package/src/commands/create.ts +43 -5
  32. package/src/commands/init.ts +1 -1
  33. package/src/commands/mcp.ts +37 -12
  34. package/src/lib/create-project.test.ts +175 -0
  35. package/src/lib/create-project.ts +170 -6
  36. package/src/lib/transform.test.ts +25 -0
  37. package/src/lib/transform.ts +1 -1
@@ -220,11 +220,8 @@ export async function transformProject(
220
220
  shadcnUI &&
221
221
  assistantUI
222
222
  ) {
223
- const allShadcn = shadcnUI.includes("utils")
224
- ? shadcnUI
225
- : [...shadcnUI, "utils"];
226
223
  const auiComponents = assistantUI.map((c) => `@assistant-ui/${c}`);
227
- const components = [...allShadcn, ...auiComponents];
224
+ const components = ["@assistant-ui/utils", ...shadcnUI, ...auiComponents];
228
225
  logger.step(`Installing components: ${components.join(", ")}...`);
229
226
  const failure = await installShadcnRegistry(
230
227
  projectDir,
@@ -233,6 +230,7 @@ export async function transformProject(
233
230
  pm,
234
231
  );
235
232
  if (failure) return { registryInstallFailure: failure };
233
+ await reconcileAssistantUIImportLayout(projectDir);
236
234
  }
237
235
  return {};
238
236
  }
@@ -385,6 +383,166 @@ function stripImportExtension(component: string): string {
385
383
  return component.replace(/\.[cm]?[tj]sx?$/, "");
386
384
  }
387
385
 
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}", {
479
+ cwd: projectDir,
480
+ ignore: LOCAL_PROJECT_ARTIFACT_GLOB_IGNORES,
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
+ }
498
+
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
+ };
517
+
518
+ const j = fullPath.endsWith(".tsx") ? parsers.tsx : parsers.ts;
519
+ let root;
520
+ try {
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
+ }
545
+
388
546
  function scanRequiredComponents(projectDir: string): RequiredComponents {
389
547
  const assistantUIComponents = new Set<string>();
390
548
  const shadcnUIComponents = new Set<string>();
@@ -396,12 +554,18 @@ function scanRequiredComponents(projectDir: string): RequiredComponents {
396
554
  const assistantUIRegex =
397
555
  /from\s+["']@\/components\/assistant-ui\/([^"']+)["']/g;
398
556
  for (const match of content.matchAll(assistantUIRegex)) {
399
- assistantUIComponents.add(stripImportExtension(match[1]!));
557
+ const item = toAssistantUIItem(match[1]!);
558
+ if (item) assistantUIComponents.add(item);
400
559
  }
401
560
 
402
561
  const uiRegex = /from\s+["']@\/components\/ui\/([^"']+)["']/g;
403
562
  for (const match of content.matchAll(uiRegex)) {
404
- shadcnUIComponents.add(stripImportExtension(match[1]!));
563
+ const name = stripImportExtension(match[1]!);
564
+ if (ASSISTANT_UI_OWNED_UI.has(name)) {
565
+ assistantUIComponents.add(name);
566
+ } else {
567
+ shadcnUIComponents.add(name);
568
+ }
405
569
  }
406
570
  }
407
571
 
@@ -41,6 +41,31 @@ describe("transform", () => {
41
41
  );
42
42
  });
43
43
 
44
+ it("fails a progress-enabled codemod that exits nonzero", async () => {
45
+ mocks.runSpawnCapture.mockResolvedValue({
46
+ code: 7,
47
+ signal: null,
48
+ stdout: "Processing file app.tsx\n",
49
+ stderr: "SyntaxError: Broken input\n",
50
+ });
51
+ const onProgress = vi.fn();
52
+
53
+ const failure = transform(
54
+ "v0-8/ui-package-split",
55
+ "/tmp/project",
56
+ { dry: true },
57
+ {
58
+ logStatus: false,
59
+ onProgress,
60
+ relevantFiles: ["/tmp/project/app.tsx"],
61
+ },
62
+ );
63
+
64
+ await expect(failure).rejects.toBeInstanceOf(SpawnExitError);
65
+ await expect(failure).rejects.toThrow("SyntaxError: Broken input");
66
+ expect(onProgress).not.toHaveBeenCalled();
67
+ });
68
+
44
69
  it("surfaces the codemod's stderr when it exits nonzero", async () => {
45
70
  mocks.runSpawnCapture.mockResolvedValue({
46
71
  code: 1,
@@ -137,7 +137,7 @@ export async function transform(
137
137
  if (result.signal !== null) {
138
138
  throw new SpawnSignalError(result.signal, false);
139
139
  }
140
- if (!options.onProgress && result.code !== 0) {
140
+ if (result.code !== 0) {
141
141
  throw new SpawnExitError(result.code || 1, result.stderr);
142
142
  }
143
143