@mnstry/atelier 0.2.0-alpha.11 → 0.2.0-alpha.13

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 (560) hide show
  1. package/CHANGELOG.md +1005 -0
  2. package/README.md +149 -9
  3. package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
  4. package/contracts/atelier-architecture.v1.schema.json +258 -0
  5. package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
  6. package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
  7. package/contracts/atelier-build.v1.schema.json +1076 -0
  8. package/contracts/atelier-capability.v1.schema.json +1252 -0
  9. package/contracts/atelier-coordination.v1.schema.json +1050 -0
  10. package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
  11. package/contracts/atelier-harness.v1.schema.json +234 -0
  12. package/contracts/atelier-ingestion.v1.schema.json +806 -0
  13. package/contracts/atelier-inquiry.v1.schema.json +1772 -0
  14. package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
  15. package/contracts/atelier-interaction.v1.schema.json +553 -0
  16. package/contracts/atelier-judgment.v1.schema.json +690 -0
  17. package/contracts/atelier-knowledge.v1.schema.json +1512 -0
  18. package/contracts/atelier-learning.v1.schema.json +1095 -0
  19. package/contracts/atelier-migration.v3.schema.json +64 -0
  20. package/contracts/atelier-obsidian-generation-manifest.v2.schema.json +372 -0
  21. package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
  22. package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
  23. package/contracts/atelier-template-adoption.v1.schema.json +223 -0
  24. package/contracts/atelier-template-profile.v1.schema.json +1168 -0
  25. package/contracts/atelier-trackable.v1.schema.json +781 -0
  26. package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
  27. package/contracts/compat-baseline.json +1 -1
  28. package/docs/architecture.md +89 -0
  29. package/docs/assurance-controls.md +4 -3
  30. package/docs/blocks/capability-stewardship.md +17 -0
  31. package/docs/blocks/claims.md +16 -3
  32. package/docs/blocks/inquiry-harnesses.md +19 -0
  33. package/docs/blocks/learning-harnesses.md +17 -0
  34. package/docs/capability-stewardship.md +269 -0
  35. package/docs/coordination.md +51 -0
  36. package/docs/decision-practices.md +111 -0
  37. package/docs/decisions.md +29 -0
  38. package/docs/evidence-navigation.md +237 -0
  39. package/docs/exact-upgrades.md +3 -2
  40. package/docs/ingestion.md +181 -0
  41. package/docs/inquiry-harnesses.md +211 -0
  42. package/docs/install.md +80 -3
  43. package/docs/intake-and-guides.md +5 -0
  44. package/docs/integration-contract-decisions.md +24 -2
  45. package/docs/interaction-and-reflection.md +72 -0
  46. package/docs/knowledge-graph.md +20 -0
  47. package/docs/knowledge-health/api.md +30 -0
  48. package/docs/knowledge-health/assessment/api.md +96 -0
  49. package/docs/knowledge-health/assessment/capabilities.md +40 -0
  50. package/docs/knowledge-health/assessment/evaluation.md +27 -0
  51. package/docs/knowledge-health/assessment/facilitator.md +30 -0
  52. package/docs/knowledge-health/assessment/quickstart.md +61 -0
  53. package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
  54. package/docs/knowledge-health/devday/facilitator.md +16 -0
  55. package/docs/knowledge-health/devday/participant-response.md +22 -0
  56. package/docs/knowledge-health/devday/public-default.md +107 -0
  57. package/docs/knowledge-health/devday/quickstart.md +18 -0
  58. package/docs/knowledge-health/devday/support.md +22 -0
  59. package/docs/knowledge-health/exercise.md +17 -0
  60. package/docs/knowledge-health/facilitator.md +20 -0
  61. package/docs/knowledge-health/quickstart.md +94 -0
  62. package/docs/knowledge-health/readme.md +15 -0
  63. package/docs/knowledge-health/support.md +34 -0
  64. package/docs/knowledge-setup.md +248 -0
  65. package/docs/knowledge-workspace.md +264 -0
  66. package/docs/learning-harnesses.md +238 -0
  67. package/docs/learning.md +215 -0
  68. package/docs/local-services.md +242 -26
  69. package/docs/obsidian-contract.md +878 -36
  70. package/docs/obsidian-plugin.md +619 -0
  71. package/docs/obsidian.md +809 -64
  72. package/docs/optional-ingestion.md +83 -0
  73. package/docs/personal-workspace.md +257 -0
  74. package/docs/practical-judgment.md +92 -0
  75. package/docs/progressive-ingestion-acceptance.md +89 -0
  76. package/docs/progressive-ingestion.md +235 -0
  77. package/docs/project-command-inventory.md +1 -0
  78. package/docs/release-engineering.md +56 -1
  79. package/docs/repo-boundary-guard.md +94 -0
  80. package/docs/research-starter-quickstart.md +66 -0
  81. package/docs/research-starter.md +114 -0
  82. package/docs/semantic-ingestion.md +198 -0
  83. package/docs/semantic-operation.md +317 -0
  84. package/docs/skill-steward.md +6 -0
  85. package/docs/template-profile.md +164 -0
  86. package/docs/template-upgrades.md +114 -0
  87. package/docs/trackables.md +111 -0
  88. package/docs/upgrade.md +68 -3
  89. package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
  90. package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
  91. package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
  92. package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
  93. package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
  94. package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
  95. package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
  96. package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
  97. package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
  98. package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
  99. package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
  100. package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
  101. package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
  102. package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
  103. package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
  104. package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
  105. package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
  106. package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
  107. package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
  108. package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
  109. package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
  110. package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
  111. package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
  112. package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
  113. package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
  114. package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
  115. package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
  116. package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
  117. package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
  118. package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
  119. package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
  120. package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
  121. package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
  122. package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
  123. package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
  124. package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
  125. package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
  126. package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
  127. package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
  128. package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
  129. package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
  130. package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
  131. package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
  132. package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
  133. package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
  134. package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
  135. package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
  136. package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
  137. package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
  138. package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
  139. package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
  140. package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
  141. package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
  142. package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
  143. package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
  144. package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
  145. package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
  146. package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
  147. package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
  148. package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
  149. package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
  150. package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
  151. package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
  152. package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
  153. package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
  154. package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
  155. package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
  156. package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
  157. package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
  158. package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
  159. package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
  160. package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
  161. package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
  162. package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
  163. package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
  164. package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
  165. package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
  166. package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
  167. package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
  168. package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
  169. package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
  170. package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
  171. package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
  172. package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
  173. package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
  174. package/fixtures/atelier-interaction/act/valid/document.json +24 -0
  175. package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
  176. package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
  177. package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
  178. package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
  179. package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
  180. package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
  181. package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
  182. package/fixtures/atelier-judgment/case/valid/document.json +75 -0
  183. package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
  184. package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
  185. package/fixtures/atelier-judgment/native-decision.md +3 -0
  186. package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
  187. package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
  188. package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
  189. package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
  190. package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
  191. package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
  192. package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
  193. package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
  194. package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
  195. package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
  196. package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
  197. package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
  198. package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
  199. package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
  200. package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
  201. package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
  202. package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
  203. package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
  204. package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
  205. package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
  206. package/fixtures/atelier-learning/invalid/authority.json +14 -0
  207. package/fixtures/atelier-learning/valid/capture.json +13 -0
  208. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
  209. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
  210. package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
  211. package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
  212. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
  213. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
  214. package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
  215. package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
  216. package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
  217. package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
  218. package/fixtures/atelier-trackable/request/valid/document.json +22 -0
  219. package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
  220. package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
  221. package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
  222. package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
  223. package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
  224. package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
  225. package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
  226. package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
  227. package/fixtures/capability-packages/adoption.example.json +70 -0
  228. package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
  229. package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
  230. package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
  231. package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
  232. package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
  233. package/fixtures/capability-packages/research/capability-package.json +88 -0
  234. package/fixtures/capability-packages/research/capability-release.json +134 -0
  235. package/fixtures/capability-packages/research/evaluation.md +15 -0
  236. package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
  237. package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
  238. package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
  239. package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
  240. package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
  241. package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
  242. package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
  243. package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
  244. package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
  245. package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
  246. package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
  247. package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
  248. package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
  249. package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
  250. package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
  251. package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
  252. package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
  253. package/fixtures/evidence-navigation/valid/claim.json +36 -0
  254. package/fixtures/evidence-navigation/valid/host.json +16 -0
  255. package/fixtures/evidence-navigation/valid/profile.json +16 -0
  256. package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
  257. package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
  258. package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
  259. package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
  260. package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
  261. package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
  262. package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
  263. package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
  264. package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
  265. package/fixtures/harnesses/learning-cycle.json +780 -0
  266. package/fixtures/inquiry/workshop.json +675 -0
  267. package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
  268. package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
  269. package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
  270. package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
  271. package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
  272. package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
  273. package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
  274. package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
  275. package/fixtures/knowledge-health/calibration-suite.json +26 -0
  276. package/fixtures/knowledge-health/calibration-trial.json +37 -0
  277. package/fixtures/knowledge-health/corrected/checklist.md +18 -0
  278. package/fixtures/knowledge-health/devday/participant-record.json +19 -0
  279. package/fixtures/knowledge-health/expected-summary.json +27 -0
  280. package/fixtures/knowledge-health/workshop/README.md +20 -0
  281. package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
  282. package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
  283. package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
  284. package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
  285. package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
  286. package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
  287. package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
  288. package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
  289. package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
  290. package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
  291. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/case-only-collision.v2.json +160 -0
  292. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/flat-note-path.v2.json +160 -0
  293. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/hidden-folder.v2.json +160 -0
  294. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/identity-in-the-body.v2.json +160 -0
  295. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/link-breaking-name.v2.json +160 -0
  296. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-identity-region.v2.json +156 -0
  297. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-layout-version.v2.json +159 -0
  298. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/note-named-like-a-folder.v2.json +160 -0
  299. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/unknown-region-kind.v2.json +160 -0
  300. package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/v1-contract-version.v2.json +161 -0
  301. package/fixtures/obsidian/contracts/generation-manifest-v2/valid/three-notes.v2.json +160 -0
  302. package/fixtures/obsidian/edits/workspace.json +1 -1
  303. package/fixtures/obsidian/materialization/expected-full.json +21 -21
  304. package/fixtures/obsidian/materialization/expected-full.layout-1.json +99 -0
  305. package/fixtures/obsidian/materialization/expected-scoped.json +7 -7
  306. package/fixtures/obsidian/materialization/expected-scoped.layout-1.json +39 -0
  307. package/fixtures/personal-workspace/overlay.json +24 -0
  308. package/fixtures/templates/local-library.v1.json +75 -0
  309. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
  310. package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
  311. package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
  312. package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
  313. package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
  314. package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
  315. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
  316. package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
  317. package/package.json +64 -6
  318. package/plugins/obsidian/main.js +444 -0
  319. package/plugins/obsidian/manifest.json +9 -0
  320. package/plugins/obsidian/styles.css +22 -0
  321. package/skills/claude/atelier-build-harness/SKILL.md +59 -0
  322. package/skills/claude/atelier-coordination/SKILL.md +32 -0
  323. package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
  324. package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
  325. package/skills/claude/atelier-interaction/SKILL.md +34 -0
  326. package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
  327. package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
  328. package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
  329. package/skills/claude/atelier-research-harness/SKILL.md +43 -0
  330. package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
  331. package/skills/claude/atelier-trackables/SKILL.md +40 -0
  332. package/skills/codex/atelier-build-harness/SKILL.md +59 -0
  333. package/skills/codex/atelier-coordination/SKILL.md +32 -0
  334. package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
  335. package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
  336. package/skills/codex/atelier-interaction/SKILL.md +34 -0
  337. package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
  338. package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
  339. package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
  340. package/skills/codex/atelier-research-harness/SKILL.md +43 -0
  341. package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
  342. package/skills/codex/atelier-trackables/SKILL.md +40 -0
  343. package/src/architecture/catalog.mjs +355 -0
  344. package/src/architecture/index.mjs +93 -0
  345. package/src/boundary/content-rules.mjs +4 -2
  346. package/src/boundary/delegation-command.mjs +158 -0
  347. package/src/boundary/delegation.mjs +396 -0
  348. package/src/boundary/policy.mjs +52 -10
  349. package/src/build/git.mjs +18 -0
  350. package/src/build/index.mjs +3 -0
  351. package/src/build/ledger.mjs +88 -0
  352. package/src/capabilities/adoption.mjs +381 -0
  353. package/src/capabilities/evidence.mjs +140 -0
  354. package/src/capabilities/files.mjs +124 -0
  355. package/src/capabilities/index.mjs +4 -0
  356. package/src/capabilities/instructions.mjs +250 -0
  357. package/src/capabilities/package.mjs +136 -0
  358. package/src/cli/command-failure.mjs +36 -0
  359. package/src/cli/execute-command.mjs +16 -2
  360. package/src/cli/run.mjs +65 -9
  361. package/src/coauthor/store.mjs +43 -2
  362. package/src/collaboration/proposals.mjs +189 -0
  363. package/src/commands/architecture.mjs +27 -0
  364. package/src/commands/boundary.mjs +3 -0
  365. package/src/commands/capability.mjs +73 -0
  366. package/src/commands/enroll.mjs +3 -0
  367. package/src/commands/harness.mjs +79 -0
  368. package/src/commands/ingest.mjs +39 -0
  369. package/src/commands/init.mjs +4 -1
  370. package/src/commands/inquiry.mjs +66 -0
  371. package/src/commands/knowledge-semantic.mjs +128 -0
  372. package/src/commands/knowledge.mjs +119 -0
  373. package/src/commands/learn.mjs +61 -0
  374. package/src/commands/obsidian.mjs +832 -62
  375. package/src/commands/practice.mjs +24 -0
  376. package/src/commands/setup.mjs +22 -2
  377. package/src/commands/trackable.mjs +19 -0
  378. package/src/contracts/corpus.mjs +36 -0
  379. package/src/coordination/index.mjs +110 -0
  380. package/src/egress/forbidden-egress.mjs +2 -0
  381. package/src/evidence-navigation/contracts.mjs +60 -0
  382. package/src/evidence-navigation/evaluate.mjs +69 -0
  383. package/src/evidence-navigation/generation.mjs +22 -0
  384. package/src/evidence-navigation/index.mjs +2 -0
  385. package/src/evidence-navigation/local.mjs +167 -0
  386. package/src/evidence-navigation/schema.generated.mjs +604 -0
  387. package/src/evidence-navigation/types.mjs +12 -0
  388. package/src/export/atelier-export-contract.mjs +35 -8
  389. package/src/graph/enroll.mjs +248 -0
  390. package/src/graph/graph.mjs +17 -1
  391. package/src/graph/knowledge-graph.mjs +58 -8
  392. package/src/harnesses/contracts.mjs +41 -0
  393. package/src/harnesses/exchange.mjs +154 -0
  394. package/src/harnesses/feedback.mjs +16 -0
  395. package/src/harnesses/history.mjs +37 -0
  396. package/src/harnesses/index.mjs +4 -0
  397. package/src/harnesses/store.mjs +29 -0
  398. package/src/ingestion/contracts.mjs +41 -0
  399. package/src/ingestion/evaluation.mjs +119 -0
  400. package/src/ingestion/optional/composition/index.mjs +171 -0
  401. package/src/ingestion/optional/evaluation/index.mjs +219 -0
  402. package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
  403. package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
  404. package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
  405. package/src/ingestion/optional/graphify/json_bridge.py +163 -0
  406. package/src/ingestion/optional/graphify/query_receiving.py +209 -0
  407. package/src/ingestion/optional/index.mjs +9 -0
  408. package/src/ingestion/optional/jev/bridge.mjs +166 -0
  409. package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
  410. package/src/ingestion/optional/jev/judgments.mjs +174 -0
  411. package/src/ingestion/optional/jev/native-translation.mjs +32 -0
  412. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
  413. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
  414. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
  415. package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
  416. package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
  417. package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
  418. package/src/ingestion/optional/vanilla/index.mjs +218 -0
  419. package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
  420. package/src/ingestion/processors.mjs +218 -0
  421. package/src/ingestion/semantic.mjs +203 -0
  422. package/src/ingestion/store.mjs +372 -0
  423. package/src/inquiry/beliefs.mjs +32 -0
  424. package/src/inquiry/contracts.mjs +23 -0
  425. package/src/inquiry/index.mjs +5 -0
  426. package/src/inquiry/ledger.mjs +135 -0
  427. package/src/inquiry/lenses.mjs +15 -0
  428. package/src/inquiry/projection.mjs +77 -0
  429. package/src/inquiry/store.mjs +35 -0
  430. package/src/intake/read-scope.mjs +12 -0
  431. package/src/intake/store.mjs +69 -6
  432. package/src/interaction/index.mjs +91 -0
  433. package/src/judgment/index.mjs +82 -0
  434. package/src/judgment/practice-evaluation.mjs +70 -0
  435. package/src/judgment/practice.mjs +144 -0
  436. package/src/knowledge/context.mjs +217 -0
  437. package/src/knowledge/index.mjs +5 -0
  438. package/src/knowledge/ingestion.mjs +79 -0
  439. package/src/knowledge/intake.mjs +33 -0
  440. package/src/knowledge/ledger.mjs +110 -0
  441. package/src/knowledge/participatory/guidance.mjs +36 -0
  442. package/src/knowledge/participatory/guided-contribution.mjs +44 -0
  443. package/src/knowledge/participatory/index.mjs +2 -0
  444. package/src/knowledge/participatory/installed-workshop.mjs +108 -0
  445. package/src/knowledge/plan.mjs +67 -0
  446. package/src/knowledge/projection.mjs +43 -0
  447. package/src/knowledge/semantic-operation.mjs +567 -0
  448. package/src/knowledge/sessions.mjs +467 -0
  449. package/src/knowledge/workspace.mjs +295 -0
  450. package/src/knowledge-health/assessment.mjs +165 -0
  451. package/src/knowledge-health/capabilities.mjs +350 -0
  452. package/src/knowledge-health/context.mjs +62 -0
  453. package/src/knowledge-health/example.mjs +47 -0
  454. package/src/knowledge-health/index.mjs +10 -0
  455. package/src/knowledge-health/owner-join.mjs +88 -0
  456. package/src/knowledge-health/proof/common.mjs +24 -0
  457. package/src/knowledge-health/proof/installed-check.mjs +100 -0
  458. package/src/knowledge-health/proof/native-readback.mjs +37 -0
  459. package/src/knowledge-health/proof/rehearse.mjs +68 -0
  460. package/src/knowledge-health/proof/validate-participant.mjs +17 -0
  461. package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
  462. package/src/knowledge-health/proof/workshop-check.mjs +36 -0
  463. package/src/knowledge-health/rule-identity.mjs +44 -0
  464. package/src/knowledge-health/workshop-composition.mjs +19 -0
  465. package/src/knowledge-health/workshop-profile.mjs +176 -0
  466. package/src/knowledge-health/workshop.mjs +99 -0
  467. package/src/learning/adapters.mjs +68 -0
  468. package/src/learning/assistance.mjs +127 -0
  469. package/src/learning/contracts.mjs +65 -0
  470. package/src/learning/store.mjs +272 -0
  471. package/src/personal-workspace/index.mjs +391 -0
  472. package/src/personal-workspace/selection.mjs +385 -0
  473. package/src/project/config.mjs +8 -0
  474. package/src/project/durable-state.mjs +3 -1
  475. package/src/project/file-class.mjs +1 -1
  476. package/src/project/private-state.mjs +10 -1
  477. package/src/projection/obsidian/contracts.mjs +224 -18
  478. package/src/projection/obsidian/edits/apply.mjs +63 -19
  479. package/src/projection/obsidian/edits/regions.mjs +137 -42
  480. package/src/projection/obsidian/materialize/index.mjs +6 -3
  481. package/src/projection/obsidian/materialize/path-registry.mjs +443 -57
  482. package/src/projection/obsidian/materialize/prepare-view.mjs +382 -131
  483. package/src/projection/obsidian/materialize/redaction.mjs +327 -0
  484. package/src/projection/obsidian/materialize/settings.mjs +113 -34
  485. package/src/projection/obsidian/personal-workspace.mjs +436 -0
  486. package/src/projection/obsidian/plugin-bridge/bundle.mjs +50 -0
  487. package/src/projection/obsidian/plugin-bridge/channel.mjs +174 -0
  488. package/src/projection/obsidian/plugin-bridge/index.mjs +10 -0
  489. package/src/projection/obsidian/proposals/adapter.mjs +47 -8
  490. package/src/projection/obsidian/proposals/observation.mjs +20 -7
  491. package/src/projection/obsidian/publication/bridge-script.mjs +11 -5
  492. package/src/projection/obsidian/publication/index.mjs +2 -1
  493. package/src/projection/obsidian/publication/publisher.mjs +312 -36
  494. package/src/projection/obsidian/publication/transport.mjs +63 -8
  495. package/src/projection/obsidian/publication/vault-list.mjs +200 -0
  496. package/src/projection/obsidian/recovery/index.mjs +4 -1
  497. package/src/projection/obsidian/recovery/restart.mjs +3 -1
  498. package/src/projection/obsidian/recovery/store.mjs +168 -8
  499. package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
  500. package/src/projection/obsidian/selection-ui/focus.mjs +14 -7
  501. package/src/projection/obsidian/selection-ui/selection-state.mjs +2 -2
  502. package/src/projection/obsidian/selection-ui/selection.mjs +8 -3
  503. package/src/readiness/readiness.mjs +10 -3
  504. package/src/reflection/index.mjs +56 -0
  505. package/src/runtime/obsidian/app-capability.mjs +104 -12
  506. package/src/runtime/obsidian/app-production-seams.mjs +161 -42
  507. package/src/runtime/obsidian/app-registration.mjs +313 -0
  508. package/src/runtime/obsidian/app-restart.mjs +154 -0
  509. package/src/runtime/obsidian/engine.mjs +288 -29
  510. package/src/runtime/obsidian/extension-points.mjs +1 -1
  511. package/src/runtime/obsidian/index.mjs +15 -8
  512. package/src/runtime/obsidian/launch-plan.mjs +91 -0
  513. package/src/runtime/obsidian/lifecycle.mjs +193 -22
  514. package/src/runtime/obsidian/login-item.mjs +332 -0
  515. package/src/runtime/obsidian/machine-settings.mjs +119 -12
  516. package/src/runtime/obsidian/opening.mjs +369 -63
  517. package/src/runtime/obsidian/pending-edits.mjs +10 -0
  518. package/src/runtime/obsidian/pipeline.mjs +71 -3
  519. package/src/runtime/obsidian/plugin-channel.mjs +289 -0
  520. package/src/runtime/obsidian/plugin-choice.mjs +182 -0
  521. package/src/runtime/obsidian/plugin-drift.mjs +124 -0
  522. package/src/runtime/obsidian/plugin-presence.mjs +61 -0
  523. package/src/runtime/obsidian/process-identity.mjs +20 -10
  524. package/src/runtime/obsidian/project-views.mjs +282 -0
  525. package/src/runtime/obsidian/questions.mjs +48 -0
  526. package/src/runtime/obsidian/release-watch.mjs +56 -0
  527. package/src/runtime/obsidian/service-main.mjs +113 -17
  528. package/src/runtime/obsidian/service-manager-production.mjs +61 -0
  529. package/src/runtime/obsidian/service-managers.mjs +275 -0
  530. package/src/runtime/obsidian/service-record.mjs +105 -5
  531. package/src/runtime/obsidian/service-server.mjs +50 -12
  532. package/src/runtime/obsidian/service.mjs +134 -7
  533. package/src/runtime/obsidian/startup-adapters.mjs +59 -21
  534. package/src/runtime/obsidian/state-store.mjs +13 -1
  535. package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
  536. package/src/runtime/obsidian/vault-location.mjs +300 -0
  537. package/src/runtime/obsidian/view-counts.mjs +34 -0
  538. package/src/server/local-sidecar.mjs +193 -7
  539. package/src/server/server.mjs +45 -6
  540. package/src/template-bindings/project.mjs +92 -0
  541. package/src/templates/profile.mjs +225 -0
  542. package/src/trackables/domain.mjs +175 -0
  543. package/src/trackables/store.mjs +114 -0
  544. package/src/ui/knowledge-page.mjs +1388 -0
  545. package/src/upgrade/explanation.mjs +1 -1
  546. package/src/upgrade/template-participant.mjs +171 -0
  547. package/src/upgrade/transaction.mjs +72 -25
  548. package/src/upgrade/upgrade.mjs +2 -1
  549. package/templates/external-project-workspace/pack-lifecycle.json +6 -1
  550. package/templates/knowledge-workspace/.gitattributes +2 -0
  551. package/templates/knowledge-workspace/AGENTS.md +27 -0
  552. package/templates/knowledge-workspace/README.md +44 -0
  553. package/templates/knowledge-workspace/atelier.project.json +22 -0
  554. package/templates/knowledge-workspace/gitignore +6 -0
  555. package/templates/knowledge-workspace/knowledge-plan.json +111 -0
  556. package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
  557. package/templates/knowledge-workspace/records/forecast.md +22 -0
  558. package/templates/knowledge-workspace/records/inspection.md +25 -0
  559. package/templates/knowledge-workspace/records/telescope.md +24 -0
  560. package/templates/knowledge-workspace/repo-access.v1.json +9 -0
@@ -3,70 +3,160 @@ import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { fileURLToPath } from 'node:url'
5
5
  import { firstString, parseArgs, resolveProjectConfig } from '../../project/config.mjs'
6
- import { runMaintenanceService } from './service.mjs'
6
+ import { workspaceStateRoot } from './machine-settings.mjs'
7
+ import { RELEASE_CHANGED, resolveServiceWorkspace, runMaintenanceService } from './service.mjs'
7
8
 
8
9
  // The process of the maintenance service. `start` runs it as a child; a unit
9
10
  // installed at the operating-system level runs it with `--startup`.
10
11
  //
11
12
  // --project=<absolute atelier.project.json> required
12
13
  // --data-root=<absolute directory> optional; otherwise the pointer, the overlay or the platform default
14
+ // --workspace-id=<identifier> named by a login item: under `--startup`, where a refusal is recorded
15
+ // when the project no longer leads to a workspace
13
16
  // --runtime-id=<identifier> given by `start`, so it can recognise the child it created
14
17
  // --interval-ms=<milliseconds> optional
15
- // --startup run by an operating-system unit; needs a consent that covers startup
18
+ // --startup run by a login item; needs a consent that covers startup
16
19
  // --adapter=obsidian-cli the editor adapter; there is no default
17
20
  //
18
21
  // Without `--adapter` the process refuses before it listens or ticks: reaching
19
22
  // a running app is a decision of whoever starts the service, never a default.
23
+ //
24
+ // Exit codes: 0 after a clean stop, and under `--startup` after a refusal too;
25
+ // 2 (EXIT_REFUSED) after a refusal otherwise; 75 (EXIT_RELEASE_CHANGED) under
26
+ // `--startup` once the package changed on disk; 1 after an error without a
27
+ // code. A refusal is any error that carries a string code, Node's system
28
+ // errors (EACCES, EADDRNOTAVAIL, EPERM) included.
20
29
 
21
30
  // Reaching a real app happens here and nowhere else in the service: the
22
31
  // production probe and the CLI transport are imported only once `--adapter`
23
32
  // selected them. The editor adapter is constructed only for an app that meets
24
33
  // the minimum version; below it, or when the version cannot be read, the
25
34
  // factory refuses, the engine records that reason and nothing is published.
26
- // An adapter qualified while no app ran carries that qualification and does
27
- // not coordinate with an app that starts before the next qualification.
35
+ // The app is asked for its version only when a view is published, without
36
+ // blocking the service. An adapter qualified while no app ran carries that
37
+ // qualification and does not coordinate with an app that starts before the
38
+ // next qualification. The engine is told what the app looks like from the
39
+ // process table and the app's vault list alone (whether it runs, which vaults
40
+ // its list shows open), so a view that did not settle is tried again as soon
41
+ // as that changes, and the app is not asked anything to find out; the list is
42
+ // only read here.
28
43
  const ADAPTERS = Object.freeze({
29
44
  'obsidian-cli': async () => {
30
- const [{ createObsidianCliAdapter }, { createProductionAppProbe }, { createQualifiedAdapterFactory }] = await Promise.all([
45
+ const [{ createObsidianCliAdapter, defaultObsidianProcessProbe }, { createProductionAppProbe }, { createQualifiedAdapterFactory }, { obsidianSandboxedBuild, obsidianUserDataDir, readObsidianSettings }, { appStateSignature }, { readAppVaultListForAllocation }, { createProductionUnheldEvidence }] = await Promise.all([
31
46
  import('../../projection/obsidian/publication/transport.mjs'), import('./app-production-seams.mjs'), import('./app-capability.mjs'),
47
+ import('../../projection/obsidian/publication/vault-list.mjs'), import('./app-registration.mjs'), import('./vault-location.mjs'), import('./unheld-evidence.mjs'),
32
48
  ])
33
49
  const adapterFactory = createQualifiedAdapterFactory({ appProbe: createProductionAppProbe(), createAdapter: ({ qualification }) => createObsidianCliAdapter({ qualification }) })
34
- return { adapterFactory, appStatus: () => { const known = adapterFactory.lastQualification(); return known === null ? null : { outcome: known.outcome, reason: known.reason, version: known.version, floor: known.floor } } }
50
+ const userDataDir = obsidianUserDataDir()
51
+ const observeApp = () => appStateSignature({ processes: defaultObsidianProcessProbe(), settings: userDataDir === null ? null : readObsidianSettings({ userDataDir }) })
52
+ // The app's own vault list, read from its file and never written, so a new vault's folder is never allocated inside
53
+ // a vault it lists nor under a name one of its vaults has; a list that cannot be read allocates nothing.
54
+ const readAppVaultList = () => readAppVaultListForAllocation({
55
+ read: () => (userDataDir === null ? { ok: false, code: 'obsidian-settings-location-unknown' } : readObsidianSettings({ userDataDir })),
56
+ sandboxed: obsidianSandboxedBuild() !== null,
57
+ })
58
+ return {
59
+ // The same file tells the publisher whether a view's first publication may go into a vault no Obsidian lists yet.
60
+ adapterFactory, engineOptions: { observeApp, readAppVaultList, readUnheldEvidence: createProductionUnheldEvidence() },
61
+ appStatus: () => { const known = adapterFactory.lastQualification(); return known === null ? null : { outcome: known.outcome, reason: known.reason, version: known.version, floor: known.floor } },
62
+ }
35
63
  },
36
64
  })
37
65
 
66
+ // What the tests read: which reader each adapter hands its engine. Calling one builds the production seams, and
67
+ // reaches no app and no file until the engine uses them.
68
+ export { ADAPTERS as SERVICE_ADAPTERS }
69
+
70
+ import { createReleaseWatch, packageRootOfEntry } from './release-watch.mjs'
38
71
  import { SERVICE_ENTRY_PATH } from './service-entry-path.mjs'
72
+ import { LAST_STARTUP_SCHEMA, openServiceLog, writeLastStartup } from './service-record.mjs'
39
73
 
40
74
  export { SERVICE_ENTRY_PATH }
41
75
  export const EXIT_REFUSED = 2
76
+ // A service a login item started exits with this code when the package it runs from changed on disk: its service
77
+ // manager restarts it (a non-zero exit), on the release now installed. EX_TEMPFAIL.
78
+ export const EXIT_RELEASE_CHANGED = 75
42
79
 
43
- const log = (entry) => { try { process.stdout.write(`${JSON.stringify(entry)}\n`) } catch { /* a closed log never ends the service */ } }
80
+ const printed = (entry) => { try { process.stdout.write(`${JSON.stringify(entry)}\n`) } catch { /* a closed log never ends the service */ } }
81
+
82
+ // The workspace of a service a login item started, when it can be found: where it keeps its log and records how its
83
+ // start ended. When the project no longer leads to one (it moved, its pointer is gone), the workspace the unit names by
84
+ // its data root and identity, if that exists; it is never created here. Null otherwise; a refusal is then printed
85
+ // only, into the unit's own output file.
86
+ function startupWorkspace({ loadProject, dataRoot, workspaceId, env = process.env, platform = process.platform }) {
87
+ try {
88
+ const workspace = resolveServiceWorkspace({ project: loadProject(), dataRoot, env, platform })
89
+ if (workspace?.workspaceRoot) return workspace
90
+ } catch { /* the workspace the unit names, below */ }
91
+ if (typeof dataRoot !== 'string' || !path.isAbsolute(dataRoot) || typeof workspaceId !== 'string') return null
92
+ try {
93
+ const root = workspaceStateRoot(path.resolve(dataRoot), workspaceId)
94
+ return fs.lstatSync(root).isDirectory() ? { workspaceId, workspaceRoot: fs.realpathSync(root) } : null
95
+ } catch { return null }
96
+ }
97
+
98
+ // The service's own log, bounded as under `start`, which hands a child that same file. Under a login item the output
99
+ // goes to the unit's file instead, which nothing bounds; it keeps only what happens before this log is open.
100
+ function workspaceLog(workspaceRoot) {
101
+ let descriptor
102
+ try { descriptor = openServiceLog(workspaceRoot).descriptor } catch { return printed }
103
+ return (entry) => { try { fs.writeSync(descriptor, `${JSON.stringify(entry)}\n`) } catch { /* a closed log never ends the service */ } }
104
+ }
105
+
106
+ function recordStartup(workspace, { at, outcome, code }) {
107
+ if (workspace === null) return
108
+ try { writeLastStartup({ ...workspace, document: { schema: LAST_STARTUP_SCHEMA, workspaceId: workspace.workspaceId, at, outcome, code } }) } catch { /* reported by the log only */ }
109
+ }
44
110
 
45
111
  // The common body of a service process: run, end cleanly on a signal, exit 0
46
112
  // after a clean stop and EXIT_REFUSED when the service refused to run.
113
+ //
114
+ // Started by a login item (`startup`), it logs into its workspace's bounded
115
+ // service log, records how the start ended (`last-startup.json`), exits 0 on
116
+ // a refusal as well, so the service manager does not start it again every
117
+ // minute over something that does not change by itself, and exits
118
+ // EXIT_RELEASE_CHANGED after the tick that found another release on disk.
119
+ // Anything else that ends it (an error nobody typed, a signal) is a crash,
120
+ // and the manager restarts it after its throttle.
47
121
  export async function runServiceProcess(options) {
122
+ const startup = options.startup === true
123
+ const workspace = startup ? startupWorkspace(options) : null
124
+ const log = workspace === null ? printed : workspaceLog(workspace.workspaceRoot)
48
125
  try {
49
- const service = await runMaintenanceService({ log, ...options })
126
+ const service = await runMaintenanceService({ log, invokedAs: process.argv[1], ...options })
127
+ if (startup) recordStartup(workspace, { at: new Date().toISOString(), outcome: 'started', code: null })
50
128
  for (const signal of ['SIGTERM', 'SIGINT']) process.on(signal, () => { void service.shutdown(`signal-${signal}`) })
51
- await service.done
129
+ const { reason } = await service.done
52
130
  // Everything durable is on disk by now; nothing left over may keep a stopped service's PID alive.
53
- process.exit(0)
131
+ process.exit(reason === RELEASE_CHANGED ? EXIT_RELEASE_CHANGED : 0)
54
132
  } catch (error) {
55
- log({ at: new Date().toISOString(), event: 'refused', code: error?.code ?? 'untyped-error', message: String(error?.message ?? error) })
56
- process.exitCode = typeof error?.code === 'string' ? EXIT_REFUSED : 1
133
+ const at = new Date().toISOString()
134
+ const code = typeof error?.code === 'string' ? error.code : null
135
+ log({ at, event: 'refused', code: code ?? 'untyped-error', message: String(error?.message ?? error) })
136
+ if (startup && code !== null) { recordStartup(workspace, { at, outcome: 'refused', code }); process.exitCode = 0; return }
137
+ process.exitCode = code !== null ? EXIT_REFUSED : 1
57
138
  }
58
139
  }
59
140
 
141
+ // The release a service a login item started runs, read through the path it was started by (`argv[1]`), so an
142
+ // upgrade, or a linked install pointed elsewhere, is seen. Null when that package cannot be read.
143
+ function watchRelease(invokedAs) {
144
+ const root = packageRootOfEntry(invokedAs) ?? packageRootOfEntry(SERVICE_ENTRY_PATH)
145
+ if (root === null) return null
146
+ try { return createReleaseWatch({ root }) } catch { return null }
147
+ }
148
+
60
149
  export function serviceOptionsFromArgv(argv, { env = process.env } = {}) {
61
150
  const args = parseArgs(argv)
62
151
  const configPath = firstString(args.project)
63
152
  if (!configPath || !path.isAbsolute(configPath)) throw Object.assign(new Error('--project must be the absolute path of a project configuration'), { code: 'service-arguments-invalid' })
64
153
  const dataRoot = firstString(args['data-root']) ?? undefined
154
+ const workspaceId = firstString(args['workspace-id']) ?? undefined
65
155
  const intervalMs = args['interval-ms'] === undefined ? undefined : Number(args['interval-ms'])
66
156
  if (intervalMs !== undefined && (!Number.isInteger(intervalMs) || intervalMs < 1)) throw Object.assign(new Error('--interval-ms must be a positive integer'), { code: 'service-arguments-invalid' })
67
157
  return {
68
158
  loadProject: () => resolveProjectConfig({ argv: [`--project=${configPath}`], cwd: path.dirname(configPath), env, writeLocalState: false }),
69
- ...(dataRoot === undefined ? {} : { dataRoot }), ...(intervalMs === undefined ? {} : { intervalMs }),
159
+ ...(dataRoot === undefined ? {} : { dataRoot }), ...(workspaceId === undefined ? {} : { workspaceId }), ...(intervalMs === undefined ? {} : { intervalMs }),
70
160
  ...(firstString(args['runtime-id']) ? { runtimeId: firstString(args['runtime-id']) } : {}),
71
161
  startup: args.startup === true, adapter: firstString(args.adapter),
72
162
  }
@@ -75,19 +165,25 @@ export function serviceOptionsFromArgv(argv, { env = process.env } = {}) {
75
165
  const invokedDirectly = (() => { try { return Boolean(process.argv[1]) && fs.realpathSync(process.argv[1]) === fs.realpathSync(fileURLToPath(import.meta.url)) } catch { return false } })()
76
166
 
77
167
  if (invokedDirectly) {
168
+ const argv = process.argv.slice(2)
169
+ // As early as this entry can: its static imports have already loaded most of the modules, so a release that changes
170
+ // between this process's start and this reading is not noticed until the next change (docs/obsidian.md says so).
171
+ const releaseWatch = argv.includes('--startup') ? watchRelease(process.argv[1]) : null
78
172
  let options = null
79
173
  try {
80
- options = serviceOptionsFromArgv(process.argv.slice(2))
174
+ options = serviceOptionsFromArgv(argv)
81
175
  if (!options.adapter || !Object.hasOwn(ADAPTERS, options.adapter)) throw Object.assign(new Error('no editor adapter was selected; pass --adapter explicitly'), { code: 'service-adapter-not-selected' })
82
176
  } catch (error) {
83
- log({ at: new Date().toISOString(), event: 'refused', code: error.code ?? 'untyped-error', message: error.message })
84
- process.exitCode = EXIT_REFUSED
177
+ printed({ at: new Date().toISOString(), event: 'refused', code: error.code ?? 'untyped-error', message: error.message })
178
+ // Under a login item arguments that do not work never will: it ends cleanly, and is not started again every minute.
179
+ process.exitCode = argv.includes('--startup') ? 0 : EXIT_REFUSED
85
180
  options = null
86
181
  }
87
182
  if (options) {
88
183
  const { adapter, ...rest } = options
89
184
  const [{ loadContributions }, { createObsidianRegistry }] = await Promise.all([import('./contributions.mjs'), import('./extension-points.mjs')])
90
185
  const registry = createObsidianRegistry({ contributions: await loadContributions() })
91
- await runServiceProcess({ ...rest, entryPath: SERVICE_ENTRY_PATH, ...(await ADAPTERS[adapter]()), engineOptions: { extensions: registry.extensions } })
186
+ const { engineOptions, ...seams } = await ADAPTERS[adapter]()
187
+ await runServiceProcess({ ...rest, entryPath: SERVICE_ENTRY_PATH, ...seams, engineOptions: { ...engineOptions, extensions: registry.extensions }, releaseWatch })
92
188
  }
93
189
  }
@@ -0,0 +1,61 @@
1
+ import { execFile } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { refuse } from './errors.mjs'
6
+ import { createLaunchdManager, createSystemdManager, homeIsAccountHome } from './service-managers.mjs'
7
+
8
+ // The production service manager of a login item: the only code that runs
9
+ // launchctl or systemctl, or writes into ~/Library/LaunchAgents or the user's
10
+ // systemd folder.
11
+ //
12
+ // It is imported in exactly one place, dynamically: the command entry
13
+ // (src/commands/obsidian.mjs), only when it runs as the real command-line
14
+ // entry. Importing it leaves a trace below that the test suite asserts is
15
+ // absent. It refuses, before anything is looked up or run:
16
+ //
17
+ // - under the Node test runner (`real-login-item-under-test`), whatever
18
+ // environment it is handed: a test hands in its own manager;
19
+ // - when HOME is not the account's own home directory
20
+ // (`login-item-home-mismatch`): launchd and systemd register a unit in
21
+ // the account's real session whatever HOME says, so a private HOME must
22
+ // never install one;
23
+ // - on Windows (`startup-platform-unqualified`) and on any platform but
24
+ // macOS and Linux (`startup-platform-unsupported`).
25
+ globalThis[Symbol.for('mnstry.atelier.obsidian.production-service-manager-loaded')] = true
26
+
27
+ const RUN_TIMEOUT_MS = 15 * 1000
28
+ const SYSTEMCTL = ['/usr/bin/systemctl', '/bin/systemctl']
29
+
30
+ // No shell, in the root directory, bounded.
31
+ function runner(env) {
32
+ return (program, args) => new Promise((resolve) => {
33
+ execFile(program, args, { env, cwd: '/', timeout: RUN_TIMEOUT_MS, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
34
+ resolve({ status: error ? (typeof error.code === 'number' ? error.code : null) : 0, stdout: String(stdout ?? ''), stderr: String(stderr ?? '') })
35
+ })
36
+ })
37
+ }
38
+
39
+ export function createProductionServiceManager({ platform = process.platform, env = process.env } = {}) {
40
+ if (process.env.NODE_TEST_CONTEXT !== undefined || env.NODE_TEST_CONTEXT !== undefined) refuse('real-login-item-under-test', 'the production login-item manager is never used under the test runner; a test hands in its own')
41
+ if (platform === 'win32') refuse('startup-platform-unqualified', 'operating-system startup on Windows has not been qualified', { platform })
42
+ if (platform !== 'darwin' && platform !== 'linux') refuse('startup-platform-unsupported', 'no login item is known for this platform', { platform: String(platform) })
43
+ const account = os.userInfo()
44
+ if (!homeIsAccountHome({ env, accountHome: account.homedir })) refuse('login-item-home-mismatch', 'HOME is not this account\'s home directory; a login item is registered in the account\'s real session whatever HOME says, so none is installed from here')
45
+ const run = runner(env)
46
+ if (platform === 'darwin') return createLaunchdManager({ run, uid: account.uid, directory: path.join(account.homedir, 'Library', 'LaunchAgents') })
47
+ const configHome = typeof env.XDG_CONFIG_HOME === 'string' && path.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : path.join(account.homedir, '.config')
48
+ const systemctl = SYSTEMCTL.find((candidate) => { try { return fs.statSync(candidate).isFile() } catch { return false } })
49
+ if (systemctl === undefined) return unavailableManager({ directory: path.join(configHome, 'systemd', 'user') })
50
+ return createSystemdManager({ run, systemctl, directory: path.join(configHome, 'systemd', 'user') })
51
+ }
52
+
53
+ // Linux without systemctl: every change answers `login-item-unavailable`, and nothing is written.
54
+ function unavailableManager({ directory }) {
55
+ const unavailable = async () => ({ ok: false, code: 'login-item-unavailable', status: null, message: 'systemctl was not found' })
56
+ return {
57
+ kind: 'systemd-user-unit', platform: 'linux', directory, fileFor: (fileName) => path.join(directory, fileName),
58
+ readUnit: () => null, install: unavailable, start: unavailable, remove: async () => ({ ok: true, removed: false }),
59
+ inspect: async ({ fileName }) => ({ ok: true, file: path.join(directory, fileName), present: false, loaded: null, running: null, pid: null, lastExit: null, disabled: null }),
60
+ }
61
+ }
@@ -0,0 +1,275 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import { atomicReplacePrivateText, readRegularTextNoFollow } from '../../project/private-state.mjs'
4
+
5
+ // The operating system's service managers, as a login item uses them: a
6
+ // launchd user agent on macOS and a systemd user unit on Linux.
7
+ //
8
+ // Each is built from the directory its unit files live in and `run`, the only
9
+ // way it starts a program: `run(program, args)` resolves { status, stdout,
10
+ // stderr }, with a null status when the program could not be run at all.
11
+ // Neither has a default here. The production ones are made only by the real
12
+ // command-line entry, through the production manager module, never under the
13
+ // test runner and never for a HOME that is not the account's own; tests hand
14
+ // in a `run` that records what it was asked and a temporary directory.
15
+ //
16
+ // What each does, in order:
17
+ //
18
+ // launchd install write the property list (mode 0644, atomically); boot
19
+ // the job out if it is loaded, because a loaded job keeps
20
+ // the definition it was loaded with, and wait until
21
+ // launchd has let it go; bootstrap it (RunAtLoad starts it);
22
+ // a file written where none was is removed again when
23
+ // launchd did not take it
24
+ // start kickstart -p: starts the job when it is not running and
25
+ // prints its PID either way
26
+ // remove bootout; delete the file
27
+ // inspect print (loaded, state, PID, last exit code) and
28
+ // print-disabled (switched off in System Settings)
29
+ // systemd install write the unit (mode 0644, atomically); daemon-reload;
30
+ // enable
31
+ // start start
32
+ // remove disable --now; delete the file; daemon-reload
33
+ // inspect show LoadState, ActiveState, SubState, UnitFileState,
34
+ // MainPID, ExecMainStatus
35
+ //
36
+ // Every answer is { ok: true, ... } or { ok: false, code, status, message }
37
+ // with one of these codes:
38
+ //
39
+ // login-item-unavailable no manager to install into (no systemd user
40
+ // instance: WSL, a container, no session bus)
41
+ // login-item-install-failed the manager refused the unit
42
+ // login-item-not-loaded a start of a job the manager does not have
43
+ // loaded (booted out, or not allowed to run)
44
+ // login-item-start-failed the manager did not start it for another reason
45
+ // login-item-remove-failed the manager did not let the job go
46
+ // login-item-file-unsafe the unit's file is a link or not a regular file
47
+ //
48
+ // The exit statuses and texts of launchctl and systemctl read here are the
49
+ // documented ones; they are exercised against recorded texts, never against
50
+ // this machine's own session.
51
+
52
+ export const SERVICE_MANAGER_KINDS = Object.freeze(['launchd-user-agent', 'systemd-user-unit'])
53
+ export const LAUNCHCTL = '/bin/launchctl'
54
+ const UNIT_FILE_MODE = 0o644
55
+ const LABEL = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
56
+ // launchctl: the service is not in the domain (print, kickstart), and the job was not loaded (bootout).
57
+ const LAUNCHD_NOT_FOUND = [113, 3]
58
+ const BOOTOUT_WAIT_MS = 10 * 1000
59
+ const BOOTOUT_POLL_MS = 100
60
+
61
+ // Whether HOME names the account's own home directory (`accountHome`, as the system records it). A login item is
62
+ // registered in the account's real session whatever HOME says, so only then is one installed.
63
+ export function homeIsAccountHome({ env, accountHome }) {
64
+ const home = env?.HOME
65
+ if (typeof home !== 'string' || !path.isAbsolute(home) || typeof accountHome !== 'string' || !path.isAbsolute(accountHome)) return false
66
+ if (path.resolve(home) === path.resolve(accountHome)) return true
67
+ try { return fs.realpathSync(home) === fs.realpathSync(accountHome) } catch { return false }
68
+ }
69
+
70
+ const failed = (code, answer, message) => ({ ok: false, code, status: answer?.status ?? null, message: message ?? firstLine(answer) })
71
+ // The first line of what a manager printed, bounded: never a whole output, which can carry an environment.
72
+ function firstLine(answer) {
73
+ const text = `${answer?.stderr ?? ''}\n${answer?.stdout ?? ''}`.split('\n').map((line) => line.trim()).find((line) => line !== '')
74
+ return text === undefined ? null : text.slice(0, 200)
75
+ }
76
+ const sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms) })
77
+
78
+ function checkUnit({ label, fileName }) {
79
+ if (typeof label !== 'string' || !LABEL.test(label)) throw new TypeError('a login item has a plain label')
80
+ if (typeof fileName !== 'string' || fileName !== path.basename(fileName) || !fileName.startsWith(label)) throw new TypeError('a unit file name is the label and its suffix')
81
+ }
82
+
83
+ // The unit's file, owned by this module: written atomically with the mode a manager accepts (neither launchd nor
84
+ // systemd reads a unit anyone else can write), never through a link.
85
+ function unitFiles(directory) {
86
+ if (typeof directory !== 'string' || !path.isAbsolute(directory)) throw new TypeError('a service manager needs the absolute directory of its unit files')
87
+ const fileFor = (fileName) => path.join(directory, fileName)
88
+ return {
89
+ directory,
90
+ fileFor,
91
+ read(fileName) {
92
+ try { return readRegularTextNoFollow(fileFor(fileName)) } catch (error) {
93
+ if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null
94
+ return undefined
95
+ }
96
+ },
97
+ write(fileName, text) {
98
+ try {
99
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 })
100
+ atomicReplacePrivateText(fileFor(fileName), text, UNIT_FILE_MODE)
101
+ return { ok: true }
102
+ } catch (error) {
103
+ return { ok: false, code: error?.code === undefined || /regular file|redirected/.test(String(error.message)) ? 'login-item-file-unsafe' : 'login-item-install-failed', status: null, message: String(error.code ?? 'not-written') }
104
+ }
105
+ },
106
+ remove(fileName) {
107
+ let before
108
+ try { before = fs.lstatSync(fileFor(fileName)) } catch (error) { if (error.code === 'ENOENT') return { ok: true, removed: false }; return { ok: false, code: 'login-item-remove-failed', status: null, message: String(error.code) } }
109
+ if (!before.isFile()) return { ok: false, code: 'login-item-file-unsafe', status: null, message: 'not a regular file' }
110
+ try { fs.unlinkSync(fileFor(fileName)); return { ok: true, removed: true } } catch (error) {
111
+ return error.code === 'ENOENT' ? { ok: true, removed: false } : { ok: false, code: 'login-item-remove-failed', status: null, message: String(error.code) }
112
+ }
113
+ },
114
+ present(fileName) { try { return fs.lstatSync(fileFor(fileName)).isFile() } catch { return false } },
115
+ }
116
+ }
117
+
118
+ // Pure. What `launchctl print <domain>/<label>` says about one job.
119
+ export function readLaunchdPrint(text) {
120
+ const value = (name) => { const match = new RegExp(`^\\s*${name} = (.+)$`, 'm').exec(String(text ?? '')); return match === null ? null : match[1].trim() }
121
+ const pid = Number(value('pid'))
122
+ const exit = Number(value('last exit code'))
123
+ return { state: value('state'), pid: Number.isSafeInteger(pid) && pid > 0 ? pid : null, lastExit: Number.isInteger(exit) ? exit : null }
124
+ }
125
+
126
+ // Pure. Whether `launchctl print-disabled <domain>` lists this label as disabled: true, false, or null when it is not
127
+ // listed. Releases of macOS print `=> disabled` / `=> enabled`, earlier ones `=> true` / `=> false`.
128
+ export function readLaunchdDisabled(text, label) {
129
+ for (const line of String(text ?? '').split('\n')) {
130
+ const match = /^\s*"([^"]+)"\s*=>\s*(\S+)\s*$/.exec(line)
131
+ if (match === null || match[1] !== label) continue
132
+ if (match[2] === 'disabled' || match[2] === 'true') return true
133
+ if (match[2] === 'enabled' || match[2] === 'false') return false
134
+ }
135
+ return null
136
+ }
137
+
138
+ export function createLaunchdManager({ run, uid, directory, waitMs = BOOTOUT_WAIT_MS, pollMs = BOOTOUT_POLL_MS } = {}) {
139
+ if (typeof run !== 'function') throw new TypeError('a service manager needs run')
140
+ if (!Number.isSafeInteger(uid) || uid < 0) throw new TypeError('a launchd manager needs the user id of its session')
141
+ const files = unitFiles(directory)
142
+ const domain = `gui/${uid}`
143
+ const target = (label) => `${domain}/${label}`
144
+ const launchctl = (...args) => run(LAUNCHCTL, args)
145
+ const loaded = async (label) => {
146
+ const answer = await launchctl('print', target(label))
147
+ return answer.status === 0 ? { loaded: true, answer } : { loaded: LAUNCHD_NOT_FOUND.includes(answer.status) ? false : null, answer }
148
+ }
149
+ // Boots the job out when launchd has it, and waits, bounded, until it is gone: bootstrap refuses a label still loaded.
150
+ async function bootout(label) {
151
+ const answer = await launchctl('bootout', target(label))
152
+ if (answer.status !== 0 && !LAUNCHD_NOT_FOUND.includes(answer.status)) return failed('login-item-remove-failed', answer)
153
+ const until = Date.now() + waitMs
154
+ for (;;) {
155
+ const now = await loaded(label)
156
+ if (now.loaded === false) return { ok: true, wasLoaded: answer.status === 0 }
157
+ if (Date.now() >= until) return failed('login-item-remove-failed', now.answer, 'launchd kept the job loaded')
158
+ await sleep(pollMs)
159
+ }
160
+ }
161
+ return {
162
+ kind: 'launchd-user-agent', platform: 'darwin', directory: files.directory, fileFor: files.fileFor,
163
+ readUnit({ label, fileName }) { checkUnit({ label, fileName }); return files.read(fileName) },
164
+ async install({ label, fileName, text }) {
165
+ checkUnit({ label, fileName })
166
+ // launchd loads every property list in LaunchAgents at login: one this installation wrote where none was is
167
+ // removed again when launchd did not take it.
168
+ const existed = files.present(fileName)
169
+ const written = files.write(fileName, text)
170
+ if (!written.ok) return written
171
+ const out = await bootout(label)
172
+ if (!out.ok) { if (!existed) files.remove(fileName); return out }
173
+ const answer = await launchctl('bootstrap', domain, files.fileFor(fileName))
174
+ if (answer.status !== 0) {
175
+ if (!existed) files.remove(fileName)
176
+ return failed(answer.status === null ? 'login-item-unavailable' : 'login-item-install-failed', answer)
177
+ }
178
+ return { ok: true, file: files.fileFor(fileName) }
179
+ },
180
+ async start({ label, fileName }) {
181
+ checkUnit({ label, fileName })
182
+ const answer = await launchctl('kickstart', '-p', target(label))
183
+ if (answer.status === 0) { const pid = Number(String(answer.stdout ?? '').trim()); return { ok: true, pid: Number.isSafeInteger(pid) && pid > 0 ? pid : null } }
184
+ return failed(LAUNCHD_NOT_FOUND.includes(answer.status) ? 'login-item-not-loaded' : 'login-item-start-failed', answer)
185
+ },
186
+ async remove({ label, fileName }) {
187
+ checkUnit({ label, fileName })
188
+ const out = await bootout(label)
189
+ if (!out.ok) return out
190
+ const removed = files.remove(fileName)
191
+ return removed.ok ? { ok: true, removed: removed.removed || out.wasLoaded } : removed
192
+ },
193
+ async inspect({ label, fileName }) {
194
+ checkUnit({ label, fileName })
195
+ const present = files.present(fileName)
196
+ const printed = await loaded(label)
197
+ const disabled = await launchctl('print-disabled', domain)
198
+ const job = printed.loaded === true ? readLaunchdPrint(printed.answer.stdout) : { state: null, pid: null, lastExit: null }
199
+ return {
200
+ ok: true, file: files.fileFor(fileName), present, loaded: printed.loaded, running: printed.loaded === true ? job.state === 'running' : printed.loaded === false ? false : null,
201
+ pid: job.pid, lastExit: job.lastExit, disabled: disabled.status === 0 ? readLaunchdDisabled(disabled.stdout, label) === true : null,
202
+ }
203
+ },
204
+ }
205
+ }
206
+
207
+ // Pure. The `KEY=value` lines of `systemctl show`.
208
+ export function readSystemdShow(text) {
209
+ const values = {}
210
+ for (const line of String(text ?? '').split('\n')) {
211
+ const at = line.indexOf('=')
212
+ if (at > 0) values[line.slice(0, at).trim()] = line.slice(at + 1).trim()
213
+ }
214
+ const pid = Number(values.MainPID)
215
+ const exit = Number(values.ExecMainStatus)
216
+ return {
217
+ loadState: values.LoadState ?? null, activeState: values.ActiveState ?? null, subState: values.SubState ?? null, unitFileState: values.UnitFileState ?? null,
218
+ pid: Number.isSafeInteger(pid) && pid > 0 ? pid : null, lastExit: values.ExecMainStatus === undefined || !Number.isInteger(exit) ? null : exit,
219
+ }
220
+ }
221
+
222
+ export function createSystemdManager({ run, systemctl, directory } = {}) {
223
+ if (typeof run !== 'function') throw new TypeError('a service manager needs run')
224
+ if (typeof systemctl !== 'string' || !path.isAbsolute(systemctl)) throw new TypeError('a systemd manager needs the absolute path of systemctl')
225
+ const files = unitFiles(directory)
226
+ const user = (...args) => run(systemctl, ['--user', ...args])
227
+ // A user instance that cannot be reached (no session bus: WSL, a container, a session without systemd) makes the
228
+ // first call fail; nothing is left behind by an installation it stopped.
229
+ const unreachable = (answer) => answer.status === null || /Failed to connect to bus|No medium found|not been booted with systemd|System has not been booted/i.test(`${answer.stderr ?? ''}`)
230
+ return {
231
+ kind: 'systemd-user-unit', platform: 'linux', directory: files.directory, fileFor: files.fileFor,
232
+ readUnit({ label, fileName }) { checkUnit({ label, fileName }); return files.read(fileName) },
233
+ async install({ label, fileName, text }) {
234
+ checkUnit({ label, fileName })
235
+ const existed = files.present(fileName)
236
+ const written = files.write(fileName, text)
237
+ if (!written.ok) return written
238
+ const reloaded = await user('daemon-reload')
239
+ if (reloaded.status !== 0) {
240
+ if (!existed) files.remove(fileName)
241
+ return failed(unreachable(reloaded) ? 'login-item-unavailable' : 'login-item-install-failed', reloaded)
242
+ }
243
+ const enabled = await user('enable', fileName)
244
+ if (enabled.status !== 0) return failed(unreachable(enabled) ? 'login-item-unavailable' : 'login-item-install-failed', enabled)
245
+ return { ok: true, file: files.fileFor(fileName) }
246
+ },
247
+ async start({ label, fileName }) {
248
+ checkUnit({ label, fileName })
249
+ const answer = await user('start', fileName)
250
+ if (answer.status === 0) return { ok: true, pid: null }
251
+ return failed(unreachable(answer) ? 'login-item-unavailable' : /not found|not loaded|does not exist/i.test(`${answer.stderr ?? ''}`) ? 'login-item-not-loaded' : 'login-item-start-failed', answer)
252
+ },
253
+ async remove({ label, fileName }) {
254
+ checkUnit({ label, fileName })
255
+ const disabled = await user('disable', '--now', fileName)
256
+ const missing = disabled.status !== 0 && /not found|not loaded|does not exist/i.test(`${disabled.stderr ?? ''}`)
257
+ if (disabled.status !== 0 && !missing) return failed(unreachable(disabled) ? 'login-item-unavailable' : 'login-item-remove-failed', disabled)
258
+ const removed = files.remove(fileName)
259
+ if (!removed.ok) return removed
260
+ await user('daemon-reload')
261
+ return { ok: true, removed: removed.removed || disabled.status === 0 }
262
+ },
263
+ async inspect({ label, fileName }) {
264
+ checkUnit({ label, fileName })
265
+ const present = files.present(fileName)
266
+ const answer = await user('show', '-p', 'LoadState,ActiveState,SubState,UnitFileState,MainPID,ExecMainStatus', fileName)
267
+ if (answer.status !== 0) return { ok: true, file: files.fileFor(fileName), present, loaded: null, running: null, pid: null, lastExit: null, disabled: null }
268
+ const unit = readSystemdShow(answer.stdout)
269
+ return {
270
+ ok: true, file: files.fileFor(fileName), present, loaded: unit.loadState === 'loaded', running: unit.activeState === 'active' || unit.activeState === 'activating',
271
+ pid: unit.pid, lastExit: unit.lastExit, disabled: unit.unitFileState === null ? null : !['enabled', 'enabled-runtime', 'linked', 'linked-runtime'].includes(unit.unitFileState),
272
+ }
273
+ },
274
+ }
275
+ }