@tokenbuddy/tokenbuddy 1.0.36 → 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.
- package/dist/src/buyer-store.d.ts +6 -1
- package/dist/src/buyer-store.js +43 -4
- package/dist/src/cli.js +2 -2
- package/dist/src/daemon.d.ts +12 -0
- package/dist/src/daemon.js +791 -61
- package/dist/src/doctor-diagnostics.js +1 -6
- package/dist/src/provider-install.d.ts +2 -2
- package/dist/src/provider-install.js +248 -2
- package/dist/src/seller-catalog.d.ts +21 -0
- package/dist/src/seller-catalog.js +17 -0
- package/dist/src/seller-route-planner.d.ts +4 -1
- package/dist/src/seller-route-planner.js +3 -0
- package/dist/src/seller-routing-strategy.d.ts +3 -0
- package/dist/src/terminal-detect.d.ts +1 -1
- package/dist/src/terminal-detect.js +3 -2
- package/package.json +15 -2
- package/static/ui/assets/index-Djfl9tw5.js +271 -0
- package/static/ui/assets/index-DkfztCkn.css +1 -0
- package/static/ui/index.html +2 -2
- package/dist/src/buyer-store.d.ts.map +0 -1
- package/dist/src/buyer-store.js.map +0 -1
- package/dist/src/clawtip-bootstrap.d.ts.map +0 -1
- package/dist/src/clawtip-bootstrap.js.map +0 -1
- package/dist/src/cli.d.ts.map +0 -1
- package/dist/src/cli.js.map +0 -1
- package/dist/src/credit-tracker.d.ts.map +0 -1
- package/dist/src/credit-tracker.js.map +0 -1
- package/dist/src/daemon.d.ts.map +0 -1
- package/dist/src/daemon.js.map +0 -1
- package/dist/src/doctor-clawtip-wallet.d.ts.map +0 -1
- package/dist/src/doctor-clawtip-wallet.js.map +0 -1
- package/dist/src/doctor-diagnostics.d.ts.map +0 -1
- package/dist/src/doctor-diagnostics.js.map +0 -1
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js.map +0 -1
- package/dist/src/init-clawtip-activation.d.ts.map +0 -1
- package/dist/src/init-clawtip-activation.js.map +0 -1
- package/dist/src/init-payment-options.d.ts.map +0 -1
- package/dist/src/init-payment-options.js.map +0 -1
- package/dist/src/init-setup.d.ts.map +0 -1
- package/dist/src/init-setup.js.map +0 -1
- package/dist/src/model-index.d.ts.map +0 -1
- package/dist/src/model-index.js.map +0 -1
- package/dist/src/package-update.d.ts.map +0 -1
- package/dist/src/package-update.js.map +0 -1
- package/dist/src/prewarm-cache.d.ts.map +0 -1
- package/dist/src/prewarm-cache.js.map +0 -1
- package/dist/src/prewarm-scheduler.d.ts.map +0 -1
- package/dist/src/prewarm-scheduler.js.map +0 -1
- package/dist/src/provider-install.d.ts.map +0 -1
- package/dist/src/provider-install.js.map +0 -1
- package/dist/src/provider-routing-config.d.ts.map +0 -1
- package/dist/src/provider-routing-config.js.map +0 -1
- package/dist/src/registry-trust.d.ts.map +0 -1
- package/dist/src/registry-trust.js.map +0 -1
- package/dist/src/route-failover.d.ts.map +0 -1
- package/dist/src/route-failover.js.map +0 -1
- package/dist/src/seller-catalog.d.ts.map +0 -1
- package/dist/src/seller-catalog.js.map +0 -1
- package/dist/src/seller-concurrency-limiter.d.ts.map +0 -1
- package/dist/src/seller-concurrency-limiter.js.map +0 -1
- package/dist/src/seller-metadata-cache.d.ts.map +0 -1
- package/dist/src/seller-metadata-cache.js.map +0 -1
- package/dist/src/seller-pool.d.ts.map +0 -1
- package/dist/src/seller-pool.js.map +0 -1
- package/dist/src/seller-route-planner.d.ts.map +0 -1
- package/dist/src/seller-route-planner.js.map +0 -1
- package/dist/src/seller-routing-config.d.ts.map +0 -1
- package/dist/src/seller-routing-config.js.map +0 -1
- package/dist/src/seller-routing-strategy.d.ts.map +0 -1
- package/dist/src/seller-routing-strategy.js.map +0 -1
- package/dist/src/stream-failover.d.ts.map +0 -1
- package/dist/src/stream-failover.js.map +0 -1
- package/dist/src/tb-clawtip-proof.d.ts.map +0 -1
- package/dist/src/tb-clawtip-proof.js.map +0 -1
- package/dist/src/tb-proxyd.d.ts.map +0 -1
- package/dist/src/tb-proxyd.js.map +0 -1
- package/dist/src/terminal-detect.d.ts.map +0 -1
- package/dist/src/terminal-detect.js.map +0 -1
- package/dist/src/terminal-image.d.ts.map +0 -1
- package/dist/src/terminal-image.js.map +0 -1
- package/src/buyer-store.ts +0 -1090
- package/src/clawtip-bootstrap.ts +0 -65
- package/src/cli.ts +0 -2243
- package/src/credit-tracker.ts +0 -295
- package/src/daemon.ts +0 -5475
- package/src/doctor-clawtip-wallet.ts +0 -95
- package/src/doctor-diagnostics.ts +0 -1026
- package/src/index.ts +0 -16
- package/src/init-clawtip-activation.ts +0 -695
- package/src/init-payment-options.ts +0 -373
- package/src/init-setup.ts +0 -165
- package/src/model-index.ts +0 -278
- package/src/package-update.ts +0 -311
- package/src/prewarm-cache.ts +0 -485
- package/src/prewarm-scheduler.ts +0 -675
- package/src/provider-install.ts +0 -1006
- package/src/provider-routing-config.ts +0 -410
- package/src/registry-trust.ts +0 -51
- package/src/route-failover.ts +0 -304
- package/src/seller-catalog.ts +0 -505
- package/src/seller-concurrency-limiter.ts +0 -161
- package/src/seller-metadata-cache.ts +0 -91
- package/src/seller-pool.ts +0 -557
- package/src/seller-route-planner.ts +0 -513
- package/src/seller-routing-config.ts +0 -211
- package/src/seller-routing-strategy.ts +0 -362
- package/src/stream-failover.ts +0 -152
- package/src/tb-clawtip-proof.ts +0 -28
- package/src/tb-proxyd.ts +0 -101
- package/src/terminal-detect.ts +0 -333
- package/src/terminal-image.ts +0 -228
- package/static/ui/assets/index-0MVXD7bH.css +0 -1
- package/static/ui/assets/index-BVbeDEwq.js +0 -271
- package/static/ui/assets/index-BVbeDEwq.js.map +0 -1
- package/tests/cli-routing.test.ts +0 -363
- package/tests/control-plane-ui-endpoints.test.ts +0 -1630
- package/tests/credit-tracker.test.ts +0 -165
- package/tests/daemon-413-fallback.test.ts +0 -92
- package/tests/daemon-classify.test.ts +0 -452
- package/tests/daemon-roles.test.ts +0 -92
- package/tests/daemon-trusted-registry-cache.test.ts +0 -132
- package/tests/e2e.test.ts +0 -366
- package/tests/image-generation-e2e.test.ts +0 -230
- package/tests/model-index.test.ts +0 -198
- package/tests/package-update.test.ts +0 -147
- package/tests/prewarm-cache.test.ts +0 -296
- package/tests/prewarm-scheduler.test.ts +0 -367
- package/tests/provider-routing-config.test.ts +0 -150
- package/tests/registry-trust.test.ts +0 -28
- package/tests/route-failover.test.ts +0 -222
- package/tests/seller-catalog-413.test.ts +0 -120
- package/tests/seller-catalog-utilities.test.ts +0 -124
- package/tests/seller-concurrency-limiter.test.ts +0 -83
- package/tests/seller-metadata-cache.test.ts +0 -89
- package/tests/seller-pool.test.ts +0 -365
- package/tests/seller-route-planner.test.ts +0 -312
- package/tests/seller-routing-config.test.ts +0 -124
- package/tests/seller-routing-strategy.test.ts +0 -167
- package/tests/stream-failover.test.ts +0 -52
- package/tests/thousand-seller.test.ts +0 -151
- package/tests/tokenbuddy.test.ts +0 -4043
- package/tsconfig.json +0 -8
package/src/model-index.ts
DELETED
|
@@ -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
|
-
}
|
package/src/package-update.ts
DELETED
|
@@ -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
|
-
}
|