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.
Files changed (54) hide show
  1. package/README.md +7 -0
  2. package/dist/codemods/v0-11/content-part-to-message-part.d.ts.map +1 -1
  3. package/dist/codemods/v0-11/content-part-to-message-part.js +126 -40
  4. package/dist/codemods/v0-11/content-part-to-message-part.js.map +1 -1
  5. package/dist/codemods/v0-12/event-names-to-camelcase.d.ts.map +1 -1
  6. package/dist/codemods/v0-12/event-names-to-camelcase.js +2 -2
  7. package/dist/codemods/v0-12/event-names-to-camelcase.js.map +1 -1
  8. package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -1
  9. package/dist/codemods/v0-12/primitive-if-to-aui-if.js +57 -41
  10. package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -1
  11. package/dist/codemods/v0-8/ui-package-split.d.ts.map +1 -1
  12. package/dist/codemods/v0-8/ui-package-split.js +11 -35
  13. package/dist/codemods/v0-8/ui-package-split.js.map +1 -1
  14. package/dist/codemods/v0-9/edge-package-split.d.ts.map +1 -1
  15. package/dist/codemods/v0-9/edge-package-split.js +15 -69
  16. package/dist/codemods/v0-9/edge-package-split.js.map +1 -1
  17. package/dist/commands/cloud.d.ts.map +1 -1
  18. package/dist/commands/cloud.js +12 -3
  19. package/dist/commands/cloud.js.map +1 -1
  20. package/dist/commands/info.d.ts.map +1 -1
  21. package/dist/commands/info.js +8 -1
  22. package/dist/commands/info.js.map +1 -1
  23. package/dist/lib/cloud-auth.d.ts +1 -0
  24. package/dist/lib/cloud-auth.d.ts.map +1 -1
  25. package/dist/lib/cloud-auth.js +4 -3
  26. package/dist/lib/cloud-auth.js.map +1 -1
  27. package/dist/lib/cloud-remote.d.ts +22 -0
  28. package/dist/lib/cloud-remote.d.ts.map +1 -0
  29. package/dist/lib/cloud-remote.js +74 -0
  30. package/dist/lib/cloud-remote.js.map +1 -0
  31. package/dist/lib/utils/package-installer.d.ts.map +1 -1
  32. package/dist/lib/utils/package-installer.js +3 -2
  33. package/dist/lib/utils/package-installer.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/codemods/v0-11/__tests__/content-part-to-message-part.test.ts +268 -2
  36. package/src/codemods/v0-11/content-part-to-message-part.ts +211 -80
  37. package/src/codemods/v0-12/__tests__/event-names-to-camelcase.test.ts +23 -0
  38. package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +150 -1
  39. package/src/codemods/v0-12/event-names-to-camelcase.ts +2 -8
  40. package/src/codemods/v0-12/primitive-if-to-aui-if.ts +92 -61
  41. package/src/codemods/v0-8/__tests__/ui-package-split.test.ts +83 -0
  42. package/src/codemods/v0-8/ui-package-split.ts +21 -59
  43. package/src/codemods/v0-9/edge-package-split.test.ts +100 -0
  44. package/src/codemods/v0-9/edge-package-split.ts +30 -139
  45. package/src/commands/cloud.test.ts +58 -0
  46. package/src/commands/cloud.ts +19 -2
  47. package/src/commands/info.ts +26 -1
  48. package/src/lib/cloud-auth.ts +14 -8
  49. package/src/lib/cloud-remote.test.ts +156 -0
  50. package/src/lib/cloud-remote.ts +150 -0
  51. package/src/lib/utils/package-installer.test.ts +39 -0
  52. package/src/lib/utils/package-installer.ts +3 -1
  53. package/templates/cloud-harness/components/assistant-ui/elements/tool-fallback.aui.tsx +17 -8
  54. package/templates/cloud-harness/package.json +2 -2
@@ -1,11 +1,8 @@
1
1
  import { createTransformer } from "../utils/createTransformer";
2
2
 
3
- // Chat runtime and converter exports moved to react-ai-sdk
4
3
  const reactAiSdkExports: string[] = [
5
- // Chat Runtime (replaces edge runtime)
6
4
  "useChatRuntime",
7
5
 
8
- // Core Types
9
6
  "CoreMessage",
10
7
  "CoreUserMessage",
11
8
  "CoreAssistantMessage",
@@ -14,13 +11,11 @@ const reactAiSdkExports: string[] = [
14
11
  "CoreAssistantMessagePart",
15
12
  "CoreToolCallMessagePart",
16
13
 
17
- // Core message converters
18
14
  "fromCoreMessages",
19
15
  "fromCoreMessage",
20
16
  "toCoreMessages",
21
17
  "toCoreMessage",
22
18
 
23
- // Language model converters
24
19
  "toLanguageModelMessages",
25
20
  "toLanguageModelTools",
26
21
  "fromLanguageModelMessages",
@@ -29,150 +24,46 @@ const reactAiSdkExports: string[] = [
29
24
  ];
30
25
 
31
26
  const migrateToEdgePackage = createTransformer(({ j, root, markAsChanged }) => {
32
- const sourcesToMigrate: string[] = ["@assistant-ui/react"];
33
- const movedAiSdkSpecifiers: any[] = [];
34
- let lastMigratedImportPath: any = null;
35
-
36
- root
37
- .find(j.ImportDeclaration)
38
- .filter((path: any) => sourcesToMigrate.includes(path.value.source.value))
39
- .forEach((path: any) => {
40
- let hadMigratedSpecifiers = false;
41
- const remainingSpecifiers: any[] = [];
42
- path.value.specifiers.forEach((specifier: any) => {
43
- if (
44
- j.ImportSpecifier.check(specifier) &&
45
- reactAiSdkExports.includes(specifier.imported.name as string)
46
- ) {
47
- movedAiSdkSpecifiers.push(specifier);
48
- hadMigratedSpecifiers = true;
49
- } else {
50
- remainingSpecifiers.push(specifier);
51
- }
52
- });
53
- if (hadMigratedSpecifiers) {
54
- lastMigratedImportPath = path;
55
- }
56
- if (remainingSpecifiers.length === 0) {
57
- j(path).remove();
58
- markAsChanged();
59
- } else if (remainingSpecifiers.length !== path.value.specifiers.length) {
60
- path.value.specifiers = remainingSpecifiers;
61
- markAsChanged();
62
- }
63
- });
64
-
65
- // Add imports for react-ai-sdk (all edge-related imports now go here)
66
- if (movedAiSdkSpecifiers.length > 0) {
67
- const existingAiSdkImport = root.find(j.ImportDeclaration, {
68
- source: { value: "@assistant-ui/react-ai-sdk" },
69
- });
70
- if (existingAiSdkImport.size() > 0) {
71
- existingAiSdkImport.forEach((path: any) => {
72
- movedAiSdkSpecifiers.forEach((specifier: any) => {
73
- if (
74
- !path.value.specifiers.some(
75
- (s: any) => s.imported.name === specifier.imported.name,
76
- )
77
- ) {
78
- path.value.specifiers.push(specifier);
79
- }
80
- });
81
- });
82
- } else {
83
- const newImport = j.importDeclaration(
84
- movedAiSdkSpecifiers,
85
- j.literal("@assistant-ui/react-ai-sdk"),
86
- );
87
- if (lastMigratedImportPath) {
88
- j(lastMigratedImportPath).insertAfter(newImport);
89
- } else {
90
- const firstImport = root.find(j.ImportDeclaration).at(0);
91
- if (firstImport.size() > 0) {
92
- firstImport.insertBefore(newImport);
93
- } else {
94
- root.get().node.program.body.unshift(newImport);
95
- }
96
- }
97
- }
98
- markAsChanged();
99
- }
100
-
101
- // Migrate imports from edge/converters
102
- root.find(j.ImportDeclaration).forEach((path: any) => {
103
- const sourceValue: string = path.value.source.value;
27
+ root.find(j.ImportDeclaration).forEach((path) => {
28
+ const source = path.node.source.value;
104
29
  if (
105
- sourceValue.startsWith("@assistant-ui/react/") &&
106
- (sourceValue.includes("edge/") ||
107
- sourceValue.includes("dangerous-in-browser/"))
30
+ typeof source === "string" &&
31
+ source.startsWith("@assistant-ui/react/") &&
32
+ (source.includes("edge/") || source.includes("dangerous-in-browser/"))
108
33
  ) {
109
- path.value.source = j.literal(
110
- sourceValue.replace(
111
- "@assistant-ui/react/",
112
- "@assistant-ui/react-ai-sdk/",
113
- ),
34
+ path.node.source = j.literal(
35
+ source.replace("@assistant-ui/react/", "@assistant-ui/react-ai-sdk/"),
114
36
  );
115
37
  markAsChanged();
38
+ return;
116
39
  }
117
- });
118
40
 
119
- // Migrate language model converter imports from react-edge to react-ai-sdk
120
- root.find(j.ImportDeclaration).forEach((path: any) => {
121
- const sourceValue: string = path.value.source.value;
122
41
  if (
123
- sourceValue === "@assistant-ui/react-edge" ||
124
- sourceValue === "@assistant-ui/react"
125
- ) {
126
- let hasLanguageModelConverters = false;
127
- const remainingSpecifiers: any[] = [];
128
- const aiSdkSpecifiers: any[] = [];
129
-
130
- path.value.specifiers.forEach((specifier: any) => {
131
- if (
132
- j.ImportSpecifier.check(specifier) &&
133
- reactAiSdkExports.includes(specifier.imported.name as string)
134
- ) {
135
- aiSdkSpecifiers.push(specifier);
136
- hasLanguageModelConverters = true;
137
- } else {
138
- remainingSpecifiers.push(specifier);
139
- }
140
- });
141
-
142
- if (hasLanguageModelConverters) {
143
- if (remainingSpecifiers.length === 0) {
144
- j(path).remove();
145
- } else {
146
- path.value.specifiers = remainingSpecifiers;
147
- }
148
-
149
- const existingAiSdkImport = root.find(j.ImportDeclaration, {
150
- source: { value: "@assistant-ui/react-ai-sdk" },
151
- });
42
+ source !== "@assistant-ui/react" &&
43
+ source !== "@assistant-ui/react-edge"
44
+ )
45
+ return;
152
46
 
153
- if (existingAiSdkImport.size() > 0) {
154
- existingAiSdkImport.forEach((importPath: any) => {
155
- aiSdkSpecifiers.forEach((specifier: any) => {
156
- if (
157
- !importPath.value.specifiers.some(
158
- (s: any) => s.imported.name === specifier.imported.name,
159
- )
160
- ) {
161
- importPath.value.specifiers.push(specifier);
162
- }
163
- });
164
- });
165
- } else {
166
- const newImport = j.importDeclaration(
167
- aiSdkSpecifiers,
168
- j.literal("@assistant-ui/react-ai-sdk"),
169
- );
170
- j(path).insertAfter(newImport);
171
- }
47
+ const specifiers = path.node.specifiers ?? [];
48
+ const moved = specifiers.filter(
49
+ (specifier) =>
50
+ j.ImportSpecifier.check(specifier) &&
51
+ reactAiSdkExports.includes(specifier.imported.name as string),
52
+ );
53
+ if (moved.length === 0) return;
172
54
 
173
- markAsChanged();
174
- }
55
+ const destination = j.literal("@assistant-ui/react-ai-sdk");
56
+ if (moved.length === specifiers.length) {
57
+ path.node.source = destination;
58
+ } else {
59
+ path.node.specifiers = specifiers.filter(
60
+ (specifier) => !moved.includes(specifier),
61
+ );
62
+ const declaration = j.importDeclaration(moved, destination);
63
+ declaration.importKind = path.node.importKind ?? "value";
64
+ j(path).insertAfter(declaration);
175
65
  }
66
+ markAsChanged();
176
67
  });
177
68
  });
178
69
 
@@ -9,6 +9,9 @@ const {
9
9
  fetchProjects,
10
10
  createProject,
11
11
  cloudAccessToken,
12
+ savedCloudAccessToken,
13
+ runCloudCommand,
14
+ loginToCloud,
12
15
  provisionCloudHarness,
13
16
  scaffold,
14
17
  select,
@@ -18,6 +21,9 @@ const {
18
21
  fetchProjects: vi.fn(),
19
22
  createProject: vi.fn(),
20
23
  cloudAccessToken: vi.fn(),
24
+ savedCloudAccessToken: vi.fn(),
25
+ runCloudCommand: vi.fn(),
26
+ loginToCloud: vi.fn(),
21
27
  provisionCloudHarness: vi.fn(),
22
28
  scaffold: vi.fn(),
23
29
  select: vi.fn(),
@@ -33,6 +39,12 @@ vi.mock("aui-auth/accounts", async (importOriginal) => ({
33
39
  vi.mock("../lib/cloud-auth", async (importOriginal) => ({
34
40
  ...(await importOriginal<typeof import("../lib/cloud-auth")>()),
35
41
  cloudAccessToken,
42
+ savedCloudAccessToken,
43
+ loginToCloud,
44
+ }));
45
+ vi.mock("../lib/cloud-remote", async (importOriginal) => ({
46
+ ...(await importOriginal<typeof import("../lib/cloud-remote")>()),
47
+ runCloudCommand,
36
48
  }));
37
49
  vi.mock("../lib/cloud-provision", async (importOriginal) => ({
38
50
  ...(await importOriginal<typeof import("../lib/cloud-provision")>()),
@@ -236,3 +248,49 @@ describe("cloud setup", () => {
236
248
  ]);
237
249
  });
238
250
  });
251
+
252
+ describe("cloud remote forwarding", () => {
253
+ const forwarded = async (argv: string[]) => {
254
+ runCloudCommand.mockClear();
255
+ runCloudCommand.mockResolvedValue(4);
256
+ await cloud.parseAsync(argv, { from: "user" });
257
+ return runCloudCommand.mock.calls.map(([args]) => args);
258
+ };
259
+
260
+ it("forwards help, unknown subcommands and their flags verbatim", async () => {
261
+ for (const argv of [
262
+ [],
263
+ ["help"],
264
+ ["--help"],
265
+ ["-h"],
266
+ ["help", "projects"],
267
+ ["projects", "list", "--org", "acme", "--json"],
268
+ ["whoami", "--version"],
269
+ ["--org=acme", "projects"],
270
+ ]) {
271
+ await expect(forwarded(argv)).resolves.toEqual([argv]);
272
+ }
273
+ expect(process.exitCode).toBe(4);
274
+ process.exitCode = undefined;
275
+ });
276
+
277
+ it("keeps login local", async () => {
278
+ loginToCloud.mockResolvedValue(undefined);
279
+ await expect(forwarded(["login", "--no-open"])).resolves.toEqual([]);
280
+ expect(loginToCloud).toHaveBeenCalledWith(
281
+ expect.anything(),
282
+ expect.objectContaining({ noOpen: true }),
283
+ );
284
+ });
285
+
286
+ it("sends the saved login without starting a new one", async () => {
287
+ savedCloudAccessToken.mockResolvedValue(null);
288
+ runCloudCommand.mockImplementationOnce(async (_args, options) => {
289
+ await expect(options.accessToken()).resolves.toBeNull();
290
+ return 0;
291
+ });
292
+ await cloud.parseAsync(["whoami"], { from: "user" });
293
+ expect(savedCloudAccessToken).toHaveBeenCalled();
294
+ expect(cloudAccessToken).not.toHaveBeenCalled();
295
+ });
296
+ });
@@ -15,8 +15,10 @@ import {
15
15
  cloudAuthConfig,
16
16
  loginToCloud,
17
17
  logoutFromCloud,
18
+ savedCloudAccessToken,
18
19
  validateCloudUrl,
19
20
  } from "../lib/cloud-auth";
21
+ import { runCloudCommand } from "../lib/cloud-remote";
20
22
  import {
21
23
  readCloudProjectConfig,
22
24
  mergeCloudEnvironment,
@@ -370,9 +372,24 @@ export const setupCloud = async (
370
372
  );
371
373
  };
372
374
 
375
+ const DEFAULT_CLOUD_URL = "https://cloud.assistant-ui.com";
376
+
373
377
  export const cloud = new Command()
374
378
  .name("cloud")
375
- .description("sign in and set up a multiplayer Assistant Cloud chat")
379
+ .description("manage Assistant Cloud (run `assistant-ui cloud help`)")
380
+ .helpOption(false)
381
+ .helpCommand(false)
382
+ .argument("[command...]")
383
+ .allowUnknownOption()
384
+ .allowExcessArguments()
385
+ .action(async (args: string[]) => {
386
+ process.exitCode = await runCloudCommand(args, {
387
+ cloudUrl: process.env.ASSISTANT_UI_CLOUD_URL ?? DEFAULT_CLOUD_URL,
388
+ accessToken: () => savedCloudAccessToken(cloudAuthConfig()),
389
+ stdout: (text) => process.stdout.write(text),
390
+ stderr: (text) => process.stderr.write(text),
391
+ });
392
+ })
376
393
  .addCommand(
377
394
  new Command("login")
378
395
  .description("sign in to Assistant Cloud using your browser")
@@ -418,7 +435,7 @@ export const cloud = new Command()
418
435
  .option(
419
436
  "--api-url <url>",
420
437
  "Assistant Cloud management URL",
421
- process.env.ASSISTANT_UI_CLOUD_URL ?? "https://cloud.assistant-ui.com",
438
+ process.env.ASSISTANT_UI_CLOUD_URL ?? DEFAULT_CLOUD_URL,
422
439
  )
423
440
  .option(
424
441
  "--access-code <code>",
@@ -38,6 +38,8 @@ const ASSISTANT_UI_PACKAGES = [
38
38
  "@assistant-ui/react-lexical",
39
39
  "@assistant-ui/react-syntax-highlighter",
40
40
  "@assistant-ui/react-hook-form",
41
+ "safe-content-frame",
42
+ "tw-shimmer",
41
43
  // Observability & DevTools
42
44
  "@assistant-ui/react-o11y",
43
45
  "@assistant-ui/react-devtools",
@@ -50,6 +52,7 @@ const ECOSYSTEM_PACKAGES = [
50
52
  "next",
51
53
  "vite",
52
54
  "expo",
55
+ "tailwindcss",
53
56
  "ai",
54
57
  "zod",
55
58
  "zustand",
@@ -112,6 +115,24 @@ function getAssistantUiPackageNames(
112
115
  return [...new Set([...ASSISTANT_UI_PACKAGES, ...declaredPackages])];
113
116
  }
114
117
 
118
+ // Adapters peer on the provider SDK they wrap, so the installed packages
119
+ // name the upstream versions a bug report needs.
120
+ function getEcosystemPackageNames(
121
+ packages: PackageInfo[],
122
+ cwd: string,
123
+ ): string[] {
124
+ const upstreamPeers = packages
125
+ .flatMap((pkg) => Object.keys(getPeerDeps(pkg.name, cwd) ?? {}))
126
+ .filter(
127
+ (name) =>
128
+ !name.startsWith("@assistant-ui/") &&
129
+ !ASSISTANT_UI_PACKAGES.includes(name),
130
+ )
131
+ .sort();
132
+
133
+ return [...new Set([...ECOSYSTEM_PACKAGES, ...upstreamPeers])];
134
+ }
135
+
115
136
  function getSpecifiedRange(
116
137
  pkg: string,
117
138
  projectPkg: Record<string, unknown>,
@@ -312,7 +333,11 @@ async function collectInfo(
312
333
  cwd,
313
334
  projectPkg,
314
335
  );
315
- const ecosystem = collectPackages(ECOSYSTEM_PACKAGES, cwd, projectPkg);
336
+ const ecosystem = collectPackages(
337
+ getEcosystemPackageNames(packages, cwd),
338
+ cwd,
339
+ projectPkg,
340
+ );
316
341
  const warnings = collectWarnings(packages, cwd, projectPkg);
317
342
 
318
343
  return {
@@ -230,16 +230,11 @@ export const loginToCloud = async (
230
230
  }
231
231
  };
232
232
 
233
- export const cloudAccessToken = async (
233
+ export const savedCloudAccessToken = async (
234
234
  config: CloudAuthConfig,
235
- options: {
236
- noOpen?: boolean;
237
- setupUrl?: string;
238
- print?: (text: string) => void;
239
- } = {},
240
- ): Promise<string> => {
235
+ ): Promise<string | null> => {
241
236
  const saved = await readCloudCredentials(config);
242
- if (!saved) return (await loginToCloud(config, options)).tokens.accessToken;
237
+ if (!saved) return null;
243
238
  const login = createDeviceLogin({
244
239
  issuer: config.issuer,
245
240
  clientId: config.clientId,
@@ -259,6 +254,17 @@ export const cloudAccessToken = async (
259
254
  }
260
255
  };
261
256
 
257
+ export const cloudAccessToken = async (
258
+ config: CloudAuthConfig,
259
+ options: {
260
+ noOpen?: boolean;
261
+ setupUrl?: string;
262
+ print?: (text: string) => void;
263
+ } = {},
264
+ ): Promise<string> =>
265
+ (await savedCloudAccessToken(config)) ??
266
+ (await loginToCloud(config, options)).tokens.accessToken;
267
+
262
268
  export const logoutFromCloud = async (
263
269
  config: CloudAuthConfig,
264
270
  ): Promise<void> => {
@@ -0,0 +1,156 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ joinCloudArgs,
4
+ parseCloudCliResponse,
5
+ runCloudCommand,
6
+ sendCloudCommand,
7
+ type CloudCliResponse,
8
+ } from "./cloud-remote";
9
+
10
+ afterEach(() => {
11
+ vi.unstubAllGlobals();
12
+ });
13
+
14
+ const harness = (reply: CloudCliResponse, token: string | null = "token") => {
15
+ const output = { stdout: "", stderr: "" };
16
+ const send = vi.fn(async () => reply);
17
+ return {
18
+ output,
19
+ send,
20
+ run: (args: string[]) =>
21
+ runCloudCommand(args, {
22
+ cloudUrl: "https://cloud.example.com",
23
+ accessToken: async () => token,
24
+ stdout: (text) => {
25
+ output.stdout += text;
26
+ },
27
+ stderr: (text) => {
28
+ output.stderr += text;
29
+ },
30
+ send,
31
+ }),
32
+ };
33
+ };
34
+
35
+ describe("cloud remote commands", () => {
36
+ it("quotes arguments so a POSIX shell split gives them back", () => {
37
+ expect(
38
+ joinCloudArgs(["projects", "list", "--org", "my org", "it's", ""]),
39
+ ).toBe(`projects list --org 'my org' 'it'\\''s' ''`);
40
+ });
41
+
42
+ it("prints the envelope and exits with its code", async () => {
43
+ const { run, send, output } = harness({
44
+ stdout: "out\n",
45
+ stderr: "err\n",
46
+ exitCode: 3,
47
+ });
48
+ await expect(run(["whoami"])).resolves.toBe(3);
49
+ expect(send).toHaveBeenCalledWith(
50
+ "https://cloud.example.com",
51
+ ["whoami"],
52
+ "token",
53
+ );
54
+ expect(output).toEqual({ stdout: "out\n", stderr: "err\n" });
55
+ });
56
+
57
+ it("fails an action it does not know before printing anything", async () => {
58
+ const { run, output } = harness({
59
+ stdout: "uploading\n",
60
+ exitCode: 0,
61
+ actions: [{ type: "upload", arg: 2 }],
62
+ });
63
+ await expect(run(["files", "push", "a.txt"])).resolves.toBe(1);
64
+ expect(output.stdout).toBe("");
65
+ expect(output.stderr).toMatch(/newer assistant-ui.*"upload"/);
66
+ });
67
+
68
+ it("prints a failed request as one line and exits 1", async () => {
69
+ const { run, send, output } = harness({ exitCode: 0 });
70
+ send.mockRejectedValueOnce(new Error("Could not reach the cloud"));
71
+ await expect(run(["whoami"])).resolves.toBe(1);
72
+ expect(output.stderr).toBe("Could not reach the cloud\n");
73
+ });
74
+
75
+ it("sends anonymously when the saved login cannot be read", async () => {
76
+ const output = { stdout: "", stderr: "" };
77
+ const send = vi.fn(async () => ({ stdout: "help\n", exitCode: 0 }));
78
+ await expect(
79
+ runCloudCommand(["help"], {
80
+ cloudUrl: "https://cloud.example.com",
81
+ accessToken: async () => {
82
+ throw new Error("Invalid saved login.");
83
+ },
84
+ stdout: (text) => {
85
+ output.stdout += text;
86
+ },
87
+ stderr: (text) => {
88
+ output.stderr += text;
89
+ },
90
+ send,
91
+ }),
92
+ ).resolves.toBe(0);
93
+ expect(send).toHaveBeenCalledWith(
94
+ "https://cloud.example.com",
95
+ ["help"],
96
+ null,
97
+ );
98
+ expect(output).toEqual({
99
+ stdout: "help\n",
100
+ stderr: "Invalid saved login.\n",
101
+ });
102
+ });
103
+
104
+ it("accepts only a well-formed envelope", () => {
105
+ expect(parseCloudCliResponse({ stdout: "x", exitCode: 0 })).toEqual({
106
+ stdout: "x",
107
+ exitCode: 0,
108
+ });
109
+ for (const bad of [
110
+ null,
111
+ "text",
112
+ { stdout: "x" },
113
+ { exitCode: 1.5 },
114
+ { exitCode: 256 },
115
+ { exitCode: -1 },
116
+ { exitCode: 0, stderr: 3 },
117
+ { exitCode: 0, actions: [{}] },
118
+ ]) {
119
+ expect(parseCloudCliResponse(bad)).toBeNull();
120
+ }
121
+ });
122
+
123
+ it("posts the command string, client and bearer token", async () => {
124
+ const fetchMock = vi.fn(async () =>
125
+ Response.json({ stdout: "ok\n", exitCode: 0 }),
126
+ );
127
+ vi.stubGlobal("fetch", fetchMock);
128
+ await expect(
129
+ sendCloudCommand("https://cloud.example.com/", ["help", "a b"], "tok"),
130
+ ).resolves.toEqual({ stdout: "ok\n", exitCode: 0 });
131
+ const [url, init] = fetchMock.mock.calls[0] as unknown as [
132
+ string,
133
+ RequestInit,
134
+ ];
135
+ expect(url).toBe("https://cloud.example.com/api/cli/run");
136
+ expect(init.headers).toMatchObject({ Authorization: "Bearer tok" });
137
+ expect(JSON.parse(init.body as string)).toMatchObject({
138
+ command: "help 'a b'",
139
+ client: { name: "assistant-ui" },
140
+ });
141
+ });
142
+
143
+ it("sends no Authorization header when signed out and rejects a non-envelope", async () => {
144
+ const fetchMock = vi.fn(
145
+ async () => new Response("<html>", { status: 502 }),
146
+ );
147
+ vi.stubGlobal("fetch", fetchMock);
148
+ await expect(
149
+ sendCloudCommand("https://cloud.example.com", ["help"], null),
150
+ ).rejects.toThrow(/Unexpected response .*502/);
151
+ const init = (
152
+ fetchMock.mock.calls[0] as unknown as [string, RequestInit]
153
+ )[1];
154
+ expect(init.headers).not.toHaveProperty("Authorization");
155
+ });
156
+ });