holistic 0.2.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CONTRIBUTING.md +121 -0
  3. package/LICENSE +21 -0
  4. package/README.md +329 -0
  5. package/bin/holistic +17 -0
  6. package/bin/holistic.cmd +23 -0
  7. package/bin/holistic.js +59 -0
  8. package/dist/__tests__/mcp-notification.test.d.ts +5 -0
  9. package/dist/__tests__/mcp-notification.test.d.ts.map +1 -0
  10. package/dist/__tests__/mcp-notification.test.js +255 -0
  11. package/dist/__tests__/mcp-notification.test.js.map +1 -0
  12. package/dist/cli.d.ts +6 -0
  13. package/dist/cli.d.ts.map +1 -0
  14. package/dist/cli.js +637 -0
  15. package/dist/cli.js.map +1 -0
  16. package/dist/core/docs.d.ts +3 -0
  17. package/dist/core/docs.d.ts.map +1 -0
  18. package/dist/core/docs.js +602 -0
  19. package/dist/core/docs.js.map +1 -0
  20. package/dist/core/git-hooks.d.ts +17 -0
  21. package/dist/core/git-hooks.d.ts.map +1 -0
  22. package/dist/core/git-hooks.js +144 -0
  23. package/dist/core/git-hooks.js.map +1 -0
  24. package/dist/core/git.d.ts +12 -0
  25. package/dist/core/git.d.ts.map +1 -0
  26. package/dist/core/git.js +121 -0
  27. package/dist/core/git.js.map +1 -0
  28. package/dist/core/lock.d.ts +2 -0
  29. package/dist/core/lock.d.ts.map +1 -0
  30. package/dist/core/lock.js +40 -0
  31. package/dist/core/lock.js.map +1 -0
  32. package/dist/core/redact.d.ts +3 -0
  33. package/dist/core/redact.d.ts.map +1 -0
  34. package/dist/core/redact.js +13 -0
  35. package/dist/core/redact.js.map +1 -0
  36. package/dist/core/setup.d.ts +35 -0
  37. package/dist/core/setup.d.ts.map +1 -0
  38. package/dist/core/setup.js +654 -0
  39. package/dist/core/setup.js.map +1 -0
  40. package/dist/core/splash.d.ts +9 -0
  41. package/dist/core/splash.d.ts.map +1 -0
  42. package/dist/core/splash.js +35 -0
  43. package/dist/core/splash.js.map +1 -0
  44. package/dist/core/state.d.ts +42 -0
  45. package/dist/core/state.d.ts.map +1 -0
  46. package/dist/core/state.js +744 -0
  47. package/dist/core/state.js.map +1 -0
  48. package/dist/core/sync.d.ts +12 -0
  49. package/dist/core/sync.d.ts.map +1 -0
  50. package/dist/core/sync.js +106 -0
  51. package/dist/core/sync.js.map +1 -0
  52. package/dist/core/types.d.ts +210 -0
  53. package/dist/core/types.d.ts.map +1 -0
  54. package/dist/core/types.js +2 -0
  55. package/dist/core/types.js.map +1 -0
  56. package/dist/daemon.d.ts +7 -0
  57. package/dist/daemon.d.ts.map +1 -0
  58. package/dist/daemon.js +242 -0
  59. package/dist/daemon.js.map +1 -0
  60. package/dist/mcp-server.d.ts +11 -0
  61. package/dist/mcp-server.d.ts.map +1 -0
  62. package/dist/mcp-server.js +266 -0
  63. package/dist/mcp-server.js.map +1 -0
  64. package/docs/handoff-walkthrough.md +119 -0
  65. package/docs/structured-metadata.md +341 -0
  66. package/package.json +67 -0
@@ -0,0 +1,654 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { writeDerivedDocs } from './docs.js';
6
+ import { captureRepoSnapshot, resolveGitDir } from './git.js';
7
+ import { installGitHooks, refreshGitHooks } from './git-hooks.js';
8
+ import { getRuntimePaths, loadState, saveState } from './state.js';
9
+ const DEFAULT_STATE_REF = "refs/holistic/state";
10
+ const DEFAULT_LEGACY_STATE_BRANCH = "holistic/state";
11
+ const TEMP_SYNC_BRANCH = "holistic-sync-tmp";
12
+ const HOLISTIC_GITATTRIBUTES_BEGIN = "# BEGIN HOLISTIC MANAGED ATTRIBUTES";
13
+ const HOLISTIC_GITATTRIBUTES_END = "# END HOLISTIC MANAGED ATTRIBUTES";
14
+ function persist(rootDir, state, paths) {
15
+ writeDerivedDocs(paths, state);
16
+ state.repoSnapshot = captureRepoSnapshot(rootDir);
17
+ saveState(paths, state);
18
+ return state;
19
+ }
20
+ function projectSlug(rootDir) {
21
+ const base = path.basename(rootDir).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
22
+ return base || "holistic-project";
23
+ }
24
+ function systemDir(paths) {
25
+ return path.join(paths.holisticDir, "system");
26
+ }
27
+ function configFile(paths) {
28
+ return path.join(paths.holisticDir, "config.json");
29
+ }
30
+ function runtimeEntryScript(name) {
31
+ const currentFile = fileURLToPath(import.meta.url);
32
+ const extension = path.extname(currentFile);
33
+ const runtimeDir = path.dirname(currentFile);
34
+ const useStripTypes = extension === ".ts";
35
+ return {
36
+ scriptPath: path.resolve(runtimeDir, `../${name}${useStripTypes ? ".ts" : ".js"}`),
37
+ useStripTypes,
38
+ };
39
+ }
40
+ function mcpConfigFile(platform, homeDir) {
41
+ switch (platform) {
42
+ case "win32":
43
+ return path.join(homeDir, "AppData", "Roaming", "Claude", "claude_desktop_config.json");
44
+ case "darwin":
45
+ return path.join(homeDir, "Library", "Application Support", "Claude", "claude_desktop_config.json");
46
+ case "linux":
47
+ default:
48
+ return path.join(homeDir, ".config", "Claude", "claude_desktop_config.json");
49
+ }
50
+ }
51
+ function quotePowerShell(value) {
52
+ return value.replace(/'/g, "''");
53
+ }
54
+ function shellQuote(value) {
55
+ return value.replace(/'/g, `'"'"'`);
56
+ }
57
+ function readRepoSetupConfig(rootDir) {
58
+ const configPath = path.join(rootDir, "holistic.repo.json");
59
+ if (!fs.existsSync(configPath)) {
60
+ return {};
61
+ }
62
+ try {
63
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
64
+ }
65
+ catch {
66
+ return {};
67
+ }
68
+ }
69
+ function relativePath(rootDir, targetPath) {
70
+ if (!targetPath) {
71
+ return null;
72
+ }
73
+ return path.relative(rootDir, targetPath).replaceAll("\\", "/");
74
+ }
75
+ function shouldManageGitAttributes(paths) {
76
+ return path.basename(paths.holisticDir) === ".holistic"
77
+ && path.basename(paths.masterDoc) === "HOLISTIC.md"
78
+ && path.basename(paths.agentsDoc) === "AGENTS.md";
79
+ }
80
+ function renderHolisticGitAttributes(paths) {
81
+ const lines = [
82
+ HOLISTIC_GITATTRIBUTES_BEGIN,
83
+ `${relativePath(paths.rootDir, paths.masterDoc)} text eol=lf`,
84
+ `${relativePath(paths.rootDir, paths.agentsDoc)} text eol=lf`,
85
+ ];
86
+ const rootHistory = relativePath(paths.rootDir, paths.rootHistoryDoc);
87
+ const rootClaude = relativePath(paths.rootDir, paths.rootClaudeDoc);
88
+ const rootGemini = relativePath(paths.rootDir, paths.rootGeminiDoc);
89
+ const holisticDir = relativePath(paths.rootDir, paths.holisticDir);
90
+ if (rootHistory) {
91
+ lines.push(`${rootHistory} text eol=lf`);
92
+ }
93
+ if (rootClaude) {
94
+ lines.push(`${rootClaude} text eol=lf`);
95
+ }
96
+ if (rootGemini) {
97
+ lines.push(`${rootGemini} text eol=lf`);
98
+ }
99
+ if (holisticDir) {
100
+ lines.push(`${holisticDir}/**/*.md text eol=lf`);
101
+ lines.push(`${holisticDir}/**/*.json text eol=lf`);
102
+ lines.push(`${holisticDir}/**/*.sh text eol=lf`);
103
+ lines.push(`${holisticDir}/**/*.ps1 text eol=crlf`);
104
+ lines.push(`${holisticDir}/**/*.cmd text eol=crlf`);
105
+ }
106
+ lines.push(HOLISTIC_GITATTRIBUTES_END);
107
+ return lines.join("\n");
108
+ }
109
+ function writeManagedGitAttributes(rootDir, paths) {
110
+ if (!shouldManageGitAttributes(paths)) {
111
+ return;
112
+ }
113
+ const attributesPath = path.join(rootDir, ".gitattributes");
114
+ const managedBlock = renderHolisticGitAttributes(paths);
115
+ const current = fs.existsSync(attributesPath) ? fs.readFileSync(attributesPath, "utf8") : "";
116
+ const pattern = new RegExp(`${HOLISTIC_GITATTRIBUTES_BEGIN}[\\s\\S]*?${HOLISTIC_GITATTRIBUTES_END}\\n?`, "m");
117
+ let next;
118
+ if (pattern.test(current)) {
119
+ next = current.replace(pattern, `${managedBlock}\n`);
120
+ }
121
+ else if (current.trim().length === 0) {
122
+ next = `${managedBlock}\n`;
123
+ }
124
+ else {
125
+ next = `${current.replace(/\s*$/, "\n\n")}${managedBlock}\n`;
126
+ }
127
+ fs.writeFileSync(attributesPath, next, "utf8");
128
+ }
129
+ function powerShellStringArray(values) {
130
+ return values.map((value) => `'${quotePowerShell(value)}'`).join(", ");
131
+ }
132
+ function shellPathList(values) {
133
+ return values.map((value) => `"${value}"`).join(" ");
134
+ }
135
+ function isDirectoryTrackedPath(trackedPath) {
136
+ return !path.extname(trackedPath);
137
+ }
138
+ function normalizeStateBranch(value) {
139
+ return value.startsWith("refs/heads/") ? value.slice("refs/heads/".length) : value;
140
+ }
141
+ function branchToRef(branch) {
142
+ return branch.startsWith("refs/") ? branch : `refs/heads/${branch}`;
143
+ }
144
+ function resolveSyncTarget(options) {
145
+ if (options.stateRef) {
146
+ return {
147
+ ref: options.stateRef,
148
+ stateRef: options.stateRef,
149
+ legacySeedBranch: options.stateRef === DEFAULT_STATE_REF ? DEFAULT_LEGACY_STATE_BRANCH : null,
150
+ };
151
+ }
152
+ if (options.stateBranch) {
153
+ const stateBranch = normalizeStateBranch(options.stateBranch);
154
+ return {
155
+ ref: branchToRef(stateBranch),
156
+ stateBranch,
157
+ };
158
+ }
159
+ return {
160
+ ref: DEFAULT_STATE_REF,
161
+ stateRef: DEFAULT_STATE_REF,
162
+ legacySeedBranch: DEFAULT_LEGACY_STATE_BRANCH,
163
+ };
164
+ }
165
+ function buildPowerShellCopyCommands(rootDir, trackedPaths) {
166
+ return trackedPaths.map((trackedPath) => {
167
+ const source = `Join-Path $root '${quotePowerShell(trackedPath.replaceAll("/", "\\"))}'`;
168
+ const destination = `Join-Path $tmp '${quotePowerShell(trackedPath.replaceAll("/", "\\"))}'`;
169
+ const destinationDir = path.dirname(trackedPath).replaceAll("/", "\\");
170
+ const ensureParent = destinationDir === "."
171
+ ? []
172
+ : [` New-Item -ItemType Directory -Path (Join-Path $tmp '${quotePowerShell(destinationDir)}') -Force | Out-Null`];
173
+ if (isDirectoryTrackedPath(trackedPath)) {
174
+ return [
175
+ ...ensureParent,
176
+ ` Copy-Item -Path (${source}) -Destination (${destination}) -Recurse -Force`,
177
+ ].join("\n");
178
+ }
179
+ return [
180
+ ...ensureParent,
181
+ ` Copy-Item -Path (${source}) -Destination (${destination}) -Force`,
182
+ ].join("\n");
183
+ });
184
+ }
185
+ function buildShellCopyCommands(trackedPaths) {
186
+ const lines = [];
187
+ for (const trackedPath of trackedPaths) {
188
+ const dirName = path.posix.dirname(trackedPath.replaceAll("\\", "/"));
189
+ if (dirName !== ".") {
190
+ lines.push(`mkdir -p "$TMPDIR/${dirName}"`);
191
+ }
192
+ if (isDirectoryTrackedPath(trackedPath)) {
193
+ lines.push(`cp -R "$ROOT/${trackedPath}" "$TMPDIR/${trackedPath}"`);
194
+ }
195
+ else {
196
+ lines.push(`cp "$ROOT/${trackedPath}" "$TMPDIR/${trackedPath}"`);
197
+ }
198
+ }
199
+ return lines;
200
+ }
201
+ function writeConfig(paths, remote, syncTarget, intervalSeconds) {
202
+ const repoConfig = readRepoSetupConfig(paths.rootDir);
203
+ const syncDefaults = repoConfig.syncDefaults ?? {};
204
+ const syncConfig = {
205
+ strategy: "state-branch",
206
+ remote,
207
+ syncOnCheckpoint: syncDefaults.syncOnCheckpoint ?? true,
208
+ syncOnHandoff: syncDefaults.syncOnHandoff ?? true,
209
+ postHandoffPush: syncDefaults.postHandoffPush ?? true,
210
+ restoreOnStartup: syncDefaults.restoreOnStartup ?? true,
211
+ trackedPaths: paths.trackedPaths,
212
+ ...(syncTarget.stateRef ? { stateRef: syncTarget.stateRef } : {}),
213
+ ...(syncTarget.stateBranch ? { stateBranch: syncTarget.stateBranch } : {}),
214
+ };
215
+ const config = {
216
+ version: 1,
217
+ autoInferSessions: true,
218
+ autoSync: syncDefaults.autoSync ?? true,
219
+ sync: syncConfig,
220
+ daemon: {
221
+ intervalSeconds,
222
+ agent: "unknown",
223
+ },
224
+ };
225
+ fs.writeFileSync(configFile(paths), JSON.stringify(config, null, 2) + "\n", "utf8");
226
+ }
227
+ function writeSystemArtifacts(rootDir, paths, intervalSeconds, remote, syncTarget) {
228
+ const sysDir = systemDir(paths);
229
+ fs.mkdirSync(sysDir, { recursive: true });
230
+ const trackedPaths = paths.trackedPaths;
231
+ const nodePath = process.execPath;
232
+ const daemonRuntime = runtimeEntryScript("daemon");
233
+ const daemonPath = daemonRuntime.scriptPath;
234
+ const restorePs1Path = path.join(sysDir, "restore-state.ps1");
235
+ const syncPs1Path = path.join(sysDir, "sync-state.ps1");
236
+ const restoreShPath = path.join(sysDir, "restore-state.sh");
237
+ const syncShPath = path.join(sysDir, "sync-state.sh");
238
+ const legacySeedRef = syncTarget.legacySeedBranch ? branchToRef(syncTarget.legacySeedBranch) : "";
239
+ const restorePs1 = [
240
+ "$ErrorActionPreference = 'Stop'",
241
+ "if (Get-Variable PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { $PSNativeCommandUseErrorActionPreference = $false }",
242
+ `$root = '${quotePowerShell(rootDir)}'`,
243
+ `$remote = '${quotePowerShell(remote)}'`,
244
+ `$stateRef = '${quotePowerShell(syncTarget.ref)}'`,
245
+ `$legacySeedRef = '${quotePowerShell(legacySeedRef)}'`,
246
+ `$tracked = @(${powerShellStringArray(trackedPaths)})`,
247
+ "$status = git -C $root status --porcelain -- $tracked 2>$null",
248
+ "if ($LASTEXITCODE -ne 0) { exit 0 }",
249
+ "if ($status) { Write-Host 'Holistic restore skipped because local Holistic files are dirty.'; exit 0 }",
250
+ "$restored = $false",
251
+ "try {",
252
+ " git -C $root fetch --quiet $remote $stateRef *> $null",
253
+ " if ($LASTEXITCODE -eq 0) {",
254
+ " git -C $root checkout FETCH_HEAD -- $tracked 2>$null | Out-Null",
255
+ " $restored = $true",
256
+ " }",
257
+ "} catch {",
258
+ " $restored = $false",
259
+ "}",
260
+ "if (-not $restored -and $legacySeedRef) {",
261
+ " try {",
262
+ " git -C $root fetch --quiet $remote $legacySeedRef *> $null",
263
+ " if ($LASTEXITCODE -eq 0) {",
264
+ " git -C $root checkout FETCH_HEAD -- $tracked 2>$null | Out-Null",
265
+ " $restored = $true",
266
+ " }",
267
+ " } catch {",
268
+ " $restored = $false",
269
+ " }",
270
+ "}",
271
+ "if (-not $restored) { Write-Host 'Holistic restore skipped because remote portable state is unavailable.'; exit 0 }",
272
+ ].join("\n");
273
+ const syncPs1 = [
274
+ "$ErrorActionPreference = 'Stop'",
275
+ "if (Get-Variable PSNativeCommandUseErrorActionPreference -ErrorAction SilentlyContinue) { $PSNativeCommandUseErrorActionPreference = $false }",
276
+ `$root = '${quotePowerShell(rootDir)}'`,
277
+ `$remote = '${quotePowerShell(remote)}'`,
278
+ `$stateRef = '${quotePowerShell(syncTarget.ref)}'`,
279
+ `$legacySeedRef = '${quotePowerShell(legacySeedRef)}'`,
280
+ "$branch = git -c core.hooksPath=NUL -C $root rev-parse --abbrev-ref HEAD",
281
+ "if ($LASTEXITCODE -ne 0) { throw 'Unable to determine current branch.' }",
282
+ "git -c core.hooksPath=NUL -C $root push $remote $branch",
283
+ "$tmp = Join-Path $env:TEMP ('holistic-state-' + [guid]::NewGuid().ToString())",
284
+ "git -c core.hooksPath=NUL -C $root worktree add --force $tmp | Out-Null",
285
+ "try {",
286
+ " Push-Location $tmp",
287
+ " $remoteStateExists = $false",
288
+ " $remoteLegacyExists = $false",
289
+ " try {",
290
+ " git -c core.hooksPath=NUL ls-remote --quiet --exit-code $remote $stateRef *> $null",
291
+ " $remoteStateExists = ($LASTEXITCODE -eq 0)",
292
+ " } catch {",
293
+ " $remoteStateExists = $false",
294
+ " }",
295
+ " if (-not $remoteStateExists -and $legacySeedRef) {",
296
+ " try {",
297
+ " git -c core.hooksPath=NUL ls-remote --quiet --exit-code $remote $legacySeedRef *> $null",
298
+ " $remoteLegacyExists = ($LASTEXITCODE -eq 0)",
299
+ " } catch {",
300
+ " $remoteLegacyExists = $false",
301
+ " }",
302
+ " }",
303
+ " if ($remoteStateExists) {",
304
+ " git -c core.hooksPath=NUL fetch --quiet $remote $stateRef *> $null",
305
+ " git -c core.hooksPath=NUL switch --detach FETCH_HEAD | Out-Null",
306
+ " } elseif ($remoteLegacyExists) {",
307
+ " git -c core.hooksPath=NUL fetch --quiet $remote $legacySeedRef *> $null",
308
+ " git -c core.hooksPath=NUL switch --detach FETCH_HEAD | Out-Null",
309
+ " } else {",
310
+ ` git -c core.hooksPath=NUL switch --orphan ${TEMP_SYNC_BRANCH} | Out-Null`,
311
+ " }",
312
+ " Get-ChildItem -Force | Where-Object { $_.Name -ne '.git' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue",
313
+ ...buildPowerShellCopyCommands(rootDir, trackedPaths),
314
+ ` git -c core.hooksPath=NUL add ${trackedPaths.map((trackedPath) => `'${quotePowerShell(trackedPath)}'`).join(" ")}`,
315
+ " git -c core.hooksPath=NUL diff --cached --quiet",
316
+ " if ($LASTEXITCODE -ne 0) {",
317
+ " git -c core.hooksPath=NUL commit -m 'chore(holistic): sync portable state' | Out-Null",
318
+ " }",
319
+ " git -c core.hooksPath=NUL push $remote HEAD:$stateRef",
320
+ "} finally {",
321
+ " Pop-Location",
322
+ " git -c core.hooksPath=NUL -C $root worktree remove --force $tmp | Out-Null",
323
+ "}",
324
+ ].join("\n");
325
+ const restoreSh = [
326
+ "#!/usr/bin/env sh",
327
+ `ROOT='${shellQuote(rootDir)}'`,
328
+ `REMOTE='${shellQuote(remote)}'`,
329
+ `STATE_REF='${shellQuote(syncTarget.ref)}'`,
330
+ `LEGACY_SEED_REF='${shellQuote(legacySeedRef)}'`,
331
+ `if ! git -C "$ROOT" diff --quiet -- ${shellPathList(trackedPaths)} 2>/dev/null; then`,
332
+ " echo 'Holistic restore skipped because local Holistic files are dirty.'",
333
+ " exit 0",
334
+ "fi",
335
+ "RESTORED=false",
336
+ "if git -C \"$ROOT\" fetch \"$REMOTE\" \"$STATE_REF\" >/dev/null 2>&1; then",
337
+ ` git -C "$ROOT" checkout FETCH_HEAD -- ${shellPathList(trackedPaths)} >/dev/null 2>&1 || true`,
338
+ " RESTORED=true",
339
+ "fi",
340
+ "if [ \"$RESTORED\" != \"true\" ] && [ -n \"$LEGACY_SEED_REF\" ]; then",
341
+ " if git -C \"$ROOT\" fetch \"$REMOTE\" \"$LEGACY_SEED_REF\" >/dev/null 2>&1; then",
342
+ ` git -C "$ROOT" checkout FETCH_HEAD -- ${shellPathList(trackedPaths)} >/dev/null 2>&1 || true`,
343
+ " RESTORED=true",
344
+ " fi",
345
+ "fi",
346
+ "if [ \"$RESTORED\" != \"true\" ]; then",
347
+ " echo 'Holistic restore skipped because remote portable state is unavailable.'",
348
+ " exit 0",
349
+ "fi",
350
+ ].join("\n");
351
+ const syncSh = [
352
+ "#!/usr/bin/env sh",
353
+ `ROOT='${shellQuote(rootDir)}'`,
354
+ `REMOTE='${shellQuote(remote)}'`,
355
+ `STATE_REF='${shellQuote(syncTarget.ref)}'`,
356
+ `LEGACY_SEED_REF='${shellQuote(legacySeedRef)}'`,
357
+ "BRANCH=$(git -c core.hooksPath=/dev/null -C \"$ROOT\" rev-parse --abbrev-ref HEAD) || exit 1",
358
+ "git -c core.hooksPath=/dev/null -C \"$ROOT\" push \"$REMOTE\" \"$BRANCH\" || exit 1",
359
+ "TMPDIR=$(mktemp -d 2>/dev/null || mktemp -d -t holistic-state)",
360
+ "git -c core.hooksPath=/dev/null -C \"$ROOT\" worktree add --force \"$TMPDIR\" >/dev/null 2>&1 || exit 1",
361
+ "cleanup() { git -c core.hooksPath=/dev/null -C \"$ROOT\" worktree remove --force \"$TMPDIR\" >/dev/null 2>&1; }",
362
+ "trap cleanup EXIT",
363
+ "cd \"$TMPDIR\" || exit 1",
364
+ "REMOTE_STATE_EXISTS=false",
365
+ "REMOTE_LEGACY_EXISTS=false",
366
+ "if git -c core.hooksPath=/dev/null ls-remote --quiet --exit-code \"$REMOTE\" \"$STATE_REF\" >/dev/null 2>&1; then",
367
+ " REMOTE_STATE_EXISTS=true",
368
+ "fi",
369
+ "if [ \"$REMOTE_STATE_EXISTS\" != \"true\" ] && [ -n \"$LEGACY_SEED_REF\" ]; then",
370
+ " if git -c core.hooksPath=/dev/null ls-remote --quiet --exit-code \"$REMOTE\" \"$LEGACY_SEED_REF\" >/dev/null 2>&1; then",
371
+ " REMOTE_LEGACY_EXISTS=true",
372
+ " fi",
373
+ "fi",
374
+ "if [ \"$REMOTE_STATE_EXISTS\" = \"true\" ]; then",
375
+ " git -c core.hooksPath=/dev/null fetch --quiet \"$REMOTE\" \"$STATE_REF\" >/dev/null 2>&1 || exit 1",
376
+ " git -c core.hooksPath=/dev/null switch --detach FETCH_HEAD >/dev/null 2>&1 || exit 1",
377
+ "elif [ \"$REMOTE_LEGACY_EXISTS\" = \"true\" ]; then",
378
+ " git -c core.hooksPath=/dev/null fetch --quiet \"$REMOTE\" \"$LEGACY_SEED_REF\" >/dev/null 2>&1 || exit 1",
379
+ " git -c core.hooksPath=/dev/null switch --detach FETCH_HEAD >/dev/null 2>&1 || exit 1",
380
+ "else",
381
+ ` git -c core.hooksPath=/dev/null switch --orphan "${TEMP_SYNC_BRANCH}" >/dev/null 2>&1 || exit 1`,
382
+ "fi",
383
+ "find . -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +",
384
+ ...buildShellCopyCommands(trackedPaths),
385
+ `git -c core.hooksPath=/dev/null add ${trackedPaths.map((trackedPath) => `"${trackedPath}"`).join(" ")}`,
386
+ "git -c core.hooksPath=/dev/null diff --cached --quiet || git -c core.hooksPath=/dev/null commit -m 'chore(holistic): sync portable state' >/dev/null 2>&1",
387
+ "git -c core.hooksPath=/dev/null push \"$REMOTE\" HEAD:\"$STATE_REF\"",
388
+ ].join("\n");
389
+ const runPs1 = [
390
+ "$ErrorActionPreference = 'Stop'",
391
+ `$node = '${quotePowerShell(nodePath)}'`,
392
+ `$daemon = '${quotePowerShell(daemonPath)}'`,
393
+ `$working = '${quotePowerShell(rootDir)}'`,
394
+ `& '${quotePowerShell(restorePs1Path)}'`,
395
+ `& $node ${daemonRuntime.useStripTypes ? "--experimental-strip-types " : ""}$daemon --interval ${intervalSeconds} --agent unknown`,
396
+ ].join("\n");
397
+ const runSh = [
398
+ "#!/usr/bin/env sh",
399
+ `cd '${shellQuote(rootDir)}' || exit 1`,
400
+ `'${shellQuote(restoreShPath)}' || true`,
401
+ `'${shellQuote(nodePath)}' ${daemonRuntime.useStripTypes ? "--experimental-strip-types " : ""}'${shellQuote(daemonPath)}' --interval ${intervalSeconds} --agent unknown`,
402
+ ].join("\n");
403
+ const readme = `# Holistic System Setup
404
+
405
+ This directory contains generated startup and sync helpers for Holistic.
406
+
407
+ Files:
408
+ - run-daemon.ps1 / run-daemon.sh: restore the portable state, then start the background daemon
409
+ - restore-state.ps1 / restore-state.sh: pull the portable Holistic state ref into the current worktree when safe
410
+ - sync-state.ps1 / sync-state.sh: push the current branch and mirror Holistic files into the portable state ref
411
+ - config in ../config.json defines the remote and portable state target
412
+ `;
413
+ fs.writeFileSync(path.join(sysDir, "run-daemon.ps1"), runPs1 + "\n", "utf8");
414
+ fs.writeFileSync(path.join(sysDir, "run-daemon.sh"), runSh + "\n", "utf8");
415
+ fs.writeFileSync(restorePs1Path, restorePs1 + "\n", "utf8");
416
+ fs.writeFileSync(syncPs1Path, syncPs1 + "\n", "utf8");
417
+ fs.writeFileSync(restoreShPath, restoreSh + "\n", "utf8");
418
+ fs.writeFileSync(syncShPath, syncSh + "\n", "utf8");
419
+ fs.writeFileSync(path.join(sysDir, "README.md"), readme, "utf8");
420
+ }
421
+ function installWindowsStartup(rootDir, paths, homeDir) {
422
+ const startupDir = path.join(homeDir, "AppData", "Roaming", "Microsoft", "Windows", "Start Menu", "Programs", "Startup");
423
+ fs.mkdirSync(startupDir, { recursive: true });
424
+ const slug = projectSlug(rootDir);
425
+ const target = path.join(startupDir, `holistic-${slug}.cmd`);
426
+ const psScript = path.join(systemDir(paths), "run-daemon.ps1");
427
+ const content = [
428
+ "@echo off",
429
+ `powershell -NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File \"${psScript}\"`,
430
+ ].join("\r\n");
431
+ fs.writeFileSync(target, content + "\r\n", "utf8");
432
+ return target;
433
+ }
434
+ function installMacosLaunchAgent(rootDir, paths, homeDir) {
435
+ const launchAgentsDir = path.join(homeDir, "Library", "LaunchAgents");
436
+ fs.mkdirSync(launchAgentsDir, { recursive: true });
437
+ const slug = projectSlug(rootDir);
438
+ const target = path.join(launchAgentsDir, `com.holistic.${slug}.plist`);
439
+ const runScript = path.join(systemDir(paths), "run-daemon.sh");
440
+ const plist = `<?xml version="1.0" encoding="UTF-8"?>
441
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
442
+ <plist version="1.0">
443
+ <dict>
444
+ <key>Label</key>
445
+ <string>com.holistic.${slug}</string>
446
+ <key>ProgramArguments</key>
447
+ <array>
448
+ <string>/bin/sh</string>
449
+ <string>${runScript}</string>
450
+ </array>
451
+ <key>RunAtLoad</key>
452
+ <true/>
453
+ <key>KeepAlive</key>
454
+ <true/>
455
+ <key>WorkingDirectory</key>
456
+ <string>${rootDir}</string>
457
+ </dict>
458
+ </plist>
459
+ `;
460
+ fs.writeFileSync(target, plist, "utf8");
461
+ return target;
462
+ }
463
+ function installLinuxUserService(rootDir, paths, homeDir) {
464
+ const userSystemdDir = path.join(homeDir, ".config", "systemd", "user");
465
+ const wantsDir = path.join(userSystemdDir, "default.target.wants");
466
+ fs.mkdirSync(userSystemdDir, { recursive: true });
467
+ fs.mkdirSync(wantsDir, { recursive: true });
468
+ const slug = projectSlug(rootDir);
469
+ const target = path.join(userSystemdDir, `holistic-${slug}.service`);
470
+ const runScript = path.join(systemDir(paths), "run-daemon.sh");
471
+ const service = `[Unit]
472
+ Description=Holistic daemon for ${slug}
473
+ After=default.target
474
+
475
+ [Service]
476
+ Type=simple
477
+ WorkingDirectory=${rootDir}
478
+ ExecStart=/bin/sh ${runScript}
479
+ Restart=always
480
+ RestartSec=15
481
+
482
+ [Install]
483
+ WantedBy=default.target
484
+ `;
485
+ fs.writeFileSync(target, service, "utf8");
486
+ const linkTarget = path.join(wantsDir, path.basename(target));
487
+ try {
488
+ if (fs.existsSync(linkTarget)) {
489
+ fs.unlinkSync(linkTarget);
490
+ }
491
+ fs.symlinkSync(target, linkTarget);
492
+ }
493
+ catch {
494
+ fs.copyFileSync(target, linkTarget);
495
+ }
496
+ return target;
497
+ }
498
+ function installDaemon(rootDir, paths, platform, homeDir) {
499
+ switch (platform) {
500
+ case "win32":
501
+ return installWindowsStartup(rootDir, paths, homeDir);
502
+ case "darwin":
503
+ return installMacosLaunchAgent(rootDir, paths, homeDir);
504
+ case "linux":
505
+ return installLinuxUserService(rootDir, paths, homeDir);
506
+ default:
507
+ return null;
508
+ }
509
+ }
510
+ function readJsonObject(filePath) {
511
+ if (!fs.existsSync(filePath)) {
512
+ return {};
513
+ }
514
+ try {
515
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
516
+ return parsed && typeof parsed === "object" ? parsed : {};
517
+ }
518
+ catch {
519
+ return {};
520
+ }
521
+ }
522
+ function writeClaudeDesktopMcpConfig(rootDir, platform, homeDir) {
523
+ const configPath = mcpConfigFile(platform, homeDir);
524
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
525
+ const runtime = runtimeEntryScript("cli");
526
+ const existing = readJsonObject(configPath);
527
+ const existingServers = existing.mcpServers && typeof existing.mcpServers === "object"
528
+ ? existing.mcpServers
529
+ : {};
530
+ const next = {
531
+ ...existing,
532
+ mcpServers: {
533
+ ...existingServers,
534
+ holistic: {
535
+ command: process.execPath,
536
+ args: [
537
+ ...(runtime.useStripTypes ? ["--experimental-strip-types"] : []),
538
+ runtime.scriptPath,
539
+ "serve",
540
+ ],
541
+ env: {
542
+ HOLISTIC_REPO: rootDir,
543
+ },
544
+ },
545
+ },
546
+ };
547
+ fs.writeFileSync(configPath, JSON.stringify(next, null, 2) + "\n", "utf8");
548
+ return configPath;
549
+ }
550
+ function buildHookCommand(rootDir, paths) {
551
+ const cliRuntime = runtimeEntryScript("cli");
552
+ return {
553
+ nodePath: process.execPath,
554
+ scriptPath: cliRuntime.scriptPath,
555
+ useStripTypes: cliRuntime.useStripTypes,
556
+ stateFilePath: path.relative(rootDir, paths.stateFile).replaceAll("\\", "/"),
557
+ syncPowerShellPath: path.relative(rootDir, path.join(systemDir(paths), "sync-state.ps1")).replaceAll("\\", "/"),
558
+ syncShellPath: path.relative(rootDir, path.join(systemDir(paths), "sync-state.sh")).replaceAll("\\", "/"),
559
+ };
560
+ }
561
+ function verifyBootstrapSetup(rootDir, result, platform, homeDir, configureMcp) {
562
+ const checks = [];
563
+ for (const hook of result.gitHooks) {
564
+ const hookPath = path.join(rootDir, ".git", "hooks", hook);
565
+ if (!fs.existsSync(hookPath)) {
566
+ throw new Error(`Expected git hook was not installed: ${hook}`);
567
+ }
568
+ }
569
+ checks.push("git-hooks");
570
+ let configuredMcpPath = null;
571
+ if (configureMcp) {
572
+ configuredMcpPath = mcpConfigFile(platform, homeDir);
573
+ const config = readJsonObject(configuredMcpPath);
574
+ const mcpServers = config.mcpServers && typeof config.mcpServers === "object"
575
+ ? config.mcpServers
576
+ : {};
577
+ if (!("holistic" in mcpServers)) {
578
+ throw new Error("Expected Claude Desktop MCP configuration for Holistic.");
579
+ }
580
+ checks.push("mcp-config");
581
+ }
582
+ if (result.installed) {
583
+ if (!result.startupTarget || !fs.existsSync(result.startupTarget)) {
584
+ throw new Error("Expected daemon startup target to exist after bootstrap.");
585
+ }
586
+ checks.push("daemon");
587
+ }
588
+ return {
589
+ checks,
590
+ mcpConfigFile: configuredMcpPath,
591
+ };
592
+ }
593
+ export function refreshHolisticHooks(rootDir) {
594
+ const paths = getRuntimePaths(rootDir);
595
+ return refreshGitHooks(rootDir, resolveGitDir(rootDir), buildHookCommand(rootDir, paths));
596
+ }
597
+ export function initializeHolistic(rootDir, options = {}) {
598
+ const { state, paths } = loadState(rootDir);
599
+ persist(rootDir, state, paths);
600
+ writeManagedGitAttributes(rootDir, paths);
601
+ const intervalSeconds = options.intervalSeconds ?? 30;
602
+ const remote = options.remote ?? "origin";
603
+ const syncTarget = resolveSyncTarget(options);
604
+ writeConfig(paths, remote, syncTarget, intervalSeconds);
605
+ writeSystemArtifacts(rootDir, paths, intervalSeconds, remote, syncTarget);
606
+ const platform = options.platform ?? process.platform;
607
+ const homeDir = options.homeDir ?? os.homedir();
608
+ let startupTarget = null;
609
+ let gitHooksInstalled = false;
610
+ let gitHooks = [];
611
+ let gitHookWarnings = [];
612
+ if (options.installDaemon) {
613
+ startupTarget = installDaemon(rootDir, paths, platform, homeDir);
614
+ }
615
+ if (options.installGitHooks) {
616
+ const hookResult = installGitHooks(rootDir, resolveGitDir(rootDir), buildHookCommand(rootDir, paths));
617
+ gitHooksInstalled = hookResult.installed;
618
+ gitHooks = hookResult.hooks;
619
+ gitHookWarnings = hookResult.warnings;
620
+ }
621
+ return {
622
+ installed: Boolean(startupTarget),
623
+ gitHooksInstalled,
624
+ gitHooks,
625
+ gitHookWarnings,
626
+ platform,
627
+ startupTarget,
628
+ systemDir: systemDir(paths),
629
+ configFile: configFile(paths),
630
+ };
631
+ }
632
+ export function bootstrapHolistic(rootDir, options = {}) {
633
+ const platform = options.platform ?? process.platform;
634
+ const homeDir = options.homeDir ?? os.homedir();
635
+ const configureMcp = options.configureMcp !== false;
636
+ const initResult = initializeHolistic(rootDir, {
637
+ ...options,
638
+ platform,
639
+ homeDir,
640
+ installDaemon: options.installDaemon !== false,
641
+ installGitHooks: options.installGitHooks !== false,
642
+ });
643
+ const configuredMcpPath = configureMcp
644
+ ? writeClaudeDesktopMcpConfig(rootDir, platform, homeDir)
645
+ : null;
646
+ const verification = verifyBootstrapSetup(rootDir, initResult, platform, homeDir, configureMcp);
647
+ return {
648
+ ...initResult,
649
+ mcpConfigured: configureMcp,
650
+ mcpConfigFile: configuredMcpPath ?? verification.mcpConfigFile,
651
+ checks: verification.checks,
652
+ };
653
+ }
654
+ //# sourceMappingURL=setup.js.map