@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.
- package/CHANGELOG.md +1005 -0
- package/README.md +149 -9
- package/contracts/atelier-adoption-policy.v2.schema.json +55 -0
- package/contracts/atelier-architecture.v1.schema.json +258 -0
- package/contracts/atelier-boundary-delegations.v1.schema.json +161 -0
- package/contracts/atelier-boundary-owner-keys.v1.schema.json +98 -0
- package/contracts/atelier-build.v1.schema.json +1076 -0
- package/contracts/atelier-capability.v1.schema.json +1252 -0
- package/contracts/atelier-coordination.v1.schema.json +1050 -0
- package/contracts/atelier-evidence-navigation.v1.schema.json +603 -0
- package/contracts/atelier-harness.v1.schema.json +234 -0
- package/contracts/atelier-ingestion.v1.schema.json +806 -0
- package/contracts/atelier-inquiry.v1.schema.json +1772 -0
- package/contracts/atelier-instruction-adoption.v1.schema.json +490 -0
- package/contracts/atelier-interaction.v1.schema.json +553 -0
- package/contracts/atelier-judgment.v1.schema.json +690 -0
- package/contracts/atelier-knowledge.v1.schema.json +1512 -0
- package/contracts/atelier-learning.v1.schema.json +1095 -0
- package/contracts/atelier-migration.v3.schema.json +64 -0
- package/contracts/atelier-obsidian-generation-manifest.v2.schema.json +372 -0
- package/contracts/atelier-personal-workspace-manifest.v1.schema.json +87 -0
- package/contracts/atelier-personal-workspace-overlay.v1.schema.json +299 -0
- package/contracts/atelier-template-adoption.v1.schema.json +223 -0
- package/contracts/atelier-template-profile.v1.schema.json +1168 -0
- package/contracts/atelier-trackable.v1.schema.json +781 -0
- package/contracts/atelier-upgrade-plan.v3.schema.json +532 -0
- package/contracts/compat-baseline.json +1 -1
- package/docs/architecture.md +89 -0
- package/docs/assurance-controls.md +4 -3
- package/docs/blocks/capability-stewardship.md +17 -0
- package/docs/blocks/claims.md +16 -3
- package/docs/blocks/inquiry-harnesses.md +19 -0
- package/docs/blocks/learning-harnesses.md +17 -0
- package/docs/capability-stewardship.md +269 -0
- package/docs/coordination.md +51 -0
- package/docs/decision-practices.md +111 -0
- package/docs/decisions.md +29 -0
- package/docs/evidence-navigation.md +237 -0
- package/docs/exact-upgrades.md +3 -2
- package/docs/ingestion.md +181 -0
- package/docs/inquiry-harnesses.md +211 -0
- package/docs/install.md +80 -3
- package/docs/intake-and-guides.md +5 -0
- package/docs/integration-contract-decisions.md +24 -2
- package/docs/interaction-and-reflection.md +72 -0
- package/docs/knowledge-graph.md +20 -0
- package/docs/knowledge-health/api.md +30 -0
- package/docs/knowledge-health/assessment/api.md +96 -0
- package/docs/knowledge-health/assessment/capabilities.md +40 -0
- package/docs/knowledge-health/assessment/evaluation.md +27 -0
- package/docs/knowledge-health/assessment/facilitator.md +30 -0
- package/docs/knowledge-health/assessment/quickstart.md +61 -0
- package/docs/knowledge-health/assessment/troubleshooting.md +23 -0
- package/docs/knowledge-health/devday/facilitator.md +16 -0
- package/docs/knowledge-health/devday/participant-response.md +22 -0
- package/docs/knowledge-health/devday/public-default.md +107 -0
- package/docs/knowledge-health/devday/quickstart.md +18 -0
- package/docs/knowledge-health/devday/support.md +22 -0
- package/docs/knowledge-health/exercise.md +17 -0
- package/docs/knowledge-health/facilitator.md +20 -0
- package/docs/knowledge-health/quickstart.md +94 -0
- package/docs/knowledge-health/readme.md +15 -0
- package/docs/knowledge-health/support.md +34 -0
- package/docs/knowledge-setup.md +248 -0
- package/docs/knowledge-workspace.md +264 -0
- package/docs/learning-harnesses.md +238 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +242 -26
- package/docs/obsidian-contract.md +878 -36
- package/docs/obsidian-plugin.md +619 -0
- package/docs/obsidian.md +809 -64
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +257 -0
- package/docs/practical-judgment.md +92 -0
- package/docs/progressive-ingestion-acceptance.md +89 -0
- package/docs/progressive-ingestion.md +235 -0
- package/docs/project-command-inventory.md +1 -0
- package/docs/release-engineering.md +56 -1
- package/docs/repo-boundary-guard.md +94 -0
- package/docs/research-starter-quickstart.md +66 -0
- package/docs/research-starter.md +114 -0
- package/docs/semantic-ingestion.md +198 -0
- package/docs/semantic-operation.md +317 -0
- package/docs/skill-steward.md +6 -0
- package/docs/template-profile.md +164 -0
- package/docs/template-upgrades.md +114 -0
- package/docs/trackables.md +111 -0
- package/docs/upgrade.md +68 -3
- package/fixtures/atelier-architecture/binding/invalid/document.json +19 -0
- package/fixtures/atelier-architecture/binding/valid/document.json +19 -0
- package/fixtures/atelier-architecture/catalog/invalid/document.json +290 -0
- package/fixtures/atelier-architecture/catalog/valid/document.json +343 -0
- package/fixtures/atelier-build-contract/attempt/invalid/document.json +20 -0
- package/fixtures/atelier-build-contract/attempt/valid/document.json +19 -0
- package/fixtures/atelier-build-contract/candidate/invalid/document.json +24 -0
- package/fixtures/atelier-build-contract/candidate/valid/document.json +23 -0
- package/fixtures/atelier-build-contract/decision/invalid/document.json +31 -0
- package/fixtures/atelier-build-contract/decision/valid/document.json +30 -0
- package/fixtures/atelier-build-contract/delivery/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/delivery/valid/document.json +17 -0
- package/fixtures/atelier-build-contract/gate/invalid/document.json +22 -0
- package/fixtures/atelier-build-contract/gate/valid/document.json +21 -0
- package/fixtures/atelier-build-contract/ledger/invalid/document.json +270 -0
- package/fixtures/atelier-build-contract/ledger/valid/document.json +269 -0
- package/fixtures/atelier-build-contract/objective/invalid/document.json +73 -0
- package/fixtures/atelier-build-contract/objective/valid/document.json +72 -0
- package/fixtures/atelier-build-contract/progress/invalid/document.json +18 -0
- package/fixtures/atelier-build-contract/progress/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/adoption/invalid/document.json +71 -0
- package/fixtures/atelier-capability-contract/adoption/valid/document.json +70 -0
- package/fixtures/atelier-capability-contract/event/invalid/document.json +18 -0
- package/fixtures/atelier-capability-contract/event/valid/document.json +17 -0
- package/fixtures/atelier-capability-contract/journal/invalid/document.json +382 -0
- package/fixtures/atelier-capability-contract/journal/valid/document.json +381 -0
- package/fixtures/atelier-capability-contract/notice/invalid/document.json +8 -0
- package/fixtures/atelier-capability-contract/notice/valid/document.json +7 -0
- package/fixtures/atelier-capability-contract/package/invalid/document.json +59 -0
- package/fixtures/atelier-capability-contract/package/valid/document.json +58 -0
- package/fixtures/atelier-capability-contract/plan/invalid/document.json +107 -0
- package/fixtures/atelier-capability-contract/plan/valid/document.json +106 -0
- package/fixtures/atelier-capability-contract/release/invalid/document.json +85 -0
- package/fixtures/atelier-capability-contract/release/valid/document.json +84 -0
- package/fixtures/atelier-capability-contract/state/invalid/document.json +316 -0
- package/fixtures/atelier-capability-contract/state/valid/document.json +315 -0
- package/fixtures/atelier-coordination/dependency/invalid/document.json +37 -0
- package/fixtures/atelier-coordination/dependency/valid/document.json +36 -0
- package/fixtures/atelier-coordination/directive/invalid/document.json +25 -0
- package/fixtures/atelier-coordination/directive/valid/document.json +24 -0
- package/fixtures/atelier-coordination/disposition/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/disposition/valid/document.json +33 -0
- package/fixtures/atelier-coordination/outcome/invalid/document.json +39 -0
- package/fixtures/atelier-coordination/outcome/valid/document.json +38 -0
- package/fixtures/atelier-coordination/recovery/invalid/document.json +34 -0
- package/fixtures/atelier-coordination/recovery/valid/document.json +33 -0
- package/fixtures/atelier-harness-contract/handoff/invalid/document.json +25 -0
- package/fixtures/atelier-harness-contract/handoff/valid/document.json +24 -0
- package/fixtures/atelier-ingestion/invalid/authority.json +19 -0
- package/fixtures/atelier-ingestion/semantic-baseline.json +26 -0
- package/fixtures/atelier-ingestion/semantic-operation.json +75 -0
- package/fixtures/atelier-ingestion/valid/plan-input.json +18 -0
- package/fixtures/atelier-inquiry-contract/assessment/invalid/document.json +41 -0
- package/fixtures/atelier-inquiry-contract/assessment/valid/document.json +40 -0
- package/fixtures/atelier-inquiry-contract/bundle/invalid/document.json +51 -0
- package/fixtures/atelier-inquiry-contract/bundle/valid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/campaign/invalid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/campaign/valid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/decision/invalid/document.json +22 -0
- package/fixtures/atelier-inquiry-contract/decision/valid/document.json +21 -0
- package/fixtures/atelier-inquiry-contract/feedback/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/feedback/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/handoff/invalid/document.json +92 -0
- package/fixtures/atelier-inquiry-contract/handoff/valid/document.json +91 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/invalid/document.json +20 -0
- package/fixtures/atelier-inquiry-contract/hypothesis/valid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/ledger/invalid/document.json +680 -0
- package/fixtures/atelier-inquiry-contract/ledger/valid/document.json +679 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/invalid/document.json +19 -0
- package/fixtures/atelier-inquiry-contract/legacy-assessment/valid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/request/invalid/document.json +50 -0
- package/fixtures/atelier-inquiry-contract/request/valid/document.json +49 -0
- package/fixtures/atelier-inquiry-contract/source/invalid/document.json +18 -0
- package/fixtures/atelier-inquiry-contract/source/valid/document.json +17 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-inquiry-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/invalid/document.json +18 -0
- package/fixtures/atelier-instruction-adoption/context-receipt/valid/document.json +17 -0
- package/fixtures/atelier-instruction-adoption/plan/invalid/document.json +63 -0
- package/fixtures/atelier-instruction-adoption/plan/valid/document.json +62 -0
- package/fixtures/atelier-instruction-adoption/receipt/invalid/document.json +22 -0
- package/fixtures/atelier-instruction-adoption/receipt/valid/document.json +21 -0
- package/fixtures/atelier-instruction-adoption/state/invalid/document.json +30 -0
- package/fixtures/atelier-instruction-adoption/state/valid/document.json +29 -0
- package/fixtures/atelier-interaction/act/invalid/document.json +25 -0
- package/fixtures/atelier-interaction/act/valid/document.json +24 -0
- package/fixtures/atelier-interaction/assessment/invalid/document.json +32 -0
- package/fixtures/atelier-interaction/assessment/valid/document.json +31 -0
- package/fixtures/atelier-interaction/observation/invalid/document.json +22 -0
- package/fixtures/atelier-interaction/observation/valid/document.json +21 -0
- package/fixtures/atelier-interaction/policy/invalid/document.json +20 -0
- package/fixtures/atelier-interaction/policy/valid/document.json +19 -0
- package/fixtures/atelier-judgment/case/invalid/document.json +76 -0
- package/fixtures/atelier-judgment/case/valid/document.json +75 -0
- package/fixtures/atelier-judgment/decision/invalid/document.json +38 -0
- package/fixtures/atelier-judgment/decision/valid/document.json +37 -0
- package/fixtures/atelier-judgment/native-decision.md +3 -0
- package/fixtures/atelier-judgment/practice-reconsideration/definition.json +92 -0
- package/fixtures/atelier-judgment/practice-reconsideration/scenario.json +435 -0
- package/fixtures/atelier-knowledge-contract/activation/invalid/document.json +26 -0
- package/fixtures/atelier-knowledge-contract/activation/valid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/contribution/invalid/document.json +34 -0
- package/fixtures/atelier-knowledge-contract/contribution/valid/document.json +33 -0
- package/fixtures/atelier-knowledge-contract/domain/invalid/document.json +41 -0
- package/fixtures/atelier-knowledge-contract/domain/valid/document.json +40 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/invalid/document.json +46 -0
- package/fixtures/atelier-knowledge-contract/domain-revision/valid/document.json +45 -0
- package/fixtures/atelier-knowledge-contract/evaluation/invalid/document.json +21 -0
- package/fixtures/atelier-knowledge-contract/evaluation/valid/document.json +20 -0
- package/fixtures/atelier-knowledge-contract/ledger/invalid/document.json +318 -0
- package/fixtures/atelier-knowledge-contract/ledger/valid/document.json +317 -0
- package/fixtures/atelier-knowledge-contract/relation/invalid/document.json +25 -0
- package/fixtures/atelier-knowledge-contract/relation/valid/document.json +24 -0
- package/fixtures/atelier-knowledge-contract/review/invalid/document.json +18 -0
- package/fixtures/atelier-knowledge-contract/review/valid/document.json +17 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/invalid/document.json +16 -0
- package/fixtures/atelier-knowledge-contract/withdrawal/valid/document.json +15 -0
- package/fixtures/atelier-learning/invalid/authority.json +14 -0
- package/fixtures/atelier-learning/valid/capture.json +13 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/authority-key.json +17 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/nonempty-ext.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/invalid/wrong-schema.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/manifest/valid/manifest.json +22 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/authority-key.json +25 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/unknown-top-level-field.json +27 -0
- package/fixtures/atelier-personal-workspace-contract/overlay/invalid/wrong-schema.json +24 -0
- package/fixtures/atelier-trackable/definition/invalid/document.json +15 -0
- package/fixtures/atelier-trackable/definition/valid/document.json +14 -0
- package/fixtures/atelier-trackable/request/invalid/document.json +23 -0
- package/fixtures/atelier-trackable/request/valid/document.json +22 -0
- package/fixtures/boundary-delegations/invalid/authority-key.v1.json +30 -0
- package/fixtures/boundary-delegations/invalid/push-operation.v1.json +28 -0
- package/fixtures/boundary-delegations/invalid/unsigned.v1.json +25 -0
- package/fixtures/boundary-delegations/valid/empty.v1.json +4 -0
- package/fixtures/boundary-delegations/valid/signed-operator.v1.json +30 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-algorithm.v1.json +15 -0
- package/fixtures/boundary-owner-keys/invalid/wrong-curve.v1.json +15 -0
- package/fixtures/boundary-owner-keys/valid/one-owner.v1.json +17 -0
- package/fixtures/capability-packages/adoption.example.json +70 -0
- package/fixtures/capability-packages/evidence-review/capability-package.json +58 -0
- package/fixtures/capability-packages/evidence-review/capability-release.json +84 -0
- package/fixtures/capability-packages/evidence-review/evaluation.md +15 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/SKILL.md +22 -0
- package/fixtures/capability-packages/evidence-review/skills/evidence-review/references/ledger.md +7 -0
- package/fixtures/capability-packages/research/capability-package.json +88 -0
- package/fixtures/capability-packages/research/capability-release.json +134 -0
- package/fixtures/capability-packages/research/evaluation.md +15 -0
- package/fixtures/capability-packages/research/skills/research-integrate/SKILL.md +29 -0
- package/fixtures/capability-packages/research/skills/research-integrate/references/integration.md +10 -0
- package/fixtures/capability-packages/research/skills/research-plan/SKILL.md +27 -0
- package/fixtures/capability-packages/research/skills/research-plan/references/inquiries.md +26 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/SKILL.md +23 -0
- package/fixtures/capability-packages/research/skills/research-synthesize/references/synthesis.md +12 -0
- package/fixtures/evidence-navigation/invalid/duplicate-operation.json +16 -0
- package/fixtures/evidence-navigation/invalid/instruction-kind.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-digest.json +36 -0
- package/fixtures/evidence-navigation/invalid/invalid-time.json +36 -0
- package/fixtures/evidence-navigation/invalid/missing-attribution.json +35 -0
- package/fixtures/evidence-navigation/invalid/reserved-extension.json +19 -0
- package/fixtures/evidence-navigation/invalid/unattributed-acceptance.json +36 -0
- package/fixtures/evidence-navigation/invalid/uncoerced-version.json +16 -0
- package/fixtures/evidence-navigation/invalid/unknown-currency.json +17 -0
- package/fixtures/evidence-navigation/invalid/unknown-selector-field.json +37 -0
- package/fixtures/evidence-navigation/invalid/unsupported-operation.json +15 -0
- package/fixtures/evidence-navigation/valid/claim.json +36 -0
- package/fixtures/evidence-navigation/valid/host.json +16 -0
- package/fixtures/evidence-navigation/valid/profile.json +16 -0
- package/fixtures/evidence-navigation/valid/snapshot.json +17 -0
- package/fixtures/harness-packages/build-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/build-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/build-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/build-harness/skills/atelier-build-harness/SKILL.md +59 -0
- package/fixtures/harness-packages/knowledge-harness/capability-package.json +61 -0
- package/fixtures/harness-packages/knowledge-harness/capability-release.json +82 -0
- package/fixtures/harness-packages/knowledge-harness/evaluation.md +3 -0
- package/fixtures/harness-packages/knowledge-harness/skills/atelier-knowledge-harness/SKILL.md +60 -0
- package/fixtures/harnesses/learning-cycle.json +780 -0
- package/fixtures/inquiry/workshop.json +675 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/discovery-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/discovery-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/discovery-harness/skills/atelier-discovery-harness/SKILL.md +48 -0
- package/fixtures/inquiry-packages/research-harness/capability-package.json +61 -0
- package/fixtures/inquiry-packages/research-harness/capability-release.json +82 -0
- package/fixtures/inquiry-packages/research-harness/evaluation.md +3 -0
- package/fixtures/inquiry-packages/research-harness/skills/atelier-research-harness/SKILL.md +43 -0
- package/fixtures/knowledge-health/calibration-suite.json +26 -0
- package/fixtures/knowledge-health/calibration-trial.json +37 -0
- package/fixtures/knowledge-health/corrected/checklist.md +18 -0
- package/fixtures/knowledge-health/devday/participant-record.json +19 -0
- package/fixtures/knowledge-health/expected-summary.json +27 -0
- package/fixtures/knowledge-health/workshop/README.md +20 -0
- package/fixtures/knowledge-health/workshop/atelier.project.json +7 -0
- package/fixtures/knowledge-health/workshop/knowledge-plan.json +65 -0
- package/fixtures/knowledge-health/workshop/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workshop/records/workshop.md +18 -0
- package/fixtures/knowledge-health/workshop/repo-access.v1.json +5 -0
- package/fixtures/knowledge-health/workspace/atelier.project.json +18 -0
- package/fixtures/knowledge-health/workspace/knowledge-plan.json +74 -0
- package/fixtures/knowledge-health/workspace/records/checklist.md +18 -0
- package/fixtures/knowledge-health/workspace/records/pilot.md +16 -0
- package/fixtures/knowledge-health/workspace/repo-access.v1.json +9 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/case-only-collision.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/flat-note-path.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/hidden-folder.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/identity-in-the-body.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/link-breaking-name.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-identity-region.v2.json +156 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/missing-layout-version.v2.json +159 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/note-named-like-a-folder.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/unknown-region-kind.v2.json +160 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/invalid/v1-contract-version.v2.json +161 -0
- package/fixtures/obsidian/contracts/generation-manifest-v2/valid/three-notes.v2.json +160 -0
- package/fixtures/obsidian/edits/workspace.json +1 -1
- package/fixtures/obsidian/materialization/expected-full.json +21 -21
- package/fixtures/obsidian/materialization/expected-full.layout-1.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +7 -7
- package/fixtures/obsidian/materialization/expected-scoped.layout-1.json +39 -0
- package/fixtures/personal-workspace/overlay.json +24 -0
- package/fixtures/templates/local-library.v1.json +75 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/invalid.json +16 -0
- package/fixtures/templates/upgrade-contracts/adoption-policy.v2/valid.json +13 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/invalid.json +24 -0
- package/fixtures/templates/upgrade-contracts/migration.v3/valid.json +21 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/invalid.json +60 -0
- package/fixtures/templates/upgrade-contracts/template-adoption.v1/valid.json +57 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/invalid.json +85 -0
- package/fixtures/templates/upgrade-contracts/upgrade-plan.v3/valid.json +82 -0
- package/package.json +64 -6
- package/plugins/obsidian/main.js +444 -0
- package/plugins/obsidian/manifest.json +9 -0
- package/plugins/obsidian/styles.css +22 -0
- package/skills/claude/atelier-build-harness/SKILL.md +59 -0
- package/skills/claude/atelier-coordination/SKILL.md +32 -0
- package/skills/claude/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/claude/atelier-interaction/SKILL.md +34 -0
- package/skills/claude/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/claude/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/claude/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/claude/atelier-research-harness/SKILL.md +43 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +53 -0
- package/skills/claude/atelier-trackables/SKILL.md +40 -0
- package/skills/codex/atelier-build-harness/SKILL.md +59 -0
- package/skills/codex/atelier-coordination/SKILL.md +32 -0
- package/skills/codex/atelier-discovery-harness/SKILL.md +48 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +29 -0
- package/skills/codex/atelier-interaction/SKILL.md +34 -0
- package/skills/codex/atelier-knowledge-harness/SKILL.md +60 -0
- package/skills/codex/atelier-knowledge-setup/SKILL.md +72 -0
- package/skills/codex/atelier-practical-judgment/SKILL.md +64 -0
- package/skills/codex/atelier-research-harness/SKILL.md +43 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +53 -0
- package/skills/codex/atelier-trackables/SKILL.md +40 -0
- package/src/architecture/catalog.mjs +355 -0
- package/src/architecture/index.mjs +93 -0
- package/src/boundary/content-rules.mjs +4 -2
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +52 -10
- package/src/build/git.mjs +18 -0
- package/src/build/index.mjs +3 -0
- package/src/build/ledger.mjs +88 -0
- package/src/capabilities/adoption.mjs +381 -0
- package/src/capabilities/evidence.mjs +140 -0
- package/src/capabilities/files.mjs +124 -0
- package/src/capabilities/index.mjs +4 -0
- package/src/capabilities/instructions.mjs +250 -0
- package/src/capabilities/package.mjs +136 -0
- package/src/cli/command-failure.mjs +36 -0
- package/src/cli/execute-command.mjs +16 -2
- package/src/cli/run.mjs +65 -9
- package/src/coauthor/store.mjs +43 -2
- package/src/collaboration/proposals.mjs +189 -0
- package/src/commands/architecture.mjs +27 -0
- package/src/commands/boundary.mjs +3 -0
- package/src/commands/capability.mjs +73 -0
- package/src/commands/enroll.mjs +3 -0
- package/src/commands/harness.mjs +79 -0
- package/src/commands/ingest.mjs +39 -0
- package/src/commands/init.mjs +4 -1
- package/src/commands/inquiry.mjs +66 -0
- package/src/commands/knowledge-semantic.mjs +128 -0
- package/src/commands/knowledge.mjs +119 -0
- package/src/commands/learn.mjs +61 -0
- package/src/commands/obsidian.mjs +832 -62
- package/src/commands/practice.mjs +24 -0
- package/src/commands/setup.mjs +22 -2
- package/src/commands/trackable.mjs +19 -0
- package/src/contracts/corpus.mjs +36 -0
- package/src/coordination/index.mjs +110 -0
- package/src/egress/forbidden-egress.mjs +2 -0
- package/src/evidence-navigation/contracts.mjs +60 -0
- package/src/evidence-navigation/evaluate.mjs +69 -0
- package/src/evidence-navigation/generation.mjs +22 -0
- package/src/evidence-navigation/index.mjs +2 -0
- package/src/evidence-navigation/local.mjs +167 -0
- package/src/evidence-navigation/schema.generated.mjs +604 -0
- package/src/evidence-navigation/types.mjs +12 -0
- package/src/export/atelier-export-contract.mjs +35 -8
- package/src/graph/enroll.mjs +248 -0
- package/src/graph/graph.mjs +17 -1
- package/src/graph/knowledge-graph.mjs +58 -8
- package/src/harnesses/contracts.mjs +41 -0
- package/src/harnesses/exchange.mjs +154 -0
- package/src/harnesses/feedback.mjs +16 -0
- package/src/harnesses/history.mjs +37 -0
- package/src/harnesses/index.mjs +4 -0
- package/src/harnesses/store.mjs +29 -0
- package/src/ingestion/contracts.mjs +41 -0
- package/src/ingestion/evaluation.mjs +119 -0
- package/src/ingestion/optional/composition/index.mjs +171 -0
- package/src/ingestion/optional/evaluation/index.mjs +219 -0
- package/src/ingestion/optional/graphify/composition-binding.mjs +60 -0
- package/src/ingestion/optional/graphify/composition_receiving.py +185 -0
- package/src/ingestion/optional/graphify/graphify_supplier.py +933 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/query_receiving.py +209 -0
- package/src/ingestion/optional/index.mjs +9 -0
- package/src/ingestion/optional/jev/bridge.mjs +166 -0
- package/src/ingestion/optional/jev/composition-binding.mjs +116 -0
- package/src/ingestion/optional/jev/judgments.mjs +174 -0
- package/src/ingestion/optional/jev/native-translation.mjs +32 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/__init__.py +7 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/adapter.py +966 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/artifacts.py +101 -0
- package/src/ingestion/optional/lightrag/atelier_lightrag_supplier/projection.py +146 -0
- package/src/ingestion/optional/lightrag/composition-binding.mjs +67 -0
- package/src/ingestion/optional/vanilla/composition-binding.mjs +102 -0
- package/src/ingestion/optional/vanilla/index.mjs +218 -0
- package/src/ingestion/optional/vanilla/native-normalization.mjs +187 -0
- package/src/ingestion/processors.mjs +218 -0
- package/src/ingestion/semantic.mjs +203 -0
- package/src/ingestion/store.mjs +372 -0
- package/src/inquiry/beliefs.mjs +32 -0
- package/src/inquiry/contracts.mjs +23 -0
- package/src/inquiry/index.mjs +5 -0
- package/src/inquiry/ledger.mjs +135 -0
- package/src/inquiry/lenses.mjs +15 -0
- package/src/inquiry/projection.mjs +77 -0
- package/src/inquiry/store.mjs +35 -0
- package/src/intake/read-scope.mjs +12 -0
- package/src/intake/store.mjs +69 -6
- package/src/interaction/index.mjs +91 -0
- package/src/judgment/index.mjs +82 -0
- package/src/judgment/practice-evaluation.mjs +70 -0
- package/src/judgment/practice.mjs +144 -0
- package/src/knowledge/context.mjs +217 -0
- package/src/knowledge/index.mjs +5 -0
- package/src/knowledge/ingestion.mjs +79 -0
- package/src/knowledge/intake.mjs +33 -0
- package/src/knowledge/ledger.mjs +110 -0
- package/src/knowledge/participatory/guidance.mjs +36 -0
- package/src/knowledge/participatory/guided-contribution.mjs +44 -0
- package/src/knowledge/participatory/index.mjs +2 -0
- package/src/knowledge/participatory/installed-workshop.mjs +108 -0
- package/src/knowledge/plan.mjs +67 -0
- package/src/knowledge/projection.mjs +43 -0
- package/src/knowledge/semantic-operation.mjs +567 -0
- package/src/knowledge/sessions.mjs +467 -0
- package/src/knowledge/workspace.mjs +295 -0
- package/src/knowledge-health/assessment.mjs +165 -0
- package/src/knowledge-health/capabilities.mjs +350 -0
- package/src/knowledge-health/context.mjs +62 -0
- package/src/knowledge-health/example.mjs +47 -0
- package/src/knowledge-health/index.mjs +10 -0
- package/src/knowledge-health/owner-join.mjs +88 -0
- package/src/knowledge-health/proof/common.mjs +24 -0
- package/src/knowledge-health/proof/installed-check.mjs +100 -0
- package/src/knowledge-health/proof/native-readback.mjs +37 -0
- package/src/knowledge-health/proof/rehearse.mjs +68 -0
- package/src/knowledge-health/proof/validate-participant.mjs +17 -0
- package/src/knowledge-health/proof/verify-native-capture.mjs +14 -0
- package/src/knowledge-health/proof/workshop-check.mjs +36 -0
- package/src/knowledge-health/rule-identity.mjs +44 -0
- package/src/knowledge-health/workshop-composition.mjs +19 -0
- package/src/knowledge-health/workshop-profile.mjs +176 -0
- package/src/knowledge-health/workshop.mjs +99 -0
- package/src/learning/adapters.mjs +68 -0
- package/src/learning/assistance.mjs +127 -0
- package/src/learning/contracts.mjs +65 -0
- package/src/learning/store.mjs +272 -0
- package/src/personal-workspace/index.mjs +391 -0
- package/src/personal-workspace/selection.mjs +385 -0
- package/src/project/config.mjs +8 -0
- package/src/project/durable-state.mjs +3 -1
- package/src/project/file-class.mjs +1 -1
- package/src/project/private-state.mjs +10 -1
- package/src/projection/obsidian/contracts.mjs +224 -18
- package/src/projection/obsidian/edits/apply.mjs +63 -19
- package/src/projection/obsidian/edits/regions.mjs +137 -42
- package/src/projection/obsidian/materialize/index.mjs +6 -3
- package/src/projection/obsidian/materialize/path-registry.mjs +443 -57
- package/src/projection/obsidian/materialize/prepare-view.mjs +382 -131
- package/src/projection/obsidian/materialize/redaction.mjs +327 -0
- package/src/projection/obsidian/materialize/settings.mjs +113 -34
- package/src/projection/obsidian/personal-workspace.mjs +436 -0
- package/src/projection/obsidian/plugin-bridge/bundle.mjs +50 -0
- package/src/projection/obsidian/plugin-bridge/channel.mjs +174 -0
- package/src/projection/obsidian/plugin-bridge/index.mjs +10 -0
- package/src/projection/obsidian/proposals/adapter.mjs +47 -8
- package/src/projection/obsidian/proposals/observation.mjs +20 -7
- package/src/projection/obsidian/publication/bridge-script.mjs +11 -5
- package/src/projection/obsidian/publication/index.mjs +2 -1
- package/src/projection/obsidian/publication/publisher.mjs +312 -36
- package/src/projection/obsidian/publication/transport.mjs +63 -8
- package/src/projection/obsidian/publication/vault-list.mjs +200 -0
- package/src/projection/obsidian/recovery/index.mjs +4 -1
- package/src/projection/obsidian/recovery/restart.mjs +3 -1
- package/src/projection/obsidian/recovery/store.mjs +168 -8
- package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
- package/src/projection/obsidian/selection-ui/focus.mjs +14 -7
- package/src/projection/obsidian/selection-ui/selection-state.mjs +2 -2
- package/src/projection/obsidian/selection-ui/selection.mjs +8 -3
- package/src/readiness/readiness.mjs +10 -3
- package/src/reflection/index.mjs +56 -0
- package/src/runtime/obsidian/app-capability.mjs +104 -12
- package/src/runtime/obsidian/app-production-seams.mjs +161 -42
- package/src/runtime/obsidian/app-registration.mjs +313 -0
- package/src/runtime/obsidian/app-restart.mjs +154 -0
- package/src/runtime/obsidian/engine.mjs +288 -29
- package/src/runtime/obsidian/extension-points.mjs +1 -1
- package/src/runtime/obsidian/index.mjs +15 -8
- package/src/runtime/obsidian/launch-plan.mjs +91 -0
- package/src/runtime/obsidian/lifecycle.mjs +193 -22
- package/src/runtime/obsidian/login-item.mjs +332 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/opening.mjs +369 -63
- package/src/runtime/obsidian/pending-edits.mjs +10 -0
- package/src/runtime/obsidian/pipeline.mjs +71 -3
- package/src/runtime/obsidian/plugin-channel.mjs +289 -0
- package/src/runtime/obsidian/plugin-choice.mjs +182 -0
- package/src/runtime/obsidian/plugin-drift.mjs +124 -0
- package/src/runtime/obsidian/plugin-presence.mjs +61 -0
- package/src/runtime/obsidian/process-identity.mjs +20 -10
- package/src/runtime/obsidian/project-views.mjs +282 -0
- package/src/runtime/obsidian/questions.mjs +48 -0
- package/src/runtime/obsidian/release-watch.mjs +56 -0
- package/src/runtime/obsidian/service-main.mjs +113 -17
- package/src/runtime/obsidian/service-manager-production.mjs +61 -0
- package/src/runtime/obsidian/service-managers.mjs +275 -0
- package/src/runtime/obsidian/service-record.mjs +105 -5
- package/src/runtime/obsidian/service-server.mjs +50 -12
- package/src/runtime/obsidian/service.mjs +134 -7
- package/src/runtime/obsidian/startup-adapters.mjs +59 -21
- package/src/runtime/obsidian/state-store.mjs +13 -1
- package/src/runtime/obsidian/unheld-evidence.mjs +84 -0
- package/src/runtime/obsidian/vault-location.mjs +300 -0
- package/src/runtime/obsidian/view-counts.mjs +34 -0
- package/src/server/local-sidecar.mjs +193 -7
- package/src/server/server.mjs +45 -6
- package/src/template-bindings/project.mjs +92 -0
- package/src/templates/profile.mjs +225 -0
- package/src/trackables/domain.mjs +175 -0
- package/src/trackables/store.mjs +114 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/explanation.mjs +1 -1
- package/src/upgrade/template-participant.mjs +171 -0
- package/src/upgrade/transaction.mjs +72 -25
- package/src/upgrade/upgrade.mjs +2 -1
- package/templates/external-project-workspace/pack-lifecycle.json +6 -1
- package/templates/knowledge-workspace/.gitattributes +2 -0
- package/templates/knowledge-workspace/AGENTS.md +27 -0
- package/templates/knowledge-workspace/README.md +44 -0
- package/templates/knowledge-workspace/atelier.project.json +22 -0
- package/templates/knowledge-workspace/gitignore +6 -0
- package/templates/knowledge-workspace/knowledge-plan.json +111 -0
- package/templates/knowledge-workspace/knowledge-plan.schema.json +281 -0
- package/templates/knowledge-workspace/records/forecast.md +22 -0
- package/templates/knowledge-workspace/records/inspection.md +25 -0
- package/templates/knowledge-workspace/records/telescope.md +24 -0
- 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 {
|
|
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
|
|
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
|
-
//
|
|
27
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
56
|
-
|
|
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(
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
+
}
|