codex-usage-dashboard 0.1.1 → 0.1.3
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 +16 -2
- package/bin/codex-usage.js +256 -65
- package/package.json +23 -21
package/README.md
CHANGED
|
@@ -50,7 +50,13 @@ The Vite dev server now serves the local pairing and sync API routes under `/api
|
|
|
50
50
|
Run this on any machine:
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
npx codex-usage-dashboard@latest connect --site "https://
|
|
53
|
+
npx codex-usage-dashboard@latest connect --site "https://codex-use-age.vercel.app"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For local development in this repo, use:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npm run connect -- --site "http://localhost:5173"
|
|
54
60
|
```
|
|
55
61
|
|
|
56
62
|
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.
|
|
@@ -81,7 +87,7 @@ If the same machine is already connected, rerun the same command to reopen the d
|
|
|
81
87
|
The generated command looks like:
|
|
82
88
|
|
|
83
89
|
```bash
|
|
84
|
-
npx codex-usage-dashboard@latest pair "https://
|
|
90
|
+
npx codex-usage-dashboard@latest pair "https://codex-use-age.vercel.app/api/pair/complete?token=..."
|
|
85
91
|
```
|
|
86
92
|
|
|
87
93
|
## Live sync after pairing
|
|
@@ -101,6 +107,14 @@ The saved device token is read from the same Codex home that the CLI uses.
|
|
|
101
107
|
- `npm run sync -- --watch`: local version of the hosted sync command
|
|
102
108
|
- `npm run collector`: legacy single-operator collector script
|
|
103
109
|
|
|
110
|
+
## Publishing
|
|
111
|
+
|
|
112
|
+
1. Run `npm version patch` (or `minor` / `major`) before every new publish.
|
|
113
|
+
2. Run `npm run release:check` to verify the tarball contents and confirm the version is still unpublished.
|
|
114
|
+
3. Run `npm publish`.
|
|
115
|
+
|
|
116
|
+
`npm publish` cannot overwrite an existing version on npm, so repeating a publish for the same version will fail with `E403`.
|
|
117
|
+
|
|
104
118
|
## Tario-specific Git workflow for Codex
|
|
105
119
|
|
|
106
120
|
If the current machine or user context indicates Tario is the operator, treat that as a master-by-default environment.
|
package/bin/codex-usage.js
CHANGED
|
@@ -290,20 +290,20 @@ async function runPairCommand(args) {
|
|
|
290
290
|
}),
|
|
291
291
|
})
|
|
292
292
|
|
|
293
|
-
const payload = await
|
|
293
|
+
const payload = await parseResponseBody(response)
|
|
294
294
|
if (!response.ok) {
|
|
295
|
-
throw new Error(payload
|
|
295
|
+
throw new Error(buildHttpErrorMessage(response, payload, 'Pairing failed.'))
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
const config = {
|
|
299
299
|
authMode: 'website-paired',
|
|
300
300
|
codexHome,
|
|
301
|
-
deviceId: payload.deviceId,
|
|
302
|
-
deviceToken: payload.deviceToken,
|
|
301
|
+
deviceId: payload.data.deviceId,
|
|
302
|
+
deviceToken: payload.data.deviceToken,
|
|
303
303
|
dashboardOrigin: new URL(pairUrl).origin,
|
|
304
304
|
label: device.label,
|
|
305
|
-
pollMs: payload.pollMs ?? DEFAULT_POLL_MS,
|
|
306
|
-
syncUrl: payload.syncUrl,
|
|
305
|
+
pollMs: payload.data.pollMs ?? DEFAULT_POLL_MS,
|
|
306
|
+
syncUrl: payload.data.syncUrl,
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
await writeConfig(codexHome, config)
|
|
@@ -325,71 +325,71 @@ async function runConnectCommand(args) {
|
|
|
325
325
|
const codexHome = resolveCodexHome(args.options['codex-home'])
|
|
326
326
|
const existingConfig = await readConfig(codexHome)
|
|
327
327
|
const client = new StdioCodexClient({ codexHome })
|
|
328
|
+
const siteOriginFromArgs = resolveSiteOrigin(args.options.site)
|
|
328
329
|
|
|
329
330
|
try {
|
|
330
331
|
await client.connect()
|
|
331
332
|
|
|
332
333
|
if (existingConfig) {
|
|
333
|
-
|
|
334
|
+
try {
|
|
335
|
+
const dashboardUrl = await resolveExistingDashboardUrl(existingConfig, args)
|
|
336
|
+
let dashboardOpenState = null
|
|
334
337
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
+
if (dashboardUrl) {
|
|
339
|
+
dashboardOpenState = await openDashboard(dashboardUrl)
|
|
340
|
+
}
|
|
338
341
|
|
|
339
|
-
|
|
340
|
-
|
|
342
|
+
await syncOnce(client, existingConfig, args)
|
|
343
|
+
logDashboardOpenState(dashboardOpenState)
|
|
341
344
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
+
if (args.options.watch) {
|
|
346
|
+
await runWatchLoop(client, existingConfig, args)
|
|
347
|
+
}
|
|
345
348
|
|
|
346
|
-
|
|
349
|
+
return
|
|
350
|
+
} catch (error) {
|
|
351
|
+
if (!isRevokedDeviceError(error)) {
|
|
352
|
+
throw error
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const siteOrigin =
|
|
356
|
+
siteOriginFromArgs ??
|
|
357
|
+
existingConfig.dashboardOrigin ??
|
|
358
|
+
deriveOriginFromUrl(existingConfig.syncUrl)
|
|
359
|
+
|
|
360
|
+
if (!siteOrigin) {
|
|
361
|
+
throw new Error(
|
|
362
|
+
`This device was unlinked from the dashboard. Rerun \`${NPX_COMMAND} connect --site "https://codex-use-age.vercel.app"\` to create a new connection.`,
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
console.log(
|
|
367
|
+
'This device was unlinked from the dashboard. Creating a new connection...',
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
const refreshedConfig = await startConnectFlow(
|
|
371
|
+
client,
|
|
372
|
+
args,
|
|
373
|
+
codexHome,
|
|
374
|
+
siteOrigin,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
if (args.options.watch) {
|
|
378
|
+
await runWatchLoop(client, refreshedConfig, args)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return
|
|
382
|
+
}
|
|
347
383
|
}
|
|
348
384
|
|
|
349
|
-
const siteOrigin =
|
|
385
|
+
const siteOrigin = siteOriginFromArgs
|
|
350
386
|
if (!siteOrigin) {
|
|
351
387
|
throw new Error(
|
|
352
388
|
'Pass --site <url> the first time you run connect, or set CODEX_USAGE_SITE_URL.',
|
|
353
389
|
)
|
|
354
390
|
}
|
|
355
391
|
|
|
356
|
-
const
|
|
357
|
-
const device = buildDevicePayload(args, codexHome)
|
|
358
|
-
const response = await fetch(new URL('/api/connect/start', siteOrigin), {
|
|
359
|
-
method: 'POST',
|
|
360
|
-
headers: {
|
|
361
|
-
'Content-Type': 'application/json',
|
|
362
|
-
},
|
|
363
|
-
body: JSON.stringify({
|
|
364
|
-
accountState: snapshot.accountState,
|
|
365
|
-
device,
|
|
366
|
-
rateLimits: snapshot.rateLimits,
|
|
367
|
-
}),
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
const payload = await parseJsonResponse(response)
|
|
371
|
-
if (!response.ok) {
|
|
372
|
-
throw new Error(payload.error ?? 'Unable to connect this machine.')
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
const config = {
|
|
376
|
-
authMode: 'guest-link',
|
|
377
|
-
codexHome,
|
|
378
|
-
dashboardOrigin: siteOrigin,
|
|
379
|
-
deviceId: payload.deviceId,
|
|
380
|
-
deviceToken: payload.deviceToken,
|
|
381
|
-
label: device.label,
|
|
382
|
-
pollMs: payload.pollMs ?? DEFAULT_POLL_MS,
|
|
383
|
-
syncUrl: payload.syncUrl,
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
await writeConfig(codexHome, config)
|
|
387
|
-
await openDashboard(payload.dashboardUrl)
|
|
388
|
-
console.log('Dashboard opened.')
|
|
389
|
-
console.log(`Config saved to ${resolveConfigPath(codexHome)}`)
|
|
390
|
-
console.log(
|
|
391
|
-
`Next: rerun \`${NPX_COMMAND} connect --site "${siteOrigin}"\` to reopen this dashboard, or \`${NPX_COMMAND} sync --watch\` for live updates only.`,
|
|
392
|
-
)
|
|
392
|
+
const config = await startConnectFlow(client, args, codexHome, siteOrigin)
|
|
393
393
|
|
|
394
394
|
if (args.options.watch) {
|
|
395
395
|
await runWatchLoop(client, config, args)
|
|
@@ -399,6 +399,50 @@ async function runConnectCommand(args) {
|
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
async function startConnectFlow(client, args, codexHome, siteOrigin) {
|
|
403
|
+
const snapshot = await readSnapshot(client, true)
|
|
404
|
+
const device = buildDevicePayload(args, codexHome)
|
|
405
|
+
const response = await fetch(new URL('/api/connect/start', siteOrigin), {
|
|
406
|
+
method: 'POST',
|
|
407
|
+
headers: {
|
|
408
|
+
'Content-Type': 'application/json',
|
|
409
|
+
},
|
|
410
|
+
body: JSON.stringify({
|
|
411
|
+
accountState: snapshot.accountState,
|
|
412
|
+
device,
|
|
413
|
+
rateLimits: snapshot.rateLimits,
|
|
414
|
+
}),
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
const payload = await parseResponseBody(response)
|
|
418
|
+
if (!response.ok) {
|
|
419
|
+
throw new Error(
|
|
420
|
+
buildHttpErrorMessage(response, payload, 'Unable to connect this machine.'),
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const config = {
|
|
425
|
+
authMode: 'guest-link',
|
|
426
|
+
codexHome,
|
|
427
|
+
dashboardOrigin: siteOrigin,
|
|
428
|
+
deviceId: payload.data.deviceId,
|
|
429
|
+
deviceToken: payload.data.deviceToken,
|
|
430
|
+
label: device.label,
|
|
431
|
+
pollMs: payload.data.pollMs ?? DEFAULT_POLL_MS,
|
|
432
|
+
syncUrl: payload.data.syncUrl,
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
await writeConfig(codexHome, config)
|
|
436
|
+
const dashboardOpenState = await openDashboard(payload.data.dashboardUrl)
|
|
437
|
+
logDashboardOpenState(dashboardOpenState)
|
|
438
|
+
console.log(`Config saved to ${resolveConfigPath(codexHome)}`)
|
|
439
|
+
console.log(
|
|
440
|
+
`Next: rerun \`${NPX_COMMAND} connect --site "${siteOrigin}"\` to reopen this dashboard, or \`${NPX_COMMAND} sync --watch\` for live updates only.`,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
return config
|
|
444
|
+
}
|
|
445
|
+
|
|
402
446
|
async function runSyncCommand(args) {
|
|
403
447
|
const codexHome = resolveCodexHome(args.options['codex-home'])
|
|
404
448
|
const config = await readConfig(codexHome)
|
|
@@ -501,9 +545,9 @@ async function syncOnce(client, config, args) {
|
|
|
501
545
|
}),
|
|
502
546
|
})
|
|
503
547
|
|
|
504
|
-
const payload = await
|
|
548
|
+
const payload = await parseResponseBody(response)
|
|
505
549
|
if (!response.ok) {
|
|
506
|
-
throw new Error(payload
|
|
550
|
+
throw new Error(buildHttpErrorMessage(response, payload, 'Sync failed.'))
|
|
507
551
|
}
|
|
508
552
|
}
|
|
509
553
|
|
|
@@ -519,12 +563,14 @@ async function resolveExistingDashboardUrl(config, args) {
|
|
|
519
563
|
}),
|
|
520
564
|
})
|
|
521
565
|
|
|
522
|
-
const payload = await
|
|
566
|
+
const payload = await parseResponseBody(response)
|
|
523
567
|
if (!response.ok) {
|
|
524
|
-
throw new Error(
|
|
568
|
+
throw new Error(
|
|
569
|
+
buildHttpErrorMessage(response, payload, 'Unable to open the dashboard.'),
|
|
570
|
+
)
|
|
525
571
|
}
|
|
526
572
|
|
|
527
|
-
return payload.dashboardUrl ?? null
|
|
573
|
+
return payload.data.dashboardUrl ?? null
|
|
528
574
|
}
|
|
529
575
|
|
|
530
576
|
const siteOrigin =
|
|
@@ -571,8 +617,21 @@ function buildDevicePayload(args, codexHome, fallbackLabel) {
|
|
|
571
617
|
async function openDashboard(url) {
|
|
572
618
|
try {
|
|
573
619
|
await openInBrowser(url)
|
|
620
|
+
return 'browser'
|
|
574
621
|
} catch {
|
|
575
622
|
console.log(`Open this URL in your browser: ${url}`)
|
|
623
|
+
return 'manual'
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function logDashboardOpenState(state) {
|
|
628
|
+
if (state === 'browser') {
|
|
629
|
+
console.log('Dashboard URL sent to your browser.')
|
|
630
|
+
return
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (state === 'manual') {
|
|
634
|
+
console.log('Dashboard URL ready.')
|
|
576
635
|
}
|
|
577
636
|
}
|
|
578
637
|
|
|
@@ -616,6 +675,18 @@ function resolveSiteOrigin(configuredValue) {
|
|
|
616
675
|
return new URL(value).origin
|
|
617
676
|
}
|
|
618
677
|
|
|
678
|
+
function deriveOriginFromUrl(value) {
|
|
679
|
+
if (typeof value !== 'string' || !value) {
|
|
680
|
+
return null
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
return new URL(value).origin
|
|
685
|
+
} catch {
|
|
686
|
+
return null
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
619
690
|
function resolveCodexHome(configuredValue) {
|
|
620
691
|
const home = configuredValue ?? process.env.CODEX_HOME ?? path.join(os.homedir(), '.codex')
|
|
621
692
|
|
|
@@ -822,16 +893,136 @@ async function readConfig(codexHome) {
|
|
|
822
893
|
return null
|
|
823
894
|
}
|
|
824
895
|
|
|
825
|
-
|
|
896
|
+
const rawConfig = JSON.parse(await readFile(configPath, 'utf8'))
|
|
897
|
+
const normalizedConfig = normalizeConfig(rawConfig)
|
|
898
|
+
|
|
899
|
+
if (JSON.stringify(normalizedConfig) !== JSON.stringify(rawConfig)) {
|
|
900
|
+
await writeConfig(codexHome, normalizedConfig)
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
return normalizedConfig
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function normalizeConfig(config) {
|
|
907
|
+
if (!config || typeof config !== 'object') {
|
|
908
|
+
return config
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const normalizedConfig = { ...config }
|
|
912
|
+
|
|
913
|
+
if (
|
|
914
|
+
!normalizedConfig.dashboardOrigin &&
|
|
915
|
+
typeof normalizedConfig.syncUrl === 'string'
|
|
916
|
+
) {
|
|
917
|
+
try {
|
|
918
|
+
normalizedConfig.dashboardOrigin = new URL(
|
|
919
|
+
normalizedConfig.syncUrl,
|
|
920
|
+
).origin
|
|
921
|
+
} catch {
|
|
922
|
+
// Leave the saved origin untouched when the sync URL is malformed.
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
if (looksLikeLegacyGuestLinkConfig(config)) {
|
|
927
|
+
normalizedConfig.authMode = 'guest-link'
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return normalizedConfig
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function looksLikeLegacyGuestLinkConfig(config) {
|
|
934
|
+
return (
|
|
935
|
+
!config.authMode &&
|
|
936
|
+
!config.dashboardOrigin &&
|
|
937
|
+
typeof config.deviceToken === 'string' &&
|
|
938
|
+
config.deviceToken.length > 0 &&
|
|
939
|
+
typeof config.syncUrl === 'string' &&
|
|
940
|
+
config.syncUrl.length > 0
|
|
941
|
+
)
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
async function parseResponseBody(response) {
|
|
945
|
+
const text = (await response.text().catch(() => '')).trim()
|
|
946
|
+
if (!text) {
|
|
947
|
+
return { data: {}, text: '' }
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
try {
|
|
951
|
+
const parsed = JSON.parse(text)
|
|
952
|
+
if (parsed && typeof parsed === 'object') {
|
|
953
|
+
return { data: parsed, text }
|
|
954
|
+
}
|
|
955
|
+
} catch {
|
|
956
|
+
// Fall back to the raw text when the response body is not JSON.
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
return { data: {}, text }
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function buildHttpErrorMessage(response, payload, fallbackMessage) {
|
|
963
|
+
const bodyError =
|
|
964
|
+
typeof payload.data?.error === 'string' && payload.data.error.trim()
|
|
965
|
+
? payload.data.error.trim()
|
|
966
|
+
: null
|
|
967
|
+
const plainText =
|
|
968
|
+
payload.text && !looksLikeHtml(payload.text)
|
|
969
|
+
? payload.text.replace(/\s+/g, ' ').trim()
|
|
970
|
+
: ''
|
|
971
|
+
const statusLabel = `${response.status}${response.statusText ? ` ${response.statusText}` : ''}`
|
|
972
|
+
const vercelError = response.headers.get('x-vercel-error')
|
|
973
|
+
const vercelId = response.headers.get('x-vercel-id')
|
|
974
|
+
|
|
975
|
+
const detail = bodyError ?? truncateText(plainText, 240)
|
|
976
|
+
let message = detail
|
|
977
|
+
? `${fallbackMessage} ${detail}`
|
|
978
|
+
: `${fallbackMessage} HTTP ${statusLabel}.`
|
|
979
|
+
|
|
980
|
+
if (!detail) {
|
|
981
|
+
return appendHttpContext(message, vercelError, vercelId)
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
if (!bodyError) {
|
|
985
|
+
message = `${message} (HTTP ${statusLabel})`
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
return appendHttpContext(message, vercelError, vercelId)
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function isRevokedDeviceError(error) {
|
|
992
|
+
return (
|
|
993
|
+
error instanceof Error &&
|
|
994
|
+
error.message.includes('This device is no longer authorized.')
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function appendHttpContext(message, vercelError, vercelId) {
|
|
999
|
+
const context = []
|
|
1000
|
+
|
|
1001
|
+
if (vercelError) {
|
|
1002
|
+
context.push(`Vercel error: ${vercelError}`)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (vercelId) {
|
|
1006
|
+
context.push(`request id: ${vercelId}`)
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (!context.length) {
|
|
1010
|
+
return message
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return `${message} [${context.join('; ')}]`
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function looksLikeHtml(text) {
|
|
1017
|
+
return /^<!doctype html>|^<html[\s>]/i.test(text)
|
|
826
1018
|
}
|
|
827
1019
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
return {}
|
|
1020
|
+
function truncateText(text, maxLength) {
|
|
1021
|
+
if (text.length <= maxLength) {
|
|
1022
|
+
return text
|
|
832
1023
|
}
|
|
833
1024
|
|
|
834
|
-
return
|
|
1025
|
+
return `${text.slice(0, maxLength - 1)}…`
|
|
835
1026
|
}
|
|
836
1027
|
|
|
837
1028
|
function waitForTermination(cleanup) {
|
package/package.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-usage-dashboard",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3",
|
|
5
5
|
"files": [
|
|
6
6
|
"README.md",
|
|
7
7
|
"bin"
|
|
8
8
|
],
|
|
9
9
|
"type": "module",
|
|
10
10
|
"bin": {
|
|
11
|
-
"codex-usage": "
|
|
12
|
-
"codex-usage-dashboard": "
|
|
11
|
+
"codex-usage": "bin/codex-usage.js",
|
|
12
|
+
"codex-usage-dashboard": "bin/codex-usage.js"
|
|
13
13
|
},
|
|
14
14
|
"scripts": {
|
|
15
15
|
"dev": "vite",
|
|
@@ -23,45 +23,47 @@
|
|
|
23
23
|
"setup:hosted": "tsx scripts/setup-hosted.ts",
|
|
24
24
|
"setup:local": "tsx scripts/setup-local.ts",
|
|
25
25
|
"collector": "tsx scripts/codex-collector.ts",
|
|
26
|
-
"collector:once": "tsx scripts/codex-collector.ts --once"
|
|
26
|
+
"collector:once": "tsx scripts/codex-collector.ts --once",
|
|
27
|
+
"release:check": "node ./scripts/check-publish.mjs",
|
|
28
|
+
"prepublishOnly": "npm run release:check"
|
|
27
29
|
},
|
|
28
30
|
"dependencies": {
|
|
31
|
+
"@openai/codex": "^0.118.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@eslint/js": "^9.39.4",
|
|
29
35
|
"@fontsource-variable/geist": "^5.2.8",
|
|
30
|
-
"@openai/codex": "^0.118.0",
|
|
31
36
|
"@radix-ui/react-slot": "^1.2.4",
|
|
37
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
32
38
|
"@supabase/supabase-js": "^2.101.1",
|
|
33
39
|
"@tanstack/react-query": "^5.96.2",
|
|
34
40
|
"@tanstack/react-query-devtools": "^5.96.2",
|
|
35
41
|
"@tanstack/react-router": "^1.168.10",
|
|
42
|
+
"@tanstack/router-plugin": "^1.167.12",
|
|
36
43
|
"@tanstack/router-devtools": "^1.166.11",
|
|
44
|
+
"@types/node": "^24.12.0",
|
|
45
|
+
"@types/react": "^19.2.14",
|
|
46
|
+
"@types/react-dom": "^19.2.3",
|
|
47
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
37
48
|
"class-variance-authority": "^0.7.1",
|
|
38
49
|
"clsx": "^2.1.1",
|
|
39
50
|
"dotenv": "^17.4.1",
|
|
51
|
+
"eslint": "^9.39.4",
|
|
52
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
53
|
+
"eslint-plugin-react-refresh": "^0.5.2",
|
|
54
|
+
"globals": "^17.4.0",
|
|
40
55
|
"lucide-react": "^1.7.0",
|
|
41
56
|
"radix-ui": "^1.4.3",
|
|
42
57
|
"react": "^19.2.4",
|
|
43
58
|
"react-dom": "^19.2.4",
|
|
44
59
|
"shadcn": "^4.1.2",
|
|
60
|
+
"tailwindcss": "^4.2.2",
|
|
45
61
|
"tailwind-merge": "^3.5.0",
|
|
46
62
|
"tw-animate-css": "^1.4.0",
|
|
47
|
-
"zod": "^4.3.6"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@eslint/js": "^9.39.4",
|
|
51
|
-
"@tailwindcss/vite": "^4.2.2",
|
|
52
|
-
"@tanstack/router-plugin": "^1.167.12",
|
|
53
|
-
"@types/node": "^24.12.0",
|
|
54
|
-
"@types/react": "^19.2.14",
|
|
55
|
-
"@types/react-dom": "^19.2.3",
|
|
56
|
-
"@vitejs/plugin-react": "^6.0.1",
|
|
57
|
-
"eslint": "^9.39.4",
|
|
58
|
-
"eslint-plugin-react-hooks": "^7.0.1",
|
|
59
|
-
"eslint-plugin-react-refresh": "^0.5.2",
|
|
60
|
-
"globals": "^17.4.0",
|
|
61
|
-
"tailwindcss": "^4.2.2",
|
|
62
63
|
"tsx": "^4.21.0",
|
|
63
64
|
"typescript": "~5.9.3",
|
|
64
65
|
"typescript-eslint": "^8.57.0",
|
|
65
|
-
"vite": "^8.0.1"
|
|
66
|
+
"vite": "^8.0.1",
|
|
67
|
+
"zod": "^4.3.6"
|
|
66
68
|
}
|
|
67
69
|
}
|