harborloop 0.3.0 → 0.4.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.
Files changed (120) hide show
  1. package/bin/checksums.json +12 -0
  2. package/bin/harbor.mjs +188 -0
  3. package/package.json +14 -30
  4. package/dist/api.d.ts +0 -109
  5. package/dist/api.js +0 -409
  6. package/dist/auth/login.d.ts +0 -14
  7. package/dist/auth/login.js +0 -158
  8. package/dist/auth/loopback.d.ts +0 -11
  9. package/dist/auth/loopback.js +0 -87
  10. package/dist/auth/pkce.d.ts +0 -7
  11. package/dist/auth/pkce.js +0 -14
  12. package/dist/auth/store.d.ts +0 -26
  13. package/dist/auth/store.js +0 -107
  14. package/dist/cli-release.d.ts +0 -31
  15. package/dist/cli-release.js +0 -102
  16. package/dist/commands/bind.d.ts +0 -23
  17. package/dist/commands/bind.js +0 -86
  18. package/dist/commands/context.d.ts +0 -12
  19. package/dist/commands/context.js +0 -80
  20. package/dist/commands/daemon.d.ts +0 -39
  21. package/dist/commands/daemon.js +0 -140
  22. package/dist/commands/doctor.d.ts +0 -16
  23. package/dist/commands/doctor.js +0 -937
  24. package/dist/commands/init.d.ts +0 -36
  25. package/dist/commands/init.js +0 -192
  26. package/dist/commands/status.d.ts +0 -5
  27. package/dist/commands/status.js +0 -164
  28. package/dist/commands/sync.d.ts +0 -40
  29. package/dist/commands/sync.js +0 -242
  30. package/dist/commands/toggle.d.ts +0 -16
  31. package/dist/commands/toggle.js +0 -69
  32. package/dist/commands/unbind.d.ts +0 -14
  33. package/dist/commands/unbind.js +0 -60
  34. package/dist/commands/workspaces.d.ts +0 -18
  35. package/dist/commands/workspaces.js +0 -41
  36. package/dist/context-budget.d.ts +0 -40
  37. package/dist/context-budget.js +0 -259
  38. package/dist/daemon/install.d.ts +0 -50
  39. package/dist/daemon/install.js +0 -192
  40. package/dist/daemon/loop.d.ts +0 -15
  41. package/dist/daemon/loop.js +0 -84
  42. package/dist/daemon/state.d.ts +0 -79
  43. package/dist/daemon/state.js +0 -213
  44. package/dist/daemon/tick.d.ts +0 -37
  45. package/dist/daemon/tick.js +0 -231
  46. package/dist/documents/apply.d.ts +0 -21
  47. package/dist/documents/apply.js +0 -58
  48. package/dist/documents/bundle.d.ts +0 -41
  49. package/dist/documents/bundle.js +0 -43
  50. package/dist/documents/inspect.d.ts +0 -12
  51. package/dist/documents/inspect.js +0 -33
  52. package/dist/documents/manifest.d.ts +0 -14
  53. package/dist/documents/manifest.js +0 -52
  54. package/dist/documents/pointer.d.ts +0 -32
  55. package/dist/documents/pointer.js +0 -135
  56. package/dist/documents/write.d.ts +0 -57
  57. package/dist/documents/write.js +0 -211
  58. package/dist/endpoints.d.ts +0 -7
  59. package/dist/endpoints.js +0 -15
  60. package/dist/env-bundle.d.ts +0 -25
  61. package/dist/env-bundle.js +0 -33
  62. package/dist/env-scan.d.ts +0 -27
  63. package/dist/env-scan.js +0 -41
  64. package/dist/env-sync.d.ts +0 -31
  65. package/dist/env-sync.js +0 -37
  66. package/dist/index.d.ts +0 -11
  67. package/dist/index.js +0 -282
  68. package/dist/install-hooks.d.ts +0 -11
  69. package/dist/install-hooks.js +0 -64
  70. package/dist/lock.d.ts +0 -31
  71. package/dist/lock.js +0 -192
  72. package/dist/machine.d.ts +0 -13
  73. package/dist/machine.js +0 -53
  74. package/dist/memories.d.ts +0 -31
  75. package/dist/memories.js +0 -74
  76. package/dist/origin.d.ts +0 -4
  77. package/dist/origin.js +0 -59
  78. package/dist/paths.d.ts +0 -12
  79. package/dist/paths.js +0 -45
  80. package/dist/providers/claude.d.ts +0 -65
  81. package/dist/providers/claude.js +0 -412
  82. package/dist/providers/codex-trust.d.ts +0 -36
  83. package/dist/providers/codex-trust.js +0 -141
  84. package/dist/providers/codex.d.ts +0 -46
  85. package/dist/providers/codex.js +0 -520
  86. package/dist/providers/cursor.d.ts +0 -45
  87. package/dist/providers/cursor.js +0 -344
  88. package/dist/providers/detect.d.ts +0 -16
  89. package/dist/providers/detect.js +0 -70
  90. package/dist/providers/hook-coverage.d.ts +0 -5
  91. package/dist/providers/hook-coverage.js +0 -47
  92. package/dist/providers/hook-entry.d.ts +0 -1
  93. package/dist/providers/hook-entry.js +0 -8
  94. package/dist/providers/surfaces.d.ts +0 -19
  95. package/dist/providers/surfaces.js +0 -91
  96. package/dist/reclaim.d.ts +0 -17
  97. package/dist/reclaim.js +0 -33
  98. package/dist/render.d.ts +0 -17
  99. package/dist/render.js +0 -61
  100. package/dist/repo-sync.d.ts +0 -27
  101. package/dist/repo-sync.js +0 -34
  102. package/dist/repos.d.ts +0 -11
  103. package/dist/repos.js +0 -82
  104. package/dist/skills.d.ts +0 -84
  105. package/dist/skills.js +0 -383
  106. package/dist/slugify.d.ts +0 -1
  107. package/dist/slugify.js +0 -8
  108. package/dist/state.d.ts +0 -23
  109. package/dist/state.js +0 -161
  110. package/dist/sync-report.d.ts +0 -59
  111. package/dist/sync-report.js +0 -87
  112. package/hooks/_recall.mjs +0 -288
  113. package/hooks/_receipt.mjs +0 -47
  114. package/hooks/_shared.mjs +0 -1610
  115. package/hooks/_upgrade.mjs +0 -51
  116. package/hooks/session-capture.mjs +0 -60
  117. package/hooks/session-compact.mjs +0 -19
  118. package/hooks/session-context.mjs +0 -258
  119. package/hooks/session-report.mjs +0 -42
  120. package/hooks/session-toolcontext.mjs +0 -183
@@ -1,192 +0,0 @@
1
- import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
4
- import { TICK_INTERVAL_MS } from './loop.js';
5
- export const LAUNCHD_LABEL = 'com.harbor.daemon';
6
- export const SYSTEMD_UNIT = 'harbor-daemon';
7
- export function installPlatform(platform = process.platform) {
8
- if (platform === 'darwin')
9
- return 'darwin';
10
- if (platform === 'linux')
11
- return 'linux';
12
- return 'unsupported';
13
- }
14
- export function daemonProgram(argv = process.argv, execPath = process.execPath) {
15
- const script = argv[1];
16
- if (script === undefined || script === '') {
17
- throw new Error('Could not work out which script to schedule. Run `harbor daemon --install` from the installed CLI.');
18
- }
19
- return { interpreter: execPath, script };
20
- }
21
- export function launchAgentPath(home = homedir()) {
22
- return join(home, 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
23
- }
24
- export function systemdDir(home = homedir()) {
25
- return join(home, '.config', 'systemd', 'user');
26
- }
27
- const escapeXml = (value) => value.replace(/[<>&'"]/g, (c) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', "'": '&apos;', '"': '&quot;' })[c]);
28
- export function launchAgentPlist(program, harborHome, intervalSeconds = Math.round(TICK_INTERVAL_MS / 1000)) {
29
- return `<?xml version="1.0" encoding="UTF-8"?>
30
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
31
- <plist version="1.0">
32
- <dict>
33
- <key>Label</key>
34
- <string>${LAUNCHD_LABEL}</string>
35
- <key>ProgramArguments</key>
36
- <array>
37
- <string>${escapeXml(program.interpreter)}</string>
38
- <string>${escapeXml(program.script)}</string>
39
- <string>daemon</string>
40
- <string>--once</string>
41
- </array>
42
- <key>RunAtLoad</key>
43
- <true/>
44
- <key>StartInterval</key>
45
- <integer>${intervalSeconds}</integer>
46
- <key>ProcessType</key>
47
- <string>Background</string>
48
- <key>StandardOutPath</key>
49
- <string>${escapeXml(join(harborHome, 'daemon.log'))}</string>
50
- <key>StandardErrorPath</key>
51
- <string>${escapeXml(join(harborHome, 'daemon.log'))}</string>
52
- </dict>
53
- </plist>
54
- `;
55
- }
56
- export function systemdService(program) {
57
- return `[Unit]
58
- Description=Harbor — keep this machine's team context current
59
- Documentation=https://github.com/harbor
60
-
61
- [Service]
62
- Type=oneshot
63
- ExecStart=${program.interpreter} ${program.script} daemon --once
64
- `;
65
- }
66
- export function systemdTimer(intervalSeconds = Math.round(TICK_INTERVAL_MS / 1000)) {
67
- return `[Unit]
68
- Description=Harbor sync timer
69
-
70
- [Timer]
71
- OnBootSec=1min
72
- OnUnitActiveSec=${intervalSeconds}s
73
- Persistent=true
74
-
75
- [Install]
76
- WantedBy=timers.target
77
- `;
78
- }
79
- export function installPlan(input) {
80
- const home = input.home ?? homedir();
81
- const platform = input.platform ?? installPlatform();
82
- if (platform === 'darwin') {
83
- const plist = launchAgentPath(home);
84
- const domain = `gui/${input.uid ?? process.getuid?.() ?? 0}`;
85
- return {
86
- platform,
87
- files: [{ path: plist, body: launchAgentPlist(input.program, input.harborHome) }],
88
- activate: [
89
- ['launchctl', 'bootout', domain, plist],
90
- ['launchctl', 'bootstrap', domain, plist],
91
- ],
92
- deactivate: [['launchctl', 'bootout', domain, plist]],
93
- };
94
- }
95
- if (platform === 'linux') {
96
- const dir = systemdDir(home);
97
- return {
98
- platform,
99
- files: [
100
- {
101
- path: join(dir, `${SYSTEMD_UNIT}.service`),
102
- body: systemdService(input.program),
103
- },
104
- { path: join(dir, `${SYSTEMD_UNIT}.timer`), body: systemdTimer() },
105
- ],
106
- activate: [
107
- ['systemctl', '--user', 'daemon-reload'],
108
- ['systemctl', '--user', 'enable', '--now', `${SYSTEMD_UNIT}.timer`],
109
- ],
110
- deactivate: [
111
- ['systemctl', '--user', 'disable', '--now', `${SYSTEMD_UNIT}.timer`],
112
- ],
113
- };
114
- }
115
- return { platform, files: [], activate: [], deactivate: [] };
116
- }
117
- export const spawnRunner = async (argv) => {
118
- const { execFile } = await import('node:child_process');
119
- return new Promise((resolve) => {
120
- execFile(argv[0], [...argv.slice(1)], (err, _stdout, stderr) => {
121
- resolve({
122
- ok: err === null,
123
- message: err === null ? '' : (stderr.trim() || err.message),
124
- });
125
- });
126
- });
127
- };
128
- export async function installDaemon(input) {
129
- const { plan } = input;
130
- if (plan.platform === 'unsupported') {
131
- return {
132
- kind: 'unsupported',
133
- files: [],
134
- manual: [],
135
- detail: 'Scheduling is not wired for this platform yet. Run `harbor daemon` in a terminal, or call `harbor daemon --once` from your own scheduler.',
136
- };
137
- }
138
- const run = input.run ?? spawnRunner;
139
- for (const file of plan.files) {
140
- await mkdir(join(file.path, '..'), { recursive: true });
141
- await writeFile(file.path, file.body, 'utf8');
142
- }
143
- const manual = [];
144
- for (const argv of plan.activate) {
145
- const result = await run(argv);
146
- if (!result.ok && argv === plan.activate[plan.activate.length - 1]) {
147
- manual.push(argv);
148
- }
149
- }
150
- return {
151
- kind: manual.length === 0 ? 'installed' : 'partial',
152
- files: plan.files.map((f) => f.path),
153
- manual,
154
- detail: manual.length === 0
155
- ? 'Harbor will keep this machine current in the background.'
156
- : 'The files are written, but the scheduler did not pick them up. Run the command below by hand.',
157
- };
158
- }
159
- export async function uninstallDaemon(input) {
160
- const { plan } = input;
161
- if (plan.platform === 'unsupported') {
162
- return {
163
- kind: 'unsupported',
164
- files: [],
165
- manual: [],
166
- detail: 'Nothing was scheduled on this platform.',
167
- };
168
- }
169
- const run = input.run ?? spawnRunner;
170
- for (const argv of plan.deactivate)
171
- await run(argv);
172
- for (const file of plan.files)
173
- await rm(file.path, { force: true });
174
- return {
175
- kind: 'removed',
176
- files: plan.files.map((f) => f.path),
177
- manual: [],
178
- detail: 'Background syncing is off. `harbor sync` still works by hand.',
179
- };
180
- }
181
- export async function installedFiles(plan) {
182
- const found = [];
183
- for (const file of plan.files) {
184
- try {
185
- await readFile(file.path, 'utf8');
186
- found.push(file.path);
187
- }
188
- catch {
189
- }
190
- }
191
- return found;
192
- }
@@ -1,15 +0,0 @@
1
- import type { Endpoints } from '../endpoints.js';
2
- import { type DaemonTickReport } from './tick.js';
3
- export declare const TICK_INTERVAL_MS: number;
4
- export declare function sleepDetected(lastCheckMs: number, nowMs: number, watchdogMs?: number): boolean;
5
- export interface DaemonLoopOptions {
6
- readonly endpoints: Endpoints;
7
- readonly intervalMs?: number;
8
- readonly env?: NodeJS.ProcessEnv;
9
- readonly fetchImpl?: typeof fetch;
10
- readonly home?: string;
11
- readonly onTick?: (report: DaemonTickReport) => void;
12
- readonly onSkipped?: (detail: string) => void;
13
- readonly signal?: AbortSignal;
14
- }
15
- export declare function runDaemonLoop(opts: DaemonLoopOptions): Promise<void>;
@@ -1,84 +0,0 @@
1
- import { recordTick, writeDaemonHeartbeat, } from './state.js';
2
- import { runDaemonTick, summarise } from './tick.js';
3
- export const TICK_INTERVAL_MS = 15 * 60 * 1000;
4
- const WATCHDOG_MS = 30_000;
5
- const SLEEP_FACTOR = 4;
6
- export function sleepDetected(lastCheckMs, nowMs, watchdogMs = WATCHDOG_MS) {
7
- return nowMs - lastCheckMs > watchdogMs * SLEEP_FACTOR;
8
- }
9
- export async function runDaemonLoop(opts) {
10
- const intervalMs = opts.intervalMs ?? TICK_INTERVAL_MS;
11
- const identity = {
12
- mode: 'loop',
13
- startedBy: 'cli',
14
- startedAt: new Date().toISOString(),
15
- pid: process.pid,
16
- intervalMs,
17
- };
18
- await writeDaemonHeartbeat({ version: 1, ...identity, lastTick: null });
19
- let inFlight = null;
20
- let queued = null;
21
- const request = async (reason) => {
22
- if (inFlight) {
23
- queued = reason;
24
- return inFlight;
25
- }
26
- inFlight = (async () => {
27
- try {
28
- const outcome = await runDaemonTick({
29
- endpoints: opts.endpoints,
30
- reason,
31
- ...(opts.env ? { env: opts.env } : {}),
32
- ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
33
- ...(opts.home === undefined ? {} : { home: opts.home }),
34
- });
35
- if (outcome.kind === 'contended') {
36
- opts.onSkipped?.(outcome.detail);
37
- return;
38
- }
39
- await recordTick(summarise(outcome.report), identity);
40
- opts.onTick?.(outcome.report);
41
- }
42
- catch (err) {
43
- process.stderr.write(`harbor daemon: tick threw — ${String(err)}\n`);
44
- }
45
- })();
46
- try {
47
- await inFlight;
48
- }
49
- finally {
50
- inFlight = null;
51
- const next = queued;
52
- queued = null;
53
- if (next !== null)
54
- void request(next);
55
- }
56
- };
57
- await request('startup');
58
- return new Promise((resolve) => {
59
- let lastCheck = Date.now();
60
- let dueAt = Date.now() + intervalMs;
61
- const watchdog = setInterval(() => {
62
- const now = Date.now();
63
- if (sleepDetected(lastCheck, now)) {
64
- lastCheck = now;
65
- dueAt = now + intervalMs;
66
- void request('resume from sleep');
67
- return;
68
- }
69
- lastCheck = now;
70
- if (now >= dueAt) {
71
- dueAt = now + intervalMs;
72
- void request('interval');
73
- }
74
- }, WATCHDOG_MS);
75
- const stop = () => {
76
- clearInterval(watchdog);
77
- resolve();
78
- };
79
- if (opts.signal?.aborted)
80
- stop();
81
- else
82
- opts.signal?.addEventListener('abort', stop, { once: true });
83
- });
84
- }
@@ -1,79 +0,0 @@
1
- export type TickArm = {
2
- readonly kind: 'ok';
3
- readonly detail: string;
4
- } | {
5
- readonly kind: 'skipped';
6
- readonly detail: string;
7
- } | {
8
- readonly kind: 'failed';
9
- readonly detail: string;
10
- };
11
- export interface TickSummary {
12
- readonly at: string;
13
- readonly reason: string;
14
- readonly durationMs: number;
15
- readonly credential: TickArm;
16
- readonly env: TickArm;
17
- readonly repos: number;
18
- readonly reposFailed: number;
19
- readonly report: TickArm;
20
- }
21
- export type DaemonMode = 'loop' | 'once';
22
- export type DaemonRunner = 'cli' | 'app';
23
- export interface DaemonHeartbeat {
24
- readonly version: 1;
25
- readonly mode: DaemonMode;
26
- readonly startedBy: DaemonRunner;
27
- readonly startedAt: string;
28
- readonly pid: number;
29
- readonly intervalMs: number;
30
- readonly lastTick: TickSummary | null;
31
- }
32
- export declare function daemonStateFile(): string;
33
- export declare function tickLeaseFile(): string;
34
- export declare const TICK_LEASE_STALE_MS = 60000;
35
- export type TickLease<T> = {
36
- readonly kind: 'ran';
37
- readonly value: T;
38
- } | {
39
- readonly kind: 'contended';
40
- readonly detail: string;
41
- };
42
- export declare function withTickLease<T>(critical: () => Promise<T>, opts?: {
43
- readonly waitMs?: number;
44
- }): Promise<TickLease<T>>;
45
- export declare function readDaemonHeartbeat(): Promise<DaemonHeartbeat | null>;
46
- export declare function writeDaemonHeartbeat(state: DaemonHeartbeat): Promise<void>;
47
- export interface DaemonIdentity {
48
- readonly mode: DaemonMode;
49
- readonly startedBy: DaemonRunner;
50
- readonly startedAt: string;
51
- readonly pid: number;
52
- readonly intervalMs: number;
53
- }
54
- export declare function recordTick(summary: TickSummary, identity: DaemonIdentity): Promise<void>;
55
- export type DaemonVerdict = {
56
- readonly kind: 'never_run';
57
- readonly detail: string;
58
- readonly fix: string;
59
- } | {
60
- readonly kind: 'idle';
61
- readonly detail: string;
62
- readonly fix: string;
63
- } | {
64
- readonly kind: 'stopped';
65
- readonly detail: string;
66
- readonly fix: string;
67
- } | {
68
- readonly kind: 'failing';
69
- readonly detail: string;
70
- readonly fix: string;
71
- } | {
72
- readonly kind: 'degraded';
73
- readonly detail: string;
74
- readonly fix: string;
75
- } | {
76
- readonly kind: 'ok';
77
- readonly detail: string;
78
- };
79
- export declare function daemonVerdict(state: DaemonHeartbeat | null, now?: Date): DaemonVerdict;
@@ -1,213 +0,0 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { ensureConfigDir, LockTimeoutError, lockPathFor, readLockHolder, updateFileLocked, withFileLock, } from '../lock.js';
4
- import { harborDir } from '../paths.js';
5
- export function daemonStateFile() {
6
- return join(harborDir(), 'daemon.json');
7
- }
8
- export function tickLeaseFile() {
9
- return join(harborDir(), 'daemon.tick');
10
- }
11
- export const TICK_LEASE_STALE_MS = 60_000;
12
- const TICK_LEASE_RENEW_MS = 15_000;
13
- const TICK_LEASE_WAIT_MS = 0;
14
- export async function withTickLease(critical, opts = {}) {
15
- const target = tickLeaseFile();
16
- await ensureConfigDir(harborDir());
17
- try {
18
- return {
19
- kind: 'ran',
20
- value: await withFileLock(target, () => critical(), {
21
- waitMs: opts.waitMs ?? TICK_LEASE_WAIT_MS,
22
- staleMs: TICK_LEASE_STALE_MS,
23
- renewMs: TICK_LEASE_RENEW_MS,
24
- }),
25
- };
26
- }
27
- catch (err) {
28
- if (err instanceof LockTimeoutError && err.lockPath === lockPathFor(target)) {
29
- return { kind: 'contended', detail: await describeHolder(target) };
30
- }
31
- throw err;
32
- }
33
- }
34
- async function describeHolder(target) {
35
- const holder = await readLockHolder(target);
36
- if (holder === null) {
37
- return 'Another Harbor scheduler on this machine is ticking. Skipped this pass.';
38
- }
39
- return (`Another Harbor scheduler on this machine is ticking (pid ${holder.pid}, ` +
40
- `since ${holder.at}). Skipped this pass.`);
41
- }
42
- function parse(raw) {
43
- if (raw.trim() === '')
44
- return null;
45
- let parsed;
46
- try {
47
- parsed = JSON.parse(raw);
48
- }
49
- catch {
50
- return null;
51
- }
52
- if (parsed === null || typeof parsed !== 'object')
53
- return null;
54
- const obj = parsed;
55
- if (typeof obj.startedAt !== 'string' || typeof obj.pid !== 'number')
56
- return null;
57
- return {
58
- version: 1,
59
- mode: obj.mode === 'once' ? 'once' : 'loop',
60
- startedBy: obj.startedBy === 'app' ? 'app' : 'cli',
61
- startedAt: obj.startedAt,
62
- pid: obj.pid,
63
- intervalMs: typeof obj.intervalMs === 'number' ? obj.intervalMs : 0,
64
- lastTick: parseTick(obj.lastTick),
65
- };
66
- }
67
- function parseArm(raw) {
68
- const obj = raw !== null && typeof raw === 'object' ? raw : {};
69
- const detail = typeof obj.detail === 'string' ? obj.detail : '';
70
- return obj.kind === 'ok' || obj.kind === 'failed'
71
- ? { kind: obj.kind, detail }
72
- : { kind: 'skipped', detail };
73
- }
74
- function parseTick(raw) {
75
- if (raw === null || typeof raw !== 'object')
76
- return null;
77
- const obj = raw;
78
- if (typeof obj.at !== 'string')
79
- return null;
80
- return {
81
- at: obj.at,
82
- reason: typeof obj.reason === 'string' ? obj.reason : '',
83
- durationMs: typeof obj.durationMs === 'number' ? obj.durationMs : 0,
84
- credential: parseArm(obj.credential),
85
- env: parseArm(obj.env),
86
- repos: typeof obj.repos === 'number' ? obj.repos : 0,
87
- reposFailed: typeof obj.reposFailed === 'number' ? obj.reposFailed : 0,
88
- report: parseArm(obj.report),
89
- };
90
- }
91
- export async function readDaemonHeartbeat() {
92
- try {
93
- return parse(await readFile(daemonStateFile(), 'utf8'));
94
- }
95
- catch {
96
- return null;
97
- }
98
- }
99
- export async function writeDaemonHeartbeat(state) {
100
- await ensureConfigDir(harborDir());
101
- await updateFileLocked(daemonStateFile(), () => `${JSON.stringify(state, null, 2)}\n`);
102
- }
103
- export async function recordTick(summary, identity) {
104
- await ensureConfigDir(harborDir());
105
- await updateFileLocked(daemonStateFile(), (existing) => {
106
- const current = parse(existing);
107
- const next = {
108
- version: 1,
109
- mode: identity.mode,
110
- startedBy: identity.startedBy,
111
- startedAt: current?.mode === identity.mode
112
- ? (current?.startedAt ?? identity.startedAt)
113
- : identity.startedAt,
114
- pid: identity.pid,
115
- intervalMs: identity.intervalMs,
116
- lastTick: summary,
117
- };
118
- return `${JSON.stringify(next, null, 2)}\n`;
119
- });
120
- }
121
- const LATE_AFTER_INTERVALS = 3;
122
- const MIN_INTERVAL_MS = 60_000;
123
- function alive(pid) {
124
- try {
125
- process.kill(pid, 0);
126
- return true;
127
- }
128
- catch (err) {
129
- return err.code === 'EPERM';
130
- }
131
- }
132
- export function daemonVerdict(state, now = new Date()) {
133
- if (state === null || state.lastTick === null) {
134
- return {
135
- kind: 'never_run',
136
- detail: 'Never run on this machine.',
137
- fix: 'Run `harbor daemon --install` to keep context current, or `harbor daemon --once` for a single pass.',
138
- };
139
- }
140
- const tick = state.lastTick;
141
- const age = now.getTime() - new Date(tick.at).getTime();
142
- const interval = Math.max(state.intervalMs, MIN_INTERVAL_MS);
143
- const ago = describeAge(age);
144
- if (age > interval * LATE_AFTER_INTERVALS) {
145
- return {
146
- kind: 'stopped',
147
- detail: `Last tick ${ago} — more than ${LATE_AFTER_INTERVALS} intervals ago.`,
148
- fix: restart(state.startedBy),
149
- };
150
- }
151
- if (state.mode === 'loop' && !alive(state.pid)) {
152
- return {
153
- kind: 'stopped',
154
- detail: `The process that ticked ${ago} (pid ${state.pid}) is gone.`,
155
- fix: restart(state.startedBy),
156
- };
157
- }
158
- if (tick.credential.kind === 'failed') {
159
- return {
160
- kind: 'failing',
161
- detail: `Ticking, but the credential is not: ${tick.credential.detail}`,
162
- fix: 'Run `harbor login` — until then nothing this machine syncs will be accepted.',
163
- };
164
- }
165
- if (tick.env.kind === 'failed') {
166
- return {
167
- kind: 'failing',
168
- detail: `Ticking ${ago}, but your own skills and documents did not sync: ${tick.env.detail}`,
169
- fix: 'Run `harbor doctor --json` for the transport error, then `harbor daemon --once`.',
170
- };
171
- }
172
- if (tick.credential.kind === 'skipped' && tick.env.kind === 'skipped' && tick.repos === 0) {
173
- return {
174
- kind: 'idle',
175
- detail: `Ticking ${ago}, with nothing to do: ${tick.env.detail}`,
176
- fix: 'Run `harbor login`, then `harbor daemon --once`.',
177
- };
178
- }
179
- if (tick.report.kind === 'failed') {
180
- return {
181
- kind: 'degraded',
182
- detail: `Files are current as of ${ago}, but this machine could not report them: ${tick.report.detail}`,
183
- fix: 'The distribution map will show this machine as stale until it can.',
184
- };
185
- }
186
- if (tick.reposFailed > 0) {
187
- return {
188
- kind: 'degraded',
189
- detail: `Ticked ${ago} — ${tick.reposFailed} of ${tick.repos} repositories failed.`,
190
- fix: 'Run `harbor daemon --once` to see which, or remove folders that have moved.',
191
- };
192
- }
193
- return {
194
- kind: 'ok',
195
- detail: `Ticked ${ago} — ${tick.repos} ${tick.repos === 1 ? 'repository' : 'repositories'}, reported.`,
196
- };
197
- }
198
- function restart(startedBy) {
199
- return startedBy === 'app'
200
- ? 'Open Harbor from Applications, or run `harbor daemon --install` so syncing no longer depends on it.'
201
- : 'Start it again with `harbor daemon --install`, or run `harbor daemon --once` now.';
202
- }
203
- function describeAge(ms) {
204
- if (ms < 90_000)
205
- return 'less than a minute ago';
206
- const minutes = Math.round(ms / 60_000);
207
- if (minutes < 90)
208
- return `${minutes} minutes ago`;
209
- const hours = Math.round(minutes / 60);
210
- if (hours < 48)
211
- return `${hours} hours ago`;
212
- return `${Math.round(hours / 24)} days ago`;
213
- }
@@ -1,37 +0,0 @@
1
- import type { Endpoints } from '../endpoints.js';
2
- import { type TickArm, type TickSummary } from './state.js';
3
- export declare const REFRESH_WHEN_LESS_THAN_MS: number;
4
- export interface DaemonTickOptions {
5
- readonly endpoints: Endpoints;
6
- readonly reason: string;
7
- readonly env?: NodeJS.ProcessEnv;
8
- readonly fetchImpl?: typeof fetch;
9
- readonly home?: string;
10
- readonly now?: () => Date;
11
- readonly lockWaitMs?: number;
12
- }
13
- export interface RepoTick {
14
- readonly path: string;
15
- readonly originUrl: string;
16
- readonly outcome: TickArm;
17
- }
18
- export interface DaemonTickReport {
19
- readonly startedAt: string;
20
- readonly finishedAt: string;
21
- readonly reason: string;
22
- readonly credential: TickArm;
23
- readonly env: TickArm;
24
- readonly repos: readonly RepoTick[];
25
- readonly report: TickArm;
26
- readonly bundleHash?: string;
27
- readonly release?: TickArm;
28
- }
29
- export type DaemonTickOutcome = {
30
- readonly kind: 'ran';
31
- readonly report: DaemonTickReport;
32
- } | {
33
- readonly kind: 'contended';
34
- readonly detail: string;
35
- };
36
- export declare function runDaemonTick(opts: DaemonTickOptions): Promise<DaemonTickOutcome>;
37
- export declare function summarise(report: DaemonTickReport): TickSummary;