@drakulavich/oura-cli 0.4.5 → 0.5.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 +65 -0
- package/README.md +44 -17
- package/dist/index.js +1290 -1063
- package/docs/schemas/activity.json +15 -4
- package/docs/schemas/cv-age.json +26 -0
- package/docs/schemas/describe.json +1 -0
- package/docs/schemas/doctor.json +25 -0
- package/docs/schemas/hr.json +10 -3
- package/docs/schemas/readiness.json +15 -4
- package/docs/schemas/sleep-periods.json +26 -0
- package/docs/schemas/sleep.json +15 -4
- package/docs/schemas/spo2.json +15 -4
- package/docs/schemas/stress.json +15 -4
- package/docs/schemas/workout.json +15 -4
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -1,20 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
configurable: true,
|
|
14
|
-
set: __exportSetter.bind(all, name)
|
|
15
|
-
});
|
|
3
|
+
var __esm = (fn, res, err) => () => {
|
|
4
|
+
if (fn)
|
|
5
|
+
try {
|
|
6
|
+
res = fn(fn = 0);
|
|
7
|
+
} catch (e) {
|
|
8
|
+
err = [e];
|
|
9
|
+
}
|
|
10
|
+
if (err)
|
|
11
|
+
throw err[0];
|
|
12
|
+
return res;
|
|
16
13
|
};
|
|
17
|
-
|
|
14
|
+
|
|
15
|
+
// node_modules/chalk/source/utilities.js
|
|
16
|
+
function stringReplaceAll(string, substring, postfix) {
|
|
17
|
+
let index = string.indexOf(substring);
|
|
18
|
+
if (index === -1) {
|
|
19
|
+
return string;
|
|
20
|
+
}
|
|
21
|
+
const substringLength = substring.length;
|
|
22
|
+
let endIndex = 0;
|
|
23
|
+
let returnValue = "";
|
|
24
|
+
do {
|
|
25
|
+
returnValue += string.slice(endIndex, index) + substring + postfix;
|
|
26
|
+
endIndex = index + substringLength;
|
|
27
|
+
index = string.indexOf(substring, endIndex);
|
|
28
|
+
} while (index !== -1);
|
|
29
|
+
returnValue += string.slice(endIndex);
|
|
30
|
+
return returnValue;
|
|
31
|
+
}
|
|
32
|
+
function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
|
33
|
+
let endIndex = 0;
|
|
34
|
+
let returnValue = "";
|
|
35
|
+
do {
|
|
36
|
+
const isGotCR = string[index - 1] === "\r";
|
|
37
|
+
returnValue += string.slice(endIndex, isGotCR ? index - 1 : index) + prefix + (isGotCR ? `\r
|
|
38
|
+
` : `
|
|
39
|
+
`) + postfix;
|
|
40
|
+
endIndex = index + 1;
|
|
41
|
+
index = string.indexOf(`
|
|
42
|
+
`, endIndex);
|
|
43
|
+
} while (index !== -1);
|
|
44
|
+
returnValue += string.slice(endIndex);
|
|
45
|
+
return returnValue;
|
|
46
|
+
}
|
|
18
47
|
|
|
19
48
|
// node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
20
49
|
function assembleStyles() {
|
|
@@ -26,7 +55,7 @@ function assembleStyles() {
|
|
|
26
55
|
close: `\x1B[${style[1]}m`
|
|
27
56
|
};
|
|
28
57
|
group[styleName] = styles[styleName];
|
|
29
|
-
codes.set(style[0], style[1]);
|
|
58
|
+
codes.set(Number.parseInt(style[0], 10), style[1]);
|
|
30
59
|
}
|
|
31
60
|
Object.defineProperty(styles, groupName, {
|
|
32
61
|
value: group,
|
|
@@ -39,12 +68,16 @@ function assembleStyles() {
|
|
|
39
68
|
});
|
|
40
69
|
styles.color.close = "\x1B[39m";
|
|
41
70
|
styles.bgColor.close = "\x1B[49m";
|
|
71
|
+
styles.underlineColor.close = "\x1B[59m";
|
|
42
72
|
styles.color.ansi = wrapAnsi16();
|
|
43
73
|
styles.color.ansi256 = wrapAnsi256();
|
|
44
74
|
styles.color.ansi16m = wrapAnsi16m();
|
|
45
75
|
styles.bgColor.ansi = wrapAnsi16(ANSI_BACKGROUND_OFFSET);
|
|
46
76
|
styles.bgColor.ansi256 = wrapAnsi256(ANSI_BACKGROUND_OFFSET);
|
|
47
77
|
styles.bgColor.ansi16m = wrapAnsi16m(ANSI_BACKGROUND_OFFSET);
|
|
78
|
+
styles.underlineColor.ansi = wrapUnderlineAnsi;
|
|
79
|
+
styles.underlineColor.ansi256 = wrapAnsi256(ANSI_UNDERLINE_OFFSET);
|
|
80
|
+
styles.underlineColor.ansi16m = wrapAnsi16m(ANSI_UNDERLINE_OFFSET);
|
|
48
81
|
Object.defineProperties(styles, {
|
|
49
82
|
rgbToAnsi256: {
|
|
50
83
|
value(red, green, blue) {
|
|
@@ -63,7 +96,7 @@ function assembleStyles() {
|
|
|
63
96
|
},
|
|
64
97
|
hexToRgb: {
|
|
65
98
|
value(hex) {
|
|
66
|
-
const matches = /[
|
|
99
|
+
const matches = /[\da-f]{6}|[\da-f]{3}/i.exec(hex.toString(16));
|
|
67
100
|
if (!matches) {
|
|
68
101
|
return [0, 0, 0];
|
|
69
102
|
}
|
|
@@ -129,7 +162,7 @@ function assembleStyles() {
|
|
|
129
162
|
});
|
|
130
163
|
return styles;
|
|
131
164
|
}
|
|
132
|
-
var ANSI_BACKGROUND_OFFSET = 10, wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`, wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`, wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`, styles, modifierNames, foregroundColorNames, backgroundColorNames, colorNames, ansiStyles, ansi_styles_default;
|
|
165
|
+
var ANSI_BACKGROUND_OFFSET = 10, ANSI_UNDERLINE_OFFSET = 20, wrapAnsi16 = (offset = 0) => (code) => `\x1B[${code + offset}m`, wrapAnsi256 = (offset = 0) => (code) => `\x1B[${38 + offset};5;${code}m`, wrapAnsi16m = (offset = 0) => (red, green, blue) => `\x1B[${38 + offset};2;${red};${green};${blue}m`, wrapUnderlineAnsi = (code) => `\x1B[58;5;${code < 90 ? code - 30 : code - 90 + 8}m`, styles, modifierNames, foregroundColorNames, backgroundColorNames, underlineColorNames, colorNames, ansiStyles, ansi_styles_default;
|
|
133
166
|
var init_ansi_styles = __esm(() => {
|
|
134
167
|
styles = {
|
|
135
168
|
modifier: {
|
|
@@ -138,6 +171,10 @@ var init_ansi_styles = __esm(() => {
|
|
|
138
171
|
dim: [2, 22],
|
|
139
172
|
italic: [3, 23],
|
|
140
173
|
underline: [4, 24],
|
|
174
|
+
underlineDouble: ["4:2", 24],
|
|
175
|
+
underlineCurly: ["4:3", 24],
|
|
176
|
+
underlineDotted: ["4:4", 24],
|
|
177
|
+
underlineDashed: ["4:5", 24],
|
|
141
178
|
overline: [53, 55],
|
|
142
179
|
inverse: [7, 27],
|
|
143
180
|
hidden: [8, 28],
|
|
@@ -182,11 +219,32 @@ var init_ansi_styles = __esm(() => {
|
|
|
182
219
|
bgMagentaBright: [105, 49],
|
|
183
220
|
bgCyanBright: [106, 49],
|
|
184
221
|
bgWhiteBright: [107, 49]
|
|
222
|
+
},
|
|
223
|
+
underlineColor: {
|
|
224
|
+
underlineBlack: ["58;5;0", 59],
|
|
225
|
+
underlineRed: ["58;5;1", 59],
|
|
226
|
+
underlineGreen: ["58;5;2", 59],
|
|
227
|
+
underlineYellow: ["58;5;3", 59],
|
|
228
|
+
underlineBlue: ["58;5;4", 59],
|
|
229
|
+
underlineMagenta: ["58;5;5", 59],
|
|
230
|
+
underlineCyan: ["58;5;6", 59],
|
|
231
|
+
underlineWhite: ["58;5;7", 59],
|
|
232
|
+
underlineBlackBright: ["58;5;8", 59],
|
|
233
|
+
underlineGray: ["58;5;8", 59],
|
|
234
|
+
underlineGrey: ["58;5;8", 59],
|
|
235
|
+
underlineRedBright: ["58;5;9", 59],
|
|
236
|
+
underlineGreenBright: ["58;5;10", 59],
|
|
237
|
+
underlineYellowBright: ["58;5;11", 59],
|
|
238
|
+
underlineBlueBright: ["58;5;12", 59],
|
|
239
|
+
underlineMagentaBright: ["58;5;13", 59],
|
|
240
|
+
underlineCyanBright: ["58;5;14", 59],
|
|
241
|
+
underlineWhiteBright: ["58;5;15", 59]
|
|
185
242
|
}
|
|
186
243
|
};
|
|
187
244
|
modifierNames = Object.keys(styles.modifier);
|
|
188
245
|
foregroundColorNames = Object.keys(styles.color);
|
|
189
246
|
backgroundColorNames = Object.keys(styles.bgColor);
|
|
247
|
+
underlineColorNames = Object.keys(styles.underlineColor);
|
|
190
248
|
colorNames = [...foregroundColorNames, ...backgroundColorNames];
|
|
191
249
|
ansiStyles = assembleStyles();
|
|
192
250
|
ansi_styles_default = ansiStyles;
|
|
@@ -202,16 +260,23 @@ function hasFlag(flag, argv = globalThis.Deno ? globalThis.Deno.args : process2.
|
|
|
202
260
|
const terminatorPosition = argv.indexOf("--");
|
|
203
261
|
return position !== -1 && (terminatorPosition === -1 || position < terminatorPosition);
|
|
204
262
|
}
|
|
263
|
+
function hasNumericForceColor() {
|
|
264
|
+
return /^\d+$/.test(env.FORCE_COLOR);
|
|
265
|
+
}
|
|
205
266
|
function envForceColor() {
|
|
206
|
-
if ("FORCE_COLOR" in env) {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return
|
|
267
|
+
if (!("FORCE_COLOR" in env)) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (env.FORCE_COLOR === "false") {
|
|
271
|
+
return 0;
|
|
272
|
+
}
|
|
273
|
+
if (env.FORCE_COLOR === "true" || env.FORCE_COLOR.length === 0) {
|
|
274
|
+
return 1;
|
|
275
|
+
}
|
|
276
|
+
if (!hasNumericForceColor()) {
|
|
277
|
+
return;
|
|
214
278
|
}
|
|
279
|
+
return Math.min(Number.parseInt(env.FORCE_COLOR, 10), 3);
|
|
215
280
|
}
|
|
216
281
|
function translateLevel(level) {
|
|
217
282
|
if (level === 0) {
|
|
@@ -241,6 +306,9 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
|
241
306
|
return 2;
|
|
242
307
|
}
|
|
243
308
|
}
|
|
309
|
+
if (forceColor !== undefined && hasNumericForceColor()) {
|
|
310
|
+
return forceColor;
|
|
311
|
+
}
|
|
244
312
|
if ("TF_BUILD" in env && "AGENT_NAME" in env) {
|
|
245
313
|
return 1;
|
|
246
314
|
}
|
|
@@ -268,7 +336,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
|
268
336
|
return min;
|
|
269
337
|
}
|
|
270
338
|
if ("TEAMCITY_VERSION" in env) {
|
|
271
|
-
return /^(9\.
|
|
339
|
+
return /^(?:9\.0*[1-9]\d*\.|\d{2,}\.)/.test(env.TEAMCITY_VERSION) ? 1 : 0;
|
|
272
340
|
}
|
|
273
341
|
if (env.COLORTERM === "truecolor") {
|
|
274
342
|
return 3;
|
|
@@ -283,7 +351,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
|
283
351
|
return 3;
|
|
284
352
|
}
|
|
285
353
|
if ("TERM_PROGRAM" in env) {
|
|
286
|
-
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
|
|
354
|
+
const version = Number.parseInt((env.TERM_PROGRAM_VERSION || "").split(".", 1)[0], 10);
|
|
287
355
|
switch (env.TERM_PROGRAM) {
|
|
288
356
|
case "iTerm.app": {
|
|
289
357
|
return version >= 3 ? 3 : 2;
|
|
@@ -293,7 +361,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
|
|
|
293
361
|
}
|
|
294
362
|
}
|
|
295
363
|
}
|
|
296
|
-
if (/-256(color)?$/i.test(env.TERM)) {
|
|
364
|
+
if (/-256(?:color)?$/i.test(env.TERM)) {
|
|
297
365
|
return 2;
|
|
298
366
|
}
|
|
299
367
|
if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env.TERM)) {
|
|
@@ -326,90 +394,39 @@ var init_supports_color = __esm(() => {
|
|
|
326
394
|
supports_color_default = supportsColor;
|
|
327
395
|
});
|
|
328
396
|
|
|
329
|
-
// node_modules/chalk/source/utilities.js
|
|
330
|
-
function stringReplaceAll(string, substring, replacer) {
|
|
331
|
-
let index = string.indexOf(substring);
|
|
332
|
-
if (index === -1) {
|
|
333
|
-
return string;
|
|
334
|
-
}
|
|
335
|
-
const substringLength = substring.length;
|
|
336
|
-
let endIndex = 0;
|
|
337
|
-
let returnValue = "";
|
|
338
|
-
do {
|
|
339
|
-
returnValue += string.slice(endIndex, index) + substring + replacer;
|
|
340
|
-
endIndex = index + substringLength;
|
|
341
|
-
index = string.indexOf(substring, endIndex);
|
|
342
|
-
} while (index !== -1);
|
|
343
|
-
returnValue += string.slice(endIndex);
|
|
344
|
-
return returnValue;
|
|
345
|
-
}
|
|
346
|
-
function stringEncaseCRLFWithFirstIndex(string, prefix, postfix, index) {
|
|
347
|
-
let endIndex = 0;
|
|
348
|
-
let returnValue = "";
|
|
349
|
-
do {
|
|
350
|
-
const gotCR = string[index - 1] === "\r";
|
|
351
|
-
returnValue += string.slice(endIndex, gotCR ? index - 1 : index) + prefix + (gotCR ? `\r
|
|
352
|
-
` : `
|
|
353
|
-
`) + postfix;
|
|
354
|
-
endIndex = index + 1;
|
|
355
|
-
index = string.indexOf(`
|
|
356
|
-
`, endIndex);
|
|
357
|
-
} while (index !== -1);
|
|
358
|
-
returnValue += string.slice(endIndex);
|
|
359
|
-
return returnValue;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
397
|
// node_modules/chalk/source/index.js
|
|
363
|
-
var exports_source = {};
|
|
364
|
-
__export(exports_source, {
|
|
365
|
-
supportsColorStderr: () => stderrColor,
|
|
366
|
-
supportsColor: () => stdoutColor,
|
|
367
|
-
modifiers: () => modifierNames,
|
|
368
|
-
modifierNames: () => modifierNames,
|
|
369
|
-
foregroundColors: () => foregroundColorNames,
|
|
370
|
-
foregroundColorNames: () => foregroundColorNames,
|
|
371
|
-
default: () => source_default,
|
|
372
|
-
colors: () => colorNames,
|
|
373
|
-
colorNames: () => colorNames,
|
|
374
|
-
chalkStderr: () => chalkStderr,
|
|
375
|
-
backgroundColors: () => backgroundColorNames,
|
|
376
|
-
backgroundColorNames: () => backgroundColorNames,
|
|
377
|
-
Chalk: () => Chalk
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
class Chalk {
|
|
381
|
-
constructor(options) {
|
|
382
|
-
return chalkFactory(options);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
398
|
function createChalk(options) {
|
|
386
399
|
return chalkFactory(options);
|
|
387
400
|
}
|
|
388
|
-
var stdoutColor, stderrColor, GENERATOR, STYLER, IS_EMPTY,
|
|
389
|
-
if (
|
|
390
|
-
throw new Error("The `level`
|
|
401
|
+
var stdoutColor, stderrColor, GENERATOR, STYLER, IS_EMPTY, LEVEL, styles2, assertValidLevel = (level) => {
|
|
402
|
+
if (!Number.isSafeInteger(level) || level < 0 || level > 3) {
|
|
403
|
+
throw new Error("The `level` should be an integer from 0 to 3");
|
|
404
|
+
}
|
|
405
|
+
}, levelDescriptor, applyOptions = (object, options = {}) => {
|
|
406
|
+
if (options.level !== undefined) {
|
|
407
|
+
assertValidLevel(options.level);
|
|
391
408
|
}
|
|
392
409
|
const colorLevel = stdoutColor ? stdoutColor.level : 0;
|
|
393
|
-
object
|
|
410
|
+
object[LEVEL] = options.level === undefined ? colorLevel : options.level;
|
|
394
411
|
}, chalkFactory = (options) => {
|
|
395
412
|
const chalk = (...strings) => strings.join(" ");
|
|
396
413
|
applyOptions(chalk, options);
|
|
397
414
|
Object.setPrototypeOf(chalk, createChalk.prototype);
|
|
398
415
|
return chalk;
|
|
399
|
-
},
|
|
416
|
+
}, createModelConverters = (model, type) => {
|
|
417
|
+
const style = ansi_styles_default[type];
|
|
400
418
|
if (model === "rgb") {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
if (level === "ansi256") {
|
|
405
|
-
return ansi_styles_default[type].ansi256(ansi_styles_default.rgbToAnsi256(...arguments_));
|
|
406
|
-
}
|
|
407
|
-
return ansi_styles_default[type].ansi(ansi_styles_default.rgbToAnsi(...arguments_));
|
|
419
|
+
const ansi = (red, green, blue) => style.ansi(ansi_styles_default.rgbToAnsi(red, green, blue));
|
|
420
|
+
const ansi256 = (red, green, blue) => style.ansi256(ansi_styles_default.rgbToAnsi256(red, green, blue));
|
|
421
|
+
return [ansi, ansi, ansi256, style.ansi16m];
|
|
408
422
|
}
|
|
409
423
|
if (model === "hex") {
|
|
410
|
-
|
|
424
|
+
const ansi = (hex) => style.ansi(ansi_styles_default.hexToAnsi(hex));
|
|
425
|
+
const ansi256 = (hex) => style.ansi256(ansi_styles_default.hexToAnsi256(hex));
|
|
426
|
+
return [ansi, ansi, ansi256, (hex) => style.ansi16m(...ansi_styles_default.hexToRgb(hex))];
|
|
411
427
|
}
|
|
412
|
-
|
|
428
|
+
const ansi = (code) => style.ansi(ansi_styles_default.ansi256ToAnsi(code));
|
|
429
|
+
return [ansi, ansi, style.ansi256, style.ansi256];
|
|
413
430
|
}, usedModels, proto, createStyler = (open, close, parent) => {
|
|
414
431
|
let openAll;
|
|
415
432
|
let closeAll;
|
|
@@ -428,14 +445,22 @@ var stdoutColor, stderrColor, GENERATOR, STYLER, IS_EMPTY, levelMapping, styles2
|
|
|
428
445
|
parent
|
|
429
446
|
};
|
|
430
447
|
}, createBuilder = (self, _styler, _isEmpty) => {
|
|
431
|
-
const builder = (...arguments_) =>
|
|
448
|
+
const builder = (...arguments_) => {
|
|
449
|
+
if (arguments_.length === 1) {
|
|
450
|
+
return applyStyle(builder, "" + arguments_[0]);
|
|
451
|
+
}
|
|
452
|
+
if (arguments_.length === 2) {
|
|
453
|
+
return applyStyle(builder, arguments_[0] + " " + arguments_[1]);
|
|
454
|
+
}
|
|
455
|
+
return applyStyle(builder, arguments_.join(" "));
|
|
456
|
+
};
|
|
432
457
|
Object.setPrototypeOf(builder, proto);
|
|
433
|
-
builder[GENERATOR] = self;
|
|
458
|
+
builder[GENERATOR] = self[GENERATOR] ?? self;
|
|
434
459
|
builder[STYLER] = _styler;
|
|
435
460
|
builder[IS_EMPTY] = _isEmpty;
|
|
436
461
|
return builder;
|
|
437
462
|
}, applyStyle = (self, string) => {
|
|
438
|
-
if (self
|
|
463
|
+
if (self[GENERATOR][LEVEL] <= 0 || !string) {
|
|
439
464
|
return self[IS_EMPTY] ? "" : string;
|
|
440
465
|
}
|
|
441
466
|
let styler = self[STYLER];
|
|
@@ -459,18 +484,22 @@ var stdoutColor, stderrColor, GENERATOR, STYLER, IS_EMPTY, levelMapping, styles2
|
|
|
459
484
|
var init_source = __esm(() => {
|
|
460
485
|
init_ansi_styles();
|
|
461
486
|
init_supports_color();
|
|
462
|
-
init_ansi_styles();
|
|
463
487
|
({ stdout: stdoutColor, stderr: stderrColor } = supports_color_default);
|
|
464
488
|
GENERATOR = Symbol("GENERATOR");
|
|
465
489
|
STYLER = Symbol("STYLER");
|
|
466
490
|
IS_EMPTY = Symbol("IS_EMPTY");
|
|
467
|
-
|
|
468
|
-
"ansi",
|
|
469
|
-
"ansi",
|
|
470
|
-
"ansi256",
|
|
471
|
-
"ansi16m"
|
|
472
|
-
];
|
|
491
|
+
LEVEL = Symbol("LEVEL");
|
|
473
492
|
styles2 = Object.create(null);
|
|
493
|
+
levelDescriptor = {
|
|
494
|
+
enumerable: true,
|
|
495
|
+
get() {
|
|
496
|
+
return this[LEVEL];
|
|
497
|
+
},
|
|
498
|
+
set(level) {
|
|
499
|
+
assertValidLevel(level);
|
|
500
|
+
this[LEVEL] = level;
|
|
501
|
+
}
|
|
502
|
+
};
|
|
474
503
|
Object.setPrototypeOf(createChalk.prototype, Function.prototype);
|
|
475
504
|
for (const [styleName, style] of Object.entries(ansi_styles_default)) {
|
|
476
505
|
styles2[styleName] = {
|
|
@@ -490,25 +519,25 @@ var init_source = __esm(() => {
|
|
|
490
519
|
};
|
|
491
520
|
usedModels = ["rgb", "hex", "ansi256"];
|
|
492
521
|
for (const model of usedModels) {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
return
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
}
|
|
522
|
+
const capitalizedModel = model[0].toUpperCase() + model.slice(1);
|
|
523
|
+
for (const [styleName, type] of [
|
|
524
|
+
[model, "color"],
|
|
525
|
+
["bg" + capitalizedModel, "bgColor"],
|
|
526
|
+
["underline" + capitalizedModel, "underlineColor"]
|
|
527
|
+
]) {
|
|
528
|
+
const { close } = ansi_styles_default[type];
|
|
529
|
+
const converters = createModelConverters(model, type);
|
|
530
|
+
styles2[styleName] = {
|
|
531
|
+
get() {
|
|
532
|
+
const styleFunction = function(first, second, third) {
|
|
533
|
+
const open = converters[this.level](first, second, third);
|
|
534
|
+
return createBuilder(this, createStyler(open, close, this[STYLER]), this[IS_EMPTY]);
|
|
535
|
+
};
|
|
536
|
+
Object.defineProperty(this, styleName, { value: styleFunction });
|
|
537
|
+
return styleFunction;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
}
|
|
512
541
|
}
|
|
513
542
|
proto = Object.defineProperties(() => {}, {
|
|
514
543
|
...styles2,
|
|
@@ -522,7 +551,7 @@ var init_source = __esm(() => {
|
|
|
522
551
|
}
|
|
523
552
|
}
|
|
524
553
|
});
|
|
525
|
-
Object.defineProperties(createChalk.prototype, styles2);
|
|
554
|
+
Object.defineProperties(createChalk.prototype, { ...styles2, level: levelDescriptor });
|
|
526
555
|
chalk = createChalk();
|
|
527
556
|
chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
528
557
|
source_default = chalk;
|
|
@@ -530,6 +559,7 @@ var init_source = __esm(() => {
|
|
|
530
559
|
|
|
531
560
|
// src/index.ts
|
|
532
561
|
init_source();
|
|
562
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
533
563
|
|
|
534
564
|
// node_modules/citty/dist/_chunks/libs/scule.mjs
|
|
535
565
|
var NUMBER_CHAR_RE = /\d/;
|
|
@@ -743,8 +773,8 @@ function parseRawArgs(args = [], opts = {}) {
|
|
|
743
773
|
return out;
|
|
744
774
|
}
|
|
745
775
|
var noColor = /* @__PURE__ */ (() => {
|
|
746
|
-
const
|
|
747
|
-
return
|
|
776
|
+
const env = globalThis.process?.env ?? {};
|
|
777
|
+
return env.NO_COLOR === "1" || env.TERM === "dumb" || env.TEST || env.CI;
|
|
748
778
|
})();
|
|
749
779
|
var _c = (c, r = 39) => (t) => noColor ? t : `\x1B[${c}m${t}\x1B[${r}m`;
|
|
750
780
|
var bold = /* @__PURE__ */ _c(1, 22);
|
|
@@ -1078,7 +1108,6 @@ init_source();
|
|
|
1078
1108
|
import { writeFileSync, chmodSync, mkdirSync } from "fs";
|
|
1079
1109
|
import { resolve, dirname } from "path";
|
|
1080
1110
|
import { homedir } from "os";
|
|
1081
|
-
import { createInterface } from "readline/promises";
|
|
1082
1111
|
|
|
1083
1112
|
// src/lib/errors.ts
|
|
1084
1113
|
init_source();
|
|
@@ -1128,12 +1157,60 @@ function formatError(err, format) {
|
|
|
1128
1157
|
hint: ${hint}` : head };
|
|
1129
1158
|
}
|
|
1130
1159
|
function emitError(err, format) {
|
|
1131
|
-
const
|
|
1132
|
-
process.stderr.write(
|
|
1160
|
+
const env = formatError(err, format);
|
|
1161
|
+
process.stderr.write(env.text + `
|
|
1133
1162
|
`);
|
|
1134
1163
|
}
|
|
1135
1164
|
|
|
1165
|
+
// src/commands/common.ts
|
|
1166
|
+
var commonArgs = {
|
|
1167
|
+
format: { type: "string", description: "Output format: table | json (auto-detected by TTY)" },
|
|
1168
|
+
token: { type: "string", description: "Inline access token (prefer env vars or `oura-cli login`)" },
|
|
1169
|
+
db: { type: "string", description: "Path to SQLite database file (env: OURA_DB_PATH)" },
|
|
1170
|
+
tz: { type: "string", description: "Display timezone (env: OURA_TZ; auto-detected)" },
|
|
1171
|
+
"no-color": { type: "boolean", default: false, description: "Disable ANSI colors (also honors NO_COLOR env)" }
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1136
1174
|
// src/commands/login.ts
|
|
1175
|
+
async function readHiddenToken(input, output) {
|
|
1176
|
+
if (!input.isTTY || !input.setRawMode) {
|
|
1177
|
+
throw new CliError("BAD_ARGS", "Interactive login requires a terminal.", "Use `oura-cli login --token` for non-interactive use.");
|
|
1178
|
+
}
|
|
1179
|
+
output.write("Paste your token (input hidden): ");
|
|
1180
|
+
input.setRawMode(true);
|
|
1181
|
+
input.resume();
|
|
1182
|
+
return new Promise((resolve, reject) => {
|
|
1183
|
+
let token = "";
|
|
1184
|
+
const finish = () => {
|
|
1185
|
+
input.off("data", onData);
|
|
1186
|
+
input.setRawMode?.(false);
|
|
1187
|
+
input.pause();
|
|
1188
|
+
output.write(`
|
|
1189
|
+
`);
|
|
1190
|
+
};
|
|
1191
|
+
const onData = (chunk) => {
|
|
1192
|
+
for (const char of chunk.toString("utf8")) {
|
|
1193
|
+
if (char === "\r" || char === `
|
|
1194
|
+
`) {
|
|
1195
|
+
finish();
|
|
1196
|
+
resolve(token);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (char === "\x03" || char === "\x04") {
|
|
1200
|
+
finish();
|
|
1201
|
+
reject(new CliError("BAD_ARGS", "Login cancelled."));
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (char === "\b" || char === "\x7F") {
|
|
1205
|
+
token = token.slice(0, -1);
|
|
1206
|
+
} else if (char >= " ") {
|
|
1207
|
+
token += char;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
input.on("data", onData);
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1137
1214
|
function writeToken(path, token) {
|
|
1138
1215
|
const trimmed = token.trim();
|
|
1139
1216
|
if (trimmed.length === 0) {
|
|
@@ -1150,37 +1227,459 @@ var loginCommand = defineCommand({
|
|
|
1150
1227
|
args: {
|
|
1151
1228
|
token: { type: "string", description: "Pass token non-interactively (e.g. for scripts)" },
|
|
1152
1229
|
path: { type: "string", description: "Where to save the token (default: $OURA_TOKEN_PATH or ~/.oura-token)" },
|
|
1153
|
-
"no-color":
|
|
1230
|
+
"no-color": commonArgs["no-color"]
|
|
1154
1231
|
},
|
|
1155
1232
|
async run({ args }) {
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1233
|
+
try {
|
|
1234
|
+
if (args["no-color"] || process.env.NO_COLOR) {
|
|
1235
|
+
await Promise.resolve().then(() => init_source());
|
|
1236
|
+
source_default.level = 0;
|
|
1237
|
+
}
|
|
1238
|
+
const target = args.path ?? process.env.OURA_TOKEN_PATH ?? resolve(homedir(), ".oura-token");
|
|
1239
|
+
let token = args.token;
|
|
1240
|
+
if (!token) {
|
|
1241
|
+
console.log("Get a Personal Access Token at https://cloud.ouraring.com/personal-access-tokens");
|
|
1242
|
+
token = await readHiddenToken(process.stdin, process.stdout);
|
|
1243
|
+
}
|
|
1244
|
+
writeToken(target, token);
|
|
1245
|
+
console.log(source_default.green(`Saved to ${target}`));
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
emitError(err, "table");
|
|
1248
|
+
process.exit(exitCodeFor(err));
|
|
1167
1249
|
}
|
|
1168
|
-
writeToken(target, token);
|
|
1169
|
-
console.log(source_default.green(`Saved to ${target}`));
|
|
1170
1250
|
}
|
|
1171
1251
|
});
|
|
1172
1252
|
|
|
1253
|
+
// src/lib/time.ts
|
|
1254
|
+
function nowUtc() {
|
|
1255
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1256
|
+
}
|
|
1257
|
+
function formatLocal(utcStr, timezone) {
|
|
1258
|
+
const dt = new Date(utcStr);
|
|
1259
|
+
const parts = new Intl.DateTimeFormat("sv-SE", {
|
|
1260
|
+
timeZone: timezone,
|
|
1261
|
+
year: "numeric",
|
|
1262
|
+
month: "2-digit",
|
|
1263
|
+
day: "2-digit",
|
|
1264
|
+
hour: "2-digit",
|
|
1265
|
+
minute: "2-digit",
|
|
1266
|
+
hour12: false
|
|
1267
|
+
}).formatToParts(dt);
|
|
1268
|
+
const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
1269
|
+
return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}`;
|
|
1270
|
+
}
|
|
1271
|
+
function formatLocalDate(utcStr, timezone) {
|
|
1272
|
+
return formatLocal(utcStr, timezone).split(" ")[0];
|
|
1273
|
+
}
|
|
1274
|
+
function todayLocal(timezone) {
|
|
1275
|
+
return formatLocalDate(nowUtc(), timezone);
|
|
1276
|
+
}
|
|
1277
|
+
function getTimezoneOffsetMs(utc, tz) {
|
|
1278
|
+
const tzPart = new Intl.DateTimeFormat("en-US", {
|
|
1279
|
+
timeZone: tz,
|
|
1280
|
+
timeZoneName: "longOffset"
|
|
1281
|
+
}).formatToParts(utc).find((p) => p.type === "timeZoneName")?.value ?? "GMT";
|
|
1282
|
+
const m = tzPart.match(/GMT([+-])(\d{2}):(\d{2})/);
|
|
1283
|
+
if (!m)
|
|
1284
|
+
return 0;
|
|
1285
|
+
const sign = m[1] === "+" ? 1 : -1;
|
|
1286
|
+
return sign * (parseInt(m[2], 10) * 3600 + parseInt(m[3], 10) * 60) * 1000;
|
|
1287
|
+
}
|
|
1288
|
+
function localMidnightMs(day, tz) {
|
|
1289
|
+
const naiveMs = new Date(`${day}T00:00:00Z`).getTime();
|
|
1290
|
+
const guessMs = naiveMs - getTimezoneOffsetMs(new Date(naiveMs), tz);
|
|
1291
|
+
return naiveMs - getTimezoneOffsetMs(new Date(guessMs), tz);
|
|
1292
|
+
}
|
|
1293
|
+
function localDateToUtcRange(localDate, timezone) {
|
|
1294
|
+
const iso = (ms) => new Date(ms).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1295
|
+
return [iso(localMidnightMs(localDate, timezone)), iso(localMidnightMs(shiftDay(localDate, 1), timezone))];
|
|
1296
|
+
}
|
|
1297
|
+
function resolveDefaultTimezone() {
|
|
1298
|
+
if (process.env.OURA_TZ)
|
|
1299
|
+
return process.env.OURA_TZ;
|
|
1300
|
+
try {
|
|
1301
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
1302
|
+
} catch {
|
|
1303
|
+
return "UTC";
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
function today(timezone) {
|
|
1307
|
+
return todayLocal(timezone ?? resolveDefaultTimezone());
|
|
1308
|
+
}
|
|
1309
|
+
function shiftDay(day, delta) {
|
|
1310
|
+
const ms = new Date(`${day}T00:00:00Z`).getTime() + delta * 86400000;
|
|
1311
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
1312
|
+
}
|
|
1313
|
+
function daysBack(endDay, n) {
|
|
1314
|
+
const out = [];
|
|
1315
|
+
for (let i = n - 1;i >= 0; i--)
|
|
1316
|
+
out.push(shiftDay(endDay, -i));
|
|
1317
|
+
return out;
|
|
1318
|
+
}
|
|
1319
|
+
var CALENDAR_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1320
|
+
function isCalendarDate(value) {
|
|
1321
|
+
if (!CALENDAR_DATE.test(value))
|
|
1322
|
+
return false;
|
|
1323
|
+
const ms = new Date(`${value}T00:00:00Z`).getTime();
|
|
1324
|
+
return !Number.isNaN(ms) && new Date(ms).toISOString().slice(0, 10) === value;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
// src/collections/types.ts
|
|
1328
|
+
function defineCollection(c) {
|
|
1329
|
+
return c;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/collections/sleep.ts
|
|
1333
|
+
var sleep = defineCollection({
|
|
1334
|
+
name: "sleep",
|
|
1335
|
+
endpoint: "daily_sleep",
|
|
1336
|
+
table: "daily_sleep",
|
|
1337
|
+
description: "Daily sleep score and contributors",
|
|
1338
|
+
conflict: "replace",
|
|
1339
|
+
rangeParams: "date",
|
|
1340
|
+
identity: [
|
|
1341
|
+
{ field: "id", description: "Oura record id" },
|
|
1342
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1343
|
+
],
|
|
1344
|
+
columns: [
|
|
1345
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1346
|
+
{ name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
|
|
1347
|
+
{ name: "score", type: "INTEGER", pick: (r) => r.score },
|
|
1348
|
+
{ name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
|
|
1349
|
+
{ name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
|
|
1350
|
+
]
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
// src/collections/readiness.ts
|
|
1354
|
+
var readiness = defineCollection({
|
|
1355
|
+
name: "readiness",
|
|
1356
|
+
endpoint: "daily_readiness",
|
|
1357
|
+
table: "daily_readiness",
|
|
1358
|
+
description: "Daily readiness score, contributors and temperature deviation",
|
|
1359
|
+
conflict: "replace",
|
|
1360
|
+
rangeParams: "date",
|
|
1361
|
+
identity: [
|
|
1362
|
+
{ field: "id", description: "Oura record id" },
|
|
1363
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1364
|
+
],
|
|
1365
|
+
columns: [
|
|
1366
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1367
|
+
{ name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
|
|
1368
|
+
{ name: "score", type: "INTEGER", pick: (r) => r.score },
|
|
1369
|
+
{ name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
|
|
1370
|
+
{ name: "temperature_deviation", type: "REAL", pick: (r) => r.temperature_deviation },
|
|
1371
|
+
{ name: "temperature_trend_deviation", type: "REAL", pick: (r) => r.temperature_trend_deviation },
|
|
1372
|
+
{ name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
|
|
1373
|
+
]
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
// src/collections/activity.ts
|
|
1377
|
+
var activity = defineCollection({
|
|
1378
|
+
name: "activity",
|
|
1379
|
+
endpoint: "daily_activity",
|
|
1380
|
+
table: "daily_activity",
|
|
1381
|
+
description: "Daily activity score, steps, calories and activity-time buckets",
|
|
1382
|
+
conflict: "replace",
|
|
1383
|
+
rangeParams: "date",
|
|
1384
|
+
identity: [
|
|
1385
|
+
{ field: "id", description: "Oura record id" },
|
|
1386
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1387
|
+
],
|
|
1388
|
+
columns: [
|
|
1389
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1390
|
+
{ name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
|
|
1391
|
+
{ name: "score", type: "INTEGER", pick: (r) => r.score },
|
|
1392
|
+
{ name: "active_calories", type: "INTEGER", pick: (r) => r.active_calories },
|
|
1393
|
+
{ name: "steps", type: "INTEGER", pick: (r) => r.steps },
|
|
1394
|
+
{ name: "equivalent_walking_distance", type: "REAL", pick: (r) => r.equivalent_walking_distance },
|
|
1395
|
+
{ name: "high_activity_time", type: "INTEGER", pick: (r) => r.high_activity_time },
|
|
1396
|
+
{ name: "medium_activity_time", type: "INTEGER", pick: (r) => r.medium_activity_time },
|
|
1397
|
+
{ name: "low_activity_time", type: "INTEGER", pick: (r) => r.low_activity_time },
|
|
1398
|
+
{ name: "sedentary_time", type: "INTEGER", pick: (r) => r.sedentary_time },
|
|
1399
|
+
{ name: "total_calories", type: "INTEGER", pick: (r) => r.total_calories },
|
|
1400
|
+
{ name: "target_calories", type: "INTEGER", pick: (r) => r.target_calories },
|
|
1401
|
+
{ name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
|
|
1402
|
+
{ name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
|
|
1403
|
+
]
|
|
1404
|
+
});
|
|
1405
|
+
|
|
1406
|
+
// src/collections/hr.ts
|
|
1407
|
+
var hr = defineCollection({
|
|
1408
|
+
name: "hr",
|
|
1409
|
+
endpoint: "heartrate",
|
|
1410
|
+
table: "heartrate",
|
|
1411
|
+
description: "Heart rate samples (bpm) with source",
|
|
1412
|
+
conflict: "ignore",
|
|
1413
|
+
rangeParams: "datetime",
|
|
1414
|
+
maxRangeDays: 30,
|
|
1415
|
+
identity: [{ field: "timestamp", format: "date-time", description: "ISO 8601 timestamp of the sample" }],
|
|
1416
|
+
columns: [
|
|
1417
|
+
{ name: "timestamp", type: "TEXT", pick: (r) => r.timestamp },
|
|
1418
|
+
{ name: "bpm", type: "INTEGER", pick: (r) => r.bpm },
|
|
1419
|
+
{ name: "source", type: "TEXT", pick: (r) => r.source },
|
|
1420
|
+
{ name: "day", type: "TEXT", pick: (r) => r.timestamp.slice(0, 10) }
|
|
1421
|
+
],
|
|
1422
|
+
indexes: [
|
|
1423
|
+
{ name: "idx_heartrate_ts", columns: ["timestamp"] },
|
|
1424
|
+
{ name: "idx_heartrate_unique", columns: ["timestamp", "source"], unique: true },
|
|
1425
|
+
{ name: "idx_heartrate_day", columns: ["day"] }
|
|
1426
|
+
]
|
|
1427
|
+
});
|
|
1428
|
+
|
|
1429
|
+
// src/collections/spo2.ts
|
|
1430
|
+
var spo2 = defineCollection({
|
|
1431
|
+
name: "spo2",
|
|
1432
|
+
endpoint: "daily_spo2",
|
|
1433
|
+
table: "daily_spo2",
|
|
1434
|
+
description: "Daily blood-oxygen average and breathing disturbance index",
|
|
1435
|
+
conflict: "replace",
|
|
1436
|
+
rangeParams: "date",
|
|
1437
|
+
identity: [
|
|
1438
|
+
{ field: "id", description: "Oura record id" },
|
|
1439
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1440
|
+
],
|
|
1441
|
+
columns: [
|
|
1442
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1443
|
+
{ name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
|
|
1444
|
+
{ name: "spo2_average", type: "REAL", pick: (r) => r.spo2_percentage?.average ?? null },
|
|
1445
|
+
{ name: "breathing_disturbance_index", type: "REAL", pick: (r) => r.breathing_disturbance_index }
|
|
1446
|
+
]
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
// src/collections/stress.ts
|
|
1450
|
+
var stress = defineCollection({
|
|
1451
|
+
name: "stress",
|
|
1452
|
+
endpoint: "daily_stress",
|
|
1453
|
+
table: "daily_stress",
|
|
1454
|
+
description: "Daily stress summary with high-stress and high-recovery seconds",
|
|
1455
|
+
conflict: "replace",
|
|
1456
|
+
rangeParams: "date",
|
|
1457
|
+
identity: [
|
|
1458
|
+
{ field: "id", description: "Oura record id" },
|
|
1459
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1460
|
+
],
|
|
1461
|
+
columns: [
|
|
1462
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1463
|
+
{ name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
|
|
1464
|
+
{ name: "day_summary", type: "TEXT", pick: (r) => r.day_summary ?? null },
|
|
1465
|
+
{ name: "recovery_high", type: "INTEGER", pick: (r) => r.recovery_high },
|
|
1466
|
+
{ name: "stress_high", type: "INTEGER", pick: (r) => r.stress_high }
|
|
1467
|
+
]
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
// src/collections/workout.ts
|
|
1471
|
+
var workout = defineCollection({
|
|
1472
|
+
name: "workout",
|
|
1473
|
+
endpoint: "workout",
|
|
1474
|
+
table: "workouts",
|
|
1475
|
+
description: "Workout sessions with activity, calories, distance and intensity",
|
|
1476
|
+
conflict: "replace",
|
|
1477
|
+
rangeParams: "date",
|
|
1478
|
+
identity: [
|
|
1479
|
+
{ field: "id", description: "Oura record id" },
|
|
1480
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1481
|
+
],
|
|
1482
|
+
columns: [
|
|
1483
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1484
|
+
{ name: "day", type: "TEXT", pick: (r) => r.day },
|
|
1485
|
+
{ name: "activity", type: "TEXT", pick: (r) => r.activity },
|
|
1486
|
+
{ name: "calories", type: "REAL", pick: (r) => r.calories },
|
|
1487
|
+
{ name: "distance", type: "REAL", pick: (r) => r.distance },
|
|
1488
|
+
{ name: "start_datetime", type: "TEXT", pick: (r) => r.start_datetime },
|
|
1489
|
+
{ name: "end_datetime", type: "TEXT", pick: (r) => r.end_datetime },
|
|
1490
|
+
{ name: "intensity", type: "TEXT", pick: (r) => r.intensity },
|
|
1491
|
+
{ name: "label", type: "TEXT", pick: (r) => r.label ?? "" },
|
|
1492
|
+
{ name: "source", type: "TEXT", pick: (r) => r.source }
|
|
1493
|
+
]
|
|
1494
|
+
});
|
|
1495
|
+
|
|
1496
|
+
// src/collections/sleep-periods.ts
|
|
1497
|
+
var sleepPeriods = defineCollection({
|
|
1498
|
+
name: "sleep-periods",
|
|
1499
|
+
endpoint: "sleep",
|
|
1500
|
+
table: "sleep_model",
|
|
1501
|
+
description: "Individual sleep periods with stages, HRV, heart rate and efficiency",
|
|
1502
|
+
conflict: "replace",
|
|
1503
|
+
rangeParams: "date",
|
|
1504
|
+
identity: [
|
|
1505
|
+
{ field: "id", description: "Oura record id" },
|
|
1506
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1507
|
+
],
|
|
1508
|
+
columns: [
|
|
1509
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1510
|
+
{ name: "day", type: "TEXT", pick: (r) => r.day },
|
|
1511
|
+
{ name: "average_breath", type: "REAL", pick: (r) => r.average_breath },
|
|
1512
|
+
{ name: "average_heart_rate", type: "REAL", pick: (r) => r.average_heart_rate },
|
|
1513
|
+
{ name: "average_hrv", type: "REAL", pick: (r) => r.average_hrv },
|
|
1514
|
+
{ name: "awake_time", type: "INTEGER", pick: (r) => r.awake_time },
|
|
1515
|
+
{ name: "bedtime_end", type: "TEXT", pick: (r) => r.bedtime_end },
|
|
1516
|
+
{ name: "bedtime_start", type: "TEXT", pick: (r) => r.bedtime_start },
|
|
1517
|
+
{ name: "deep_sleep_duration", type: "INTEGER", pick: (r) => r.deep_sleep_duration },
|
|
1518
|
+
{ name: "efficiency", type: "INTEGER", pick: (r) => r.efficiency },
|
|
1519
|
+
{ name: "latency", type: "INTEGER", pick: (r) => r.latency },
|
|
1520
|
+
{ name: "light_sleep_duration", type: "INTEGER", pick: (r) => r.light_sleep_duration },
|
|
1521
|
+
{ name: "lowest_heart_rate", type: "INTEGER", pick: (r) => r.lowest_heart_rate },
|
|
1522
|
+
{ name: "period", type: "INTEGER", pick: (r) => r.period },
|
|
1523
|
+
{ name: "rem_sleep_duration", type: "INTEGER", pick: (r) => r.rem_sleep_duration },
|
|
1524
|
+
{ name: "restless_periods", type: "INTEGER", pick: (r) => r.restless_periods },
|
|
1525
|
+
{ name: "time_in_bed", type: "INTEGER", pick: (r) => r.time_in_bed },
|
|
1526
|
+
{ name: "total_sleep_duration", type: "INTEGER", pick: (r) => r.total_sleep_duration },
|
|
1527
|
+
{ name: "type", type: "TEXT", pick: (r) => r.type ?? null }
|
|
1528
|
+
]
|
|
1529
|
+
});
|
|
1530
|
+
|
|
1531
|
+
// src/collections/cv-age.ts
|
|
1532
|
+
var cvAge = defineCollection({
|
|
1533
|
+
name: "cv-age",
|
|
1534
|
+
endpoint: "daily_cardiovascular_age",
|
|
1535
|
+
table: "cardiovascular_age",
|
|
1536
|
+
description: "Daily cardiovascular (vascular) age estimate",
|
|
1537
|
+
conflict: "replace",
|
|
1538
|
+
rangeParams: "date",
|
|
1539
|
+
identity: [
|
|
1540
|
+
{ field: "id", description: "Oura record id" },
|
|
1541
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1542
|
+
],
|
|
1543
|
+
columns: [
|
|
1544
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1545
|
+
{ name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
|
|
1546
|
+
{ name: "vascular_age", type: "INTEGER", pick: (r) => r.vascular_age }
|
|
1547
|
+
]
|
|
1548
|
+
});
|
|
1549
|
+
|
|
1550
|
+
// src/collections/index.ts
|
|
1551
|
+
var COLLECTIONS = [
|
|
1552
|
+
sleep,
|
|
1553
|
+
readiness,
|
|
1554
|
+
activity,
|
|
1555
|
+
hr,
|
|
1556
|
+
spo2,
|
|
1557
|
+
stress,
|
|
1558
|
+
workout,
|
|
1559
|
+
sleepPeriods,
|
|
1560
|
+
cvAge
|
|
1561
|
+
];
|
|
1562
|
+
function names() {
|
|
1563
|
+
return COLLECTIONS.map((c) => c.name);
|
|
1564
|
+
}
|
|
1565
|
+
function byName(name) {
|
|
1566
|
+
return COLLECTIONS.find((c) => c.name === name);
|
|
1567
|
+
}
|
|
1568
|
+
function insertSql(c) {
|
|
1569
|
+
const verb = c.conflict === "replace" ? "INSERT OR REPLACE" : "INSERT OR IGNORE";
|
|
1570
|
+
const cols = c.columns.map((col) => col.name).join(", ");
|
|
1571
|
+
const marks = c.columns.map(() => "?").join(", ");
|
|
1572
|
+
return `${verb} INTO ${c.table} (${cols}) VALUES (${marks})`;
|
|
1573
|
+
}
|
|
1574
|
+
function rowValues(c, row) {
|
|
1575
|
+
return c.columns.map((col) => col.pick(row));
|
|
1576
|
+
}
|
|
1577
|
+
var MS_PER_DAY = 86400000;
|
|
1578
|
+
function dateQueries(start, end, maxDays) {
|
|
1579
|
+
if (!maxDays)
|
|
1580
|
+
return [{ start_date: start, end_date: end }];
|
|
1581
|
+
const out = [];
|
|
1582
|
+
for (let s = start;s <= end; s = shiftDay(s, maxDays)) {
|
|
1583
|
+
const e = shiftDay(s, maxDays - 1);
|
|
1584
|
+
out.push({ start_date: s, end_date: e < end ? e : end });
|
|
1585
|
+
}
|
|
1586
|
+
return out;
|
|
1587
|
+
}
|
|
1588
|
+
function datetimeQueries(start, end, tz, maxDays) {
|
|
1589
|
+
const from = Date.parse(localDateToUtcRange(start, tz)[0]);
|
|
1590
|
+
const to = Date.parse(localDateToUtcRange(end, tz)[1]) - 1;
|
|
1591
|
+
const span = (maxDays ?? Infinity) * MS_PER_DAY;
|
|
1592
|
+
const out = [];
|
|
1593
|
+
for (let s = from;s <= to; s += span) {
|
|
1594
|
+
out.push({ start_datetime: new Date(s).toISOString(), end_datetime: new Date(Math.min(s + span - 1, to)).toISOString() });
|
|
1595
|
+
}
|
|
1596
|
+
return out;
|
|
1597
|
+
}
|
|
1598
|
+
function rangeQueries(c, start, end, tz) {
|
|
1599
|
+
if (start > end)
|
|
1600
|
+
return [];
|
|
1601
|
+
return c.rangeParams === "date" ? dateQueries(start, end, c.maxRangeDays) : datetimeQueries(start, end, tz, c.maxRangeDays);
|
|
1602
|
+
}
|
|
1603
|
+
async function fetchCollection(client, c, start, end, tz) {
|
|
1604
|
+
const rows = [];
|
|
1605
|
+
for (const query of rangeQueries(c, start, end, tz)) {
|
|
1606
|
+
for (const row of await client.fetch(c.endpoint, query))
|
|
1607
|
+
rows.push(row);
|
|
1608
|
+
}
|
|
1609
|
+
return rows;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1173
1612
|
// src/commands/describe.ts
|
|
1174
|
-
|
|
1613
|
+
var OUTPUT_SCHEMAS = { doctor: "docs/schemas/doctor.json" };
|
|
1614
|
+
var ENUM_ARGS = {
|
|
1615
|
+
fetch: { collection: names() },
|
|
1616
|
+
report: { period: ["week", "month"] }
|
|
1617
|
+
};
|
|
1618
|
+
function resolved(def) {
|
|
1619
|
+
if (typeof def === "function" || def instanceof Promise) {
|
|
1620
|
+
throw new Error("describe: lazy subcommands are not supported; register plain CommandDef objects.");
|
|
1621
|
+
}
|
|
1622
|
+
return def;
|
|
1623
|
+
}
|
|
1624
|
+
function describeArgs(command, args) {
|
|
1625
|
+
const out = [];
|
|
1626
|
+
for (const [key, raw] of Object.entries(args ?? {})) {
|
|
1627
|
+
if (raw === commonArgs[key])
|
|
1628
|
+
continue;
|
|
1629
|
+
const a = raw;
|
|
1630
|
+
const values = ENUM_ARGS[command]?.[key] ?? (a.type === "enum" ? [...a.options ?? []] : undefined);
|
|
1631
|
+
if (a.type === "positional") {
|
|
1632
|
+
out.push({
|
|
1633
|
+
name: a.required ? `<${key}>` : `[${key}]`,
|
|
1634
|
+
type: "string",
|
|
1635
|
+
required: a.required === true,
|
|
1636
|
+
description: a.description,
|
|
1637
|
+
...values ? { values } : {}
|
|
1638
|
+
});
|
|
1639
|
+
} else {
|
|
1640
|
+
out.push({
|
|
1641
|
+
name: `--${key}`,
|
|
1642
|
+
type: values ? "enum" : String(a.type ?? "string"),
|
|
1643
|
+
required: false,
|
|
1644
|
+
description: a.description,
|
|
1645
|
+
...values ? { values } : {}
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
return out;
|
|
1650
|
+
}
|
|
1651
|
+
function resolvedMeta(meta) {
|
|
1652
|
+
if (typeof meta === "function" || meta instanceof Promise)
|
|
1653
|
+
return {};
|
|
1654
|
+
return meta ?? {};
|
|
1655
|
+
}
|
|
1656
|
+
function describeCommandDef(name, def) {
|
|
1657
|
+
const meta = resolvedMeta(def.meta);
|
|
1658
|
+
const cmd = {
|
|
1659
|
+
name,
|
|
1660
|
+
description: meta.description ?? "",
|
|
1661
|
+
args: describeArgs(name, def.args)
|
|
1662
|
+
};
|
|
1663
|
+
if (OUTPUT_SCHEMAS[name])
|
|
1664
|
+
cmd.outputSchema = OUTPUT_SCHEMAS[name];
|
|
1665
|
+
if (name === "fetch")
|
|
1666
|
+
cmd.outputSchemas = Object.fromEntries(names().map((n) => [n, `docs/schemas/${n}.json`]));
|
|
1667
|
+
const subs = def.subCommands;
|
|
1668
|
+
if (subs) {
|
|
1669
|
+
cmd.subcommands = Object.entries(subs).map(([subName, subDef]) => {
|
|
1670
|
+
const sub = resolved(subDef);
|
|
1671
|
+
const subMeta = resolvedMeta(sub.meta);
|
|
1672
|
+
return { name: subName, description: subMeta.description ?? "", args: describeArgs(subName, sub.args) };
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
return cmd;
|
|
1676
|
+
}
|
|
1677
|
+
function buildManifest(version, commands) {
|
|
1175
1678
|
return {
|
|
1176
1679
|
name: "oura-cli",
|
|
1177
1680
|
version,
|
|
1178
1681
|
compatManifestCommand: "oura-cli manifest",
|
|
1179
|
-
auth: {
|
|
1180
|
-
envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH"],
|
|
1181
|
-
tokenFile: "~/.oura-token",
|
|
1182
|
-
loginCommand: "oura-cli login"
|
|
1183
|
-
},
|
|
1682
|
+
auth: { envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH"], tokenFile: "~/.oura-token", loginCommand: "oura-cli login" },
|
|
1184
1683
|
globalFlags: [
|
|
1185
1684
|
{ name: "--format", type: "enum", values: ["table", "json"], description: "Output format (auto-detected by TTY when omitted)" },
|
|
1186
1685
|
{ name: "--db", type: "string", description: "Override SQLite database path (env: OURA_DB_PATH)" },
|
|
@@ -1195,396 +1694,48 @@ function buildManifest(version) {
|
|
|
1195
1694
|
{ code: 3, meaning: "API or network error" },
|
|
1196
1695
|
{ code: 4, meaning: "database or local storage error" }
|
|
1197
1696
|
],
|
|
1198
|
-
commands: [
|
|
1199
|
-
{ name: "login", description: "Save an Oura Personal Access Token for future commands.", args: [
|
|
1200
|
-
{ name: "--token", type: "string", required: false, description: "Pass token non-interactively" },
|
|
1201
|
-
{ name: "--path", type: "string", required: false, description: "Override token file path" }
|
|
1202
|
-
] },
|
|
1203
|
-
{ name: "describe", description: "Emit a machine-readable manifest of commands, args, and outputs.", args: [] },
|
|
1204
|
-
{
|
|
1205
|
-
name: "sleep",
|
|
1206
|
-
description: "Fetch daily sleep scores from Oura API. Pick a subcommand: today | date <day> | week.",
|
|
1207
|
-
args: [],
|
|
1208
|
-
outputSchema: "docs/schemas/sleep.json",
|
|
1209
|
-
subcommands: [
|
|
1210
|
-
{ name: "today", description: "Today's sleep data.", args: [] },
|
|
1211
|
-
{ name: "date", description: "Sleep data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
|
|
1212
|
-
{ name: "week", description: "Last 7 days of sleep data.", args: [] }
|
|
1213
|
-
]
|
|
1214
|
-
},
|
|
1215
|
-
{
|
|
1216
|
-
name: "readiness",
|
|
1217
|
-
description: "Fetch daily readiness scores from Oura API. Pick a subcommand: today | date <day> | week.",
|
|
1218
|
-
args: [],
|
|
1219
|
-
outputSchema: "docs/schemas/readiness.json",
|
|
1220
|
-
subcommands: [
|
|
1221
|
-
{ name: "today", description: "Today's readiness data.", args: [] },
|
|
1222
|
-
{ name: "date", description: "Readiness data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
|
|
1223
|
-
{ name: "week", description: "Last 7 days of readiness data.", args: [] }
|
|
1224
|
-
]
|
|
1225
|
-
},
|
|
1226
|
-
{
|
|
1227
|
-
name: "activity",
|
|
1228
|
-
description: "Fetch daily activity scores from Oura API. Pick a subcommand: today | date <day> | week.",
|
|
1229
|
-
args: [],
|
|
1230
|
-
outputSchema: "docs/schemas/activity.json",
|
|
1231
|
-
subcommands: [
|
|
1232
|
-
{ name: "today", description: "Today's activity data.", args: [] },
|
|
1233
|
-
{ name: "date", description: "Activity data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
|
|
1234
|
-
{ name: "week", description: "Last 7 days of activity data.", args: [] }
|
|
1235
|
-
]
|
|
1236
|
-
},
|
|
1237
|
-
{
|
|
1238
|
-
name: "hr",
|
|
1239
|
-
description: "Fetch heart rate samples from Oura API. Pick a subcommand: today | date <day> | week.",
|
|
1240
|
-
args: [],
|
|
1241
|
-
outputSchema: "docs/schemas/hr.json",
|
|
1242
|
-
subcommands: [
|
|
1243
|
-
{ name: "today", description: "Today's heart rate data.", args: [] },
|
|
1244
|
-
{ name: "date", description: "Heart rate data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
|
|
1245
|
-
{ name: "week", description: "Last 7 days of heart rate data.", args: [] }
|
|
1246
|
-
]
|
|
1247
|
-
},
|
|
1248
|
-
{
|
|
1249
|
-
name: "spo2",
|
|
1250
|
-
description: "Fetch blood oxygen (SpO2) data from Oura API. Pick a subcommand: today | date <day> | week.",
|
|
1251
|
-
args: [],
|
|
1252
|
-
outputSchema: "docs/schemas/spo2.json",
|
|
1253
|
-
subcommands: [
|
|
1254
|
-
{ name: "today", description: "Today's SpO2 data.", args: [] },
|
|
1255
|
-
{ name: "date", description: "SpO2 data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
|
|
1256
|
-
{ name: "week", description: "Last 7 days of SpO2 data.", args: [] }
|
|
1257
|
-
]
|
|
1258
|
-
},
|
|
1259
|
-
{
|
|
1260
|
-
name: "stress",
|
|
1261
|
-
description: "Fetch daily stress data from Oura API. Pick a subcommand: today | date <day> | week.",
|
|
1262
|
-
args: [],
|
|
1263
|
-
outputSchema: "docs/schemas/stress.json",
|
|
1264
|
-
subcommands: [
|
|
1265
|
-
{ name: "today", description: "Today's stress data.", args: [] },
|
|
1266
|
-
{ name: "date", description: "Stress data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
|
|
1267
|
-
{ name: "week", description: "Last 7 days of stress data.", args: [] }
|
|
1268
|
-
]
|
|
1269
|
-
},
|
|
1270
|
-
{
|
|
1271
|
-
name: "workout",
|
|
1272
|
-
description: "Fetch workout data from Oura API. Pick a subcommand: today | date <day> | week.",
|
|
1273
|
-
args: [],
|
|
1274
|
-
outputSchema: "docs/schemas/workout.json",
|
|
1275
|
-
subcommands: [
|
|
1276
|
-
{ name: "today", description: "Today's workout data.", args: [] },
|
|
1277
|
-
{ name: "date", description: "Workout data for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
|
|
1278
|
-
{ name: "week", description: "Last 7 days of workout data.", args: [] }
|
|
1279
|
-
]
|
|
1280
|
-
},
|
|
1281
|
-
{ name: "sync", description: "Sync all Oura collections into the local database.", args: [] },
|
|
1282
|
-
{
|
|
1283
|
-
name: "db",
|
|
1284
|
-
description: "Query and manage the local SQLite cache. Pick a subcommand.",
|
|
1285
|
-
args: [],
|
|
1286
|
-
subcommands: [
|
|
1287
|
-
{ name: "today", description: "Today's summary from local DB.", args: [] },
|
|
1288
|
-
{ name: "date", description: "Summary for a specific date.", args: [{ name: "<day>", type: "date", format: "YYYY-MM-DD", required: true, description: "Target date." }] },
|
|
1289
|
-
{ name: "week", description: "Last 7 days from local DB.", args: [] },
|
|
1290
|
-
{ name: "trends", description: "Score and metric trends over N days (default 30).", args: [{ name: "[days]", type: "number", required: false, description: "Window size in days." }] },
|
|
1291
|
-
{ name: "stats", description: "Row counts, date range, record highs.", args: [] },
|
|
1292
|
-
{ name: "import", description: "Sync new data from Oura API into the local DB.", args: [] },
|
|
1293
|
-
{ name: "reset", description: "Destroy and rebuild the database from exported CSVs.", args: [] }
|
|
1294
|
-
]
|
|
1295
|
-
},
|
|
1296
|
-
{
|
|
1297
|
-
name: "report",
|
|
1298
|
-
description: "Generate a narrative health report from local data.",
|
|
1299
|
-
args: [
|
|
1300
|
-
{ name: "--period", type: "enum", values: ["week", "month"], description: "Report window (default week)." }
|
|
1301
|
-
]
|
|
1302
|
-
}
|
|
1303
|
-
]
|
|
1697
|
+
commands: Object.entries(commands).map(([name, def]) => describeCommandDef(name, resolved(def)))
|
|
1304
1698
|
};
|
|
1305
1699
|
}
|
|
1306
|
-
function describeCommand(version) {
|
|
1700
|
+
function describeCommand(version, getCommands) {
|
|
1307
1701
|
return defineCommand({
|
|
1308
1702
|
meta: { name: "describe", description: "Emit a machine-readable manifest of commands, args, and outputs." },
|
|
1309
1703
|
args: {},
|
|
1310
1704
|
run() {
|
|
1311
|
-
console.log(JSON.stringify(buildManifest(version), null, 2));
|
|
1705
|
+
console.log(JSON.stringify(buildManifest(version, getCommands()), null, 2));
|
|
1312
1706
|
}
|
|
1313
1707
|
});
|
|
1314
1708
|
}
|
|
1315
1709
|
|
|
1316
|
-
// src/
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
return options.dbPath;
|
|
1328
|
-
if (options.envVar && process.env[options.envVar])
|
|
1329
|
-
return process.env[options.envVar];
|
|
1330
|
-
const dir = options.defaultDir ?? ".oura-cli";
|
|
1331
|
-
const file = options.defaultFile ?? "oura.db";
|
|
1332
|
-
return resolve2(homedir2(), dir, file);
|
|
1333
|
-
}
|
|
1334
|
-
function openDatabase(options = {}) {
|
|
1335
|
-
const dbPath = getDbPath(options);
|
|
1336
|
-
if (dbPath !== ":memory:") {
|
|
1337
|
-
mkdirSync2(resolve2(dbPath, ".."), { recursive: true });
|
|
1710
|
+
// src/db/sync.ts
|
|
1711
|
+
var BACKFILL_DAYS = 30;
|
|
1712
|
+
var FRESHNESS_TABLES = ["daily_sleep", "daily_readiness", "daily_activity"];
|
|
1713
|
+
async function importDaily(db, client, clock, log) {
|
|
1714
|
+
const { today, tz } = clock;
|
|
1715
|
+
const _log = log ?? (() => {});
|
|
1716
|
+
const lastDates = [];
|
|
1717
|
+
for (const tbl of FRESHNESS_TABLES) {
|
|
1718
|
+
const row = db.query(`SELECT MAX(day) as d FROM ${tbl}`).get();
|
|
1719
|
+
if (row?.d)
|
|
1720
|
+
lastDates.push(row.d);
|
|
1338
1721
|
}
|
|
1339
|
-
const
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
db.exec(m.sql);
|
|
1354
|
-
db.query("INSERT INTO _schema_version (version) VALUES (?)").run(m.version);
|
|
1355
|
-
}
|
|
1722
|
+
const isFirstSync = lastDates.length === 0;
|
|
1723
|
+
const startDate = isFirstSync ? shiftDay(today, -BACKFILL_DAYS) : lastDates.sort()[0];
|
|
1724
|
+
_log(isFirstSync ? `First sync \u2014 backfilling the last ${BACKFILL_DAYS} days: ${startDate} \u2192 ${today}` : `Syncing ${startDate} \u2192 ${today}`);
|
|
1725
|
+
const counts = {};
|
|
1726
|
+
for (const c of COLLECTIONS) {
|
|
1727
|
+
const rows = await fetchCollection(client, c, startDate, today, tz);
|
|
1728
|
+
const stmt = db.query(insertSql(c));
|
|
1729
|
+
db.transaction((rs) => {
|
|
1730
|
+
for (const r of rs)
|
|
1731
|
+
stmt.run(...rowValues(c, r));
|
|
1732
|
+
})(rows);
|
|
1733
|
+
counts[c.table] = rows.length;
|
|
1734
|
+
if (rows.length > 0)
|
|
1735
|
+
_log(` + ${c.table}: ${rows.length} rows`);
|
|
1356
1736
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
// src/db/schema.ts
|
|
1360
|
-
var MIGRATIONS = [
|
|
1361
|
-
{
|
|
1362
|
-
version: 1,
|
|
1363
|
-
sql: `
|
|
1364
|
-
CREATE TABLE IF NOT EXISTS daily_sleep (
|
|
1365
|
-
id TEXT PRIMARY KEY,
|
|
1366
|
-
day TEXT UNIQUE,
|
|
1367
|
-
score INTEGER,
|
|
1368
|
-
contributors TEXT,
|
|
1369
|
-
timestamp TEXT
|
|
1370
|
-
);
|
|
1371
|
-
CREATE TABLE IF NOT EXISTS daily_readiness (
|
|
1372
|
-
id TEXT PRIMARY KEY,
|
|
1373
|
-
day TEXT UNIQUE,
|
|
1374
|
-
score INTEGER,
|
|
1375
|
-
contributors TEXT,
|
|
1376
|
-
temperature_deviation REAL,
|
|
1377
|
-
temperature_trend_deviation REAL,
|
|
1378
|
-
timestamp TEXT
|
|
1379
|
-
);
|
|
1380
|
-
CREATE TABLE IF NOT EXISTS daily_activity (
|
|
1381
|
-
id TEXT PRIMARY KEY,
|
|
1382
|
-
day TEXT UNIQUE,
|
|
1383
|
-
score INTEGER,
|
|
1384
|
-
active_calories INTEGER,
|
|
1385
|
-
steps INTEGER,
|
|
1386
|
-
equivalent_walking_distance REAL,
|
|
1387
|
-
high_activity_time INTEGER,
|
|
1388
|
-
medium_activity_time INTEGER,
|
|
1389
|
-
low_activity_time INTEGER,
|
|
1390
|
-
sedentary_time INTEGER,
|
|
1391
|
-
total_calories INTEGER,
|
|
1392
|
-
target_calories INTEGER,
|
|
1393
|
-
contributors TEXT,
|
|
1394
|
-
timestamp TEXT
|
|
1395
|
-
);
|
|
1396
|
-
CREATE TABLE IF NOT EXISTS daily_spo2 (
|
|
1397
|
-
id TEXT PRIMARY KEY,
|
|
1398
|
-
day TEXT UNIQUE,
|
|
1399
|
-
spo2_average REAL,
|
|
1400
|
-
breathing_disturbance_index REAL
|
|
1401
|
-
);
|
|
1402
|
-
CREATE TABLE IF NOT EXISTS daily_stress (
|
|
1403
|
-
id TEXT PRIMARY KEY,
|
|
1404
|
-
day TEXT UNIQUE,
|
|
1405
|
-
day_summary TEXT,
|
|
1406
|
-
recovery_high INTEGER,
|
|
1407
|
-
stress_high INTEGER
|
|
1408
|
-
);
|
|
1409
|
-
CREATE TABLE IF NOT EXISTS heartrate (
|
|
1410
|
-
timestamp TEXT,
|
|
1411
|
-
bpm INTEGER,
|
|
1412
|
-
source TEXT,
|
|
1413
|
-
day TEXT
|
|
1414
|
-
);
|
|
1415
|
-
CREATE INDEX IF NOT EXISTS idx_heartrate_ts ON heartrate(timestamp);
|
|
1416
|
-
CREATE UNIQUE INDEX IF NOT EXISTS idx_heartrate_unique ON heartrate(timestamp, source);
|
|
1417
|
-
CREATE INDEX IF NOT EXISTS idx_heartrate_day ON heartrate(day);
|
|
1418
|
-
CREATE TABLE IF NOT EXISTS vo2max (
|
|
1419
|
-
id TEXT PRIMARY KEY,
|
|
1420
|
-
day TEXT UNIQUE,
|
|
1421
|
-
vo2_max REAL,
|
|
1422
|
-
timestamp TEXT
|
|
1423
|
-
);
|
|
1424
|
-
CREATE TABLE IF NOT EXISTS cardiovascular_age (
|
|
1425
|
-
id TEXT PRIMARY KEY,
|
|
1426
|
-
day TEXT UNIQUE,
|
|
1427
|
-
vascular_age INTEGER
|
|
1428
|
-
);
|
|
1429
|
-
CREATE TABLE IF NOT EXISTS workouts (
|
|
1430
|
-
id TEXT PRIMARY KEY,
|
|
1431
|
-
day TEXT,
|
|
1432
|
-
activity TEXT,
|
|
1433
|
-
calories REAL,
|
|
1434
|
-
distance REAL,
|
|
1435
|
-
start_datetime TEXT,
|
|
1436
|
-
end_datetime TEXT,
|
|
1437
|
-
intensity TEXT,
|
|
1438
|
-
label TEXT,
|
|
1439
|
-
source TEXT
|
|
1440
|
-
);
|
|
1441
|
-
CREATE TABLE IF NOT EXISTS sleep_model (
|
|
1442
|
-
id TEXT PRIMARY KEY,
|
|
1443
|
-
day TEXT,
|
|
1444
|
-
average_breath REAL,
|
|
1445
|
-
average_heart_rate REAL,
|
|
1446
|
-
average_hrv REAL,
|
|
1447
|
-
awake_time INTEGER,
|
|
1448
|
-
bedtime_end TEXT,
|
|
1449
|
-
bedtime_start TEXT,
|
|
1450
|
-
deep_sleep_duration INTEGER,
|
|
1451
|
-
efficiency INTEGER,
|
|
1452
|
-
latency INTEGER,
|
|
1453
|
-
light_sleep_duration INTEGER,
|
|
1454
|
-
lowest_heart_rate INTEGER,
|
|
1455
|
-
period INTEGER,
|
|
1456
|
-
rem_sleep_duration INTEGER,
|
|
1457
|
-
restless_periods INTEGER,
|
|
1458
|
-
time_in_bed INTEGER,
|
|
1459
|
-
total_sleep_duration INTEGER,
|
|
1460
|
-
type TEXT
|
|
1461
|
-
);
|
|
1462
|
-
`
|
|
1463
|
-
},
|
|
1464
|
-
{
|
|
1465
|
-
version: 2,
|
|
1466
|
-
sql: `
|
|
1467
|
-
CREATE VIEW IF NOT EXISTS v_weekly_sleep AS
|
|
1468
|
-
SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score, COUNT(*) as days
|
|
1469
|
-
FROM daily_sleep WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
|
|
1470
|
-
|
|
1471
|
-
CREATE VIEW IF NOT EXISTS v_weekly_readiness AS
|
|
1472
|
-
SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score,
|
|
1473
|
-
ROUND(AVG(temperature_deviation),2) as avg_temp_dev, COUNT(*) as days
|
|
1474
|
-
FROM daily_readiness WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
|
|
1475
|
-
|
|
1476
|
-
CREATE VIEW IF NOT EXISTS v_weekly_activity AS
|
|
1477
|
-
SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score,
|
|
1478
|
-
SUM(steps) as total_steps, SUM(active_calories) as total_active_cal, COUNT(*) as days
|
|
1479
|
-
FROM daily_activity WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
|
|
1480
|
-
|
|
1481
|
-
CREATE VIEW IF NOT EXISTS v_sleep_detail AS
|
|
1482
|
-
SELECT day, ROUND(total_sleep_duration/3600.0,1) as sleep_hours,
|
|
1483
|
-
ROUND(deep_sleep_duration/3600.0,1) as deep_hours,
|
|
1484
|
-
ROUND(rem_sleep_duration/3600.0,1) as rem_hours,
|
|
1485
|
-
average_hrv, average_heart_rate as avg_hr, lowest_heart_rate as lowest_hr, efficiency
|
|
1486
|
-
FROM sleep_model ORDER BY day DESC;
|
|
1487
|
-
`
|
|
1488
|
-
}
|
|
1489
|
-
];
|
|
1490
|
-
|
|
1491
|
-
// src/db/database.ts
|
|
1492
|
-
var DB_OPTIONS = {
|
|
1493
|
-
envVar: "OURA_DB_PATH",
|
|
1494
|
-
defaultDir: ".oura-cli",
|
|
1495
|
-
defaultFile: "oura.db"
|
|
1496
|
-
};
|
|
1497
|
-
function getDbPath2(options = {}) {
|
|
1498
|
-
return getDbPath({ ...DB_OPTIONS, ...options });
|
|
1499
|
-
}
|
|
1500
|
-
function openDatabase2(options = {}) {
|
|
1501
|
-
return openDatabase({ ...DB_OPTIONS, ...options });
|
|
1502
|
-
}
|
|
1503
|
-
function ensureSchema2(db) {
|
|
1504
|
-
ensureSchema(db, MIGRATIONS);
|
|
1505
|
-
}
|
|
1506
|
-
|
|
1507
|
-
// src/db/import.ts
|
|
1508
|
-
async function importDaily(db, client, log) {
|
|
1509
|
-
const _log = log ?? (() => {});
|
|
1510
|
-
const today = new Date().toISOString().slice(0, 10);
|
|
1511
|
-
const lastDates = [];
|
|
1512
|
-
for (const tbl of ["daily_sleep", "daily_readiness", "daily_activity"]) {
|
|
1513
|
-
const row = db.query(`SELECT MAX(day) as d FROM ${tbl}`).get();
|
|
1514
|
-
if (row?.d)
|
|
1515
|
-
lastDates.push(row.d);
|
|
1516
|
-
}
|
|
1517
|
-
const startDate = lastDates.length > 0 ? lastDates.sort()[0] : new Date(Date.now() - 30 * 86400000).toISOString().slice(0, 10);
|
|
1518
|
-
_log(`Syncing from ${startDate} to ${today}`);
|
|
1519
|
-
const counts = {};
|
|
1520
|
-
const sleepData = await client.fetch("daily_sleep", startDate, today);
|
|
1521
|
-
const insertSleep = db.query("INSERT OR REPLACE INTO daily_sleep VALUES (?,?,?,?,?)");
|
|
1522
|
-
for (const s of sleepData) {
|
|
1523
|
-
insertSleep.run(s.id, s.day, s.score, JSON.stringify(s.contributors), s.timestamp);
|
|
1524
|
-
_log(` + sleep ${s.day}`);
|
|
1525
|
-
}
|
|
1526
|
-
counts.daily_sleep = sleepData.length;
|
|
1527
|
-
const readinessData = await client.fetch("daily_readiness", startDate, today);
|
|
1528
|
-
const insertReadiness = db.query("INSERT OR REPLACE INTO daily_readiness VALUES (?,?,?,?,?,?,?)");
|
|
1529
|
-
for (const r of readinessData) {
|
|
1530
|
-
insertReadiness.run(r.id, r.day, r.score, JSON.stringify(r.contributors), r.temperature_deviation, r.temperature_trend_deviation, r.timestamp);
|
|
1531
|
-
_log(` + readiness ${r.day}`);
|
|
1532
|
-
}
|
|
1533
|
-
counts.daily_readiness = readinessData.length;
|
|
1534
|
-
const activityData = await client.fetch("daily_activity", startDate, today);
|
|
1535
|
-
const insertActivity = db.query("INSERT OR REPLACE INTO daily_activity VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
|
1536
|
-
for (const a of activityData) {
|
|
1537
|
-
insertActivity.run(a.id, a.day, a.score, a.active_calories, a.steps, a.equivalent_walking_distance, a.high_activity_time, a.medium_activity_time, a.low_activity_time, a.sedentary_time, a.total_calories, a.target_calories, JSON.stringify(a.contributors), a.timestamp);
|
|
1538
|
-
_log(` + activity ${a.day}`);
|
|
1539
|
-
}
|
|
1540
|
-
counts.daily_activity = activityData.length;
|
|
1541
|
-
const hrData = await client.fetch("heartrate", today, today);
|
|
1542
|
-
const insertHr = db.query("INSERT OR IGNORE INTO heartrate VALUES (?,?,?,?)");
|
|
1543
|
-
for (const h of hrData) {
|
|
1544
|
-
const day = h.timestamp.slice(0, 10);
|
|
1545
|
-
insertHr.run(h.timestamp, h.bpm, h.source, day);
|
|
1546
|
-
}
|
|
1547
|
-
counts.heartrate = hrData.length;
|
|
1548
|
-
if (hrData.length > 0)
|
|
1549
|
-
_log(` + heartrate ${hrData.length} records`);
|
|
1550
|
-
const spo2Data = await client.fetch("daily_spo2", startDate, today);
|
|
1551
|
-
const insertSpo2 = db.query("INSERT OR REPLACE INTO daily_spo2 VALUES (?,?,?,?)");
|
|
1552
|
-
for (const s of spo2Data) {
|
|
1553
|
-
const avg = s.spo2_percentage?.average ?? null;
|
|
1554
|
-
insertSpo2.run(s.id, s.day, avg, s.breathing_disturbance_index);
|
|
1555
|
-
_log(` + spo2 ${s.day}`);
|
|
1556
|
-
}
|
|
1557
|
-
counts.daily_spo2 = spo2Data.length;
|
|
1558
|
-
const stressData = await client.fetch("daily_stress", startDate, today);
|
|
1559
|
-
const insertStress = db.query("INSERT OR REPLACE INTO daily_stress VALUES (?,?,?,?,?)");
|
|
1560
|
-
for (const s of stressData) {
|
|
1561
|
-
insertStress.run(s.id, s.day, s.day_summary, s.recovery_high, s.stress_high);
|
|
1562
|
-
_log(` + stress ${s.day}`);
|
|
1563
|
-
}
|
|
1564
|
-
counts.daily_stress = stressData.length;
|
|
1565
|
-
const workoutData = await client.fetch("workout", startDate, today);
|
|
1566
|
-
const insertWorkout = db.query("INSERT OR REPLACE INTO workouts VALUES (?,?,?,?,?,?,?,?,?,?)");
|
|
1567
|
-
for (const w of workoutData) {
|
|
1568
|
-
insertWorkout.run(w.id, w.day, w.activity, w.calories, w.distance, w.start_datetime, w.end_datetime, w.intensity, w.label ?? "", w.source);
|
|
1569
|
-
_log(` + workout ${w.day} ${w.activity}`);
|
|
1570
|
-
}
|
|
1571
|
-
counts.workouts = workoutData.length;
|
|
1572
|
-
const sleepPeriods = await client.fetch("sleep", startDate, today);
|
|
1573
|
-
const insertSleepModel = db.query(`INSERT OR REPLACE INTO sleep_model VALUES (${Array(19).fill("?").join(",")})`);
|
|
1574
|
-
for (const sp of sleepPeriods) {
|
|
1575
|
-
insertSleepModel.run(sp.id, sp.day, sp.average_breath, sp.average_heart_rate, sp.average_hrv, sp.awake_time, sp.bedtime_end, sp.bedtime_start, sp.deep_sleep_duration, sp.efficiency, sp.latency, sp.light_sleep_duration, sp.lowest_heart_rate, sp.period, sp.rem_sleep_duration, sp.restless_periods, sp.time_in_bed, sp.total_sleep_duration, sp.type);
|
|
1576
|
-
_log(` + sleep_period ${sp.day} (${sp.type})`);
|
|
1577
|
-
}
|
|
1578
|
-
counts.sleep_model = sleepPeriods.length;
|
|
1579
|
-
const cvData = await client.fetch("daily_cardiovascular_age", startDate, today);
|
|
1580
|
-
const insertCv = db.query("INSERT OR REPLACE INTO cardiovascular_age VALUES (?,?,?)");
|
|
1581
|
-
for (const c of cvData) {
|
|
1582
|
-
insertCv.run(c.id, c.day, c.vascular_age);
|
|
1583
|
-
_log(` + cardiovascular_age ${c.day}`);
|
|
1584
|
-
}
|
|
1585
|
-
counts.cardiovascular_age = cvData.length;
|
|
1586
|
-
_log("Import complete.");
|
|
1587
|
-
return { startDate, endDate: today, counts };
|
|
1737
|
+
_log("Import complete.");
|
|
1738
|
+
return { startDate, endDate: today, counts, isFirstSync };
|
|
1588
1739
|
}
|
|
1589
1740
|
|
|
1590
1741
|
// src/db/queries.ts
|
|
@@ -1612,9 +1763,8 @@ function getDaySummary(db, day) {
|
|
|
1612
1763
|
efficiency: sm?.efficiency ?? null
|
|
1613
1764
|
};
|
|
1614
1765
|
}
|
|
1615
|
-
function getTrends(db, days) {
|
|
1616
|
-
const
|
|
1617
|
-
const start = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10);
|
|
1766
|
+
function getTrends(db, days, today) {
|
|
1767
|
+
const start = shiftDay(today, -days);
|
|
1618
1768
|
const results = [];
|
|
1619
1769
|
const metrics = [
|
|
1620
1770
|
["Sleep Score", "daily_sleep", "score"],
|
|
@@ -1635,25 +1785,13 @@ function getTrends(db, days) {
|
|
|
1635
1785
|
}
|
|
1636
1786
|
return results;
|
|
1637
1787
|
}
|
|
1638
|
-
function getStats(db) {
|
|
1639
|
-
const
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
"daily_activity",
|
|
1643
|
-
"daily_spo2",
|
|
1644
|
-
"daily_stress",
|
|
1645
|
-
"heartrate",
|
|
1646
|
-
"vo2max",
|
|
1647
|
-
"cardiovascular_age",
|
|
1648
|
-
"workouts",
|
|
1649
|
-
"sleep_model"
|
|
1650
|
-
];
|
|
1651
|
-
const tables = tableNames.map((table) => {
|
|
1652
|
-
const row = db.query(`SELECT COUNT(*) as cnt FROM ${table}`).get();
|
|
1653
|
-
return { table, rows: row.cnt };
|
|
1788
|
+
function getStats(db, today) {
|
|
1789
|
+
const tables = COLLECTIONS.map((c) => {
|
|
1790
|
+
const row = db.query(`SELECT COUNT(*) as cnt FROM ${c.table}`).get();
|
|
1791
|
+
return { table: c.table, rows: row.cnt };
|
|
1654
1792
|
});
|
|
1655
1793
|
const range = db.query("SELECT MIN(day) as first, MAX(day) as last FROM daily_sleep").get();
|
|
1656
|
-
const trends = getTrends(db, 99999);
|
|
1794
|
+
const trends = getTrends(db, 99999, today);
|
|
1657
1795
|
const mostSteps = db.query("SELECT day, steps FROM daily_activity WHERE steps IS NOT NULL ORDER BY steps DESC LIMIT 1").get();
|
|
1658
1796
|
const bestSleep = db.query("SELECT day, score FROM daily_sleep WHERE score IS NOT NULL ORDER BY score DESC LIMIT 1").get();
|
|
1659
1797
|
return {
|
|
@@ -1667,7 +1805,7 @@ function getStats(db) {
|
|
|
1667
1805
|
};
|
|
1668
1806
|
}
|
|
1669
1807
|
|
|
1670
|
-
// src/format.ts
|
|
1808
|
+
// src/render/format.ts
|
|
1671
1809
|
init_source();
|
|
1672
1810
|
function scoreColor(score) {
|
|
1673
1811
|
if (score === null)
|
|
@@ -1683,9 +1821,22 @@ function fmtHours(h) {
|
|
|
1683
1821
|
return source_default.gray("\u2014");
|
|
1684
1822
|
return `${h}h`;
|
|
1685
1823
|
}
|
|
1686
|
-
function
|
|
1824
|
+
function isEmptyDay(s) {
|
|
1825
|
+
return s.sleep_score === null && s.readiness_score === null && s.activity_score === null && s.steps === null && s.stress === null && s.spo2 === null && s.temp_deviation === null && s.sleep_hours === null && s.deep_hours === null && s.rem_hours === null && s.avg_hrv === null && s.lowest_hr === null && s.efficiency === null;
|
|
1826
|
+
}
|
|
1827
|
+
function formatDaySummary(summary, format, emptyHint) {
|
|
1687
1828
|
if (format === "json")
|
|
1688
1829
|
return JSON.stringify(summary, null, 2);
|
|
1830
|
+
if (emptyHint && isEmptyDay(summary)) {
|
|
1831
|
+
return [
|
|
1832
|
+
"",
|
|
1833
|
+
source_default.bold(` ${summary.day}`),
|
|
1834
|
+
source_default.gray("\u2500".repeat(50)),
|
|
1835
|
+
` No Oura data for ${summary.day} yet.`,
|
|
1836
|
+
` ${emptyHint}`
|
|
1837
|
+
].join(`
|
|
1838
|
+
`);
|
|
1839
|
+
}
|
|
1689
1840
|
const lines = [
|
|
1690
1841
|
"",
|
|
1691
1842
|
source_default.bold(` ${summary.day}`),
|
|
@@ -1709,9 +1860,26 @@ function formatDaySummary(summary, format) {
|
|
|
1709
1860
|
return lines.join(`
|
|
1710
1861
|
`);
|
|
1711
1862
|
}
|
|
1712
|
-
function
|
|
1863
|
+
function formatImportSummary(result) {
|
|
1864
|
+
const c = result.counts;
|
|
1865
|
+
return [
|
|
1866
|
+
` Imported ${result.startDate} \u2192 ${result.endDate}:`,
|
|
1867
|
+
` sleep ${c.daily_sleep ?? 0} readiness ${c.daily_readiness ?? 0} activity ${c.daily_activity ?? 0} sleep periods ${c.sleep_model ?? 0}`,
|
|
1868
|
+
` spo2 ${c.daily_spo2 ?? 0} stress ${c.daily_stress ?? 0} workouts ${c.workouts ?? 0} heart rate ${c.heartrate ?? 0} cardiovascular age ${c.cardiovascular_age ?? 0}`
|
|
1869
|
+
].join(`
|
|
1870
|
+
`);
|
|
1871
|
+
}
|
|
1872
|
+
function formatWeekTable(days, format, emptyHint) {
|
|
1713
1873
|
if (format === "json")
|
|
1714
1874
|
return JSON.stringify(days, null, 2);
|
|
1875
|
+
if (emptyHint && days.length > 0 && days.every(isEmptyDay)) {
|
|
1876
|
+
return [
|
|
1877
|
+
"",
|
|
1878
|
+
" No Oura data for the last 7 days yet.",
|
|
1879
|
+
` ${emptyHint}`
|
|
1880
|
+
].join(`
|
|
1881
|
+
`);
|
|
1882
|
+
}
|
|
1715
1883
|
const header = `${"Day".padEnd(12)} ${"Sleep".padStart(6)} ${"Ready".padStart(6)} ${"Activity".padStart(9)} ${"Steps".padStart(7)} ${"Stress".padEnd(10)}`;
|
|
1716
1884
|
const sep = source_default.gray("\u2500".repeat(56));
|
|
1717
1885
|
const rows = days.map((d) => `${d.day.padEnd(12)} ${scoreColor(d.sleep_score).padStart(6)} ${scoreColor(d.readiness_score).padStart(6)} ` + `${scoreColor(d.activity_score).padStart(9)} ${String(d.steps ?? "\u2014").padStart(7)} ${(d.stress ?? "\u2014").padEnd(10)}`);
|
|
@@ -1762,32 +1930,236 @@ function formatStats(stats, format) {
|
|
|
1762
1930
|
`);
|
|
1763
1931
|
}
|
|
1764
1932
|
|
|
1765
|
-
// src/
|
|
1933
|
+
// src/commands/run-command.ts
|
|
1934
|
+
init_source();
|
|
1935
|
+
|
|
1936
|
+
// src/db/open.ts
|
|
1937
|
+
import { Database } from "bun:sqlite";
|
|
1938
|
+
import { resolve as resolve2, dirname as dirname2 } from "path";
|
|
1939
|
+
import { homedir as homedir2 } from "os";
|
|
1940
|
+
import { mkdirSync as mkdirSync2 } from "fs";
|
|
1941
|
+
|
|
1942
|
+
// src/db/migrations.ts
|
|
1943
|
+
var MIGRATIONS = [
|
|
1944
|
+
{
|
|
1945
|
+
version: 1,
|
|
1946
|
+
sql: `
|
|
1947
|
+
CREATE TABLE IF NOT EXISTS daily_sleep (
|
|
1948
|
+
id TEXT PRIMARY KEY,
|
|
1949
|
+
day TEXT UNIQUE,
|
|
1950
|
+
score INTEGER,
|
|
1951
|
+
contributors TEXT,
|
|
1952
|
+
timestamp TEXT
|
|
1953
|
+
);
|
|
1954
|
+
CREATE TABLE IF NOT EXISTS daily_readiness (
|
|
1955
|
+
id TEXT PRIMARY KEY,
|
|
1956
|
+
day TEXT UNIQUE,
|
|
1957
|
+
score INTEGER,
|
|
1958
|
+
contributors TEXT,
|
|
1959
|
+
temperature_deviation REAL,
|
|
1960
|
+
temperature_trend_deviation REAL,
|
|
1961
|
+
timestamp TEXT
|
|
1962
|
+
);
|
|
1963
|
+
CREATE TABLE IF NOT EXISTS daily_activity (
|
|
1964
|
+
id TEXT PRIMARY KEY,
|
|
1965
|
+
day TEXT UNIQUE,
|
|
1966
|
+
score INTEGER,
|
|
1967
|
+
active_calories INTEGER,
|
|
1968
|
+
steps INTEGER,
|
|
1969
|
+
equivalent_walking_distance REAL,
|
|
1970
|
+
high_activity_time INTEGER,
|
|
1971
|
+
medium_activity_time INTEGER,
|
|
1972
|
+
low_activity_time INTEGER,
|
|
1973
|
+
sedentary_time INTEGER,
|
|
1974
|
+
total_calories INTEGER,
|
|
1975
|
+
target_calories INTEGER,
|
|
1976
|
+
contributors TEXT,
|
|
1977
|
+
timestamp TEXT
|
|
1978
|
+
);
|
|
1979
|
+
CREATE TABLE IF NOT EXISTS daily_spo2 (
|
|
1980
|
+
id TEXT PRIMARY KEY,
|
|
1981
|
+
day TEXT UNIQUE,
|
|
1982
|
+
spo2_average REAL,
|
|
1983
|
+
breathing_disturbance_index REAL
|
|
1984
|
+
);
|
|
1985
|
+
CREATE TABLE IF NOT EXISTS daily_stress (
|
|
1986
|
+
id TEXT PRIMARY KEY,
|
|
1987
|
+
day TEXT UNIQUE,
|
|
1988
|
+
day_summary TEXT,
|
|
1989
|
+
recovery_high INTEGER,
|
|
1990
|
+
stress_high INTEGER
|
|
1991
|
+
);
|
|
1992
|
+
CREATE TABLE IF NOT EXISTS heartrate (
|
|
1993
|
+
timestamp TEXT,
|
|
1994
|
+
bpm INTEGER,
|
|
1995
|
+
source TEXT,
|
|
1996
|
+
day TEXT
|
|
1997
|
+
);
|
|
1998
|
+
CREATE INDEX IF NOT EXISTS idx_heartrate_ts ON heartrate(timestamp);
|
|
1999
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_heartrate_unique ON heartrate(timestamp, source);
|
|
2000
|
+
CREATE INDEX IF NOT EXISTS idx_heartrate_day ON heartrate(day);
|
|
2001
|
+
CREATE TABLE IF NOT EXISTS vo2max (
|
|
2002
|
+
id TEXT PRIMARY KEY,
|
|
2003
|
+
day TEXT UNIQUE,
|
|
2004
|
+
vo2_max REAL,
|
|
2005
|
+
timestamp TEXT
|
|
2006
|
+
);
|
|
2007
|
+
CREATE TABLE IF NOT EXISTS cardiovascular_age (
|
|
2008
|
+
id TEXT PRIMARY KEY,
|
|
2009
|
+
day TEXT UNIQUE,
|
|
2010
|
+
vascular_age INTEGER
|
|
2011
|
+
);
|
|
2012
|
+
CREATE TABLE IF NOT EXISTS workouts (
|
|
2013
|
+
id TEXT PRIMARY KEY,
|
|
2014
|
+
day TEXT,
|
|
2015
|
+
activity TEXT,
|
|
2016
|
+
calories REAL,
|
|
2017
|
+
distance REAL,
|
|
2018
|
+
start_datetime TEXT,
|
|
2019
|
+
end_datetime TEXT,
|
|
2020
|
+
intensity TEXT,
|
|
2021
|
+
label TEXT,
|
|
2022
|
+
source TEXT
|
|
2023
|
+
);
|
|
2024
|
+
CREATE TABLE IF NOT EXISTS sleep_model (
|
|
2025
|
+
id TEXT PRIMARY KEY,
|
|
2026
|
+
day TEXT,
|
|
2027
|
+
average_breath REAL,
|
|
2028
|
+
average_heart_rate REAL,
|
|
2029
|
+
average_hrv REAL,
|
|
2030
|
+
awake_time INTEGER,
|
|
2031
|
+
bedtime_end TEXT,
|
|
2032
|
+
bedtime_start TEXT,
|
|
2033
|
+
deep_sleep_duration INTEGER,
|
|
2034
|
+
efficiency INTEGER,
|
|
2035
|
+
latency INTEGER,
|
|
2036
|
+
light_sleep_duration INTEGER,
|
|
2037
|
+
lowest_heart_rate INTEGER,
|
|
2038
|
+
period INTEGER,
|
|
2039
|
+
rem_sleep_duration INTEGER,
|
|
2040
|
+
restless_periods INTEGER,
|
|
2041
|
+
time_in_bed INTEGER,
|
|
2042
|
+
total_sleep_duration INTEGER,
|
|
2043
|
+
type TEXT
|
|
2044
|
+
);
|
|
2045
|
+
`
|
|
2046
|
+
},
|
|
2047
|
+
{
|
|
2048
|
+
version: 2,
|
|
2049
|
+
sql: `
|
|
2050
|
+
CREATE VIEW IF NOT EXISTS v_weekly_sleep AS
|
|
2051
|
+
SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score, COUNT(*) as days
|
|
2052
|
+
FROM daily_sleep WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
|
|
2053
|
+
|
|
2054
|
+
CREATE VIEW IF NOT EXISTS v_weekly_readiness AS
|
|
2055
|
+
SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score,
|
|
2056
|
+
ROUND(AVG(temperature_deviation),2) as avg_temp_dev, COUNT(*) as days
|
|
2057
|
+
FROM daily_readiness WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
|
|
2058
|
+
|
|
2059
|
+
CREATE VIEW IF NOT EXISTS v_weekly_activity AS
|
|
2060
|
+
SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score,
|
|
2061
|
+
SUM(steps) as total_steps, SUM(active_calories) as total_active_cal, COUNT(*) as days
|
|
2062
|
+
FROM daily_activity WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
|
|
2063
|
+
|
|
2064
|
+
CREATE VIEW IF NOT EXISTS v_sleep_detail AS
|
|
2065
|
+
SELECT day, ROUND(total_sleep_duration/3600.0,1) as sleep_hours,
|
|
2066
|
+
ROUND(deep_sleep_duration/3600.0,1) as deep_hours,
|
|
2067
|
+
ROUND(rem_sleep_duration/3600.0,1) as rem_hours,
|
|
2068
|
+
average_hrv, average_heart_rate as avg_hr, lowest_heart_rate as lowest_hr, efficiency
|
|
2069
|
+
FROM sleep_model ORDER BY day DESC;
|
|
2070
|
+
`
|
|
2071
|
+
}
|
|
2072
|
+
];
|
|
2073
|
+
|
|
2074
|
+
// src/db/open.ts
|
|
2075
|
+
function getDbPath(explicit) {
|
|
2076
|
+
if (explicit)
|
|
2077
|
+
return explicit;
|
|
2078
|
+
if (process.env.OURA_DB_PATH)
|
|
2079
|
+
return process.env.OURA_DB_PATH;
|
|
2080
|
+
return resolve2(homedir2(), ".oura-cli", "oura.db");
|
|
2081
|
+
}
|
|
2082
|
+
function openDatabase(explicit) {
|
|
2083
|
+
const dbPath = getDbPath(explicit);
|
|
2084
|
+
if (dbPath !== ":memory:")
|
|
2085
|
+
mkdirSync2(dirname2(dbPath), { recursive: true });
|
|
2086
|
+
const db = new Database(dbPath);
|
|
2087
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
2088
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
2089
|
+
return db;
|
|
2090
|
+
}
|
|
2091
|
+
function schemaVersion(db) {
|
|
2092
|
+
db.exec("CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER NOT NULL)");
|
|
2093
|
+
const row = db.query("SELECT MAX(version) AS v FROM _schema_version").get();
|
|
2094
|
+
return row?.v ?? 0;
|
|
2095
|
+
}
|
|
2096
|
+
function ensureSchema(db, migrations = MIGRATIONS) {
|
|
2097
|
+
const current = schemaVersion(db);
|
|
2098
|
+
for (const m of migrations) {
|
|
2099
|
+
if (m.version > current) {
|
|
2100
|
+
db.exec(m.sql);
|
|
2101
|
+
db.query("INSERT INTO _schema_version (version) VALUES (?)").run(m.version);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// src/api/token.ts
|
|
1766
2107
|
import { readFileSync } from "fs";
|
|
1767
2108
|
import { resolve as resolve3 } from "path";
|
|
1768
2109
|
import { homedir as homedir3 } from "os";
|
|
2110
|
+
function defaultTokenPath() {
|
|
2111
|
+
return process.env.OURA_TOKEN_PATH ?? resolve3(homedir3(), ".oura-token");
|
|
2112
|
+
}
|
|
2113
|
+
function resolveToken(explicit, tokenPath) {
|
|
2114
|
+
if (explicit)
|
|
2115
|
+
return { token: explicit.trim(), source: "--token" };
|
|
2116
|
+
if (process.env.OURA_TOKEN)
|
|
2117
|
+
return { token: process.env.OURA_TOKEN.trim(), source: "OURA_TOKEN" };
|
|
2118
|
+
const path = tokenPath ?? defaultTokenPath();
|
|
2119
|
+
try {
|
|
2120
|
+
return { token: readFileSync(path, "utf-8").trim(), source: path };
|
|
2121
|
+
} catch {
|
|
2122
|
+
return { token: null, source: path };
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
// src/api/client.ts
|
|
1769
2127
|
var BASE_URL = "https://api.ouraring.com/v2/usercollection";
|
|
2128
|
+
var MAX_PAGES = 1e4;
|
|
1770
2129
|
|
|
1771
2130
|
class OuraClient {
|
|
1772
2131
|
token;
|
|
1773
2132
|
constructor(options = {}) {
|
|
1774
|
-
const
|
|
1775
|
-
if (
|
|
1776
|
-
|
|
1777
|
-
} else {
|
|
1778
|
-
const tokenPath = options.tokenPath ?? process.env.OURA_TOKEN_PATH ?? resolve3(homedir3(), ".oura-token");
|
|
1779
|
-
try {
|
|
1780
|
-
this.token = readFileSync(tokenPath, "utf-8").trim();
|
|
1781
|
-
} catch {
|
|
1782
|
-
throw new CliError("TOKEN_MISSING", `No Oura access token at ${tokenPath}.`, "Run `oura-cli login` or set OURA_TOKEN.");
|
|
1783
|
-
}
|
|
2133
|
+
const { token, source } = resolveToken(options.token, options.tokenPath);
|
|
2134
|
+
if (!token) {
|
|
2135
|
+
throw new CliError("TOKEN_MISSING", `No Oura access token at ${source}.`, "Run `oura-cli login` or set OURA_TOKEN.");
|
|
1784
2136
|
}
|
|
2137
|
+
this.token = token;
|
|
2138
|
+
}
|
|
2139
|
+
async fetch(endpoint, query) {
|
|
2140
|
+
const rows = [];
|
|
2141
|
+
const seenTokens = new Set;
|
|
2142
|
+
let nextToken = null;
|
|
2143
|
+
do {
|
|
2144
|
+
if (seenTokens.size >= MAX_PAGES) {
|
|
2145
|
+
throw new CliError("API_ERROR", `Oura API returned more than ${MAX_PAGES} pages for ${endpoint}; stopping.`);
|
|
2146
|
+
}
|
|
2147
|
+
const params = new URLSearchParams(query);
|
|
2148
|
+
if (nextToken)
|
|
2149
|
+
params.set("next_token", nextToken);
|
|
2150
|
+
const page = await this.getPage(`${BASE_URL}/${endpoint}?${params}`);
|
|
2151
|
+
for (const row of page.data)
|
|
2152
|
+
rows.push(row);
|
|
2153
|
+
nextToken = page.next_token;
|
|
2154
|
+
if (nextToken && seenTokens.has(nextToken)) {
|
|
2155
|
+
throw new CliError("API_ERROR", `Oura API repeated pagination token for ${endpoint}; stopping to avoid a loop.`);
|
|
2156
|
+
}
|
|
2157
|
+
if (nextToken)
|
|
2158
|
+
seenTokens.add(nextToken);
|
|
2159
|
+
} while (nextToken);
|
|
2160
|
+
return rows;
|
|
1785
2161
|
}
|
|
1786
|
-
async
|
|
1787
|
-
const params = new URLSearchParams({ start_date: startDate });
|
|
1788
|
-
if (endDate)
|
|
1789
|
-
params.set("end_date", endDate);
|
|
1790
|
-
const url = `${BASE_URL}/${endpoint}?${params}`;
|
|
2162
|
+
async getPage(url) {
|
|
1791
2163
|
const response = await fetch(url, {
|
|
1792
2164
|
headers: { Authorization: `Bearer ${this.token}` }
|
|
1793
2165
|
});
|
|
@@ -1806,59 +2178,11 @@ class OuraClient {
|
|
|
1806
2178
|
} catch {
|
|
1807
2179
|
throw new CliError("API_ERROR", "Empty response body from Oura API.");
|
|
1808
2180
|
}
|
|
1809
|
-
|
|
2181
|
+
const body = json;
|
|
2182
|
+
return { data: body.data ?? [], next_token: body.next_token ?? null };
|
|
1810
2183
|
}
|
|
1811
2184
|
}
|
|
1812
2185
|
|
|
1813
|
-
// src/lib/time.ts
|
|
1814
|
-
function nowUtc() {
|
|
1815
|
-
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1816
|
-
}
|
|
1817
|
-
function formatLocal(utcStr, timezone) {
|
|
1818
|
-
const dt = new Date(utcStr);
|
|
1819
|
-
const parts = new Intl.DateTimeFormat("sv-SE", {
|
|
1820
|
-
timeZone: timezone,
|
|
1821
|
-
year: "numeric",
|
|
1822
|
-
month: "2-digit",
|
|
1823
|
-
day: "2-digit",
|
|
1824
|
-
hour: "2-digit",
|
|
1825
|
-
minute: "2-digit",
|
|
1826
|
-
hour12: false
|
|
1827
|
-
}).formatToParts(dt);
|
|
1828
|
-
const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
1829
|
-
return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}`;
|
|
1830
|
-
}
|
|
1831
|
-
function formatLocalDate(utcStr, timezone) {
|
|
1832
|
-
return formatLocal(utcStr, timezone).split(" ")[0];
|
|
1833
|
-
}
|
|
1834
|
-
function todayLocal(timezone) {
|
|
1835
|
-
return formatLocalDate(nowUtc(), timezone);
|
|
1836
|
-
}
|
|
1837
|
-
function resolveDefaultTimezone() {
|
|
1838
|
-
if (process.env.OURA_TZ)
|
|
1839
|
-
return process.env.OURA_TZ;
|
|
1840
|
-
try {
|
|
1841
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
1842
|
-
} catch {
|
|
1843
|
-
return "UTC";
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
// src/commands/helpers.ts
|
|
1848
|
-
function getClient(opts) {
|
|
1849
|
-
return new OuraClient(opts.token ? { token: opts.token } : {});
|
|
1850
|
-
}
|
|
1851
|
-
function todayDate(timezone) {
|
|
1852
|
-
return todayLocal(timezone ?? resolveDefaultTimezone());
|
|
1853
|
-
}
|
|
1854
|
-
function dateRange(days, timezone) {
|
|
1855
|
-
const tz = timezone ?? resolveDefaultTimezone();
|
|
1856
|
-
const end = todayLocal(tz);
|
|
1857
|
-
const startMs = new Date(`${end}T00:00:00Z`).getTime() - (days - 1) * 86400000;
|
|
1858
|
-
const start = new Date(startMs).toISOString().slice(0, 10);
|
|
1859
|
-
return { start, end };
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
2186
|
// src/lib/format-resolve.ts
|
|
1863
2187
|
function resolveFormat({ explicit, isTty }) {
|
|
1864
2188
|
if (explicit === undefined)
|
|
@@ -1868,341 +2192,146 @@ function resolveFormat({ explicit, isTty }) {
|
|
|
1868
2192
|
throw new CliError("BAD_ARGS", `Unknown --format value: "${explicit}". Use "table" or "json".`);
|
|
1869
2193
|
}
|
|
1870
2194
|
|
|
1871
|
-
// src/
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
db: { type: "string", description: "Path to SQLite database file (env: OURA_DB_PATH)" },
|
|
1876
|
-
tz: { type: "string", description: "Display timezone (env: OURA_TZ; auto-detected)" },
|
|
1877
|
-
"no-color": { type: "boolean", default: false, description: "Disable ANSI colors (also honors NO_COLOR env)" }
|
|
1878
|
-
};
|
|
1879
|
-
function handleError(err, args) {
|
|
1880
|
-
const fmt = resolveFormat({
|
|
1881
|
-
explicit: args.format,
|
|
1882
|
-
isTty: process.stdout.isTTY === true
|
|
1883
|
-
});
|
|
1884
|
-
emitError(err, fmt);
|
|
1885
|
-
process.exit(exitCodeFor(err));
|
|
1886
|
-
}
|
|
1887
|
-
function applyNoColor(args) {
|
|
1888
|
-
if (args["no-color"] || process.env.NO_COLOR) {
|
|
1889
|
-
Promise.resolve().then(() => (init_source(), exports_source)).then(({ default: chalk2 }) => {
|
|
1890
|
-
chalk2.level = 0;
|
|
1891
|
-
});
|
|
2195
|
+
// src/lib/validate.ts
|
|
2196
|
+
function assertCalendarDate(value, label) {
|
|
2197
|
+
if (!isCalendarDate(value)) {
|
|
2198
|
+
throw new CliError("BAD_ARGS", `${label} must be a real YYYY-MM-DD date, got "${value}".`);
|
|
1892
2199
|
}
|
|
2200
|
+
return value;
|
|
1893
2201
|
}
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
const dbPath = getDbPath2({ dbPath: opts.db });
|
|
1899
|
-
mkdirSync3(dirname2(dbPath), { recursive: true });
|
|
1900
|
-
const db = openDatabase2({ dbPath: opts.db });
|
|
1901
|
-
ensureSchema2(db);
|
|
1902
|
-
const client = getClient(opts);
|
|
1903
|
-
const log = format === "table" ? console.log : undefined;
|
|
1904
|
-
const importResult = await importDaily(db, client, log);
|
|
1905
|
-
const today = getDaySummary(db, todayDate(opts.tz));
|
|
1906
|
-
db.close();
|
|
1907
|
-
if (format === "json") {
|
|
1908
|
-
console.log(JSON.stringify({ import: importResult, today }, null, 2));
|
|
1909
|
-
} else {
|
|
1910
|
-
console.log(formatDaySummary(today, format));
|
|
2202
|
+
function assertPositiveInt(value, label) {
|
|
2203
|
+
const n = Number(value);
|
|
2204
|
+
if (!/^\d+$/.test(value.trim()) || !Number.isSafeInteger(n) || n < 1) {
|
|
2205
|
+
throw new CliError("BAD_ARGS", `${label} must be a positive integer, got "${value}".`);
|
|
1911
2206
|
}
|
|
2207
|
+
return n;
|
|
1912
2208
|
}
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
await runSync({ format: args.format, db: args.db, token: args.token, tz: args.tz });
|
|
1920
|
-
} catch (err) {
|
|
1921
|
-
handleError(err, args);
|
|
1922
|
-
}
|
|
2209
|
+
function assertTimezone(tz) {
|
|
2210
|
+
try {
|
|
2211
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
2212
|
+
return tz;
|
|
2213
|
+
} catch {
|
|
2214
|
+
throw new CliError("BAD_ARGS", `Unknown timezone "${tz}".`, "Use an IANA name such as Europe/Berlin (env: OURA_TZ, flag: --tz).");
|
|
1923
2215
|
}
|
|
1924
|
-
});
|
|
1925
|
-
|
|
1926
|
-
// src/commands/db.ts
|
|
1927
|
-
import { mkdirSync as mkdirSync4, unlinkSync } from "fs";
|
|
1928
|
-
import { dirname as dirname3 } from "path";
|
|
1929
|
-
|
|
1930
|
-
// src/db/csv-import.ts
|
|
1931
|
-
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
1932
|
-
import { join } from "path";
|
|
1933
|
-
var CSV_DIR = join(process.env.HOME ?? "", "Documents/OpenClaw/projects/oura-ring/data/App Data");
|
|
1934
|
-
function parseCSV(filename) {
|
|
1935
|
-
const path = join(CSV_DIR, filename);
|
|
1936
|
-
if (!existsSync(path))
|
|
1937
|
-
return [];
|
|
1938
|
-
const text = readFileSync2(path, "utf-8");
|
|
1939
|
-
const lines = text.split(`
|
|
1940
|
-
`).filter((l) => l.trim());
|
|
1941
|
-
if (lines.length < 2)
|
|
1942
|
-
return [];
|
|
1943
|
-
const headers = lines[0].split(";");
|
|
1944
|
-
return lines.slice(1).map((line) => {
|
|
1945
|
-
const vals = line.split(";");
|
|
1946
|
-
const row = {};
|
|
1947
|
-
headers.forEach((h, i) => {
|
|
1948
|
-
row[h] = vals[i] ?? "";
|
|
1949
|
-
});
|
|
1950
|
-
return row;
|
|
1951
|
-
});
|
|
1952
|
-
}
|
|
1953
|
-
function num(v) {
|
|
1954
|
-
if (!v || v === "")
|
|
1955
|
-
return null;
|
|
1956
|
-
const n = Number(v);
|
|
1957
|
-
return isNaN(n) ? null : n;
|
|
1958
2216
|
}
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
activityTx();
|
|
1989
|
-
log(`daily_activity: ${activity.length} rows`);
|
|
1990
|
-
const spo2 = parseCSV("dailyspo2.csv");
|
|
1991
|
-
const insertSpo2 = db.query("INSERT OR REPLACE INTO daily_spo2 VALUES (?,?,?,?)");
|
|
1992
|
-
const spo2Tx = db.transaction(() => {
|
|
1993
|
-
for (const r of spo2) {
|
|
1994
|
-
let avg = null;
|
|
1995
|
-
try {
|
|
1996
|
-
const parsed = JSON.parse(r.spo2_percentage);
|
|
1997
|
-
avg = parsed?.average ?? null;
|
|
1998
|
-
} catch {}
|
|
1999
|
-
insertSpo2.run(r.id, r.day, avg, num(r.breathing_disturbance_index));
|
|
2217
|
+
|
|
2218
|
+
// src/commands/run-command.ts
|
|
2219
|
+
var processIo = {
|
|
2220
|
+
stdout: (s) => {
|
|
2221
|
+
process.stdout.write(s + `
|
|
2222
|
+
`);
|
|
2223
|
+
},
|
|
2224
|
+
stderr: (s) => {
|
|
2225
|
+
process.stderr.write(s + `
|
|
2226
|
+
`);
|
|
2227
|
+
},
|
|
2228
|
+
exit: (code) => process.exit(code),
|
|
2229
|
+
isTty: process.stdout.isTTY === true
|
|
2230
|
+
};
|
|
2231
|
+
async function execute(def, args, io = processIo) {
|
|
2232
|
+
if (args["no-color"] || process.env.NO_COLOR)
|
|
2233
|
+
source_default.level = 0;
|
|
2234
|
+
let db;
|
|
2235
|
+
let format = io.isTty ? "table" : "json";
|
|
2236
|
+
let exitCode = 0;
|
|
2237
|
+
try {
|
|
2238
|
+
format = resolveFormat({ explicit: args.format, isTty: io.isTty });
|
|
2239
|
+
const outputFormat = def.jsonOnly ? "json" : format;
|
|
2240
|
+
const tz = assertTimezone(args.tz ?? resolveDefaultTimezone());
|
|
2241
|
+
const ctx = { format: outputFormat, tz, today: today(tz) };
|
|
2242
|
+
if (def.needs?.db) {
|
|
2243
|
+
db = openDatabase(args.db);
|
|
2244
|
+
ensureSchema(db);
|
|
2245
|
+
ctx.db = db;
|
|
2000
2246
|
}
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
log(`daily_spo2: ${spo2.length} rows`);
|
|
2004
|
-
const stress = parseCSV("dailystress.csv");
|
|
2005
|
-
const insertStress = db.query("INSERT OR REPLACE INTO daily_stress VALUES (?,?,?,?,?)");
|
|
2006
|
-
const stressTx = db.transaction(() => {
|
|
2007
|
-
for (const r of stress)
|
|
2008
|
-
insertStress.run(r.id, r.day, str(r.day_summary), num(r.recovery_high), num(r.stress_high));
|
|
2009
|
-
});
|
|
2010
|
-
stressTx();
|
|
2011
|
-
log(`daily_stress: ${stress.length} rows`);
|
|
2012
|
-
const hr = parseCSV("heartrate.csv");
|
|
2013
|
-
const insertHr = db.query("INSERT OR IGNORE INTO heartrate VALUES (?,?,?,?)");
|
|
2014
|
-
const hrTx = db.transaction(() => {
|
|
2015
|
-
for (const r of hr) {
|
|
2016
|
-
const day = r.timestamp?.slice(0, 10) ?? null;
|
|
2017
|
-
insertHr.run(r.timestamp, num(r.bpm), str(r.source), day);
|
|
2247
|
+
if (def.needs?.client) {
|
|
2248
|
+
ctx.client = new OuraClient(args.token ? { token: args.token } : {});
|
|
2018
2249
|
}
|
|
2250
|
+
const out = await def.run(ctx, args);
|
|
2251
|
+
io.stdout(outputFormat === "json" ? JSON.stringify(out.json, null, 2) : out.text());
|
|
2252
|
+
exitCode = out.exitCode ?? 0;
|
|
2253
|
+
} catch (err) {
|
|
2254
|
+
io.stderr(formatError(err, format).text);
|
|
2255
|
+
exitCode = exitCodeFor(err);
|
|
2256
|
+
} finally {
|
|
2257
|
+
db?.close();
|
|
2258
|
+
}
|
|
2259
|
+
if (exitCode !== 0)
|
|
2260
|
+
io.exit(exitCode);
|
|
2261
|
+
}
|
|
2262
|
+
function dataCommand(def) {
|
|
2263
|
+
return defineCommand({
|
|
2264
|
+
meta: def.meta,
|
|
2265
|
+
args: { ...commonArgs, ...def.args ?? {} },
|
|
2266
|
+
run: ({ args }) => execute(def, args, processIo)
|
|
2019
2267
|
});
|
|
2020
|
-
hrTx();
|
|
2021
|
-
log(`heartrate: ${hr.length} rows`);
|
|
2022
|
-
const sleepModel = parseCSV("sleepmodel.csv");
|
|
2023
|
-
const insertSM = db.query("INSERT OR REPLACE INTO sleep_model VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)");
|
|
2024
|
-
const smTx = db.transaction(() => {
|
|
2025
|
-
for (const r of sleepModel)
|
|
2026
|
-
insertSM.run(r.id, r.day, num(r.average_breath), num(r.average_heart_rate), num(r.average_hrv), num(r.awake_time), str(r.bedtime_end), str(r.bedtime_start), num(r.deep_sleep_duration), num(r.efficiency), num(r.latency), num(r.light_sleep_duration), num(r.lowest_heart_rate), num(r.period), num(r.rem_sleep_duration), num(r.restless_periods), num(r.time_in_bed), num(r.total_sleep_duration), str(r.type));
|
|
2027
|
-
});
|
|
2028
|
-
smTx();
|
|
2029
|
-
log(`sleep_model: ${sleepModel.length} rows`);
|
|
2030
|
-
const vo2 = parseCSV("vo2max.csv");
|
|
2031
|
-
const insertVo2 = db.query("INSERT OR REPLACE INTO vo2max VALUES (?,?,?,?)");
|
|
2032
|
-
const vo2Tx = db.transaction(() => {
|
|
2033
|
-
for (const r of vo2)
|
|
2034
|
-
insertVo2.run(r.id, r.day, num(r.vo2_max), str(r.timestamp));
|
|
2035
|
-
});
|
|
2036
|
-
vo2Tx();
|
|
2037
|
-
log(`vo2max: ${vo2.length} rows`);
|
|
2038
|
-
const cv = parseCSV("dailycardiovascularage.csv");
|
|
2039
|
-
const insertCv = db.query("INSERT OR REPLACE INTO cardiovascular_age VALUES (?,?,?)");
|
|
2040
|
-
const cvTx = db.transaction(() => {
|
|
2041
|
-
for (const r of cv)
|
|
2042
|
-
insertCv.run(r.id, r.day, num(r.vascular_age));
|
|
2043
|
-
});
|
|
2044
|
-
cvTx();
|
|
2045
|
-
log(`cardiovascular_age: ${cv.length} rows`);
|
|
2046
|
-
const workouts = parseCSV("workout.csv");
|
|
2047
|
-
const insertW = db.query("INSERT OR REPLACE INTO workouts VALUES (?,?,?,?,?,?,?,?,?,?)");
|
|
2048
|
-
const wTx = db.transaction(() => {
|
|
2049
|
-
for (const r of workouts)
|
|
2050
|
-
insertW.run(r.id, r.day, str(r.activity), num(r.calories), num(r.distance), str(r.start_datetime), str(r.end_datetime), str(r.intensity), str(r.label), str(r.source));
|
|
2051
|
-
});
|
|
2052
|
-
wTx();
|
|
2053
|
-
log(`workouts: ${workouts.length} rows`);
|
|
2054
|
-
log("CSV import complete.");
|
|
2055
2268
|
}
|
|
2056
2269
|
|
|
2270
|
+
// src/commands/sync.ts
|
|
2271
|
+
async function runSync(ctx) {
|
|
2272
|
+
const lines = [];
|
|
2273
|
+
const log = ctx.format === "table" ? (m) => lines.push(m) : undefined;
|
|
2274
|
+
const importResult = await importDaily(ctx.db, ctx.client, { today: ctx.today, tz: ctx.tz }, log);
|
|
2275
|
+
const today = getDaySummary(ctx.db, ctx.today);
|
|
2276
|
+
return {
|
|
2277
|
+
json: { import: importResult, today },
|
|
2278
|
+
text: () => [...lines, formatImportSummary(importResult), formatDaySummary(today, "table")].join(`
|
|
2279
|
+
`)
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
var syncCommand = dataCommand({
|
|
2283
|
+
meta: { name: "sync", description: "Import latest data from Oura API and return today's summary" },
|
|
2284
|
+
needs: { db: true, client: true },
|
|
2285
|
+
run: runSync
|
|
2286
|
+
});
|
|
2287
|
+
|
|
2057
2288
|
// src/commands/db.ts
|
|
2289
|
+
var SYNC_HINT = "Run `oura-cli sync` to download your data. Oura publishes a day's summary after that night's sleep syncs from the ring.";
|
|
2058
2290
|
var dbCommand = defineCommand({
|
|
2059
2291
|
meta: { name: "db", description: "Query and manage the local SQLite database" },
|
|
2060
2292
|
subCommands: {
|
|
2061
|
-
|
|
2062
|
-
meta: { name: "import", description: "Sync new data from Oura API into local database (alias of sync)" },
|
|
2063
|
-
args: { ...commonArgs },
|
|
2064
|
-
async run({ args }) {
|
|
2065
|
-
applyNoColor(args);
|
|
2066
|
-
try {
|
|
2067
|
-
await runSync({ format: args.format, db: args.db, token: args.token, tz: args.tz });
|
|
2068
|
-
} catch (err) {
|
|
2069
|
-
handleError(err, args);
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
}),
|
|
2073
|
-
today: defineCommand({
|
|
2293
|
+
today: dataCommand({
|
|
2074
2294
|
meta: { name: "today", description: "Today's summary from local database" },
|
|
2075
|
-
|
|
2076
|
-
run(
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
|
|
2080
|
-
const db = openDatabase2({ dbPath: args.db });
|
|
2081
|
-
ensureSchema2(db);
|
|
2082
|
-
const summary = getDaySummary(db, todayDate(args.tz));
|
|
2083
|
-
console.log(formatDaySummary(summary, format));
|
|
2084
|
-
db.close();
|
|
2085
|
-
} catch (err) {
|
|
2086
|
-
handleError(err, args);
|
|
2087
|
-
}
|
|
2295
|
+
needs: { db: true },
|
|
2296
|
+
run(ctx) {
|
|
2297
|
+
const summary = getDaySummary(ctx.db, ctx.today);
|
|
2298
|
+
return { json: summary, text: () => formatDaySummary(summary, "table", SYNC_HINT) };
|
|
2088
2299
|
}
|
|
2089
2300
|
}),
|
|
2090
|
-
date:
|
|
2301
|
+
date: dataCommand({
|
|
2091
2302
|
meta: { name: "date", description: "Summary for specific date from local database" },
|
|
2092
|
-
args: {
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
try {
|
|
2099
|
-
const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
|
|
2100
|
-
const db = openDatabase2({ dbPath: args.db });
|
|
2101
|
-
ensureSchema2(db);
|
|
2102
|
-
const summary = getDaySummary(db, args.day);
|
|
2103
|
-
console.log(formatDaySummary(summary, format));
|
|
2104
|
-
db.close();
|
|
2105
|
-
} catch (err) {
|
|
2106
|
-
handleError(err, args);
|
|
2107
|
-
}
|
|
2303
|
+
args: { day: { type: "positional", required: true, description: "Target date (YYYY-MM-DD)" } },
|
|
2304
|
+
needs: { db: true },
|
|
2305
|
+
run(ctx, args) {
|
|
2306
|
+
const day = assertCalendarDate(String(args.day), "<day>");
|
|
2307
|
+
const summary = getDaySummary(ctx.db, day);
|
|
2308
|
+
return { json: summary, text: () => formatDaySummary(summary, "table") };
|
|
2108
2309
|
}
|
|
2109
2310
|
}),
|
|
2110
|
-
week:
|
|
2311
|
+
week: dataCommand({
|
|
2111
2312
|
meta: { name: "week", description: "Last 7 days from local database" },
|
|
2112
|
-
|
|
2113
|
-
run(
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
|
|
2117
|
-
const db = openDatabase2({ dbPath: args.db });
|
|
2118
|
-
ensureSchema2(db);
|
|
2119
|
-
const days = [];
|
|
2120
|
-
for (let i = 6;i >= 0; i--) {
|
|
2121
|
-
const d = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
|
|
2122
|
-
days.push(getDaySummary(db, d));
|
|
2123
|
-
}
|
|
2124
|
-
console.log(formatWeekTable(days, format));
|
|
2125
|
-
db.close();
|
|
2126
|
-
} catch (err) {
|
|
2127
|
-
handleError(err, args);
|
|
2128
|
-
}
|
|
2313
|
+
needs: { db: true },
|
|
2314
|
+
run(ctx) {
|
|
2315
|
+
const days = daysBack(ctx.today, 7).map((d) => getDaySummary(ctx.db, d));
|
|
2316
|
+
return { json: days, text: () => formatWeekTable(days, "table", "Run `oura-cli sync`, then `oura-cli db week` again.") };
|
|
2129
2317
|
}
|
|
2130
2318
|
}),
|
|
2131
|
-
trends:
|
|
2319
|
+
trends: dataCommand({
|
|
2132
2320
|
meta: { name: "trends", description: "Score and metric trends over N days (default: 30)" },
|
|
2133
|
-
args: {
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
try {
|
|
2140
|
-
const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
|
|
2141
|
-
const n = args.days ? parseInt(args.days, 10) : 30;
|
|
2142
|
-
const db = openDatabase2({ dbPath: args.db });
|
|
2143
|
-
ensureSchema2(db);
|
|
2144
|
-
const trends = getTrends(db, n);
|
|
2145
|
-
console.log(formatTrends(trends, n, format));
|
|
2146
|
-
db.close();
|
|
2147
|
-
} catch (err) {
|
|
2148
|
-
handleError(err, args);
|
|
2149
|
-
}
|
|
2321
|
+
args: { days: { type: "positional", required: false, description: "Window size in days (default: 30)" } },
|
|
2322
|
+
needs: { db: true },
|
|
2323
|
+
run(ctx, args) {
|
|
2324
|
+
const n = args.days === undefined ? 30 : assertPositiveInt(String(args.days), "<days>");
|
|
2325
|
+
const trends = getTrends(ctx.db, n, ctx.today);
|
|
2326
|
+
return { json: trends, text: () => formatTrends(trends, n, "table") };
|
|
2150
2327
|
}
|
|
2151
2328
|
}),
|
|
2152
|
-
stats:
|
|
2329
|
+
stats: dataCommand({
|
|
2153
2330
|
meta: { name: "stats", description: "Row counts, date range, and record highs from local database" },
|
|
2154
|
-
|
|
2155
|
-
run(
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
|
|
2159
|
-
const db = openDatabase2({ dbPath: args.db });
|
|
2160
|
-
ensureSchema2(db);
|
|
2161
|
-
const stats = getStats(db);
|
|
2162
|
-
console.log(formatStats(stats, format));
|
|
2163
|
-
db.close();
|
|
2164
|
-
} catch (err) {
|
|
2165
|
-
handleError(err, args);
|
|
2166
|
-
}
|
|
2167
|
-
}
|
|
2168
|
-
}),
|
|
2169
|
-
reset: defineCommand({
|
|
2170
|
-
meta: { name: "reset", description: "Destroy and rebuild database from exported CSV files" },
|
|
2171
|
-
args: {
|
|
2172
|
-
...commonArgs,
|
|
2173
|
-
force: { type: "boolean", default: false, description: "Confirm destructive reset" }
|
|
2174
|
-
},
|
|
2175
|
-
run({ args }) {
|
|
2176
|
-
applyNoColor(args);
|
|
2177
|
-
try {
|
|
2178
|
-
if (!args.force) {
|
|
2179
|
-
console.log(JSON.stringify({ error: "Use --force to confirm destructive reset." }));
|
|
2180
|
-
process.exit(1);
|
|
2181
|
-
}
|
|
2182
|
-
const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
|
|
2183
|
-
const dbPath = getDbPath2({ dbPath: args.db });
|
|
2184
|
-
try {
|
|
2185
|
-
unlinkSync(dbPath);
|
|
2186
|
-
} catch {}
|
|
2187
|
-
try {
|
|
2188
|
-
unlinkSync(dbPath + "-wal");
|
|
2189
|
-
} catch {}
|
|
2190
|
-
try {
|
|
2191
|
-
unlinkSync(dbPath + "-shm");
|
|
2192
|
-
} catch {}
|
|
2193
|
-
const log = format === "table" ? console.log : undefined;
|
|
2194
|
-
log?.("Database deleted.");
|
|
2195
|
-
mkdirSync4(dirname3(dbPath), { recursive: true });
|
|
2196
|
-
const db = openDatabase2({ dbPath: args.db });
|
|
2197
|
-
ensureSchema2(db);
|
|
2198
|
-
importFromCSV(db, log ?? (() => {}));
|
|
2199
|
-
if (format === "json") {
|
|
2200
|
-
console.log(JSON.stringify({ status: "reset complete" }));
|
|
2201
|
-
}
|
|
2202
|
-
db.close();
|
|
2203
|
-
} catch (err) {
|
|
2204
|
-
handleError(err, args);
|
|
2205
|
-
}
|
|
2331
|
+
needs: { db: true },
|
|
2332
|
+
run(ctx) {
|
|
2333
|
+
const stats = getStats(ctx.db, ctx.today);
|
|
2334
|
+
return { json: stats, text: () => formatStats(stats, "table") };
|
|
2206
2335
|
}
|
|
2207
2336
|
})
|
|
2208
2337
|
}
|
|
@@ -2217,17 +2346,15 @@ function dayLabel(dateStr) {
|
|
|
2217
2346
|
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
2218
2347
|
return `${day} ${dd}/${mm}`;
|
|
2219
2348
|
}
|
|
2220
|
-
function getReport(db, days) {
|
|
2349
|
+
function getReport(db, days, today) {
|
|
2221
2350
|
const period = days <= 7 ? "week" : "month";
|
|
2222
|
-
const
|
|
2223
|
-
const
|
|
2224
|
-
const
|
|
2225
|
-
const
|
|
2226
|
-
const prevWeekEnd = new Date(today.getTime() - days * 86400000).toISOString().slice(0, 10);
|
|
2227
|
-
const prevWeekStart = new Date(today.getTime() - (days * 2 - 1) * 86400000).toISOString().slice(0, 10);
|
|
2351
|
+
const weekEnd = today;
|
|
2352
|
+
const weekStart = shiftDay(today, -(days - 1));
|
|
2353
|
+
const prevWeekEnd = shiftDay(today, -days);
|
|
2354
|
+
const prevWeekStart = shiftDay(today, -(days * 2 - 1));
|
|
2228
2355
|
const dailyRows = [];
|
|
2229
2356
|
for (let i = days - 1;i >= 0; i--) {
|
|
2230
|
-
const d =
|
|
2357
|
+
const d = shiftDay(today, -i);
|
|
2231
2358
|
const sl = db.query("SELECT score FROM daily_sleep WHERE day=?").get(d);
|
|
2232
2359
|
const rd = db.query("SELECT score FROM daily_readiness WHERE day=?").get(d);
|
|
2233
2360
|
const ac = db.query("SELECT score, steps FROM daily_activity WHERE day=?").get(d);
|
|
@@ -2295,7 +2422,7 @@ function getReport(db, days) {
|
|
|
2295
2422
|
return { period, weekStart, weekEnd, days: dailyRows, averages, spo2, patterns: { lowSleep, lowReadiness, highActivity }, sleepDetails, recommendations };
|
|
2296
2423
|
}
|
|
2297
2424
|
|
|
2298
|
-
// src/format-report.ts
|
|
2425
|
+
// src/render/format-report.ts
|
|
2299
2426
|
init_source();
|
|
2300
2427
|
function colorizeScore(n) {
|
|
2301
2428
|
if (n >= 85)
|
|
@@ -2365,6 +2492,14 @@ function formatReport(data, format, period) {
|
|
|
2365
2492
|
}
|
|
2366
2493
|
lines.push(source_default.gray(` ${data.weekStart} \u2014 ${data.weekEnd}`));
|
|
2367
2494
|
lines.push("");
|
|
2495
|
+
const hasReportData = data.days.some((day) => day.sleep !== null || day.readiness !== null || day.activity !== null || day.steps !== null) || data.averages.length > 0 || data.spo2 !== null || data.sleepDetails !== null;
|
|
2496
|
+
if (!hasReportData) {
|
|
2497
|
+
lines.push(" No Oura data is available for this report yet.");
|
|
2498
|
+
lines.push(" Run `oura-cli sync` to download your data, then run `oura-cli report` again.");
|
|
2499
|
+
lines.push("");
|
|
2500
|
+
return lines.join(`
|
|
2501
|
+
`);
|
|
2502
|
+
}
|
|
2368
2503
|
if (period === "week") {
|
|
2369
2504
|
lines.push(source_default.bold(" Last 7 Days:"));
|
|
2370
2505
|
lines.push(source_default.gray(" " + "\u2500".repeat(52)));
|
|
@@ -2436,29 +2571,17 @@ function formatReport(data, format, period) {
|
|
|
2436
2571
|
}
|
|
2437
2572
|
|
|
2438
2573
|
// src/commands/report.ts
|
|
2439
|
-
var reportCommand =
|
|
2574
|
+
var reportCommand = dataCommand({
|
|
2440
2575
|
meta: { name: "report", description: "Generate a narrative health report from local data." },
|
|
2441
|
-
args: {
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
try {
|
|
2448
|
-
const period = args.period;
|
|
2449
|
-
if (period !== "week" && period !== "month") {
|
|
2450
|
-
throw new CliError("BAD_ARGS", `--period must be "week" or "month", got "${period}".`);
|
|
2451
|
-
}
|
|
2452
|
-
const format = resolveFormat({ explicit: args.format, isTty: process.stdout.isTTY === true });
|
|
2453
|
-
const db = openDatabase2({ dbPath: args.db });
|
|
2454
|
-
ensureSchema2(db);
|
|
2455
|
-
const days = period === "week" ? 7 : 30;
|
|
2456
|
-
const data = getReport(db, days);
|
|
2457
|
-
console.log(formatReport(data, format, period));
|
|
2458
|
-
db.close();
|
|
2459
|
-
} catch (err) {
|
|
2460
|
-
handleError(err, args);
|
|
2576
|
+
args: { period: { type: "string", description: "Report window: week | month", default: "week" } },
|
|
2577
|
+
needs: { db: true },
|
|
2578
|
+
run(ctx, args) {
|
|
2579
|
+
const period = args.period;
|
|
2580
|
+
if (period !== "week" && period !== "month") {
|
|
2581
|
+
throw new CliError("BAD_ARGS", `--period must be "week" or "month", got "${period}".`);
|
|
2461
2582
|
}
|
|
2583
|
+
const data = getReport(ctx.db, period === "week" ? 7 : 30, ctx.today);
|
|
2584
|
+
return { json: data, text: () => formatReport(data, "table", period) };
|
|
2462
2585
|
}
|
|
2463
2586
|
});
|
|
2464
2587
|
|
|
@@ -2472,8 +2595,8 @@ function healthcheckCommand(version) {
|
|
|
2472
2595
|
let ok = true;
|
|
2473
2596
|
let error;
|
|
2474
2597
|
try {
|
|
2475
|
-
const db =
|
|
2476
|
-
|
|
2598
|
+
const db = openDatabase(args.db);
|
|
2599
|
+
ensureSchema(db);
|
|
2477
2600
|
db.query("SELECT 1").get();
|
|
2478
2601
|
db.close();
|
|
2479
2602
|
} catch (err) {
|
|
@@ -2485,96 +2608,209 @@ function healthcheckCommand(version) {
|
|
|
2485
2608
|
});
|
|
2486
2609
|
}
|
|
2487
2610
|
|
|
2611
|
+
// src/render/doctor-table.ts
|
|
2612
|
+
init_source();
|
|
2613
|
+
function statusSymbol(status) {
|
|
2614
|
+
if (status === "ok")
|
|
2615
|
+
return source_default.green("\u2713");
|
|
2616
|
+
if (status === "warn")
|
|
2617
|
+
return source_default.yellow("!");
|
|
2618
|
+
return source_default.red("\u2717");
|
|
2619
|
+
}
|
|
2620
|
+
function formatDoctorTable(result) {
|
|
2621
|
+
const lines = ["", source_default.bold(" Doctor"), source_default.gray("\u2500".repeat(50))];
|
|
2622
|
+
for (const c of result.checks) {
|
|
2623
|
+
lines.push(` ${statusSymbol(c.status)} ${c.id.padEnd(12)} ${c.detail}`);
|
|
2624
|
+
}
|
|
2625
|
+
lines.push("");
|
|
2626
|
+
const next = result.nextStep ?? (result.ok ? "nothing \u2014 everything looks healthy." : "see the failing checks above.");
|
|
2627
|
+
lines.push(` Next: ${next}`);
|
|
2628
|
+
return lines.join(`
|
|
2629
|
+
`);
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
// src/commands/doctor.ts
|
|
2633
|
+
async function runChecks(deps) {
|
|
2634
|
+
const checks = [];
|
|
2635
|
+
const { token, source } = deps.resolveToken();
|
|
2636
|
+
if (token) {
|
|
2637
|
+
checks.push({ id: "token", status: "ok", detail: `Token found via ${source}.` });
|
|
2638
|
+
} else {
|
|
2639
|
+
checks.push({ id: "token", status: "fail", detail: `No token found (checked ${source}).`, fix: "oura-cli login" });
|
|
2640
|
+
}
|
|
2641
|
+
if (!token) {
|
|
2642
|
+
checks.push({ id: "token-valid", status: "fail", detail: "No token to validate.", fix: "oura-cli login" });
|
|
2643
|
+
} else if (deps.offline) {
|
|
2644
|
+
checks.push({ id: "token-valid", status: "ok", detail: "Skipped (--offline)." });
|
|
2645
|
+
} else {
|
|
2646
|
+
try {
|
|
2647
|
+
const client = deps.createClient(token);
|
|
2648
|
+
await client.fetch("daily_sleep", { start_date: deps.today, end_date: deps.today });
|
|
2649
|
+
checks.push({ id: "token-valid", status: "ok", detail: "Token accepted by the Oura API." });
|
|
2650
|
+
} catch (err) {
|
|
2651
|
+
if (err instanceof CliError && err.code === "TOKEN_INVALID") {
|
|
2652
|
+
checks.push({ id: "token-valid", status: "fail", detail: err.message, fix: "oura-cli login" });
|
|
2653
|
+
} else {
|
|
2654
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2655
|
+
checks.push({ id: "token-valid", status: "warn", detail: `Could not reach the Oura API: ${msg}` });
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
let db = null;
|
|
2660
|
+
try {
|
|
2661
|
+
const opened = deps.openDb();
|
|
2662
|
+
db = opened.db;
|
|
2663
|
+
checks.push({ id: "database", status: "ok", detail: `Database ready at ${opened.path}.` });
|
|
2664
|
+
} catch (err) {
|
|
2665
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2666
|
+
checks.push({ id: "database", status: "fail", detail: msg });
|
|
2667
|
+
}
|
|
2668
|
+
if (db) {
|
|
2669
|
+
const last = latestDataDay(db);
|
|
2670
|
+
if (!last) {
|
|
2671
|
+
checks.push({ id: "data", status: "warn", detail: "No data in the local cache yet.", fix: "oura-cli sync" });
|
|
2672
|
+
} else {
|
|
2673
|
+
const ageDays = Math.round((new Date(`${deps.today}T00:00:00Z`).getTime() - new Date(`${last}T00:00:00Z`).getTime()) / 86400000);
|
|
2674
|
+
if (ageDays > 2) {
|
|
2675
|
+
checks.push({ id: "data", status: "warn", detail: `Most recent data is from ${last} (${ageDays} days ago).`, fix: "oura-cli sync" });
|
|
2676
|
+
} else {
|
|
2677
|
+
checks.push({ id: "data", status: "ok", detail: `Data current through ${last}.` });
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
} else {
|
|
2681
|
+
checks.push({ id: "data", status: "fail", detail: "Cannot check data \u2014 database unavailable." });
|
|
2682
|
+
}
|
|
2683
|
+
db?.close();
|
|
2684
|
+
const ok = checks.every((c) => c.status === "ok");
|
|
2685
|
+
const nextStep = checks.find((c) => c.status !== "ok")?.fix ?? null;
|
|
2686
|
+
return { ok, checks, nextStep };
|
|
2687
|
+
}
|
|
2688
|
+
var DATA_TABLES = ["daily_sleep", "daily_readiness", "daily_activity"];
|
|
2689
|
+
function latestDataDay(db) {
|
|
2690
|
+
let latest = null;
|
|
2691
|
+
for (const tbl of DATA_TABLES) {
|
|
2692
|
+
const row = db.query(`SELECT MAX(day) as d FROM ${tbl}`).get();
|
|
2693
|
+
if (row?.d && (!latest || row.d > latest))
|
|
2694
|
+
latest = row.d;
|
|
2695
|
+
}
|
|
2696
|
+
return latest;
|
|
2697
|
+
}
|
|
2698
|
+
function exitCodeForChecks(checks) {
|
|
2699
|
+
const fail = checks.find((c) => c.status === "fail");
|
|
2700
|
+
if (!fail)
|
|
2701
|
+
return 0;
|
|
2702
|
+
if (fail.id === "token" || fail.id === "token-valid")
|
|
2703
|
+
return exitCodeFor(new CliError("TOKEN_MISSING", fail.detail));
|
|
2704
|
+
return exitCodeFor(new CliError("DB_ERROR", fail.detail));
|
|
2705
|
+
}
|
|
2706
|
+
var doctorCommand = dataCommand({
|
|
2707
|
+
meta: { name: "doctor", description: "Diagnose token, database, and sync health, and suggest the next step." },
|
|
2708
|
+
args: { offline: { type: "boolean", default: false, description: "Skip the live Oura API token-validation call" } },
|
|
2709
|
+
async run(ctx, args) {
|
|
2710
|
+
const deps = {
|
|
2711
|
+
resolveToken: () => resolveToken(args.token),
|
|
2712
|
+
openDb: () => {
|
|
2713
|
+
const db = openDatabase(args.db);
|
|
2714
|
+
ensureSchema(db);
|
|
2715
|
+
return { db, path: getDbPath(args.db) };
|
|
2716
|
+
},
|
|
2717
|
+
createClient: (token) => new OuraClient({ token }),
|
|
2718
|
+
offline: args.offline === true,
|
|
2719
|
+
today: ctx.today
|
|
2720
|
+
};
|
|
2721
|
+
const result = await runChecks(deps);
|
|
2722
|
+
return {
|
|
2723
|
+
json: result,
|
|
2724
|
+
text: () => formatDoctorTable(result),
|
|
2725
|
+
exitCode: exitCodeForChecks(result.checks)
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
});
|
|
2729
|
+
|
|
2488
2730
|
// src/commands/manifest.ts
|
|
2489
|
-
|
|
2731
|
+
var EXAMPLES = {
|
|
2732
|
+
fetch: ["oura-cli fetch sleep", "oura-cli fetch hr --days 7", "oura-cli fetch workout --from 2026-05-01 --to 2026-05-31"],
|
|
2733
|
+
db: ["oura-cli db today", "oura-cli db week --format json"],
|
|
2734
|
+
report: ["oura-cli report --period week"],
|
|
2735
|
+
doctor: ["oura-cli doctor --offline"]
|
|
2736
|
+
};
|
|
2737
|
+
function buildOpenclawManifest(version, commands) {
|
|
2738
|
+
const m = buildManifest(version, commands);
|
|
2739
|
+
return {
|
|
2740
|
+
id: "oura-cli",
|
|
2741
|
+
version,
|
|
2742
|
+
runtime: "bun",
|
|
2743
|
+
bin: "oura-cli",
|
|
2744
|
+
description: "Oura Ring CLI \u2014 query and analyze Oura Ring health data. Designed for humans and agents.",
|
|
2745
|
+
commands: m.commands.map((c) => ({
|
|
2746
|
+
name: c.name,
|
|
2747
|
+
description: c.description,
|
|
2748
|
+
examples: EXAMPLES[c.name] ?? [`oura-cli ${c.name}`]
|
|
2749
|
+
})),
|
|
2750
|
+
envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH", "OURA_DB_PATH", "OURA_TZ"],
|
|
2751
|
+
healthcheck: { command: "healthcheck", expects: { ok: "boolean", version: "string", latencyMs: "number" } }
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
function manifestCommand(version, getCommands) {
|
|
2490
2755
|
return defineCommand({
|
|
2491
2756
|
meta: { name: "manifest", description: "Print openclaw-tool-registry-compatible manifest as JSON." },
|
|
2492
2757
|
args: {},
|
|
2493
2758
|
run() {
|
|
2494
|
-
console.log(JSON.stringify(
|
|
2495
|
-
id: "oura-cli",
|
|
2496
|
-
version,
|
|
2497
|
-
runtime: "bun",
|
|
2498
|
-
bin: "oura-cli",
|
|
2499
|
-
description: "Oura Ring CLI \u2014 query and analyze Oura Ring health data. Designed for humans and agents.",
|
|
2500
|
-
commands: [
|
|
2501
|
-
{ name: "login", description: "Save an Oura Personal Access Token.", examples: ["oura-cli login"] },
|
|
2502
|
-
{ name: "describe", description: "Emit a machine-readable manifest of commands.", examples: ["oura-cli describe"] },
|
|
2503
|
-
{ name: "sleep", description: "Fetch daily sleep scores from Oura API.", examples: ["oura-cli sleep --start 2026-05-01"] },
|
|
2504
|
-
{ name: "readiness", description: "Fetch daily readiness scores from Oura API.", examples: ["oura-cli readiness --start 2026-05-01"] },
|
|
2505
|
-
{ name: "activity", description: "Fetch daily activity scores from Oura API.", examples: ["oura-cli activity --start 2026-05-01"] },
|
|
2506
|
-
{ name: "hr", description: "Fetch heart rate samples from Oura API.", examples: ["oura-cli hr --start 2026-05-01"] },
|
|
2507
|
-
{ name: "spo2", description: "Fetch blood oxygen (SpO2) data from Oura API.", examples: ["oura-cli spo2 --start 2026-05-01"] },
|
|
2508
|
-
{ name: "stress", description: "Fetch daily stress data from Oura API.", examples: ["oura-cli stress --start 2026-05-01"] },
|
|
2509
|
-
{ name: "workout", description: "Fetch workout data from Oura API.", examples: ["oura-cli workout --start 2026-05-01"] },
|
|
2510
|
-
{ name: "sync", description: "Sync all Oura collections into the local DB.", examples: ["oura-cli sync"] },
|
|
2511
|
-
{ name: "db", description: "Query the local SQLite cache.", examples: ["oura-cli db today"] },
|
|
2512
|
-
{ name: "report", description: "Render a weekly or monthly summary report.", examples: ["oura-cli report --period week"] },
|
|
2513
|
-
{ name: "healthcheck", description: "Quick local DB health probe.", examples: ["oura-cli healthcheck"] },
|
|
2514
|
-
{ name: "manifest", description: "Print openclaw-tool-registry-compatible manifest as JSON.", examples: ["oura-cli manifest"] }
|
|
2515
|
-
],
|
|
2516
|
-
envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH", "OURA_DB_PATH", "OURA_TZ"],
|
|
2517
|
-
healthcheck: { command: "healthcheck", expects: { ok: "boolean", version: "string", latencyMs: "number" } }
|
|
2518
|
-
}, null, 2));
|
|
2759
|
+
console.log(JSON.stringify(buildOpenclawManifest(version, getCommands()), null, 2));
|
|
2519
2760
|
}
|
|
2520
2761
|
});
|
|
2521
2762
|
}
|
|
2522
2763
|
|
|
2523
|
-
// src/commands/
|
|
2524
|
-
function
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
}
|
|
2574
|
-
})
|
|
2575
|
-
}
|
|
2576
|
-
});
|
|
2577
|
-
}
|
|
2764
|
+
// src/commands/fetch.ts
|
|
2765
|
+
function resolveRange(opts) {
|
|
2766
|
+
const modes = [opts.day !== undefined, opts.from !== undefined || opts.to !== undefined, opts.days !== undefined].filter(Boolean).length;
|
|
2767
|
+
if (modes > 1)
|
|
2768
|
+
throw new CliError("BAD_ARGS", "Use only one of --day, --from/--to, or --days.");
|
|
2769
|
+
if (opts.day !== undefined) {
|
|
2770
|
+
const d = assertCalendarDate(opts.day, "--day");
|
|
2771
|
+
return { start: d, end: d };
|
|
2772
|
+
}
|
|
2773
|
+
if (opts.from !== undefined || opts.to !== undefined) {
|
|
2774
|
+
if (opts.from === undefined || opts.to === undefined)
|
|
2775
|
+
throw new CliError("BAD_ARGS", "--from and --to must be given together.");
|
|
2776
|
+
const start = assertCalendarDate(opts.from, "--from");
|
|
2777
|
+
const end = assertCalendarDate(opts.to, "--to");
|
|
2778
|
+
if (start > end)
|
|
2779
|
+
throw new CliError("BAD_ARGS", `--from (${start}) must not be after --to (${end}).`);
|
|
2780
|
+
return { start, end };
|
|
2781
|
+
}
|
|
2782
|
+
if (opts.days !== undefined) {
|
|
2783
|
+
const n = assertPositiveInt(opts.days, "--days");
|
|
2784
|
+
return { start: shiftDay(opts.today, -(n - 1)), end: opts.today };
|
|
2785
|
+
}
|
|
2786
|
+
return { start: opts.today, end: opts.today };
|
|
2787
|
+
}
|
|
2788
|
+
var fetchCommand = dataCommand({
|
|
2789
|
+
meta: { name: "fetch", description: "Fetch raw records for one Oura collection straight from the API (JSON)." },
|
|
2790
|
+
args: {
|
|
2791
|
+
collection: { type: "positional", required: true, description: `Collection: ${names().join(" | ")}` },
|
|
2792
|
+
day: { type: "string", description: "Single day (YYYY-MM-DD). Default: today." },
|
|
2793
|
+
from: { type: "string", description: "Range start (YYYY-MM-DD); requires --to" },
|
|
2794
|
+
to: { type: "string", description: "Range end (YYYY-MM-DD); requires --from" },
|
|
2795
|
+
days: { type: "string", description: "Last N days ending today" }
|
|
2796
|
+
},
|
|
2797
|
+
jsonOnly: true,
|
|
2798
|
+
async run(ctx, args) {
|
|
2799
|
+
const c = byName(args.collection);
|
|
2800
|
+
if (!c)
|
|
2801
|
+
throw new CliError("BAD_ARGS", `Unknown collection "${args.collection}".`, `Valid collections: ${names().join(", ")}`);
|
|
2802
|
+
const { start, end } = resolveRange({
|
|
2803
|
+
day: args.day,
|
|
2804
|
+
from: args.from,
|
|
2805
|
+
to: args.to,
|
|
2806
|
+
days: args.days,
|
|
2807
|
+
today: ctx.today
|
|
2808
|
+
});
|
|
2809
|
+
const client = new OuraClient(args.token ? { token: args.token } : {});
|
|
2810
|
+
const data = await fetchCollection(client, c, start, end, ctx.tz);
|
|
2811
|
+
return { json: data, text: () => JSON.stringify(data, null, 2) };
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2578
2814
|
|
|
2579
2815
|
// src/lib/argv-normalize.ts
|
|
2580
2816
|
var GLOBAL_FLAGS_WITH_VALUE = new Set(["--format", "--token", "--db", "--tz"]);
|
|
@@ -2583,14 +2819,9 @@ var SUBCOMMANDS = new Set([
|
|
|
2583
2819
|
"login",
|
|
2584
2820
|
"describe",
|
|
2585
2821
|
"healthcheck",
|
|
2822
|
+
"doctor",
|
|
2586
2823
|
"manifest",
|
|
2587
|
-
"
|
|
2588
|
-
"readiness",
|
|
2589
|
-
"activity",
|
|
2590
|
-
"hr",
|
|
2591
|
-
"spo2",
|
|
2592
|
-
"stress",
|
|
2593
|
-
"workout",
|
|
2824
|
+
"fetch",
|
|
2594
2825
|
"sync",
|
|
2595
2826
|
"db",
|
|
2596
2827
|
"report"
|
|
@@ -2631,10 +2862,21 @@ function normalizeArgv(argv) {
|
|
|
2631
2862
|
}
|
|
2632
2863
|
|
|
2633
2864
|
// src/index.ts
|
|
2634
|
-
var VERSION = "
|
|
2865
|
+
var VERSION = JSON.parse(readFileSync2(new URL("../package.json", import.meta.url), "utf-8")).version;
|
|
2635
2866
|
if (process.argv.includes("--no-color") || process.env.NO_COLOR) {
|
|
2636
2867
|
source_default.level = 0;
|
|
2637
2868
|
}
|
|
2869
|
+
var subCommands = {
|
|
2870
|
+
login: loginCommand,
|
|
2871
|
+
describe: describeCommand(VERSION, () => subCommands),
|
|
2872
|
+
healthcheck: healthcheckCommand(VERSION),
|
|
2873
|
+
doctor: doctorCommand,
|
|
2874
|
+
manifest: manifestCommand(VERSION, () => subCommands),
|
|
2875
|
+
fetch: fetchCommand,
|
|
2876
|
+
sync: syncCommand,
|
|
2877
|
+
db: dbCommand,
|
|
2878
|
+
report: reportCommand
|
|
2879
|
+
};
|
|
2638
2880
|
var main = defineCommand({
|
|
2639
2881
|
meta: {
|
|
2640
2882
|
name: "oura-cli",
|
|
@@ -2642,22 +2884,7 @@ var main = defineCommand({
|
|
|
2642
2884
|
description: "Oura Ring CLI \u2014 query and analyze Oura Ring health data. Designed for humans and agents."
|
|
2643
2885
|
},
|
|
2644
2886
|
args: { ...commonArgs },
|
|
2645
|
-
subCommands
|
|
2646
|
-
login: loginCommand,
|
|
2647
|
-
describe: describeCommand(VERSION),
|
|
2648
|
-
healthcheck: healthcheckCommand(VERSION),
|
|
2649
|
-
manifest: manifestCommand(VERSION),
|
|
2650
|
-
sleep: createApiCommand("sleep", "Fetch daily sleep scores from Oura API.", "daily_sleep"),
|
|
2651
|
-
readiness: createApiCommand("readiness", "Fetch daily readiness scores from Oura API.", "daily_readiness"),
|
|
2652
|
-
activity: createApiCommand("activity", "Fetch daily activity scores from Oura API.", "daily_activity"),
|
|
2653
|
-
hr: createApiCommand("hr", "Fetch heart rate samples from Oura API.", "heartrate"),
|
|
2654
|
-
spo2: createApiCommand("spo2", "Fetch blood oxygen (SpO2) data from Oura API.", "daily_spo2"),
|
|
2655
|
-
stress: createApiCommand("stress", "Fetch daily stress data from Oura API.", "daily_stress"),
|
|
2656
|
-
workout: createApiCommand("workout", "Fetch workout data from Oura API.", "workout"),
|
|
2657
|
-
sync: syncCommand,
|
|
2658
|
-
db: dbCommand,
|
|
2659
|
-
report: reportCommand
|
|
2660
|
-
}
|
|
2887
|
+
subCommands
|
|
2661
2888
|
});
|
|
2662
2889
|
var normalized = normalizeArgv(process.argv);
|
|
2663
2890
|
runMain(main, { rawArgs: normalized.slice(2) });
|