assistant-ui 0.0.119 → 0.0.121

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 (92) hide show
  1. package/README.md +60 -1
  2. package/dist/commands/cloud.d.ts +25 -0
  3. package/dist/commands/cloud.d.ts.map +1 -0
  4. package/dist/commands/cloud.js +194 -0
  5. package/dist/commands/cloud.js.map +1 -0
  6. package/dist/commands/create.d.ts.map +1 -1
  7. package/dist/commands/create.js +8 -0
  8. package/dist/commands/create.js.map +1 -1
  9. package/dist/commands/info.js +1 -1
  10. package/dist/lib/agent-skill.d.ts +2 -2
  11. package/dist/lib/agent-skill.js +1 -1
  12. package/dist/lib/agent-skill.js.map +1 -1
  13. package/dist/lib/cloud-abort.d.ts +2 -0
  14. package/dist/lib/cloud-abort.d.ts.map +1 -0
  15. package/dist/lib/cloud-abort.js +13 -0
  16. package/dist/lib/cloud-abort.js.map +1 -0
  17. package/dist/lib/cloud-auth.d.ts +28 -0
  18. package/dist/lib/cloud-auth.d.ts.map +1 -0
  19. package/dist/lib/cloud-auth.js +169 -0
  20. package/dist/lib/cloud-auth.js.map +1 -0
  21. package/dist/lib/cloud-config.d.ts +21 -0
  22. package/dist/lib/cloud-config.d.ts.map +1 -0
  23. package/dist/lib/cloud-config.js +76 -0
  24. package/dist/lib/cloud-config.js.map +1 -0
  25. package/dist/lib/cloud-provision.d.ts +20 -0
  26. package/dist/lib/cloud-provision.d.ts.map +1 -0
  27. package/dist/lib/cloud-provision.js +55 -0
  28. package/dist/lib/cloud-provision.js.map +1 -0
  29. package/dist/lib/cloud-setup-login.d.ts +9 -0
  30. package/dist/lib/cloud-setup-login.d.ts.map +1 -0
  31. package/dist/lib/cloud-setup-login.js +137 -0
  32. package/dist/lib/cloud-setup-login.js.map +1 -0
  33. package/dist/lib/cloud-url.d.ts +2 -0
  34. package/dist/lib/cloud-url.d.ts.map +1 -0
  35. package/dist/lib/cloud-url.js +14 -0
  36. package/dist/lib/cloud-url.js.map +1 -0
  37. package/dist/lib/transform.js +1 -1
  38. package/dist/program.d.ts.map +1 -1
  39. package/dist/program.js +2 -1
  40. package/dist/program.js.map +1 -1
  41. package/package.json +17 -13
  42. package/src/commands/cloud.test.ts +238 -0
  43. package/src/commands/cloud.ts +455 -0
  44. package/src/commands/create.ts +8 -0
  45. package/src/lib/agent-skill.ts +1 -1
  46. package/src/lib/cloud-abort.ts +12 -0
  47. package/src/lib/cloud-auth.test.ts +500 -0
  48. package/src/lib/cloud-auth.ts +273 -0
  49. package/src/lib/cloud-config.test.ts +82 -0
  50. package/src/lib/cloud-config.ts +138 -0
  51. package/src/lib/cloud-provision.test.ts +132 -0
  52. package/src/lib/cloud-provision.ts +93 -0
  53. package/src/lib/cloud-setup-login.test.ts +361 -0
  54. package/src/lib/cloud-setup-login.ts +240 -0
  55. package/src/lib/cloud-url.ts +19 -0
  56. package/src/program.ts +2 -0
  57. package/templates/cloud-harness/.assistant-ui/template.json +3 -0
  58. package/templates/cloud-harness/.env.example +1 -0
  59. package/templates/cloud-harness/.gitignore +42 -0
  60. package/templates/cloud-harness/README.md +28 -0
  61. package/templates/cloud-harness/app/api/chat/route.ts +13 -0
  62. package/templates/cloud-harness/app/api/credential/route.ts +14 -0
  63. package/templates/cloud-harness/app/assistant.tsx +83 -0
  64. package/templates/cloud-harness/app/favicon.ico +0 -0
  65. package/templates/cloud-harness/app/globals.css +139 -0
  66. package/templates/cloud-harness/app/layout.tsx +37 -0
  67. package/templates/cloud-harness/app/page.tsx +5 -0
  68. package/templates/cloud-harness/components/assistant-ui/elements/attachment.aui.tsx +279 -0
  69. package/templates/cloud-harness/components/assistant-ui/elements/markdown-text.tsx +238 -0
  70. package/templates/cloud-harness/components/assistant-ui/elements/thread.aui.tsx +521 -0
  71. package/templates/cloud-harness/components/assistant-ui/elements/tool-fallback.aui.tsx +1153 -0
  72. package/templates/cloud-harness/components/assistant-ui/elements/tooltip-icon-button.tsx +49 -0
  73. package/templates/cloud-harness/components/ui/avatar.tsx +109 -0
  74. package/templates/cloud-harness/components/ui/button.tsx +58 -0
  75. package/templates/cloud-harness/components/ui/collapsible.tsx +21 -0
  76. package/templates/cloud-harness/components/ui/dialog.tsx +159 -0
  77. package/templates/cloud-harness/components/ui/textarea.tsx +18 -0
  78. package/templates/cloud-harness/components/ui/tooltip.tsx +69 -0
  79. package/templates/cloud-harness/components.json +24 -0
  80. package/templates/cloud-harness/hooks/use-attachment-src.ts +46 -0
  81. package/templates/cloud-harness/hooks/use-copy-to-clipboard.ts +54 -0
  82. package/templates/cloud-harness/lib/utils.ts +1 -0
  83. package/templates/cloud-harness/next.config.ts +6 -0
  84. package/templates/cloud-harness/package.json +51 -0
  85. package/templates/cloud-harness/postcss.config.mjs +5 -0
  86. package/templates/cloud-harness/tsconfig.json +33 -0
  87. package/templates/cloud-harness/vendor/README.md +13 -0
  88. package/templates/cloud-harness/vendor/assistant-ui-react-harness-sdk-0.0.2.tgz +0 -0
  89. package/templates/cloud-harness/vendor/harness-sdk-0.3.1.tgz +0 -0
  90. package/templates/cloud-harness/vendor/provenance.json +38 -0
  91. package/templates/cloud-harness/vendor/statewire-0.19.3.tgz +0 -0
  92. /package/{LICENSE → templates/cloud-harness/vendor/LICENSE} +0 -0
@@ -0,0 +1,137 @@
1
+ import { abortable } from "./cloud-abort.js";
2
+ import { validateCloudUrl } from "./cloud-url.js";
3
+ import { randomUUID } from "node:crypto";
4
+ import { StatewireClient, StatewireHttp } from "statewire";
5
+ //#region src/lib/cloud-setup-login.ts
6
+ const validInputs = (inputs) => Array.isArray(inputs) && inputs.every((input) => input !== null && typeof input === "object" && typeof input.id === "string" && typeof input.status === "string");
7
+ const setupLoginUrl = (authorization, issuer, attempt = randomUUID()) => {
8
+ const url = new URL(authorization.verificationUriComplete);
9
+ const issuerOrigin = new URL(issuer).origin;
10
+ if (!["https://accounts.assistant-ui.com", "https://accounts.aui.dev"].includes(issuerOrigin) || url.origin !== issuerOrigin || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(attempt) || !/^[A-Z0-9-]{4,32}$/.test(authorization.userCode) || url.pathname !== "/device" || url.username || url.password || url.hash || url.searchParams.get("user_code") !== authorization.userCode || url.searchParams.getAll("user_code").length !== 1 || !Number.isFinite(authorization.expiresAt) || authorization.expiresAt <= Date.now() || authorization.expiresAt > Date.now() + 6e5) throw new Error("Accounts returned an invalid device approval link.");
11
+ const publicUrl = new URL("/device", url.origin);
12
+ publicUrl.searchParams.set("user_code", authorization.userCode);
13
+ publicUrl.searchParams.set("setup_attempt", attempt);
14
+ publicUrl.searchParams.set("setup_expires", String(authorization.expiresAt));
15
+ return publicUrl.href;
16
+ };
17
+ const connectCloudLoginSetup = async (url) => {
18
+ const target = validateCloudUrl(url);
19
+ const controller = new AbortController();
20
+ const client = new StatewireClient({
21
+ transport: StatewireHttp({ url: target }),
22
+ onError: (error) => controller.abort(error)
23
+ });
24
+ let unsubscribe = () => {};
25
+ let inputId;
26
+ let sessionId;
27
+ let pendingHref;
28
+ let pendingAsk;
29
+ let terminalResult;
30
+ let completion;
31
+ let disposed = false;
32
+ const reconcile = () => {
33
+ const state = client.state;
34
+ if (state && !validInputs(state.inputs)) {
35
+ controller.abort(/* @__PURE__ */ new Error("The setup returned invalid inputs."));
36
+ return Promise.resolve();
37
+ }
38
+ if (disposed || inputId === void 0 || terminalResult === void 0 || state?.id !== sessionId || !Array.isArray(state.inputs) || !["planning", "installing"].includes(state.status) || ["dismissed", "answered"].includes(state.inputs.find((input) => input.id === inputId)?.status ?? "")) return Promise.resolve();
39
+ completion ??= abortable(client.commands["checkout/answer"]({
40
+ inputId,
41
+ answer: terminalResult
42
+ }), AbortSignal.timeout(1e4));
43
+ return completion;
44
+ };
45
+ const checkSession = () => {
46
+ const next = client.state;
47
+ if (next && !validInputs(next.inputs)) {
48
+ controller.abort(/* @__PURE__ */ new Error("The setup returned invalid inputs."));
49
+ return;
50
+ }
51
+ if (next?.id === sessionId && Array.isArray(next.inputs) && pendingHref) inputId ??= next.inputs.find((input) => input.preset === "assistant-ui-cli-login" && input.help?.href === pendingHref)?.id;
52
+ if (next?.version !== 2 || next.id !== sessionId || !Array.isArray(next.inputs) || !["planning", "installing"].includes(next.status) || inputId !== void 0 && next.inputs.find((input) => input.id === inputId)?.status === "dismissed") controller.abort(/* @__PURE__ */ new Error("The setup login was cancelled."));
53
+ if (client.connection.status === "stopped") controller.abort(/* @__PURE__ */ new Error("The setup connection stopped."));
54
+ reconcile().catch(() => {});
55
+ };
56
+ const dispose = async () => {
57
+ try {
58
+ if (pendingAsk && terminalResult) {
59
+ await abortable(pendingAsk, AbortSignal.timeout(1e4)).catch(() => {});
60
+ await reconcile().catch(() => {});
61
+ }
62
+ } finally {
63
+ disposed = true;
64
+ unsubscribe();
65
+ client.dispose();
66
+ }
67
+ };
68
+ try {
69
+ let resolveReady;
70
+ const ready = new Promise((resolve) => {
71
+ resolveReady = resolve;
72
+ });
73
+ unsubscribe = client.subscribe(() => {
74
+ if (client.state !== void 0) resolveReady();
75
+ if (client.connection.status === "stopped") controller.abort(/* @__PURE__ */ new Error("The setup connection stopped."));
76
+ });
77
+ if (client.state !== void 0) resolveReady();
78
+ if (client.connection.status === "stopped") controller.abort(/* @__PURE__ */ new Error("The setup connection stopped."));
79
+ await abortable(ready, AbortSignal.any([controller.signal, AbortSignal.timeout(1e4)]));
80
+ const state = client.state;
81
+ if (state?.version !== 2 || !state.id || !validInputs(state.inputs) || !["planning", "installing"].includes(state.status)) throw new Error("Start the setup in the wizard before signing in the CLI.");
82
+ sessionId = state.id;
83
+ unsubscribe();
84
+ unsubscribe = client.subscribe(checkSession);
85
+ checkSession();
86
+ } catch (error) {
87
+ await dispose();
88
+ throw error;
89
+ }
90
+ const command = (run) => {
91
+ controller.signal.throwIfAborted();
92
+ return abortable(run(), AbortSignal.any([controller.signal, AbortSignal.timeout(1e4)]));
93
+ };
94
+ return {
95
+ signal: controller.signal,
96
+ async publish(authorization, issuer) {
97
+ controller.signal.throwIfAborted();
98
+ const href = setupLoginUrl(authorization, issuer);
99
+ pendingHref = href;
100
+ pendingAsk = client.commands["agent/ask"]({
101
+ kind: "text",
102
+ preset: "assistant-ui-cli-login",
103
+ prompt: "Sign in to Assistant Cloud",
104
+ help: {
105
+ summary: "Authorize the CLI in your browser, then return to this setup.",
106
+ href
107
+ }
108
+ }).then(async (result) => {
109
+ if (!disposed && client.state?.id === sessionId) {
110
+ inputId = result.inputId;
111
+ checkSession();
112
+ await reconcile().catch(() => {});
113
+ }
114
+ return result;
115
+ });
116
+ try {
117
+ await command(() => pendingAsk);
118
+ checkSession();
119
+ controller.signal.throwIfAborted();
120
+ } catch (error) {
121
+ terminalResult = controller.signal.aborted ? "cancelled" : "failed";
122
+ await reconcile().catch(() => {});
123
+ throw error;
124
+ }
125
+ return href;
126
+ },
127
+ async complete(result) {
128
+ terminalResult = result;
129
+ await reconcile();
130
+ },
131
+ dispose
132
+ };
133
+ };
134
+ //#endregion
135
+ export { connectCloudLoginSetup, setupLoginUrl };
136
+
137
+ //# sourceMappingURL=cloud-setup-login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-setup-login.js","names":[],"sources":["../../src/lib/cloud-setup-login.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport type { DeviceAuthorization } from \"aui-auth/device\";\nimport { StatewireClient, StatewireHttp } from \"statewire\";\nimport { abortable } from \"./cloud-abort\";\nimport { validateCloudUrl } from \"./cloud-url\";\n\ntype SetupState = {\n version: number;\n id: string | null;\n status: string;\n inputs: {\n id: string;\n status: string;\n preset?: string;\n help?: { href?: string };\n }[];\n};\n\ntype SetupCommands = {\n \"agent/ask\": (input: {\n kind: \"text\";\n preset: string;\n prompt: string;\n help: { summary: string; href: string };\n }) => { inputId: string };\n \"checkout/answer\": (answer: { inputId: string; answer: string }) => void;\n};\n\nconst validInputs = (inputs: unknown): inputs is SetupState[\"inputs\"] =>\n Array.isArray(inputs) &&\n inputs.every(\n (input) =>\n input !== null &&\n typeof input === \"object\" &&\n typeof input.id === \"string\" &&\n typeof input.status === \"string\",\n );\n\nexport const setupLoginUrl = (\n authorization: DeviceAuthorization,\n issuer: string,\n attempt: string = randomUUID(),\n) => {\n const url = new URL(authorization.verificationUriComplete);\n const issuerOrigin = new URL(issuer).origin;\n if (\n ![\"https://accounts.assistant-ui.com\", \"https://accounts.aui.dev\"].includes(\n issuerOrigin,\n ) ||\n url.origin !== issuerOrigin ||\n !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(\n attempt,\n ) ||\n !/^[A-Z0-9-]{4,32}$/.test(authorization.userCode) ||\n url.pathname !== \"/device\" ||\n url.username ||\n url.password ||\n url.hash ||\n url.searchParams.get(\"user_code\") !== authorization.userCode ||\n url.searchParams.getAll(\"user_code\").length !== 1 ||\n !Number.isFinite(authorization.expiresAt) ||\n authorization.expiresAt <= Date.now() ||\n authorization.expiresAt > Date.now() + 10 * 60 * 1000\n ) {\n throw new Error(\"Accounts returned an invalid device approval link.\");\n }\n const publicUrl = new URL(\"/device\", url.origin);\n publicUrl.searchParams.set(\"user_code\", authorization.userCode);\n publicUrl.searchParams.set(\"setup_attempt\", attempt);\n publicUrl.searchParams.set(\"setup_expires\", String(authorization.expiresAt));\n return publicUrl.href;\n};\n\nexport const connectCloudLoginSetup = async (url: string) => {\n const target = validateCloudUrl(url);\n const controller = new AbortController();\n const client = new StatewireClient<SetupState | undefined, SetupCommands>({\n transport: StatewireHttp({ url: target }),\n onError: (error) => controller.abort(error),\n });\n let unsubscribe = () => {};\n let inputId: string | undefined;\n let sessionId: string;\n let pendingHref: string | undefined;\n let pendingAsk: Promise<{ inputId: string }> | undefined;\n let terminalResult: \"signed-in\" | \"cancelled\" | \"failed\" | undefined;\n let completion: Promise<void> | undefined;\n let disposed = false;\n const reconcile = () => {\n const state = client.state;\n if (state && !validInputs(state.inputs)) {\n controller.abort(new Error(\"The setup returned invalid inputs.\"));\n return Promise.resolve();\n }\n if (\n disposed ||\n inputId === undefined ||\n terminalResult === undefined ||\n state?.id !== sessionId ||\n !Array.isArray(state.inputs) ||\n ![\"planning\", \"installing\"].includes(state.status) ||\n [\"dismissed\", \"answered\"].includes(\n state.inputs.find((input) => input.id === inputId)?.status ?? \"\",\n )\n )\n return Promise.resolve();\n completion ??= abortable(\n client.commands[\"checkout/answer\"]({ inputId, answer: terminalResult }),\n AbortSignal.timeout(10_000),\n );\n return completion;\n };\n const checkSession = () => {\n const next = client.state;\n if (next && !validInputs(next.inputs)) {\n controller.abort(new Error(\"The setup returned invalid inputs.\"));\n return;\n }\n if (next?.id === sessionId && Array.isArray(next.inputs) && pendingHref) {\n inputId ??= next.inputs.find(\n (input) =>\n input.preset === \"assistant-ui-cli-login\" &&\n input.help?.href === pendingHref,\n )?.id;\n }\n if (\n next?.version !== 2 ||\n next.id !== sessionId ||\n !Array.isArray(next.inputs) ||\n ![\"planning\", \"installing\"].includes(next.status) ||\n (inputId !== undefined &&\n next.inputs.find((input) => input.id === inputId)?.status ===\n \"dismissed\")\n ) {\n controller.abort(new Error(\"The setup login was cancelled.\"));\n }\n if (client.connection.status === \"stopped\")\n controller.abort(new Error(\"The setup connection stopped.\"));\n void reconcile().catch(() => {});\n };\n const dispose = async () => {\n try {\n if (pendingAsk && terminalResult) {\n await abortable(pendingAsk, AbortSignal.timeout(10_000)).catch(\n () => {},\n );\n await reconcile().catch(() => {});\n }\n } finally {\n disposed = true;\n unsubscribe();\n client.dispose();\n }\n };\n try {\n let resolveReady!: () => void;\n const ready = new Promise<void>((resolve) => {\n resolveReady = resolve;\n });\n unsubscribe = client.subscribe(() => {\n if (client.state !== undefined) resolveReady();\n if (client.connection.status === \"stopped\") {\n controller.abort(new Error(\"The setup connection stopped.\"));\n }\n });\n if (client.state !== undefined) resolveReady();\n if (client.connection.status === \"stopped\") {\n controller.abort(new Error(\"The setup connection stopped.\"));\n }\n await abortable(\n ready,\n AbortSignal.any([controller.signal, AbortSignal.timeout(10_000)]),\n );\n const state = client.state!;\n if (\n state?.version !== 2 ||\n !state.id ||\n !validInputs(state.inputs) ||\n ![\"planning\", \"installing\"].includes(state.status)\n ) {\n throw new Error(\n \"Start the setup in the wizard before signing in the CLI.\",\n );\n }\n sessionId = state.id;\n unsubscribe();\n unsubscribe = client.subscribe(checkSession);\n checkSession();\n } catch (error) {\n await dispose();\n throw error;\n }\n const command = <T>(run: () => Promise<T>) => {\n controller.signal.throwIfAborted();\n return abortable(\n run(),\n AbortSignal.any([controller.signal, AbortSignal.timeout(10_000)]),\n );\n };\n return {\n signal: controller.signal,\n async publish(authorization: DeviceAuthorization, issuer: string) {\n controller.signal.throwIfAborted();\n const href = setupLoginUrl(authorization, issuer);\n pendingHref = href;\n pendingAsk = client.commands[\"agent/ask\"]({\n kind: \"text\",\n preset: \"assistant-ui-cli-login\",\n prompt: \"Sign in to Assistant Cloud\",\n help: {\n summary:\n \"Authorize the CLI in your browser, then return to this setup.\",\n href,\n },\n }).then(async (result) => {\n if (!disposed && client.state?.id === sessionId) {\n inputId = result.inputId;\n checkSession();\n await reconcile().catch(() => {});\n }\n return result;\n });\n try {\n await command(() => pendingAsk!);\n checkSession();\n controller.signal.throwIfAborted();\n } catch (error) {\n terminalResult = controller.signal.aborted ? \"cancelled\" : \"failed\";\n await reconcile().catch(() => {});\n throw error;\n }\n return href;\n },\n async complete(result: \"signed-in\" | \"cancelled\" | \"failed\") {\n terminalResult = result;\n await reconcile();\n },\n dispose,\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare const validateCloudUrl: (value: string) => string;
2
+ //# sourceMappingURL=cloud-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-url.d.ts","sourceRoot":"","sources":["../../src/lib/cloud-url.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAAM,KAAG,MAkBhD,CAAC"}
@@ -0,0 +1,14 @@
1
+ //#region src/lib/cloud-url.ts
2
+ const validateCloudUrl = (value) => {
3
+ const url = new URL(value);
4
+ if (url.username || url.password || url.search || url.hash || url.protocol !== "https:" && !(url.protocol === "http:" && [
5
+ "localhost",
6
+ "127.0.0.1",
7
+ "[::1]"
8
+ ].includes(url.hostname))) throw new Error("Cloud, accounts and setup URLs must use HTTPS or HTTP localhost for development.");
9
+ return value.replace(/\/+$/, "");
10
+ };
11
+ //#endregion
12
+ export { validateCloudUrl };
13
+
14
+ //# sourceMappingURL=cloud-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cloud-url.js","names":[],"sources":["../../src/lib/cloud-url.ts"],"sourcesContent":["export const validateCloudUrl = (value: string): string => {\n const url = new URL(value);\n if (\n url.username ||\n url.password ||\n url.search ||\n url.hash ||\n (url.protocol !== \"https:\" &&\n !(\n url.protocol === \"http:\" &&\n [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(url.hostname)\n ))\n ) {\n throw new Error(\n \"Cloud, accounts and setup URLs must use HTTPS or HTTP localhost for development.\",\n );\n }\n return value.replace(/\\/+$/, \"\");\n};\n"],"mappings":";AAAA,MAAa,oBAAoB,UAA0B;CACzD,MAAM,MAAM,IAAI,IAAI,KAAK;CACzB,IACE,IAAI,YACJ,IAAI,YACJ,IAAI,UACJ,IAAI,QACH,IAAI,aAAa,YAChB,EACE,IAAI,aAAa,WACjB;EAAC;EAAa;EAAa;CAAO,CAAC,CAAC,SAAS,IAAI,QAAQ,IAG7D,MAAM,IAAI,MACR,kFACF;CAEF,OAAO,MAAM,QAAQ,QAAQ,EAAE;AACjC"}
@@ -2,8 +2,8 @@ import { SpawnExitError, SpawnSignalError, runSpawnCapture } from "./run-spawn.j
2
2
  import * as fs$1 from "node:fs";
3
3
  import path from "node:path";
4
4
  import { sync } from "glob";
5
- import debug from "debug";
6
5
  import { fileURLToPath } from "node:url";
6
+ import debug from "debug";
7
7
  //#region src/lib/transform.ts
8
8
  const log = debug("codemod:transform");
9
9
  const __filename = fileURLToPath(import.meta.url);
@@ -1 +1 @@
1
- {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,YAAY,YAc3B"}
1
+ {"version":3,"file":"program.d.ts","sourceRoot":"","sources":["../src/program.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAYpC,wBAAgB,YAAY,YAe3B"}
package/dist/program.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { add } from "./commands/add.js";
2
2
  import { agent } from "./commands/agent.js";
3
3
  import { create } from "./commands/create.js";
4
+ import { cloud } from "./commands/cloud.js";
4
5
  import { doctor } from "./commands/doctor.js";
5
6
  import { info } from "./commands/info.js";
6
7
  import { init } from "./commands/init.js";
@@ -10,7 +11,7 @@ import { codemodCommand, upgradeCommand } from "./commands/upgrade.js";
10
11
  import { Command } from "commander";
11
12
  //#region src/program.ts
12
13
  function buildProgram() {
13
- return new Command().name("assistant-ui").description("add components and dependencies to your project").addCommand(add).addCommand(create).addCommand(init).addCommand(mcp).addCommand(codemodCommand).addCommand(upgradeCommand).addCommand(update).addCommand(agent).addCommand(info).addCommand(doctor);
14
+ return new Command().name("assistant-ui").description("add components and dependencies to your project").addCommand(add).addCommand(create).addCommand(init).addCommand(mcp).addCommand(codemodCommand).addCommand(upgradeCommand).addCommand(update).addCommand(agent).addCommand(cloud).addCommand(info).addCommand(doctor);
14
15
  }
15
16
  //#endregion
16
17
  export { buildProgram };
@@ -1 +1 @@
1
- {"version":3,"file":"program.js","names":[],"sources":["../src/program.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { create } from \"./commands/create\";\nimport { add } from \"./commands/add\";\nimport { codemodCommand, upgradeCommand } from \"./commands/upgrade\";\nimport { init } from \"./commands/init\";\nimport { update } from \"./commands/update\";\nimport { mcp } from \"./commands/mcp\";\nimport { agent } from \"./commands/agent\";\nimport { info } from \"./commands/info\";\nimport { doctor } from \"./commands/doctor\";\n\nexport function buildProgram() {\n return new Command()\n .name(\"assistant-ui\")\n .description(\"add components and dependencies to your project\")\n .addCommand(add)\n .addCommand(create)\n .addCommand(init)\n .addCommand(mcp)\n .addCommand(codemodCommand)\n .addCommand(upgradeCommand)\n .addCommand(update)\n .addCommand(agent)\n .addCommand(info)\n .addCommand(doctor);\n}\n"],"mappings":";;;;;;;;;;;AAWA,SAAgB,eAAe;CAC7B,OAAO,IAAI,QAAQ,CAAC,CACjB,KAAK,cAAc,CAAC,CACpB,YAAY,iDAAiD,CAAC,CAC9D,WAAW,GAAG,CAAC,CACf,WAAW,MAAM,CAAC,CAClB,WAAW,IAAI,CAAC,CAChB,WAAW,GAAG,CAAC,CACf,WAAW,cAAc,CAAC,CAC1B,WAAW,cAAc,CAAC,CAC1B,WAAW,MAAM,CAAC,CAClB,WAAW,KAAK,CAAC,CACjB,WAAW,IAAI,CAAC,CAChB,WAAW,MAAM;AACtB"}
1
+ {"version":3,"file":"program.js","names":[],"sources":["../src/program.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { create } from \"./commands/create\";\nimport { add } from \"./commands/add\";\nimport { codemodCommand, upgradeCommand } from \"./commands/upgrade\";\nimport { init } from \"./commands/init\";\nimport { update } from \"./commands/update\";\nimport { mcp } from \"./commands/mcp\";\nimport { agent } from \"./commands/agent\";\nimport { info } from \"./commands/info\";\nimport { doctor } from \"./commands/doctor\";\nimport { cloud } from \"./commands/cloud\";\n\nexport function buildProgram() {\n return new Command()\n .name(\"assistant-ui\")\n .description(\"add components and dependencies to your project\")\n .addCommand(add)\n .addCommand(create)\n .addCommand(init)\n .addCommand(mcp)\n .addCommand(codemodCommand)\n .addCommand(upgradeCommand)\n .addCommand(update)\n .addCommand(agent)\n .addCommand(cloud)\n .addCommand(info)\n .addCommand(doctor);\n}\n"],"mappings":";;;;;;;;;;;;AAYA,SAAgB,eAAe;CAC7B,OAAO,IAAI,QAAQ,CAAC,CACjB,KAAK,cAAc,CAAC,CACpB,YAAY,iDAAiD,CAAC,CAC9D,WAAW,GAAG,CAAC,CACf,WAAW,MAAM,CAAC,CAClB,WAAW,IAAI,CAAC,CAChB,WAAW,GAAG,CAAC,CACf,WAAW,cAAc,CAAC,CAC1B,WAAW,cAAc,CAAC,CAC1B,WAAW,MAAM,CAAC,CAClB,WAAW,KAAK,CAAC,CACjB,WAAW,KAAK,CAAC,CACjB,WAAW,IAAI,CAAC,CAChB,WAAW,MAAM;AACtB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistant-ui",
3
- "version": "0.0.119",
3
+ "version": "0.0.121",
4
4
  "description": "CLI for assistant-ui",
5
5
  "keywords": [
6
6
  "cli",
@@ -21,13 +21,15 @@
21
21
  "bin",
22
22
  "dist",
23
23
  "src",
24
- "README.md"
24
+ "README.md",
25
+ "templates/cloud-harness"
25
26
  ],
26
27
  "sideEffects": false,
27
28
  "dependencies": {
28
- "@assistant-ui/agent-launcher": "^0.1.16",
29
+ "@assistant-ui/agent-launcher": "^0.1.17",
29
30
  "@clack/prompts": "^1.8.1",
30
- "chalk": "^6.0.0",
31
+ "aui-auth": "^0.0.11",
32
+ "chalk": "^6.0.1",
31
33
  "cli-progress": "^3.12.0",
32
34
  "commander": "^15.0.0",
33
35
  "cross-spawn": "^7.0.6",
@@ -37,18 +39,20 @@
37
39
  "glob": "^13.0.6",
38
40
  "jscodeshift": "^17.4.0",
39
41
  "jsonc-parser": "^3.3.1",
40
- "semver": "^7.8.5"
42
+ "react": "^19.3.0",
43
+ "semver": "^7.8.5",
44
+ "statewire": "^0.19.4"
41
45
  },
42
46
  "devDependencies": {
43
- "@assistant-ui/x-buildutils": "0.0.29",
47
+ "@assistant-ui/x-buildutils": "0.0.30",
44
48
  "@types/cli-progress": "^3.11.6",
45
49
  "@types/cross-spawn": "^6.0.6",
46
50
  "@types/debug": "^4.1.13",
47
51
  "@types/jscodeshift": "^17.3.0",
48
- "@types/node": "^26.5.1",
52
+ "@types/node": "^26.6.3",
49
53
  "@types/semver": "^7.8.0",
50
- "@vitest/coverage-v8": "^5.0.1",
51
- "vitest": "^5.0.1"
54
+ "@vitest/coverage-v8": "^5.0.3",
55
+ "vitest": "^5.0.3"
52
56
  },
53
57
  "publishConfig": {
54
58
  "access": "public",
@@ -64,10 +68,10 @@
64
68
  "url": "https://github.com/assistant-ui/assistant-ui/issues"
65
69
  },
66
70
  "scripts": {
67
- "build": "aui-build",
68
- "test": "vitest run",
71
+ "build": "aui-build && node scripts/copy-cloud-harness-template.mjs",
72
+ "test": "vitest run && node --test scripts/copy-cloud-harness-template.test.mjs",
73
+ "test:coverage": "vitest run --coverage --coverage.reporter=text --coverage.reporter=html --coverage.reporter=json-summary",
69
74
  "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
70
- "test:watch": "vitest",
71
- "test:coverage": "vitest run --coverage"
75
+ "test:watch": "vitest"
72
76
  }
73
77
  }
@@ -0,0 +1,238 @@
1
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+ import { setupCloud, cloud } from "./cloud";
6
+
7
+ const {
8
+ fetchOrganizations,
9
+ fetchProjects,
10
+ createProject,
11
+ cloudAccessToken,
12
+ provisionCloudHarness,
13
+ scaffold,
14
+ select,
15
+ password,
16
+ } = vi.hoisted(() => ({
17
+ fetchOrganizations: vi.fn(),
18
+ fetchProjects: vi.fn(),
19
+ createProject: vi.fn(),
20
+ cloudAccessToken: vi.fn(),
21
+ provisionCloudHarness: vi.fn(),
22
+ scaffold: vi.fn(),
23
+ select: vi.fn(),
24
+ password: vi.fn(),
25
+ }));
26
+
27
+ vi.mock("aui-auth/accounts", async (importOriginal) => ({
28
+ ...(await importOriginal<typeof import("aui-auth/accounts")>()),
29
+ fetchOrganizations,
30
+ fetchProjects,
31
+ createProject,
32
+ }));
33
+ vi.mock("../lib/cloud-auth", async (importOriginal) => ({
34
+ ...(await importOriginal<typeof import("../lib/cloud-auth")>()),
35
+ cloudAccessToken,
36
+ }));
37
+ vi.mock("../lib/cloud-provision", async (importOriginal) => ({
38
+ ...(await importOriginal<typeof import("../lib/cloud-provision")>()),
39
+ provisionCloudHarness,
40
+ }));
41
+ vi.mock("./create", async (importOriginal) => ({
42
+ ...(await importOriginal<typeof import("./create")>()),
43
+ create: { parseAsync: scaffold },
44
+ }));
45
+ vi.mock("@clack/prompts", async (importOriginal) => ({
46
+ ...(await importOriginal<typeof import("@clack/prompts")>()),
47
+ select,
48
+ password,
49
+ }));
50
+
51
+ const directories: string[] = [];
52
+ afterEach(async () => {
53
+ await Promise.all(
54
+ directories
55
+ .splice(0)
56
+ .map((directory) => rm(directory, { recursive: true, force: true })),
57
+ );
58
+ });
59
+
60
+ const fixture = async () => {
61
+ const directory = await mkdtemp(
62
+ path.join(os.tmpdir(), "assistant-ui-cloud-command-"),
63
+ );
64
+ directories.push(directory);
65
+ cloudAccessToken.mockResolvedValue("oauth-access");
66
+ fetchOrganizations.mockResolvedValue([
67
+ { id: "org-1", slug: "team", name: "Team" },
68
+ ]);
69
+ fetchProjects.mockResolvedValue([
70
+ { id: "project-1", slug: "chat", name: "Team chat" },
71
+ ]);
72
+ provisionCloudHarness.mockResolvedValue({
73
+ project_id: "project-1",
74
+ workspace_id: "shared-workspace",
75
+ harness_id: "harness-1",
76
+ harness_origin: "https://harness.example.com",
77
+ api_origin: "https://project.example.com",
78
+ api_key: "cloud-server-key",
79
+ });
80
+ scaffold.mockResolvedValue(undefined);
81
+ return directory;
82
+ };
83
+
84
+ const options = {
85
+ backendUrl: "http://localhost:3000/api/chat",
86
+ apiUrl: "https://cloud.example.com",
87
+ yes: true,
88
+ skipInstall: true,
89
+ accessCode: "hackathon-code",
90
+ };
91
+
92
+ describe("cloud setup", () => {
93
+ it("selects an existing project and scaffolds a shared harness with server-only credentials", async () => {
94
+ const directory = await fixture();
95
+ await setupCloud(directory, { ...options, org: "team", project: "chat" });
96
+ expect(createProject).not.toHaveBeenCalled();
97
+ expect(scaffold).toHaveBeenCalledWith(
98
+ expect.arrayContaining([
99
+ "cloud-harness",
100
+ "--skip-install",
101
+ "--no-skills",
102
+ ]),
103
+ { from: "user" },
104
+ );
105
+ expect(provisionCloudHarness).toHaveBeenCalledWith(
106
+ options.apiUrl,
107
+ "oauth-access",
108
+ expect.objectContaining({
109
+ org_id: "org-1",
110
+ project_id: "project-1",
111
+ access_code: "hackathon-code",
112
+ backend_urls: [],
113
+ allow_localhost: true,
114
+ }),
115
+ );
116
+ const env = await readFile(path.join(directory, ".env.local"), "utf8");
117
+ expect(env).toContain('ASSISTANT_API_KEY="cloud-server-key"');
118
+ expect(env).not.toContain("NEXT_PUBLIC_ASSISTANT_API_KEY");
119
+ expect(
120
+ await readFile(
121
+ path.join(directory, ".assistant-ui", "cloud.json"),
122
+ "utf8",
123
+ ),
124
+ ).not.toContain("cloud-server-key");
125
+ });
126
+
127
+ it("does not provision again or overwrite customizations when setup is repeated", async () => {
128
+ const directory = await fixture();
129
+ await setupCloud(directory, options);
130
+ const envFile = path.join(directory, ".env.local");
131
+ const custom = `${(await readFile(envFile, "utf8")).replace(/^ASSISTANT_API_KEY=/m, "export ASSISTANT_API_KEY=")}OPENAI_API_KEY=custom-key\nCUSTOM_SETTING=keep-me\n`;
132
+ await writeFile(envFile, custom);
133
+ const before = provisionCloudHarness.mock.calls.length;
134
+ await setupCloud(directory, options);
135
+ expect(provisionCloudHarness.mock.calls.length).toBe(before);
136
+ expect(await readFile(envFile, "utf8")).toBe(custom);
137
+ });
138
+
139
+ it("requires an explicit project when noninteractive users have several choices", async () => {
140
+ const directory = await fixture();
141
+ fetchProjects.mockResolvedValue([
142
+ { id: "one", slug: "one", name: "One" },
143
+ { id: "two", slug: "two", name: "Two" },
144
+ ]);
145
+ await expect(setupCloud(directory, options)).rejects.toThrow("--project");
146
+ });
147
+
148
+ it("reuses an organization created by an interrupted setup", async () => {
149
+ const directory = await fixture();
150
+ const before = fetchProjects.mock.calls.length;
151
+ await setupCloud(directory, {
152
+ ...options,
153
+ newOrg: "team",
154
+ project: "chat",
155
+ });
156
+ expect(fetchProjects.mock.calls.slice(before)).toEqual([
157
+ [expect.anything(), "oauth-access", { id: "org-1" }],
158
+ ]);
159
+ expect(fetchProjects).toHaveBeenCalledWith(
160
+ expect.anything(),
161
+ "oauth-access",
162
+ {
163
+ id: "org-1",
164
+ },
165
+ );
166
+ expect(provisionCloudHarness).toHaveBeenCalledWith(
167
+ options.apiUrl,
168
+ "oauth-access",
169
+ expect.objectContaining({ org_id: "org-1" }),
170
+ );
171
+ });
172
+
173
+ it("rejects a changed backend before silently reusing configured resources", async () => {
174
+ const directory = await fixture();
175
+ await setupCloud(directory, options);
176
+ const before = provisionCloudHarness.mock.calls.length;
177
+ await expect(
178
+ setupCloud(directory, {
179
+ ...options,
180
+ backendUrl: "https://app.example.com/api/chat",
181
+ }),
182
+ ).rejects.toThrow("backend allowlist in the Cloud dashboard");
183
+ expect(provisionCloudHarness.mock.calls.length).toBe(before);
184
+ });
185
+
186
+ it("rejects conflicting selection flags before logging in", async () => {
187
+ const directory = await fixture();
188
+ const before = cloudAccessToken.mock.calls.length;
189
+ await expect(
190
+ setupCloud(directory, { ...options, org: "team", newOrg: "other" }),
191
+ ).rejects.toThrow("Choose --org or --new-org");
192
+ expect(cloudAccessToken.mock.calls.length).toBe(before);
193
+ });
194
+
195
+ it("rejects URL credentials and invalid organization slugs before logging in", async () => {
196
+ const directory = await fixture();
197
+ const before = cloudAccessToken.mock.calls.length;
198
+ await expect(
199
+ setupCloud(directory, {
200
+ ...options,
201
+ backendUrl: "https://user:secret@app.example.com/api/chat",
202
+ }),
203
+ ).rejects.toThrow("credentials");
204
+ await expect(
205
+ setupCloud(directory, { ...options, newOrg: "Bad Org!" }),
206
+ ).rejects.toThrow("Organization slugs");
207
+ expect(cloudAccessToken.mock.calls.length).toBe(before);
208
+ });
209
+
210
+ it("stops before provisioning when organization selection is cancelled", async () => {
211
+ const directory = await fixture();
212
+ fetchOrganizations.mockResolvedValue([
213
+ { id: "one", slug: "one", name: "One" },
214
+ { id: "two", slug: "two", name: "Two" },
215
+ ]);
216
+ const prompts =
217
+ await vi.importActual<typeof import("@clack/prompts")>("@clack/prompts");
218
+ const cancelled = await prompts.select({
219
+ message: "Cancel test",
220
+ options: [{ value: "one", label: "One" }],
221
+ signal: AbortSignal.abort(),
222
+ });
223
+ select.mockResolvedValue(cancelled);
224
+ const before = provisionCloudHarness.mock.calls.length;
225
+ await expect(
226
+ setupCloud(directory, { ...options, yes: false }),
227
+ ).rejects.toThrow();
228
+ expect(provisionCloudHarness.mock.calls.length).toBe(before);
229
+ });
230
+
231
+ it("exposes login, logout, and setup as cloud commands", () => {
232
+ expect(cloud.commands.map((command) => command.name())).toEqual([
233
+ "login",
234
+ "logout",
235
+ "setup",
236
+ ]);
237
+ });
238
+ });