@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
|
@@ -1,33 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
3
4
|
import path from 'node:path'
|
|
5
|
+
import tty from 'node:tty'
|
|
4
6
|
import { fileURLToPath } from 'node:url'
|
|
5
7
|
import { AtelierDiagnosticError, resolveProjectConfig } from '../project/config.mjs'
|
|
8
|
+
import { readRegularTextNoFollow } from '../project/private-state.mjs'
|
|
6
9
|
import { ObsidianContractRefusal } from '../projection/obsidian/contracts.mjs'
|
|
7
10
|
import { applyPolicyDigest } from '../projection/obsidian/edits/policy.mjs'
|
|
8
|
-
import { MINIMUM_APP_VERSION } from '../runtime/obsidian/app-capability.mjs'
|
|
11
|
+
import { MINIMUM_APP_VERSION, inspectApp } from '../runtime/obsidian/app-capability.mjs'
|
|
9
12
|
import { loadContributions } from '../runtime/obsidian/contributions.mjs'
|
|
10
13
|
import { isoTime } from '../runtime/obsidian/documents.mjs'
|
|
11
14
|
import { readObsidianEnablement } from '../runtime/obsidian/enablement.mjs'
|
|
12
15
|
import { ObsidianMaintenanceRefusal, refuse } from '../runtime/obsidian/errors.mjs'
|
|
13
16
|
import { BUILT_IN_OPERATIONS, UNAVAILABLE_APPLY_OPERATION, createObsidianRegistry } from '../runtime/obsidian/extension-points.mjs'
|
|
14
|
-
import { LIFECYCLE_PRIMITIVES, readServiceStatusDocument, requestServiceTick, serviceStatus, startService, stopService } from '../runtime/obsidian/lifecycle.mjs'
|
|
17
|
+
import { LIFECYCLE_PRIMITIVES, readServiceStatusDocument, requestServiceTick, runtimeRelease, serviceStatus, startService, stopService } from '../runtime/obsidian/lifecycle.mjs'
|
|
15
18
|
import {
|
|
16
|
-
authorizeAutomaticApply, defaultMachineSettings, ensureWorkspaceIdentity, installApplyPolicy,
|
|
17
|
-
revokeApplyPolicy, writeMachineSettings,
|
|
19
|
+
DECISIONS, ONLY_YOU_AUDIENCES, authorizeAutomaticApply, defaultMachineSettings, ensureWorkspaceIdentity, installApplyPolicy, localPointerPath, protectedRoots,
|
|
20
|
+
readInstalledApplyPolicy, readMachineSettings, revokeApplyPolicy, withDecision, writeMachineSettings,
|
|
18
21
|
} from '../runtime/obsidian/machine-settings.mjs'
|
|
22
|
+
import {
|
|
23
|
+
currentLoginItemPlan, installLoginItem, loginItemDataRoot, loginItemLabel, loginItemStarter, loginItemStatus, planLoginItem, projectNameOf, readLoginItemRecord, realNodePath,
|
|
24
|
+
removeLoginItem, resolveLoginItemEntry, temporaryRoots,
|
|
25
|
+
} from '../runtime/obsidian/login-item.mjs'
|
|
19
26
|
import { APPLY_UNAVAILABLE, OPENING_OUTCOMES, OPENING_PRIMITIVES, REASON_NEXT, nextStep, openScopeForOracleTests, resolveScope, scopeReport } from '../runtime/obsidian/opening.mjs'
|
|
20
27
|
import { currentPluginChoice, writePluginChoice } from '../runtime/obsidian/plugin-choice.mjs'
|
|
21
|
-
import { pluginPresenceOf, turnPluginOnNext, withPluginReportedVersion } from '../runtime/obsidian/plugin-presence.mjs'
|
|
22
|
-
import {
|
|
28
|
+
import { pluginPresenceOf, turnPluginOnNext, withPluginFilesPending, withPluginReportedVersion } from '../runtime/obsidian/plugin-presence.mjs'
|
|
29
|
+
import { planViewAdd, viewFromRequest, writeViewPlan } from '../runtime/obsidian/project-views.mjs'
|
|
30
|
+
import { askGoAhead, createQuestioner } from '../runtime/obsidian/questions.mjs'
|
|
31
|
+
import { readServiceSettings } from '../runtime/obsidian/service-record.mjs'
|
|
23
32
|
import { resolveServiceWorkspace } from '../runtime/obsidian/service.mjs'
|
|
24
|
-
import { buildStartupAdapter } from '../runtime/obsidian/startup-adapters.mjs'
|
|
33
|
+
import { STARTUP_PLATFORMS, buildStartupAdapter, startupSearchPath } from '../runtime/obsidian/startup-adapters.mjs'
|
|
34
|
+
import { OBSIDIAN_SETTINGS_FILE, obsidianSandboxedBuild, obsidianUserDataDir, readObsidianSettings } from '../projection/obsidian/publication/vault-list.mjs'
|
|
35
|
+
import { checkVaultParent, projectDisplayName, vaultFolderName } from '../runtime/obsidian/vault-location.mjs'
|
|
36
|
+
import { PublicationRefusal, allocationFile, hasCommittedGeneration, readVaultAllocation, vaultRootFor } from '../projection/obsidian/recovery/store.mjs'
|
|
25
37
|
|
|
26
38
|
// `atelier obsidian <operation>`: status, views, audiences, apply policy, the
|
|
27
39
|
// owned maintenance service, and opening a view.
|
|
28
40
|
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
41
|
+
// Every input is an argument. A person at a terminal (`isInteractive`) is
|
|
42
|
+
// asked before a change to a file they commit, and can answer with --yes
|
|
43
|
+
// beforehand; anyone else is never asked, and their command is their consent.
|
|
44
|
+
// With `--json` exactly one JSON document is printed on stdout, for a refusal
|
|
45
|
+
// too.
|
|
31
46
|
//
|
|
32
47
|
// Exit codes: 0 done (for `open`: the view is current and open); 1 an error
|
|
33
48
|
// nobody typed; 2 a typed refusal or a usage error; 3 the operation ran and
|
|
@@ -37,8 +52,15 @@ import { buildStartupAdapter } from '../runtime/obsidian/startup-adapters.mjs'
|
|
|
37
52
|
// Reaching the installed app or the operating system goes through seams
|
|
38
53
|
// (`appProbe`, `launcher`, `registry`, `service`). A caller passes them. The production
|
|
39
54
|
// seams are constructed only when this module runs as the real command-line
|
|
40
|
-
// entry (`production: true`) AND `--adapter=obsidian-cli`
|
|
41
|
-
//
|
|
55
|
+
// entry (`production: true`) AND the adapter was selected: `--adapter=obsidian-cli`
|
|
56
|
+
// given now, or given once before and remembered in this workspace's machine
|
|
57
|
+
// settings (`selectAdapter`). A remembered adapter is never used under the test
|
|
58
|
+
// runner. There is no default.
|
|
59
|
+
//
|
|
60
|
+
// What a person decided is remembered per workspace on this machine
|
|
61
|
+
// (`atelier obsidian settings` shows it). The first start of the maintenance
|
|
62
|
+
// service records who allowed it: `--consent-actor ID`, or, for a person at a
|
|
63
|
+
// terminal, their account's name (`isInteractive`).
|
|
42
64
|
|
|
43
65
|
export const COMMAND_SCHEMA = 'atelier-obsidian-command/v1'
|
|
44
66
|
export const EXIT = Object.freeze({ ok: 0, error: 1, refused: 2, notSuccess: 3 })
|
|
@@ -47,26 +69,56 @@ const PRODUCTION_ADAPTER = 'obsidian-cli'
|
|
|
47
69
|
const MAX_POLICY_BYTES = 64 * 1024
|
|
48
70
|
const AUDIENCE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
49
71
|
|
|
50
|
-
export const USAGE = `Usage: atelier obsidian <operation> [--project atelier.project.json] [--data-root DIR] [--json]
|
|
72
|
+
export const USAGE = `Usage: atelier obsidian <operation> [--project atelier.project.json] [--data-root DIR] [--json] [--no-input]
|
|
51
73
|
|
|
52
74
|
status Enablement, machine settings, service and per-view freshness. Read-only.
|
|
53
|
-
|
|
54
|
-
|
|
75
|
+
settings What this machine remembers for this workspace, and how to change it. Read-only.
|
|
76
|
+
scope list | scope show ID The views this project declares. Read-only. (\`view list | show ID\` too.)
|
|
77
|
+
view add ID (--all | --folder PATH [--folder PATH ...] [--repo R] | --tag T) [--expand DEPTH:MAX] [--default]
|
|
78
|
+
[--allow-empty] [--yes]
|
|
79
|
+
Add a view to the project's configuration, which you commit: every note, the
|
|
80
|
+
notes under folders of one repository, or those with a tag, optionally with the
|
|
81
|
+
notes they link to (DEPTH steps, MAX notes in all). Shows the change and how many
|
|
82
|
+
notes the view would show, and asks at a terminal.
|
|
83
|
+
audience show | set me|A,B | clear The audiences this machine lets into a view (private; none by default).
|
|
84
|
+
\`me\` is only you: every audience but sensitive, which is added by name, and
|
|
85
|
+
the notes without a classification, which no other list shows.
|
|
86
|
+
location show | set DIR [--allow-synced-location]
|
|
87
|
+
Where this workspace's vaults live, as "<project> (<view>)"; each is allocated
|
|
88
|
+
there at its first publication. A vault published already stays where it is.
|
|
55
89
|
mode show | set manual|automatic Whether queued edits wait for a person or are applied under the policy.
|
|
56
90
|
policy show | install FILE | revoke The private apply policy. Automatic mode needs an installed, active one.
|
|
57
91
|
policy digest FILE The digest FILE has to carry to be installed. Reads FILE; writes nothing.
|
|
58
|
-
service start [--consent-actor ID] --adapter=${PRODUCTION_ADAPTER}
|
|
59
|
-
service status | stop The owned maintenance service of this workspace.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
92
|
+
service start [--consent-actor ID] [--adapter=${PRODUCTION_ADAPTER}]
|
|
93
|
+
service status | stop The owned maintenance service of this workspace. start replaces one of an
|
|
94
|
+
earlier release still running after an upgrade.
|
|
95
|
+
service unit --print [--adapter=${PRODUCTION_ADAPTER}]
|
|
96
|
+
Print the login item (launchd agent, systemd user unit). Writes nothing.
|
|
97
|
+
service unit --install [--consent-actor ID] [--adapter=${PRODUCTION_ADAPTER}]
|
|
98
|
+
Install the login item: maintenance starts when you log in. macOS and Linux.
|
|
99
|
+
service unit --remove Remove the login item; the consent goes back to the service alone, and the
|
|
100
|
+
service is started for this session only.
|
|
101
|
+
open [--scope ID] [--consent-actor ID] [--allow-stale] [--restart-obsidian] [--wait-ms MS] [--adapter=${PRODUCTION_ADAPTER}]
|
|
63
102
|
Start or reconnect maintenance, verify the view, add it to Obsidian and open it.
|
|
103
|
+
--restart-obsidian lets open quit a running Obsidian it cannot reach (its command
|
|
104
|
+
line off, or no vault open), add the vault and start it again; never implied.
|
|
105
|
+
--wait-ms bounds the wait for maintenance's tick.
|
|
106
|
+
uninstall Stop maintenance and remove the login item. Vaults, private state, the project
|
|
107
|
+
file and Obsidian's vault list are kept, and where each is is printed.
|
|
64
108
|
plugin show [--scope ID] Whether Atelier's plugin is on in a view's vault, and whether it holds it open.
|
|
65
109
|
plugin on [--scope ID] [--adapter=${PRODUCTION_ADAPTER}]
|
|
66
110
|
Offer Atelier's plugin again in a vault where it was turned off; a running
|
|
67
111
|
service publishes the view at once (with --adapter, one of an earlier release
|
|
68
112
|
is replaced first).
|
|
69
113
|
|
|
114
|
+
Reaching the installed app needs --adapter=${PRODUCTION_ADAPTER} once for a workspace; it is remembered after that.
|
|
115
|
+
Obsidian's own settings file (obsidian.json) is written only while no Obsidian runs: open adds the view's vault to its
|
|
116
|
+
list, turns its command line on (cli: true), and creates the file for an Obsidian that never started; every write is
|
|
117
|
+
shown in open's answer, with the backup of the file as it was.
|
|
118
|
+
The first start of the maintenance service records who allows it: --consent-actor ID, or, for a person at a terminal,
|
|
119
|
+
their account's name; service unit --install records who allows it to start at login the same way (at a terminal, the
|
|
120
|
+
actor already recorded is kept). --no-input, --json, a CI environment or ATELIER_NONINTERACTIVE=1 mean no person is at a terminal.
|
|
121
|
+
|
|
70
122
|
Contributed operations, registered by the modules shipped under src/runtime/obsidian/contributions/:
|
|
71
123
|
apply list | show EDIT | run EDIT [--actor ID] | recover
|
|
72
124
|
Pending edits, and the explicit apply of one of them to its source file.
|
|
@@ -83,7 +135,77 @@ Pending edits additionally report ${APPLY_UNAVAILABLE} while no apply operation
|
|
|
83
135
|
Minimum Obsidian version: ${MINIMUM_APP_VERSION}.
|
|
84
136
|
Exit codes: 0 done; 1 internal error; 2 refusal or usage; 3 ran, and the answer is not success.`
|
|
85
137
|
|
|
86
|
-
|
|
138
|
+
// Every option of the command. `atelier obsidian --help` names each one but `help` and `project-config` (the older
|
|
139
|
+
// spelling of `project`); a test holds it to that.
|
|
140
|
+
export const FLAGS = Object.freeze({
|
|
141
|
+
json: 'flag', 'allow-stale': 'flag', 'restart-obsidian': 'flag', print: 'flag', install: 'flag', remove: 'flag', help: 'flag', 'no-input': 'flag', 'allow-synced-location': 'flag', all: 'flag', default: 'flag', 'allow-empty': 'flag', yes: 'flag',
|
|
142
|
+
project: 'value', 'project-config': 'value', 'data-root': 'value', scope: 'value', 'consent-actor': 'value', actor: 'value', adapter: 'value', 'wait-ms': 'value', repo: 'value', tag: 'value',
|
|
143
|
+
expand: 'value', folder: 'values',
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
147
|
+
|
|
148
|
+
// Which adapter this run may use, or a typed refusal. Pure. `flag` is --adapter as given; `remembered` is the adapter
|
|
149
|
+
// this workspace's machine settings remember (or null); `production` says this module runs as the real command-line
|
|
150
|
+
// entry. A remembered adapter is used only by the real entry, and never under the test runner: a test that reaches an
|
|
151
|
+
// app names the adapter, and runs where no app of the developer's can be reached.
|
|
152
|
+
export function selectAdapter({ flag, remembered = null, production = false, env = process.env } = {}) {
|
|
153
|
+
if (flag !== undefined) {
|
|
154
|
+
if (flag !== PRODUCTION_ADAPTER) refuse('app-adapter-not-selected', `no editor adapter is called ${String(flag).slice(0, 40)}; the only one is ${PRODUCTION_ADAPTER}`)
|
|
155
|
+
return { adapter: PRODUCTION_ADAPTER, source: 'flag' }
|
|
156
|
+
}
|
|
157
|
+
if (remembered === PRODUCTION_ADAPTER && production === true) {
|
|
158
|
+
if (env.NODE_TEST_CONTEXT !== undefined) refuse('remembered-adapter-under-test', 'a remembered adapter is never used under the test runner; pass the seams, or the adapter explicitly')
|
|
159
|
+
return { adapter: PRODUCTION_ADAPTER, source: 'remembered' }
|
|
160
|
+
}
|
|
161
|
+
return refuse('app-adapter-not-selected', 'no editor adapter was selected; reaching the installed app is never a default')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Whether a person is at a terminal: nothing says otherwise, and both standard input and output are one. Pure.
|
|
165
|
+
// `terminal` is `{ stdin, stdout }`, each true when that stream is a terminal.
|
|
166
|
+
export function isInteractive({ flags = {}, env = process.env, terminal = { stdin: false, stdout: false } } = {}) {
|
|
167
|
+
if (flags.json === true || flags['no-input'] === true) return false
|
|
168
|
+
if (env.ATELIER_NONINTERACTIVE === '1') return false
|
|
169
|
+
if (typeof env.CI === 'string' && env.CI !== '' && env.CI !== 'false' && env.CI !== '0') return false
|
|
170
|
+
return terminal.stdin === true && terminal.stdout === true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The account's name as an actor identifier, or null when it is not one.
|
|
174
|
+
export const accountActor = (username) => (typeof username === 'string' && IDENTIFIER.test(username) ? username : null)
|
|
175
|
+
|
|
176
|
+
// How each remembered answer is changed; null where no command changes it.
|
|
177
|
+
const DECISION_CHANGES = Object.freeze({
|
|
178
|
+
audience: '`atelier obsidian audience set me|A,B` decides who may see the vaults again',
|
|
179
|
+
location: '`atelier obsidian location set DIR` decides where the vaults of views not published yet live',
|
|
180
|
+
loginItem: '`atelier obsidian service unit --install` starts maintenance at login; `service unit --remove` stops that',
|
|
181
|
+
adapter: null,
|
|
182
|
+
consent: '`atelier obsidian service stop`, then `service start --consent-actor ID`, records another actor',
|
|
183
|
+
})
|
|
184
|
+
const SOURCE_WORDS = Object.freeze({ question: 'answered at a terminal', command: 'given on the command line', defaults: 'the defaults', v1: 'set before this release' })
|
|
185
|
+
|
|
186
|
+
// One remembered answer in words, with when and how it was given.
|
|
187
|
+
function decisionWords(name, decision, audienceAllow = []) {
|
|
188
|
+
if (decision === null) return name === 'audience' && audienceAllow.length === 0 ? 'not decided; no audience is allowed, so every view is empty' : 'not decided'
|
|
189
|
+
const what = name === 'audience'
|
|
190
|
+
? `${decision.choice === 'only-you' ? `only you (${audienceAllow.join(', ')})` : audienceAllow.length === 0 ? 'no audience: every view is empty' : audienceAllow.join(', ')}; notes without a classification ${decision.unclassified}`
|
|
191
|
+
: name === 'location' ? decision.parent : decision.choice
|
|
192
|
+
return `${what} (${SOURCE_WORDS[decision.via]}, ${decision.decidedAt}${decision.decidedBy === null ? '' : `, by ${decision.decidedBy}`})`
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function decisionSummary(decisions) {
|
|
196
|
+
const words = {
|
|
197
|
+
audience: (decision) => `who may see ${decision.choice === 'only-you' ? 'only you' : 'a list of audiences'}`,
|
|
198
|
+
location: (decision) => `vaults in ${decision.parent}`,
|
|
199
|
+
loginItem: (decision) => `start at login ${decision.choice}`,
|
|
200
|
+
adapter: (decision) => `app through ${decision.choice}`,
|
|
201
|
+
}
|
|
202
|
+
const decided = DECISIONS.filter((name) => decisions[name] !== null)
|
|
203
|
+
return decided.length === 0 ? 'nothing yet' : decided.map((name) => words[name](decisions[name])).join('; ')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function accountName() {
|
|
207
|
+
try { return os.userInfo().username } catch { return null }
|
|
208
|
+
}
|
|
87
209
|
|
|
88
210
|
function parse(argv) {
|
|
89
211
|
const positionals = []
|
|
@@ -97,24 +219,119 @@ function parse(argv) {
|
|
|
97
219
|
if (FLAGS[name] === 'flag') { if (inline !== undefined) refuse('usage', `--${name} takes no value`); flags[name] = true; continue }
|
|
98
220
|
const value = inline ?? argv[index += 1]
|
|
99
221
|
if (value === undefined || value === '' || (inline === undefined && value.startsWith('--'))) refuse('usage', `--${name} needs a value`)
|
|
100
|
-
|
|
222
|
+
// An option that takes several values is given once for each.
|
|
223
|
+
flags[name] = FLAGS[name] === 'values' ? [...(flags[name] ?? []), value] : value
|
|
101
224
|
}
|
|
102
225
|
return { positionals, flags }
|
|
103
226
|
}
|
|
104
227
|
|
|
105
|
-
|
|
228
|
+
// What `open` did to the person's Obsidian, in words: a restart they asked for, and every write to its settings file.
|
|
229
|
+
const STATE_WORDS = Object.freeze({ 'cli-turned-off': 'its command line was turned off', 'no-vault-open': 'it had no vault open' })
|
|
230
|
+
function restartLines(restart) {
|
|
231
|
+
if (restart === undefined || restart === null) return []
|
|
232
|
+
const why = STATE_WORDS[restart.state] ?? restart.state
|
|
233
|
+
const copy = restart.settingsCopy && restart.signalled ? [` A copy of Obsidian's settings, taken just before the first signal, is kept in ${restart.settingsCopy}.`] : []
|
|
234
|
+
const asked = restart.signals === 2 ? `it was sent SIGTERM twice (process ${restart.pid}), and has not gone` : `it was asked to quit (SIGTERM to process ${restart.pid}) and has not`
|
|
235
|
+
if (restart.quit !== true) return [`Obsidian: not restarted (${restart.reason}${restart.detail ? `: ${restart.detail}` : ''}); ${restart.signalled ? asked : 'it was not asked to quit'}.`, ...copy]
|
|
236
|
+
const quit = !restart.signalled ? 'already gone'
|
|
237
|
+
: restart.signals === 2 ? `quit: SIGTERM to its main process, ${restart.pid}, closed its windows, and a second SIGTERM ended that process, left without a window, at once, without its quit handlers`
|
|
238
|
+
: `quit (SIGTERM to its main process, ${restart.pid})`
|
|
239
|
+
return [`Obsidian: restarted as you asked, because ${why}: ${quit}; ${restart.startedAgain === true ? 'then started again plainly, which reopens the vaults it had open' : restart.startedAgain === false ? 'it could not be started again: start it yourself' : 'not started again yet'}.`, ...copy]
|
|
240
|
+
}
|
|
241
|
+
function obsidianSettingsLines(settings) {
|
|
242
|
+
if (settings === undefined || settings === null) return []
|
|
243
|
+
const file = settings.file ?? 'obsidian.json'
|
|
244
|
+
const what = settings.created
|
|
245
|
+
? `created ${file}${settings.directoryCreated ? ' (and its folder, private to you)' : ''}, because Obsidian never started on this account: this vault flagged open, and the command line on (cli: true)`
|
|
246
|
+
: [settings.vaultAdded ? `added this vault to the vault list in ${file}, flagged open` : null, settings.cliTurnedOn ? `turned the command line on (cli: true)${settings.vaultAdded ? '' : ` in ${file}`}` : null].filter(Boolean).join('; ')
|
|
247
|
+
return [`Obsidian's settings, written while it was quit: ${what}.`, ...(settings.backupPath ? [` The file as it was is kept in ${settings.backupPath}.`] : [])]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const isTyped = (error) => error instanceof ObsidianMaintenanceRefusal || error instanceof AtelierDiagnosticError || error instanceof ObsidianContractRefusal || error instanceof PublicationRefusal
|
|
106
251
|
const plainMessage = (error) => String(error.message ?? '').replace(new RegExp(`^${error.code}: `), '')
|
|
107
252
|
const NEXT = Object.freeze({
|
|
108
253
|
usage: 'run `atelier obsidian --help`',
|
|
109
254
|
'seams-required': 'pass the seams, or run the real command-line entry',
|
|
110
|
-
'app-adapter-not-selected': `pass --adapter=${PRODUCTION_ADAPTER} to let this command reach the installed app`,
|
|
111
|
-
'
|
|
255
|
+
'app-adapter-not-selected': `pass --adapter=${PRODUCTION_ADAPTER} to let this command reach the installed app; it is remembered for this workspace`,
|
|
256
|
+
'remembered-adapter-under-test': `a test passes its own seams, or --adapter=${PRODUCTION_ADAPTER} with a HOME that leads to no app of the developer's`,
|
|
257
|
+
'startup-consent-required': 'pass --consent-actor ID to record who allows the maintenance service to run (at a terminal, your account\'s name is recorded)',
|
|
112
258
|
'automatic-mode-refused': 'install an active automatic policy with `obsidian policy install FILE`',
|
|
113
259
|
[APPLY_UNAVAILABLE]: 'no apply operation is registered on this command; edits stay preserved and queued',
|
|
114
260
|
'policy-digest-mismatch': 'set the "digest" member of the file to the expected digest (`obsidian policy digest FILE` prints it), then install again',
|
|
115
|
-
disabled: 'declare the Obsidian settings in the project configuration',
|
|
261
|
+
disabled: 'declare the Obsidian settings in the project configuration: `atelier obsidian view add everything --all` does it',
|
|
262
|
+
'view-would-be-empty': 'check the folders or the tag, and who may see them (`atelier obsidian audience show`; `audience set me` admits every note that is only yours, those without a classification included, and leaves out `sensitive`; a list such as `me,sensitive` shows no note without a classification); --allow-empty declares the view anyway',
|
|
263
|
+
'view-exists': 'choose another name; `atelier obsidian view list` shows the views declared',
|
|
264
|
+
'view-repository-ambiguous': 'name the repository the folders are in with --repo',
|
|
265
|
+
'project-config-format-unknown': 'add the member shown under "detail" to the project configuration by hand, under "ext"',
|
|
266
|
+
'project-config-changed': 'run the command again',
|
|
267
|
+
'canonical-graph-invalid': '`atelier graph --check` names the errors in the project\'s notes; fix them, then run the command again',
|
|
268
|
+
unanswered: 'run the command again at a terminal, or pass --yes',
|
|
269
|
+
'startup-platform-unqualified': 'a login item is offered on macOS and Linux; `atelier obsidian open` starts maintenance when you open a view',
|
|
270
|
+
'startup-platform-unsupported': 'a login item is offered on macOS and Linux; `atelier obsidian open` starts maintenance when you open a view',
|
|
271
|
+
'login-item-needs-installed-package': 'install @mnstry/atelier in the project (`npm i -D @mnstry/atelier`), then run the command there',
|
|
272
|
+
'login-item-home-mismatch': 'run the command in your own session, with HOME set to your home directory',
|
|
273
|
+
'real-login-item-under-test': 'a test passes its own service manager',
|
|
274
|
+
'login-item-unavailable': 'no service manager of your session answered (on Linux: systemd --user); `atelier obsidian open` starts maintenance when you open a view',
|
|
275
|
+
'login-item-install-failed': 'see `atelier obsidian status`; `atelier obsidian service unit --install` tries again',
|
|
276
|
+
'login-item-remove-failed': 'see `atelier obsidian status`; `atelier obsidian service unit --remove` tries again',
|
|
277
|
+
'login-item-file-unsafe': 'the login item\'s file is a link or not a regular file and is left alone; remove it by hand, then try again',
|
|
278
|
+
'invalid-login-item': 'the login item record in private state does not validate; inspect it, remove it, then install the login item again',
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// The login item of `status`, in words.
|
|
282
|
+
function loginItemWords(item, platform) {
|
|
283
|
+
if (item.installed === null) return `unreadable (${item.reason})`
|
|
284
|
+
if (item.installed === false) return 'off'
|
|
285
|
+
const where = item.file
|
|
286
|
+
if (item.programPresent === false) return `installed, but the Node or the package it runs is gone, so it cannot start: install @mnstry/atelier in the project, then \`atelier obsidian service unit --install\`, or remove it with \`service unit --remove\`; ${where}`
|
|
287
|
+
return {
|
|
288
|
+
loaded: `on (${item.running ? 'running' : 'loaded'}); ${where}`,
|
|
289
|
+
'switched-off': `installed, ${platform === 'darwin' ? 'switched off in System Settings' : 'disabled'}; it does not start at login; ${where}`,
|
|
290
|
+
'file-missing': `installed, but its file is gone: it does not start at login; \`atelier obsidian service unit --install\` puts it back; ${where}`,
|
|
291
|
+
'not-loaded': `installed, not loaded by the service manager; it starts at the next login; ${where}`,
|
|
292
|
+
}[item.state] ?? `installed; ${where}`
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// What a start through the login item adds to the answer of `service start` and `open`.
|
|
296
|
+
function loginItemLines(item) {
|
|
297
|
+
if (item === undefined || item === null) return []
|
|
298
|
+
return [
|
|
299
|
+
...(item.refreshed === true ? ['login item refreshed: its unit is written as it would be now, and loaded again'] : []),
|
|
300
|
+
...(item.via === 'child' ? [`the login item did not start the service (${item.reason}); it was started for this session only`] : []),
|
|
301
|
+
]
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// A start's answer in words: started, replaced, or already running, and which release when that is why it was kept.
|
|
305
|
+
function serviceWords(result) {
|
|
306
|
+
if (result.replaced === 'outdated' && result.started) return `${result.state} (restarted: the service of an earlier release was replaced)`
|
|
307
|
+
if (result.started) return `${result.state} (started)`
|
|
308
|
+
if (result.alreadyRunning) {
|
|
309
|
+
if (result.release === 'later') return `${result.state} (already running, a later release; it is not replaced by this one)`
|
|
310
|
+
if (result.release === 'outdated') return `${result.state} (already running, an earlier release, not replaced: ${result.reason})`
|
|
311
|
+
return `${result.state} (already running)`
|
|
312
|
+
}
|
|
313
|
+
return `${result.state} (${result.reason})`
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// What `status` suggests after the login item did not start the service.
|
|
317
|
+
const LOGIN_ITEM_NEXT = Object.freeze({
|
|
318
|
+
'startup-consent-absent': 'install it again with `atelier obsidian service unit --install --consent-actor ID`',
|
|
319
|
+
'service-port-occupied': 'something else answers on the recorded port; see `atelier obsidian service status`',
|
|
320
|
+
'service-settings-absent': 'install it again with `atelier obsidian service unit --install --consent-actor ID`',
|
|
116
321
|
})
|
|
117
322
|
|
|
323
|
+
// What a view would show, in words: `counts` of viewCounts, and who they were counted for.
|
|
324
|
+
function viewCountWords(scopeId, counts, audience) {
|
|
325
|
+
const who = audience.decided ? (audience.allow.length === 0 ? 'no audience' : audience.allow.join(', ')) : 'only you, until you decide'
|
|
326
|
+
const reasons = [
|
|
327
|
+
...(counts.withheld.unclassified > 0 ? [`${counts.withheld.unclassified} carry no classification`] : []),
|
|
328
|
+
...(counts.withheld.audience > 0 ? [`${counts.withheld.audience} an audience not admitted`] : []),
|
|
329
|
+
]
|
|
330
|
+
const shown = `view ${scopeId} would show ${counts.shown === 0 ? 'no note' : `${counts.shown} note(s)`} (who may see: ${who})`
|
|
331
|
+
if (counts.named === 0) return `${shown}: it names no note`
|
|
332
|
+
return reasons.length === 0 ? shown : `${shown}; of the ${counts.named} note(s) it names, ${reasons.join(' and ')}`
|
|
333
|
+
}
|
|
334
|
+
|
|
118
335
|
// The decisions the command's oracles are sensitive to are those of opening and of the lifecycle; tests substitute
|
|
119
336
|
// broken ones here to prove the oracles can fail. `runObsidianCommand` always uses the production ones.
|
|
120
337
|
export async function runObsidianCommandForOracleTests(options = {}, rules = {}) {
|
|
@@ -123,9 +340,22 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
123
340
|
const {
|
|
124
341
|
argv = [], seams = null, production = false, env = process.env, cwd = process.cwd(), platform = process.platform, clock = () => new Date(),
|
|
125
342
|
stdout = (text) => process.stdout.write(`${text}\n`), stderr = (text) => process.stderr.write(`${text}\n`), contributions = null, contributionsDirectory,
|
|
343
|
+
// Whether standard input and output are terminals, and the name of the account this runs as. A test injects both;
|
|
344
|
+
// under the test runner the process's own terminal is never looked at.
|
|
345
|
+
terminal = env.NODE_TEST_CONTEXT === undefined ? { stdin: tty.isatty(0), stdout: tty.isatty(1) } : { stdin: false, stdout: false },
|
|
346
|
+
account = accountName,
|
|
347
|
+
// The home folder a location is read against (`~/`) and checked for sync clients; a test names its own.
|
|
348
|
+
homedir = env.NODE_TEST_CONTEXT === undefined ? os.homedir() : null,
|
|
349
|
+
// Where a person at a terminal is asked, and answers; a test types into its own.
|
|
350
|
+
input = env.NODE_TEST_CONTEXT === undefined ? process.stdin : null, output = env.NODE_TEST_CONTEXT === undefined ? process.stdout : null,
|
|
126
351
|
} = options
|
|
127
352
|
let json = argv.includes('--json')
|
|
128
353
|
let operationName = null
|
|
354
|
+
let questions = null
|
|
355
|
+
const questioner = () => {
|
|
356
|
+
if (input === null || output === null) throw new Error('a question needs an input and an output')
|
|
357
|
+
return (questions ??= createQuestioner({ input, output }))
|
|
358
|
+
}
|
|
129
359
|
const emit = ({ exit, document, human }) => {
|
|
130
360
|
const complete = { schema: COMMAND_SCHEMA, ok: exit === EXIT.ok, operation: operationName, ...document }
|
|
131
361
|
if (json) stdout(JSON.stringify(complete, null, 2))
|
|
@@ -147,18 +377,77 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
147
377
|
const applyAvailable = registry.extensions.applyOperation() !== UNAVAILABLE_APPLY_OPERATION
|
|
148
378
|
const lifecycle = { loadProject, dataRoot, env, platform, ...(options.probeTimeoutMs === undefined ? {} : { probeTimeoutMs: options.probeTimeoutMs }) }
|
|
149
379
|
|
|
150
|
-
|
|
380
|
+
// The adapter of this run: given now, or remembered by this workspace (read only when the real entry has no flag).
|
|
381
|
+
const chooseAdapter = () => selectAdapter({ flag: flags.adapter, remembered: flags.adapter === undefined && production === true ? rememberedAdapter() : null, production, env })
|
|
382
|
+
// The entry this command may start, and its arguments.
|
|
383
|
+
const entrySeam = async () => {
|
|
151
384
|
if (seams !== null) { if (typeof seams.service?.entryPath !== 'string') refuse('seams-required', 'the service seam names the entry this command may start'); return seams.service }
|
|
152
|
-
|
|
385
|
+
chooseAdapter()
|
|
153
386
|
const { SERVICE_ENTRY_PATH } = await import('../runtime/obsidian/service-entry-path.mjs')
|
|
154
387
|
return { entryPath: SERVICE_ENTRY_PATH, entryArgs: [`--adapter=${PRODUCTION_ADAPTER}`] }
|
|
155
388
|
}
|
|
389
|
+
// The service manager of a login item: the caller's (`serviceManager`, or `seams.serviceManager`), or, for the real
|
|
390
|
+
// command-line entry only, the production one, which refuses under the test runner and for a HOME that is not the
|
|
391
|
+
// account's own. Without `required`, null where none can be had.
|
|
392
|
+
const managerSeam = async ({ required = true } = {}) => {
|
|
393
|
+
const given = options.serviceManager ?? seams?.serviceManager ?? null
|
|
394
|
+
if (given !== null) return given
|
|
395
|
+
if (seams !== null || production !== true) { if (required) refuse('seams-required', 'no service manager was passed for the login item'); return null }
|
|
396
|
+
try {
|
|
397
|
+
const { createProductionServiceManager } = await import('../runtime/obsidian/service-manager-production.mjs')
|
|
398
|
+
return createProductionServiceManager({ platform, env })
|
|
399
|
+
} catch (error) {
|
|
400
|
+
if (!required && isTyped(error)) return null
|
|
401
|
+
throw error
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
const nodePath = () => options.nodePath ?? realNodePath()
|
|
405
|
+
// The service seam: once this workspace has a login item, the service is started through its manager, running the
|
|
406
|
+
// entry the item names; a unit that differs from what would be written now is written again on the way.
|
|
407
|
+
const serviceSeam = async () => {
|
|
408
|
+
const entry = await entrySeam()
|
|
409
|
+
const { project, workspace } = readable()
|
|
410
|
+
let record = null
|
|
411
|
+
try { record = workspace === null ? null : readLoginItemRecord(workspace) } catch (error) { if (!isTyped(error)) throw error }
|
|
412
|
+
if (record === null) return entry
|
|
413
|
+
const manager = await managerSeam({ required: false })
|
|
414
|
+
if (manager === null) return entry
|
|
415
|
+
const plan = currentLoginItemPlan({ record, project, workspaceRoot: workspace.workspaceRoot, dataRoot, env, platform, ownEntry: entry.entryPath, entryArgs: entry.entryArgs ?? [], nodePath: nodePath() })
|
|
416
|
+
// An item that names an entry that is gone, with nothing to refresh it to from here, cannot start anything.
|
|
417
|
+
if (plan === null && !fs.existsSync(record.program.entry)) return entry
|
|
418
|
+
return { ...entry, entryPath: plan?.program.entry ?? record.program.entry, loginItem: loginItemStarter({ workspace, manager, record, plan, clock }) }
|
|
419
|
+
}
|
|
156
420
|
const appSeams = async () => {
|
|
157
|
-
if (seams !== null) return { appProbe: seams.appProbe, launcher: seams.launcher, registry: seams.registry }
|
|
158
|
-
|
|
421
|
+
if (seams !== null) return { appProbe: seams.appProbe, launcher: seams.launcher, registry: seams.registry, quitter: seams.quitter ?? null }
|
|
422
|
+
chooseAdapter()
|
|
159
423
|
const { createProductionAppSeams } = await import('../runtime/obsidian/app-production-seams.mjs')
|
|
160
424
|
return createProductionAppSeams({ env, platform })
|
|
161
425
|
}
|
|
426
|
+
// The app's vault list, read only, for a check of where vaults may live: through the app when it runs and answers,
|
|
427
|
+
// else from its settings file. { source: 'app' | 'file' | 'none', vaults } ('none': the app never ran here), or
|
|
428
|
+
// { source: 'unread', vaults: null, reason }. The app is reached only with the adapter (given or remembered, or the
|
|
429
|
+
// caller's seams); without it, only the file is read, and never under the test runner.
|
|
430
|
+
const appVaultList = async () => {
|
|
431
|
+
const quietly = async (read) => { try { return await read() } catch { return null } }
|
|
432
|
+
const reach = seams !== null ? seams : adapterSelected() ? await appSeams() : null
|
|
433
|
+
const registry = reach?.registry ?? null
|
|
434
|
+
if (registry !== null && reach.appProbe !== undefined) {
|
|
435
|
+
const seen = await inspectApp(reach.appProbe)
|
|
436
|
+
if (seen.running === true && typeof seen.version === 'string') {
|
|
437
|
+
const listed = await quietly(() => registry.listThroughApp())
|
|
438
|
+
if (listed?.answered === true) return { source: 'app', vaults: listed.vaults }
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
const settings = registry !== null ? await quietly(() => registry.readSettings())
|
|
442
|
+
: env.NODE_TEST_CONTEXT !== undefined ? { ok: false, code: 'app-vault-list-under-test' }
|
|
443
|
+
: obsidianSandboxedBuild({ platform, env }) !== null ? { ok: false, code: 'obsidian-sandboxed' }
|
|
444
|
+
: readObsidianSettings({ userDataDir: obsidianUserDataDir({ platform, env }) })
|
|
445
|
+
if (settings?.ok === true) return { source: 'file', vaults: settings.vaults ?? {} }
|
|
446
|
+
if (settings?.code === 'obsidian-settings-missing') return { source: 'none', vaults: {} }
|
|
447
|
+
return { source: 'unread', vaults: null, reason: typeof settings?.code === 'string' ? settings.code : 'obsidian-settings-unreadable' }
|
|
448
|
+
}
|
|
449
|
+
// Whether this run may reach the installed app: --adapter given, or remembered by the workspace for the real entry.
|
|
450
|
+
const adapterSelected = () => { try { chooseAdapter(); return true } catch (error) { if (isTyped(error)) return false; throw error } }
|
|
162
451
|
// Atelier's plugin as the running service sees it, for one view. A vault that turned it off says so from its own list
|
|
163
452
|
// and the private record too, whether or not the service runs.
|
|
164
453
|
const pluginView = (running, workspace, scopeId) => {
|
|
@@ -167,8 +456,11 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
167
456
|
return { present: false, reason: 'turned-off-in-this-vault', next: turnPluginOnNext(scopeId) }
|
|
168
457
|
}
|
|
169
458
|
const pluginOf = async (scopeId) => pluginPresenceOf((await readServiceStatusDocument(lifecycle, lifecycleRules)).document, scopeId)
|
|
170
|
-
const pluginLine = (plugin) =>
|
|
459
|
+
const pluginLine = (plugin) => `${plugin.present ? `; plugin present (Obsidian ${plugin.appVersion})` : plugin.reason === 'turned-off-in-this-vault' ? '; plugin turned off in this vault' : `; plugin not present (${plugin.reason})`}${plugin.files === 'waits-for-app' ? '; plugin files wait for the app' : ''}`
|
|
171
460
|
|
|
461
|
+
// A project bound to a personal workspace takes its views from the overlay, and has no place for these decisions
|
|
462
|
+
// yet: the operations that would write them refuse before anything is resolved or written.
|
|
463
|
+
const refuseOnPersonalBinding = async (project, operation) => (await import('../projection/obsidian/personal-workspace.mjs')).refuseOnPersonalBinding(project, operation)
|
|
172
464
|
const configured = () => {
|
|
173
465
|
const project = loadProject()
|
|
174
466
|
const enablement = readObsidianEnablement(project)
|
|
@@ -189,15 +481,225 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
189
481
|
return { project, enablement, workspace: workspace?.workspaceRoot ? workspace : null, workspaceId: workspace?.workspaceId ?? null }
|
|
190
482
|
}
|
|
191
483
|
const machineOf = (workspace) => (workspace === null ? null : readMachineSettings(workspace))
|
|
192
|
-
const
|
|
484
|
+
const rememberedAdapter = () => machineOf(readable().workspace)?.decisions.adapter?.choice ?? null
|
|
485
|
+
const interactive = isInteractive({ flags, env, terminal })
|
|
486
|
+
// Who allows the maintenance service to run, for a start this run may make: --consent-actor; else, for a person at a
|
|
487
|
+
// terminal and only while this workspace has no consent recorded, their account's name. A derived consent never
|
|
488
|
+
// replaces a recorded one. `source` says which.
|
|
489
|
+
// With a login item installed, the service is started through it, so the consent covers startup too, or the item's
|
|
490
|
+
// own start would be refused (`seam.loginItem`).
|
|
491
|
+
const consentOfThisRun = (seam = null) => {
|
|
492
|
+
const coverage = seam?.loginItem ? 'service-and-startup' : 'service'
|
|
493
|
+
if (flags['consent-actor'] !== undefined) return { consent: { actor: flags['consent-actor'], coverage }, source: 'flag' }
|
|
494
|
+
if (!interactive) return { consent: undefined, source: null }
|
|
495
|
+
const { workspace } = readable()
|
|
496
|
+
if (workspace !== null && readServiceSettings(workspace) !== null) return { consent: undefined, source: null }
|
|
497
|
+
const actor = accountActor(account())
|
|
498
|
+
// `derived`: the start records it only while no consent is recorded, checked again under the start lock.
|
|
499
|
+
return actor === null ? { consent: undefined, source: null } : { consent: { actor, coverage, derived: true }, source: 'account' }
|
|
500
|
+
}
|
|
501
|
+
// --adapter, given to an operation that starts or reaches the service of an enabled project, is remembered the first
|
|
502
|
+
// time: the workspace is prepared and the decision written before anything starts, so no later run needs the flag.
|
|
503
|
+
// Answers whether this run remembered it.
|
|
504
|
+
// Remembering never stands in the way of the operation: a workspace that cannot be prepared refuses the operation
|
|
505
|
+
// itself, which says so in its own terms.
|
|
506
|
+
const rememberAdapter = (decidedBy) => {
|
|
507
|
+
if (flags.adapter !== PRODUCTION_ADAPTER) return false
|
|
508
|
+
try {
|
|
509
|
+
if (readable().enablement.state !== 'enabled') return false
|
|
510
|
+
const { workspace, repositoryRoots, now } = writable()
|
|
511
|
+
const current = machineOf(workspace) ?? defaultMachineSettings({ workspaceId: workspace.workspaceId, updatedAt: now })
|
|
512
|
+
if (current.decisions.adapter?.choice === PRODUCTION_ADAPTER) return false
|
|
513
|
+
const decided = withDecision(current, 'adapter', { choice: PRODUCTION_ADAPTER }, { decidedAt: now, decidedBy: decidedBy ?? accountActor(account()), via: 'command' })
|
|
514
|
+
writeMachineSettings({ ...workspace, repositoryRoots, settings: { ...decided, updatedAt: now } })
|
|
515
|
+
return true
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (isTyped(error)) return false
|
|
518
|
+
throw error
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
// What a run that starts or reaches the service says about what it remembered, in the document and in words.
|
|
522
|
+
const rememberedLines = ({ adapterRemembered, consent }) => [
|
|
523
|
+
...(consent.source === 'account' ? [`The maintenance service is allowed by ${consent.consent.actor}, this account; recorded for this workspace.`] : []),
|
|
524
|
+
...(adapterRemembered ? [`Remembered for this workspace: the app is reached through ${PRODUCTION_ADAPTER}; --adapter is not needed again.`] : []),
|
|
525
|
+
]
|
|
193
526
|
const shownMachine = (machine, workspace) => {
|
|
194
|
-
const settings = machine ?? {
|
|
527
|
+
const settings = machine ?? defaultMachineSettings({ workspaceId: workspace?.workspaceId ?? 'ws-unprepared', updatedAt: isoTime(clock) })
|
|
195
528
|
const automatic = workspace === null ? { authorized: false, reason: 'machine-settings-absent' } : authorizeAutomaticApply(workspace)
|
|
196
|
-
return {
|
|
529
|
+
return {
|
|
530
|
+
maintenanceMode: settings.maintenanceMode, audienceAllow: settings.audienceAllow, applyPolicy: settings.applyPolicy, automaticApply: { authorized: automatic.authorized, reason: automatic.reason },
|
|
531
|
+
decisions: settings.decisions,
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// After a run that may have started the service: whether the consent it derived is the one now recorded.
|
|
535
|
+
const consentRecorded = (consent) => {
|
|
536
|
+
if (consent.source !== 'account') return consent
|
|
537
|
+
const { workspace } = readable()
|
|
538
|
+
const recorded = workspace === null ? null : readServiceSettings(workspace)
|
|
539
|
+
return recorded?.consent.actor === consent.consent.actor ? consent : { consent: undefined, source: null }
|
|
197
540
|
}
|
|
198
541
|
const applyShown = { available: applyAvailable, state: applyAvailable ? 'available' : APPLY_UNAVAILABLE, operationId: registry.extensions.applyOperation().id }
|
|
199
542
|
const [, sub, value] = positionals
|
|
200
543
|
|
|
544
|
+
// The login item's text as `--install` would write it now. Writes nothing and asks no service manager.
|
|
545
|
+
const printUnit = async () => {
|
|
546
|
+
const { project, workspace } = readable()
|
|
547
|
+
if (workspace === null) refuse('service-workspace-not-prepared', 'this workspace has no private state yet; `service start` prepares it')
|
|
548
|
+
const entry = await entrySeam()
|
|
549
|
+
const record = readLoginItemRecord(workspace)
|
|
550
|
+
const temporary = temporaryRoots()
|
|
551
|
+
const { entryPath } = resolveLoginItemEntry({ project, ownEntry: entry.entryPath, temporary })
|
|
552
|
+
const label = record?.label ?? loginItemLabel({ platform, projectName: projectNameOf(project), workspaceId: workspace.workspaceId })
|
|
553
|
+
const { platform: unitPlatform, kind, fileName, text } = planLoginItem({
|
|
554
|
+
platform, project, workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId, dataRoot: loginItemDataRoot({ project, dataRoot, env, platform }), label, entryPath,
|
|
555
|
+
entryArgs: entry.entryArgs ?? [], nodePath: nodePath(),
|
|
556
|
+
searchPath: record?.searchPath ?? startupSearchPath(env.PATH, { temporary }),
|
|
557
|
+
})
|
|
558
|
+
return { exit: EXIT.ok, document: { unit: { platform: unitPlatform, kind, fileName, text }, installed: record !== null, note: 'the service runs under this unit only with a recorded consent that covers startup; `service unit --install` records one' }, human: [text] }
|
|
559
|
+
}
|
|
560
|
+
// Who allows the service to run at login, for `--install`: --consent-actor; else, for a person at a terminal, the
|
|
561
|
+
// actor this workspace already recorded (its coverage is raised to startup), or the account's name when none is.
|
|
562
|
+
// Asking for the login item at a terminal is that person's consent to it. A program names its actor.
|
|
563
|
+
const startupConsent = () => {
|
|
564
|
+
if (flags['consent-actor'] !== undefined) return { actor: flags['consent-actor'] }
|
|
565
|
+
if (!interactive) return undefined
|
|
566
|
+
const { workspace } = readable()
|
|
567
|
+
const recorded = workspace === null ? null : readServiceSettings(workspace)
|
|
568
|
+
const actor = recorded?.consent.actor ?? accountActor(account())
|
|
569
|
+
return actor === null ? undefined : { actor }
|
|
570
|
+
}
|
|
571
|
+
// Installs the login item, then starts the service through it (launchd has usually started it already).
|
|
572
|
+
const installUnit = async () => {
|
|
573
|
+
configured()
|
|
574
|
+
if (!STARTUP_PLATFORMS.includes(platform)) buildStartupAdapter({ platform })
|
|
575
|
+
const entry = await entrySeam()
|
|
576
|
+
const manager = await managerSeam()
|
|
577
|
+
const consent = startupConsent()
|
|
578
|
+
// A service of an earlier release still running would make the unit's own first start refuse (another runtime
|
|
579
|
+
// answers): it is stopped as its owner stops it once the consent is recorded, just before the unit is loaded.
|
|
580
|
+
let replaced = null
|
|
581
|
+
const beforeInstall = async ({ entryPath }) => {
|
|
582
|
+
if (runtimeRelease(await serviceStatus(lifecycle, lifecycleRules), entryPath) !== 'outdated') return
|
|
583
|
+
if ((await stopService(lifecycle, lifecycleRules)).stopped) replaced = 'outdated'
|
|
584
|
+
}
|
|
585
|
+
const installed = await installLoginItem({
|
|
586
|
+
loadProject, dataRoot, env, platform, manager, ownEntry: entry.entryPath, entryArgs: entry.entryArgs ?? [], nodePath: nodePath(), pathValue: env.PATH, clock, beforeInstall,
|
|
587
|
+
...(consent === undefined ? {} : { consent }),
|
|
588
|
+
})
|
|
589
|
+
if (!installed.installed) {
|
|
590
|
+
// The earlier release was stopped for nothing: the installed one runs in its place for this session.
|
|
591
|
+
const service = replaced === null ? null : (({ child: _child, ...result }) => result)(await startService({ ...lifecycle, detached: true, ...entry, replaceOutdated: true }, lifecycleRules))
|
|
592
|
+
return {
|
|
593
|
+
exit: EXIT.notSuccess, document: { loginItem: installed, ...(service === null ? {} : { service: { ...service, replaced } }) },
|
|
594
|
+
human: [`login item not installed: ${installed.reason}${installed.message ? ` (${installed.message})` : ''}`, `Next: ${NEXT[installed.reason] ?? 'see `atelier obsidian status`'}`, ...(service === null ? [] : [`service: ${serviceWords({ ...service, replaced })}`])],
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const remembered = rememberLoginItem('on')
|
|
598
|
+
const adapterRemembered = rememberAdapter(consent?.actor)
|
|
599
|
+
const seam = await serviceSeam()
|
|
600
|
+
const started = (({ child: _child, ...result }) => result)(await startService({ ...lifecycle, detached: true, ...seam, replaceOutdated: true }, lifecycleRules))
|
|
601
|
+
const service = replaced === null || started.replaced !== undefined ? started : { ...started, replaced }
|
|
602
|
+
const running = service.state === 'healthy' || service.state === 'busy'
|
|
603
|
+
return {
|
|
604
|
+
exit: running ? EXIT.ok : EXIT.notSuccess, document: { loginItem: installed, service, rememberedNow: { loginItem: remembered ? 'on' : null, adapter: adapterRemembered } },
|
|
605
|
+
human: [
|
|
606
|
+
`login item installed: ${installed.file}`, `it runs ${installed.entry.path}${installed.entry.source === 'project' ? ', the package installed in this project' : ''}`,
|
|
607
|
+
...(platform === 'darwin' ? ['macOS lists it as "node" under Login Items & Extensions, and may say "Background Items Added"'] : []),
|
|
608
|
+
`service: ${serviceWords(service)}`, ...loginItemLines(service.loginItem),
|
|
609
|
+
...(consent !== undefined && flags['consent-actor'] === undefined ? [`Maintenance at login is allowed by ${consent.actor}; recorded for this workspace.`] : []),
|
|
610
|
+
...rememberedLines({ adapterRemembered, consent: { source: null } }),
|
|
611
|
+
],
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
// Removes the login item. A service it ran is stopped as its manager unloads it, and is started again at once as a
|
|
615
|
+
// process of its own for this session, under the consent now recorded (the service alone), so the vaults stay fresh
|
|
616
|
+
// until the person logs out. That start needs the adapter, given or remembered; without it, `open` starts it.
|
|
617
|
+
const removeUnit = async () => {
|
|
618
|
+
if (!STARTUP_PLATFORMS.includes(platform)) buildStartupAdapter({ platform })
|
|
619
|
+
const manager = await managerSeam()
|
|
620
|
+
const removed = await removeLoginItem({ loadProject, dataRoot, env, platform, manager, clock })
|
|
621
|
+
const failed = removed.removed !== true && removed.reason !== undefined && removed.reason !== 'workspace-not-prepared'
|
|
622
|
+
if (failed) return { exit: EXIT.notSuccess, document: { loginItem: removed }, human: [`login item not removed: ${removed.reason}${removed.message ? ` (${removed.message})` : ''}`, `Next: ${NEXT[removed.reason] ?? 'see `atelier obsidian status`'}`] }
|
|
623
|
+
const remembered = removed.reason === 'workspace-not-prepared' ? false : rememberLoginItem('off')
|
|
624
|
+
let service = null
|
|
625
|
+
if (removed.removed === true) {
|
|
626
|
+
let entry = null
|
|
627
|
+
try { entry = await entrySeam() } catch (error) { if (!isTyped(error)) throw error; service = { state: 'stopped', started: false, reason: error.code } }
|
|
628
|
+
if (entry !== null) {
|
|
629
|
+
try { service = (({ child: _child, ...result }) => result)(await startService({ ...lifecycle, detached: true, ...entry, replaceOutdated: true }, lifecycleRules)) } catch (error) {
|
|
630
|
+
if (!isTyped(error)) throw error
|
|
631
|
+
service = { state: 'stopped', started: false, reason: error.code }
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
service ??= await serviceStatus(lifecycle, lifecycleRules)
|
|
636
|
+
const running = ['healthy', 'busy'].includes(service.state)
|
|
637
|
+
return {
|
|
638
|
+
exit: EXIT.ok, document: { loginItem: removed, service: { state: service.state, reason: service.reason ?? null, started: service.started === true }, rememberedNow: { loginItem: remembered ? 'off' : null } },
|
|
639
|
+
human: [
|
|
640
|
+
removed.removed ? `login item removed: ${removed.file}` : 'no login item was installed', 'the consent now covers the service alone',
|
|
641
|
+
`service: ${service.state}${running ? (service.started ? ' (started for this session only; it does not start at login)' : '') : `${service.reason ? ` (${service.reason})` : ''}; \`atelier obsidian open\` starts it again`}`,
|
|
642
|
+
],
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
// The person's answer to "start at login?", remembered for this workspace as the `loginItem` decision of its machine
|
|
646
|
+
// settings, given on the command line. By whom: the actor named, else a person at a terminal by the account's name,
|
|
647
|
+
// else nobody known. Answers whether it wrote; an answer already remembered is not written again.
|
|
648
|
+
const rememberLoginItem = (choice) => {
|
|
649
|
+
if (readable().enablement.reason === 'not-configured') return false
|
|
650
|
+
const { workspace, repositoryRoots, now } = writable()
|
|
651
|
+
const current = machineOf(workspace) ?? defaultMachineSettings({ workspaceId: workspace.workspaceId, updatedAt: now })
|
|
652
|
+
if (current.decisions.loginItem?.choice === choice) return false
|
|
653
|
+
const decidedBy = flags['consent-actor'] ?? (interactive ? accountActor(account()) : null)
|
|
654
|
+
const decided = withDecision(current, 'loginItem', { choice }, { decidedAt: now, decidedBy, via: 'command' })
|
|
655
|
+
writeMachineSettings({ ...workspace, repositoryRoots, settings: { ...decided, updatedAt: now } })
|
|
656
|
+
return true
|
|
657
|
+
}
|
|
658
|
+
// Where what `uninstall` keeps is: the vaults, the private state, the project file and Obsidian's vault list. The
|
|
659
|
+
// vaults are those under the data root and every folder allocated to a view where the workspace decided its vaults
|
|
660
|
+
// live, a view no longer declared included; a record that cannot be read names none.
|
|
661
|
+
const keptLocations = (project, workspace) => {
|
|
662
|
+
const vaultsDirectory = workspace === null ? null : path.join(workspace.workspaceRoot, 'vaults')
|
|
663
|
+
let underDataRoot = []
|
|
664
|
+
try { underDataRoot = vaultsDirectory === null ? [] : fs.readdirSync(vaultsDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path.join(vaultsDirectory, entry.name)) } catch { underDataRoot = [] }
|
|
665
|
+
const allocated = []
|
|
666
|
+
if (workspace !== null) {
|
|
667
|
+
const records = path.join(workspace.workspaceRoot, 'state', 'allocations')
|
|
668
|
+
let names = []
|
|
669
|
+
try { names = fs.readdirSync(records).filter((name) => name.endsWith('.json')) } catch { names = [] }
|
|
670
|
+
for (const name of names) {
|
|
671
|
+
// Whatever the file is or holds (a link, a FIFO, not JSON, `null`, another view's record), it names a folder only
|
|
672
|
+
// when it is a valid record of its own view, which the store would read too; nothing here can stop `uninstall`.
|
|
673
|
+
try {
|
|
674
|
+
// Read as the store reads a record: a regular file only, never through a link, never a FIFO that would block.
|
|
675
|
+
const scopeId = JSON.parse(readRegularTextNoFollow(path.join(records, name)))?.scopeId
|
|
676
|
+
if (typeof scopeId !== 'string' || path.basename(allocationFile(workspace.workspaceRoot, scopeId)) !== name) continue
|
|
677
|
+
const allocation = readVaultAllocation({ ...workspace, scopeId })
|
|
678
|
+
if (allocation !== null) allocated.push(allocation.path)
|
|
679
|
+
} catch { /* a record that cannot be read names no folder */ }
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const vaults = [...new Set([...underDataRoot, ...allocated])].sort()
|
|
683
|
+
const userDataDir = obsidianUserDataDir({ platform, env })
|
|
684
|
+
return { vaults, privateState: workspace?.workspaceRoot ?? null, projectFile: project.configPath ?? null, pointer: project.configPath ? localPointerPath(project) : null, obsidianList: userDataDir === null ? null : path.join(userDataDir, OBSIDIAN_SETTINGS_FILE) }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Where a view's vault is, or will be: { path, origin }. A view the next tick will place is shown where it would go,
|
|
688
|
+
// if that name is still free then (`to-be-allocated`).
|
|
689
|
+
// A view whose record cannot be read, or was lost, says why, as `status` does, and the other views are still shown.
|
|
690
|
+
const vaultWhere = (workspace, scopeId, decided, projectName) => {
|
|
691
|
+
if (workspace === null) return { path: null, origin: 'workspace-not-prepared' }
|
|
692
|
+
let found
|
|
693
|
+
try { found = vaultRootFor({ ...workspace, scopeId }) } catch (error) {
|
|
694
|
+
if (!isTyped(error)) throw error
|
|
695
|
+
return { path: null, origin: 'unreadable', reason: error.code }
|
|
696
|
+
}
|
|
697
|
+
if (found.origin === 'legacy-data-root' && decided !== null && !hasCommittedGeneration({ ...workspace, scopeId })) {
|
|
698
|
+
return { path: path.join(decided.parent, vaultFolderName({ projectName, scopeId })), origin: 'to-be-allocated' }
|
|
699
|
+
}
|
|
700
|
+
return { path: found.path, origin: found.origin }
|
|
701
|
+
}
|
|
702
|
+
|
|
201
703
|
const operations = {
|
|
202
704
|
async status() {
|
|
203
705
|
const { project, enablement, workspace, workspaceId } = readable()
|
|
@@ -205,25 +707,34 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
205
707
|
const running = service.state === 'healthy' ? (await readServiceStatusDocument(lifecycle, lifecycleRules)).document : null
|
|
206
708
|
// What the service last learned about the app, and what a person can do about it.
|
|
207
709
|
const app = running?.app ? { ...running.app, next: running.app.outcome === 'qualified' ? null : nextStep(running.app.outcome, running.app.reason) } : null
|
|
710
|
+
// The login item: its record, what its manager says (asked only when one is recorded), and how its last start ended.
|
|
711
|
+
const item = workspace === null ? { installed: false, lastStartup: null } : await loginItemStatus({ workspace, manager: () => managerSeam({ required: false }) })
|
|
712
|
+
const refusedAtLogin = item.lastStartup?.outcome === 'refused' && item.lastStartup.code !== 'service-already-running' && !['healthy', 'busy'].includes(service.state) ? item.lastStartup.code : null
|
|
713
|
+
const loginItem = { ...item, didNotStart: refusedAtLogin, next: refusedAtLogin === null ? null : LOGIN_ITEM_NEXT[refusedAtLogin] ?? NEXT[refusedAtLogin] ?? 'see `atelier obsidian service status`, and the private log it names' }
|
|
208
714
|
const scopes = workspace === null
|
|
209
715
|
? enablement.scopes.map(({ scopeId }) => ({ scopeId, outcome: enablement.state === 'disabled' ? 'disabled' : 'not-prepared', reason: enablement.state === 'disabled' ? enablement.reason : 'workspace-not-prepared' }))
|
|
210
716
|
: enablement.scopes.map(({ scopeId }) => {
|
|
211
|
-
const { vaultRoot: _vault, summary: _summary, ...
|
|
212
|
-
|
|
717
|
+
const { vaultRoot: _vault, summary: _summary, ...found } = scopeReport({ workspace, scopeId, repositoryRoots: protectedRoots(project), serviceState: service.state, applyAvailable }, openingRules)
|
|
718
|
+
// A view whose vault the next tick allocates elsewhere says where, not the data root it will not use.
|
|
719
|
+
const report = found.vault?.origin === 'legacy-data-root' ? { ...found, vault: vaultWhere(workspace, scopeId, machineOf(workspace)?.decisions.location ?? null, projectDisplayName(project)) } : found
|
|
720
|
+
const plugin = withPluginFilesPending(pluginView(running, workspace, scopeId), report.freshness)
|
|
213
721
|
return enablement.state === 'disabled' ? { ...report, outcome: 'disabled', reason: enablement.reason, next: OPENING_OUTCOMES.disabled.next, plugin } : { ...report, plugin }
|
|
214
722
|
})
|
|
215
723
|
const document = {
|
|
216
724
|
enablement: { state: enablement.state, reason: enablement.reason, defaultScopeId: enablement.defaultScopeId }, workspace: { workspaceId, prepared: workspace !== null },
|
|
217
725
|
machine: shownMachine(machineOf(workspace), workspace), apply: applyShown,
|
|
218
726
|
service: { state: service.state, reason: service.reason ?? null, address: service.address ?? null, runtimeId: service.record?.runtimeId ?? null, pid: service.record?.pid ?? null, lastTick: running?.lastTick ?? null, lastError: running?.lastError ?? null, app },
|
|
219
|
-
app: { probed: false, minimumVersion: MINIMUM_APP_VERSION }, scopes, extensions: registry.extensions.describe(), operations: registry.operations.describe(),
|
|
727
|
+
loginItem, app: { probed: false, minimumVersion: MINIMUM_APP_VERSION }, scopes, extensions: registry.extensions.describe(), operations: registry.operations.describe(),
|
|
220
728
|
}
|
|
221
729
|
return {
|
|
222
730
|
exit: EXIT.ok, document,
|
|
223
731
|
human: [
|
|
224
732
|
`obsidian: ${enablement.state} (${enablement.reason}); mode ${document.machine.maintenanceMode}; audiences ${document.machine.audienceAllow.join(', ') || 'none'}`,
|
|
733
|
+
`remembered: ${decisionSummary(document.machine.decisions)}`,
|
|
225
734
|
`service: ${service.state} (${service.reason ?? 'no reason'})${app ? `; app ${app.outcome} (${app.reason})` : ''}`,
|
|
226
735
|
...(app?.next ? [`Next for the app: ${app.next}`] : []),
|
|
736
|
+
`login item: ${loginItemWords(loginItem, platform)}`,
|
|
737
|
+
...(refusedAtLogin === null ? [] : [`the login item did not start the service: ${refusedAtLogin}`, `Next: ${loginItem.next}`]),
|
|
227
738
|
`apply: ${applyShown.state}`,
|
|
228
739
|
...scopes.flatMap((scope) => [
|
|
229
740
|
`view ${scope.scopeId}: ${scope.outcome} (${scope.reason})${scope.pendingEdits?.open ? `; ${scope.pendingEdits.open} pending edit(s), apply ${scope.pendingEdits.apply}` : ''}${scope.plugin ? pluginLine(scope.plugin) : ''}`,
|
|
@@ -234,6 +745,125 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
234
745
|
}
|
|
235
746
|
},
|
|
236
747
|
|
|
748
|
+
// What this machine remembers for this workspace, and how each answer is changed. Read-only.
|
|
749
|
+
async settings() {
|
|
750
|
+
const { workspace, workspaceId } = readable()
|
|
751
|
+
const { audienceAllow, decisions } = shownMachine(machineOf(workspace), workspace)
|
|
752
|
+
const service = workspace === null ? null : readServiceSettings(workspace)
|
|
753
|
+
const consent = service === null ? null : service.consent
|
|
754
|
+
const document = { workspace: { workspaceId, prepared: workspace !== null }, decisions, audienceAllow, consent, change: DECISION_CHANGES }
|
|
755
|
+
return {
|
|
756
|
+
exit: EXIT.ok, document,
|
|
757
|
+
human: [
|
|
758
|
+
`who may see: ${decisionWords('audience', decisions.audience, audienceAllow)}`,
|
|
759
|
+
`where vaults live: ${decisionWords('location', decisions.location)}`,
|
|
760
|
+
`start at login: ${decisionWords('loginItem', decisions.loginItem)}`,
|
|
761
|
+
`reaching the app: ${decisionWords('adapter', decisions.adapter)}`,
|
|
762
|
+
`maintenance allowed by: ${consent === null ? 'nobody yet' : `${consent.actor} (${consent.coverage}, since ${consent.grantedAt})`}`,
|
|
763
|
+
...Object.values(DECISION_CHANGES).filter((change) => change !== null).map((change) => `Change: ${change}`),
|
|
764
|
+
],
|
|
765
|
+
}
|
|
766
|
+
},
|
|
767
|
+
|
|
768
|
+
// Where this workspace's vaults live. Deciding it places the vaults of views not published yet; a vault published
|
|
769
|
+
// already, under the data root or where an earlier decision placed it, stays where it is.
|
|
770
|
+
async location() {
|
|
771
|
+
const where = vaultWhere
|
|
772
|
+
const views = (project, enablement, workspace, decided) => enablement.scopes.map(({ scopeId }) => ({ scopeId, ...where(workspace, scopeId, decided, projectDisplayName(project)) }))
|
|
773
|
+
const lines = (list) => list.map((view) => `view ${view.scopeId}: ${view.path ?? 'no vault yet'} (${view.origin}${view.reason === undefined ? '' : `: ${view.reason}`})`)
|
|
774
|
+
if (sub === 'show' || sub === undefined) {
|
|
775
|
+
const { project, enablement, workspace } = readable()
|
|
776
|
+
const { decisions } = shownMachine(machineOf(workspace), workspace)
|
|
777
|
+
const shown = views(project, enablement, workspace, decisions.location)
|
|
778
|
+
return { exit: EXIT.ok, document: { location: decisions.location, views: shown }, human: [`where vaults live: ${decisionWords('location', decisions.location)}`, ...lines(shown)] }
|
|
779
|
+
}
|
|
780
|
+
if (sub !== 'set' || value === undefined) refuse('usage', 'location show | location set DIR [--allow-synced-location]')
|
|
781
|
+
// `~/` is read against the home folder, which a shell does not do after `--x=`; under the test runner a home
|
|
782
|
+
// folder is only ever one the test named.
|
|
783
|
+
const tilde = value === '~' || value.startsWith('~/')
|
|
784
|
+
if (tilde && typeof homedir !== 'string') refuse('real-vault-location-under-test', 'a location under the home folder is never used under the test runner; name an absolute folder')
|
|
785
|
+
// Resolved, so a trailing separator (as tab completion leaves it) is no refusal.
|
|
786
|
+
const parent = tilde ? path.resolve(path.join(homedir, value.slice(1))) : path.resolve(cwd, value)
|
|
787
|
+
await refuseOnPersonalBinding(loadProject(), 'location set')
|
|
788
|
+
const { project, enablement, workspace, repositoryRoots, now } = writable()
|
|
789
|
+
// A record that cannot be read names no folder here; the engine refuses its view on its own.
|
|
790
|
+
const allocatedPaths = enablement.scopes.map(({ scopeId }) => vaultWhere(workspace, scopeId, null, null)).filter((found) => found.origin === 'allocated').map((found) => found.path)
|
|
791
|
+
// A folder inside, or holding, a vault the app lists is refused now; when the list cannot be read, it is checked
|
|
792
|
+
// again, and has to be read, before any vault is allocated there.
|
|
793
|
+
const list = await appVaultList()
|
|
794
|
+
const warnings = checkVaultParent({ parent, workspaceRoot: workspace.workspaceRoot, repositoryRoots, vaults: list.vaults, allocatedPaths, allowSynced: flags['allow-synced-location'] === true, homedir: homedir ?? undefined, platform })
|
|
795
|
+
const appVaultListShown = { source: list.source, ...(list.reason === undefined ? {} : { reason: list.reason }) }
|
|
796
|
+
const current = machineOf(workspace) ?? defaultMachineSettings({ workspaceId: workspace.workspaceId, updatedAt: now })
|
|
797
|
+
const decided = withDecision(current, 'location', { parent }, { decidedAt: now, decidedBy: accountActor(account()), via: 'command' })
|
|
798
|
+
writeMachineSettings({ ...workspace, repositoryRoots, settings: { ...decided, updatedAt: now } })
|
|
799
|
+
const placed = views(project, enablement, workspace, decided.decisions.location)
|
|
800
|
+
return {
|
|
801
|
+
exit: EXIT.ok, document: { location: decided.decisions.location, warnings, appVaultList: appVaultListShown, views: placed, takesEffect: 'next-tick' },
|
|
802
|
+
human: [
|
|
803
|
+
`where vaults live: ${parent}; a view's vault is allocated there at its first publication, as "${projectDisplayName(project)} (<view>)"`,
|
|
804
|
+
...(warnings.synced === null ? [] : [`Warning: ${warnings.synced} keeps this folder in step with other machines.`]),
|
|
805
|
+
...(warnings.protected === null ? [] : [`Warning: macOS asks before Obsidian or the maintenance service may read your ${warnings.protected} folder.`]),
|
|
806
|
+
...(list.source === 'unread' ? [`Obsidian's vault list could not be read (${list.reason}); no vault is allocated there until it can be, and it is checked again then.`] : []),
|
|
807
|
+
...lines(placed),
|
|
808
|
+
],
|
|
809
|
+
}
|
|
810
|
+
},
|
|
811
|
+
|
|
812
|
+
// One more view in the project's configuration. A person at a terminal sees the change and how many notes the view
|
|
813
|
+
// would show, and is asked; --yes answers beforehand. Anyone else gets the change made, and shown.
|
|
814
|
+
async view() {
|
|
815
|
+
if (sub === 'list' || sub === 'show' || sub === undefined) return operations.scope()
|
|
816
|
+
if (sub !== 'add' || value === undefined) refuse('usage', 'view add ID (--all | --folder PATH [--folder PATH ...] [--repo R] | --tag T) [--expand DEPTH:MAX] [--default] [--allow-empty] [--yes]')
|
|
817
|
+
await refuseOnPersonalBinding(loadProject(), 'view add')
|
|
818
|
+
const { project, workspace, workspaceId } = readable()
|
|
819
|
+
const repositories = project.repos.filter((repo) => !repo.external && typeof repo.name === 'string').map((repo) => repo.name)
|
|
820
|
+
const scope = viewFromRequest({ scopeId: value, all: flags.all === true, folders: flags.folder ?? [], repo: flags.repo, tag: flags.tag, expand: flags.expand, repositories })
|
|
821
|
+
const plan = planViewAdd(project, { scope, makeDefault: flags.default === true })
|
|
822
|
+
// Counted as this machine decided who may see; while nobody decided, for "only you", the answer offered first,
|
|
823
|
+
// which shows the notes without a classification too.
|
|
824
|
+
const machine = machineOf(workspace)
|
|
825
|
+
const decided = machine?.decisions.audience ?? null
|
|
826
|
+
const audience = { allow: decided === null ? ONLY_YOU_AUDIENCES : machine.audienceAllow, decided: decided !== null }
|
|
827
|
+
const { eligibilityFor, onlyYouEligibility } = await import('../runtime/obsidian/pipeline.mjs')
|
|
828
|
+
const { viewCounts } = await import('../runtime/obsidian/view-counts.mjs')
|
|
829
|
+
const eligibility = decided === null ? onlyYouEligibility({ project }) : eligibilityFor({ machine, project })
|
|
830
|
+
const counts = viewCounts({ project, audienceAllow: audience.allow, scope, eligibility, ...(workspaceId === null ? {} : { workspaceId }) })
|
|
831
|
+
const countLine = viewCountWords(scope.scopeId, counts, audience)
|
|
832
|
+
// No audience admitted on this machine (`audience clear`): the view is empty whatever it selects, and the way out is
|
|
833
|
+
// deciding who may see, not classifying notes.
|
|
834
|
+
const nobody = audience.decided && audience.allow.length === 0 ? '; no audience is admitted on this machine: `atelier obsidian audience set me` admits yours' : ''
|
|
835
|
+
if (counts.shown === 0 && flags['allow-empty'] !== true) refuse('view-would-be-empty', `${countLine}${nobody}; nothing was written`, { scopeId: scope.scopeId, counts, audience })
|
|
836
|
+
const file = path.basename(project.configPath)
|
|
837
|
+
const shownPlan = [
|
|
838
|
+
`Atelier will add the view "${scope.scopeId}" to ${file}${plan.ignore.needed ? ', and .atelier-local/ to .gitignore' : ''} (you commit it):`,
|
|
839
|
+
...plan.diff.trimEnd().split('\n').map((line) => ` ${line}`),
|
|
840
|
+
countLine,
|
|
841
|
+
]
|
|
842
|
+
const asked = interactive && flags.yes !== true
|
|
843
|
+
if (asked) {
|
|
844
|
+
for (const line of shownPlan) stdout(line)
|
|
845
|
+
const yes = await askGoAhead(questioner(), 'Write this change? [Y/n] ')
|
|
846
|
+
if (yes === null) refuse('unanswered', 'no answer came; nothing was written')
|
|
847
|
+
if (!yes) return { exit: EXIT.notSuccess, document: { scope, declined: true, written: [], diff: plan.diff, counts, audience }, human: ['Nothing was written.'] }
|
|
848
|
+
}
|
|
849
|
+
const { written } = writeViewPlan(plan)
|
|
850
|
+
const committed = written.map((item) => path.relative(project.configDir, item).split(path.sep).join('/'))
|
|
851
|
+
// The counts are for "only you" while nobody decided, but the maintenance service publishes nothing until someone
|
|
852
|
+
// does: deciding comes first.
|
|
853
|
+
const openStep = `\`atelier obsidian open${plan.settings.defaultScopeId === scope.scopeId ? '' : ` --scope ${scope.scopeId}`}\` shows it in Obsidian`
|
|
854
|
+
const next = audience.decided ? [openStep] : ['`atelier obsidian audience set me` lets only you see its notes; until someone decides, the view publishes none', openStep]
|
|
855
|
+
return {
|
|
856
|
+
exit: EXIT.ok,
|
|
857
|
+
document: { scope, defaultScopeId: plan.settings.defaultScopeId ?? null, declined: false, written: committed, diff: plan.diff, counts, audience, next },
|
|
858
|
+
human: [
|
|
859
|
+
...(asked ? [] : shownPlan),
|
|
860
|
+
`Added the view ${scope.scopeId}${plan.settings.defaultScopeId === scope.scopeId ? ', the default' : ''}. Commit ${committed.join(' and ')} when you are ready; Atelier commits nothing.`,
|
|
861
|
+
...(plan.settings.enabled === true ? [] : ['The projection is turned off in this project ("enabled": false), so nothing is published until it is turned on.']),
|
|
862
|
+
`Next: ${next[0]}`, ...next.slice(1).map((step) => `Then: ${step}`),
|
|
863
|
+
],
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
|
|
237
867
|
async scope() {
|
|
238
868
|
const { enablement } = readable()
|
|
239
869
|
if (sub === 'list' || sub === undefined) return { exit: EXIT.ok, document: { defaultScopeId: enablement.defaultScopeId, scopes: enablement.scopes }, human: enablement.scopes.map((scope) => `${scope.scopeId}\t${scope.mode}${scope.scopeId === enablement.defaultScopeId ? '\tdefault' : ''}`) }
|
|
@@ -245,19 +875,32 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
245
875
|
async audience() {
|
|
246
876
|
if (sub === 'show' || sub === undefined) {
|
|
247
877
|
const { workspace } = readable()
|
|
248
|
-
const { audienceAllow } = shownMachine(machineOf(workspace), workspace)
|
|
249
|
-
return { exit: EXIT.ok, document: { audienceAllow
|
|
878
|
+
const { audienceAllow, decisions } = shownMachine(machineOf(workspace), workspace)
|
|
879
|
+
return { exit: EXIT.ok, document: { audienceAllow, choice: decisions.audience?.choice ?? null, unclassified: decisions.audience?.unclassified ?? 'withheld' }, human: [decisionWords('audience', decisions.audience, audienceAllow)] }
|
|
250
880
|
}
|
|
251
|
-
if (sub !== 'set' && sub !== 'clear') refuse('usage', 'audience show | audience set A,B | audience clear')
|
|
252
|
-
const
|
|
253
|
-
if (sub === 'set' &&
|
|
254
|
-
if (
|
|
881
|
+
if (sub !== 'set' && sub !== 'clear') refuse('usage', 'audience show | audience set me|A,B | audience clear')
|
|
882
|
+
const named = sub === 'clear' ? [] : String(value ?? '').split(',').map((item) => item.trim()).filter((item) => item !== '')
|
|
883
|
+
if (sub === 'set' && named.length === 0) refuse('usage', 'audience set needs at least one audience; `audience clear` allows none')
|
|
884
|
+
if (named.some((item) => !AUDIENCE.test(item)) || new Set(named).size !== named.length || named.length > 64) refuse('invalid-audience', 'audiences are distinct identifiers, at most 64')
|
|
885
|
+
// `me` stands for "only you". Named beside other audiences, it adds its own to theirs, and the list is the person's own choice.
|
|
886
|
+
const audienceAllow = [...new Set(named.flatMap((item) => (item === 'me' ? ONLY_YOU_AUDIENCES : [item])))]
|
|
887
|
+
const choice = named.length === 1 && named[0] === 'me' ? 'only-you' : 'custom'
|
|
888
|
+
await refuseOnPersonalBinding(loadProject(), `audience ${sub}`)
|
|
255
889
|
const { workspace, repositoryRoots, now } = writable()
|
|
256
890
|
const current = machineOf(workspace) ?? defaultMachineSettings({ workspaceId: workspace.workspaceId, updatedAt: now })
|
|
257
|
-
|
|
258
|
-
|
|
891
|
+
// A vault that is only yours shows the notes that carry no classification too; any other list withholds them.
|
|
892
|
+
const unclassified = choice === 'only-you' ? 'shown' : 'withheld'
|
|
893
|
+
const changed = JSON.stringify(current.audienceAllow) !== JSON.stringify(audienceAllow) || (current.decisions.audience?.unclassified ?? 'withheld') !== unclassified
|
|
894
|
+
const decided = withDecision({ ...current, audienceAllow }, 'audience', { choice, unclassified }, { decidedAt: now, decidedBy: accountActor(account()), via: 'command' })
|
|
895
|
+
writeMachineSettings({ ...workspace, repositoryRoots, settings: { ...decided, updatedAt: now } })
|
|
259
896
|
// The engine compares a digest of these on every tick: a change invalidates every view at the next one.
|
|
260
|
-
return {
|
|
897
|
+
return {
|
|
898
|
+
exit: EXIT.ok, document: { audienceAllow, choice, unclassified, changed, takesEffect: 'next-tick' },
|
|
899
|
+
human: [
|
|
900
|
+
`audiences: ${choice === 'only-you' ? `only you (${audienceAllow.join(', ')})` : audienceAllow.join(', ') || 'none'}; notes without a classification ${unclassified}; views are rebuilt at the next tick`,
|
|
901
|
+
...(choice === 'only-you' ? [] : ['Notes without a classification are shown only in a vault that is only yours: `atelier obsidian audience set me`.']),
|
|
902
|
+
],
|
|
903
|
+
}
|
|
261
904
|
},
|
|
262
905
|
|
|
263
906
|
async mode() {
|
|
@@ -325,44 +968,58 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
325
968
|
if (sub === 'start') {
|
|
326
969
|
configured()
|
|
327
970
|
const seam = await serviceSeam()
|
|
328
|
-
const
|
|
971
|
+
const consent = consentOfThisRun(seam)
|
|
972
|
+
const adapterRemembered = rememberAdapter(consent.consent?.actor)
|
|
973
|
+
const result = shown(await startService({ ...lifecycle, detached: true, ...seam, replaceOutdated: true, ...(consent.consent === undefined ? {} : { consent: consent.consent }) }, lifecycleRules))
|
|
974
|
+
const recorded = consentRecorded(consent)
|
|
329
975
|
const running = result.state === 'healthy' || result.state === 'busy'
|
|
330
|
-
return {
|
|
976
|
+
return {
|
|
977
|
+
exit: running ? EXIT.ok : EXIT.notSuccess,
|
|
978
|
+
document: { service: result, rememberedNow: { adapter: adapterRemembered, consentActor: recorded.source === 'account' ? recorded.consent.actor : null } },
|
|
979
|
+
human: [
|
|
980
|
+
result.state === 'busy' && !result.started ? 'running, and busy in a long tick; nothing was started beside it' : serviceWords(result),
|
|
981
|
+
...(result.release === 'later' ? [`Next: ${REASON_NEXT['service-other-release']}`] : []),
|
|
982
|
+
...rememberedLines({ adapterRemembered, consent: recorded }), ...loginItemLines(result.loginItem),
|
|
983
|
+
],
|
|
984
|
+
}
|
|
331
985
|
}
|
|
332
986
|
if (sub === 'stop') {
|
|
333
987
|
const result = await stopService(lifecycle, lifecycleRules)
|
|
334
988
|
return { exit: result.refused ? EXIT.notSuccess : EXIT.ok, document: { service: result }, human: [result.refused ? `not stopped: ${result.state} (${result.reason})${result.retry ? '; ask again in a moment' : ''}` : result.stopped ? 'stopped' : `nothing to stop (${result.reason})`] }
|
|
335
989
|
}
|
|
336
|
-
if (sub !== 'unit' || flags.print
|
|
337
|
-
|
|
338
|
-
if (
|
|
339
|
-
|
|
340
|
-
// Pure text from the builder. Nothing is written, installed or handed to a service manager.
|
|
341
|
-
const unit = buildStartupAdapter({
|
|
342
|
-
platform, label: serviceNameFor(workspace.workspaceId), nodePath: process.execPath, entryPath: seam.entryPath, logPath: servicePaths(workspace.workspaceRoot).log,
|
|
343
|
-
args: [`--project=${project.configPath}`, ...(dataRoot === undefined ? [] : [`--data-root=${dataRoot}`]), ...(seam.entryArgs ?? [])],
|
|
344
|
-
})
|
|
345
|
-
return { exit: EXIT.ok, document: { unit, installed: false, note: 'running this unit needs a recorded consent that covers operating-system startup' }, human: [unit.text] }
|
|
990
|
+
if (sub !== 'unit' || [flags.print, flags.install, flags.remove].filter((flag) => flag === true).length !== 1) refuse('usage', 'service start | status | stop | unit --print | unit --install | unit --remove')
|
|
991
|
+
if (flags.install === true) return installUnit()
|
|
992
|
+
if (flags.remove === true) return removeUnit()
|
|
993
|
+
return printUnit()
|
|
346
994
|
},
|
|
347
995
|
|
|
348
996
|
async open() {
|
|
349
997
|
const seam = await serviceSeam()
|
|
350
998
|
const app = await appSeams()
|
|
351
999
|
const { appProbe } = app
|
|
1000
|
+
const consent = consentOfThisRun(seam)
|
|
1001
|
+
const adapterRemembered = rememberAdapter(consent.consent?.actor)
|
|
352
1002
|
// The view whose plugin may report the app version; an unknown one is refused by open itself.
|
|
353
1003
|
const requested = () => { try { return resolveScope(readObsidianEnablement(loadProject()), flags.scope) } catch { return null } }
|
|
354
1004
|
const result = await openScopeForOracleTests({
|
|
355
1005
|
...lifecycle, ...app,
|
|
356
1006
|
appProbe: typeof appProbe?.inspect === 'function' && typeof appProbe?.vaultState === 'function' ? withPluginReportedVersion(appProbe, async () => { const scopeId = requested(); return scopeId === null ? null : pluginOf(scopeId) }) : appProbe,
|
|
357
|
-
service: seam, scopeId: flags.scope, consent, allowStale: flags['allow-stale'] === true, extensions: registry.extensions,
|
|
1007
|
+
service: seam, scopeId: flags.scope, consent: consent.consent, allowStale: flags['allow-stale'] === true, restartApp: flags['restart-obsidian'] === true, extensions: registry.extensions,
|
|
358
1008
|
...(flags['wait-ms'] === undefined ? {} : { tickTimeoutMs: Number(flags['wait-ms']) || undefined }), ...(options.open ?? {}),
|
|
359
1009
|
}, openingRules, lifecycleRules)
|
|
1010
|
+
const recorded = consentRecorded(consent)
|
|
360
1011
|
const { ok: _ok, ...opened } = result
|
|
361
|
-
const plugin = typeof result.scopeId === 'string' ? await pluginOf(result.scopeId) : null
|
|
362
|
-
const
|
|
1012
|
+
const plugin = typeof result.scopeId === 'string' ? withPluginFilesPending(await pluginOf(result.scopeId), result.freshness) : null
|
|
1013
|
+
const rememberedNow = { adapter: adapterRemembered, consentActor: recorded.source === 'account' ? recorded.consent.actor : null }
|
|
1014
|
+
const document = plugin === null ? { ...opened, rememberedNow } : { ...opened, plugin, rememberedNow }
|
|
363
1015
|
return {
|
|
364
1016
|
exit: result.ok ? EXIT.ok : EXIT.notSuccess, document,
|
|
365
|
-
human: [
|
|
1017
|
+
human: [
|
|
1018
|
+
`${result.outcome}: ${result.summary}${result.reason ? ` (${result.reason})` : ''}${plugin ? pluginLine(plugin) : ''}`, `Next: ${result.next}`, ...(plugin?.files === 'waits-for-app' ? [`Next for the plugin: ${plugin.next}`] : []), ...(result.service?.restarted ? [`service: restarted (${result.service.restarted})`] : []),
|
|
1019
|
+
...loginItemLines(result.service?.loginItem), ...restartLines(result.restart), ...obsidianSettingsLines(result.obsidianSettings),
|
|
1020
|
+
...(result.duplicates ? [`open in Obsidian as: ${result.duplicates.map((entry) => entry.path).join(', ')}`] : []), ...(result.pendingEdits?.open ? [`${result.pendingEdits.open} pending edit(s); apply ${result.pendingEdits.apply}`] : []),
|
|
1021
|
+
...rememberedLines({ adapterRemembered, consent: recorded }),
|
|
1022
|
+
],
|
|
366
1023
|
}
|
|
367
1024
|
},
|
|
368
1025
|
|
|
@@ -377,9 +1034,9 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
377
1034
|
const scopeId = resolveScope(enablement, flags.scope)
|
|
378
1035
|
const choice = writePluginChoice({ ...workspace, scopeId, state: 'requested', reason: 'requested-by-command', clock })
|
|
379
1036
|
// The request is recorded either way; a service that cannot be asked leaves it to the view's next publication.
|
|
380
|
-
// With the installed entry to start (`--adapter
|
|
1037
|
+
// With the installed entry to start (`--adapter`, given or remembered), a service of an earlier release still running after an upgrade
|
|
381
1038
|
// is replaced before its tick, under the consent already recorded, as `open` does; without it, it is reported.
|
|
382
|
-
const replaceable = seams !== null ||
|
|
1039
|
+
const replaceable = seams !== null || adapterSelected()
|
|
383
1040
|
let asked
|
|
384
1041
|
try { asked = await requestServiceTick({ ...lifecycle, scopeId, ...(replaceable ? { service: await serviceSeam() } : {}) }, lifecycleRules) } catch (error) { if (!isTyped(error)) throw error; asked = { requested: false, reason: error.code } }
|
|
385
1042
|
const view = asked.tick?.scopes?.find((scope) => scope.scopeId === scopeId) ?? null
|
|
@@ -413,6 +1070,36 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
413
1070
|
}
|
|
414
1071
|
},
|
|
415
1072
|
|
|
1073
|
+
// Maintenance of this workspace ends here: the login item goes first, so its manager does not start the service
|
|
1074
|
+
// again, then the proven service stops. Everything a person made or may want back stays, and is named.
|
|
1075
|
+
async uninstall() {
|
|
1076
|
+
const project = loadProject()
|
|
1077
|
+
let workspace = null
|
|
1078
|
+
try { const found = resolveServiceWorkspace({ project, dataRoot, env, platform }); workspace = found?.workspaceRoot ? found : null } catch (error) { if (!isTyped(error)) throw error }
|
|
1079
|
+
const record = workspace === null ? null : readLoginItemRecord(workspace)
|
|
1080
|
+
// What is kept is read before anything is stopped or removed, so nothing it finds can stop the answer after that.
|
|
1081
|
+
const kept = keptLocations(project, workspace)
|
|
1082
|
+
const item = record === null ? { removed: false, reason: 'not-installed' } : await removeLoginItem({ loadProject, dataRoot, env, platform, manager: await managerSeam(), clock })
|
|
1083
|
+
const service = workspace === null ? { state: 'stopped', stopped: false, refused: false, reason: 'workspace-not-prepared' } : await stopService(lifecycle, lifecycleRules)
|
|
1084
|
+
const itemGone = item.removed === true || item.reason === 'not-installed' || item.reason === 'workspace-not-prepared'
|
|
1085
|
+
const serviceGone = service.stopped === true || service.state === 'stopped'
|
|
1086
|
+
const remembered = workspace !== null && itemGone ? rememberLoginItem('off') : false
|
|
1087
|
+
return {
|
|
1088
|
+
exit: itemGone && serviceGone ? EXIT.ok : EXIT.notSuccess,
|
|
1089
|
+
document: { loginItem: item, service, kept, rememberedNow: { loginItem: remembered ? 'off' : null } },
|
|
1090
|
+
human: [
|
|
1091
|
+
`login item: ${item.removed ? `removed (${item.file})` : item.reason === 'not-installed' || item.reason === 'workspace-not-prepared' ? 'none was installed' : `not removed (${item.reason})`}`,
|
|
1092
|
+
`service: ${service.stopped ? 'stopped' : service.refused ? `not stopped: ${service.state} (${service.reason})` : `not running (${service.reason})`}`,
|
|
1093
|
+
...(itemGone && serviceGone ? [] : [`Next: ${!itemGone ? NEXT[item.reason] ?? 'run `atelier obsidian uninstall` again' : service.retry ? 'run `atelier obsidian uninstall` again in a moment' : 'see `atelier obsidian service status`'}`]),
|
|
1094
|
+
'Kept as they are:',
|
|
1095
|
+
...kept.vaults.map((vault) => ` vault ${vault}`),
|
|
1096
|
+
...(kept.privateState === null ? [] : [` private state ${kept.privateState}`]),
|
|
1097
|
+
` project file ${kept.projectFile ?? '(none)'}; its Obsidian settings stay`,
|
|
1098
|
+
...(kept.obsidianList === null ? [] : [` Obsidian's list ${kept.obsidianList}; the vaults stay in it`]),
|
|
1099
|
+
],
|
|
1100
|
+
}
|
|
1101
|
+
},
|
|
1102
|
+
|
|
416
1103
|
// The placeholder that answers when no contribution registered an apply operation; the shipped source-apply contribution replaces it.
|
|
417
1104
|
async apply() {
|
|
418
1105
|
return refuse(APPLY_UNAVAILABLE, 'no apply operation is registered; pending edits stay preserved and queued', { operationId: registry.extensions.applyOperation().id })
|
|
@@ -424,6 +1111,8 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
424
1111
|
// One option table serves every operation. No built-in operation has a use for an actor, and a contributed one takes
|
|
425
1112
|
// it only when it declares so (`options: ['actor']`); the apply operation then decides which of its verbs takes it.
|
|
426
1113
|
if (flags.actor !== undefined && !(Array.isArray(contributed?.options) && contributed.options.includes('actor'))) refuse('usage', '--actor belongs to `apply run`; this operation does not take it')
|
|
1114
|
+
// Stopping the person's app is asked for one run of `open`, by name, and by nothing else.
|
|
1115
|
+
if (flags['restart-obsidian'] === true && operationName !== 'open') refuse('usage', '--restart-obsidian belongs to `open`; this operation does not take it')
|
|
427
1116
|
const result = contributed !== null
|
|
428
1117
|
? await contributed.run({ args: positionals.slice(1), flags: { ...flags }, registry, loadProject, dataRoot, env, platform, clock, readable, writable })
|
|
429
1118
|
: await operations[operationName]()
|
|
@@ -436,6 +1125,8 @@ export async function runObsidianCommandForOracleTests(options = {}, rules = {})
|
|
|
436
1125
|
}
|
|
437
1126
|
const next = NEXT[error.code] ?? error.hint ?? 'see `atelier obsidian status`'
|
|
438
1127
|
return emit({ exit: EXIT.refused, document: { error: { code: error.code, message: plainMessage(error), next, detail: error.detail ?? {} } }, human: [`[${error.code}] ${plainMessage(error)}`, `Next: ${next}`] })
|
|
1128
|
+
} finally {
|
|
1129
|
+
questions?.close()
|
|
439
1130
|
}
|
|
440
1131
|
}
|
|
441
1132
|
|