@etweisberg/garmin-connect-mcp 0.1.13 → 0.1.14
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/test.js +250 -101
- package/dist/tools.js +42 -26
- package/package.json +1 -1
package/dist/test.js
CHANGED
|
@@ -1,207 +1,356 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Integration test suite for garmin-connect-mcp.
|
|
3
|
+
* Tests the actual MCP tool handlers (not just raw API endpoints)
|
|
4
|
+
* to catch bugs in zip extraction, file I/O, date defaults, etc.
|
|
5
|
+
*
|
|
3
6
|
* Requires a valid session at ~/.garmin-connect-mcp/session.json.
|
|
4
7
|
* Run: npm test
|
|
5
8
|
*/
|
|
6
|
-
import {
|
|
9
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import { registerTools } from "./tools.js";
|
|
11
|
+
import { existsSync, rmSync } from "node:fs";
|
|
12
|
+
import { getSharedClient } from "./garmin-client.js";
|
|
13
|
+
const TEST_FIT_DIR = "/tmp/garmin-mcp-test-fit";
|
|
14
|
+
async function callTool(server, name, args = {}) {
|
|
15
|
+
// Access internal tool handler via the server's registered tools
|
|
16
|
+
const result = (await server._registeredTools[name].handler({ ...args }, { signal: new AbortController().signal }));
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
function getToolText(result) {
|
|
20
|
+
return result.content[0]?.text ?? "";
|
|
21
|
+
}
|
|
22
|
+
function getToolJson(result) {
|
|
23
|
+
return JSON.parse(getToolText(result));
|
|
24
|
+
}
|
|
7
25
|
const tests = [
|
|
8
|
-
// ── Session
|
|
26
|
+
// ── Session ────────────────────────────────────────────────────────
|
|
9
27
|
{
|
|
10
28
|
name: "check-session",
|
|
11
|
-
run: (
|
|
29
|
+
run: async (server) => {
|
|
30
|
+
const result = await callTool(server, "check-session");
|
|
31
|
+
if (result.isError)
|
|
32
|
+
throw new Error(getToolText(result));
|
|
33
|
+
const data = getToolJson(result);
|
|
34
|
+
if (data.status !== "ok")
|
|
35
|
+
throw new Error("status not ok");
|
|
36
|
+
},
|
|
12
37
|
},
|
|
13
38
|
{
|
|
14
39
|
name: "get-user-profile",
|
|
15
|
-
run: (
|
|
40
|
+
run: async (server) => {
|
|
41
|
+
const result = await callTool(server, "get-user-profile");
|
|
42
|
+
if (result.isError)
|
|
43
|
+
throw new Error(getToolText(result));
|
|
44
|
+
const data = getToolJson(result);
|
|
45
|
+
if (!data.id)
|
|
46
|
+
throw new Error("no user id");
|
|
47
|
+
},
|
|
16
48
|
},
|
|
17
49
|
// ── Activities ─────────────────────────────────────────────────────
|
|
18
50
|
{
|
|
19
51
|
name: "list-activities",
|
|
20
|
-
run: (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
52
|
+
run: async (server) => {
|
|
53
|
+
const result = await callTool(server, "list-activities", {
|
|
54
|
+
limit: 2,
|
|
55
|
+
start: 0,
|
|
56
|
+
});
|
|
57
|
+
if (result.isError)
|
|
58
|
+
throw new Error(getToolText(result));
|
|
59
|
+
const data = getToolJson(result);
|
|
60
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
61
|
+
throw new Error("no activities");
|
|
62
|
+
},
|
|
24
63
|
},
|
|
25
64
|
{
|
|
26
65
|
name: "get-activity",
|
|
27
|
-
run: (
|
|
66
|
+
run: async (server) => {
|
|
67
|
+
const result = await callTool(server, "get-activity", {
|
|
68
|
+
activityId: activityId,
|
|
69
|
+
});
|
|
70
|
+
if (result.isError)
|
|
71
|
+
throw new Error(getToolText(result));
|
|
72
|
+
const data = getToolJson(result);
|
|
73
|
+
if (!data.summaryDTO)
|
|
74
|
+
throw new Error("no summaryDTO");
|
|
75
|
+
},
|
|
28
76
|
},
|
|
29
77
|
{
|
|
30
78
|
name: "get-activity-details",
|
|
31
|
-
run: (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
79
|
+
run: async (server) => {
|
|
80
|
+
const result = await callTool(server, "get-activity-details", {
|
|
81
|
+
activityId: activityId,
|
|
82
|
+
maxChartSize: 10000,
|
|
83
|
+
});
|
|
84
|
+
if (result.isError)
|
|
85
|
+
throw new Error(getToolText(result));
|
|
86
|
+
const data = getToolJson(result);
|
|
87
|
+
if (!data.metricDescriptors)
|
|
88
|
+
throw new Error("no metricDescriptors");
|
|
89
|
+
},
|
|
36
90
|
},
|
|
37
91
|
{
|
|
38
92
|
name: "get-activity-splits",
|
|
39
|
-
run: (
|
|
93
|
+
run: async (server) => {
|
|
94
|
+
const result = await callTool(server, "get-activity-splits", {
|
|
95
|
+
activityId: activityId,
|
|
96
|
+
});
|
|
97
|
+
if (result.isError)
|
|
98
|
+
throw new Error(getToolText(result));
|
|
99
|
+
const data = getToolJson(result);
|
|
100
|
+
if (!data.lapDTOs)
|
|
101
|
+
throw new Error("no lapDTOs");
|
|
102
|
+
},
|
|
40
103
|
},
|
|
41
104
|
{
|
|
42
105
|
name: "get-activity-hr-zones",
|
|
43
|
-
run: (
|
|
106
|
+
run: async (server) => {
|
|
107
|
+
const result = await callTool(server, "get-activity-hr-zones", {
|
|
108
|
+
activityId: activityId,
|
|
109
|
+
});
|
|
110
|
+
if (result.isError)
|
|
111
|
+
throw new Error(getToolText(result));
|
|
112
|
+
const data = getToolJson(result);
|
|
113
|
+
if (!Array.isArray(data) || data.length !== 5)
|
|
114
|
+
throw new Error(`expected 5 zones, got ${data.length}`);
|
|
115
|
+
},
|
|
44
116
|
},
|
|
45
117
|
{
|
|
46
118
|
name: "get-activity-polyline",
|
|
47
|
-
run: (
|
|
119
|
+
run: async (server) => {
|
|
120
|
+
const result = await callTool(server, "get-activity-polyline", {
|
|
121
|
+
activityId: activityId,
|
|
122
|
+
});
|
|
123
|
+
if (result.isError)
|
|
124
|
+
throw new Error(getToolText(result));
|
|
125
|
+
},
|
|
48
126
|
},
|
|
49
127
|
{
|
|
50
128
|
name: "get-activity-weather",
|
|
51
|
-
run: (
|
|
129
|
+
run: async (server) => {
|
|
130
|
+
const result = await callTool(server, "get-activity-weather", {
|
|
131
|
+
activityId: activityId,
|
|
132
|
+
});
|
|
133
|
+
if (result.isError)
|
|
134
|
+
throw new Error(getToolText(result));
|
|
135
|
+
},
|
|
52
136
|
},
|
|
53
137
|
{
|
|
54
|
-
name: "download-fit",
|
|
55
|
-
run: (
|
|
138
|
+
name: "download-fit (zip extraction + file write)",
|
|
139
|
+
run: async (server) => {
|
|
140
|
+
// Clean up from previous runs
|
|
141
|
+
if (existsSync(TEST_FIT_DIR))
|
|
142
|
+
rmSync(TEST_FIT_DIR, { recursive: true });
|
|
143
|
+
const result = await callTool(server, "download-fit", {
|
|
144
|
+
activityId: activityId,
|
|
145
|
+
outputDir: TEST_FIT_DIR,
|
|
146
|
+
});
|
|
147
|
+
if (result.isError)
|
|
148
|
+
throw new Error(getToolText(result));
|
|
149
|
+
const text = getToolText(result);
|
|
150
|
+
if (!text.includes("Downloaded FIT file"))
|
|
151
|
+
throw new Error(`unexpected response: ${text}`);
|
|
152
|
+
// Verify file actually exists on disk
|
|
153
|
+
const expectedPath = `${TEST_FIT_DIR}/${activityId}.fit`;
|
|
154
|
+
if (!existsSync(expectedPath))
|
|
155
|
+
throw new Error(`FIT file not found at ${expectedPath}`);
|
|
156
|
+
},
|
|
56
157
|
},
|
|
57
|
-
// ── Daily Health
|
|
158
|
+
// ── Daily Health (test date defaults) ──────────────────────────────
|
|
58
159
|
{
|
|
59
|
-
name: "get-daily-summary",
|
|
60
|
-
run: (
|
|
61
|
-
|
|
62
|
-
|
|
160
|
+
name: "get-daily-summary (default date)",
|
|
161
|
+
run: async (server) => {
|
|
162
|
+
const result = await callTool(server, "get-daily-summary");
|
|
163
|
+
if (result.isError)
|
|
164
|
+
throw new Error(getToolText(result));
|
|
165
|
+
const data = getToolJson(result);
|
|
166
|
+
if (!data.calendarDate)
|
|
167
|
+
throw new Error("no calendarDate");
|
|
168
|
+
},
|
|
63
169
|
},
|
|
64
170
|
{
|
|
65
171
|
name: "get-daily-heart-rate",
|
|
66
|
-
run: (
|
|
172
|
+
run: async (server) => {
|
|
173
|
+
const result = await callTool(server, "get-daily-heart-rate");
|
|
174
|
+
if (result.isError)
|
|
175
|
+
throw new Error(getToolText(result));
|
|
176
|
+
},
|
|
67
177
|
},
|
|
68
178
|
{
|
|
69
179
|
name: "get-daily-stress",
|
|
70
|
-
run: (
|
|
180
|
+
run: async (server) => {
|
|
181
|
+
const result = await callTool(server, "get-daily-stress");
|
|
182
|
+
if (result.isError)
|
|
183
|
+
throw new Error(getToolText(result));
|
|
184
|
+
},
|
|
71
185
|
},
|
|
72
186
|
{
|
|
73
187
|
name: "get-daily-summary-chart",
|
|
74
|
-
run: (
|
|
75
|
-
|
|
76
|
-
|
|
188
|
+
run: async (server) => {
|
|
189
|
+
const result = await callTool(server, "get-daily-summary-chart");
|
|
190
|
+
if (result.isError)
|
|
191
|
+
throw new Error(getToolText(result));
|
|
192
|
+
},
|
|
77
193
|
},
|
|
78
194
|
{
|
|
79
195
|
name: "get-daily-intensity-minutes",
|
|
80
|
-
run: (
|
|
196
|
+
run: async (server) => {
|
|
197
|
+
const result = await callTool(server, "get-daily-intensity-minutes");
|
|
198
|
+
if (result.isError)
|
|
199
|
+
throw new Error(getToolText(result));
|
|
200
|
+
},
|
|
81
201
|
},
|
|
82
202
|
{
|
|
83
203
|
name: "get-daily-movement",
|
|
84
|
-
run: (
|
|
85
|
-
|
|
86
|
-
|
|
204
|
+
run: async (server) => {
|
|
205
|
+
const result = await callTool(server, "get-daily-movement");
|
|
206
|
+
if (result.isError)
|
|
207
|
+
throw new Error(getToolText(result));
|
|
208
|
+
},
|
|
87
209
|
},
|
|
88
210
|
{
|
|
89
211
|
name: "get-daily-respiration",
|
|
90
|
-
run: (
|
|
212
|
+
run: async (server) => {
|
|
213
|
+
const result = await callTool(server, "get-daily-respiration");
|
|
214
|
+
if (result.isError)
|
|
215
|
+
throw new Error(getToolText(result));
|
|
216
|
+
},
|
|
91
217
|
},
|
|
92
|
-
// ── Sleep
|
|
218
|
+
// ── Sleep / Body Battery / HRV ─────────────────────────────────────
|
|
93
219
|
{
|
|
94
220
|
name: "get-sleep",
|
|
95
|
-
run: (
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
221
|
+
run: async (server) => {
|
|
222
|
+
const result = await callTool(server, "get-sleep");
|
|
223
|
+
if (result.isError)
|
|
224
|
+
throw new Error(getToolText(result));
|
|
225
|
+
},
|
|
99
226
|
},
|
|
100
227
|
{
|
|
101
228
|
name: "get-body-battery",
|
|
102
|
-
run: (
|
|
229
|
+
run: async (server) => {
|
|
230
|
+
const result = await callTool(server, "get-body-battery");
|
|
231
|
+
if (result.isError)
|
|
232
|
+
throw new Error(getToolText(result));
|
|
233
|
+
},
|
|
103
234
|
},
|
|
104
235
|
{
|
|
105
236
|
name: "get-hrv",
|
|
106
|
-
run: (
|
|
237
|
+
run: async (server) => {
|
|
238
|
+
const result = await callTool(server, "get-hrv");
|
|
239
|
+
if (result.isError)
|
|
240
|
+
throw new Error(getToolText(result));
|
|
241
|
+
// noData is acceptable
|
|
242
|
+
},
|
|
107
243
|
},
|
|
108
|
-
// ── Weight
|
|
244
|
+
// ── Weight / Records / Fitness ─────────────────────────────────────
|
|
109
245
|
{
|
|
110
246
|
name: "get-weight",
|
|
111
|
-
run: (
|
|
112
|
-
|
|
113
|
-
|
|
247
|
+
run: async (server) => {
|
|
248
|
+
const thirtyDaysAgo = new Date(Date.now() - 30 * 86400000)
|
|
249
|
+
.toISOString()
|
|
250
|
+
.slice(0, 10);
|
|
251
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
252
|
+
const result = await callTool(server, "get-weight", {
|
|
253
|
+
startDate: thirtyDaysAgo,
|
|
254
|
+
endDate: today,
|
|
255
|
+
});
|
|
256
|
+
if (result.isError)
|
|
257
|
+
throw new Error(getToolText(result));
|
|
258
|
+
},
|
|
114
259
|
},
|
|
115
|
-
// ── Personal Records ───────────────────────────────────────────────
|
|
116
260
|
{
|
|
117
|
-
name: "get-personal-records",
|
|
118
|
-
run: (
|
|
119
|
-
|
|
120
|
-
|
|
261
|
+
name: "get-personal-records (displayName resolution)",
|
|
262
|
+
run: async (server) => {
|
|
263
|
+
const result = await callTool(server, "get-personal-records");
|
|
264
|
+
if (result.isError)
|
|
265
|
+
throw new Error(getToolText(result));
|
|
266
|
+
const data = getToolJson(result);
|
|
267
|
+
if (!Array.isArray(data))
|
|
268
|
+
throw new Error("expected array");
|
|
269
|
+
},
|
|
121
270
|
},
|
|
122
|
-
// ── Fitness Stats / Reports ────────────────────────────────────────
|
|
123
271
|
{
|
|
124
272
|
name: "get-fitness-stats",
|
|
125
|
-
run: (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
273
|
+
run: async (server) => {
|
|
274
|
+
const thirtyDaysAgo = new Date(Date.now() - 30 * 86400000)
|
|
275
|
+
.toISOString()
|
|
276
|
+
.slice(0, 10);
|
|
277
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
278
|
+
const result = await callTool(server, "get-fitness-stats", {
|
|
279
|
+
startDate: thirtyDaysAgo,
|
|
280
|
+
endDate: today,
|
|
281
|
+
aggregation: "daily",
|
|
282
|
+
metric: "duration",
|
|
283
|
+
});
|
|
284
|
+
if (result.isError)
|
|
285
|
+
throw new Error(getToolText(result));
|
|
286
|
+
},
|
|
135
287
|
},
|
|
136
288
|
{
|
|
137
289
|
name: "get-vo2max",
|
|
138
|
-
run: (
|
|
290
|
+
run: async (server) => {
|
|
291
|
+
const result = await callTool(server, "get-vo2max");
|
|
292
|
+
if (result.isError)
|
|
293
|
+
throw new Error(getToolText(result));
|
|
294
|
+
},
|
|
139
295
|
},
|
|
140
296
|
{
|
|
141
297
|
name: "get-hr-zones-config",
|
|
142
|
-
run: (
|
|
298
|
+
run: async (server) => {
|
|
299
|
+
const result = await callTool(server, "get-hr-zones-config");
|
|
300
|
+
if (result.isError)
|
|
301
|
+
throw new Error(getToolText(result));
|
|
302
|
+
const data = getToolJson(result);
|
|
303
|
+
if (!Array.isArray(data) || data.length === 0)
|
|
304
|
+
throw new Error("no zones returned");
|
|
305
|
+
},
|
|
143
306
|
},
|
|
144
307
|
];
|
|
308
|
+
// Resolved during bootstrap
|
|
309
|
+
let activityId = "";
|
|
145
310
|
async function main() {
|
|
146
|
-
console.log("garmin-connect-mcp integration tests\n");
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
console.log("Bootstrapping...");
|
|
154
|
-
const settings = (await client.get("userprofile-service/userprofile/settings"));
|
|
155
|
-
const displayName = settings.displayName;
|
|
156
|
-
if (!displayName) {
|
|
157
|
-
console.error("FATAL: Could not resolve displayName from settings");
|
|
158
|
-
console.error("Response:", JSON.stringify(settings).slice(0, 500));
|
|
159
|
-
await client.close();
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
console.log(` displayName: ${displayName}`);
|
|
311
|
+
console.log("garmin-connect-mcp integration tests (tool-level)\n");
|
|
312
|
+
// Set up a real MCP server with all tools registered
|
|
313
|
+
const server = new McpServer({
|
|
314
|
+
name: "garmin-connect-mcp-test",
|
|
315
|
+
version: "0.0.0",
|
|
316
|
+
});
|
|
317
|
+
registerTools(server);
|
|
163
318
|
// Bootstrap: get a recent activityId
|
|
164
|
-
|
|
165
|
-
const
|
|
319
|
+
console.log("Bootstrapping...");
|
|
320
|
+
const listResult = await callTool(server, "list-activities", {
|
|
321
|
+
limit: 1,
|
|
322
|
+
start: 0,
|
|
323
|
+
});
|
|
324
|
+
const activities = getToolJson(listResult);
|
|
325
|
+
activityId = String(activities[0]?.activityId ?? "");
|
|
166
326
|
if (!activityId) {
|
|
167
327
|
console.error("FATAL: No activities found");
|
|
168
|
-
await client.close();
|
|
169
328
|
process.exit(1);
|
|
170
329
|
}
|
|
171
|
-
console.log(` activityId: ${activityId}`);
|
|
172
|
-
console.log(` date range: ${thirtyDaysAgo} → ${today}\n`);
|
|
173
|
-
const ctx = {
|
|
174
|
-
client,
|
|
175
|
-
displayName,
|
|
176
|
-
activityId,
|
|
177
|
-
today,
|
|
178
|
-
thirtyDaysAgo,
|
|
179
|
-
};
|
|
330
|
+
console.log(` activityId: ${activityId}\n`);
|
|
180
331
|
let passed = 0;
|
|
181
332
|
let failed = 0;
|
|
182
333
|
for (const test of tests) {
|
|
183
334
|
const start = Date.now();
|
|
184
335
|
try {
|
|
185
|
-
|
|
186
|
-
if (result === undefined) {
|
|
187
|
-
throw new Error("undefined response");
|
|
188
|
-
}
|
|
336
|
+
await test.run(server);
|
|
189
337
|
const ms = Date.now() - start;
|
|
190
|
-
|
|
191
|
-
console.log(` PASS ${test.name} (${ms}ms)${noData ? " [no data for date]" : ""}`);
|
|
338
|
+
console.log(` PASS ${test.name} (${ms}ms)`);
|
|
192
339
|
passed++;
|
|
193
340
|
}
|
|
194
341
|
catch (e) {
|
|
195
342
|
const ms = Date.now() - start;
|
|
196
343
|
const msg = e instanceof Error ? e.message : String(e);
|
|
197
|
-
// Truncate long error messages
|
|
198
344
|
const short = msg.length > 120 ? msg.slice(0, 120) + "..." : msg;
|
|
199
345
|
console.log(` FAIL ${test.name} (${ms}ms) — ${short}`);
|
|
200
346
|
failed++;
|
|
201
347
|
}
|
|
202
348
|
}
|
|
349
|
+
// Cleanup
|
|
350
|
+
if (existsSync(TEST_FIT_DIR))
|
|
351
|
+
rmSync(TEST_FIT_DIR, { recursive: true });
|
|
203
352
|
console.log(`\nResults: ${passed} passed, ${failed} failed, ${passed + failed} total`);
|
|
204
|
-
await
|
|
353
|
+
await getSharedClient().close();
|
|
205
354
|
process.exit(failed > 0 ? 1 : 0);
|
|
206
355
|
}
|
|
207
356
|
main().catch((err) => {
|
package/dist/tools.js
CHANGED
|
@@ -396,43 +396,59 @@ Present results as a markdown table: | Tool | Status | Notes |
|
|
|
396
396
|
Count total passed vs failed at the end.`);
|
|
397
397
|
});
|
|
398
398
|
}
|
|
399
|
+
import { inflateRawSync } from "node:zlib";
|
|
399
400
|
/**
|
|
400
|
-
* Minimal zip extraction — finds
|
|
401
|
-
*
|
|
401
|
+
* Minimal zip extraction — finds the first .fit file using the central
|
|
402
|
+
* directory (which always has correct sizes, unlike local headers that
|
|
403
|
+
* may use data descriptors with size=0).
|
|
402
404
|
*/
|
|
403
405
|
function extractFitFromZip(buf, activityId) {
|
|
404
|
-
//
|
|
405
|
-
let
|
|
406
|
-
while (
|
|
407
|
-
if (buf[
|
|
408
|
-
buf[
|
|
409
|
-
buf[
|
|
410
|
-
buf[
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
406
|
+
// Find End of Central Directory (EOCD): PK\x05\x06
|
|
407
|
+
let eocdOffset = buf.length - 22;
|
|
408
|
+
while (eocdOffset >= 0) {
|
|
409
|
+
if (buf[eocdOffset] === 0x50 &&
|
|
410
|
+
buf[eocdOffset + 1] === 0x4b &&
|
|
411
|
+
buf[eocdOffset + 2] === 0x05 &&
|
|
412
|
+
buf[eocdOffset + 3] === 0x06)
|
|
413
|
+
break;
|
|
414
|
+
eocdOffset--;
|
|
415
|
+
}
|
|
416
|
+
if (eocdOffset < 0)
|
|
417
|
+
return null;
|
|
418
|
+
const cdOffset = buf.readUInt32LE(eocdOffset + 16);
|
|
419
|
+
const cdEntries = buf.readUInt16LE(eocdOffset + 10);
|
|
420
|
+
// Walk central directory entries: PK\x01\x02
|
|
421
|
+
let pos = cdOffset;
|
|
422
|
+
for (let i = 0; i < cdEntries; i++) {
|
|
423
|
+
if (buf[pos] !== 0x50 ||
|
|
424
|
+
buf[pos + 1] !== 0x4b ||
|
|
425
|
+
buf[pos + 2] !== 0x01 ||
|
|
426
|
+
buf[pos + 3] !== 0x02)
|
|
427
|
+
break;
|
|
428
|
+
const method = buf.readUInt16LE(pos + 10);
|
|
429
|
+
const compressedSize = buf.readUInt32LE(pos + 20);
|
|
430
|
+
const uncompressedSize = buf.readUInt32LE(pos + 24);
|
|
431
|
+
const nameLength = buf.readUInt16LE(pos + 28);
|
|
432
|
+
const extraLength = buf.readUInt16LE(pos + 30);
|
|
433
|
+
const commentLength = buf.readUInt16LE(pos + 32);
|
|
434
|
+
const localHeaderOffset = buf.readUInt32LE(pos + 42);
|
|
435
|
+
const name = buf.toString("utf-8", pos + 46, pos + 46 + nameLength);
|
|
436
|
+
if (name.endsWith(".fit")) {
|
|
437
|
+
// Read local header to find data start
|
|
438
|
+
const localNameLen = buf.readUInt16LE(localHeaderOffset + 26);
|
|
439
|
+
const localExtraLen = buf.readUInt16LE(localHeaderOffset + 28);
|
|
440
|
+
const dataStart = localHeaderOffset + 30 + localNameLen + localExtraLen;
|
|
441
|
+
if (method === 0) {
|
|
420
442
|
const data = buf.subarray(dataStart, dataStart + uncompressedSize);
|
|
421
443
|
return { name: `${activityId}.fit`, data: Buffer.from(data) };
|
|
422
444
|
}
|
|
423
|
-
if (
|
|
424
|
-
// Deflate compressed — use Node's zlib
|
|
445
|
+
if (method === 8) {
|
|
425
446
|
const compressed = buf.subarray(dataStart, dataStart + compressedSize);
|
|
426
447
|
const data = inflateRawSync(compressed);
|
|
427
448
|
return { name: `${activityId}.fit`, data };
|
|
428
449
|
}
|
|
429
|
-
// Skip to next file header
|
|
430
|
-
offset = dataStart + compressedSize;
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
offset++;
|
|
434
450
|
}
|
|
451
|
+
pos += 46 + nameLength + extraLength + commentLength;
|
|
435
452
|
}
|
|
436
453
|
return null;
|
|
437
454
|
}
|
|
438
|
-
import { inflateRawSync } from "node:zlib";
|