@cortexkit/opencode-anthropic-auth 1.15.1 → 1.17.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.
- package/README.md +14 -6
- package/dist/cli.js +6 -6
- package/dist/fable-fallback.d.ts +2 -0
- package/dist/{index-qejbx8c9.js → index-63keqwnf.js} +1 -1
- package/dist/{index-jkbar3k3.js → index-am5pq04s.js} +1 -1
- package/dist/{index-qsaph2g4.js → index-f5r96vyv.js} +1 -1
- package/dist/index-gj7emmj5.js +2 -0
- package/dist/index-xvfesm3w.js +422 -0
- package/dist/{index-6gdd6e4s.js → index-y2n9h7p6.js} +1 -1
- package/dist/index.js +12 -12
- package/dist/{logger-vxnb5hjr.js → logger-ywfmddsw.js} +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state.d.ts +52 -2
- package/dist/sidebar-state.js +1 -1
- package/dist/tui/command-dialogs.d.ts +12 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/tui.d.ts +2 -0
- package/package.json +2 -2
- package/src/sidebar-state.ts +381 -4
- package/src/tui/command-dialogs.tsx +35 -13
- package/src/tui-compiled/sidebar-state.ts +381 -4
- package/src/tui-compiled/tui/command-dialogs.tsx +22 -10
- package/src/tui-compiled/tui.tsx +147 -104
- package/src/tui.tsx +34 -2
- package/dist/index-bxs26mw4.js +0 -2
- package/dist/index-z03wayzs.js +0 -417
package/dist/sidebar-state.d.ts
CHANGED
|
@@ -13,6 +13,23 @@ export interface AccountQuota {
|
|
|
13
13
|
five_hour?: QuotaWindow;
|
|
14
14
|
seven_day?: QuotaWindow;
|
|
15
15
|
scoped?: ScopedQuotaWindow[];
|
|
16
|
+
extraUsage?: {
|
|
17
|
+
used: {
|
|
18
|
+
amountMinor: number;
|
|
19
|
+
currency: string;
|
|
20
|
+
exponent: number;
|
|
21
|
+
};
|
|
22
|
+
limit: {
|
|
23
|
+
amountMinor: number;
|
|
24
|
+
currency: string;
|
|
25
|
+
exponent: number;
|
|
26
|
+
};
|
|
27
|
+
utilizationPercent?: number;
|
|
28
|
+
severity?: string;
|
|
29
|
+
exhausted: boolean;
|
|
30
|
+
};
|
|
31
|
+
bindingWindow?: string;
|
|
32
|
+
fallbackAdvised?: boolean;
|
|
16
33
|
}
|
|
17
34
|
export interface SidebarAccountState {
|
|
18
35
|
id: string;
|
|
@@ -20,6 +37,7 @@ export interface SidebarAccountState {
|
|
|
20
37
|
quota: AccountQuota | null;
|
|
21
38
|
enabled: boolean;
|
|
22
39
|
needsReauth: boolean;
|
|
40
|
+
tierLabel?: string;
|
|
23
41
|
}
|
|
24
42
|
export interface FableRecoverySidebarState {
|
|
25
43
|
sessionId: string;
|
|
@@ -30,6 +48,7 @@ export interface FableRecoverySidebarState {
|
|
|
30
48
|
export interface SidebarState {
|
|
31
49
|
main: {
|
|
32
50
|
quota: AccountQuota | null;
|
|
51
|
+
tierLabel?: string;
|
|
33
52
|
quotaBackedOff?: boolean;
|
|
34
53
|
quotaBackoffUntil?: number;
|
|
35
54
|
refreshBackedOff?: boolean;
|
|
@@ -54,8 +73,38 @@ export interface SidebarState {
|
|
|
54
73
|
export declare function getSidebarStateFile(): string;
|
|
55
74
|
export declare const DEFAULT_SIDEBAR_STATE: SidebarState;
|
|
56
75
|
export declare function normalizeSidebarState(raw: unknown): SidebarState;
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
interface SidebarStateWriteTestHooks {
|
|
77
|
+
afterMergeRead?: (stateFile: string) => void | Promise<void>;
|
|
78
|
+
beforeRename?: (stateFile: string, tempFile: string) => void | Promise<void>;
|
|
79
|
+
afterRename?: (stateFile: string) => void | Promise<void>;
|
|
80
|
+
afterStaleLockStat?: (lockDir: string) => void | Promise<void>;
|
|
81
|
+
onStaleLockClaimed?: (lockDir: string) => void | Promise<void>;
|
|
82
|
+
onLockAcquired?: (lockDir: string) => void | Promise<void>;
|
|
83
|
+
beforeReleaseDirectoryRemoval?: (lockDir: string) => void | Promise<void>;
|
|
84
|
+
lockBudgetMs?: number;
|
|
85
|
+
lockRetryMinMs?: number;
|
|
86
|
+
lockRetryMaxMs?: number;
|
|
87
|
+
}
|
|
88
|
+
export declare function __setSidebarStateWriteTestHooks(hooks: SidebarStateWriteTestHooks | null): void;
|
|
89
|
+
export interface InitialSidebarRoutingTestHooks {
|
|
90
|
+
beforeSidebarRead?: () => void | Promise<void>;
|
|
91
|
+
beforeStorageLoad?: () => void | Promise<void>;
|
|
92
|
+
afterStorageLoad?: () => void | Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
export declare function __setInitialSidebarRoutingTestHooks(hooks: InitialSidebarRoutingTestHooks | null): void;
|
|
95
|
+
export declare function getInitialSidebarRoutingTestHooks(): InitialSidebarRoutingTestHooks | null;
|
|
96
|
+
export declare function __acquireSidebarStateLockForTest(stateFile: string): Promise<(() => Promise<void>) | null>;
|
|
97
|
+
export declare function getSidebarState(stateFile?: string): Promise<SidebarState>;
|
|
98
|
+
export interface SidebarStateWriteOptions {
|
|
99
|
+
routingAuthoritative?: boolean;
|
|
100
|
+
resolvePreservedRouting?: (current: SidebarState) => (Pick<SidebarState, 'activeId' | 'route'> & {
|
|
101
|
+
state?: SidebarState;
|
|
102
|
+
}) | undefined | Promise<(Pick<SidebarState, 'activeId' | 'route'> & {
|
|
103
|
+
state?: SidebarState;
|
|
104
|
+
}) | undefined>;
|
|
105
|
+
onRoutingResolved?: (routing: Pick<SidebarState, 'activeId' | 'route'>) => void;
|
|
106
|
+
}
|
|
107
|
+
export declare function setSidebarState(state: SidebarState, stateFile?: string, options?: SidebarStateWriteOptions): Promise<void>;
|
|
59
108
|
export declare function drainSidebarWrites(): Promise<void>;
|
|
60
109
|
export declare function resolveActiveAccount(state: SidebarState): {
|
|
61
110
|
id: string;
|
|
@@ -79,3 +128,4 @@ export interface QuotaPacing {
|
|
|
79
128
|
runsOutAt: string | null;
|
|
80
129
|
}
|
|
81
130
|
export declare function computeQuotaPacing(window: QuotaWindow, windowMs: number, now: number): QuotaPacing | null;
|
|
131
|
+
export {};
|
package/dist/sidebar-state.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q}from"./index-gj7emmj5.js";import"./index-xvfesm3w.js";import"./index-y2n9h7p6.js";import"./index-f5r96vyv.js";export{i as setSidebarState,k as resolveActiveAccount,c as normalizeSidebarState,a as getSidebarStateFile,h as getSidebarState,f as getInitialSidebarRoutingTestHooks,m as getFableRecoverySummary,n as getCollapsedQuotaSummary,l as formatScopedQuotaLabel,j as drainSidebarWrites,q as computeQuotaPacing,d as __setSidebarStateWriteTestHooks,e as __setInitialSidebarRoutingTestHooks,g as __acquireSidebarStateLockForTest,p as SEVEN_DAY_MS,o as FIVE_HOUR_MS,b as DEFAULT_SIDEBAR_STATE};
|
|
@@ -11,5 +11,17 @@ type KillswitchDialogConfig = {
|
|
|
11
11
|
accounts?: Record<string, Record<string, number>>;
|
|
12
12
|
};
|
|
13
13
|
export declare function buildKillswitchThresholdSeed(config: KillswitchDialogConfig, accountIds: string[]): string;
|
|
14
|
+
export declare function buildAccountDialogOption(account: {
|
|
15
|
+
id: string;
|
|
16
|
+
label: string;
|
|
17
|
+
role: string;
|
|
18
|
+
enabled: boolean;
|
|
19
|
+
quotaPercent: number | null;
|
|
20
|
+
tierLabel?: string;
|
|
21
|
+
}): {
|
|
22
|
+
description?: string | undefined;
|
|
23
|
+
title: string;
|
|
24
|
+
value: string;
|
|
25
|
+
};
|
|
14
26
|
export declare function openCommandDialog(api: TuiPluginApi, payload: OpenDialogPayload, apply: ApplyFn): void;
|
|
15
27
|
export {};
|
package/dist/tui-preferences.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{pc as c,qc as C,rc as h}from"./index-f5r96vyv.js";var m=C((v,k)=>{(function(Q){if(typeof k==="object"&&typeof k.exports==="object"){var J=Q(h,v);if(J!==void 0)k.exports=J}else if(typeof define==="function"&&define.amd)define(["require","exports","./impl/format","./impl/edit","./impl/scanner","./impl/parser"],Q)})(function(Q,J){Object.defineProperty(J,"__esModule",{value:!0}),J.applyEdits=J.modify=J.format=J.printParseErrorCode=J.ParseErrorCode=J.stripComments=J.visit=J.getNodeValue=J.getNodePath=J.findNodeAtOffset=J.findNodeAtLocation=J.parseTree=J.parse=J.getLocation=J.SyntaxKind=J.ScanError=J.createScanner=void 0;let X=Q("./impl/format"),Z=Q("./impl/edit"),$=Q("./impl/scanner"),G=Q("./impl/parser");J.createScanner=$.createScanner;var V;(function(z){z[z.None=0]="None",z[z.UnexpectedEndOfComment=1]="UnexpectedEndOfComment",z[z.UnexpectedEndOfString=2]="UnexpectedEndOfString",z[z.UnexpectedEndOfNumber=3]="UnexpectedEndOfNumber",z[z.InvalidUnicode=4]="InvalidUnicode",z[z.InvalidEscapeCharacter=5]="InvalidEscapeCharacter",z[z.InvalidCharacter=6]="InvalidCharacter"})(V||(J.ScanError=V={}));var Y;(function(z){z[z.OpenBraceToken=1]="OpenBraceToken",z[z.CloseBraceToken=2]="CloseBraceToken",z[z.OpenBracketToken=3]="OpenBracketToken",z[z.CloseBracketToken=4]="CloseBracketToken",z[z.CommaToken=5]="CommaToken",z[z.ColonToken=6]="ColonToken",z[z.NullKeyword=7]="NullKeyword",z[z.TrueKeyword=8]="TrueKeyword",z[z.FalseKeyword=9]="FalseKeyword",z[z.StringLiteral=10]="StringLiteral",z[z.NumericLiteral=11]="NumericLiteral",z[z.LineCommentTrivia=12]="LineCommentTrivia",z[z.BlockCommentTrivia=13]="BlockCommentTrivia",z[z.LineBreakTrivia=14]="LineBreakTrivia",z[z.Trivia=15]="Trivia",z[z.Unknown=16]="Unknown",z[z.EOF=17]="EOF"})(Y||(J.SyntaxKind=Y={})),J.getLocation=G.getLocation,J.parse=G.parse,J.parseTree=G.parseTree,J.findNodeAtLocation=G.findNodeAtLocation,J.findNodeAtOffset=G.findNodeAtOffset,J.getNodePath=G.getNodePath,J.getNodeValue=G.getNodeValue,J.visit=G.visit,J.stripComments=G.stripComments;var L;(function(z){z[z.InvalidSymbol=1]="InvalidSymbol",z[z.InvalidNumberFormat=2]="InvalidNumberFormat",z[z.PropertyNameExpected=3]="PropertyNameExpected",z[z.ValueExpected=4]="ValueExpected",z[z.ColonExpected=5]="ColonExpected",z[z.CommaExpected=6]="CommaExpected",z[z.CloseBraceExpected=7]="CloseBraceExpected",z[z.CloseBracketExpected=8]="CloseBracketExpected",z[z.EndOfFileExpected=9]="EndOfFileExpected",z[z.InvalidCommentToken=10]="InvalidCommentToken",z[z.UnexpectedEndOfComment=11]="UnexpectedEndOfComment",z[z.UnexpectedEndOfString=12]="UnexpectedEndOfString",z[z.UnexpectedEndOfNumber=13]="UnexpectedEndOfNumber",z[z.InvalidUnicode=14]="InvalidUnicode",z[z.InvalidEscapeCharacter=15]="InvalidEscapeCharacter",z[z.InvalidCharacter=16]="InvalidCharacter"})(L||(J.ParseErrorCode=L={}));function B(z){switch(z){case 1:return"InvalidSymbol";case 2:return"InvalidNumberFormat";case 3:return"PropertyNameExpected";case 4:return"ValueExpected";case 5:return"ColonExpected";case 6:return"CommaExpected";case 7:return"CloseBraceExpected";case 8:return"CloseBracketExpected";case 9:return"EndOfFileExpected";case 10:return"InvalidCommentToken";case 11:return"UnexpectedEndOfComment";case 12:return"UnexpectedEndOfString";case 13:return"UnexpectedEndOfNumber";case 14:return"InvalidUnicode";case 15:return"InvalidEscapeCharacter";case 16:return"InvalidCharacter"}return"<unknown ParseErrorCode>"}J.printParseErrorCode=B;function j(z,H,N){return X.format(z,H,N)}J.format=j;function A(z,H,N,R){return Z.setProperty(z,H,N,R)}J.modify=A;function O(z,H){let N=H.slice(0).sort((U,D)=>{let T=U.offset-D.offset;if(T===0)return U.length-D.length;return T}),R=z.length;for(let U=N.length-1;U>=0;U--){let D=N[U];if(D.offset+D.length<=R)z=Z.applyEdit(z,D);else throw Error("Overlapping edit");R=D.offset}return z}J.applyEdits=O})});var q=c(m(),1);import{watch as y}from"node:fs";import{mkdir as S,readFile as w,rename as E,writeFile as p}from"node:fs/promises";import{homedir as l}from"node:os";import{basename as x,dirname as P,join as _}from"node:path";import{clearTimeout as F,setTimeout as f}from"node:timers";var i="OPENCODE_TUI_PREFERENCES_FILE",s="tui-preferences.jsonc";function b(){let Q=process.env[i];if(Q)return Q;let J=process.env.OPENCODE_CONFIG_DIR||_(process.env.XDG_CONFIG_HOME||_(l(),".config"),"opencode");return _(J,s)}function I(Q){return typeof Q==="object"&&Q!==null&&!Array.isArray(Q)}async function Vz(){try{let Q=await w(b(),"utf8"),J=[],X=q.parse(Q,J,{allowTrailingComma:!0});if(J.length>0)return{};return I(X)?X:{}}catch{return{}}}var d="anthropic-auth",n=160,u={forceToTop:!1,order:n,startCollapsed:!1,rememberCollapsed:!0,collapsed:null,pollMs:1500,refreshDebounceMs:200,header:{label:"CLAUDE",showVersion:!0},sections:{quota:!0,fallbackAccounts:!0,routing:!0,cache:!0,health:!0,pacing:!0},appearance:{barWidth:10,barFilledChar:"█",barEmptyChar:"░",warnThreshold:50,errorThreshold:80}};function W(Q,J){return typeof Q==="boolean"?Q:J}function M(Q,J,X,Z){if(typeof Q!=="number"||!Number.isFinite(Q))return J;return Math.min(Math.max(Math.round(Q),X),Z)}function a(Q,J,X){if(typeof Q!=="string"||Q.length===0)return J;return Q.slice(0,X)}function K(Q,J){if(typeof Q!=="string")return J;return[...Q][0]??J}function Yz(Q){let J=Q[d];if(!I(J))return structuredClone(u);let X=u,Z=I(J.header)?J.header:{},$=I(J.sections)?J.sections:{},G=I(J.appearance)?J.appearance:{},V=M(G.warnThreshold,X.appearance.warnThreshold,0,99),Y=Math.max(M(G.errorThreshold,X.appearance.errorThreshold,0,100),Math.min(V+1,100));return{forceToTop:W(J.forceToTop,X.forceToTop),order:M(J.order,X.order,-1e4,1e4),startCollapsed:W(J.startCollapsed,X.startCollapsed),rememberCollapsed:W(J.rememberCollapsed,X.rememberCollapsed),collapsed:typeof J.collapsed==="boolean"?J.collapsed:null,pollMs:M(J.pollMs,X.pollMs,500,30000),refreshDebounceMs:M(J.refreshDebounceMs,X.refreshDebounceMs,50,5000),header:{label:a(Z.label,X.header.label,20),showVersion:W(Z.showVersion,X.header.showVersion)},sections:{quota:W($.quota,X.sections.quota),fallbackAccounts:W($.fallbackAccounts,X.sections.fallbackAccounts),routing:W($.routing,X.sections.routing),cache:W($.cache,X.sections.cache),health:W($.health,X.sections.health),pacing:W($.pacing,X.sections.pacing)},appearance:{barWidth:M(G.barWidth,X.appearance.barWidth,4,40),barFilledChar:K(G.barFilledChar,X.appearance.barFilledChar),barEmptyChar:K(G.barEmptyChar,X.appearance.barEmptyChar),warnThreshold:V,errorThreshold:Y}}}var t=-1e5;function Hz(Q,J,X){let Z=Q[J];if(!I(Z))return X;if(Z.forceToTop===!0)return t+Object.keys(Q).indexOf(J);return M(Z.order,X,-1e4,1e4)}var o=`// Shared preferences for opencode TUI plugins.
|
|
2
2
|
// One top-level key per plugin (short name). See each plugin's README for
|
|
3
3
|
// its supported settings. This file is safe to hand-edit; plugins update
|
|
4
4
|
// individual keys in place and preserve comments.
|
package/dist/tui.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cortexkit/opencode-anthropic-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"@opencode-ai/plugin": "*"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@cortexkit/anthropic-auth-core": "1.
|
|
64
|
+
"@cortexkit/anthropic-auth-core": "1.17.0",
|
|
65
65
|
"@opentui/core": "0.4.3",
|
|
66
66
|
"@opentui/solid": "0.4.3",
|
|
67
67
|
"jsonc-parser": "^3.3.1",
|
package/src/sidebar-state.ts
CHANGED
|
@@ -15,6 +15,15 @@ export interface AccountQuota {
|
|
|
15
15
|
five_hour?: QuotaWindow
|
|
16
16
|
seven_day?: QuotaWindow
|
|
17
17
|
scoped?: ScopedQuotaWindow[]
|
|
18
|
+
extraUsage?: {
|
|
19
|
+
used: { amountMinor: number; currency: string; exponent: number }
|
|
20
|
+
limit: { amountMinor: number; currency: string; exponent: number }
|
|
21
|
+
utilizationPercent?: number
|
|
22
|
+
severity?: string
|
|
23
|
+
exhausted: boolean
|
|
24
|
+
}
|
|
25
|
+
bindingWindow?: string
|
|
26
|
+
fallbackAdvised?: boolean
|
|
18
27
|
}
|
|
19
28
|
|
|
20
29
|
export interface SidebarAccountState {
|
|
@@ -25,6 +34,7 @@ export interface SidebarAccountState {
|
|
|
25
34
|
// True when the account's refresh token is permanently dead (400
|
|
26
35
|
// invalid_grant) and it needs a re-login — distinct from a transient backoff.
|
|
27
36
|
needsReauth: boolean
|
|
37
|
+
tierLabel?: string
|
|
28
38
|
}
|
|
29
39
|
|
|
30
40
|
export interface FableRecoverySidebarState {
|
|
@@ -37,6 +47,7 @@ export interface FableRecoverySidebarState {
|
|
|
37
47
|
export interface SidebarState {
|
|
38
48
|
main: {
|
|
39
49
|
quota: AccountQuota | null
|
|
50
|
+
tierLabel?: string
|
|
40
51
|
quotaBackedOff?: boolean
|
|
41
52
|
quotaBackoffUntil?: number
|
|
42
53
|
refreshBackedOff?: boolean
|
|
@@ -56,9 +67,20 @@ export interface SidebarState {
|
|
|
56
67
|
lastUpdated: number
|
|
57
68
|
}
|
|
58
69
|
|
|
59
|
-
import {
|
|
70
|
+
import { randomUUID } from 'node:crypto'
|
|
71
|
+
import {
|
|
72
|
+
mkdir,
|
|
73
|
+
readFile,
|
|
74
|
+
rename,
|
|
75
|
+
rm,
|
|
76
|
+
rmdir,
|
|
77
|
+
stat,
|
|
78
|
+
unlink,
|
|
79
|
+
writeFile,
|
|
80
|
+
} from 'node:fs/promises'
|
|
60
81
|
import { tmpdir } from 'node:os'
|
|
61
82
|
import { dirname, join } from 'node:path'
|
|
83
|
+
import { logger } from '@cortexkit/anthropic-auth-core'
|
|
62
84
|
|
|
63
85
|
const STATE_FILE_ENV = 'OPENCODE_ANTHROPIC_AUTH_SIDEBAR_STATE_FILE'
|
|
64
86
|
const DEFAULT_STATE_DIR = join(tmpdir(), 'opencode-anthropic-auth')
|
|
@@ -136,9 +158,53 @@ function normalizeAccountQuota(value: unknown): AccountQuota | null {
|
|
|
136
158
|
quota.scoped = scoped
|
|
137
159
|
}
|
|
138
160
|
|
|
161
|
+
if (isRecord(value.extraUsage)) {
|
|
162
|
+
const used = normalizeQuotaMoney(value.extraUsage.used)
|
|
163
|
+
const limit = normalizeQuotaMoney(value.extraUsage.limit)
|
|
164
|
+
if (used && limit && typeof value.extraUsage.exhausted === 'boolean') {
|
|
165
|
+
quota.extraUsage = {
|
|
166
|
+
used,
|
|
167
|
+
limit,
|
|
168
|
+
...(typeof value.extraUsage.utilizationPercent === 'number' &&
|
|
169
|
+
Number.isFinite(value.extraUsage.utilizationPercent) && {
|
|
170
|
+
utilizationPercent: value.extraUsage.utilizationPercent,
|
|
171
|
+
}),
|
|
172
|
+
...(typeof value.extraUsage.severity === 'string' && {
|
|
173
|
+
severity: value.extraUsage.severity,
|
|
174
|
+
}),
|
|
175
|
+
exhausted: value.extraUsage.exhausted,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (typeof value.bindingWindow === 'string' && value.bindingWindow.trim()) {
|
|
180
|
+
quota.bindingWindow = value.bindingWindow.trim()
|
|
181
|
+
}
|
|
182
|
+
if (typeof value.fallbackAdvised === 'boolean') {
|
|
183
|
+
quota.fallbackAdvised = value.fallbackAdvised
|
|
184
|
+
}
|
|
185
|
+
|
|
139
186
|
return Object.keys(quota).length ? quota : null
|
|
140
187
|
}
|
|
141
188
|
|
|
189
|
+
function normalizeQuotaMoney(value: unknown) {
|
|
190
|
+
if (!isRecord(value)) return undefined
|
|
191
|
+
if (
|
|
192
|
+
!Number.isInteger(value.amountMinor) ||
|
|
193
|
+
typeof value.currency !== 'string' ||
|
|
194
|
+
!/^[A-Za-z]{3}$/.test(value.currency.trim()) ||
|
|
195
|
+
!Number.isInteger(value.exponent) ||
|
|
196
|
+
(value.exponent as number) < 0 ||
|
|
197
|
+
(value.exponent as number) > 20
|
|
198
|
+
) {
|
|
199
|
+
return undefined
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
amountMinor: value.amountMinor as number,
|
|
203
|
+
currency: value.currency.trim(),
|
|
204
|
+
exponent: value.exponent as number,
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
142
208
|
export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
143
209
|
if (!isRecord(raw)) return { ...DEFAULT_SIDEBAR_STATE }
|
|
144
210
|
|
|
@@ -146,6 +212,9 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
146
212
|
if (isRecord(raw.main)) {
|
|
147
213
|
const m = raw.main
|
|
148
214
|
main.quota = normalizeAccountQuota(m.quota)
|
|
215
|
+
if (typeof m.tierLabel === 'string' && m.tierLabel.trim()) {
|
|
216
|
+
main.tierLabel = m.tierLabel.trim()
|
|
217
|
+
}
|
|
149
218
|
if (typeof m.quotaBackedOff === 'boolean')
|
|
150
219
|
main.quotaBackedOff = m.quotaBackedOff
|
|
151
220
|
if (typeof m.quotaBackoffUntil === 'number')
|
|
@@ -167,6 +236,10 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
167
236
|
enabled: typeof entry.enabled === 'boolean' ? entry.enabled : false,
|
|
168
237
|
needsReauth:
|
|
169
238
|
typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
|
|
239
|
+
tierLabel:
|
|
240
|
+
typeof entry.tierLabel === 'string' && entry.tierLabel.trim()
|
|
241
|
+
? entry.tierLabel.trim()
|
|
242
|
+
: undefined,
|
|
170
243
|
}))
|
|
171
244
|
: []
|
|
172
245
|
|
|
@@ -238,23 +311,327 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
|
|
|
238
311
|
|
|
239
312
|
let writeChain: Promise<void> = Promise.resolve()
|
|
240
313
|
|
|
241
|
-
|
|
314
|
+
// Wait through ordinary contention while staying below the 2s stale-eviction window.
|
|
315
|
+
const SIDEBAR_LOCK_BUDGET_MS = 1_000
|
|
316
|
+
const SIDEBAR_LOCK_STALE_MS = 2_000
|
|
317
|
+
const SIDEBAR_LOCK_RETRY_MIN_MS = 5
|
|
318
|
+
const SIDEBAR_LOCK_RETRY_MAX_MS = 15
|
|
319
|
+
|
|
320
|
+
interface SidebarStateWriteTestHooks {
|
|
321
|
+
afterMergeRead?: (stateFile: string) => void | Promise<void>
|
|
322
|
+
beforeRename?: (stateFile: string, tempFile: string) => void | Promise<void>
|
|
323
|
+
afterRename?: (stateFile: string) => void | Promise<void>
|
|
324
|
+
afterStaleLockStat?: (lockDir: string) => void | Promise<void>
|
|
325
|
+
onStaleLockClaimed?: (lockDir: string) => void | Promise<void>
|
|
326
|
+
onLockAcquired?: (lockDir: string) => void | Promise<void>
|
|
327
|
+
beforeReleaseDirectoryRemoval?: (lockDir: string) => void | Promise<void>
|
|
328
|
+
lockBudgetMs?: number
|
|
329
|
+
lockRetryMinMs?: number
|
|
330
|
+
lockRetryMaxMs?: number
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
let sidebarStateWriteTestHooks: SidebarStateWriteTestHooks | null = null
|
|
334
|
+
|
|
335
|
+
export function __setSidebarStateWriteTestHooks(
|
|
336
|
+
hooks: SidebarStateWriteTestHooks | null,
|
|
337
|
+
): void {
|
|
338
|
+
sidebarStateWriteTestHooks = hooks
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Boot-routing test hooks live here rather than in index.ts: the plugin entry
|
|
342
|
+
// module must export nothing beyond the plugin factory — opencode's plugin
|
|
343
|
+
// loader treats extra entry-point exports as plugin candidates and fails to
|
|
344
|
+
// load the plugin (same incident class as the removed
|
|
345
|
+
// __setBootProfileHydrationForTest export).
|
|
346
|
+
export interface InitialSidebarRoutingTestHooks {
|
|
347
|
+
beforeSidebarRead?: () => void | Promise<void>
|
|
348
|
+
beforeStorageLoad?: () => void | Promise<void>
|
|
349
|
+
afterStorageLoad?: () => void | Promise<void>
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
let initialSidebarRoutingTestHooks: InitialSidebarRoutingTestHooks | null = null
|
|
353
|
+
|
|
354
|
+
export function __setInitialSidebarRoutingTestHooks(
|
|
355
|
+
hooks: InitialSidebarRoutingTestHooks | null,
|
|
356
|
+
): void {
|
|
357
|
+
initialSidebarRoutingTestHooks = hooks
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export function getInitialSidebarRoutingTestHooks(): InitialSidebarRoutingTestHooks | null {
|
|
361
|
+
return initialSidebarRoutingTestHooks
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function sleep(ms: number): Promise<void> {
|
|
365
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function acquireSidebarStateLock(stateFile: string): Promise<{
|
|
369
|
+
release: () => Promise<void>
|
|
370
|
+
ownsLock: () => Promise<boolean>
|
|
371
|
+
} | null> {
|
|
372
|
+
const lockDir = `${stateFile}.lock`
|
|
373
|
+
const budgetMs =
|
|
374
|
+
sidebarStateWriteTestHooks?.lockBudgetMs ?? SIDEBAR_LOCK_BUDGET_MS
|
|
375
|
+
const retryMinMs =
|
|
376
|
+
sidebarStateWriteTestHooks?.lockRetryMinMs ?? SIDEBAR_LOCK_RETRY_MIN_MS
|
|
377
|
+
const retryMaxMs =
|
|
378
|
+
sidebarStateWriteTestHooks?.lockRetryMaxMs ?? SIDEBAR_LOCK_RETRY_MAX_MS
|
|
379
|
+
const deadline = Date.now() + budgetMs
|
|
380
|
+
|
|
381
|
+
while (true) {
|
|
382
|
+
try {
|
|
383
|
+
await mkdir(lockDir)
|
|
384
|
+
const ownerId = randomUUID()
|
|
385
|
+
const ownerFile = join(lockDir, ownerId)
|
|
386
|
+
await writeFile(ownerFile, '', {
|
|
387
|
+
encoding: 'utf8',
|
|
388
|
+
mode: 0o600,
|
|
389
|
+
flag: 'wx',
|
|
390
|
+
})
|
|
391
|
+
await sidebarStateWriteTestHooks?.onLockAcquired?.(lockDir)
|
|
392
|
+
return {
|
|
393
|
+
ownsLock: async () => {
|
|
394
|
+
try {
|
|
395
|
+
await stat(ownerFile)
|
|
396
|
+
return true
|
|
397
|
+
} catch {
|
|
398
|
+
return false
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
release: async () => {
|
|
402
|
+
// Unlinking this acquisition's unique file makes the filesystem reject
|
|
403
|
+
// a stale release after eviction handed the path to a successor.
|
|
404
|
+
try {
|
|
405
|
+
await unlink(ownerFile)
|
|
406
|
+
} catch {
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
await sidebarStateWriteTestHooks?.beforeReleaseDirectoryRemoval?.(
|
|
410
|
+
lockDir,
|
|
411
|
+
)
|
|
412
|
+
await rmdir(lockDir).catch(() => {})
|
|
413
|
+
},
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
417
|
+
logger.trace('sidebar', 'state lock unavailable; write skipped', {
|
|
418
|
+
stateFile,
|
|
419
|
+
error: error instanceof Error ? error.message : String(error),
|
|
420
|
+
})
|
|
421
|
+
return null
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
const lockStat = await stat(lockDir)
|
|
427
|
+
if (Date.now() - lockStat.mtimeMs > SIDEBAR_LOCK_STALE_MS) {
|
|
428
|
+
await sidebarStateWriteTestHooks?.afterStaleLockStat?.(lockDir)
|
|
429
|
+
const evictedLockDir = `${lockDir}.evict-${process.pid}-${randomUUID()}`
|
|
430
|
+
try {
|
|
431
|
+
await rename(lockDir, evictedLockDir)
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
434
|
+
throw error
|
|
435
|
+
}
|
|
436
|
+
await sidebarStateWriteTestHooks?.onStaleLockClaimed?.(lockDir)
|
|
437
|
+
await rm(evictedLockDir, { recursive: true, force: true }).catch(
|
|
438
|
+
() => {},
|
|
439
|
+
)
|
|
440
|
+
continue
|
|
441
|
+
}
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
|
|
444
|
+
logger.trace('sidebar', 'state lock inspection failed; write skipped', {
|
|
445
|
+
stateFile,
|
|
446
|
+
error: error instanceof Error ? error.message : String(error),
|
|
447
|
+
})
|
|
448
|
+
return null
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const remainingMs = deadline - Date.now()
|
|
452
|
+
if (remainingMs <= 0) {
|
|
453
|
+
logger.warn('sidebar', 'lock budget exhausted, write skipped', {
|
|
454
|
+
stateFile,
|
|
455
|
+
budgetMs,
|
|
456
|
+
})
|
|
457
|
+
return null
|
|
458
|
+
}
|
|
459
|
+
const jitterMs =
|
|
460
|
+
retryMinMs + Math.floor(Math.random() * (retryMaxMs - retryMinMs + 1))
|
|
461
|
+
await sleep(Math.min(jitterMs, remainingMs))
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function __acquireSidebarStateLockForTest(
|
|
466
|
+
stateFile: string,
|
|
467
|
+
): Promise<(() => Promise<void>) | null> {
|
|
468
|
+
return (await acquireSidebarStateLock(stateFile))?.release ?? null
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function writeSidebarStateAtomic(
|
|
472
|
+
stateFile: string,
|
|
473
|
+
state: SidebarState,
|
|
474
|
+
ownsLock: () => Promise<boolean>,
|
|
475
|
+
): Promise<'written' | 'lock-lost-before-rename' | 'lock-lost-after-rename'> {
|
|
476
|
+
const tempFile = `${stateFile}.${process.pid}.${randomUUID()}.tmp`
|
|
477
|
+
await writeFile(tempFile, JSON.stringify(state), {
|
|
478
|
+
encoding: 'utf8',
|
|
479
|
+
mode: 0o600,
|
|
480
|
+
})
|
|
242
481
|
try {
|
|
243
|
-
|
|
482
|
+
if (!(await ownsLock())) {
|
|
483
|
+
logger.trace('sidebar', 'state lock lost before rename; write aborted', {
|
|
484
|
+
stateFile,
|
|
485
|
+
})
|
|
486
|
+
await rm(tempFile, { force: true }).catch(() => {})
|
|
487
|
+
return 'lock-lost-before-rename'
|
|
488
|
+
}
|
|
489
|
+
await sidebarStateWriteTestHooks?.beforeRename?.(stateFile, tempFile)
|
|
490
|
+
// No production await separates this ownership fence from rename. A process
|
|
491
|
+
// freeze between the adjacent syscalls remains possible, so rename is also
|
|
492
|
+
// fenced from the other side below.
|
|
493
|
+
if (!(await ownsLock())) {
|
|
494
|
+
logger.trace('sidebar', 'state lock lost before rename; write aborted', {
|
|
495
|
+
stateFile,
|
|
496
|
+
})
|
|
497
|
+
await rm(tempFile, { force: true }).catch(() => {})
|
|
498
|
+
return 'lock-lost-before-rename'
|
|
499
|
+
}
|
|
500
|
+
await rename(tempFile, stateFile)
|
|
501
|
+
await sidebarStateWriteTestHooks?.afterRename?.(stateFile)
|
|
502
|
+
if (!(await ownsLock())) return 'lock-lost-after-rename'
|
|
503
|
+
return 'written'
|
|
504
|
+
} catch (error) {
|
|
505
|
+
await rm(tempFile, { force: true }).catch(() => {})
|
|
506
|
+
throw error
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export async function getSidebarState(
|
|
511
|
+
stateFile = getSidebarStateFile(),
|
|
512
|
+
): Promise<SidebarState> {
|
|
513
|
+
try {
|
|
514
|
+
const raw = await readFile(stateFile, 'utf8')
|
|
244
515
|
return normalizeSidebarState(JSON.parse(raw))
|
|
245
516
|
} catch {
|
|
246
517
|
return DEFAULT_SIDEBAR_STATE
|
|
247
518
|
}
|
|
248
519
|
}
|
|
249
520
|
|
|
521
|
+
export interface SidebarStateWriteOptions {
|
|
522
|
+
routingAuthoritative?: boolean
|
|
523
|
+
resolvePreservedRouting?: (current: SidebarState) =>
|
|
524
|
+
| (Pick<SidebarState, 'activeId' | 'route'> & {
|
|
525
|
+
state?: SidebarState
|
|
526
|
+
})
|
|
527
|
+
| undefined
|
|
528
|
+
| Promise<
|
|
529
|
+
| (Pick<SidebarState, 'activeId' | 'route'> & {
|
|
530
|
+
state?: SidebarState
|
|
531
|
+
})
|
|
532
|
+
| undefined
|
|
533
|
+
>
|
|
534
|
+
onRoutingResolved?: (
|
|
535
|
+
routing: Pick<SidebarState, 'activeId' | 'route'>,
|
|
536
|
+
) => void
|
|
537
|
+
}
|
|
538
|
+
|
|
250
539
|
export async function setSidebarState(
|
|
251
540
|
state: SidebarState,
|
|
252
541
|
stateFile = getSidebarStateFile(),
|
|
542
|
+
options: SidebarStateWriteOptions = {},
|
|
253
543
|
): Promise<void> {
|
|
254
544
|
writeChain = writeChain
|
|
255
545
|
.then(async () => {
|
|
256
546
|
await mkdir(dirname(stateFile), { recursive: true })
|
|
257
|
-
|
|
547
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
548
|
+
const repairingPostRenameLoss = attempt === 1
|
|
549
|
+
const lock = await acquireSidebarStateLock(stateFile)
|
|
550
|
+
// Sidebar frames are display-only and refresh within seconds; dropping one
|
|
551
|
+
// is safer than clobbering routing state that may stay authoritative until
|
|
552
|
+
// another process handles its next request.
|
|
553
|
+
if (!lock) {
|
|
554
|
+
if (repairingPostRenameLoss) {
|
|
555
|
+
logger.warn(
|
|
556
|
+
'sidebar',
|
|
557
|
+
'post-rename repair lock unavailable; write skipped',
|
|
558
|
+
{ stateFile },
|
|
559
|
+
)
|
|
560
|
+
}
|
|
561
|
+
return
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
let stateToWrite = state
|
|
565
|
+
let result:
|
|
566
|
+
| 'written'
|
|
567
|
+
| 'lock-lost-before-rename'
|
|
568
|
+
| 'lock-lost-after-rename'
|
|
569
|
+
try {
|
|
570
|
+
if (options.routingAuthoritative === false) {
|
|
571
|
+
const current = await getSidebarState(stateFile)
|
|
572
|
+
const preservedRouting =
|
|
573
|
+
await options.resolvePreservedRouting?.(current)
|
|
574
|
+
if (preservedRouting) {
|
|
575
|
+
const preservedState = preservedRouting.state ?? state
|
|
576
|
+
stateToWrite = {
|
|
577
|
+
...preservedState,
|
|
578
|
+
activeId: preservedRouting.activeId,
|
|
579
|
+
route: preservedRouting.route,
|
|
580
|
+
lastUpdated: Math.max(
|
|
581
|
+
preservedState.lastUpdated,
|
|
582
|
+
current.lastUpdated,
|
|
583
|
+
),
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
await sidebarStateWriteTestHooks?.afterMergeRead?.(stateFile)
|
|
587
|
+
} else if (repairingPostRenameLoss) {
|
|
588
|
+
const current = await getSidebarState(stateFile)
|
|
589
|
+
// A successor owns its fresh account/quota and recovery snapshots;
|
|
590
|
+
// this routing frame may only republish its decision and shared UI state.
|
|
591
|
+
stateToWrite = {
|
|
592
|
+
...current,
|
|
593
|
+
activeId: state.activeId,
|
|
594
|
+
route: state.route,
|
|
595
|
+
relay: state.relay,
|
|
596
|
+
fastMode: state.fastMode,
|
|
597
|
+
cacheKeep: state.cacheKeep,
|
|
598
|
+
lastUpdated: Math.max(current.lastUpdated, state.lastUpdated),
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
result = await writeSidebarStateAtomic(
|
|
602
|
+
stateFile,
|
|
603
|
+
stateToWrite,
|
|
604
|
+
lock.ownsLock,
|
|
605
|
+
)
|
|
606
|
+
} finally {
|
|
607
|
+
await lock.release()
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (result === 'lock-lost-after-rename') {
|
|
611
|
+
if (repairingPostRenameLoss) {
|
|
612
|
+
logger.warn(
|
|
613
|
+
'sidebar',
|
|
614
|
+
'post-rename repair lost lock; write skipped',
|
|
615
|
+
{ stateFile },
|
|
616
|
+
)
|
|
617
|
+
return
|
|
618
|
+
}
|
|
619
|
+
logger.warn(
|
|
620
|
+
'sidebar',
|
|
621
|
+
'state lock lost after rename; repairing write',
|
|
622
|
+
{
|
|
623
|
+
stateFile,
|
|
624
|
+
},
|
|
625
|
+
)
|
|
626
|
+
continue
|
|
627
|
+
}
|
|
628
|
+
if (result === 'lock-lost-before-rename') return
|
|
629
|
+
options.onRoutingResolved?.({
|
|
630
|
+
activeId: stateToWrite.activeId,
|
|
631
|
+
route: stateToWrite.route,
|
|
632
|
+
})
|
|
633
|
+
return
|
|
634
|
+
}
|
|
258
635
|
})
|
|
259
636
|
.catch(() => {
|
|
260
637
|
// Best-effort — sidebar is non-critical
|