@cortexkit/opencode-openai-auth 0.7.2 → 0.8.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 (114) hide show
  1. package/README.md +9 -5
  2. package/dist/{index-9jpc8yct.js → index-02h7g5f9.js} +1 -1
  3. package/dist/index-2xppkvkx.js +2 -0
  4. package/dist/index-3w4vmckx.js +2 -0
  5. package/dist/{index-zxqb5ydj.js → index-507jh8bv.js} +1 -1
  6. package/dist/{index-zg1x8sty.js → index-944gsj1s.js} +1 -1
  7. package/dist/index-nshbgkz1.js +198 -0
  8. package/dist/{index-87145jkw.js → index-xyg86jgt.js} +1 -1
  9. package/dist/index.d.ts +381 -35
  10. package/dist/index.js +66 -161
  11. package/dist/protocol-Cv9ZmU0p.d.ts +24 -0
  12. package/dist/quota-normalize-wap0rne8.js +1 -0
  13. package/dist/rpc/port-file.d.ts +11 -9
  14. package/dist/rpc/port-file.js +1 -1
  15. package/dist/rpc/protocol.d.ts +2 -21
  16. package/dist/rpc/rpc-client.d.ts +6 -4
  17. package/dist/rpc/rpc-client.js +1 -1
  18. package/dist/rpc/rpc-dir.d.ts +7 -5
  19. package/dist/rpc/rpc-dir.js +1 -1
  20. package/dist/sidebar-state.d.ts +84 -83
  21. package/dist/sidebar-state.js +1 -1
  22. package/dist/{token-bvp0y166.js → token-jdvpwk60.js} +1 -1
  23. package/dist/token-util-zzm7paz9.js +1 -0
  24. package/dist/tui-preferences.d.ts +27 -26
  25. package/dist/tui-preferences.js +1 -1
  26. package/dist/tui.d.ts +18 -17
  27. package/package.json +7 -14
  28. package/src/tui/entry.mjs +2 -2
  29. package/src/tui-compiled/{core/account-paths.ts → raw/core-account-paths.ts} +29 -18
  30. package/src/tui-compiled/raw/logger.ts +53 -0
  31. package/src/tui-compiled/{rpc/port-file.ts → raw/rpc-port-file.ts} +1 -1
  32. package/src/tui-compiled/raw/rpc-protocol.ts +12 -0
  33. package/src/{rpc/rpc-client.ts → tui-compiled/raw/rpc-rpc-client.ts} +2 -2
  34. package/src/{sidebar-state.ts → tui-compiled/raw/sidebar-state.ts} +2 -2
  35. package/src/{tui/command-dialogs.tsx → tui-compiled/raw/tui-command-dialogs.tsx} +5 -5
  36. package/src/{tui.tsx → tui-compiled/raw/tui.tsx} +11 -10
  37. package/src/{core/account-paths.ts → tui-compiled/runtime/core-account-paths.ts} +29 -18
  38. package/src/tui-compiled/runtime/logger.ts +53 -0
  39. package/src/{rpc/port-file.ts → tui-compiled/runtime/rpc-port-file.ts} +1 -1
  40. package/src/tui-compiled/runtime/rpc-protocol.ts +12 -0
  41. package/src/tui-compiled/{rpc/rpc-client.ts → runtime/rpc-rpc-client.ts} +2 -2
  42. package/src/tui-compiled/{sidebar-state.ts → runtime/sidebar-state.ts} +2 -2
  43. package/src/tui-compiled/{tui/command-dialogs.tsx → runtime/tui-command-dialogs.tsx} +3 -4
  44. package/src/tui-compiled/{tui.tsx → runtime/tui.tsx} +10 -10
  45. package/src/tui-compiled/shared/internal.ts +6 -0
  46. package/src/{logger.ts → tui-compiled/shared/logger.ts} +82 -21
  47. package/src/tui-compiled/shared/paths.ts +33 -0
  48. package/src/{core → tui-compiled/shared}/refresh-file-lock.ts +7 -4
  49. package/dist/cli.d.ts +0 -2
  50. package/dist/cli.js +0 -15
  51. package/dist/codex-http.d.ts +0 -3
  52. package/dist/commands.d.ts +0 -88
  53. package/dist/config.d.ts +0 -47
  54. package/dist/core/account-paths.d.ts +0 -25
  55. package/dist/core/accounts.d.ts +0 -330
  56. package/dist/core/atomic-write.d.ts +0 -1
  57. package/dist/core/background-quota-refresh.d.ts +0 -46
  58. package/dist/core/backoff.d.ts +0 -53
  59. package/dist/core/cachekeep.d.ts +0 -118
  60. package/dist/core/oauth.d.ts +0 -138
  61. package/dist/core/provider.d.ts +0 -70
  62. package/dist/core/quota-manager.d.ts +0 -192
  63. package/dist/core/refresh-all-quota.d.ts +0 -86
  64. package/dist/core/refresh-file-lock.d.ts +0 -12
  65. package/dist/core/reset-credits.d.ts +0 -116
  66. package/dist/core/sticky-routing.d.ts +0 -43
  67. package/dist/dump.d.ts +0 -20
  68. package/dist/hosted-web-search.d.ts +0 -5
  69. package/dist/index-8f1200qx.js +0 -6
  70. package/dist/index-cfjed1st.js +0 -89
  71. package/dist/index-nrvjr97q.js +0 -3
  72. package/dist/index-qaggaze1.js +0 -2
  73. package/dist/index-sdcpzbnk.js +0 -2
  74. package/dist/logger.d.ts +0 -22
  75. package/dist/model-costs.d.ts +0 -29
  76. package/dist/prompt-context.d.ts +0 -19
  77. package/dist/quota-normalize-dvwtmy7f.js +0 -1
  78. package/dist/quota-normalize.d.ts +0 -43
  79. package/dist/raw-ws-bun.d.ts +0 -40
  80. package/dist/raw-ws-node.d.ts +0 -37
  81. package/dist/raw-ws-upgrade.d.ts +0 -2
  82. package/dist/raw-ws.d.ts +0 -3
  83. package/dist/response-stream-error.d.ts +0 -12
  84. package/dist/rpc/notifications.d.ts +0 -5
  85. package/dist/rpc/rpc-server.d.ts +0 -17
  86. package/dist/token-util-jcj7k7pa.js +0 -1
  87. package/dist/tui/command-dialogs.d.ts +0 -21
  88. package/dist/util/error.d.ts +0 -1
  89. package/dist/util/open-url.d.ts +0 -6
  90. package/dist/util/proxy-env.d.ts +0 -4
  91. package/dist/util/record.d.ts +0 -1
  92. package/dist/util/stable-json.d.ts +0 -1
  93. package/dist/util/uuid-v7.d.ts +0 -1
  94. package/dist/version.d.ts +0 -1
  95. package/dist/ws-pool.d.ts +0 -57
  96. package/dist/ws.d.ts +0 -71
  97. package/src/rpc/notifications.ts +0 -54
  98. package/src/rpc/protocol.ts +0 -33
  99. package/src/rpc/rpc-server.ts +0 -172
  100. package/src/tui-compiled/core/refresh-file-lock.ts +0 -381
  101. package/src/tui-compiled/logger.ts +0 -196
  102. package/src/tui-compiled/rpc/protocol.ts +0 -33
  103. package/src/util/error.ts +0 -9
  104. package/src/util/open-url.ts +0 -30
  105. package/src/util/proxy-env.ts +0 -42
  106. package/src/util/record.ts +0 -3
  107. package/src/util/stable-json.ts +0 -15
  108. package/src/util/uuid-v7.ts +0 -18
  109. /package/src/{rpc/rpc-dir.ts → tui-compiled/raw/rpc-rpc-dir.ts} +0 -0
  110. /package/src/tui-compiled/{tui-preferences.ts → raw/tui-preferences.ts} +0 -0
  111. /package/src/tui-compiled/{rpc/rpc-dir.ts → runtime/rpc-rpc-dir.ts} +0 -0
  112. /package/src/{tui-preferences.ts → tui-compiled/runtime/tui-preferences.ts} +0 -0
  113. /package/src/tui-compiled/{util → shared/util}/error.ts +0 -0
  114. /package/src/tui-compiled/{util → shared/util}/open-url.ts +0 -0
@@ -1,23 +1,28 @@
1
1
  import { realpathSync } from 'node:fs'
2
2
  import { homedir } from 'node:os'
3
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'
4
10
 
5
11
  /**
6
- * Where the account store lives.
12
+ * Where OpenCode's account store lives.
7
13
  *
8
- * This is the ONLY definition. It used to be duplicated verbatim in both
9
- * `accounts.ts` and `refresh-file-lock.ts`, because `accounts.ts` imports the
10
- * lock and the reverse import would have been a cycle. Two copies of a path
11
- * resolver is a bad trade for that: the lock and the write must agree on the
12
- * exact file, and if the copies ever drifted, a writer would take a lock on one
13
- * path while writing another — mutual exclusion silently lost on a file holding
14
- * credentials. Nothing would fail a test, since each copy works on its own.
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.
15
19
  *
16
- * This module imports nothing from either side, so both can depend on it.
20
+ * The file names come from the core so the two hosts cannot drift apart on what
21
+ * the store is called.
17
22
  */
18
23
 
19
- export const ACCOUNT_FILE_NAME = 'openai-auth.json'
20
- export const ACCOUNT_STATE_FILE_NAME = 'openai-auth-state.json'
24
+ export type { AccountPaths }
25
+ export { ACCOUNT_FILE_NAME, ACCOUNT_STATE_FILE_NAME, deriveStatePath }
21
26
 
22
27
  function getConfigDir() {
23
28
  if (process.env.OPENCODE_CONFIG_DIR?.trim()) {
@@ -36,13 +41,6 @@ export function getAccountStoragePath() {
36
41
  )
37
42
  }
38
43
 
39
- /** Derive the state-file path from the config path without reading env vars. */
40
- export function deriveStatePath(configPath: string): string {
41
- return basename(configPath) === ACCOUNT_FILE_NAME
42
- ? join(dirname(configPath), ACCOUNT_STATE_FILE_NAME)
43
- : `${configPath}.state.json`
44
- }
45
-
46
44
  function normalizePathForComparison(path: string, platform: NodeJS.Platform) {
47
45
  const resolved = resolve(path)
48
46
  return platform === 'win32' || platform === 'darwin'
@@ -101,3 +99,16 @@ export function getAccountStatePath(configPath = getAccountStoragePath()) {
101
99
  }
102
100
  return deriveStatePath(configPath)
103
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
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * OpenCode's log destination.
3
+ *
4
+ * The logger itself lives in the shared core so both hosts get the same
5
+ * redaction rules. What stays here is the part that is specific to this host:
6
+ * which file the lines go to, which level floor applies, and when the buffer is
7
+ * drained at process exit. Those are the only three things that read this
8
+ * host's environment variables, and the core reads none.
9
+ *
10
+ * The init runs at module load rather than from one call site because every
11
+ * entry point in this package — the plugin loader, the RPC server, the separate
12
+ * TUI process — reaches the logger through this module, and a core logger no
13
+ * host has initialised silently drops every line. Both values are passed as
14
+ * functions so the destination and level keep resolving on each write, which is
15
+ * what they do today.
16
+ */
17
+ import { tmpdir } from 'node:os'
18
+ import { join } from 'node:path'
19
+ import {
20
+ flushLogs,
21
+ initLogger,
22
+ type Level,
23
+ } from '../shared/internal.ts'
24
+
25
+ export {
26
+ createLogger,
27
+ flushForTest,
28
+ flushLogs,
29
+ type Level,
30
+ redact,
31
+ redactStrings,
32
+ setLogLevel,
33
+ } from '../shared/internal.ts'
34
+
35
+ const LEVELS: readonly Level[] = ['error', 'warn', 'info', 'debug', 'trace']
36
+
37
+ export function getLogFile(): string {
38
+ return (
39
+ process.env.OPENCODE_OPENAI_AUTH_LOG_FILE ??
40
+ join(tmpdir(), 'opencode-openai-auth.log')
41
+ )
42
+ }
43
+
44
+ export function getEnvLogLevel(): Level | undefined {
45
+ const env = process.env.OPENCODE_OPENAI_AUTH_LOG_LEVEL as Level | undefined
46
+ return env && LEVELS.includes(env) ? env : undefined
47
+ }
48
+
49
+ initLogger({ file: getLogFile, level: getEnvLogLevel })
50
+
51
+ // Under `bun test` the buffer is drained explicitly by the tests that assert on
52
+ // it, and registering a process-wide exit handler per test file is noise.
53
+ if (process.env.NODE_ENV !== 'test') process.on('exit', flushLogs)
@@ -10,7 +10,7 @@ import {
10
10
  writeFile,
11
11
  } from 'node:fs/promises'
12
12
  import { join, resolve } from 'node:path'
13
- import { createLogger } from '../logger'
13
+ import { createLogger } from './logger.ts'
14
14
 
15
15
  const log = createLogger('rpc')
16
16
 
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The RPC payload types now live in the shared core, because both hosts build
3
+ * the same payloads. This file stays on disk as a re-export so the existing
4
+ * build entry and every importer that names `rpc/protocol` keep working.
5
+ */
6
+ export type {
7
+ ApplyRequest,
8
+ ApplyResult,
9
+ CommandModalName,
10
+ OpenDialogPayload,
11
+ RpcNotification,
12
+ } from '../shared/internal.ts'
@@ -1,5 +1,5 @@
1
- import { discoverPortFile, type PortFileEntry } from './port-file'
2
- import type { ApplyRequest, ApplyResult, RpcNotification } from './protocol'
1
+ import { discoverPortFile, type PortFileEntry } from './rpc-port-file.ts'
2
+ import type { ApplyRequest, ApplyResult, RpcNotification } from './rpc-protocol.ts'
3
3
 
4
4
  export interface RpcClient {
5
5
  pending: (
@@ -155,8 +155,8 @@ import { createHash, randomUUID } from 'node:crypto'
155
155
  import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
156
156
  import { tmpdir } from 'node:os'
157
157
  import { dirname, join } from 'node:path'
158
- import { acquireRefreshFileLock } from './core/refresh-file-lock'
159
- import { createLogger } from './logger'
158
+ import { acquireRefreshFileLock } from '../shared/internal.ts'
159
+ import { createLogger } from './logger.ts'
160
160
 
161
161
  const logSb = createLogger('sidebar')
162
162
 
@@ -1,16 +1,16 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
+
3
+ import { errorMessage, openUrl } from '../shared/internal.ts'
2
4
  import type { TuiPluginApi } from '@opencode-ai/plugin/tui'
3
- import { createLogger } from '../logger'
4
- import type { OpenDialogPayload } from '../rpc/protocol.js'
5
+ import { createLogger } from './logger.ts'
6
+ import type { OpenDialogPayload } from './rpc-protocol.ts'
5
7
  import {
6
8
  type AccountQuota,
7
9
  getCollapsedQuotaSummary,
8
10
  getSidebarState,
9
11
  resolveSessionSidebarRouting,
10
12
  type SidebarState,
11
- } from '../sidebar-state.js'
12
- import { errorMessage } from '../util/error'
13
- import { openUrl } from '../util/open-url'
13
+ } from './sidebar-state.ts'
14
14
 
15
15
  const log = createLogger('rpc-tui')
16
16
 
@@ -16,10 +16,10 @@ import {
16
16
  onCleanup,
17
17
  Show,
18
18
  } from 'solid-js'
19
- import { createLogger } from './logger.js'
20
- import type { ApplyRequest, CommandModalName } from './rpc/protocol.js'
21
- import { createRpcClient } from './rpc/rpc-client.js'
22
- import { resolveRpcDir } from './rpc/rpc-dir.js'
19
+ import { createLogger } from './logger.ts'
20
+ import type { ApplyRequest, CommandModalName } from './rpc-protocol.ts'
21
+ import { createRpcClient } from './rpc-rpc-client.ts'
22
+ import { resolveRpcDir } from './rpc-rpc-dir.ts'
23
23
  import {
24
24
  type AccountQuota,
25
25
  computeQuotaPacing,
@@ -33,8 +33,8 @@ import {
33
33
  resolveSessionSidebarRouting,
34
34
  resolveSessionStickyAccount,
35
35
  type SidebarState,
36
- } from './sidebar-state.js'
37
- import { openCommandDialog } from './tui/command-dialogs.js'
36
+ } from './sidebar-state.ts'
37
+ import { openCommandDialog } from './tui-command-dialogs.tsx'
38
38
  import {
39
39
  type AppearancePrefs,
40
40
  computeEffectiveOrder,
@@ -45,7 +45,7 @@ import {
45
45
  readTuiPreferencesFile,
46
46
  resolveOpenaiAuthPrefs,
47
47
  watchTuiPreferences,
48
- } from './tui-preferences.js'
48
+ } from './tui-preferences.ts'
49
49
 
50
50
  const RPC_POLL_MS = 500
51
51
  let rpcPollStarted = false
@@ -61,14 +61,15 @@ export function buildApplyRequest(
61
61
  return { command, arguments: arguments_, sessionId }
62
62
  }
63
63
 
64
- // Read package metadata from either the raw src/ entry or its generated
65
- // src/tui-compiled/ counterpart. Avoid a JSON import because package.json sits
66
- // outside the declaration build's rootDir.
64
+ // Read package metadata from the development source or either generated TUI
65
+ // variant. Avoid a JSON import because package.json sits outside the declaration
66
+ // build's rootDir.
67
67
  const PLUGIN_VERSION: string = (() => {
68
68
  const here = dirname(fileURLToPath(import.meta.url))
69
69
  for (const packageFile of [
70
70
  join(here, '..', 'package.json'),
71
71
  join(here, '..', '..', 'package.json'),
72
+ join(here, '..', '..', '..', 'package.json'),
72
73
  ]) {
73
74
  try {
74
75
  const raw = readFileSync(packageFile, 'utf8')
@@ -1,23 +1,28 @@
1
1
  import { realpathSync } from 'node:fs'
2
2
  import { homedir } from 'node:os'
3
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'
4
10
 
5
11
  /**
6
- * Where the account store lives.
12
+ * Where OpenCode's account store lives.
7
13
  *
8
- * This is the ONLY definition. It used to be duplicated verbatim in both
9
- * `accounts.ts` and `refresh-file-lock.ts`, because `accounts.ts` imports the
10
- * lock and the reverse import would have been a cycle. Two copies of a path
11
- * resolver is a bad trade for that: the lock and the write must agree on the
12
- * exact file, and if the copies ever drifted, a writer would take a lock on one
13
- * path while writing another — mutual exclusion silently lost on a file holding
14
- * credentials. Nothing would fail a test, since each copy works on its own.
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.
15
19
  *
16
- * This module imports nothing from either side, so both can depend on it.
20
+ * The file names come from the core so the two hosts cannot drift apart on what
21
+ * the store is called.
17
22
  */
18
23
 
19
- export const ACCOUNT_FILE_NAME = 'openai-auth.json'
20
- export const ACCOUNT_STATE_FILE_NAME = 'openai-auth-state.json'
24
+ export type { AccountPaths }
25
+ export { ACCOUNT_FILE_NAME, ACCOUNT_STATE_FILE_NAME, deriveStatePath }
21
26
 
22
27
  function getConfigDir() {
23
28
  if (process.env.OPENCODE_CONFIG_DIR?.trim()) {
@@ -36,13 +41,6 @@ export function getAccountStoragePath() {
36
41
  )
37
42
  }
38
43
 
39
- /** Derive the state-file path from the config path without reading env vars. */
40
- export function deriveStatePath(configPath: string): string {
41
- return basename(configPath) === ACCOUNT_FILE_NAME
42
- ? join(dirname(configPath), ACCOUNT_STATE_FILE_NAME)
43
- : `${configPath}.state.json`
44
- }
45
-
46
44
  function normalizePathForComparison(path: string, platform: NodeJS.Platform) {
47
45
  const resolved = resolve(path)
48
46
  return platform === 'win32' || platform === 'darwin'
@@ -101,3 +99,16 @@ export function getAccountStatePath(configPath = getAccountStoragePath()) {
101
99
  }
102
100
  return deriveStatePath(configPath)
103
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
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * OpenCode's log destination.
3
+ *
4
+ * The logger itself lives in the shared core so both hosts get the same
5
+ * redaction rules. What stays here is the part that is specific to this host:
6
+ * which file the lines go to, which level floor applies, and when the buffer is
7
+ * drained at process exit. Those are the only three things that read this
8
+ * host's environment variables, and the core reads none.
9
+ *
10
+ * The init runs at module load rather than from one call site because every
11
+ * entry point in this package — the plugin loader, the RPC server, the separate
12
+ * TUI process — reaches the logger through this module, and a core logger no
13
+ * host has initialised silently drops every line. Both values are passed as
14
+ * functions so the destination and level keep resolving on each write, which is
15
+ * what they do today.
16
+ */
17
+ import { tmpdir } from 'node:os'
18
+ import { join } from 'node:path'
19
+ import {
20
+ flushLogs,
21
+ initLogger,
22
+ type Level,
23
+ } from '../shared/internal.ts'
24
+
25
+ export {
26
+ createLogger,
27
+ flushForTest,
28
+ flushLogs,
29
+ type Level,
30
+ redact,
31
+ redactStrings,
32
+ setLogLevel,
33
+ } from '../shared/internal.ts'
34
+
35
+ const LEVELS: readonly Level[] = ['error', 'warn', 'info', 'debug', 'trace']
36
+
37
+ export function getLogFile(): string {
38
+ return (
39
+ process.env.OPENCODE_OPENAI_AUTH_LOG_FILE ??
40
+ join(tmpdir(), 'opencode-openai-auth.log')
41
+ )
42
+ }
43
+
44
+ export function getEnvLogLevel(): Level | undefined {
45
+ const env = process.env.OPENCODE_OPENAI_AUTH_LOG_LEVEL as Level | undefined
46
+ return env && LEVELS.includes(env) ? env : undefined
47
+ }
48
+
49
+ initLogger({ file: getLogFile, level: getEnvLogLevel })
50
+
51
+ // Under `bun test` the buffer is drained explicitly by the tests that assert on
52
+ // it, and registering a process-wide exit handler per test file is noise.
53
+ if (process.env.NODE_ENV !== 'test') process.on('exit', flushLogs)
@@ -10,7 +10,7 @@ import {
10
10
  writeFile,
11
11
  } from 'node:fs/promises'
12
12
  import { join, resolve } from 'node:path'
13
- import { createLogger } from '../logger'
13
+ import { createLogger } from './logger.ts'
14
14
 
15
15
  const log = createLogger('rpc')
16
16
 
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The RPC payload types now live in the shared core, because both hosts build
3
+ * the same payloads. This file stays on disk as a re-export so the existing
4
+ * build entry and every importer that names `rpc/protocol` keep working.
5
+ */
6
+ export type {
7
+ ApplyRequest,
8
+ ApplyResult,
9
+ CommandModalName,
10
+ OpenDialogPayload,
11
+ RpcNotification,
12
+ } from '../shared/internal.ts'
@@ -1,5 +1,5 @@
1
- import { discoverPortFile, type PortFileEntry } from './port-file'
2
- import type { ApplyRequest, ApplyResult, RpcNotification } from './protocol'
1
+ import { discoverPortFile, type PortFileEntry } from './rpc-port-file.ts'
2
+ import type { ApplyRequest, ApplyResult, RpcNotification } from './rpc-protocol.ts'
3
3
 
4
4
  export interface RpcClient {
5
5
  pending: (
@@ -155,8 +155,8 @@ import { createHash, randomUUID } from 'node:crypto'
155
155
  import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
156
156
  import { tmpdir } from 'node:os'
157
157
  import { dirname, join } from 'node:path'
158
- import { acquireRefreshFileLock } from './core/refresh-file-lock'
159
- import { createLogger } from './logger'
158
+ import { acquireRefreshFileLock } from '../shared/internal.ts'
159
+ import { createLogger } from './logger.ts'
160
160
 
161
161
  const logSb = createLogger('sidebar')
162
162
 
@@ -7,10 +7,9 @@ import { createElement as _$createElement } from "opentui:runtime-module:%40open
7
7
  import { createComponent as _$createComponent } from "opentui:runtime-module:%40opentui%2Fsolid";
8
8
  /** @jsxImportSource @opentui/solid */
9
9
 
10
- import { createLogger } from "../logger";
11
- import { getCollapsedQuotaSummary, getSidebarState, resolveSessionSidebarRouting } from "../sidebar-state.js";
12
- import { errorMessage } from "../util/error";
13
- import { openUrl } from "../util/open-url";
10
+ import { errorMessage, openUrl } from "../shared/internal.ts";
11
+ import { createLogger } from "./logger.ts";
12
+ import { getCollapsedQuotaSummary, getSidebarState, resolveSessionSidebarRouting } from "./sidebar-state.ts";
14
13
  const log = createLogger('rpc-tui');
15
14
  export function buildCachekeepDialogOptions(payload) {
16
15
  const enabled = payload.knobs.enabled === true;
@@ -12,12 +12,12 @@ import { readFileSync } from "node:fs";
12
12
  import { dirname, join } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
  import { createEffect, createSignal, For, onCleanup, Show } from "opentui:runtime-module:solid-js";
15
- import { createLogger } from "./logger.js";
16
- import { createRpcClient } from "./rpc/rpc-client.js";
17
- import { resolveRpcDir } from "./rpc/rpc-dir.js";
18
- import { computeQuotaPacing, DEFAULT_SIDEBAR_STATE, getCollapsedQuotaSummary, getPresentQuotaWindows, getSidebarState, resolveActiveAccount, resolveSessionSidebarRouting, resolveSessionStickyAccount } from "./sidebar-state.js";
19
- import { openCommandDialog } from "./tui/command-dialogs.js";
20
- import { computeEffectiveOrder, DEFAULT_SLOT_ORDER, PLUGIN_KEY, queueTuiPreferenceUpdate, readTuiPreferencesFile, resolveOpenaiAuthPrefs, watchTuiPreferences } from "./tui-preferences.js";
15
+ import { createLogger } from "./logger.ts";
16
+ import { createRpcClient } from "./rpc-rpc-client.ts";
17
+ import { resolveRpcDir } from "./rpc-rpc-dir.ts";
18
+ import { computeQuotaPacing, DEFAULT_SIDEBAR_STATE, getCollapsedQuotaSummary, getPresentQuotaWindows, getSidebarState, resolveActiveAccount, resolveSessionSidebarRouting, resolveSessionStickyAccount } from "./sidebar-state.ts";
19
+ import { openCommandDialog } from "./tui-command-dialogs.tsx";
20
+ import { computeEffectiveOrder, DEFAULT_SLOT_ORDER, PLUGIN_KEY, queueTuiPreferenceUpdate, readTuiPreferencesFile, resolveOpenaiAuthPrefs, watchTuiPreferences } from "./tui-preferences.ts";
21
21
  const RPC_POLL_MS = 500;
22
22
  let rpcPollStarted = false;
23
23
  const log = createLogger('rpc-tui');
@@ -30,12 +30,12 @@ export function buildApplyRequest(command, arguments_, sessionId) {
30
30
  };
31
31
  }
32
32
 
33
- // Read package metadata from either the raw src/ entry or its generated
34
- // src/tui-compiled/ counterpart. Avoid a JSON import because package.json sits
35
- // outside the declaration build's rootDir.
33
+ // Read package metadata from the development source or either generated TUI
34
+ // variant. Avoid a JSON import because package.json sits outside the declaration
35
+ // build's rootDir.
36
36
  const PLUGIN_VERSION = (() => {
37
37
  const here = dirname(fileURLToPath(import.meta.url));
38
- for (const packageFile of [join(here, '..', 'package.json'), join(here, '..', '..', 'package.json')]) {
38
+ for (const packageFile of [join(here, '..', 'package.json'), join(here, '..', '..', 'package.json'), join(here, '..', '..', '..', 'package.json')]) {
39
39
  try {
40
40
  const raw = readFileSync(packageFile, 'utf8');
41
41
  const version = JSON.parse(raw).version;
@@ -0,0 +1,6 @@
1
+ // Generated by scripts/build-tui.ts. Do not edit.
2
+ export * from './logger.ts'
3
+ export * from './paths.ts'
4
+ export * from './refresh-file-lock.ts'
5
+ export * from './util/error.ts'
6
+ export * from './util/open-url.ts'
@@ -5,10 +5,8 @@ import {
5
5
  renameSync,
6
6
  statSync,
7
7
  } from 'node:fs'
8
- import { tmpdir } from 'node:os'
9
- import { join } from 'node:path'
10
8
 
11
- type Level = 'error' | 'warn' | 'info' | 'debug' | 'trace'
9
+ export type Level = 'error' | 'warn' | 'info' | 'debug' | 'trace'
12
10
  const ORDER: Record<Level, number> = {
13
11
  error: 0,
14
12
  warn: 1,
@@ -16,23 +14,63 @@ const ORDER: Record<Level, number> = {
16
14
  debug: 3,
17
15
  trace: 4,
18
16
  }
19
- const isTestEnv = process.env.NODE_ENV === 'test'
20
17
  const MAX_BYTES = 5 * 1024 * 1024
21
18
 
22
- function logFile(): string {
23
- return (
24
- process.env.OPENCODE_OPENAI_AUTH_LOG_FILE ??
25
- join(tmpdir(), 'opencode-openai-auth.log')
26
- )
27
- }
19
+ /**
20
+ * Where lines are written, and the level floor, as the host supplied them.
21
+ *
22
+ * Both start unset. A host decides where its log lives — that decision reads
23
+ * host environment variables and host directories, neither of which belongs in
24
+ * shared code — and calls `initLogger` before it runs any command. Until then
25
+ * every `log.*` call is a silent no-op: throwing would turn the first command a
26
+ * host forgot to wire into a crash, and buffering would hold credential-bearing
27
+ * lines for an init that may never arrive.
28
+ */
29
+ let logFileSource: string | (() => string) | undefined
30
+ let initLevelSource: Level | (() => Level | undefined) | undefined
28
31
  let runtimeLevel: Level | undefined
32
+
33
+ export interface InitLoggerOptions {
34
+ /**
35
+ * Path of the file lines are appended to, or a function returning it.
36
+ *
37
+ * A function is what a host passes when its destination can move while the
38
+ * process runs — an operator or a test changing the variable that names the
39
+ * log file expects the next line to land in the new file, and a value
40
+ * captured once at init would keep writing to the old one.
41
+ */
42
+ file: string | (() => string)
43
+ /** Level floor applied when no `setLogLevel` call has overridden it. */
44
+ level?: Level | (() => Level | undefined)
45
+ }
46
+
47
+ /**
48
+ * Point the logger at a host's file and level. Idempotent: calling it again
49
+ * replaces both. A runtime level installed by `setLogLevel` is deliberately
50
+ * left alone, because it is the operator's explicit choice and outranks the
51
+ * floor a host computed at start-up.
52
+ */
53
+ export function initLogger(options: InitLoggerOptions): void {
54
+ logFileSource = options.file
55
+ initLevelSource = options.level
56
+ }
57
+
29
58
  export function setLogLevel(l: Level | undefined) {
30
59
  if (l === undefined || l in ORDER) runtimeLevel = l
31
60
  }
61
+
62
+ function logFilePath(): string | undefined {
63
+ if (logFileSource === undefined) return undefined
64
+ const resolved =
65
+ typeof logFileSource === 'function' ? logFileSource() : logFileSource
66
+ return resolved || undefined
67
+ }
68
+
32
69
  function configuredLevel(): Level {
33
70
  if (runtimeLevel) return runtimeLevel
34
- const env = process.env.OPENCODE_OPENAI_AUTH_LOG_LEVEL as Level | undefined
35
- if (env && env in ORDER) return env
71
+ const floor =
72
+ typeof initLevelSource === 'function' ? initLevelSource() : initLevelSource
73
+ if (floor && floor in ORDER) return floor
36
74
  return 'info'
37
75
  }
38
76
 
@@ -123,9 +161,8 @@ function chmodPrivate(path: string) {
123
161
  /* never throw */
124
162
  }
125
163
  }
126
- function rotateIfNeeded() {
164
+ function rotateIfNeeded(f: string) {
127
165
  try {
128
- const f = logFile()
129
166
  if (!(existsSync(f) && statSync(f).size > MAX_BYTES)) return
130
167
  for (let i = ROTATE_KEEP - 1; i >= 1; i--) {
131
168
  if (existsSync(`${f}.${i}`)) {
@@ -141,13 +178,19 @@ function rotateIfNeeded() {
141
178
  /* never throw */
142
179
  }
143
180
  }
144
- function flush() {
181
+
182
+ /**
183
+ * Write whatever is buffered. Safe to call synchronously from a process-exit
184
+ * handler, which is how each host drains the buffer on shutdown.
185
+ */
186
+ export function flushLogs() {
145
187
  if (!buffer.length) return
188
+ const file = logFilePath()
146
189
  const text = buffer.join('')
147
190
  buffer = []
191
+ if (!file) return
148
192
  try {
149
- rotateIfNeeded()
150
- const file = logFile()
193
+ rotateIfNeeded(file)
151
194
  if (existsSync(file)) chmodPrivate(file)
152
195
  appendFileSync(file, text, { encoding: 'utf8', mode: 0o600 })
153
196
  } catch {
@@ -158,10 +201,9 @@ function schedule() {
158
201
  if (!timer)
159
202
  timer = setTimeout(() => {
160
203
  timer = undefined
161
- flush()
204
+ flushLogs()
162
205
  }, 500)
163
206
  }
164
- if (!isTestEnv) process.on('exit', flush)
165
207
 
166
208
  function safeSerialize(data: unknown): string {
167
209
  try {
@@ -172,13 +214,16 @@ function safeSerialize(data: unknown): string {
172
214
  }
173
215
 
174
216
  function emit(channel: string, level: Level, message: string, data?: unknown) {
217
+ // No destination means no host has initialised the logger. Drop the line
218
+ // rather than buffering it: see the note on logFileSource above.
219
+ if (logFileSource === undefined) return
175
220
  if (ORDER[level] > ORDER[configuredLevel()]) return
176
221
  const line =
177
222
  `[${new Date().toISOString()}] ${level.toUpperCase()} [${channel}] ${message}` +
178
223
  (data === undefined ? '' : safeSerialize(data)) +
179
224
  '\n'
180
225
  buffer.push(line)
181
- if (buffer.length >= 50) flush()
226
+ if (buffer.length >= 50) flushLogs()
182
227
  else schedule()
183
228
  }
184
229
 
@@ -192,5 +237,21 @@ export function createLogger(channel: string) {
192
237
  }
193
238
  }
194
239
  export async function flushForTest() {
195
- flush()
240
+ flushLogs()
241
+ }
242
+
243
+ /**
244
+ * Return the logger to its uninitialised state. Only a test needs this: a
245
+ * single process runs every test file, so a file path left over from one test
246
+ * would keep a later "logger was never initialised" case writing lines.
247
+ */
248
+ export function resetLoggerForTest() {
249
+ buffer = []
250
+ if (timer) {
251
+ clearTimeout(timer)
252
+ timer = undefined
253
+ }
254
+ logFileSource = undefined
255
+ initLevelSource = undefined
256
+ runtimeLevel = undefined
196
257
  }