step-overflow 0.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/LICENSE +21 -0
- package/README.ja.md +111 -0
- package/README.md +111 -0
- package/dist/cli.js +28 -0
- package/dist/commands/add.js +188 -0
- package/dist/commands/config.js +92 -0
- package/dist/commands/init.js +193 -0
- package/dist/commands/log.js +41 -0
- package/dist/commands/open.js +78 -0
- package/dist/commands/status.js +55 -0
- package/dist/commands/sync.js +25 -0
- package/dist/lib/achievements.js +322 -0
- package/dist/lib/animation.js +70 -0
- package/dist/lib/calories.js +32 -0
- package/dist/lib/config.js +38 -0
- package/dist/lib/csv.js +55 -0
- package/dist/lib/git.js +97 -0
- package/dist/lib/html.js +437 -0
- package/dist/lib/open-url.js +15 -0
- package/dist/lib/prompt.js +19 -0
- package/dist/lib/routes.js +348 -0
- package/package.json +51 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
function getHour(record) {
|
|
2
|
+
return parseInt(record.datetime.split("T")[1]?.split(":")[0] ?? "12");
|
|
3
|
+
}
|
|
4
|
+
function getDate(record) {
|
|
5
|
+
return record.datetime.split("T")[0];
|
|
6
|
+
}
|
|
7
|
+
function getDayOfWeek(record) {
|
|
8
|
+
return new Date(record.datetime).getDay();
|
|
9
|
+
}
|
|
10
|
+
function hasMultipleOnSameDay(records) {
|
|
11
|
+
const dates = records.map(getDate);
|
|
12
|
+
return dates.length !== new Set(dates).size;
|
|
13
|
+
}
|
|
14
|
+
export const ACHIEVEMENTS = [
|
|
15
|
+
// ── Distance ──
|
|
16
|
+
{
|
|
17
|
+
id: "first_step",
|
|
18
|
+
name: "First Step",
|
|
19
|
+
description: "Record your first walk",
|
|
20
|
+
check: (ctx) => ctx.allRecords.length >= 1,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "10k",
|
|
24
|
+
name: "10K",
|
|
25
|
+
description: "Walk 10 km total",
|
|
26
|
+
check: (ctx) => ctx.totalKm >= 10,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "century",
|
|
30
|
+
name: "Century",
|
|
31
|
+
description: "Walk 100 km total",
|
|
32
|
+
check: (ctx) => ctx.totalKm >= 100,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: "500_club",
|
|
36
|
+
name: "500 Club",
|
|
37
|
+
description: "Walk 500 km total",
|
|
38
|
+
check: (ctx) => ctx.totalKm >= 500,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "thousand",
|
|
42
|
+
name: "Thousand Miles",
|
|
43
|
+
description: "Walk 1,000 km total",
|
|
44
|
+
check: (ctx) => ctx.totalKm >= 1000,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "2000_club",
|
|
48
|
+
name: "2,000 Club",
|
|
49
|
+
description: "Walk further than the length of the Rhine",
|
|
50
|
+
check: (ctx) => ctx.totalKm >= 2000,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "5000_club",
|
|
54
|
+
name: "5,000 Club",
|
|
55
|
+
description: "Walk further than the length of Japan",
|
|
56
|
+
check: (ctx) => ctx.totalKm >= 5000,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "ten_thousand",
|
|
60
|
+
name: "10K Club",
|
|
61
|
+
description: "Five digits of distance",
|
|
62
|
+
check: (ctx) => ctx.totalKm >= 10000,
|
|
63
|
+
},
|
|
64
|
+
// ── Route Completion ──
|
|
65
|
+
{
|
|
66
|
+
id: "first_journey",
|
|
67
|
+
name: "First Journey",
|
|
68
|
+
description: "Complete your first route",
|
|
69
|
+
check: (ctx) => ctx.completedRoutes.length >= 1,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "alpinist",
|
|
73
|
+
name: "Alpinist",
|
|
74
|
+
description: "Circle Mont Blanc through three countries",
|
|
75
|
+
check: (ctx) => ctx.completedRoutes.includes("tmb"),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "tokaido_master",
|
|
79
|
+
name: "Tokaido Master",
|
|
80
|
+
description: "Walk the path of the samurai",
|
|
81
|
+
check: (ctx) => ctx.completedRoutes.includes("tokaido"),
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "pharaohs_path",
|
|
85
|
+
name: "Pharaoh's Path",
|
|
86
|
+
description: "Walk the Nile from Aswan to the Pyramids",
|
|
87
|
+
check: (ctx) => ctx.completedRoutes.includes("nile_valley"),
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: "hannibals_legacy",
|
|
91
|
+
name: "Hannibal's Legacy",
|
|
92
|
+
description: "March with elephants across the Alps",
|
|
93
|
+
check: (ctx) => ctx.completedRoutes.includes("hannibal"),
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: "via_francigena",
|
|
97
|
+
name: "Via Francigena",
|
|
98
|
+
description: "Pilgrim from Canterbury to the Eternal City",
|
|
99
|
+
check: (ctx) => ctx.completedRoutes.includes("via_francigena"),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
id: "pilgrim",
|
|
103
|
+
name: "Pilgrim",
|
|
104
|
+
description: "Complete the Camino de Santiago",
|
|
105
|
+
check: (ctx) => ctx.completedRoutes.includes("camino"),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "route66_rider",
|
|
109
|
+
name: "Route 66 Rider",
|
|
110
|
+
description: "Get your kicks on Route 66",
|
|
111
|
+
check: (ctx) => ctx.completedRoutes.includes("route66"),
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "desert_caravan",
|
|
115
|
+
name: "Desert Caravan",
|
|
116
|
+
description: "Cross the Sahara with gold and salt",
|
|
117
|
+
check: (ctx) => ctx.completedRoutes.includes("trans_sahara"),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: "sun_walker",
|
|
121
|
+
name: "Sun Walker",
|
|
122
|
+
description: "Walk the Royal Road of the Sun",
|
|
123
|
+
check: (ctx) => ctx.completedRoutes.includes("qhapaq_nan"),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "spice_master",
|
|
127
|
+
name: "Spice Master",
|
|
128
|
+
description: "Master the seas of the spice trade",
|
|
129
|
+
check: (ctx) => ctx.completedRoutes.includes("spice_trader"),
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "silk_merchant",
|
|
133
|
+
name: "Silk Merchant",
|
|
134
|
+
description: "Trade silk and ideas across continents",
|
|
135
|
+
check: (ctx) => ctx.completedRoutes.includes("silk_road"),
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: "transcontinental",
|
|
139
|
+
name: "Transcontinental",
|
|
140
|
+
description: "Cross a continent on foot",
|
|
141
|
+
check: (ctx) => ctx.completedRoutes.includes("route66") ||
|
|
142
|
+
ctx.completedRoutes.includes("around"),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "globe_trotter",
|
|
146
|
+
name: "Globe Trotter",
|
|
147
|
+
description: "Complete Around the World",
|
|
148
|
+
check: (ctx) => ctx.completedRoutes.includes("around"),
|
|
149
|
+
},
|
|
150
|
+
// ── Route Collection ──
|
|
151
|
+
{
|
|
152
|
+
id: "world_traveler",
|
|
153
|
+
name: "World Traveler",
|
|
154
|
+
description: "Complete 3 routes",
|
|
155
|
+
check: (ctx) => ctx.completedRoutes.length >= 3,
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: "five_journeys",
|
|
159
|
+
name: "Five Journeys",
|
|
160
|
+
description: "Complete 5 different routes",
|
|
161
|
+
check: (ctx) => ctx.completedRoutes.length >= 5,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: "legendary_walker",
|
|
165
|
+
name: "Legendary Walker",
|
|
166
|
+
description: "Walk every route in the world",
|
|
167
|
+
check: (ctx) => ctx.completedRoutes.length >= 12,
|
|
168
|
+
},
|
|
169
|
+
// ── Single Walk Records ──
|
|
170
|
+
{
|
|
171
|
+
id: "five_k_walk",
|
|
172
|
+
name: "5K Walk",
|
|
173
|
+
description: "Walk 5 km in a single session",
|
|
174
|
+
check: (ctx) => ctx.allRecords.some((r) => r.distance_km >= 5),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
id: "ten_k_walk",
|
|
178
|
+
name: "10K Walk",
|
|
179
|
+
description: "Double digits in a single walk",
|
|
180
|
+
check: (ctx) => ctx.allRecords.some((r) => r.distance_km >= 10),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
id: "half_marathon",
|
|
184
|
+
name: "Half Marathon",
|
|
185
|
+
description: "Complete a half marathon distance",
|
|
186
|
+
check: (ctx) => ctx.allRecords.some((r) => r.distance_km >= 21.1),
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: "marathon",
|
|
190
|
+
name: "Marathon",
|
|
191
|
+
description: "Walk 42.195 km in a single session",
|
|
192
|
+
check: (ctx) => ctx.allRecords.some((r) => r.distance_km >= 42.195),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "speed_demon",
|
|
196
|
+
name: "Speed Demon",
|
|
197
|
+
description: "Walk at 6+ km/h",
|
|
198
|
+
check: (ctx) => ctx.allRecords.some((r) => r.speed_kmh >= 6),
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
id: "long_haul",
|
|
202
|
+
name: "Long Haul",
|
|
203
|
+
description: "Walk 120+ minutes in a single session",
|
|
204
|
+
check: (ctx) => ctx.allRecords.some((r) => r.time_min >= 120),
|
|
205
|
+
},
|
|
206
|
+
// ── Time & Habit ──
|
|
207
|
+
{
|
|
208
|
+
id: "early_bird",
|
|
209
|
+
name: "Early Bird",
|
|
210
|
+
description: "Record a walk before 6:00 AM",
|
|
211
|
+
check: (ctx) => ctx.allRecords.some((r) => getHour(r) < 6),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: "night_owl",
|
|
215
|
+
name: "Night Owl",
|
|
216
|
+
description: "Record a walk after 22:00",
|
|
217
|
+
check: (ctx) => ctx.allRecords.some((r) => getHour(r) >= 22),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: "weekend_warrior",
|
|
221
|
+
name: "Weekend Warrior",
|
|
222
|
+
description: "Record a walk on the weekend",
|
|
223
|
+
check: (ctx) => ctx.allRecords.some((r) => {
|
|
224
|
+
const day = getDayOfWeek(r);
|
|
225
|
+
return day === 0 || day === 6;
|
|
226
|
+
}),
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "double_up",
|
|
230
|
+
name: "Double Up",
|
|
231
|
+
description: "Record two walks in a single day",
|
|
232
|
+
check: (ctx) => hasMultipleOnSameDay(ctx.allRecords),
|
|
233
|
+
},
|
|
234
|
+
// ── Walk Count ──
|
|
235
|
+
{
|
|
236
|
+
id: "5_walks",
|
|
237
|
+
name: "5 Walks",
|
|
238
|
+
description: "Your first handful of walks",
|
|
239
|
+
check: (ctx) => ctx.allRecords.length >= 5,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "10_walks",
|
|
243
|
+
name: "10 Walks",
|
|
244
|
+
description: "Double digits",
|
|
245
|
+
check: (ctx) => ctx.allRecords.length >= 10,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "50_walks",
|
|
249
|
+
name: "50 Walks",
|
|
250
|
+
description: "Halfway to a hundred",
|
|
251
|
+
check: (ctx) => ctx.allRecords.length >= 50,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
id: "100_walks",
|
|
255
|
+
name: "100 Walks",
|
|
256
|
+
description: "The Centurion",
|
|
257
|
+
check: (ctx) => ctx.allRecords.length >= 100,
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
id: "200_walks",
|
|
261
|
+
name: "200 Walks",
|
|
262
|
+
description: "Walking is part of your life",
|
|
263
|
+
check: (ctx) => ctx.allRecords.length >= 200,
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: "300_walks",
|
|
267
|
+
name: "300 Walks",
|
|
268
|
+
description: "A walk a day keeps the doctor away",
|
|
269
|
+
check: (ctx) => ctx.allRecords.length >= 300,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "400_walks",
|
|
273
|
+
name: "400 Walks",
|
|
274
|
+
description: "Your shoes need replacing",
|
|
275
|
+
check: (ctx) => ctx.allRecords.length >= 400,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: "500_walks",
|
|
279
|
+
name: "500 Walks",
|
|
280
|
+
description: "Half a thousand steps forward",
|
|
281
|
+
check: (ctx) => ctx.allRecords.length >= 500,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "600_walks",
|
|
285
|
+
name: "600 Walks",
|
|
286
|
+
description: "The road knows your name",
|
|
287
|
+
check: (ctx) => ctx.allRecords.length >= 600,
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
id: "700_walks",
|
|
291
|
+
name: "700 Walks",
|
|
292
|
+
description: "Walking legend in the making",
|
|
293
|
+
check: (ctx) => ctx.allRecords.length >= 700,
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
id: "800_walks",
|
|
297
|
+
name: "800 Walks",
|
|
298
|
+
description: "Unstoppable force of nature",
|
|
299
|
+
check: (ctx) => ctx.allRecords.length >= 800,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
id: "900_walks",
|
|
303
|
+
name: "900 Walks",
|
|
304
|
+
description: "The final stretch to four digits",
|
|
305
|
+
check: (ctx) => ctx.allRecords.length >= 900,
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
id: "1000_walks",
|
|
309
|
+
name: "1,000 Walks",
|
|
310
|
+
description: "One thousand walks. Legendary.",
|
|
311
|
+
check: (ctx) => ctx.allRecords.length >= 1000,
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
id: "10000_walks",
|
|
315
|
+
name: "10,000 Walks",
|
|
316
|
+
description: "Beyond all limits",
|
|
317
|
+
check: (ctx) => ctx.allRecords.length >= 10000,
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
export function checkNewAchievements(ctx, unlocked) {
|
|
321
|
+
return ACHIEVEMENTS.filter((a) => !unlocked[a.id] && a.check(ctx));
|
|
322
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import logUpdate from "log-update";
|
|
2
|
+
// 6-frame running stick figure with arm swing and leg stride
|
|
3
|
+
// Each frame: head, torso+arms, legs (all 5 chars wide for alignment)
|
|
4
|
+
const FRAMES = [
|
|
5
|
+
// Frame 0: right leg forward stride, left arm reach
|
|
6
|
+
[
|
|
7
|
+
" o ",
|
|
8
|
+
" -|\\ ",
|
|
9
|
+
" / > ",
|
|
10
|
+
],
|
|
11
|
+
// Frame 1: legs crossing, arms pull in
|
|
12
|
+
[
|
|
13
|
+
" o ",
|
|
14
|
+
" /|\\ ",
|
|
15
|
+
" || ",
|
|
16
|
+
],
|
|
17
|
+
// Frame 2: left leg forward stride, right arm reach
|
|
18
|
+
[
|
|
19
|
+
" o ",
|
|
20
|
+
" /|- ",
|
|
21
|
+
" < \\ ",
|
|
22
|
+
],
|
|
23
|
+
// Frame 3: push off, legs crossing back
|
|
24
|
+
[
|
|
25
|
+
" o ",
|
|
26
|
+
" /|\\ ",
|
|
27
|
+
" || ",
|
|
28
|
+
],
|
|
29
|
+
// Frame 4: airborne, legs tucked
|
|
30
|
+
[
|
|
31
|
+
" o ",
|
|
32
|
+
" -|- ",
|
|
33
|
+
" > < ",
|
|
34
|
+
],
|
|
35
|
+
// Frame 5: landing, arms down
|
|
36
|
+
[
|
|
37
|
+
" o ",
|
|
38
|
+
" /|\\ ",
|
|
39
|
+
" / \\ ",
|
|
40
|
+
],
|
|
41
|
+
];
|
|
42
|
+
const GROUND_CHAR = "\u2500";
|
|
43
|
+
const GROUND_LEN = 46;
|
|
44
|
+
export async function playWalkAnimation(until, minMs = 1000) {
|
|
45
|
+
const frameWidth = 5;
|
|
46
|
+
let frameIdx = 0;
|
|
47
|
+
let pos = 0;
|
|
48
|
+
const interval = setInterval(() => {
|
|
49
|
+
const frame = FRAMES[frameIdx % FRAMES.length];
|
|
50
|
+
const pad = " ".repeat(pos);
|
|
51
|
+
const ground = GROUND_CHAR.repeat(GROUND_LEN);
|
|
52
|
+
const lines = [
|
|
53
|
+
pad + frame[0],
|
|
54
|
+
pad + frame[1],
|
|
55
|
+
pad + frame[2],
|
|
56
|
+
ground,
|
|
57
|
+
];
|
|
58
|
+
logUpdate(lines.join("\n"));
|
|
59
|
+
frameIdx++;
|
|
60
|
+
pos += 2;
|
|
61
|
+
if (pos > GROUND_LEN - frameWidth)
|
|
62
|
+
pos = 0;
|
|
63
|
+
}, 120);
|
|
64
|
+
await Promise.all([
|
|
65
|
+
until.catch(() => { }),
|
|
66
|
+
new Promise((resolve) => setTimeout(resolve, minMs)),
|
|
67
|
+
]);
|
|
68
|
+
clearInterval(interval);
|
|
69
|
+
logUpdate.clear();
|
|
70
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// MET values for walking (Compendium of Physical Activities)
|
|
2
|
+
// Speed (km/h) -> METs mapping points for linear interpolation
|
|
3
|
+
const MET_TABLE = [
|
|
4
|
+
[2.0, 2.0],
|
|
5
|
+
[3.2, 2.8],
|
|
6
|
+
[4.0, 3.5],
|
|
7
|
+
[4.8, 4.3],
|
|
8
|
+
[5.6, 5.0],
|
|
9
|
+
[6.4, 7.0],
|
|
10
|
+
[8.0, 8.3],
|
|
11
|
+
];
|
|
12
|
+
export function getMets(speedKmh) {
|
|
13
|
+
if (speedKmh <= MET_TABLE[0][0])
|
|
14
|
+
return MET_TABLE[0][1];
|
|
15
|
+
if (speedKmh >= MET_TABLE[MET_TABLE.length - 1][0]) {
|
|
16
|
+
return MET_TABLE[MET_TABLE.length - 1][1];
|
|
17
|
+
}
|
|
18
|
+
for (let i = 0; i < MET_TABLE.length - 1; i++) {
|
|
19
|
+
const [s0, m0] = MET_TABLE[i];
|
|
20
|
+
const [s1, m1] = MET_TABLE[i + 1];
|
|
21
|
+
if (speedKmh >= s0 && speedKmh <= s1) {
|
|
22
|
+
const ratio = (speedKmh - s0) / (s1 - s0);
|
|
23
|
+
return m0 + ratio * (m1 - m0);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return 3.5; // fallback: normal walking
|
|
27
|
+
}
|
|
28
|
+
export function calcCalories(speedKmh, timeMin, weightKg) {
|
|
29
|
+
const mets = getMets(speedKmh);
|
|
30
|
+
const timeHours = timeMin / 60;
|
|
31
|
+
return Math.round(mets * weightKg * timeHours);
|
|
32
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
function xdgConfigHome() {
|
|
6
|
+
return process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
7
|
+
}
|
|
8
|
+
function xdgDataHome() {
|
|
9
|
+
return process.env.XDG_DATA_HOME || join(homedir(), ".local", "share");
|
|
10
|
+
}
|
|
11
|
+
const CONFIG_DIR = join(xdgConfigHome(), "step-overflow");
|
|
12
|
+
const CONFIG_PATH = join(CONFIG_DIR, "config.json");
|
|
13
|
+
export function getDataDir() {
|
|
14
|
+
return join(xdgDataHome(), "step-overflow");
|
|
15
|
+
}
|
|
16
|
+
export function configExists() {
|
|
17
|
+
return existsSync(CONFIG_PATH);
|
|
18
|
+
}
|
|
19
|
+
export async function loadConfig() {
|
|
20
|
+
if (!configExists()) {
|
|
21
|
+
throw new Error("step-overflow is not initialized. Run `stp init` first.");
|
|
22
|
+
}
|
|
23
|
+
const raw = await readFile(CONFIG_PATH, "utf-8");
|
|
24
|
+
return JSON.parse(raw);
|
|
25
|
+
}
|
|
26
|
+
export async function saveConfig(config) {
|
|
27
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
28
|
+
await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n");
|
|
29
|
+
}
|
|
30
|
+
export function getCsvPath(config) {
|
|
31
|
+
return join(config.local_path, "data", "walking.csv");
|
|
32
|
+
}
|
|
33
|
+
export function getPagesUrl(config) {
|
|
34
|
+
if (config.visibility === "private")
|
|
35
|
+
return null;
|
|
36
|
+
const repoName = config.repo.split("/")[1];
|
|
37
|
+
return `https://${config.username}.github.io/${repoName}/`;
|
|
38
|
+
}
|
package/dist/lib/csv.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFile, appendFile, writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
const HEADER = "datetime,time_min,speed_kmh,distance_km,weight_kg";
|
|
5
|
+
export async function appendRecord(csvPath, record) {
|
|
6
|
+
const dir = dirname(csvPath);
|
|
7
|
+
await mkdir(dir, { recursive: true });
|
|
8
|
+
if (!existsSync(csvPath)) {
|
|
9
|
+
await writeFile(csvPath, HEADER + "\n");
|
|
10
|
+
}
|
|
11
|
+
const weight = record.weight_kg !== null ? String(record.weight_kg) : "";
|
|
12
|
+
const line = [
|
|
13
|
+
record.datetime,
|
|
14
|
+
record.time_min,
|
|
15
|
+
record.speed_kmh,
|
|
16
|
+
record.distance_km.toFixed(2),
|
|
17
|
+
weight,
|
|
18
|
+
].join(",");
|
|
19
|
+
await appendFile(csvPath, line + "\n");
|
|
20
|
+
}
|
|
21
|
+
export async function readRecords(csvPath) {
|
|
22
|
+
if (!existsSync(csvPath))
|
|
23
|
+
return [];
|
|
24
|
+
const content = await readFile(csvPath, "utf-8");
|
|
25
|
+
const lines = content.trim().split("\n");
|
|
26
|
+
if (lines.length < 2)
|
|
27
|
+
return [];
|
|
28
|
+
return lines.slice(1).flatMap((line) => {
|
|
29
|
+
const parts = line.split(",");
|
|
30
|
+
if (parts.length < 4)
|
|
31
|
+
return [];
|
|
32
|
+
const [datetime, time_min, speed_kmh, distance_km, weight_kg] = parts;
|
|
33
|
+
const record = {
|
|
34
|
+
datetime,
|
|
35
|
+
time_min: parseFloat(time_min),
|
|
36
|
+
speed_kmh: parseFloat(speed_kmh),
|
|
37
|
+
distance_km: parseFloat(distance_km),
|
|
38
|
+
weight_kg: weight_kg ? parseFloat(weight_kg) || null : null,
|
|
39
|
+
};
|
|
40
|
+
if (isNaN(record.time_min) || isNaN(record.speed_kmh) || isNaN(record.distance_km)) {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
return [record];
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export async function initCsv(csvPath) {
|
|
47
|
+
await mkdir(dirname(csvPath), { recursive: true });
|
|
48
|
+
try {
|
|
49
|
+
await writeFile(csvPath, HEADER + "\n", { flag: "wx" });
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
if (e.code !== "EEXIST")
|
|
53
|
+
throw e;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
const exec = promisify(execFile);
|
|
4
|
+
async function run(cmd, args, cwd) {
|
|
5
|
+
return exec(cmd, args, { cwd });
|
|
6
|
+
}
|
|
7
|
+
export async function checkPrerequisites() {
|
|
8
|
+
let git = false;
|
|
9
|
+
let gh = false;
|
|
10
|
+
try {
|
|
11
|
+
await run("git", ["--version"]);
|
|
12
|
+
git = true;
|
|
13
|
+
}
|
|
14
|
+
catch { }
|
|
15
|
+
try {
|
|
16
|
+
await run("gh", ["--version"]);
|
|
17
|
+
gh = true;
|
|
18
|
+
}
|
|
19
|
+
catch { }
|
|
20
|
+
return { git, gh };
|
|
21
|
+
}
|
|
22
|
+
export async function isGhAuthenticated() {
|
|
23
|
+
try {
|
|
24
|
+
await run("gh", ["auth", "status"]);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function getGhUsername() {
|
|
32
|
+
const { stdout } = await run("gh", [
|
|
33
|
+
"api",
|
|
34
|
+
"user",
|
|
35
|
+
"--jq",
|
|
36
|
+
".login",
|
|
37
|
+
]);
|
|
38
|
+
return stdout.trim();
|
|
39
|
+
}
|
|
40
|
+
export async function createRepo(name, visibility) {
|
|
41
|
+
const { stdout } = await run("gh", [
|
|
42
|
+
"repo",
|
|
43
|
+
"create",
|
|
44
|
+
name,
|
|
45
|
+
`--${visibility}`,
|
|
46
|
+
]);
|
|
47
|
+
return stdout.trim();
|
|
48
|
+
}
|
|
49
|
+
export async function enableGitHubPages(fullName) {
|
|
50
|
+
try {
|
|
51
|
+
await run("gh", [
|
|
52
|
+
"api",
|
|
53
|
+
"--method",
|
|
54
|
+
"POST",
|
|
55
|
+
`repos/${fullName}/pages`,
|
|
56
|
+
"-f",
|
|
57
|
+
"build_type=legacy",
|
|
58
|
+
"-f",
|
|
59
|
+
"source[branch]=main",
|
|
60
|
+
"-f",
|
|
61
|
+
"source[path]=/docs",
|
|
62
|
+
]);
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
export async function gitAdd(files, cwd) {
|
|
70
|
+
await run("git", ["add", ...files], cwd);
|
|
71
|
+
}
|
|
72
|
+
export async function gitCommit(message, cwd) {
|
|
73
|
+
await run("git", ["commit", "-m", message], cwd);
|
|
74
|
+
}
|
|
75
|
+
export async function gitPush(cwd) {
|
|
76
|
+
await run("git", ["push"], cwd);
|
|
77
|
+
}
|
|
78
|
+
export async function hasUnpushedCommits(cwd) {
|
|
79
|
+
try {
|
|
80
|
+
const { stdout } = await run("git", ["log", "--oneline", "origin/main..HEAD"], cwd);
|
|
81
|
+
return stdout.trim().length > 0;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export async function gitInit(cwd) {
|
|
88
|
+
await run("git", ["init"], cwd);
|
|
89
|
+
await run("git", ["branch", "-M", "main"], cwd);
|
|
90
|
+
}
|
|
91
|
+
export async function gitAddRemote(fullName, cwd) {
|
|
92
|
+
const url = `https://github.com/${fullName}.git`;
|
|
93
|
+
await run("git", ["remote", "add", "origin", url], cwd);
|
|
94
|
+
}
|
|
95
|
+
export async function gitPushFirst(cwd) {
|
|
96
|
+
await run("git", ["push", "-u", "origin", "main"], cwd);
|
|
97
|
+
}
|