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,36 +0,0 @@
1
- import type { Endpoints } from '../endpoints.js';
2
- import { type InstallOutcome } from '../daemon/install.js';
3
- import { type AgentId } from '../providers/detect.js';
4
- export interface InitOptions {
5
- readonly cwd: string;
6
- readonly endpoints: Endpoints;
7
- readonly bind?: boolean;
8
- readonly noBrowser?: boolean;
9
- readonly workspaceId?: string;
10
- readonly json?: boolean;
11
- readonly daemon?: boolean;
12
- readonly codex?: boolean;
13
- readonly cursor?: boolean;
14
- }
15
- export interface AgentInstall {
16
- readonly agent: AgentId;
17
- readonly provider: string;
18
- readonly connectionId: string;
19
- readonly configPath: string;
20
- readonly verifiedHooks: number | null;
21
- readonly trustSkipped: string | null;
22
- }
23
- export interface InitResult {
24
- readonly workspaceName: string;
25
- readonly settingsPath: string;
26
- readonly hooksDir: string;
27
- readonly origins: readonly string[];
28
- readonly boundRepo: string | null;
29
- readonly migratedLegacyState: boolean;
30
- readonly installs: readonly AgentInstall[];
31
- readonly daemon: InstallOutcome | null;
32
- readonly fellBackToClaude: boolean;
33
- }
34
- export declare function runInit(opts: InitOptions): Promise<InitResult>;
35
- export declare function renderInit(result: InitResult): string;
36
- export declare function runLogout(): Promise<void>;
@@ -1,192 +0,0 @@
1
- import { createRepoBinding, listRepoBindings, mintMcpToken, } from '../api.js';
2
- import { login } from '../auth/login.js';
3
- import { readAuth, writeAuth } from '../auth/store.js';
4
- import { harborDir } from '../paths.js';
5
- import { daemonProgram, installDaemon, installPlan, } from '../daemon/install.js';
6
- import { installHookScripts } from '../install-hooks.js';
7
- import { gitRoot, repoOrigin } from '../origin.js';
8
- import * as claude from '../providers/claude.js';
9
- import * as codex from '../providers/codex.js';
10
- import * as cursor from '../providers/cursor.js';
11
- import { stampAndVerify } from '../providers/codex-trust.js';
12
- import { detectAgents, selectAgents, MINT_PROVIDER, } from '../providers/detect.js';
13
- import { migrateLegacyState, updateSyncedConfig } from '../state.js';
14
- export async function runInit(opts) {
15
- const { endpoints } = opts;
16
- const migrated = (await migrateLegacyState()) === 'migrated';
17
- const { auth } = await login(endpoints, {
18
- noBrowser: opts.noBrowser,
19
- workspaceId: opts.workspaceId,
20
- });
21
- const selection = selectAgents(await detectAgents(), {
22
- codex: opts.codex,
23
- cursor: opts.cursor,
24
- });
25
- const minted = new Map();
26
- for (const agent of selection.agents) {
27
- minted.set(agent, await mintMcpToken(endpoints.apiUrl, auth.runnerToken, auth.workspaceId, MINT_PROVIDER[agent]));
28
- }
29
- const primary = selection.agents.includes('claude') ? 'claude' : selection.agents[0];
30
- await writeAuth({
31
- ...auth,
32
- mcpMintedAt: new Date().toISOString(),
33
- mcpConnectionId: minted.get(primary)?.connectionId,
34
- });
35
- const origin = repoOrigin(opts.cwd);
36
- const repoRoot = gitRoot(opts.cwd);
37
- let bound = null;
38
- const shouldBind = opts.bind ?? origin !== '';
39
- if (shouldBind && origin !== '') {
40
- const existing = await listRepoBindings(endpoints.apiUrl, auth.runnerToken, auth.workspaceId);
41
- bound =
42
- existing.find((b) => b.originUrl === origin) ??
43
- (await createRepoBinding(endpoints.apiUrl, auth.runnerToken, auth.workspaceId, {
44
- originUrl: origin,
45
- }));
46
- }
47
- const bindings = await listRepoBindings(endpoints.apiUrl, auth.runnerToken, auth.workspaceId);
48
- const config = await updateSyncedConfig((current) => ({
49
- ...current,
50
- apiBase: endpoints.apiUrl,
51
- origins: bindings.map((b) => b.originUrl),
52
- }));
53
- const install = await installHookScripts();
54
- const installs = [];
55
- for (const agent of selection.agents) {
56
- const token = minted.get(agent);
57
- const provider = MINT_PROVIDER[agent];
58
- if (agent === 'claude') {
59
- const configPath = await claude.installMcpServer({
60
- mcpUrl: endpoints.mcpUrl,
61
- token: token.token,
62
- repoPath: repoRoot ?? undefined,
63
- originUrl: origin !== '' ? origin : undefined,
64
- });
65
- await claude.installHooks(install.paths);
66
- installs.push({
67
- agent,
68
- provider,
69
- connectionId: token.connectionId,
70
- configPath,
71
- verifiedHooks: null,
72
- trustSkipped: null,
73
- });
74
- continue;
75
- }
76
- if (agent === 'cursor') {
77
- const cursorConfigPath = await cursor.installMcpServer({
78
- mcpUrl: endpoints.mcpUrl,
79
- token: token.token,
80
- });
81
- await cursor.installHooks(install.paths);
82
- installs.push({
83
- agent,
84
- provider,
85
- connectionId: token.connectionId,
86
- configPath: cursorConfigPath,
87
- verifiedHooks: null,
88
- trustSkipped: null,
89
- });
90
- continue;
91
- }
92
- const configPath = await codex.installMcpServer({
93
- mcpUrl: endpoints.mcpUrl,
94
- token: token.token,
95
- });
96
- await codex.installHooks(install.paths);
97
- const trust = await stampAndVerify({ probeCwd: opts.cwd });
98
- installs.push({
99
- agent,
100
- provider,
101
- connectionId: token.connectionId,
102
- configPath,
103
- verifiedHooks: trust.skippedReason === null ? trust.verified : null,
104
- trustSkipped: trust.skippedReason,
105
- });
106
- }
107
- const claudeInstall = installs.find((i) => i.agent === 'claude');
108
- const daemon = opts.daemon === false ? null : await scheduleDaemon();
109
- return {
110
- workspaceName: auth.workspaceName,
111
- settingsPath: claudeInstall?.configPath ?? installs[0]?.configPath ?? '',
112
- hooksDir: install.dir,
113
- origins: config.origins,
114
- boundRepo: bound?.originUrl ?? null,
115
- migratedLegacyState: migrated,
116
- installs,
117
- fellBackToClaude: selection.fellBack,
118
- daemon,
119
- };
120
- }
121
- async function scheduleDaemon() {
122
- try {
123
- return await installDaemon({
124
- plan: installPlan({ program: daemonProgram(), harborHome: harborDir() }),
125
- });
126
- }
127
- catch (error) {
128
- return {
129
- kind: 'unsupported',
130
- files: [],
131
- manual: [],
132
- detail: `Could not schedule the daemon automatically (${error instanceof Error ? error.message : 'unknown error'}). Run \`harbor daemon --install\` once, or \`harbor daemon\` in a terminal — until something ticks, nothing you connect will report.`,
133
- };
134
- }
135
- }
136
- function daemonLine(outcome) {
137
- if (outcome === null)
138
- return 'not scheduled (--no-daemon)';
139
- switch (outcome.kind) {
140
- case 'installed':
141
- return `scheduled (${outcome.files.length} unit${outcome.files.length === 1 ? '' : 's'})`;
142
- case 'partial':
143
- return 'partly scheduled — see below';
144
- case 'removed':
145
- return 'removed';
146
- case 'unsupported':
147
- return 'NOT scheduled — see below';
148
- }
149
- }
150
- export function renderInit(result) {
151
- const lines = [`Connected to workspace "${result.workspaceName}".`, ''];
152
- for (const install of result.installs) {
153
- const label = install.agent.padEnd(10);
154
- lines.push(` ${label} ${install.configPath}`);
155
- if (install.trustSkipped !== null) {
156
- lines.push(` ⚠ hooks written but NOT trusted — ${install.trustSkipped}`, ' Codex will not run them. Re-run `harbor init` once `codex` is available.');
157
- }
158
- else if (install.verifiedHooks !== null && install.verifiedHooks === 0) {
159
- lines.push(' ⚠ Codex registered none of our hooks. `harbor doctor` has the detail.');
160
- }
161
- else if (install.verifiedHooks !== null) {
162
- lines.push(` ${install.verifiedHooks} hooks trusted`);
163
- }
164
- }
165
- lines.push(` hooks ${result.hooksDir}`, ` repos ${result.origins.length} bound${result.boundRepo ? ` (this one: ${result.boundRepo})` : ''}`, ` daemon ${daemonLine(result.daemon)}`);
166
- const manual = result.daemon?.manual ?? [];
167
- if (result.daemon && result.daemon.kind !== 'installed') {
168
- lines.push('', result.daemon.detail);
169
- for (const command of manual) {
170
- lines.push(` ${command.join(' ')}`);
171
- }
172
- }
173
- if (result.fellBackToClaude) {
174
- lines.push('', 'No agent was detected on this machine, so Claude Code was configured as', 'the default. Re-run `harbor init` after installing Claude Code or Codex.');
175
- }
176
- if (result.migratedLegacyState) {
177
- lines.push('', 'Adopted the existing desktop-app setup from ~/.milestone.');
178
- }
179
- if (!result.boundRepo) {
180
- lines.push('', 'This directory is not a bound repo. Run `harbor bind` inside one to', 'give its sessions a project scope.');
181
- }
182
- const started = result.installs.map((i) => `\`${i.agent}\``).join(' or ');
183
- lines.push('', `Start ${started} in a bound repo. Check it with \`harbor doctor\`.`);
184
- return lines.join('\n');
185
- }
186
- export async function runLogout() {
187
- const auth = await readAuth();
188
- if (!auth)
189
- return;
190
- const { clearAuth } = await import('../auth/store.js');
191
- await clearAuth();
192
- }
@@ -1,5 +0,0 @@
1
- import type { Check } from '../render.js';
2
- export interface StatusOptions {
3
- readonly cwd: string;
4
- }
5
- export declare function runStatus(opts: StatusOptions): Promise<Check[]>;
@@ -1,164 +0,0 @@
1
- import { readAuth } from '../auth/store.js';
2
- import { repoOrigin } from '../origin.js';
3
- import * as claude from '../providers/claude.js';
4
- import * as codex from '../providers/codex.js';
5
- import * as cursor from '../providers/cursor.js';
6
- import { detectAgents } from '../providers/detect.js';
7
- import { readSyncedConfig, resolveArms } from '../state.js';
8
- export async function runStatus(opts) {
9
- const [auth, config, settings, claudeMcpConfig, present, codexConfig, codexHooksJson, cursorMcpJson, cursorHooksJson,] = await Promise.all([
10
- readAuth(),
11
- readSyncedConfig(),
12
- claude.readClaudeSettings(),
13
- claude.readClaudeMcpConfig(),
14
- detectAgents(),
15
- codex.readCodexConfig(),
16
- codex.readCodexHooksJson(),
17
- cursor.readCursorMcpJson(),
18
- cursor.readCursorHooksJson(),
19
- ]);
20
- const origin = repoOrigin(opts.cwd);
21
- const bound = Boolean(config && origin !== '' && config.origins.includes(origin));
22
- const arms = config ? resolveArms(config.pause, origin) : null;
23
- const agents = [];
24
- appendClaudeCheck(agents, present, settings, claudeMcpConfig);
25
- appendCodexCheck(agents, present, codexConfig, codexHooksJson);
26
- appendCursorCheck(agents, present, cursorMcpJson, cursorHooksJson);
27
- if (agents.length === 0)
28
- agents.push(claudeNotInstalled());
29
- return [
30
- {
31
- label: 'Workspace',
32
- state: auth ? 'ok' : 'fail',
33
- detail: auth ? auth.workspaceName : 'not signed in',
34
- ...(auth ? {} : { fix: 'harbor init' }),
35
- },
36
- {
37
- label: 'Repo',
38
- state: origin === '' ? 'warn' : bound ? 'ok' : 'fail',
39
- detail: origin === ''
40
- ? 'no git origin here'
41
- : bound
42
- ? origin
43
- : `${origin} — not bound`,
44
- ...(origin !== '' && !bound ? { fix: 'harbor bind' } : {}),
45
- },
46
- ...agents,
47
- {
48
- label: 'Recording',
49
- state: !arms ? 'unknown' : arms.recall && arms.capture ? 'ok' : 'paused',
50
- detail: !arms
51
- ? 'not configured'
52
- : arms.recall && arms.capture
53
- ? 'on'
54
- : arms.recall
55
- ? 'recall on, recording paused on purpose'
56
- : 'paused, on purpose',
57
- },
58
- ];
59
- }
60
- function claudeNotInstalled() {
61
- return {
62
- label: 'Hooks',
63
- state: 'fail',
64
- detail: 'not installed',
65
- fix: 'harbor init',
66
- };
67
- }
68
- function appendClaudeCheck(checks, present, settings, mcpConfig) {
69
- const hooks = claude.readInstalledHooks(settings);
70
- const hasMcp = claude.readInstalledToken(mcpConfig) !== null;
71
- if (!present.claude && !hasMcp && !hooks.installed)
72
- return;
73
- if (!hooks.installed) {
74
- checks.push(claudeNotInstalled());
75
- return;
76
- }
77
- checks.push({ label: 'Hooks', state: 'ok', detail: 'installed' });
78
- }
79
- function appendCodexCheck(checks, present, config, hooksJson) {
80
- const hasMcp = codex.hasMcpServer(config);
81
- const hooks = codex.readInstalledHooks(hooksJson);
82
- if (!present.codex && !hasMcp && !hooks.installed)
83
- return;
84
- if (!hasMcp && !hooks.installed) {
85
- checks.push({
86
- label: 'Codex',
87
- state: 'warn',
88
- detail: 'installed here, but not configured',
89
- fix: 'harbor init',
90
- });
91
- return;
92
- }
93
- if (!hooks.installed) {
94
- checks.push({
95
- label: 'Codex',
96
- state: 'fail',
97
- detail: 'MCP configured, but no hooks',
98
- fix: 'harbor init',
99
- });
100
- return;
101
- }
102
- if (!hasMcp) {
103
- checks.push({
104
- label: 'Codex',
105
- state: 'fail',
106
- detail: 'hooks registered, but no MCP bearer',
107
- fix: 'harbor init',
108
- });
109
- return;
110
- }
111
- const stamps = codex.countHookTrustStamps(config, codex.resolveCodexPaths().hooksJsonPath);
112
- if (stamps === 0) {
113
- checks.push({
114
- label: 'Codex',
115
- state: 'fail',
116
- detail: `${hooks.commands.length} hooks registered and NONE trusted`,
117
- fix: 'harbor init (stamps the trust hashes `codex` reports)',
118
- });
119
- return;
120
- }
121
- checks.push({
122
- label: 'Codex',
123
- state: 'ok',
124
- detail: `${hooks.commands.length} installed, ${stamps} trusted`,
125
- });
126
- }
127
- function appendCursorCheck(checks, present, mcpJson, hooksJson) {
128
- const hasMcp = cursor.hasMcpServer(mcpJson);
129
- const hooks = cursor.readInstalledHooks(hooksJson);
130
- if (!present.cursor && !hasMcp && !hooks.installed)
131
- return;
132
- if (!hasMcp && !hooks.installed) {
133
- checks.push({
134
- label: 'Cursor',
135
- state: 'warn',
136
- detail: 'installed here, but not configured',
137
- fix: 'harbor init',
138
- });
139
- return;
140
- }
141
- if (!hooks.installed) {
142
- checks.push({
143
- label: 'Cursor',
144
- state: 'fail',
145
- detail: 'MCP configured, but no hooks',
146
- fix: 'harbor init',
147
- });
148
- return;
149
- }
150
- if (!hasMcp) {
151
- checks.push({
152
- label: 'Cursor',
153
- state: 'fail',
154
- detail: 'hooks registered, but no MCP bearer',
155
- fix: 'harbor init',
156
- });
157
- return;
158
- }
159
- checks.push({
160
- label: 'Cursor',
161
- state: 'ok',
162
- detail: `${hooks.commands.length} installed`,
163
- });
164
- }
@@ -1,40 +0,0 @@
1
- import type { AgentPresence } from '../providers/detect.js';
2
- import type { ApplyDocumentsOutcome } from '../documents/apply.js';
3
- import type { Endpoints } from '../endpoints.js';
4
- import { type EnvDocumentOutcome, type EnvSkillOutcome } from '../env-sync.js';
5
- import type { MemorySyncSummary } from '../memories.js';
6
- import type { SkillSyncSummary } from '../skills.js';
7
- import { type PostSyncReportResult } from '../sync-report.js';
8
- export interface SyncOptions {
9
- readonly cwd: string;
10
- readonly endpoints: Endpoints;
11
- readonly fetchImpl?: typeof fetch;
12
- readonly env?: NodeJS.ProcessEnv;
13
- readonly home?: string;
14
- readonly agents?: AgentPresence;
15
- }
16
- export type { EnvDocumentOutcome, EnvSkillOutcome };
17
- export interface SyncedPayloads {
18
- readonly kind: 'synced';
19
- readonly documents: ApplyDocumentsOutcome;
20
- readonly skills: SkillSyncSummary;
21
- readonly envSkills: EnvSkillOutcome;
22
- readonly envDocuments: EnvDocumentOutcome;
23
- readonly memories: MemorySyncSummary;
24
- readonly report: PostSyncReportResult;
25
- }
26
- export interface SyncResult {
27
- readonly originUrl: string;
28
- readonly repoPath: string;
29
- readonly credentialSource: 'env' | 'file';
30
- readonly outcome: SyncedPayloads | {
31
- readonly kind: 'sync_disabled';
32
- readonly reason: 'workspace' | 'admin';
33
- } | {
34
- readonly kind: 'error';
35
- readonly message: string;
36
- };
37
- }
38
- export declare function runSync(opts: SyncOptions): Promise<SyncResult>;
39
- export declare function renderSync(result: SyncResult): string;
40
- export declare function exitCodeForSync(result: SyncResult): number;
@@ -1,242 +0,0 @@
1
- import { readAuth, resolveCredentials } from '../auth/store.js';
2
- import { syncEnvPlane, } from '../env-sync.js';
3
- import { gitRoot, repoOrigin } from '../origin.js';
4
- import { syncRepoPlane } from '../repo-sync.js';
5
- import { noteCheckout } from '../repos.js';
6
- import { buildSyncReport, postSyncReport, } from '../sync-report.js';
7
- import { NotARepoError, NotSignedInError } from './bind.js';
8
- export async function runSync(opts) {
9
- const credentials = await resolveCredentials(opts.env);
10
- if (!credentials)
11
- throw new NotSignedInError();
12
- const repoPath = gitRoot(opts.cwd);
13
- const originUrl = repoOrigin(opts.cwd);
14
- if (repoPath === null || originUrl === '')
15
- throw new NotARepoError(opts.cwd);
16
- const stored = credentials.source === 'file' ? await readAuth() : null;
17
- const apiBase = stored?.apiUrl ?? opts.endpoints.apiUrl;
18
- const base = {
19
- originUrl,
20
- repoPath,
21
- credentialSource: credentials.source,
22
- };
23
- const repo = await syncRepoPlane({
24
- apiBase,
25
- workspaceId: credentials.workspaceId,
26
- bearerToken: credentials.runnerToken,
27
- repoPath,
28
- originUrl,
29
- ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
30
- ...(opts.home === undefined ? {} : { home: opts.home }),
31
- ...(opts.agents === undefined ? {} : { agents: opts.agents }),
32
- });
33
- if (repo.kind === 'sync_disabled') {
34
- return { ...base, outcome: { kind: 'sync_disabled', reason: repo.reason } };
35
- }
36
- if (repo.kind === 'error') {
37
- return { ...base, outcome: { kind: 'error', message: repo.message } };
38
- }
39
- const env = await syncEnvPlane({
40
- apiBase,
41
- workspaceId: credentials.workspaceId,
42
- bearerToken: credentials.runnerToken,
43
- ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
44
- ...(opts.home === undefined ? {} : { home: opts.home }),
45
- });
46
- const report = await reportThisMachine({
47
- apiBase,
48
- workspaceId: credentials.workspaceId,
49
- bearerToken: credentials.runnerToken,
50
- ...(env.bundleHash === undefined ? {} : { bundleHash: env.bundleHash }),
51
- ...(opts.home === undefined ? {} : { home: opts.home }),
52
- ...(opts.fetchImpl ? { fetchImpl: opts.fetchImpl } : {}),
53
- });
54
- await noteCheckout(originUrl, repoPath);
55
- return {
56
- ...base,
57
- outcome: {
58
- kind: 'synced',
59
- documents: repo.documents,
60
- skills: repo.skills,
61
- envSkills: env.skills,
62
- envDocuments: env.documents,
63
- memories: repo.memories,
64
- report,
65
- },
66
- };
67
- }
68
- async function reportThisMachine(input) {
69
- try {
70
- const report = await buildSyncReport({
71
- ...(input.home === undefined ? {} : { home: input.home }),
72
- ...(input.bundleHash === undefined ? {} : { bundleHash: input.bundleHash }),
73
- });
74
- return await postSyncReport({
75
- apiBase: input.apiBase,
76
- workspaceId: input.workspaceId,
77
- bearerToken: input.bearerToken,
78
- report,
79
- ...(input.fetchImpl ? { fetchImpl: input.fetchImpl } : {}),
80
- });
81
- }
82
- catch (err) {
83
- return {
84
- kind: 'error',
85
- status: 'network',
86
- message: err instanceof Error ? err.message : String(err),
87
- };
88
- }
89
- }
90
- export function renderSync(result) {
91
- const lines = [];
92
- const { outcome } = result;
93
- if (outcome.kind === 'sync_disabled') {
94
- lines.push(outcome.reason === 'workspace'
95
- ? 'Sync is switched off for this workspace. Nothing was written.'
96
- : 'Sync is switched off for this repository. Nothing was written.');
97
- lines.push('Turn it back on in the webapp, then run `harbor sync` again.');
98
- return lines.join('\n');
99
- }
100
- if (outcome.kind === 'error') {
101
- return `Could not fetch this repo's context: ${outcome.message}`;
102
- }
103
- const count = (n, one, many) => `${n} ${n === 1 ? one : many}`;
104
- const skillsSynced = outcome.skills.results.filter((r) => r.status === 'synced');
105
- if (skillsSynced.length > 0) {
106
- lines.push(`${count(skillsSynced.length, 'skill', 'skills')} in ${outcome.skills.dir}`);
107
- }
108
- for (const result of outcome.skills.results) {
109
- if (result.status === 'removed') {
110
- lines.push(` ${result.dir ?? result.file} — removed (no longer connected)`);
111
- }
112
- if (result.status === 'forked') {
113
- lines.push(` ${result.dir ?? result.file} — you edited this one; left alone, not updated`);
114
- }
115
- if (result.status === 'skipped_theirs') {
116
- lines.push(` ${result.dir ?? result.file} — a skill you wrote already has this name; not overwritten`);
117
- }
118
- }
119
- const env = outcome.envSkills;
120
- if (env.kind === 'unsupported') {
121
- lines.push('This backend does not serve env skills yet — none were written.');
122
- }
123
- else if (env.kind === 'error') {
124
- lines.push('Could not fetch your env skills — nothing in your home was touched.');
125
- }
126
- else {
127
- for (const root of env.summary.roots) {
128
- const synced = root.results.filter((r) => r.status === 'synced');
129
- if (synced.length > 0) {
130
- lines.push(`${count(synced.length, 'skill', 'skills')} in ${root.dir} (yours, every repo)`);
131
- }
132
- for (const result of root.results) {
133
- const name = result.dir ?? result.file;
134
- if (result.status === 'removed') {
135
- lines.push(` ${name} — removed from your env (no longer connected)`);
136
- }
137
- if (result.status === 'forked') {
138
- lines.push(` ${name} — you edited this one; left alone, not updated`);
139
- }
140
- if (result.status === 'skipped_theirs') {
141
- lines.push(` ${name} — a skill you wrote already has this name; not overwritten`);
142
- }
143
- }
144
- }
145
- }
146
- const memoriesSynced = outcome.memories.results.filter((r) => r.status === 'synced');
147
- if (memoriesSynced.length > 0) {
148
- lines.push(`${count(memoriesSynced.length, 'memory', 'memories')} in ${outcome.memories.dir}`);
149
- }
150
- const documents = outcome.documents;
151
- if (documents.kind === 'unsupported') {
152
- lines.push('This backend does not serve documents yet — none were written.');
153
- }
154
- else if (documents.reclaimed) {
155
- const removed = documents.documents.filter((r) => r.status === 'removed').length;
156
- lines.push(`No documents are connected to this repo any more — removed ${count(removed, 'file', 'files')} from ${documents.dir}.`);
157
- for (const pointer of documents.pointer) {
158
- lines.push(` ${pointer.file} — pointer line ${pointer.status}`);
159
- }
160
- }
161
- else {
162
- const synced = documents.documents.filter((r) => r.status === 'synced');
163
- if (synced.length > 0) {
164
- lines.push(`${count(synced.length, 'document', 'documents')} in ${documents.dir}`);
165
- }
166
- for (const entry of documents.documents) {
167
- if (entry.status === 'synced')
168
- continue;
169
- if (entry.status === 'skipped_theirs') {
170
- lines.push(` ${entry.file} — skipped: that name is a file we never wrote.`);
171
- continue;
172
- }
173
- if (entry.status === 'forked') {
174
- lines.push(` ${entry.file} — has local edits, so it was left alone and stops updating.`);
175
- continue;
176
- }
177
- lines.push(` ${entry.file} — removed (no longer connected)`);
178
- }
179
- for (const pointer of documents.pointer) {
180
- if (pointer.status === 'present')
181
- continue;
182
- lines.push(` ${pointer.file} — pointer line ${pointer.status}`);
183
- }
184
- }
185
- const envDocs = outcome.envDocuments;
186
- if (envDocs.kind === 'unsupported') {
187
- lines.push('This backend does not serve env documents yet — none were written.');
188
- }
189
- else if (envDocs.kind === 'error') {
190
- lines.push('Could not fetch your env documents — nothing in your home was touched.');
191
- }
192
- else if (envDocs.outcome.kind === 'unsupported') {
193
- lines.push('This backend does not serve env documents yet — none were written.');
194
- }
195
- else if (envDocs.outcome.reclaimed) {
196
- const removed = envDocs.outcome.documents.filter((r) => r.status === 'removed').length;
197
- lines.push(`No documents reach you any more — removed ${count(removed, 'file', 'files')} from ${envDocs.outcome.dir}.`);
198
- for (const pointer of envDocs.outcome.pointer) {
199
- lines.push(` ${pointer.file} — pointer line ${pointer.status}`);
200
- }
201
- }
202
- else {
203
- const synced = envDocs.outcome.documents.filter((r) => r.status === 'synced');
204
- if (synced.length > 0) {
205
- lines.push(`${count(synced.length, 'document', 'documents')} in ${envDocs.outcome.dir} (yours, every repo)`);
206
- }
207
- for (const entry of envDocs.outcome.documents) {
208
- if (entry.status === 'synced')
209
- continue;
210
- if (entry.status === 'skipped_theirs') {
211
- lines.push(` ${entry.file} — skipped: that name is a file we never wrote.`);
212
- continue;
213
- }
214
- if (entry.status === 'forked') {
215
- lines.push(` ${entry.file} — has local edits, so it was left alone and stops updating.`);
216
- continue;
217
- }
218
- lines.push(` ${entry.file} — removed from your env (no longer connected)`);
219
- }
220
- for (const pointer of envDocs.outcome.pointer) {
221
- if (pointer.status === 'present')
222
- continue;
223
- lines.push(` ${pointer.file} — pointer line ${pointer.status}`);
224
- }
225
- }
226
- const receipt = outcome.report.kind === 'error'
227
- ? [
228
- `⚠️ The files landed, but this machine could not report them: ${outcome.report.message}`,
229
- " Your team's map will show this machine as stale until the next sync.",
230
- ]
231
- : [];
232
- if (lines.length === 0) {
233
- return [
234
- 'Nothing is connected to this repo yet — no skills, memories or documents.',
235
- ...receipt,
236
- ].join('\n');
237
- }
238
- return [...lines, ...receipt].join('\n');
239
- }
240
- export function exitCodeForSync(result) {
241
- return result.outcome.kind === 'error' ? 1 : 0;
242
- }