@yaroslavhaidash/gitstats-cli 0.2.1 → 0.3.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/README.md +1 -1
- package/dist/cli.js +80 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,4 +14,4 @@ What leaves the machine, per repo: an HMAC-SHA256 of the normalised remote URL k
|
|
|
14
14
|
|
|
15
15
|
You are trusting the operator: `npx github:…` runs the committed `dist/` from this repo. Read `src/cli.ts` (~400 lines) or watch the payload with a proxy.
|
|
16
16
|
|
|
17
|
-
Commands: `sync` · `status` · `add <path>` · `roots add <dir>` · `emails add <email>` · `names on|off` · `unlink` (also revokes server-side). Config lives in `~/.gitstats/config.json` (mode 600).
|
|
17
|
+
Commands: `sync` · `status` · `add <path>` · `roots add <dir>` · `emails add <email>` · `names on|off` · `update` · `unlink` (also revokes server-side). It keeps itself up to date: every sync checks npm at most once a day and installs a newer version before finishing, `sync --no-update` skips that. Config lives in `~/.gitstats/config.json` (mode 600).
|
package/dist/cli.js
CHANGED
|
@@ -5,20 +5,21 @@
|
|
|
5
5
|
*
|
|
6
6
|
* npx @yaroslavhaidash/gitstats-cli@latest link
|
|
7
7
|
* pair this computer, scan for repos, sync, install a daily sync
|
|
8
|
-
* gitstats sync fetch each repo's default branch, recount the last year, upload (idempotent;
|
|
8
|
+
* gitstats sync fetch each repo's default branch, recount the last year, upload (idempotent;
|
|
9
|
+
* --no-fetch skips the fetch, --no-update skips the daily version check)
|
|
9
10
|
* gitstats status show what is linked and when it last ran
|
|
10
11
|
* gitstats add <path> track a repo outside the scanned folders
|
|
11
12
|
* gitstats roots add <dir> scan another folder (e.g. one outside your home directory)
|
|
12
13
|
* gitstats emails add <e> attribute commits made with another email to you
|
|
13
14
|
* gitstats names on|off also send repo names (off by default; your own page then labels private repos by hash)
|
|
14
15
|
* gitstats pause | resume stop / restart the background sync without unlinking
|
|
15
|
-
* gitstats update fetch the latest published version
|
|
16
|
+
* gitstats update fetch the latest published version now (sync does this on its own, once a day)
|
|
16
17
|
* gitstats unlink revoke this computer and remove the schedule and local config
|
|
17
18
|
*/
|
|
18
19
|
import { spawnSync } from "node:child_process";
|
|
19
20
|
import { createHmac } from "node:crypto";
|
|
20
21
|
import { createInterface } from "node:readline/promises";
|
|
21
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, statSync, writeFileSync, rmSync, cpSync } from "node:fs";
|
|
22
|
+
import { appendFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, statSync, writeFileSync, rmSync, cpSync } from "node:fs";
|
|
22
23
|
import { homedir, hostname, platform, tmpdir } from "node:os";
|
|
23
24
|
import { basename, dirname, join, resolve } from "node:path";
|
|
24
25
|
import { fileURLToPath } from "node:url";
|
|
@@ -30,12 +31,26 @@ const CONFIG = join(DIR, "config.json");
|
|
|
30
31
|
const SELF = join(DIR, "cli");
|
|
31
32
|
const DAYS = 365;
|
|
32
33
|
const PENDING_DAYS = 30;
|
|
34
|
+
const SYNC_LOG = join(DIR, "sync.log");
|
|
35
|
+
const UPDATE_EVERY_MS = 24 * 60 * 60 * 1000;
|
|
36
|
+
const REGISTRY = `https://registry.npmjs.org/${PKG.replace("/", "%2F")}/latest`;
|
|
33
37
|
const SKIP_DIRS = new Set(["node_modules", "Library", "Applications", ".Trash", "vendor", "target", "build", "dist", ".venv", "venv", "__pycache__", "Pods", "DerivedData", "go", ".cargo", ".rustup", ".npm", ".cache", ".local", "snap", "AppData"]);
|
|
34
38
|
const args = process.argv.slice(2);
|
|
35
39
|
const cmd = args[0] ?? "help";
|
|
36
40
|
function log(msg) {
|
|
37
41
|
console.log(msg);
|
|
38
42
|
}
|
|
43
|
+
/** Background runs already redirect their output here; a manual run appends directly so a failed
|
|
44
|
+
* update check leaves the same trail either way. */
|
|
45
|
+
function logFile(msg) {
|
|
46
|
+
try {
|
|
47
|
+
mkdirSync(DIR, { recursive: true });
|
|
48
|
+
appendFileSync(SYNC_LOG, `${new Date().toISOString()} ${msg}\n`);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
/* the log is best effort; it must never break a sync */
|
|
52
|
+
}
|
|
53
|
+
}
|
|
39
54
|
function loadConfig() {
|
|
40
55
|
if (!existsSync(CONFIG))
|
|
41
56
|
return null;
|
|
@@ -294,7 +309,7 @@ function count(c, since) {
|
|
|
294
309
|
}
|
|
295
310
|
async function upload(c, reports) {
|
|
296
311
|
const payload = reports.map(({ remoteHash, name, language, weeks, days, pending }) => ({ remoteHash, name, language, weeks, days, pending }));
|
|
297
|
-
const { status, body } = await post(c.server, "/api/ingest", { repos: payload }, c.token);
|
|
312
|
+
const { status, body } = await post(c.server, "/api/ingest", { cliVersion: runningVersion(), repos: payload }, c.token);
|
|
298
313
|
if (status !== 200 || !body) {
|
|
299
314
|
c.lastSync = { at: new Date().toISOString(), repos: 0, weeks: 0, error: `server answered ${status}` };
|
|
300
315
|
saveConfig(c);
|
|
@@ -302,9 +317,14 @@ async function upload(c, reports) {
|
|
|
302
317
|
}
|
|
303
318
|
c.lastSync = { at: new Date().toISOString(), repos: body.repos, weeks: body.weeks };
|
|
304
319
|
saveConfig(c);
|
|
320
|
+
if (body.outdated)
|
|
321
|
+
log(`this computer runs ${runningVersion()}; the board expects ${body.minVersion} or newer — run \`gitstats update\``);
|
|
305
322
|
return body;
|
|
306
323
|
}
|
|
307
324
|
async function sync(c, quiet = false) {
|
|
325
|
+
const restarted = await selfUpdate(c);
|
|
326
|
+
if (restarted !== null)
|
|
327
|
+
process.exit(restarted);
|
|
308
328
|
const since = new Date(Date.now() - DAYS * 86_400_000).toISOString().slice(0, 10);
|
|
309
329
|
const { scanned, reports } = count(c, since);
|
|
310
330
|
const body = await upload(c, reports);
|
|
@@ -336,9 +356,9 @@ function writeShim() {
|
|
|
336
356
|
}
|
|
337
357
|
return script;
|
|
338
358
|
}
|
|
339
|
-
function
|
|
359
|
+
function readVersion(pkgRoot) {
|
|
340
360
|
try {
|
|
341
|
-
const pkg = JSON.parse(readFileSync(join(
|
|
361
|
+
const pkg = JSON.parse(readFileSync(join(pkgRoot, "package.json"), "utf8"));
|
|
342
362
|
const v = typeof pkg === "object" && pkg !== null ? pkg.version : null;
|
|
343
363
|
return typeof v === "string" ? v : "unknown";
|
|
344
364
|
}
|
|
@@ -346,6 +366,24 @@ function installedVersion() {
|
|
|
346
366
|
return "unknown";
|
|
347
367
|
}
|
|
348
368
|
}
|
|
369
|
+
function installedVersion() {
|
|
370
|
+
return readVersion(SELF);
|
|
371
|
+
}
|
|
372
|
+
/** The copy that is executing right now — an npx run is not the installed one. */
|
|
373
|
+
function runningVersion() {
|
|
374
|
+
return readVersion(resolve(dirname(fileURLToPath(import.meta.url)), ".."));
|
|
375
|
+
}
|
|
376
|
+
/** Numeric semver compare; a pre-release suffix is ignored, this package never ships one. */
|
|
377
|
+
function isNewer(candidate, current) {
|
|
378
|
+
const parts = (v) => v.split(/[-+]/)[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
379
|
+
const a = parts(candidate);
|
|
380
|
+
const b = parts(current);
|
|
381
|
+
for (let i = 0; i < 3; i++) {
|
|
382
|
+
if ((a[i] ?? 0) !== (b[i] ?? 0))
|
|
383
|
+
return (a[i] ?? 0) > (b[i] ?? 0);
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
349
387
|
/**
|
|
350
388
|
* Replace ~/.gitstats/cli with the latest published package. The schedule points at an absolute
|
|
351
389
|
* path inside it and the config lives beside it, so neither is touched.
|
|
@@ -388,6 +426,41 @@ function update() {
|
|
|
388
426
|
rmSync(tmp, { recursive: true, force: true });
|
|
389
427
|
}
|
|
390
428
|
}
|
|
429
|
+
/**
|
|
430
|
+
* Asks the registry for a newer version at most once a day, installs it and restarts the sync under
|
|
431
|
+
* it. Nothing here may fail a sync: offline, a registry error or a bad download is one log line and
|
|
432
|
+
* the current version carries on. `--no-update` skips the check entirely.
|
|
433
|
+
* Returns the exit code of the restarted sync, or null when this process should carry on itself.
|
|
434
|
+
*/
|
|
435
|
+
async function selfUpdate(c) {
|
|
436
|
+
if (args.includes("--no-update"))
|
|
437
|
+
return null;
|
|
438
|
+
const last = c.lastUpdateCheck ? Date.parse(c.lastUpdateCheck) : 0;
|
|
439
|
+
if (Number.isFinite(last) && Date.now() - last < UPDATE_EVERY_MS)
|
|
440
|
+
return null;
|
|
441
|
+
// Stamped before the fetch, so a registry that is down is retried tomorrow and not every sync.
|
|
442
|
+
c.lastUpdateCheck = new Date().toISOString();
|
|
443
|
+
saveConfig(c);
|
|
444
|
+
const current = runningVersion();
|
|
445
|
+
try {
|
|
446
|
+
const res = await fetch(REGISTRY, { signal: AbortSignal.timeout(5000) });
|
|
447
|
+
if (!res.ok)
|
|
448
|
+
throw new Error(`registry answered ${res.status}`);
|
|
449
|
+
const meta = await res.json();
|
|
450
|
+
const latest = typeof meta === "object" && meta !== null ? meta.version : null;
|
|
451
|
+
if (typeof latest !== "string")
|
|
452
|
+
throw new Error("registry sent no version");
|
|
453
|
+
if (!isNewer(latest, current))
|
|
454
|
+
return null;
|
|
455
|
+
update();
|
|
456
|
+
const fresh = spawnSync(process.execPath, [join(SELF, "dist", "cli.js"), "sync", "--quiet"], { stdio: "inherit" });
|
|
457
|
+
return fresh.status ?? 1;
|
|
458
|
+
}
|
|
459
|
+
catch (e) {
|
|
460
|
+
logFile(`update check failed (${e instanceof Error ? e.message : String(e)}); staying on ${current}`);
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
391
464
|
function installSchedule() {
|
|
392
465
|
const script = installSelf();
|
|
393
466
|
const node = process.execPath;
|
|
@@ -611,7 +684,7 @@ async function main() {
|
|
|
611
684
|
return;
|
|
612
685
|
}
|
|
613
686
|
default:
|
|
614
|
-
log("usage: gitstats <link [--root <dir>]... [--yes] | sync | status | add <path> | roots add <dir> | emails add <email> | names on|off | pause | resume | update | unlink>");
|
|
687
|
+
log("usage: gitstats <link [--root <dir>]... [--yes] | sync [--no-fetch] [--no-update] | status | add <path> | roots add <dir> | emails add <email> | names on|off | pause | resume | update | unlink>");
|
|
615
688
|
}
|
|
616
689
|
}
|
|
617
690
|
main().catch((e) => {
|