@tokenbuddy/tokenbuddy 1.0.35 → 1.0.37

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 (143) hide show
  1. package/dist/src/buyer-store.d.ts +6 -1
  2. package/dist/src/buyer-store.js +43 -4
  3. package/dist/src/cli.js +2 -2
  4. package/dist/src/daemon.d.ts +12 -0
  5. package/dist/src/daemon.js +791 -61
  6. package/dist/src/doctor-diagnostics.js +1 -6
  7. package/dist/src/provider-install.d.ts +2 -2
  8. package/dist/src/provider-install.js +248 -2
  9. package/dist/src/seller-catalog.d.ts +21 -0
  10. package/dist/src/seller-catalog.js +17 -0
  11. package/dist/src/seller-route-planner.d.ts +4 -1
  12. package/dist/src/seller-route-planner.js +3 -0
  13. package/dist/src/seller-routing-strategy.d.ts +3 -0
  14. package/dist/src/terminal-detect.d.ts +1 -1
  15. package/dist/src/terminal-detect.js +3 -2
  16. package/package.json +15 -2
  17. package/static/ui/assets/index-Djfl9tw5.js +271 -0
  18. package/static/ui/assets/index-DkfztCkn.css +1 -0
  19. package/static/ui/index.html +2 -2
  20. package/dist/src/buyer-store.d.ts.map +0 -1
  21. package/dist/src/buyer-store.js.map +0 -1
  22. package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
  23. package/dist/src/clawtip-bootstrap.js.map +0 -1
  24. package/dist/src/cli.d.ts.map +0 -1
  25. package/dist/src/cli.js.map +0 -1
  26. package/dist/src/credit-tracker.d.ts.map +0 -1
  27. package/dist/src/credit-tracker.js.map +0 -1
  28. package/dist/src/daemon.d.ts.map +0 -1
  29. package/dist/src/daemon.js.map +0 -1
  30. package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
  31. package/dist/src/doctor-clawtip-wallet.js.map +0 -1
  32. package/dist/src/doctor-diagnostics.d.ts.map +0 -1
  33. package/dist/src/doctor-diagnostics.js.map +0 -1
  34. package/dist/src/index.d.ts.map +0 -1
  35. package/dist/src/index.js.map +0 -1
  36. package/dist/src/init-clawtip-activation.d.ts.map +0 -1
  37. package/dist/src/init-clawtip-activation.js.map +0 -1
  38. package/dist/src/init-payment-options.d.ts.map +0 -1
  39. package/dist/src/init-payment-options.js.map +0 -1
  40. package/dist/src/init-setup.d.ts.map +0 -1
  41. package/dist/src/init-setup.js.map +0 -1
  42. package/dist/src/model-index.d.ts.map +0 -1
  43. package/dist/src/model-index.js.map +0 -1
  44. package/dist/src/package-update.d.ts.map +0 -1
  45. package/dist/src/package-update.js.map +0 -1
  46. package/dist/src/prewarm-cache.d.ts.map +0 -1
  47. package/dist/src/prewarm-cache.js.map +0 -1
  48. package/dist/src/prewarm-scheduler.d.ts.map +0 -1
  49. package/dist/src/prewarm-scheduler.js.map +0 -1
  50. package/dist/src/provider-install.d.ts.map +0 -1
  51. package/dist/src/provider-install.js.map +0 -1
  52. package/dist/src/provider-routing-config.d.ts.map +0 -1
  53. package/dist/src/provider-routing-config.js.map +0 -1
  54. package/dist/src/registry-trust.d.ts.map +0 -1
  55. package/dist/src/registry-trust.js.map +0 -1
  56. package/dist/src/route-failover.d.ts.map +0 -1
  57. package/dist/src/route-failover.js.map +0 -1
  58. package/dist/src/seller-catalog.d.ts.map +0 -1
  59. package/dist/src/seller-catalog.js.map +0 -1
  60. package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
  61. package/dist/src/seller-concurrency-limiter.js.map +0 -1
  62. package/dist/src/seller-metadata-cache.d.ts.map +0 -1
  63. package/dist/src/seller-metadata-cache.js.map +0 -1
  64. package/dist/src/seller-pool.d.ts.map +0 -1
  65. package/dist/src/seller-pool.js.map +0 -1
  66. package/dist/src/seller-route-planner.d.ts.map +0 -1
  67. package/dist/src/seller-route-planner.js.map +0 -1
  68. package/dist/src/seller-routing-config.d.ts.map +0 -1
  69. package/dist/src/seller-routing-config.js.map +0 -1
  70. package/dist/src/seller-routing-strategy.d.ts.map +0 -1
  71. package/dist/src/seller-routing-strategy.js.map +0 -1
  72. package/dist/src/stream-failover.d.ts.map +0 -1
  73. package/dist/src/stream-failover.js.map +0 -1
  74. package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
  75. package/dist/src/tb-clawtip-proof.js.map +0 -1
  76. package/dist/src/tb-proxyd.d.ts.map +0 -1
  77. package/dist/src/tb-proxyd.js.map +0 -1
  78. package/dist/src/terminal-detect.d.ts.map +0 -1
  79. package/dist/src/terminal-detect.js.map +0 -1
  80. package/dist/src/terminal-image.d.ts.map +0 -1
  81. package/dist/src/terminal-image.js.map +0 -1
  82. package/src/buyer-store.ts +0 -1090
  83. package/src/clawtip-bootstrap.ts +0 -65
  84. package/src/cli.ts +0 -2243
  85. package/src/credit-tracker.ts +0 -295
  86. package/src/daemon.ts +0 -5475
  87. package/src/doctor-clawtip-wallet.ts +0 -95
  88. package/src/doctor-diagnostics.ts +0 -1026
  89. package/src/index.ts +0 -16
  90. package/src/init-clawtip-activation.ts +0 -695
  91. package/src/init-payment-options.ts +0 -373
  92. package/src/init-setup.ts +0 -165
  93. package/src/model-index.ts +0 -278
  94. package/src/package-update.ts +0 -311
  95. package/src/prewarm-cache.ts +0 -485
  96. package/src/prewarm-scheduler.ts +0 -675
  97. package/src/provider-install.ts +0 -1006
  98. package/src/provider-routing-config.ts +0 -410
  99. package/src/registry-trust.ts +0 -51
  100. package/src/route-failover.ts +0 -304
  101. package/src/seller-catalog.ts +0 -505
  102. package/src/seller-concurrency-limiter.ts +0 -161
  103. package/src/seller-metadata-cache.ts +0 -91
  104. package/src/seller-pool.ts +0 -557
  105. package/src/seller-route-planner.ts +0 -513
  106. package/src/seller-routing-config.ts +0 -211
  107. package/src/seller-routing-strategy.ts +0 -362
  108. package/src/stream-failover.ts +0 -152
  109. package/src/tb-clawtip-proof.ts +0 -28
  110. package/src/tb-proxyd.ts +0 -101
  111. package/src/terminal-detect.ts +0 -333
  112. package/src/terminal-image.ts +0 -228
  113. package/static/ui/assets/index-0MVXD7bH.css +0 -1
  114. package/static/ui/assets/index-BVbeDEwq.js +0 -271
  115. package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
  116. package/tests/cli-routing.test.ts +0 -363
  117. package/tests/control-plane-ui-endpoints.test.ts +0 -1630
  118. package/tests/credit-tracker.test.ts +0 -165
  119. package/tests/daemon-413-fallback.test.ts +0 -92
  120. package/tests/daemon-classify.test.ts +0 -452
  121. package/tests/daemon-roles.test.ts +0 -92
  122. package/tests/daemon-trusted-registry-cache.test.ts +0 -132
  123. package/tests/e2e.test.ts +0 -366
  124. package/tests/image-generation-e2e.test.ts +0 -230
  125. package/tests/model-index.test.ts +0 -198
  126. package/tests/package-update.test.ts +0 -147
  127. package/tests/prewarm-cache.test.ts +0 -296
  128. package/tests/prewarm-scheduler.test.ts +0 -367
  129. package/tests/provider-routing-config.test.ts +0 -150
  130. package/tests/registry-trust.test.ts +0 -28
  131. package/tests/route-failover.test.ts +0 -222
  132. package/tests/seller-catalog-413.test.ts +0 -120
  133. package/tests/seller-catalog-utilities.test.ts +0 -124
  134. package/tests/seller-concurrency-limiter.test.ts +0 -83
  135. package/tests/seller-metadata-cache.test.ts +0 -89
  136. package/tests/seller-pool.test.ts +0 -365
  137. package/tests/seller-route-planner.test.ts +0 -312
  138. package/tests/seller-routing-config.test.ts +0 -124
  139. package/tests/seller-routing-strategy.test.ts +0 -167
  140. package/tests/stream-failover.test.ts +0 -52
  141. package/tests/thousand-seller.test.ts +0 -151
  142. package/tests/tokenbuddy.test.ts +0 -4043
  143. package/tsconfig.json +0 -8
@@ -1,278 +0,0 @@
1
- import { createModuleLogger } from "@tokenbuddy/logging";
2
- import { isBuyerVisibleRegistrySeller, type RegistrySeller } from "./seller-catalog.js";
3
-
4
- const logger = createModuleLogger("tb-proxyd:model-index");
5
-
6
- /**
7
- * Normalize a model id for index lookup. Trims whitespace and lowercases
8
- * the value so that `claude-sonnet-4-5` and `Claude-Sonnet-4-5 ` resolve to
9
- * the same entry. v1.2 model matching is case-insensitive.
10
- */
11
- function normalizeModelId(modelId: string): string {
12
- return modelId.trim().toLowerCase();
13
- }
14
-
15
- /**
16
- * Result of resolving a model id against the index.
17
- *
18
- * - `empty` is returned when the index has been built but no seller serves
19
- * the requested model. The caller should treat this as "no compatible
20
- * seller" and surface the empty result to the user.
21
- * - `sellers` is a snapshot array of the matching registry entries. Order
22
- * follows the registry's `defaultSeller` preference then declaration order.
23
- */
24
- /**
25
- * `ModelIndex.resolve()` 的返回结果。
26
- * - `empty` 等价于 `matched = false && sellers = []`,路由层应当视为"无可用 seller"。
27
- * - `sellers` 是按 `defaultSeller` 优先 + 声明顺序排序后的快照数组。
28
- */
29
- export interface ModelIndexResolution {
30
- /** 原始请求的模型 ID(未归一化) */
31
- modelId: string;
32
- /** 索引里是否至少有一个 seller 命中(已归一化匹配) */
33
- matched: boolean;
34
- /** 命中的 seller 列表(已按 default + 声明顺序排序的快照) */
35
- sellers: RegistrySeller[];
36
- /** 当前索引里 `models` 字段缺失的 seller 数(用于诊断和告警) */
37
- missingModelsFlag: number;
38
- }
39
-
40
- interface ModelIndexInternals {
41
- byModel: Map<string, RegistrySeller[]>;
42
- bySeller: Map<string, RegistrySeller>;
43
- registryVersion: number;
44
- registryFetchedAt: number;
45
- defaultSellerId?: string;
46
- missingModelsCount: number;
47
- totalSellers: number;
48
- }
49
-
50
- /**
51
- * In-memory index mapping `modelId -> sellers[]` from a fetched registry
52
- * snapshot. The index is rebuilt atomically on every `rebuild()` call so
53
- * callers always observe a consistent snapshot (Node is single-threaded, but
54
- * the rebuild path copies data before swapping the internal maps).
55
- */
56
- export class ModelIndex {
57
- private internals: ModelIndexInternals = emptyInternals();
58
-
59
- /**
60
- * Atomically replace the index contents with a new registry snapshot.
61
- * Sellers that do not declare `models` are kept in the `bySeller` reverse
62
- * map so they can still be addressed by id, but they are excluded from
63
- * `sellersFor()` results. The count of such sellers is exposed via
64
- * `missingModelsCount` and via the `models_refresh.seller_missing_models`
65
- * log event so operators can fix upstream registry payloads.
66
- */
67
- rebuild(sellers: RegistrySeller[], opts: { registryVersion?: number; defaultSellerId?: string } = {}): void {
68
- const byModel = new Map<string, RegistrySeller[]>();
69
- const bySeller = new Map<string, RegistrySeller>();
70
- let missingModels = 0;
71
-
72
- for (const seller of sellers) {
73
- if (!seller || !seller.id) {
74
- continue;
75
- }
76
- if (!isBuyerVisibleRegistrySeller(seller)) {
77
- continue;
78
- }
79
- // v1.2 registry schema 用 "anthropic_messages" 作为协议名(OpenAI / ClawTip
80
- // / 外部 client 都用这个),但 buyer 内部 `endpointProtocol` 对 /v1/messages
81
- // 返 "messages"(更短、更易读)。在 modelIndex 重建时做 alias 映射,让两边
82
- // 都能匹配到同一 seller。alias 只在内存里生效,不回写 registry。
83
- if (
84
- Array.isArray(seller.supportedProtocols) &&
85
- seller.supportedProtocols.includes("anthropic_messages") &&
86
- !seller.supportedProtocols.includes("messages")
87
- ) {
88
- seller.supportedProtocols = [...seller.supportedProtocols, "messages"];
89
- }
90
- bySeller.set(seller.id, seller);
91
- if (!Array.isArray(seller.models) || seller.models.length === 0) {
92
- missingModels += 1;
93
- logger.warn("models_refresh.seller_missing_models", "registry seller entry missing models array; excluded from model index", {
94
- sellerId: seller.id,
95
- sellerUrl: seller.url
96
- });
97
- continue;
98
- }
99
- for (const raw of seller.models) {
100
- if (typeof raw !== "string" || raw.trim() === "") {
101
- continue;
102
- }
103
- const key = normalizeModelId(raw);
104
- const bucket = byModel.get(key);
105
- if (bucket) {
106
- bucket.push(seller);
107
- } else {
108
- byModel.set(key, [seller]);
109
- }
110
- }
111
- }
112
-
113
- this.internals = {
114
- byModel,
115
- bySeller,
116
- registryVersion: opts.registryVersion ?? 0,
117
- registryFetchedAt: Date.now(),
118
- defaultSellerId: opts.defaultSellerId,
119
- missingModelsCount: missingModels,
120
- totalSellers: bySeller.size
121
- };
122
-
123
- logger.info("models_refresh.rebuilt", "model index rebuilt from registry snapshot", {
124
- registryVersion: this.internals.registryVersion,
125
- sellerCount: this.internals.totalSellers,
126
- modelCount: byModel.size,
127
- missingModels: missingModels,
128
- defaultSellerId: opts.defaultSellerId ?? null
129
- });
130
- }
131
-
132
- /**
133
- * Resolve sellers for a given model id, optionally filtered by protocol
134
- * and payment method. Returns a snapshot; mutating the result does not
135
- * affect the index.
136
- */
137
- sellersFor(modelId: string, filter?: { protocol?: string; paymentMethod?: string }): RegistrySeller[] {
138
- const key = normalizeModelId(modelId);
139
- const bucket = this.internals.byModel.get(key);
140
- if (!bucket) {
141
- return [];
142
- }
143
- if (!filter) {
144
- return bucket.slice();
145
- }
146
- return bucket.filter((seller) => matchesFilter(seller, filter));
147
- }
148
-
149
- /**
150
- * Resolve a single model id with diagnostic metadata. Used by route-failover
151
- * to log structured "no compatible seller" events without losing the
152
- * missing-models count.
153
- */
154
- resolve(modelId: string, filter?: { protocol?: string; paymentMethod?: string }): ModelIndexResolution {
155
- return {
156
- modelId,
157
- matched: this.internals.byModel.has(normalizeModelId(modelId)),
158
- sellers: this.sellersFor(modelId, filter),
159
- missingModelsFlag: this.internals.missingModelsCount
160
- };
161
- }
162
-
163
- /**
164
- * Returns the registry seller entry by id, or `undefined` if the seller is
165
- * not present in the latest snapshot. Used by the route-failover to look up
166
- * a seller for token bookkeeping even when the requested model is unknown.
167
- */
168
- getSeller(sellerId: string): RegistrySeller | undefined {
169
- return this.internals.bySeller.get(sellerId);
170
- }
171
-
172
- /**
173
- * List every model id known to the index. Intended for diagnostics
174
- * (`tb doctor`) and CLI completion; not used on the hot path.
175
- */
176
- knownModelIds(): string[] {
177
- return Array.from(this.internals.byModel.keys());
178
- }
179
-
180
- /**
181
- * Drop stale seller entries from the reverse map when the registry has not
182
- * surfaced them within `staleAfterMs`. Model-keyed buckets are derived from
183
- * the same `bySeller` set so dropping sellers implicitly drops their
184
- * model associations. Returns the number of removed entries.
185
- */
186
- prune(lastSeenAt: Map<string, number>, staleAfterMs: number, now: number = Date.now()): number {
187
- if (staleAfterMs <= 0) {
188
- return 0;
189
- }
190
- const cutoff = now - staleAfterMs;
191
- const toRemove: string[] = [];
192
- for (const [sellerId, seen] of lastSeenAt.entries()) {
193
- if (seen < cutoff && this.internals.bySeller.has(sellerId)) {
194
- toRemove.push(sellerId);
195
- }
196
- }
197
- if (toRemove.length === 0) {
198
- return 0;
199
- }
200
-
201
- const byModel = new Map<string, RegistrySeller[]>();
202
- for (const [key, sellers] of this.internals.byModel.entries()) {
203
- const filtered = sellers.filter((seller) => !toRemove.includes(seller.id));
204
- if (filtered.length > 0) {
205
- byModel.set(key, filtered);
206
- }
207
- }
208
- const bySeller = new Map<string, RegistrySeller>();
209
- for (const [sellerId, seller] of this.internals.bySeller.entries()) {
210
- if (!toRemove.includes(sellerId)) {
211
- bySeller.set(sellerId, seller);
212
- }
213
- }
214
-
215
- this.internals = {
216
- ...this.internals,
217
- byModel,
218
- bySeller,
219
- registryFetchedAt: now
220
- };
221
-
222
- logger.info("models_refresh.pruned", "stale sellers pruned from model index", {
223
- removedCount: toRemove.length,
224
- remainingSellers: bySeller.size,
225
- remainingModels: byModel.size
226
- });
227
- return toRemove.length;
228
- }
229
-
230
- /**
231
- * Snapshot of the internal counters for diagnostics. Cheap to call; does
232
- * not copy the maps.
233
- */
234
- stats(): {
235
- sellerCount: number;
236
- modelCount: number;
237
- missingModelsCount: number;
238
- registryVersion: number;
239
- registryFetchedAt: number;
240
- defaultSellerId?: string;
241
- } {
242
- return {
243
- sellerCount: this.internals.totalSellers,
244
- modelCount: this.internals.byModel.size,
245
- missingModelsCount: this.internals.missingModelsCount,
246
- registryVersion: this.internals.registryVersion,
247
- registryFetchedAt: this.internals.registryFetchedAt,
248
- defaultSellerId: this.internals.defaultSellerId
249
- };
250
- }
251
- }
252
-
253
- function emptyInternals(): ModelIndexInternals {
254
- return {
255
- byModel: new Map(),
256
- bySeller: new Map(),
257
- registryVersion: 0,
258
- registryFetchedAt: 0,
259
- missingModelsCount: 0,
260
- totalSellers: 0
261
- };
262
- }
263
-
264
- function matchesFilter(seller: RegistrySeller, filter: { protocol?: string; paymentMethod?: string }): boolean {
265
- if (filter.protocol) {
266
- const protocols = seller.supportedProtocols ?? [];
267
- if (!protocols.includes(filter.protocol)) {
268
- return false;
269
- }
270
- }
271
- if (filter.paymentMethod) {
272
- const methods = seller.paymentMethods ?? [];
273
- if (!methods.includes(filter.paymentMethod)) {
274
- return false;
275
- }
276
- }
277
- return true;
278
- }
@@ -1,311 +0,0 @@
1
- import * as fs from "fs";
2
- import * as os from "os";
3
- import * as path from "path";
4
- import { execFileSync, spawn } from "child_process";
5
- import { fileURLToPath } from "url";
6
-
7
- export const TOKENBUDDY_LAUNCHD_LABEL = "com.tokenbuddy.proxyd";
8
- const DEFAULT_PACKAGE_NAME = "tokenbuddy";
9
-
10
- export interface InstalledPackageManifest {
11
- name: string;
12
- version: string;
13
- }
14
-
15
- export interface PackageUpdateCheck {
16
- packageName: string;
17
- currentVersion: string;
18
- latestVersion: string;
19
- updateAvailable: boolean;
20
- registryUrl: string;
21
- installCommand: string;
22
- }
23
-
24
- export interface PackageInstallResult {
25
- attempted: boolean;
26
- succeeded: boolean;
27
- command: string;
28
- args: string[];
29
- error?: string;
30
- }
31
-
32
- export interface PackageRestartResult {
33
- attempted: boolean;
34
- restarted: boolean;
35
- method: "launchd";
36
- scheduled?: boolean;
37
- plistPath?: string;
38
- target?: string;
39
- error?: string;
40
- }
41
-
42
- export interface PackageUpdateResult {
43
- check: PackageUpdateCheck;
44
- install: PackageInstallResult;
45
- restart: PackageRestartResult;
46
- }
47
-
48
- export interface CheckPackageUpdateDeps {
49
- fetch?: typeof fetch;
50
- env?: NodeJS.ProcessEnv;
51
- argv?: string[];
52
- cwd?: string;
53
- }
54
-
55
- export interface RunPackageUpdateDeps extends CheckPackageUpdateDeps {
56
- npmCommand?: string;
57
- runNpmInstall?: (command: string, args: string[]) => void;
58
- restartProxyd?: (controlPort: number) => Promise<PackageRestartResult>;
59
- }
60
-
61
- export interface RunPackageUpdateOptions {
62
- apply: boolean;
63
- controlPort: number;
64
- }
65
-
66
- interface RegistryDocument {
67
- "dist-tags"?: {
68
- latest?: unknown;
69
- };
70
- }
71
-
72
- function currentModuleDir(): string {
73
- if (typeof __dirname !== "undefined") {
74
- return __dirname;
75
- }
76
-
77
- const stack = new Error().stack || "";
78
- const fileUrlMatch = stack.match(/(file:\/\/\/[^)\n]+\/package-update\.js):\d+:\d+/);
79
- if (fileUrlMatch) {
80
- return path.dirname(fileURLToPath(fileUrlMatch[1]));
81
- }
82
-
83
- const filePathMatch = stack.match(/(\/[^)\n]+\/package-update\.(?:js|ts)):\d+:\d+/);
84
- if (filePathMatch) {
85
- return path.dirname(filePathMatch[1]);
86
- }
87
-
88
- return process.cwd();
89
- }
90
-
91
- function candidatePackageRoots(argv: string[] = process.argv, cwd = process.cwd()): string[] {
92
- return [
93
- argv[1],
94
- path.join(cwd, "packages", "tokenbuddy-cli"),
95
- path.resolve(currentModuleDir(), ".."),
96
- cwd,
97
- ].filter((candidate): candidate is string => Boolean(candidate));
98
- }
99
-
100
- export function readInstalledPackageManifest(
101
- argv: string[] = process.argv,
102
- cwd = process.cwd(),
103
- ): InstalledPackageManifest {
104
- const seen = new Set<string>();
105
- for (const candidateRoot of candidatePackageRoots(argv, cwd)) {
106
- let current = fs.existsSync(candidateRoot) ? fs.realpathSync(candidateRoot) : candidateRoot;
107
- if (!fs.existsSync(current)) continue;
108
- if (!fs.statSync(current).isDirectory()) {
109
- current = path.dirname(current);
110
- }
111
- while (!seen.has(current)) {
112
- seen.add(current);
113
- const packageJsonPath = path.join(current, "package.json");
114
- if (fs.existsSync(packageJsonPath)) {
115
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as { name?: unknown; version?: unknown };
116
- if (typeof packageJson.version === "string" && packageJson.version.length > 0) {
117
- const name = typeof packageJson.name === "string" && packageJson.name.length > 0
118
- ? packageJson.name
119
- : DEFAULT_PACKAGE_NAME;
120
- if (name === DEFAULT_PACKAGE_NAME || name === "@tokenbuddy/tokenbuddy") {
121
- return { name, version: packageJson.version };
122
- }
123
- }
124
- }
125
- const parent = path.dirname(current);
126
- if (parent === current) break;
127
- current = parent;
128
- }
129
- }
130
- return { name: DEFAULT_PACKAGE_NAME, version: "0.0.0" };
131
- }
132
-
133
- function registryUrl(packageName: string): string {
134
- return `https://registry.npmjs.org/${encodeURIComponent(packageName)}`;
135
- }
136
-
137
- function packageNameForUpdate(manifest: InstalledPackageManifest, env: NodeJS.ProcessEnv = process.env): string {
138
- const override = env.TOKENBUDDY_UPDATE_PACKAGE_NAME?.trim();
139
- if (override) {
140
- return override;
141
- }
142
- return manifest.name || DEFAULT_PACKAGE_NAME;
143
- }
144
-
145
- function parseSemver(value: string): [number, number, number] | null {
146
- const match = value.trim().match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
147
- if (!match) {
148
- return null;
149
- }
150
- return [Number(match[1]), Number(match[2]), Number(match[3])];
151
- }
152
-
153
- function isVersionGreater(left: string, right: string): boolean {
154
- const parsedLeft = parseSemver(left);
155
- const parsedRight = parseSemver(right);
156
- if (!parsedLeft || !parsedRight) {
157
- return left !== right;
158
- }
159
- for (let index = 0; index < parsedLeft.length; index += 1) {
160
- if (parsedLeft[index] > parsedRight[index]) return true;
161
- if (parsedLeft[index] < parsedRight[index]) return false;
162
- }
163
- return false;
164
- }
165
-
166
- export async function checkPackageUpdate(deps: CheckPackageUpdateDeps = {}): Promise<PackageUpdateCheck> {
167
- const env = deps.env ?? process.env;
168
- const manifest = readInstalledPackageManifest(deps.argv, deps.cwd);
169
- const packageName = packageNameForUpdate(manifest, env);
170
- const url = registryUrl(packageName);
171
- const fetchImpl = deps.fetch ?? fetch;
172
- const res = await fetchImpl(url);
173
- if (!res.ok) {
174
- throw new Error(`npm registry returned HTTP ${res.status} for ${packageName}`);
175
- }
176
- const registry = await res.json() as RegistryDocument;
177
- const latestVersion = registry["dist-tags"]?.latest;
178
- if (typeof latestVersion !== "string" || latestVersion.length === 0) {
179
- throw new Error(`npm registry response for ${packageName} is missing dist-tags.latest`);
180
- }
181
- const installCommand = `npm install -g ${packageName}@${latestVersion}`;
182
- return {
183
- packageName,
184
- currentVersion: manifest.version,
185
- latestVersion,
186
- updateAvailable: isVersionGreater(latestVersion, manifest.version),
187
- registryUrl: url,
188
- installCommand,
189
- };
190
- }
191
-
192
- function npmCommand(env: NodeJS.ProcessEnv, override?: string): string {
193
- return override || env.TOKENBUDDY_UPDATE_NPM_COMMAND || "npm";
194
- }
195
-
196
- function defaultRunNpmInstall(command: string, args: string[]): void {
197
- execFileSync(command, args, {
198
- stdio: ["ignore", "pipe", "pipe"],
199
- maxBuffer: 8 * 1024 * 1024,
200
- });
201
- }
202
-
203
- function commandErrorMessage(error: unknown): string {
204
- const withOutput = error as { message?: string; stderr?: Buffer; stdout?: Buffer };
205
- const stderr = withOutput.stderr?.toString("utf8").trim();
206
- const stdout = withOutput.stdout?.toString("utf8").trim();
207
- return stderr || stdout || (error instanceof Error ? error.message : String(error));
208
- }
209
-
210
- export async function runPackageUpdate(
211
- options: RunPackageUpdateOptions,
212
- deps: RunPackageUpdateDeps = {},
213
- ): Promise<PackageUpdateResult> {
214
- const check = await checkPackageUpdate(deps);
215
- const command = npmCommand(deps.env ?? process.env, deps.npmCommand);
216
- const args = ["install", "-g", `${check.packageName}@${check.latestVersion}`];
217
- const install: PackageInstallResult = {
218
- attempted: false,
219
- succeeded: false,
220
- command,
221
- args,
222
- };
223
- const restart: PackageRestartResult = {
224
- attempted: false,
225
- restarted: false,
226
- method: "launchd",
227
- };
228
-
229
- if (!options.apply || !check.updateAvailable) {
230
- return {
231
- check,
232
- install,
233
- restart,
234
- };
235
- }
236
-
237
- install.attempted = true;
238
- try {
239
- (deps.runNpmInstall ?? defaultRunNpmInstall)(command, args);
240
- install.succeeded = true;
241
- } catch (error: unknown) {
242
- install.error = commandErrorMessage(error);
243
- return { check, install, restart };
244
- }
245
-
246
- if (!deps.restartProxyd) {
247
- restart.error = "tb-proxyd restart runner is not configured";
248
- return { check, install, restart };
249
- }
250
-
251
- return {
252
- check,
253
- install,
254
- restart: await deps.restartProxyd(options.controlPort),
255
- };
256
- }
257
-
258
- function launchdUserDomain(): string {
259
- if (typeof process.getuid === "function") {
260
- return `gui/${process.getuid()}`;
261
- }
262
- return "gui/501";
263
- }
264
-
265
- function launchdServiceTarget(label: string): string {
266
- return `${launchdUserDomain()}/${label}`;
267
- }
268
-
269
- export function scheduleLaunchAgentRestart(
270
- deps: {
271
- platform?: NodeJS.Platform;
272
- homeDir?: string;
273
- existsSync?: (filePath: string) => boolean;
274
- spawn?: typeof spawn;
275
- } = {},
276
- ): PackageRestartResult {
277
- const platform = deps.platform ?? process.platform;
278
- const homeDir = deps.homeDir ?? os.homedir();
279
- const plistPath = path.join(homeDir, "Library", "LaunchAgents", `${TOKENBUDDY_LAUNCHD_LABEL}.plist`);
280
- const base = {
281
- attempted: false,
282
- restarted: false,
283
- method: "launchd" as const,
284
- plistPath,
285
- };
286
- if (platform !== "darwin") {
287
- return {
288
- ...base,
289
- error: "tb-proxyd restart is only supported for the macOS LaunchAgent service.",
290
- };
291
- }
292
- if (!(deps.existsSync ?? fs.existsSync)(plistPath)) {
293
- return {
294
- ...base,
295
- error: "LaunchAgent plist is missing. Run `tb init` to install tb-proxyd as a service first.",
296
- };
297
- }
298
-
299
- const target = launchdServiceTarget(TOKENBUDDY_LAUNCHD_LABEL);
300
- const child = (deps.spawn ?? spawn)("sh", ["-c", `sleep 0.35; launchctl kickstart -k ${target}`], {
301
- detached: true,
302
- stdio: "ignore",
303
- });
304
- child.unref();
305
- return {
306
- ...base,
307
- attempted: true,
308
- scheduled: true,
309
- target,
310
- };
311
- }