hacklab 0.13.1 → 0.14.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.
@@ -0,0 +1,265 @@
1
+ import * as clack from '@clack/prompts';
2
+ import { dailySyncState, installDailySync } from '../daily-sync.js';
3
+ import { captureEvent, identifyUser } from '../posthog.js';
4
+ import { loadPromptConsent, savePromptConsent, } from '../prompt-consent.js';
5
+ import { rebuildScanState } from '../scanners/incremental.js';
6
+ import { collectToolScans, mergeToolScans, } from '../scanners/index.js';
7
+ import { loadSession, resolveAppUrl, saveSession, } from '../session.js';
8
+ import { scanConsentedPromptStats, uploadTokenScan } from '../sync.js';
9
+ import { bold, dim, error, info, success } from '../ui.js';
10
+ import { ensureHandleClaimed, performLogin } from './login.js';
11
+ import { formatScanReceipt } from './scan.js';
12
+ /**
13
+ * `hacklab setup` — the front door.
14
+ *
15
+ * scan → anonymous rank → GitHub auth → one consent question → upload → daemon
16
+ *
17
+ * The one deliberate composite command in the CLI (DESIGN.md's "one job per
18
+ * command" carries a recorded exception for it): face-to-face testing showed
19
+ * that install → `login` → `scan` reads as three unrelated chores to someone who
20
+ * has never used hacklab, and people stalled between them. Each stage still
21
+ * degrades on its own terms, and every piece remains its own command for anyone
22
+ * who wants to re-run just that piece.
23
+ */
24
+ export async function setup() {
25
+ clack.intro(bold('Hacklab CLI setup'));
26
+ console.log(dim('scans your AI usage, connects GitHub, and starts a background usage sync'));
27
+ console.log('');
28
+ const existing = await loadSession();
29
+ const appUrl = resolveAppUrl(existing);
30
+ const syncState = await dailySyncState();
31
+ // ── Already set up ───────────────────────────────────────────────────────
32
+ // A finished account AND a live daemon means there is genuinely nothing to
33
+ // do. Anything less falls through and skips only the stages already done.
34
+ if (existing?.handle && existing.usernameClaimed && syncState === 'current') {
35
+ console.log(`already set up — you're ${bold(`@${existing.handle}`)}`);
36
+ console.log(dim('re-scan with `hacklab scan`, re-upload with `hacklab sync`'));
37
+ clack.outro(dim(`${appUrl}/${existing.handle}`));
38
+ return;
39
+ }
40
+ // ── Scan (before auth — an anonymous scan needs no account) ──────────────
41
+ const spin = clack.spinner();
42
+ spin.start('scanning local AI tool usage');
43
+ let results;
44
+ try {
45
+ results = await collectToolScans();
46
+ }
47
+ catch (err) {
48
+ spin.stop('scan failed');
49
+ clack.cancel(`couldn't read local AI usage: ${err instanceof Error ? err.message : String(err)}`);
50
+ process.exit(1);
51
+ }
52
+ spin.stop('scan complete');
53
+ const scan = mergeToolScans(results);
54
+ if (scan.grandTotal > 0) {
55
+ console.log('');
56
+ for (const line of formatScanReceipt(scan))
57
+ console.log(line);
58
+ console.log('');
59
+ }
60
+ else {
61
+ // No hard floor: a hackathon-door signup on a fresh laptop is exactly who
62
+ // this flow is for, so the default is yes.
63
+ const cont = await clack.confirm({
64
+ message: 'No AI usage found on this machine. Set up your account anyway?',
65
+ initialValue: true,
66
+ });
67
+ if (clack.isCancel(cont) || !cont) {
68
+ clack.outro(dim('come back after some Claude Code / Codex / Cursor sessions.'));
69
+ return;
70
+ }
71
+ }
72
+ // ── Anonymous rank preview (a hook, never a gate) ────────────────────────
73
+ let previewRank;
74
+ if (scan.grandTotal > 0) {
75
+ const preview = await fetchRankPreview(appUrl, scan.grandTotal);
76
+ if (preview) {
77
+ previewRank = preview.rank;
78
+ console.log(`${bold(`you'd be #${preview.rank}`)} of ${preview.ofTotal} hackers`);
79
+ console.log('');
80
+ }
81
+ }
82
+ // ── GitHub auth (the exact device flow `hacklab login` runs) ─────────────
83
+ let session;
84
+ let claimFailed;
85
+ if (existing) {
86
+ // Reuse a session that's already on disk. It may still be half-finished
87
+ // (auth succeeded, the claim didn't), so run the claim guard over it.
88
+ const outcome = await ensureHandleClaimed(existing, 2);
89
+ if (outcome.session !== existing)
90
+ await saveSession(outcome.session);
91
+ session = outcome.session;
92
+ claimFailed = outcome.claimFailed;
93
+ console.log(session.handle
94
+ ? `signed in as @${session.handle}`
95
+ : `signed in as ${session.email}`);
96
+ console.log('');
97
+ }
98
+ else {
99
+ try {
100
+ const outcome = await performLogin({ claimAttempts: 2 });
101
+ session = outcome.session;
102
+ claimFailed = outcome.claimFailed;
103
+ }
104
+ catch (err) {
105
+ clack.cancel(`GitHub sign-in failed: ${err instanceof Error ? err.message : String(err)}. run \`hacklab setup\` again.`);
106
+ process.exit(1);
107
+ }
108
+ console.log('');
109
+ console.log(session.handle
110
+ ? `signed in as @${session.handle}`
111
+ : `signed in as ${session.email}`);
112
+ console.log('');
113
+ }
114
+ // A lost claim must never be silent here: the web onboarding page polls
115
+ // `username_claimed` and would sit spinning forever.
116
+ if (claimFailed) {
117
+ error("couldn't finish claiming your handle");
118
+ info(dim('run `hacklab login` again to finish it'));
119
+ console.log('');
120
+ }
121
+ // ── The one question in the flow ─────────────────────────────────────────
122
+ const consent = await askConversationConsent();
123
+ // ── Upload ───────────────────────────────────────────────────────────────
124
+ spin.start('saving your usage');
125
+ let rank;
126
+ try {
127
+ const promptStats = await scanConsentedPromptStats(consent);
128
+ const uploaded = await uploadTokenScan(session, scan, {
129
+ interactive: true,
130
+ timeoutMs: 120_000,
131
+ promptStats,
132
+ });
133
+ // A full scan just went out, so re-base the minutely tick's incremental
134
+ // state on it — otherwise the first tick re-uploads the whole history.
135
+ await rebuildScanState(results);
136
+ spin.stop('usage saved');
137
+ const after = uploaded.rankAfter;
138
+ if (typeof after === 'number' && Number.isFinite(after))
139
+ rank = after;
140
+ if (rank)
141
+ console.log(`rank: #${rank}`);
142
+ }
143
+ catch {
144
+ // Never fatal — the account exists, and `hacklab sync` is the recovery.
145
+ spin.stop('usage sync deferred');
146
+ console.log(dim('run `hacklab sync` later to upload it'));
147
+ }
148
+ // ── Daemon (silent; the intro line is the whole disclosure) ──────────────
149
+ await installDaemon(syncState, session.handle);
150
+ // ── Tail ─────────────────────────────────────────────────────────────────
151
+ console.log('');
152
+ success(session.handle ? `you're in — ${appUrl}/${session.handle}` : "you're in");
153
+ clack.outro(dim('head back to your browser — the page will move on by itself'));
154
+ if (session.handle) {
155
+ await identifyUser(session.handle, {
156
+ $set: {
157
+ email: session.email,
158
+ handle: session.handle,
159
+ app_url: appUrl,
160
+ },
161
+ $set_once: { joined_at: new Date().toISOString() },
162
+ });
163
+ await captureEvent(session.handle, 'cli_setup_completed', {
164
+ tokens_total: scan.grandTotal,
165
+ ...((rank ?? previewRank) ? { rank: rank ?? previewRank } : {}),
166
+ prompt_consent: consent,
167
+ });
168
+ }
169
+ }
170
+ /**
171
+ * Ask once whether to share conversation text, defaulting to yes.
172
+ *
173
+ * Deliberately NOT prompt-consent.ts's `askYesNo` (which refuses to carry a
174
+ * default): setup is the one place a default-yes is the product decision, and
175
+ * `clack.confirm` with `initialValue: true` still requires the user to accept it
176
+ * — a keypress they perform, not a value applied behind their back. `sync`'s
177
+ * no-default disclosure is untouched. A non-TTY run has nobody to accept, so it
178
+ * stays at 'none'.
179
+ */
180
+ async function askConversationConsent() {
181
+ const stored = await loadPromptConsent();
182
+ if (stored) {
183
+ console.log(dim(`conversation sharing: ${stored} — change with \`hacklab config prompt-stats <full|stats|none>\``));
184
+ console.log('');
185
+ return stored;
186
+ }
187
+ if (!process.stdin.isTTY)
188
+ return 'none';
189
+ console.log(bold('share your prompts?'));
190
+ console.log('hacklab scores how well you work with AI. your real prompts give the');
191
+ console.log('scoring model actual evidence — a sharper score, and a profile that');
192
+ console.log('stands out. a sample of up to 20,000 characters is read by the model,');
193
+ console.log('scored, and discarded. never stored, never shown to anyone.');
194
+ console.log(dim('change anytime: hacklab config prompt-stats <full|stats|none>'));
195
+ console.log('');
196
+ const answer = await clack.confirm({
197
+ message: 'share a sample of your prompts?',
198
+ initialValue: true,
199
+ });
200
+ // Only the two tiers that were actually asked about. 'stats' (numbers, no
201
+ // text) was never on the table here; it stays reachable via `hacklab config`.
202
+ const tier = !clack.isCancel(answer) && answer === true ? 'full' : 'none';
203
+ await savePromptConsent(tier);
204
+ return tier;
205
+ }
206
+ /** The anonymous "you'd be #N of M" call. Any failure is skipped in silence. */
207
+ async function fetchRankPreview(appUrl, totalTokens) {
208
+ try {
209
+ const res = await fetch(`${appUrl}/api/rank/preview`, {
210
+ method: 'POST',
211
+ headers: { 'Content-Type': 'application/json' },
212
+ body: JSON.stringify({ totalTokens }),
213
+ signal: AbortSignal.timeout(10_000),
214
+ });
215
+ if (!res.ok)
216
+ return null;
217
+ const data = (await res.json());
218
+ if (typeof data.rank !== 'number' || typeof data.ofTotal !== 'number') {
219
+ return null;
220
+ }
221
+ return { rank: data.rank, ofTotal: data.ofTotal };
222
+ }
223
+ catch {
224
+ return null;
225
+ }
226
+ }
227
+ /**
228
+ * Arm the background sync without asking — the intro line already disclosed it.
229
+ * Same install-once / repair-'stale' rules `scan` uses: a reinstall rewrites the
230
+ * plists/units and bounces the jobs, so only touch the scheduler when something
231
+ * is actually wrong, and only announce a genuinely fresh install.
232
+ */
233
+ async function installDaemon(state, handle) {
234
+ if (state === 'current')
235
+ return;
236
+ const result = await installDailySync();
237
+ if (!result.ok) {
238
+ // A failed *repair* means the jobs are still scheduled — printing cron
239
+ // instructions there talks the user into a second, duplicate schedule.
240
+ if (state === 'missing') {
241
+ console.log(dim(result.instructions));
242
+ if (handle) {
243
+ await captureEvent(handle, 'cli_daily_sync_manual', {
244
+ mechanism: result.mechanism,
245
+ source: 'setup',
246
+ });
247
+ }
248
+ }
249
+ return;
250
+ }
251
+ if (!result.recorded) {
252
+ console.log(dim('could not record daily-sync state — config unwritable'));
253
+ return;
254
+ }
255
+ if (state === 'missing') {
256
+ console.log(dim(`background sync scheduled (${result.mechanism})`));
257
+ if (handle) {
258
+ await captureEvent(handle, 'cli_daily_sync_installed', {
259
+ mechanism: result.mechanism,
260
+ source: 'setup',
261
+ });
262
+ }
263
+ }
264
+ }
265
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.js","sourceRoot":"","sources":["../../src/commands/setup.ts"],"names":[],"mappings":"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"}
package/dist/config.d.ts CHANGED
@@ -1,6 +1,24 @@
1
+ /**
2
+ * What the OS-native background jobs were last installed with — written by
3
+ * daily-sync.ts on a successful install, cleared on uninstall. `command`
4
+ * fingerprints the scheduled `node <script>` pair plus the template generation
5
+ * that produced the jobs, so a routine run can tell a working schedule from one
6
+ * worth replacing; hour/minute pin the daily slot so a reinstall doesn't re-roll
7
+ * it. Opaque here: daily-sync.ts owns the format.
8
+ */
9
+ export type DailySyncRecord = {
10
+ command: string;
11
+ hour?: number;
12
+ minute?: number;
13
+ /** False when the scheduler accepted the daily job but refused the minutely
14
+ * tick (a Windows policy cap), so a later run doesn't read that machine as a
15
+ * half-install and reinstall on every scan. */
16
+ tick?: boolean;
17
+ };
1
18
  export type HacklabConfig = {
2
19
  cursorApiKey?: string;
3
20
  cursorEmail?: string;
21
+ dailySync?: DailySyncRecord;
4
22
  /**
5
23
  * Consent for uploading Claude Code conversation data: 'none' | 'stats' |
6
24
  * 'full'. Absent means never asked — see prompt-consent.ts, which owns the
@@ -36,4 +54,18 @@ export type CursorAuth = {
36
54
  export declare function resolveCursorAuth(): Promise<CursorAuth>;
37
55
  export declare function loadConfig(): Promise<HacklabConfig>;
38
56
  export declare function saveConfig(config: HacklabConfig): Promise<void>;
57
+ /**
58
+ * Read-modify-write the config file for background callers that must not lose
59
+ * the rest of it. Unlike `loadConfig`, which flattens every read failure to
60
+ * `{}`, this refuses to write when the file exists but can't be parsed —
61
+ * spreading `{}` over an unreadable config would silently drop the user's cursor
62
+ * key and prompt-stats consent, and nothing here is worth that.
63
+ *
64
+ * `mutate` returns the config to write, or null for "nothing to change" (no
65
+ * write at all — a background job shouldn't rewrite config.json for nothing).
66
+ * Never throws: resolves true when the config reflects the mutation, false when
67
+ * the write was refused or failed, so the caller can react to not being
68
+ * remembered.
69
+ */
70
+ export declare function updateConfig(mutate: (config: HacklabConfig) => HacklabConfig | null): Promise<boolean>;
39
71
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,CAAA;AAID,sFAAsF;AACtF,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;AAExD,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,gBAAgB,CAAA;IAC9B,WAAW,EAAE,gBAAgB,CAAA;CAC9B,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CAgB7D;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC,CAOzD;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrE"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;mDAE+C;IAC/C,IAAI,CAAC,EAAE,OAAO,CAAA;CACf,CAAA;AAED,MAAM,MAAM,aAAa,GAAG;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,eAAe,CAAA;IAC3B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,CAAA;AAID,sFAAsF;AACtF,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAA;AAExD,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,EAAE,gBAAgB,CAAA;IAC9B,WAAW,EAAE,gBAAgB,CAAA;CAC9B,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,UAAU,CAAC,CAgB7D;AAED,wBAAsB,UAAU,IAAI,OAAO,CAAC,aAAa,CAAC,CAOzD;AAED,wBAAsB,UAAU,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAGrE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,aAAa,GAAG,IAAI,GACtD,OAAO,CAAC,OAAO,CAAC,CA6BlB"}
package/dist/config.js CHANGED
@@ -46,4 +46,48 @@ export async function saveConfig(config) {
46
46
  await mkdir(dirname(CONFIG_PATH), { recursive: true });
47
47
  await writeFile(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
48
48
  }
49
+ /**
50
+ * Read-modify-write the config file for background callers that must not lose
51
+ * the rest of it. Unlike `loadConfig`, which flattens every read failure to
52
+ * `{}`, this refuses to write when the file exists but can't be parsed —
53
+ * spreading `{}` over an unreadable config would silently drop the user's cursor
54
+ * key and prompt-stats consent, and nothing here is worth that.
55
+ *
56
+ * `mutate` returns the config to write, or null for "nothing to change" (no
57
+ * write at all — a background job shouldn't rewrite config.json for nothing).
58
+ * Never throws: resolves true when the config reflects the mutation, false when
59
+ * the write was refused or failed, so the caller can react to not being
60
+ * remembered.
61
+ */
62
+ export async function updateConfig(mutate) {
63
+ let current = {};
64
+ try {
65
+ const parsed = JSON.parse(await readFile(CONFIG_PATH, 'utf8'));
66
+ // A file that parses to a non-object (`null`, an array, a bare string) is
67
+ // just as unreadable as one that doesn't parse: refuse it too.
68
+ if (typeof parsed !== 'object' ||
69
+ parsed === null ||
70
+ Array.isArray(parsed)) {
71
+ return false;
72
+ }
73
+ current = parsed;
74
+ }
75
+ catch (err) {
76
+ // No config yet is the normal first-run case; anything else means there IS
77
+ // content we can't see, so leave it alone.
78
+ if (err?.code !== 'ENOENT')
79
+ return false;
80
+ }
81
+ const next = mutate(current);
82
+ if (!next)
83
+ return true;
84
+ try {
85
+ await saveConfig(next);
86
+ return true;
87
+ }
88
+ catch {
89
+ // Read-only home, no permission, disk full — the caller decides what to say.
90
+ return false;
91
+ }
92
+ }
49
93
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACjC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAazC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,EAAE,aAAa,CAAC,CAAA;AAY9D;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAA;IACjC,MAAM,IAAI,GAAG,CACX,OAAe,EACf,WAA+B,EACS,EAAE;QAC1C,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAA;QAC7C,IAAI,QAAQ;YAAE,OAAO,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAA;QACtC,IAAI,WAAW;YAAE,OAAO,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAA;QAC/C,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IAC5B,CAAC,CAAA;IAED,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,GAAG,IAAI,CAAC,gBAAgB,EAAE,MAAM,CAAC,YAAY,CAAC,CAAA;IAC1E,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,cAAc,EAAE,MAAM,CAAC,WAAW,CAAC,CAAA;IAErE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,CAAA;AACrD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAkB,CAAA;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,MAAqB;IACpD,MAAM,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACtD,MAAM,SAAS,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;AAC9E,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
@@ -2,15 +2,27 @@ export type SyncCommand = {
2
2
  node: string;
3
3
  script: string;
4
4
  };
5
- export type InstallResult = {
6
- ok: true;
5
+ type ScheduledJobs = {
7
6
  mechanism: 'launchd' | 'systemd' | 'schtasks';
8
7
  detail: string;
9
- } | {
8
+ /** Whether the minutely tick job registered too. Only the schtasks path can
9
+ * come back false: some Windows policies cap task frequency, and a refused
10
+ * tick costs freshness, not the streak — so we keep the daily job and
11
+ * remember not to expect a tick task that will never exist. */
12
+ tick: boolean;
13
+ };
14
+ type InstallFailure = {
10
15
  ok: false;
11
16
  mechanism: 'manual' | 'unsupported';
12
17
  instructions: string;
13
18
  };
19
+ export type InstallResult = ({
20
+ ok: true;
21
+ /** Whether we managed to remember this install (see updateConfig). False
22
+ * means the next run can't tell the schedule is current and will reinstall
23
+ * — worth saying out loud rather than churning in silence. */
24
+ recorded: boolean;
25
+ } & ScheduledJobs) | InstallFailure;
14
26
  /** The daily background job writes here; launchd/systemd also point stdout+stderr
15
27
  * at it. Lives next to the session file so it honors HACKLAB_SESSION_PATH. */
16
28
  export declare function syncLogPath(): string;
@@ -30,6 +42,10 @@ export declare function clearSyncPaused(): Promise<void>;
30
42
  /** Absolute `node` + the CLI's own entry script, so the scheduler doesn't depend
31
43
  * on a `hacklab` bin being on its PATH. */
32
44
  export declare function resolveSyncCommand(): SyncCommand;
45
+ /** Identity of what the jobs were installed with: the command they run plus the
46
+ * template generation that produced them. Stored after a successful install so a
47
+ * later run can tell an up-to-date schedule from one worth replacing. */
48
+ export declare function syncCommandFingerprint(cmd: SyncCommand): string;
33
49
  export declare function launchdPlist(cmd: SyncCommand, time: {
34
50
  hour: number;
35
51
  minute: number;
@@ -64,11 +80,38 @@ export declare function schtasksCreateArgs(wrapperPath: string, time: {
64
80
  /** Args for `schtasks /Create` for the tick: every minute, forever. */
65
81
  export declare function schtasksTickCreateArgs(wrapperPath: string): string[];
66
82
  export declare function manualInstructions(cmd: SyncCommand): string;
67
- /** True when the OS scheduler already has the daily job. */
68
- export declare function dailySyncInstalled(): Promise<boolean>;
83
+ /**
84
+ * True when the OS scheduler has the background jobs. A half-install (say a
85
+ * plist that got deleted) reads as not installed, so the next run repairs it.
86
+ *
87
+ * `expectTick: false` drops the tick from that expectation, for the one machine
88
+ * that legitimately has none: a Windows box whose policy refused the minutely
89
+ * task. Without it such a box would read as half-installed forever and reinstall
90
+ * on every scan. Callers that don't know either way get the strict check.
91
+ */
92
+ export declare function dailySyncInstalled({ expectTick, }?: {
93
+ expectTick?: boolean;
94
+ }): Promise<boolean>;
95
+ /**
96
+ * What a routine run (e.g. `scan`) should do about the background jobs:
97
+ *
98
+ * 'missing' — no jobs, or only half of them: install, and say so.
99
+ * 'stale' — jobs exist but what they run is broken or unknown: reinstall
100
+ * quietly.
101
+ * 'current' — jobs exist and still work: do nothing.
102
+ *
103
+ * "Still work" is deliberately weaker than "match what this process would
104
+ * install": someone alternating node versions or checkouts flips the fingerprint
105
+ * on every run while both schedules remain perfectly runnable, and rebuilding
106
+ * the jobs each way round would be a permanent ping-pong. Only a command that
107
+ * has actually gone missing (or a record we can't read, which includes installs
108
+ * predating the fingerprint) earns a reinstall.
109
+ */
110
+ export declare function dailySyncState(): Promise<'missing' | 'stale' | 'current'>;
69
111
  /** Install (or refresh) both background jobs (tick + daily) for the current OS. */
70
112
  export declare function installDailySync(): Promise<InstallResult>;
71
113
  /** Remove both background jobs. Best-effort and never throws, so it's safe to
72
114
  * call from `logout` whether or not they were ever installed. */
73
115
  export declare function uninstallDailySync(): Promise<void>;
116
+ export {};
74
117
  //# sourceMappingURL=daily-sync.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"daily-sync.d.ts","sourceRoot":"","sources":["../src/daily-sync.ts"],"names":[],"mappings":"AA+BA,MAAM,MAAM,WAAW,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAA;AAE1D,MAAM,MAAM,aAAa,GACrB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,SAAS,EAAE,SAAS,GAAG,SAAS,GAAG,UAAU,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC3E;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,SAAS,EAAE,QAAQ,GAAG,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAAA;AAgB5E;8EAC8E;AAC9E,wBAAgB,WAAW,IAAI,MAAM,CAEpC;AAQD,eAAO,MAAM,kBAAkB,UAAY,CAAA;AAC3C,eAAO,MAAM,mBAAmB,MAAM,CAAA;AAEtC;oEACoE;AACpE,wBAAsB,WAAW,CAC/B,QAAQ,SAAqB,EAC7B,SAAS,SAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAUf;AAED,0EAA0E;AAC1E,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO/D;AAED;wDACwD;AACxD,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOlE;AAED,qEAAqE;AACrE,wBAAsB,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAO7D;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAMrD;AAED;2CAC2C;AAC3C,wBAAgB,kBAAkB,IAAI,WAAW,CAShD;AAyCD,wBAAgB,YAAY,CAC1B,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACtC,OAAO,EAAE,MAAM,GACd,MAAM,CAcR;AAED;kFACkF;AAClF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAS1E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CASvD;AAED,wBAAgB,YAAY,IAAI,MAAM,CAcrC;AAED,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAQ3D;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAgBzC;AAED;8EAC8E;AAC9E,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;wCACwC;AACxC,wBAAgB,eAAe,IAAI,MAAM,CAExC;AAED;;;;wDAIwD;AACxD,wBAAgB,eAAe,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEzE;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED;gFACgF;AAChF,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACrC,MAAM,EAAE,CAaV;AAED,uEAAuE;AACvE,wBAAgB,sBAAsB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,EAAE,CAapE;AAID,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,CAS3D;AAiID,4DAA4D;AAC5D,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,OAAO,CAAC,CAoB3D;AAED,mFAAmF;AACnF,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,aAAa,CAAC,CAwB/D;AAED;iEACiE;AACjE,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CA0BxD"}
1
+ {"version":3,"file":"daily-sync.d.ts","sourceRoot":"","sources":["../src/daily-sync.ts"],"names":[],"mappings":"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"}