codex-usage-dashboard 0.1.0 → 0.1.1

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.
package/README.md CHANGED
@@ -7,7 +7,7 @@ Multi-user Codex usage tracking with a Vercel-hosted dashboard, Supabase Auth, a
7
7
  - Vite + TanStack Router + React Query
8
8
  - Supabase Auth + Postgres
9
9
  - Vercel Functions for pairing and sync ingest
10
- - Local Codex access through `codex app-server`
10
+ - Local Codex access through a bundled `@openai/codex` `app-server`
11
11
 
12
12
  ## What changed
13
13
 
@@ -47,12 +47,20 @@ The Vite dev server now serves the local pairing and sync API routes under `/api
47
47
 
48
48
  ## Direct connect flow
49
49
 
50
- Run this on any machine that already has Codex installed:
50
+ Run this on any machine:
51
51
 
52
52
  ```bash
53
53
  npx codex-usage-dashboard@latest connect --site "https://your-site.vercel.app"
54
54
  ```
55
55
 
56
+ The package now brings a compatible `@openai/codex` CLI dependency with it, so `connect`, `pair`, and `sync` do not require a separate global Codex install.
57
+
58
+ If the machine has never logged into Codex before, authenticate once with:
59
+
60
+ ```bash
61
+ npx @openai/codex@latest login
62
+ ```
63
+
56
64
  That command:
57
65
 
58
66
  - starts a local `codex app-server`
@@ -68,7 +76,7 @@ If the same machine is already connected, rerun the same command to reopen the d
68
76
 
69
77
  1. Sign in on the website.
70
78
  2. Click `Create pairing command`.
71
- 3. Run the generated command on the machine that already has Codex installed.
79
+ 3. Run the generated command on the machine you want to pair.
72
80
 
73
81
  The generated command looks like:
74
82
 
@@ -3,19 +3,28 @@
3
3
  import { spawn } from 'node:child_process'
4
4
  import { existsSync } from 'node:fs'
5
5
  import { mkdir, readFile, writeFile } from 'node:fs/promises'
6
+ import { createRequire } from 'node:module'
6
7
  import os from 'node:os'
7
8
  import path from 'node:path'
8
9
  import process from 'node:process'
9
10
 
10
11
  const DEFAULT_POLL_MS = 60_000
12
+ const CODEX_HELP_TIMEOUT_MS = 15_000
11
13
  const CONFIG_FILE_NAME = 'codex-usage-sync.json'
12
14
  const NPX_COMMAND = 'npx codex-usage-dashboard@latest'
13
15
 
16
+ const require = createRequire(import.meta.url)
17
+
18
+ let codexAppServerSupportPromise = null
19
+ let resolvedCodexExecutable = null
20
+
14
21
  class StdioCodexClient {
15
22
  constructor({ codexHome }) {
16
23
  this.codexHome = codexHome
17
24
  this.child = null
18
25
  this.buffer = ''
26
+ this.isClosing = false
27
+ this.lastStderrMessage = ''
19
28
  this.pending = new Map()
20
29
  this.requestId = 0
21
30
  this.notificationHandler = null
@@ -26,17 +35,9 @@ class StdioCodexClient {
26
35
  return
27
36
  }
28
37
 
38
+ this.buffer = ''
39
+ this.lastStderrMessage = ''
29
40
  this.child = await this.spawnAppServer()
30
- this.child.stdout.setEncoding('utf8')
31
- this.child.stdout.on('data', (chunk) => {
32
- this.consumeStdout(chunk)
33
- })
34
- this.child.stderr.on('data', (chunk) => {
35
- const message = chunk.toString().trim()
36
- if (message) {
37
- console.error(`[codex] ${message}`)
38
- }
39
- })
40
41
 
41
42
  await this.request('initialize', {
42
43
  capabilities: {},
@@ -54,13 +55,12 @@ class StdioCodexClient {
54
55
  return
55
56
  }
56
57
 
57
- this.child.kill('SIGTERM')
58
+ const child = this.child
58
59
  this.child = null
60
+ this.isClosing = true
61
+ child.kill('SIGTERM')
59
62
 
60
- for (const pending of this.pending.values()) {
61
- pending.reject(new Error('Codex app-server closed.'))
62
- }
63
- this.pending.clear()
63
+ this.rejectPending(new Error('Codex app-server closed.'))
64
64
  }
65
65
 
66
66
  onNotification(handler) {
@@ -73,17 +73,25 @@ class StdioCodexClient {
73
73
  }
74
74
 
75
75
  const id = ++this.requestId
76
- this.child.stdin.write(
77
- `${JSON.stringify({
78
- id,
79
- jsonrpc: '2.0',
80
- method,
81
- ...(params ? { params } : {}),
82
- })}\n`,
83
- )
84
-
85
76
  return new Promise((resolve, reject) => {
86
77
  this.pending.set(id, { reject, resolve })
78
+
79
+ this.child.stdin.write(
80
+ `${JSON.stringify({
81
+ id,
82
+ jsonrpc: '2.0',
83
+ method,
84
+ ...(params ? { params } : {}),
85
+ })}\n`,
86
+ (error) => {
87
+ if (!error) {
88
+ return
89
+ }
90
+
91
+ this.pending.delete(id)
92
+ reject(error)
93
+ },
94
+ )
87
95
  })
88
96
  }
89
97
 
@@ -151,32 +159,78 @@ class StdioCodexClient {
151
159
  }
152
160
  }
153
161
 
154
- spawnAppServer() {
162
+ rejectPending(error) {
163
+ for (const pending of this.pending.values()) {
164
+ pending.reject(error)
165
+ }
166
+
167
+ this.pending.clear()
168
+ }
169
+
170
+ async spawnAppServer() {
171
+ await ensureCodexAppServerSupport()
172
+ const codexExecutable = resolveCodexExecutable()
173
+
155
174
  return new Promise((resolve, reject) => {
156
- const child = spawn('codex', ['app-server', '--listen', 'stdio://'], {
157
- env: {
158
- ...process.env,
159
- ...(this.codexHome ? { CODEX_HOME: this.codexHome } : {}),
175
+ const child = spawn(
176
+ codexExecutable.command,
177
+ [...codexExecutable.argsPrefix, 'app-server', '--listen', 'stdio://'],
178
+ {
179
+ env: {
180
+ ...process.env,
181
+ ...(this.codexHome ? { CODEX_HOME: this.codexHome } : {}),
182
+ },
183
+ stdio: ['pipe', 'pipe', 'pipe'],
160
184
  },
161
- stdio: ['pipe', 'pipe', 'pipe'],
185
+ )
186
+
187
+ child.stdout.setEncoding('utf8')
188
+ child.stdout.on('data', (chunk) => {
189
+ this.consumeStdout(chunk)
190
+ })
191
+
192
+ child.stderr.on('data', (chunk) => {
193
+ const message = chunk.toString().trim()
194
+ if (!message) {
195
+ return
196
+ }
197
+
198
+ const lines = message.split(/\r?\n/)
199
+ this.lastStderrMessage = lines[lines.length - 1] ?? message
200
+ console.error(`[codex] ${message}`)
162
201
  })
163
202
 
164
203
  child.once('error', (error) => {
165
204
  reject(
166
205
  new Error(
167
206
  error.code === 'ENOENT'
168
- ? 'The `codex` command is not installed on this machine.'
207
+ ? 'Codex CLI is not available. Reinstall `codex-usage-dashboard` or install it globally with `npm install -g @openai/codex`.'
169
208
  : error.message,
170
209
  ),
171
210
  )
172
211
  })
173
212
 
213
+ child.once('exit', (code, signal) => {
214
+ if (this.child === child) {
215
+ this.child = null
216
+ }
217
+
218
+ const expectedShutdown = this.isClosing
219
+ this.isClosing = false
220
+
221
+ if (expectedShutdown) {
222
+ return
223
+ }
224
+
225
+ const error = buildCodexAppServerExitError(
226
+ code,
227
+ signal,
228
+ this.lastStderrMessage,
229
+ )
230
+ this.rejectPending(error)
231
+ })
232
+
174
233
  child.once('spawn', () => {
175
- child.on('exit', (code) => {
176
- if (code !== 0 && code !== null) {
177
- console.error(`[codex] app-server exited with code ${code}`)
178
- }
179
- })
180
234
  resolve(child)
181
235
  })
182
236
  })
@@ -489,7 +543,7 @@ async function readSnapshot(client, failWhenLoggedOut) {
489
543
  if (!accountState.account) {
490
544
  if (failWhenLoggedOut) {
491
545
  throw new Error(
492
- 'No logged-in Codex account was found. Run `codex login` and try again.',
546
+ 'No logged-in Codex account was found. Run `npx @openai/codex@latest login` (or `codex login` if installed globally) and try again.',
493
547
  )
494
548
  }
495
549
 
@@ -598,6 +652,161 @@ function openInBrowser(url) {
598
652
  })
599
653
  }
600
654
 
655
+ async function ensureCodexAppServerSupport() {
656
+ if (!codexAppServerSupportPromise) {
657
+ codexAppServerSupportPromise = inspectCodexCliForAppServer().catch(
658
+ (error) => {
659
+ codexAppServerSupportPromise = null
660
+ throw error
661
+ },
662
+ )
663
+ }
664
+
665
+ await codexAppServerSupportPromise
666
+ }
667
+
668
+ async function inspectCodexCliForAppServer() {
669
+ const codexExecutable = resolveCodexExecutable()
670
+ const help = await runCommandCapture(
671
+ codexExecutable.command,
672
+ [...codexExecutable.argsPrefix, '--help'],
673
+ { timeoutMs: CODEX_HELP_TIMEOUT_MS },
674
+ )
675
+
676
+ const helpText = `${help.stdout}\n${help.stderr}`
677
+ if (help.code === 0 && /\bapp-server\b/.test(helpText)) {
678
+ return
679
+ }
680
+
681
+ const version = await runCommandCapture(
682
+ codexExecutable.command,
683
+ [...codexExecutable.argsPrefix, '--version'],
684
+ { timeoutMs: CODEX_HELP_TIMEOUT_MS },
685
+ )
686
+ const versionText = normalizeCodexVersion(version.stdout || version.stderr)
687
+ throw new Error(
688
+ `Resolved Codex CLI ${versionText} (${codexExecutable.label}) does not support \`codex app-server\`. Reinstall \`codex-usage-dashboard\` or update Codex with \`npm install -g @openai/codex\`, then rerun this command.`,
689
+ )
690
+ }
691
+
692
+ function resolveCodexExecutable() {
693
+ if (resolvedCodexExecutable) {
694
+ return resolvedCodexExecutable
695
+ }
696
+
697
+ const bundledBinPath = resolveBundledCodexBin()
698
+ if (bundledBinPath) {
699
+ resolvedCodexExecutable = {
700
+ argsPrefix: [bundledBinPath],
701
+ command: process.execPath,
702
+ label: 'bundled @openai/codex',
703
+ }
704
+ return resolvedCodexExecutable
705
+ }
706
+
707
+ resolvedCodexExecutable = {
708
+ argsPrefix: [],
709
+ command: 'codex',
710
+ label: 'global codex',
711
+ }
712
+ return resolvedCodexExecutable
713
+ }
714
+
715
+ function resolveBundledCodexBin() {
716
+ try {
717
+ return require.resolve('@openai/codex/bin/codex.js')
718
+ } catch {
719
+ return null
720
+ }
721
+ }
722
+
723
+ function runCommandCapture(command, args, { timeoutMs }) {
724
+ return new Promise((resolve, reject) => {
725
+ let stdout = ''
726
+ let stderr = ''
727
+ let settled = false
728
+
729
+ const child = spawn(command, args, {
730
+ env: process.env,
731
+ stdio: ['ignore', 'pipe', 'pipe'],
732
+ })
733
+
734
+ const timeout = setTimeout(() => {
735
+ if (settled) {
736
+ return
737
+ }
738
+
739
+ settled = true
740
+ child.kill('SIGTERM')
741
+ reject(
742
+ new Error(
743
+ `Timed out while probing \`${command} ${args.join(' ')}\`.`,
744
+ ),
745
+ )
746
+ }, timeoutMs)
747
+
748
+ child.stdout.on('data', (chunk) => {
749
+ stdout += chunk.toString()
750
+ })
751
+
752
+ child.stderr.on('data', (chunk) => {
753
+ stderr += chunk.toString()
754
+ })
755
+
756
+ child.once('error', (error) => {
757
+ if (settled) {
758
+ return
759
+ }
760
+
761
+ settled = true
762
+ clearTimeout(timeout)
763
+ reject(
764
+ new Error(
765
+ error.code === 'ENOENT'
766
+ ? 'The `codex` command is not installed on this machine.'
767
+ : error.message,
768
+ ),
769
+ )
770
+ })
771
+
772
+ child.once('close', (code, signal) => {
773
+ if (settled) {
774
+ return
775
+ }
776
+
777
+ settled = true
778
+ clearTimeout(timeout)
779
+ resolve({
780
+ code,
781
+ signal,
782
+ stderr: stderr.trim(),
783
+ stdout: stdout.trim(),
784
+ })
785
+ })
786
+ })
787
+ }
788
+
789
+ function normalizeCodexVersion(versionText) {
790
+ const normalized = versionText.trim().replace(/^codex-cli\s+/i, '')
791
+ return normalized || 'unknown'
792
+ }
793
+
794
+ function buildCodexAppServerExitError(code, signal, stderrMessage) {
795
+ const exitDetail =
796
+ typeof code === 'number'
797
+ ? `exit code ${code}`
798
+ : signal
799
+ ? `signal ${signal}`
800
+ : 'no exit code'
801
+
802
+ const message = [`Codex app-server exited unexpectedly (${exitDetail}).`]
803
+ if (stderrMessage) {
804
+ message.push(stderrMessage)
805
+ }
806
+
807
+ return new Error(message.join(' '))
808
+ }
809
+
601
810
  async function writeConfig(codexHome, config) {
602
811
  await mkdir(codexHome, { recursive: true })
603
812
  await writeFile(
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "codex-usage-dashboard",
3
3
  "private": false,
4
- "version": "0.1.0",
4
+ "version": "0.1.1",
5
5
  "files": [
6
6
  "README.md",
7
7
  "bin"
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "@fontsource-variable/geist": "^5.2.8",
30
+ "@openai/codex": "^0.118.0",
30
31
  "@radix-ui/react-slot": "^1.2.4",
31
32
  "@supabase/supabase-js": "^2.101.1",
32
33
  "@tanstack/react-query": "^5.96.2",