pi-free 2.0.14 → 2.1.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/CHANGELOG.md +90 -0
- package/README.md +64 -78
- package/banner.svg +21 -36
- package/config.ts +123 -9
- package/constants.ts +3 -9
- package/index.ts +14 -15
- package/lib/built-in-toggle.ts +29 -16
- package/lib/json-persistence.ts +90 -22
- package/lib/logger.ts +21 -12
- package/lib/model-detection.ts +2 -12
- package/lib/model-enhancer.ts +11 -2
- package/lib/model-metadata.ts +387 -0
- package/lib/open-browser.ts +74 -24
- package/lib/paths.ts +90 -0
- package/lib/probe-cache.ts +19 -19
- package/lib/provider-cache.ts +74 -28
- package/lib/provider-compat.ts +58 -9
- package/lib/provider-probe.ts +188 -0
- package/lib/registry.ts +1 -5
- package/lib/session-start-metrics.ts +46 -0
- package/lib/telemetry.ts +115 -86
- package/lib/types.ts +22 -2
- package/lib/util.ts +80 -21
- package/package.json +7 -2
- package/provider-failover/benchmark-lookup.ts +17 -5
- package/provider-helper.ts +11 -2
- package/providers/cline/cline-models.ts +12 -2
- package/providers/cline/cline-xml-bridge.ts +974 -0
- package/providers/cline/cline.ts +67 -176
- package/providers/crofai/crofai.ts +6 -1
- package/providers/deepinfra/deepinfra.ts +69 -2
- package/providers/dynamic-built-in/index.ts +237 -2
- package/providers/kilo/kilo-models.ts +3 -1
- package/providers/kilo/kilo.ts +268 -41
- package/providers/model-fetcher.ts +18 -55
- package/providers/novita/novita.ts +69 -2
- package/providers/ollama/ollama.ts +48 -24
- package/providers/opencode-session.ts +67 -2
- package/providers/routeway/routeway.ts +188 -2
- package/providers/sambanova/sambanova.ts +67 -1
- package/providers/together/together.ts +69 -2
- package/providers/tokenrouter/tokenrouter.ts +378 -0
- package/providers/zenmux/zenmux.ts +6 -1
- package/scripts/check-extensions.mjs +32 -16
- package/providers/nvidia/nvidia.ts +0 -504
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TokenRouter Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* TokenRouter is an OpenAI-compatible API gateway routing to 90+ models
|
|
5
|
+
* across multiple providers (OpenAI, Anthropic, Google, DeepSeek, Qwen, etc.).
|
|
6
|
+
*
|
|
7
|
+
* API: https://api.tokenrouter.com/v1
|
|
8
|
+
* Models: /v1/models
|
|
9
|
+
*
|
|
10
|
+
* Setup:
|
|
11
|
+
* TOKENROUTER_API_KEY=sk-...
|
|
12
|
+
* # or add tokenrouter_api_key to ~/.pi/free.json
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
ExtensionAPI,
|
|
17
|
+
ProviderModelConfig,
|
|
18
|
+
} from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import type { AssistantMessage, ThinkingContent } from "@earendil-works/pi-ai";
|
|
20
|
+
import {
|
|
21
|
+
getTokenrouterApiKey,
|
|
22
|
+
getTokenrouterShowPaid,
|
|
23
|
+
applyHidden,
|
|
24
|
+
} from "../../config.ts";
|
|
25
|
+
import {
|
|
26
|
+
BASE_URL_TOKENROUTER,
|
|
27
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
28
|
+
PROVIDER_TOKENROUTER,
|
|
29
|
+
} from "../../constants.ts";
|
|
30
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
31
|
+
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
32
|
+
import {
|
|
33
|
+
DEEPSEEK_PROXY_COMPAT,
|
|
34
|
+
getProxyModelCompat,
|
|
35
|
+
isLikelyReasoningModel,
|
|
36
|
+
} from "../../lib/provider-compat.ts";
|
|
37
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
38
|
+
import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
|
|
39
|
+
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
40
|
+
|
|
41
|
+
const _logger = createLogger("tokenrouter");
|
|
42
|
+
|
|
43
|
+
// =============================================================================
|
|
44
|
+
// Reasoning cleanup
|
|
45
|
+
// TokenRouter's MiniMax-M3 model sometimes emits DeepSeek-style `<think>`
|
|
46
|
+
// reasoning tags inline in the assistant text. Pi does not strip them, so we
|
|
47
|
+
// extract them into proper ThinkingContent blocks on message_end.
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
interface ExtractedThinking {
|
|
51
|
+
text: string;
|
|
52
|
+
thinking: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function collapseWhitespace(text: string): string {
|
|
56
|
+
return text
|
|
57
|
+
.replace(/\r\n/g, "\n")
|
|
58
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
59
|
+
.replace(/[ \t]+/g, " ")
|
|
60
|
+
.trim();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function extractThinkBlocks(text: string): ExtractedThinking {
|
|
64
|
+
const openTag = "<think>";
|
|
65
|
+
const closeTag = "</think>";
|
|
66
|
+
const thinkingParts: string[] = [];
|
|
67
|
+
const textParts: string[] = [];
|
|
68
|
+
let cursor = 0;
|
|
69
|
+
|
|
70
|
+
while (cursor < text.length) {
|
|
71
|
+
const openStart = text.indexOf(openTag, cursor);
|
|
72
|
+
if (openStart === -1) {
|
|
73
|
+
textParts.push(text.slice(cursor));
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
textParts.push(text.slice(cursor, openStart));
|
|
78
|
+
const valueStart = openStart + openTag.length;
|
|
79
|
+
const closeStart = text.indexOf(closeTag, valueStart);
|
|
80
|
+
if (closeStart === -1) {
|
|
81
|
+
// Unclosed think tag: treat remainder as thinking.
|
|
82
|
+
thinkingParts.push(text.slice(valueStart));
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
thinkingParts.push(text.slice(valueStart, closeStart));
|
|
87
|
+
cursor = closeStart + closeTag.length;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
text: collapseWhitespace(textParts.join("")),
|
|
92
|
+
thinking: collapseWhitespace(thinkingParts.join("\n\n")),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isTokenRouterModel(model: { provider?: string }): boolean {
|
|
97
|
+
return model.provider === PROVIDER_TOKENROUTER;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// Known Free Models
|
|
102
|
+
// TokenRouter doesn't expose pricing via /v1/models, so known-free models
|
|
103
|
+
// are hardcoded. Detected via name suffix also catches `:free`-tagged models.
|
|
104
|
+
// =============================================================================
|
|
105
|
+
|
|
106
|
+
const MINIMAX_M3_ID = "MiniMax-M3";
|
|
107
|
+
const KNOWN_FREE_MODELS = new Set([MINIMAX_M3_ID]);
|
|
108
|
+
const MINIMAX_ADAPTIVE_COMPAT: NonNullable<ProviderModelConfig["compat"]> = {
|
|
109
|
+
...DEEPSEEK_PROXY_COMPAT,
|
|
110
|
+
thinkingFormat: "deepseek",
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// =============================================================================
|
|
114
|
+
// Types
|
|
115
|
+
// =============================================================================
|
|
116
|
+
|
|
117
|
+
interface TokenRouterModel {
|
|
118
|
+
id: string;
|
|
119
|
+
object: string;
|
|
120
|
+
created: number;
|
|
121
|
+
owned_by: string;
|
|
122
|
+
supported_endpoint_types: string[];
|
|
123
|
+
tags?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// Helpers
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
/** Text-capable chat endpoints (excludes image/video/audio-only types) */
|
|
131
|
+
const CHAT_ENDPOINT_TYPES = new Set([
|
|
132
|
+
"openai",
|
|
133
|
+
"openai-response",
|
|
134
|
+
"anthropic",
|
|
135
|
+
"anthropic-compatible",
|
|
136
|
+
"gemini",
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
function isTextChatModel(model: TokenRouterModel): boolean {
|
|
140
|
+
const tags = (model.tags ?? "").toLowerCase();
|
|
141
|
+
// Exclude models whose only tags are non-text
|
|
142
|
+
const nonTextTags = ["image", "video", "audio"];
|
|
143
|
+
const hasNonTextTag = nonTextTags.some((t) => tags.includes(t));
|
|
144
|
+
const hasTextTag = tags.includes("text");
|
|
145
|
+
// If it has a text tag, include it. If only non-text tags, exclude.
|
|
146
|
+
if (hasTextTag) return true;
|
|
147
|
+
if (hasNonTextTag && !hasTextTag) return false;
|
|
148
|
+
// No tags or empty tags: check endpoint types
|
|
149
|
+
return model.supported_endpoint_types.some((t) => CHAT_ENDPOINT_TYPES.has(t));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isTokenRouterMinimaxModel(modelId: string): boolean {
|
|
153
|
+
return modelId.toLowerCase().includes("minimax");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function finalizeTokenRouterModel(
|
|
157
|
+
model: ProviderModelConfig,
|
|
158
|
+
): ProviderModelConfig {
|
|
159
|
+
if (!isTokenRouterMinimaxModel(model.id)) return model;
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
...model,
|
|
163
|
+
reasoning: true,
|
|
164
|
+
compat: {
|
|
165
|
+
...MINIMAX_ADAPTIVE_COMPAT,
|
|
166
|
+
...(model.compat ?? {}),
|
|
167
|
+
thinkingFormat: "deepseek",
|
|
168
|
+
supportsReasoningEffort: true,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function normalizeAssistantMessage(
|
|
174
|
+
message: AssistantMessage,
|
|
175
|
+
): AssistantMessage {
|
|
176
|
+
const newContent: AssistantMessage["content"] = [];
|
|
177
|
+
let extractedThinking = "";
|
|
178
|
+
|
|
179
|
+
for (const block of message.content) {
|
|
180
|
+
if (block.type !== "text") {
|
|
181
|
+
newContent.push(block);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const extracted = extractThinkBlocks(block.text);
|
|
186
|
+
if (extracted.thinking) {
|
|
187
|
+
extractedThinking = extractedThinking
|
|
188
|
+
? `${extractedThinking}\n\n${extracted.thinking}`
|
|
189
|
+
: extracted.thinking;
|
|
190
|
+
}
|
|
191
|
+
if (extracted.text) {
|
|
192
|
+
newContent.push({ ...block, text: extracted.text });
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (extractedThinking) {
|
|
197
|
+
newContent.push({
|
|
198
|
+
type: "thinking",
|
|
199
|
+
thinking: extractedThinking,
|
|
200
|
+
} as ThinkingContent);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { ...message, content: newContent };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function patchTokenRouterMinimaxThinkingPayload(
|
|
207
|
+
payload: unknown,
|
|
208
|
+
): unknown {
|
|
209
|
+
if (typeof payload !== "object" || payload === null) return payload;
|
|
210
|
+
const body = payload as {
|
|
211
|
+
model?: unknown;
|
|
212
|
+
thinking?: { type?: unknown };
|
|
213
|
+
};
|
|
214
|
+
if (!isTokenRouterMinimaxModel(String(body.model ?? ""))) return payload;
|
|
215
|
+
if (body.thinking?.type !== "enabled") return payload;
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
...body,
|
|
219
|
+
thinking: {
|
|
220
|
+
...body.thinking,
|
|
221
|
+
type: "adaptive",
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function mapTokenRouterModel(
|
|
227
|
+
model: TokenRouterModel,
|
|
228
|
+
): ProviderModelConfig & {
|
|
229
|
+
_pricingKnown?: boolean;
|
|
230
|
+
_freeKnown?: boolean;
|
|
231
|
+
_isFree?: boolean;
|
|
232
|
+
} {
|
|
233
|
+
const name = cleanModelName(model.id);
|
|
234
|
+
const isMinimax = isTokenRouterMinimaxModel(model.id);
|
|
235
|
+
const reasoning = isMinimax || isLikelyReasoningModel({ id: model.id, name });
|
|
236
|
+
const isResponseApi =
|
|
237
|
+
model.supported_endpoint_types.includes("openai-response");
|
|
238
|
+
const isKnownFree = KNOWN_FREE_MODELS.has(model.id);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
id: model.id,
|
|
242
|
+
name,
|
|
243
|
+
reasoning,
|
|
244
|
+
input: ["text"],
|
|
245
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
246
|
+
contextWindow: 128_000,
|
|
247
|
+
maxTokens: 16_384,
|
|
248
|
+
compat: {
|
|
249
|
+
...(isMinimax
|
|
250
|
+
? MINIMAX_ADAPTIVE_COMPAT
|
|
251
|
+
: getProxyModelCompat({ id: model.id, name })),
|
|
252
|
+
// openai-response models use a different API shape
|
|
253
|
+
...(isResponseApi ? { apiType: "openai-response" as const } : {}),
|
|
254
|
+
},
|
|
255
|
+
// Known-free models bypass pricing detection entirely
|
|
256
|
+
_freeKnown: isKnownFree,
|
|
257
|
+
_isFree: isKnownFree,
|
|
258
|
+
// Non-free models signal no pricing data (name-based detection only)
|
|
259
|
+
_pricingKnown: false,
|
|
260
|
+
} as ProviderModelConfig & { _pricingKnown?: boolean };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// =============================================================================
|
|
264
|
+
// Fetch Models
|
|
265
|
+
// =============================================================================
|
|
266
|
+
|
|
267
|
+
async function fetchTokenRouterModels(
|
|
268
|
+
apiKey: string,
|
|
269
|
+
): Promise<ProviderModelConfig[]> {
|
|
270
|
+
_logger.info("[tokenrouter] Fetching models from TokenRouter API...");
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const response = await fetchWithRetry(
|
|
274
|
+
`${BASE_URL_TOKENROUTER}/models`,
|
|
275
|
+
{
|
|
276
|
+
headers: {
|
|
277
|
+
Authorization: `Bearer ${apiKey}`,
|
|
278
|
+
Accept: "application/json",
|
|
279
|
+
"Content-Type": "application/json",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
3,
|
|
283
|
+
1000,
|
|
284
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
if (!response.ok) {
|
|
288
|
+
throw new Error(`TokenRouter API error: ${response.status}`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const json = (await response.json()) as { data?: TokenRouterModel[] };
|
|
292
|
+
const models = (json.data ?? []).filter(isTextChatModel);
|
|
293
|
+
|
|
294
|
+
_logger.info(`[tokenrouter] Fetched ${models.length} text chat models`);
|
|
295
|
+
const enriched = await safeEnrichModelsWithModelsDev(
|
|
296
|
+
models.map(mapTokenRouterModel),
|
|
297
|
+
{ providerId: PROVIDER_TOKENROUTER },
|
|
298
|
+
);
|
|
299
|
+
return applyHidden(
|
|
300
|
+
enriched.map(finalizeTokenRouterModel),
|
|
301
|
+
PROVIDER_TOKENROUTER,
|
|
302
|
+
);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
_logger.error("[tokenrouter] Failed to fetch models", {
|
|
305
|
+
error: error instanceof Error ? error.message : String(error),
|
|
306
|
+
});
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// =============================================================================
|
|
312
|
+
// Extension Entry Point
|
|
313
|
+
// =============================================================================
|
|
314
|
+
|
|
315
|
+
export default async function tokenRouterProvider(pi: ExtensionAPI) {
|
|
316
|
+
const apiKey = getTokenrouterApiKey();
|
|
317
|
+
|
|
318
|
+
if (!apiKey) {
|
|
319
|
+
_logger.info("[tokenrouter] Skipping — TOKENROUTER_API_KEY not set.");
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const allModels = await fetchTokenRouterModels(apiKey);
|
|
324
|
+
|
|
325
|
+
if (allModels.length === 0) {
|
|
326
|
+
_logger.warn("[tokenrouter] No text chat models available");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const freeModels = allModels.filter((m) =>
|
|
331
|
+
isFreeModel({ ...m, provider: PROVIDER_TOKENROUTER }, allModels),
|
|
332
|
+
);
|
|
333
|
+
const stored = { free: freeModels, all: allModels };
|
|
334
|
+
|
|
335
|
+
_logger.info(
|
|
336
|
+
`[tokenrouter] Registered ${allModels.length} models (${freeModels.length} free)`,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
const reRegister = createReRegister(pi, {
|
|
340
|
+
providerId: PROVIDER_TOKENROUTER,
|
|
341
|
+
baseUrl: BASE_URL_TOKENROUTER,
|
|
342
|
+
apiKey,
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
registerWithGlobalToggle(PROVIDER_TOKENROUTER, stored, reRegister, true);
|
|
346
|
+
|
|
347
|
+
pi.on("before_provider_request", (event) =>
|
|
348
|
+
patchTokenRouterMinimaxThinkingPayload(event.payload),
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
pi.on("message_end", (event, ctx) => {
|
|
352
|
+
if (!isTokenRouterModel(ctx.model ?? {})) return;
|
|
353
|
+
if (event.message.role !== "assistant") return;
|
|
354
|
+
return { message: normalizeAssistantMessage(event.message) };
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
setupProvider(
|
|
358
|
+
pi,
|
|
359
|
+
{
|
|
360
|
+
providerId: PROVIDER_TOKENROUTER,
|
|
361
|
+
initialShowPaid: getTokenrouterShowPaid(),
|
|
362
|
+
tosUrl: "https://tokenrouter.com/terms",
|
|
363
|
+
reRegister: (models, _stored) => {
|
|
364
|
+
if (_stored) {
|
|
365
|
+
stored.free = _stored.free;
|
|
366
|
+
stored.all = _stored.all;
|
|
367
|
+
}
|
|
368
|
+
reRegister(models);
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
stored,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const showPaid = getTokenrouterShowPaid();
|
|
375
|
+
const initialModels =
|
|
376
|
+
showPaid && stored.all.length > 0 ? stored.all : freeModels;
|
|
377
|
+
reRegister(initialModels);
|
|
378
|
+
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
PROVIDER_ZENMUX,
|
|
28
28
|
} from "../../constants.ts";
|
|
29
29
|
import { createLogger } from "../../lib/logger.ts";
|
|
30
|
+
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
30
31
|
import { getProxyModelCompat } from "../../lib/provider-compat.ts";
|
|
31
32
|
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
32
33
|
import { fetchWithRetry } from "../../lib/util.ts";
|
|
@@ -97,7 +98,7 @@ async function fetchZenmuxModels(
|
|
|
97
98
|
|
|
98
99
|
_logger.info(`[zenmux] Fetched ${models.length} models`);
|
|
99
100
|
|
|
100
|
-
|
|
101
|
+
const mapped = models.map((m) => {
|
|
101
102
|
const hasPricings = m.pricings !== undefined;
|
|
102
103
|
return {
|
|
103
104
|
id: m.id,
|
|
@@ -118,6 +119,10 @@ async function fetchZenmuxModels(
|
|
|
118
119
|
_pricingKnown: hasPricings,
|
|
119
120
|
} as ProviderModelConfig & { _pricingKnown?: boolean };
|
|
120
121
|
});
|
|
122
|
+
|
|
123
|
+
return await safeEnrichModelsWithModelsDev(mapped, {
|
|
124
|
+
providerId: PROVIDER_ZENMUX,
|
|
125
|
+
});
|
|
121
126
|
} catch (error) {
|
|
122
127
|
_logger.error("[zenmux] Failed to fetch models:", {
|
|
123
128
|
error: error instanceof Error ? error.message : String(error),
|
|
@@ -14,29 +14,45 @@ import { dirname, join, resolve } from "node:path";
|
|
|
14
14
|
const installDir = resolve(process.argv[2] ?? ".");
|
|
15
15
|
const fromSource = process.argv[2] == null;
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
function resolveNpm() {
|
|
17
|
+
function resolveNpmCli() {
|
|
19
18
|
for (const p of [
|
|
20
|
-
"
|
|
21
|
-
"/usr/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
: "",
|
|
19
|
+
join(dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js"),
|
|
20
|
+
"/usr/lib/node_modules/npm/bin/npm-cli.js",
|
|
21
|
+
"/usr/local/lib/node_modules/npm/bin/npm-cli.js",
|
|
22
|
+
"/usr/share/nodejs/npm/bin/npm-cli.js",
|
|
25
23
|
]) {
|
|
26
|
-
if (
|
|
24
|
+
if (existsSync(p)) return p;
|
|
27
25
|
}
|
|
28
|
-
|
|
26
|
+
throw new Error("Could not find npm-cli.js in known Node/npm locations");
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
function runNpmPackDryRun() {
|
|
30
|
+
return execFileSync(
|
|
31
|
+
process.execPath,
|
|
32
|
+
[resolveNpmCli(), "pack", "--dry-run", "--json"],
|
|
33
|
+
{ encoding: "utf8" },
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parsePackFileList(out) {
|
|
38
|
+
try {
|
|
39
|
+
const packed = JSON.parse(out);
|
|
40
|
+
return packed.flatMap((entry) =>
|
|
41
|
+
(entry.files ?? []).map((file) => file.path).filter(Boolean),
|
|
42
|
+
);
|
|
43
|
+
} catch {
|
|
37
44
|
return out
|
|
38
45
|
.split("\n")
|
|
39
|
-
.map((
|
|
46
|
+
.map((line) => line.match(/npm notice \S+\s+(.+)/)?.[1]?.trim())
|
|
47
|
+
.filter(Boolean);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getFiles() {
|
|
52
|
+
if (fromSource) {
|
|
53
|
+
// Use npm pack --dry-run to inspect exactly what would be published.
|
|
54
|
+
const out = runNpmPackDryRun();
|
|
55
|
+
return parsePackFileList(out)
|
|
40
56
|
.filter((f) => f && (f.endsWith(".ts") || f.endsWith(".mjs")))
|
|
41
57
|
.map((f) => join(installDir, f));
|
|
42
58
|
}
|