assistant-ui 0.0.123 → 0.0.125
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 +7 -0
- package/dist/codemods/v0-11/content-part-to-message-part.d.ts.map +1 -1
- package/dist/codemods/v0-11/content-part-to-message-part.js +126 -40
- package/dist/codemods/v0-11/content-part-to-message-part.js.map +1 -1
- package/dist/codemods/v0-12/event-names-to-camelcase.d.ts.map +1 -1
- package/dist/codemods/v0-12/event-names-to-camelcase.js +2 -2
- package/dist/codemods/v0-12/event-names-to-camelcase.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 +57 -41
- package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -1
- package/dist/codemods/v0-8/ui-package-split.d.ts.map +1 -1
- package/dist/codemods/v0-8/ui-package-split.js +11 -35
- package/dist/codemods/v0-8/ui-package-split.js.map +1 -1
- package/dist/codemods/v0-9/edge-package-split.d.ts.map +1 -1
- package/dist/codemods/v0-9/edge-package-split.js +15 -69
- package/dist/codemods/v0-9/edge-package-split.js.map +1 -1
- package/dist/commands/cloud.d.ts.map +1 -1
- package/dist/commands/cloud.js +12 -3
- package/dist/commands/cloud.js.map +1 -1
- package/dist/commands/info.d.ts.map +1 -1
- package/dist/commands/info.js +8 -1
- package/dist/commands/info.js.map +1 -1
- package/dist/lib/cloud-auth.d.ts +1 -0
- package/dist/lib/cloud-auth.d.ts.map +1 -1
- package/dist/lib/cloud-auth.js +4 -3
- package/dist/lib/cloud-auth.js.map +1 -1
- package/dist/lib/cloud-remote.d.ts +22 -0
- package/dist/lib/cloud-remote.d.ts.map +1 -0
- package/dist/lib/cloud-remote.js +74 -0
- package/dist/lib/cloud-remote.js.map +1 -0
- package/dist/lib/utils/package-installer.d.ts.map +1 -1
- package/dist/lib/utils/package-installer.js +3 -2
- package/dist/lib/utils/package-installer.js.map +1 -1
- package/package.json +1 -1
- package/src/codemods/v0-11/__tests__/content-part-to-message-part.test.ts +268 -2
- package/src/codemods/v0-11/content-part-to-message-part.ts +211 -80
- package/src/codemods/v0-12/__tests__/event-names-to-camelcase.test.ts +23 -0
- package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +150 -1
- package/src/codemods/v0-12/event-names-to-camelcase.ts +2 -8
- package/src/codemods/v0-12/primitive-if-to-aui-if.ts +92 -61
- package/src/codemods/v0-8/__tests__/ui-package-split.test.ts +83 -0
- package/src/codemods/v0-8/ui-package-split.ts +21 -59
- package/src/codemods/v0-9/edge-package-split.test.ts +100 -0
- package/src/codemods/v0-9/edge-package-split.ts +30 -139
- package/src/commands/cloud.test.ts +58 -0
- package/src/commands/cloud.ts +19 -2
- package/src/commands/info.ts +26 -1
- package/src/lib/cloud-auth.ts +14 -8
- package/src/lib/cloud-remote.test.ts +156 -0
- package/src/lib/cloud-remote.ts +150 -0
- package/src/lib/utils/package-installer.test.ts +39 -0
- package/src/lib/utils/package-installer.ts +3 -1
- package/templates/cloud-harness/components/assistant-ui/elements/tool-fallback.aui.tsx +17 -8
- package/templates/cloud-harness/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloud-auth.js","names":[],"sources":["../../src/lib/cloud-auth.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { renameSync, statSync, unlinkSync } from \"node:fs\";\nimport {\n chmod,\n link,\n mkdir,\n readFile,\n rename,\n rm,\n writeFile,\n} from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport {\n createDeviceLogin,\n isAccessDenied,\n type DeviceCredentials,\n} from \"aui-auth/device\";\nimport { abortable } from \"./cloud-abort\";\nimport { validateCloudUrl } from \"./cloud-url\";\n\nexport { validateCloudUrl } from \"./cloud-url\";\n\nexport type CloudAuthConfig = {\n issuer: string;\n clientId: string;\n credentialsFile: string;\n};\n\ntype SavedCredentials = DeviceCredentials & {\n issuer: string;\n clientId: string;\n};\n\nexport const cloudAuthConfig = (): CloudAuthConfig => ({\n issuer: validateCloudUrl(\n process.env.ASSISTANT_UI_ACCOUNTS_URL ??\n \"https://accounts.assistant-ui.com\",\n ),\n clientId: process.env.ASSISTANT_UI_OIDC_CLIENT_ID ?? \"assistant-ui-cli\",\n credentialsFile: path.join(\n process.env.ASSISTANT_UI_CONFIG_DIR ??\n path.join(os.homedir(), \".assistant-ui\"),\n \"credentials.json\",\n ),\n});\n\nexport const readCloudCredentials = async (\n config: CloudAuthConfig,\n options: { matchConfig?: boolean } = {},\n): Promise<SavedCredentials | null> => {\n let contents: string;\n try {\n contents = await readFile(config.credentialsFile, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n }\n let saved: SavedCredentials;\n try {\n saved = JSON.parse(contents) as SavedCredentials;\n } catch {\n throw new Error(\"Invalid saved login. Run assistant-ui cloud login again.\");\n }\n if (!saved || typeof saved !== \"object\")\n throw new Error(\"Invalid saved login. Run assistant-ui cloud login again.\");\n if (\n options.matchConfig !== false &&\n (saved.issuer !== config.issuer || saved.clientId !== config.clientId)\n ) {\n return null;\n }\n if (\n !saved.tokens?.accessToken ||\n !saved.user?.id ||\n typeof saved.tokens.accessTokenExpiresAt !== \"number\"\n ) {\n throw new Error(\"Invalid saved login. Run assistant-ui cloud login again.\");\n }\n return saved;\n};\n\nexport const saveCloudCredentials = async (\n config: CloudAuthConfig,\n credentials: DeviceCredentials,\n signal?: AbortSignal,\n): Promise<void> => {\n await mkdir(path.dirname(config.credentialsFile), {\n recursive: true,\n mode: 0o700,\n });\n const temporary = `${config.credentialsFile}.${randomUUID()}.tmp`;\n const backup = `${temporary}.previous`;\n let previous = false;\n let replacement: ReturnType<typeof statSync> | undefined;\n let renamed = false;\n try {\n try {\n await link(config.credentialsFile, backup);\n previous = true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n }\n await writeFile(\n temporary,\n `${JSON.stringify({ ...credentials, issuer: config.issuer, clientId: config.clientId }, null, 2)}\\n`,\n { mode: 0o600, flag: \"wx\" },\n );\n await chmod(temporary, 0o600);\n replacement = statSync(temporary);\n signal?.throwIfAborted();\n await rename(temporary, config.credentialsFile);\n renamed = true;\n signal?.throwIfAborted();\n } catch (error) {\n if (renamed && replacement) {\n let current: ReturnType<typeof statSync> | undefined;\n try {\n current = statSync(config.credentialsFile);\n } catch (failure) {\n if ((failure as NodeJS.ErrnoException).code !== \"ENOENT\") throw failure;\n }\n if (current?.dev === replacement.dev && current.ino === replacement.ino) {\n if (previous) renameSync(backup, config.credentialsFile);\n else unlinkSync(config.credentialsFile);\n }\n }\n throw error;\n } finally {\n await rm(temporary, { force: true });\n await rm(backup, { force: true });\n }\n};\n\nconst openBrowser = (url: string): void => {\n const command =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"rundll32\"\n : \"xdg-open\";\n const args =\n process.platform === \"win32\" ? [\"url.dll,FileProtocolHandler\", url] : [url];\n const child = spawn(command, args, { detached: true, stdio: \"ignore\" });\n child.on(\"error\", () => {});\n child.unref();\n};\n\nexport const loginToCloud = async (\n config: CloudAuthConfig,\n options: {\n noOpen?: boolean;\n setupUrl?: string;\n print?: (text: string) => void;\n } = {},\n): Promise<DeviceCredentials> => {\n if (\n options.setupUrl &&\n ![\"assistant-ui-cli\", \"assistant-ui-cli-dev\"].includes(config.clientId)\n ) {\n throw new Error(\n \"Wizard sign-in requires the assistant-ui CLI OAuth client.\",\n );\n }\n const print = options.print ?? console.log;\n const setup = options.setupUrl\n ? await (\n await import(\"./cloud-setup-login\")\n ).connectCloudLoginSetup(options.setupUrl)\n : undefined;\n const signal = AbortSignal.any([\n AbortSignal.timeout(10 * 60 * 1000),\n ...(setup ? [setup.signal] : []),\n ]);\n const login = createDeviceLogin({\n issuer: config.issuer,\n clientId: config.clientId,\n fetch: (input, init) => {\n const url = new URL(input instanceof Request ? input.url : String(input));\n return fetch(\n input,\n url.pathname === \"/api/auth/device/code\"\n ? {\n ...init,\n signal: AbortSignal.any([\n signal,\n ...(init?.signal ? [init.signal] : []),\n ]),\n }\n : init,\n );\n },\n });\n let credentials: DeviceCredentials | undefined;\n let committed = false;\n try {\n signal.throwIfAborted();\n const authorization = await abortable(login.start(), signal);\n signal.throwIfAborted();\n const approvalUrl =\n (await setup?.publish(authorization, config.issuer)) ??\n authorization.verificationUriComplete;\n print(`Confirm code ${authorization.userCode} at ${approvalUrl}`);\n if (!options.noOpen && !setup)\n openBrowser(authorization.verificationUriComplete);\n credentials = await login.wait(authorization, { signal });\n signal.throwIfAborted();\n await saveCloudCredentials(config, credentials, signal);\n committed = true;\n await setup?.complete(\"signed-in\").catch(() => {\n print(\"Signed in locally. The setup connection could not be updated.\");\n });\n print(`Signed in as ${credentials.user.email || credentials.user.id}.`);\n return credentials;\n } catch (error) {\n if (credentials && !committed) {\n await login.revoke(credentials).catch(() => {\n print(\n \"The cancelled login could not be revoked. End it from your Accounts Sessions page.\",\n );\n });\n }\n await setup\n ?.complete(signal.aborted ? \"cancelled\" : \"failed\")\n .catch(() => {});\n throw error;\n } finally {\n await setup?.dispose();\n }\n};\n\nexport const cloudAccessToken = async (\n config: CloudAuthConfig,\n options: {\n noOpen?: boolean;\n setupUrl?: string;\n print?: (text: string) => void;\n } = {},\n): Promise<string> => {\n const saved = await readCloudCredentials(config);\n if (!saved) return (await loginToCloud(config, options)).tokens.accessToken;\n const login = createDeviceLogin({\n issuer: config.issuer,\n clientId: config.clientId,\n });\n try {\n const credentials = await login.ensureFresh(saved);\n if (credentials !== saved) await saveCloudCredentials(config, credentials);\n return credentials.tokens.accessToken;\n } catch (error) {\n if (isAccessDenied(error)) {\n await rm(config.credentialsFile, { force: true });\n throw new Error(\n \"Your login expired. Run assistant-ui cloud login again.\",\n );\n }\n throw error;\n }\n};\n\nexport const logoutFromCloud = async (\n config: CloudAuthConfig,\n): Promise<void> => {\n const saved = await readCloudCredentials(config, { matchConfig: false });\n if (saved) {\n await createDeviceLogin({\n issuer: validateCloudUrl(saved.issuer),\n clientId: saved.clientId,\n }).revoke(saved);\n }\n await rm(config.credentialsFile, { force: true });\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cloud-auth.js","names":[],"sources":["../../src/lib/cloud-auth.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport { renameSync, statSync, unlinkSync } from \"node:fs\";\nimport {\n chmod,\n link,\n mkdir,\n readFile,\n rename,\n rm,\n writeFile,\n} from \"node:fs/promises\";\nimport os from \"node:os\";\nimport path from \"node:path\";\nimport {\n createDeviceLogin,\n isAccessDenied,\n type DeviceCredentials,\n} from \"aui-auth/device\";\nimport { abortable } from \"./cloud-abort\";\nimport { validateCloudUrl } from \"./cloud-url\";\n\nexport { validateCloudUrl } from \"./cloud-url\";\n\nexport type CloudAuthConfig = {\n issuer: string;\n clientId: string;\n credentialsFile: string;\n};\n\ntype SavedCredentials = DeviceCredentials & {\n issuer: string;\n clientId: string;\n};\n\nexport const cloudAuthConfig = (): CloudAuthConfig => ({\n issuer: validateCloudUrl(\n process.env.ASSISTANT_UI_ACCOUNTS_URL ??\n \"https://accounts.assistant-ui.com\",\n ),\n clientId: process.env.ASSISTANT_UI_OIDC_CLIENT_ID ?? \"assistant-ui-cli\",\n credentialsFile: path.join(\n process.env.ASSISTANT_UI_CONFIG_DIR ??\n path.join(os.homedir(), \".assistant-ui\"),\n \"credentials.json\",\n ),\n});\n\nexport const readCloudCredentials = async (\n config: CloudAuthConfig,\n options: { matchConfig?: boolean } = {},\n): Promise<SavedCredentials | null> => {\n let contents: string;\n try {\n contents = await readFile(config.credentialsFile, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") return null;\n throw error;\n }\n let saved: SavedCredentials;\n try {\n saved = JSON.parse(contents) as SavedCredentials;\n } catch {\n throw new Error(\"Invalid saved login. Run assistant-ui cloud login again.\");\n }\n if (!saved || typeof saved !== \"object\")\n throw new Error(\"Invalid saved login. Run assistant-ui cloud login again.\");\n if (\n options.matchConfig !== false &&\n (saved.issuer !== config.issuer || saved.clientId !== config.clientId)\n ) {\n return null;\n }\n if (\n !saved.tokens?.accessToken ||\n !saved.user?.id ||\n typeof saved.tokens.accessTokenExpiresAt !== \"number\"\n ) {\n throw new Error(\"Invalid saved login. Run assistant-ui cloud login again.\");\n }\n return saved;\n};\n\nexport const saveCloudCredentials = async (\n config: CloudAuthConfig,\n credentials: DeviceCredentials,\n signal?: AbortSignal,\n): Promise<void> => {\n await mkdir(path.dirname(config.credentialsFile), {\n recursive: true,\n mode: 0o700,\n });\n const temporary = `${config.credentialsFile}.${randomUUID()}.tmp`;\n const backup = `${temporary}.previous`;\n let previous = false;\n let replacement: ReturnType<typeof statSync> | undefined;\n let renamed = false;\n try {\n try {\n await link(config.credentialsFile, backup);\n previous = true;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error;\n }\n await writeFile(\n temporary,\n `${JSON.stringify({ ...credentials, issuer: config.issuer, clientId: config.clientId }, null, 2)}\\n`,\n { mode: 0o600, flag: \"wx\" },\n );\n await chmod(temporary, 0o600);\n replacement = statSync(temporary);\n signal?.throwIfAborted();\n await rename(temporary, config.credentialsFile);\n renamed = true;\n signal?.throwIfAborted();\n } catch (error) {\n if (renamed && replacement) {\n let current: ReturnType<typeof statSync> | undefined;\n try {\n current = statSync(config.credentialsFile);\n } catch (failure) {\n if ((failure as NodeJS.ErrnoException).code !== \"ENOENT\") throw failure;\n }\n if (current?.dev === replacement.dev && current.ino === replacement.ino) {\n if (previous) renameSync(backup, config.credentialsFile);\n else unlinkSync(config.credentialsFile);\n }\n }\n throw error;\n } finally {\n await rm(temporary, { force: true });\n await rm(backup, { force: true });\n }\n};\n\nconst openBrowser = (url: string): void => {\n const command =\n process.platform === \"darwin\"\n ? \"open\"\n : process.platform === \"win32\"\n ? \"rundll32\"\n : \"xdg-open\";\n const args =\n process.platform === \"win32\" ? [\"url.dll,FileProtocolHandler\", url] : [url];\n const child = spawn(command, args, { detached: true, stdio: \"ignore\" });\n child.on(\"error\", () => {});\n child.unref();\n};\n\nexport const loginToCloud = async (\n config: CloudAuthConfig,\n options: {\n noOpen?: boolean;\n setupUrl?: string;\n print?: (text: string) => void;\n } = {},\n): Promise<DeviceCredentials> => {\n if (\n options.setupUrl &&\n ![\"assistant-ui-cli\", \"assistant-ui-cli-dev\"].includes(config.clientId)\n ) {\n throw new Error(\n \"Wizard sign-in requires the assistant-ui CLI OAuth client.\",\n );\n }\n const print = options.print ?? console.log;\n const setup = options.setupUrl\n ? await (\n await import(\"./cloud-setup-login\")\n ).connectCloudLoginSetup(options.setupUrl)\n : undefined;\n const signal = AbortSignal.any([\n AbortSignal.timeout(10 * 60 * 1000),\n ...(setup ? [setup.signal] : []),\n ]);\n const login = createDeviceLogin({\n issuer: config.issuer,\n clientId: config.clientId,\n fetch: (input, init) => {\n const url = new URL(input instanceof Request ? input.url : String(input));\n return fetch(\n input,\n url.pathname === \"/api/auth/device/code\"\n ? {\n ...init,\n signal: AbortSignal.any([\n signal,\n ...(init?.signal ? [init.signal] : []),\n ]),\n }\n : init,\n );\n },\n });\n let credentials: DeviceCredentials | undefined;\n let committed = false;\n try {\n signal.throwIfAborted();\n const authorization = await abortable(login.start(), signal);\n signal.throwIfAborted();\n const approvalUrl =\n (await setup?.publish(authorization, config.issuer)) ??\n authorization.verificationUriComplete;\n print(`Confirm code ${authorization.userCode} at ${approvalUrl}`);\n if (!options.noOpen && !setup)\n openBrowser(authorization.verificationUriComplete);\n credentials = await login.wait(authorization, { signal });\n signal.throwIfAborted();\n await saveCloudCredentials(config, credentials, signal);\n committed = true;\n await setup?.complete(\"signed-in\").catch(() => {\n print(\"Signed in locally. The setup connection could not be updated.\");\n });\n print(`Signed in as ${credentials.user.email || credentials.user.id}.`);\n return credentials;\n } catch (error) {\n if (credentials && !committed) {\n await login.revoke(credentials).catch(() => {\n print(\n \"The cancelled login could not be revoked. End it from your Accounts Sessions page.\",\n );\n });\n }\n await setup\n ?.complete(signal.aborted ? \"cancelled\" : \"failed\")\n .catch(() => {});\n throw error;\n } finally {\n await setup?.dispose();\n }\n};\n\nexport const savedCloudAccessToken = async (\n config: CloudAuthConfig,\n): Promise<string | null> => {\n const saved = await readCloudCredentials(config);\n if (!saved) return null;\n const login = createDeviceLogin({\n issuer: config.issuer,\n clientId: config.clientId,\n });\n try {\n const credentials = await login.ensureFresh(saved);\n if (credentials !== saved) await saveCloudCredentials(config, credentials);\n return credentials.tokens.accessToken;\n } catch (error) {\n if (isAccessDenied(error)) {\n await rm(config.credentialsFile, { force: true });\n throw new Error(\n \"Your login expired. Run assistant-ui cloud login again.\",\n );\n }\n throw error;\n }\n};\n\nexport const cloudAccessToken = async (\n config: CloudAuthConfig,\n options: {\n noOpen?: boolean;\n setupUrl?: string;\n print?: (text: string) => void;\n } = {},\n): Promise<string> =>\n (await savedCloudAccessToken(config)) ??\n (await loginToCloud(config, options)).tokens.accessToken;\n\nexport const logoutFromCloud = async (\n config: CloudAuthConfig,\n): Promise<void> => {\n const saved = await readCloudCredentials(config, { matchConfig: false });\n if (saved) {\n await createDeviceLogin({\n issuer: validateCloudUrl(saved.issuer),\n clientId: saved.clientId,\n }).revoke(saved);\n }\n await rm(config.credentialsFile, { force: true });\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare const CLOUD_CLI_RUN_PATH = "/api/cli/run";
|
|
2
|
+
export type CloudCliAction = {
|
|
3
|
+
type: string;
|
|
4
|
+
[key: string]: unknown;
|
|
5
|
+
};
|
|
6
|
+
export type CloudCliResponse = {
|
|
7
|
+
stdout?: string;
|
|
8
|
+
stderr?: string;
|
|
9
|
+
exitCode: number;
|
|
10
|
+
actions?: CloudCliAction[];
|
|
11
|
+
};
|
|
12
|
+
export declare const joinCloudArgs: (args: readonly string[]) => string;
|
|
13
|
+
export declare const parseCloudCliResponse: (value: unknown) => CloudCliResponse | null;
|
|
14
|
+
export declare const sendCloudCommand: (cloudUrl: string, args: readonly string[], accessToken: string | null) => Promise<CloudCliResponse>;
|
|
15
|
+
export declare const runCloudCommand: (args: readonly string[], deps: {
|
|
16
|
+
cloudUrl: string;
|
|
17
|
+
accessToken: () => Promise<string | null>;
|
|
18
|
+
stdout: (text: string) => void;
|
|
19
|
+
stderr: (text: string) => void;
|
|
20
|
+
send?: typeof sendCloudCommand;
|
|
21
|
+
}) => Promise<number>;
|
|
22
|
+
//# sourceMappingURL=cloud-remote.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-remote.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-remote.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,kBAAkB,iBAAiB,CAAC;AAEjD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CAAE,CAAC;AAEtE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;CAC5B,CAAC;AAIF,eAAO,MAAM,aAAa,GAAI,MAAM,SAAS,MAAM,EAAE,KAAG,MAG1C,CAAC;AAEf,eAAO,MAAM,qBAAqB,GAChC,OAAO,OAAO,KACb,gBAAgB,GAAG,IA+BrB,CAAC;AAOF,eAAO,MAAM,gBAAgB,GAC3B,UAAU,MAAM,EAChB,MAAM,SAAS,MAAM,EAAE,EACvB,aAAa,MAAM,GAAG,IAAI,KACzB,OAAO,CAAC,gBAAgB,CAkC1B,CAAC;AASF,eAAO,MAAM,eAAe,GAC1B,MAAM,SAAS,MAAM,EAAE,EACvB,MAAM;IACJ,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,IAAI,CAAC,EAAE,OAAO,gBAAgB,CAAC;CAChC,KACA,OAAO,CAAC,MAAM,CAiChB,CAAC"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { validateCloudUrl } from "./cloud-url.js";
|
|
2
|
+
//#region src/lib/cloud-remote.ts
|
|
3
|
+
const CLOUD_CLI_RUN_PATH = "/api/cli/run";
|
|
4
|
+
const SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
5
|
+
const joinCloudArgs = (args) => args.map((arg) => SAFE.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`).join(" ");
|
|
6
|
+
const parseCloudCliResponse = (value) => {
|
|
7
|
+
if (!value || typeof value !== "object") return null;
|
|
8
|
+
const body = value;
|
|
9
|
+
const exitCode = body["exitCode"];
|
|
10
|
+
if (typeof exitCode !== "number" || !Number.isInteger(exitCode) || exitCode < 0 || exitCode > 255) return null;
|
|
11
|
+
for (const key of ["stdout", "stderr"]) if (body[key] !== void 0 && typeof body[key] !== "string") return null;
|
|
12
|
+
const actions = body["actions"];
|
|
13
|
+
if (actions !== void 0 && !(Array.isArray(actions) && actions.every((action) => !!action && typeof action === "object" && typeof action.type === "string"))) return null;
|
|
14
|
+
return body;
|
|
15
|
+
};
|
|
16
|
+
const cliVersion = "0.0.125";
|
|
17
|
+
const sendCloudCommand = async (cloudUrl, args, accessToken) => {
|
|
18
|
+
const url = `${validateCloudUrl(cloudUrl)}${CLOUD_CLI_RUN_PATH}`;
|
|
19
|
+
let response;
|
|
20
|
+
try {
|
|
21
|
+
response = await fetch(url, {
|
|
22
|
+
method: "POST",
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
...accessToken ? { Authorization: `Bearer ${accessToken}` } : {}
|
|
26
|
+
},
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
command: joinCloudArgs(args),
|
|
29
|
+
client: {
|
|
30
|
+
name: "assistant-ui",
|
|
31
|
+
version: cliVersion,
|
|
32
|
+
platform: `${process.platform}-${process.arch}`
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
redirect: "error",
|
|
36
|
+
signal: AbortSignal.timeout(6e4)
|
|
37
|
+
});
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const cause = error.cause;
|
|
40
|
+
throw new Error(`Could not reach ${url}: ${cause?.message || cause?.code || error.message}`);
|
|
41
|
+
}
|
|
42
|
+
const body = parseCloudCliResponse(await response.json().catch(() => null));
|
|
43
|
+
if (!body) throw new Error(`Unexpected response from ${url} (${response.status} ${response.statusText}).`);
|
|
44
|
+
return body;
|
|
45
|
+
};
|
|
46
|
+
const ACTION_HANDLERS = {};
|
|
47
|
+
const runCloudCommand = async (args, deps) => {
|
|
48
|
+
let response;
|
|
49
|
+
try {
|
|
50
|
+
response = await (deps.send ?? sendCloudCommand)(deps.cloudUrl, args, await deps.accessToken().catch((error) => {
|
|
51
|
+
deps.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
52
|
+
return null;
|
|
53
|
+
}));
|
|
54
|
+
} catch (error) {
|
|
55
|
+
deps.stderr(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
const unknown = response.actions?.find((action) => !Object.hasOwn(ACTION_HANDLERS, action.type));
|
|
59
|
+
if (unknown) {
|
|
60
|
+
deps.stderr(`This command needs a newer assistant-ui (unsupported action "${unknown.type}"). Update the CLI and try again.\n`);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
if (response.stdout) deps.stdout(response.stdout);
|
|
64
|
+
if (response.stderr) deps.stderr(response.stderr);
|
|
65
|
+
for (const action of response.actions ?? []) {
|
|
66
|
+
const code = await ACTION_HANDLERS[action.type](action);
|
|
67
|
+
if (code !== 0) return code;
|
|
68
|
+
}
|
|
69
|
+
return response.exitCode;
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { CLOUD_CLI_RUN_PATH, joinCloudArgs, parseCloudCliResponse, runCloudCommand, sendCloudCommand };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=cloud-remote.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloud-remote.js","names":[],"sources":["../../src/lib/cloud-remote.ts"],"sourcesContent":["import { validateCloudUrl } from \"./cloud-url\";\n\nexport const CLOUD_CLI_RUN_PATH = \"/api/cli/run\";\n\nexport type CloudCliAction = { type: string; [key: string]: unknown };\n\nexport type CloudCliResponse = {\n stdout?: string;\n stderr?: string;\n exitCode: number;\n actions?: CloudCliAction[];\n};\n\nconst SAFE = /^[A-Za-z0-9_@%+=:,./-]+$/;\n\nexport const joinCloudArgs = (args: readonly string[]): string =>\n args\n .map((arg) => (SAFE.test(arg) ? arg : `'${arg.replaceAll(\"'\", `'\\\\''`)}'`))\n .join(\" \");\n\nexport const parseCloudCliResponse = (\n value: unknown,\n): CloudCliResponse | null => {\n if (!value || typeof value !== \"object\") return null;\n const body = value as Record<string, unknown>;\n const exitCode = body[\"exitCode\"];\n if (\n typeof exitCode !== \"number\" ||\n !Number.isInteger(exitCode) ||\n exitCode < 0 ||\n exitCode > 255\n ) {\n return null;\n }\n for (const key of [\"stdout\", \"stderr\"]) {\n if (body[key] !== undefined && typeof body[key] !== \"string\") return null;\n }\n const actions = body[\"actions\"];\n if (\n actions !== undefined &&\n !(\n Array.isArray(actions) &&\n actions.every(\n (action) =>\n !!action &&\n typeof action === \"object\" &&\n typeof (action as CloudCliAction).type === \"string\",\n )\n )\n ) {\n return null;\n }\n return body as CloudCliResponse;\n};\n\nconst cliVersion: string =\n typeof __AUI_PACKAGE_VERSION__ === \"string\"\n ? __AUI_PACKAGE_VERSION__\n : \"0.0.0\";\n\nexport const sendCloudCommand = async (\n cloudUrl: string,\n args: readonly string[],\n accessToken: string | null,\n): Promise<CloudCliResponse> => {\n const url = `${validateCloudUrl(cloudUrl)}${CLOUD_CLI_RUN_PATH}`;\n let response: Response;\n try {\n response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}),\n },\n body: JSON.stringify({\n command: joinCloudArgs(args),\n client: {\n name: \"assistant-ui\",\n version: cliVersion,\n platform: `${process.platform}-${process.arch}`,\n },\n }),\n redirect: \"error\",\n signal: AbortSignal.timeout(60_000),\n });\n } catch (error) {\n const cause = (error as Error).cause as NodeJS.ErrnoException | undefined;\n throw new Error(\n `Could not reach ${url}: ${cause?.message || cause?.code || (error as Error).message}`,\n );\n }\n const body = parseCloudCliResponse(await response.json().catch(() => null));\n if (!body) {\n throw new Error(\n `Unexpected response from ${url} (${response.status} ${response.statusText}).`,\n );\n }\n return body;\n};\n\n// A response naming an action this client lacks fails before printing, so the\n// server can roll a new action out behind the client version it is sent.\nconst ACTION_HANDLERS: Record<\n string,\n (action: CloudCliAction) => Promise<number>\n> = {};\n\nexport const runCloudCommand = async (\n args: readonly string[],\n deps: {\n cloudUrl: string;\n accessToken: () => Promise<string | null>;\n stdout: (text: string) => void;\n stderr: (text: string) => void;\n send?: typeof sendCloudCommand;\n },\n): Promise<number> => {\n let response: CloudCliResponse;\n try {\n response = await (deps.send ?? sendCloudCommand)(\n deps.cloudUrl,\n args,\n await deps.accessToken().catch((error: unknown) => {\n deps.stderr(\n `${error instanceof Error ? error.message : String(error)}\\n`,\n );\n return null;\n }),\n );\n } catch (error) {\n deps.stderr(`${error instanceof Error ? error.message : String(error)}\\n`);\n return 1;\n }\n const unknown = response.actions?.find(\n (action) => !Object.hasOwn(ACTION_HANDLERS, action.type),\n );\n if (unknown) {\n deps.stderr(\n `This command needs a newer assistant-ui (unsupported action \"${unknown.type}\"). Update the CLI and try again.\\n`,\n );\n return 1;\n }\n if (response.stdout) deps.stdout(response.stdout);\n if (response.stderr) deps.stderr(response.stderr);\n for (const action of response.actions ?? []) {\n const code = await ACTION_HANDLERS[action.type]!(action);\n if (code !== 0) return code;\n }\n return response.exitCode;\n};\n"],"mappings":";;AAEA,MAAa,qBAAqB;AAWlC,MAAM,OAAO;AAEb,MAAa,iBAAiB,SAC5B,KACG,KAAK,QAAS,KAAK,KAAK,GAAG,IAAI,MAAM,IAAI,IAAI,WAAW,KAAK,OAAO,EAAE,EAAG,CAAC,CAC1E,KAAK,GAAG;AAEb,MAAa,yBACX,UAC4B;CAC5B,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO;CAChD,MAAM,OAAO;CACb,MAAM,WAAW,KAAK;CACtB,IACE,OAAO,aAAa,YACpB,CAAC,OAAO,UAAU,QAAQ,KAC1B,WAAW,KACX,WAAW,KAEX,OAAO;CAET,KAAK,MAAM,OAAO,CAAC,UAAU,QAAQ,GACnC,IAAI,KAAK,SAAS,KAAA,KAAa,OAAO,KAAK,SAAS,UAAU,OAAO;CAEvE,MAAM,UAAU,KAAK;CACrB,IACE,YAAY,KAAA,KACZ,EACE,MAAM,QAAQ,OAAO,KACrB,QAAQ,OACL,WACC,CAAC,CAAC,UACF,OAAO,WAAW,YAClB,OAAQ,OAA0B,SAAS,QAC/C,IAGF,OAAO;CAET,OAAO;AACT;AAEA,MAAM,aAAA;AAKN,MAAa,mBAAmB,OAC9B,UACA,MACA,gBAC8B;CAC9B,MAAM,MAAM,GAAG,iBAAiB,QAAQ,IAAI;CAC5C,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,MAAM,KAAK;GAC1B,QAAQ;GACR,SAAS;IACP,gBAAgB;IAChB,GAAI,cAAc,EAAE,eAAe,UAAU,cAAc,IAAI,CAAC;GAClE;GACA,MAAM,KAAK,UAAU;IACnB,SAAS,cAAc,IAAI;IAC3B,QAAQ;KACN,MAAM;KACN,SAAS;KACT,UAAU,GAAG,QAAQ,SAAS,GAAG,QAAQ;IAC3C;GACF,CAAC;GACD,UAAU;GACV,QAAQ,YAAY,QAAQ,GAAM;EACpC,CAAC;CACH,SAAS,OAAO;EACd,MAAM,QAAS,MAAgB;EAC/B,MAAM,IAAI,MACR,mBAAmB,IAAI,IAAI,OAAO,WAAW,OAAO,QAAS,MAAgB,SAC/E;CACF;CACA,MAAM,OAAO,sBAAsB,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,IAAI,CAAC;CAC1E,IAAI,CAAC,MACH,MAAM,IAAI,MACR,4BAA4B,IAAI,IAAI,SAAS,OAAO,GAAG,SAAS,WAAW,GAC7E;CAEF,OAAO;AACT;AAIA,MAAM,kBAGF,CAAC;AAEL,MAAa,kBAAkB,OAC7B,MACA,SAOoB;CACpB,IAAI;CACJ,IAAI;EACF,WAAW,OAAO,KAAK,QAAQ,iBAAA,CAC7B,KAAK,UACL,MACA,MAAM,KAAK,YAAY,CAAC,CAAC,OAAO,UAAmB;GACjD,KAAK,OACH,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAC5D;GACA,OAAO;EACT,CAAC,CACH;CACF,SAAS,OAAO;EACd,KAAK,OAAO,GAAG,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE,GAAG;EACzE,OAAO;CACT;CACA,MAAM,UAAU,SAAS,SAAS,MAC/B,WAAW,CAAC,OAAO,OAAO,iBAAiB,OAAO,IAAI,CACzD;CACA,IAAI,SAAS;EACX,KAAK,OACH,gEAAgE,QAAQ,KAAK,oCAC/E;EACA,OAAO;CACT;CACA,IAAI,SAAS,QAAQ,KAAK,OAAO,SAAS,MAAM;CAChD,IAAI,SAAS,QAAQ,KAAK,OAAO,SAAS,MAAM;CAChD,KAAK,MAAM,UAAU,SAAS,WAAW,CAAC,GAAG;EAC3C,MAAM,OAAO,MAAM,gBAAgB,OAAO,KAAK,CAAE,MAAM;EACvD,IAAI,SAAS,GAAG,OAAO;CACzB;CACA,OAAO,SAAS;AAClB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package-installer.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/package-installer.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"package-installer.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/package-installer.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;IACpB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
|
@@ -12,8 +12,9 @@ async function installPackageIfNeeded(config) {
|
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
const answer = await askQuestion(config.promptMessage);
|
|
15
|
-
if (answer === "" || answer.toLowerCase().startsWith("y"))
|
|
16
|
-
|
|
15
|
+
if (answer === "" || answer.toLowerCase().startsWith("y")) {
|
|
16
|
+
if (!await installPackage(config.packageName)) throw new Error(`Failed to install ${config.packageName}`);
|
|
17
|
+
} else logger.info("Skipping installation.");
|
|
17
18
|
}
|
|
18
19
|
//#endregion
|
|
19
20
|
export { installPackageIfNeeded };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package-installer.js","names":[],"sources":["../../../src/lib/utils/package-installer.ts"],"sourcesContent":["import { scanForImport } from \"./file-scanner\";\nimport {\n isPackageInstalled,\n askQuestion,\n installPackage,\n} from \"./package-manager\";\nimport { logger } from \"./logger\";\n\nexport interface PackageInstallConfig {\n packageName: string;\n importPatterns: string[];\n promptMessage: string;\n skipMessage: string;\n notFoundMessage: string;\n}\n\nexport async function installPackageIfNeeded(\n config: PackageInstallConfig,\n): Promise<void> {\n const found = scanForImport(config.importPatterns);\n\n if (!found) {\n logger.info(config.notFoundMessage);\n return;\n }\n\n if (isPackageInstalled(config.packageName)) {\n logger.info(config.skipMessage);\n return;\n }\n\n const answer = await askQuestion(config.promptMessage);\n if (answer === \"\" || answer.toLowerCase().startsWith(\"y\")) {\n await installPackage(config.packageName);\n } else {\n logger.info(\"Skipping installation.\");\n }\n}\n"],"mappings":";;;;AAgBA,eAAsB,uBACpB,QACe;CAGf,IAAI,CAFU,cAAc,OAAO,cAE1B,GAAG;EACV,OAAO,KAAK,OAAO,eAAe;EAClC;CACF;CAEA,IAAI,mBAAmB,OAAO,WAAW,GAAG;EAC1C,OAAO,KAAK,OAAO,WAAW;EAC9B;CACF;CAEA,MAAM,SAAS,MAAM,YAAY,OAAO,aAAa;CACrD,IAAI,WAAW,MAAM,OAAO,YAAY,CAAC,CAAC,WAAW,GAAG,
|
|
1
|
+
{"version":3,"file":"package-installer.js","names":[],"sources":["../../../src/lib/utils/package-installer.ts"],"sourcesContent":["import { scanForImport } from \"./file-scanner\";\nimport {\n isPackageInstalled,\n askQuestion,\n installPackage,\n} from \"./package-manager\";\nimport { logger } from \"./logger\";\n\nexport interface PackageInstallConfig {\n packageName: string;\n importPatterns: string[];\n promptMessage: string;\n skipMessage: string;\n notFoundMessage: string;\n}\n\nexport async function installPackageIfNeeded(\n config: PackageInstallConfig,\n): Promise<void> {\n const found = scanForImport(config.importPatterns);\n\n if (!found) {\n logger.info(config.notFoundMessage);\n return;\n }\n\n if (isPackageInstalled(config.packageName)) {\n logger.info(config.skipMessage);\n return;\n }\n\n const answer = await askQuestion(config.promptMessage);\n if (answer === \"\" || answer.toLowerCase().startsWith(\"y\")) {\n if (!(await installPackage(config.packageName))) {\n throw new Error(`Failed to install ${config.packageName}`);\n }\n } else {\n logger.info(\"Skipping installation.\");\n }\n}\n"],"mappings":";;;;AAgBA,eAAsB,uBACpB,QACe;CAGf,IAAI,CAFU,cAAc,OAAO,cAE1B,GAAG;EACV,OAAO,KAAK,OAAO,eAAe;EAClC;CACF;CAEA,IAAI,mBAAmB,OAAO,WAAW,GAAG;EAC1C,OAAO,KAAK,OAAO,WAAW;EAC9B;CACF;CAEA,MAAM,SAAS,MAAM,YAAY,OAAO,aAAa;CACrD,IAAI,WAAW,MAAM,OAAO,YAAY,CAAC,CAAC,WAAW,GAAG,GAClD;MAAA,CAAE,MAAM,eAAe,OAAO,WAAW,GAC3C,MAAM,IAAI,MAAM,qBAAqB,OAAO,aAAa;CAAA,OAG3D,OAAO,KAAK,wBAAwB;AAExC"}
|
package/package.json
CHANGED
|
@@ -158,14 +158,14 @@ function MyComponent() {
|
|
|
158
158
|
|
|
159
159
|
it("should handle complex type unions and generics", () => {
|
|
160
160
|
const input = `
|
|
161
|
-
import { ThreadAssistantContentPart, ToolCallContentPart } from "@assistant-ui/react";
|
|
161
|
+
import { ThreadAssistantContentPart, ToolCallContentPart, TextContentPart } from "@assistant-ui/react";
|
|
162
162
|
|
|
163
163
|
type MyUnion = ThreadAssistantContentPart | TextContentPart;
|
|
164
164
|
type MyGeneric = Array<ToolCallContentPart<{ query: string }, string>>;
|
|
165
165
|
`;
|
|
166
166
|
|
|
167
167
|
const expected = `
|
|
168
|
-
import { ThreadAssistantMessagePart, ToolCallMessagePart } from "@assistant-ui/react";
|
|
168
|
+
import { ThreadAssistantMessagePart, ToolCallMessagePart, TextMessagePart } from "@assistant-ui/react";
|
|
169
169
|
|
|
170
170
|
type MyUnion = ThreadAssistantMessagePart | TextMessagePart;
|
|
171
171
|
type MyGeneric = Array<ToolCallMessagePart<{ query: string }, string>>;
|
|
@@ -228,3 +228,269 @@ function MyComponent() {
|
|
|
228
228
|
expect(applyTransform(input).trim()).toBe(expected.trim());
|
|
229
229
|
});
|
|
230
230
|
});
|
|
231
|
+
|
|
232
|
+
describe("content-part import bindings", () => {
|
|
233
|
+
it.each([
|
|
234
|
+
[
|
|
235
|
+
"type references",
|
|
236
|
+
`import * as AUI from "@assistant-ui/react";
|
|
237
|
+
import * as X from "./local";
|
|
238
|
+
type Part = AUI.TextContentPart;
|
|
239
|
+
type LocalPart = X.TextContentPart;
|
|
240
|
+
function shadow(AUI: typeof X) { return null as AUI.TextContentPart | null; }`,
|
|
241
|
+
`import * as AUI from "@assistant-ui/react";
|
|
242
|
+
import * as X from "./local";
|
|
243
|
+
type Part = AUI.TextMessagePart;
|
|
244
|
+
type LocalPart = X.TextContentPart;
|
|
245
|
+
function shadow(AUI: typeof X) { return null as AUI.TextMessagePart | null; }`,
|
|
246
|
+
],
|
|
247
|
+
[
|
|
248
|
+
"JSX members",
|
|
249
|
+
`import * as AUI from "@assistant-ui/react";
|
|
250
|
+
import * as X from "./local";
|
|
251
|
+
const View = () => <AUI.ContentPartPrimitive.Text />;
|
|
252
|
+
const LocalView = () => <X.ContentPartPrimitive.Text />;
|
|
253
|
+
function shadow(AUI: typeof X) { return <AUI.ContentPartPrimitive.Text />; }`,
|
|
254
|
+
`import * as AUI from "@assistant-ui/react";
|
|
255
|
+
import * as X from "./local";
|
|
256
|
+
const View = () => <AUI.MessagePartPrimitive.Text />;
|
|
257
|
+
const LocalView = () => <X.ContentPartPrimitive.Text />;
|
|
258
|
+
function shadow(AUI: typeof X) { return <AUI.ContentPartPrimitive.Text />; }`,
|
|
259
|
+
],
|
|
260
|
+
[
|
|
261
|
+
"calls",
|
|
262
|
+
`import * as AUI from "@assistant-ui/react";
|
|
263
|
+
import * as X from "./local";
|
|
264
|
+
const part = AUI.useContentPart();
|
|
265
|
+
const localPart = X.useContentPart();
|
|
266
|
+
function shadow(AUI: typeof X) { return AUI.useContentPart(); }`,
|
|
267
|
+
`import * as AUI from "@assistant-ui/react";
|
|
268
|
+
import * as X from "./local";
|
|
269
|
+
const part = AUI.useMessagePart();
|
|
270
|
+
const localPart = X.useContentPart();
|
|
271
|
+
function shadow(AUI: typeof X) { return AUI.useContentPart(); }`,
|
|
272
|
+
],
|
|
273
|
+
])(
|
|
274
|
+
"renames assistant-ui namespace %s without changing other bindings",
|
|
275
|
+
(_, input, expected) => {
|
|
276
|
+
expect(applyTransform(input)).toBe(expected);
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
it("renames qualified types from a type-only namespace import", () => {
|
|
281
|
+
const input = `import type * as AUI from "@assistant-ui/react";
|
|
282
|
+
type Part = AUI.TextContentPart;`;
|
|
283
|
+
const expected = `import type * as AUI from "@assistant-ui/react";
|
|
284
|
+
type Part = AUI.TextMessagePart;`;
|
|
285
|
+
expect(applyTransform(input)).toBe(expected);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("preserves qualified types shadowed by a type-space declaration", () => {
|
|
289
|
+
const input = `import * as AUI from "@assistant-ui/react";
|
|
290
|
+
type Part = AUI.TextContentPart;
|
|
291
|
+
namespace Local {
|
|
292
|
+
namespace AUI { export type TextContentPart = string; }
|
|
293
|
+
type Part = AUI.TextContentPart;
|
|
294
|
+
}`;
|
|
295
|
+
const expected = `import * as AUI from "@assistant-ui/react";
|
|
296
|
+
type Part = AUI.TextMessagePart;
|
|
297
|
+
namespace Local {
|
|
298
|
+
namespace AUI { export type TextContentPart = string; }
|
|
299
|
+
type Part = AUI.TextContentPart;
|
|
300
|
+
}`;
|
|
301
|
+
expect(applyTransform(input)).toBe(expected);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("resolves typeof qualified names as values", () => {
|
|
305
|
+
const input = `import * as AUI from "@assistant-ui/react";
|
|
306
|
+
import * as X from "./local";
|
|
307
|
+
type Part = typeof AUI.TextContentPart;
|
|
308
|
+
function shadow(AUI: typeof X) { return null as typeof AUI.TextContentPart | null; }`;
|
|
309
|
+
const expected = `import * as AUI from "@assistant-ui/react";
|
|
310
|
+
import * as X from "./local";
|
|
311
|
+
type Part = typeof AUI.TextMessagePart;
|
|
312
|
+
function shadow(AUI: typeof X) { return null as typeof AUI.TextContentPart | null; }`;
|
|
313
|
+
expect(applyTransform(input)).toBe(expected);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it.each([
|
|
317
|
+
'import { useContentPart } from "./custom-hooks";\nexport const read = () => useContentPart();',
|
|
318
|
+
'import type { TextContentPart } from "./custom-types";\nexport const read = (part: TextContentPart) => part.text;',
|
|
319
|
+
'import { ContentPartPrimitive } from "./custom-ui";\nexport const Text = () => <ContentPartPrimitive.Text />;',
|
|
320
|
+
'import { MessagePrimitive } from "./custom-ui";\nexport const Content = () => <MessagePrimitive.Content />;',
|
|
321
|
+
])("preserves foreign imports and references: %s", (custom) => {
|
|
322
|
+
const input = `import { ThreadPrimitive } from "@assistant-ui/react";\n${custom}`;
|
|
323
|
+
expect(applyTransform(input)).toBe(input);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("preserves unbound names", () => {
|
|
327
|
+
const input = `import { ThreadPrimitive } from "@assistant-ui/react";
|
|
328
|
+
export const read = () => useContentPart();
|
|
329
|
+
export type Part = TextContentPart;`;
|
|
330
|
+
expect(applyTransform(input)).toBe(input);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("preserves an import alias while leaving another module's canonical name alone", () => {
|
|
334
|
+
const output =
|
|
335
|
+
applyTransform(`import { useContentPart as usePart, TextContentPart as Part } from "@assistant-ui/react";
|
|
336
|
+
import { useContentPart } from "./custom-hooks";
|
|
337
|
+
export const read = (part: Part) => [usePart(), useContentPart(), part];`);
|
|
338
|
+
expect(output).toContain("useMessagePart as usePart");
|
|
339
|
+
expect(output).toContain("TextMessagePart as Part");
|
|
340
|
+
expect(output).toContain("[usePart(), useContentPart(), part]");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("preserves shadowed parameters and uses an import alias to keep their references intact", () => {
|
|
344
|
+
const output =
|
|
345
|
+
applyTransform(`import { useContentPart } from "@assistant-ui/react";
|
|
346
|
+
export const read = () => useContentPart();
|
|
347
|
+
export const custom = (useContentPart) => useContentPart();`);
|
|
348
|
+
expect(output).toContain("useMessagePart as useContentPart");
|
|
349
|
+
expect(output).toContain("export const read = () => useContentPart();");
|
|
350
|
+
expect(output).toContain(
|
|
351
|
+
"export const custom = (useContentPart) => useContentPart();",
|
|
352
|
+
);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it.each([
|
|
356
|
+
"export type Identity<TextContentPart> = TextContentPart;",
|
|
357
|
+
"export function custom() { type TextContentPart = { custom: string }; return null as TextContentPart | null; }",
|
|
358
|
+
'export type Keys = { [TextContentPart in "a" | "b"]: TextContentPart };',
|
|
359
|
+
])("preserves type declarations and parameters: %s", (body) => {
|
|
360
|
+
const output =
|
|
361
|
+
applyTransform(`import type { TextContentPart } from "@assistant-ui/react";
|
|
362
|
+
export type Part = TextContentPart;
|
|
363
|
+
${body}`);
|
|
364
|
+
expect(output).toContain("TextMessagePart as TextContentPart");
|
|
365
|
+
expect(output).toContain("export type Part = TextContentPart;");
|
|
366
|
+
expect(output).toContain(body);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("preserves ambient declarations with the same name", () => {
|
|
370
|
+
const output =
|
|
371
|
+
applyTransform(`import { useContentPart } from "@assistant-ui/react";
|
|
372
|
+
export const read = () => useContentPart();
|
|
373
|
+
namespace Custom {
|
|
374
|
+
export declare function useContentPart(): string;
|
|
375
|
+
export const read = () => useContentPart();
|
|
376
|
+
}`);
|
|
377
|
+
expect(output).toContain("useMessagePart as useContentPart");
|
|
378
|
+
expect(output).toContain(
|
|
379
|
+
"export declare function useContentPart(): string;",
|
|
380
|
+
);
|
|
381
|
+
expect(output.match(/read = \(\) => useContentPart\(\)/g)).toHaveLength(2);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it.each([
|
|
385
|
+
'import type { TextContentPart } from "@assistant-ui/react";',
|
|
386
|
+
'import { type TextContentPart } from "@assistant-ui/react";',
|
|
387
|
+
])("renames unshadowed type-only references: %s", (declaration) => {
|
|
388
|
+
const output = applyTransform(
|
|
389
|
+
`${declaration}\nexport type Part = Array<TextContentPart>;`,
|
|
390
|
+
);
|
|
391
|
+
expect(output).toContain(
|
|
392
|
+
declaration.replace("TextContentPart", "TextMessagePart"),
|
|
393
|
+
);
|
|
394
|
+
expect(output).toContain("Array<TextMessagePart>");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it.each([
|
|
398
|
+
'const useMessagePart = () => "custom";\nexport const read = () => [useContentPart(), useMessagePart()];',
|
|
399
|
+
"export const read = (useMessagePart) => [useContentPart(), useMessagePart()];",
|
|
400
|
+
])("preserves names when the replacement would collide: %s", (body) => {
|
|
401
|
+
const output = applyTransform(
|
|
402
|
+
`import { useContentPart } from "@assistant-ui/react";\n${body}`,
|
|
403
|
+
);
|
|
404
|
+
expect(output).toContain("useMessagePart as useContentPart");
|
|
405
|
+
expect(output).toContain(body);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it("avoids capturing a type reference with a generic parameter", () => {
|
|
409
|
+
const output =
|
|
410
|
+
applyTransform(`import type { TextContentPart } from "@assistant-ui/react";
|
|
411
|
+
export type Pair<TextMessagePart> = [TextContentPart, TextMessagePart];`);
|
|
412
|
+
expect(output).toContain("TextMessagePart as TextContentPart");
|
|
413
|
+
expect(output).toContain("[TextContentPart, TextMessagePart]");
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("keeps object keys, member names, JSX attributes, and public export names", () => {
|
|
417
|
+
const output =
|
|
418
|
+
applyTransform(`import { useContentPart, ContentPartPrimitive } from "@assistant-ui/react";
|
|
419
|
+
export const hooks = { useContentPart };
|
|
420
|
+
export const read = (other) => other.useContentPart();
|
|
421
|
+
export const View = () => <ContentPartPrimitive.Text useContentPart={useContentPart} />;
|
|
422
|
+
export { useContentPart };
|
|
423
|
+
export { useContentPart as custom } from "./custom-hooks";`);
|
|
424
|
+
expect(output).toMatch(/useContentPart: useMessagePart/);
|
|
425
|
+
expect(output).toContain("other.useContentPart()");
|
|
426
|
+
expect(output).toContain(
|
|
427
|
+
"<MessagePartPrimitive.Text useContentPart={useMessagePart}",
|
|
428
|
+
);
|
|
429
|
+
expect(output).toContain("export { useMessagePart as useContentPart }");
|
|
430
|
+
expect(output).toContain(
|
|
431
|
+
'export { useContentPart as custom } from "./custom-hooks"',
|
|
432
|
+
);
|
|
433
|
+
expect(applyTransform(output)).toBe(output);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("preserves type-only exports while renaming their local references", () => {
|
|
437
|
+
const output =
|
|
438
|
+
applyTransform(`import type { TextContentPart } from "@assistant-ui/react";
|
|
439
|
+
export type { TextContentPart };
|
|
440
|
+
export { type TextContentPart as PublicPart };`);
|
|
441
|
+
expect(output).toContain(
|
|
442
|
+
"export type { TextMessagePart as TextContentPart }",
|
|
443
|
+
);
|
|
444
|
+
expect(output).toContain("export { type TextMessagePart as PublicPart }");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("resolves the MessagePrimitive import before renaming Content", () => {
|
|
448
|
+
const output =
|
|
449
|
+
applyTransform(`import { MessagePrimitive as Message } from "@assistant-ui/react";
|
|
450
|
+
export const View = () => <Message.Content />;
|
|
451
|
+
export const content = Message.Content;
|
|
452
|
+
export const dynamic = (Content) => Message[Content];
|
|
453
|
+
export const custom = (Message) => <Message.Content />;`);
|
|
454
|
+
expect(output).toContain("<Message.Parts />");
|
|
455
|
+
expect(output).toContain("export const content = Message.Parts;");
|
|
456
|
+
expect(output).toContain("Message[Content]");
|
|
457
|
+
expect(output).toContain(
|
|
458
|
+
"export const custom = (Message) => <Message.Content />;",
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe("content-part re-exports", () => {
|
|
464
|
+
it.each([
|
|
465
|
+
[
|
|
466
|
+
'export { useContentPart } from "@assistant-ui/react";',
|
|
467
|
+
'export { useMessagePart as useContentPart } from "@assistant-ui/react";',
|
|
468
|
+
],
|
|
469
|
+
[
|
|
470
|
+
'export type { TextContentPart } from "@assistant-ui/react";',
|
|
471
|
+
'export type { TextMessagePart as TextContentPart } from "@assistant-ui/react";',
|
|
472
|
+
],
|
|
473
|
+
[
|
|
474
|
+
'export { type TextContentPart as Part } from "@assistant-ui/react";',
|
|
475
|
+
'export { type TextMessagePart as Part } from "@assistant-ui/react";',
|
|
476
|
+
],
|
|
477
|
+
[
|
|
478
|
+
'export { useContentPart as usePart } from "@assistant-ui/react";',
|
|
479
|
+
'export { useMessagePart as usePart } from "@assistant-ui/react";',
|
|
480
|
+
],
|
|
481
|
+
])(
|
|
482
|
+
"migrates the source symbol while preserving its public name: %s",
|
|
483
|
+
(input, expected) => {
|
|
484
|
+
const output = applyTransform(input);
|
|
485
|
+
expect(output).toBe(expected);
|
|
486
|
+
expect(applyTransform(output)).toBe(output);
|
|
487
|
+
},
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
it("preserves unrelated re-exports and export namespaces", () => {
|
|
491
|
+
const input = `export { useContentPart } from "./custom-hooks";
|
|
492
|
+
export type { TextContentPart } from "./custom-types";
|
|
493
|
+
export * as ContentPartPrimitive from "@assistant-ui/react";`;
|
|
494
|
+
expect(applyTransform(input)).toBe(input);
|
|
495
|
+
});
|
|
496
|
+
});
|