@mnstry/atelier 0.2.0-alpha.12 → 0.2.0-alpha.14
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 +894 -0
- package/README.md +159 -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 +267 -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 +210 -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 +237 -0
- package/docs/learning.md +215 -0
- package/docs/local-services.md +223 -19
- package/docs/obsidian-contract.md +246 -15
- package/docs/obsidian-plugin.md +252 -7
- package/docs/obsidian.md +874 -52
- package/docs/optional-ingestion.md +83 -0
- package/docs/personal-workspace.md +266 -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 +50 -0
- package/docs/repo-boundary-guard.md +120 -3
- 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 +318 -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 +219 -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 +76 -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 +60 -5
- package/plugins/obsidian/main.js +57 -8
- package/plugins/obsidian/manifest.json +1 -1
- 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 +536 -69
- package/src/boundary/delegation-command.mjs +158 -0
- package/src/boundary/delegation.mjs +396 -0
- package/src/boundary/policy.mjs +185 -64
- 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 +66 -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 +772 -65
- 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 +30 -6
- package/src/graph/knowledge-graph.mjs +77 -9
- 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 +938 -0
- package/src/ingestion/optional/graphify/json_bridge.py +163 -0
- package/src/ingestion/optional/graphify/native-closure-pins.json +275 -0
- package/src/ingestion/optional/graphify/native-pins.json +306 -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 +49 -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 +401 -0
- package/src/project/config.mjs +8 -0
- package/src/project/durable-state.mjs +3 -1
- package/src/project/file-class.mjs +1 -1
- package/src/project/path-match.mjs +2 -1
- package/src/projection/obsidian/edits/apply.mjs +55 -16
- package/src/projection/obsidian/materialize/prepare-view.mjs +87 -5
- package/src/projection/obsidian/personal-workspace.mjs +519 -0
- package/src/projection/obsidian/proposals/adapter.mjs +79 -17
- package/src/projection/obsidian/proposals/observation.mjs +50 -4
- package/src/projection/obsidian/publication/publisher.mjs +214 -18
- 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/projection/project.mjs +3 -2
- 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 +437 -56
- 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 +108 -23
- package/src/runtime/obsidian/login-item.mjs +335 -0
- package/src/runtime/obsidian/machine-settings.mjs +119 -12
- package/src/runtime/obsidian/observation.mjs +36 -4
- package/src/runtime/obsidian/opening.mjs +173 -38
- package/src/runtime/obsidian/personal-selection.mjs +125 -0
- package/src/runtime/obsidian/pipeline.mjs +115 -6
- 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/private-lock.mjs +39 -4
- 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 +148 -19
- 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 +102 -9
- 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/runtime/obsidian/view-permission.mjs +334 -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/control-system.mjs +201 -0
- package/src/ui/html-primitives.mjs +3 -0
- package/src/ui/knowledge-page.mjs +1388 -0
- package/src/upgrade/closure-diagnostics.mjs +135 -0
- package/src/upgrade/closure.mjs +649 -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 +20 -7
- package/templates/external-project-workspace/pack-lifecycle.json +7 -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
|
@@ -5,7 +5,7 @@ import { MAX_OBSIDIAN_SETTINGS_BYTES, OBSIDIAN_SETTINGS_FILE, enclosingVaults, f
|
|
|
5
5
|
import { openRegularFileNoFollow, realPathAsStored, syncPrivateDirectory } from '../../project/private-state.mjs'
|
|
6
6
|
|
|
7
7
|
// Adding a view's vault to Obsidian's own vault list while Obsidian is not
|
|
8
|
-
// running, so that
|
|
8
|
+
// running, so that the app opens it when it starts.
|
|
9
9
|
//
|
|
10
10
|
// Outside Atelier's own storage, it is the only file of another application
|
|
11
11
|
// Atelier writes. The list is `obsidian.json` in the app's user-data directory
|
|
@@ -18,10 +18,11 @@ import { openRegularFileNoFollow, realPathAsStored, syncPrivateDirectory } from
|
|
|
18
18
|
//
|
|
19
19
|
// - never for a Flatpak or snap build, which reads its list inside its
|
|
20
20
|
// sandbox and never this file (`sandbox`, see obsidianSandboxedBuild);
|
|
21
|
-
// -
|
|
21
|
+
// - an existing file only in an existing user-data directory, both this
|
|
22
22
|
// user's own, a real directory and a regular file (no link, and no second
|
|
23
|
-
// name: a hard link would keep the old list), holding a JSON object
|
|
24
|
-
//
|
|
23
|
+
// name: a hard link would keep the old list), holding a JSON object;
|
|
24
|
+
// - a file that does not exist (Obsidian never started on this account) is
|
|
25
|
+
// created, and only then: see createObsidianSettings below;
|
|
25
26
|
// - never for a vault inside a folder the list has as a vault already: the
|
|
26
27
|
// app would show its notes in that vault too, and a call run in its folder
|
|
27
28
|
// would reach that vault (see vault-list.mjs);
|
|
@@ -29,6 +30,12 @@ import { openRegularFileNoFollow, realPathAsStored, syncPrivateDirectory } from
|
|
|
29
30
|
// it, as values: the app itself rewrites the file with JSON.stringify;
|
|
30
31
|
// - the new entry is { path: the vault root's real path, ts: now, open:
|
|
31
32
|
// true } under a fresh 16-hex id that no entry has;
|
|
33
|
+
// - the command-line switch: whenever the list is written, `cli` is set to
|
|
34
|
+
// true when it is not (the default of a new installation is off, and
|
|
35
|
+
// `open` needs the command line while the app holds the vault). A vault the
|
|
36
|
+
// list has already, with `cli` not true, is written for the switch alone.
|
|
37
|
+
// Only here, so only while no Obsidian runs: a running app keeps its
|
|
38
|
+
// switch in memory and would write it back;
|
|
32
39
|
// - the new file is no larger than a settings file this module reads;
|
|
33
40
|
// - the bytes as they were are kept beside it in
|
|
34
41
|
// `obsidian.json.atelier-backup-<UTC time>`, fsynced, before the file is
|
|
@@ -46,13 +53,14 @@ const currentUid = () => (typeof process.getuid === 'function' ? process.getuid(
|
|
|
46
53
|
const compactUtc = (ms) => new Date(ms).toISOString().replace(/[-:.]/g, '')
|
|
47
54
|
const BACKUP = new RegExp(`^${OBSIDIAN_SETTINGS_FILE.replaceAll('.', '\\.')}\\.atelier-backup-\\d{8}T\\d{9}Z$`)
|
|
48
55
|
|
|
49
|
-
// Keeps the first backup, the list as it was before Atelier ever wrote it,
|
|
50
|
-
// Their names sort as their times. A backup
|
|
51
|
-
|
|
56
|
+
// Keeps the first backup, the list as it was before Atelier ever wrote it, `latest`, and the names in `keep` (a copy
|
|
57
|
+
// the same run took before it quit the app); removes the others between. Their names sort as their times. A backup
|
|
58
|
+
// that cannot be removed stays.
|
|
59
|
+
function pruneBackups(directory, latest, keep = []) {
|
|
52
60
|
let names
|
|
53
61
|
try { names = fs.readdirSync(directory).filter((name) => BACKUP.test(name)).sort() } catch { return }
|
|
54
62
|
for (const name of names.slice(1)) {
|
|
55
|
-
if (name === latest) continue
|
|
63
|
+
if (name === latest || keep.includes(name)) continue
|
|
56
64
|
const file = path.join(directory, name)
|
|
57
65
|
try { if (fs.lstatSync(file).isFile()) fs.unlinkSync(file) } catch { /* it stays */ }
|
|
58
66
|
}
|
|
@@ -82,35 +90,113 @@ function readBytesNoFollow(file) {
|
|
|
82
90
|
try { return fs.readFileSync(descriptor) } finally { try { fs.closeSync(descriptor) } catch { /* read already, or failed */ } }
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
+
// Creates the settings file of an Obsidian that never started on this account:
|
|
94
|
+
// the file does not exist, and the process table says, positively, that no
|
|
95
|
+
// Obsidian runs, read before and again immediately before the file appears.
|
|
96
|
+
// The app reads the file when it starts (a missing or unreadable one reads as
|
|
97
|
+
// no settings) and opens every vault flagged open, so a plain start opens this
|
|
98
|
+
// one, with its command line on. Only:
|
|
99
|
+
//
|
|
100
|
+
// - in the user-data directory as it is when that is this user's own real
|
|
101
|
+
// directory, or, when it does not exist, in one created here with mode
|
|
102
|
+
// 0700 inside an existing real parent directory (a missing parent is
|
|
103
|
+
// `obsidian-settings-missing`: nothing is created above the directory);
|
|
104
|
+
// - with exactly { "vaults": { "<16 hex>": { path, ts, open: true } },
|
|
105
|
+
// "cli": true }, mode 0600;
|
|
106
|
+
// - by an exclusive create: the complete bytes are written, fsynced, to a
|
|
107
|
+
// temporary file in the directory, which is then linked to the file's name
|
|
108
|
+
// (a link fails on any name there, a file the app wrote meanwhile
|
|
109
|
+
// included, which is kept: `obsidian-settings-changed`), and the
|
|
110
|
+
// temporary name removed. A reader never sees a part of the file.
|
|
111
|
+
//
|
|
112
|
+
// There is nothing to back up. A refusal removes what it created, the
|
|
113
|
+
// directory included.
|
|
114
|
+
function createObsidianSettings({ userDataDir, vaultRoot, absent, now, randomBytes, uid }) {
|
|
115
|
+
const owned = (stat) => uid === null || stat.uid === uid
|
|
116
|
+
let directory = null
|
|
117
|
+
try { directory = fs.lstatSync(userDataDir) } catch (error) { if (error.code !== 'ENOENT') return refused('obsidian-settings-unreadable', 'the Obsidian settings directory cannot be read') }
|
|
118
|
+
let folder
|
|
119
|
+
try { folder = realPathAsStored(vaultRoot) } catch { return refused('vault-root-missing', 'the view\'s vault folder does not exist') }
|
|
120
|
+
if (!fs.statSync(folder).isDirectory()) return refused('vault-root-missing', 'the view\'s vault folder is not a directory')
|
|
121
|
+
let madeDirectory = false
|
|
122
|
+
if (directory === null) {
|
|
123
|
+
const parent = path.dirname(userDataDir)
|
|
124
|
+
let above
|
|
125
|
+
try { above = fs.lstatSync(parent) } catch { above = null }
|
|
126
|
+
if (above === null || above.isSymbolicLink() || !above.isDirectory() || !owned(above)) return refused('obsidian-settings-missing', 'Obsidian has no settings directory on this account, and the folder it would be in is missing, a link or not this user\'s own')
|
|
127
|
+
try { fs.mkdirSync(userDataDir, { mode: 0o700 }) } catch (error) {
|
|
128
|
+
return refused(error.code === 'EEXIST' ? 'obsidian-settings-changed' : 'obsidian-settings-unwritable', 'the Obsidian settings directory cannot be created')
|
|
129
|
+
}
|
|
130
|
+
madeDirectory = true
|
|
131
|
+
directory = fs.lstatSync(userDataDir)
|
|
132
|
+
}
|
|
133
|
+
const removeDirectory = () => { if (madeDirectory) try { fs.rmdirSync(userDataDir) } catch { /* something else is in it now: it stays */ } }
|
|
134
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return refused('obsidian-settings-unsafe', 'the Obsidian settings directory is a link or not a directory')
|
|
135
|
+
if (!owned(directory)) return refused('obsidian-settings-not-owned', 'the Obsidian settings directory belongs to another user')
|
|
136
|
+
const id = randomBytes(8).toString('hex')
|
|
137
|
+
const at = now()
|
|
138
|
+
const document = { vaults: { [id]: { path: folder, ts: at, open: true } }, cli: true }
|
|
139
|
+
const bytes = Buffer.from(JSON.stringify(document), 'utf8')
|
|
140
|
+
if (bytes.length > MAX_OBSIDIAN_SETTINGS_BYTES) { removeDirectory(); return refused('obsidian-settings-too-large', 'with this vault the Obsidian settings file would be larger than a settings file can be') }
|
|
141
|
+
const file = path.join(userDataDir, OBSIDIAN_SETTINGS_FILE)
|
|
142
|
+
const temporary = path.join(userDataDir, `.${OBSIDIAN_SETTINGS_FILE}.atelier-${randomBytes(6).toString('hex')}.tmp`)
|
|
143
|
+
const undo = () => { try { fs.unlinkSync(temporary) } catch { /* already gone */ } removeDirectory() }
|
|
144
|
+
try { writeNewFile(temporary, bytes, 0o600) } catch { undo(); return refused('obsidian-settings-unwritable', 'the Obsidian settings directory cannot be written') }
|
|
145
|
+
// Immediately before the file appears: still no app.
|
|
146
|
+
if (!absent()) { undo(); return refused('app-may-be-running', 'an Obsidian process appeared, and its settings belong to it') }
|
|
147
|
+
try { fs.linkSync(temporary, file) } catch (error) {
|
|
148
|
+
undo()
|
|
149
|
+
return refused(error.code === 'EEXIST' ? 'obsidian-settings-changed' : 'obsidian-settings-unwritable', error.code === 'EEXIST' ? 'an Obsidian settings file appeared while it was being created, and it was kept as it is' : 'the Obsidian settings file cannot be created')
|
|
150
|
+
}
|
|
151
|
+
try { fs.unlinkSync(temporary) } catch { /* the file is complete; a second name is refused by the next write */ }
|
|
152
|
+
try { syncPrivateDirectory(userDataDir); if (madeDirectory) syncPrivateDirectory(path.dirname(userDataDir)) } catch { /* the file exists; a directory that cannot be fsynced is not undone */ }
|
|
153
|
+
const stillAbsent = absent()
|
|
154
|
+
const written = readObsidianSettings({ userDataDir, uid })
|
|
155
|
+
const entry = written.ok ? findVaultEntry(written.vaults, vaultRoot) : null
|
|
156
|
+
const reason = !stillAbsent ? 'app-started-during-registration' : entry === null ? 'registration-not-read-back' : null
|
|
157
|
+
return {
|
|
158
|
+
ok: true, registered: 'created', entry: entry ?? { id, path: folder, open: true }, confirmed: reason === null, vaults: written.ok ? written.vaults : document.vaults,
|
|
159
|
+
file, created: { directory: madeDirectory }, cliTurnedOn: true, ...(reason === null ? {} : { reason }),
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// { ok: true, registered: 'already' | 'written' | 'created', entry: { id, path, open }, confirmed, vaults, file, cliTurnedOn,
|
|
164
|
+
// backupPath?, created?, reason? } or a typed refusal { ok: false, code, message }. `registered` is 'already' when the
|
|
165
|
+
// list had the vault (then the file was written only to turn the command line on, `cliTurnedOn`), 'written' when this
|
|
166
|
+
// vault was added to an existing file, 'created' when the file was created (createObsidianSettings). `confirmed` is
|
|
167
|
+
// false when an app appeared right after the rename (`app-started-during-registration`): it may have read the list
|
|
168
|
+
// before it, so whoever asked verifies through the app; and when the written file could not be read back to find the
|
|
169
|
+
// entry (`registration-not-read-back`). `vaults` is the list as it was found or written. `processProbe()` answers
|
|
170
|
+
// 'absent', 'running' or 'unknown'; only 'absent' allows a write. `sandbox` names a Flatpak or snap build found for
|
|
93
171
|
// this account, which refuses (`obsidian-sandboxed`).
|
|
94
|
-
|
|
172
|
+
// `keepBackups`: backup names (in the file's directory) this write must not prune: the copy `open` kept before it
|
|
173
|
+
// quit the app for this write.
|
|
174
|
+
export function registerVaultInObsidianSettings({ userDataDir, vaultRoot, processProbe, sandbox = null, now = () => Date.now(), randomBytes = cryptoRandomBytes, uid = currentUid(), keepBackups = [] } = {}) {
|
|
95
175
|
if (typeof processProbe !== 'function') throw new TypeError('registering a vault needs a process probe')
|
|
96
176
|
if (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot) || vaultRoot.includes('\u0000')) throw new TypeError('vaultRoot must be an absolute path')
|
|
97
177
|
if (sandbox !== null) return refused('obsidian-sandboxed', `this Obsidian is a ${sandbox} build, which reads its vault list inside its sandbox, where Atelier does not write`)
|
|
98
178
|
const absent = () => { try { return processProbe() === 'absent' } catch { return false } }
|
|
99
179
|
if (!absent()) return refused('app-may-be-running', 'an Obsidian process may be running, and its vault list belongs to it')
|
|
100
180
|
const settings = readObsidianSettings({ userDataDir, uid })
|
|
181
|
+
// No file: Obsidian never started on this account. A missing directory is refused as missing when it has no parent.
|
|
182
|
+
if (!settings.ok && settings.code === 'obsidian-settings-missing' && typeof userDataDir === 'string' && path.isAbsolute(userDataDir)) return createObsidianSettings({ userDataDir, vaultRoot, absent, now, randomBytes, uid })
|
|
101
183
|
if (!settings.ok) return settings
|
|
102
184
|
const known = findVaultEntry(settings.vaults, vaultRoot)
|
|
103
|
-
|
|
185
|
+
const switchOn = settings.document.cli !== true
|
|
186
|
+
if (known && !switchOn) return { ok: true, registered: 'already', entry: known, confirmed: true, vaults: settings.vaults, file: settings.file, cliTurnedOn: false }
|
|
104
187
|
if (settings.links > 1) return refused('obsidian-settings-unsafe', 'the Obsidian settings file has a second name (a hard link), which a replacement would leave with the old list')
|
|
105
|
-
if (enclosingVaults({ vaults: settings.vaults, vaultRoot }).length > 0) return refused('vault-inside-another-vault', 'Obsidian lists a vault at a folder that contains this vault\'s folder')
|
|
188
|
+
if (!known && enclosingVaults({ vaults: settings.vaults, vaultRoot }).length > 0) return refused('vault-inside-another-vault', 'Obsidian lists a vault at a folder that contains this vault\'s folder')
|
|
106
189
|
|
|
107
|
-
let folder
|
|
108
|
-
|
|
109
|
-
if (!
|
|
110
|
-
|
|
111
|
-
|
|
190
|
+
let folder = null
|
|
191
|
+
let id = null
|
|
192
|
+
if (!known) {
|
|
193
|
+
try { folder = realPathAsStored(vaultRoot) } catch { return refused('vault-root-missing', 'the view\'s vault folder does not exist') }
|
|
194
|
+
if (!fs.statSync(folder).isDirectory()) return refused('vault-root-missing', 'the view\'s vault folder is not a directory')
|
|
195
|
+
do { id = randomBytes(8).toString('hex') } while (Object.hasOwn(settings.vaults, id))
|
|
196
|
+
}
|
|
112
197
|
const at = now()
|
|
113
|
-
|
|
198
|
+
// The switch keeps its place when the file has one; otherwise it is added last, as the app would.
|
|
199
|
+
const document = { ...settings.document, ...(known ? {} : { vaults: { ...settings.vaults, [id]: { path: folder, ts: at, open: true } } }), ...(switchOn ? { cli: true } : {}) }
|
|
114
200
|
const bytes = Buffer.from(JSON.stringify(document), 'utf8')
|
|
115
201
|
if (bytes.length > MAX_OBSIDIAN_SETTINGS_BYTES) return refused('obsidian-settings-too-large', 'with this vault the Obsidian settings file would be larger than a settings file can be')
|
|
116
202
|
const directory = path.dirname(settings.file)
|
|
@@ -140,15 +226,76 @@ export function registerVaultInObsidianSettings({ userDataDir, vaultRoot, proces
|
|
|
140
226
|
}
|
|
141
227
|
created.splice(0)
|
|
142
228
|
try { syncPrivateDirectory(directory) } catch { /* the rename is done; a directory that cannot be fsynced is not undone */ }
|
|
143
|
-
pruneBackups(directory, path.basename(backupPath))
|
|
229
|
+
pruneBackups(directory, path.basename(backupPath), (Array.isArray(keepBackups) ? keepBackups : []).filter((name) => typeof name === 'string').map((name) => path.basename(name)))
|
|
144
230
|
const stillAbsent = absent()
|
|
145
231
|
const written = readObsidianSettings({ userDataDir, uid })
|
|
146
232
|
const entry = written.ok ? findVaultEntry(written.vaults, vaultRoot) : null
|
|
147
233
|
const reason = !stillAbsent ? 'app-started-during-registration' : entry === null ? 'registration-not-read-back' : null
|
|
148
234
|
return {
|
|
149
|
-
ok: true, registered: 'written', entry: entry ?? { id, path: folder, open: true }, backupPath, confirmed: reason === null, vaults: written.ok ? written.vaults : document.vaults,
|
|
150
|
-
...(reason === null ? {} : { reason }),
|
|
235
|
+
ok: true, registered: known ? 'already' : 'written', entry: entry ?? known ?? { id, path: folder, open: true }, backupPath, confirmed: reason === null, vaults: written.ok ? written.vaults : document.vaults,
|
|
236
|
+
file: settings.file, cliTurnedOn: switchOn, ...(reason === null ? {} : { reason }),
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Read-only, for `open` before it quits a running app to restart it: whether registerVaultInObsidianSettings would
|
|
241
|
+
// write this vault into the settings file `settings` (a readObsidianSettings answer, read while the app runs), as far
|
|
242
|
+
// as a read can tell. { ok: true, create } or { ok: false, code } with the refusal the write would give: an
|
|
243
|
+
// unreadable, linked, foreign, sandboxed or too large file, a vault inside another listed vault, a vault folder that
|
|
244
|
+
// is missing. A file that does not exist (an app that has shown only its starter window writes none) is the create
|
|
245
|
+
// case, `create: true`, checked as createObsidianSettings checks before it writes: the vault folder is a directory,
|
|
246
|
+
// and the user-data directory is this user's own real directory, or is absent inside this user's own real parent.
|
|
247
|
+
// What only the moment of the write can tell (an app that appears, a file that changes) is still checked then.
|
|
248
|
+
export function settingsWriteOutlook({ settings, vaultRoot, now = () => Date.now(), uid = currentUid() } = {}) {
|
|
249
|
+
const no = (code) => ({ ok: false, code })
|
|
250
|
+
if (typeof vaultRoot !== 'string' || !path.isAbsolute(vaultRoot)) return no('vault-root-missing')
|
|
251
|
+
if (settings?.ok !== true && settings?.code === 'obsidian-settings-missing') {
|
|
252
|
+
const userDataDir = settings.userDataDir
|
|
253
|
+
if (typeof userDataDir !== 'string' || !path.isAbsolute(userDataDir)) return no('obsidian-settings-location-unknown')
|
|
254
|
+
try { if (!fs.statSync(realPathAsStored(vaultRoot)).isDirectory()) return no('vault-root-missing') } catch { return no('vault-root-missing') }
|
|
255
|
+
const owned = (stat) => uid === null || stat.uid === uid
|
|
256
|
+
let directory = null
|
|
257
|
+
try { directory = fs.lstatSync(userDataDir) } catch (error) { if (error.code !== 'ENOENT') return no('obsidian-settings-unreadable') }
|
|
258
|
+
if (directory !== null) {
|
|
259
|
+
if (directory.isSymbolicLink() || !directory.isDirectory()) return no('obsidian-settings-unsafe')
|
|
260
|
+
return owned(directory) ? { ok: true, create: true } : no('obsidian-settings-not-owned')
|
|
261
|
+
}
|
|
262
|
+
let above = null
|
|
263
|
+
try { above = fs.lstatSync(path.dirname(userDataDir)) } catch { above = null }
|
|
264
|
+
return above !== null && !above.isSymbolicLink() && above.isDirectory() && owned(above) ? { ok: true, create: true } : no('obsidian-settings-missing')
|
|
265
|
+
}
|
|
266
|
+
if (settings?.ok !== true) return no(typeof settings?.code === 'string' ? settings.code : 'obsidian-settings-unreadable')
|
|
267
|
+
if (typeof settings.links === 'number' && settings.links > 1) return no('obsidian-settings-unsafe')
|
|
268
|
+
const size = Buffer.isBuffer(settings.bytes) ? settings.bytes.length : 0
|
|
269
|
+
const known = findVaultEntry(settings.vaults, vaultRoot)
|
|
270
|
+
if (known) return size + 16 > MAX_OBSIDIAN_SETTINGS_BYTES ? no('obsidian-settings-too-large') : { ok: true, create: false }
|
|
271
|
+
if (enclosingVaults({ vaults: settings.vaults, vaultRoot }).length > 0) return no('vault-inside-another-vault')
|
|
272
|
+
let folder
|
|
273
|
+
try { folder = realPathAsStored(vaultRoot); if (!fs.statSync(folder).isDirectory()) return no('vault-root-missing') } catch { return no('vault-root-missing') }
|
|
274
|
+
const entry = Buffer.byteLength(JSON.stringify({ '0123456789abcdef': { path: folder, ts: now(), open: true } }), 'utf8')
|
|
275
|
+
return size + entry + 16 > MAX_OBSIDIAN_SETTINGS_BYTES ? no('obsidian-settings-too-large') : { ok: true, create: false }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Keeps a copy of the settings file as it is, beside it, under the backup name the write uses
|
|
279
|
+
// (`obsidian.json.atelier-backup-<UTC time>`), fsynced, with the file's mode. Read-only for the file itself. `open`
|
|
280
|
+
// takes it right before the first signal to a running app, so a list the app was writing when it ended can be
|
|
281
|
+
// restored. { ok: true, backupPath } or a typed refusal (readObsidianSettings's, or `obsidian-settings-unwritable`).
|
|
282
|
+
// The write that follows in the same run keeps it (`keepBackups`); a later write prunes it as any other backup.
|
|
283
|
+
export function backupObsidianSettings({ userDataDir, now = () => Date.now(), uid = currentUid() } = {}) {
|
|
284
|
+
const settings = readObsidianSettings({ userDataDir, uid })
|
|
285
|
+
if (!settings.ok) return settings
|
|
286
|
+
const directory = path.dirname(settings.file)
|
|
287
|
+
for (let offset = 0; offset < 3; offset += 1) {
|
|
288
|
+
const backupPath = path.join(directory, `${OBSIDIAN_SETTINGS_FILE}.atelier-backup-${compactUtc(now() + offset)}`)
|
|
289
|
+
try {
|
|
290
|
+
writeNewFile(backupPath, settings.bytes, settings.mode)
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error.code === 'EEXIST') continue
|
|
293
|
+
return refused('obsidian-settings-unwritable', 'a copy of the Obsidian settings file cannot be kept beside it')
|
|
294
|
+
}
|
|
295
|
+
try { syncPrivateDirectory(directory) } catch { /* the copy is written */ }
|
|
296
|
+
return { ok: true, backupPath }
|
|
151
297
|
}
|
|
298
|
+
return refused('obsidian-settings-unwritable', 'a copy of the Obsidian settings file cannot be kept beside it')
|
|
152
299
|
}
|
|
153
300
|
|
|
154
301
|
// What the app looks like, as one comparable text, from what can be seen
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { performance } from 'node:perf_hooks'
|
|
3
|
+
|
|
4
|
+
// Quitting a running Obsidian so that `open` can add a vault and turn its
|
|
5
|
+
// command line on, only when a person asked for it (`--restart-obsidian`).
|
|
6
|
+
//
|
|
7
|
+
// `open` restarts the app in two states only, where it runs but cannot be
|
|
8
|
+
// reached: its command line is turned off, or no vault is open (and the app
|
|
9
|
+
// does not list the view's vault). The settings file then belongs to the
|
|
10
|
+
// running app, which would write its own list back over any change, and the
|
|
11
|
+
// app cannot be asked to add a vault. Before anything is sent, `open` checks,
|
|
12
|
+
// read-only, that the write the restart is for can succeed (opening.mjs); right
|
|
13
|
+
// before the first signal, once the main process is proven, `beforeSignal` keeps
|
|
14
|
+
// a copy of the settings file, and a copy that cannot be kept sends nothing.
|
|
15
|
+
//
|
|
16
|
+
// SIGTERM, which Electron handles as a quit, is sent to one process: the app's
|
|
17
|
+
// main process, proven from the process table, and never to anything else. The
|
|
18
|
+
// main process is proven when, in a table of this machine's processes as
|
|
19
|
+
// `pid ppid uid comm` (`comm` is the process's argv[0] as macOS reports it, not
|
|
20
|
+
// a verified image: a process of this user can name itself anything, which is
|
|
21
|
+
// why two candidates prove nothing):
|
|
22
|
+
//
|
|
23
|
+
// - every process of the app (the names the process probe counts: `Obsidian`
|
|
24
|
+
// and the bundle's `Obsidian Helper…`) runs as this user;
|
|
25
|
+
// - exactly one of them names the app bundle's main executable by an absolute
|
|
26
|
+
// path (`…/Contents/MacOS/Obsidian`); a process shown by its short name
|
|
27
|
+
// only is not proven;
|
|
28
|
+
// - every other one is a child of that process (its helpers).
|
|
29
|
+
//
|
|
30
|
+
// Anything else (two instances, another user's app, a helper of another
|
|
31
|
+
// parent, a table that cannot be read) is `app-main-process-unproven`, and no
|
|
32
|
+
// signal is sent. Immediately before the signal the one process is read again
|
|
33
|
+
// (`ppid uid start-time comm`) and must be of this user, under the parent the
|
|
34
|
+
// table showed, with the same name, and read the same twice.
|
|
35
|
+
//
|
|
36
|
+
// What SIGTERM does (1.13.7, checked on an isolated instance): the app handles
|
|
37
|
+
// the first one as a quit. It closes every window, keeping the vaults that were
|
|
38
|
+
// open flagged open in its list, and its helper processes end; on macOS the main
|
|
39
|
+
// process can then stay, with no window, as a macOS app does. Electron handles
|
|
40
|
+
// only the first SIGTERM as a quit: a second one ends the process at once,
|
|
41
|
+
// without its quit handlers. It is sent only after that same process has shown
|
|
42
|
+
// no window and no helper (it is alone in the table, and reads the same) at
|
|
43
|
+
// every reading for at least `lingerMs`, measured on a monotonic clock; a
|
|
44
|
+
// reading that cannot be parsed does not count as alone. That is a margin, not
|
|
45
|
+
// a proof that nothing is left to save. There is never a third signal, and
|
|
46
|
+
// never another kind; an app that is still not gone is reported
|
|
47
|
+
// (`app-did-not-quit`) and left to the person. `signals` counts the signals
|
|
48
|
+
// that were delivered.
|
|
49
|
+
//
|
|
50
|
+
// Qualified on macOS only: on Linux a quit app is started again with a link,
|
|
51
|
+
// which drops the other vaults' reopen flags (launch-plan.mjs), so the promise
|
|
52
|
+
// that they reopen would not hold there.
|
|
53
|
+
//
|
|
54
|
+
// Nothing here reads the process table or sends a signal itself: every seam is
|
|
55
|
+
// injected, with no default. Only the production seams module of the command
|
|
56
|
+
// entry and the opt-in real-app suite, for its isolated app, construct one.
|
|
57
|
+
|
|
58
|
+
export const RESTART_PLATFORMS = Object.freeze(['darwin'])
|
|
59
|
+
const APP_PROCESS = /^obsidian(?: helper\b.*)?$/i
|
|
60
|
+
const MAIN_EXECUTABLE = /^\/.+\.app\/Contents\/MacOS\/Obsidian$/i
|
|
61
|
+
// `pid ppid uid comm`. macOS prints the uid of `nobody` as -2: a uid is signed.
|
|
62
|
+
export const PROCESS_ROW = /^\s*(\d+)\s+(\d+)\s+(-?\d+)\s+(.+?)\s*$/
|
|
63
|
+
// `ppid uid start-time comm`, one process read again.
|
|
64
|
+
const DESCRIBED = /^\s*(\d+)\s+(-?\d+)\s+(.+?)\s*$/
|
|
65
|
+
|
|
66
|
+
const unproven = (detail) => ({ state: 'unproven', reason: 'app-main-process-unproven', detail })
|
|
67
|
+
const rowsOf = (table) => table.split('\n').filter((line) => line.trim() !== '').map((line) => PROCESS_ROW.exec(line))
|
|
68
|
+
|
|
69
|
+
// Pure. The app's main process in a process table (`ps -A -o pid=,ppid=,uid=,comm=`), for the account `uid`:
|
|
70
|
+
// { state: 'proven', pid, ppid, executable } | { state: 'absent' } | { state: 'unproven', reason, detail }.
|
|
71
|
+
export function provenMainProcess({ table, uid } = {}) {
|
|
72
|
+
if (typeof table !== 'string' || table.trim() === '') return unproven('the process table could not be read')
|
|
73
|
+
if (!Number.isSafeInteger(uid) || uid < 0) return unproven('the account is not known')
|
|
74
|
+
const matches = rowsOf(table)
|
|
75
|
+
if (matches.some((match) => match === null)) return unproven('the process table has a line that is not a process')
|
|
76
|
+
const rows = matches.map((match) => ({ pid: Number(match[1]), ppid: Number(match[2]), uid: Number(match[3]), executable: match[4] }))
|
|
77
|
+
const app = rows.filter((row) => APP_PROCESS.test(path.posix.basename(row.executable)))
|
|
78
|
+
if (app.length === 0) return { state: 'absent' }
|
|
79
|
+
if (app.some((row) => row.uid !== uid)) return unproven('an Obsidian process runs as another user')
|
|
80
|
+
const main = app.filter((row) => /^obsidian$/i.test(path.posix.basename(row.executable)))
|
|
81
|
+
if (main.length !== 1) return unproven(main.length === 0 ? 'no Obsidian main process is in the process table' : 'more than one Obsidian main process runs')
|
|
82
|
+
const [one] = main
|
|
83
|
+
if (!MAIN_EXECUTABLE.test(one.executable)) return unproven('the main process does not name the app bundle\'s executable')
|
|
84
|
+
if (app.some((row) => row !== one && row.ppid !== one.pid)) return unproven('an Obsidian helper process is not a child of the main process')
|
|
85
|
+
return { state: 'proven', pid: one.pid, ppid: one.ppid, executable: one.executable }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// `readTable()` -> the process table as provenMainProcess reads it (throws or answers '' when it cannot)
|
|
89
|
+
// `readProcess(pid)` -> that process read again as `ppid uid start-time comm`, or null
|
|
90
|
+
// `signal(pid)` -> sends SIGTERM to exactly that pid; throws when it was not delivered
|
|
91
|
+
// `processProbe()` -> 'running' | 'absent' | 'unknown', the probe the settings write uses
|
|
92
|
+
//
|
|
93
|
+
// quit({ beforeSignal }) answers { quit: true, pid, signalled, signals, settingsCopy? } once the probe says `absent`,
|
|
94
|
+
// or { quit: false, reason, signalled, signals, pid?, detail?, settingsCopy? }: `restart-platform-unqualified`,
|
|
95
|
+
// `app-main-process-unproven` and the refusal of `beforeSignal` before any signal, `app-did-not-quit` after one, when
|
|
96
|
+
// the probe did not say `absent` within `waitMs`. `beforeSignal()`, when given, runs once the main process is proven
|
|
97
|
+
// and read again, immediately before the first signal: { ok: true, backupPath? } lets it be sent, anything else
|
|
98
|
+
// ({ ok: false, code }) sends nothing. `now` is a monotonic clock in milliseconds.
|
|
99
|
+
export function createAppQuitter({ platform, uid, readTable, readProcess, signal, processProbe, sleep = (ms) => new Promise((resolve) => { setTimeout(resolve, ms) }), now = () => performance.now(), waitMs = 30_000, pollMs = 250, lingerMs = 5000 } = {}) {
|
|
100
|
+
for (const [name, seam] of Object.entries({ readTable, readProcess, signal, processProbe })) if (typeof seam !== 'function') throw new TypeError(`quitting Obsidian needs an injected ${name}`)
|
|
101
|
+
const probe = () => { try { return processProbe() } catch { return 'unknown' } }
|
|
102
|
+
const describe = (pid) => { try { const line = readProcess(pid); return typeof line === 'string' && line.trim() !== '' ? line.trim() : null } catch { return null } }
|
|
103
|
+
const table = () => { try { const text = readTable(); return typeof text === 'string' ? text : '' } catch { return '' } }
|
|
104
|
+
// The process read again is the one the table proved: this user's, under the parent the table showed, same name.
|
|
105
|
+
const isFound = (line, found) => {
|
|
106
|
+
const match = line === null ? null : DESCRIBED.exec(line)
|
|
107
|
+
return match !== null && Number(match[1]) === found.ppid && Number(match[2]) === uid && match[3].endsWith(found.executable)
|
|
108
|
+
}
|
|
109
|
+
// The proven main process alone in the table, still the process `seen` described: its windows closed, it did not quit.
|
|
110
|
+
const lingersAlone = (found, seen) => {
|
|
111
|
+
const matches = rowsOf(table())
|
|
112
|
+
if (matches.length === 0 || matches.some((match) => match === null)) return false
|
|
113
|
+
const rows = matches.filter((match) => APP_PROCESS.test(path.posix.basename(match[4])))
|
|
114
|
+
return rows.length === 1 && Number(rows[0][1]) === found.pid && describe(found.pid) === seen
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
async quit({ beforeSignal = null } = {}) {
|
|
118
|
+
if (!RESTART_PLATFORMS.includes(platform)) return { quit: false, reason: 'restart-platform-unqualified', signalled: false, signals: 0 }
|
|
119
|
+
const found = provenMainProcess({ table: table(), uid })
|
|
120
|
+
if (found.state === 'absent') return probe() === 'absent' ? { quit: true, pid: null, signalled: false, signals: 0 } : { quit: false, reason: 'app-main-process-unproven', detail: 'the process table shows no Obsidian, and the process probe does not say it is gone', signalled: false, signals: 0 }
|
|
121
|
+
if (found.state !== 'proven') return { quit: false, reason: found.reason, detail: found.detail, signalled: false, signals: 0 }
|
|
122
|
+
const seen = describe(found.pid)
|
|
123
|
+
if (!isFound(seen, found)) return { quit: false, reason: 'app-main-process-unproven', detail: 'the main process read again is not the one the table showed', signalled: false, signals: 0 }
|
|
124
|
+
// Immediately before the signal: still the same process.
|
|
125
|
+
if (describe(found.pid) !== seen) return { quit: false, reason: 'app-main-process-unproven', detail: 'the main process changed while it was being proven', signalled: false, signals: 0 }
|
|
126
|
+
let copy = {}
|
|
127
|
+
if (typeof beforeSignal === 'function') {
|
|
128
|
+
let kept
|
|
129
|
+
try { kept = await beforeSignal() } catch { kept = null }
|
|
130
|
+
if (kept?.ok !== true) return { quit: false, reason: typeof kept?.code === 'string' ? kept.code : 'obsidian-settings-unwritable', signalled: false, signals: 0 }
|
|
131
|
+
if (typeof kept.backupPath === 'string') copy = { settingsCopy: kept.backupPath }
|
|
132
|
+
}
|
|
133
|
+
try { signal(found.pid) } catch { return { quit: false, reason: 'app-main-process-unproven', detail: 'the main process could not be signalled', signalled: false, signals: 0, ...copy } }
|
|
134
|
+
let signals = 1
|
|
135
|
+
let second = false
|
|
136
|
+
let aloneSince = null
|
|
137
|
+
const deadline = now() + waitMs
|
|
138
|
+
for (;;) {
|
|
139
|
+
if (probe() === 'absent') return { quit: true, pid: found.pid, signalled: true, signals, ...copy }
|
|
140
|
+
if (now() >= deadline) return { quit: false, reason: 'app-did-not-quit', pid: found.pid, signalled: true, signals, ...copy }
|
|
141
|
+
if (!second) {
|
|
142
|
+
if (!lingersAlone(found, seen)) aloneSince = null
|
|
143
|
+
else if (aloneSince === null) aloneSince = now()
|
|
144
|
+
else if (now() - aloneSince >= lingerMs) {
|
|
145
|
+
// Once, whether or not it is delivered.
|
|
146
|
+
second = true
|
|
147
|
+
try { signal(found.pid); signals = 2 } catch { /* not delivered: not counted */ }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
await sleep(pollMs)
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
}
|