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,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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
30
|
+
typeof source === "string" &&
|
|
31
|
+
source.startsWith("@assistant-ui/react/") &&
|
|
32
|
+
(source.includes("edge/") || source.includes("dangerous-in-browser/"))
|
|
108
33
|
) {
|
|
109
|
-
path.
|
|
110
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
)
|
|
126
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
+
});
|
package/src/commands/cloud.ts
CHANGED
|
@@ -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("
|
|
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 ??
|
|
438
|
+
process.env.ASSISTANT_UI_CLOUD_URL ?? DEFAULT_CLOUD_URL,
|
|
422
439
|
)
|
|
423
440
|
.option(
|
|
424
441
|
"--access-code <code>",
|
package/src/commands/info.ts
CHANGED
|
@@ -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(
|
|
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 {
|
package/src/lib/cloud-auth.ts
CHANGED
|
@@ -230,16 +230,11 @@ export const loginToCloud = async (
|
|
|
230
230
|
}
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
-
export const
|
|
233
|
+
export const savedCloudAccessToken = async (
|
|
234
234
|
config: CloudAuthConfig,
|
|
235
|
-
|
|
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
|
|
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
|
+
});
|