@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
|
@@ -1,25 +1,39 @@
|
|
|
1
|
-
import { execFile, spawnSync } from 'node:child_process'
|
|
1
|
+
import { execFile, execFileSync, spawnSync } from 'node:child_process'
|
|
2
2
|
import fs from 'node:fs'
|
|
3
|
+
import { performance } from 'node:perf_hooks'
|
|
3
4
|
import path from 'node:path'
|
|
4
|
-
import { defaultCliPath, defaultObsidianProcessProbe } from '../../projection/obsidian/publication/transport.mjs'
|
|
5
|
-
import {
|
|
5
|
+
import { NEUTRAL_DIRECTORY, defaultCliPath, defaultObsidianProcessProbe, routedCall } from '../../projection/obsidian/publication/transport.mjs'
|
|
6
|
+
import { obsidianSandboxedBuild, obsidianUserDataDir, readObsidianSettings } from '../../projection/obsidian/publication/vault-list.mjs'
|
|
7
|
+
import { realPathAsStored } from '../../project/private-state.mjs'
|
|
8
|
+
import { appAnswered, readEvalAnswer, readVersionAnswer } from './app-capability.mjs'
|
|
9
|
+
import { backupObsidianSettings, registerVaultInObsidianSettings } from './app-registration.mjs'
|
|
10
|
+
import { createAppQuitter } from './app-restart.mjs'
|
|
11
|
+
import { launchPlan, runLaunchPlan, urlProcessed } from './launch-plan.mjs'
|
|
6
12
|
|
|
7
13
|
// The production seams of `obsidian open` and of the service's adapter
|
|
8
|
-
// factory: the only code that asks the installed Obsidian anything
|
|
9
|
-
// operating system to open it.
|
|
14
|
+
// factory: the only code that asks the installed Obsidian anything, asks the
|
|
15
|
+
// operating system to open it, or quits it (for a restart a person asked for).
|
|
10
16
|
//
|
|
11
17
|
// This module is imported by exactly two places, both real command-line
|
|
12
18
|
// entries and both behind an explicit `--adapter=obsidian-cli`: the service
|
|
13
19
|
// entry (service-main.mjs) and the command entry (src/commands/obsidian.mjs,
|
|
14
|
-
// `production: true`). No test imports it in-process, and
|
|
15
|
-
// a trace below that the test suite asserts is absent.
|
|
16
|
-
//
|
|
17
|
-
//
|
|
20
|
+
// `production: true`). No test of the default suite imports it in-process, and
|
|
21
|
+
// importing it leaves a trace below that the test suite asserts is absent.
|
|
22
|
+
// Tests import it in child processes only, with a private HOME: with a
|
|
23
|
+
// stand-in for the command-line tool, to read the answers it parses, and with
|
|
24
|
+
// a stand-in execFile, to check the launcher's executors; the opt-in real-app
|
|
25
|
+
// suite imports it to reach an isolated app through that app's private HOME.
|
|
18
26
|
//
|
|
19
|
-
// Status:
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
27
|
+
// Status: exercised against an isolated Obsidian 1.13.7 (installer 1.12.7) on
|
|
28
|
+
// macOS by the opt-in real-app suite in test/obsidian-first-open-real-app.test.mjs;
|
|
29
|
+
// every answer it cannot establish is the failing one.
|
|
30
|
+
//
|
|
31
|
+
// Where each call runs matters: the app answers a command in the window of the
|
|
32
|
+
// vault its list routes the call to, opening that vault when it is closed (see
|
|
33
|
+
// vault-list.mjs). A call about one vault goes where its route says, in its
|
|
34
|
+
// folder or naming its id, and is not made without a route that reaches only
|
|
35
|
+
// that vault; every other call runs in a directory that is no vault, so the
|
|
36
|
+
// directory this process was started in never picks, or opens, a vault.
|
|
23
37
|
globalThis[Symbol.for('mnstry.atelier.obsidian.production-seams-loaded')] = true
|
|
24
38
|
|
|
25
39
|
const CLI_TIMEOUT_MS = 5000
|
|
@@ -34,74 +48,179 @@ function cliExists(cliPath) {
|
|
|
34
48
|
try { return fs.statSync(cliPath).isFile() } catch { return false }
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
const NOT_ASKED = Object.freeze({ version: null, noVaultOpen: false })
|
|
51
|
+
const NOT_ASKED = Object.freeze({ version: null, noVaultOpen: false, cliOff: false })
|
|
38
52
|
|
|
39
53
|
const observationOf = ({ platform, cliPath, processes, answer }) => {
|
|
40
54
|
const appPath = installedAppPath(platform)
|
|
41
55
|
const present = cliExists(cliPath)
|
|
42
|
-
const answered = answer.version !== null || answer.noVaultOpen
|
|
56
|
+
const answered = answer.version !== null || answer.noVaultOpen || answer.cliOff === true
|
|
43
57
|
return {
|
|
44
58
|
installed: appPath === null ? (present !== false && (answered || present === true)) : fs.existsSync(appPath),
|
|
45
59
|
cli: present === true || answered,
|
|
46
60
|
running: processes === 'running' ? true : processes === 'absent' ? false : null,
|
|
47
61
|
version: answer.version,
|
|
48
62
|
...(answer.noVaultOpen ? { noVaultOpen: true } : {}),
|
|
63
|
+
...(answer.cliOff === true ? { cliOff: true } : {}),
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
|
|
52
|
-
|
|
53
|
-
|
|
67
|
+
// `processProbe` reads the process table ('running', 'absent' or 'unknown');
|
|
68
|
+
// `workingDirectory` is where calls that are about no vault run.
|
|
69
|
+
export function createProductionAppProbe({ platform = process.platform, env = process.env, cliPath = defaultCliPath(platform), processProbe = () => defaultObsidianProcessProbe({ platform }), workingDirectory = NEUTRAL_DIRECTORY } = {}) {
|
|
70
|
+
const options = { env, cwd: workingDirectory, timeout: CLI_TIMEOUT_MS, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 1024 * 1024 }
|
|
71
|
+
const processes = () => { try { return processProbe() } catch { return 'unknown' } }
|
|
54
72
|
return {
|
|
55
73
|
// For the service's adapter factory, which is synchronous. The version is asked for only while an app runs.
|
|
56
74
|
// Both output streams are read, whatever the exit status: the answer with no vault open is not a version.
|
|
57
75
|
inspectSync() {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
76
|
+
const seen = processes()
|
|
77
|
+
if (seen === 'absent') return observationOf({ platform, cliPath, processes: seen, answer: NOT_ASKED })
|
|
60
78
|
let answer = NOT_ASKED
|
|
61
79
|
try {
|
|
62
80
|
const reply = spawnSync(cliPath, ['version'], { ...options, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
63
81
|
answer = readVersionAnswer({ stdout: reply.stdout, stderr: reply.stderr, exited: reply.error === undefined && reply.status === 0 })
|
|
64
82
|
} catch { answer = NOT_ASKED }
|
|
65
|
-
return observationOf({ platform, cliPath, processes, answer })
|
|
83
|
+
return observationOf({ platform, cliPath, processes: seen, answer })
|
|
66
84
|
},
|
|
67
85
|
inspect() {
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
86
|
+
const seen = processes()
|
|
87
|
+
if (seen === 'absent') return Promise.resolve(observationOf({ platform, cliPath, processes: seen, answer: NOT_ASKED }))
|
|
70
88
|
return new Promise((resolve) => {
|
|
71
|
-
execFile(cliPath, ['version'], options, (error, stdout, stderr) => resolve(observationOf({ platform, cliPath, processes, answer: readVersionAnswer({ stdout, stderr, exited: !error }) })))
|
|
89
|
+
execFile(cliPath, ['version'], options, (error, stdout, stderr) => resolve(observationOf({ platform, cliPath, processes: seen, answer: readVersionAnswer({ stdout, stderr, exited: !error }) })))
|
|
72
90
|
})
|
|
73
91
|
},
|
|
74
|
-
// Whether the app answers for exactly this vault and has finished reading it.
|
|
75
|
-
|
|
76
|
-
|
|
92
|
+
// Whether the app answers for exactly this vault and has finished reading it. Asked where `route` (a
|
|
93
|
+
// `vaultRoute` of the app's list) says: from inside the vault's folder, or naming its id, so the app answers in
|
|
94
|
+
// that vault's window whichever window has focus, and opens it when it is known and closed. Without a route that
|
|
95
|
+
// reaches only this vault nothing is asked. Both sides are compared by real path, in the stored spelling.
|
|
96
|
+
vaultState({ vaultRoot, route }) {
|
|
97
|
+
let target
|
|
98
|
+
try { target = realPathAsStored(vaultRoot) } catch { return Promise.resolve({ answered: false, indexReady: false }) }
|
|
99
|
+
if (route?.how !== 'folder' && route?.how !== 'id') return Promise.resolve({ answered: false, indexReady: false })
|
|
100
|
+
const where = routedCall(route, workingDirectory)
|
|
77
101
|
return new Promise((resolve) => {
|
|
78
|
-
execFile(cliPath, ['eval', `code=${
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return resolve({ answered: value.basePath === vaultRoot, indexReady: value.basePath === vaultRoot && value.ready === true })
|
|
84
|
-
} catch { return resolve({ answered: false, indexReady: false }) }
|
|
102
|
+
execFile(cliPath, [...where.args, 'eval', `code=${VAULT_STATE_CODE}`], { ...options, cwd: where.cwd }, (error, stdout, stderr) => {
|
|
103
|
+
const answer = readEvalAnswer({ stdout, stderr, failed: Boolean(error) })
|
|
104
|
+
const value = answer.answered ? answer.value : null
|
|
105
|
+
const answered = value !== null && typeof value === 'object' && value.basePath === target
|
|
106
|
+
resolve({ answered, indexReady: answered && value.ready === true })
|
|
85
107
|
})
|
|
86
108
|
})
|
|
87
109
|
},
|
|
88
110
|
}
|
|
89
111
|
}
|
|
90
112
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
113
|
+
// The fixed scripts the app runs for `open`. The only variable input is a JSON
|
|
114
|
+
// payload that travels base64-encoded, so a path never becomes code. The app's
|
|
115
|
+
// folder is taken as the file system stores it, as the target is and as the
|
|
116
|
+
// bridge takes it, so a vault the app lists under another letter case answers.
|
|
117
|
+
const VAULT_STATE_CODE = "(()=>{let p=app.vault.adapter.basePath;try{const f=require('fs');p=process.platform!=='win32'&&f.realpathSync.native?f.realpathSync.native(p):f.realpathSync(p)}catch(e){}return JSON.stringify({basePath:p,ready:app.metadataCache.initialized===true})})()"
|
|
118
|
+
const VAULT_LIST_CODE = "(()=>JSON.stringify({vaults:require('electron').ipcRenderer.sendSync('vault-list')}))()"
|
|
119
|
+
// `vault-open` with `false` adds an existing folder to the app's vault list (the app writes its own file) and opens
|
|
120
|
+
// it in a window; it answers true, or a message. With `true` it would create a new folder, which is never wanted.
|
|
121
|
+
const VAULT_REGISTER_SCRIPT = "return JSON.stringify({result:require('electron').ipcRenderer.sendSync('vault-open',P.path,false)})"
|
|
122
|
+
function vaultRegisterCode(vaultRoot) {
|
|
123
|
+
if (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot) || vaultRoot.includes('\u0000')) throw new TypeError('vaultRoot must be an absolute path')
|
|
124
|
+
const data = Buffer.from(JSON.stringify({ path: vaultRoot }), 'utf8').toString('base64')
|
|
125
|
+
return `(()=>{const P=JSON.parse(new TextDecoder().decode(Uint8Array.from(atob('${data}'),c=>c.charCodeAt(0))));${VAULT_REGISTER_SCRIPT}})()`
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// The vault list of the app, and adding this view's vault to it:
|
|
129
|
+
//
|
|
130
|
+
// listThroughApp() -> { answered: true, vaults } | { answered: false, reason }
|
|
131
|
+
// registerThroughApp({ vaultRoot }) -> { answered: true, result } | { answered: false, reason }
|
|
132
|
+
// readSettings() -> readObsidianSettings answer (never writes)
|
|
133
|
+
// registerInSettings({ vaultRoot, keepBackups }) -> registerVaultInObsidianSettings answer
|
|
134
|
+
// backupSettings() -> backupObsidianSettings answer (a copy beside the file; the file is not written)
|
|
135
|
+
//
|
|
136
|
+
// Through the app only while it runs and answers; in its settings file only
|
|
137
|
+
// while no Obsidian runs, which registerVaultInObsidianSettings checks itself.
|
|
138
|
+
// A Flatpak or snap build found for this account (`sandbox`) never reads that
|
|
139
|
+
// file: it is then neither read nor written (`obsidian-sandboxed`), and a vault
|
|
140
|
+
// is added through the app only. Adding a vault opens a window, so a call may
|
|
141
|
+
// take longer than a version answer.
|
|
142
|
+
export function createProductionAppRegistry({
|
|
143
|
+
platform = process.platform, env = process.env, cliPath = defaultCliPath(platform), processProbe = () => defaultObsidianProcessProbe({ platform }), workingDirectory = NEUTRAL_DIRECTORY,
|
|
144
|
+
userDataDir = obsidianUserDataDir({ platform, env }), sandbox = obsidianSandboxedBuild({ platform, env }), timeoutMs = CLI_TIMEOUT_MS * 3,
|
|
145
|
+
} = {}) {
|
|
146
|
+
const sandboxed = { ok: false, code: 'obsidian-sandboxed', message: `this Obsidian is a ${sandbox} build, which reads its vault list inside its sandbox, where Atelier does not write` }
|
|
147
|
+
const evaluate = (code) => new Promise((resolve) => {
|
|
148
|
+
execFile(cliPath, ['eval', `code=${code}`], { env, cwd: workingDirectory, timeout: timeoutMs, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => resolve(readEvalAnswer({ stdout, stderr, failed: Boolean(error) })))
|
|
149
|
+
})
|
|
94
150
|
return {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
151
|
+
async listThroughApp() {
|
|
152
|
+
const answer = await evaluate(VAULT_LIST_CODE)
|
|
153
|
+
if (!answer.answered) return answer
|
|
154
|
+
const vaults = answer.value?.vaults
|
|
155
|
+
return vaults !== null && typeof vaults === 'object' && !Array.isArray(vaults) ? { answered: true, vaults } : { answered: false, reason: 'no-value' }
|
|
156
|
+
},
|
|
157
|
+
async registerThroughApp({ vaultRoot }) {
|
|
158
|
+
const answer = await evaluate(vaultRegisterCode(vaultRoot))
|
|
159
|
+
return answer.answered ? { answered: true, result: answer.value?.result ?? null } : answer
|
|
160
|
+
},
|
|
161
|
+
readSettings: () => (sandbox === null ? readObsidianSettings({ userDataDir }) : sandboxed),
|
|
162
|
+
// A copy of the settings file as it is, before a restart sends the app any signal (backupObsidianSettings).
|
|
163
|
+
backupSettings: () => (sandbox !== null ? sandboxed : userDataDir === null
|
|
164
|
+
? { ok: false, code: 'obsidian-settings-location-unknown', message: 'where Obsidian keeps its settings on this system is not known' }
|
|
165
|
+
: backupObsidianSettings({ userDataDir })),
|
|
166
|
+
registerInSettings: ({ vaultRoot, keepBackups = [] }) => (sandbox !== null ? sandboxed : userDataDir === null
|
|
167
|
+
? { ok: false, code: 'obsidian-settings-location-unknown', message: 'where Obsidian keeps its settings on this system is not known' }
|
|
168
|
+
: registerVaultInObsidianSettings({ userDataDir, vaultRoot, processProbe, keepBackups })),
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Shows the vault in Obsidian by the steps of launchPlan, carried out by
|
|
173
|
+
// runLaunchPlan: a quit app is started plainly (so it reopens every vault its
|
|
174
|
+
// list flags open) and handed the vault's URL through its command-line tool once
|
|
175
|
+
// the app itself answers the tool; a running app is handed the URL through its
|
|
176
|
+
// tool, which takes a URL even with its command line turned off, and through the
|
|
177
|
+
// operating system when the tool did not take it. A URL is never handed to the
|
|
178
|
+
// operating system for an app this launch started. No shell. `execFile` is
|
|
179
|
+
// node's, and a stand-in in the test of these executors, which runs in a child
|
|
180
|
+
// process (the default suite never imports this module in-process).
|
|
181
|
+
export function createProductionLauncher({
|
|
182
|
+
platform = process.platform, env = process.env, cliPath = defaultCliPath(platform), workingDirectory = NEUTRAL_DIRECTORY, waitMs, pollMs, sleep, execFile: runFile = execFile,
|
|
183
|
+
} = {}) {
|
|
184
|
+
const osCommand = platform === 'darwin' ? '/usr/bin/open' : platform === 'linux' ? 'xdg-open' : null
|
|
185
|
+
const run = (file, args, timeout = 15_000) => new Promise((resolve) => {
|
|
186
|
+
runFile(file, args, { env, cwd: workingDirectory, timeout, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 1024 * 1024 }, (error, stdout, stderr) => resolve({ failed: Boolean(error), stdout: String(stdout ?? ''), stderr: String(stderr ?? '') }))
|
|
187
|
+
})
|
|
188
|
+
return {
|
|
189
|
+
// Starts a quit app with no URL, so it reopens the vaults its list flags open: for giving back an app `open`
|
|
190
|
+
// quit to restart it, when the vault could not be added after all. macOS only, as the restart is.
|
|
191
|
+
startPlain: async () => platform === 'darwin' && !(await run(osCommand, ['-b', 'md.obsidian'])).failed,
|
|
192
|
+
// `startedByThisOpen`: this `open` started the app with an earlier launch; the link goes through its tool only.
|
|
193
|
+
open({ vaultId = null, vaultPath = null, appRunning, startedByThisOpen = false }) {
|
|
194
|
+
const plan = launchPlan({ platform, appRunning, vaultId, vaultPath })
|
|
195
|
+
return runLaunchPlan(plan, {
|
|
196
|
+
startedEarlier: startedByThisOpen === true,
|
|
197
|
+
start: async () => !(await run(osCommand, ['-b', 'md.obsidian'])).failed,
|
|
198
|
+
answered: async () => { const reply = await run(cliPath, ['version'], CLI_TIMEOUT_MS); return appAnswered({ stdout: reply.stdout, stderr: reply.stderr, exited: !reply.failed }) },
|
|
199
|
+
handLink: async (uri) => urlProcessed((await run(cliPath, [uri], CLI_TIMEOUT_MS)).stdout),
|
|
200
|
+
osOpen: async (uri) => !(await run(osCommand, [uri])).failed,
|
|
201
|
+
...(waitMs === undefined ? {} : { waitMs }), ...(pollMs === undefined ? {} : { pollMs }), ...(sleep === undefined ? {} : { sleep }),
|
|
100
202
|
})
|
|
101
203
|
},
|
|
102
204
|
}
|
|
103
205
|
}
|
|
104
206
|
|
|
207
|
+
// Quits the running app for a restart a person asked for (app-restart.mjs): SIGTERM to its main process only, proven
|
|
208
|
+
// from this machine's process table (`ps`, never a pattern search) as this account's, then waits until the process
|
|
209
|
+
// probe says no Obsidian runs. The table is `pid ppid uid executable`; one process is described again by its parent,
|
|
210
|
+
// user, start time and executable immediately before the signal.
|
|
211
|
+
export function createProductionAppQuitter({ platform = process.platform, processProbe = () => defaultObsidianProcessProbe({ platform }), waitMs } = {}) {
|
|
212
|
+
const ps = (args) => execFileSync('/bin/ps', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 16 * 1024 * 1024, timeout: 5000, killSignal: 'SIGKILL' })
|
|
213
|
+
return createAppQuitter({
|
|
214
|
+
platform, uid: typeof process.getuid === 'function' ? process.getuid() : null, processProbe,
|
|
215
|
+
readTable: () => ps(['-ww', '-A', '-o', 'pid=,ppid=,uid=,comm=']),
|
|
216
|
+
readProcess: (pid) => ps(['-ww', '-o', 'ppid=,uid=,lstart=,comm=', '-p', String(pid)]),
|
|
217
|
+
signal: (pid) => process.kill(pid, 'SIGTERM'),
|
|
218
|
+
// The wait and the linger are measured on a monotonic clock: a step of the wall clock changes neither.
|
|
219
|
+
now: () => performance.now(),
|
|
220
|
+
...(waitMs === undefined ? {} : { waitMs }),
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
|
|
105
224
|
export function createProductionAppSeams(options = {}) {
|
|
106
|
-
return { appProbe: createProductionAppProbe(options), launcher: createProductionLauncher(options) }
|
|
225
|
+
return { appProbe: createProductionAppProbe(options), launcher: createProductionLauncher(options), registry: createProductionAppRegistry(options), quitter: createProductionAppQuitter(options) }
|
|
107
226
|
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { randomBytes as cryptoRandomBytes } from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { MAX_OBSIDIAN_SETTINGS_BYTES, OBSIDIAN_SETTINGS_FILE, enclosingVaults, findVaultEntry, readObsidianSettings } from '../../projection/obsidian/publication/vault-list.mjs'
|
|
5
|
+
import { openRegularFileNoFollow, realPathAsStored, syncPrivateDirectory } from '../../project/private-state.mjs'
|
|
6
|
+
|
|
7
|
+
// Adding a view's vault to Obsidian's own vault list while Obsidian is not
|
|
8
|
+
// running, so that the app opens it when it starts.
|
|
9
|
+
//
|
|
10
|
+
// Outside Atelier's own storage, it is the only file of another application
|
|
11
|
+
// Atelier writes. The list is `obsidian.json` in the app's user-data directory
|
|
12
|
+
// (see vault-list.mjs). The app reads it when it starts and rewrites all of it
|
|
13
|
+
// whenever its list changes, so while it runs the file is the app's: a vault
|
|
14
|
+
// is then added through the app itself (`open` does that through its command
|
|
15
|
+
// line), never here. Here, the file is written only while the injected process
|
|
16
|
+
// probe says, positively, that no Obsidian runs, read immediately before and
|
|
17
|
+
// again immediately before the rename, and:
|
|
18
|
+
//
|
|
19
|
+
// - never for a Flatpak or snap build, which reads its list inside its
|
|
20
|
+
// sandbox and never this file (`sandbox`, see obsidianSandboxedBuild);
|
|
21
|
+
// - an existing file only in an existing user-data directory, both this
|
|
22
|
+
// user's own, a real directory and a regular file (no link, and no second
|
|
23
|
+
// name: a hard link would keep the old list), holding a JSON object;
|
|
24
|
+
// - a file that does not exist (Obsidian never started on this account) is
|
|
25
|
+
// created, and only then: see createObsidianSettings below;
|
|
26
|
+
// - never for a vault inside a folder the list has as a vault already: the
|
|
27
|
+
// app would show its notes in that vault too, and a call run in its folder
|
|
28
|
+
// would reach that vault (see vault-list.mjs);
|
|
29
|
+
// - every other key and every other vault entry is kept as the app wrote
|
|
30
|
+
// it, as values: the app itself rewrites the file with JSON.stringify;
|
|
31
|
+
// - the new entry is { path: the vault root's real path, ts: now, open:
|
|
32
|
+
// true } under a fresh 16-hex id that no entry has;
|
|
33
|
+
// - the command-line switch: whenever the list is written, `cli` is set to
|
|
34
|
+
// true when it is not (the default of a new installation is off, and
|
|
35
|
+
// `open` needs the command line while the app holds the vault). A vault the
|
|
36
|
+
// list has already, with `cli` not true, is written for the switch alone.
|
|
37
|
+
// Only here, so only while no Obsidian runs: a running app keeps its
|
|
38
|
+
// switch in memory and would write it back;
|
|
39
|
+
// - the new file is no larger than a settings file this module reads;
|
|
40
|
+
// - the bytes as they were are kept beside it in
|
|
41
|
+
// `obsidian.json.atelier-backup-<UTC time>`, fsynced, before the file is
|
|
42
|
+
// replaced; the replacement is a temporary file in the same directory,
|
|
43
|
+
// fsynced, renamed over it, and the directory is fsynced. Of these
|
|
44
|
+
// backups, the first (the list as it was before Atelier wrote it) and the
|
|
45
|
+
// latest are kept, and any between them removed;
|
|
46
|
+
// - a file that changed since it was read is not replaced.
|
|
47
|
+
//
|
|
48
|
+
// Every refusal writes nothing, leaves nothing behind and is typed: a file
|
|
49
|
+
// created on the way is removed again whichever step after its creation fails.
|
|
50
|
+
|
|
51
|
+
const refused = (code, message) => ({ ok: false, code, message })
|
|
52
|
+
const currentUid = () => (typeof process.getuid === 'function' ? process.getuid() : null)
|
|
53
|
+
const compactUtc = (ms) => new Date(ms).toISOString().replace(/[-:.]/g, '')
|
|
54
|
+
const BACKUP = new RegExp(`^${OBSIDIAN_SETTINGS_FILE.replaceAll('.', '\\.')}\\.atelier-backup-\\d{8}T\\d{9}Z$`)
|
|
55
|
+
|
|
56
|
+
// Keeps the first backup, the list as it was before Atelier ever wrote it, `latest`, and the names in `keep` (a copy
|
|
57
|
+
// the same run took before it quit the app); removes the others between. Their names sort as their times. A backup
|
|
58
|
+
// that cannot be removed stays.
|
|
59
|
+
function pruneBackups(directory, latest, keep = []) {
|
|
60
|
+
let names
|
|
61
|
+
try { names = fs.readdirSync(directory).filter((name) => BACKUP.test(name)).sort() } catch { return }
|
|
62
|
+
for (const name of names.slice(1)) {
|
|
63
|
+
if (name === latest || keep.includes(name)) continue
|
|
64
|
+
const file = path.join(directory, name)
|
|
65
|
+
try { if (fs.lstatSync(file).isFile()) fs.unlinkSync(file) } catch { /* it stays */ }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Creates `file`, which must not exist (an exclusive create fails on anything
|
|
70
|
+
// there, a link included), with these bytes, fsynced. The file exists exactly
|
|
71
|
+
// when the create returned; any failure after it removes the file again.
|
|
72
|
+
function writeNewFile(file, bytes, mode) {
|
|
73
|
+
const descriptor = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW ?? 0), mode)
|
|
74
|
+
try {
|
|
75
|
+
try {
|
|
76
|
+
fs.writeFileSync(descriptor, bytes)
|
|
77
|
+
fs.fchmodSync(descriptor, mode)
|
|
78
|
+
fs.fsyncSync(descriptor)
|
|
79
|
+
} finally {
|
|
80
|
+
fs.closeSync(descriptor)
|
|
81
|
+
}
|
|
82
|
+
} catch (error) {
|
|
83
|
+
try { fs.unlinkSync(file) } catch { /* already gone */ }
|
|
84
|
+
throw error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readBytesNoFollow(file) {
|
|
89
|
+
const descriptor = openRegularFileNoFollow(file)
|
|
90
|
+
try { return fs.readFileSync(descriptor) } finally { try { fs.closeSync(descriptor) } catch { /* read already, or failed */ } }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Creates the settings file of an Obsidian that never started on this account:
|
|
94
|
+
// the file does not exist, and the process table says, positively, that no
|
|
95
|
+
// Obsidian runs, read before and again immediately before the file appears.
|
|
96
|
+
// The app reads the file when it starts (a missing or unreadable one reads as
|
|
97
|
+
// no settings) and opens every vault flagged open, so a plain start opens this
|
|
98
|
+
// one, with its command line on. Only:
|
|
99
|
+
//
|
|
100
|
+
// - in the user-data directory as it is when that is this user's own real
|
|
101
|
+
// directory, or, when it does not exist, in one created here with mode
|
|
102
|
+
// 0700 inside an existing real parent directory (a missing parent is
|
|
103
|
+
// `obsidian-settings-missing`: nothing is created above the directory);
|
|
104
|
+
// - with exactly { "vaults": { "<16 hex>": { path, ts, open: true } },
|
|
105
|
+
// "cli": true }, mode 0600;
|
|
106
|
+
// - by an exclusive create: the complete bytes are written, fsynced, to a
|
|
107
|
+
// temporary file in the directory, which is then linked to the file's name
|
|
108
|
+
// (a link fails on any name there, a file the app wrote meanwhile
|
|
109
|
+
// included, which is kept: `obsidian-settings-changed`), and the
|
|
110
|
+
// temporary name removed. A reader never sees a part of the file.
|
|
111
|
+
//
|
|
112
|
+
// There is nothing to back up. A refusal removes what it created, the
|
|
113
|
+
// directory included.
|
|
114
|
+
function createObsidianSettings({ userDataDir, vaultRoot, absent, now, randomBytes, uid }) {
|
|
115
|
+
const owned = (stat) => uid === null || stat.uid === uid
|
|
116
|
+
let directory = null
|
|
117
|
+
try { directory = fs.lstatSync(userDataDir) } catch (error) { if (error.code !== 'ENOENT') return refused('obsidian-settings-unreadable', 'the Obsidian settings directory cannot be read') }
|
|
118
|
+
let folder
|
|
119
|
+
try { folder = realPathAsStored(vaultRoot) } catch { return refused('vault-root-missing', 'the view\'s vault folder does not exist') }
|
|
120
|
+
if (!fs.statSync(folder).isDirectory()) return refused('vault-root-missing', 'the view\'s vault folder is not a directory')
|
|
121
|
+
let madeDirectory = false
|
|
122
|
+
if (directory === null) {
|
|
123
|
+
const parent = path.dirname(userDataDir)
|
|
124
|
+
let above
|
|
125
|
+
try { above = fs.lstatSync(parent) } catch { above = null }
|
|
126
|
+
if (above === null || above.isSymbolicLink() || !above.isDirectory() || !owned(above)) return refused('obsidian-settings-missing', 'Obsidian has no settings directory on this account, and the folder it would be in is missing, a link or not this user\'s own')
|
|
127
|
+
try { fs.mkdirSync(userDataDir, { mode: 0o700 }) } catch (error) {
|
|
128
|
+
return refused(error.code === 'EEXIST' ? 'obsidian-settings-changed' : 'obsidian-settings-unwritable', 'the Obsidian settings directory cannot be created')
|
|
129
|
+
}
|
|
130
|
+
madeDirectory = true
|
|
131
|
+
directory = fs.lstatSync(userDataDir)
|
|
132
|
+
}
|
|
133
|
+
const removeDirectory = () => { if (madeDirectory) try { fs.rmdirSync(userDataDir) } catch { /* something else is in it now: it stays */ } }
|
|
134
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return refused('obsidian-settings-unsafe', 'the Obsidian settings directory is a link or not a directory')
|
|
135
|
+
if (!owned(directory)) return refused('obsidian-settings-not-owned', 'the Obsidian settings directory belongs to another user')
|
|
136
|
+
const id = randomBytes(8).toString('hex')
|
|
137
|
+
const at = now()
|
|
138
|
+
const document = { vaults: { [id]: { path: folder, ts: at, open: true } }, cli: true }
|
|
139
|
+
const bytes = Buffer.from(JSON.stringify(document), 'utf8')
|
|
140
|
+
if (bytes.length > MAX_OBSIDIAN_SETTINGS_BYTES) { removeDirectory(); return refused('obsidian-settings-too-large', 'with this vault the Obsidian settings file would be larger than a settings file can be') }
|
|
141
|
+
const file = path.join(userDataDir, OBSIDIAN_SETTINGS_FILE)
|
|
142
|
+
const temporary = path.join(userDataDir, `.${OBSIDIAN_SETTINGS_FILE}.atelier-${randomBytes(6).toString('hex')}.tmp`)
|
|
143
|
+
const undo = () => { try { fs.unlinkSync(temporary) } catch { /* already gone */ } removeDirectory() }
|
|
144
|
+
try { writeNewFile(temporary, bytes, 0o600) } catch { undo(); return refused('obsidian-settings-unwritable', 'the Obsidian settings directory cannot be written') }
|
|
145
|
+
// Immediately before the file appears: still no app.
|
|
146
|
+
if (!absent()) { undo(); return refused('app-may-be-running', 'an Obsidian process appeared, and its settings belong to it') }
|
|
147
|
+
try { fs.linkSync(temporary, file) } catch (error) {
|
|
148
|
+
undo()
|
|
149
|
+
return refused(error.code === 'EEXIST' ? 'obsidian-settings-changed' : 'obsidian-settings-unwritable', error.code === 'EEXIST' ? 'an Obsidian settings file appeared while it was being created, and it was kept as it is' : 'the Obsidian settings file cannot be created')
|
|
150
|
+
}
|
|
151
|
+
try { fs.unlinkSync(temporary) } catch { /* the file is complete; a second name is refused by the next write */ }
|
|
152
|
+
try { syncPrivateDirectory(userDataDir); if (madeDirectory) syncPrivateDirectory(path.dirname(userDataDir)) } catch { /* the file exists; a directory that cannot be fsynced is not undone */ }
|
|
153
|
+
const stillAbsent = absent()
|
|
154
|
+
const written = readObsidianSettings({ userDataDir, uid })
|
|
155
|
+
const entry = written.ok ? findVaultEntry(written.vaults, vaultRoot) : null
|
|
156
|
+
const reason = !stillAbsent ? 'app-started-during-registration' : entry === null ? 'registration-not-read-back' : null
|
|
157
|
+
return {
|
|
158
|
+
ok: true, registered: 'created', entry: entry ?? { id, path: folder, open: true }, confirmed: reason === null, vaults: written.ok ? written.vaults : document.vaults,
|
|
159
|
+
file, created: { directory: madeDirectory }, cliTurnedOn: true, ...(reason === null ? {} : { reason }),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// { ok: true, registered: 'already' | 'written' | 'created', entry: { id, path, open }, confirmed, vaults, file, cliTurnedOn,
|
|
164
|
+
// backupPath?, created?, reason? } or a typed refusal { ok: false, code, message }. `registered` is 'already' when the
|
|
165
|
+
// list had the vault (then the file was written only to turn the command line on, `cliTurnedOn`), 'written' when this
|
|
166
|
+
// vault was added to an existing file, 'created' when the file was created (createObsidianSettings). `confirmed` is
|
|
167
|
+
// false when an app appeared right after the rename (`app-started-during-registration`): it may have read the list
|
|
168
|
+
// before it, so whoever asked verifies through the app; and when the written file could not be read back to find the
|
|
169
|
+
// entry (`registration-not-read-back`). `vaults` is the list as it was found or written. `processProbe()` answers
|
|
170
|
+
// 'absent', 'running' or 'unknown'; only 'absent' allows a write. `sandbox` names a Flatpak or snap build found for
|
|
171
|
+
// this account, which refuses (`obsidian-sandboxed`).
|
|
172
|
+
// `keepBackups`: backup names (in the file's directory) this write must not prune: the copy `open` kept before it
|
|
173
|
+
// quit the app for this write.
|
|
174
|
+
export function registerVaultInObsidianSettings({ userDataDir, vaultRoot, processProbe, sandbox = null, now = () => Date.now(), randomBytes = cryptoRandomBytes, uid = currentUid(), keepBackups = [] } = {}) {
|
|
175
|
+
if (typeof processProbe !== 'function') throw new TypeError('registering a vault needs a process probe')
|
|
176
|
+
if (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot) || vaultRoot.includes('\u0000')) throw new TypeError('vaultRoot must be an absolute path')
|
|
177
|
+
if (sandbox !== null) return refused('obsidian-sandboxed', `this Obsidian is a ${sandbox} build, which reads its vault list inside its sandbox, where Atelier does not write`)
|
|
178
|
+
const absent = () => { try { return processProbe() === 'absent' } catch { return false } }
|
|
179
|
+
if (!absent()) return refused('app-may-be-running', 'an Obsidian process may be running, and its vault list belongs to it')
|
|
180
|
+
const settings = readObsidianSettings({ userDataDir, uid })
|
|
181
|
+
// No file: Obsidian never started on this account. A missing directory is refused as missing when it has no parent.
|
|
182
|
+
if (!settings.ok && settings.code === 'obsidian-settings-missing' && typeof userDataDir === 'string' && path.isAbsolute(userDataDir)) return createObsidianSettings({ userDataDir, vaultRoot, absent, now, randomBytes, uid })
|
|
183
|
+
if (!settings.ok) return settings
|
|
184
|
+
const known = findVaultEntry(settings.vaults, vaultRoot)
|
|
185
|
+
const switchOn = settings.document.cli !== true
|
|
186
|
+
if (known && !switchOn) return { ok: true, registered: 'already', entry: known, confirmed: true, vaults: settings.vaults, file: settings.file, cliTurnedOn: false }
|
|
187
|
+
if (settings.links > 1) return refused('obsidian-settings-unsafe', 'the Obsidian settings file has a second name (a hard link), which a replacement would leave with the old list')
|
|
188
|
+
if (!known && enclosingVaults({ vaults: settings.vaults, vaultRoot }).length > 0) return refused('vault-inside-another-vault', 'Obsidian lists a vault at a folder that contains this vault\'s folder')
|
|
189
|
+
|
|
190
|
+
let folder = null
|
|
191
|
+
let id = null
|
|
192
|
+
if (!known) {
|
|
193
|
+
try { folder = realPathAsStored(vaultRoot) } catch { return refused('vault-root-missing', 'the view\'s vault folder does not exist') }
|
|
194
|
+
if (!fs.statSync(folder).isDirectory()) return refused('vault-root-missing', 'the view\'s vault folder is not a directory')
|
|
195
|
+
do { id = randomBytes(8).toString('hex') } while (Object.hasOwn(settings.vaults, id))
|
|
196
|
+
}
|
|
197
|
+
const at = now()
|
|
198
|
+
// The switch keeps its place when the file has one; otherwise it is added last, as the app would.
|
|
199
|
+
const document = { ...settings.document, ...(known ? {} : { vaults: { ...settings.vaults, [id]: { path: folder, ts: at, open: true } } }), ...(switchOn ? { cli: true } : {}) }
|
|
200
|
+
const bytes = Buffer.from(JSON.stringify(document), 'utf8')
|
|
201
|
+
if (bytes.length > MAX_OBSIDIAN_SETTINGS_BYTES) return refused('obsidian-settings-too-large', 'with this vault the Obsidian settings file would be larger than a settings file can be')
|
|
202
|
+
const directory = path.dirname(settings.file)
|
|
203
|
+
const backupPath = path.join(directory, `${OBSIDIAN_SETTINGS_FILE}.atelier-backup-${compactUtc(at)}`)
|
|
204
|
+
const temporary = path.join(directory, `.${OBSIDIAN_SETTINGS_FILE}.atelier-${randomBytes(6).toString('hex')}.tmp`)
|
|
205
|
+
const created = []
|
|
206
|
+
const removeCreated = () => { for (const file of created.splice(0)) try { fs.unlinkSync(file) } catch { /* already gone */ } }
|
|
207
|
+
try {
|
|
208
|
+
writeNewFile(temporary, bytes, settings.mode)
|
|
209
|
+
created.push(temporary)
|
|
210
|
+
writeNewFile(backupPath, settings.bytes, settings.mode)
|
|
211
|
+
created.push(backupPath)
|
|
212
|
+
} catch (error) {
|
|
213
|
+
removeCreated()
|
|
214
|
+
return refused(error.code === 'EEXIST' ? 'obsidian-settings-changed' : 'obsidian-settings-unwritable', 'the Obsidian settings directory cannot be written')
|
|
215
|
+
}
|
|
216
|
+
// Immediately before the rename: still no app, and still the bytes that were read.
|
|
217
|
+
if (!absent()) { removeCreated(); return refused('app-may-be-running', 'an Obsidian process appeared, and its vault list belongs to it') }
|
|
218
|
+
let current
|
|
219
|
+
try { current = readBytesNoFollow(settings.file) } catch { current = null }
|
|
220
|
+
if (current === null || !current.equals(settings.bytes)) { removeCreated(); return refused('obsidian-settings-changed', 'the Obsidian settings file changed while the vault was being added') }
|
|
221
|
+
try {
|
|
222
|
+
fs.renameSync(temporary, settings.file)
|
|
223
|
+
} catch {
|
|
224
|
+
removeCreated()
|
|
225
|
+
return refused('obsidian-settings-unwritable', 'the Obsidian settings file cannot be replaced')
|
|
226
|
+
}
|
|
227
|
+
created.splice(0)
|
|
228
|
+
try { syncPrivateDirectory(directory) } catch { /* the rename is done; a directory that cannot be fsynced is not undone */ }
|
|
229
|
+
pruneBackups(directory, path.basename(backupPath), (Array.isArray(keepBackups) ? keepBackups : []).filter((name) => typeof name === 'string').map((name) => path.basename(name)))
|
|
230
|
+
const stillAbsent = absent()
|
|
231
|
+
const written = readObsidianSettings({ userDataDir, uid })
|
|
232
|
+
const entry = written.ok ? findVaultEntry(written.vaults, vaultRoot) : null
|
|
233
|
+
const reason = !stillAbsent ? 'app-started-during-registration' : entry === null ? 'registration-not-read-back' : null
|
|
234
|
+
return {
|
|
235
|
+
ok: true, registered: known ? 'already' : 'written', entry: entry ?? known ?? { id, path: folder, open: true }, backupPath, confirmed: reason === null, vaults: written.ok ? written.vaults : document.vaults,
|
|
236
|
+
file: settings.file, cliTurnedOn: switchOn, ...(reason === null ? {} : { reason }),
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Read-only, for `open` before it quits a running app to restart it: whether registerVaultInObsidianSettings would
|
|
241
|
+
// write this vault into the settings file `settings` (a readObsidianSettings answer, read while the app runs), as far
|
|
242
|
+
// as a read can tell. { ok: true, create } or { ok: false, code } with the refusal the write would give: an
|
|
243
|
+
// unreadable, linked, foreign, sandboxed or too large file, a vault inside another listed vault, a vault folder that
|
|
244
|
+
// is missing. A file that does not exist (an app that has shown only its starter window writes none) is the create
|
|
245
|
+
// case, `create: true`, checked as createObsidianSettings checks before it writes: the vault folder is a directory,
|
|
246
|
+
// and the user-data directory is this user's own real directory, or is absent inside this user's own real parent.
|
|
247
|
+
// What only the moment of the write can tell (an app that appears, a file that changes) is still checked then.
|
|
248
|
+
export function settingsWriteOutlook({ settings, vaultRoot, now = () => Date.now(), uid = currentUid() } = {}) {
|
|
249
|
+
const no = (code) => ({ ok: false, code })
|
|
250
|
+
if (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot)) return no('vault-root-missing')
|
|
251
|
+
if (settings?.ok !== true && settings?.code === 'obsidian-settings-missing') {
|
|
252
|
+
const userDataDir = settings.userDataDir
|
|
253
|
+
if (typeof userDataDir !== 'string' || !path.isAbsolute(userDataDir)) return no('obsidian-settings-location-unknown')
|
|
254
|
+
try { if (!fs.statSync(realPathAsStored(vaultRoot)).isDirectory()) return no('vault-root-missing') } catch { return no('vault-root-missing') }
|
|
255
|
+
const owned = (stat) => uid === null || stat.uid === uid
|
|
256
|
+
let directory = null
|
|
257
|
+
try { directory = fs.lstatSync(userDataDir) } catch (error) { if (error.code !== 'ENOENT') return no('obsidian-settings-unreadable') }
|
|
258
|
+
if (directory !== null) {
|
|
259
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return no('obsidian-settings-unsafe')
|
|
260
|
+
return owned(directory) ? { ok: true, create: true } : no('obsidian-settings-not-owned')
|
|
261
|
+
}
|
|
262
|
+
let above = null
|
|
263
|
+
try { above = fs.lstatSync(path.dirname(userDataDir)) } catch { above = null }
|
|
264
|
+
return above !== null && !above.isSymbolicLink() && above.isDirectory() && owned(above) ? { ok: true, create: true } : no('obsidian-settings-missing')
|
|
265
|
+
}
|
|
266
|
+
if (settings?.ok !== true) return no(typeof settings?.code === 'string' ? settings.code : 'obsidian-settings-unreadable')
|
|
267
|
+
if (typeof settings.links === 'number' && settings.links > 1) return no('obsidian-settings-unsafe')
|
|
268
|
+
const size = Buffer.isBuffer(settings.bytes) ? settings.bytes.length : 0
|
|
269
|
+
const known = findVaultEntry(settings.vaults, vaultRoot)
|
|
270
|
+
if (known) return size + 16 > MAX_OBSIDIAN_SETTINGS_BYTES ? no('obsidian-settings-too-large') : { ok: true, create: false }
|
|
271
|
+
if (enclosingVaults({ vaults: settings.vaults, vaultRoot }).length > 0) return no('vault-inside-another-vault')
|
|
272
|
+
let folder
|
|
273
|
+
try { folder = realPathAsStored(vaultRoot); if (!fs.statSync(folder).isDirectory()) return no('vault-root-missing') } catch { return no('vault-root-missing') }
|
|
274
|
+
const entry = Buffer.byteLength(JSON.stringify({ '0123456789abcdef': { path: folder, ts: now(), open: true } }), 'utf8')
|
|
275
|
+
return size + entry + 16 > MAX_OBSIDIAN_SETTINGS_BYTES ? no('obsidian-settings-too-large') : { ok: true, create: false }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Keeps a copy of the settings file as it is, beside it, under the backup name the write uses
|
|
279
|
+
// (`obsidian.json.atelier-backup-<UTC time>`), fsynced, with the file's mode. Read-only for the file itself. `open`
|
|
280
|
+
// takes it right before the first signal to a running app, so a list the app was writing when it ended can be
|
|
281
|
+
// restored. { ok: true, backupPath } or a typed refusal (readObsidianSettings's, or `obsidian-settings-unwritable`).
|
|
282
|
+
// The write that follows in the same run keeps it (`keepBackups`); a later write prunes it as any other backup.
|
|
283
|
+
export function backupObsidianSettings({ userDataDir, now = () => Date.now(), uid = currentUid() } = {}) {
|
|
284
|
+
const settings = readObsidianSettings({ userDataDir, uid })
|
|
285
|
+
if (!settings.ok) return settings
|
|
286
|
+
const directory = path.dirname(settings.file)
|
|
287
|
+
for (let offset = 0; offset < 3; offset += 1) {
|
|
288
|
+
const backupPath = path.join(directory, `${OBSIDIAN_SETTINGS_FILE}.atelier-backup-${compactUtc(now() + offset)}`)
|
|
289
|
+
try {
|
|
290
|
+
writeNewFile(backupPath, settings.bytes, settings.mode)
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error.code === 'EEXIST') continue
|
|
293
|
+
return refused('obsidian-settings-unwritable', 'a copy of the Obsidian settings file cannot be kept beside it')
|
|
294
|
+
}
|
|
295
|
+
try { syncPrivateDirectory(directory) } catch { /* the copy is written */ }
|
|
296
|
+
return { ok: true, backupPath }
|
|
297
|
+
}
|
|
298
|
+
return refused('obsidian-settings-unwritable', 'a copy of the Obsidian settings file cannot be kept beside it')
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// What the app looks like, as one comparable text, from what can be seen
|
|
302
|
+
// without asking it anything: whether an Obsidian process runs (`processes`, a
|
|
303
|
+
// process probe's 'running', 'absent' or 'unknown'), and which vaults its list
|
|
304
|
+
// shows open (`settings`, a `readObsidianSettings` answer or null). The app
|
|
305
|
+
// writes that list whenever a vault window opens or closes. The engine tries a
|
|
306
|
+
// view that did not settle again as soon as this changes; the app itself is
|
|
307
|
+
// asked only when a view is published.
|
|
308
|
+
export function appStateSignature({ processes = null, settings = null } = {}) {
|
|
309
|
+
const open = settings?.ok === true
|
|
310
|
+
? Object.values(settings.vaults).filter((entry) => entry !== null && typeof entry === 'object' && entry.open === true && typeof entry.path === 'string').map((entry) => entry.path).sort()
|
|
311
|
+
: null
|
|
312
|
+
return JSON.stringify([typeof processes === 'string' ? processes : null, open])
|
|
313
|
+
}
|