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
package/dist/index.js DELETED
@@ -1,282 +0,0 @@
1
- #!/usr/bin/env node
2
- import { realpathSync } from 'node:fs';
3
- import { fileURLToPath } from 'node:url';
4
- import { runBind, renderBind, NoWorkspaceChosenError, NotARepoError, NotSignedInError, } from './commands/bind.js';
5
- import { runWorkspaces, renderWorkspaces } from './commands/workspaces.js';
6
- import { exitCodeForDaemon, renderDaemon, runDaemon } from './commands/daemon.js';
7
- import { contextToJson, renderContext, runContext } from './commands/context.js';
8
- import { runDoctor } from './commands/doctor.js';
9
- import { renderInit, runInit, runLogout } from './commands/init.js';
10
- import { runStatus } from './commands/status.js';
11
- import { exitCodeForSync, renderSync, runSync } from './commands/sync.js';
12
- import { renderToggle, runOff, runOn } from './commands/toggle.js';
13
- import { renderUnbind, runUnbind } from './commands/unbind.js';
14
- import { resolveEndpoints } from './endpoints.js';
15
- import { checksToJson, exitCodeFor, renderChecks } from './render.js';
16
- export const VERBS = [
17
- 'init',
18
- 'login',
19
- 'logout',
20
- 'bind',
21
- 'unbind',
22
- 'sync',
23
- 'daemon',
24
- 'status',
25
- 'workspaces',
26
- 'doctor',
27
- 'context',
28
- 'on',
29
- 'off',
30
- 'help',
31
- 'version',
32
- ];
33
- const VALUE_FLAGS = new Set(['workspace', 'project', 'cwd', 'days']);
34
- export function parseArgs(argv) {
35
- const positional = [];
36
- const flags = {};
37
- for (let i = 0; i < argv.length; i += 1) {
38
- const arg = argv[i];
39
- if (arg === '--')
40
- continue;
41
- if (arg.startsWith('--')) {
42
- const [name, ...rest] = arg.slice(2).split('=');
43
- if (rest.length > 0) {
44
- flags[name] = rest.join('=');
45
- continue;
46
- }
47
- const next = argv[i + 1];
48
- if (VALUE_FLAGS.has(name) && next !== undefined && !next.startsWith('-')) {
49
- flags[name] = next;
50
- i += 1;
51
- continue;
52
- }
53
- flags[name] = true;
54
- continue;
55
- }
56
- if (arg.startsWith('-') && arg.length > 1) {
57
- flags[arg.slice(1)] = true;
58
- continue;
59
- }
60
- positional.push(arg);
61
- }
62
- const first = positional[0];
63
- if (first === undefined) {
64
- if (flags.version === true || flags.v === true)
65
- return { verb: 'version', flags };
66
- }
67
- if (flags.help === true || flags.h === true)
68
- return { verb: 'help', flags };
69
- const verb = VERBS.includes(first ?? '')
70
- ? first
71
- : first === undefined
72
- ? 'help'
73
- : 'help';
74
- if (first !== undefined && !VERBS.includes(first)) {
75
- flags.unknownVerb = first;
76
- }
77
- if (verb === 'bind' && positional[1] && flags.project === undefined) {
78
- flags.project = positional[1];
79
- }
80
- return { verb, flags };
81
- }
82
- const USAGE = `harbor — your team's knowledge, in your terminal
83
-
84
- harbor init sign in, install the hooks, bind this repo, schedule the daemon
85
- (configures the agents it finds;
86
- --codex/--no-codex, --cursor/--no-cursor, --no-daemon)
87
- harbor doctor is it actually working? (--offline, --json)
88
- harbor status this repo, from local files only
89
- harbor context what this machine pays for on every turn, and who put it there
90
- (--verbose lists every skill, --json)
91
- harbor bind [project] tie this repo to a project
92
- harbor workspaces which workspaces you can reach, and the one this machine uses
93
- harbor sync put this repo's team skills, memories and documents on disk
94
- (no desktop app needed; HARBOR_TOKEN + HARBOR_WORKSPACE_ID for CI)
95
- harbor daemon keep every bound repo AND your own env current, on a timer
96
- (--install to schedule it, --once for one pass, --status)
97
- harbor unbind take those files back off this machine
98
- (local only — the repo stays bound; files you edited stay put)
99
- harbor off pause this repo (--capture keeps recall, --global)
100
- harbor on resume (--global)
101
- harbor login re-authenticate (--no-browser to paste a code,
102
- --workspace <id> to switch workspace, no browser needed)
103
- harbor logout forget this machine's credential
104
-
105
- Points at the hosted harbor.
106
- `;
107
- function output(json, jsonValue, text) {
108
- process.stdout.write(json ? `${JSON.stringify(jsonValue, null, 2)}\n` : `${text}\n`);
109
- }
110
- export async function main(argv) {
111
- const { verb, flags } = parseArgs(argv);
112
- const json = flags.json === true;
113
- const cwd = typeof flags.cwd === 'string' ? flags.cwd : process.cwd();
114
- const endpoints = resolveEndpoints();
115
- switch (verb) {
116
- case 'version': {
117
- const { readFile } = await import('node:fs/promises');
118
- const { fileURLToPath } = await import('node:url');
119
- const { dirname, join } = await import('node:path');
120
- const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
121
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
122
- output(json, { version: pkg.version }, pkg.version);
123
- return 0;
124
- }
125
- case 'help': {
126
- process.stdout.write(USAGE);
127
- if (typeof flags.unknownVerb === 'string') {
128
- process.stderr.write(`\nUnknown command: ${flags.unknownVerb}\n`);
129
- return 2;
130
- }
131
- return 0;
132
- }
133
- case 'init': {
134
- const result = await runInit({
135
- cwd,
136
- endpoints,
137
- noBrowser: flags['no-browser'] === true,
138
- workspaceId: typeof flags.workspace === 'string' ? flags.workspace : undefined,
139
- bind: flags['no-bind'] === true ? false : undefined,
140
- codex: flags['no-codex'] === true ? false : flags.codex === true ? true : undefined,
141
- cursor: flags['no-cursor'] === true ? false : flags.cursor === true ? true : undefined,
142
- daemon: flags['no-daemon'] === true ? false : undefined,
143
- });
144
- const { envShadowWarning: initShadow } = await import('./auth/store.js');
145
- const initWarning = initShadow();
146
- if (initWarning)
147
- process.stderr.write(`${initWarning}\n`);
148
- output(json, result, renderInit(result));
149
- return 0;
150
- }
151
- case 'login': {
152
- const { login } = await import('./auth/login.js');
153
- const { envShadowWarning } = await import('./auth/store.js');
154
- const { auth, reused } = await login(endpoints, {
155
- noBrowser: flags['no-browser'] === true,
156
- workspaceId: typeof flags.workspace === 'string' ? flags.workspace : undefined,
157
- });
158
- const shadow = envShadowWarning();
159
- if (shadow)
160
- process.stderr.write(`${shadow}\n`);
161
- output(json, { workspace: auth.workspaceName, reused }, reused
162
- ? `Already signed in to "${auth.workspaceName}".`
163
- : `Signed in to "${auth.workspaceName}".`);
164
- return 0;
165
- }
166
- case 'logout': {
167
- await runLogout();
168
- output(json, { ok: true }, 'Credential removed. Hooks and config left in place.');
169
- return 0;
170
- }
171
- case 'bind': {
172
- const result = await runBind({
173
- cwd,
174
- endpoints,
175
- project: typeof flags.project === 'string' ? flags.project : undefined,
176
- });
177
- output(json, result, renderBind(result));
178
- return 0;
179
- }
180
- case 'unbind': {
181
- const result = await runUnbind({ cwd });
182
- output(json, result, renderUnbind(result));
183
- return 0;
184
- }
185
- case 'sync': {
186
- const result = await runSync({ cwd, endpoints });
187
- output(json, result, renderSync(result));
188
- return exitCodeForSync(result);
189
- }
190
- case 'daemon': {
191
- const result = await runDaemon({
192
- endpoints,
193
- once: flags.once === true,
194
- install: flags.install === true,
195
- uninstall: flags.uninstall === true,
196
- status: flags.status === true,
197
- });
198
- if (json && result.mode !== 'ran')
199
- output(true, result, '');
200
- else
201
- process.stdout.write(`${renderDaemon(result)}\n`);
202
- return exitCodeForDaemon(result);
203
- }
204
- case 'status': {
205
- const checks = await runStatus({ cwd });
206
- output(json, checksToJson(checks), renderChecks(checks));
207
- return exitCodeFor(checks);
208
- }
209
- case 'workspaces': {
210
- const result = await runWorkspaces({ endpoints });
211
- output(json, result, renderWorkspaces(result));
212
- return 0;
213
- }
214
- case 'doctor': {
215
- const report = await runDoctor({
216
- cwd,
217
- endpoints,
218
- offline: flags.offline === true,
219
- windowDays: typeof flags.days === 'string' ? Number.parseInt(flags.days, 10) : undefined,
220
- });
221
- output(json, { checks: checksToJson(report.checks), metrics: report.metrics }, renderChecks(report.checks));
222
- return exitCodeFor(report.checks);
223
- }
224
- case 'context': {
225
- const result = await runContext({ cwd, verbose: flags.verbose === true });
226
- output(json, contextToJson(result), renderContext(result));
227
- return 0;
228
- }
229
- case 'off': {
230
- const result = await runOff({
231
- cwd,
232
- captureOnly: flags.capture === true,
233
- receiptOnly: flags.receipt === true,
234
- global: flags.global === true,
235
- });
236
- output(json, result, renderToggle(result));
237
- return 0;
238
- }
239
- case 'on': {
240
- const result = await runOn({
241
- cwd,
242
- receiptOnly: flags.receipt === true,
243
- global: flags.global === true,
244
- });
245
- output(json, result, renderToggle(result));
246
- return 0;
247
- }
248
- }
249
- }
250
- function reportError(err, debug) {
251
- if (debug) {
252
- process.stderr.write(`${err.stack ?? String(err)}\n`);
253
- return;
254
- }
255
- if (err instanceof NotSignedInError ||
256
- err instanceof NoWorkspaceChosenError ||
257
- err instanceof NotARepoError) {
258
- process.stderr.write(`${err.message}\n`);
259
- return;
260
- }
261
- process.stderr.write(`${err.message ?? String(err)}\n`);
262
- }
263
- export function isProgramEntry(argv1, moduleUrl, resolve = realpathSync) {
264
- if (argv1 === undefined)
265
- return false;
266
- try {
267
- return resolve(argv1) === resolve(fileURLToPath(moduleUrl));
268
- }
269
- catch {
270
- return false;
271
- }
272
- }
273
- if (isProgramEntry(process.argv[1], import.meta.url)) {
274
- main(process.argv.slice(2))
275
- .then((code) => {
276
- process.exitCode = code;
277
- })
278
- .catch((err) => {
279
- reportError(err, process.argv.includes('--debug'));
280
- process.exitCode = 1;
281
- });
282
- }
@@ -1,11 +0,0 @@
1
- import { type HookPaths } from './providers/claude.js';
2
- export declare const HOOK_FILES: readonly string[];
3
- export declare function packagedHooksDir(): string;
4
- export declare function hookPathsIn(dir: string): HookPaths;
5
- export interface HookInstallResult {
6
- readonly dir: string;
7
- readonly files: readonly string[];
8
- readonly paths: HookPaths;
9
- }
10
- export declare function installHookScripts(sourceDir?: string): Promise<HookInstallResult>;
11
- export declare function hookScriptsAreCurrent(sourceDir?: string): Promise<boolean>;
@@ -1,64 +0,0 @@
1
- import { chmod, copyFile, mkdir, readFile, readdir, rename } from 'node:fs/promises';
2
- import { dirname, join } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { hooksBinDir } from './paths.js';
5
- import { HOOK_CAPTURE_BASENAME, HOOK_COMPACT_BASENAME, HOOK_CONTEXT_BASENAME, HOOK_REPORT_BASENAME, HOOK_TOOLCONTEXT_BASENAME, } from './providers/claude.js';
6
- export const HOOK_FILES = [
7
- '_shared.mjs',
8
- '_recall.mjs',
9
- '_receipt.mjs',
10
- '_upgrade.mjs',
11
- HOOK_CONTEXT_BASENAME,
12
- HOOK_REPORT_BASENAME,
13
- HOOK_CAPTURE_BASENAME,
14
- HOOK_TOOLCONTEXT_BASENAME,
15
- HOOK_COMPACT_BASENAME,
16
- ];
17
- export function packagedHooksDir() {
18
- return join(dirname(fileURLToPath(import.meta.url)), '..', 'hooks');
19
- }
20
- export function hookPathsIn(dir) {
21
- return {
22
- contextHookPath: join(dir, HOOK_CONTEXT_BASENAME),
23
- reportHookPath: join(dir, HOOK_REPORT_BASENAME),
24
- captureHookPath: join(dir, HOOK_CAPTURE_BASENAME),
25
- toolContextHookPath: join(dir, HOOK_TOOLCONTEXT_BASENAME),
26
- compactHookPath: join(dir, HOOK_COMPACT_BASENAME),
27
- };
28
- }
29
- export async function installHookScripts(sourceDir = packagedHooksDir()) {
30
- const dir = hooksBinDir();
31
- await mkdir(dir, { recursive: true, mode: 0o700 });
32
- const available = new Set(await readdir(sourceDir));
33
- const missing = HOOK_FILES.filter((f) => !available.has(f));
34
- if (missing.length > 0) {
35
- throw new Error(`Hook scripts missing from ${sourceDir}: ${missing.join(', ')}. ` +
36
- `This is a packaging fault, not a configuration one.`);
37
- }
38
- for (const file of HOOK_FILES) {
39
- const target = join(dir, file);
40
- const tmp = `${target}.${process.pid}.tmp`;
41
- await copyFile(join(sourceDir, file), tmp);
42
- const executable = !file.startsWith('_');
43
- await chmod(tmp, executable ? 0o700 : 0o600);
44
- await rename(tmp, target);
45
- }
46
- return { dir, files: HOOK_FILES, paths: hookPathsIn(dir) };
47
- }
48
- export async function hookScriptsAreCurrent(sourceDir = packagedHooksDir()) {
49
- const dir = hooksBinDir();
50
- for (const file of HOOK_FILES) {
51
- try {
52
- const [installed, shipped] = await Promise.all([
53
- readFile(join(dir, file), 'utf8'),
54
- readFile(join(sourceDir, file), 'utf8'),
55
- ]);
56
- if (installed !== shipped)
57
- return false;
58
- }
59
- catch {
60
- return false;
61
- }
62
- }
63
- return true;
64
- }
package/dist/lock.d.ts DELETED
@@ -1,31 +0,0 @@
1
- export declare const LOCK_STALE_MS = 10000;
2
- export declare const LOCK_WAIT_MS = 15000;
3
- export declare class LockTimeoutError extends Error {
4
- readonly lockPath: string;
5
- constructor(lockPath: string);
6
- }
7
- export declare function lockPathFor(targetPath: string): string;
8
- export declare function readLockHolder(targetPath: string): Promise<{
9
- readonly pid: number;
10
- readonly host: string;
11
- readonly at: string;
12
- } | null>;
13
- export interface LockHandle {
14
- stillHeld(): Promise<boolean>;
15
- }
16
- export interface FileLockOptions {
17
- readonly waitMs?: number;
18
- readonly staleMs?: number;
19
- readonly renewMs?: number;
20
- }
21
- export declare function withFileLock<T>(targetPath: string, critical: (lock: LockHandle) => Promise<T>, opts?: FileLockOptions): Promise<T>;
22
- export declare function writeFileAtomic(path: string, contents: string, options?: {
23
- readonly mode?: number | 'preserve';
24
- }): Promise<void>;
25
- export declare function ensureConfigDir(dir: string): Promise<void>;
26
- export declare function readIfExists(path: string): Promise<string>;
27
- export declare class LockLostError extends Error {
28
- readonly path: string;
29
- constructor(path: string);
30
- }
31
- export declare function updateFileLocked(path: string, transform: (existing: string) => string | Promise<string>): Promise<void>;
package/dist/lock.js DELETED
@@ -1,192 +0,0 @@
1
- import { constants as fsConstants } from 'node:fs';
2
- import { chmod, mkdir, open, readFile, realpath, rename, rm, stat, utimes, writeFile, } from 'node:fs/promises';
3
- import { hostname } from 'node:os';
4
- import { createHash, randomUUID } from 'node:crypto';
5
- import { dirname, join } from 'node:path';
6
- import { locksDir } from './paths.js';
7
- export const LOCK_STALE_MS = 10_000;
8
- export const LOCK_WAIT_MS = 15_000;
9
- const POLL_MIN_MS = 5;
10
- const POLL_MAX_MS = 100;
11
- export class LockTimeoutError extends Error {
12
- lockPath;
13
- constructor(lockPath) {
14
- super(`Timed out waiting for ${lockPath}. Another harbor process ` +
15
- `is holding it; retry in a moment.`);
16
- this.lockPath = lockPath;
17
- this.name = 'LockTimeoutError';
18
- }
19
- }
20
- export function lockPathFor(targetPath) {
21
- const digest = createHash('sha256').update(targetPath).digest('hex').slice(0, 16);
22
- return join(locksDir(), `${digest}.lock`);
23
- }
24
- async function tryAcquire(lockPath, target) {
25
- const token = randomUUID();
26
- try {
27
- const handle = await open(lockPath, fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_WRONLY, 0o600);
28
- const owner = {
29
- pid: process.pid,
30
- host: hostname(),
31
- at: new Date().toISOString(),
32
- target,
33
- token,
34
- };
35
- try {
36
- await handle.writeFile(JSON.stringify(owner), 'utf8');
37
- }
38
- finally {
39
- await handle.close();
40
- }
41
- return token;
42
- }
43
- catch (err) {
44
- if (err.code === 'EEXIST')
45
- return null;
46
- throw err;
47
- }
48
- }
49
- export async function readLockHolder(targetPath) {
50
- try {
51
- const raw = JSON.parse(await readFile(lockPathFor(targetPath), 'utf8'));
52
- if (typeof raw.pid !== 'number' || typeof raw.at !== 'string')
53
- return null;
54
- return { pid: raw.pid, host: typeof raw.host === 'string' ? raw.host : '', at: raw.at };
55
- }
56
- catch {
57
- return null;
58
- }
59
- }
60
- async function stillOurs(lockPath, token) {
61
- try {
62
- const owner = JSON.parse(await readFile(lockPath, 'utf8'));
63
- return owner.token === token;
64
- }
65
- catch {
66
- return false;
67
- }
68
- }
69
- async function releaseIfOurs(lockPath, token) {
70
- if (!(await stillOurs(lockPath, token)))
71
- return;
72
- await rm(lockPath, { force: true });
73
- }
74
- async function breakIfStale(lockPath, staleMs) {
75
- try {
76
- const info = await stat(lockPath);
77
- if (Date.now() - info.mtimeMs < staleMs)
78
- return false;
79
- await rm(lockPath, { force: true });
80
- return true;
81
- }
82
- catch {
83
- return false;
84
- }
85
- }
86
- async function touchIfOurs(lockPath, token) {
87
- try {
88
- if (!(await stillOurs(lockPath, token)))
89
- return;
90
- const now = new Date();
91
- await utimes(lockPath, now, now);
92
- }
93
- catch {
94
- }
95
- }
96
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
97
- export async function withFileLock(targetPath, critical, opts = {}) {
98
- const lockPath = lockPathFor(targetPath);
99
- const staleMs = opts.staleMs ?? LOCK_STALE_MS;
100
- await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
101
- const deadline = Date.now() + (opts.waitMs ?? LOCK_WAIT_MS);
102
- let delay = POLL_MIN_MS;
103
- let token = null;
104
- for (;;) {
105
- token = await tryAcquire(lockPath, targetPath);
106
- if (token !== null)
107
- break;
108
- if (await breakIfStale(lockPath, staleMs))
109
- continue;
110
- if (Date.now() >= deadline)
111
- throw new LockTimeoutError(lockPath);
112
- await sleep(delay);
113
- delay = Math.min(delay * 2, POLL_MAX_MS);
114
- }
115
- const held = token;
116
- const renew = opts.renewMs === undefined
117
- ? null
118
- : setInterval(() => void touchIfOurs(lockPath, held), opts.renewMs);
119
- renew?.unref?.();
120
- try {
121
- return await critical({ stillHeld: () => stillOurs(lockPath, held) });
122
- }
123
- finally {
124
- if (renew)
125
- clearInterval(renew);
126
- await releaseIfOurs(lockPath, held);
127
- }
128
- }
129
- export async function writeFileAtomic(path, contents, options = {}) {
130
- const requested = options.mode ?? 0o600;
131
- const mode = requested === 'preserve'
132
- ? await stat(path)
133
- .then((s) => s.mode & 0o777)
134
- .catch(() => null)
135
- : requested;
136
- const target = await realpath(path).catch(() => path);
137
- const tmp = `${target}.${process.pid}.tmp`;
138
- await mkdir(dirname(target), { recursive: true, mode: 0o700 });
139
- await writeFile(tmp, contents, {
140
- encoding: 'utf8',
141
- ...(mode === null ? {} : { mode }),
142
- });
143
- await rename(tmp, target);
144
- }
145
- export async function ensureConfigDir(dir) {
146
- await mkdir(dir, { recursive: true, mode: 0o700 });
147
- if (process.platform === 'win32')
148
- return;
149
- try {
150
- await chmod(dir, 0o700);
151
- }
152
- catch {
153
- }
154
- }
155
- export async function readIfExists(path) {
156
- try {
157
- return await readFile(path, 'utf8');
158
- }
159
- catch (err) {
160
- if (err.code === 'ENOENT')
161
- return '';
162
- throw err;
163
- }
164
- }
165
- export class LockLostError extends Error {
166
- path;
167
- constructor(path) {
168
- super(`Could not complete an exclusive update of ${path}: the lock was broken ` +
169
- `mid-write on every attempt. Another harbor process is ` +
170
- `contending for it, or this machine was suspended mid-update; retry.`);
171
- this.path = path;
172
- this.name = 'LockLostError';
173
- }
174
- }
175
- const UPDATE_ATTEMPTS = 3;
176
- export async function updateFileLocked(path, transform) {
177
- for (let attempt = 0; attempt < UPDATE_ATTEMPTS; attempt += 1) {
178
- const lostTheLock = await withFileLock(path, async (lock) => {
179
- const existing = await readIfExists(path);
180
- const next = await transform(existing);
181
- if (next === existing)
182
- return false;
183
- if (!(await lock.stillHeld()))
184
- return true;
185
- await writeFileAtomic(path, next);
186
- return false;
187
- });
188
- if (!lostTheLock)
189
- return;
190
- }
191
- throw new LockLostError(path);
192
- }
package/dist/machine.d.ts DELETED
@@ -1,13 +0,0 @@
1
- export interface MachineIdentity {
2
- readonly id: string;
3
- readonly createdAt: string;
4
- }
5
- export interface MachineFacts extends MachineIdentity {
6
- readonly hostname: string;
7
- readonly os: string;
8
- readonly osVersion: string;
9
- readonly cliVersion: string;
10
- }
11
- export declare function readMachineIdentity(): Promise<MachineIdentity>;
12
- export declare function readCliVersion(): Promise<string>;
13
- export declare function readMachineFacts(): Promise<MachineFacts>;
package/dist/machine.js DELETED
@@ -1,53 +0,0 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
3
- import { hostname, platform, release } from 'node:os';
4
- import { dirname, join } from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import { ensureConfigDir, updateFileLocked } from './lock.js';
7
- import { machineFile } from './paths.js';
8
- const isIdentity = (raw) => {
9
- if (raw === null || typeof raw !== 'object')
10
- return false;
11
- const obj = raw;
12
- return typeof obj['id'] === 'string' && obj['id'].length > 0;
13
- };
14
- export async function readMachineIdentity() {
15
- let identity;
16
- await ensureConfigDir(dirname(machineFile()));
17
- await updateFileLocked(machineFile(), (existing) => {
18
- if (existing.trim() !== '') {
19
- try {
20
- const parsed = JSON.parse(existing);
21
- if (isIdentity(parsed)) {
22
- identity = parsed;
23
- return existing;
24
- }
25
- }
26
- catch {
27
- }
28
- }
29
- identity = { id: randomUUID(), createdAt: new Date().toISOString() };
30
- return `${JSON.stringify(identity, null, 2)}\n`;
31
- });
32
- return identity;
33
- }
34
- export async function readCliVersion() {
35
- try {
36
- const pkgPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json');
37
- const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
38
- return typeof pkg.version === 'string' ? pkg.version : 'unknown';
39
- }
40
- catch {
41
- return 'unknown';
42
- }
43
- }
44
- export async function readMachineFacts() {
45
- const identity = await readMachineIdentity();
46
- return {
47
- ...identity,
48
- hostname: hostname(),
49
- os: platform(),
50
- osVersion: release(),
51
- cliVersion: await readCliVersion(),
52
- };
53
- }
@@ -1,31 +0,0 @@
1
- export interface MemoryPayload {
2
- readonly id: string;
3
- readonly name: string;
4
- readonly content: string;
5
- }
6
- export type MemoryResult = {
7
- readonly id: string;
8
- readonly file: string;
9
- readonly status: 'synced';
10
- } | {
11
- readonly file: string;
12
- readonly status: 'removed';
13
- };
14
- export interface MemorySyncSummary {
15
- readonly dir: string;
16
- readonly results: readonly MemoryResult[];
17
- }
18
- export declare function encodeProjectPath(projectPath: string): string;
19
- export declare function syncMemoryFilesToHome(opts: {
20
- readonly projectPath: string;
21
- readonly memories: readonly MemoryPayload[];
22
- readonly home?: string;
23
- }): Promise<MemorySyncSummary>;
24
- export declare function removeProjectMemoryFiles(opts: {
25
- readonly projectPath: string;
26
- readonly home?: string;
27
- }): Promise<{
28
- readonly dir: string;
29
- readonly results: readonly MemoryResult[];
30
- readonly status: 'cleaned' | 'no_dir';
31
- }>;