assistant-ui 0.0.124 → 0.0.126
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-12/primitive-if-to-aui-if.d.ts.map +1 -1
- package/dist/codemods/v0-12/primitive-if-to-aui-if.js +4 -4
- package/dist/codemods/v0-12/primitive-if-to-aui-if.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/index.js +0 -0
- 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/package.json +7 -7
- package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +35 -1
- package/src/codemods/v0-12/primitive-if-to-aui-if.ts +5 -4
- 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/templates/cloud-harness/components/assistant-ui/elements/attachment-state.ts +59 -0
- package/templates/cloud-harness/components/assistant-ui/elements/attachment.aui.tsx +11 -44
- package/templates/cloud-harness/package.json +11 -11
- package/templates/cloud-harness/.gitignore +0 -42
|
@@ -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.126";
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "assistant-ui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.126",
|
|
4
4
|
"description": "CLI for assistant-ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cli",
|
|
@@ -39,20 +39,20 @@
|
|
|
39
39
|
"glob": "^13.0.6",
|
|
40
40
|
"jscodeshift": "^17.4.0",
|
|
41
41
|
"jsonc-parser": "^3.3.1",
|
|
42
|
-
"react": "^19.3.
|
|
42
|
+
"react": "^19.3.1",
|
|
43
43
|
"semver": "^7.8.5",
|
|
44
|
-
"statewire": "^0.
|
|
44
|
+
"statewire": "^0.20.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@assistant-ui/x-buildutils": "0.0.
|
|
47
|
+
"@assistant-ui/x-buildutils": "0.0.31",
|
|
48
48
|
"@types/cli-progress": "^3.11.6",
|
|
49
49
|
"@types/cross-spawn": "^6.0.6",
|
|
50
50
|
"@types/debug": "^4.1.13",
|
|
51
51
|
"@types/jscodeshift": "^17.3.0",
|
|
52
|
-
"@types/node": "^26.6.
|
|
52
|
+
"@types/node": "^26.6.5",
|
|
53
53
|
"@types/semver": "^7.8.0",
|
|
54
|
-
"@vitest/coverage-v8": "^5.0.
|
|
55
|
-
"vitest": "^5.0.
|
|
54
|
+
"@vitest/coverage-v8": "^5.0.5",
|
|
55
|
+
"vitest": "^5.0.5"
|
|
56
56
|
},
|
|
57
57
|
"publishConfig": {
|
|
58
58
|
"access": "public",
|
|
@@ -969,7 +969,7 @@ function MyComponent() {
|
|
|
969
969
|
});
|
|
970
970
|
});
|
|
971
971
|
|
|
972
|
-
describe("condition
|
|
972
|
+
describe("condition semantics", () => {
|
|
973
973
|
const compileCondition = (jsx: string) => {
|
|
974
974
|
const output = applyTransform(`
|
|
975
975
|
import { MessagePrimitive, ComposerPrimitive } from "@assistant-ui/react";
|
|
@@ -982,6 +982,40 @@ const view = ${jsx};
|
|
|
982
982
|
) => boolean;
|
|
983
983
|
};
|
|
984
984
|
|
|
985
|
+
it.each(["hasBranches", "lastOrHover"])(
|
|
986
|
+
"keeps %s={false} unconditional",
|
|
987
|
+
(filter) => {
|
|
988
|
+
const condition = compileCondition(
|
|
989
|
+
`<MessagePrimitive.If ${filter}={false} />`,
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
for (const branchCount of [1, 2]) {
|
|
993
|
+
for (const isLast of [false, true]) {
|
|
994
|
+
for (const isHovering of [false, true]) {
|
|
995
|
+
expect(
|
|
996
|
+
condition({ message: { branchCount, isLast, isHovering } }),
|
|
997
|
+
).toBe(true);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
);
|
|
1003
|
+
|
|
1004
|
+
it.each(["hasBranches", "lastOrHover"])(
|
|
1005
|
+
"keeps the role filter when %s={false}",
|
|
1006
|
+
(filter) => {
|
|
1007
|
+
const condition = compileCondition(
|
|
1008
|
+
`<MessagePrimitive.If ${filter}={false} assistant />`,
|
|
1009
|
+
);
|
|
1010
|
+
const message = { branchCount: 1, isLast: false, isHovering: false };
|
|
1011
|
+
|
|
1012
|
+
expect(condition({ message: { ...message, role: "assistant" } })).toBe(
|
|
1013
|
+
true,
|
|
1014
|
+
);
|
|
1015
|
+
expect(condition({ message: { ...message, role: "user" } })).toBe(false);
|
|
1016
|
+
},
|
|
1017
|
+
);
|
|
1018
|
+
|
|
985
1019
|
it.each([
|
|
986
1020
|
{
|
|
987
1021
|
jsx: "<MessagePrimitive.If speaking={false} />",
|
|
@@ -42,8 +42,8 @@ const messagePropMap: Record<
|
|
|
42
42
|
expression: `s.message.role ${v === false ? "!==" : "==="} "system"`,
|
|
43
43
|
negated: false,
|
|
44
44
|
}),
|
|
45
|
-
hasBranches: () => ({
|
|
46
|
-
expression: "s.message.branchCount >= 2",
|
|
45
|
+
hasBranches: (v) => ({
|
|
46
|
+
expression: v === false ? "true" : "s.message.branchCount >= 2",
|
|
47
47
|
negated: false,
|
|
48
48
|
}),
|
|
49
49
|
copied: (v) => ({
|
|
@@ -54,8 +54,9 @@ const messagePropMap: Record<
|
|
|
54
54
|
expression: "s.message.isLast",
|
|
55
55
|
negated: v === false,
|
|
56
56
|
}),
|
|
57
|
-
lastOrHover: () => ({
|
|
58
|
-
expression:
|
|
57
|
+
lastOrHover: (v) => ({
|
|
58
|
+
expression:
|
|
59
|
+
v === false ? "true" : "s.message.isHovering || s.message.isLast",
|
|
59
60
|
negated: false,
|
|
60
61
|
}),
|
|
61
62
|
speaking: (v) => ({
|
|
@@ -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
|
+
});
|