@mnstry/atelier 0.2.0-alpha.5 → 0.2.0-alpha.8
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 +191 -0
- package/README.md +86 -25
- package/SECURITY.md +6 -1
- package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
- package/contracts/atelier-coauthor.v1.schema.json +163 -0
- package/contracts/atelier-decision-request.v1.schema.json +476 -0
- package/contracts/atelier-decision-result.v1.schema.json +500 -0
- package/contracts/atelier-guide.v1.schema.json +227 -0
- package/contracts/atelier-intake.v1.schema.json +151 -0
- package/contracts/atelier-migration.v2.schema.json +61 -0
- package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
- package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
- package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
- package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
- package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
- package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
- package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
- package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
- package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
- package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
- package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
- package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
- package/contracts/atelier-presentation.v1.schema.json +1293 -0
- package/contracts/atelier-repository-observation.v1.schema.json +163 -0
- package/contracts/atelier-review.v1.schema.json +1601 -0
- package/contracts/atelier-skill-steward.v1.schema.json +692 -0
- package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
- package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
- package/contracts/atelier-vault-publication.v1.schema.json +58 -0
- package/docs/assurance-controls.md +12 -2
- package/docs/atelier-runtime.md +13 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/claims.md +28 -18
- package/docs/blocks/will-not-do.md +12 -3
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/decisions.md +167 -0
- package/docs/exact-upgrades.md +159 -0
- package/docs/extension-pack-lifecycle.md +33 -0
- package/docs/guided-upgrades.md +99 -0
- package/docs/install.md +65 -9
- package/docs/intake-and-guides.md +65 -0
- package/docs/integration-and-review-plan.md +409 -0
- package/docs/integration-contract-decisions.md +22 -0
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +227 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +687 -0
- package/docs/obsidian.md +387 -0
- package/docs/portable-presentation.md +328 -0
- package/docs/presentation-consumer-boundaries.md +75 -0
- package/docs/presentation-review-disposition.md +47 -0
- package/docs/project-command-inventory.md +38 -0
- package/docs/project-options.md +53 -0
- package/docs/release-engineering.md +99 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +70 -2
- package/docs/vault-service.md +292 -0
- package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
- package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
- package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
- package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
- package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
- package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
- package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
- package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
- package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
- package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
- package/fixtures/atelier-intake/valid/source.v1.json +7 -0
- package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
- package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
- package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
- package/fixtures/atelier-review/valid/bound.v1.json +229 -0
- package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
- package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
- package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
- package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
- package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
- package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
- package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
- package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
- package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
- package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
- package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
- package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
- package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
- package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
- package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
- package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
- package/fixtures/decisions/request/valid/triage.v1.json +60 -0
- package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
- package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
- package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
- package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
- package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
- package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
- package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
- package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
- package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
- package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
- package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
- package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
- package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
- package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
- package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
- package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
- package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
- package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
- package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
- package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
- package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
- package/fixtures/obsidian/edits/workspace.json +65 -0
- package/fixtures/obsidian/materialization/expected-full.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
- package/fixtures/obsidian/materialization/workspace.json +54 -0
- package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +61 -6
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-open/SKILL.md +5 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
- package/skills/claude/mnstry-readiness/SKILL.md +5 -0
- package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/codex/atelier-open/SKILL.md +5 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
- package/skills/codex/mnstry-readiness/SKILL.md +5 -0
- package/src/access/preview-enforcer.mjs +84 -0
- package/src/boundary/content-rules.mjs +9 -4
- package/src/boundary/policy.mjs +74 -42
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +61 -3
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +16 -33
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/extension-pack.mjs +15 -8
- package/src/commands/init.mjs +44 -5
- package/src/commands/obsidian.mjs +368 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/setup.mjs +51 -14
- package/src/commands/skills.mjs +199 -0
- package/src/commands/sync.mjs +107 -0
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +35 -0
- package/src/decisions/contracts.d.mts +96 -0
- package/src/decisions/contracts.mjs +321 -0
- package/src/disclosure/content-scan.mjs +13 -0
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +64 -2
- package/src/graph/knowledge-graph.mjs +548 -24
- package/src/guides/contracts.mjs +54 -0
- package/src/index.mjs +29 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +50 -49
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +166 -0
- package/src/project/private-state.mjs +11 -1
- package/src/projection/obsidian/contracts.mjs +571 -0
- package/src/projection/obsidian/edits/align.mjs +169 -0
- package/src/projection/obsidian/edits/apply.mjs +889 -0
- package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
- package/src/projection/obsidian/edits/contribution.mjs +108 -0
- package/src/projection/obsidian/edits/index.mjs +19 -0
- package/src/projection/obsidian/edits/object-identity.mjs +61 -0
- package/src/projection/obsidian/edits/object-store.mjs +517 -0
- package/src/projection/obsidian/edits/observe.mjs +123 -0
- package/src/projection/obsidian/edits/policy.mjs +123 -0
- package/src/projection/obsidian/edits/regions.mjs +648 -0
- package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
- package/src/projection/obsidian/materialize/index.mjs +7 -0
- package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
- package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
- package/src/projection/obsidian/materialize/settings.mjs +57 -0
- package/src/projection/obsidian/materialize/stage.mjs +27 -0
- package/src/projection/obsidian/proposals/adapter.mjs +572 -0
- package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
- package/src/projection/obsidian/proposals/contribution.mjs +55 -0
- package/src/projection/obsidian/proposals/index.mjs +23 -0
- package/src/projection/obsidian/proposals/observation.mjs +216 -0
- package/src/projection/obsidian/proposals/queue.mjs +277 -0
- package/src/projection/obsidian/proposals/router.mjs +168 -0
- package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
- package/src/projection/obsidian/publication/exchange.mjs +117 -0
- package/src/projection/obsidian/publication/index.mjs +5 -0
- package/src/projection/obsidian/publication/publisher.mjs +588 -0
- package/src/projection/obsidian/publication/test-seam.mjs +16 -0
- package/src/projection/obsidian/publication/transport.mjs +139 -0
- package/src/projection/obsidian/recovery/index.mjs +6 -0
- package/src/projection/obsidian/recovery/journal.mjs +134 -0
- package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
- package/src/projection/obsidian/recovery/restart.mjs +316 -0
- package/src/projection/obsidian/recovery/store.mjs +234 -0
- package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
- package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
- package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
- package/src/projection/obsidian/selection-ui/index.mjs +12 -0
- package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
- package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
- package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
- package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
- package/src/projection/project.mjs +28 -19
- package/src/readiness-protocols/evidence.mjs +282 -0
- package/src/readiness-protocols/runtime.mjs +4 -3
- package/src/readiness-protocols/source-read.mjs +39 -0
- package/src/runtime/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -0
- package/src/runtime/obsidian/app-capability.mjs +110 -0
- package/src/runtime/obsidian/app-production-seams.mjs +96 -0
- package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
- package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
- package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
- package/src/runtime/obsidian/contributions.mjs +27 -0
- package/src/runtime/obsidian/documents.mjs +37 -0
- package/src/runtime/obsidian/enablement.mjs +53 -0
- package/src/runtime/obsidian/engine.mjs +532 -0
- package/src/runtime/obsidian/errors.mjs +14 -0
- package/src/runtime/obsidian/extension-points.mjs +103 -0
- package/src/runtime/obsidian/index.mjs +34 -0
- package/src/runtime/obsidian/lifecycle.mjs +258 -0
- package/src/runtime/obsidian/machine-settings.mjs +260 -0
- package/src/runtime/obsidian/observation.mjs +158 -0
- package/src/runtime/obsidian/opening.mjs +243 -0
- package/src/runtime/obsidian/pending-edits.mjs +141 -0
- package/src/runtime/obsidian/pipeline.mjs +122 -0
- package/src/runtime/obsidian/private-lock.mjs +156 -0
- package/src/runtime/obsidian/process-identity.mjs +58 -0
- package/src/runtime/obsidian/service-client.mjs +66 -0
- package/src/runtime/obsidian/service-main.mjs +89 -0
- package/src/runtime/obsidian/service-record.mjs +169 -0
- package/src/runtime/obsidian/service-server.mjs +121 -0
- package/src/runtime/obsidian/service.mjs +216 -0
- package/src/runtime/obsidian/startup-adapters.mjs +87 -0
- package/src/runtime/obsidian/state-store.mjs +180 -0
- package/src/runtime/obsidian/sync-notice.mjs +33 -0
- package/src/runtime/obsidian/tick-loop.mjs +84 -0
- package/src/runtime/obsidian/watchers.mjs +36 -0
- package/src/runtime/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +803 -0
- package/src/server/local-sidecar.mjs +44 -0
- package/src/server/server.mjs +1 -0
- package/src/skills/steward.mjs +780 -0
- package/src/ui/presentation/browser.mjs +278 -0
- package/src/ui/presentation/contract.mjs +116 -0
- package/src/ui/presentation/index.mjs +7 -0
- package/src/ui/presentation/native.mjs +153 -0
- package/src/ui/presentation/proof.mjs +57 -0
- package/src/ui/presentation/schema-check.mjs +30 -0
- package/src/ui/presentation/schema.generated.mjs +1371 -0
- package/src/ui/presentation/state.mjs +43 -0
- package/src/ui/presentation/styles.mjs +67 -0
- package/src/ui/presentation/tokens.mjs +74 -0
- package/src/ui/presentation/web.mjs +85 -0
- package/src/ui/review-page.mjs +32 -0
- package/src/upgrade/explanation.mjs +33 -0
- package/src/upgrade/provenance.mjs +338 -0
- package/src/upgrade/transaction-files.mjs +103 -0
- package/src/upgrade/transaction.mjs +475 -0
- package/src/upgrade/upgrade.mjs +63 -28
- package/src/vault/hosts.mjs +21 -0
- package/src/vault/identity.mjs +19 -0
- package/src/vault/index.mjs +7 -0
- package/src/vault/interface.mjs +11 -0
- package/src/vault/metadata.mjs +31 -0
- package/src/vault/privacy.mjs +148 -0
- package/src/vault/probe.mjs +89 -0
- package/src/vault/service.mjs +131 -0
- package/src/vault/source.mjs +45 -0
- package/src/vault/storage.mjs +23 -0
- package/templates/distribution-workspace/.claude/launch.json +6 -2
- package/templates/distribution-workspace/gitignore +1 -0
- package/templates/distribution-workspace/project/README.md +2 -2
- package/templates/external-project-workspace/README.md +45 -0
- package/templates/external-project-workspace/answers.example.json +4 -0
- package/templates/external-project-workspace/atelier.project.json +40 -0
- package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
- package/templates/external-project-workspace/gitignore +8 -0
- package/templates/external-project-workspace/pack-lifecycle.json +14 -0
- package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
- package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
- package/templates/external-project-workspace/repo-access.v1.json +9 -0
- package/templates/external-project-workspace/source/README.md +11 -0
- package/templates/launch.json +6 -2
- package/templates/private-domain-workspace/.claude/launch.json +6 -2
- package/templates/private-domain-workspace/domain/README.md +2 -2
- package/templates/private-domain-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/.claude/launch.json +6 -2
- package/templates/shared-project-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/project/README.md +2 -2
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
atomicReplacePrivateText,
|
|
6
|
+
ensureContainedPrivateDirectory,
|
|
7
|
+
openRegularFileNoFollow,
|
|
8
|
+
readRegularTextNoFollow,
|
|
9
|
+
} from '../project/private-state.mjs'
|
|
10
|
+
import { runGit } from './git-adapter.mjs'
|
|
11
|
+
|
|
12
|
+
export const ATELIER_RUNTIME_STATE_SCHEMA = 'atelier-runtime-state@v1'
|
|
13
|
+
export const ATELIER_RUNTIME_ENROLLMENT_SCHEMA = 'atelier-runtime-enrollment@v1'
|
|
14
|
+
export const ATELIER_RUNTIME_TRACE_SCHEMA = 'atelier-runtime-operation@v1'
|
|
15
|
+
export const ATELIER_RUNTIME_TRACE_MAX_BYTES = 2 * 1024 * 1024
|
|
16
|
+
export const ATELIER_RUNTIME_TRACE_MAX_RECORDS = 2048
|
|
17
|
+
export const ATELIER_RUNTIME_TRACE_MAX_EVENT_BYTES = 256 * 1024
|
|
18
|
+
export const ATELIER_RUNTIME_PLAN_MAX_AGE_MS = 24 * 60 * 60 * 1000
|
|
19
|
+
export const ATELIER_RUNTIME_PLAN_MAX_FILES = 256
|
|
20
|
+
export const ATELIER_RUNTIME_PLAN_MAX_BYTES = 4 * 1024 * 1024
|
|
21
|
+
export const ATELIER_RUNTIME_STATE_MAX_BYTES = 512 * 1024
|
|
22
|
+
export const ATELIER_RUNTIME_LIVE_OWNER_MAX_AGE_MS = 24 * 60 * 60 * 1000
|
|
23
|
+
|
|
24
|
+
function lstatIfPresent(file) {
|
|
25
|
+
try {
|
|
26
|
+
return fs.lstatSync(file)
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error?.code === 'ENOENT') return null
|
|
29
|
+
throw error
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function runtimeWorkspaceRoot(file) {
|
|
34
|
+
const absolute = path.resolve(file)
|
|
35
|
+
const marker = `${path.sep}.atelier-local${path.sep}`
|
|
36
|
+
const index = absolute.lastIndexOf(marker)
|
|
37
|
+
if (index <= 0) throw new Error('runtime state path is outside .atelier-local')
|
|
38
|
+
return absolute.slice(0, index)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function secureRuntimeLeaf(file) {
|
|
42
|
+
const workspaceRoot = runtimeWorkspaceRoot(file)
|
|
43
|
+
const directory = ensureContainedPrivateDirectory({
|
|
44
|
+
workspaceRoot,
|
|
45
|
+
directory: path.dirname(path.resolve(file)),
|
|
46
|
+
label: 'runtime state directory',
|
|
47
|
+
})
|
|
48
|
+
return path.join(directory, path.basename(file))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function atomicWriteJson(file, value) {
|
|
52
|
+
const secured = secureRuntimeLeaf(file)
|
|
53
|
+
atomicReplacePrivateText(secured, `${JSON.stringify(value, null, 2)}\n`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function readJsonIfPresent(file) {
|
|
57
|
+
const secured = secureRuntimeLeaf(file)
|
|
58
|
+
const stat = lstatIfPresent(secured)
|
|
59
|
+
if (!stat) return null
|
|
60
|
+
if (stat.size > ATELIER_RUNTIME_PLAN_MAX_BYTES) throw new Error('runtime JSON exceeds the resident byte ceiling')
|
|
61
|
+
return JSON.parse(readRegularTextNoFollow(secured))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function removeRuntimeLeaf(file) {
|
|
65
|
+
const secured = secureRuntimeLeaf(file)
|
|
66
|
+
const stat = lstatIfPresent(secured)
|
|
67
|
+
if (!stat) return false
|
|
68
|
+
if (stat.isSymbolicLink() || !stat.isFile()) throw new Error('state leaf is not a regular file')
|
|
69
|
+
fs.unlinkSync(secured)
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function runtimePlanInventory(plansPath) {
|
|
74
|
+
const directory = ensureContainedPrivateDirectory({
|
|
75
|
+
workspaceRoot: runtimeWorkspaceRoot(plansPath),
|
|
76
|
+
directory: plansPath,
|
|
77
|
+
label: 'runtime plans directory',
|
|
78
|
+
})
|
|
79
|
+
const files = []
|
|
80
|
+
let bytes = 0
|
|
81
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
82
|
+
if (!entry.name.endsWith('.json')) continue
|
|
83
|
+
if (entry.isSymbolicLink() || !entry.isFile()) throw new Error('runtime plans directory contains a redirected or non-file entry')
|
|
84
|
+
const file = path.join(directory, entry.name)
|
|
85
|
+
const stat = fs.lstatSync(file)
|
|
86
|
+
files.push({ file, name: entry.name, bytes: stat.size, mtimeMs: stat.mtimeMs })
|
|
87
|
+
bytes += stat.size
|
|
88
|
+
}
|
|
89
|
+
return { files, bytes }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function runtimePaths(repoRoot) {
|
|
93
|
+
const root = path.join(repoRoot, '.atelier-local', 'runtime')
|
|
94
|
+
return {
|
|
95
|
+
root,
|
|
96
|
+
enrollment: path.join(root, 'enrollment.json'),
|
|
97
|
+
state: path.join(root, 'state.json'),
|
|
98
|
+
control: path.join(root, 'control.json'),
|
|
99
|
+
trace: path.join(root, 'operations.ndjson'),
|
|
100
|
+
plans: path.join(root, 'plans'),
|
|
101
|
+
lock: path.join(root, 'operation.lock'),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function ensureRuntimeStateRoot(repoRoot, gitExecutable) {
|
|
106
|
+
const probe = '.atelier-local/runtime/.ignore-probe'
|
|
107
|
+
const ignored = runGit(gitExecutable, repoRoot, ['check-ignore', '-q', probe], { allowFailure: true })
|
|
108
|
+
if (!ignored.ok) {
|
|
109
|
+
throw new Error('.atelier-local/ must be Git-ignored before Atelier Sync writes machine-local state')
|
|
110
|
+
}
|
|
111
|
+
const root = ensureContainedPrivateDirectory({
|
|
112
|
+
workspaceRoot: repoRoot,
|
|
113
|
+
directory: path.join(repoRoot, '.atelier-local', 'runtime'),
|
|
114
|
+
label: 'runtime state directory',
|
|
115
|
+
})
|
|
116
|
+
const plans = ensureContainedPrivateDirectory({
|
|
117
|
+
workspaceRoot: repoRoot,
|
|
118
|
+
directory: path.join(root, 'plans'),
|
|
119
|
+
label: 'runtime plans directory',
|
|
120
|
+
})
|
|
121
|
+
return { ...runtimePaths(repoRoot), root, plans, lock: path.join(root, 'operation.lock') }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function stableJson(value) {
|
|
125
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`
|
|
126
|
+
if (!value || typeof value !== 'object') return JSON.stringify(value)
|
|
127
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function digest(value) {
|
|
131
|
+
return crypto.createHash('sha256').update(stableJson(value)).digest('hex')
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function readOperationTrace(tracePath) {
|
|
135
|
+
const secured = secureRuntimeLeaf(tracePath)
|
|
136
|
+
const stat = lstatIfPresent(secured)
|
|
137
|
+
if (!stat) return []
|
|
138
|
+
if (stat.size > ATELIER_RUNTIME_TRACE_MAX_BYTES) throw new Error('runtime trace exceeds the resident byte ceiling')
|
|
139
|
+
const records = readRegularTextNoFollow(secured).split('\n').filter(Boolean).map((line, index) => {
|
|
140
|
+
try {
|
|
141
|
+
return JSON.parse(line)
|
|
142
|
+
} catch (error) {
|
|
143
|
+
throw new Error(`invalid runtime trace line ${index + 1}: ${error.message}`)
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
if (records.length > ATELIER_RUNTIME_TRACE_MAX_RECORDS) throw new Error('runtime trace exceeds the resident record ceiling')
|
|
147
|
+
let previousHash = null
|
|
148
|
+
records.forEach((record, index) => {
|
|
149
|
+
const { hash, ...unsigned } = record
|
|
150
|
+
if (record.sequence !== index + 1) throw new Error(`runtime trace sequence gap at line ${index + 1}`)
|
|
151
|
+
if ((record.previousHash ?? null) !== previousHash) throw new Error(`runtime trace chain mismatch at line ${index + 1}`)
|
|
152
|
+
if (digest(unsigned) !== hash) throw new Error(`runtime trace hash mismatch at line ${index + 1}`)
|
|
153
|
+
previousHash = hash
|
|
154
|
+
})
|
|
155
|
+
return records
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function boundedTraceEvent(event) {
|
|
159
|
+
const encoded = JSON.stringify(event)
|
|
160
|
+
const originalBytes = Buffer.byteLength(encoded)
|
|
161
|
+
if (originalBytes <= ATELIER_RUNTIME_TRACE_MAX_EVENT_BYTES) return event
|
|
162
|
+
return {
|
|
163
|
+
at: event?.at,
|
|
164
|
+
operation: event?.operation || 'trace-event',
|
|
165
|
+
outcome: event?.outcome || 'recorded',
|
|
166
|
+
...(event?.operationId ? { operationId: String(event.operationId) } : {}),
|
|
167
|
+
...(event?.mode ? { mode: String(event.mode) } : {}),
|
|
168
|
+
details: {
|
|
169
|
+
truncated: true,
|
|
170
|
+
originalBytes,
|
|
171
|
+
originalSha256: crypto.createHash('sha256').update(encoded).digest('hex'),
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function traceRecoveryCheckpoint(secured, event, reason) {
|
|
177
|
+
const stat = lstatIfPresent(secured)
|
|
178
|
+
let priorSha256 = null
|
|
179
|
+
try {
|
|
180
|
+
if ((stat?.size ?? 0) <= ATELIER_RUNTIME_TRACE_MAX_BYTES) {
|
|
181
|
+
priorSha256 = crypto.createHash('sha256').update(readRegularTextNoFollow(secured)).digest('hex')
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// The recovery checkpoint still records the size and reason if the old
|
|
185
|
+
// regular file cannot be read completely.
|
|
186
|
+
}
|
|
187
|
+
const checkpointUnsigned = {
|
|
188
|
+
schema: ATELIER_RUNTIME_TRACE_SCHEMA,
|
|
189
|
+
sequence: 1,
|
|
190
|
+
previousHash: null,
|
|
191
|
+
at: event.at,
|
|
192
|
+
operation: 'trace-checkpoint',
|
|
193
|
+
outcome: 'recovered',
|
|
194
|
+
details: { reason, priorBytes: stat?.size ?? 0, priorSha256 },
|
|
195
|
+
}
|
|
196
|
+
const checkpoint = { ...checkpointUnsigned, hash: digest(checkpointUnsigned) }
|
|
197
|
+
atomicReplacePrivateText(secured, `${JSON.stringify(checkpoint)}\n`)
|
|
198
|
+
return checkpoint
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function appendOperationTrace(tracePath, event) {
|
|
202
|
+
const secured = secureRuntimeLeaf(tracePath)
|
|
203
|
+
const boundedEvent = boundedTraceEvent(event)
|
|
204
|
+
let records
|
|
205
|
+
try {
|
|
206
|
+
records = readOperationTrace(secured)
|
|
207
|
+
} catch (error) {
|
|
208
|
+
records = [traceRecoveryCheckpoint(secured, boundedEvent, error.message)]
|
|
209
|
+
}
|
|
210
|
+
let previousHash = records.at(-1)?.hash ?? null
|
|
211
|
+
const nextProbe = JSON.stringify({ schema: ATELIER_RUNTIME_TRACE_SCHEMA, sequence: records.length + 1, previousHash, ...boundedEvent, hash: '0'.repeat(64) })
|
|
212
|
+
const currentBytes = lstatIfPresent(secured)?.size ?? 0
|
|
213
|
+
if (records.length >= ATELIER_RUNTIME_TRACE_MAX_RECORDS || currentBytes + Buffer.byteLength(`${nextProbe}\n`) > ATELIER_RUNTIME_TRACE_MAX_BYTES) {
|
|
214
|
+
const checkpointUnsigned = {
|
|
215
|
+
schema: ATELIER_RUNTIME_TRACE_SCHEMA,
|
|
216
|
+
sequence: 1,
|
|
217
|
+
previousHash: null,
|
|
218
|
+
at: boundedEvent.at,
|
|
219
|
+
operation: 'trace-checkpoint',
|
|
220
|
+
outcome: 'compacted',
|
|
221
|
+
details: { compactedRecords: records.length, priorLastHash: previousHash },
|
|
222
|
+
}
|
|
223
|
+
const checkpoint = { ...checkpointUnsigned, hash: digest(checkpointUnsigned) }
|
|
224
|
+
atomicReplacePrivateText(secured, `${JSON.stringify(checkpoint)}\n`)
|
|
225
|
+
records = [checkpoint]
|
|
226
|
+
previousHash = checkpoint.hash
|
|
227
|
+
}
|
|
228
|
+
const unsigned = {
|
|
229
|
+
schema: ATELIER_RUNTIME_TRACE_SCHEMA,
|
|
230
|
+
sequence: records.length + 1,
|
|
231
|
+
previousHash,
|
|
232
|
+
...boundedEvent,
|
|
233
|
+
}
|
|
234
|
+
const record = { ...unsigned, hash: digest(unsigned) }
|
|
235
|
+
const descriptor = openRegularFileNoFollow(
|
|
236
|
+
secured,
|
|
237
|
+
fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT,
|
|
238
|
+
0o600,
|
|
239
|
+
)
|
|
240
|
+
try {
|
|
241
|
+
fs.writeFileSync(descriptor, `${JSON.stringify(record)}\n`)
|
|
242
|
+
fs.fsyncSync(descriptor)
|
|
243
|
+
} finally {
|
|
244
|
+
fs.closeSync(descriptor)
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
fs.chmodSync(secured, 0o600)
|
|
248
|
+
} catch {
|
|
249
|
+
// Windows inherits the current user's ACL; POSIX mode is best effort.
|
|
250
|
+
}
|
|
251
|
+
return record
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function processAlive(pid) {
|
|
255
|
+
if (!Number.isInteger(pid) || pid <= 0) return false
|
|
256
|
+
try {
|
|
257
|
+
process.kill(pid, 0)
|
|
258
|
+
return true
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return error?.code === 'EPERM'
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function removeOwnedLock(lockPath, lockNonce) {
|
|
265
|
+
const stat = lstatIfPresent(lockPath)
|
|
266
|
+
if (!stat) return
|
|
267
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error('runtime lock is redirected or not a directory')
|
|
268
|
+
const ownerFile = path.join(lockPath, 'owner.json')
|
|
269
|
+
const owner = readJsonIfPresent(ownerFile)
|
|
270
|
+
if (!lockNonce || owner?.lockNonce !== lockNonce) throw new Error('runtime lock ownership changed before release')
|
|
271
|
+
const quarantine = `${lockPath}.release-${lockNonce}`
|
|
272
|
+
fs.renameSync(lockPath, quarantine)
|
|
273
|
+
fs.rmSync(quarantine, { recursive: true, force: true })
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function acquireRepositoryLock(paths, { operation, clock = () => new Date().toISOString(), ownerWriteGraceMs = 30_000 } = {}) {
|
|
277
|
+
ensureContainedPrivateDirectory({ workspaceRoot: runtimeWorkspaceRoot(paths.root), directory: paths.root, label: 'runtime state directory' })
|
|
278
|
+
const lockNonce = crypto.randomBytes(16).toString('hex')
|
|
279
|
+
try {
|
|
280
|
+
fs.mkdirSync(paths.lock, { mode: 0o700 })
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (error.code !== 'EEXIST') throw error
|
|
283
|
+
const lockStat = lstatIfPresent(paths.lock)
|
|
284
|
+
if (!lockStat || lockStat.isSymbolicLink() || !lockStat.isDirectory()) {
|
|
285
|
+
throw new Error('runtime lock is redirected or not a directory')
|
|
286
|
+
}
|
|
287
|
+
let owner = null
|
|
288
|
+
try {
|
|
289
|
+
owner = readJsonIfPresent(path.join(paths.lock, 'owner.json'))
|
|
290
|
+
} catch (ownerError) {
|
|
291
|
+
if (/redirected|regular file|escapes workspace/.test(ownerError.message)) throw ownerError
|
|
292
|
+
// A truncated owner file is not authority to delete a possibly live lock.
|
|
293
|
+
}
|
|
294
|
+
const ownerTimestamp = Date.parse(owner?.acquiredAt || '')
|
|
295
|
+
const ownerAgeMs = Date.now() - (Number.isFinite(ownerTimestamp) ? ownerTimestamp : lockStat.mtimeMs)
|
|
296
|
+
if (owner && processAlive(owner.pid) && ownerAgeMs < ATELIER_RUNTIME_LIVE_OWNER_MAX_AGE_MS) {
|
|
297
|
+
return { ok: false, code: 'repository-busy', owner }
|
|
298
|
+
}
|
|
299
|
+
const currentLockStat = lstatIfPresent(paths.lock)
|
|
300
|
+
if (!currentLockStat || currentLockStat.isSymbolicLink() || !currentLockStat.isDirectory()) {
|
|
301
|
+
throw new Error('runtime lock is redirected or not a directory')
|
|
302
|
+
}
|
|
303
|
+
const ageMs = Date.now() - currentLockStat.mtimeMs
|
|
304
|
+
if (ageMs < ownerWriteGraceMs) return { ok: false, code: 'repository-busy', owner: owner || { operation: 'lock-owner-pending', ageMs } }
|
|
305
|
+
const claimPath = path.join(paths.lock, 'recovery.claim')
|
|
306
|
+
let claimDescriptor
|
|
307
|
+
try {
|
|
308
|
+
claimDescriptor = openRegularFileNoFollow(
|
|
309
|
+
claimPath,
|
|
310
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
311
|
+
0o600,
|
|
312
|
+
)
|
|
313
|
+
fs.writeFileSync(claimDescriptor, lockNonce)
|
|
314
|
+
fs.fsyncSync(claimDescriptor)
|
|
315
|
+
} catch (claimError) {
|
|
316
|
+
if (claimError?.code === 'EEXIST') {
|
|
317
|
+
const claimStat = lstatIfPresent(claimPath)
|
|
318
|
+
if (!claimStat || claimStat.isSymbolicLink() || !claimStat.isFile()) throw new Error('runtime recovery claim is redirected or not a regular file')
|
|
319
|
+
const claimAgeMs = Date.now() - claimStat.mtimeMs
|
|
320
|
+
if (claimAgeMs < ownerWriteGraceMs) {
|
|
321
|
+
return { ok: false, code: 'repository-busy', owner: owner || { operation: 'stale-lock-recovery', ageMs: claimAgeMs } }
|
|
322
|
+
}
|
|
323
|
+
const staleClaim = `${claimPath}.stale-${lockNonce}`
|
|
324
|
+
try {
|
|
325
|
+
fs.renameSync(claimPath, staleClaim)
|
|
326
|
+
const movedClaimStat = lstatIfPresent(staleClaim)
|
|
327
|
+
if (!movedClaimStat || movedClaimStat.dev !== claimStat.dev || movedClaimStat.ino !== claimStat.ino) {
|
|
328
|
+
if (movedClaimStat && !lstatIfPresent(claimPath)) fs.renameSync(staleClaim, claimPath)
|
|
329
|
+
return { ok: false, code: 'repository-busy', owner: owner || { operation: 'stale-lock-recovery-identity-changed' } }
|
|
330
|
+
}
|
|
331
|
+
fs.unlinkSync(staleClaim)
|
|
332
|
+
claimDescriptor = openRegularFileNoFollow(
|
|
333
|
+
claimPath,
|
|
334
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
|
|
335
|
+
0o600,
|
|
336
|
+
)
|
|
337
|
+
fs.writeFileSync(claimDescriptor, lockNonce)
|
|
338
|
+
fs.fsyncSync(claimDescriptor)
|
|
339
|
+
} catch (retryError) {
|
|
340
|
+
if (retryError?.code === 'EEXIST' || retryError?.code === 'ENOENT') {
|
|
341
|
+
return { ok: false, code: 'repository-busy', owner: owner || { operation: 'stale-lock-recovery-lost' } }
|
|
342
|
+
}
|
|
343
|
+
throw retryError
|
|
344
|
+
}
|
|
345
|
+
} else if (claimError?.code === 'ENOENT') {
|
|
346
|
+
return { ok: false, code: 'repository-busy', owner: owner || { operation: 'stale-lock-recovery' } }
|
|
347
|
+
} else {
|
|
348
|
+
throw claimError
|
|
349
|
+
}
|
|
350
|
+
} finally {
|
|
351
|
+
if (claimDescriptor != null) fs.closeSync(claimDescriptor)
|
|
352
|
+
}
|
|
353
|
+
let currentOwner = null
|
|
354
|
+
try {
|
|
355
|
+
currentOwner = readJsonIfPresent(path.join(paths.lock, 'owner.json'))
|
|
356
|
+
} catch (ownerError) {
|
|
357
|
+
if (/redirected|regular file|escapes workspace/.test(ownerError.message)) throw ownerError
|
|
358
|
+
// The exclusive recovery claim and age gate permit quarantining a stale,
|
|
359
|
+
// corrupt owner record; corruption is never authority to touch a new lock.
|
|
360
|
+
}
|
|
361
|
+
const currentOwnerTimestamp = Date.parse(currentOwner?.acquiredAt || '')
|
|
362
|
+
const currentOwnerAgeMs = Date.now() - (Number.isFinite(currentOwnerTimestamp) ? currentOwnerTimestamp : lockStat.mtimeMs)
|
|
363
|
+
if (currentOwner && processAlive(currentOwner.pid) && currentOwnerAgeMs < ATELIER_RUNTIME_LIVE_OWNER_MAX_AGE_MS) {
|
|
364
|
+
try {
|
|
365
|
+
if (readRegularTextNoFollow(claimPath) === lockNonce) fs.unlinkSync(claimPath)
|
|
366
|
+
} catch {
|
|
367
|
+
// The current owner controls the lock; leave it untouched on uncertainty.
|
|
368
|
+
}
|
|
369
|
+
return { ok: false, code: 'repository-busy', owner: currentOwner }
|
|
370
|
+
}
|
|
371
|
+
const quarantine = `${paths.lock}.stale-${lockNonce}`
|
|
372
|
+
try {
|
|
373
|
+
const beforeRecovery = lstatIfPresent(paths.lock)
|
|
374
|
+
if (!beforeRecovery || beforeRecovery.dev !== lockStat.dev || beforeRecovery.ino !== lockStat.ino) {
|
|
375
|
+
return { ok: false, code: 'repository-busy', owner: { operation: 'stale-lock-recovery-identity-changed' } }
|
|
376
|
+
}
|
|
377
|
+
fs.renameSync(paths.lock, quarantine)
|
|
378
|
+
const movedLock = lstatIfPresent(quarantine)
|
|
379
|
+
if (!movedLock || movedLock.dev !== lockStat.dev || movedLock.ino !== lockStat.ino) {
|
|
380
|
+
if (movedLock && !lstatIfPresent(paths.lock)) fs.renameSync(quarantine, paths.lock)
|
|
381
|
+
return { ok: false, code: 'repository-busy', owner: { operation: 'stale-lock-recovery-identity-changed' } }
|
|
382
|
+
}
|
|
383
|
+
fs.mkdirSync(paths.lock, { mode: 0o700 })
|
|
384
|
+
} catch (recoveryError) {
|
|
385
|
+
if (recoveryError?.code === 'EEXIST' || recoveryError?.code === 'ENOENT') {
|
|
386
|
+
if (lstatIfPresent(quarantine)) fs.rmSync(quarantine, { recursive: true, force: true })
|
|
387
|
+
return { ok: false, code: 'repository-busy', owner: { operation: 'stale-lock-recovery-lost' } }
|
|
388
|
+
}
|
|
389
|
+
throw recoveryError
|
|
390
|
+
}
|
|
391
|
+
fs.rmSync(quarantine, { recursive: true, force: true })
|
|
392
|
+
}
|
|
393
|
+
const owner = { lockNonce, pid: process.pid, operation, acquiredAt: clock() }
|
|
394
|
+
atomicWriteJson(path.join(paths.lock, 'owner.json'), owner)
|
|
395
|
+
return {
|
|
396
|
+
ok: true,
|
|
397
|
+
owner,
|
|
398
|
+
release() {
|
|
399
|
+
removeOwnedLock(paths.lock, lockNonce)
|
|
400
|
+
},
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export function writeRuntimeState(paths, state) {
|
|
405
|
+
let value = { schema: ATELIER_RUNTIME_STATE_SCHEMA, ...state }
|
|
406
|
+
const encodedBytes = Buffer.byteLength(`${JSON.stringify(value, null, 2)}\n`)
|
|
407
|
+
if (encodedBytes > ATELIER_RUNTIME_STATE_MAX_BYTES) {
|
|
408
|
+
value = {
|
|
409
|
+
schema: ATELIER_RUNTIME_STATE_SCHEMA,
|
|
410
|
+
status: 'attention',
|
|
411
|
+
code: 'runtime-state-resident-ceiling',
|
|
412
|
+
message: 'runtime state exceeded its resident byte ceiling and was compacted',
|
|
413
|
+
incidentId: state.incidentId ?? null,
|
|
414
|
+
updatedAt: state.updatedAt ?? new Date().toISOString(),
|
|
415
|
+
observation: state.observation ? {
|
|
416
|
+
sourceSchema: state.observation.sourceSchema ?? state.observation.schema,
|
|
417
|
+
observedAt: state.observation.observedAt,
|
|
418
|
+
complete: false,
|
|
419
|
+
root: state.observation.root,
|
|
420
|
+
branch: state.observation.branch ? { branch: state.observation.branch.branch, head: state.observation.branch.head } : null,
|
|
421
|
+
status: state.observation.status ? { clean: state.observation.status.clean, digest: state.observation.status.digest } : null,
|
|
422
|
+
blockers: [{ code: 'runtime-state-resident-ceiling', message: 'full resident state was too large to retain', details: { limitBytes: ATELIER_RUNTIME_STATE_MAX_BYTES, observedBytes: encodedBytes } }],
|
|
423
|
+
} : null,
|
|
424
|
+
details: { limitBytes: ATELIER_RUNTIME_STATE_MAX_BYTES, observedBytes: encodedBytes },
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
atomicWriteJson(paths.state, value)
|
|
428
|
+
return value
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function readRuntimeControl(paths) {
|
|
432
|
+
return readJsonIfPresent(paths.control) || { paused: false, reason: null, updatedAt: null }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export function writeRuntimeControl(paths, { paused, reason = null, updatedAt = new Date().toISOString() }) {
|
|
436
|
+
const control = { paused: Boolean(paused), reason: paused ? String(reason || 'paused by user') : null, updatedAt }
|
|
437
|
+
atomicWriteJson(paths.control, control)
|
|
438
|
+
return control
|
|
439
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { refuse } from './errors.mjs'
|
|
2
|
+
|
|
3
|
+
// Whether the installed Obsidian may be published through and opened.
|
|
4
|
+
//
|
|
5
|
+
// Nothing here looks at this machine. What is installed is learned through an
|
|
6
|
+
// injected `appProbe`:
|
|
7
|
+
//
|
|
8
|
+
// appProbe.inspect() -> { installed, cli, running, version }
|
|
9
|
+
// appProbe.vaultState({ vaultRoot }) -> { answered, indexReady }
|
|
10
|
+
//
|
|
11
|
+
// `installed` and `cli` are booleans, `running` is true, false or null (not
|
|
12
|
+
// known) and `version` is the app's own version text or null. The production
|
|
13
|
+
// probe is a separate module that only the real command-line entries load;
|
|
14
|
+
// every test passes its own.
|
|
15
|
+
|
|
16
|
+
// The publication protocol sets the view's undocumented `lastSavedData` field.
|
|
17
|
+
// 1.13.7 is the only app version that protocol was proven on, so it is the
|
|
18
|
+
// floor. Raising the ceiling needs the protocol cases re-run on that release.
|
|
19
|
+
export const MINIMUM_APP_VERSION = '1.13.7'
|
|
20
|
+
|
|
21
|
+
export const APP_OUTCOMES = Object.freeze(['qualified', 'app-missing', 'app-cli-unavailable', 'app-version-unsupported'])
|
|
22
|
+
|
|
23
|
+
const VERSION = /^v?(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})\.(0|[1-9]\d{0,8})(?:-([0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*))?(?:\+[0-9A-Za-z.-]+)?(?=$|[\s(])/
|
|
24
|
+
|
|
25
|
+
// { major, minor, patch, prerelease: string[] } or null. Only a version at the
|
|
26
|
+
// very start of the text counts; anything else is not a version.
|
|
27
|
+
export function parseAppVersion(text) {
|
|
28
|
+
if (typeof text !== 'string' || text.length > 200) return null
|
|
29
|
+
const match = VERSION.exec(text.trim())
|
|
30
|
+
if (!match) return null
|
|
31
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]), prerelease: match[4] === undefined ? [] : match[4].split('.') }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function comparePrerelease(left, right) {
|
|
35
|
+
// No prerelease ranks above any prerelease of the same version.
|
|
36
|
+
if (left.length === 0 || right.length === 0) return left.length === right.length ? 0 : left.length === 0 ? 1 : -1
|
|
37
|
+
for (let index = 0; index < Math.max(left.length, right.length); index += 1) {
|
|
38
|
+
if (left[index] === undefined) return -1
|
|
39
|
+
if (right[index] === undefined) return 1
|
|
40
|
+
const [a, b] = [left[index], right[index]]
|
|
41
|
+
const [aNumeric, bNumeric] = [/^\d+$/.test(a), /^\d+$/.test(b)]
|
|
42
|
+
if (aNumeric && bNumeric) { if (Number(a) !== Number(b)) return Number(a) < Number(b) ? -1 : 1 } else if (aNumeric !== bNumeric) return aNumeric ? -1 : 1
|
|
43
|
+
else if (a !== b) return a < b ? -1 : 1
|
|
44
|
+
}
|
|
45
|
+
return 0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Pure. -1, 0 or 1; null when either side is not a version.
|
|
49
|
+
export function compareAppVersions(left, right) {
|
|
50
|
+
const [a, b] = [parseAppVersion(left), parseAppVersion(right)]
|
|
51
|
+
if (a === null || b === null) return null
|
|
52
|
+
for (const part of ['major', 'minor', 'patch']) if (a[part] !== b[part]) return a[part] < b[part] ? -1 : 1
|
|
53
|
+
return comparePrerelease(a.prerelease, b.prerelease)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Pure. A version that cannot be read does not meet the floor.
|
|
57
|
+
export function meetsMinimumAppVersion(version, floor = MINIMUM_APP_VERSION) {
|
|
58
|
+
const order = compareAppVersions(version, floor)
|
|
59
|
+
return order !== null && order >= 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pure. `requireVersion: false` lets an installed app that is positively not
|
|
63
|
+
// running pass without a version: nothing is published through an app that
|
|
64
|
+
// does not run, and the version is asked for again once it does.
|
|
65
|
+
export function qualifyApp(observation, { requireVersion = true, floor = MINIMUM_APP_VERSION } = {}) {
|
|
66
|
+
const seen = observation !== null && typeof observation === 'object' ? observation : {}
|
|
67
|
+
const base = { floor, version: typeof seen.version === 'string' ? seen.version.slice(0, 80) : null, running: seen.running === true ? true : seen.running === false ? false : null }
|
|
68
|
+
if (seen.installed !== true) return { ...base, outcome: 'app-missing', reason: 'no-app-found' }
|
|
69
|
+
if (seen.cli !== true) return { ...base, outcome: 'app-cli-unavailable', reason: 'cli-capability-absent' }
|
|
70
|
+
if (base.version === null) {
|
|
71
|
+
if (!requireVersion && base.running === false) return { ...base, outcome: 'qualified', reason: 'app-not-running-version-not-needed', versionChecked: false }
|
|
72
|
+
return { ...base, outcome: 'app-version-unsupported', reason: 'version-unknown' }
|
|
73
|
+
}
|
|
74
|
+
if (parseAppVersion(base.version) === null) return { ...base, outcome: 'app-version-unsupported', reason: 'version-unreadable' }
|
|
75
|
+
if (!meetsMinimumAppVersion(base.version, floor)) return { ...base, outcome: 'app-version-unsupported', reason: 'below-minimum-version' }
|
|
76
|
+
return { ...base, outcome: 'qualified', reason: 'meets-minimum-version', versionChecked: true }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function inspectApp(appProbe) {
|
|
80
|
+
if (typeof appProbe?.inspect !== 'function') throw new TypeError('an appProbe with inspect() is required')
|
|
81
|
+
try { return await appProbe.inspect() } catch { return { installed: false, cli: false, running: null, version: null } }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// The adapter factory of a service that reaches a real app. The editor adapter
|
|
85
|
+
// is constructed only after the app qualified; otherwise the factory refuses,
|
|
86
|
+
// typed, and the engine records that reason and publishes nothing. The answer
|
|
87
|
+
// is remembered briefly so a tick over many views asks once.
|
|
88
|
+
export function createQualifiedAdapterFactory({ appProbe, createAdapter, floor = MINIMUM_APP_VERSION, maxAgeMs = 10_000, now = () => Date.now() } = {}) {
|
|
89
|
+
if (typeof appProbe?.inspectSync !== 'function') throw new TypeError('the qualified adapter factory needs an appProbe with inspectSync()')
|
|
90
|
+
if (typeof createAdapter !== 'function') throw new TypeError('the qualified adapter factory needs createAdapter')
|
|
91
|
+
let last = null
|
|
92
|
+
const qualification = () => {
|
|
93
|
+
if (last === null || now() - last.at > maxAgeMs) {
|
|
94
|
+
let observation
|
|
95
|
+
try { observation = appProbe.inspectSync() } catch { observation = null }
|
|
96
|
+
last = { at: now(), result: qualifyApp(observation, { requireVersion: false, floor }) }
|
|
97
|
+
}
|
|
98
|
+
return last.result
|
|
99
|
+
}
|
|
100
|
+
const factory = (input) => {
|
|
101
|
+
const result = qualification()
|
|
102
|
+
// No app at all is not an unqualified app: the publisher's own path needs none, and its adapter finds no process.
|
|
103
|
+
if (result.outcome !== 'qualified' && result.outcome !== 'app-missing') refuse(result.outcome, 'the installed Obsidian does not qualify; nothing is published through it', { reason: result.reason, floor: result.floor, version: result.version })
|
|
104
|
+
return createAdapter(input)
|
|
105
|
+
}
|
|
106
|
+
factory.qualification = qualification
|
|
107
|
+
// What was last learned, without asking again: for a status answer.
|
|
108
|
+
factory.lastQualification = () => last?.result ?? null
|
|
109
|
+
return factory
|
|
110
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { execFile, execFileSync } from 'node:child_process'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { defaultCliPath, defaultObsidianProcessProbe } from '../../projection/obsidian/publication/transport.mjs'
|
|
5
|
+
|
|
6
|
+
// The production seams of `obsidian open` and of the service's adapter
|
|
7
|
+
// factory: the only code that asks the installed Obsidian anything or asks the
|
|
8
|
+
// operating system to open it.
|
|
9
|
+
//
|
|
10
|
+
// This module is imported by exactly two places, both real command-line
|
|
11
|
+
// entries and both behind an explicit `--adapter=obsidian-cli`: the service
|
|
12
|
+
// entry (service-main.mjs) and the command entry (src/commands/obsidian.mjs,
|
|
13
|
+
// `production: true`). No test imports it, and importing it leaves a trace
|
|
14
|
+
// below that the test suite asserts is absent.
|
|
15
|
+
//
|
|
16
|
+
// Status: written against the documented command-line interface and NOT yet
|
|
17
|
+
// exercised against a running app. The acceptance work qualifies it on an
|
|
18
|
+
// isolated host before any of it is relied on; until then every answer it
|
|
19
|
+
// cannot establish is the failing one.
|
|
20
|
+
globalThis[Symbol.for('mnstry.atelier.obsidian.production-seams-loaded')] = true
|
|
21
|
+
|
|
22
|
+
const CLI_TIMEOUT_MS = 5000
|
|
23
|
+
|
|
24
|
+
function installedAppPath(platform) {
|
|
25
|
+
if (platform === 'darwin') return '/Applications/Obsidian.app'
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function cliExists(cliPath) {
|
|
30
|
+
if (!path.isAbsolute(cliPath)) return null // found through PATH or not at all: only running it tells
|
|
31
|
+
try { return fs.statSync(cliPath).isFile() } catch { return false }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const observationOf = ({ platform, cliPath, processes, versionText }) => {
|
|
35
|
+
const appPath = installedAppPath(platform)
|
|
36
|
+
const present = cliExists(cliPath)
|
|
37
|
+
const answered = typeof versionText === 'string' && versionText.trim() !== ''
|
|
38
|
+
return {
|
|
39
|
+
installed: appPath === null ? (present !== false && (answered || present === true)) : fs.existsSync(appPath),
|
|
40
|
+
cli: present === true || answered,
|
|
41
|
+
running: processes === 'running' ? true : processes === 'absent' ? false : null,
|
|
42
|
+
version: answered ? versionText.trim() : null,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function createProductionAppProbe({ platform = process.platform, env = process.env, cliPath = defaultCliPath(platform) } = {}) {
|
|
47
|
+
const options = { env, timeout: CLI_TIMEOUT_MS, killSignal: 'SIGKILL', encoding: 'utf8', maxBuffer: 1024 * 1024 }
|
|
48
|
+
return {
|
|
49
|
+
// For the service's adapter factory, which is synchronous. The version is asked for only while an app runs.
|
|
50
|
+
inspectSync() {
|
|
51
|
+
const processes = defaultObsidianProcessProbe({ platform })
|
|
52
|
+
let versionText = null
|
|
53
|
+
if (processes !== 'absent') { try { versionText = execFileSync(cliPath, ['version'], { ...options, stdio: ['ignore', 'pipe', 'ignore'] }) } catch { versionText = null } }
|
|
54
|
+
return observationOf({ platform, cliPath, processes, versionText })
|
|
55
|
+
},
|
|
56
|
+
inspect() {
|
|
57
|
+
const processes = defaultObsidianProcessProbe({ platform })
|
|
58
|
+
if (processes === 'absent') return Promise.resolve(observationOf({ platform, cliPath, processes, versionText: null }))
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
execFile(cliPath, ['version'], options, (error, stdout) => resolve(observationOf({ platform, cliPath, processes, versionText: error ? null : stdout })))
|
|
61
|
+
})
|
|
62
|
+
},
|
|
63
|
+
// Whether the app answers for exactly this vault and has finished reading it.
|
|
64
|
+
vaultState({ vaultRoot }) {
|
|
65
|
+
const code = '(()=>JSON.stringify({basePath:app.vault.adapter.basePath,ready:app.metadataCache.initialized===true}))()'
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
execFile(cliPath, ['eval', `code=${code}`], options, (error, stdout) => {
|
|
68
|
+
if (error) return resolve({ answered: false, indexReady: false })
|
|
69
|
+
try {
|
|
70
|
+
const text = String(stdout)
|
|
71
|
+
const value = JSON.parse(JSON.parse(text.slice(text.indexOf('=> ') + 3)))
|
|
72
|
+
return resolve({ answered: value.basePath === vaultRoot, indexReady: value.basePath === vaultRoot && value.ready === true })
|
|
73
|
+
} catch { return resolve({ answered: false, indexReady: false }) }
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Asks the operating system to open the vault in Obsidian. No shell.
|
|
81
|
+
export function createProductionLauncher({ platform = process.platform, env = process.env } = {}) {
|
|
82
|
+
const command = platform === 'darwin' ? '/usr/bin/open' : platform === 'linux' ? 'xdg-open' : null
|
|
83
|
+
return {
|
|
84
|
+
open({ vaultRoot }) {
|
|
85
|
+
if (command === null) return Promise.resolve({ launched: false, reason: 'launcher-platform-unqualified' })
|
|
86
|
+
const uri = `obsidian://open?path=${encodeURIComponent(vaultRoot)}`
|
|
87
|
+
return new Promise((resolve) => {
|
|
88
|
+
execFile(command, [uri], { env, timeout: 15_000 }, (error) => resolve(error ? { launched: false, reason: 'os-open-failed' } : { launched: true, reason: 'os-open-accepted' }))
|
|
89
|
+
})
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createProductionAppSeams(options = {}) {
|
|
95
|
+
return { appProbe: createProductionAppProbe(options), launcher: createProductionLauncher(options) }
|
|
96
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { createProposalAdapterContribution } from '../../../projection/obsidian/proposals/contribution.mjs'
|
|
2
|
+
|
|
3
|
+
// The proposal adapter: what the engine hands structural edits to on a tick, and `atelier obsidian proposals`.
|
|
4
|
+
export default createProposalAdapterContribution()
|