@assistant-ui/core 0.1.3 → 0.1.5

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 (111) hide show
  1. package/dist/react/AssistantRuntimeProvider.d.ts +10 -0
  2. package/dist/react/AssistantRuntimeProvider.d.ts.map +1 -0
  3. package/dist/react/AssistantRuntimeProvider.js +16 -0
  4. package/dist/react/AssistantRuntimeProvider.js.map +1 -0
  5. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts +15 -0
  6. package/dist/react/adapters/LocalStorageThreadListAdapter.d.ts.map +1 -0
  7. package/dist/react/adapters/LocalStorageThreadListAdapter.js +154 -0
  8. package/dist/react/adapters/LocalStorageThreadListAdapter.js.map +1 -0
  9. package/dist/react/adapters/TitleGenerationAdapter.d.ts +6 -0
  10. package/dist/react/adapters/TitleGenerationAdapter.d.ts.map +1 -0
  11. package/dist/react/adapters/TitleGenerationAdapter.js +15 -0
  12. package/dist/react/adapters/TitleGenerationAdapter.js.map +1 -0
  13. package/dist/react/index.d.ts +3 -0
  14. package/dist/react/index.d.ts.map +1 -1
  15. package/dist/react/index.js +6 -0
  16. package/dist/react/index.js.map +1 -1
  17. package/dist/react/primitive-hooks/index.d.ts +15 -0
  18. package/dist/react/primitive-hooks/index.d.ts.map +1 -0
  19. package/dist/react/primitive-hooks/index.js +15 -0
  20. package/dist/react/primitive-hooks/index.js.map +1 -0
  21. package/dist/react/primitive-hooks/useActionBarCopy.d.ts +10 -0
  22. package/dist/react/primitive-hooks/useActionBarCopy.d.ts.map +1 -0
  23. package/dist/react/primitive-hooks/useActionBarCopy.js +26 -0
  24. package/dist/react/primitive-hooks/useActionBarCopy.js.map +1 -0
  25. package/dist/react/primitive-hooks/useActionBarEdit.d.ts +5 -0
  26. package/dist/react/primitive-hooks/useActionBarEdit.d.ts.map +1 -0
  27. package/dist/react/primitive-hooks/useActionBarEdit.js +11 -0
  28. package/dist/react/primitive-hooks/useActionBarEdit.js.map +1 -0
  29. package/dist/react/primitive-hooks/useActionBarFeedback.d.ts +9 -0
  30. package/dist/react/primitive-hooks/useActionBarFeedback.d.ts.map +1 -0
  31. package/dist/react/primitive-hooks/useActionBarFeedback.js +19 -0
  32. package/dist/react/primitive-hooks/useActionBarFeedback.js.map +1 -0
  33. package/dist/react/primitive-hooks/useActionBarReload.d.ts +5 -0
  34. package/dist/react/primitive-hooks/useActionBarReload.d.ts.map +1 -0
  35. package/dist/react/primitive-hooks/useActionBarReload.js +13 -0
  36. package/dist/react/primitive-hooks/useActionBarReload.js.map +1 -0
  37. package/dist/react/primitive-hooks/useComposerAddAttachment.d.ts +6 -0
  38. package/dist/react/primitive-hooks/useComposerAddAttachment.d.ts.map +1 -0
  39. package/dist/react/primitive-hooks/useComposerAddAttachment.js +11 -0
  40. package/dist/react/primitive-hooks/useComposerAddAttachment.js.map +1 -0
  41. package/dist/react/primitive-hooks/useComposerCancel.d.ts +5 -0
  42. package/dist/react/primitive-hooks/useComposerCancel.d.ts.map +1 -0
  43. package/dist/react/primitive-hooks/useComposerCancel.js +11 -0
  44. package/dist/react/primitive-hooks/useComposerCancel.js.map +1 -0
  45. package/dist/react/primitive-hooks/useComposerSend.d.ts +5 -0
  46. package/dist/react/primitive-hooks/useComposerSend.d.ts.map +1 -0
  47. package/dist/react/primitive-hooks/useComposerSend.js +11 -0
  48. package/dist/react/primitive-hooks/useComposerSend.js.map +1 -0
  49. package/dist/react/primitive-hooks/useEditComposerCancel.d.ts +4 -0
  50. package/dist/react/primitive-hooks/useEditComposerCancel.d.ts.map +1 -0
  51. package/dist/react/primitive-hooks/useEditComposerCancel.js +10 -0
  52. package/dist/react/primitive-hooks/useEditComposerCancel.js.map +1 -0
  53. package/dist/react/primitive-hooks/useEditComposerSend.d.ts +5 -0
  54. package/dist/react/primitive-hooks/useEditComposerSend.d.ts.map +1 -0
  55. package/dist/react/primitive-hooks/useEditComposerSend.js +11 -0
  56. package/dist/react/primitive-hooks/useEditComposerSend.js.map +1 -0
  57. package/dist/react/primitive-hooks/useMessageBranching.d.ts +7 -0
  58. package/dist/react/primitive-hooks/useMessageBranching.d.ts.map +1 -0
  59. package/dist/react/primitive-hooks/useMessageBranching.js +15 -0
  60. package/dist/react/primitive-hooks/useMessageBranching.js.map +1 -0
  61. package/dist/react/primitive-hooks/useMessageReload.d.ts +5 -0
  62. package/dist/react/primitive-hooks/useMessageReload.d.ts.map +1 -0
  63. package/dist/react/primitive-hooks/useMessageReload.js +11 -0
  64. package/dist/react/primitive-hooks/useMessageReload.js.map +1 -0
  65. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts +2 -0
  66. package/dist/react/primitive-hooks/useThreadIsEmpty.d.ts.map +1 -0
  67. package/dist/react/primitive-hooks/useThreadIsEmpty.js +5 -0
  68. package/dist/react/primitive-hooks/useThreadIsEmpty.js.map +1 -0
  69. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts +2 -0
  70. package/dist/react/primitive-hooks/useThreadIsRunning.d.ts.map +1 -0
  71. package/dist/react/primitive-hooks/useThreadIsRunning.js +5 -0
  72. package/dist/react/primitive-hooks/useThreadIsRunning.js.map +1 -0
  73. package/dist/react/primitive-hooks/useThreadMessages.d.ts +3 -0
  74. package/dist/react/primitive-hooks/useThreadMessages.d.ts.map +1 -0
  75. package/dist/react/primitive-hooks/useThreadMessages.js +5 -0
  76. package/dist/react/primitive-hooks/useThreadMessages.js.map +1 -0
  77. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.d.ts.map +1 -1
  78. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js +2 -1
  79. package/dist/react/runtimes/cloud/useCloudThreadListAdapter.js.map +1 -1
  80. package/dist/react/runtimes/index.d.ts +1 -0
  81. package/dist/react/runtimes/index.d.ts.map +1 -1
  82. package/dist/react/runtimes/index.js +1 -0
  83. package/dist/react/runtimes/index.js.map +1 -1
  84. package/dist/react/runtimes/useLocalRuntime.d.ts +28 -0
  85. package/dist/react/runtimes/useLocalRuntime.d.ts.map +1 -0
  86. package/dist/react/runtimes/useLocalRuntime.js +64 -0
  87. package/dist/react/runtimes/useLocalRuntime.js.map +1 -0
  88. package/package.json +2 -2
  89. package/src/react/AssistantRuntimeProvider.tsx +33 -0
  90. package/src/react/adapters/LocalStorageThreadListAdapter.tsx +227 -0
  91. package/src/react/adapters/TitleGenerationAdapter.ts +20 -0
  92. package/src/react/index.ts +15 -0
  93. package/src/react/primitive-hooks/index.ts +20 -0
  94. package/src/react/primitive-hooks/useActionBarCopy.ts +38 -0
  95. package/src/react/primitive-hooks/useActionBarEdit.ts +13 -0
  96. package/src/react/primitive-hooks/useActionBarFeedback.ts +28 -0
  97. package/src/react/primitive-hooks/useActionBarReload.ts +18 -0
  98. package/src/react/primitive-hooks/useComposerAddAttachment.ts +17 -0
  99. package/src/react/primitive-hooks/useComposerCancel.ts +13 -0
  100. package/src/react/primitive-hooks/useComposerSend.ts +15 -0
  101. package/src/react/primitive-hooks/useEditComposerCancel.ts +12 -0
  102. package/src/react/primitive-hooks/useEditComposerSend.ts +13 -0
  103. package/src/react/primitive-hooks/useMessageBranching.ts +18 -0
  104. package/src/react/primitive-hooks/useMessageReload.ts +13 -0
  105. package/src/react/primitive-hooks/useThreadIsEmpty.ts +5 -0
  106. package/src/react/primitive-hooks/useThreadIsRunning.ts +5 -0
  107. package/src/react/primitive-hooks/useThreadMessages.ts +6 -0
  108. package/src/react/runtimes/cloud/useCloudThreadListAdapter.tsx +3 -1
  109. package/src/react/runtimes/index.ts +6 -0
  110. package/src/react/runtimes/useLocalRuntime.ts +101 -0
  111. package/src/tests/no-unsafe-process-env.test.ts +58 -0
@@ -0,0 +1,20 @@
1
+ export { useThreadMessages } from "./useThreadMessages";
2
+ export { useThreadIsRunning } from "./useThreadIsRunning";
3
+ export { useThreadIsEmpty } from "./useThreadIsEmpty";
4
+ export { useComposerSend } from "./useComposerSend";
5
+ export { useComposerCancel } from "./useComposerCancel";
6
+ export { useMessageReload } from "./useMessageReload";
7
+ export { useMessageBranching } from "./useMessageBranching";
8
+ export {
9
+ useActionBarCopy,
10
+ type UseActionBarCopyOptions,
11
+ } from "./useActionBarCopy";
12
+ export { useActionBarEdit } from "./useActionBarEdit";
13
+ export { useActionBarReload } from "./useActionBarReload";
14
+ export {
15
+ useActionBarFeedbackPositive,
16
+ useActionBarFeedbackNegative,
17
+ } from "./useActionBarFeedback";
18
+ export { useComposerAddAttachment } from "./useComposerAddAttachment";
19
+ export { useEditComposerCancel } from "./useEditComposerCancel";
20
+ export { useEditComposerSend } from "./useEditComposerSend";
@@ -0,0 +1,38 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ export type UseActionBarCopyOptions = {
5
+ copiedDuration?: number | undefined;
6
+ copyToClipboard?: ((text: string) => void | Promise<void>) | undefined;
7
+ };
8
+
9
+ export const useActionBarCopy = ({
10
+ copiedDuration = 3000,
11
+ copyToClipboard,
12
+ }: UseActionBarCopyOptions = {}) => {
13
+ const aui = useAui();
14
+ const disabled = useAuiState((s) => {
15
+ return !(
16
+ (s.message.role !== "assistant" ||
17
+ s.message.status?.type !== "running") &&
18
+ s.message.parts.some((c) => c.type === "text" && c.text.length > 0)
19
+ );
20
+ });
21
+ const isCopied = useAuiState((s) => s.message.isCopied);
22
+ const isEditing = useAuiState((s) => s.composer.isEditing);
23
+ const composerValue = useAuiState((s) => s.composer.text);
24
+
25
+ const copy = useCallback(() => {
26
+ const valueToCopy = isEditing ? composerValue : aui.message().getCopyText();
27
+ if (!valueToCopy) return;
28
+
29
+ const write = copyToClipboard ?? (() => {});
30
+ const result = write(valueToCopy);
31
+ Promise.resolve(result).then(() => {
32
+ aui.message().setIsCopied(true);
33
+ setTimeout(() => aui.message().setIsCopied(false), copiedDuration);
34
+ });
35
+ }, [aui, isEditing, composerValue, copiedDuration, copyToClipboard]);
36
+
37
+ return { copy, disabled, isCopied };
38
+ };
@@ -0,0 +1,13 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ export const useActionBarEdit = () => {
5
+ const aui = useAui();
6
+ const disabled = useAuiState((s) => s.composer.isEditing);
7
+
8
+ const edit = useCallback(() => {
9
+ aui.composer().beginEdit();
10
+ }, [aui]);
11
+
12
+ return { edit, disabled };
13
+ };
@@ -0,0 +1,28 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ export const useActionBarFeedbackPositive = () => {
5
+ const aui = useAui();
6
+ const isSubmitted = useAuiState(
7
+ (s) => s.message.metadata.submittedFeedback?.type === "positive",
8
+ );
9
+
10
+ const submit = useCallback(() => {
11
+ aui.message().submitFeedback({ type: "positive" });
12
+ }, [aui]);
13
+
14
+ return { submit, isSubmitted };
15
+ };
16
+
17
+ export const useActionBarFeedbackNegative = () => {
18
+ const aui = useAui();
19
+ const isSubmitted = useAuiState(
20
+ (s) => s.message.metadata.submittedFeedback?.type === "negative",
21
+ );
22
+
23
+ const submit = useCallback(() => {
24
+ aui.message().submitFeedback({ type: "negative" });
25
+ }, [aui]);
26
+
27
+ return { submit, isSubmitted };
28
+ };
@@ -0,0 +1,18 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ export const useActionBarReload = () => {
5
+ const aui = useAui();
6
+ const disabled = useAuiState(
7
+ (s) =>
8
+ s.thread.isRunning ||
9
+ s.thread.isDisabled ||
10
+ s.message.role !== "assistant",
11
+ );
12
+
13
+ const reload = useCallback(() => {
14
+ aui.message().reload();
15
+ }, [aui]);
16
+
17
+ return { reload, disabled };
18
+ };
@@ -0,0 +1,17 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+ import type { CreateAttachment } from "../../types/attachment";
4
+
5
+ export const useComposerAddAttachment = () => {
6
+ const aui = useAui();
7
+ const disabled = useAuiState((s) => !s.composer.isEditing);
8
+
9
+ const addAttachment = useCallback(
10
+ (file: File | CreateAttachment) => {
11
+ return aui.composer().addAttachment(file);
12
+ },
13
+ [aui],
14
+ );
15
+
16
+ return { addAttachment, disabled };
17
+ };
@@ -0,0 +1,13 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ export const useComposerCancel = () => {
5
+ const aui = useAui();
6
+ const disabled = useAuiState((s) => !s.composer.canCancel);
7
+
8
+ const cancel = useCallback(() => {
9
+ aui.composer().cancel();
10
+ }, [aui]);
11
+
12
+ return { cancel, disabled };
13
+ };
@@ -0,0 +1,15 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ export const useComposerSend = () => {
5
+ const aui = useAui();
6
+ const disabled = useAuiState(
7
+ (s) => s.thread.isRunning || !s.composer.isEditing || s.composer.isEmpty,
8
+ );
9
+
10
+ const send = useCallback(() => {
11
+ aui.composer().send();
12
+ }, [aui]);
13
+
14
+ return { send, disabled };
15
+ };
@@ -0,0 +1,12 @@
1
+ import { useCallback } from "react";
2
+ import { useAui } from "@assistant-ui/store";
3
+
4
+ export const useEditComposerCancel = () => {
5
+ const aui = useAui();
6
+
7
+ const cancel = useCallback(() => {
8
+ aui.composer().cancel();
9
+ }, [aui]);
10
+
11
+ return { cancel };
12
+ };
@@ -0,0 +1,13 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ export const useEditComposerSend = () => {
5
+ const aui = useAui();
6
+ const disabled = useAuiState((s) => s.composer.isEmpty);
7
+
8
+ const send = useCallback(() => {
9
+ aui.composer().send();
10
+ }, [aui]);
11
+
12
+ return { send, disabled };
13
+ };
@@ -0,0 +1,18 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ export const useMessageBranching = () => {
5
+ const aui = useAui();
6
+ const branchNumber = useAuiState((s) => s.message.branchNumber);
7
+ const branchCount = useAuiState((s) => s.message.branchCount);
8
+
9
+ const goToPrev = useCallback(() => {
10
+ aui.message().switchToBranch({ position: "previous" });
11
+ }, [aui]);
12
+
13
+ const goToNext = useCallback(() => {
14
+ aui.message().switchToBranch({ position: "next" });
15
+ }, [aui]);
16
+
17
+ return { branchNumber, branchCount, goToPrev, goToNext };
18
+ };
@@ -0,0 +1,13 @@
1
+ import { useCallback } from "react";
2
+ import { useAui, useAuiState } from "@assistant-ui/store";
3
+
4
+ export const useMessageReload = () => {
5
+ const aui = useAui();
6
+ const canReload = useAuiState((s) => s.message.role === "assistant");
7
+
8
+ const reload = useCallback(() => {
9
+ aui.message().reload();
10
+ }, [aui]);
11
+
12
+ return { reload, canReload };
13
+ };
@@ -0,0 +1,5 @@
1
+ import { useAuiState } from "@assistant-ui/store";
2
+
3
+ export const useThreadIsEmpty = (): boolean => {
4
+ return useAuiState((s) => s.thread.isEmpty);
5
+ };
@@ -0,0 +1,5 @@
1
+ import { useAuiState } from "@assistant-ui/store";
2
+
3
+ export const useThreadIsRunning = (): boolean => {
4
+ return useAuiState((s) => s.thread.isRunning);
5
+ };
@@ -0,0 +1,6 @@
1
+ import { useAuiState } from "@assistant-ui/store";
2
+ import type { MessageState } from "../../store/scopes/message";
3
+
4
+ export const useThreadMessages = (): readonly MessageState[] => {
5
+ return useAuiState((s) => s.thread.messages);
6
+ };
@@ -26,7 +26,9 @@ type CloudThreadListAdapterOptions = {
26
26
  delete?: ((threadId: string) => Promise<void>) | undefined;
27
27
  };
28
28
 
29
- const baseUrl = process?.env?.["NEXT_PUBLIC_ASSISTANT_BASE_URL"];
29
+ const baseUrl =
30
+ typeof process !== "undefined" &&
31
+ process?.env?.["NEXT_PUBLIC_ASSISTANT_BASE_URL"];
30
32
  const autoCloud = baseUrl
31
33
  ? new AssistantCloud({ baseUrl, anonymous: true })
32
34
  : undefined;
@@ -28,3 +28,9 @@ export {
28
28
  useAssistantCloudThreadHistoryAdapter,
29
29
  CloudFileAttachmentAdapter,
30
30
  } from "./cloud";
31
+
32
+ export {
33
+ useLocalRuntime,
34
+ splitLocalRuntimeOptions,
35
+ type LocalRuntimeOptions,
36
+ } from "./useLocalRuntime";
@@ -0,0 +1,101 @@
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
+ import type {
3
+ AssistantRuntime,
4
+ ChatModelAdapter,
5
+ ThreadMessageLike,
6
+ } from "../../index";
7
+ import type { LocalRuntimeOptionsBase } from "../../runtimes/local/local-runtime-options";
8
+ import { AssistantRuntimeImpl, LocalRuntimeCore } from "../../internal";
9
+ import { useAuiState } from "@assistant-ui/store";
10
+ import { useRemoteThreadListRuntime } from "./useRemoteThreadListRuntime";
11
+ import { useCloudThreadListAdapter } from "./cloud";
12
+ import { useRuntimeAdapters } from "./RuntimeAdapterProvider";
13
+ import type { AssistantCloud } from "assistant-cloud";
14
+
15
+ export type LocalRuntimeOptions = Omit<LocalRuntimeOptionsBase, "adapters"> & {
16
+ cloud?: AssistantCloud | undefined;
17
+ initialMessages?: readonly ThreadMessageLike[] | undefined;
18
+ adapters?: Omit<LocalRuntimeOptionsBase["adapters"], "chatModel"> | undefined;
19
+ };
20
+
21
+ const useLocalThreadRuntime = (
22
+ chatModel: ChatModelAdapter,
23
+ { initialMessages, ...options }: LocalRuntimeOptions,
24
+ ): AssistantRuntime => {
25
+ const { modelContext, ...threadListAdapters } = useRuntimeAdapters() ?? {};
26
+ const opt = {
27
+ ...options,
28
+ adapters: {
29
+ ...threadListAdapters,
30
+ ...options.adapters,
31
+ chatModel,
32
+ },
33
+ };
34
+
35
+ const [runtime] = useState(() => new LocalRuntimeCore(opt, initialMessages));
36
+
37
+ const threadIdRef = useRef<string | undefined>(undefined);
38
+ threadIdRef.current = useAuiState((s) => s.threadListItem.remoteId);
39
+
40
+ useEffect(() => {
41
+ runtime.threads
42
+ .getMainThreadRuntimeCore()
43
+ .__internal_setGetThreadId(() => threadIdRef.current);
44
+ }, [runtime]);
45
+
46
+ useEffect(() => {
47
+ return () => {
48
+ runtime.threads.getMainThreadRuntimeCore().detach();
49
+ };
50
+ }, [runtime]);
51
+
52
+ useEffect(() => {
53
+ runtime.threads.getMainThreadRuntimeCore().__internal_setOptions(opt);
54
+ runtime.threads.getMainThreadRuntimeCore().__internal_load();
55
+ });
56
+
57
+ useEffect(() => {
58
+ if (!modelContext) return undefined;
59
+ return runtime.registerModelContextProvider(modelContext);
60
+ }, [modelContext, runtime]);
61
+
62
+ return useMemo(() => new AssistantRuntimeImpl(runtime), [runtime]);
63
+ };
64
+
65
+ export const splitLocalRuntimeOptions = <T extends LocalRuntimeOptions>(
66
+ options: T,
67
+ ) => {
68
+ const {
69
+ cloud,
70
+ initialMessages,
71
+ maxSteps,
72
+ adapters,
73
+ unstable_humanToolNames,
74
+ ...rest
75
+ } = options;
76
+
77
+ return {
78
+ localRuntimeOptions: {
79
+ cloud,
80
+ initialMessages,
81
+ maxSteps,
82
+ adapters,
83
+ unstable_humanToolNames,
84
+ },
85
+ otherOptions: rest,
86
+ };
87
+ };
88
+
89
+ export const useLocalRuntime = (
90
+ chatModel: ChatModelAdapter,
91
+ { cloud, ...options }: LocalRuntimeOptions = {},
92
+ ): AssistantRuntime => {
93
+ const cloudAdapter = useCloudThreadListAdapter({ cloud });
94
+ return useRemoteThreadListRuntime({
95
+ runtimeHook: function RuntimeHook() {
96
+ return useLocalThreadRuntime(chatModel, options);
97
+ },
98
+ adapter: cloudAdapter,
99
+ allowNesting: true,
100
+ });
101
+ };
@@ -0,0 +1,58 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { resolve, relative, join } from "node:path";
4
+
5
+ const SRC_DIR = resolve(__dirname, "..");
6
+
7
+ function findFiles(dir: string, ext: string[]): string[] {
8
+ const results: string[] = [];
9
+ for (const entry of readdirSync(dir)) {
10
+ const full = join(dir, entry);
11
+ if (statSync(full).isDirectory()) {
12
+ results.push(...findFiles(full, ext));
13
+ } else if (ext.some((e) => full.endsWith(e))) {
14
+ results.push(full);
15
+ }
16
+ }
17
+ return results;
18
+ }
19
+
20
+ describe("no unsafe process.env access", () => {
21
+ it("all process.env access for non-NODE_ENV vars must have a typeof process guard", () => {
22
+ const files = findFiles(SRC_DIR, [".ts", ".tsx"]).filter(
23
+ (f) => !f.includes("/tests/") && !f.includes(".test."),
24
+ );
25
+
26
+ const violations: string[] = [];
27
+
28
+ for (const file of files) {
29
+ const content = readFileSync(file, "utf-8");
30
+
31
+ // Check if file has process.env access beyond NODE_ENV
32
+ // Match process.env, process?.env, process.env?, process?.env?
33
+ const hasNonNodeEnvProcessAccess =
34
+ /process\??\.env\??(?!\.NODE_ENV\b)/.test(
35
+ content.replace(/process\??\.env\??\.NODE_ENV/g, ""),
36
+ );
37
+
38
+ if (!hasNonNodeEnvProcessAccess) continue;
39
+
40
+ // File accesses process.env for non-NODE_ENV vars — must have typeof guard
41
+ const hasTypeofGuard = /typeof process\s*!==\s*["']undefined["']/.test(
42
+ content,
43
+ );
44
+
45
+ if (!hasTypeofGuard) {
46
+ violations.push(relative(SRC_DIR, file));
47
+ }
48
+ }
49
+
50
+ expect(
51
+ violations,
52
+ `These files access process.env (non-NODE_ENV) without a typeof process !== "undefined" guard.\n` +
53
+ `@assistant-ui/core lacks @types/node, so bare process.env access crashes in Vite and other bundlers.\n` +
54
+ `Add: typeof process !== "undefined" ? process.env?.["VAR_NAME"] : undefined\n\n` +
55
+ `Files:\n${violations.map((f) => ` - ${f}`).join("\n")}`,
56
+ ).toEqual([]);
57
+ });
58
+ });