hacklab 0.22.2 → 26.921.1
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 +23 -430
- package/dist/index.js +132 -23
- package/package.json +20 -42
- package/LICENSE +0 -21
- package/dist/agent-handoff.d.ts +0 -51
- package/dist/agent-handoff.d.ts.map +0 -1
- package/dist/agent-handoff.js +0 -91
- package/dist/agent-handoff.js.map +0 -1
- package/dist/api-client.d.ts +0 -18
- package/dist/api-client.d.ts.map +0 -1
- package/dist/api-client.js +0 -40
- package/dist/api-client.js.map +0 -1
- package/dist/belt.d.ts +0 -25
- package/dist/belt.d.ts.map +0 -1
- package/dist/belt.js +0 -113
- package/dist/belt.js.map +0 -1
- package/dist/card.d.ts +0 -66
- package/dist/card.d.ts.map +0 -1
- package/dist/card.js +0 -151
- package/dist/card.js.map +0 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -238
- package/dist/cli.js.map +0 -1
- package/dist/commands/book.d.ts +0 -14
- package/dist/commands/book.d.ts.map +0 -1
- package/dist/commands/book.js +0 -90
- package/dist/commands/book.js.map +0 -1
- package/dist/commands/chat.d.ts +0 -36
- package/dist/commands/chat.d.ts.map +0 -1
- package/dist/commands/chat.js +0 -642
- package/dist/commands/chat.js.map +0 -1
- package/dist/commands/config.d.ts +0 -2
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js +0 -84
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/daemon.d.ts +0 -19
- package/dist/commands/daemon.d.ts.map +0 -1
- package/dist/commands/daemon.js +0 -72
- package/dist/commands/daemon.js.map +0 -1
- package/dist/commands/drop.d.ts +0 -12
- package/dist/commands/drop.d.ts.map +0 -1
- package/dist/commands/drop.js +0 -103
- package/dist/commands/drop.js.map +0 -1
- package/dist/commands/drops.d.ts +0 -9
- package/dist/commands/drops.d.ts.map +0 -1
- package/dist/commands/drops.js +0 -96
- package/dist/commands/drops.js.map +0 -1
- package/dist/commands/essay.d.ts +0 -40
- package/dist/commands/essay.d.ts.map +0 -1
- package/dist/commands/essay.js +0 -651
- package/dist/commands/essay.js.map +0 -1
- package/dist/commands/hackathon.d.ts +0 -2
- package/dist/commands/hackathon.d.ts.map +0 -1
- package/dist/commands/hackathon.js +0 -893
- package/dist/commands/hackathon.js.map +0 -1
- package/dist/commands/hacker.d.ts +0 -2
- package/dist/commands/hacker.d.ts.map +0 -1
- package/dist/commands/hacker.js +0 -87
- package/dist/commands/hacker.js.map +0 -1
- package/dist/commands/jobs.d.ts +0 -18
- package/dist/commands/jobs.d.ts.map +0 -1
- package/dist/commands/jobs.js +0 -138
- package/dist/commands/jobs.js.map +0 -1
- package/dist/commands/keys.d.ts +0 -2
- package/dist/commands/keys.d.ts.map +0 -1
- package/dist/commands/keys.js +0 -138
- package/dist/commands/keys.js.map +0 -1
- package/dist/commands/login.d.ts +0 -83
- package/dist/commands/login.d.ts.map +0 -1
- package/dist/commands/login.js +0 -236
- package/dist/commands/login.js.map +0 -1
- package/dist/commands/logout.d.ts +0 -2
- package/dist/commands/logout.d.ts.map +0 -1
- package/dist/commands/logout.js +0 -22
- package/dist/commands/logout.js.map +0 -1
- package/dist/commands/org-jobs.d.ts +0 -50
- package/dist/commands/org-jobs.d.ts.map +0 -1
- package/dist/commands/org-jobs.js +0 -383
- package/dist/commands/org-jobs.js.map +0 -1
- package/dist/commands/org.d.ts +0 -67
- package/dist/commands/org.d.ts.map +0 -1
- package/dist/commands/org.js +0 -1244
- package/dist/commands/org.js.map +0 -1
- package/dist/commands/ping.d.ts +0 -2
- package/dist/commands/ping.d.ts.map +0 -1
- package/dist/commands/ping.js +0 -50
- package/dist/commands/ping.js.map +0 -1
- package/dist/commands/profile.d.ts +0 -72
- package/dist/commands/profile.d.ts.map +0 -1
- package/dist/commands/profile.js +0 -672
- package/dist/commands/profile.js.map +0 -1
- package/dist/commands/project.d.ts +0 -2
- package/dist/commands/project.d.ts.map +0 -1
- package/dist/commands/project.js +0 -583
- package/dist/commands/project.js.map +0 -1
- package/dist/commands/referral.d.ts +0 -6
- package/dist/commands/referral.d.ts.map +0 -1
- package/dist/commands/referral.js +0 -93
- package/dist/commands/referral.js.map +0 -1
- package/dist/commands/rtfm.d.ts +0 -9
- package/dist/commands/rtfm.d.ts.map +0 -1
- package/dist/commands/rtfm.js +0 -114
- package/dist/commands/rtfm.js.map +0 -1
- package/dist/commands/scan.d.ts +0 -13
- package/dist/commands/scan.d.ts.map +0 -1
- package/dist/commands/scan.js +0 -286
- package/dist/commands/scan.js.map +0 -1
- package/dist/commands/scout.d.ts +0 -18
- package/dist/commands/scout.d.ts.map +0 -1
- package/dist/commands/scout.js +0 -232
- package/dist/commands/scout.js.map +0 -1
- package/dist/commands/setup-rail.d.ts +0 -58
- package/dist/commands/setup-rail.d.ts.map +0 -1
- package/dist/commands/setup-rail.js +0 -135
- package/dist/commands/setup-rail.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -442
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/sync.d.ts +0 -9
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js +0 -345
- package/dist/commands/sync.js.map +0 -1
- package/dist/commands/update.d.ts +0 -2
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js +0 -92
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/whoami.d.ts +0 -2
- package/dist/commands/whoami.d.ts.map +0 -1
- package/dist/commands/whoami.js +0 -41
- package/dist/commands/whoami.js.map +0 -1
- package/dist/config.d.ts +0 -78
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -93
- package/dist/config.js.map +0 -1
- package/dist/daily-sync.d.ts +0 -150
- package/dist/daily-sync.d.ts.map +0 -1
- package/dist/daily-sync.js +0 -665
- package/dist/daily-sync.js.map +0 -1
- package/dist/dossier.d.ts +0 -73
- package/dist/dossier.d.ts.map +0 -1
- package/dist/dossier.js +0 -141
- package/dist/dossier.js.map +0 -1
- package/dist/field-normalize.d.ts +0 -4
- package/dist/field-normalize.d.ts.map +0 -1
- package/dist/field-normalize.js +0 -8
- package/dist/field-normalize.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/job-fields.d.ts +0 -47
- package/dist/job-fields.d.ts.map +0 -1
- package/dist/job-fields.js +0 -148
- package/dist/job-fields.js.map +0 -1
- package/dist/machine.d.ts +0 -15
- package/dist/machine.d.ts.map +0 -1
- package/dist/machine.js +0 -64
- package/dist/machine.js.map +0 -1
- package/dist/org-fields.d.ts +0 -57
- package/dist/org-fields.d.ts.map +0 -1
- package/dist/org-fields.js +0 -167
- package/dist/org-fields.js.map +0 -1
- package/dist/posthog.d.ts +0 -38
- package/dist/posthog.d.ts.map +0 -1
- package/dist/posthog.js +0 -224
- package/dist/posthog.js.map +0 -1
- package/dist/project-fields.d.ts +0 -40
- package/dist/project-fields.d.ts.map +0 -1
- package/dist/project-fields.js +0 -79
- package/dist/project-fields.js.map +0 -1
- package/dist/prompt-consent.d.ts +0 -73
- package/dist/prompt-consent.d.ts.map +0 -1
- package/dist/prompt-consent.js +0 -191
- package/dist/prompt-consent.js.map +0 -1
- package/dist/prompt-stats.d.ts +0 -226
- package/dist/prompt-stats.d.ts.map +0 -1
- package/dist/prompt-stats.js +0 -561
- package/dist/prompt-stats.js.map +0 -1
- package/dist/referral.d.ts +0 -14
- package/dist/referral.d.ts.map +0 -1
- package/dist/referral.js +0 -28
- package/dist/referral.js.map +0 -1
- package/dist/registry.d.ts +0 -15
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js +0 -180
- package/dist/registry.js.map +0 -1
- package/dist/resolve-command.d.ts +0 -20
- package/dist/resolve-command.d.ts.map +0 -1
- package/dist/resolve-command.js +0 -21
- package/dist/resolve-command.js.map +0 -1
- package/dist/scanners/antigravity.d.ts +0 -41
- package/dist/scanners/antigravity.d.ts.map +0 -1
- package/dist/scanners/antigravity.js +0 -210
- package/dist/scanners/antigravity.js.map +0 -1
- package/dist/scanners/github-copilot-telemetry.d.ts +0 -23
- package/dist/scanners/github-copilot-telemetry.d.ts.map +0 -1
- package/dist/scanners/github-copilot-telemetry.js +0 -220
- package/dist/scanners/github-copilot-telemetry.js.map +0 -1
- package/dist/scanners/github-copilot.d.ts +0 -68
- package/dist/scanners/github-copilot.d.ts.map +0 -1
- package/dist/scanners/github-copilot.js +0 -262
- package/dist/scanners/github-copilot.js.map +0 -1
- package/dist/scanners/incremental.d.ts +0 -282
- package/dist/scanners/incremental.d.ts.map +0 -1
- package/dist/scanners/incremental.js +0 -788
- package/dist/scanners/incremental.js.map +0 -1
- package/dist/scanners/index.d.ts +0 -116
- package/dist/scanners/index.d.ts.map +0 -1
- package/dist/scanners/index.js +0 -726
- package/dist/scanners/index.js.map +0 -1
- package/dist/scanners/sqlite.d.ts +0 -18
- package/dist/scanners/sqlite.d.ts.map +0 -1
- package/dist/scanners/sqlite.js +0 -63
- package/dist/scanners/sqlite.js.map +0 -1
- package/dist/scanners/util.d.ts +0 -112
- package/dist/scanners/util.d.ts.map +0 -1
- package/dist/scanners/util.js +0 -167
- package/dist/scanners/util.js.map +0 -1
- package/dist/session.d.ts +0 -106
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js +0 -191
- package/dist/session.js.map +0 -1
- package/dist/share-card-colors.d.ts +0 -19
- package/dist/share-card-colors.d.ts.map +0 -1
- package/dist/share-card-colors.js +0 -36
- package/dist/share-card-colors.js.map +0 -1
- package/dist/share-card.d.ts +0 -32
- package/dist/share-card.d.ts.map +0 -1
- package/dist/share-card.js +0 -420
- package/dist/share-card.js.map +0 -1
- package/dist/share.d.ts +0 -17
- package/dist/share.d.ts.map +0 -1
- package/dist/share.js +0 -77
- package/dist/share.js.map +0 -1
- package/dist/sync.d.ts +0 -100
- package/dist/sync.d.ts.map +0 -1
- package/dist/sync.js +0 -304
- package/dist/sync.js.map +0 -1
- package/dist/ui.d.ts +0 -33
- package/dist/ui.d.ts.map +0 -1
- package/dist/ui.js +0 -127
- package/dist/ui.js.map +0 -1
- package/dist/utils/npmGlobal.d.ts +0 -30
- package/dist/utils/npmGlobal.d.ts.map +0 -1
- package/dist/utils/npmGlobal.js +0 -69
- package/dist/utils/npmGlobal.js.map +0 -1
- package/dist/utils/openBrowser.d.ts +0 -14
- package/dist/utils/openBrowser.d.ts.map +0 -1
- package/dist/utils/openBrowser.js +0 -47
- package/dist/utils/openBrowser.js.map +0 -1
- package/dist/utils/updateCheck.d.ts +0 -16
- package/dist/utils/updateCheck.d.ts.map +0 -1
- package/dist/utils/updateCheck.js +0 -111
- package/dist/utils/updateCheck.js.map +0 -1
- package/dist/utils/waitForEnter.d.ts +0 -12
- package/dist/utils/waitForEnter.d.ts.map +0 -1
- package/dist/utils/waitForEnter.js +0 -43
- package/dist/utils/waitForEnter.js.map +0 -1
package/dist/daily-sync.js
DELETED
|
@@ -1,665 +0,0 @@
|
|
|
1
|
-
import { spawn } from 'node:child_process';
|
|
2
|
-
import { realpathSync } from 'node:fs';
|
|
3
|
-
import { appendFile, mkdir, readFile, rm, stat, writeFile, } from 'node:fs/promises';
|
|
4
|
-
import { homedir, platform } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { loadConfig, updateConfig } from './config.js';
|
|
7
|
-
import { getSessionPath } from './session.js';
|
|
8
|
-
import { bold, dim } from './ui.js';
|
|
9
|
-
// Sets up (and tears down) the two OS-native background jobs: a `hacklab sync
|
|
10
|
-
// --tick` every minute (the incremental one — reads only what the AI tools
|
|
11
|
-
// appended since the last run) and the full `hacklab sync --quiet` once a day
|
|
12
|
-
// (the repair pass, which also re-bases the tick's state). launchd on macOS, a
|
|
13
|
-
// systemd user timer on Linux, Task Scheduler on Windows, and printed
|
|
14
|
-
// instructions when none of those fit. The scheduled command is resolved to an
|
|
15
|
-
// absolute `node <entry>` pair so a scheduler running with a bare PATH
|
|
16
|
-
// (cron/launchd/systemd often have almost none) can launch it without a
|
|
17
|
-
// `hacklab` shim.
|
|
18
|
-
const LAUNCHD_LABEL = 'so.hacklab.sync';
|
|
19
|
-
const LAUNCHD_TICK_LABEL = 'so.hacklab.tick';
|
|
20
|
-
const SYSTEMD_UNIT = 'hacklab-sync';
|
|
21
|
-
const SYSTEMD_TICK_UNIT = 'hacklab-tick';
|
|
22
|
-
const SCHTASKS_TASK = 'hacklab-sync';
|
|
23
|
-
const SCHTASKS_TICK_TASK = 'hacklab-tick';
|
|
24
|
-
/** Bump when the plist/unit/wrapper templates, the log path derivation, or the
|
|
25
|
-
* way the command is resolved change, so machines with the old artifacts
|
|
26
|
-
* reinstall instead of keeping a schedule this version would no longer write.
|
|
27
|
-
* Part of the stored fingerprint.
|
|
28
|
-
*
|
|
29
|
-
* 2: the scheduled node path is now the version-stable one where there is one
|
|
30
|
-
* (see stableNodePath) — every existing install points at whatever
|
|
31
|
-
* `process.execPath` happened to be, so they all need re-arming. */
|
|
32
|
-
const TEMPLATE_VERSION = 2;
|
|
33
|
-
/** Run a command without ever rejecting: resolves the exit code (127 when the
|
|
34
|
-
* binary can't even be spawned, e.g. `systemctl` absent). */
|
|
35
|
-
function run(cmd, args, opts = {}) {
|
|
36
|
-
return new Promise((resolve) => {
|
|
37
|
-
const child = spawn(cmd, args, { stdio: 'ignore', ...opts });
|
|
38
|
-
child.on('error', () => resolve(127));
|
|
39
|
-
child.on('close', (code) => resolve(code ?? 0));
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/** The daily background job writes here; launchd/systemd also point stdout+stderr
|
|
43
|
-
* at it. Lives next to the session file so it honors HACKLAB_SESSION_PATH. */
|
|
44
|
-
export function syncLogPath() {
|
|
45
|
-
return join(dirname(getSessionPath()), 'sync.log');
|
|
46
|
-
}
|
|
47
|
-
function pausedMarkerPath() {
|
|
48
|
-
return join(dirname(getSessionPath()), 'sync-paused');
|
|
49
|
-
}
|
|
50
|
-
// The tick runs 1440 times a day, so the log has to be bounded even if every
|
|
51
|
-
// one of those runs has something to say.
|
|
52
|
-
export const SYNC_LOG_MAX_BYTES = 1_000_000;
|
|
53
|
-
export const SYNC_LOG_KEEP_LINES = 200;
|
|
54
|
-
/** Cut an oversized sync.log down to its last few hundred lines. Best-effort,
|
|
55
|
-
* and a no-op while the log is small — which is the normal case. */
|
|
56
|
-
export async function trimSyncLog(maxBytes = SYNC_LOG_MAX_BYTES, keepLines = SYNC_LOG_KEEP_LINES) {
|
|
57
|
-
try {
|
|
58
|
-
const path = syncLogPath();
|
|
59
|
-
if ((await stat(path)).size <= maxBytes)
|
|
60
|
-
return;
|
|
61
|
-
const lines = (await readFile(path, 'utf8')).split('\n');
|
|
62
|
-
const kept = lines.slice(-keepLines).join('\n');
|
|
63
|
-
await writeFile(path, kept.endsWith('\n') ? kept : `${kept}\n`, 'utf8');
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
// no log yet, or we can't rewrite it — either way, nothing to do
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/** Append a timestamped line to the background-sync log (best-effort). */
|
|
70
|
-
export async function appendSyncLog(line) {
|
|
71
|
-
try {
|
|
72
|
-
await mkdir(dirname(syncLogPath()), { recursive: true });
|
|
73
|
-
await appendFile(syncLogPath(), `${new Date().toISOString()} ${line}\n`);
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// logging must never break the job
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
/** Record that the background sync can't continue (e.g. the session expired), so
|
|
80
|
-
* the next interactive `hacklab` run can surface it. */
|
|
81
|
-
export async function markSyncPaused(reason) {
|
|
82
|
-
try {
|
|
83
|
-
await mkdir(dirname(pausedMarkerPath()), { recursive: true });
|
|
84
|
-
await writeFile(pausedMarkerPath(), reason, 'utf8');
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
// best-effort
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
/** The reason the background sync is paused, or null if it isn't. */
|
|
91
|
-
export async function readSyncPaused() {
|
|
92
|
-
try {
|
|
93
|
-
const reason = (await readFile(pausedMarkerPath(), 'utf8')).trim();
|
|
94
|
-
return reason.length > 0 ? reason : null;
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
return null;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
export async function clearSyncPaused() {
|
|
101
|
-
try {
|
|
102
|
-
await rm(pausedMarkerPath(), { force: true });
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// best-effort
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/** realpath, or null when the path doesn't resolve (missing, dangling symlink). */
|
|
109
|
-
function realOrNull(path) {
|
|
110
|
-
try {
|
|
111
|
-
return realpathSync(path);
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
return null;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
/** Well-known absolute paths that keep pointing at the current node across an
|
|
118
|
-
* upgrade. Windows has none to add: `process.execPath` there is already a
|
|
119
|
-
* version-stable install location, not a per-version keg. */
|
|
120
|
-
function stableNodeCandidates() {
|
|
121
|
-
if (platform() === 'win32')
|
|
122
|
-
return [];
|
|
123
|
-
const voltaHome = process.env.VOLTA_HOME;
|
|
124
|
-
const candidates = [
|
|
125
|
-
'/opt/homebrew/bin/node',
|
|
126
|
-
'/usr/local/bin/node',
|
|
127
|
-
'/usr/bin/node',
|
|
128
|
-
voltaHome ? join(voltaHome, 'bin', 'node') : null,
|
|
129
|
-
join(homedir(), '.volta', 'bin', 'node'),
|
|
130
|
-
];
|
|
131
|
-
return [...new Set(candidates.filter((c) => c !== null))];
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* An absolute path to *this* node that survives the next upgrade of it.
|
|
135
|
-
*
|
|
136
|
-
* `process.execPath` under a package manager is version-scoped:
|
|
137
|
-
* `/opt/homebrew/Cellar/node/26.5.0/bin/node`. `brew upgrade node` relinks
|
|
138
|
-
* `/opt/homebrew/bin/node` to the new keg and moves the shared libraries with
|
|
139
|
-
* it, leaving the old absolute path resolving to a binary that can no longer
|
|
140
|
-
* start ("Library not loaded: …/libada.3.dylib"). A scheduler holding that path
|
|
141
|
-
* dies silently, which is exactly what happened.
|
|
142
|
-
*
|
|
143
|
-
* So: resolve each well-known stable location and take the first that is
|
|
144
|
-
* *literally the same binary* as the one running now — identity by resolved
|
|
145
|
-
* real path, never by version string and never by trusting the symlink blindly.
|
|
146
|
-
* No match (fnm/nvm/asdf shims are per-shell and version-scoped, so they never
|
|
147
|
-
* match) falls back to `process.execPath`, which is what shipped before.
|
|
148
|
-
*
|
|
149
|
-
* It stays an absolute path either way, never a bare `node`: launchd, systemd
|
|
150
|
-
* and Task Scheduler run with almost no PATH, which is the whole reason this
|
|
151
|
-
* function returns a path at all. Don't "simplify" it to `node`.
|
|
152
|
-
*/
|
|
153
|
-
export function stableNodePath() {
|
|
154
|
-
const real = realOrNull(process.execPath);
|
|
155
|
-
if (!real)
|
|
156
|
-
return process.execPath;
|
|
157
|
-
for (const candidate of stableNodeCandidates()) {
|
|
158
|
-
if (realOrNull(candidate) === real)
|
|
159
|
-
return candidate;
|
|
160
|
-
}
|
|
161
|
-
return process.execPath;
|
|
162
|
-
}
|
|
163
|
-
/** Absolute `node` + the CLI's own entry script, so the scheduler doesn't depend
|
|
164
|
-
* on a `hacklab` bin being on its PATH. */
|
|
165
|
-
export function resolveSyncCommand() {
|
|
166
|
-
const argv1 = process.argv[1];
|
|
167
|
-
let script = argv1 ?? 'hacklab';
|
|
168
|
-
try {
|
|
169
|
-
if (argv1)
|
|
170
|
-
script = realpathSync(argv1);
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
// argv1 not resolvable (unusual) — fall back to the raw path.
|
|
174
|
-
}
|
|
175
|
-
return { node: stableNodePath(), script };
|
|
176
|
-
}
|
|
177
|
-
/** Identity of what the jobs were installed with: the command they run plus the
|
|
178
|
-
* template generation that produced them. Stored after a successful install so a
|
|
179
|
-
* later run can tell an up-to-date schedule from one worth replacing. */
|
|
180
|
-
export function syncCommandFingerprint(cmd) {
|
|
181
|
-
// JSON, not concatenation: a path can contain any separator we'd pick.
|
|
182
|
-
return JSON.stringify({
|
|
183
|
-
v: TEMPLATE_VERSION,
|
|
184
|
-
node: cmd.node,
|
|
185
|
-
script: cmd.script,
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
/** The command a stored fingerprint describes, or null when it's from an older
|
|
189
|
-
* (or unrecognizable) format — which reads as "reinstall and re-record". */
|
|
190
|
-
function parseFingerprint(fingerprint) {
|
|
191
|
-
if (!fingerprint)
|
|
192
|
-
return null;
|
|
193
|
-
try {
|
|
194
|
-
const parsed = JSON.parse(fingerprint);
|
|
195
|
-
if (typeof parsed !== 'object' || parsed === null)
|
|
196
|
-
return null;
|
|
197
|
-
const { v, node, script } = parsed;
|
|
198
|
-
if (v !== TEMPLATE_VERSION)
|
|
199
|
-
return null;
|
|
200
|
-
if (typeof node !== 'string' || typeof script !== 'string')
|
|
201
|
-
return null;
|
|
202
|
-
return { node, script };
|
|
203
|
-
}
|
|
204
|
-
catch {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
async function exists(path) {
|
|
209
|
-
try {
|
|
210
|
-
await stat(path);
|
|
211
|
-
return true;
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
return false;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Whether the command the scheduler is already running still works — both halves
|
|
219
|
-
* of it still on disk. A user who alternates node versions (nvm) or checkouts
|
|
220
|
-
* flips the fingerprint back and forth without breaking anything, and tearing
|
|
221
|
-
* the jobs down and rebuilding them on every alternation is worse than leaving a
|
|
222
|
-
* working schedule alone.
|
|
223
|
-
*/
|
|
224
|
-
async function recordedCommandUsable(record) {
|
|
225
|
-
const recorded = parseFingerprint(record.command);
|
|
226
|
-
if (!recorded)
|
|
227
|
-
return false;
|
|
228
|
-
return (await exists(recorded.node)) && (await exists(recorded.script));
|
|
229
|
-
}
|
|
230
|
-
/** Randomize the time so installs don't all hit the API at the same instant. */
|
|
231
|
-
function randomTime() {
|
|
232
|
-
return {
|
|
233
|
-
hour: Math.floor(Math.random() * 24),
|
|
234
|
-
minute: Math.floor(Math.random() * 60),
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
function storedTime(record) {
|
|
238
|
-
const { hour, minute } = record ?? {};
|
|
239
|
-
const valid = (n, max) => typeof n === 'number' && Number.isInteger(n) && n >= 0 && n < max;
|
|
240
|
-
return valid(hour, 24) && valid(minute, 60)
|
|
241
|
-
? { hour: hour, minute: minute }
|
|
242
|
-
: null;
|
|
243
|
-
}
|
|
244
|
-
async function loadDailySyncRecord() {
|
|
245
|
-
return (await loadConfig()).dailySync;
|
|
246
|
-
}
|
|
247
|
-
/** Remember this install. False when the config couldn't be written — the jobs
|
|
248
|
-
* are scheduled either way, but nothing will know that next time. */
|
|
249
|
-
async function saveDailySyncRecord(record) {
|
|
250
|
-
return updateConfig((config) => ({ ...config, dailySync: record }));
|
|
251
|
-
}
|
|
252
|
-
async function clearDailySyncRecord() {
|
|
253
|
-
// No record means nothing to clear: `logout` and `daemon off` shouldn't
|
|
254
|
-
// rewrite config.json for every user who never had a schedule.
|
|
255
|
-
await updateConfig(({ dailySync, ...rest }) => (dailySync ? rest : null));
|
|
256
|
-
}
|
|
257
|
-
function xmlEscape(s) {
|
|
258
|
-
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
259
|
-
}
|
|
260
|
-
const pad2 = (n) => String(n).padStart(2, '0');
|
|
261
|
-
/** The plist skeleton both jobs share — same command shape, different schedule. */
|
|
262
|
-
function launchdDoc(label, cmd, mode, schedule, logPath) {
|
|
263
|
-
const args = [cmd.node, cmd.script, 'sync', mode]
|
|
264
|
-
.map((a) => ` <string>${xmlEscape(a)}</string>`)
|
|
265
|
-
.join('\n');
|
|
266
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
267
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
268
|
-
<plist version="1.0">
|
|
269
|
-
<dict>
|
|
270
|
-
<key>Label</key>
|
|
271
|
-
<string>${label}</string>
|
|
272
|
-
<key>ProgramArguments</key>
|
|
273
|
-
<array>
|
|
274
|
-
${args}
|
|
275
|
-
</array>
|
|
276
|
-
${schedule}
|
|
277
|
-
<key>StandardOutPath</key>
|
|
278
|
-
<string>${xmlEscape(logPath)}</string>
|
|
279
|
-
<key>StandardErrorPath</key>
|
|
280
|
-
<string>${xmlEscape(logPath)}</string>
|
|
281
|
-
<key>ProcessType</key>
|
|
282
|
-
<string>Background</string>
|
|
283
|
-
</dict>
|
|
284
|
-
</plist>
|
|
285
|
-
`;
|
|
286
|
-
}
|
|
287
|
-
export function launchdPlist(cmd, time, logPath) {
|
|
288
|
-
return launchdDoc(LAUNCHD_LABEL, cmd, '--quiet', ` <key>StartCalendarInterval</key>
|
|
289
|
-
<dict>
|
|
290
|
-
<key>Hour</key>
|
|
291
|
-
<integer>${time.hour}</integer>
|
|
292
|
-
<key>Minute</key>
|
|
293
|
-
<integer>${time.minute}</integer>
|
|
294
|
-
</dict>`, logPath);
|
|
295
|
-
}
|
|
296
|
-
/** The minutely tick. StartInterval (not a calendar interval) so launchd just
|
|
297
|
-
* re-runs it every 60s; a tick with nothing to report exits without a request. */
|
|
298
|
-
export function launchdTickPlist(cmd, logPath) {
|
|
299
|
-
return launchdDoc(LAUNCHD_TICK_LABEL, cmd, '--tick', ` <key>StartInterval</key>
|
|
300
|
-
<integer>60</integer>`, logPath);
|
|
301
|
-
}
|
|
302
|
-
export function systemdService(cmd) {
|
|
303
|
-
// Quote node + script so paths with spaces survive systemd's parser.
|
|
304
|
-
return `[Unit]
|
|
305
|
-
Description=hacklab daily token sync
|
|
306
|
-
|
|
307
|
-
[Service]
|
|
308
|
-
Type=oneshot
|
|
309
|
-
ExecStart="${cmd.node}" "${cmd.script}" sync --quiet
|
|
310
|
-
`;
|
|
311
|
-
}
|
|
312
|
-
export function systemdTimer() {
|
|
313
|
-
// OnCalendar=daily with a randomized delay spreads load across users; Persistent
|
|
314
|
-
// catches up a run missed while the machine was off.
|
|
315
|
-
return `[Unit]
|
|
316
|
-
Description=hacklab daily token sync
|
|
317
|
-
|
|
318
|
-
[Timer]
|
|
319
|
-
OnCalendar=daily
|
|
320
|
-
Persistent=true
|
|
321
|
-
RandomizedDelaySec=3600
|
|
322
|
-
|
|
323
|
-
[Install]
|
|
324
|
-
WantedBy=timers.target
|
|
325
|
-
`;
|
|
326
|
-
}
|
|
327
|
-
export function systemdTickService(cmd) {
|
|
328
|
-
return `[Unit]
|
|
329
|
-
Description=hacklab token tick
|
|
330
|
-
|
|
331
|
-
[Service]
|
|
332
|
-
Type=oneshot
|
|
333
|
-
ExecStart="${cmd.node}" "${cmd.script}" sync --tick
|
|
334
|
-
`;
|
|
335
|
-
}
|
|
336
|
-
export function systemdTickTimer() {
|
|
337
|
-
// OnUnitActiveSec re-arms a minute after each run finishes (so a slow tick
|
|
338
|
-
// can't stack up), OnBootSec gives the desktop a moment to settle first, and
|
|
339
|
-
// AccuracySec lets systemd batch the wakeup with others — a minutely timer is
|
|
340
|
-
// otherwise a needless drain on a laptop.
|
|
341
|
-
return `[Unit]
|
|
342
|
-
Description=hacklab token tick
|
|
343
|
-
|
|
344
|
-
[Timer]
|
|
345
|
-
OnBootSec=2min
|
|
346
|
-
OnUnitActiveSec=1min
|
|
347
|
-
AccuracySec=30s
|
|
348
|
-
|
|
349
|
-
[Install]
|
|
350
|
-
WantedBy=timers.target
|
|
351
|
-
`;
|
|
352
|
-
}
|
|
353
|
-
/** Where the Windows scheduled task's wrapper batch file lives. Next to the
|
|
354
|
-
* session file so it honors HACKLAB_SESSION_PATH, mirroring syncLogPath(). */
|
|
355
|
-
export function syncWrapperPath() {
|
|
356
|
-
return join(dirname(getSessionPath()), 'hacklab-sync.cmd');
|
|
357
|
-
}
|
|
358
|
-
/** The tick's own wrapper — same pattern, its own file so the two tasks can be
|
|
359
|
-
* created and deleted independently. */
|
|
360
|
-
export function tickWrapperPath() {
|
|
361
|
-
return join(dirname(getSessionPath()), 'hacklab-tick.cmd');
|
|
362
|
-
}
|
|
363
|
-
/** The batch wrapper Task Scheduler runs. schtasks' `/TR` chokes on the nested
|
|
364
|
-
* quotes a full `"node" "script" …` command needs, so we point the task at this
|
|
365
|
-
* one-line .cmd instead and do the quoting here, where plain-batch rules apply.
|
|
366
|
-
* Output is redirected to the same sync.log the job also appends to, so a crash
|
|
367
|
-
* before the CLI's own logging still leaves a trace. */
|
|
368
|
-
export function schtasksWrapper(cmd, logPath) {
|
|
369
|
-
return `@echo off\r\n"${cmd.node}" "${cmd.script}" sync --quiet >> "${logPath}" 2>&1\r\n`;
|
|
370
|
-
}
|
|
371
|
-
/** Same wrapper for the minutely tick, logging to the same sync.log. */
|
|
372
|
-
export function schtasksTickWrapper(cmd, logPath) {
|
|
373
|
-
return `@echo off\r\n"${cmd.node}" "${cmd.script}" sync --tick >> "${logPath}" 2>&1\r\n`;
|
|
374
|
-
}
|
|
375
|
-
/** Args for `schtasks /Create`. The `/TR` value is a single pre-quoted path;
|
|
376
|
-
* spawn must run with windowsVerbatimArguments so Node doesn't re-escape it. */
|
|
377
|
-
export function schtasksCreateArgs(wrapperPath, time) {
|
|
378
|
-
return [
|
|
379
|
-
'/Create',
|
|
380
|
-
'/SC',
|
|
381
|
-
'DAILY',
|
|
382
|
-
'/TN',
|
|
383
|
-
SCHTASKS_TASK,
|
|
384
|
-
'/TR',
|
|
385
|
-
`"${wrapperPath}"`,
|
|
386
|
-
'/ST',
|
|
387
|
-
`${pad2(time.hour)}:${pad2(time.minute)}`,
|
|
388
|
-
'/F',
|
|
389
|
-
];
|
|
390
|
-
}
|
|
391
|
-
/** Args for `schtasks /Create` for the tick: every minute, forever. */
|
|
392
|
-
export function schtasksTickCreateArgs(wrapperPath) {
|
|
393
|
-
return [
|
|
394
|
-
'/Create',
|
|
395
|
-
'/SC',
|
|
396
|
-
'MINUTE',
|
|
397
|
-
'/MO',
|
|
398
|
-
'1',
|
|
399
|
-
'/TN',
|
|
400
|
-
SCHTASKS_TICK_TASK,
|
|
401
|
-
'/TR',
|
|
402
|
-
`"${wrapperPath}"`,
|
|
403
|
-
'/F',
|
|
404
|
-
];
|
|
405
|
-
}
|
|
406
|
-
// The fallback for "Linux without a systemd user manager" and "an OS we don't
|
|
407
|
-
// auto-schedule on" alike, so it names no specific mechanism it tried.
|
|
408
|
-
export function manualInstructions(cmd) {
|
|
409
|
-
const bin = `"${cmd.node}" "${cmd.script}"`;
|
|
410
|
-
return [
|
|
411
|
-
{ cadence: 'every minute', command: `${bin} sync --tick` },
|
|
412
|
-
{ cadence: 'once a day', command: `${bin} sync --quiet` },
|
|
413
|
-
];
|
|
414
|
-
}
|
|
415
|
-
/** Render a ManualSchedule as ready-to-print lines: a dim explainer, then each
|
|
416
|
-
* job as a dim cadence label above its bold, copy-pasteable command. Blank
|
|
417
|
-
* lines separate the two commands so neither reads as a wrapped continuation of
|
|
418
|
-
* the other. */
|
|
419
|
-
export function formatManualSchedule(schedule) {
|
|
420
|
-
const lines = [
|
|
421
|
-
dim("Schedule these with cron, a systemd timer, or your init system's scheduler:"),
|
|
422
|
-
];
|
|
423
|
-
for (const { cadence, command } of schedule) {
|
|
424
|
-
lines.push('', dim(`${cadence}:`), bold(command));
|
|
425
|
-
}
|
|
426
|
-
return lines;
|
|
427
|
-
}
|
|
428
|
-
function launchAgentPath(label) {
|
|
429
|
-
return join(homedir(), 'Library', 'LaunchAgents', `${label}.plist`);
|
|
430
|
-
}
|
|
431
|
-
/** Write a plist and (re)load it. Unload first so a re-run replaces the agent
|
|
432
|
-
* rather than stacking a second copy. */
|
|
433
|
-
async function loadAgent(path, contents) {
|
|
434
|
-
await mkdir(dirname(path), { recursive: true });
|
|
435
|
-
await writeFile(path, contents, 'utf8');
|
|
436
|
-
await run('launchctl', ['unload', path]);
|
|
437
|
-
return run('launchctl', ['load', '-w', path]);
|
|
438
|
-
}
|
|
439
|
-
async function installLaunchd(cmd, logPath, time) {
|
|
440
|
-
const plistPath = launchAgentPath(LAUNCHD_LABEL);
|
|
441
|
-
const loaded = await loadAgent(plistPath, launchdPlist(cmd, time, logPath));
|
|
442
|
-
await loadAgent(launchAgentPath(LAUNCHD_TICK_LABEL), launchdTickPlist(cmd, logPath));
|
|
443
|
-
// A plist in ~/Library/LaunchAgents also loads at next login, so even if
|
|
444
|
-
// `load` fails now the agent activates then — report that honestly rather
|
|
445
|
-
// than claiming it's already running.
|
|
446
|
-
return {
|
|
447
|
-
ok: true,
|
|
448
|
-
mechanism: 'launchd',
|
|
449
|
-
tick: true,
|
|
450
|
-
detail: loaded === 0
|
|
451
|
-
? `tick every minute, full sync daily around ${pad2(time.hour)}:${pad2(time.minute)} (${plistPath})`
|
|
452
|
-
: `installed (${plistPath}) — activates at next login`,
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
async function installSystemd(cmd) {
|
|
456
|
-
// systemd captures stdout/stderr in the journal (journalctl --user), so unlike
|
|
457
|
-
// launchd there's no log file to point at here.
|
|
458
|
-
// Probe the user manager before writing anything, so a box without systemd
|
|
459
|
-
// (or without a user D-Bus) gets clean manual instructions and no litter.
|
|
460
|
-
if ((await run('systemctl', ['--user', 'daemon-reload'])) !== 0) {
|
|
461
|
-
return {
|
|
462
|
-
ok: false,
|
|
463
|
-
mechanism: 'manual',
|
|
464
|
-
instructions: manualInstructions(cmd),
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
const dir = join(homedir(), '.config', 'systemd', 'user');
|
|
468
|
-
await mkdir(dir, { recursive: true });
|
|
469
|
-
await writeFile(join(dir, `${SYSTEMD_UNIT}.service`), systemdService(cmd), 'utf8');
|
|
470
|
-
await writeFile(join(dir, `${SYSTEMD_UNIT}.timer`), systemdTimer(), 'utf8');
|
|
471
|
-
await writeFile(join(dir, `${SYSTEMD_TICK_UNIT}.service`), systemdTickService(cmd), 'utf8');
|
|
472
|
-
await writeFile(join(dir, `${SYSTEMD_TICK_UNIT}.timer`), systemdTickTimer(), 'utf8');
|
|
473
|
-
await run('systemctl', ['--user', 'daemon-reload']);
|
|
474
|
-
await run('systemctl', ['--user', 'enable', '--now', `${SYSTEMD_UNIT}.timer`]);
|
|
475
|
-
await run('systemctl', [
|
|
476
|
-
'--user',
|
|
477
|
-
'enable',
|
|
478
|
-
'--now',
|
|
479
|
-
`${SYSTEMD_TICK_UNIT}.timer`,
|
|
480
|
-
]);
|
|
481
|
-
return {
|
|
482
|
-
ok: true,
|
|
483
|
-
mechanism: 'systemd',
|
|
484
|
-
tick: true,
|
|
485
|
-
detail: `systemd user timers (${SYSTEMD_TICK_UNIT}.timer every minute, ${SYSTEMD_UNIT}.timer daily); logs in journalctl --user -u ${SYSTEMD_UNIT}. To keep them running while logged out: loginctl enable-linger`,
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
async function installSchtasks(cmd, time) {
|
|
489
|
-
const wrapperPath = syncWrapperPath();
|
|
490
|
-
await mkdir(dirname(wrapperPath), { recursive: true });
|
|
491
|
-
await writeFile(wrapperPath, schtasksWrapper(cmd, syncLogPath()), 'utf8');
|
|
492
|
-
await writeFile(tickWrapperPath(), schtasksTickWrapper(cmd, syncLogPath()), 'utf8');
|
|
493
|
-
// /F overwrites an existing task (idempotent re-install). Verbatim args so the
|
|
494
|
-
// pre-quoted /TR path reaches schtasks intact. A non-zero exit (e.g. schtasks
|
|
495
|
-
// absent, or policy blocks task creation) falls back to manual instructions.
|
|
496
|
-
const created = await run('schtasks', schtasksCreateArgs(wrapperPath, time), {
|
|
497
|
-
windowsVerbatimArguments: true,
|
|
498
|
-
});
|
|
499
|
-
if (created !== 0) {
|
|
500
|
-
return {
|
|
501
|
-
ok: false,
|
|
502
|
-
mechanism: 'manual',
|
|
503
|
-
instructions: manualInstructions(cmd),
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
// The daily job is the one that must exist; a refused minutely task (some
|
|
507
|
-
// policies cap task frequency) costs freshness, not the streak.
|
|
508
|
-
const tick = await run('schtasks', schtasksTickCreateArgs(tickWrapperPath()), { windowsVerbatimArguments: true });
|
|
509
|
-
return {
|
|
510
|
-
ok: true,
|
|
511
|
-
mechanism: 'schtasks',
|
|
512
|
-
tick: tick === 0,
|
|
513
|
-
detail: `${tick === 0 ? 'tick every minute, ' : ''}full sync daily around ${pad2(time.hour)}:${pad2(time.minute)} (Task Scheduler task "${SCHTASKS_TASK}")`,
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* True when the OS scheduler has the background jobs. A half-install (say a
|
|
518
|
-
* plist that got deleted) reads as not installed, so the next run repairs it.
|
|
519
|
-
*
|
|
520
|
-
* `expectTick: false` drops the tick from that expectation, for the one machine
|
|
521
|
-
* that legitimately has none: a Windows box whose policy refused the minutely
|
|
522
|
-
* task. Without it such a box would read as half-installed forever and reinstall
|
|
523
|
-
* on every scan. Callers that don't know either way get the strict check.
|
|
524
|
-
*/
|
|
525
|
-
export async function dailySyncInstalled({ expectTick = true, } = {}) {
|
|
526
|
-
const os = platform();
|
|
527
|
-
const tickToo = (daily, tick) => expectTick ? [daily, tick] : [daily];
|
|
528
|
-
try {
|
|
529
|
-
if (os === 'darwin') {
|
|
530
|
-
for (const label of tickToo(LAUNCHD_LABEL, LAUNCHD_TICK_LABEL)) {
|
|
531
|
-
await stat(launchAgentPath(label));
|
|
532
|
-
}
|
|
533
|
-
return true;
|
|
534
|
-
}
|
|
535
|
-
if (os === 'linux') {
|
|
536
|
-
const dir = join(homedir(), '.config', 'systemd', 'user');
|
|
537
|
-
for (const unit of tickToo(SYSTEMD_UNIT, SYSTEMD_TICK_UNIT)) {
|
|
538
|
-
await stat(join(dir, `${unit}.timer`));
|
|
539
|
-
}
|
|
540
|
-
return true;
|
|
541
|
-
}
|
|
542
|
-
if (os === 'win32') {
|
|
543
|
-
for (const task of tickToo(SCHTASKS_TASK, SCHTASKS_TICK_TASK)) {
|
|
544
|
-
if ((await run('schtasks', ['/Query', '/TN', task])) !== 0)
|
|
545
|
-
return false;
|
|
546
|
-
}
|
|
547
|
-
return true;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
catch {
|
|
551
|
-
// missing file / scheduler
|
|
552
|
-
}
|
|
553
|
-
return false;
|
|
554
|
-
}
|
|
555
|
-
/**
|
|
556
|
-
* What a routine run (e.g. `scan`) should do about the background jobs:
|
|
557
|
-
*
|
|
558
|
-
* 'missing' — no jobs, or only half of them: install, and say so.
|
|
559
|
-
* 'stale' — jobs exist but what they run is broken or unknown: reinstall
|
|
560
|
-
* quietly.
|
|
561
|
-
* 'current' — jobs exist and still work: do nothing.
|
|
562
|
-
*
|
|
563
|
-
* "Still work" is deliberately weaker than "match what this process would
|
|
564
|
-
* install": someone alternating node versions or checkouts flips the fingerprint
|
|
565
|
-
* on every run while both schedules remain perfectly runnable, and rebuilding
|
|
566
|
-
* the jobs each way round would be a permanent ping-pong. Only a command that
|
|
567
|
-
* has actually gone missing (or a record we can't read, which includes installs
|
|
568
|
-
* predating the fingerprint) earns a reinstall.
|
|
569
|
-
*/
|
|
570
|
-
export async function dailySyncState() {
|
|
571
|
-
const record = await loadDailySyncRecord();
|
|
572
|
-
// Only a record that saw the tick get refused excuses a missing tick job.
|
|
573
|
-
const expectTick = record?.tick !== false;
|
|
574
|
-
if (!(await dailySyncInstalled({ expectTick })))
|
|
575
|
-
return 'missing';
|
|
576
|
-
if (!record?.command)
|
|
577
|
-
return 'stale';
|
|
578
|
-
if (record.command === syncCommandFingerprint(resolveSyncCommand())) {
|
|
579
|
-
return 'current';
|
|
580
|
-
}
|
|
581
|
-
return (await recordedCommandUsable(record)) ? 'current' : 'stale';
|
|
582
|
-
}
|
|
583
|
-
/** Install (or refresh) both background jobs (tick + daily) for the current OS. */
|
|
584
|
-
export async function installDailySync() {
|
|
585
|
-
const cmd = resolveSyncCommand();
|
|
586
|
-
try {
|
|
587
|
-
await mkdir(dirname(syncLogPath()), { recursive: true }).catch(() => undefined);
|
|
588
|
-
// Reuse the slot we picked the first time: re-rolling it on every reinstall
|
|
589
|
-
// would keep moving the user's daily sync around the clock.
|
|
590
|
-
const time = storedTime(await loadDailySyncRecord()) ?? randomTime();
|
|
591
|
-
const os = platform();
|
|
592
|
-
let result;
|
|
593
|
-
if (os === 'darwin') {
|
|
594
|
-
result = await installLaunchd(cmd, syncLogPath(), time);
|
|
595
|
-
}
|
|
596
|
-
else if (os === 'linux') {
|
|
597
|
-
// systemd does its own load-spreading (RandomizedDelaySec), so `time` is
|
|
598
|
-
// recorded but unused there.
|
|
599
|
-
result = await installSystemd(cmd);
|
|
600
|
-
}
|
|
601
|
-
else if (os === 'win32') {
|
|
602
|
-
result = await installSchtasks(cmd, time);
|
|
603
|
-
}
|
|
604
|
-
else {
|
|
605
|
-
return {
|
|
606
|
-
ok: false,
|
|
607
|
-
mechanism: 'unsupported',
|
|
608
|
-
instructions: manualInstructions(cmd),
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
if (!result.ok)
|
|
612
|
-
return result;
|
|
613
|
-
const recorded = await saveDailySyncRecord({
|
|
614
|
-
command: syncCommandFingerprint(cmd),
|
|
615
|
-
...time,
|
|
616
|
-
tick: result.tick,
|
|
617
|
-
});
|
|
618
|
-
return { ...result, recorded };
|
|
619
|
-
}
|
|
620
|
-
catch {
|
|
621
|
-
// A failed write (permissions, read-only home, …) must not crash the command
|
|
622
|
-
// ritual — fall back to printable instructions.
|
|
623
|
-
return {
|
|
624
|
-
ok: false,
|
|
625
|
-
mechanism: 'manual',
|
|
626
|
-
instructions: manualInstructions(cmd),
|
|
627
|
-
};
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
/** Remove both background jobs. Best-effort and never throws, so it's safe to
|
|
631
|
-
* call from `logout` whether or not they were ever installed. */
|
|
632
|
-
export async function uninstallDailySync() {
|
|
633
|
-
const os = platform();
|
|
634
|
-
try {
|
|
635
|
-
if (os === 'darwin') {
|
|
636
|
-
for (const label of [LAUNCHD_LABEL, LAUNCHD_TICK_LABEL]) {
|
|
637
|
-
const plistPath = launchAgentPath(label);
|
|
638
|
-
await run('launchctl', ['unload', plistPath]);
|
|
639
|
-
await rm(plistPath, { force: true });
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
else if (os === 'linux') {
|
|
643
|
-
const dir = join(homedir(), '.config', 'systemd', 'user');
|
|
644
|
-
for (const unit of [SYSTEMD_UNIT, SYSTEMD_TICK_UNIT]) {
|
|
645
|
-
await run('systemctl', ['--user', 'disable', '--now', `${unit}.timer`]);
|
|
646
|
-
await rm(join(dir, `${unit}.timer`), { force: true });
|
|
647
|
-
await rm(join(dir, `${unit}.service`), { force: true });
|
|
648
|
-
}
|
|
649
|
-
await run('systemctl', ['--user', 'daemon-reload']);
|
|
650
|
-
}
|
|
651
|
-
else if (os === 'win32') {
|
|
652
|
-
await run('schtasks', ['/Delete', '/TN', SCHTASKS_TASK, '/F']);
|
|
653
|
-
await run('schtasks', ['/Delete', '/TN', SCHTASKS_TICK_TASK, '/F']);
|
|
654
|
-
await rm(syncWrapperPath(), { force: true });
|
|
655
|
-
await rm(tickWrapperPath(), { force: true });
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
catch {
|
|
659
|
-
// best-effort
|
|
660
|
-
}
|
|
661
|
-
// Forget the command and the slot too, so a fresh install after a re-join
|
|
662
|
-
// picks a new random time rather than resurrecting the old one.
|
|
663
|
-
await clearDailySyncRecord();
|
|
664
|
-
}
|
|
665
|
-
//# sourceMappingURL=daily-sync.js.map
|