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.
- package/dist/codemods/v0-12/assistant-api-to-aui.d.ts.map +1 -1
- package/dist/codemods/v0-12/assistant-api-to-aui.js +87 -62
- package/dist/codemods/v0-12/assistant-api-to-aui.js.map +1 -1
- package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -1
- package/dist/codemods/v0-12/primitive-if-to-aui-if.js +31 -45
- package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -1
- package/dist/commands/add.d.ts +1 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +4 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +33 -10
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/info.js +2 -1
- package/dist/commands/info.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +4 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +52 -40
- package/dist/commands/mcp.js.map +1 -1
- package/dist/commands/update.js +6 -7
- package/dist/commands/update.js.map +1 -1
- package/dist/commands/upgrade.d.ts.map +1 -1
- package/dist/commands/upgrade.js +5 -2
- package/dist/commands/upgrade.js.map +1 -1
- package/dist/index.js +2 -6
- package/dist/index.js.map +1 -1
- package/dist/lib/agent-skill.d.ts +1 -1
- package/dist/lib/agent-skill.d.ts.map +1 -1
- package/dist/lib/agent-skill.js.map +1 -1
- package/dist/lib/create-project.d.ts +11 -3
- package/dist/lib/create-project.d.ts.map +1 -1
- package/dist/lib/create-project.js +127 -43
- package/dist/lib/create-project.js.map +1 -1
- package/dist/lib/handle-cli-error.d.ts +5 -0
- package/dist/lib/handle-cli-error.d.ts.map +1 -0
- package/dist/lib/handle-cli-error.js +17 -0
- package/dist/lib/handle-cli-error.js.map +1 -0
- package/dist/lib/install-ai-sdk-lib.js +8 -1
- package/dist/lib/install-ai-sdk-lib.js.map +1 -1
- package/dist/lib/install-edge-lib.js +4 -8
- package/dist/lib/install-edge-lib.js.map +1 -1
- package/dist/lib/run-spawn.d.ts +16 -2
- package/dist/lib/run-spawn.d.ts.map +1 -1
- package/dist/lib/run-spawn.js +87 -9
- package/dist/lib/run-spawn.js.map +1 -1
- package/dist/lib/transform.d.ts +1 -1
- package/dist/lib/transform.d.ts.map +1 -1
- package/dist/lib/transform.js +12 -25
- package/dist/lib/transform.js.map +1 -1
- package/dist/lib/upgrade.js +20 -16
- package/dist/lib/upgrade.js.map +1 -1
- package/dist/lib/utils/file-scanner.d.ts +9 -1
- package/dist/lib/utils/file-scanner.d.ts.map +1 -1
- package/dist/lib/utils/file-scanner.js +22 -20
- package/dist/lib/utils/file-scanner.js.map +1 -1
- package/dist/lib/utils/package-installer.js +1 -1
- package/dist/lib/utils/package-manager.d.ts +3 -1
- package/dist/lib/utils/package-manager.d.ts.map +1 -1
- package/dist/lib/utils/package-manager.js +22 -10
- package/dist/lib/utils/package-manager.js.map +1 -1
- package/package.json +7 -7
- package/plugin/skills/assistant-ui/SKILL.md +4 -4
- package/src/codemods/v0-12/__tests__/assistant-api-to-aui.test.ts +370 -0
- package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +100 -0
- package/src/codemods/v0-12/assistant-api-to-aui.ts +227 -134
- package/src/codemods/v0-12/primitive-if-to-aui-if.ts +44 -72
- package/src/commands/add.ts +5 -5
- package/src/commands/create.ts +41 -7
- package/src/commands/info.ts +1 -0
- package/src/commands/init.ts +4 -6
- package/src/commands/mcp.ts +57 -41
- package/src/commands/update.ts +6 -8
- package/src/commands/upgrade.ts +5 -2
- package/src/index.ts +2 -7
- package/src/lib/agent-skill.ts +2 -1
- package/src/lib/create-project.test.ts +175 -0
- package/src/lib/create-project.ts +189 -59
- package/src/lib/handle-cli-error.test.ts +67 -0
- package/src/lib/handle-cli-error.ts +17 -0
- package/src/lib/install-ai-sdk-lib.ts +13 -1
- package/src/lib/install-edge-lib.ts +7 -8
- package/src/lib/run-spawn.test.ts +148 -0
- package/src/lib/run-spawn.ts +121 -11
- package/src/lib/transform.test.ts +87 -0
- package/src/lib/transform.ts +21 -37
- package/src/lib/upgrade.ts +23 -19
- package/src/lib/utils/file-scanner.ts +26 -20
- 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
|
-
|
|
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 = [...
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
426
|
-
|
|
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
|
-
|
|
429
|
-
|
|
518
|
+
const j = fullPath.endsWith(".tsx") ? parsers.tsx : parsers.ts;
|
|
519
|
+
let root;
|
|
430
520
|
try {
|
|
431
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
13
|
+
"@assistant-ui/ai-sdk is already installed. Skipping installation.",
|
|
15
14
|
notFoundMessage: "No Edge Runtime imports found; skipping installation.",
|
|
16
15
|
});
|
|
17
16
|
}
|