@preapexis/pi-kit 1.0.13 → 1.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/extensions/litellm-provider.ts +302 -0
- package/package.json +2 -2
- package/scripts/git-release.mjs +144 -0
- package/tests/extensions.test.ts +31 -19
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// cSpell:words litellm preapexis deepseek
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
type LiteLlmModelInfo = {
|
|
5
|
+
model_name?: string;
|
|
6
|
+
litellm_params?: {
|
|
7
|
+
model?: string;
|
|
8
|
+
};
|
|
9
|
+
model_info?: {
|
|
10
|
+
id?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
display_name?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
max_tokens?: number;
|
|
15
|
+
max_input_tokens?: number;
|
|
16
|
+
context_window?: number;
|
|
17
|
+
input_cost_per_token?: number;
|
|
18
|
+
output_cost_per_token?: number;
|
|
19
|
+
supports_vision?: boolean;
|
|
20
|
+
supports_function_calling?: boolean;
|
|
21
|
+
mode?: string;
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type OpenAiModel = {
|
|
26
|
+
id?: string;
|
|
27
|
+
object?: string;
|
|
28
|
+
owned_by?: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type ModelPayload = {
|
|
32
|
+
data?: Array<LiteLlmModelInfo | OpenAiModel>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type PiModel = {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
reasoning: boolean;
|
|
39
|
+
input: Array<"text" | "image">;
|
|
40
|
+
cost: {
|
|
41
|
+
input: number;
|
|
42
|
+
output: number;
|
|
43
|
+
cacheRead: number;
|
|
44
|
+
cacheWrite: number;
|
|
45
|
+
};
|
|
46
|
+
contextWindow: number;
|
|
47
|
+
maxTokens: number;
|
|
48
|
+
compat?: {
|
|
49
|
+
supportsDeveloperRole?: boolean;
|
|
50
|
+
supportsReasoningEffort?: boolean;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const PROVIDER_ID = "litellm";
|
|
55
|
+
const DEFAULT_BASE_URL = "http://localhost:4000/v1";
|
|
56
|
+
|
|
57
|
+
export default async function (pi: ExtensionAPI): Promise<void> {
|
|
58
|
+
const baseUrl = normalizeBaseUrl(
|
|
59
|
+
process.env.LITELLM_BASE_URL ?? DEFAULT_BASE_URL
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
async function registerLiteLlmProvider(): Promise<void> {
|
|
63
|
+
const models = await discoverModels(baseUrl);
|
|
64
|
+
|
|
65
|
+
pi.registerProvider(PROVIDER_ID, {
|
|
66
|
+
name: "LiteLLM",
|
|
67
|
+
baseUrl,
|
|
68
|
+
apiKey: "$LITELLM_API_KEY",
|
|
69
|
+
api: "openai-completions",
|
|
70
|
+
models
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await registerLiteLlmProvider();
|
|
75
|
+
|
|
76
|
+
pi.registerCommand("litellm-refresh", {
|
|
77
|
+
description: "Refresh LiteLLM models from the LiteLLM proxy",
|
|
78
|
+
handler: async (_args, ctx) => {
|
|
79
|
+
try {
|
|
80
|
+
await registerLiteLlmProvider();
|
|
81
|
+
|
|
82
|
+
ctx.ui.notify(
|
|
83
|
+
[
|
|
84
|
+
"LiteLLM models refreshed.",
|
|
85
|
+
"",
|
|
86
|
+
`Base URL: ${baseUrl}`,
|
|
87
|
+
"",
|
|
88
|
+
"Run /model to select a LiteLLM model."
|
|
89
|
+
].join("\n"),
|
|
90
|
+
"info"
|
|
91
|
+
);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
ctx.ui.notify(
|
|
94
|
+
[
|
|
95
|
+
"LiteLLM model refresh failed.",
|
|
96
|
+
"",
|
|
97
|
+
error instanceof Error ? error.message : String(error)
|
|
98
|
+
].join("\n"),
|
|
99
|
+
"error"
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function normalizeBaseUrl(value: string): string {
|
|
107
|
+
return value.replace(/\/+$/, "");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function getProxyRoot(baseUrl: string): string {
|
|
111
|
+
return baseUrl.endsWith("/v1") ? baseUrl.slice(0, -3) : baseUrl;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function discoverModels(baseUrl: string): Promise<PiModel[]> {
|
|
115
|
+
const proxyRoot = getProxyRoot(baseUrl);
|
|
116
|
+
|
|
117
|
+
const endpoints = [
|
|
118
|
+
`${proxyRoot}/v1/model/info`,
|
|
119
|
+
`${proxyRoot}/model/info`,
|
|
120
|
+
`${baseUrl}/models`
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
for (const endpoint of endpoints) {
|
|
124
|
+
try {
|
|
125
|
+
const payload = await fetchModelPayload(endpoint);
|
|
126
|
+
const models = payloadToPiModels(payload);
|
|
127
|
+
|
|
128
|
+
if (models.length > 0) {
|
|
129
|
+
return models;
|
|
130
|
+
}
|
|
131
|
+
} catch {
|
|
132
|
+
// Try the next endpoint.
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const fallbackModels = getFallbackModels();
|
|
137
|
+
|
|
138
|
+
if (fallbackModels.length > 0) {
|
|
139
|
+
return fallbackModels;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
throw new Error(
|
|
143
|
+
[
|
|
144
|
+
"Could not discover LiteLLM models.",
|
|
145
|
+
"",
|
|
146
|
+
`Tried: ${endpoints.join(", ")}`,
|
|
147
|
+
"",
|
|
148
|
+
"Make sure LiteLLM is running and set LITELLM_API_KEY if your proxy requires auth.",
|
|
149
|
+
"You can also set LITELLM_MODELS as a comma-separated fallback."
|
|
150
|
+
].join("\n")
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function fetchModelPayload(url: string): Promise<ModelPayload> {
|
|
155
|
+
const headers: Record<string, string> = {
|
|
156
|
+
accept: "application/json"
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const apiKey = process.env.LITELLM_API_KEY;
|
|
160
|
+
|
|
161
|
+
if (apiKey) {
|
|
162
|
+
headers.authorization = `Bearer ${apiKey}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const response = await fetch(url, {
|
|
166
|
+
method: "GET",
|
|
167
|
+
headers
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
throw new Error(`${url} returned HTTP ${response.status}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return (await response.json()) as ModelPayload;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function payloadToPiModels(payload: ModelPayload): PiModel[] {
|
|
178
|
+
const data = Array.isArray(payload.data) ? payload.data : [];
|
|
179
|
+
const seen = new Set<string>();
|
|
180
|
+
const models: PiModel[] = [];
|
|
181
|
+
|
|
182
|
+
for (const item of data) {
|
|
183
|
+
const model = modelFromPayloadItem(item);
|
|
184
|
+
|
|
185
|
+
if (!model) continue;
|
|
186
|
+
if (seen.has(model.id)) continue;
|
|
187
|
+
|
|
188
|
+
seen.add(model.id);
|
|
189
|
+
models.push(model);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return models.sort((a, b) => a.id.localeCompare(b.id));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function modelFromPayloadItem(
|
|
196
|
+
item: LiteLlmModelInfo | OpenAiModel
|
|
197
|
+
): PiModel | undefined {
|
|
198
|
+
const modelInfo = "model_info" in item ? item.model_info : undefined;
|
|
199
|
+
|
|
200
|
+
const id =
|
|
201
|
+
"model_name" in item && item.model_name
|
|
202
|
+
? item.model_name
|
|
203
|
+
: "id" in item && item.id
|
|
204
|
+
? item.id
|
|
205
|
+
: modelInfo?.id;
|
|
206
|
+
|
|
207
|
+
if (!id) return undefined;
|
|
208
|
+
|
|
209
|
+
const name = modelInfo?.display_name ?? modelInfo?.name ?? id;
|
|
210
|
+
|
|
211
|
+
const contextWindow =
|
|
212
|
+
modelInfo?.context_window ?? modelInfo?.max_input_tokens ?? 128000;
|
|
213
|
+
|
|
214
|
+
const maxTokens = modelInfo?.max_tokens ?? 4096;
|
|
215
|
+
|
|
216
|
+
const supportsVision =
|
|
217
|
+
modelInfo?.supports_vision === true || looksLikeVisionModel(id);
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
id,
|
|
221
|
+
name,
|
|
222
|
+
reasoning: looksLikeReasoningModel(id),
|
|
223
|
+
input: supportsVision ? ["text", "image"] : ["text"],
|
|
224
|
+
cost: {
|
|
225
|
+
input: costPerMillion(modelInfo?.input_cost_per_token),
|
|
226
|
+
output: costPerMillion(modelInfo?.output_cost_per_token),
|
|
227
|
+
cacheRead: 0,
|
|
228
|
+
cacheWrite: 0
|
|
229
|
+
},
|
|
230
|
+
contextWindow,
|
|
231
|
+
maxTokens,
|
|
232
|
+
compat: {
|
|
233
|
+
supportsDeveloperRole: false,
|
|
234
|
+
supportsReasoningEffort: false
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function costPerMillion(value: number | undefined): number {
|
|
240
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return value * 1_000_000;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function looksLikeVisionModel(id: string): boolean {
|
|
248
|
+
const lower = id.toLowerCase();
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
lower.includes("vision") ||
|
|
252
|
+
lower.includes("vl") ||
|
|
253
|
+
lower.includes("gpt-4o") ||
|
|
254
|
+
lower.includes("gemini") ||
|
|
255
|
+
lower.includes("claude")
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function looksLikeReasoningModel(id: string): boolean {
|
|
260
|
+
const lower = id.toLowerCase();
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
lower.includes("reason") ||
|
|
264
|
+
lower.includes("thinking") ||
|
|
265
|
+
lower.includes("o1") ||
|
|
266
|
+
lower.includes("o3") ||
|
|
267
|
+
lower.includes("o4") ||
|
|
268
|
+
lower.includes("r1") ||
|
|
269
|
+
lower.includes("deepseek")
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function getFallbackModels(): PiModel[] {
|
|
274
|
+
const raw = process.env.LITELLM_MODELS;
|
|
275
|
+
|
|
276
|
+
if (!raw?.trim()) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return raw
|
|
281
|
+
.split(",")
|
|
282
|
+
.map((value) => value.trim())
|
|
283
|
+
.filter(Boolean)
|
|
284
|
+
.map((id) => ({
|
|
285
|
+
id,
|
|
286
|
+
name: id,
|
|
287
|
+
reasoning: looksLikeReasoningModel(id),
|
|
288
|
+
input: looksLikeVisionModel(id) ? ["text", "image"] : ["text"],
|
|
289
|
+
cost: {
|
|
290
|
+
input: 0,
|
|
291
|
+
output: 0,
|
|
292
|
+
cacheRead: 0,
|
|
293
|
+
cacheWrite: 0
|
|
294
|
+
},
|
|
295
|
+
contextWindow: 128000,
|
|
296
|
+
maxTokens: 4096,
|
|
297
|
+
compat: {
|
|
298
|
+
supportsDeveloperRole: false,
|
|
299
|
+
supportsReasoningEffort: false
|
|
300
|
+
}
|
|
301
|
+
}));
|
|
302
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preapexis/pi-kit",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Personal Pi coding-agent kit with safety extensions, status UI, prompt workflows, skills, and themes.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"scripts": {
|
|
34
34
|
"test": "vitest run",
|
|
35
35
|
"test:watch": "vitest",
|
|
36
|
-
"
|
|
36
|
+
"git": "node scripts/git-release.mjs"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"typescript": "^5.5.0",
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
4
|
+
import readline from "node:readline/promises";
|
|
5
|
+
|
|
6
|
+
const rl = readline.createInterface({ input, output });
|
|
7
|
+
|
|
8
|
+
const message =
|
|
9
|
+
process.env.npm_config_msg ||
|
|
10
|
+
process.argv.slice(2).join(" ") ||
|
|
11
|
+
"chore: release";
|
|
12
|
+
|
|
13
|
+
function run(command, args) {
|
|
14
|
+
console.log(`\n> ${command} ${args.join(" ")}\n`);
|
|
15
|
+
|
|
16
|
+
const result = spawnSync(command, args, {
|
|
17
|
+
stdio: "inherit",
|
|
18
|
+
shell: true
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (result.status !== 0) {
|
|
22
|
+
console.error(`\nCommand failed: ${command} ${args.join(" ")}`);
|
|
23
|
+
process.exit(result.status ?? 1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function askYesNo(question, defaultValue = false) {
|
|
28
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
29
|
+
const answer = (await rl.question(`${question} (${suffix}): `))
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase();
|
|
32
|
+
|
|
33
|
+
if (!answer) return defaultValue;
|
|
34
|
+
|
|
35
|
+
return answer === "y" || answer === "yes";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function askVersionType() {
|
|
39
|
+
while (true) {
|
|
40
|
+
const answer = (await rl.question("Version bump type? patch/minor/major: "))
|
|
41
|
+
.trim()
|
|
42
|
+
.toLowerCase();
|
|
43
|
+
|
|
44
|
+
if (["patch", "minor", "major"].includes(answer)) {
|
|
45
|
+
return answer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log("Please type patch, minor, or major.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function readJson(path) {
|
|
53
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function writeJson(path, data) {
|
|
57
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function bumpVersion(version, type) {
|
|
61
|
+
const parts = version.split(".").map(Number);
|
|
62
|
+
|
|
63
|
+
if (parts.length !== 3 || parts.some(Number.isNaN)) {
|
|
64
|
+
throw new Error(`Invalid version: ${version}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (type === "major") {
|
|
68
|
+
parts[0] += 1;
|
|
69
|
+
parts[1] = 0;
|
|
70
|
+
parts[2] = 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (type === "minor") {
|
|
74
|
+
parts[1] += 1;
|
|
75
|
+
parts[2] = 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (type === "patch") {
|
|
79
|
+
parts[2] += 1;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return parts.join(".");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function updatePackageVersion(nextVersion) {
|
|
86
|
+
const pkg = readJson("package.json");
|
|
87
|
+
|
|
88
|
+
pkg.version = nextVersion;
|
|
89
|
+
writeJson("package.json", pkg);
|
|
90
|
+
|
|
91
|
+
if (existsSync("package-lock.json")) {
|
|
92
|
+
const lock = readJson("package-lock.json");
|
|
93
|
+
|
|
94
|
+
if (lock.version) {
|
|
95
|
+
lock.version = nextVersion;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (lock.packages?.[""]) {
|
|
99
|
+
lock.packages[""].version = nextVersion;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
writeJson("package-lock.json", lock);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const shouldBump = await askYesNo(
|
|
108
|
+
"Do you want to bump package version?",
|
|
109
|
+
true
|
|
110
|
+
);
|
|
111
|
+
let nextVersion = null;
|
|
112
|
+
let tag = null;
|
|
113
|
+
|
|
114
|
+
if (shouldBump) {
|
|
115
|
+
const bumpType = await askVersionType();
|
|
116
|
+
const pkg = readJson("package.json");
|
|
117
|
+
|
|
118
|
+
nextVersion = bumpVersion(pkg.version, bumpType);
|
|
119
|
+
tag = `v${nextVersion}`;
|
|
120
|
+
|
|
121
|
+
updatePackageVersion(nextVersion);
|
|
122
|
+
|
|
123
|
+
console.log(`\nBumped version to ${nextVersion}\n`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const shouldPublish = await askYesNo("Do you want to publish to npm?", false);
|
|
127
|
+
|
|
128
|
+
run("git", ["add", "."]);
|
|
129
|
+
run("git", ["commit", "-m", `"${message}"`]);
|
|
130
|
+
|
|
131
|
+
if (tag) {
|
|
132
|
+
run("git", ["tag", tag]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
run("git", ["push", "--follow-tags"]);
|
|
136
|
+
|
|
137
|
+
if (shouldPublish) {
|
|
138
|
+
run("npm", ["publish", "--access", "public"]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
console.log("\nDone.\n");
|
|
142
|
+
} finally {
|
|
143
|
+
rl.close();
|
|
144
|
+
}
|
package/tests/extensions.test.ts
CHANGED
|
@@ -1,49 +1,61 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { checkTypeScriptSyntax, listFiles, readFile } from "./helpers.js";
|
|
4
|
+
|
|
5
|
+
function hasPiDefaultExport(source: string): boolean {
|
|
6
|
+
return (
|
|
7
|
+
/export\s+default\s+async\s+function\b/.test(source) ||
|
|
8
|
+
/export\s+default\s+function\b/.test(source) ||
|
|
9
|
+
/export\s+default\s+\(/.test(source) ||
|
|
10
|
+
/export\s*\{[^}]*\bas\s+default\b[^}]*\}/.test(source)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
9
13
|
|
|
10
14
|
describe("extensions", () => {
|
|
11
15
|
const extDir = path.resolve("extensions");
|
|
12
16
|
|
|
13
17
|
it("should have at least one extension file", async () => {
|
|
14
18
|
const files = await listFiles(extDir);
|
|
15
|
-
const tsFiles = files.filter((
|
|
19
|
+
const tsFiles = files.filter((file: string) => file.endsWith(".ts"));
|
|
20
|
+
|
|
16
21
|
expect(tsFiles.length).toBeGreaterThan(0);
|
|
17
22
|
});
|
|
18
23
|
|
|
19
|
-
it("should export a default function", async () => {
|
|
24
|
+
it("should export a default extension function", async () => {
|
|
20
25
|
const files = await listFiles(extDir);
|
|
21
|
-
const tsFiles = files.filter((
|
|
26
|
+
const tsFiles = files.filter((file: string) => file.endsWith(".ts"));
|
|
22
27
|
|
|
23
28
|
for (const file of tsFiles) {
|
|
24
29
|
const source = await readFile(file);
|
|
25
|
-
|
|
30
|
+
|
|
31
|
+
expect(
|
|
32
|
+
hasPiDefaultExport(source),
|
|
33
|
+
`${path.basename(file)} should export a default function`
|
|
34
|
+
).toBe(true);
|
|
26
35
|
}
|
|
27
36
|
});
|
|
28
37
|
|
|
29
|
-
it("should have
|
|
38
|
+
it("should have valid TypeScript syntax", async () => {
|
|
30
39
|
const files = await listFiles(extDir);
|
|
31
|
-
const tsFiles = files.filter((
|
|
40
|
+
const tsFiles = files.filter((file: string) => file.endsWith(".ts"));
|
|
32
41
|
|
|
33
42
|
for (const file of tsFiles) {
|
|
34
43
|
const source = await readFile(file);
|
|
35
44
|
const result = checkTypeScriptSyntax(source, path.basename(file));
|
|
36
|
-
|
|
45
|
+
|
|
46
|
+
expect(result.errors, `${path.basename(file)} has syntax errors`).toEqual(
|
|
47
|
+
[]
|
|
48
|
+
);
|
|
37
49
|
}
|
|
38
50
|
});
|
|
39
51
|
|
|
40
|
-
it("should not contain duplicate
|
|
52
|
+
it("should not contain duplicate extension file names", async () => {
|
|
41
53
|
const files = await listFiles(extDir);
|
|
42
|
-
const tsFiles = files.filter((
|
|
54
|
+
const tsFiles = files.filter((file: string) => file.endsWith(".ts"));
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
const names = tsFiles.map((f) => path.basename(f, ".ts"));
|
|
56
|
+
const names = tsFiles.map((file: string) => path.basename(file, ".ts"));
|
|
46
57
|
const uniqueNames = new Set(names);
|
|
58
|
+
|
|
47
59
|
expect(uniqueNames.size).toBe(names.length);
|
|
48
60
|
});
|
|
49
61
|
});
|