@cortexkit/opencode-openai-auth 0.11.1 → 1.0.0

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 (204) hide show
  1. package/README.md +15 -26
  2. package/dist/index-766sfrh5.js +1 -0
  3. package/dist/index-8kj7sa1p.js +12 -0
  4. package/dist/index-8rqfbhfw.js +221 -0
  5. package/dist/index-b3q3j39x.js +2 -0
  6. package/dist/index-bmd7x5aq.js +2 -0
  7. package/dist/index-eqh53k8v.js +2 -0
  8. package/dist/{index-sdt2d2ra.js → index-hy9030jh.js} +1 -1
  9. package/dist/index-jyr8y8k4.js +7 -0
  10. package/dist/index-rv08jmd4.js +2 -0
  11. package/dist/index.d.ts +578 -145
  12. package/dist/index.js +1 -110
  13. package/dist/pool-lifecycle-DbDmgabd.d.ts +2430 -0
  14. package/dist/port-file-Do3lQIEk.d.ts +29 -0
  15. package/dist/protocol-BOBdkqqm.d.ts +198 -0
  16. package/dist/rpc/port-file.d.ts +4 -17
  17. package/dist/rpc/port-file.js +1 -1
  18. package/dist/rpc/protocol.d.ts +2 -2
  19. package/dist/rpc/protocol.js +1 -0
  20. package/dist/rpc/rpc-client.d.ts +5 -4
  21. package/dist/rpc/rpc-client.js +1 -1
  22. package/dist/rpc/rpc-dir.d.ts +2 -0
  23. package/dist/rpc/rpc-dir.js +1 -1
  24. package/dist/sidebar-state-D6cUKgkm.d.ts +385 -0
  25. package/dist/sidebar-state.d.ts +2 -2
  26. package/dist/sidebar-state.js +1 -1
  27. package/dist/token-util-wdza526f.js +1 -0
  28. package/dist/{token-9823y0pc.js → token-ypk61xj0.js} +1 -1
  29. package/dist/tui-preferences.js +5 -2
  30. package/dist/tui.d.ts +9 -4
  31. package/dist/v2/server.d.ts +54 -0
  32. package/dist/v2/server.js +4 -0
  33. package/package.json +21 -9
  34. package/src/tui/entry.mjs +26 -25
  35. package/src/tui-compiled/raw/logger.js +24 -0
  36. package/src/tui-compiled/raw/rpc/protocol.js +3 -0
  37. package/src/tui-compiled/raw/rpc/rpc-client.js +7 -0
  38. package/src/tui-compiled/raw/rpc/rpc-dir.js +27 -0
  39. package/src/tui-compiled/raw/selector.js +21 -0
  40. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +138 -0
  41. package/src/tui-compiled/raw/shared/04a6357627de5ed0-errors.js +38 -0
  42. package/src/tui-compiled/raw/shared/053cc17b5419261e-codec.js +12 -0
  43. package/src/tui-compiled/raw/shared/0766d78c6f481f18-watcher.js +157 -0
  44. package/src/tui-compiled/raw/shared/08eef45626a94992-reserved.js +13 -0
  45. package/src/tui-compiled/raw/shared/39c3cb05738fcf6d-projection.js +134 -0
  46. package/src/tui-compiled/raw/shared/3c2d4169d00f907e-tui-support.js +5 -0
  47. package/src/tui-compiled/raw/shared/3e7d45a77c6ad4d5-index.js +4 -0
  48. package/src/tui-compiled/raw/shared/415792e85d80819e-capture-sink.js +13 -0
  49. package/src/tui-compiled/raw/shared/42ae93ab554fcec2-open-url.js +28 -0
  50. package/src/tui-compiled/raw/shared/43fc9e6b994519c7-refresh-lock.js +136 -0
  51. package/src/tui-compiled/raw/shared/458b934e92490331-mutate.js +500 -0
  52. package/src/tui-compiled/raw/shared/45bf5343bbbf9add-admission.js +142 -0
  53. package/src/tui-compiled/raw/shared/46f3dff1266160e8-client.js +2 -0
  54. package/src/tui-compiled/raw/shared/4825e03209d6ae3e-with-lock.js +48 -0
  55. package/src/tui-compiled/raw/shared/4f36d333d081d040-merge.js +162 -0
  56. package/src/tui-compiled/raw/shared/54eba94fd64c8c3c-index.js +7 -0
  57. package/src/tui-compiled/raw/shared/7211cfec06a298bc-atomic-write.js +80 -0
  58. package/src/tui-compiled/raw/shared/754d6017f8e419cf-builtins.js +534 -0
  59. package/src/tui-compiled/raw/shared/75bd8c3bd63b315f-identity.js +79 -0
  60. package/src/tui-compiled/raw/shared/7a91bb630e6f4804-index.js +1 -0
  61. package/src/tui-compiled/raw/shared/826342a4b6a8b89b-model.js +16 -0
  62. package/src/tui-compiled/raw/shared/83068c0dbe8a6f13-publication.js +304 -0
  63. package/src/tui-compiled/raw/shared/836fc615de6afbd9-settings.js +120 -0
  64. package/src/tui-compiled/raw/shared/83bc3ba9d0fe2761-runtime.js +68 -0
  65. package/src/tui-compiled/raw/shared/8a18abe4fa2977c8-menu.js +300 -0
  66. package/src/tui-compiled/raw/shared/901e149c5a0dbbff-sticky.js +339 -0
  67. package/src/tui-compiled/raw/shared/9020c407ce64f672-ordered.js +58 -0
  68. package/src/tui-compiled/raw/shared/905bffc0220f43d0-map.js +105 -0
  69. package/src/tui-compiled/raw/shared/94820e324f2b4c09-seam.js +301 -0
  70. package/src/tui-compiled/raw/shared/97484d9bb98c705b-provider-state.js +226 -0
  71. package/src/tui-compiled/raw/shared/9968e3cade6969a1-index.js +2 -0
  72. package/src/tui-compiled/raw/shared/9b4da614c45f9cd0-lock-constants.js +14 -0
  73. package/src/tui-compiled/raw/shared/9d3fc9e37456504e-refresh.js +225 -0
  74. package/src/tui-compiled/raw/shared/9deae3c4da4fd63b-hooks.js +34 -0
  75. package/src/tui-compiled/raw/shared/a09b6a533c11858f-tui-preferences.js +77 -0
  76. package/src/tui-compiled/raw/shared/a62a27a204266acd-attribution.js +60 -0
  77. package/src/tui-compiled/raw/shared/a910277ac43a8626-pull.js +143 -0
  78. package/src/tui-compiled/raw/shared/b85c5301fddcb98a-paths.js +5 -0
  79. package/src/tui-compiled/raw/shared/c14988621dc60a75-port-file.js +217 -0
  80. package/src/tui-compiled/raw/shared/c64d7afbc14ca125-sidebar-file.js +157 -0
  81. package/src/tui-compiled/raw/shared/c6b176708c446d6d-pins.js +34 -0
  82. package/src/tui-compiled/raw/shared/c7d252bedee01600-pi.js +206 -0
  83. package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +120 -0
  84. package/src/tui-compiled/raw/shared/cc9e0702a752cea7-index.js +3 -0
  85. package/src/tui-compiled/raw/shared/cd94316a888b0a8e-schema.js +754 -0
  86. package/src/tui-compiled/raw/shared/d319517e3ea6ad70-redact.js +83 -0
  87. package/src/tui-compiled/raw/shared/d5a8f1061ee2f114-index.js +5 -0
  88. package/src/tui-compiled/raw/shared/d669d72c40e9e116-logger.js +19 -0
  89. package/src/tui-compiled/raw/shared/d7dcba96fea1685c-engine.js +246 -0
  90. package/src/tui-compiled/raw/shared/dbef697d1df1cd84-index.js +5 -0
  91. package/src/tui-compiled/raw/shared/e4acc4cb713f300c-protocol.js +3 -0
  92. package/src/tui-compiled/raw/shared/e8cd4929507508d8-refresh-file-lock.js +501 -0
  93. package/src/tui-compiled/raw/shared/ec971142548df90e-index.js +6 -0
  94. package/src/tui-compiled/raw/shared/f37fcefc7aedbf25-error.js +11 -0
  95. package/src/tui-compiled/raw/shared/fbcdcd0f925e74c0-rpc-client.js +253 -0
  96. package/src/tui-compiled/raw/shared/fd45e82f2f9c0a0d-torn.js +294 -0
  97. package/src/tui-compiled/raw/shared/fe02abeadc535f77-rows.js +1258 -0
  98. package/src/tui-compiled/raw/sidebar-state.js +960 -0
  99. package/src/tui-compiled/raw/tui/command-dialogs.tsx +424 -0
  100. package/src/tui-compiled/raw/tui-preferences.js +135 -0
  101. package/src/tui-compiled/raw/tui.tsx +135 -143
  102. package/src/tui-compiled/runtime/logger.js +24 -0
  103. package/src/tui-compiled/runtime/rpc/protocol.js +3 -0
  104. package/src/tui-compiled/runtime/rpc/rpc-client.js +7 -0
  105. package/src/tui-compiled/runtime/rpc/rpc-dir.js +27 -0
  106. package/src/tui-compiled/runtime/selector.js +21 -0
  107. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +138 -0
  108. package/src/tui-compiled/runtime/shared/04a6357627de5ed0-errors.js +38 -0
  109. package/src/tui-compiled/runtime/shared/053cc17b5419261e-codec.js +12 -0
  110. package/src/tui-compiled/runtime/shared/0766d78c6f481f18-watcher.js +157 -0
  111. package/src/tui-compiled/runtime/shared/08eef45626a94992-reserved.js +13 -0
  112. package/src/tui-compiled/runtime/shared/39c3cb05738fcf6d-projection.js +134 -0
  113. package/src/tui-compiled/runtime/shared/3c2d4169d00f907e-tui-support.js +5 -0
  114. package/src/tui-compiled/runtime/shared/3e7d45a77c6ad4d5-index.js +4 -0
  115. package/src/tui-compiled/runtime/shared/415792e85d80819e-capture-sink.js +13 -0
  116. package/src/tui-compiled/runtime/shared/42ae93ab554fcec2-open-url.js +28 -0
  117. package/src/tui-compiled/runtime/shared/43fc9e6b994519c7-refresh-lock.js +136 -0
  118. package/src/tui-compiled/runtime/shared/458b934e92490331-mutate.js +500 -0
  119. package/src/tui-compiled/runtime/shared/45bf5343bbbf9add-admission.js +142 -0
  120. package/src/tui-compiled/runtime/shared/46f3dff1266160e8-client.js +2 -0
  121. package/src/tui-compiled/runtime/shared/4825e03209d6ae3e-with-lock.js +48 -0
  122. package/src/tui-compiled/runtime/shared/4f36d333d081d040-merge.js +162 -0
  123. package/src/tui-compiled/runtime/shared/54eba94fd64c8c3c-index.js +7 -0
  124. package/src/tui-compiled/runtime/shared/7211cfec06a298bc-atomic-write.js +80 -0
  125. package/src/tui-compiled/runtime/shared/754d6017f8e419cf-builtins.js +534 -0
  126. package/src/tui-compiled/runtime/shared/75bd8c3bd63b315f-identity.js +79 -0
  127. package/src/tui-compiled/runtime/shared/7a91bb630e6f4804-index.js +1 -0
  128. package/src/tui-compiled/runtime/shared/826342a4b6a8b89b-model.js +16 -0
  129. package/src/tui-compiled/runtime/shared/83068c0dbe8a6f13-publication.js +304 -0
  130. package/src/tui-compiled/runtime/shared/836fc615de6afbd9-settings.js +120 -0
  131. package/src/tui-compiled/runtime/shared/83bc3ba9d0fe2761-runtime.js +68 -0
  132. package/src/tui-compiled/runtime/shared/8a18abe4fa2977c8-menu.js +300 -0
  133. package/src/tui-compiled/runtime/shared/901e149c5a0dbbff-sticky.js +339 -0
  134. package/src/tui-compiled/runtime/shared/9020c407ce64f672-ordered.js +58 -0
  135. package/src/tui-compiled/runtime/shared/905bffc0220f43d0-map.js +105 -0
  136. package/src/tui-compiled/runtime/shared/94820e324f2b4c09-seam.js +301 -0
  137. package/src/tui-compiled/runtime/shared/97484d9bb98c705b-provider-state.js +226 -0
  138. package/src/tui-compiled/runtime/shared/9968e3cade6969a1-index.js +2 -0
  139. package/src/tui-compiled/runtime/shared/9b4da614c45f9cd0-lock-constants.js +14 -0
  140. package/src/tui-compiled/runtime/shared/9d3fc9e37456504e-refresh.js +225 -0
  141. package/src/tui-compiled/runtime/shared/9deae3c4da4fd63b-hooks.js +34 -0
  142. package/src/tui-compiled/runtime/shared/a09b6a533c11858f-tui-preferences.js +77 -0
  143. package/src/tui-compiled/runtime/shared/a62a27a204266acd-attribution.js +60 -0
  144. package/src/tui-compiled/runtime/shared/a910277ac43a8626-pull.js +143 -0
  145. package/src/tui-compiled/runtime/shared/b85c5301fddcb98a-paths.js +5 -0
  146. package/src/tui-compiled/runtime/shared/c14988621dc60a75-port-file.js +217 -0
  147. package/src/tui-compiled/runtime/shared/c64d7afbc14ca125-sidebar-file.js +157 -0
  148. package/src/tui-compiled/runtime/shared/c6b176708c446d6d-pins.js +34 -0
  149. package/src/tui-compiled/runtime/shared/c7d252bedee01600-pi.js +206 -0
  150. package/src/tui-compiled/runtime/shared/c852a15cfe489d12-pool.js +120 -0
  151. package/src/tui-compiled/runtime/shared/cc9e0702a752cea7-index.js +3 -0
  152. package/src/tui-compiled/runtime/shared/cd94316a888b0a8e-schema.js +754 -0
  153. package/src/tui-compiled/runtime/shared/d319517e3ea6ad70-redact.js +83 -0
  154. package/src/tui-compiled/runtime/shared/d5a8f1061ee2f114-index.js +5 -0
  155. package/src/tui-compiled/runtime/shared/d669d72c40e9e116-logger.js +19 -0
  156. package/src/tui-compiled/runtime/shared/d7dcba96fea1685c-engine.js +246 -0
  157. package/src/tui-compiled/runtime/shared/dbef697d1df1cd84-index.js +5 -0
  158. package/src/tui-compiled/runtime/shared/e4acc4cb713f300c-protocol.js +3 -0
  159. package/src/tui-compiled/runtime/shared/e8cd4929507508d8-refresh-file-lock.js +501 -0
  160. package/src/tui-compiled/runtime/shared/ec971142548df90e-index.js +6 -0
  161. package/src/tui-compiled/runtime/shared/f37fcefc7aedbf25-error.js +11 -0
  162. package/src/tui-compiled/runtime/shared/fbcdcd0f925e74c0-rpc-client.js +253 -0
  163. package/src/tui-compiled/runtime/shared/fd45e82f2f9c0a0d-torn.js +294 -0
  164. package/src/tui-compiled/runtime/shared/fe02abeadc535f77-rows.js +1258 -0
  165. package/src/tui-compiled/runtime/sidebar-state.js +960 -0
  166. package/src/tui-compiled/runtime/tui/command-dialogs.js +378 -0
  167. package/src/tui-compiled/runtime/tui-preferences.js +135 -0
  168. package/src/tui-compiled/runtime/{tui.tsx → tui.js} +288 -316
  169. package/dist/index-4effjmr6.js +0 -2
  170. package/dist/index-9ehhb051.js +0 -2
  171. package/dist/index-g3hsdjh4.js +0 -2
  172. package/dist/index-hh52v8sb.js +0 -2
  173. package/dist/index-jwn4r3sw.js +0 -1
  174. package/dist/index-sq36b120.js +0 -211
  175. package/dist/internal-BTndPmW2.d.ts +0 -914
  176. package/dist/quota-normalize-08hpp51v.js +0 -1
  177. package/dist/sidebar-state-CoKYKcLn.d.ts +0 -220
  178. package/dist/token-util-ypgz7rge.js +0 -1
  179. package/src/tui-compiled/raw/core-account-paths.ts +0 -114
  180. package/src/tui-compiled/raw/core-custody-state.ts +0 -243
  181. package/src/tui-compiled/raw/logger.ts +0 -53
  182. package/src/tui-compiled/raw/rpc-port-file.ts +0 -177
  183. package/src/tui-compiled/raw/rpc-protocol.ts +0 -12
  184. package/src/tui-compiled/raw/rpc-rpc-client.ts +0 -82
  185. package/src/tui-compiled/raw/rpc-rpc-dir.ts +0 -46
  186. package/src/tui-compiled/raw/sidebar-state.ts +0 -1659
  187. package/src/tui-compiled/raw/tui-command-dialogs.tsx +0 -1096
  188. package/src/tui-compiled/raw/tui-preferences.ts +0 -328
  189. package/src/tui-compiled/runtime/core-account-paths.ts +0 -114
  190. package/src/tui-compiled/runtime/core-custody-state.ts +0 -243
  191. package/src/tui-compiled/runtime/logger.ts +0 -53
  192. package/src/tui-compiled/runtime/rpc-port-file.ts +0 -177
  193. package/src/tui-compiled/runtime/rpc-protocol.ts +0 -12
  194. package/src/tui-compiled/runtime/rpc-rpc-client.ts +0 -82
  195. package/src/tui-compiled/runtime/rpc-rpc-dir.ts +0 -46
  196. package/src/tui-compiled/runtime/sidebar-state.ts +0 -1659
  197. package/src/tui-compiled/runtime/tui-command-dialogs.tsx +0 -855
  198. package/src/tui-compiled/runtime/tui-preferences.ts +0 -328
  199. package/src/tui-compiled/shared/internal.ts +0 -6
  200. package/src/tui-compiled/shared/logger.ts +0 -264
  201. package/src/tui-compiled/shared/paths.ts +0 -33
  202. package/src/tui-compiled/shared/refresh-file-lock.ts +0 -384
  203. package/src/tui-compiled/shared/util/error.ts +0 -9
  204. package/src/tui-compiled/shared/util/open-url.ts +0 -30
@@ -1,328 +0,0 @@
1
- import { watch } from 'node:fs'
2
- import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
3
- import { homedir } from 'node:os'
4
- import { basename, dirname, join } from 'node:path'
5
- import { applyEdits, modify, type ParseError, parse } from 'jsonc-parser'
6
-
7
- export const TUI_PREFS_FILE_ENV = 'OPENCODE_TUI_PREFERENCES_FILE'
8
- const FILE_NAME = 'tui-preferences.jsonc'
9
-
10
- // Shared preferences file for opencode TUI plugins. One top-level key per
11
- // plugin (short name, e.g. "openai-auth"). The file is optional: every
12
- // reader must fall back to defaults when it is missing or malformed.
13
- export function getTuiPreferencesFile(): string {
14
- const override = process.env[TUI_PREFS_FILE_ENV]
15
- if (override) return override
16
- const configDir =
17
- process.env.OPENCODE_CONFIG_DIR ||
18
- join(process.env.XDG_CONFIG_HOME || join(homedir(), '.config'), 'opencode')
19
- return join(configDir, FILE_NAME)
20
- }
21
-
22
- function isRecord(value: unknown): value is Record<string, unknown> {
23
- return typeof value === 'object' && value !== null && !Array.isArray(value)
24
- }
25
-
26
- // Tolerant read: missing file, parse errors, or a non-object root all resolve
27
- // to {} so the sidebar never crashes on user-edited content. jsonc-parser's
28
- // fault-tolerant parse can still hand back a partial object for an
29
- // unterminated/bracketed file or trailing garbage, so we collect errors and
30
- // treat any reported fault as malformed.
31
- export async function readTuiPreferencesFile(): Promise<
32
- Record<string, unknown>
33
- > {
34
- try {
35
- const raw = await readFile(getTuiPreferencesFile(), 'utf8')
36
- const errors: ParseError[] = []
37
- const root: unknown = parse(raw, errors, { allowTrailingComma: true })
38
- if (errors.length > 0) return {}
39
- return isRecord(root) ? root : {}
40
- } catch {
41
- return {}
42
- }
43
- }
44
-
45
- export const PLUGIN_KEY = 'openai-auth'
46
- export const DEFAULT_SLOT_ORDER = 160
47
-
48
- export interface OpenaiAuthTuiPrefs {
49
- forceToTop: boolean
50
- order: number
51
- startCollapsed: boolean
52
- rememberCollapsed: boolean
53
- // null = never persisted; seed the UI from startCollapsed instead.
54
- collapsed: boolean | null
55
- pollMs: number
56
- refreshDebounceMs: number
57
- header: {
58
- label: string
59
- showVersion: boolean
60
- }
61
- sections: {
62
- quota: boolean
63
- fallbackAccounts: boolean
64
- routing: boolean
65
- health: boolean
66
- pacing: boolean
67
- }
68
- appearance: {
69
- barWidth: number
70
- barFilledChar: string
71
- barEmptyChar: string
72
- warnThreshold: number
73
- errorThreshold: number
74
- }
75
- }
76
-
77
- export type AppearancePrefs = OpenaiAuthTuiPrefs['appearance']
78
-
79
- export const DEFAULT_PREFS: OpenaiAuthTuiPrefs = {
80
- forceToTop: false,
81
- order: DEFAULT_SLOT_ORDER,
82
- startCollapsed: false,
83
- rememberCollapsed: true,
84
- collapsed: null,
85
- pollMs: 1500,
86
- refreshDebounceMs: 200,
87
- header: { label: 'OPENAI', showVersion: true },
88
- sections: {
89
- quota: true,
90
- fallbackAccounts: true,
91
- routing: true,
92
- health: true,
93
- pacing: true,
94
- },
95
- appearance: {
96
- barWidth: 10,
97
- barFilledChar: '\u2588',
98
- barEmptyChar: '\u2591',
99
- warnThreshold: 50,
100
- errorThreshold: 80,
101
- },
102
- }
103
-
104
- function bool(value: unknown, fallback: boolean): boolean {
105
- return typeof value === 'boolean' ? value : fallback
106
- }
107
-
108
- function int(
109
- value: unknown,
110
- fallback: number,
111
- min: number,
112
- max: number,
113
- ): number {
114
- if (typeof value !== 'number' || !Number.isFinite(value)) return fallback
115
- return Math.min(Math.max(Math.round(value), min), max)
116
- }
117
-
118
- function label(value: unknown, fallback: string, maxLength: number): string {
119
- if (typeof value !== 'string' || value.length === 0) return fallback
120
- return value.slice(0, maxLength)
121
- }
122
-
123
- function char(value: unknown, fallback: string): string {
124
- if (typeof value !== 'string') return fallback
125
- const first = [...value][0]
126
- return first ?? fallback
127
- }
128
-
129
- // Per-key validation: every value is independently clamped/defaulted so one
130
- // bad entry never poisons the rest. Never throws.
131
- export function resolveOpenaiAuthPrefs(
132
- root: Record<string, unknown>,
133
- ): OpenaiAuthTuiPrefs {
134
- const entry = root[PLUGIN_KEY]
135
- if (!isRecord(entry)) return structuredClone(DEFAULT_PREFS)
136
-
137
- const d = DEFAULT_PREFS
138
- const header = isRecord(entry.header) ? entry.header : {}
139
- const sections = isRecord(entry.sections) ? entry.sections : {}
140
- const appearance = isRecord(entry.appearance) ? entry.appearance : {}
141
-
142
- const warnThreshold = int(
143
- appearance.warnThreshold,
144
- d.appearance.warnThreshold,
145
- 0,
146
- 99,
147
- )
148
- const errorThreshold = Math.max(
149
- int(appearance.errorThreshold, d.appearance.errorThreshold, 0, 100),
150
- Math.min(warnThreshold + 1, 100),
151
- )
152
-
153
- return {
154
- forceToTop: bool(entry.forceToTop, d.forceToTop),
155
- order: int(entry.order, d.order, -10000, 10000),
156
- startCollapsed: bool(entry.startCollapsed, d.startCollapsed),
157
- rememberCollapsed: bool(entry.rememberCollapsed, d.rememberCollapsed),
158
- collapsed: typeof entry.collapsed === 'boolean' ? entry.collapsed : null,
159
- pollMs: int(entry.pollMs, d.pollMs, 500, 30000),
160
- refreshDebounceMs: int(
161
- entry.refreshDebounceMs,
162
- d.refreshDebounceMs,
163
- 50,
164
- 5000,
165
- ),
166
- header: {
167
- label: label(header.label, d.header.label, 20),
168
- showVersion: bool(header.showVersion, d.header.showVersion),
169
- },
170
- sections: {
171
- quota: bool(sections.quota, d.sections.quota),
172
- fallbackAccounts: bool(
173
- sections.fallbackAccounts,
174
- d.sections.fallbackAccounts,
175
- ),
176
- routing: bool(sections.routing, d.sections.routing),
177
- health: bool(sections.health, d.sections.health),
178
- pacing: bool(sections.pacing, d.sections.pacing),
179
- },
180
- appearance: {
181
- barWidth: int(appearance.barWidth, d.appearance.barWidth, 4, 40),
182
- barFilledChar: char(appearance.barFilledChar, d.appearance.barFilledChar),
183
- barEmptyChar: char(appearance.barEmptyChar, d.appearance.barEmptyChar),
184
- warnThreshold,
185
- errorThreshold,
186
- },
187
- }
188
- }
189
-
190
- const FORCE_TOP_BASE = -100000
191
-
192
- // Shared forceToTop convention: forced plugins sort below FORCE_TOP_BASE,
193
- // ordered among themselves by their top-level key's position in the file, so
194
- // users reprioritize by reordering keys. The user-facing `order` knob clamps
195
- // to -10000..10000, strictly above the forced band, so a manual order can
196
- // never beat forceToTop. Host slots render ascending by order; opencode's
197
- // built-ins occupy 100-500.
198
- //
199
- // Key-naming requirement: plugin keys must be non-integer-like short names
200
- // (e.g. "openai-auth"). JavaScript object key iteration order hoists
201
- // integer-like keys ("0", "1", "42") ahead of any string keys, which would
202
- // skew the indexOf-based ordering of forced plugins. The shared
203
- // `tui-preferences.jsonc` convention requires non-numeric names, so this
204
- // implementation does not paper over the JS quirk.
205
- export function computeEffectiveOrder(
206
- root: Record<string, unknown>,
207
- pluginKey: string,
208
- defaultOrder: number,
209
- ): number {
210
- const entry = root[pluginKey]
211
- if (!isRecord(entry)) return defaultOrder
212
- if (entry.forceToTop === true) {
213
- return FORCE_TOP_BASE + Object.keys(root).indexOf(pluginKey)
214
- }
215
- return int(entry.order, defaultOrder, -10000, 10000)
216
- }
217
-
218
- const TEMPLATE = `// Shared preferences for opencode TUI plugins.
219
- // One top-level key per plugin (short name). See each plugin's README for
220
- // its supported settings. This file is safe to hand-edit; plugins update
221
- // individual keys in place and preserve comments.
222
- {}
223
- `
224
-
225
- type JsonValue = string | number | boolean | null
226
-
227
- async function writePreference(
228
- pluginKey: string,
229
- path: string[],
230
- value: JsonValue,
231
- ): Promise<void> {
232
- const file = getTuiPreferencesFile()
233
- await mkdir(dirname(file), { recursive: true })
234
- let text: string
235
- try {
236
- text = await readFile(file, 'utf8')
237
- } catch {
238
- text = ''
239
- }
240
- if (text.trim() === '') text = TEMPLATE
241
- const edits = modify(text, [pluginKey, ...path], value, {
242
- formattingOptions: { insertSpaces: true, tabSize: 2 },
243
- })
244
- const next = applyEdits(text, edits)
245
- const tmp = `${file}.${process.pid}.tmp`
246
- await writeFile(tmp, next, 'utf8')
247
- await rename(tmp, file)
248
- }
249
-
250
- let writeChain: Promise<void> = Promise.resolve()
251
-
252
- // Writes are serialized on a promise chain: each update re-reads the file,
253
- // applies a minimal comment-preserving edit to one property, and replaces the
254
- // file atomically (temp + rename in the same directory). Best-effort by
255
- // design — preferences are never worth crashing the TUI over.
256
- export function queueTuiPreferenceUpdate(
257
- pluginKey: string,
258
- path: string[],
259
- value: JsonValue,
260
- ): Promise<void> {
261
- writeChain = writeChain
262
- .then(() => writePreference(pluginKey, path, value))
263
- .catch(() => {})
264
- return writeChain
265
- }
266
-
267
- const WATCH_DEBOUNCE_MS = 150
268
-
269
- // Watches the directory rather than the file: editors and our own atomic
270
- // writes replace the file via rename, which kills file-level watchers.
271
- //
272
- // Filtering is two-stage:
273
- // 1. Filename pre-filter: only debounce events for the preferences file
274
- // name, or our atomic-write temp file. This is a cheap first pass that
275
- // drops the common case (unrelated sibling files).
276
- // 2. Content check inside the debounce: after the timer fires, re-read
277
- // the file and compare it against the last seen content. Only fire
278
- // `onChange` when the content actually changed. This is the authority.
279
- // Some platforms (notably macOS FSEvents and a few inotify backends)
280
- // can misattribute a rename of an unrelated sibling file to the
281
- // real preferences filename in addition to emitting the sibling's
282
- // own event, so a name-only filter still produces a stray callback.
283
- // A content comparison is robust against that, against coalesced
284
- // events, and against mtime granularity.
285
- //
286
- // Returns a disposer; never throws.
287
- export function watchTuiPreferences(onChange: () => void): () => void {
288
- const file = getTuiPreferencesFile()
289
- const name = basename(file)
290
- let timer: ReturnType<typeof setTimeout> | null = null
291
- let lastSeen: string | null = null
292
- // Seed asynchronously; a real change that fires before the seed resolves
293
- // still wins because the debounce re-reads the file fresh and compares
294
- // against `lastSeen` (which will be `null` → does not match → fires).
295
- void readFile(file, 'utf8')
296
- .then((text) => {
297
- if (lastSeen === null) lastSeen = text
298
- })
299
- .catch(() => {})
300
- try {
301
- const watcher = watch(dirname(file), (_event, filename) => {
302
- // Exact match against the preferences file name, plus the temp file
303
- // we use for atomic writes (carrying our pid + `.tmp`).
304
- const isOurs =
305
- filename === name ||
306
- (filename?.startsWith(`${name}.`) && filename.endsWith('.tmp'))
307
- if (filename != null && !isOurs) return
308
- if (timer) clearTimeout(timer)
309
- timer = setTimeout(() => {
310
- timer = null
311
- void readFile(file, 'utf8')
312
- .catch(() => null)
313
- .then((text) => {
314
- if (text === null) return
315
- if (text === lastSeen) return
316
- lastSeen = text
317
- onChange()
318
- })
319
- }, WATCH_DEBOUNCE_MS)
320
- })
321
- return () => {
322
- if (timer) clearTimeout(timer)
323
- watcher.close()
324
- }
325
- } catch {
326
- return () => {}
327
- }
328
- }
@@ -1,114 +0,0 @@
1
- import { realpathSync } from 'node:fs'
2
- import { homedir } from 'node:os'
3
- import { basename, dirname, join, resolve } from 'node:path'
4
- import {
5
- ACCOUNT_FILE_NAME,
6
- ACCOUNT_STATE_FILE_NAME,
7
- type AccountPaths,
8
- deriveStatePath,
9
- } from '../shared/internal.ts'
10
-
11
- /**
12
- * Where OpenCode's account store lives.
13
- *
14
- * This is the ONLY definition of this host's resolution rules. The store code
15
- * itself takes both paths as arguments and resolves nothing, so the lock and
16
- * the write it guards can never disagree about which file they mean — a
17
- * disagreement would lose mutual exclusion on a file holding credentials while
18
- * every test still passed, since each side works on its own.
19
- *
20
- * The file names come from the core so the two hosts cannot drift apart on what
21
- * the store is called.
22
- */
23
-
24
- export type { AccountPaths }
25
- export { ACCOUNT_FILE_NAME, ACCOUNT_STATE_FILE_NAME, deriveStatePath }
26
-
27
- function getConfigDir() {
28
- if (process.env.OPENCODE_CONFIG_DIR?.trim()) {
29
- return process.env.OPENCODE_CONFIG_DIR.trim()
30
- }
31
- return join(
32
- process.env.XDG_CONFIG_HOME || join(homedir(), '.config'),
33
- 'opencode',
34
- )
35
- }
36
-
37
- export function getAccountStoragePath() {
38
- return (
39
- process.env.OPENCODE_OPENAI_AUTH_FILE?.trim() ||
40
- join(getConfigDir(), ACCOUNT_FILE_NAME)
41
- )
42
- }
43
-
44
- function normalizePathForComparison(path: string, platform: NodeJS.Platform) {
45
- const resolved = resolve(path)
46
- return platform === 'win32' || platform === 'darwin'
47
- ? resolved.toLowerCase()
48
- : resolved
49
- }
50
-
51
- function realpathForComparison(path: string): string | undefined {
52
- try {
53
- return realpathSync.native(path)
54
- } catch {
55
- try {
56
- return join(realpathSync.native(dirname(path)), basename(path))
57
- } catch {
58
- return undefined
59
- }
60
- }
61
- }
62
-
63
- /**
64
- * Detect path aliases without requiring either file to exist. This is defense
65
- * in depth: hardlinks and bind mounts can still make distinct paths share a
66
- * file, because neither is distinguishable through pathname identity.
67
- */
68
- export function accountPathsCollide(
69
- configPath: string,
70
- statePath: string,
71
- platform: NodeJS.Platform = process.platform,
72
- ) {
73
- if (
74
- normalizePathForComparison(configPath, platform) ===
75
- normalizePathForComparison(statePath, platform)
76
- ) {
77
- return true
78
- }
79
-
80
- const configIdentity = realpathForComparison(configPath)
81
- const stateIdentity = realpathForComparison(statePath)
82
- return (
83
- configIdentity !== undefined &&
84
- stateIdentity !== undefined &&
85
- normalizePathForComparison(configIdentity, platform) ===
86
- normalizePathForComparison(stateIdentity, platform)
87
- )
88
- }
89
-
90
- export function getAccountStatePath(configPath = getAccountStoragePath()) {
91
- const explicit = process.env.OPENCODE_OPENAI_AUTH_STATE_FILE?.trim()
92
- if (explicit) {
93
- if (accountPathsCollide(configPath, explicit)) {
94
- throw new Error(
95
- `OPENCODE_OPENAI_AUTH_STATE_FILE resolves to the config path (${resolve(configPath)}). Set OPENCODE_OPENAI_AUTH_STATE_FILE to a different file.`,
96
- )
97
- }
98
- return explicit
99
- }
100
- return deriveStatePath(configPath)
101
- }
102
-
103
- /**
104
- * Both file paths a store call needs, resolved the way this host resolves them.
105
- *
106
- * Every call site that used to pass a single config path and let the store
107
- * derive the state path now passes this pair instead, so the state-file
108
- * override keeps being honoured in exactly the places it was before.
109
- */
110
- export function getAccountPaths(
111
- configPath = getAccountStoragePath(),
112
- ): AccountPaths {
113
- return { configPath, statePath: getAccountStatePath(configPath) }
114
- }
@@ -1,243 +0,0 @@
1
- export type CustodyMode = 'local' | 'claustrum'
2
-
3
- export type ManifestCustodyState = 'absent' | 'present' | 'unreadable'
4
-
5
- export type LocalCustodyState =
6
- | 'real'
7
- | 'tombstone'
8
- | 'empty'
9
- | 'gone'
10
- | 'slot-absent'
11
-
12
- export type VaultCustodyState =
13
- | 'serves'
14
- | 'cold'
15
- | 'needs_reauth'
16
- | 'identity_mismatch'
17
- | 'no_handle'
18
-
19
- export const CUSTODY_INERT_REASONS = [
20
- 'needs-login',
21
- 'mode-mismatch',
22
- 'corrupt-under-binding',
23
- 'manifest-unreadable',
24
- 'unbound-under-claustrum',
25
- 'orphan-tombstone',
26
- 'vault-cold',
27
- 'vault-reauth',
28
- 'identity-mismatch',
29
- 'no-handle',
30
- 'takeover-incomplete',
31
- 'new-local-family-under-claustrum',
32
- 'takeover-incomplete/vault-unavailable',
33
- 'takeover-incomplete/slot-absent',
34
- ] as const
35
-
36
- export type CustodyInertReason = (typeof CUSTODY_INERT_REASONS)[number]
37
-
38
- export type CustodyVerdict =
39
- | { kind: 'LOCAL' }
40
- | {
41
- kind: 'VAULT'
42
- canonicalize?: boolean
43
- installTombstone?: boolean
44
- }
45
- | {
46
- kind: 'INERT'
47
- reason: CustodyInertReason
48
- installTombstone?: boolean
49
- canonicalize?: boolean
50
- }
51
- | { kind: 'NEEDS_LOGIN'; reason?: 'corrupt' }
52
-
53
- export interface EvaluateCustodyStartupInput {
54
- mode: CustodyMode
55
- manifest: ManifestCustodyState
56
- local: LocalCustodyState
57
- vault: () => VaultCustodyState
58
- manifestFailureReason?: string
59
- fingerprintMatch?: boolean
60
- verifiedInProcessLogin: boolean
61
- isMain?: boolean
62
- }
63
-
64
- function unreachable(value: never): never {
65
- return value
66
- }
67
-
68
- function withCanonicalization<
69
- T extends Extract<CustodyVerdict, { kind: 'VAULT' | 'INERT' }>,
70
- >(local: LocalCustodyState, verdict: T): T | (T & { canonicalize: true }) {
71
- return local === 'empty' ? { ...verdict, canonicalize: true } : verdict
72
- }
73
-
74
- function evaluateLocal(local: LocalCustodyState): CustodyVerdict {
75
- switch (local) {
76
- case 'real':
77
- return { kind: 'LOCAL' }
78
- case 'tombstone':
79
- case 'empty':
80
- case 'slot-absent':
81
- return { kind: 'NEEDS_LOGIN' }
82
- case 'gone':
83
- return { kind: 'NEEDS_LOGIN', reason: 'corrupt' }
84
- }
85
- return unreachable(local)
86
- }
87
-
88
- function evaluateLocalWithBinding(
89
- local: LocalCustodyState,
90
- verifiedInProcessLogin: boolean,
91
- ): CustodyVerdict {
92
- switch (local) {
93
- case 'real':
94
- return verifiedInProcessLogin
95
- ? { kind: 'LOCAL' }
96
- : { kind: 'INERT', reason: 'needs-login' }
97
- case 'tombstone':
98
- case 'empty':
99
- case 'slot-absent':
100
- return { kind: 'INERT', reason: 'mode-mismatch' }
101
- case 'gone':
102
- return { kind: 'INERT', reason: 'corrupt-under-binding' }
103
- }
104
- return unreachable(local)
105
- }
106
-
107
- function evaluateUnboundClaustrum(local: LocalCustodyState): CustodyVerdict {
108
- switch (local) {
109
- case 'tombstone':
110
- case 'empty':
111
- return { kind: 'INERT', reason: 'orphan-tombstone' }
112
- case 'real':
113
- case 'gone':
114
- case 'slot-absent':
115
- return { kind: 'INERT', reason: 'unbound-under-claustrum' }
116
- }
117
- return unreachable(local)
118
- }
119
-
120
- function evaluateTombstone(
121
- local: Extract<LocalCustodyState, 'tombstone' | 'empty'>,
122
- vault: VaultCustodyState,
123
- ): CustodyVerdict {
124
- switch (vault) {
125
- case 'serves':
126
- return withCanonicalization(local, { kind: 'VAULT' })
127
- case 'cold':
128
- return withCanonicalization(local, {
129
- kind: 'INERT',
130
- reason: 'vault-cold',
131
- })
132
- case 'needs_reauth':
133
- return withCanonicalization(local, {
134
- kind: 'INERT',
135
- reason: 'vault-reauth',
136
- })
137
- case 'identity_mismatch':
138
- return withCanonicalization(local, {
139
- kind: 'INERT',
140
- reason: 'identity-mismatch',
141
- })
142
- case 'no_handle':
143
- return withCanonicalization(local, { kind: 'INERT', reason: 'no-handle' })
144
- }
145
- return unreachable(vault)
146
- }
147
-
148
- function evaluateReal(
149
- vault: VaultCustodyState,
150
- fingerprintMatch: boolean | undefined,
151
- ): CustodyVerdict {
152
- switch (vault) {
153
- case 'serves':
154
- return fingerprintMatch === false
155
- ? { kind: 'INERT', reason: 'new-local-family-under-claustrum' }
156
- : { kind: 'INERT', reason: 'takeover-incomplete' }
157
- case 'cold':
158
- case 'needs_reauth':
159
- return { kind: 'INERT', reason: 'takeover-incomplete/vault-unavailable' }
160
- case 'identity_mismatch':
161
- return { kind: 'INERT', reason: 'identity-mismatch' }
162
- case 'no_handle':
163
- return { kind: 'INERT', reason: 'no-handle' }
164
- }
165
- return unreachable(vault)
166
- }
167
-
168
- function evaluateGone(vault: VaultCustodyState): CustodyVerdict {
169
- switch (vault) {
170
- case 'serves':
171
- return { kind: 'VAULT', installTombstone: true }
172
- case 'cold':
173
- return { kind: 'INERT', reason: 'vault-cold', installTombstone: true }
174
- case 'needs_reauth':
175
- return { kind: 'INERT', reason: 'vault-reauth', installTombstone: true }
176
- case 'identity_mismatch':
177
- return { kind: 'INERT', reason: 'identity-mismatch' }
178
- case 'no_handle':
179
- return { kind: 'INERT', reason: 'no-handle' }
180
- }
181
- return unreachable(vault)
182
- }
183
-
184
- function evaluateSlotAbsent(vault: VaultCustodyState): CustodyVerdict {
185
- switch (vault) {
186
- case 'serves':
187
- case 'cold':
188
- case 'needs_reauth':
189
- return { kind: 'INERT', reason: 'takeover-incomplete/slot-absent' }
190
- case 'identity_mismatch':
191
- return { kind: 'INERT', reason: 'identity-mismatch' }
192
- case 'no_handle':
193
- return { kind: 'INERT', reason: 'no-handle' }
194
- }
195
- return unreachable(vault)
196
- }
197
-
198
- function evaluatePresentClaustrum(
199
- local: LocalCustodyState,
200
- vault: VaultCustodyState,
201
- fingerprintMatch: boolean | undefined,
202
- ): CustodyVerdict {
203
- switch (local) {
204
- case 'tombstone':
205
- case 'empty':
206
- return evaluateTombstone(local, vault)
207
- case 'real':
208
- return evaluateReal(vault, fingerprintMatch)
209
- case 'gone':
210
- return evaluateGone(vault)
211
- case 'slot-absent':
212
- return evaluateSlotAbsent(vault)
213
- }
214
- return unreachable(local)
215
- }
216
-
217
- export function evaluateCustodyStartup(
218
- input: EvaluateCustodyStartupInput,
219
- ): CustodyVerdict {
220
- if (input.manifest === 'unreadable') {
221
- return { kind: 'INERT', reason: 'manifest-unreadable' }
222
- }
223
-
224
- switch (input.mode) {
225
- case 'local':
226
- return input.manifest === 'present'
227
- ? evaluateLocalWithBinding(input.local, input.verifiedInProcessLogin)
228
- : evaluateLocal(input.local)
229
- case 'claustrum':
230
- if (input.manifest === 'absent') {
231
- return evaluateUnboundClaustrum(input.local)
232
- }
233
- if (input.manifest === 'present') {
234
- return evaluatePresentClaustrum(
235
- input.local,
236
- input.vault(),
237
- input.fingerprintMatch,
238
- )
239
- }
240
- return unreachable(input.manifest)
241
- }
242
- return unreachable(input.mode)
243
- }