@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
|
@@ -12,7 +12,7 @@ import { processRunsRecordedExecutable } from './process-identity.mjs'
|
|
|
12
12
|
import { DEFAULT_PROBE_TIMEOUT_MS, LOOPBACK_HOSTS, probeHealth, requestLoopback } from './service-client.mjs'
|
|
13
13
|
import { SERVICE_ENTRY_PATH } from './service-entry-path.mjs'
|
|
14
14
|
import {
|
|
15
|
-
CONSENT_COVERAGES, SERVICE_SETTINGS_SCHEMA, executableIdentity, openServiceLog, publicRecord, readServiceRecord, readServiceSettings,
|
|
15
|
+
CONSENT_COVERAGES, SERVICE_SETTINGS_SCHEMA, executableIdentity, openServiceLog, publicRecord, readLastStartup, readServiceRecord, readServiceSettings, releaseOfEntry, removeServiceRecord,
|
|
16
16
|
serviceNameFor, servicePaths, writeServiceSettings,
|
|
17
17
|
} from './service-record.mjs'
|
|
18
18
|
import { resolveServiceWorkspace } from './service.mjs'
|
|
@@ -105,8 +105,18 @@ function freeLoopbackPort(host) {
|
|
|
105
105
|
})
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
// The service settings of this workspace with the address and consent given, written when that changes anything: the
|
|
109
|
+
// first start needs a consent naming its actor, and a consent for another actor or coverage replaces the recorded one.
|
|
110
|
+
// Installing and removing a login item raise and lower the coverage through it (login-item.mjs).
|
|
111
|
+
export async function ensureServiceSettings(workspace, { host, port, consent, now }) {
|
|
112
|
+
return resolveSettings(workspace, { host, port, consent, now })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function resolveSettings({ workspaceRoot, workspaceId }, { host, port, consent: given, now }) {
|
|
109
116
|
const current = readServiceSettings({ workspaceRoot, workspaceId })
|
|
117
|
+
// A consent derived from the account of a person at a terminal (`derived: true`) stands only for a workspace that has
|
|
118
|
+
// none. Read here, under the start lock, so one recorded meanwhile by another run is never replaced or narrowed.
|
|
119
|
+
const consent = given?.derived === true && current !== null ? undefined : given
|
|
110
120
|
if (host !== undefined && !LOOPBACK_HOSTS.includes(host)) refuse('service-address-not-loopback', 'the service host must be the literal 127.0.0.1 or ::1')
|
|
111
121
|
if (port !== undefined && (!Number.isInteger(port) || port < 1024 || port > 65535)) refuse('service-address-not-loopback', 'the service port must be between 1024 and 65535')
|
|
112
122
|
if (consent !== undefined && (typeof consent?.actor !== 'string' || !CONSENT_COVERAGES.includes(consent.coverage ?? 'service'))) refuse('startup-consent-required', 'a consent names its actor and what it covers')
|
|
@@ -121,11 +131,55 @@ async function resolveSettings({ workspaceRoot, workspaceId }, { host, port, con
|
|
|
121
131
|
return changed ? writeServiceSettings({ workspaceRoot, workspaceId, settings: next }) : current
|
|
122
132
|
}
|
|
123
133
|
|
|
134
|
+
// Asks the service manager of the installed login item to start the service, and waits, bounded, for this
|
|
135
|
+
// workspace's service to prove itself (healthy, or busy in its first tick) with a record naming the digest of the
|
|
136
|
+
// entry the item runs. The runtime identifier is the service's own there, so that digest is what makes it the one
|
|
137
|
+
// asked for. A service that refused under the item exited cleanly and recorded why (`last-startup.json`); that code is
|
|
138
|
+
// the answer. { result } or, when the manager did not start it (the item is not loaded: switched off in System
|
|
139
|
+
// Settings, or no user service manager), { fallback: code }.
|
|
140
|
+
async function startThroughLoginItem({ workspace, loginItem, entryPath, deadline, probeTimeoutMs, alive, rules }) {
|
|
141
|
+
const { workspaceRoot, workspaceId } = workspace
|
|
142
|
+
const lastStartup = () => { try { return JSON.stringify(readLastStartup(workspace)) } catch { return null } }
|
|
143
|
+
const before = lastStartup()
|
|
144
|
+
let asked
|
|
145
|
+
try { asked = await loginItem.start() } catch { asked = { ok: false, code: 'login-item-start-failed' } }
|
|
146
|
+
const item = { via: 'login-item', ...(asked?.refreshed === true ? { refreshed: true } : {}) }
|
|
147
|
+
if (asked?.ok !== true) return { fallback: typeof asked?.code === 'string' ? asked.code : 'login-item-start-failed', refreshed: asked?.refreshed === true }
|
|
148
|
+
// A unit written again on the way names the entry of now. One whose entry cannot be read cannot prove anything.
|
|
149
|
+
let expected
|
|
150
|
+
try { expected = executableIdentity(typeof asked.entryPath === 'string' ? asked.entryPath : entryPath).digest } catch { return { result: { state: 'start-failed', started: false, alreadyRunning: false, workspaceId, reason: 'login-item-entry-missing', observed: null, logPath: servicePaths(workspaceRoot).loginItemLog, loginItem: item } } }
|
|
151
|
+
let last = null
|
|
152
|
+
const proves = (status) => status?.record?.executable?.digest === expected
|
|
153
|
+
while (Date.now() < deadline) {
|
|
154
|
+
last = await evaluate(workspace, { probeTimeoutMs, alive, rules }).catch((error) => ({ state: 'refused', reason: error.code ?? 'untyped-error' }))
|
|
155
|
+
if (last.state === 'healthy' && proves(last)) return { result: { ...shown(last), started: true, alreadyRunning: false, loginItem: item } }
|
|
156
|
+
const now = lastStartup()
|
|
157
|
+
if (now !== before && now !== null) {
|
|
158
|
+
const startup = JSON.parse(now)
|
|
159
|
+
if (startup?.outcome === 'refused') return { result: { state: 'start-failed', started: false, alreadyRunning: false, workspaceId, reason: startup.code, observed: last.state ?? null, logPath: servicePaths(workspaceRoot).log, loginItem: item } }
|
|
160
|
+
}
|
|
161
|
+
await sleep(50)
|
|
162
|
+
}
|
|
163
|
+
// As for a child started here: a service that proved itself and went straight into a long first tick runs, and is
|
|
164
|
+
// answered as busy only once the wait for it to be healthy is over.
|
|
165
|
+
if (last?.state === 'busy' && proves(last)) return { result: { ...shown(last), started: true, alreadyRunning: false, busy: true, loginItem: item } }
|
|
166
|
+
return { result: { state: 'start-failed', started: false, alreadyRunning: false, workspaceId, reason: 'health-never-proved-ownership', observed: last?.state ?? null, logPath: servicePaths(workspaceRoot).loginItemLog, loginItem: item } }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// `loginItem`, for a workspace with an installed login item: { start() }, which asks its service manager to start the
|
|
170
|
+
// service (login-item.mjs). The service is then started through it, never as a child beside it, so two starts never
|
|
171
|
+
// compete; `entryPath` is the entry the item runs. When the manager does not start it, the service is started as a
|
|
172
|
+
// child, and the answer says why (`loginItem: { via: 'child', reason }`).
|
|
173
|
+
//
|
|
174
|
+
// `replaceOutdated`: a proven runtime of this workspace that runs an earlier release than the entry this start would run
|
|
175
|
+
// (runtimeRelease) is stopped as `stopService` stops it and started again in its place, under the consent already
|
|
176
|
+
// recorded (`replaced: 'outdated'`); one of a later release is never replaced (`release: 'later'`). Without it, any
|
|
177
|
+
// proven runtime is answered as already running.
|
|
124
178
|
export async function startService(options = {}, rules = LIFECYCLE_PRIMITIVES) {
|
|
125
179
|
const {
|
|
126
180
|
loadProject, dataRoot, host, port, consent, detached = false, entryPath = SERVICE_ENTRY_PATH, entryArgs = [], intervalMs,
|
|
127
181
|
startTimeoutMs = DEFAULT_START_TIMEOUT_MS, probeTimeoutMs, clock = () => new Date(), env = process.env, platform = process.platform,
|
|
128
|
-
spawn = childProcess.spawn, execPath = process.execPath, alive = isProcessAlive, randomBytes = cryptoRandomBytes,
|
|
182
|
+
spawn = childProcess.spawn, execPath = process.execPath, alive = isProcessAlive, randomBytes = cryptoRandomBytes, loginItem = null, replaceOutdated = false,
|
|
129
183
|
} = options
|
|
130
184
|
const { project, workspace } = context({ loadProject, dataRoot, env, platform, create: true })
|
|
131
185
|
const { workspaceRoot, workspaceId } = workspace
|
|
@@ -146,11 +200,26 @@ export async function startService(options = {}, rules = LIFECYCLE_PRIMITIVES) {
|
|
|
146
200
|
|
|
147
201
|
let child = null
|
|
148
202
|
try {
|
|
149
|
-
|
|
150
|
-
|
|
203
|
+
let before = await evaluate(workspace, { probeTimeoutMs, alive, rules })
|
|
204
|
+
// A runtime of an earlier release, still running after an upgrade, is replaced when asked to; it is stopped through its
|
|
205
|
+
// own listener, as its owner stops it, and nothing else is ever stopped here.
|
|
206
|
+
let replaced = null
|
|
207
|
+
if (rules.isOurs(before)) {
|
|
208
|
+
const standing = replaceOutdated ? runtimeRelease(before, entryPath) : null
|
|
209
|
+
if (standing === 'later') return { ...shown(before), started: false, alreadyRunning: true, release: 'later' }
|
|
210
|
+
if (standing !== 'outdated') return { ...shown(before), started: false, alreadyRunning: true }
|
|
211
|
+
const stopped = await stopService({ loadProject, dataRoot, env, platform, probeTimeoutMs, alive }, rules)
|
|
212
|
+
if (!stopped.stopped) return { ...shown(before), started: false, alreadyRunning: true, release: 'outdated', reason: stopped.reason ?? 'the-runtime-was-not-stopped' }
|
|
213
|
+
replaced = 'outdated'
|
|
214
|
+
before = await evaluate(workspace, { probeTimeoutMs, alive, rules })
|
|
215
|
+
}
|
|
151
216
|
// Running, and in a long tick: nothing is started beside it and nothing replaces it.
|
|
152
217
|
if (before.state === 'busy') return { ...shown(before), started: false, alreadyRunning: true, busy: true }
|
|
153
|
-
|
|
218
|
+
// With a login item, a listener that has not written its record yet may be the service its manager is starting
|
|
219
|
+
// right now (it listens before it records itself): the start below asks the manager, which starts nothing beside
|
|
220
|
+
// a running job, and accepts only a runtime that proves itself. Anything else stays occupied and is never adopted.
|
|
221
|
+
const starting = loginItem !== null && before.state === 'occupied' && before.reason === 'a-listener-without-a-record'
|
|
222
|
+
if (rules.refusesOccupied(before) && !starting) refuse('service-port-occupied', 'something that is not this service answers on the loopback port; it is never taken over', { reason: before.reason, address: before.address })
|
|
154
223
|
// `stale-record` and `pid-not-ours`: nothing listens, so the recorded runtime is not serving. Its PID is never signalled;
|
|
155
224
|
// the new service replaces the record once it listens. Nothing under recovery or staging is touched on the way.
|
|
156
225
|
const settings = await resolveSettings(workspace, { host, port, consent, now: isoTime(clock) })
|
|
@@ -159,6 +228,15 @@ export async function startService(options = {}, rules = LIFECYCLE_PRIMITIVES) {
|
|
|
159
228
|
if (moved.kind !== 'refused') refuse('service-port-occupied', 'something already answers on the selected loopback port; it is never taken over', { address: { host: settings.host, port: settings.port } })
|
|
160
229
|
}
|
|
161
230
|
|
|
231
|
+
// An installed login item starts it. Only when its manager does not is a child started, and the answer says why.
|
|
232
|
+
let fallback = null
|
|
233
|
+
if (loginItem !== null) {
|
|
234
|
+
const managed = await startThroughLoginItem({ workspace, loginItem, entryPath, deadline, probeTimeoutMs, alive, rules })
|
|
235
|
+
if (managed.result) return replaced === null ? managed.result : { ...managed.result, replaced }
|
|
236
|
+
fallback = { via: 'child', reason: managed.fallback, ...(managed.refreshed ? { refreshed: true } : {}) }
|
|
237
|
+
}
|
|
238
|
+
const withFallback = (result) => ({ ...result, ...(fallback === null ? {} : { loginItem: fallback }), ...(replaced === null ? {} : { replaced }) })
|
|
239
|
+
|
|
162
240
|
const runtimeId = `rt-${randomBytes(16).toString('hex')}`
|
|
163
241
|
const executable = executableIdentity(entryPath)
|
|
164
242
|
const log = openServiceLog(workspaceRoot)
|
|
@@ -180,19 +258,19 @@ export async function startService(options = {}, rules = LIFECYCLE_PRIMITIVES) {
|
|
|
180
258
|
last = await evaluate(workspace, { probeTimeoutMs, alive, rules }).catch((error) => ({ state: 'refused', reason: error.code ?? 'untyped-error' }))
|
|
181
259
|
if (last.state === 'healthy' && last.record.runtimeId === runtimeId && last.record.pid === child.pid) {
|
|
182
260
|
if (detached) child.unref()
|
|
183
|
-
return { ...shown(last), started: true, alreadyRunning: false, ...(detached ? {} : { child }) }
|
|
261
|
+
return withFallback({ ...shown(last), started: true, alreadyRunning: false, ...(detached ? {} : { child }) })
|
|
184
262
|
}
|
|
185
263
|
await sleep(50)
|
|
186
264
|
}
|
|
187
265
|
// The child created here wrote the record and went straight into a long first tick: it runs, and is not stopped.
|
|
188
266
|
if (exited === null && last?.state === 'busy' && last.record?.runtimeId === runtimeId && last.record?.pid === child.pid) {
|
|
189
267
|
if (detached) child.unref()
|
|
190
|
-
return { ...shown(last), started: true, alreadyRunning: false, busy: true, ...(detached ? {} : { child }) }
|
|
268
|
+
return withFallback({ ...shown(last), started: true, alreadyRunning: false, busy: true, ...(detached ? {} : { child }) })
|
|
191
269
|
}
|
|
192
270
|
// Ownership was never proven: only the child created here is stopped, by its handle.
|
|
193
271
|
if (exited === null) { child.kill(); const until = Date.now() + 5000; while (exited === null && Date.now() < until) await sleep(25) }
|
|
194
272
|
removeServiceRecord({ workspaceRoot, workspaceId, runtimeId, pid: child.pid })
|
|
195
|
-
return { state: 'start-failed', started: false, alreadyRunning: false, workspaceId, reason: exited?.error ?? (exited?.code === null || exited === null ? 'health-never-proved-ownership' : `service-exited-${exited.code}`), observed: last?.state ?? null, logPath: log.file }
|
|
273
|
+
return withFallback({ state: 'start-failed', started: false, alreadyRunning: false, workspaceId, reason: exited?.error ?? (exited?.code === null || exited === null ? 'health-never-proved-ownership' : `service-exited-${exited.code}`), observed: last?.state ?? null, logPath: log.file })
|
|
196
274
|
} finally {
|
|
197
275
|
lock.release()
|
|
198
276
|
}
|
|
@@ -242,7 +320,7 @@ async function askProvenRuntime(options, rules, { method, operation, timeoutMs,
|
|
|
242
320
|
|
|
243
321
|
// Pure. How the release a runtime recorded at its start (`executable.ext.release`, see releaseIdentity: the package
|
|
244
322
|
// version and a digest of every runtime module) stands to the installed one, `installed` being { entry, release }, the
|
|
245
|
-
// digest of the entry this command would start and
|
|
323
|
+
// digest of the entry this command would start and the release that entry's package holds now (releaseOfEntry):
|
|
246
324
|
// current the same version, entry module and modules;
|
|
247
325
|
// outdated an earlier version; the same version with another entry module or other modules; or no release named,
|
|
248
326
|
// as releases before the one that began recording it record none;
|
|
@@ -261,11 +339,13 @@ export function releaseStanding(executable, installed) {
|
|
|
261
339
|
}
|
|
262
340
|
|
|
263
341
|
// How the release the proven runtime runs stands to the installed one (releaseStanding), `entryPath` being the entry
|
|
264
|
-
// this command would start
|
|
342
|
+
// this command would start: its digest and the release are both read from that entry, so with a login item the
|
|
343
|
+
// installed release is the project's own package, read through the path the item names, whichever package this command
|
|
344
|
+
// runs from. Null when the runtime is not proven, or the installed side cannot be read.
|
|
265
345
|
export function runtimeRelease(status, entryPath) {
|
|
266
346
|
if (status?.state !== 'healthy' || typeof status.record?.executable?.digest !== 'string' || typeof entryPath !== 'string') return null
|
|
267
347
|
let installed
|
|
268
|
-
try { installed = { entry: executableIdentity(entryPath).digest, release:
|
|
348
|
+
try { installed = { entry: executableIdentity(entryPath).digest, release: releaseOfEntry(entryPath) } } catch { return null }
|
|
269
349
|
return releaseStanding(status.record.executable, installed)
|
|
270
350
|
}
|
|
271
351
|
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import os from 'node:os'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import { atomicReplacePrivateText, ensureContainedPrivateDirectory, readRegularTextNoFollow } from '../../project/private-state.mjs'
|
|
6
|
+
import { canonicalJson, closedObject, isoTime } from './documents.mjs'
|
|
7
|
+
import { refuse } from './errors.mjs'
|
|
8
|
+
import { ensureServiceSettings } from './lifecycle.mjs'
|
|
9
|
+
import { ensureWorkspaceIdentity, readLocalPointer, resolveDataRoot } from './machine-settings.mjs'
|
|
10
|
+
import { SERVICE_MANAGER_KINDS } from './service-managers.mjs'
|
|
11
|
+
import { readLastStartup, readServiceSettings, serviceNameFor, servicePaths, writeServiceSettings } from './service-record.mjs'
|
|
12
|
+
import { resolveServiceWorkspace } from './service.mjs'
|
|
13
|
+
import { STARTUP_PLATFORMS, buildStartupAdapter, startupSearchPath } from './startup-adapters.mjs'
|
|
14
|
+
|
|
15
|
+
// A login item: the operating system starts this workspace's maintenance
|
|
16
|
+
// service when the person logs in, so a vault is fresh after a restart
|
|
17
|
+
// without anything being run.
|
|
18
|
+
//
|
|
19
|
+
// macOS a launchd user agent: ~/Library/LaunchAgents/ai.mnstry.atelier.<project>.<workspace-id>.plist
|
|
20
|
+
// Linux a systemd user unit: ~/.config/systemd/user/atelier-obsidian-<workspace-id>.service
|
|
21
|
+
// Windows not offered (startup-platform-unqualified)
|
|
22
|
+
//
|
|
23
|
+
// One per workspace, each with its own label, port, log and consent, each
|
|
24
|
+
// running its own project's installed release. The unit runs the service
|
|
25
|
+
// entry of the package installed in the project, named by its path, so an
|
|
26
|
+
// upgrade in the project is what the next start runs; the Node that ran the
|
|
27
|
+
// installing command, by its real path, so a per-shell link of a version
|
|
28
|
+
// manager is never named; the search path the person had then, absolute
|
|
29
|
+
// entries only; the data root the workspace was resolved under and the
|
|
30
|
+
// workspace, so the service at login finds the same workspace whatever its
|
|
31
|
+
// manager's environment carries (XDG_DATA_HOME, the overlay's variable), and
|
|
32
|
+
// records a refusal there even when the project no longer leads to it; and
|
|
33
|
+
// `--startup`, under which the service refuses to run without a consent that
|
|
34
|
+
// covers startup.
|
|
35
|
+
//
|
|
36
|
+
// Installing records that consent first, since the manager starts the
|
|
37
|
+
// service as soon as it loads the unit, then hands the text to the injected
|
|
38
|
+
// service manager (service-managers.mjs) and keeps
|
|
39
|
+
//
|
|
40
|
+
// state/service/login-item.json atelier-obsidian-login-item/v1
|
|
41
|
+
//
|
|
42
|
+
// { label, file, digest of the text, program: { node, entry }, searchPath }.
|
|
43
|
+
// The label is allocated once and kept. Once installed, the service is
|
|
44
|
+
// started through the manager (loginItemStarter), never as a child beside
|
|
45
|
+
// it; a unit that differs from what would be written now (a Node that was
|
|
46
|
+
// removed, another entry, an earlier release's format) is written again on
|
|
47
|
+
// that way and reloaded, keeping the search path recorded at installation.
|
|
48
|
+
// Removing lowers the consent to the service alone first, so a unit left
|
|
49
|
+
// behind by a failed removal could only refuse, then removes the unit and
|
|
50
|
+
// the record. A person who switched the item off (System Settings, or
|
|
51
|
+
// `systemctl --user disable`) is not overridden: the manager is asked first,
|
|
52
|
+
// the unit is neither written again nor started through it, and the service
|
|
53
|
+
// is started as a child for that command only.
|
|
54
|
+
|
|
55
|
+
export const LOGIN_ITEM_SCHEMA = 'atelier-obsidian-login-item/v1'
|
|
56
|
+
export const LOGIN_ITEM_PACKAGE = '@mnstry/atelier'
|
|
57
|
+
const ENTRY_IN_PACKAGE = Object.freeze(['src', 'runtime', 'obsidian', 'service-main.mjs'])
|
|
58
|
+
const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
59
|
+
const TIMESTAMP = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
|
60
|
+
const DIGEST = /^sha256:[0-9a-f]{64}$/
|
|
61
|
+
const CONTROL = /[\u0000-\u001f\u007f]/
|
|
62
|
+
// A package runner's cache: npx, pnpm dlx, bunx. A package there can be removed by the runner at any time.
|
|
63
|
+
const RUNNER_CACHES = Object.freeze([/[\\/]_npx[\\/]/, /[\\/]pnpm[\\/]dlx[\\/]/, /[\\/]\.bun[\\/]install[\\/]cache[\\/]/])
|
|
64
|
+
|
|
65
|
+
const sha256 = (text) => `sha256:${createHash('sha256').update(text).digest('hex')}`
|
|
66
|
+
const isAbsolutePlain = (value) => typeof value === 'string' && path.isAbsolute(value) && !CONTROL.test(value)
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// The record
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function validateLoginItem(document, workspaceId) {
|
|
73
|
+
const code = 'invalid-login-item'
|
|
74
|
+
closedObject(document, { required: ['schema', 'workspaceId', 'kind', 'label', 'file', 'digest', 'program', 'searchPath', 'installedAt', 'updatedAt'] }, code, 'the login item record')
|
|
75
|
+
closedObject(document.program, { required: ['node', 'entry'] }, code, 'the program of the login item')
|
|
76
|
+
const ok = document.schema === LOGIN_ITEM_SCHEMA && document.workspaceId === workspaceId && SERVICE_MANAGER_KINDS.includes(document.kind)
|
|
77
|
+
&& typeof document.label === 'string' && IDENTIFIER.test(document.label) && isAbsolutePlain(document.file) && path.basename(document.file).startsWith(document.label)
|
|
78
|
+
&& typeof document.digest === 'string' && DIGEST.test(document.digest) && isAbsolutePlain(document.program.node) && isAbsolutePlain(document.program.entry)
|
|
79
|
+
&& (document.searchPath === null || (typeof document.searchPath === 'string' && document.searchPath.split(':').every(isAbsolutePlain)))
|
|
80
|
+
&& TIMESTAMP.test(document.installedAt) && TIMESTAMP.test(document.updatedAt)
|
|
81
|
+
if (!ok) refuse(code, 'the login item record is malformed')
|
|
82
|
+
return document
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function readLoginItemRecord({ workspaceRoot, workspaceId }) {
|
|
86
|
+
let text
|
|
87
|
+
try { text = readRegularTextNoFollow(servicePaths(workspaceRoot).loginItem) } catch (error) {
|
|
88
|
+
if (error.code === 'ENOENT') return null
|
|
89
|
+
return refuse('invalid-login-item', 'the login item record cannot be read', { cause: error.code ?? 'unreadable' })
|
|
90
|
+
}
|
|
91
|
+
let document
|
|
92
|
+
try { document = JSON.parse(text) } catch { return refuse('invalid-login-item', 'the login item record is not JSON') }
|
|
93
|
+
return validateLoginItem(document, workspaceId)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function writeLoginItemRecord({ workspaceRoot, workspaceId, record }) {
|
|
97
|
+
validateLoginItem(record, workspaceId)
|
|
98
|
+
const directory = ensureContainedPrivateDirectory({ workspaceRoot, directory: servicePaths(workspaceRoot).directory, label: 'Obsidian service state' })
|
|
99
|
+
atomicReplacePrivateText(path.join(directory, 'login-item.json'), canonicalJson(record))
|
|
100
|
+
return record
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function removeLoginItemRecord({ workspaceRoot }) {
|
|
104
|
+
try { fs.unlinkSync(servicePaths(workspaceRoot).loginItem); return true } catch (error) { if (error.code === 'ENOENT') return false; throw error }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// What a unit is made of
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
// The part of a launchd label that stands for the project: lower-case letters, digits and dashes, at most 40, from
|
|
112
|
+
// its name, so a person who looks in ~/Library/LaunchAgents can tell whose it is. Pure.
|
|
113
|
+
export function labelSlug(name) {
|
|
114
|
+
const slug = String(name ?? '').normalize('NFKD').replace(/[\u0300-\u036f]/g, '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40).replace(/-+$/, '')
|
|
115
|
+
return slug === '' ? 'project' : slug
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// The label of this workspace's login item, as it is allocated the first time. Pure.
|
|
119
|
+
export function loginItemLabel({ platform, projectName, workspaceId }) {
|
|
120
|
+
const id = String(workspaceId).replaceAll(':', '_')
|
|
121
|
+
return platform === 'darwin' ? `ai.mnstry.atelier.${labelSlug(projectName)}.${id}` : serviceNameFor(workspaceId).replaceAll(':', '_')
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// The name people read for a project: its configured `name`, else the name of its folder.
|
|
125
|
+
export const projectNameOf = (project) => (typeof project?.config?.name === 'string' && project.config.name.trim() !== '' ? project.config.name : path.basename(project?.configDir ?? 'project'))
|
|
126
|
+
|
|
127
|
+
// Absolute folders whose contents do not outlive a restart, or not for long.
|
|
128
|
+
export function temporaryRoots({ tmpdir = os.tmpdir() } = {}) {
|
|
129
|
+
const roots = new Set(['/tmp', '/private/tmp', '/var/tmp', '/private/var/tmp'])
|
|
130
|
+
if (isAbsolutePlain(tmpdir)) { roots.add(path.resolve(tmpdir)); try { roots.add(fs.realpathSync(tmpdir)) } catch { /* only as given */ } }
|
|
131
|
+
return [...roots]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const inside = (file, root) => file === root || file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)
|
|
135
|
+
const isFile = (file) => { try { return fs.statSync(file).isFile() } catch { return false } }
|
|
136
|
+
|
|
137
|
+
// The entry a login item runs: the service entry of the package installed for this project, found as Node finds a
|
|
138
|
+
// package from the project's folder (in its node_modules or a folder's above), named by that path and not its real
|
|
139
|
+
// path, so an upgrade in place is what the next start runs; else `ownEntry`, the entry of the package this command
|
|
140
|
+
// runs from, unless that one is in a package runner's cache (npx) or a temporary folder, which are gone after a
|
|
141
|
+
// restart (`login-item-needs-installed-package`). { entryPath, source: 'project' | 'command' }.
|
|
142
|
+
export function resolveLoginItemEntry({ project, ownEntry, temporary = temporaryRoots(), exists = isFile } = {}) {
|
|
143
|
+
if (typeof project?.configDir === 'string' && path.isAbsolute(project.configDir)) {
|
|
144
|
+
for (let directory = path.resolve(project.configDir); ; directory = path.dirname(directory)) {
|
|
145
|
+
const candidate = path.join(directory, 'node_modules', ...LOGIN_ITEM_PACKAGE.split('/'), ...ENTRY_IN_PACKAGE)
|
|
146
|
+
if (exists(candidate)) return { entryPath: candidate, source: 'project' }
|
|
147
|
+
if (path.dirname(directory) === directory) break
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const transient = typeof ownEntry !== 'string' || !path.isAbsolute(ownEntry) || RUNNER_CACHES.some((pattern) => pattern.test(ownEntry)) || temporary.some((root) => inside(path.resolve(ownEntry), root))
|
|
151
|
+
if (transient) refuse('login-item-needs-installed-package', 'a login item runs the package installed in the project, and this project has none; the command runs from a package runner\'s cache or a temporary folder, which may be gone after a restart')
|
|
152
|
+
return { entryPath: ownEntry, source: 'command' }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// The Node a unit names: the one running this command, by its real path.
|
|
156
|
+
export const realNodePath = (execPath = process.execPath) => { try { return fs.realpathSync(execPath) } catch { return execPath } }
|
|
157
|
+
|
|
158
|
+
// The data root this workspace is resolved under for this command (the flag, the pointer, the overlay's preference or
|
|
159
|
+
// the platform default), which a unit names.
|
|
160
|
+
export const loginItemDataRoot = ({ project, dataRoot, env = process.env, platform = process.platform }) => resolveDataRoot({ dataRoot, pointer: readLocalPointer(project), project, env, platform })
|
|
161
|
+
|
|
162
|
+
// The unit this workspace's login item would have now. `dataRoot` is the resolved one (loginItemDataRoot), always named;
|
|
163
|
+
// `entryArgs` follow `--project`, `--data-root` and `--workspace-id`. Pure but for the builder's refusals.
|
|
164
|
+
export function planLoginItem({ platform, project, workspaceRoot, workspaceId, dataRoot, label, entryPath, entryArgs = [], nodePath, searchPath = null }) {
|
|
165
|
+
if (!isAbsolutePlain(dataRoot)) throw new TypeError('a login item names the absolute data root of its workspace')
|
|
166
|
+
if (typeof workspaceId !== 'string' || workspaceId === '') throw new TypeError('a login item names its workspace')
|
|
167
|
+
const args = [`--project=${project.configPath}`, `--data-root=${dataRoot}`, `--workspace-id=${workspaceId}`, ...entryArgs]
|
|
168
|
+
const unit = buildStartupAdapter({ platform, label, nodePath, entryPath, args, logPath: servicePaths(workspaceRoot).loginItemLog, searchPath })
|
|
169
|
+
return { ...unit, label, digest: sha256(unit.text), program: { node: nodePath, entry: entryPath }, searchPath }
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function checkManager(manager, platform) {
|
|
173
|
+
if (!STARTUP_PLATFORMS.includes(platform)) buildStartupAdapter({ platform })
|
|
174
|
+
const operations = ['readUnit', 'install', 'start', 'remove', 'inspect']
|
|
175
|
+
if (manager === null || typeof manager !== 'object' || !operations.every((name) => typeof manager[name] === 'function') || !SERVICE_MANAGER_KINDS.includes(manager.kind)) {
|
|
176
|
+
throw new TypeError('a login item needs an injected service manager')
|
|
177
|
+
}
|
|
178
|
+
if (manager.platform !== platform) throw new TypeError('the service manager is not this platform\'s')
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Install, remove, start through it, and what it looks like
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
// Installs, or installs again, this workspace's login item. `consent` ({ actor }) is the person's consent to the
|
|
186
|
+
// service running at login; without it, a recorded consent must already cover startup (`startup-consent-required`).
|
|
187
|
+
// `ownEntry` and `entryArgs` are the entry this command would start and its arguments (`--adapter=...`).
|
|
188
|
+
// `beforeInstall({ entryPath })`, when given, runs once the consent is recorded and before the manager loads the unit,
|
|
189
|
+
// which starts the service at once: the command stops a service of an earlier release there, so that start runs the
|
|
190
|
+
// entry the unit names instead of finding the earlier one running.
|
|
191
|
+
// { installed: true, label, file, kind, entry: { path, source }, node, searchPath } or, when the manager did not take
|
|
192
|
+
// the unit, { installed: false, reason, label } with the consent recorded before, or, when none was, the one just
|
|
193
|
+
// recorded covering the service alone.
|
|
194
|
+
export async function installLoginItem(options = {}) {
|
|
195
|
+
const { loadProject, dataRoot, env = process.env, platform = process.platform, manager, consent, ownEntry, entryArgs = [], nodePath = realNodePath(), pathValue = env.PATH, clock = () => new Date(), temporary = temporaryRoots(), beforeInstall = null } = options
|
|
196
|
+
checkManager(manager, platform)
|
|
197
|
+
const project = loadProject()
|
|
198
|
+
ensureWorkspaceIdentity({ project, ...(dataRoot === undefined ? {} : { dataRoot }) })
|
|
199
|
+
const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform, create: true })
|
|
200
|
+
const { entryPath, source } = resolveLoginItemEntry({ project, ownEntry, temporary })
|
|
201
|
+
const recorded = readLoginItemRecord(workspace)
|
|
202
|
+
const label = recorded?.label ?? loginItemLabel({ platform, projectName: projectNameOf(project), workspaceId: workspace.workspaceId })
|
|
203
|
+
const plan = planLoginItem({
|
|
204
|
+
platform, project, workspaceRoot: workspace.workspaceRoot, workspaceId: workspace.workspaceId, dataRoot: loginItemDataRoot({ project, dataRoot, env, platform }), label, entryPath, entryArgs, nodePath,
|
|
205
|
+
searchPath: startupSearchPath(pathValue, { temporary }),
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// The consent first: the manager starts the service as soon as it loads the unit.
|
|
209
|
+
const now = isoTime(clock)
|
|
210
|
+
const before = readServiceSettings(workspace)
|
|
211
|
+
if (consent === undefined && before?.consent.coverage !== 'service-and-startup') refuse('startup-consent-required', 'a login item needs a consent that covers the service starting at login; name who gives it')
|
|
212
|
+
if (consent !== undefined && (typeof consent?.actor !== 'string' || consent.actor === '')) refuse('startup-consent-required', 'a consent names its actor')
|
|
213
|
+
await ensureServiceSettings(workspace, { consent: consent === undefined ? undefined : { actor: consent.actor, coverage: 'service-and-startup' }, now })
|
|
214
|
+
if (typeof beforeInstall === 'function') await beforeInstall({ entryPath })
|
|
215
|
+
|
|
216
|
+
const installed = await manager.install({ label, fileName: plan.fileName, text: plan.text })
|
|
217
|
+
if (!installed.ok) {
|
|
218
|
+
restoreConsent(workspace, before, now)
|
|
219
|
+
return { installed: false, reason: installed.code, label, message: installed.message ?? null }
|
|
220
|
+
}
|
|
221
|
+
writeLoginItemRecord({
|
|
222
|
+
...workspace,
|
|
223
|
+
record: { schema: LOGIN_ITEM_SCHEMA, workspaceId: workspace.workspaceId, kind: manager.kind, label, file: installed.file, digest: plan.digest, program: plan.program, searchPath: plan.searchPath, installedAt: recorded?.installedAt ?? now, updatedAt: now },
|
|
224
|
+
})
|
|
225
|
+
return { installed: true, label, file: installed.file, kind: manager.kind, entry: { path: entryPath, source }, node: nodePath, searchPath: plan.searchPath }
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Back to the consent that was recorded before an installation the manager refused: the service may still run, as a
|
|
229
|
+
// child, under the consent it had.
|
|
230
|
+
function restoreConsent(workspace, before, now) {
|
|
231
|
+
const current = readServiceSettings(workspace)
|
|
232
|
+
if (current === null) return
|
|
233
|
+
const consent = before?.consent ?? { ...current.consent, coverage: 'service' }
|
|
234
|
+
if (JSON.stringify(consent) !== JSON.stringify(current.consent)) writeServiceSettings({ ...workspace, settings: { ...current, consent, updatedAt: now } })
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Removes this workspace's login item: the consent is lowered to the service alone first, then the manager unloads
|
|
238
|
+
// and deletes the unit (a service it runs is stopped as it stops it), then the record goes. { removed, label, file }
|
|
239
|
+
// or { removed: false, reason } when the manager kept it.
|
|
240
|
+
export async function removeLoginItem(options = {}) {
|
|
241
|
+
const { loadProject, dataRoot, env = process.env, platform = process.platform, manager, clock = () => new Date() } = options
|
|
242
|
+
checkManager(manager, platform)
|
|
243
|
+
const project = loadProject()
|
|
244
|
+
const workspace = resolveServiceWorkspace({ project, dataRoot, env, platform })
|
|
245
|
+
if (!workspace?.workspaceRoot) return { removed: false, reason: 'workspace-not-prepared' }
|
|
246
|
+
const record = readLoginItemRecord(workspace)
|
|
247
|
+
const label = record?.label ?? loginItemLabel({ platform, projectName: projectNameOf(project), workspaceId: workspace.workspaceId })
|
|
248
|
+
const fileName = record === null ? `${label}${manager.kind === 'launchd-user-agent' ? '.plist' : '.service'}` : path.basename(record.file)
|
|
249
|
+
const now = isoTime(clock)
|
|
250
|
+
const settings = readServiceSettings(workspace)
|
|
251
|
+
if (settings?.consent.coverage === 'service-and-startup') await ensureServiceSettings(workspace, { consent: { actor: settings.consent.actor, coverage: 'service' }, now })
|
|
252
|
+
const removed = await manager.remove({ label, fileName })
|
|
253
|
+
if (!removed.ok) return { removed: false, reason: removed.code, label, message: removed.message ?? null }
|
|
254
|
+
removeLoginItemRecord(workspace)
|
|
255
|
+
return { removed: removed.removed === true || record !== null, label, file: manager.fileFor(fileName) }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// The unit this workspace's installed login item would have now, keeping its label and the search path recorded when
|
|
259
|
+
// it was installed; null when no entry for it can be found from here (the command runs from a package runner and the
|
|
260
|
+
// project has no package installed): what is installed then stays as it is.
|
|
261
|
+
export function currentLoginItemPlan({ record, project, workspaceRoot, dataRoot, env = process.env, platform, ownEntry, entryArgs = [], nodePath = realNodePath(), temporary = temporaryRoots() }) {
|
|
262
|
+
try {
|
|
263
|
+
const { entryPath } = resolveLoginItemEntry({ project, ownEntry, temporary })
|
|
264
|
+
return planLoginItem({
|
|
265
|
+
platform, project, workspaceRoot, workspaceId: record.workspaceId, dataRoot: loginItemDataRoot({ project, dataRoot, env, platform }), label: record.label, entryPath, entryArgs, nodePath,
|
|
266
|
+
searchPath: record.searchPath,
|
|
267
|
+
})
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error?.code === 'login-item-needs-installed-package' || error?.code === 'startup-adapter-input-invalid') return null
|
|
270
|
+
throw error
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// { entryPath, start() } for startService: the entry the item runs, and a start through its manager. The manager is
|
|
275
|
+
// asked first whether the person switched the item off (System Settings, `systemctl --user disable`): one that is off
|
|
276
|
+
// is neither written again, reloaded, enabled nor started (`login-item-switched-off`), and startService starts a child
|
|
277
|
+
// for that command only. A unit that differs from `plan` is written again and reloaded first (the answer then carries
|
|
278
|
+
// `refreshed: true`); a unit whose file is gone was removed by somebody and is not put back.
|
|
279
|
+
export function loginItemStarter({ workspace, manager, record, plan = null, clock = () => new Date() }) {
|
|
280
|
+
const fileName = path.basename(record.file)
|
|
281
|
+
let entryPath = record.program.entry
|
|
282
|
+
return {
|
|
283
|
+
get entryPath() { return entryPath },
|
|
284
|
+
async start() {
|
|
285
|
+
let refreshed = false
|
|
286
|
+
const text = manager.readUnit({ label: record.label, fileName })
|
|
287
|
+
if (text === null) return { ok: false, code: 'login-item-file-missing', refreshed }
|
|
288
|
+
let seen = null
|
|
289
|
+
try { seen = await manager.inspect({ label: record.label, fileName }) } catch { seen = null }
|
|
290
|
+
if (seen?.ok === true && seen.disabled === true) return { ok: false, code: 'login-item-switched-off', refreshed }
|
|
291
|
+
if (plan !== null && plan.fileName === fileName && text !== plan.text) {
|
|
292
|
+
const installed = await manager.install({ label: record.label, fileName, text: plan.text })
|
|
293
|
+
if (!installed.ok) return { ok: false, code: installed.code, refreshed }
|
|
294
|
+
writeLoginItemRecord({ ...workspace, record: { ...record, file: installed.file, digest: plan.digest, program: plan.program, updatedAt: isoTime(clock) } })
|
|
295
|
+
entryPath = plan.program.entry
|
|
296
|
+
refreshed = true
|
|
297
|
+
}
|
|
298
|
+
const started = await manager.start({ label: record.label, fileName })
|
|
299
|
+
return { ...started, refreshed, entryPath }
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// What this workspace's login item looks like: the record, what the manager says about it, and how the last start by
|
|
305
|
+
// it ended. `manager` is a manager, or a function that answers one (asked only when an item is recorded), or null
|
|
306
|
+
// (it cannot be asked from here): the record alone is reported then.
|
|
307
|
+
// state loaded | not-loaded | switched-off | file-missing | unknown
|
|
308
|
+
export async function loginItemStatus({ workspace, manager: given = null }) {
|
|
309
|
+
const lastStartup = (() => { try { return readLastStartup(workspace) } catch { return null } })()
|
|
310
|
+
let record
|
|
311
|
+
try { record = readLoginItemRecord(workspace) } catch (error) { return { installed: null, reason: error.code ?? 'invalid-login-item', lastStartup } }
|
|
312
|
+
if (record === null) return { installed: false, lastStartup }
|
|
313
|
+
// A unit whose entry or Node is gone cannot start: its manager tries again every minute until it is removed or installed again.
|
|
314
|
+
const present = (file) => { try { return fs.statSync(file).isFile() } catch { return false } }
|
|
315
|
+
const shown = {
|
|
316
|
+
installed: true, label: record.label, file: record.file, kind: record.kind, entry: record.program.entry, node: record.program.node, installedAt: record.installedAt,
|
|
317
|
+
programPresent: present(record.program.entry) && present(record.program.node), lastStartup,
|
|
318
|
+
}
|
|
319
|
+
const manager = typeof given === 'function' ? await given() : given
|
|
320
|
+
if (manager === null) return { ...shown, state: 'unknown', inspected: false }
|
|
321
|
+
const fileName = path.basename(record.file)
|
|
322
|
+
let seen = null
|
|
323
|
+
try { seen = await manager.inspect({ label: record.label, fileName }) } catch { seen = null }
|
|
324
|
+
let text = null
|
|
325
|
+
try { text = manager.readUnit({ label: record.label, fileName }) } catch { text = null }
|
|
326
|
+
const state = seen === null || seen.ok !== true ? 'unknown'
|
|
327
|
+
: seen.present === false ? 'file-missing'
|
|
328
|
+
: seen.disabled === true ? 'switched-off'
|
|
329
|
+
: seen.loaded === true ? 'loaded'
|
|
330
|
+
: seen.loaded === false ? 'not-loaded' : 'unknown'
|
|
331
|
+
return { ...shown, state, inspected: true, running: seen?.running ?? null, pid: seen?.pid ?? null, lastExit: seen?.lastExit ?? null, changedOnDisk: typeof text === 'string' && sha256(text) !== record.digest }
|
|
332
|
+
}
|