@tokenbuddy/tokenbuddy 1.0.36 → 1.0.38

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 (146) hide show
  1. package/dist/src/buyer-store.d.ts +7 -2
  2. package/dist/src/buyer-store.js +46 -7
  3. package/dist/src/cli.d.ts +1 -0
  4. package/dist/src/cli.js +15 -7
  5. package/dist/src/daemon.d.ts +12 -0
  6. package/dist/src/daemon.js +791 -61
  7. package/dist/src/doctor-diagnostics.js +1 -6
  8. package/dist/src/provider-install.d.ts +2 -2
  9. package/dist/src/provider-install.js +248 -2
  10. package/dist/src/seller-catalog.d.ts +21 -0
  11. package/dist/src/seller-catalog.js +17 -0
  12. package/dist/src/seller-route-planner.d.ts +4 -1
  13. package/dist/src/seller-route-planner.js +3 -0
  14. package/dist/src/seller-routing-strategy.d.ts +3 -0
  15. package/dist/src/terminal-detect.d.ts +1 -1
  16. package/dist/src/terminal-detect.js +3 -2
  17. package/dist/src/workdir.d.ts +10 -0
  18. package/dist/src/workdir.js +26 -0
  19. package/package.json +15 -2
  20. package/static/ui/assets/index-Djfl9tw5.js +271 -0
  21. package/static/ui/assets/index-DkfztCkn.css +1 -0
  22. package/static/ui/index.html +2 -2
  23. package/dist/src/buyer-store.d.ts.map +0 -1
  24. package/dist/src/buyer-store.js.map +0 -1
  25. package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
  26. package/dist/src/clawtip-bootstrap.js.map +0 -1
  27. package/dist/src/cli.d.ts.map +0 -1
  28. package/dist/src/cli.js.map +0 -1
  29. package/dist/src/credit-tracker.d.ts.map +0 -1
  30. package/dist/src/credit-tracker.js.map +0 -1
  31. package/dist/src/daemon.d.ts.map +0 -1
  32. package/dist/src/daemon.js.map +0 -1
  33. package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
  34. package/dist/src/doctor-clawtip-wallet.js.map +0 -1
  35. package/dist/src/doctor-diagnostics.d.ts.map +0 -1
  36. package/dist/src/doctor-diagnostics.js.map +0 -1
  37. package/dist/src/index.d.ts.map +0 -1
  38. package/dist/src/index.js.map +0 -1
  39. package/dist/src/init-clawtip-activation.d.ts.map +0 -1
  40. package/dist/src/init-clawtip-activation.js.map +0 -1
  41. package/dist/src/init-payment-options.d.ts.map +0 -1
  42. package/dist/src/init-payment-options.js.map +0 -1
  43. package/dist/src/init-setup.d.ts.map +0 -1
  44. package/dist/src/init-setup.js.map +0 -1
  45. package/dist/src/model-index.d.ts.map +0 -1
  46. package/dist/src/model-index.js.map +0 -1
  47. package/dist/src/package-update.d.ts.map +0 -1
  48. package/dist/src/package-update.js.map +0 -1
  49. package/dist/src/prewarm-cache.d.ts.map +0 -1
  50. package/dist/src/prewarm-cache.js.map +0 -1
  51. package/dist/src/prewarm-scheduler.d.ts.map +0 -1
  52. package/dist/src/prewarm-scheduler.js.map +0 -1
  53. package/dist/src/provider-install.d.ts.map +0 -1
  54. package/dist/src/provider-install.js.map +0 -1
  55. package/dist/src/provider-routing-config.d.ts.map +0 -1
  56. package/dist/src/provider-routing-config.js.map +0 -1
  57. package/dist/src/registry-trust.d.ts.map +0 -1
  58. package/dist/src/registry-trust.js.map +0 -1
  59. package/dist/src/route-failover.d.ts.map +0 -1
  60. package/dist/src/route-failover.js.map +0 -1
  61. package/dist/src/seller-catalog.d.ts.map +0 -1
  62. package/dist/src/seller-catalog.js.map +0 -1
  63. package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
  64. package/dist/src/seller-concurrency-limiter.js.map +0 -1
  65. package/dist/src/seller-metadata-cache.d.ts.map +0 -1
  66. package/dist/src/seller-metadata-cache.js.map +0 -1
  67. package/dist/src/seller-pool.d.ts.map +0 -1
  68. package/dist/src/seller-pool.js.map +0 -1
  69. package/dist/src/seller-route-planner.d.ts.map +0 -1
  70. package/dist/src/seller-route-planner.js.map +0 -1
  71. package/dist/src/seller-routing-config.d.ts.map +0 -1
  72. package/dist/src/seller-routing-config.js.map +0 -1
  73. package/dist/src/seller-routing-strategy.d.ts.map +0 -1
  74. package/dist/src/seller-routing-strategy.js.map +0 -1
  75. package/dist/src/stream-failover.d.ts.map +0 -1
  76. package/dist/src/stream-failover.js.map +0 -1
  77. package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
  78. package/dist/src/tb-clawtip-proof.js.map +0 -1
  79. package/dist/src/tb-proxyd.d.ts.map +0 -1
  80. package/dist/src/tb-proxyd.js.map +0 -1
  81. package/dist/src/terminal-detect.d.ts.map +0 -1
  82. package/dist/src/terminal-detect.js.map +0 -1
  83. package/dist/src/terminal-image.d.ts.map +0 -1
  84. package/dist/src/terminal-image.js.map +0 -1
  85. package/src/buyer-store.ts +0 -1090
  86. package/src/clawtip-bootstrap.ts +0 -65
  87. package/src/cli.ts +0 -2243
  88. package/src/credit-tracker.ts +0 -295
  89. package/src/daemon.ts +0 -5475
  90. package/src/doctor-clawtip-wallet.ts +0 -95
  91. package/src/doctor-diagnostics.ts +0 -1026
  92. package/src/index.ts +0 -16
  93. package/src/init-clawtip-activation.ts +0 -695
  94. package/src/init-payment-options.ts +0 -373
  95. package/src/init-setup.ts +0 -165
  96. package/src/model-index.ts +0 -278
  97. package/src/package-update.ts +0 -311
  98. package/src/prewarm-cache.ts +0 -485
  99. package/src/prewarm-scheduler.ts +0 -675
  100. package/src/provider-install.ts +0 -1006
  101. package/src/provider-routing-config.ts +0 -410
  102. package/src/registry-trust.ts +0 -51
  103. package/src/route-failover.ts +0 -304
  104. package/src/seller-catalog.ts +0 -505
  105. package/src/seller-concurrency-limiter.ts +0 -161
  106. package/src/seller-metadata-cache.ts +0 -91
  107. package/src/seller-pool.ts +0 -557
  108. package/src/seller-route-planner.ts +0 -513
  109. package/src/seller-routing-config.ts +0 -211
  110. package/src/seller-routing-strategy.ts +0 -362
  111. package/src/stream-failover.ts +0 -152
  112. package/src/tb-clawtip-proof.ts +0 -28
  113. package/src/tb-proxyd.ts +0 -101
  114. package/src/terminal-detect.ts +0 -333
  115. package/src/terminal-image.ts +0 -228
  116. package/static/ui/assets/index-0MVXD7bH.css +0 -1
  117. package/static/ui/assets/index-BVbeDEwq.js +0 -271
  118. package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
  119. package/tests/cli-routing.test.ts +0 -363
  120. package/tests/control-plane-ui-endpoints.test.ts +0 -1630
  121. package/tests/credit-tracker.test.ts +0 -165
  122. package/tests/daemon-413-fallback.test.ts +0 -92
  123. package/tests/daemon-classify.test.ts +0 -452
  124. package/tests/daemon-roles.test.ts +0 -92
  125. package/tests/daemon-trusted-registry-cache.test.ts +0 -132
  126. package/tests/e2e.test.ts +0 -366
  127. package/tests/image-generation-e2e.test.ts +0 -230
  128. package/tests/model-index.test.ts +0 -198
  129. package/tests/package-update.test.ts +0 -147
  130. package/tests/prewarm-cache.test.ts +0 -296
  131. package/tests/prewarm-scheduler.test.ts +0 -367
  132. package/tests/provider-routing-config.test.ts +0 -150
  133. package/tests/registry-trust.test.ts +0 -28
  134. package/tests/route-failover.test.ts +0 -222
  135. package/tests/seller-catalog-413.test.ts +0 -120
  136. package/tests/seller-catalog-utilities.test.ts +0 -124
  137. package/tests/seller-concurrency-limiter.test.ts +0 -83
  138. package/tests/seller-metadata-cache.test.ts +0 -89
  139. package/tests/seller-pool.test.ts +0 -365
  140. package/tests/seller-route-planner.test.ts +0 -312
  141. package/tests/seller-routing-config.test.ts +0 -124
  142. package/tests/seller-routing-strategy.test.ts +0 -167
  143. package/tests/stream-failover.test.ts +0 -52
  144. package/tests/thousand-seller.test.ts +0 -151
  145. package/tests/tokenbuddy.test.ts +0 -4043
  146. 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
- }