@amoree-code/ocean 0.0.0-stage → 1.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 (300) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +284 -2
  3. package/dist/application/auth/auth-orchestrator.js +77 -0
  4. package/dist/application/clients/clients.js +646 -0
  5. package/dist/application/clients/editors.js +398 -0
  6. package/dist/application/clients/hooks.js +335 -0
  7. package/dist/application/clients/native.js +410 -0
  8. package/dist/application/clients/opencode-plugin.js +158 -0
  9. package/dist/application/clients/registry.js +188 -0
  10. package/dist/application/gateway/webhook-gateway.js +137 -0
  11. package/dist/application/install/provider-installer.js +184 -0
  12. package/dist/application/integrations/charter-copies.js +76 -0
  13. package/dist/application/integrations/claude-vscode.js +106 -0
  14. package/dist/application/integrations/engine-entry-repair.js +105 -0
  15. package/dist/application/integrations/skill-filter.js +254 -0
  16. package/dist/application/layout/layout-apply.js +443 -0
  17. package/dist/application/layout/layout-plan.js +236 -0
  18. package/dist/application/layout/workspace-template.js +34 -0
  19. package/dist/application/onboarding/state.js +23 -0
  20. package/dist/application/onboarding/steps.js +249 -0
  21. package/dist/application/profiles/profile-distribution.js +53 -0
  22. package/dist/application/profiles/profile-suggestion.js +39 -0
  23. package/dist/application/profiles/profile-upgrade-service.js +208 -0
  24. package/dist/application/pull/history-index.js +476 -0
  25. package/dist/application/pull/project-match.js +132 -0
  26. package/dist/application/pull/pull.js +202 -0
  27. package/dist/application/pull/server-capture.js +307 -0
  28. package/dist/application/scheduler/local-scheduler.js +210 -0
  29. package/dist/composition/runtime.js +64 -0
  30. package/dist/engine-root.js +9 -0
  31. package/dist/infrastructure/filesystem/profile-loader.js +46 -0
  32. package/dist/infrastructure/filesystem/runtime-config-loader.js +29 -0
  33. package/dist/infrastructure/filesystem/skill-loader.js +84 -0
  34. package/dist/infrastructure/filesystem/workspace-capability.js +37 -0
  35. package/dist/infrastructure/observability/runtime-logger.js +16 -0
  36. package/dist/infrastructure/process/agent-tool-executor.js +99 -0
  37. package/dist/infrastructure/process/cli-process.js +49 -0
  38. package/dist/infrastructure/process/interactive-process.js +145 -0
  39. package/dist/infrastructure/process/process-supervisor.js +194 -0
  40. package/dist/infrastructure/process/service.js +252 -0
  41. package/dist/infrastructure/providers/browser-provider.js +2 -0
  42. package/dist/infrastructure/providers/claude-sdk.js +137 -0
  43. package/dist/infrastructure/providers/client-home.js +14 -0
  44. package/dist/infrastructure/providers/fake-browser-provider.js +146 -0
  45. package/dist/infrastructure/providers/ollama-embedder.js +74 -0
  46. package/dist/infrastructure/providers/openai-compatible.js +268 -0
  47. package/dist/infrastructure/providers/playwright-browser-provider.js +380 -0
  48. package/dist/infrastructure/providers/provider-capabilities.js +50 -0
  49. package/dist/infrastructure/providers/provider-invocation.js +184 -0
  50. package/dist/infrastructure/providers/provider-registry.js +77 -0
  51. package/dist/infrastructure/providers/providers.js +134 -0
  52. package/dist/infrastructure/wrappers/wrapper-manager.js +374 -0
  53. package/dist/interfaces/cli/auth-command.js +54 -0
  54. package/dist/interfaces/cli/browser-bridge-command.js +93 -0
  55. package/dist/interfaces/cli/browser-command.js +119 -0
  56. package/dist/interfaces/cli/capture-command.js +125 -0
  57. package/dist/interfaces/cli/client-test-command.js +133 -0
  58. package/dist/interfaces/cli/clients-command.js +129 -0
  59. package/dist/interfaces/cli/command-registry.js +1211 -0
  60. package/dist/interfaces/cli/context-command.js +172 -0
  61. package/dist/interfaces/cli/daily-command.js +131 -0
  62. package/dist/interfaces/cli/governance-command.js +55 -0
  63. package/dist/interfaces/cli/handoff-command.js +35 -0
  64. package/dist/interfaces/cli/help-command.js +207 -0
  65. package/dist/interfaces/cli/idea-command.js +47 -0
  66. package/dist/interfaces/cli/intercept-command.js +317 -0
  67. package/dist/interfaces/cli/layout-command.js +27 -0
  68. package/dist/interfaces/cli/market-command.js +52 -0
  69. package/dist/interfaces/cli/mcp-gateway-command.js +30 -0
  70. package/dist/interfaces/cli/memory-command.js +317 -0
  71. package/dist/interfaces/cli/migrate-command.js +26 -0
  72. package/dist/interfaces/cli/observe-command.js +78 -0
  73. package/dist/interfaces/cli/onboard-command.js +123 -0
  74. package/dist/interfaces/cli/operate-command.js +59 -0
  75. package/dist/interfaces/cli/profiles-command.js +70 -0
  76. package/dist/interfaces/cli/pull-command.js +54 -0
  77. package/dist/interfaces/cli/setup-command.js +143 -0
  78. package/dist/interfaces/cli/tasks-command.js +249 -0
  79. package/dist/main.js +49 -0
  80. package/dist/node-warnings.js +17 -0
  81. package/dist/version.js +8 -0
  82. package/market/mcp/chrome-devtools/ocean.capability.json +28 -0
  83. package/market/mcp/gateway/ocean.capability.json +21 -0
  84. package/market/mcp/obsidian/ocean.capability.json +21 -0
  85. package/market/mcp/ocean/ocean.capability.json +21 -0
  86. package/market/mcp/playwright/ocean.capability.json +23 -0
  87. package/node_modules/@ocean/brain/dist/brain/brain-eval.js +29 -0
  88. package/node_modules/@ocean/brain/dist/brain/brain-markdown.js +226 -0
  89. package/node_modules/@ocean/brain/dist/brain/brain-reindex.js +247 -0
  90. package/node_modules/@ocean/brain/dist/brain/brain-service.js +320 -0
  91. package/node_modules/@ocean/brain/dist/capture/inbox-sync.js +22 -0
  92. package/node_modules/@ocean/brain/dist/evidence/verification-report.js +13 -0
  93. package/node_modules/@ocean/brain/dist/infrastructure/brain/brain-index.js +285 -0
  94. package/node_modules/@ocean/brain/dist/memory/brain-dump.js +145 -0
  95. package/node_modules/@ocean/brain/dist/memory/daily-narrative.js +142 -0
  96. package/node_modules/@ocean/brain/dist/memory/index-sync.js +124 -0
  97. package/node_modules/@ocean/brain/dist/memory/lesson-activation.js +93 -0
  98. package/node_modules/@ocean/brain/dist/memory/lesson-learning.js +188 -0
  99. package/node_modules/@ocean/brain/dist/memory/lessons.js +321 -0
  100. package/node_modules/@ocean/brain/dist/memory/memory-audit.js +100 -0
  101. package/node_modules/@ocean/brain/dist/memory/memory-digest.js +49 -0
  102. package/node_modules/@ocean/brain/dist/memory/memory-harvest.js +345 -0
  103. package/node_modules/@ocean/brain/dist/memory/memory-paths.js +174 -0
  104. package/node_modules/@ocean/brain/dist/memory/memory-review.js +278 -0
  105. package/node_modules/@ocean/brain/dist/memory/memory-trash.js +152 -0
  106. package/node_modules/@ocean/brain/dist/memory/memory-write.js +216 -0
  107. package/node_modules/@ocean/brain/dist/memory/model-narrative.js +129 -0
  108. package/node_modules/@ocean/brain/dist/memory/profile-facts.js +103 -0
  109. package/node_modules/@ocean/brain/dist/memory/session-promotion.js +61 -0
  110. package/node_modules/@ocean/brain/dist/memory/session-summary.js +113 -0
  111. package/node_modules/@ocean/brain/package.json +11 -0
  112. package/node_modules/@ocean/context/dist/context/context-compression.js +66 -0
  113. package/node_modules/@ocean/context/dist/context/context-cost.js +587 -0
  114. package/node_modules/@ocean/context/dist/context/context-ladder.js +363 -0
  115. package/node_modules/@ocean/context/dist/context/context-packet.js +213 -0
  116. package/node_modules/@ocean/context/dist/context/context-references.js +255 -0
  117. package/node_modules/@ocean/context/dist/context/intent-router.js +259 -0
  118. package/node_modules/@ocean/context/dist/context/resource-injection.js +36 -0
  119. package/node_modules/@ocean/context/package.json +11 -0
  120. package/node_modules/@ocean/core/dist/domain/brain/brain-record.js +22 -0
  121. package/node_modules/@ocean/core/dist/domain/brain/record-scope.js +22 -0
  122. package/node_modules/@ocean/core/dist/domain/capabilities/browser-contract.js +64 -0
  123. package/node_modules/@ocean/core/dist/domain/capabilities/capability-contract.js +16 -0
  124. package/node_modules/@ocean/core/dist/domain/capabilities/capability-manifest.js +72 -0
  125. package/node_modules/@ocean/core/dist/domain/context/context-validator.js +5 -0
  126. package/node_modules/@ocean/core/dist/domain/context/context.js +34 -0
  127. package/node_modules/@ocean/core/dist/domain/context/project-id.js +30 -0
  128. package/node_modules/@ocean/core/dist/domain/evidence/evidence.js +16 -0
  129. package/node_modules/@ocean/core/dist/domain/mcp/mcp-contract.js +36 -0
  130. package/node_modules/@ocean/core/dist/domain/mcp/memory-contract.js +41 -0
  131. package/node_modules/@ocean/core/dist/domain/ports/brain-index-port.js +7 -0
  132. package/node_modules/@ocean/core/dist/domain/ports/browser-port.js +8 -0
  133. package/node_modules/@ocean/core/dist/domain/ports/embedder-port.js +2 -0
  134. package/node_modules/@ocean/core/dist/domain/ports/platform-ports.js +2 -0
  135. package/node_modules/@ocean/core/dist/domain/ports/runtime-ports.js +2 -0
  136. package/node_modules/@ocean/core/dist/domain/ports/session-store-port.js +2 -0
  137. package/node_modules/@ocean/core/dist/domain/process/process-events.js +5 -0
  138. package/node_modules/@ocean/core/dist/domain/profiles/profile-distribution.js +19 -0
  139. package/node_modules/@ocean/core/dist/domain/profiles/profile-policy.js +34 -0
  140. package/node_modules/@ocean/core/dist/domain/profiles/profile-selection.js +73 -0
  141. package/node_modules/@ocean/core/dist/domain/profiles/profile-upgrade.js +123 -0
  142. package/node_modules/@ocean/core/dist/domain/profiles/profile-validator.js +5 -0
  143. package/node_modules/@ocean/core/dist/domain/profiles/profile.js +218 -0
  144. package/node_modules/@ocean/core/dist/domain/profiles/runtime-config.js +30 -0
  145. package/node_modules/@ocean/core/dist/domain/providers/provider-capability.js +28 -0
  146. package/node_modules/@ocean/core/dist/domain/providers/provider.js +2 -0
  147. package/node_modules/@ocean/core/dist/domain/redaction/redaction.js +86 -0
  148. package/node_modules/@ocean/core/dist/domain/runs/agent-tools.js +92 -0
  149. package/node_modules/@ocean/core/dist/domain/runs/execution-plan.js +71 -0
  150. package/node_modules/@ocean/core/dist/domain/runs/run-contract.js +38 -0
  151. package/node_modules/@ocean/core/dist/domain/sessions/entry-contract.js +31 -0
  152. package/node_modules/@ocean/core/dist/domain/sessions/handoff.js +78 -0
  153. package/node_modules/@ocean/core/dist/domain/sessions/session.js +58 -0
  154. package/node_modules/@ocean/core/dist/domain/skills/default-skills.js +29 -0
  155. package/node_modules/@ocean/core/dist/domain/skills/skill-validator.js +5 -0
  156. package/node_modules/@ocean/core/dist/domain/skills/skill.js +11 -0
  157. package/node_modules/@ocean/core/dist/domain/tasks/task-id.js +85 -0
  158. package/node_modules/@ocean/core/dist/shared/fs-utils.js +212 -0
  159. package/node_modules/@ocean/core/dist/shared/local-date.js +16 -0
  160. package/node_modules/@ocean/core/dist/shared/paths.js +324 -0
  161. package/node_modules/@ocean/core/dist/shared/project-resolution.js +1065 -0
  162. package/node_modules/@ocean/core/dist/shared/task-reference.js +133 -0
  163. package/node_modules/@ocean/core/package.json +14 -0
  164. package/node_modules/@ocean/loop/dist/handoff/handoff-service.js +133 -0
  165. package/node_modules/@ocean/loop/dist/hooks/lifecycle-hooks.js +15 -0
  166. package/node_modules/@ocean/loop/dist/hooks/session-end-hook.js +183 -0
  167. package/node_modules/@ocean/loop/dist/hooks/session-start-hook.js +237 -0
  168. package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-store.js +690 -0
  169. package/node_modules/@ocean/loop/dist/infrastructure/persistence/session-validator.js +5 -0
  170. package/node_modules/@ocean/loop/dist/loops/iteration-graph.js +147 -0
  171. package/node_modules/@ocean/loop/dist/loops/task-loop-record.js +2 -0
  172. package/node_modules/@ocean/loop/dist/loops/task-loop.js +281 -0
  173. package/node_modules/@ocean/loop/dist/operations/memory-knowledge-operations.js +263 -0
  174. package/node_modules/@ocean/loop/dist/operations/operation-contract.js +227 -0
  175. package/node_modules/@ocean/loop/dist/operations/operation-scope.js +28 -0
  176. package/node_modules/@ocean/loop/dist/operations/project-operations.js +93 -0
  177. package/node_modules/@ocean/loop/dist/operations/record-io.js +65 -0
  178. package/node_modules/@ocean/loop/dist/operations/record-operations.js +45 -0
  179. package/node_modules/@ocean/loop/dist/operations/task-linker.js +40 -0
  180. package/node_modules/@ocean/loop/dist/operations/task-operations.js +279 -0
  181. package/node_modules/@ocean/loop/dist/operations/write-guard.js +157 -0
  182. package/node_modules/@ocean/loop/dist/runs/prompt-assembly.js +112 -0
  183. package/node_modules/@ocean/loop/dist/runs/run-agent.js +425 -0
  184. package/node_modules/@ocean/loop/dist/runs/run-authorization.js +24 -0
  185. package/node_modules/@ocean/loop/dist/runs/tool-loop.js +173 -0
  186. package/node_modules/@ocean/loop/dist/sessions/session-closeout.js +150 -0
  187. package/node_modules/@ocean/loop/dist/sessions/session-pointer.js +133 -0
  188. package/node_modules/@ocean/loop/dist/sessions/session-projects.js +26 -0
  189. package/node_modules/@ocean/loop/dist/sessions/session-retention.js +132 -0
  190. package/node_modules/@ocean/loop/dist/tasks/archive-tasks.js +210 -0
  191. package/node_modules/@ocean/loop/dist/tasks/checkpoint-task.js +31 -0
  192. package/node_modules/@ocean/loop/dist/tasks/claim-task.js +126 -0
  193. package/node_modules/@ocean/loop/dist/tasks/list-tasks.js +113 -0
  194. package/node_modules/@ocean/loop/dist/tasks/new-task.js +42 -0
  195. package/node_modules/@ocean/loop/dist/tasks/renumber-tasks.js +134 -0
  196. package/node_modules/@ocean/loop/dist/tasks/verification-readiness.js +78 -0
  197. package/node_modules/@ocean/loop/dist/tasks/verify-task.js +234 -0
  198. package/node_modules/@ocean/loop/package.json +11 -0
  199. package/node_modules/@ocean/market/dist/adapters/mcp-adapter.js +64 -0
  200. package/node_modules/@ocean/market/dist/adapters/owned-directory.js +89 -0
  201. package/node_modules/@ocean/market/dist/adapters/plugin-adapter.js +39 -0
  202. package/node_modules/@ocean/market/dist/adapters/skill-adapter.js +32 -0
  203. package/node_modules/@ocean/market/dist/capability/adapter.js +2 -0
  204. package/node_modules/@ocean/market/dist/capability/catalog.js +63 -0
  205. package/node_modules/@ocean/market/dist/capability/content-digest.js +40 -0
  206. package/node_modules/@ocean/market/dist/capability/registry.js +81 -0
  207. package/node_modules/@ocean/market/dist/install/default-marketplace.js +47 -0
  208. package/node_modules/@ocean/market/dist/install/lock.js +34 -0
  209. package/node_modules/@ocean/market/dist/install/marketplace-service.js +165 -0
  210. package/node_modules/@ocean/market/dist/mcp/browser-bridge.js +263 -0
  211. package/node_modules/@ocean/market/dist/mcp/mcp-connection.js +67 -0
  212. package/node_modules/@ocean/market/dist/mcp/mcp-gateway.js +317 -0
  213. package/node_modules/@ocean/market/dist/mcp/mcp-launch.js +102 -0
  214. package/node_modules/@ocean/market/dist/mcp/mcp-runner.js +32 -0
  215. package/node_modules/@ocean/market/dist/security/description-scan.js +45 -0
  216. package/node_modules/@ocean/market/dist/security/permissions.js +57 -0
  217. package/node_modules/@ocean/market/dist/skills/core-skill-sync.js +113 -0
  218. package/node_modules/@ocean/market/dist/skills/signal-detection.js +84 -0
  219. package/node_modules/@ocean/market/dist/skills/skill-hub.js +220 -0
  220. package/node_modules/@ocean/market/package.json +11 -0
  221. package/node_modules/@ocean/services/dist/browser/browser-service.js +124 -0
  222. package/node_modules/@ocean/services/dist/browser/browser-session.js +287 -0
  223. package/node_modules/@ocean/services/dist/browser/browser-task-runner.js +70 -0
  224. package/node_modules/@ocean/services/dist/doctor/workspace-doctor.js +547 -0
  225. package/node_modules/@ocean/services/dist/infrastructure/mcp/call-ledger.js +114 -0
  226. package/node_modules/@ocean/services/dist/infrastructure/mcp/mcp-client.js +162 -0
  227. package/node_modules/@ocean/services/dist/infrastructure/mcp/obsidian-server.js +251 -0
  228. package/node_modules/@ocean/services/dist/infrastructure/mcp/ocean-server.js +880 -0
  229. package/node_modules/@ocean/services/dist/infrastructure/mcp/server-version.js +20 -0
  230. package/node_modules/@ocean/services/dist/infrastructure/mcp/tool-registry.js +29 -0
  231. package/node_modules/@ocean/services/dist/obsidian/conflict-log.js +68 -0
  232. package/node_modules/@ocean/services/dist/obsidian/inbox-promotion.js +80 -0
  233. package/node_modules/@ocean/services/dist/obsidian/vault-discovery.js +113 -0
  234. package/node_modules/@ocean/services/dist/obsidian/vault-ingestion.js +77 -0
  235. package/node_modules/@ocean/services/dist/obsidian/vault-sync.js +73 -0
  236. package/node_modules/@ocean/services/dist/obsidian/vault-writer.js +75 -0
  237. package/node_modules/@ocean/services/package.json +11 -0
  238. package/ocean-marketplace.json +415 -0
  239. package/package.json +67 -4
  240. package/skills/core/business-logic/SKILL.md +22 -0
  241. package/skills/core/catch-up/SKILL.md +17 -0
  242. package/skills/core/core-thinking/SKILL.md +11 -0
  243. package/skills/core/design-thinking/SKILL.md +19 -0
  244. package/skills/core/graft/SKILL.md +156 -0
  245. package/skills/core/loop/SKILL.md +20 -0
  246. package/skills/core/session-handoff/SKILL.md +15 -0
  247. package/skills/core/use-browser/SKILL.md +69 -0
  248. package/skills/core/verification/SKILL.md +11 -0
  249. package/skills/index.json +56 -0
  250. package/templates/README.md +4 -0
  251. package/templates/profiles/default.json +51 -0
  252. package/templates/profiles/developer.json +51 -0
  253. package/templates/profiles/reviewer.json +51 -0
  254. package/templates/profiles/strategist.json +51 -0
  255. package/templates/workspace/00-inbox/INBOX.md +7 -0
  256. package/templates/workspace/00-inbox/candidates/.gitkeep +0 -0
  257. package/templates/workspace/00-inbox/native/.gitkeep +0 -0
  258. package/templates/workspace/01-daily/.gitkeep +0 -0
  259. package/templates/workspace/02-personal/CONFLICTS.md +7 -0
  260. package/templates/workspace/02-personal/MEMORY.md +15 -0
  261. package/templates/workspace/02-personal/goals/.gitkeep +0 -0
  262. package/templates/workspace/02-personal/identity/.gitkeep +0 -0
  263. package/templates/workspace/02-personal/preferences/.gitkeep +0 -0
  264. package/templates/workspace/03-professional/.gitkeep +0 -0
  265. package/templates/workspace/04-projects/PROJECTS.md +5 -0
  266. package/templates/workspace/04-projects/_template/docs/.gitkeep +0 -0
  267. package/templates/workspace/04-projects/_template/memory/.gitkeep +0 -0
  268. package/templates/workspace/04-projects/_template/tasks/.gitkeep +0 -0
  269. package/templates/workspace/05-knowledge/KNOWLEDGE.md +8 -0
  270. package/templates/workspace/05-knowledge/decisions/.gitkeep +0 -0
  271. package/templates/workspace/05-knowledge/lessons/.gitkeep +0 -0
  272. package/templates/workspace/06-templates/TEMPLATES.md +25 -0
  273. package/templates/workspace/06-templates/decision.md +8 -0
  274. package/templates/workspace/06-templates/intelligence.md +32 -0
  275. package/templates/workspace/06-templates/project-claude.md +48 -0
  276. package/templates/workspace/06-templates/session.md +38 -0
  277. package/templates/workspace/06-templates/task.md +42 -0
  278. package/templates/workspace/MANIFEST.json +51 -0
  279. package/templates/workspace/bridge/config/CONFIG.md +21 -0
  280. package/templates/workspace/bridge/config/runtime.json +4 -0
  281. package/templates/workspace/charter/core.md +104 -0
  282. package/templates/workspace/charter/policies/README.md +31 -0
  283. package/templates/workspace/charter/policies/claude-code-context.md +90 -0
  284. package/templates/workspace/charter/policies/concise-output.md +29 -0
  285. package/templates/workspace/charter/policies/context.md +91 -0
  286. package/templates/workspace/charter/policies/contracts/task.md +189 -0
  287. package/templates/workspace/charter/policies/git.md +60 -0
  288. package/templates/workspace/charter/policies/graph.md +47 -0
  289. package/templates/workspace/charter/policies/knowledge.md +33 -0
  290. package/templates/workspace/charter/policies/lifecycle.md +119 -0
  291. package/templates/workspace/charter/policies/memory.md +67 -0
  292. package/templates/workspace/charter/policies/minimal-change.md +43 -0
  293. package/templates/workspace/charter/policies/models.md +57 -0
  294. package/templates/workspace/charter/policies/privacy-terms.txt +3 -0
  295. package/templates/workspace/charter/policies/response.md +39 -0
  296. package/templates/workspace/charter/policies/skill-observer.md +44 -0
  297. package/templates/workspace/charter/policies/stack.md +23 -0
  298. package/templates/workspace/charter/policies/task.md +139 -0
  299. package/templates/workspace/charter/policies/verification.md +29 -0
  300. package/templates/workspace/sessions/.gitkeep +0 -0
@@ -0,0 +1,443 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { chmod, copyFile, cp, lstat, mkdir, readdir, readFile, readlink, realpath, rename, rm, stat, symlink, writeFile, } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { oldPathPattern, planLayout, } from "./layout-plan.js";
6
+ // T-243 phase 6: the writing half of `ocean layout`. It copies — never moves — each old tree to
7
+ // its flat location through a staging folder, repoints the files the plan found, and keeps a
8
+ // journal and byte-for-byte backups so `rollback` can put every pointer back. The old trees are
9
+ // never written or deleted here; removing them is the owner's step once the new layout is proven.
10
+ const STAGING = ".layout-staging";
11
+ const BACKUPS = ".ocean-layout-backups";
12
+ // The records' sentinel arrives last, so a reader never takes a half-copied flat tree as current.
13
+ const LAST = "04-projects";
14
+ async function exists(target) {
15
+ return lstat(target).then(() => true, () => false);
16
+ }
17
+ // Every entry under a tree: files by sha256, symlinks by their target, directories as such.
18
+ async function manifest(target) {
19
+ const entries = {};
20
+ const walk = async (current, relative) => {
21
+ const info = await lstat(current);
22
+ if (info.isSymbolicLink())
23
+ entries[relative] = `link:${await readlink(current)}`;
24
+ else if (info.isDirectory()) {
25
+ entries[relative] = "dir";
26
+ for (const name of (await readdir(current)).sort())
27
+ await walk(path.join(current, name), path.posix.join(relative, name));
28
+ }
29
+ else
30
+ entries[relative] = `sha256:${createHash("sha256")
31
+ .update(await readFile(current))
32
+ .digest("hex")}`;
33
+ };
34
+ await walk(target, ".");
35
+ return entries;
36
+ }
37
+ function differences(expected, actual) {
38
+ const keys = new Set([...Object.keys(expected), ...Object.keys(actual)]);
39
+ return [...keys].filter((key) => expected[key] !== actual[key]).sort();
40
+ }
41
+ const sha256 = (content) => createHash("sha256").update(content).digest("hex");
42
+ // Never follows anything already sitting at the temporary name.
43
+ const temporaryName = (target) => `${target}.ocean-layout-${randomBytes(6).toString("hex")}`;
44
+ async function writeAtomic(file, content) {
45
+ // Write through a symlinked dotfile to the file it names, so the link itself survives.
46
+ const target = await realpath(file).catch(() => file);
47
+ const mode = (await stat(target)).mode & 0o7777;
48
+ const temporary = temporaryName(target);
49
+ try {
50
+ await writeFile(temporary, content, { mode, flag: "wx" });
51
+ await chmod(temporary, mode);
52
+ await rename(temporary, target);
53
+ }
54
+ catch (error) {
55
+ await rm(temporary, { force: true });
56
+ throw error;
57
+ }
58
+ }
59
+ async function replaceLink(link, target) {
60
+ const temporary = temporaryName(link);
61
+ try {
62
+ await symlink(target, temporary);
63
+ await rename(temporary, link);
64
+ }
65
+ catch (error) {
66
+ await rm(temporary, { force: true });
67
+ throw error;
68
+ }
69
+ }
70
+ // What a pointer currently is, in the form its journal entry records.
71
+ async function pointerState(pointer) {
72
+ return pointer.kind === "symlink"
73
+ ? readlink(pointer.target).catch(() => "")
74
+ : readFile(pointer.target).then(sha256, () => "");
75
+ }
76
+ // `<p>/brain/x` → `<p>/x` and `<p>/kernel/bridge/x` → `<p>/bridge/x`, by the plan's own pattern.
77
+ export function repoint(text, root, home, areas) {
78
+ return text.replace(oldPathPattern(root, home, areas), (_, prefix, area) => area === "brain" ? prefix : `${prefix}/bridge`);
79
+ }
80
+ // The old areas a set of moves empties, i.e. the only ones whose paths go stale.
81
+ const movedAreas = (moves) => [
82
+ ...(moves.some(({ from }) => from.startsWith("brain/"))
83
+ ? ["brain"]
84
+ : []),
85
+ ...(moves.some(({ from }) => from === "kernel/bridge")
86
+ ? ["kernel/bridge"]
87
+ : []),
88
+ ];
89
+ // A pointer inside an old tree is rewritten in its new copy; the old tree is left untouched.
90
+ function newLocation(file, root, moves) {
91
+ for (const { from, to } of moves) {
92
+ const old = path.join(root, from);
93
+ if (file === old || file.startsWith(`${old}${path.sep}`))
94
+ return path.join(root, to, path.relative(old, file));
95
+ }
96
+ return file;
97
+ }
98
+ function applyGitignore(source, plan) {
99
+ const lines = source.split(/\r?\n/);
100
+ const newline = source.includes("\r\n") ? "\r\n" : "\n";
101
+ for (const change of plan.gitignore) {
102
+ if (change.from === null)
103
+ continue;
104
+ const index = lines.indexOf(change.from);
105
+ if (index < 0)
106
+ continue;
107
+ if (change.to === null)
108
+ lines.splice(index, 1);
109
+ else
110
+ lines[index] = change.to;
111
+ }
112
+ const additions = plan.gitignore
113
+ .filter((change) => change.from === null && change.to !== null)
114
+ .map((change) => change.to);
115
+ if (additions.length) {
116
+ if (lines.at(-1) === "")
117
+ lines.pop();
118
+ lines.push(...additions, "");
119
+ }
120
+ return lines.join(newline);
121
+ }
122
+ async function writeJournal(backup, journal) {
123
+ const file = path.join(backup, "journal.json");
124
+ const temporary = temporaryName(file);
125
+ await writeFile(temporary, `${JSON.stringify(journal, null, 2)}\n`, {
126
+ mode: 0o600,
127
+ flag: "wx",
128
+ });
129
+ await rename(temporary, file);
130
+ }
131
+ // An empty HOME or a root that is not an Ocean workspace (e.g. a fallback to the default root when
132
+ // OCEAN_ROOT is empty) must never be migrated or rolled back.
133
+ function requireHome(home) {
134
+ if (!path.isAbsolute(home))
135
+ throw new Error(`refusing: HOME is not an absolute path (${JSON.stringify(home)}).`);
136
+ }
137
+ export async function applyLayout(root, home = os.homedir()) {
138
+ requireHome(home);
139
+ const staging = path.join(root, STAGING);
140
+ if (await exists(staging))
141
+ throw new Error(`${staging} exists: an earlier apply was interrupted. Run \`ocean layout rollback\` first.`);
142
+ const unfinished = await latestJournal(home, root);
143
+ if (unfinished && unfinished.journal.step !== "done")
144
+ throw new Error(`an earlier apply stopped at step "${unfinished.journal.step}" (${unfinished.backup}). Run \`ocean layout rollback\` first.`);
145
+ const plan = await planLayout(root, home);
146
+ if (!plan.workspace)
147
+ throw new Error(`refusing: ${root} holds no Ocean workspace (no 04-projects in either layout). Is OCEAN_ROOT set?`);
148
+ if (!plan.moves.length)
149
+ return { applied: false, reason: "nothing to move: already flat" };
150
+ if (!plan.ready)
151
+ throw new Error(`refusing to apply: ${plan.collisions.length} collision(s) — ${plan.collisions
152
+ .map(({ from, to }) => `${from} → ${to}`)
153
+ .join(", ")}. Resolve them by hand, then re-run \`ocean layout plan\`.`);
154
+ // 1. Snapshot every pointer before anything changes.
155
+ const startedAt = new Date().toISOString();
156
+ const backup = path.join(home, BACKUPS, startedAt.replace(/[:.]/g, "-"));
157
+ await mkdir(path.join(backup, "pointers"), { recursive: true, mode: 0o700 });
158
+ await chmod(path.join(home, BACKUPS), 0o700);
159
+ const moves = plan.moves.map(({ from, to }) => ({ from, to }));
160
+ const pointers = [];
161
+ const targets = new Set();
162
+ const realRoot = await realpath(root);
163
+ for (const [index, pointer] of plan.pointers.entries()) {
164
+ if (pointer.kind === "symlink") {
165
+ const link = await readlink(pointer.file);
166
+ const target = newLocation(pointer.file, root, moves);
167
+ pointers.push({
168
+ file: pointer.file,
169
+ kind: "symlink",
170
+ target,
171
+ inTree: target !== pointer.file,
172
+ backup: link,
173
+ original: link,
174
+ });
175
+ continue;
176
+ }
177
+ // Resolve first: two pointers can name one real file (a symlinked dotfile and its target),
178
+ // and a link can lead into an old tree or out of the root.
179
+ // Spelled under `root` as given (on macOS /var resolves to /private/var), so a file in an old
180
+ // tree is recognised as one.
181
+ const resolved = await realpath(pointer.file);
182
+ const real = resolved.startsWith(`${realRoot}${path.sep}`)
183
+ ? path.join(root, path.relative(realRoot, resolved))
184
+ : resolved;
185
+ const target = newLocation(real, root, moves);
186
+ if (targets.has(target))
187
+ continue;
188
+ targets.add(target);
189
+ const name = `${index}-${path.basename(pointer.file)}`;
190
+ const copy = path.join(backup, "pointers", name);
191
+ await copyFile(real, copy);
192
+ pointers.push({
193
+ file: pointer.file,
194
+ kind: "file",
195
+ target,
196
+ inTree: target !== real,
197
+ backup: name,
198
+ original: sha256(await readFile(copy)),
199
+ });
200
+ }
201
+ const gitignorePath = path.join(root, ".gitignore");
202
+ const hasGitignore = plan.gitignore.length > 0 && (await exists(gitignorePath));
203
+ let gitignoreOriginal;
204
+ if (hasGitignore) {
205
+ await copyFile(gitignorePath, path.join(backup, "gitignore"));
206
+ gitignoreOriginal = sha256(await readFile(path.join(backup, "gitignore")));
207
+ }
208
+ const journal = {
209
+ version: 1,
210
+ root,
211
+ home,
212
+ startedAt,
213
+ step: "snapshotted",
214
+ moves,
215
+ pointers,
216
+ gitignore: hasGitignore ? "gitignore" : null,
217
+ gitignoreOriginal,
218
+ manifests: {},
219
+ };
220
+ await writeJournal(backup, journal);
221
+ // 2. Copy into staging and prove the copy byte-for-byte before it becomes visible.
222
+ await mkdir(staging);
223
+ for (const { from, to } of moves) {
224
+ const source = path.join(root, from);
225
+ const staged = path.join(staging, to);
226
+ await cp(source, staged, {
227
+ recursive: true,
228
+ errorOnExist: true,
229
+ force: false,
230
+ preserveTimestamps: true,
231
+ verbatimSymlinks: true,
232
+ });
233
+ const changed = differences(await manifest(source), await manifest(staged));
234
+ if (changed.length)
235
+ throw new Error(`copy of ${from} does not match its source (${changed.slice(0, 5).join(", ")}${changed.length > 5 ? ", …" : ""}); was it written during the copy? Nothing is live yet — run \`ocean layout rollback\`.`);
236
+ }
237
+ // A write to an old tree after its copy was checked would be missing from the new layout:
238
+ // check every copy again, all at once, immediately before anything becomes visible.
239
+ for (const { from, to } of moves) {
240
+ const changed = differences(await manifest(path.join(root, from)), await manifest(path.join(staging, to)));
241
+ if (changed.length)
242
+ throw new Error(`${from} changed while the copy was being made (${changed.slice(0, 5).join(", ")}); stop every writer (runtime, client sessions) and run \`ocean layout rollback\`, then apply again.`);
243
+ }
244
+ journal.step = "copied";
245
+ await writeJournal(backup, journal);
246
+ // 3. Rename into place, the records' sentinel last.
247
+ const ordered = [...moves].sort((a, b) => Number(a.to === LAST) - Number(b.to === LAST));
248
+ for (const { to } of ordered)
249
+ await rename(path.join(staging, to), path.join(root, to));
250
+ await rm(staging, { recursive: true });
251
+ journal.step = "swapped";
252
+ await writeJournal(backup, journal);
253
+ // 4. Repoint: files outside the root in place, files inside an old tree in their new copy.
254
+ const areas = movedAreas(moves);
255
+ let repointed = 0;
256
+ const skipped = [];
257
+ for (const pointer of pointers) {
258
+ const { target } = pointer;
259
+ // Deleted since the snapshot: nothing left to repoint, and rollback leaves it deleted.
260
+ if (!(await exists(target))) {
261
+ skipped.push(pointer.file);
262
+ continue;
263
+ }
264
+ // Journal first: an interruption after this line leaves the pointer either still original or
265
+ // exactly applied, and rollback recognises both.
266
+ if (pointer.kind === "symlink") {
267
+ pointer.applied = repoint(pointer.backup, root, home, areas);
268
+ await writeJournal(backup, journal);
269
+ await replaceLink(target, pointer.applied);
270
+ }
271
+ else {
272
+ const current = await readFile(target);
273
+ // Edited since the snapshot (the copy can take a while): back up what is there now, so
274
+ // rollback restores that and not an older version.
275
+ if (!pointer.inTree && sha256(current) !== pointer.original) {
276
+ await writeFile(path.join(backup, "pointers", pointer.backup), current);
277
+ pointer.original = sha256(current);
278
+ }
279
+ const content = repoint(current.toString("utf8"), root, home, areas);
280
+ pointer.applied = sha256(content);
281
+ await writeJournal(backup, journal);
282
+ await writeAtomic(target, content);
283
+ }
284
+ repointed += 1;
285
+ }
286
+ if (hasGitignore) {
287
+ const content = applyGitignore(await readFile(gitignorePath, "utf8"), plan);
288
+ journal.gitignoreApplied = sha256(content);
289
+ await writeJournal(backup, journal);
290
+ await writeAtomic(gitignorePath, content);
291
+ }
292
+ for (const { to } of moves)
293
+ journal.manifests[to] = await manifest(path.join(root, to));
294
+ journal.step = "done";
295
+ await writeJournal(backup, journal);
296
+ return {
297
+ applied: true,
298
+ reason: "new layout in place; the old trees are untouched",
299
+ backup,
300
+ moved: moves.map(({ from, to }) => `${from} → ${to}`),
301
+ repointed,
302
+ skipped,
303
+ };
304
+ }
305
+ // Top-level names a record move can never target: the machinery and the repo's own files.
306
+ const RESERVED = new Set(["brain", "kernel", "bridge", ".git", ".gitignore"]);
307
+ // The journal decides what rollback deletes and writes, so only the moves apply can make and
308
+ // pointer entries that name a real backup are accepted.
309
+ async function validateJournal(journal, root, home) {
310
+ const fail = (why) => {
311
+ throw new Error(`refusing to roll back: the journal ${why}.`);
312
+ };
313
+ if (journal.version !== 1)
314
+ fail("has an unknown version");
315
+ if (journal.root !== root || journal.home !== home)
316
+ fail("belongs to another root or HOME");
317
+ const segment = /^(?!\.\.?$)[^/\\]+$/;
318
+ for (const { from, to } of journal.moves) {
319
+ const bridge = from === "kernel/bridge" && to === "bridge";
320
+ const record = segment.test(to) && from === `brain/${to}` && !RESERVED.has(to);
321
+ if (!bridge && !record)
322
+ fail(`names a move apply never makes (${from} → ${to})`);
323
+ }
324
+ const bases = [home, root];
325
+ for (const base of [home, root])
326
+ bases.push(await realpath(base).catch(() => base));
327
+ const ownedBy = (file) => path.isAbsolute(file) &&
328
+ bases.some((base) => file.startsWith(`${base}${path.sep}`));
329
+ for (const pointer of journal.pointers) {
330
+ if (!ownedBy(pointer.file) || !ownedBy(pointer.target))
331
+ fail(`names a pointer outside HOME and the root (${pointer.target})`);
332
+ if (pointer.kind === "file" && !segment.test(pointer.backup))
333
+ fail(`names a backup outside its folder (${pointer.backup})`);
334
+ }
335
+ if (journal.gitignore !== null && journal.gitignore !== "gitignore")
336
+ fail("names an unexpected .gitignore backup");
337
+ }
338
+ async function latestJournal(home, root) {
339
+ const folder = path.join(home, BACKUPS);
340
+ const names = (await readdir(folder).catch(() => [])).sort().reverse();
341
+ for (const name of names) {
342
+ const backup = path.join(folder, name);
343
+ let journal = null;
344
+ try {
345
+ journal = JSON.parse(await readFile(path.join(backup, "journal.json"), "utf8"));
346
+ }
347
+ catch {
348
+ continue;
349
+ }
350
+ if (journal?.root === root && journal.step !== "rolled-back")
351
+ return { backup, journal };
352
+ }
353
+ return null;
354
+ }
355
+ export async function rollbackLayout(root, home = os.homedir()) {
356
+ requireHome(home);
357
+ const found = await latestJournal(home, root);
358
+ const staging = path.join(root, STAGING);
359
+ if (!found) {
360
+ if (await exists(staging))
361
+ throw new Error(`${staging} exists but no journal under ${path.join(home, BACKUPS)} names this root; remove it by hand after checking it.`);
362
+ return { rolledBack: false, reason: "no apply to roll back" };
363
+ }
364
+ const { backup, journal } = found;
365
+ await validateJournal(journal, root, home);
366
+ const { moves } = journal;
367
+ const placed = [];
368
+ for (const move of moves)
369
+ if (await exists(path.join(root, move.to)))
370
+ placed.push(move);
371
+ // Check everything before touching anything: the old trees must still be there, and a new tree
372
+ // may only go if it is exactly what apply left — a write since then would otherwise be lost.
373
+ for (const { from } of moves)
374
+ if (!(await exists(path.join(root, from))))
375
+ throw new Error(`refusing to roll back: ${from} is gone, so the new copy is the only one left.`);
376
+ if (journal.step === "done") {
377
+ for (const { to } of placed) {
378
+ const changed = differences(journal.manifests[to] ?? {}, await manifest(path.join(root, to)));
379
+ if (changed.length)
380
+ throw new Error(`refusing to roll back: ${to} changed since apply (${changed.slice(0, 5).join(", ")}${changed.length > 5 ? ", …" : ""}). Move those changes to the old tree first.`);
381
+ }
382
+ }
383
+ else if (placed.length) {
384
+ // Interrupted mid-rename or mid-repoint: a placed tree still equals its source, except for
385
+ // the pointers apply rewrote inside it.
386
+ for (const { from, to } of placed) {
387
+ const tree = path.join(root, to);
388
+ const rewritten = new Set(journal.pointers
389
+ .filter((pointer) => pointer.inTree && pointer.target.startsWith(`${tree}${path.sep}`))
390
+ .map((pointer) => path.relative(tree, pointer.target).split(path.sep).join("/")));
391
+ const changed = differences(await manifest(path.join(root, from)), await manifest(tree)).filter((entry) => !rewritten.has(entry));
392
+ if (changed.length)
393
+ throw new Error(`refusing to roll back: ${to} differs from ${from} (${changed.slice(0, 5).join(", ")}).`);
394
+ }
395
+ }
396
+ // A pointer apply never reached was never touched: whatever it holds now is the user's.
397
+ // One apply did reach is restored while it is exactly as apply left it, left alone while it is
398
+ // still original (interrupted between journal and write), and anything else is an edit.
399
+ const edited = [];
400
+ const toRestore = [];
401
+ for (const pointer of journal.pointers) {
402
+ if (pointer.inTree || !pointer.applied)
403
+ continue;
404
+ const state = await pointerState(pointer);
405
+ if (state === pointer.applied)
406
+ toRestore.push(pointer);
407
+ else if (state !== pointer.original)
408
+ edited.push(pointer.target);
409
+ }
410
+ const gitignorePath = path.join(root, ".gitignore");
411
+ const gitignoreState = await readFile(gitignorePath).then(sha256, () => "");
412
+ const restoreGitignore = journal.gitignoreApplied !== undefined &&
413
+ gitignoreState === journal.gitignoreApplied;
414
+ if (journal.gitignoreApplied !== undefined &&
415
+ !restoreGitignore &&
416
+ gitignoreState !== journal.gitignoreOriginal)
417
+ edited.push(gitignorePath);
418
+ if (edited.length)
419
+ throw new Error(`refusing to roll back: changed since apply — ${edited.join(", ")}. Restoring the backup would lose that edit; merge it by hand from ${path.join(backup, "pointers")}, then run rollback again.`);
420
+ let restored = 0;
421
+ for (const pointer of toRestore) {
422
+ if (pointer.kind === "symlink")
423
+ await replaceLink(pointer.target, pointer.backup);
424
+ else
425
+ await writeAtomic(pointer.target, await readFile(path.join(backup, "pointers", pointer.backup), "utf8"));
426
+ restored += 1;
427
+ }
428
+ if (journal.gitignore && restoreGitignore)
429
+ await writeAtomic(gitignorePath, await readFile(path.join(backup, journal.gitignore), "utf8"));
430
+ await rm(staging, { recursive: true, force: true });
431
+ for (const { to } of placed)
432
+ await rm(path.join(root, to), { recursive: true });
433
+ journal.step = "rolled-back";
434
+ await writeJournal(backup, journal);
435
+ return {
436
+ rolledBack: true,
437
+ reason: "pointers restored; the new copies removed; the old trees were never touched",
438
+ backup,
439
+ removed: placed.map(({ to }) => to),
440
+ restored,
441
+ };
442
+ }
443
+ //# sourceMappingURL=layout-apply.js.map
@@ -0,0 +1,236 @@
1
+ import { lstat, readdir, readFile, readlink, stat } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { workspaceLayout } from "@ocean/core/shared/paths.js";
5
+ const OS_JUNK = new Set([".DS_Store"]);
6
+ const RECORD_AREAS = [
7
+ "00-inbox",
8
+ "01-daily",
9
+ "02-personal",
10
+ "03-professional",
11
+ "04-projects",
12
+ "05-knowledge",
13
+ "06-templates",
14
+ "99-archive",
15
+ ".index",
16
+ "charter",
17
+ "INDEX.md",
18
+ ];
19
+ const KNOWN_TOP_LEVEL = new Set([
20
+ ...RECORD_AREAS,
21
+ ...OS_JUNK,
22
+ ".git",
23
+ ".gitignore",
24
+ "README.md",
25
+ "brain",
26
+ "kernel",
27
+ "bridge",
28
+ ]);
29
+ // Client config and shell files outside the root that may point into it. Relative to HOME.
30
+ const HOME_POINTERS = [
31
+ ".claude/CLAUDE.md",
32
+ ".claude/AGENTS.md",
33
+ ".claude/settings.json",
34
+ ".claude.json",
35
+ "AGENTS.md",
36
+ ".codex/AGENTS.md",
37
+ ".codex/config.toml",
38
+ ".gemini/GEMINI.md",
39
+ ".gemini/settings.json",
40
+ ".zshrc",
41
+ ".zprofile",
42
+ ".bashrc",
43
+ ".bash_profile",
44
+ ".profile",
45
+ ".config/fish/config.fish",
46
+ ];
47
+ // Folders scanned recursively: shell drop-ins, launchd jobs, and the skill and agent copies each
48
+ // client keeps of its own.
49
+ const HOME_POINTER_DIRS = [
50
+ ".config/fish/conf.d",
51
+ "Library/LaunchAgents",
52
+ ".claude/skills",
53
+ ".claude/agents",
54
+ ".codex/skills",
55
+ ".gemini/commands",
56
+ ".gemini/skills",
57
+ ".agents/skills",
58
+ ];
59
+ // Bridge folders that hold data, not configuration; never scanned for pointers.
60
+ const BRIDGE_DATA_DIRS = new Set(["sessions", "archive", "browser"]);
61
+ const MAX_POINTER_BYTES = 4 * 1024 * 1024;
62
+ async function exists(target) {
63
+ return lstat(target).then(() => true, () => false);
64
+ }
65
+ async function entries(directory) {
66
+ return readdir(directory).catch(() => []);
67
+ }
68
+ async function measure(target) {
69
+ const info = await lstat(target);
70
+ if (info.isSymbolicLink())
71
+ return { files: 0, bytes: 0, symlinks: 1 };
72
+ if (!info.isDirectory())
73
+ return { files: 1, bytes: info.size, symlinks: 0 };
74
+ const total = { files: 0, bytes: 0, symlinks: 0 };
75
+ for (const name of await entries(target)) {
76
+ const child = await measure(path.join(target, name));
77
+ total.files += child.files;
78
+ total.bytes += child.bytes;
79
+ total.symlinks += child.symlinks;
80
+ }
81
+ return total;
82
+ }
83
+ async function filesUnder(directory, skip = new Set()) {
84
+ const found = [];
85
+ for (const name of await entries(directory)) {
86
+ if (skip.has(name))
87
+ continue;
88
+ const target = path.join(directory, name);
89
+ const info = await lstat(target).catch(() => null);
90
+ if (info?.isDirectory())
91
+ found.push(...(await filesUnder(target)));
92
+ else if (info?.isFile() || info?.isSymbolicLink())
93
+ found.push(target);
94
+ }
95
+ return found;
96
+ }
97
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
98
+ export function oldPathPattern(root, home, areas) {
99
+ const roots = [root];
100
+ if (root.startsWith(`${home}${path.sep}`)) {
101
+ const relative = path.relative(home, root);
102
+ roots.push(`~/${relative}`, `$HOME/${relative}`, `\${HOME}/${relative}`);
103
+ }
104
+ const prefixes = roots.map(escapeRegExp).join("|");
105
+ return new RegExp(`(?<![\\w.-])(${prefixes})/(${areas.map(escapeRegExp).join("|")})(?![\\w.-])`, "g");
106
+ }
107
+ function countMatches(text, pattern) {
108
+ return text.match(pattern)?.length ?? 0;
109
+ }
110
+ // Symlinks anywhere in a tree that moves: an absolute target into the old tree would dangle once
111
+ // the old tree is gone.
112
+ async function symlinksUnder(directory) {
113
+ const found = [];
114
+ for (const name of await entries(directory)) {
115
+ const target = path.join(directory, name);
116
+ const info = await lstat(target).catch(() => null);
117
+ if (info?.isSymbolicLink())
118
+ found.push(target);
119
+ else if (info?.isDirectory())
120
+ found.push(...(await symlinksUnder(target)));
121
+ }
122
+ return found;
123
+ }
124
+ async function pointerFiles(home, bridge, charter, movedTrees) {
125
+ const candidates = HOME_POINTERS.map((file) => path.join(home, file));
126
+ for (const directory of HOME_POINTER_DIRS)
127
+ candidates.push(...(await filesUnder(path.join(home, directory))));
128
+ candidates.push(...(await filesUnder(bridge, BRIDGE_DATA_DIRS)));
129
+ candidates.push(...(await filesUnder(charter)));
130
+ for (const tree of movedTrees)
131
+ candidates.push(...(await symlinksUnder(tree)));
132
+ return [...new Set(candidates)];
133
+ }
134
+ async function scanPointers(root, home, layout) {
135
+ const areas = [];
136
+ if (layout.records === "nested")
137
+ areas.push("brain");
138
+ if (layout.bridge === "nested")
139
+ areas.push("kernel/bridge");
140
+ if (!areas.length)
141
+ return [];
142
+ const pattern = oldPathPattern(root, home, areas);
143
+ const bridge = path.join(root, layout.bridge === "nested" ? "kernel/bridge" : "bridge");
144
+ const charter = path.join(root, layout.records === "nested" ? "brain/charter" : "charter");
145
+ const pointers = [];
146
+ const movedTrees = areas.map((area) => path.join(root, area));
147
+ for (const file of await pointerFiles(home, bridge, charter, movedTrees)) {
148
+ const info = await lstat(file).catch(() => null);
149
+ if (info?.isSymbolicLink()) {
150
+ const target = await readlink(file).catch(() => "");
151
+ const references = countMatches(target, pattern);
152
+ if (references) {
153
+ pointers.push({ file, kind: "symlink", references });
154
+ continue;
155
+ }
156
+ }
157
+ // A symlinked dotfile (e.g. ~/.zshrc into a dotfiles repo) is read through its link.
158
+ const resolved = info?.isSymbolicLink()
159
+ ? await stat(file).catch(() => null)
160
+ : info;
161
+ if (!resolved?.isFile() || resolved.size > MAX_POINTER_BYTES)
162
+ continue;
163
+ const text = await readFile(file, "utf8").catch(() => "");
164
+ if (text.includes("\0"))
165
+ continue;
166
+ const references = countMatches(text, pattern);
167
+ if (references)
168
+ pointers.push({ file, kind: "file", references });
169
+ }
170
+ return pointers;
171
+ }
172
+ async function gitignoreChanges(root) {
173
+ const source = await readFile(path.join(root, ".gitignore"), "utf8").catch(() => null);
174
+ if (source === null)
175
+ return [];
176
+ const lines = source.split(/\r?\n/);
177
+ const changes = [];
178
+ // brain/<rest>, /brain/<rest> and !brain/<rest> lose the brain/ segment; a line that named
179
+ // brain/ itself has nothing left to name and is dropped. Each line is kept verbatim in `from`.
180
+ for (const line of lines) {
181
+ const match = /^(\s*!?\/?)brain(?:\/(.*))?$/.exec(line);
182
+ if (!match)
183
+ continue;
184
+ const rest = match[2] ?? "";
185
+ changes.push({ from: line, to: rest.trim() ? `${match[1]}${rest}` : null });
186
+ }
187
+ if (!lines.some((line) => /^\/?bridge\/?$/.test(line.trim())))
188
+ changes.push({ from: null, to: "/bridge/" });
189
+ return changes;
190
+ }
191
+ export async function planLayout(root, home = os.homedir()) {
192
+ const layout = workspaceLayout(root);
193
+ const isDirectory = (target) => lstat(target).then((info) => info.isDirectory(), () => false);
194
+ const workspace = (await isDirectory(path.join(root, "04-projects"))) ||
195
+ (await isDirectory(path.join(root, "brain", "04-projects")));
196
+ const moves = [];
197
+ const collisions = [];
198
+ const addMove = async (from, to) => {
199
+ moves.push({ from, to, ...(await measure(path.join(root, from))) });
200
+ if (await exists(path.join(root, to)))
201
+ collisions.push({ from, to });
202
+ };
203
+ if (layout.records === "nested")
204
+ for (const name of (await entries(path.join(root, "brain"))).sort())
205
+ if (!OS_JUNK.has(name))
206
+ await addMove(`brain/${name}`, name);
207
+ if (layout.bridge === "nested")
208
+ await addMove("kernel/bridge", "bridge");
209
+ // Two moves onto one target (e.g. brain/bridge and kernel/bridge, or names that differ only in
210
+ // case on a case-insensitive volume) collide even though the target does not exist yet.
211
+ const byTarget = new Map();
212
+ for (const move of moves) {
213
+ const key = move.to.toLowerCase();
214
+ byTarget.set(key, [...(byTarget.get(key) ?? []), move]);
215
+ }
216
+ for (const group of byTarget.values())
217
+ if (group.length > 1)
218
+ for (const { from, to } of group)
219
+ if (!collisions.some((collision) => collision.from === from))
220
+ collisions.push({ from, to });
221
+ const strays = (await entries(root))
222
+ .filter((name) => !KNOWN_TOP_LEVEL.has(name))
223
+ .sort();
224
+ return {
225
+ root,
226
+ workspace,
227
+ layout,
228
+ moves,
229
+ collisions,
230
+ strays,
231
+ pointers: await scanPointers(root, home, layout),
232
+ gitignore: moves.length ? await gitignoreChanges(root) : [],
233
+ ready: workspace && moves.length > 0 && collisions.length === 0,
234
+ };
235
+ }
236
+ //# sourceMappingURL=layout-plan.js.map