@mnstry/atelier 0.2.0-alpha.12 → 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 +661 -0
- package/README.md +142 -6
- 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-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/docs/architecture.md +89 -0
- package/docs/assurance-controls.md +2 -2
- 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 +181 -16
- package/docs/obsidian-contract.md +223 -15
- package/docs/obsidian-plugin.md +38 -4
- package/docs/obsidian.md +613 -51
- 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 +49 -0
- 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 +47 -2
- 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/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 +59 -5
- 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 +754 -63
- 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 +30 -0
- package/src/coordination/index.mjs +110 -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/projection/obsidian/edits/apply.mjs +55 -16
- package/src/projection/obsidian/personal-workspace.mjs +436 -0
- package/src/projection/obsidian/proposals/adapter.mjs +47 -8
- package/src/projection/obsidian/proposals/observation.mjs +10 -2
- package/src/projection/obsidian/publication/publisher.mjs +142 -13
- package/src/projection/obsidian/publication/vault-list.mjs +4 -3
- package/src/projection/obsidian/recovery/index.mjs +4 -1
- package/src/projection/obsidian/recovery/store.mjs +160 -7
- package/src/projection/obsidian/selection-ui/contribution.mjs +6 -5
- package/src/readiness/readiness.mjs +10 -3
- package/src/reflection/index.mjs +56 -0
- package/src/runtime/obsidian/app-production-seams.mjs +48 -16
- package/src/runtime/obsidian/app-registration.mjs +175 -28
- package/src/runtime/obsidian/app-restart.mjs +154 -0
- package/src/runtime/obsidian/engine.mjs +163 -20
- package/src/runtime/obsidian/extension-points.mjs +1 -1
- package/src/runtime/obsidian/index.mjs +10 -7
- package/src/runtime/obsidian/launch-plan.mjs +7 -4
- package/src/runtime/obsidian/lifecycle.mjs +92 -12
- package/src/runtime/obsidian/login-item.mjs +332 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/opening.mjs +171 -37
- package/src/runtime/obsidian/pipeline.mjs +71 -3
- package/src/runtime/obsidian/plugin-channel.mjs +11 -3
- package/src/runtime/obsidian/plugin-choice.mjs +16 -4
- package/src/runtime/obsidian/plugin-drift.mjs +59 -11
- package/src/runtime/obsidian/plugin-presence.mjs +14 -1
- 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 +99 -16
- 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 +87 -23
- package/src/runtime/obsidian/service.mjs +36 -3
- package/src/runtime/obsidian/startup-adapters.mjs +59 -21
- 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 +169 -2
- package/src/server/server.mjs +28 -2
- 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 +1 -0
- 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
|
@@ -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
|
+
}
|
|
@@ -11,11 +11,14 @@ import { LOOPBACK_HOSTS } from './service-client.mjs'
|
|
|
11
11
|
// Private documents of the maintenance service of one workspace, under
|
|
12
12
|
//
|
|
13
13
|
// <data>/obsidian/<workspace-id>/state/service/
|
|
14
|
-
// runtime.json
|
|
15
|
-
// settings.json
|
|
16
|
-
// last-error.json
|
|
17
|
-
//
|
|
18
|
-
//
|
|
14
|
+
// runtime.json the adapter record of the running service (service-state v1)
|
|
15
|
+
// settings.json what this machine chose: loopback host, port, startup consent
|
|
16
|
+
// last-error.json the last tick that failed for a reason nobody typed
|
|
17
|
+
// last-startup.json how the last start by the login item ended: started, or the refusal's code
|
|
18
|
+
// login-item.json the installed login item (login-item.mjs)
|
|
19
|
+
// service.log the operational log of the service process
|
|
20
|
+
// login-item.log what a login item's service printed before it could open service.log
|
|
21
|
+
// start-lock/ serializes `start` for this workspace
|
|
19
22
|
//
|
|
20
23
|
// Owner-only, replaced atomically, outside every repository and every vault.
|
|
21
24
|
// Every document is validated on every read. One that does not validate, or
|
|
@@ -24,7 +27,9 @@ import { LOOPBACK_HOSTS } from './service-client.mjs'
|
|
|
24
27
|
|
|
25
28
|
export const SERVICE_SETTINGS_SCHEMA = 'atelier-obsidian-service-settings/v1'
|
|
26
29
|
export const SERVICE_ERROR_SCHEMA = 'atelier-obsidian-service-last-error/v1'
|
|
30
|
+
export const LAST_STARTUP_SCHEMA = 'atelier-obsidian-last-startup/v1'
|
|
27
31
|
export const CONSENT_COVERAGES = Object.freeze(['service', 'service-and-startup'])
|
|
32
|
+
export const STARTUP_OUTCOMES = Object.freeze(['started', 'refused'])
|
|
28
33
|
|
|
29
34
|
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
30
35
|
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
@@ -41,6 +46,7 @@ export function servicePaths(workspaceRoot) {
|
|
|
41
46
|
return {
|
|
42
47
|
stateLocation: path.join(workspaceRoot, 'state'), directory, record: path.join(directory, 'runtime.json'), settings: path.join(directory, 'settings.json'),
|
|
43
48
|
lastError: path.join(directory, 'last-error.json'), log: path.join(directory, 'service.log'), startLock: path.join(directory, 'start-lock'),
|
|
49
|
+
lastStartup: path.join(directory, 'last-startup.json'), loginItem: path.join(directory, 'login-item.json'), loginItemLog: path.join(directory, 'login-item.log'),
|
|
44
50
|
}
|
|
45
51
|
}
|
|
46
52
|
|
|
@@ -73,29 +79,60 @@ const releases = new Map()
|
|
|
73
79
|
// maintains; outside those it reads nothing else of the package but the
|
|
74
80
|
// version in `package.json`. A release that changes any of it, and not only
|
|
75
81
|
// the entry, differs. A package without `plugins/` reads as one with an empty
|
|
76
|
-
// one.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
82
|
+
// one.
|
|
83
|
+
|
|
84
|
+
// Every regular file a release identity covers, in a fixed order: `visit(relative, absolute)`. A missing `src/` or
|
|
85
|
+
// `contracts/` throws (the package cannot be read); a missing `plugins/` is empty.
|
|
86
|
+
export function walkRelease(root, visit) {
|
|
87
|
+
const walk = (relative, { optional = false } = {}) => {
|
|
88
|
+
let entries
|
|
89
|
+
try { entries = fs.readdirSync(path.join(root, relative), { withFileTypes: true }) } catch (error) { if (optional && error.code === 'ENOENT') return; throw error }
|
|
90
|
+
for (const entry of entries.sort(byName)) {
|
|
91
|
+
const child = `${relative}/${entry.name}`
|
|
92
|
+
if (entry.isDirectory()) walk(child)
|
|
93
|
+
else if (entry.isFile()) visit(child, path.join(root, child))
|
|
89
94
|
}
|
|
90
|
-
for (const part of ['src', 'contracts']) walk(part)
|
|
91
|
-
walk('plugins', { optional: true })
|
|
92
|
-
let version = null
|
|
93
|
-
try { const read = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version; version = typeof read === 'string' ? read : null } catch { version = null }
|
|
94
|
-
releases.set(root, Object.freeze({ version, digest: `sha256:${hash.digest('hex')}` }))
|
|
95
95
|
}
|
|
96
|
+
for (const part of ['src', 'contracts']) walk(part)
|
|
97
|
+
walk('plugins', { optional: true })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// The release identity of the package at `root`, read now and never cached: { version, digest }.
|
|
101
|
+
export function readReleaseIdentity({ root }) {
|
|
102
|
+
if (typeof root !== 'string' || !path.isAbsolute(root)) throw new TypeError('a release identity is read from the absolute root of a package')
|
|
103
|
+
const hash = createHash('sha256')
|
|
104
|
+
walkRelease(root, (relative, absolute) => { hash.update(`${relative}\0${createHash('sha256').update(fs.readFileSync(absolute)).digest('hex')}\n`) })
|
|
105
|
+
let version = null
|
|
106
|
+
try { const read = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).version; version = typeof read === 'string' ? read : null } catch { version = null }
|
|
107
|
+
return Object.freeze({ version, digest: `sha256:${hash.digest('hex')}` })
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// readReleaseIdentity, computed once per process and package root, so a service keeps the identity of the code it
|
|
111
|
+
// loaded.
|
|
112
|
+
export function releaseIdentity({ root = PACKAGE_ROOT } = {}) {
|
|
113
|
+
if (!releases.has(root)) releases.set(root, readReleaseIdentity({ root }))
|
|
96
114
|
return releases.get(root)
|
|
97
115
|
}
|
|
98
116
|
|
|
117
|
+
const ENTRY_IN_PACKAGE = Object.freeze(['src', 'runtime', 'obsidian', 'service-main.mjs'])
|
|
118
|
+
|
|
119
|
+
// The root of the package whose service entry is `entryPath` (`<root>/src/runtime/obsidian/service-main.mjs`), as that
|
|
120
|
+
// path names it, without resolving links; null for any other entry.
|
|
121
|
+
export function packageRootOfEntry(entryPath) {
|
|
122
|
+
if (typeof entryPath !== 'string' || !path.isAbsolute(entryPath)) return null
|
|
123
|
+
const parts = path.resolve(entryPath).split(path.sep)
|
|
124
|
+
if (parts.length <= ENTRY_IN_PACKAGE.length || ENTRY_IN_PACKAGE.some((part, index) => parts[parts.length - ENTRY_IN_PACKAGE.length + index] !== part)) return null
|
|
125
|
+
return parts.slice(0, parts.length - ENTRY_IN_PACKAGE.length).join(path.sep) || path.sep
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The release that starting `entryPath` would run, read now from the package that path names (so a link pointed
|
|
129
|
+
// elsewhere, or a `file:` install, is the package it leads to now); for an entry that is not a package's service entry,
|
|
130
|
+
// this package's own, as it was loaded.
|
|
131
|
+
export function releaseOfEntry(entryPath) {
|
|
132
|
+
const root = packageRootOfEntry(entryPath)
|
|
133
|
+
return root === null ? releaseIdentity() : readReleaseIdentity({ root })
|
|
134
|
+
}
|
|
135
|
+
|
|
99
136
|
// ---------------------------------------------------------------------------
|
|
100
137
|
// The adapter record
|
|
101
138
|
// ---------------------------------------------------------------------------
|
|
@@ -195,6 +232,33 @@ export function writeLastServiceError({ workspaceRoot, workspaceId, document })
|
|
|
195
232
|
return document
|
|
196
233
|
}
|
|
197
234
|
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// How the last start by a login item ended
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
// { schema, workspaceId, at, outcome: 'started' | 'refused', code }: the code of the refusal, null for a start. Written by
|
|
240
|
+
// the service when it runs with `--startup`, so a login item that exits cleanly on a refusal (and is therefore not
|
|
241
|
+
// started again in a loop) still says why.
|
|
242
|
+
function validateLastStartup(document, workspaceId) {
|
|
243
|
+
const code = 'invalid-service-last-startup'
|
|
244
|
+
closedObject(document, { required: ['schema', 'workspaceId', 'at', 'outcome', 'code'] }, code, 'the last startup')
|
|
245
|
+
const ok = document.schema === LAST_STARTUP_SCHEMA && document.workspaceId === workspaceId && TIMESTAMP.test(document.at) && STARTUP_OUTCOMES.includes(document.outcome)
|
|
246
|
+
&& (document.outcome === 'started' ? document.code === null : typeof document.code === 'string' && /^[A-Za-z0-9_.:-]{1,64}$/.test(document.code))
|
|
247
|
+
if (!ok) refuse(code, 'the last startup is malformed')
|
|
248
|
+
return document
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function readLastStartup({ workspaceRoot, workspaceId }) {
|
|
252
|
+
const document = readJson(servicePaths(workspaceRoot).lastStartup, 'invalid-service-last-startup', 'the last startup')
|
|
253
|
+
return document === null ? null : validateLastStartup(document, workspaceId)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function writeLastStartup({ workspaceRoot, workspaceId, document }) {
|
|
257
|
+
validateLastStartup(document, workspaceId)
|
|
258
|
+
atomicReplacePrivateText(path.join(serviceDirectory(workspaceRoot), 'last-startup.json'), canonicalJson(document))
|
|
259
|
+
return document
|
|
260
|
+
}
|
|
261
|
+
|
|
198
262
|
// The log is bounded: past the ceiling it becomes the one previous log, and a new one begins.
|
|
199
263
|
export const SERVICE_LOG_MAX_BYTES = 1024 * 1024
|
|
200
264
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes as cryptoRandomBytes } from 'node:crypto'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
3
4
|
import { AtelierDiagnosticError } from '../../project/config.mjs'
|
|
4
5
|
import { ObsidianContractRefusal } from '../../projection/obsidian/contracts.mjs'
|
|
5
6
|
import { prepareView as productionPrepareView } from '../../projection/obsidian/materialize/index.mjs'
|
|
@@ -48,9 +49,16 @@ import { createFsWatcherFactory } from './watchers.mjs'
|
|
|
48
49
|
//
|
|
49
50
|
// `adapterFactory` has no default here either: whoever starts the service
|
|
50
51
|
// decides whether it may reach a running app.
|
|
52
|
+
//
|
|
53
|
+
// Started by a login item (`startup`), the service also asks `releaseWatch`
|
|
54
|
+
// after every tick whether the package it runs from changed on disk; when it
|
|
55
|
+
// did, it finishes that tick and shuts down with the reason RELEASE_CHANGED,
|
|
56
|
+
// which its process turns into an exit code the service manager restarts, on
|
|
57
|
+
// the new release (release-watch.mjs, service-main.mjs).
|
|
51
58
|
|
|
52
59
|
export const SERVICE_STATUS_SCHEMA = 'atelier-obsidian-service-status/v1'
|
|
53
60
|
export const DEFAULT_SHUTDOWN_GRACE_MS = 30 * 1000
|
|
61
|
+
export const RELEASE_CHANGED = 'release-changed'
|
|
54
62
|
|
|
55
63
|
// Where a workspace keeps its private state, without creating an identity.
|
|
56
64
|
export function resolveServiceWorkspace({ project, dataRoot, env = process.env, platform = process.platform, create = false }) {
|
|
@@ -85,6 +93,11 @@ export async function runMaintenanceService(options = {}) {
|
|
|
85
93
|
engineOptions = {}, createEngine = createMaintenanceEngine,
|
|
86
94
|
// What the adapter factory last learned about the installed app, when it qualifies one. Codes and versions only.
|
|
87
95
|
appStatus = null,
|
|
96
|
+
// Under `startup`: { changed() } (createReleaseWatch), asked after every tick.
|
|
97
|
+
releaseWatch = null,
|
|
98
|
+
// The entry as this process was started by it (`process.argv[1]`), when that is not its real path: a login item
|
|
99
|
+
// names the installed entry by its path. It is recorded, so the process table can still prove this process ours.
|
|
100
|
+
invokedAs = null,
|
|
88
101
|
} = options
|
|
89
102
|
if (typeof loadProject !== 'function') throw new TypeError('the service needs loadProject')
|
|
90
103
|
if (typeof adapterFactory !== 'function') throw new TypeError('the service needs an adapterFactory')
|
|
@@ -107,6 +120,7 @@ export async function runMaintenanceService(options = {}) {
|
|
|
107
120
|
}
|
|
108
121
|
|
|
109
122
|
const executable = executableIdentity(entryPath)
|
|
123
|
+
const alias = typeof invokedAs === 'string' && path.isAbsolute(invokedAs) && path.resolve(invokedAs) !== executable.path ? path.resolve(invokedAs) : null
|
|
110
124
|
const startedAt = isoTime(clock)
|
|
111
125
|
const identity = { serviceName: serviceNameFor(workspaceId), workspaceId, runtimeId, pid, host, port, executableDigest: executable.digest, startedAt }
|
|
112
126
|
const bearer = randomBytes(32).toString('base64url')
|
|
@@ -126,7 +140,7 @@ export async function runMaintenanceService(options = {}) {
|
|
|
126
140
|
// view without the plugin for this tick; the view itself is prepared as ever.
|
|
127
141
|
const pluginFor = (scopeId) => {
|
|
128
142
|
try {
|
|
129
|
-
const vaultRoot = viewVaultRoot(workspaceRoot, scopeId)
|
|
143
|
+
const vaultRoot = viewVaultRoot(workspaceRoot, scopeId, workspaceId)
|
|
130
144
|
const { choice, community } = decidePluginChoice({ workspaceRoot, workspaceId, scopeId, vaultRoot, clock })
|
|
131
145
|
const off = choice.state === 'off'
|
|
132
146
|
const bearer = ensurePluginBearer({ workspaceRoot, workspaceId, scopeId, randomBytes, clock })
|
|
@@ -193,6 +207,18 @@ export async function runMaintenanceService(options = {}) {
|
|
|
193
207
|
log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
|
|
194
208
|
}
|
|
195
209
|
}
|
|
210
|
+
// What a publication of this tick could not change is no drift to ask about again (plugin-drift.mjs).
|
|
211
|
+
const settleDriftLooks = () => {
|
|
212
|
+
try { pluginDrift.settle([...pluginChannel.bearers().keys()]) } catch (error) {
|
|
213
|
+
log({ at: isoTime(clock), event: 'plugin-drift-not-read', code: errorCode(error), name: errorName(error) })
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// A package that changed on disk: another release, loaded module by module into this process from then on.
|
|
218
|
+
const releaseChanged = () => {
|
|
219
|
+
if (!startup || releaseWatch === null) return false
|
|
220
|
+
try { return releaseWatch.changed() === true } catch { return false }
|
|
221
|
+
}
|
|
196
222
|
|
|
197
223
|
const loop = createTickLoop({
|
|
198
224
|
intervalMs, maxBackoffMs, onOutcome: recordOutcome,
|
|
@@ -200,7 +226,14 @@ export async function runMaintenanceService(options = {}) {
|
|
|
200
226
|
// The service runs only while its record names it: a replaced or removed record ends it, cleanly.
|
|
201
227
|
if (!recordIsOurs()) { void shutdown('record-no-longer-names-this-runtime'); return { state: 'stopping', reason: 'record-no-longer-names-this-runtime' } }
|
|
202
228
|
askForDriftedViews()
|
|
203
|
-
|
|
229
|
+
try {
|
|
230
|
+
const report = await engine.tick()
|
|
231
|
+
if (report?.state === 'ticked') settleDriftLooks()
|
|
232
|
+
return report
|
|
233
|
+
} finally {
|
|
234
|
+
// After the tick, whether it succeeded or not: the next one runs in a process of the release now on disk.
|
|
235
|
+
if (!stopping && releaseChanged()) { log({ at: isoTime(clock), event: RELEASE_CHANGED }); void shutdown(RELEASE_CHANGED) }
|
|
236
|
+
}
|
|
204
237
|
},
|
|
205
238
|
})
|
|
206
239
|
|
|
@@ -296,7 +329,7 @@ export async function runMaintenanceService(options = {}) {
|
|
|
296
329
|
workspaceRoot, workspaceId,
|
|
297
330
|
record: {
|
|
298
331
|
schema: 'atelier-obsidian-service-state/v1', contractVersion: '1.0.0', workspaceId, serviceName: identity.serviceName, host, port, runtimeId, pid,
|
|
299
|
-
executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath, release: releaseIdentity() } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
|
|
332
|
+
executable: { path: executable.path, digest: executable.digest, ext: { runner: process.execPath, release: releaseIdentity(), ...(alias === null ? {} : { invokedAs: alias }) } }, stateLocation: servicePaths(workspaceRoot).stateLocation,
|
|
300
333
|
health: { status: 'healthy', checkedAt: startedAt }, consent: settings.consent, ext: { bearer },
|
|
301
334
|
},
|
|
302
335
|
})
|