assistant-ui 0.0.120 → 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 +11 -7
  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,455 @@
1
+ import { Command, Option } from "commander";
2
+ import * as p from "@clack/prompts";
3
+ import {
4
+ createProject,
5
+ fetchOrganizations,
6
+ fetchProjects,
7
+ type AccountsOrganization,
8
+ type AccountsProject,
9
+ } from "aui-auth/accounts";
10
+ import { access, readFile } from "node:fs/promises";
11
+ import path from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+ import {
14
+ cloudAccessToken,
15
+ cloudAuthConfig,
16
+ loginToCloud,
17
+ logoutFromCloud,
18
+ validateCloudUrl,
19
+ } from "../lib/cloud-auth";
20
+ import {
21
+ readCloudProjectConfig,
22
+ mergeCloudEnvironment,
23
+ readOptionalFile,
24
+ writeCloudProjectConfig,
25
+ } from "../lib/cloud-config";
26
+ import {
27
+ provisionCloudHarness,
28
+ resolveProvisionRequest,
29
+ } from "../lib/cloud-provision";
30
+ import { logger } from "../lib/utils/logger";
31
+ import { create } from "./create";
32
+
33
+ class SetupCancelled extends Error {}
34
+
35
+ const answer = <T>(value: T | symbol): Exclude<T, symbol> => {
36
+ if (p.isCancel(value)) throw new SetupCancelled();
37
+ return value as Exclude<T, symbol>;
38
+ };
39
+
40
+ type SetupOptions = {
41
+ org?: string;
42
+ newOrg?: string;
43
+ project?: string;
44
+ newProject?: string;
45
+ name?: string;
46
+ harnessName?: string;
47
+ backendUrl: string;
48
+ apiUrl: string;
49
+ accessCode?: string;
50
+ setupUrl?: string;
51
+ open?: boolean;
52
+ yes?: boolean;
53
+ skipInstall?: boolean;
54
+ useNpm?: boolean;
55
+ usePnpm?: boolean;
56
+ useYarn?: boolean;
57
+ useBun?: boolean;
58
+ debugSourceRoot?: string;
59
+ };
60
+
61
+ const selectResource = async <
62
+ T extends { id: string; slug: string; name: string },
63
+ >(
64
+ resources: T[],
65
+ selected: string | undefined,
66
+ kind: string,
67
+ yes: boolean,
68
+ ): Promise<T | null> => {
69
+ if (selected) {
70
+ const resource = resources.find(
71
+ ({ id, slug }) => selected === id || selected === slug,
72
+ );
73
+ if (!resource)
74
+ throw new Error(`${kind} "${selected}" was not found in your account.`);
75
+ return resource;
76
+ }
77
+ if (resources.length === 0) return null;
78
+ if (resources.length === 1) return resources[0]!;
79
+ if (yes)
80
+ throw new Error(
81
+ `Choose a ${kind.toLowerCase()} with --${kind.toLowerCase()} when using --yes.`,
82
+ );
83
+ const id = answer(
84
+ await p.select({
85
+ message: `Choose a ${kind.toLowerCase()}`,
86
+ options: resources.map((resource) => ({
87
+ value: resource.id,
88
+ label: `${resource.name} (${resource.slug})`,
89
+ })),
90
+ }),
91
+ );
92
+ return resources.find((resource) => resource.id === id)!;
93
+ };
94
+
95
+ const projectSlug = (value: string): string =>
96
+ value
97
+ .toLowerCase()
98
+ .replace(/[^a-z0-9]+/g, "-")
99
+ .replace(/^-|-$/g, "");
100
+
101
+ const organizationForSetup = async (
102
+ issuer: string,
103
+ token: string,
104
+ options: SetupOptions,
105
+ ): Promise<AccountsOrganization> => {
106
+ const organizations = await fetchOrganizations({ issuer }, token);
107
+ if (!options.newOrg) {
108
+ const existing = await selectResource(
109
+ organizations,
110
+ options.org,
111
+ "Org",
112
+ !!options.yes,
113
+ );
114
+ if (existing) return existing;
115
+ }
116
+ const slug =
117
+ options.newOrg ??
118
+ (options.yes
119
+ ? undefined
120
+ : answer(
121
+ await p.text({
122
+ message: "Choose a slug for your new organization",
123
+ validate: (value) =>
124
+ /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value ?? "")
125
+ ? undefined
126
+ : "Use lowercase letters, numbers, and hyphens.",
127
+ }),
128
+ ));
129
+ if (!slug)
130
+ throw new Error(
131
+ "No organizations found. Pass --new-org <slug> to create one.",
132
+ );
133
+ const existing = organizations.find(
134
+ (organization) => organization.slug === slug,
135
+ );
136
+ if (existing) return existing;
137
+ const response = await fetch(`${issuer}/api/oidc/organizations`, {
138
+ method: "POST",
139
+ headers: {
140
+ Authorization: `Bearer ${token}`,
141
+ "Content-Type": "application/json",
142
+ },
143
+ body: JSON.stringify({ name: options.name ?? slug, slug }),
144
+ signal: AbortSignal.timeout(30_000),
145
+ redirect: "error",
146
+ });
147
+ if (!response.ok)
148
+ throw new Error(`Could not create organization (${response.status}).`);
149
+ return (await response.json()) as AccountsOrganization;
150
+ };
151
+
152
+ const projectForSetup = async (
153
+ issuer: string,
154
+ token: string,
155
+ organization: AccountsOrganization,
156
+ directory: string,
157
+ options: SetupOptions,
158
+ ): Promise<AccountsProject> => {
159
+ const projects = await fetchProjects({ issuer }, token, {
160
+ id: organization.id,
161
+ });
162
+ if (!options.newProject) {
163
+ const existing = await selectResource(
164
+ projects,
165
+ options.project,
166
+ "Project",
167
+ !!options.yes,
168
+ );
169
+ if (existing) return existing;
170
+ }
171
+ const slug = options.newProject ?? projectSlug(path.basename(directory));
172
+ if (!slug)
173
+ throw new Error("Choose a project slug with --new-project <slug>.");
174
+ const created = await createProject(
175
+ { issuer },
176
+ token,
177
+ { id: organization.id },
178
+ {
179
+ name: options.name ?? path.basename(directory),
180
+ slug,
181
+ },
182
+ );
183
+ if ("reason" in created) {
184
+ const existing = projects.find((project) => project.slug === slug);
185
+ if (existing) return existing;
186
+ throw new Error(
187
+ `Project slug "${slug}" already exists. Select it with --project or choose another slug.`,
188
+ );
189
+ }
190
+ return created;
191
+ };
192
+
193
+ const scaffoldCloudProject = async (
194
+ directory: string,
195
+ options: SetupOptions,
196
+ ): Promise<void> => {
197
+ try {
198
+ await access(path.join(directory, "package.json"));
199
+ const marker = JSON.parse(
200
+ await readFile(
201
+ path.join(directory, ".assistant-ui", "template.json"),
202
+ "utf8",
203
+ ),
204
+ ) as { template?: string };
205
+ if (marker.template !== "cloud-harness")
206
+ throw new Error(
207
+ "This directory is not a cloud-harness app. Use a new directory.",
208
+ );
209
+ return;
210
+ } catch (error) {
211
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
212
+ if (await readOptionalFile(path.join(directory, "package.json"))) {
213
+ throw new Error(
214
+ "This directory already contains an app. Use a new directory for the multiplayer scaffold.",
215
+ );
216
+ }
217
+ }
218
+ const packageRoot = path.resolve(
219
+ path.dirname(fileURLToPath(import.meta.url)),
220
+ "../..",
221
+ );
222
+ const args = [
223
+ directory,
224
+ "--template",
225
+ "cloud-harness",
226
+ "--no-skills",
227
+ "--debug-source-root",
228
+ options.debugSourceRoot ?? packageRoot,
229
+ ];
230
+ if (options.skipInstall) args.push("--skip-install");
231
+ for (const flag of ["useNpm", "usePnpm", "useYarn", "useBun"] as const) {
232
+ if (options[flag]) args.push(`--use-${flag.slice(3).toLowerCase()}`);
233
+ }
234
+ await create.parseAsync(args, { from: "user" });
235
+ };
236
+
237
+ export const setupCloud = async (
238
+ directory: string,
239
+ options: SetupOptions,
240
+ ): Promise<void> => {
241
+ if (options.org && options.newOrg)
242
+ throw new Error("Choose --org or --new-org.");
243
+ if (options.project && options.newProject)
244
+ throw new Error("Choose --project or --new-project.");
245
+ validateCloudUrl(options.apiUrl);
246
+ const backend = new URL(options.backendUrl);
247
+ if (backend.username || backend.password || backend.search || backend.hash)
248
+ throw new Error(
249
+ "Backend URLs must not include credentials, queries, or fragments.",
250
+ );
251
+ if (
252
+ options.newOrg !== undefined &&
253
+ !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(options.newOrg)
254
+ )
255
+ throw new Error(
256
+ "Organization slugs must use lowercase letters, numbers, and hyphens.",
257
+ );
258
+ if (
259
+ backend.protocol !== "https:" &&
260
+ !(
261
+ backend.protocol === "http:" &&
262
+ ["localhost", "127.0.0.1", "[::1]"].includes(backend.hostname)
263
+ )
264
+ ) {
265
+ throw new Error(
266
+ "Use an HTTPS backend URL or an HTTP localhost URL for development.",
267
+ );
268
+ }
269
+ const target = path.resolve(directory);
270
+ const previous = await readCloudProjectConfig(target);
271
+ if (previous) {
272
+ if (previous.backendUrl !== backend.href)
273
+ throw new Error(
274
+ "This harness is configured for another backend URL. Update its backend allowlist in the Cloud dashboard and the backendUrl in .assistant-ui/cloud.json before rerunning setup, or use a new directory.",
275
+ );
276
+ if (
277
+ (options.project &&
278
+ options.project !== previous.projectId &&
279
+ options.project !== previous.projectSlug) ||
280
+ (options.newProject && options.newProject !== previous.projectSlug) ||
281
+ (options.org &&
282
+ options.org !== previous.organizationId &&
283
+ options.org !== previous.organizationSlug) ||
284
+ (options.newOrg && options.newOrg !== previous.organizationSlug)
285
+ )
286
+ throw new Error(
287
+ "This directory is already configured for another organization or project. Use a new directory.",
288
+ );
289
+ const environment = await readOptionalFile(path.join(target, ".env.local"));
290
+ if (!/^\s*(?:export\s+)?ASSISTANT_API_KEY\s*=\s*.+/m.test(environment))
291
+ throw new Error(
292
+ "This project is configured but its .env.local API key is missing. Restore it before running setup again.",
293
+ );
294
+ const expected = mergeCloudEnvironment(environment, {
295
+ NEXT_PUBLIC_ASSISTANT_BASE_URL: previous.apiOrigin,
296
+ NEXT_PUBLIC_ASSISTANT_HARNESS_URL: previous.harnessOrigin,
297
+ NEXT_PUBLIC_ASSISTANT_WORKSPACE_ID: previous.workspaceId,
298
+ });
299
+ if (expected !== environment)
300
+ throw new Error(
301
+ "Cloud settings are missing from .env.local. Restore them before running setup again.",
302
+ );
303
+ logger.success(
304
+ `Already configured: ${previous.projectSlug} / ${previous.harnessId}`,
305
+ );
306
+ return;
307
+ }
308
+ const auth = cloudAuthConfig();
309
+ const token = await cloudAccessToken(auth, {
310
+ noOpen: options.open === false,
311
+ ...(options.setupUrl && { setupUrl: options.setupUrl }),
312
+ print: logger.info,
313
+ });
314
+ const organization = await organizationForSetup(auth.issuer, token, options);
315
+ const project = await projectForSetup(
316
+ auth.issuer,
317
+ token,
318
+ organization,
319
+ target,
320
+ options,
321
+ );
322
+ await scaffoldCloudProject(target, options);
323
+ const accessCode =
324
+ options.accessCode ??
325
+ process.env.ASSISTANT_UI_ACCESS_CODE ??
326
+ (options.yes
327
+ ? undefined
328
+ : answer(
329
+ await p.password({
330
+ message:
331
+ "Hackathon access code (leave empty if your plan includes harnesses)",
332
+ }),
333
+ ));
334
+ const request = await resolveProvisionRequest(
335
+ target,
336
+ {
337
+ org_id: organization.id,
338
+ project_id: project.id,
339
+ harness_name: options.harnessName ?? `${project.name} chat`,
340
+ backend_urls: backend.protocol === "http:" ? [] : [backend.href],
341
+ allow_localhost: backend.protocol === "http:",
342
+ },
343
+ accessCode,
344
+ );
345
+ const result = await provisionCloudHarness(options.apiUrl, token, request);
346
+ await writeCloudProjectConfig(
347
+ target,
348
+ {
349
+ organizationId: organization.id,
350
+ organizationSlug: organization.slug,
351
+ projectId: result.project_id,
352
+ projectSlug: project.slug,
353
+ harnessId: result.harness_id,
354
+ harnessOrigin: result.harness_origin,
355
+ apiOrigin: result.api_origin,
356
+ workspaceId: result.workspace_id,
357
+ backendUrl: backend.href,
358
+ },
359
+ result.api_key,
360
+ );
361
+ logger.success(`Multiplayer chat configured for ${project.name}.`);
362
+ logger.info(`Project: ${target}`);
363
+ logger.info(
364
+ "Add OPENAI_API_KEY to .env.local, then start the app with your package manager's dev command.",
365
+ );
366
+ logger.info(
367
+ backend.protocol === "http:"
368
+ ? `Open ${backend.origin}/#main in two browser windows to try shared chat. Deploy or expose the app to share it with teammates.`
369
+ : 'Open your deployed app and use "Share this chat" to invite teammates to the same conversation.',
370
+ );
371
+ };
372
+
373
+ export const cloud = new Command()
374
+ .name("cloud")
375
+ .description("sign in and set up a multiplayer Assistant Cloud chat")
376
+ .addCommand(
377
+ new Command("login")
378
+ .description("sign in to Assistant Cloud using your browser")
379
+ .option(
380
+ "--no-open",
381
+ "print the device sign-in URL without opening a browser",
382
+ )
383
+ .option(
384
+ "--setup-url <url>",
385
+ "show device sign-in in an active setup wizard",
386
+ )
387
+ .action(async (options: { open?: boolean; setupUrl?: string }) => {
388
+ await loginToCloud(cloudAuthConfig(), {
389
+ noOpen: options.open === false,
390
+ ...(options.setupUrl && { setupUrl: options.setupUrl }),
391
+ print: logger.info,
392
+ });
393
+ }),
394
+ )
395
+ .addCommand(
396
+ new Command("logout")
397
+ .description("revoke your CLI login and remove saved credentials")
398
+ .action(async () => {
399
+ await logoutFromCloud(cloudAuthConfig());
400
+ logger.success("Signed out.");
401
+ }),
402
+ )
403
+ .addCommand(
404
+ new Command("setup")
405
+ .description("create a shared AI chat and provision its cloud harness")
406
+ .argument("[directory]", "new project directory", "multiplayer-chat")
407
+ .option("--org <id-or-slug>", "use an existing organization")
408
+ .option("--new-org <slug>", "create an organization")
409
+ .option("--project <id-or-slug>", "use an existing project")
410
+ .option("--new-project <slug>", "create a project")
411
+ .option("--name <name>", "display name for a new project or organization")
412
+ .option("--harness-name <name>", "name for the multiplayer harness")
413
+ .option(
414
+ "--backend-url <url>",
415
+ "absolute URL of the app's chat route",
416
+ "http://localhost:3000/api/chat",
417
+ )
418
+ .option(
419
+ "--api-url <url>",
420
+ "Assistant Cloud management URL",
421
+ process.env.ASSISTANT_UI_CLOUD_URL ?? "https://cloud.assistant-ui.com",
422
+ )
423
+ .option(
424
+ "--access-code <code>",
425
+ "redeem your hackathon harness access code",
426
+ )
427
+ .option(
428
+ "--no-open",
429
+ "print the device sign-in URL without opening a browser",
430
+ )
431
+ .option(
432
+ "--setup-url <url>",
433
+ "show device sign-in in an active setup wizard",
434
+ )
435
+ .option("--yes", "skip prompts; select resources with flags")
436
+ .option("--skip-install", "skip installing scaffold dependencies")
437
+ .option("--use-npm", "install with npm")
438
+ .option("--use-pnpm", "install with pnpm")
439
+ .option("--use-yarn", "install with yarn")
440
+ .option("--use-bun", "install with bun")
441
+ .addOption(
442
+ new Option(
443
+ "--debug-source-root <path>",
444
+ "local assistant-ui source root",
445
+ ).hideHelp(),
446
+ )
447
+ .action(async (directory: string, options: SetupOptions) => {
448
+ try {
449
+ await setupCloud(directory, options);
450
+ } catch (error) {
451
+ if (!(error instanceof SetupCancelled)) throw error;
452
+ p.cancel("Cloud setup cancelled. Rerun the same command to resume.");
453
+ }
454
+ }),
455
+ );
@@ -69,6 +69,14 @@ export const PROJECT_METADATA: ProjectMetadata[] = [
69
69
  path: "templates/cloud-clerk",
70
70
  hasLocalComponents: false,
71
71
  },
72
+ {
73
+ name: "cloud-harness",
74
+ label: "Shared cloud chat",
75
+ description: "A hosted harness with a shared conversation across browsers",
76
+ category: "template",
77
+ path: "templates/cloud-harness",
78
+ hasLocalComponents: true,
79
+ },
72
80
  {
73
81
  name: "langchain",
74
82
  label: "LangChain",
@@ -10,7 +10,7 @@ import {
10
10
  import { type PackageManagerName } from "./utils/package-manager";
11
11
 
12
12
  export const SKILLS_PACKAGE = "assistant-ui/skills";
13
- export const SKILLS_COMMIT = "4007601e2e16762060f02d45ad82f453eed5409a";
13
+ export const SKILLS_COMMIT = "139674dc888ee076982b6726e8e6f5d0fe0b5f67";
14
14
  export const SKILLS_PLUGIN_SOURCE = `gh:${SKILLS_PACKAGE}/assistant-ui#${SKILLS_COMMIT}`;
15
15
 
16
16
  export function resolveSkillsInstall(params: {
@@ -0,0 +1,12 @@
1
+ export const abortable = <T>(
2
+ promise: Promise<T>,
3
+ signal: AbortSignal,
4
+ ): Promise<T> =>
5
+ new Promise((resolve, reject) => {
6
+ const abort = () => reject(signal.reason);
7
+ signal.addEventListener("abort", abort, { once: true });
8
+ if (signal.aborted) abort();
9
+ promise.then(resolve, reject).finally(() => {
10
+ signal.removeEventListener("abort", abort);
11
+ });
12
+ });