@drakulavich/oura-cli 0.5.0 → 0.5.2
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 +37 -0
- package/README.md +11 -9
- package/dist/index.js +924 -734
- package/docs/schemas/doctor.json +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1105,9 +1105,9 @@ function _getBuiltinFlags(long, short, userNames, userAliases) {
|
|
|
1105
1105
|
|
|
1106
1106
|
// src/commands/login.ts
|
|
1107
1107
|
init_source();
|
|
1108
|
-
import { writeFileSync, chmodSync, mkdirSync } from "fs";
|
|
1109
|
-
import { resolve, dirname } from "path";
|
|
1110
|
-
import { homedir } from "os";
|
|
1108
|
+
import { writeFileSync, chmodSync as chmodSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
1109
|
+
import { resolve as resolve3, dirname as dirname2 } from "path";
|
|
1110
|
+
import { homedir as homedir3 } from "os";
|
|
1111
1111
|
|
|
1112
1112
|
// src/lib/errors.ts
|
|
1113
1113
|
init_source();
|
|
@@ -1144,8 +1144,8 @@ function redactSecrets(s) {
|
|
|
1144
1144
|
}
|
|
1145
1145
|
function formatError(err, format) {
|
|
1146
1146
|
const code = err instanceof CliError ? err.code : "UNKNOWN";
|
|
1147
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1148
|
-
const hint = err instanceof CliError ? err.hint : undefined;
|
|
1147
|
+
const message = redactSecrets(err instanceof Error ? err.message : String(err));
|
|
1148
|
+
const hint = err instanceof CliError && err.hint ? redactSecrets(err.hint) : undefined;
|
|
1149
1149
|
if (format === "json") {
|
|
1150
1150
|
return {
|
|
1151
1151
|
kind: "json",
|
|
@@ -1171,110 +1171,407 @@ var commonArgs = {
|
|
|
1171
1171
|
"no-color": { type: "boolean", default: false, description: "Disable ANSI colors (also honors NO_COLOR env)" }
|
|
1172
1172
|
};
|
|
1173
1173
|
|
|
1174
|
-
// src/commands/
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1174
|
+
// src/commands/run-command.ts
|
|
1175
|
+
init_source();
|
|
1176
|
+
|
|
1177
|
+
// src/db/open.ts
|
|
1178
|
+
import { Database, SQLiteError } from "bun:sqlite";
|
|
1179
|
+
import { resolve, dirname } from "path";
|
|
1180
|
+
import { homedir } from "os";
|
|
1181
|
+
import { chmodSync, existsSync, mkdirSync } from "fs";
|
|
1182
|
+
|
|
1183
|
+
// src/db/migrations.ts
|
|
1184
|
+
var MIGRATIONS = [
|
|
1185
|
+
{
|
|
1186
|
+
version: 1,
|
|
1187
|
+
sql: `
|
|
1188
|
+
CREATE TABLE IF NOT EXISTS daily_sleep (
|
|
1189
|
+
id TEXT PRIMARY KEY,
|
|
1190
|
+
day TEXT UNIQUE,
|
|
1191
|
+
score INTEGER,
|
|
1192
|
+
contributors TEXT,
|
|
1193
|
+
timestamp TEXT
|
|
1194
|
+
);
|
|
1195
|
+
CREATE TABLE IF NOT EXISTS daily_readiness (
|
|
1196
|
+
id TEXT PRIMARY KEY,
|
|
1197
|
+
day TEXT UNIQUE,
|
|
1198
|
+
score INTEGER,
|
|
1199
|
+
contributors TEXT,
|
|
1200
|
+
temperature_deviation REAL,
|
|
1201
|
+
temperature_trend_deviation REAL,
|
|
1202
|
+
timestamp TEXT
|
|
1203
|
+
);
|
|
1204
|
+
CREATE TABLE IF NOT EXISTS daily_activity (
|
|
1205
|
+
id TEXT PRIMARY KEY,
|
|
1206
|
+
day TEXT UNIQUE,
|
|
1207
|
+
score INTEGER,
|
|
1208
|
+
active_calories INTEGER,
|
|
1209
|
+
steps INTEGER,
|
|
1210
|
+
equivalent_walking_distance REAL,
|
|
1211
|
+
high_activity_time INTEGER,
|
|
1212
|
+
medium_activity_time INTEGER,
|
|
1213
|
+
low_activity_time INTEGER,
|
|
1214
|
+
sedentary_time INTEGER,
|
|
1215
|
+
total_calories INTEGER,
|
|
1216
|
+
target_calories INTEGER,
|
|
1217
|
+
contributors TEXT,
|
|
1218
|
+
timestamp TEXT
|
|
1219
|
+
);
|
|
1220
|
+
CREATE TABLE IF NOT EXISTS daily_spo2 (
|
|
1221
|
+
id TEXT PRIMARY KEY,
|
|
1222
|
+
day TEXT UNIQUE,
|
|
1223
|
+
spo2_average REAL,
|
|
1224
|
+
breathing_disturbance_index REAL
|
|
1225
|
+
);
|
|
1226
|
+
CREATE TABLE IF NOT EXISTS daily_stress (
|
|
1227
|
+
id TEXT PRIMARY KEY,
|
|
1228
|
+
day TEXT UNIQUE,
|
|
1229
|
+
day_summary TEXT,
|
|
1230
|
+
recovery_high INTEGER,
|
|
1231
|
+
stress_high INTEGER
|
|
1232
|
+
);
|
|
1233
|
+
CREATE TABLE IF NOT EXISTS heartrate (
|
|
1234
|
+
timestamp TEXT,
|
|
1235
|
+
bpm INTEGER,
|
|
1236
|
+
source TEXT,
|
|
1237
|
+
day TEXT
|
|
1238
|
+
);
|
|
1239
|
+
CREATE INDEX IF NOT EXISTS idx_heartrate_ts ON heartrate(timestamp);
|
|
1240
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_heartrate_unique ON heartrate(timestamp, source);
|
|
1241
|
+
CREATE INDEX IF NOT EXISTS idx_heartrate_day ON heartrate(day);
|
|
1242
|
+
CREATE TABLE IF NOT EXISTS vo2max (
|
|
1243
|
+
id TEXT PRIMARY KEY,
|
|
1244
|
+
day TEXT UNIQUE,
|
|
1245
|
+
vo2_max REAL,
|
|
1246
|
+
timestamp TEXT
|
|
1247
|
+
);
|
|
1248
|
+
CREATE TABLE IF NOT EXISTS cardiovascular_age (
|
|
1249
|
+
id TEXT PRIMARY KEY,
|
|
1250
|
+
day TEXT UNIQUE,
|
|
1251
|
+
vascular_age INTEGER
|
|
1252
|
+
);
|
|
1253
|
+
CREATE TABLE IF NOT EXISTS workouts (
|
|
1254
|
+
id TEXT PRIMARY KEY,
|
|
1255
|
+
day TEXT,
|
|
1256
|
+
activity TEXT,
|
|
1257
|
+
calories REAL,
|
|
1258
|
+
distance REAL,
|
|
1259
|
+
start_datetime TEXT,
|
|
1260
|
+
end_datetime TEXT,
|
|
1261
|
+
intensity TEXT,
|
|
1262
|
+
label TEXT,
|
|
1263
|
+
source TEXT
|
|
1264
|
+
);
|
|
1265
|
+
CREATE TABLE IF NOT EXISTS sleep_model (
|
|
1266
|
+
id TEXT PRIMARY KEY,
|
|
1267
|
+
day TEXT,
|
|
1268
|
+
average_breath REAL,
|
|
1269
|
+
average_heart_rate REAL,
|
|
1270
|
+
average_hrv REAL,
|
|
1271
|
+
awake_time INTEGER,
|
|
1272
|
+
bedtime_end TEXT,
|
|
1273
|
+
bedtime_start TEXT,
|
|
1274
|
+
deep_sleep_duration INTEGER,
|
|
1275
|
+
efficiency INTEGER,
|
|
1276
|
+
latency INTEGER,
|
|
1277
|
+
light_sleep_duration INTEGER,
|
|
1278
|
+
lowest_heart_rate INTEGER,
|
|
1279
|
+
period INTEGER,
|
|
1280
|
+
rem_sleep_duration INTEGER,
|
|
1281
|
+
restless_periods INTEGER,
|
|
1282
|
+
time_in_bed INTEGER,
|
|
1283
|
+
total_sleep_duration INTEGER,
|
|
1284
|
+
type TEXT
|
|
1285
|
+
);
|
|
1286
|
+
`
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
version: 2,
|
|
1290
|
+
sql: `
|
|
1291
|
+
CREATE VIEW IF NOT EXISTS v_weekly_sleep AS
|
|
1292
|
+
SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score, COUNT(*) as days
|
|
1293
|
+
FROM daily_sleep WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
|
|
1294
|
+
|
|
1295
|
+
CREATE VIEW IF NOT EXISTS v_weekly_readiness AS
|
|
1296
|
+
SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score,
|
|
1297
|
+
ROUND(AVG(temperature_deviation),2) as avg_temp_dev, COUNT(*) as days
|
|
1298
|
+
FROM daily_readiness WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
|
|
1299
|
+
|
|
1300
|
+
CREATE VIEW IF NOT EXISTS v_weekly_activity AS
|
|
1301
|
+
SELECT strftime('%Y-W%W', day) as week, ROUND(AVG(score),1) as avg_score,
|
|
1302
|
+
SUM(steps) as total_steps, SUM(active_calories) as total_active_cal, COUNT(*) as days
|
|
1303
|
+
FROM daily_activity WHERE score IS NOT NULL GROUP BY week ORDER BY week DESC;
|
|
1304
|
+
|
|
1305
|
+
CREATE VIEW IF NOT EXISTS v_sleep_detail AS
|
|
1306
|
+
SELECT day, ROUND(total_sleep_duration/3600.0,1) as sleep_hours,
|
|
1307
|
+
ROUND(deep_sleep_duration/3600.0,1) as deep_hours,
|
|
1308
|
+
ROUND(rem_sleep_duration/3600.0,1) as rem_hours,
|
|
1309
|
+
average_hrv, average_heart_rate as avg_hr, lowest_heart_rate as lowest_hr, efficiency
|
|
1310
|
+
FROM sleep_model ORDER BY day DESC;
|
|
1311
|
+
`
|
|
1178
1312
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
-
});
|
|
1313
|
+
];
|
|
1314
|
+
|
|
1315
|
+
// src/db/open.ts
|
|
1316
|
+
var DB_HINT = "Check the path in --db / OURA_DB_PATH and that the file is a SQLite database oura-cli created.";
|
|
1317
|
+
var BUSY_HINT = "Another oura-cli process is using this database; wait for it to finish and retry.";
|
|
1318
|
+
var BUSY_TIMEOUT_MS = 5000;
|
|
1319
|
+
function dbError(what, err) {
|
|
1320
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
1321
|
+
const hint = /database is locked|SQLITE_BUSY/i.test(detail) ? BUSY_HINT : DB_HINT;
|
|
1322
|
+
return new CliError("DB_ERROR", `${what}: ${detail}`, hint);
|
|
1213
1323
|
}
|
|
1214
|
-
function
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1324
|
+
function asDbError(err) {
|
|
1325
|
+
return err instanceof SQLiteError ? dbError("Database query failed", err) : undefined;
|
|
1326
|
+
}
|
|
1327
|
+
function getDbPath(explicit) {
|
|
1328
|
+
if (explicit)
|
|
1329
|
+
return explicit;
|
|
1330
|
+
if (process.env.OURA_DB_PATH)
|
|
1331
|
+
return process.env.OURA_DB_PATH;
|
|
1332
|
+
return resolve(homedir(), ".oura-cli", "oura.db");
|
|
1333
|
+
}
|
|
1334
|
+
function openDatabase(explicit) {
|
|
1335
|
+
const dbPath = getDbPath(explicit);
|
|
1336
|
+
try {
|
|
1337
|
+
const onDisk = dbPath !== ":memory:";
|
|
1338
|
+
const isNew = onDisk && !existsSync(dbPath);
|
|
1339
|
+
if (onDisk)
|
|
1340
|
+
mkdirSync(dirname(dbPath), { recursive: true, mode: 448 });
|
|
1341
|
+
const db = new Database(dbPath);
|
|
1342
|
+
if (isNew)
|
|
1343
|
+
chmodSync(dbPath, 384);
|
|
1344
|
+
db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
|
|
1345
|
+
const mode = db.query("PRAGMA journal_mode").get();
|
|
1346
|
+
if (mode.journal_mode !== "wal")
|
|
1347
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
1348
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
1349
|
+
return db;
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
throw dbError(`Cannot open database ${dbPath}`, err);
|
|
1223
1352
|
}
|
|
1224
1353
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
if (
|
|
1235
|
-
|
|
1236
|
-
|
|
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);
|
|
1354
|
+
function schemaVersion(db) {
|
|
1355
|
+
db.exec("CREATE TABLE IF NOT EXISTS _schema_version (version INTEGER NOT NULL)");
|
|
1356
|
+
const row = db.query("SELECT MAX(version) AS v FROM _schema_version").get();
|
|
1357
|
+
return row?.v ?? 0;
|
|
1358
|
+
}
|
|
1359
|
+
function ensureSchema(db, migrations = MIGRATIONS) {
|
|
1360
|
+
try {
|
|
1361
|
+
const current = schemaVersion(db);
|
|
1362
|
+
for (const m of migrations) {
|
|
1363
|
+
if (m.version > current) {
|
|
1364
|
+
db.exec(m.sql);
|
|
1365
|
+
db.query("INSERT INTO _schema_version (version) VALUES (?)").run(m.version);
|
|
1243
1366
|
}
|
|
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));
|
|
1249
1367
|
}
|
|
1368
|
+
} catch (err) {
|
|
1369
|
+
throw dbError("Schema migration failed", err);
|
|
1250
1370
|
}
|
|
1251
|
-
});
|
|
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
1371
|
}
|
|
1274
|
-
|
|
1275
|
-
|
|
1372
|
+
|
|
1373
|
+
// src/api/token.ts
|
|
1374
|
+
import { readFileSync } from "fs";
|
|
1375
|
+
import { resolve as resolve2 } from "path";
|
|
1376
|
+
import { homedir as homedir2 } from "os";
|
|
1377
|
+
function defaultTokenPath() {
|
|
1378
|
+
return process.env.OURA_TOKEN_PATH ?? resolve2(homedir2(), ".oura-token");
|
|
1276
1379
|
}
|
|
1277
|
-
function
|
|
1380
|
+
function resolveToken(explicit, tokenPath) {
|
|
1381
|
+
if (explicit)
|
|
1382
|
+
return { token: explicit.trim(), source: "--token" };
|
|
1383
|
+
if (process.env.OURA_TOKEN)
|
|
1384
|
+
return { token: process.env.OURA_TOKEN.trim(), source: "OURA_TOKEN" };
|
|
1385
|
+
const path = tokenPath ?? defaultTokenPath();
|
|
1386
|
+
try {
|
|
1387
|
+
return { token: readFileSync(path, "utf-8").trim(), source: path };
|
|
1388
|
+
} catch {
|
|
1389
|
+
return { token: null, source: path };
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// src/api/client.ts
|
|
1394
|
+
var BASE_URL = "https://api.ouraring.com/v2/usercollection";
|
|
1395
|
+
var MAX_PAGES = 1e4;
|
|
1396
|
+
|
|
1397
|
+
class OuraClient {
|
|
1398
|
+
token;
|
|
1399
|
+
constructor(options = {}) {
|
|
1400
|
+
const { token, source } = resolveToken(options.token, options.tokenPath);
|
|
1401
|
+
if (!token) {
|
|
1402
|
+
throw new CliError("TOKEN_MISSING", `No Oura access token at ${source}.`, "Run `oura-cli login` or set OURA_TOKEN.");
|
|
1403
|
+
}
|
|
1404
|
+
if (/\s/.test(token)) {
|
|
1405
|
+
throw new CliError("TOKEN_INVALID", `The token from ${source} contains whitespace or a line break; a token is a single line.`, "Fix the file or variable, or run `oura-cli login` again.");
|
|
1406
|
+
}
|
|
1407
|
+
this.token = token;
|
|
1408
|
+
}
|
|
1409
|
+
async fetch(endpoint, query) {
|
|
1410
|
+
const rows = [];
|
|
1411
|
+
const seenTokens = new Set;
|
|
1412
|
+
let nextToken = null;
|
|
1413
|
+
do {
|
|
1414
|
+
if (seenTokens.size >= MAX_PAGES) {
|
|
1415
|
+
throw new CliError("API_ERROR", `Oura API returned more than ${MAX_PAGES} pages for ${endpoint}; stopping.`);
|
|
1416
|
+
}
|
|
1417
|
+
const params = new URLSearchParams(query);
|
|
1418
|
+
if (nextToken)
|
|
1419
|
+
params.set("next_token", nextToken);
|
|
1420
|
+
const page = await this.getPage(`${BASE_URL}/${endpoint}?${params}`);
|
|
1421
|
+
for (const row of page.data)
|
|
1422
|
+
rows.push(row);
|
|
1423
|
+
nextToken = page.next_token;
|
|
1424
|
+
if (nextToken && seenTokens.has(nextToken)) {
|
|
1425
|
+
throw new CliError("API_ERROR", `Oura API repeated pagination token for ${endpoint}; stopping to avoid a loop.`);
|
|
1426
|
+
}
|
|
1427
|
+
if (nextToken)
|
|
1428
|
+
seenTokens.add(nextToken);
|
|
1429
|
+
} while (nextToken);
|
|
1430
|
+
return rows;
|
|
1431
|
+
}
|
|
1432
|
+
async getPage(url) {
|
|
1433
|
+
const response = await fetch(url, {
|
|
1434
|
+
headers: { Authorization: `Bearer ${this.token}` }
|
|
1435
|
+
});
|
|
1436
|
+
if (!response.ok) {
|
|
1437
|
+
const rawBody = await response.text();
|
|
1438
|
+
const redacted = redactSecrets(rawBody);
|
|
1439
|
+
const body = redacted.length > 200 ? redacted.slice(0, 200) + "\u2026 (truncated)" : redacted;
|
|
1440
|
+
if (response.status === 401 || response.status === 403) {
|
|
1441
|
+
throw new CliError("TOKEN_INVALID", `Oura API ${response.status}: ${body}`, "Run `oura-cli login` with a fresh Personal Access Token, or check OURA_TOKEN.");
|
|
1442
|
+
}
|
|
1443
|
+
throw new CliError("API_ERROR", `Oura API ${response.status}: ${body}`);
|
|
1444
|
+
}
|
|
1445
|
+
let json;
|
|
1446
|
+
try {
|
|
1447
|
+
json = await response.json();
|
|
1448
|
+
} catch {
|
|
1449
|
+
throw new CliError("API_ERROR", "Empty response body from Oura API.");
|
|
1450
|
+
}
|
|
1451
|
+
const body = json;
|
|
1452
|
+
return { data: body.data ?? [], next_token: body.next_token ?? null };
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/lib/argv-normalize.ts
|
|
1457
|
+
var GLOBAL_FLAGS_WITH_VALUE = new Set(["--format", "--token", "--db", "--tz"]);
|
|
1458
|
+
var GLOBAL_FLAGS_BOOLEAN = new Set(["--no-color"]);
|
|
1459
|
+
var SUBCOMMANDS = new Set([
|
|
1460
|
+
"login",
|
|
1461
|
+
"describe",
|
|
1462
|
+
"healthcheck",
|
|
1463
|
+
"doctor",
|
|
1464
|
+
"manifest",
|
|
1465
|
+
"fetch",
|
|
1466
|
+
"sync",
|
|
1467
|
+
"db",
|
|
1468
|
+
"report"
|
|
1469
|
+
]);
|
|
1470
|
+
function normalizeArgv(argv) {
|
|
1471
|
+
const [bun, script, ...rest] = argv;
|
|
1472
|
+
const subIdx = rest.findIndex((a) => SUBCOMMANDS.has(a));
|
|
1473
|
+
if (subIdx < 0)
|
|
1474
|
+
return argv;
|
|
1475
|
+
const before = rest.slice(0, subIdx);
|
|
1476
|
+
const subcommandOnwards = rest.slice(subIdx);
|
|
1477
|
+
const hoisted = [];
|
|
1478
|
+
const leftover = [];
|
|
1479
|
+
for (let i = 0;i < before.length; i++) {
|
|
1480
|
+
const tok = before[i];
|
|
1481
|
+
if (tok.includes("=")) {
|
|
1482
|
+
const name = tok.slice(0, tok.indexOf("="));
|
|
1483
|
+
if (GLOBAL_FLAGS_WITH_VALUE.has(name) || GLOBAL_FLAGS_BOOLEAN.has(name)) {
|
|
1484
|
+
hoisted.push(tok);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
if (GLOBAL_FLAGS_WITH_VALUE.has(tok)) {
|
|
1489
|
+
hoisted.push(tok);
|
|
1490
|
+
if (i + 1 < before.length) {
|
|
1491
|
+
hoisted.push(before[i + 1]);
|
|
1492
|
+
i++;
|
|
1493
|
+
}
|
|
1494
|
+
continue;
|
|
1495
|
+
}
|
|
1496
|
+
if (GLOBAL_FLAGS_BOOLEAN.has(tok)) {
|
|
1497
|
+
hoisted.push(tok);
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
leftover.push(tok);
|
|
1501
|
+
}
|
|
1502
|
+
const dd = subcommandOnwards.indexOf("--");
|
|
1503
|
+
if (dd >= 0) {
|
|
1504
|
+
return [bun, script, ...leftover, ...subcommandOnwards.slice(0, dd), ...hoisted, ...subcommandOnwards.slice(dd)];
|
|
1505
|
+
}
|
|
1506
|
+
return [bun, script, ...leftover, ...subcommandOnwards, ...hoisted];
|
|
1507
|
+
}
|
|
1508
|
+
function isVersionRequest(rawArgs) {
|
|
1509
|
+
for (let i = 0;i < rawArgs.length; i++) {
|
|
1510
|
+
const tok = rawArgs[i];
|
|
1511
|
+
if (SUBCOMMANDS.has(tok))
|
|
1512
|
+
return false;
|
|
1513
|
+
if (GLOBAL_FLAGS_WITH_VALUE.has(tok)) {
|
|
1514
|
+
i++;
|
|
1515
|
+
continue;
|
|
1516
|
+
}
|
|
1517
|
+
if (tok === "--version" || tok === "-v")
|
|
1518
|
+
return true;
|
|
1519
|
+
}
|
|
1520
|
+
return false;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// src/lib/format-resolve.ts
|
|
1524
|
+
function resolveFormat({ explicit, isTty }) {
|
|
1525
|
+
if (explicit === undefined)
|
|
1526
|
+
return isTty ? "table" : "json";
|
|
1527
|
+
if (explicit === "table" || explicit === "json")
|
|
1528
|
+
return explicit;
|
|
1529
|
+
throw new CliError("BAD_ARGS", `Unknown --format value: "${explicit}". Use "table" or "json".`);
|
|
1530
|
+
}
|
|
1531
|
+
function formatFromArgv(argv, isTty) {
|
|
1532
|
+
let explicit;
|
|
1533
|
+
for (let i = 0;i < argv.length; i++) {
|
|
1534
|
+
const a = argv[i];
|
|
1535
|
+
if (a === "--format") {
|
|
1536
|
+
explicit = argv[i + 1];
|
|
1537
|
+
i++;
|
|
1538
|
+
} else if (a.startsWith("--format="))
|
|
1539
|
+
explicit = a.slice("--format=".length);
|
|
1540
|
+
else if (GLOBAL_FLAGS_WITH_VALUE.has(a))
|
|
1541
|
+
i++;
|
|
1542
|
+
}
|
|
1543
|
+
try {
|
|
1544
|
+
return resolveFormat({ explicit, isTty });
|
|
1545
|
+
} catch {
|
|
1546
|
+
return isTty ? "table" : "json";
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// src/lib/time.ts
|
|
1551
|
+
function nowUtc() {
|
|
1552
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
1553
|
+
}
|
|
1554
|
+
function formatLocal(utcStr, timezone) {
|
|
1555
|
+
const dt = new Date(utcStr);
|
|
1556
|
+
const parts = new Intl.DateTimeFormat("sv-SE", {
|
|
1557
|
+
timeZone: timezone,
|
|
1558
|
+
year: "numeric",
|
|
1559
|
+
month: "2-digit",
|
|
1560
|
+
day: "2-digit",
|
|
1561
|
+
hour: "2-digit",
|
|
1562
|
+
minute: "2-digit",
|
|
1563
|
+
hour12: false
|
|
1564
|
+
}).formatToParts(dt);
|
|
1565
|
+
const get = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
1566
|
+
return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}`;
|
|
1567
|
+
}
|
|
1568
|
+
function formatLocalDate(utcStr, timezone) {
|
|
1569
|
+
return formatLocal(utcStr, timezone).split(" ")[0];
|
|
1570
|
+
}
|
|
1571
|
+
function todayLocal(timezone) {
|
|
1572
|
+
return formatLocalDate(nowUtc(), timezone);
|
|
1573
|
+
}
|
|
1574
|
+
function getTimezoneOffsetMs(utc, tz) {
|
|
1278
1575
|
const tzPart = new Intl.DateTimeFormat("en-US", {
|
|
1279
1576
|
timeZone: tz,
|
|
1280
1577
|
timeZoneName: "longOffset"
|
|
@@ -1324,72 +1621,254 @@ function isCalendarDate(value) {
|
|
|
1324
1621
|
return !Number.isNaN(ms) && new Date(ms).toISOString().slice(0, 10) === value;
|
|
1325
1622
|
}
|
|
1326
1623
|
|
|
1327
|
-
// src/
|
|
1328
|
-
function
|
|
1329
|
-
|
|
1624
|
+
// src/lib/validate.ts
|
|
1625
|
+
function assertCalendarDate(value, label) {
|
|
1626
|
+
if (!isCalendarDate(value)) {
|
|
1627
|
+
throw new CliError("BAD_ARGS", `${label} must be a real YYYY-MM-DD date, got "${value}".`);
|
|
1628
|
+
}
|
|
1629
|
+
return value;
|
|
1630
|
+
}
|
|
1631
|
+
function assertPositiveInt(value, label) {
|
|
1632
|
+
const n = Number(value);
|
|
1633
|
+
if (!/^\d+$/.test(value.trim()) || !Number.isSafeInteger(n) || n < 1) {
|
|
1634
|
+
throw new CliError("BAD_ARGS", `${label} must be a positive integer, got "${value}".`);
|
|
1635
|
+
}
|
|
1636
|
+
return n;
|
|
1637
|
+
}
|
|
1638
|
+
function assertTimezone(tz) {
|
|
1639
|
+
try {
|
|
1640
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
1641
|
+
return tz;
|
|
1642
|
+
} catch {
|
|
1643
|
+
throw new CliError("BAD_ARGS", `Unknown timezone "${tz}".`, "Use an IANA name such as Europe/Berlin (env: OURA_TZ, flag: --tz).");
|
|
1644
|
+
}
|
|
1330
1645
|
}
|
|
1331
1646
|
|
|
1332
|
-
// src/
|
|
1333
|
-
var
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1647
|
+
// src/commands/run-command.ts
|
|
1648
|
+
var processIo = {
|
|
1649
|
+
stdout: (s) => {
|
|
1650
|
+
process.stdout.write(s + `
|
|
1651
|
+
`);
|
|
1652
|
+
},
|
|
1653
|
+
stderr: (s) => {
|
|
1654
|
+
process.stderr.write(s + `
|
|
1655
|
+
`);
|
|
1656
|
+
},
|
|
1657
|
+
exit: (code) => process.exit(code),
|
|
1658
|
+
isTty: process.stdout.isTTY === true
|
|
1659
|
+
};
|
|
1660
|
+
var camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1661
|
+
function assertKnownArgs(declared, args) {
|
|
1662
|
+
const known = new Set(["_"]);
|
|
1663
|
+
let positionals = 0;
|
|
1664
|
+
for (const [name, def] of Object.entries(declared)) {
|
|
1665
|
+
known.add(name);
|
|
1666
|
+
known.add(camel(name));
|
|
1667
|
+
if (name.startsWith("no-"))
|
|
1668
|
+
known.add(name.slice(3));
|
|
1669
|
+
for (const alias of [def.alias ?? []].flat())
|
|
1670
|
+
known.add(alias);
|
|
1671
|
+
if (def.type === "positional")
|
|
1672
|
+
positionals++;
|
|
1673
|
+
}
|
|
1674
|
+
const unknown = Object.keys(args).filter((k) => !known.has(k));
|
|
1675
|
+
if (unknown.length > 0) {
|
|
1676
|
+
const flags = unknown.map((f) => f.length === 1 ? `-${f}` : `--${f}`).join(", ");
|
|
1677
|
+
const hint = unknown.every((f) => f.length === 1) ? 'oura-cli has no single-letter flags; a value that starts with "-" must come after "--".' : "Run the command with --help to see its flags.";
|
|
1678
|
+
throw new CliError("BAD_ARGS", `Unknown flag${unknown.length > 1 ? "s" : ""}: ${flags}.`, hint);
|
|
1679
|
+
}
|
|
1680
|
+
const extra = (args._ ?? []).slice(positionals);
|
|
1681
|
+
if (extra.length > 0) {
|
|
1682
|
+
throw new CliError("BAD_ARGS", `Unexpected argument${extra.length > 1 ? "s" : ""}: ${extra.join(" ")}.`, "Run the command with --help to see its arguments.");
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
async function execute(def, args, io = processIo) {
|
|
1686
|
+
if (args["no-color"] === true || args.color === false || process.env.NO_COLOR)
|
|
1687
|
+
source_default.level = 0;
|
|
1688
|
+
let db;
|
|
1689
|
+
let format = io.isTty ? "table" : "json";
|
|
1690
|
+
let exitCode = 0;
|
|
1691
|
+
try {
|
|
1692
|
+
format = resolveFormat({ explicit: args.format, isTty: io.isTty });
|
|
1693
|
+
assertKnownArgs({ ...commonArgs, ...def.args ?? {} }, args);
|
|
1694
|
+
const outputFormat = def.jsonOnly ? "json" : format;
|
|
1695
|
+
const tz = assertTimezone(args.tz ?? resolveDefaultTimezone());
|
|
1696
|
+
const ctx = { format: outputFormat, tz, today: today(tz) };
|
|
1697
|
+
if (def.needs?.db) {
|
|
1698
|
+
db = openDatabase(args.db);
|
|
1699
|
+
ensureSchema(db);
|
|
1700
|
+
ctx.db = db;
|
|
1701
|
+
}
|
|
1702
|
+
if (def.needs?.client) {
|
|
1703
|
+
ctx.client = new OuraClient(args.token ? { token: args.token } : {});
|
|
1704
|
+
}
|
|
1705
|
+
const out = await def.run(ctx, args);
|
|
1706
|
+
io.stdout(outputFormat === "json" ? JSON.stringify(out.json, null, 2) : out.text());
|
|
1707
|
+
exitCode = out.exitCode ?? 0;
|
|
1708
|
+
} catch (raw) {
|
|
1709
|
+
const err = asDbError(raw) ?? raw;
|
|
1710
|
+
io.stderr(formatError(err, format).text);
|
|
1711
|
+
exitCode = exitCodeFor(err);
|
|
1712
|
+
} finally {
|
|
1713
|
+
db?.close();
|
|
1714
|
+
}
|
|
1715
|
+
if (exitCode !== 0)
|
|
1716
|
+
io.exit(exitCode);
|
|
1717
|
+
}
|
|
1718
|
+
function dataCommand(def) {
|
|
1719
|
+
return defineCommand({
|
|
1720
|
+
meta: def.meta,
|
|
1721
|
+
args: { ...commonArgs, ...def.args ?? {} },
|
|
1722
|
+
run: ({ args }) => execute(def, args, processIo)
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// src/commands/login.ts
|
|
1727
|
+
async function readHiddenToken(input, output) {
|
|
1728
|
+
if (!input.isTTY || !input.setRawMode) {
|
|
1729
|
+
throw new CliError("BAD_ARGS", "Interactive login requires a terminal.", "Use `oura-cli login --token` for non-interactive use.");
|
|
1730
|
+
}
|
|
1731
|
+
output.write("Paste your token (input hidden): ");
|
|
1732
|
+
input.setRawMode(true);
|
|
1733
|
+
input.resume();
|
|
1734
|
+
return new Promise((resolve, reject) => {
|
|
1735
|
+
let token = "";
|
|
1736
|
+
const finish = () => {
|
|
1737
|
+
input.off("data", onData);
|
|
1738
|
+
input.setRawMode?.(false);
|
|
1739
|
+
input.pause();
|
|
1740
|
+
output.write(`
|
|
1741
|
+
`);
|
|
1742
|
+
};
|
|
1743
|
+
const onData = (chunk) => {
|
|
1744
|
+
for (const char of chunk.toString("utf8")) {
|
|
1745
|
+
if (char === "\r" || char === `
|
|
1746
|
+
`) {
|
|
1747
|
+
finish();
|
|
1748
|
+
resolve(token);
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
if (char === "\x03" || char === "\x04") {
|
|
1752
|
+
finish();
|
|
1753
|
+
reject(new CliError("BAD_ARGS", "Login cancelled."));
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (char === "\b" || char === "\x7F") {
|
|
1757
|
+
token = token.slice(0, -1);
|
|
1758
|
+
} else if (char >= " ") {
|
|
1759
|
+
token += char;
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
};
|
|
1763
|
+
input.on("data", onData);
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
function writeToken(path, token) {
|
|
1767
|
+
const trimmed = token.trim();
|
|
1768
|
+
if (trimmed.length === 0) {
|
|
1769
|
+
throw new CliError("BAD_ARGS", "Token cannot be empty.");
|
|
1770
|
+
}
|
|
1771
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
1772
|
+
writeFileSync(path, trimmed, { encoding: "utf-8" });
|
|
1773
|
+
if (process.platform !== "win32") {
|
|
1774
|
+
chmodSync2(path, 384);
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
var loginCommand = defineCommand({
|
|
1778
|
+
meta: { name: "login", description: "Save an Oura Personal Access Token for future commands." },
|
|
1779
|
+
args: {
|
|
1780
|
+
...commonArgs,
|
|
1781
|
+
token: { type: "string", description: "Pass token non-interactively (e.g. for scripts)" },
|
|
1782
|
+
path: { type: "string", description: "Where to save the token (default: $OURA_TOKEN_PATH or ~/.oura-token)" }
|
|
1783
|
+
},
|
|
1784
|
+
async run({ args }) {
|
|
1785
|
+
try {
|
|
1786
|
+
assertKnownArgs({ ...commonArgs, token: {}, path: {} }, args);
|
|
1787
|
+
if (args["no-color"] || process.env.NO_COLOR) {
|
|
1788
|
+
await Promise.resolve().then(() => init_source());
|
|
1789
|
+
source_default.level = 0;
|
|
1790
|
+
}
|
|
1791
|
+
const target = args.path ?? process.env.OURA_TOKEN_PATH ?? resolve3(homedir3(), ".oura-token");
|
|
1792
|
+
let token = args.token;
|
|
1793
|
+
if (!token) {
|
|
1794
|
+
console.log("Get a Personal Access Token at https://cloud.ouraring.com/personal-access-tokens");
|
|
1795
|
+
token = await readHiddenToken(process.stdin, process.stdout);
|
|
1796
|
+
}
|
|
1797
|
+
writeToken(target, token);
|
|
1798
|
+
console.log(source_default.green(`Saved to ${target}`));
|
|
1799
|
+
} catch (err) {
|
|
1800
|
+
emitError(err, "table");
|
|
1801
|
+
process.exit(exitCodeFor(err));
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
});
|
|
1805
|
+
|
|
1806
|
+
// src/collections/types.ts
|
|
1807
|
+
function defineCollection(c) {
|
|
1808
|
+
return c;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/collections/sleep.ts
|
|
1812
|
+
var sleep = defineCollection({
|
|
1813
|
+
name: "sleep",
|
|
1814
|
+
endpoint: "daily_sleep",
|
|
1815
|
+
table: "daily_sleep",
|
|
1816
|
+
description: "Daily sleep score and contributors",
|
|
1817
|
+
conflict: "replace",
|
|
1818
|
+
rangeParams: "date",
|
|
1819
|
+
identity: [
|
|
1820
|
+
{ field: "id", description: "Oura record id" },
|
|
1821
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1822
|
+
],
|
|
1823
|
+
columns: [
|
|
1824
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1825
|
+
{ name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
|
|
1826
|
+
{ name: "score", type: "INTEGER", pick: (r) => r.score },
|
|
1827
|
+
{ name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
|
|
1828
|
+
{ name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
|
|
1829
|
+
]
|
|
1830
|
+
});
|
|
1831
|
+
|
|
1832
|
+
// src/collections/readiness.ts
|
|
1833
|
+
var readiness = defineCollection({
|
|
1834
|
+
name: "readiness",
|
|
1835
|
+
endpoint: "daily_readiness",
|
|
1836
|
+
table: "daily_readiness",
|
|
1837
|
+
description: "Daily readiness score, contributors and temperature deviation",
|
|
1838
|
+
conflict: "replace",
|
|
1839
|
+
rangeParams: "date",
|
|
1840
|
+
identity: [
|
|
1841
|
+
{ field: "id", description: "Oura record id" },
|
|
1842
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1843
|
+
],
|
|
1844
|
+
columns: [
|
|
1845
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1846
|
+
{ name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
|
|
1847
|
+
{ name: "score", type: "INTEGER", pick: (r) => r.score },
|
|
1848
|
+
{ name: "contributors", type: "TEXT", pick: (r) => JSON.stringify(r.contributors) },
|
|
1849
|
+
{ name: "temperature_deviation", type: "REAL", pick: (r) => r.temperature_deviation },
|
|
1850
|
+
{ name: "temperature_trend_deviation", type: "REAL", pick: (r) => r.temperature_trend_deviation },
|
|
1851
|
+
{ name: "timestamp", type: "TEXT", pick: (r) => r.timestamp }
|
|
1852
|
+
]
|
|
1853
|
+
});
|
|
1854
|
+
|
|
1855
|
+
// src/collections/activity.ts
|
|
1856
|
+
var activity = defineCollection({
|
|
1857
|
+
name: "activity",
|
|
1858
|
+
endpoint: "daily_activity",
|
|
1859
|
+
table: "daily_activity",
|
|
1860
|
+
description: "Daily activity score, steps, calories and activity-time buckets",
|
|
1861
|
+
conflict: "replace",
|
|
1862
|
+
rangeParams: "date",
|
|
1863
|
+
identity: [
|
|
1864
|
+
{ field: "id", description: "Oura record id" },
|
|
1865
|
+
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
1866
|
+
],
|
|
1867
|
+
columns: [
|
|
1868
|
+
{ name: "id", type: "TEXT", pk: true, pick: (r) => r.id },
|
|
1869
|
+
{ name: "day", type: "TEXT", unique: true, pick: (r) => r.day },
|
|
1870
|
+
{ name: "score", type: "INTEGER", pick: (r) => r.score },
|
|
1871
|
+
{ name: "active_calories", type: "INTEGER", pick: (r) => r.active_calories },
|
|
1393
1872
|
{ name: "steps", type: "INTEGER", pick: (r) => r.steps },
|
|
1394
1873
|
{ name: "equivalent_walking_distance", type: "REAL", pick: (r) => r.equivalent_walking_distance },
|
|
1395
1874
|
{ name: "high_activity_time", type: "INTEGER", pick: (r) => r.high_activity_time },
|
|
@@ -1475,6 +1954,7 @@ var workout = defineCollection({
|
|
|
1475
1954
|
description: "Workout sessions with activity, calories, distance and intensity",
|
|
1476
1955
|
conflict: "replace",
|
|
1477
1956
|
rangeParams: "date",
|
|
1957
|
+
dayRangeOffset: [0, 1],
|
|
1478
1958
|
identity: [
|
|
1479
1959
|
{ field: "id", description: "Oura record id" },
|
|
1480
1960
|
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
@@ -1501,6 +1981,7 @@ var sleepPeriods = defineCollection({
|
|
|
1501
1981
|
description: "Individual sleep periods with stages, HRV, heart rate and efficiency",
|
|
1502
1982
|
conflict: "replace",
|
|
1503
1983
|
rangeParams: "date",
|
|
1984
|
+
dayRangeOffset: [-1, 0],
|
|
1504
1985
|
identity: [
|
|
1505
1986
|
{ field: "id", description: "Oura record id" },
|
|
1506
1987
|
{ field: "day", format: "date", description: "Date the record applies to (YYYY-MM-DD)" }
|
|
@@ -1575,13 +2056,14 @@ function rowValues(c, row) {
|
|
|
1575
2056
|
return c.columns.map((col) => col.pick(row));
|
|
1576
2057
|
}
|
|
1577
2058
|
var MS_PER_DAY = 86400000;
|
|
1578
|
-
function dateQueries(start, end, maxDays) {
|
|
2059
|
+
function dateQueries(start, end, maxDays, offset) {
|
|
2060
|
+
const query = (s, e) => ({ start_date: shiftDay(s, offset[0]), end_date: shiftDay(e, offset[1]) });
|
|
1579
2061
|
if (!maxDays)
|
|
1580
|
-
return [
|
|
2062
|
+
return [query(start, end)];
|
|
1581
2063
|
const out = [];
|
|
1582
2064
|
for (let s = start;s <= end; s = shiftDay(s, maxDays)) {
|
|
1583
2065
|
const e = shiftDay(s, maxDays - 1);
|
|
1584
|
-
out.push(
|
|
2066
|
+
out.push(query(s, e < end ? e : end));
|
|
1585
2067
|
}
|
|
1586
2068
|
return out;
|
|
1587
2069
|
}
|
|
@@ -1598,7 +2080,7 @@ function datetimeQueries(start, end, tz, maxDays) {
|
|
|
1598
2080
|
function rangeQueries(c, start, end, tz) {
|
|
1599
2081
|
if (start > end)
|
|
1600
2082
|
return [];
|
|
1601
|
-
return c.rangeParams === "date" ? dateQueries(start, end, c.maxRangeDays) : datetimeQueries(start, end, tz, c.maxRangeDays);
|
|
2083
|
+
return c.rangeParams === "date" ? dateQueries(start, end, c.maxRangeDays, c.dayRangeOffset ?? [0, 0]) : datetimeQueries(start, end, tz, c.maxRangeDays);
|
|
1602
2084
|
}
|
|
1603
2085
|
async function fetchCollection(client, c, start, end, tz) {
|
|
1604
2086
|
const rows = [];
|
|
@@ -1610,7 +2092,7 @@ async function fetchCollection(client, c, start, end, tz) {
|
|
|
1610
2092
|
}
|
|
1611
2093
|
|
|
1612
2094
|
// src/commands/describe.ts
|
|
1613
|
-
var OUTPUT_SCHEMAS = { doctor: "docs/schemas/doctor.json" };
|
|
2095
|
+
var OUTPUT_SCHEMAS = { doctor: "docs/schemas/doctor.json", describe: "docs/schemas/describe.json" };
|
|
1614
2096
|
var ENUM_ARGS = {
|
|
1615
2097
|
fetch: { collection: names() },
|
|
1616
2098
|
report: { period: ["week", "month"] }
|
|
@@ -1700,8 +2182,9 @@ function buildManifest(version, commands) {
|
|
|
1700
2182
|
function describeCommand(version, getCommands) {
|
|
1701
2183
|
return defineCommand({
|
|
1702
2184
|
meta: { name: "describe", description: "Emit a machine-readable manifest of commands, args, and outputs." },
|
|
1703
|
-
args: {},
|
|
1704
|
-
run() {
|
|
2185
|
+
args: { ...commonArgs },
|
|
2186
|
+
run({ args }) {
|
|
2187
|
+
assertKnownArgs(commonArgs, args);
|
|
1705
2188
|
console.log(JSON.stringify(buildManifest(version, getCommands()), null, 2));
|
|
1706
2189
|
}
|
|
1707
2190
|
});
|
|
@@ -1709,33 +2192,41 @@ function describeCommand(version, getCommands) {
|
|
|
1709
2192
|
|
|
1710
2193
|
// src/db/sync.ts
|
|
1711
2194
|
var BACKFILL_DAYS = 30;
|
|
1712
|
-
|
|
1713
|
-
|
|
2195
|
+
function lastDay(db, table) {
|
|
2196
|
+
return db.query(`SELECT MAX(day) AS d FROM ${table}`).get().d;
|
|
2197
|
+
}
|
|
2198
|
+
function rowCount(db, table) {
|
|
2199
|
+
return db.query(`SELECT COUNT(*) AS n FROM ${table}`).get().n;
|
|
2200
|
+
}
|
|
2201
|
+
async function importDaily(db, client, clock, log, window = {}) {
|
|
1714
2202
|
const { today, tz } = clock;
|
|
1715
2203
|
const _log = log ?? (() => {});
|
|
1716
|
-
const
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
}
|
|
1722
|
-
const isFirstSync =
|
|
1723
|
-
const startDate =
|
|
1724
|
-
_log(isFirstSync ? `First sync \u2014 backfilling the last ${BACKFILL_DAYS} days: ${startDate} \u2192 ${
|
|
1725
|
-
const
|
|
1726
|
-
|
|
1727
|
-
|
|
2204
|
+
const end = window.to ?? today;
|
|
2205
|
+
const backfillStart = shiftDay(end, -(BACKFILL_DAYS - 1));
|
|
2206
|
+
const plan = COLLECTIONS.map((c) => {
|
|
2207
|
+
const last = lastDay(db, c.table);
|
|
2208
|
+
return { c, last, start: window.from ?? last ?? backfillStart };
|
|
2209
|
+
});
|
|
2210
|
+
const isFirstSync = plan.every((p) => p.last === null);
|
|
2211
|
+
const startDate = plan.map((p) => p.start).sort()[0];
|
|
2212
|
+
_log(isFirstSync && window.from === undefined ? `First sync \u2014 backfilling the last ${BACKFILL_DAYS} days: ${startDate} \u2192 ${end}` : `Syncing ${startDate} \u2192 ${end}`);
|
|
2213
|
+
const fetched = {};
|
|
2214
|
+
const added = {};
|
|
2215
|
+
for (const { c, start } of plan) {
|
|
2216
|
+
const rows = await fetchCollection(client, c, start, end, tz);
|
|
2217
|
+
const before = rowCount(db, c.table);
|
|
1728
2218
|
const stmt = db.query(insertSql(c));
|
|
1729
2219
|
db.transaction((rs) => {
|
|
1730
2220
|
for (const r of rs)
|
|
1731
2221
|
stmt.run(...rowValues(c, r));
|
|
1732
2222
|
})(rows);
|
|
1733
|
-
|
|
2223
|
+
fetched[c.table] = rows.length;
|
|
2224
|
+
added[c.table] = rowCount(db, c.table) - before;
|
|
1734
2225
|
if (rows.length > 0)
|
|
1735
|
-
_log(` + ${c.table}: ${rows.length}
|
|
2226
|
+
_log(` + ${c.table}: ${rows.length} fetched, ${added[c.table]} new`);
|
|
1736
2227
|
}
|
|
1737
2228
|
_log("Import complete.");
|
|
1738
|
-
return { startDate, endDate:
|
|
2229
|
+
return { startDate, endDate: end, fetched, added, isFirstSync };
|
|
1739
2230
|
}
|
|
1740
2231
|
|
|
1741
2232
|
// src/db/queries.ts
|
|
@@ -1764,7 +2255,7 @@ function getDaySummary(db, day) {
|
|
|
1764
2255
|
};
|
|
1765
2256
|
}
|
|
1766
2257
|
function getTrends(db, days, today) {
|
|
1767
|
-
const start = shiftDay(today, -days);
|
|
2258
|
+
const start = shiftDay(today, -(days - 1));
|
|
1768
2259
|
const results = [];
|
|
1769
2260
|
const metrics = [
|
|
1770
2261
|
["Sleep Score", "daily_sleep", "score"],
|
|
@@ -1780,498 +2271,170 @@ function getTrends(db, days, today) {
|
|
|
1780
2271
|
}
|
|
1781
2272
|
}
|
|
1782
2273
|
const sp = db.query("SELECT AVG(spo2_average) as avg, MIN(spo2_average) as min, MAX(spo2_average) as max, COUNT(*) as count FROM daily_spo2 WHERE day BETWEEN ? AND ?").get(start, today);
|
|
1783
|
-
if (sp.count > 0 && sp.avg !== null) {
|
|
1784
|
-
results.push({ label: "SpO2", avg: +sp.avg.toFixed(1), min: +sp.min.toFixed(1), max: +sp.max.toFixed(1), count: sp.count });
|
|
1785
|
-
}
|
|
1786
|
-
return results;
|
|
1787
|
-
}
|
|
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 };
|
|
1792
|
-
});
|
|
1793
|
-
const range = db.query("SELECT MIN(day) as first, MAX(day) as last FROM daily_sleep").get();
|
|
1794
|
-
const trends = getTrends(db, 99999, today);
|
|
1795
|
-
const mostSteps = db.query("SELECT day, steps FROM daily_activity WHERE steps IS NOT NULL ORDER BY steps DESC LIMIT 1").get();
|
|
1796
|
-
const bestSleep = db.query("SELECT day, score FROM daily_sleep WHERE score IS NOT NULL ORDER BY score DESC LIMIT 1").get();
|
|
1797
|
-
return {
|
|
1798
|
-
tables,
|
|
1799
|
-
dateRange: range,
|
|
1800
|
-
trends,
|
|
1801
|
-
records: {
|
|
1802
|
-
mostSteps: mostSteps ?? null,
|
|
1803
|
-
bestSleep: bestSleep ?? null
|
|
1804
|
-
}
|
|
1805
|
-
};
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
// src/render/format.ts
|
|
1809
|
-
init_source();
|
|
1810
|
-
function scoreColor(score) {
|
|
1811
|
-
if (score === null)
|
|
1812
|
-
return source_default.gray("\u2014");
|
|
1813
|
-
if (score >= 85)
|
|
1814
|
-
return source_default.green(String(score));
|
|
1815
|
-
if (score >= 70)
|
|
1816
|
-
return source_default.yellow(String(score));
|
|
1817
|
-
return source_default.red(String(score));
|
|
1818
|
-
}
|
|
1819
|
-
function fmtHours(h) {
|
|
1820
|
-
if (h === null)
|
|
1821
|
-
return source_default.gray("\u2014");
|
|
1822
|
-
return `${h}h`;
|
|
1823
|
-
}
|
|
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) {
|
|
1828
|
-
if (format === "json")
|
|
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
|
-
}
|
|
1840
|
-
const lines = [
|
|
1841
|
-
"",
|
|
1842
|
-
source_default.bold(` ${summary.day}`),
|
|
1843
|
-
source_default.gray("\u2500".repeat(50)),
|
|
1844
|
-
` Sleep: ${scoreColor(summary.sleep_score)} Readiness: ${scoreColor(summary.readiness_score)} Activity: ${scoreColor(summary.activity_score)}`,
|
|
1845
|
-
` Steps: ${summary.steps ?? source_default.gray("\u2014")}`
|
|
1846
|
-
];
|
|
1847
|
-
if (summary.spo2 !== null)
|
|
1848
|
-
lines.push(` SpO2: ${summary.spo2}%`);
|
|
1849
|
-
if (summary.temp_deviation !== null) {
|
|
1850
|
-
const sign = summary.temp_deviation >= 0 ? "+" : "";
|
|
1851
|
-
lines.push(` Temp: ${sign}${summary.temp_deviation}\xB0C`);
|
|
1852
|
-
}
|
|
1853
|
-
if (summary.stress)
|
|
1854
|
-
lines.push(` Stress: ${summary.stress}`);
|
|
1855
|
-
if (summary.sleep_hours !== null) {
|
|
1856
|
-
lines.push("");
|
|
1857
|
-
lines.push(` Sleep: ${fmtHours(summary.sleep_hours)} total | ${fmtHours(summary.deep_hours)} deep | ${fmtHours(summary.rem_hours)} REM`);
|
|
1858
|
-
lines.push(` HRV: ${summary.avg_hrv ?? "\u2014"} Lowest HR: ${summary.lowest_hr ?? "\u2014"} Efficiency: ${summary.efficiency ?? "\u2014"}%`);
|
|
1859
|
-
}
|
|
1860
|
-
return lines.join(`
|
|
1861
|
-
`);
|
|
1862
|
-
}
|
|
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) {
|
|
1873
|
-
if (format === "json")
|
|
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
|
-
}
|
|
1883
|
-
const header = `${"Day".padEnd(12)} ${"Sleep".padStart(6)} ${"Ready".padStart(6)} ${"Activity".padStart(9)} ${"Steps".padStart(7)} ${"Stress".padEnd(10)}`;
|
|
1884
|
-
const sep = source_default.gray("\u2500".repeat(56));
|
|
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)}`);
|
|
1886
|
-
return [`
|
|
1887
|
-
Last 7 Days`, sep, ` ${header}`, sep, ...rows.map((r) => ` ${r}`)].join(`
|
|
1888
|
-
`);
|
|
1889
|
-
}
|
|
1890
|
-
function formatTrends(trends, days, format) {
|
|
1891
|
-
if (format === "json")
|
|
1892
|
-
return JSON.stringify(trends, null, 2);
|
|
1893
|
-
const lines = [
|
|
1894
|
-
"",
|
|
1895
|
-
source_default.bold(` Trends: last ${days} days`),
|
|
1896
|
-
source_default.gray("\u2500".repeat(50))
|
|
1897
|
-
];
|
|
1898
|
-
for (const t of trends) {
|
|
1899
|
-
lines.push(` ${t.label.padEnd(15)} avg: ${String(t.avg).padStart(5)} min: ${String(t.min).padStart(5)} max: ${String(t.max).padStart(5)} (${t.count} days)`);
|
|
1900
|
-
}
|
|
1901
|
-
return lines.join(`
|
|
1902
|
-
`);
|
|
1903
|
-
}
|
|
1904
|
-
function formatStats(stats, format) {
|
|
1905
|
-
if (format === "json")
|
|
1906
|
-
return JSON.stringify(stats, null, 2);
|
|
1907
|
-
const lines = [
|
|
1908
|
-
"",
|
|
1909
|
-
source_default.bold(" Database Statistics"),
|
|
1910
|
-
source_default.gray("\u2550".repeat(50))
|
|
1911
|
-
];
|
|
1912
|
-
for (const t of stats.tables) {
|
|
1913
|
-
lines.push(` ${t.table.padEnd(22)} ${String(t.rows).padStart(8)} rows`);
|
|
1914
|
-
}
|
|
1915
|
-
if (stats.dateRange.first) {
|
|
1916
|
-
lines.push(`
|
|
1917
|
-
Date range: ${stats.dateRange.first} \u2192 ${stats.dateRange.last}`);
|
|
1918
|
-
}
|
|
1919
|
-
for (const t of stats.trends) {
|
|
1920
|
-
lines.push(` ${t.label.padEnd(15)} avg: ${String(t.avg).padStart(5)} min: ${String(t.min).padStart(5)} max: ${String(t.max).padStart(5)}`);
|
|
1921
|
-
}
|
|
1922
|
-
if (stats.records.mostSteps) {
|
|
1923
|
-
lines.push(`
|
|
1924
|
-
Most steps: ${stats.records.mostSteps.steps} on ${stats.records.mostSteps.day}`);
|
|
1925
|
-
}
|
|
1926
|
-
if (stats.records.bestSleep) {
|
|
1927
|
-
lines.push(` Best sleep: ${stats.records.bestSleep.score} on ${stats.records.bestSleep.day}`);
|
|
1928
|
-
}
|
|
1929
|
-
return lines.join(`
|
|
1930
|
-
`);
|
|
1931
|
-
}
|
|
1932
|
-
|
|
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
|
|
2107
|
-
import { readFileSync } from "fs";
|
|
2108
|
-
import { resolve as resolve3 } from "path";
|
|
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
|
|
2127
|
-
var BASE_URL = "https://api.ouraring.com/v2/usercollection";
|
|
2128
|
-
var MAX_PAGES = 1e4;
|
|
2129
|
-
|
|
2130
|
-
class OuraClient {
|
|
2131
|
-
token;
|
|
2132
|
-
constructor(options = {}) {
|
|
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.");
|
|
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;
|
|
2161
|
-
}
|
|
2162
|
-
async getPage(url) {
|
|
2163
|
-
const response = await fetch(url, {
|
|
2164
|
-
headers: { Authorization: `Bearer ${this.token}` }
|
|
2165
|
-
});
|
|
2166
|
-
if (!response.ok) {
|
|
2167
|
-
const rawBody = await response.text();
|
|
2168
|
-
const redacted = redactSecrets(rawBody);
|
|
2169
|
-
const body = redacted.length > 200 ? redacted.slice(0, 200) + "\u2026 (truncated)" : redacted;
|
|
2170
|
-
if (response.status === 401 || response.status === 403) {
|
|
2171
|
-
throw new CliError("TOKEN_INVALID", `Oura API ${response.status}: ${body}`);
|
|
2172
|
-
}
|
|
2173
|
-
throw new CliError("API_ERROR", `Oura API ${response.status}: ${body}`);
|
|
2174
|
-
}
|
|
2175
|
-
let json;
|
|
2176
|
-
try {
|
|
2177
|
-
json = await response.json();
|
|
2178
|
-
} catch {
|
|
2179
|
-
throw new CliError("API_ERROR", "Empty response body from Oura API.");
|
|
2180
|
-
}
|
|
2181
|
-
const body = json;
|
|
2182
|
-
return { data: body.data ?? [], next_token: body.next_token ?? null };
|
|
2274
|
+
if (sp.count > 0 && sp.avg !== null) {
|
|
2275
|
+
results.push({ label: "SpO2", avg: +sp.avg.toFixed(1), min: +sp.min.toFixed(1), max: +sp.max.toFixed(1), count: sp.count });
|
|
2183
2276
|
}
|
|
2277
|
+
return results;
|
|
2184
2278
|
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2279
|
+
function getStats(db, today) {
|
|
2280
|
+
const tables = COLLECTIONS.map((c) => {
|
|
2281
|
+
const row = db.query(`SELECT COUNT(*) as cnt FROM ${c.table}`).get();
|
|
2282
|
+
return { table: c.table, rows: row.cnt };
|
|
2283
|
+
});
|
|
2284
|
+
const range = db.query("SELECT MIN(day) as first, MAX(day) as last FROM daily_sleep").get();
|
|
2285
|
+
const trends = getTrends(db, 99999, today);
|
|
2286
|
+
const mostSteps = db.query("SELECT day, steps FROM daily_activity WHERE steps IS NOT NULL ORDER BY steps DESC LIMIT 1").get();
|
|
2287
|
+
const bestSleep = db.query("SELECT day, score FROM daily_sleep WHERE score IS NOT NULL ORDER BY score DESC LIMIT 1").get();
|
|
2288
|
+
return {
|
|
2289
|
+
tables,
|
|
2290
|
+
dateRange: range,
|
|
2291
|
+
trends,
|
|
2292
|
+
records: {
|
|
2293
|
+
mostSteps: mostSteps ?? null,
|
|
2294
|
+
bestSleep: bestSleep ?? null
|
|
2295
|
+
}
|
|
2296
|
+
};
|
|
2193
2297
|
}
|
|
2194
2298
|
|
|
2195
|
-
// src/
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2299
|
+
// src/render/format.ts
|
|
2300
|
+
init_source();
|
|
2301
|
+
function scoreColor(score) {
|
|
2302
|
+
if (score === null)
|
|
2303
|
+
return source_default.gray("\u2014");
|
|
2304
|
+
if (score >= 85)
|
|
2305
|
+
return source_default.green(String(score));
|
|
2306
|
+
if (score >= 70)
|
|
2307
|
+
return source_default.yellow(String(score));
|
|
2308
|
+
return source_default.red(String(score));
|
|
2201
2309
|
}
|
|
2202
|
-
function
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
}
|
|
2207
|
-
return n;
|
|
2310
|
+
function fmtHours(h) {
|
|
2311
|
+
if (h === null)
|
|
2312
|
+
return source_default.gray("\u2014");
|
|
2313
|
+
return `${h}h`;
|
|
2208
2314
|
}
|
|
2209
|
-
function
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2315
|
+
function isEmptyDay(s) {
|
|
2316
|
+
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;
|
|
2317
|
+
}
|
|
2318
|
+
function formatDaySummary(summary, format, emptyHint) {
|
|
2319
|
+
if (format === "json")
|
|
2320
|
+
return JSON.stringify(summary, null, 2);
|
|
2321
|
+
if (emptyHint && isEmptyDay(summary)) {
|
|
2322
|
+
return [
|
|
2323
|
+
"",
|
|
2324
|
+
source_default.bold(` ${summary.day}`),
|
|
2325
|
+
source_default.gray("\u2500".repeat(50)),
|
|
2326
|
+
` No Oura data for ${summary.day} yet.`,
|
|
2327
|
+
` ${emptyHint}`
|
|
2328
|
+
].join(`
|
|
2329
|
+
`);
|
|
2330
|
+
}
|
|
2331
|
+
const lines = [
|
|
2332
|
+
"",
|
|
2333
|
+
source_default.bold(` ${summary.day}`),
|
|
2334
|
+
source_default.gray("\u2500".repeat(50)),
|
|
2335
|
+
` Sleep: ${scoreColor(summary.sleep_score)} Readiness: ${scoreColor(summary.readiness_score)} Activity: ${scoreColor(summary.activity_score)}`,
|
|
2336
|
+
` Steps: ${summary.steps ?? source_default.gray("\u2014")}`
|
|
2337
|
+
];
|
|
2338
|
+
if (summary.spo2 !== null)
|
|
2339
|
+
lines.push(` SpO2: ${summary.spo2}%`);
|
|
2340
|
+
if (summary.temp_deviation !== null) {
|
|
2341
|
+
const sign = summary.temp_deviation >= 0 ? "+" : "";
|
|
2342
|
+
lines.push(` Temp: ${sign}${summary.temp_deviation}\xB0C`);
|
|
2343
|
+
}
|
|
2344
|
+
if (summary.stress)
|
|
2345
|
+
lines.push(` Stress: ${summary.stress}`);
|
|
2346
|
+
if (summary.sleep_hours !== null) {
|
|
2347
|
+
lines.push("");
|
|
2348
|
+
lines.push(` Sleep: ${fmtHours(summary.sleep_hours)} total | ${fmtHours(summary.deep_hours)} deep | ${fmtHours(summary.rem_hours)} REM`);
|
|
2349
|
+
lines.push(` HRV: ${summary.avg_hrv ?? "\u2014"} Lowest HR: ${summary.lowest_hr ?? "\u2014"} Efficiency: ${summary.efficiency ?? "\u2014"}%`);
|
|
2215
2350
|
}
|
|
2351
|
+
return lines.join(`
|
|
2352
|
+
`);
|
|
2216
2353
|
}
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2354
|
+
function formatImportSummary(result) {
|
|
2355
|
+
const n = (table) => `${result.fetched[table] ?? 0} (+${result.added[table] ?? 0})`;
|
|
2356
|
+
return [
|
|
2357
|
+
` Fetched ${result.startDate} \u2192 ${result.endDate}, rows fetched (+new):`,
|
|
2358
|
+
` sleep ${n("daily_sleep")} readiness ${n("daily_readiness")} activity ${n("daily_activity")} sleep periods ${n("sleep_model")}`,
|
|
2359
|
+
` spo2 ${n("daily_spo2")} stress ${n("daily_stress")} workouts ${n("workouts")} heart rate ${n("heartrate")} cardiovascular age ${n("cardiovascular_age")}`
|
|
2360
|
+
].join(`
|
|
2222
2361
|
`);
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2362
|
+
}
|
|
2363
|
+
function formatWeekTable(days, format, emptyHint) {
|
|
2364
|
+
if (format === "json")
|
|
2365
|
+
return JSON.stringify(days, null, 2);
|
|
2366
|
+
if (emptyHint && days.length > 0 && days.every(isEmptyDay)) {
|
|
2367
|
+
return [
|
|
2368
|
+
"",
|
|
2369
|
+
" No Oura data for the last 7 days yet.",
|
|
2370
|
+
` ${emptyHint}`
|
|
2371
|
+
].join(`
|
|
2226
2372
|
`);
|
|
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;
|
|
2246
|
-
}
|
|
2247
|
-
if (def.needs?.client) {
|
|
2248
|
-
ctx.client = new OuraClient(args.token ? { token: args.token } : {});
|
|
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
2373
|
}
|
|
2259
|
-
|
|
2260
|
-
|
|
2374
|
+
const header = `${"Day".padEnd(12)} ${"Sleep".padStart(6)} ${"Ready".padStart(6)} ${"Activity".padStart(9)} ${"Steps".padStart(7)} ${"Stress".padEnd(10)}`;
|
|
2375
|
+
const sep = source_default.gray("\u2500".repeat(56));
|
|
2376
|
+
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)}`);
|
|
2377
|
+
return [`
|
|
2378
|
+
Last 7 Days`, sep, ` ${header}`, sep, ...rows.map((r) => ` ${r}`)].join(`
|
|
2379
|
+
`);
|
|
2261
2380
|
}
|
|
2262
|
-
function
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
})
|
|
2381
|
+
function formatTrends(trends, days, format) {
|
|
2382
|
+
if (format === "json")
|
|
2383
|
+
return JSON.stringify(trends, null, 2);
|
|
2384
|
+
const lines = [
|
|
2385
|
+
"",
|
|
2386
|
+
source_default.bold(` Trends: last ${days} days`),
|
|
2387
|
+
source_default.gray("\u2500".repeat(50))
|
|
2388
|
+
];
|
|
2389
|
+
for (const t of trends) {
|
|
2390
|
+
lines.push(` ${t.label.padEnd(15)} avg: ${String(t.avg).padStart(5)} min: ${String(t.min).padStart(5)} max: ${String(t.max).padStart(5)} (${t.count} days)`);
|
|
2391
|
+
}
|
|
2392
|
+
return lines.join(`
|
|
2393
|
+
`);
|
|
2394
|
+
}
|
|
2395
|
+
function formatStats(stats, format) {
|
|
2396
|
+
if (format === "json")
|
|
2397
|
+
return JSON.stringify(stats, null, 2);
|
|
2398
|
+
const lines = [
|
|
2399
|
+
"",
|
|
2400
|
+
source_default.bold(" Database Statistics"),
|
|
2401
|
+
source_default.gray("\u2550".repeat(50))
|
|
2402
|
+
];
|
|
2403
|
+
for (const t of stats.tables) {
|
|
2404
|
+
lines.push(` ${t.table.padEnd(22)} ${String(t.rows).padStart(8)} rows`);
|
|
2405
|
+
}
|
|
2406
|
+
if (stats.dateRange.first) {
|
|
2407
|
+
lines.push(`
|
|
2408
|
+
Date range: ${stats.dateRange.first} \u2192 ${stats.dateRange.last}`);
|
|
2409
|
+
}
|
|
2410
|
+
for (const t of stats.trends) {
|
|
2411
|
+
lines.push(` ${t.label.padEnd(15)} avg: ${String(t.avg).padStart(5)} min: ${String(t.min).padStart(5)} max: ${String(t.max).padStart(5)}`);
|
|
2412
|
+
}
|
|
2413
|
+
if (stats.records.mostSteps) {
|
|
2414
|
+
lines.push(`
|
|
2415
|
+
Most steps: ${stats.records.mostSteps.steps} on ${stats.records.mostSteps.day}`);
|
|
2416
|
+
}
|
|
2417
|
+
if (stats.records.bestSleep) {
|
|
2418
|
+
lines.push(` Best sleep: ${stats.records.bestSleep.score} on ${stats.records.bestSleep.day}`);
|
|
2419
|
+
}
|
|
2420
|
+
return lines.join(`
|
|
2421
|
+
`);
|
|
2268
2422
|
}
|
|
2269
2423
|
|
|
2270
2424
|
// src/commands/sync.ts
|
|
2271
|
-
|
|
2425
|
+
function resolveWindow(opts) {
|
|
2426
|
+
if (opts.to !== undefined && opts.from === undefined)
|
|
2427
|
+
throw new CliError("BAD_ARGS", "--to requires --from.");
|
|
2428
|
+
const from = opts.from === undefined ? undefined : assertCalendarDate(opts.from, "--from");
|
|
2429
|
+
const to = opts.to === undefined ? undefined : assertCalendarDate(opts.to, "--to");
|
|
2430
|
+
if (from !== undefined && to !== undefined && from > to)
|
|
2431
|
+
throw new CliError("BAD_ARGS", `--from (${from}) must not be after --to (${to}).`);
|
|
2432
|
+
return { from, to };
|
|
2433
|
+
}
|
|
2434
|
+
async function runSync(ctx, window = {}) {
|
|
2272
2435
|
const lines = [];
|
|
2273
2436
|
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);
|
|
2437
|
+
const importResult = await importDaily(ctx.db, ctx.client, { today: ctx.today, tz: ctx.tz }, log, window);
|
|
2275
2438
|
const today = getDaySummary(ctx.db, ctx.today);
|
|
2276
2439
|
return {
|
|
2277
2440
|
json: { import: importResult, today },
|
|
@@ -2281,8 +2444,12 @@ async function runSync(ctx) {
|
|
|
2281
2444
|
}
|
|
2282
2445
|
var syncCommand = dataCommand({
|
|
2283
2446
|
meta: { name: "sync", description: "Import latest data from Oura API and return today's summary" },
|
|
2447
|
+
args: {
|
|
2448
|
+
from: { type: "string", description: "Re-fetch every collection from this day (YYYY-MM-DD) instead of from its last stored day" },
|
|
2449
|
+
to: { type: "string", description: "End of the explicit window (YYYY-MM-DD, default: today); requires --from" }
|
|
2450
|
+
},
|
|
2284
2451
|
needs: { db: true, client: true },
|
|
2285
|
-
run: runSync
|
|
2452
|
+
run: (ctx, args) => runSync(ctx, resolveWindow({ from: args.from, to: args.to }))
|
|
2286
2453
|
});
|
|
2287
2454
|
|
|
2288
2455
|
// src/commands/db.ts
|
|
@@ -2346,15 +2513,19 @@ function dayLabel(dateStr) {
|
|
|
2346
2513
|
const mm = String(d.getUTCMonth() + 1).padStart(2, "0");
|
|
2347
2514
|
return `${day} ${dd}/${mm}`;
|
|
2348
2515
|
}
|
|
2349
|
-
function getReport(db, days, today) {
|
|
2516
|
+
function getReport(db, days, today, tz = "UTC") {
|
|
2350
2517
|
const period = days <= 7 ? "week" : "month";
|
|
2351
2518
|
const weekEnd = today;
|
|
2352
2519
|
const weekStart = shiftDay(today, -(days - 1));
|
|
2353
2520
|
const prevWeekEnd = shiftDay(today, -days);
|
|
2354
2521
|
const prevWeekStart = shiftDay(today, -(days * 2 - 1));
|
|
2522
|
+
const windowDays = daysBack(today, days);
|
|
2523
|
+
const lastUpload = db.query("SELECT MAX(timestamp) AS t FROM heartrate").get().t;
|
|
2524
|
+
const isComplete = (d) => d < today && (lastUpload === null || Date.parse(localDateToUtcRange(d, tz)[1]) <= Date.parse(lastUpload));
|
|
2525
|
+
const completeThrough = [...windowDays].reverse().find(isComplete) ?? null;
|
|
2526
|
+
const activityEnd = completeThrough ?? shiftDay(weekStart, -1);
|
|
2355
2527
|
const dailyRows = [];
|
|
2356
|
-
for (
|
|
2357
|
-
const d = shiftDay(today, -i);
|
|
2528
|
+
for (const d of windowDays) {
|
|
2358
2529
|
const sl = db.query("SELECT score FROM daily_sleep WHERE day=?").get(d);
|
|
2359
2530
|
const rd = db.query("SELECT score FROM daily_readiness WHERE day=?").get(d);
|
|
2360
2531
|
const ac = db.query("SELECT score, steps FROM daily_activity WHERE day=?").get(d);
|
|
@@ -2364,7 +2535,8 @@ function getReport(db, days, today) {
|
|
|
2364
2535
|
sleep: sl?.score ?? null,
|
|
2365
2536
|
readiness: rd?.score ?? null,
|
|
2366
2537
|
activity: ac?.score ?? null,
|
|
2367
|
-
steps: ac?.steps ?? null
|
|
2538
|
+
steps: ac?.steps ?? null,
|
|
2539
|
+
partial: ac != null && !isComplete(d)
|
|
2368
2540
|
});
|
|
2369
2541
|
}
|
|
2370
2542
|
const metrics = [
|
|
@@ -2375,7 +2547,8 @@ function getReport(db, days, today) {
|
|
|
2375
2547
|
];
|
|
2376
2548
|
const averages = [];
|
|
2377
2549
|
for (const [label, table, col, isSteps] of metrics) {
|
|
2378
|
-
const
|
|
2550
|
+
const end = table === "daily_activity" ? activityEnd : weekEnd;
|
|
2551
|
+
const curr = db.query(`SELECT AVG(${col}) as avg, MIN(${col}) as min, MAX(${col}) as max, COUNT(${col}) as cnt FROM ${table} WHERE day BETWEEN ? AND ?`).get(weekStart, end);
|
|
2379
2552
|
const prev = db.query(`SELECT AVG(${col}) as avg FROM ${table} WHERE day BETWEEN ? AND ?`).get(prevWeekStart, prevWeekEnd);
|
|
2380
2553
|
if (curr.cnt > 0 && curr.avg !== null) {
|
|
2381
2554
|
const diff = prev.avg !== null ? curr.avg - prev.avg : null;
|
|
@@ -2384,6 +2557,7 @@ function getReport(db, days, today) {
|
|
|
2384
2557
|
avg: curr.avg,
|
|
2385
2558
|
min: curr.min,
|
|
2386
2559
|
max: curr.max,
|
|
2560
|
+
count: curr.cnt,
|
|
2387
2561
|
prevAvg: prev.avg,
|
|
2388
2562
|
diff,
|
|
2389
2563
|
isSteps
|
|
@@ -2394,7 +2568,7 @@ function getReport(db, days, today) {
|
|
|
2394
2568
|
const spo2 = sp.cnt > 0 && sp.avg !== null ? { avg: +sp.avg.toFixed(1), min: +sp.min.toFixed(1), max: +sp.max.toFixed(1) } : null;
|
|
2395
2569
|
const lowSleep = db.query("SELECT day, score FROM daily_sleep WHERE day BETWEEN ? AND ? AND score < 70 ORDER BY score").all(weekStart, weekEnd).map((r) => ({ ...r, dayLabel: dayLabel(r.day) }));
|
|
2396
2570
|
const lowReadiness = db.query("SELECT day, score FROM daily_readiness WHERE day BETWEEN ? AND ? AND score < 70 ORDER BY score").all(weekStart, weekEnd).map((r) => ({ ...r, dayLabel: dayLabel(r.day) }));
|
|
2397
|
-
const highActivity = db.query("SELECT day, score, steps FROM daily_activity WHERE day BETWEEN ? AND ? AND score >= 90 ORDER BY score DESC").all(weekStart,
|
|
2571
|
+
const highActivity = db.query("SELECT day, score, steps FROM daily_activity WHERE day BETWEEN ? AND ? AND score >= 90 ORDER BY score DESC").all(weekStart, activityEnd).map((r) => ({ ...r, dayLabel: dayLabel(r.day) }));
|
|
2398
2572
|
const sd = db.query(`SELECT AVG(total_sleep_duration) as totalSleep, AVG(deep_sleep_duration) as deepSleep,
|
|
2399
2573
|
AVG(rem_sleep_duration) as remSleep, AVG(light_sleep_duration) as lightSleep,
|
|
2400
2574
|
AVG(efficiency) as efficiency, AVG(average_hrv) as hrv, AVG(lowest_heart_rate) as lowestHr
|
|
@@ -2403,7 +2577,7 @@ function getReport(db, days, today) {
|
|
|
2403
2577
|
const recommendations = [];
|
|
2404
2578
|
const avgSleep = db.query("SELECT AVG(score) as avg FROM daily_sleep WHERE day BETWEEN ? AND ?").get(weekStart, weekEnd);
|
|
2405
2579
|
const avgReady = db.query("SELECT AVG(score) as avg FROM daily_readiness WHERE day BETWEEN ? AND ?").get(weekStart, weekEnd);
|
|
2406
|
-
const avgSteps = db.query("SELECT AVG(steps) as avg FROM daily_activity WHERE day BETWEEN ? AND ?").get(weekStart,
|
|
2580
|
+
const avgSteps = db.query("SELECT AVG(steps) as avg FROM daily_activity WHERE day BETWEEN ? AND ?").get(weekStart, activityEnd);
|
|
2407
2581
|
if (avgSleep.avg !== null && avgSleep.avg < 75) {
|
|
2408
2582
|
recommendations.push("sleep_low");
|
|
2409
2583
|
} else if (avgSleep.avg !== null && avgSleep.avg >= 85) {
|
|
@@ -2419,7 +2593,7 @@ function getReport(db, days, today) {
|
|
|
2419
2593
|
} else if (avgSteps.avg !== null && avgSteps.avg >= 1e4) {
|
|
2420
2594
|
recommendations.push("steps_great");
|
|
2421
2595
|
}
|
|
2422
|
-
return { period, weekStart, weekEnd, days: dailyRows, averages, spo2, patterns: { lowSleep, lowReadiness, highActivity }, sleepDetails, recommendations };
|
|
2596
|
+
return { period, weekStart, weekEnd, days: dailyRows, completeThrough, lastUpload, averages, spo2, patterns: { lowSleep, lowReadiness, highActivity }, sleepDetails, recommendations };
|
|
2423
2597
|
}
|
|
2424
2598
|
|
|
2425
2599
|
// src/render/format-report.ts
|
|
@@ -2475,12 +2649,24 @@ function bucketDaysIntoWeeks(days) {
|
|
|
2475
2649
|
avgSleep: sleepVals.length > 0 ? sleepVals.reduce((a, b) => a + b, 0) / sleepVals.length : null,
|
|
2476
2650
|
avgReadiness: readinessVals.length > 0 ? readinessVals.reduce((a, b) => a + b, 0) / readinessVals.length : null,
|
|
2477
2651
|
avgActivity: activityVals.length > 0 ? activityVals.reduce((a, b) => a + b, 0) / activityVals.length : null,
|
|
2478
|
-
totalSteps: stepsVals.length > 0 ? stepsVals.reduce((a, b) => a + b, 0) : null
|
|
2652
|
+
totalSteps: stepsVals.length > 0 ? stepsVals.reduce((a, b) => a + b, 0) : null,
|
|
2653
|
+
partial: chunk.some((d) => d.partial)
|
|
2479
2654
|
});
|
|
2480
2655
|
}
|
|
2481
2656
|
return buckets;
|
|
2482
2657
|
}
|
|
2483
|
-
function
|
|
2658
|
+
function partialDayNote(data, tz) {
|
|
2659
|
+
const partial = data.days.filter((d) => d.partial);
|
|
2660
|
+
if (partial.length === 0)
|
|
2661
|
+
return null;
|
|
2662
|
+
const newest = partial[partial.length - 1];
|
|
2663
|
+
const newestLabel = newest.day === data.weekEnd ? "today" : newest.dayLabel;
|
|
2664
|
+
const which = partial.length === 1 ? `${newestLabel} is` : `${newestLabel} and ${partial.length - 1} earlier day${partial.length > 2 ? "s" : ""} are`;
|
|
2665
|
+
const synced = data.lastUpload ? `ring last synced ${formatLocal(data.lastUpload, tz)}` : "ring sync time unknown";
|
|
2666
|
+
const covers = data.completeThrough ? `through ${data.completeThrough}` : "no complete day yet";
|
|
2667
|
+
return ` * ${which} still accumulating (${synced}); activity averages cover ${covers}.`;
|
|
2668
|
+
}
|
|
2669
|
+
function formatReport(data, format, period, tz = "UTC") {
|
|
2484
2670
|
if (format === "json")
|
|
2485
2671
|
return JSON.stringify(data, null, 2);
|
|
2486
2672
|
const lines = [];
|
|
@@ -2491,6 +2677,9 @@ function formatReport(data, format, period) {
|
|
|
2491
2677
|
lines.push(source_default.bold(" Oura Monthly Report"));
|
|
2492
2678
|
}
|
|
2493
2679
|
lines.push(source_default.gray(` ${data.weekStart} \u2014 ${data.weekEnd}`));
|
|
2680
|
+
const note = partialDayNote(data, tz);
|
|
2681
|
+
if (note)
|
|
2682
|
+
lines.push(source_default.yellow(note));
|
|
2494
2683
|
lines.push("");
|
|
2495
2684
|
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
2685
|
if (!hasReportData) {
|
|
@@ -2506,7 +2695,7 @@ function formatReport(data, format, period) {
|
|
|
2506
2695
|
lines.push(` ${"Day".padEnd(10)} ${"Sleep".padStart(6)} ${"Ready".padStart(6)} ${"Active".padStart(7)} ${"Steps".padStart(8)}`);
|
|
2507
2696
|
lines.push(source_default.gray(" " + "\u2500".repeat(52)));
|
|
2508
2697
|
for (const d of data.days) {
|
|
2509
|
-
lines.push(` ${d.dayLabel.padEnd(10)} ${scoreCell(d.sleep, 6)} ${scoreCell(d.readiness, 6)} ${scoreCell(d.activity, 7)} ${stepsCell(d.steps, 8)}`);
|
|
2698
|
+
lines.push(` ${(d.partial ? d.dayLabel + "*" : d.dayLabel).padEnd(10)} ${scoreCell(d.sleep, 6)} ${scoreCell(d.readiness, 6)} ${scoreCell(d.activity, 7)} ${stepsCell(d.steps, 8)}`);
|
|
2510
2699
|
}
|
|
2511
2700
|
lines.push("");
|
|
2512
2701
|
} else {
|
|
@@ -2519,7 +2708,7 @@ function formatReport(data, format, period) {
|
|
|
2519
2708
|
const avgSleepInt = b.avgSleep !== null ? Math.round(b.avgSleep) : null;
|
|
2520
2709
|
const avgReadyInt = b.avgReadiness !== null ? Math.round(b.avgReadiness) : null;
|
|
2521
2710
|
const avgActiveInt = b.avgActivity !== null ? Math.round(b.avgActivity) : null;
|
|
2522
|
-
lines.push(` ${b.weekOf.padEnd(12)} ${scoreCell(avgSleepInt, 6)} ${scoreCell(avgReadyInt, 6)} ${scoreCell(avgActiveInt, 7)} ${stepsCell(b.totalSteps, 10)}`);
|
|
2711
|
+
lines.push(` ${(b.partial ? b.weekOf + "*" : b.weekOf).padEnd(12)} ${scoreCell(avgSleepInt, 6)} ${scoreCell(avgReadyInt, 6)} ${scoreCell(avgActiveInt, 7)} ${stepsCell(b.totalSteps, 10)}`);
|
|
2523
2712
|
}
|
|
2524
2713
|
lines.push("");
|
|
2525
2714
|
}
|
|
@@ -2580,17 +2769,18 @@ var reportCommand = dataCommand({
|
|
|
2580
2769
|
if (period !== "week" && period !== "month") {
|
|
2581
2770
|
throw new CliError("BAD_ARGS", `--period must be "week" or "month", got "${period}".`);
|
|
2582
2771
|
}
|
|
2583
|
-
const data = getReport(ctx.db, period === "week" ? 7 : 30, ctx.today);
|
|
2584
|
-
return { json: data, text: () => formatReport(data, "table", period) };
|
|
2772
|
+
const data = getReport(ctx.db, period === "week" ? 7 : 30, ctx.today, ctx.tz);
|
|
2773
|
+
return { json: data, text: () => formatReport(data, "table", period, ctx.tz) };
|
|
2585
2774
|
}
|
|
2586
2775
|
});
|
|
2587
2776
|
|
|
2588
2777
|
// src/commands/healthcheck.ts
|
|
2589
2778
|
function healthcheckCommand(version) {
|
|
2590
2779
|
return defineCommand({
|
|
2591
|
-
meta: { name: "healthcheck", description: "Quick local DB health probe (JSON: {ok, version, latencyMs})." },
|
|
2780
|
+
meta: { name: "healthcheck", description: "Quick local DB health probe (JSON: {ok, version, latencyMs}, plus error when ok is false)." },
|
|
2592
2781
|
args: { ...commonArgs },
|
|
2593
2782
|
run({ args }) {
|
|
2783
|
+
assertKnownArgs(commonArgs, args);
|
|
2594
2784
|
const start = Date.now();
|
|
2595
2785
|
let ok = true;
|
|
2596
2786
|
let error;
|
|
@@ -2615,6 +2805,8 @@ function statusSymbol(status) {
|
|
|
2615
2805
|
return source_default.green("\u2713");
|
|
2616
2806
|
if (status === "warn")
|
|
2617
2807
|
return source_default.yellow("!");
|
|
2808
|
+
if (status === "skip")
|
|
2809
|
+
return source_default.gray("\u2013");
|
|
2618
2810
|
return source_default.red("\u2717");
|
|
2619
2811
|
}
|
|
2620
2812
|
function formatDoctorTable(result) {
|
|
@@ -2641,7 +2833,7 @@ async function runChecks(deps) {
|
|
|
2641
2833
|
if (!token) {
|
|
2642
2834
|
checks.push({ id: "token-valid", status: "fail", detail: "No token to validate.", fix: "oura-cli login" });
|
|
2643
2835
|
} else if (deps.offline) {
|
|
2644
|
-
checks.push({ id: "token-valid", status: "
|
|
2836
|
+
checks.push({ id: "token-valid", status: "skip", detail: "Not checked (--offline)." });
|
|
2645
2837
|
} else {
|
|
2646
2838
|
try {
|
|
2647
2839
|
const client = deps.createClient(token);
|
|
@@ -2681,8 +2873,9 @@ async function runChecks(deps) {
|
|
|
2681
2873
|
checks.push({ id: "data", status: "fail", detail: "Cannot check data \u2014 database unavailable." });
|
|
2682
2874
|
}
|
|
2683
2875
|
db?.close();
|
|
2684
|
-
const
|
|
2685
|
-
const
|
|
2876
|
+
const settled = (s) => s === "ok" || s === "skip";
|
|
2877
|
+
const ok = checks.every((c) => settled(c.status));
|
|
2878
|
+
const nextStep = checks.find((c) => !settled(c.status))?.fix ?? null;
|
|
2686
2879
|
return { ok, checks, nextStep };
|
|
2687
2880
|
}
|
|
2688
2881
|
var DATA_TABLES = ["daily_sleep", "daily_readiness", "daily_activity"];
|
|
@@ -2748,14 +2941,15 @@ function buildOpenclawManifest(version, commands) {
|
|
|
2748
2941
|
examples: EXAMPLES[c.name] ?? [`oura-cli ${c.name}`]
|
|
2749
2942
|
})),
|
|
2750
2943
|
envVars: ["OURA_TOKEN", "OURA_TOKEN_PATH", "OURA_DB_PATH", "OURA_TZ"],
|
|
2751
|
-
healthcheck: { command: "healthcheck", expects: { ok: "boolean", version: "string", latencyMs: "number" } }
|
|
2944
|
+
healthcheck: { command: "healthcheck", expects: { ok: "boolean", version: "string", latencyMs: "number", error: "string, present only when ok is false" } }
|
|
2752
2945
|
};
|
|
2753
2946
|
}
|
|
2754
2947
|
function manifestCommand(version, getCommands) {
|
|
2755
2948
|
return defineCommand({
|
|
2756
2949
|
meta: { name: "manifest", description: "Print openclaw-tool-registry-compatible manifest as JSON." },
|
|
2757
|
-
args: {},
|
|
2758
|
-
run() {
|
|
2950
|
+
args: { ...commonArgs },
|
|
2951
|
+
run({ args }) {
|
|
2952
|
+
assertKnownArgs(commonArgs, args);
|
|
2759
2953
|
console.log(JSON.stringify(buildOpenclawManifest(version, getCommands()), null, 2));
|
|
2760
2954
|
}
|
|
2761
2955
|
});
|
|
@@ -2812,53 +3006,26 @@ var fetchCommand = dataCommand({
|
|
|
2812
3006
|
}
|
|
2813
3007
|
});
|
|
2814
3008
|
|
|
2815
|
-
// src/lib/
|
|
2816
|
-
var
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
"
|
|
2820
|
-
|
|
2821
|
-
"
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
"report"
|
|
2828
|
-
]);
|
|
2829
|
-
function normalizeArgv(argv) {
|
|
2830
|
-
const [bun, script, ...rest] = argv;
|
|
2831
|
-
const subIdx = rest.findIndex((a) => SUBCOMMANDS.has(a));
|
|
2832
|
-
if (subIdx < 0)
|
|
2833
|
-
return argv;
|
|
2834
|
-
const before = rest.slice(0, subIdx);
|
|
2835
|
-
const subcommandOnwards = rest.slice(subIdx);
|
|
2836
|
-
const hoisted = [];
|
|
2837
|
-
const leftover = [];
|
|
2838
|
-
for (let i = 0;i < before.length; i++) {
|
|
2839
|
-
const tok = before[i];
|
|
2840
|
-
if (tok.includes("=")) {
|
|
2841
|
-
const name = tok.slice(0, tok.indexOf("="));
|
|
2842
|
-
if (GLOBAL_FLAGS_WITH_VALUE.has(name) || GLOBAL_FLAGS_BOOLEAN.has(name)) {
|
|
2843
|
-
hoisted.push(tok);
|
|
2844
|
-
continue;
|
|
2845
|
-
}
|
|
2846
|
-
}
|
|
2847
|
-
if (GLOBAL_FLAGS_WITH_VALUE.has(tok)) {
|
|
2848
|
-
hoisted.push(tok);
|
|
2849
|
-
if (i + 1 < before.length) {
|
|
2850
|
-
hoisted.push(before[i + 1]);
|
|
2851
|
-
i++;
|
|
2852
|
-
}
|
|
2853
|
-
continue;
|
|
2854
|
-
}
|
|
2855
|
-
if (GLOBAL_FLAGS_BOOLEAN.has(tok)) {
|
|
2856
|
-
hoisted.push(tok);
|
|
2857
|
-
continue;
|
|
3009
|
+
// src/lib/citty-error.ts
|
|
3010
|
+
var ANSI = /\u001b\[[0-9;]*m/g;
|
|
3011
|
+
function fromCittyError(err, removedCommandHints = {}) {
|
|
3012
|
+
const code = err?.code;
|
|
3013
|
+
if (typeof code !== "string")
|
|
3014
|
+
return err;
|
|
3015
|
+
const message = (err instanceof Error ? err.message : String(err)).replace(ANSI, "");
|
|
3016
|
+
switch (code) {
|
|
3017
|
+
case "E_UNKNOWN_COMMAND": {
|
|
3018
|
+
const name = message.replace(/^Unknown command\s*/, "").trim();
|
|
3019
|
+
const hint = Object.hasOwn(removedCommandHints, name) ? removedCommandHints[name] : "Run `oura-cli --help` for the list of commands.";
|
|
3020
|
+
return new CliError("BAD_ARGS", `Unknown command "${name}".`, hint);
|
|
2858
3021
|
}
|
|
2859
|
-
|
|
3022
|
+
case "EARG":
|
|
3023
|
+
return new CliError("BAD_ARGS", message.endsWith(".") ? message : `${message}.`, "Run the command with --help to see its arguments.");
|
|
3024
|
+
case "E_NO_COMMAND":
|
|
3025
|
+
return new CliError("BAD_ARGS", "No command specified.", "Run `oura-cli --help` for the list of commands.");
|
|
3026
|
+
default:
|
|
3027
|
+
return err;
|
|
2860
3028
|
}
|
|
2861
|
-
return [bun, script, ...leftover, ...subcommandOnwards, ...hoisted];
|
|
2862
3029
|
}
|
|
2863
3030
|
|
|
2864
3031
|
// src/index.ts
|
|
@@ -2866,7 +3033,7 @@ var VERSION = JSON.parse(readFileSync2(new URL("../package.json", import.meta.ur
|
|
|
2866
3033
|
if (process.argv.includes("--no-color") || process.env.NO_COLOR) {
|
|
2867
3034
|
source_default.level = 0;
|
|
2868
3035
|
}
|
|
2869
|
-
var subCommands = {
|
|
3036
|
+
var subCommands = Object.assign(Object.create(null), {
|
|
2870
3037
|
login: loginCommand,
|
|
2871
3038
|
describe: describeCommand(VERSION, () => subCommands),
|
|
2872
3039
|
healthcheck: healthcheckCommand(VERSION),
|
|
@@ -2876,6 +3043,18 @@ var subCommands = {
|
|
|
2876
3043
|
sync: syncCommand,
|
|
2877
3044
|
db: dbCommand,
|
|
2878
3045
|
report: reportCommand
|
|
3046
|
+
});
|
|
3047
|
+
var FETCH_HINT = "The per-collection commands were replaced in 0.5.0 by `oura-cli fetch <collection>`, e.g. `oura-cli fetch sleep --day 2026-09-01`. Run `oura-cli fetch --help`.";
|
|
3048
|
+
var REMOVED_COMMANDS = {
|
|
3049
|
+
reset: "`db reset` was removed in 0.5.0; delete the database file (`--db` / OURA_DB_PATH) and run `oura-cli sync` to rebuild it.",
|
|
3050
|
+
import: "`db import` was removed in 0.5.0; `oura-cli sync` downloads and stores everything.",
|
|
3051
|
+
sleep: FETCH_HINT,
|
|
3052
|
+
readiness: FETCH_HINT,
|
|
3053
|
+
activity: FETCH_HINT,
|
|
3054
|
+
hr: FETCH_HINT,
|
|
3055
|
+
spo2: FETCH_HINT,
|
|
3056
|
+
stress: FETCH_HINT,
|
|
3057
|
+
workout: FETCH_HINT
|
|
2879
3058
|
};
|
|
2880
3059
|
var main = defineCommand({
|
|
2881
3060
|
meta: {
|
|
@@ -2886,5 +3065,16 @@ var main = defineCommand({
|
|
|
2886
3065
|
args: { ...commonArgs },
|
|
2887
3066
|
subCommands
|
|
2888
3067
|
});
|
|
2889
|
-
var
|
|
2890
|
-
|
|
3068
|
+
var rawArgs = normalizeArgv(process.argv).slice(2);
|
|
3069
|
+
var wantsHelp = rawArgs.some((a) => a === "--help" || a === "-h") || rawArgs.length === 0 && process.stdout.isTTY === true;
|
|
3070
|
+
if (isVersionRequest(rawArgs)) {
|
|
3071
|
+
console.log(VERSION);
|
|
3072
|
+
} else if (wantsHelp) {
|
|
3073
|
+
runMain(main, { rawArgs });
|
|
3074
|
+
} else {
|
|
3075
|
+
runCommand(main, { rawArgs }).catch((raw) => {
|
|
3076
|
+
const err = fromCittyError(raw, REMOVED_COMMANDS);
|
|
3077
|
+
emitError(err, formatFromArgv(rawArgs, process.stdout.isTTY === true));
|
|
3078
|
+
process.exit(exitCodeFor(err));
|
|
3079
|
+
});
|
|
3080
|
+
}
|