@askdkc/kiokuko 0.1.17 → 0.1.19
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/README.ja.md +91 -112
- package/README.ko.md +90 -102
- package/README.md +103 -262
- package/README.zh-CN.md +88 -102
- package/dist/agent-file/atomic-write.d.ts +90 -3
- package/dist/agent-file/atomic-write.d.ts.map +1 -1
- package/dist/agent-file/atomic-write.js +990 -14
- package/dist/agent-file/atomic-write.js.map +1 -1
- package/dist/agent-file/managed-block.d.ts +12 -1
- package/dist/agent-file/managed-block.d.ts.map +1 -1
- package/dist/agent-file/managed-block.js +49 -4
- package/dist/agent-file/managed-block.js.map +1 -1
- package/dist/agent-file/render.d.ts +1 -1
- package/dist/agent-file/render.d.ts.map +1 -1
- package/dist/agent-file/render.js +10 -7
- package/dist/agent-file/render.js.map +1 -1
- package/dist/akinator/agent-task.d.ts +11 -22
- package/dist/akinator/agent-task.d.ts.map +1 -1
- package/dist/akinator/agent-task.js +454 -99
- package/dist/akinator/agent-task.js.map +1 -1
- package/dist/akinator/capabilities.d.ts +18 -6
- package/dist/akinator/capabilities.d.ts.map +1 -1
- package/dist/akinator/capabilities.js +72 -35
- package/dist/akinator/capabilities.js.map +1 -1
- package/dist/akinator/capability-binding.d.ts +11 -0
- package/dist/akinator/capability-binding.d.ts.map +1 -0
- package/dist/akinator/capability-binding.js +56 -0
- package/dist/akinator/capability-binding.js.map +1 -0
- package/dist/akinator/domain.js +2 -2
- package/dist/akinator/domain.js.map +1 -1
- package/dist/akinator/knowledge-path.d.ts.map +1 -1
- package/dist/akinator/knowledge-path.js +409 -39
- package/dist/akinator/knowledge-path.js.map +1 -1
- package/dist/akinator/orchestrator.d.ts +3 -4
- package/dist/akinator/orchestrator.d.ts.map +1 -1
- package/dist/akinator/orchestrator.js +1 -4
- package/dist/akinator/orchestrator.js.map +1 -1
- package/dist/akinator/service.d.ts +0 -4
- package/dist/akinator/service.d.ts.map +1 -1
- package/dist/akinator/service.js +19 -35
- package/dist/akinator/service.js.map +1 -1
- package/dist/akinator/skill-discovery-attempt.d.ts +20 -0
- package/dist/akinator/skill-discovery-attempt.d.ts.map +1 -0
- package/dist/akinator/skill-discovery-attempt.js +390 -0
- package/dist/akinator/skill-discovery-attempt.js.map +1 -0
- package/dist/akinator/store.d.ts.map +1 -1
- package/dist/akinator/store.js +107 -52
- package/dist/akinator/store.js.map +1 -1
- package/dist/akinator/types.d.ts +0 -12
- package/dist/akinator/types.d.ts.map +1 -1
- package/dist/bin/kiokuko.js +0 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +534 -148
- package/dist/cli.js.map +1 -1
- package/dist/client/server-client.d.ts +5 -3
- package/dist/client/server-client.d.ts.map +1 -1
- package/dist/client/server-client.js +486 -181
- package/dist/client/server-client.js.map +1 -1
- package/dist/commands/agent.d.ts +2 -0
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +861 -86
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/backup.d.ts +2 -1
- package/dist/commands/backup.d.ts.map +1 -1
- package/dist/commands/backup.js +64 -8
- package/dist/commands/backup.js.map +1 -1
- package/dist/commands/doctor.d.ts +5 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +70 -26
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/export.d.ts +0 -4
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +191 -32
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/import.d.ts +10 -1
- package/dist/commands/import.d.ts.map +1 -1
- package/dist/commands/import.js +592 -113
- package/dist/commands/import.js.map +1 -1
- package/dist/commands/init.d.ts +19 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +569 -22
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/purge.d.ts.map +1 -1
- package/dist/commands/purge.js +3 -0
- package/dist/commands/purge.js.map +1 -1
- package/dist/commands/setup.d.ts +34 -8
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +418 -86
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/skills.d.ts +9 -0
- package/dist/commands/skills.d.ts.map +1 -0
- package/dist/commands/skills.js +317 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/use.d.ts +10 -2
- package/dist/commands/use.d.ts.map +1 -1
- package/dist/commands/use.js +851 -73
- package/dist/commands/use.js.map +1 -1
- package/dist/config/paths.d.ts +11 -1
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +64 -29
- package/dist/config/paths.js.map +1 -1
- package/dist/config/project-config.d.ts +1 -0
- package/dist/config/project-config.d.ts.map +1 -1
- package/dist/config/project-config.js +70 -32
- package/dist/config/project-config.js.map +1 -1
- package/dist/context/broker.d.ts +28 -14
- package/dist/context/broker.d.ts.map +1 -1
- package/dist/context/broker.js +604 -145
- package/dist/context/broker.js.map +1 -1
- package/dist/context/delivery.d.ts +6 -21
- package/dist/context/delivery.d.ts.map +1 -1
- package/dist/context/delivery.js +196 -147
- package/dist/context/delivery.js.map +1 -1
- package/dist/context/feedback.d.ts +11 -12
- package/dist/context/feedback.d.ts.map +1 -1
- package/dist/context/feedback.js +229 -261
- package/dist/context/feedback.js.map +1 -1
- package/dist/context/ranking.d.ts +1 -12
- package/dist/context/ranking.d.ts.map +1 -1
- package/dist/context/ranking.js +13 -4
- package/dist/context/ranking.js.map +1 -1
- package/dist/context/recommendations.d.ts +0 -5
- package/dist/context/recommendations.d.ts.map +1 -1
- package/dist/context/recommendations.js +0 -1
- package/dist/context/recommendations.js.map +1 -1
- package/dist/context/run-state.d.ts +26 -0
- package/dist/context/run-state.d.ts.map +1 -0
- package/dist/context/run-state.js +399 -0
- package/dist/context/run-state.js.map +1 -0
- package/dist/context/scoped-broker.d.ts +17 -5
- package/dist/context/scoped-broker.d.ts.map +1 -1
- package/dist/context/scoped-broker.js +512 -106
- package/dist/context/scoped-broker.js.map +1 -1
- package/dist/context/selection-state.d.ts +19 -0
- package/dist/context/selection-state.d.ts.map +1 -0
- package/dist/context/selection-state.js +250 -0
- package/dist/context/selection-state.js.map +1 -0
- package/dist/db/adapter.d.ts +5 -3
- package/dist/db/adapter.d.ts.map +1 -1
- package/dist/db/adapter.js +6 -3
- package/dist/db/adapter.js.map +1 -1
- package/dist/db/capabilities.d.ts.map +1 -1
- package/dist/db/capabilities.js +34 -16
- package/dist/db/capabilities.js.map +1 -1
- package/dist/db/connection.d.ts +8 -0
- package/dist/db/connection.d.ts.map +1 -1
- package/dist/db/connection.js +55 -26
- package/dist/db/connection.js.map +1 -1
- package/dist/db/migrate.d.ts +28 -7
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +106 -78
- package/dist/db/migrate.js.map +1 -1
- package/dist/db/sqlite-retry.d.ts +11 -0
- package/dist/db/sqlite-retry.d.ts.map +1 -0
- package/dist/db/sqlite-retry.js +66 -0
- package/dist/db/sqlite-retry.js.map +1 -0
- package/dist/db/transaction.d.ts +13 -0
- package/dist/db/transaction.d.ts.map +1 -1
- package/dist/db/transaction.js +53 -24
- package/dist/db/transaction.js.map +1 -1
- package/dist/db/upgrade-backup.d.ts +58 -2
- package/dist/db/upgrade-backup.d.ts.map +1 -1
- package/dist/db/upgrade-backup.js +757 -28
- package/dist/db/upgrade-backup.js.map +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/gateway/agent-service.d.ts +4 -1
- package/dist/gateway/agent-service.d.ts.map +1 -1
- package/dist/gateway/agent-service.js +71 -58
- package/dist/gateway/agent-service.js.map +1 -1
- package/dist/gateway/checkpoint-service.d.ts +1 -0
- package/dist/gateway/checkpoint-service.d.ts.map +1 -1
- package/dist/gateway/checkpoint-service.js +47 -24
- package/dist/gateway/checkpoint-service.js.map +1 -1
- package/dist/ledger/archive.d.ts +1 -4
- package/dist/ledger/archive.d.ts.map +1 -1
- package/dist/ledger/archive.js +77 -39
- package/dist/ledger/archive.js.map +1 -1
- package/dist/ledger/maintenance.d.ts +0 -11
- package/dist/ledger/maintenance.d.ts.map +1 -1
- package/dist/ledger/maintenance.js +24 -40
- package/dist/ledger/maintenance.js.map +1 -1
- package/dist/ledger/projection.d.ts +0 -1
- package/dist/ledger/projection.d.ts.map +1 -1
- package/dist/ledger/projection.js.map +1 -1
- package/dist/ledger/promotion.d.ts +0 -9
- package/dist/ledger/promotion.d.ts.map +1 -1
- package/dist/ledger/promotion.js.map +1 -1
- package/dist/ledger/query.d.ts +1 -15
- package/dist/ledger/query.d.ts.map +1 -1
- package/dist/ledger/query.js.map +1 -1
- package/dist/ledger/store.d.ts +0 -1
- package/dist/ledger/store.d.ts.map +1 -1
- package/dist/ledger/store.js +11 -8
- package/dist/ledger/store.js.map +1 -1
- package/dist/ledger/types.d.ts +0 -4
- package/dist/ledger/types.d.ts.map +1 -1
- package/dist/ledger/types.js.map +1 -1
- package/dist/ledger/validate.d.ts.map +1 -1
- package/dist/ledger/validate.js +0 -5
- package/dist/ledger/validate.js.map +1 -1
- package/dist/mcp/server.d.ts +4 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +105 -54
- package/dist/mcp/server.js.map +1 -1
- package/dist/memory/audit.d.ts +0 -1
- package/dist/memory/audit.d.ts.map +1 -1
- package/dist/memory/audit.js +0 -1
- package/dist/memory/audit.js.map +1 -1
- package/dist/memory/curator.d.ts.map +1 -1
- package/dist/memory/curator.js +172 -96
- package/dist/memory/curator.js.map +1 -1
- package/dist/memory/entries.d.ts +12 -2
- package/dist/memory/entries.d.ts.map +1 -1
- package/dist/memory/entries.js +198 -114
- package/dist/memory/entries.js.map +1 -1
- package/dist/memory/federated-retrieval.d.ts +12 -1
- package/dist/memory/federated-retrieval.d.ts.map +1 -1
- package/dist/memory/federated-retrieval.js +183 -70
- package/dist/memory/federated-retrieval.js.map +1 -1
- package/dist/memory/hybrid-retrieval.d.ts +5 -11
- package/dist/memory/hybrid-retrieval.d.ts.map +1 -1
- package/dist/memory/hybrid-retrieval.js +78 -58
- package/dist/memory/hybrid-retrieval.js.map +1 -1
- package/dist/memory/lifecycle.d.ts.map +1 -1
- package/dist/memory/lifecycle.js +17 -13
- package/dist/memory/lifecycle.js.map +1 -1
- package/dist/memory/rebuild-search.d.ts +5 -0
- package/dist/memory/rebuild-search.d.ts.map +1 -1
- package/dist/memory/rebuild-search.js +101 -76
- package/dist/memory/rebuild-search.js.map +1 -1
- package/dist/memory/retrieval-query.d.ts +0 -5
- package/dist/memory/retrieval-query.d.ts.map +1 -1
- package/dist/memory/retrieval-query.js +0 -5
- package/dist/memory/retrieval-query.js.map +1 -1
- package/dist/memory/retrieval.d.ts.map +1 -1
- package/dist/memory/retrieval.js +25 -131
- package/dist/memory/retrieval.js.map +1 -1
- package/dist/memory/revisions.d.ts +76 -1
- package/dist/memory/revisions.d.ts.map +1 -1
- package/dist/memory/revisions.js +539 -35
- package/dist/memory/revisions.js.map +1 -1
- package/dist/memory/scoped-memory.d.ts +16 -1
- package/dist/memory/scoped-memory.d.ts.map +1 -1
- package/dist/memory/scoped-memory.js +240 -107
- package/dist/memory/scoped-memory.js.map +1 -1
- package/dist/memory/secrets.d.ts +2 -0
- package/dist/memory/secrets.d.ts.map +1 -1
- package/dist/memory/secrets.js +36 -0
- package/dist/memory/secrets.js.map +1 -1
- package/dist/memory/structured-memory.d.ts +3 -2
- package/dist/memory/structured-memory.d.ts.map +1 -1
- package/dist/memory/structured-memory.js +54 -22
- package/dist/memory/structured-memory.js.map +1 -1
- package/dist/memory/workspaces.d.ts +9 -0
- package/dist/memory/workspaces.d.ts.map +1 -1
- package/dist/memory/workspaces.js +67 -0
- package/dist/memory/workspaces.js.map +1 -1
- package/dist/package-version.d.ts +2 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +7 -0
- package/dist/package-version.js.map +1 -0
- package/dist/repository/binding.d.ts +5 -0
- package/dist/repository/binding.d.ts.map +1 -1
- package/dist/repository/binding.js +117 -17
- package/dist/repository/binding.js.map +1 -1
- package/dist/repository/framework-version.d.ts +8 -0
- package/dist/repository/framework-version.d.ts.map +1 -0
- package/dist/repository/framework-version.js +29 -0
- package/dist/repository/framework-version.js.map +1 -0
- package/dist/repository/identity-value.d.ts +4 -0
- package/dist/repository/identity-value.d.ts.map +1 -0
- package/dist/repository/identity-value.js +24 -0
- package/dist/repository/identity-value.js.map +1 -0
- package/dist/repository/identity.d.ts.map +1 -1
- package/dist/repository/identity.js +2 -0
- package/dist/repository/identity.js.map +1 -1
- package/dist/repository/project-fingerprint.d.ts +14 -2
- package/dist/repository/project-fingerprint.d.ts.map +1 -1
- package/dist/repository/project-fingerprint.js +312 -33
- package/dist/repository/project-fingerprint.js.map +1 -1
- package/dist/repository/remote-url.d.ts +0 -1
- package/dist/repository/remote-url.d.ts.map +1 -1
- package/dist/repository/remote-url.js +0 -1
- package/dist/repository/remote-url.js.map +1 -1
- package/dist/runtime-version.d.ts +2 -1
- package/dist/runtime-version.d.ts.map +1 -1
- package/dist/runtime-version.js +21 -4
- package/dist/runtime-version.js.map +1 -1
- package/dist/serialization/boundary-json.d.ts +20 -0
- package/dist/serialization/boundary-json.d.ts.map +1 -0
- package/dist/serialization/boundary-json.js +129 -0
- package/dist/serialization/boundary-json.js.map +1 -0
- package/dist/serialization/envelope.js +1 -1
- package/dist/serialization/envelope.js.map +1 -1
- package/dist/serialization/validate.d.ts +7 -0
- package/dist/serialization/validate.d.ts.map +1 -1
- package/dist/serialization/validate.js +53 -13
- package/dist/serialization/validate.js.map +1 -1
- package/dist/server/app.js +1 -1
- package/dist/server/app.js.map +1 -1
- package/dist/server/http.d.ts +4 -3
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +90 -50
- package/dist/server/http.js.map +1 -1
- package/dist/server/idempotency.d.ts +0 -6
- package/dist/server/idempotency.d.ts.map +1 -1
- package/dist/server/idempotency.js.map +1 -1
- package/dist/server/router.d.ts.map +1 -1
- package/dist/server/router.js +21 -4
- package/dist/server/router.js.map +1 -1
- package/dist/server/routes/agent-capability-gate.d.ts +40 -0
- package/dist/server/routes/agent-capability-gate.d.ts.map +1 -0
- package/dist/server/routes/agent-capability-gate.js +182 -0
- package/dist/server/routes/agent-capability-gate.js.map +1 -0
- package/dist/server/routes/agent-events.d.ts.map +1 -1
- package/dist/server/routes/agent-events.js +8 -1
- package/dist/server/routes/agent-events.js.map +1 -1
- package/dist/server/routes/agent-intake.d.ts.map +1 -1
- package/dist/server/routes/agent-intake.js +10 -2
- package/dist/server/routes/agent-intake.js.map +1 -1
- package/dist/server/routes/agent-promotions.d.ts.map +1 -1
- package/dist/server/routes/agent-promotions.js +5 -2
- package/dist/server/routes/agent-promotions.js.map +1 -1
- package/dist/server/routes/agent-runs.d.ts.map +1 -1
- package/dist/server/routes/agent-runs.js +18 -4
- package/dist/server/routes/agent-runs.js.map +1 -1
- package/dist/server/routes/request-binding.d.ts +5 -0
- package/dist/server/routes/request-binding.d.ts.map +1 -0
- package/dist/server/routes/request-binding.js +49 -0
- package/dist/server/routes/request-binding.js.map +1 -0
- package/dist/server/routes/task5-support.d.ts +1 -8
- package/dist/server/routes/task5-support.d.ts.map +1 -1
- package/dist/server/routes/task5-support.js +1 -17
- package/dist/server/routes/task5-support.js.map +1 -1
- package/dist/server/routes/task5.d.ts.map +1 -1
- package/dist/server/routes/task5.js +106 -36
- package/dist/server/routes/task5.js.map +1 -1
- package/dist/server/write-queue.js +2 -2
- package/dist/server/write-queue.js.map +1 -1
- package/dist/setup/claude-config.d.ts +2 -1
- package/dist/setup/claude-config.d.ts.map +1 -1
- package/dist/setup/claude-config.js +54 -4
- package/dist/setup/claude-config.js.map +1 -1
- package/dist/setup/client-detection.d.ts +7 -2
- package/dist/setup/client-detection.d.ts.map +1 -1
- package/dist/setup/client-detection.js +27 -9
- package/dist/setup/client-detection.js.map +1 -1
- package/dist/setup/hermes-config.d.ts +2 -1
- package/dist/setup/hermes-config.d.ts.map +1 -1
- package/dist/setup/hermes-config.js +53 -12
- package/dist/setup/hermes-config.js.map +1 -1
- package/dist/setup/legacy-client-cleanup.d.ts +6 -0
- package/dist/setup/legacy-client-cleanup.d.ts.map +1 -0
- package/dist/setup/legacy-client-cleanup.js +152 -0
- package/dist/setup/legacy-client-cleanup.js.map +1 -0
- package/dist/setup/managed-text.d.ts.map +1 -1
- package/dist/setup/managed-text.js +11 -1
- package/dist/setup/managed-text.js.map +1 -1
- package/dist/setup/opencode-config.d.ts +2 -1
- package/dist/setup/opencode-config.d.ts.map +1 -1
- package/dist/setup/opencode-config.js +57 -5
- package/dist/setup/opencode-config.js.map +1 -1
- package/dist/setup/render.d.ts +2 -1
- package/dist/setup/render.d.ts.map +1 -1
- package/dist/setup/render.js +91 -17
- package/dist/setup/render.js.map +1 -1
- package/dist/setup/standard-skills.d.ts +16 -2
- package/dist/setup/standard-skills.d.ts.map +1 -1
- package/dist/setup/standard-skills.js +37 -13
- package/dist/setup/standard-skills.js.map +1 -1
- package/dist/setup/strict-json.d.ts +7 -0
- package/dist/setup/strict-json.d.ts.map +1 -0
- package/dist/setup/strict-json.js +98 -0
- package/dist/setup/strict-json.js.map +1 -0
- package/dist/setup/strict-toml.d.ts +7 -0
- package/dist/setup/strict-toml.d.ts.map +1 -0
- package/dist/setup/strict-toml.js +470 -0
- package/dist/setup/strict-toml.js.map +1 -0
- package/dist/skills/cache.d.ts +15 -0
- package/dist/skills/cache.d.ts.map +1 -0
- package/dist/skills/cache.js +19 -0
- package/dist/skills/cache.js.map +1 -0
- package/dist/skills/candidate-ranking.d.ts +15 -0
- package/dist/skills/candidate-ranking.d.ts.map +1 -0
- package/dist/skills/candidate-ranking.js +52 -0
- package/dist/skills/candidate-ranking.js.map +1 -0
- package/dist/skills/chunking.d.ts +11 -0
- package/dist/skills/chunking.d.ts.map +1 -0
- package/dist/skills/chunking.js +291 -0
- package/dist/skills/chunking.js.map +1 -0
- package/dist/skills/config.d.ts +19 -0
- package/dist/skills/config.d.ts.map +1 -0
- package/dist/skills/config.js +103 -0
- package/dist/skills/config.js.map +1 -0
- package/dist/skills/discovery-service.d.ts +26 -0
- package/dist/skills/discovery-service.d.ts.map +1 -0
- package/dist/skills/discovery-service.js +921 -0
- package/dist/skills/discovery-service.js.map +1 -0
- package/dist/skills/external-transport.d.ts +2 -0
- package/dist/skills/external-transport.d.ts.map +1 -0
- package/dist/skills/external-transport.js +30 -0
- package/dist/skills/external-transport.js.map +1 -0
- package/dist/skills/find.d.ts +16 -0
- package/dist/skills/find.d.ts.map +1 -0
- package/dist/skills/find.js +58 -0
- package/dist/skills/find.js.map +1 -0
- package/dist/skills/gap-detection.d.ts +23 -0
- package/dist/skills/gap-detection.d.ts.map +1 -0
- package/dist/skills/gap-detection.js +62 -0
- package/dist/skills/gap-detection.js.map +1 -0
- package/dist/skills/import-preparation.d.ts +4 -0
- package/dist/skills/import-preparation.d.ts.map +1 -0
- package/dist/skills/import-preparation.js +20 -0
- package/dist/skills/import-preparation.js.map +1 -0
- package/dist/skills/materialization-authority.d.ts +11 -0
- package/dist/skills/materialization-authority.d.ts.map +1 -0
- package/dist/skills/materialization-authority.js +92 -0
- package/dist/skills/materialization-authority.js.map +1 -0
- package/dist/skills/materialization-service.d.ts +22 -0
- package/dist/skills/materialization-service.d.ts.map +1 -0
- package/dist/skills/materialization-service.js +101 -0
- package/dist/skills/materialization-service.js.map +1 -0
- package/dist/skills/official-catalog.d.ts +31 -0
- package/dist/skills/official-catalog.d.ts.map +1 -0
- package/dist/skills/official-catalog.js +110 -0
- package/dist/skills/official-catalog.js.map +1 -0
- package/dist/skills/providers/schema.d.ts +30 -0
- package/dist/skills/providers/schema.d.ts.map +1 -0
- package/dist/skills/providers/schema.js +354 -0
- package/dist/skills/providers/schema.js.map +1 -0
- package/dist/skills/providers/skills-sh-compat.d.ts +17 -0
- package/dist/skills/providers/skills-sh-compat.d.ts.map +1 -0
- package/dist/skills/providers/skills-sh-compat.js +71 -0
- package/dist/skills/providers/skills-sh-compat.js.map +1 -0
- package/dist/skills/providers/skills-sh-v1.d.ts +23 -0
- package/dist/skills/providers/skills-sh-v1.d.ts.map +1 -0
- package/dist/skills/providers/skills-sh-v1.js +134 -0
- package/dist/skills/providers/skills-sh-v1.js.map +1 -0
- package/dist/skills/query-builder.d.ts +18 -0
- package/dist/skills/query-builder.d.ts.map +1 -0
- package/dist/skills/query-builder.js +93 -0
- package/dist/skills/query-builder.js.map +1 -0
- package/dist/skills/source/errors.d.ts +9 -0
- package/dist/skills/source/errors.d.ts.map +1 -0
- package/dist/skills/source/errors.js +15 -0
- package/dist/skills/source/errors.js.map +1 -0
- package/dist/skills/source/frontmatter.d.ts +9 -0
- package/dist/skills/source/frontmatter.d.ts.map +1 -0
- package/dist/skills/source/frontmatter.js +77 -0
- package/dist/skills/source/frontmatter.js.map +1 -0
- package/dist/skills/source/github-fetcher.d.ts +20 -0
- package/dist/skills/source/github-fetcher.d.ts.map +1 -0
- package/dist/skills/source/github-fetcher.js +292 -0
- package/dist/skills/source/github-fetcher.js.map +1 -0
- package/dist/skills/source/snapshot-validator.d.ts +28 -0
- package/dist/skills/source/snapshot-validator.d.ts.map +1 -0
- package/dist/skills/source/snapshot-validator.js +186 -0
- package/dist/skills/source/snapshot-validator.js.map +1 -0
- package/dist/skills/store.d.ts +181 -0
- package/dist/skills/store.d.ts.map +1 -0
- package/dist/skills/store.js +1710 -0
- package/dist/skills/store.js.map +1 -0
- package/dist/skills/types.d.ts +169 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +2 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/web/i18n.d.ts +32 -0
- package/dist/web/i18n.d.ts.map +1 -1
- package/dist/web/i18n.js +128 -0
- package/dist/web/i18n.js.map +1 -1
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +386 -38
- package/dist/web/server.js.map +1 -1
- package/dist/web/ui.d.ts +4 -0
- package/dist/web/ui.d.ts.map +1 -1
- package/dist/web/ui.js +382 -6
- package/dist/web/ui.js.map +1 -1
- package/migrations/009_external_skill_discovery.sql +126 -0
- package/package.json +4 -3
- package/skills/kiokuko-single-purpose-functions/SKILL.md +288 -0
- package/skills/kiokuko-single-purpose-functions/references/kiokuko-patterns.md +344 -0
- package/skills/kiokuko-single-purpose-functions/references/review-checklist.md +97 -0
- package/skills/kiokuko-ui-design-soul/SKILL.md +1186 -28
- package/templates/AGENTS.md +8 -7
- package/dist/client/runtime-discovery.d.ts +0 -15
- package/dist/client/runtime-discovery.d.ts.map +0 -1
- package/dist/client/runtime-discovery.js +0 -45
- package/dist/client/runtime-discovery.js.map +0 -1
- package/dist/commands/call.d.ts +0 -7
- package/dist/commands/call.d.ts.map +0 -1
- package/dist/commands/call.js +0 -10
- package/dist/commands/call.js.map +0 -1
- package/dist/commands/link.d.ts +0 -6
- package/dist/commands/link.d.ts.map +0 -1
- package/dist/commands/link.js +0 -6
- package/dist/commands/link.js.map +0 -1
- package/dist/commands/promote.d.ts +0 -6
- package/dist/commands/promote.d.ts.map +0 -1
- package/dist/commands/promote.js +0 -6
- package/dist/commands/promote.js.map +0 -1
- package/dist/commands/read.d.ts +0 -6
- package/dist/commands/read.d.ts.map +0 -1
- package/dist/commands/read.js +0 -4
- package/dist/commands/read.js.map +0 -1
- package/dist/commands/recall.d.ts +0 -6
- package/dist/commands/recall.d.ts.map +0 -1
- package/dist/commands/recall.js +0 -4
- package/dist/commands/recall.js.map +0 -1
- package/dist/commands/record.d.ts +0 -6
- package/dist/commands/record.d.ts.map +0 -1
- package/dist/commands/record.js +0 -4
- package/dist/commands/record.js.map +0 -1
- package/dist/commands/search.d.ts +0 -6
- package/dist/commands/search.d.ts.map +0 -1
- package/dist/commands/search.js +0 -4
- package/dist/commands/search.js.map +0 -1
- package/dist/commands/supersede.d.ts +0 -6
- package/dist/commands/supersede.d.ts.map +0 -1
- package/dist/commands/supersede.js +0 -6
- package/dist/commands/supersede.js.map +0 -1
- package/dist/knowledge/sources.d.ts +0 -72
- package/dist/knowledge/sources.d.ts.map +0 -1
- package/dist/knowledge/sources.js +0 -388
- package/dist/knowledge/sources.js.map +0 -1
- package/dist/setup/opencode-evidence.d.ts +0 -37
- package/dist/setup/opencode-evidence.d.ts.map +0 -1
- package/dist/setup/opencode-evidence.js +0 -58
- package/dist/setup/opencode-evidence.js.map +0 -1
- package/dist/setup/opencode-loop-guard.d.ts +0 -9
- package/dist/setup/opencode-loop-guard.d.ts.map +0 -1
- package/dist/setup/opencode-loop-guard.js +0 -248
- package/dist/setup/opencode-loop-guard.js.map +0 -1
|
@@ -1,36 +1,1012 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constants } from 'node:fs';
|
|
2
|
+
import { chmod, link, lstat, mkdir, open, readdir, rename, unlink, } from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { TextDecoder } from 'node:util';
|
|
4
6
|
import { KiokukoError } from '../errors.js';
|
|
5
|
-
export
|
|
7
|
+
export class AtomicCleanupFailure extends Error {
|
|
8
|
+
artifactPath;
|
|
9
|
+
constructor(
|
|
10
|
+
/** Exact pathname of the owned artifact, when its full binding was proven after the failure. */
|
|
11
|
+
artifactPath, options) {
|
|
12
|
+
super('Atomic artifact cleanup failed or could not be proven complete', options);
|
|
13
|
+
this.artifactPath = artifactPath;
|
|
14
|
+
this.name = 'AtomicCleanupFailure';
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class AtomicCommittedMutationError extends AggregateError {
|
|
18
|
+
outcome;
|
|
19
|
+
operationError;
|
|
20
|
+
constructor(outcome, operationError) {
|
|
21
|
+
super([operationError, ...outcome.cleanupFailures], 'File target committed before post-install validation failed');
|
|
22
|
+
this.outcome = outcome;
|
|
23
|
+
this.operationError = operationError;
|
|
24
|
+
this.name = 'AtomicCommittedMutationError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class AtomicCommittedUnlinkError extends AggregateError {
|
|
28
|
+
outcome;
|
|
29
|
+
operationError;
|
|
30
|
+
constructor(outcome, operationError) {
|
|
31
|
+
super([operationError, ...outcome.cleanupFailures], 'File target was quarantined before post-unlink validation failed');
|
|
32
|
+
this.outcome = outcome;
|
|
33
|
+
this.operationError = operationError;
|
|
34
|
+
this.name = 'AtomicCommittedUnlinkError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function assertAtomicCleanupComplete(result) {
|
|
38
|
+
if (result.cleanupFailures.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
throw new AggregateError(result.cleanupFailures, 'File mutation committed, but committed-artifact cleanup failed');
|
|
41
|
+
}
|
|
42
|
+
function errno(error) {
|
|
43
|
+
return error instanceof Error && 'code' in error
|
|
44
|
+
? String(error.code)
|
|
45
|
+
: undefined;
|
|
46
|
+
}
|
|
47
|
+
function isMissingFile(error) {
|
|
48
|
+
return errno(error) === 'ENOENT';
|
|
49
|
+
}
|
|
50
|
+
function isSymlinkFailure(error) {
|
|
51
|
+
return ['ELOOP', 'EMLINK'].includes(errno(error) ?? '');
|
|
52
|
+
}
|
|
53
|
+
function isAlreadyExists(error) {
|
|
54
|
+
return errno(error) === 'EEXIST';
|
|
55
|
+
}
|
|
56
|
+
function changedAfterPlanning(filePath) {
|
|
57
|
+
return new KiokukoError('CONFLICT', 'Setup target changed after planning', filePath === undefined ? {} : { target: filePath });
|
|
58
|
+
}
|
|
59
|
+
function identityFromStat(info) {
|
|
60
|
+
const device = BigInt(info.dev);
|
|
61
|
+
const inode = BigInt(info.ino);
|
|
62
|
+
if (inode === 0n) {
|
|
63
|
+
throw new KiokukoError('SECURITY_REJECTION', 'The filesystem does not expose a stable file identity required for safe mutation');
|
|
64
|
+
}
|
|
65
|
+
return { device, inode };
|
|
66
|
+
}
|
|
67
|
+
function sameIdentity(left, right) {
|
|
68
|
+
return left.device === right.device && left.inode === right.inode;
|
|
69
|
+
}
|
|
70
|
+
function sameSnapshot(left, right) {
|
|
71
|
+
if (left === undefined || right === undefined)
|
|
72
|
+
return left === right;
|
|
73
|
+
return sameIdentity(left.identity, right.identity)
|
|
74
|
+
&& left.content === right.content
|
|
75
|
+
&& left.mode === right.mode;
|
|
76
|
+
}
|
|
77
|
+
function artifactFromOpenHandle(info, content) {
|
|
78
|
+
if (!info.isFile()) {
|
|
79
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Atomic artifact is not a regular file');
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
snapshot: {
|
|
83
|
+
content,
|
|
84
|
+
mode: Number(info.mode) & 0o777,
|
|
85
|
+
identity: identityFromStat(info),
|
|
86
|
+
},
|
|
87
|
+
linkCount: BigInt(info.nlink),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
async function readOpenHandleArtifact(handle, filePath) {
|
|
91
|
+
const before = await handle.stat();
|
|
92
|
+
if (!before.isFile() || !Number.isSafeInteger(before.size) || before.size < 0) {
|
|
93
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Atomic artifact cannot be read safely');
|
|
94
|
+
}
|
|
95
|
+
const buffer = Buffer.alloc(before.size);
|
|
96
|
+
let offset = 0;
|
|
97
|
+
while (offset < buffer.length) {
|
|
98
|
+
const result = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
99
|
+
if (result.bytesRead === 0) {
|
|
100
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Atomic artifact changed while it was read');
|
|
101
|
+
}
|
|
102
|
+
offset += result.bytesRead;
|
|
103
|
+
}
|
|
104
|
+
const after = await handle.stat();
|
|
105
|
+
const initial = artifactFromOpenHandle(before, decodeUtf8(buffer, filePath));
|
|
106
|
+
const final = artifactFromOpenHandle(after, initial.snapshot.content);
|
|
107
|
+
if (!sameArtifact(initial, final) || before.size !== after.size) {
|
|
108
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Atomic artifact changed while it was read');
|
|
109
|
+
}
|
|
110
|
+
return final;
|
|
111
|
+
}
|
|
112
|
+
function sameArtifact(left, right) {
|
|
113
|
+
return left !== undefined
|
|
114
|
+
&& sameSnapshot(left.snapshot, right.snapshot)
|
|
115
|
+
&& left.linkCount === right.linkCount;
|
|
116
|
+
}
|
|
117
|
+
function withLinkCount(artifact, linkCount) {
|
|
118
|
+
return { snapshot: artifact.snapshot, linkCount };
|
|
119
|
+
}
|
|
120
|
+
function requireArtifact(current, expected, message) {
|
|
121
|
+
if (current === undefined || !sameArtifact(current, expected)) {
|
|
122
|
+
throw new KiokukoError('INTEGRITY_ERROR', message);
|
|
123
|
+
}
|
|
124
|
+
return current;
|
|
125
|
+
}
|
|
126
|
+
function ensureContained(root, filePath) {
|
|
127
|
+
const resolvedRoot = path.resolve(root);
|
|
128
|
+
const target = path.resolve(filePath);
|
|
129
|
+
if (target === resolvedRoot || !target.startsWith(`${resolvedRoot}${path.sep}`)) {
|
|
130
|
+
throw new KiokukoError('VALIDATION_ERROR', 'Managed file must remain inside its repository root');
|
|
131
|
+
}
|
|
132
|
+
return { root: resolvedRoot, target };
|
|
133
|
+
}
|
|
134
|
+
async function rejectLinkedComponents(root, filePath) {
|
|
135
|
+
const contained = ensureContained(root, filePath);
|
|
136
|
+
const relative = path.relative(contained.root, contained.target);
|
|
137
|
+
let current = contained.root;
|
|
138
|
+
for (const component of relative.split(path.sep)) {
|
|
139
|
+
current = path.join(current, component);
|
|
140
|
+
try {
|
|
141
|
+
const info = await lstat(current);
|
|
142
|
+
if (info.isSymbolicLink()) {
|
|
143
|
+
throw new KiokukoError('SECURITY_REJECTION', 'Refusing to traverse a symbolic link');
|
|
144
|
+
}
|
|
145
|
+
if (current !== contained.target && !info.isDirectory()) {
|
|
146
|
+
throw new KiokukoError('VALIDATION_ERROR', `Expected a directory: ${current}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (isMissingFile(error))
|
|
151
|
+
return;
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function decodeUtf8(buffer, filePath) {
|
|
157
|
+
try {
|
|
158
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
throw new KiokukoError('VALIDATION_ERROR', `Managed text file is not valid UTF-8: ${filePath}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
export async function readRegularFile(filePath, dependencies = {}) {
|
|
165
|
+
if (dependencies.containmentRoot !== undefined) {
|
|
166
|
+
await rejectLinkedComponents(dependencies.containmentRoot, filePath);
|
|
167
|
+
}
|
|
168
|
+
let plannedIdentity;
|
|
6
169
|
try {
|
|
7
|
-
const
|
|
8
|
-
if (
|
|
9
|
-
throw new KiokukoError('SECURITY_REJECTION',
|
|
170
|
+
const planned = await lstat(filePath, { bigint: true });
|
|
171
|
+
if (planned.isSymbolicLink()) {
|
|
172
|
+
throw new KiokukoError('SECURITY_REJECTION', 'Refusing to follow a symbolic link');
|
|
173
|
+
}
|
|
174
|
+
if (!planned.isFile())
|
|
175
|
+
throw new KiokukoError('VALIDATION_ERROR', `Expected a regular file: ${filePath}`);
|
|
176
|
+
plannedIdentity = { device: planned.dev, inode: planned.ino };
|
|
177
|
+
if (plannedIdentity.inode === 0n) {
|
|
178
|
+
throw new KiokukoError('SECURITY_REJECTION', 'The filesystem does not expose a stable file identity required for safe mutation');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
if (isMissingFile(error))
|
|
183
|
+
return undefined;
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
let handle;
|
|
187
|
+
const platform = dependencies.platform ?? process.platform;
|
|
188
|
+
const flags = platform === 'win32'
|
|
189
|
+
? constants.O_RDONLY
|
|
190
|
+
: constants.O_RDONLY | constants.O_NOFOLLOW;
|
|
191
|
+
try {
|
|
192
|
+
handle = await open(filePath, flags);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
if (isMissingFile(error))
|
|
196
|
+
throw changedAfterPlanning();
|
|
197
|
+
if (isSymlinkFailure(error))
|
|
198
|
+
throw new KiokukoError('SECURITY_REJECTION', 'Refusing to follow a symbolic link');
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
let operationResult;
|
|
202
|
+
let operationFailed = false;
|
|
203
|
+
let operationError;
|
|
204
|
+
try {
|
|
205
|
+
await dependencies.afterOpen?.(filePath, handle);
|
|
206
|
+
const info = await handle.stat();
|
|
10
207
|
if (!info.isFile())
|
|
11
208
|
throw new KiokukoError('VALIDATION_ERROR', `Expected a regular file: ${filePath}`);
|
|
12
|
-
|
|
209
|
+
const identity = identityFromStat(info);
|
|
210
|
+
if (!sameIdentity(identity, plannedIdentity))
|
|
211
|
+
throw changedAfterPlanning();
|
|
212
|
+
const content = decodeUtf8(await handle.readFile(), filePath);
|
|
213
|
+
operationResult = { value: { content, mode: info.mode & 0o777, identity } };
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
operationFailed = true;
|
|
217
|
+
operationError = error;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
await (dependencies.closeHandle ?? ((openHandle) => openHandle.close()))(handle);
|
|
221
|
+
}
|
|
222
|
+
catch (closeError) {
|
|
223
|
+
if (operationFailed) {
|
|
224
|
+
throw new AggregateError([operationError, closeError], 'Regular-file read failed and its file descriptor could not be closed');
|
|
225
|
+
}
|
|
226
|
+
throw closeError;
|
|
227
|
+
}
|
|
228
|
+
if (operationFailed)
|
|
229
|
+
throw operationError;
|
|
230
|
+
if (operationResult === undefined) {
|
|
231
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Regular-file read produced no result');
|
|
232
|
+
}
|
|
233
|
+
return operationResult.value;
|
|
234
|
+
}
|
|
235
|
+
async function readBoundArtifact(filePath, containmentRoot) {
|
|
236
|
+
const snapshot = await readRegularFile(filePath, {
|
|
237
|
+
...(containmentRoot === undefined ? {} : { containmentRoot }),
|
|
238
|
+
});
|
|
239
|
+
if (snapshot === undefined)
|
|
240
|
+
return undefined;
|
|
241
|
+
let current;
|
|
242
|
+
try {
|
|
243
|
+
current = await lstat(filePath, { bigint: true });
|
|
13
244
|
}
|
|
14
245
|
catch (error) {
|
|
15
|
-
if (error
|
|
246
|
+
if (isMissingFile(error))
|
|
16
247
|
return undefined;
|
|
17
248
|
throw error;
|
|
18
249
|
}
|
|
250
|
+
if (current.isSymbolicLink()) {
|
|
251
|
+
throw new KiokukoError('SECURITY_REJECTION', 'Refusing to follow a symbolic link');
|
|
252
|
+
}
|
|
253
|
+
if (!current.isFile()) {
|
|
254
|
+
throw new KiokukoError('VALIDATION_ERROR', `Expected a regular file: ${filePath}`);
|
|
255
|
+
}
|
|
256
|
+
const identity = { device: current.dev, inode: current.ino };
|
|
257
|
+
if (identity.inode === 0n) {
|
|
258
|
+
throw new KiokukoError('SECURITY_REJECTION', 'The filesystem does not expose a stable file identity required for safe mutation');
|
|
259
|
+
}
|
|
260
|
+
if (!sameIdentity(identity, snapshot.identity) || Number(current.mode & 511n) !== snapshot.mode) {
|
|
261
|
+
throw changedAfterPlanning(filePath);
|
|
262
|
+
}
|
|
263
|
+
return { snapshot, linkCount: current.nlink };
|
|
264
|
+
}
|
|
265
|
+
/** Read an exact directory identity without following its final path component. */
|
|
266
|
+
export async function readDirectoryIdentity(directoryPath) {
|
|
267
|
+
let info;
|
|
268
|
+
try {
|
|
269
|
+
info = await lstat(directoryPath, { bigint: true });
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (isMissingFile(error))
|
|
273
|
+
return undefined;
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
if (info.isSymbolicLink()) {
|
|
277
|
+
throw new KiokukoError('SECURITY_REJECTION', 'Refusing to traverse a symbolic link');
|
|
278
|
+
}
|
|
279
|
+
if (!info.isDirectory()) {
|
|
280
|
+
throw new KiokukoError('VALIDATION_ERROR', `Expected a directory: ${directoryPath}`);
|
|
281
|
+
}
|
|
282
|
+
if (info.ino === 0n) {
|
|
283
|
+
throw new KiokukoError('SECURITY_REJECTION', 'The filesystem does not expose a stable directory identity required for safe mutation');
|
|
284
|
+
}
|
|
285
|
+
return { device: info.dev, inode: info.ino };
|
|
286
|
+
}
|
|
287
|
+
const ATOMIC_QUARANTINE_NAME = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.(deleted|previous|rollback)$/iu;
|
|
288
|
+
const ATOMIC_QUARANTINE_CLEANUP_NAME = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.(deleted|previous|rollback)\.[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\.cleanup$/iu;
|
|
289
|
+
/**
|
|
290
|
+
* Bind a native atomic-write quarantine for this exact basename without
|
|
291
|
+
* following path components. Its snapshot may constrain convergence, but it
|
|
292
|
+
* never grants the caller ownership of the artifact.
|
|
293
|
+
*/
|
|
294
|
+
export async function readPendingAtomicMutation(filePath, expectedParentDirectory, containmentRoot) {
|
|
295
|
+
if (containmentRoot !== undefined) {
|
|
296
|
+
await rejectLinkedComponents(containmentRoot, filePath);
|
|
297
|
+
}
|
|
298
|
+
const directory = path.dirname(filePath);
|
|
299
|
+
const parent = await bindMutationDirectory(directory, expectedParentDirectory);
|
|
300
|
+
const basename = path.basename(filePath);
|
|
301
|
+
const activePrefix = `.${basename}.`;
|
|
302
|
+
const cleanupPrefix = `..${basename}.`;
|
|
303
|
+
const names = await readdir(directory);
|
|
304
|
+
await requireBoundDirectory(parent);
|
|
305
|
+
if (containmentRoot !== undefined) {
|
|
306
|
+
await rejectLinkedComponents(containmentRoot, filePath);
|
|
307
|
+
}
|
|
308
|
+
const matches = names.flatMap((name) => {
|
|
309
|
+
const match = name.startsWith(activePrefix)
|
|
310
|
+
? ATOMIC_QUARANTINE_NAME.exec(name.slice(activePrefix.length))
|
|
311
|
+
: name.startsWith(cleanupPrefix)
|
|
312
|
+
? ATOMIC_QUARANTINE_CLEANUP_NAME.exec(name.slice(cleanupPrefix.length))
|
|
313
|
+
: null;
|
|
314
|
+
if (match === null)
|
|
315
|
+
return [];
|
|
316
|
+
const kind = match[1];
|
|
317
|
+
if (kind !== 'deleted' && kind !== 'previous' && kind !== 'rollback')
|
|
318
|
+
return [];
|
|
319
|
+
return [{ name, kind }];
|
|
320
|
+
});
|
|
321
|
+
if (matches.length === 0)
|
|
322
|
+
return undefined;
|
|
323
|
+
if (matches.length !== 1) {
|
|
324
|
+
throw new KiokukoError('CONFLICT', 'Multiple atomic quarantines exist for one target', { target: filePath });
|
|
325
|
+
}
|
|
326
|
+
const match = matches[0];
|
|
327
|
+
if (match === undefined)
|
|
328
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Atomic quarantine match vanished');
|
|
329
|
+
const artifactPath = path.join(directory, match.name);
|
|
330
|
+
const artifact = await readArtifactInBoundDirectory(artifactPath, parent, containmentRoot);
|
|
331
|
+
if (artifact === undefined)
|
|
332
|
+
throw changedAfterPlanning(artifactPath);
|
|
333
|
+
if (artifact.linkCount !== 1n) {
|
|
334
|
+
throw new KiokukoError('SECURITY_REJECTION', 'Atomic quarantine must have exactly one link', { target: artifactPath });
|
|
335
|
+
}
|
|
336
|
+
await requireBoundDirectory(parent);
|
|
337
|
+
return { kind: match.kind, snapshot: artifact.snapshot };
|
|
338
|
+
}
|
|
339
|
+
async function readBoundDirectory(directoryPath) {
|
|
340
|
+
const identity = await readDirectoryIdentity(directoryPath);
|
|
341
|
+
if (identity === undefined)
|
|
342
|
+
throw changedAfterPlanning(directoryPath);
|
|
343
|
+
return {
|
|
344
|
+
directoryPath,
|
|
345
|
+
identity,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
async function requireBoundDirectory(binding) {
|
|
349
|
+
const current = await readBoundDirectory(binding.directoryPath);
|
|
350
|
+
if (!sameIdentity(current.identity, binding.identity)) {
|
|
351
|
+
throw changedAfterPlanning(binding.directoryPath);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
async function readArtifactInBoundDirectory(filePath, parent, containmentRoot) {
|
|
355
|
+
await requireBoundDirectory(parent);
|
|
356
|
+
const artifact = await readBoundArtifact(filePath, containmentRoot);
|
|
357
|
+
await requireBoundDirectory(parent);
|
|
358
|
+
return artifact;
|
|
359
|
+
}
|
|
360
|
+
async function requireAbsentInBoundDirectory(filePath, parent, message, containmentRoot) {
|
|
361
|
+
if (await readArtifactInBoundDirectory(filePath, parent, containmentRoot) !== undefined) {
|
|
362
|
+
throw new KiokukoError('INTEGRITY_ERROR', message, { target: filePath });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function bindMutationDirectory(directoryPath, expected) {
|
|
366
|
+
const binding = await readBoundDirectory(directoryPath);
|
|
367
|
+
if (expected !== undefined && !sameIdentity(binding.identity, expected)) {
|
|
368
|
+
throw changedAfterPlanning(directoryPath);
|
|
369
|
+
}
|
|
370
|
+
return binding;
|
|
371
|
+
}
|
|
372
|
+
export async function assertFileExpectation(filePath, expectation) {
|
|
373
|
+
const parent = expectation.expectedParentDirectory === undefined
|
|
374
|
+
? undefined
|
|
375
|
+
: await bindMutationDirectory(path.dirname(filePath), expectation.expectedParentDirectory);
|
|
376
|
+
const current = await readRegularFile(filePath, {
|
|
377
|
+
...(expectation.containmentRoot === undefined ? {} : { containmentRoot: expectation.containmentRoot }),
|
|
378
|
+
});
|
|
379
|
+
if (!sameSnapshot(current, expectation.expected))
|
|
380
|
+
throw changedAfterPlanning(filePath);
|
|
381
|
+
if (parent !== undefined)
|
|
382
|
+
await requireBoundDirectory(parent);
|
|
383
|
+
for (const absentPath of expectation.mustRemainAbsent ?? []) {
|
|
384
|
+
const alternate = await readRegularFile(absentPath, {
|
|
385
|
+
...(expectation.containmentRoot === undefined ? {} : { containmentRoot: expectation.containmentRoot }),
|
|
386
|
+
});
|
|
387
|
+
if (alternate !== undefined)
|
|
388
|
+
throw changedAfterPlanning(absentPath);
|
|
389
|
+
if (parent !== undefined)
|
|
390
|
+
await requireBoundDirectory(parent);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async function cleanupBoundArtifact(beforeCleanup, filePath, expected, parent) {
|
|
394
|
+
if (expected === undefined) {
|
|
395
|
+
return new AtomicCleanupFailure(undefined, {
|
|
396
|
+
cause: new KiokukoError('INTEGRITY_ERROR', 'Refusing to remove an atomic artifact whose exact identity was not bound'),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const disposalPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.cleanup`);
|
|
400
|
+
try {
|
|
401
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent), expected, 'Refusing to remove a changed atomic artifact');
|
|
402
|
+
await requireAbsentInBoundDirectory(disposalPath, parent, 'Atomic cleanup quarantine already exists');
|
|
403
|
+
await rename(filePath, disposalPath);
|
|
404
|
+
requireArtifact(await readArtifactInBoundDirectory(disposalPath, parent), expected, 'Atomic cleanup quarantine does not match the bound artifact');
|
|
405
|
+
await requireAbsentInBoundDirectory(filePath, parent, 'Atomic cleanup source pathname was replaced');
|
|
406
|
+
await beforeCleanup?.(disposalPath);
|
|
407
|
+
requireArtifact(await readArtifactInBoundDirectory(disposalPath, parent), expected, 'Atomic cleanup quarantine changed during the cleanup hook');
|
|
408
|
+
await requireAbsentInBoundDirectory(filePath, parent, 'Atomic cleanup hook recreated the source pathname');
|
|
409
|
+
await unlink(disposalPath);
|
|
410
|
+
await requireAbsentInBoundDirectory(disposalPath, parent, 'Atomic cleanup quarantine was not removed');
|
|
411
|
+
return undefined;
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
return bindOwnedCleanupFailure(error, [
|
|
415
|
+
{ artifactPath: disposalPath, expected },
|
|
416
|
+
{ artifactPath: filePath, expected },
|
|
417
|
+
], parent);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Report a recovery pathname only after rebinding the exact inode, bytes, and
|
|
422
|
+
* mode. A changed link count is itself a cleanup failure, but it does not make
|
|
423
|
+
* the pathname's inode ownership ambiguous.
|
|
424
|
+
*/
|
|
425
|
+
async function bindOwnedCleanupFailure(cause, candidates, parent) {
|
|
426
|
+
const observationErrors = [];
|
|
427
|
+
for (const candidate of candidates) {
|
|
428
|
+
try {
|
|
429
|
+
await requireBoundDirectory(parent);
|
|
430
|
+
const current = await readBoundArtifact(candidate.artifactPath);
|
|
431
|
+
await requireBoundDirectory(parent);
|
|
432
|
+
if (current !== undefined
|
|
433
|
+
&& sameSnapshot(current.snapshot, candidate.expected.snapshot)) {
|
|
434
|
+
return new AtomicCleanupFailure(candidate.artifactPath, { cause });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch (observationError) {
|
|
438
|
+
observationErrors.push(observationError);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return new AtomicCleanupFailure(undefined, {
|
|
442
|
+
cause: observationErrors.length === 0
|
|
443
|
+
? cause
|
|
444
|
+
: new AggregateError([cause, ...observationErrors], 'Atomic cleanup failed and no owned artifact pathname could be proven'),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
async function cleanupBeforeCommit(beforeCleanup, filePath, operationError, expected, parent) {
|
|
448
|
+
const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, filePath, expected, parent);
|
|
449
|
+
if (cleanupFailure !== undefined) {
|
|
450
|
+
throw new AggregateError([operationError, cleanupFailure], 'Atomic write failed and temporary-file cleanup also failed');
|
|
451
|
+
}
|
|
452
|
+
if (operationError instanceof KiokukoError || operationError instanceof AggregateError)
|
|
453
|
+
throw operationError;
|
|
454
|
+
throw new KiokukoError('PARTIAL_FAILURE', `Unable to atomically write ${filePath}`, {
|
|
455
|
+
cause: operationError instanceof Error ? operationError.message : String(operationError),
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
async function restoreQuarantinedTarget(quarantinePath, filePath, beforeCleanup, quarantined, parent) {
|
|
459
|
+
try {
|
|
460
|
+
requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent), quarantined, 'Quarantined target changed before rollback');
|
|
461
|
+
await requireAbsentInBoundDirectory(filePath, parent, 'Rollback target pathname is no longer absent');
|
|
462
|
+
await link(quarantinePath, filePath);
|
|
463
|
+
const linked = withLinkCount(quarantined, quarantined.linkCount + 1n);
|
|
464
|
+
requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent), linked, 'Quarantined target changed while rollback was installed');
|
|
465
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent), linked, 'Restored target does not match the bound quarantine');
|
|
466
|
+
const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, quarantinePath, linked, parent);
|
|
467
|
+
if (cleanupFailure !== undefined)
|
|
468
|
+
return [cleanupFailure];
|
|
469
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent), quarantined, 'Restored target changed during quarantine cleanup');
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
catch (error) {
|
|
473
|
+
return [await bindOwnedCleanupFailure(error, [
|
|
474
|
+
{ artifactPath: quarantinePath, expected: quarantined },
|
|
475
|
+
{ artifactPath: quarantinePath, expected: withLinkCount(quarantined, quarantined.linkCount + 1n) },
|
|
476
|
+
], parent)];
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function throwWithArtifactFailures(error, failures, message) {
|
|
480
|
+
if (failures.length === 0)
|
|
481
|
+
throw error;
|
|
482
|
+
throw new AggregateError([error, ...failures], message);
|
|
483
|
+
}
|
|
484
|
+
async function throwAfterOriginalRestoreAttempt(operationError, failures, filePath, expected, parent, message) {
|
|
485
|
+
let current;
|
|
486
|
+
try {
|
|
487
|
+
await requireBoundDirectory(parent);
|
|
488
|
+
current = await readRegularFile(filePath);
|
|
489
|
+
await requireBoundDirectory(parent);
|
|
490
|
+
}
|
|
491
|
+
catch (validationError) {
|
|
492
|
+
throw new AtomicCommittedUnlinkError({
|
|
493
|
+
cleanupFailures: [
|
|
494
|
+
...failures,
|
|
495
|
+
new AtomicCleanupFailure(undefined, { cause: validationError }),
|
|
496
|
+
],
|
|
497
|
+
}, operationError);
|
|
498
|
+
}
|
|
499
|
+
if (sameSnapshot(current, expected)) {
|
|
500
|
+
if (failures.length === 0)
|
|
501
|
+
throw operationError;
|
|
502
|
+
throw new AggregateError([operationError, ...failures], message);
|
|
503
|
+
}
|
|
504
|
+
throw new AtomicCommittedUnlinkError({ cleanupFailures: failures }, operationError);
|
|
505
|
+
}
|
|
506
|
+
async function conditionalUnlinkInstalled(temporaryPath, filePath, expected, beforeCleanup, dependencies, parent, state) {
|
|
507
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent), expected, 'Installed target changed before conditional rollback');
|
|
508
|
+
const quarantinePath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.rollback`);
|
|
509
|
+
await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional rollback quarantine already exists');
|
|
510
|
+
await dependencies.beforeRename?.(filePath, quarantinePath);
|
|
511
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent), expected, 'Installed target changed during the conditional rollback hook');
|
|
512
|
+
await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional rollback hook created its quarantine path');
|
|
513
|
+
await rename(filePath, quarantinePath);
|
|
514
|
+
// Built-in rename fulfillment is the commit boundary. Every later failure
|
|
515
|
+
// reports an absent-target committed outcome.
|
|
516
|
+
let postRenameError;
|
|
517
|
+
try {
|
|
518
|
+
await dependencies.afterRename?.(filePath, quarantinePath);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
postRenameError = error;
|
|
522
|
+
}
|
|
523
|
+
try {
|
|
524
|
+
requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent), expected, 'Installed target changed while entering conditional rollback');
|
|
525
|
+
await requireAbsentInBoundDirectory(filePath, parent, 'Conditional rollback source pathname was recreated');
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
const cleanupFailures = [await bindOwnedCleanupFailure(error, [
|
|
529
|
+
{ artifactPath: quarantinePath, expected },
|
|
530
|
+
], parent)];
|
|
531
|
+
try {
|
|
532
|
+
const currentTemporary = await readArtifactInBoundDirectory(temporaryPath, parent);
|
|
533
|
+
if (currentTemporary !== undefined
|
|
534
|
+
&& sameSnapshot(currentTemporary.snapshot, expected.snapshot)) {
|
|
535
|
+
state.temporaryLinkCount = currentTemporary.linkCount;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch (observationError) {
|
|
539
|
+
cleanupFailures.push(new AtomicCleanupFailure(undefined, { cause: observationError }));
|
|
540
|
+
}
|
|
541
|
+
throw new AtomicCommittedUnlinkError({
|
|
542
|
+
cleanupFailures,
|
|
543
|
+
}, postRenameError ?? error);
|
|
544
|
+
}
|
|
545
|
+
const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, quarantinePath, expected, parent);
|
|
546
|
+
const cleanupFailures = cleanupFailure === undefined ? [] : [cleanupFailure];
|
|
547
|
+
if (cleanupFailure === undefined) {
|
|
548
|
+
state.temporaryLinkCount = 1n;
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
try {
|
|
552
|
+
const currentTemporary = await readArtifactInBoundDirectory(temporaryPath, parent);
|
|
553
|
+
if (currentTemporary !== undefined
|
|
554
|
+
&& sameSnapshot(currentTemporary.snapshot, expected.snapshot)) {
|
|
555
|
+
state.temporaryLinkCount = currentTemporary.linkCount;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
catch (observationError) {
|
|
559
|
+
cleanupFailures.push(new AtomicCleanupFailure(undefined, { cause: observationError }));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const outcome = { cleanupFailures };
|
|
563
|
+
try {
|
|
564
|
+
await requireBoundDirectory(parent);
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
throw new AtomicCommittedUnlinkError(outcome, error);
|
|
568
|
+
}
|
|
569
|
+
if (postRenameError !== undefined) {
|
|
570
|
+
throw new AtomicCommittedUnlinkError(outcome, postRenameError);
|
|
571
|
+
}
|
|
572
|
+
return outcome;
|
|
19
573
|
}
|
|
20
|
-
|
|
574
|
+
async function conditionalInstall(temporaryPath, filePath, expectation, beforeCleanup, temporary, parent, state, dependencies) {
|
|
575
|
+
await requireBoundDirectory(parent);
|
|
576
|
+
await assertFileExpectation(filePath, expectation);
|
|
577
|
+
const expected = expectation.expected;
|
|
578
|
+
let quarantinePath;
|
|
579
|
+
let quarantined;
|
|
580
|
+
if (expected !== undefined) {
|
|
581
|
+
const plannedTarget = await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot);
|
|
582
|
+
if (plannedTarget === undefined || !sameSnapshot(plannedTarget.snapshot, expected)) {
|
|
583
|
+
throw changedAfterPlanning(filePath);
|
|
584
|
+
}
|
|
585
|
+
quarantinePath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.previous`);
|
|
586
|
+
await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional update quarantine already exists', expectation.containmentRoot);
|
|
587
|
+
await dependencies.beforeRename?.(filePath, quarantinePath);
|
|
588
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot), plannedTarget, 'Planned target changed during the update quarantine hook');
|
|
589
|
+
await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional update hook created its quarantine path', expectation.containmentRoot);
|
|
590
|
+
await rename(filePath, quarantinePath);
|
|
591
|
+
// Built-in rename fulfillment is the commit boundary. Retain the exact
|
|
592
|
+
// pre-rename binding before any post-rename hook or pathname observation.
|
|
593
|
+
quarantined = plannedTarget;
|
|
594
|
+
let postRenameError;
|
|
595
|
+
try {
|
|
596
|
+
await dependencies.afterRename?.(filePath, quarantinePath);
|
|
597
|
+
}
|
|
598
|
+
catch (error) {
|
|
599
|
+
postRenameError = error;
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent, expectation.containmentRoot), quarantined, 'Quarantined target changed after update rename');
|
|
603
|
+
await requireAbsentInBoundDirectory(filePath, parent, 'Update quarantine left or recreated the target pathname', expectation.containmentRoot);
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
throw new AtomicCommittedUnlinkError({
|
|
607
|
+
cleanupFailures: [await bindOwnedCleanupFailure(error, [
|
|
608
|
+
{ artifactPath: quarantinePath, expected: quarantined },
|
|
609
|
+
], parent)],
|
|
610
|
+
}, postRenameError ?? error);
|
|
611
|
+
}
|
|
612
|
+
if (postRenameError !== undefined) {
|
|
613
|
+
throw new AtomicCommittedUnlinkError({
|
|
614
|
+
cleanupFailures: [await bindOwnedCleanupFailure(postRenameError, [
|
|
615
|
+
{ artifactPath: quarantinePath, expected: quarantined },
|
|
616
|
+
], parent)],
|
|
617
|
+
}, postRenameError);
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
await dependencies.afterQuarantine?.(filePath);
|
|
621
|
+
requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent, expectation.containmentRoot), quarantined, 'Quarantined target changed after quarantine');
|
|
622
|
+
await requireAbsentInBoundDirectory(filePath, parent, 'Quarantine hook recreated the target pathname', expectation.containmentRoot);
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
const failures = await restoreQuarantinedTarget(quarantinePath, filePath, beforeCleanup, quarantined, parent);
|
|
626
|
+
await throwAfterOriginalRestoreAttempt(error, failures, filePath, expected, parent, 'Conditional installation was interrupted and rollback left an artifact');
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
const validatePreInstall = async () => {
|
|
630
|
+
requireArtifact(await readArtifactInBoundDirectory(temporaryPath, parent, expectation.containmentRoot), temporary, 'Atomic temporary file changed before installation');
|
|
631
|
+
if (expected === undefined) {
|
|
632
|
+
if (await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot) !== undefined) {
|
|
633
|
+
throw changedAfterPlanning(filePath);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
await requireAbsentInBoundDirectory(filePath, parent, 'Conditional installation target is no longer absent', expectation.containmentRoot);
|
|
638
|
+
}
|
|
639
|
+
if (quarantinePath !== undefined && quarantined !== undefined) {
|
|
640
|
+
requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent, expectation.containmentRoot), quarantined, 'Quarantined target changed before installation');
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
try {
|
|
644
|
+
await validatePreInstall();
|
|
645
|
+
await dependencies.beforeLink?.(temporaryPath, filePath);
|
|
646
|
+
await validatePreInstall();
|
|
647
|
+
try {
|
|
648
|
+
await link(temporaryPath, filePath);
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
if (isAlreadyExists(error))
|
|
652
|
+
throw changedAfterPlanning(filePath);
|
|
653
|
+
throw error;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
catch (error) {
|
|
657
|
+
const failures = quarantinePath === undefined || quarantined === undefined
|
|
658
|
+
? []
|
|
659
|
+
: await restoreQuarantinedTarget(quarantinePath, filePath, beforeCleanup, quarantined, parent);
|
|
660
|
+
if (quarantined !== undefined && expected !== undefined) {
|
|
661
|
+
await throwAfterOriginalRestoreAttempt(error, failures, filePath, expected, parent, 'Conditional installation failed and rollback left an artifact');
|
|
662
|
+
}
|
|
663
|
+
throwWithArtifactFailures(error, failures, 'Conditional installation failed and rollback left an artifact');
|
|
664
|
+
}
|
|
665
|
+
// Built-in link fulfillment is the commit boundary. Record the second link
|
|
666
|
+
// before any hook or pathname validation can fail.
|
|
667
|
+
state.temporaryLinkCount = 2n;
|
|
668
|
+
const linkedTemporary = withLinkCount(temporary, 2n);
|
|
669
|
+
const validateInstalled = async () => {
|
|
670
|
+
requireArtifact(await readArtifactInBoundDirectory(temporaryPath, parent, expectation.containmentRoot), linkedTemporary, 'Atomic temporary file changed after installation');
|
|
671
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot), linkedTemporary, 'Installed target changed after installation');
|
|
672
|
+
if (quarantinePath !== undefined && quarantined !== undefined) {
|
|
673
|
+
requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent, expectation.containmentRoot), quarantined, 'Quarantined target changed after installation');
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
try {
|
|
677
|
+
await dependencies.afterLink?.(temporaryPath, filePath);
|
|
678
|
+
await validateInstalled();
|
|
679
|
+
await dependencies.afterInstall?.(filePath);
|
|
680
|
+
await validateInstalled();
|
|
681
|
+
}
|
|
682
|
+
catch (error) {
|
|
683
|
+
const cleanupFailures = quarantinePath === undefined || quarantined === undefined
|
|
684
|
+
? []
|
|
685
|
+
: [await bindOwnedCleanupFailure(error, [
|
|
686
|
+
{ artifactPath: quarantinePath, expected: quarantined },
|
|
687
|
+
], parent)];
|
|
688
|
+
throw new AtomicCommittedMutationError({ installed: temporary.snapshot, cleanupFailures }, error);
|
|
689
|
+
}
|
|
690
|
+
for (const absentPath of expectation.mustRemainAbsent ?? []) {
|
|
691
|
+
let alternate;
|
|
692
|
+
try {
|
|
693
|
+
await requireBoundDirectory(parent);
|
|
694
|
+
alternate = await readRegularFile(absentPath, {
|
|
695
|
+
...(expectation.containmentRoot === undefined ? {} : { containmentRoot: expectation.containmentRoot }),
|
|
696
|
+
});
|
|
697
|
+
await requireBoundDirectory(parent);
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
const displacedOriginal = quarantinePath === undefined || quarantined === undefined
|
|
701
|
+
? []
|
|
702
|
+
: [await bindOwnedCleanupFailure(error, [
|
|
703
|
+
{ artifactPath: quarantinePath, expected: quarantined },
|
|
704
|
+
], parent)];
|
|
705
|
+
throw new AtomicCommittedMutationError({ installed: temporary.snapshot, cleanupFailures: displacedOriginal }, error);
|
|
706
|
+
}
|
|
707
|
+
if (alternate !== undefined) {
|
|
708
|
+
const conflict = changedAfterPlanning(absentPath);
|
|
709
|
+
let rollback;
|
|
710
|
+
try {
|
|
711
|
+
rollback = await conditionalUnlinkInstalled(temporaryPath, filePath, linkedTemporary, beforeCleanup, dependencies, parent, state);
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
const rollbackFailure = new AggregateError([conflict, error], 'Alternate target appeared and conditional rollback failed');
|
|
715
|
+
if (error instanceof AtomicCommittedUnlinkError) {
|
|
716
|
+
const failures = [...error.outcome.cleanupFailures];
|
|
717
|
+
if (quarantinePath !== undefined
|
|
718
|
+
&& quarantined !== undefined
|
|
719
|
+
&& expected !== undefined) {
|
|
720
|
+
failures.push(...await restoreQuarantinedTarget(quarantinePath, filePath, beforeCleanup, quarantined, parent));
|
|
721
|
+
await throwAfterOriginalRestoreAttempt(rollbackFailure, failures, filePath, expected, parent, 'Alternate target appeared; original target was restored but artifact cleanup failed');
|
|
722
|
+
}
|
|
723
|
+
throwWithArtifactFailures(rollbackFailure, failures, 'Alternate target appeared and committed rollback left an artifact');
|
|
724
|
+
}
|
|
725
|
+
const displacedOriginal = quarantinePath === undefined || quarantined === undefined
|
|
726
|
+
? []
|
|
727
|
+
: [await bindOwnedCleanupFailure(error, [
|
|
728
|
+
{ artifactPath: quarantinePath, expected: quarantined },
|
|
729
|
+
], parent)];
|
|
730
|
+
throw new AtomicCommittedMutationError({
|
|
731
|
+
installed: temporary.snapshot,
|
|
732
|
+
cleanupFailures: displacedOriginal,
|
|
733
|
+
}, rollbackFailure);
|
|
734
|
+
}
|
|
735
|
+
const failures = [...rollback.cleanupFailures];
|
|
736
|
+
if (quarantinePath !== undefined
|
|
737
|
+
&& quarantined !== undefined
|
|
738
|
+
&& expected !== undefined) {
|
|
739
|
+
failures.push(...await restoreQuarantinedTarget(quarantinePath, filePath, beforeCleanup, quarantined, parent));
|
|
740
|
+
await throwAfterOriginalRestoreAttempt(conflict, failures, filePath, expected, parent, 'Alternate target appeared; original target was restored but artifact cleanup failed');
|
|
741
|
+
}
|
|
742
|
+
throwWithArtifactFailures(conflict, failures, 'Alternate target appeared and committed rollback left an artifact');
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
const cleanupFailures = [];
|
|
746
|
+
if (quarantinePath !== undefined && quarantined !== undefined) {
|
|
747
|
+
const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, quarantinePath, quarantined, parent);
|
|
748
|
+
if (cleanupFailure !== undefined)
|
|
749
|
+
cleanupFailures.push(cleanupFailure);
|
|
750
|
+
}
|
|
751
|
+
try {
|
|
752
|
+
await requireBoundDirectory(parent);
|
|
753
|
+
}
|
|
754
|
+
catch (error) {
|
|
755
|
+
throw new AtomicCommittedMutationError({ installed: temporary.snapshot, cleanupFailures }, error);
|
|
756
|
+
}
|
|
757
|
+
return { installed: temporary.snapshot, cleanupFailures };
|
|
758
|
+
}
|
|
759
|
+
async function atomicWriteTextInternal(filePath, content, mode, dependencyOverrides, expectation) {
|
|
760
|
+
const changeMode = dependencyOverrides.chmod ?? chmod;
|
|
761
|
+
const beforeCleanup = dependencyOverrides.beforeCleanup;
|
|
21
762
|
const directory = path.dirname(filePath);
|
|
22
|
-
|
|
763
|
+
if (expectation?.containmentRoot !== undefined) {
|
|
764
|
+
await rejectLinkedComponents(expectation.containmentRoot, filePath);
|
|
765
|
+
}
|
|
766
|
+
if (expectation?.expectedParentDirectory === undefined) {
|
|
767
|
+
await mkdir(directory, { recursive: true });
|
|
768
|
+
}
|
|
769
|
+
else {
|
|
770
|
+
await assertFileExpectation(filePath, expectation);
|
|
771
|
+
}
|
|
772
|
+
const parent = await bindMutationDirectory(directory, expectation?.expectedParentDirectory);
|
|
773
|
+
if (expectation?.containmentRoot !== undefined) {
|
|
774
|
+
await rejectLinkedComponents(expectation.containmentRoot, filePath);
|
|
775
|
+
}
|
|
23
776
|
const temporaryPath = path.join(directory, `.${path.basename(filePath)}.${randomUUID()}.tmp`);
|
|
777
|
+
let temporaryHandle;
|
|
778
|
+
let temporaryCreated = false;
|
|
779
|
+
let temporary;
|
|
24
780
|
try {
|
|
25
|
-
await
|
|
26
|
-
|
|
27
|
-
await
|
|
781
|
+
temporaryHandle = await open(temporaryPath, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL, mode);
|
|
782
|
+
temporaryCreated = true;
|
|
783
|
+
await requireBoundDirectory(parent);
|
|
784
|
+
const opened = artifactFromOpenHandle(await temporaryHandle.stat(), '');
|
|
785
|
+
if (opened.linkCount !== 1n) {
|
|
786
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Atomic temporary file must have exactly one link');
|
|
787
|
+
}
|
|
788
|
+
await temporaryHandle.writeFile(content, { encoding: 'utf8' });
|
|
789
|
+
await temporaryHandle.sync();
|
|
790
|
+
const written = await readOpenHandleArtifact(temporaryHandle, temporaryPath);
|
|
791
|
+
if (!sameIdentity(written.snapshot.identity, opened.snapshot.identity)
|
|
792
|
+
|| written.snapshot.content !== content
|
|
793
|
+
|| written.linkCount !== 1n) {
|
|
794
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Atomic temporary file changed during preparation');
|
|
795
|
+
}
|
|
796
|
+
await requireBoundDirectory(parent);
|
|
797
|
+
await changeMode(temporaryPath, mode);
|
|
798
|
+
await temporaryHandle.sync();
|
|
799
|
+
const prepared = await readOpenHandleArtifact(temporaryHandle, temporaryPath);
|
|
800
|
+
if (!sameIdentity(prepared.snapshot.identity, opened.snapshot.identity)
|
|
801
|
+
|| prepared.snapshot.content !== content
|
|
802
|
+
|| prepared.snapshot.mode !== (mode & 0o777)
|
|
803
|
+
|| prepared.linkCount !== 1n) {
|
|
804
|
+
throw new KiokukoError('INTEGRITY_ERROR', 'Atomic temporary file changed during mode preparation');
|
|
805
|
+
}
|
|
806
|
+
await requireBoundDirectory(parent);
|
|
807
|
+
requireArtifact(await readBoundArtifact(temporaryPath, expectation?.containmentRoot), prepared, 'Atomic temporary pathname changed during mode preparation');
|
|
808
|
+
await dependencyOverrides.beforeCommit?.(filePath);
|
|
809
|
+
await requireBoundDirectory(parent);
|
|
810
|
+
const descriptorPrepared = await readOpenHandleArtifact(temporaryHandle, temporaryPath);
|
|
811
|
+
requireArtifact(descriptorPrepared, prepared, 'Atomic temporary file changed before commit');
|
|
812
|
+
temporary = requireArtifact(await readBoundArtifact(temporaryPath, expectation?.containmentRoot), prepared, 'Atomic temporary pathname changed before commit');
|
|
813
|
+
await temporaryHandle.close();
|
|
814
|
+
temporaryHandle = undefined;
|
|
28
815
|
}
|
|
29
816
|
catch (error) {
|
|
30
|
-
|
|
817
|
+
let operationError = error;
|
|
818
|
+
let cleanupBinding;
|
|
819
|
+
if (temporaryHandle !== undefined) {
|
|
820
|
+
try {
|
|
821
|
+
cleanupBinding = await readOpenHandleArtifact(temporaryHandle, temporaryPath);
|
|
822
|
+
}
|
|
823
|
+
catch (bindingError) {
|
|
824
|
+
operationError = new AggregateError([operationError, bindingError], 'Atomic write failed and its temporary artifact could not be rebound');
|
|
825
|
+
}
|
|
826
|
+
try {
|
|
827
|
+
await temporaryHandle.close();
|
|
828
|
+
}
|
|
829
|
+
catch (closeError) {
|
|
830
|
+
operationError = new AggregateError([operationError, closeError], 'Atomic write failed and its temporary handle could not be closed');
|
|
831
|
+
}
|
|
832
|
+
temporaryHandle = undefined;
|
|
833
|
+
}
|
|
834
|
+
if (temporaryCreated) {
|
|
835
|
+
return cleanupBeforeCommit(beforeCleanup, temporaryPath, operationError, cleanupBinding, parent);
|
|
836
|
+
}
|
|
837
|
+
if (operationError instanceof KiokukoError || operationError instanceof AggregateError)
|
|
838
|
+
throw operationError;
|
|
31
839
|
throw new KiokukoError('PARTIAL_FAILURE', `Unable to atomically write ${filePath}`, {
|
|
32
|
-
cause:
|
|
840
|
+
cause: operationError instanceof Error ? operationError.message : String(operationError),
|
|
33
841
|
});
|
|
34
842
|
}
|
|
843
|
+
if (expectation === undefined) {
|
|
844
|
+
try {
|
|
845
|
+
requireArtifact(await readArtifactInBoundDirectory(temporaryPath, parent), temporary, 'Atomic temporary file changed before rename');
|
|
846
|
+
await dependencyOverrides.beforeRename?.(temporaryPath, filePath);
|
|
847
|
+
requireArtifact(await readArtifactInBoundDirectory(temporaryPath, parent), temporary, 'Atomic temporary file changed during the rename hook');
|
|
848
|
+
await rename(temporaryPath, filePath);
|
|
849
|
+
}
|
|
850
|
+
catch (error) {
|
|
851
|
+
return cleanupBeforeCommit(beforeCleanup, temporaryPath, error, temporary, parent);
|
|
852
|
+
}
|
|
853
|
+
// Built-in rename fulfillment is the commit boundary. Construct the
|
|
854
|
+
// committed outcome before any post-rename hook or observation.
|
|
855
|
+
const outcome = { installed: temporary.snapshot, cleanupFailures: [] };
|
|
856
|
+
try {
|
|
857
|
+
await dependencyOverrides.afterRename?.(temporaryPath, filePath);
|
|
858
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent), temporary, 'Renamed target does not match the bound temporary file');
|
|
859
|
+
await requireAbsentInBoundDirectory(temporaryPath, parent, 'Unconditional rename left or recreated its temporary pathname');
|
|
860
|
+
await dependencyOverrides.afterInstall?.(filePath);
|
|
861
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent), temporary, 'Installed target changed after rename');
|
|
862
|
+
await requireAbsentInBoundDirectory(temporaryPath, parent, 'Post-install hook recreated the temporary pathname');
|
|
863
|
+
return outcome;
|
|
864
|
+
}
|
|
865
|
+
catch (error) {
|
|
866
|
+
const cleanupFailures = [];
|
|
867
|
+
try {
|
|
868
|
+
const installed = await readArtifactInBoundDirectory(filePath, parent);
|
|
869
|
+
const temporaryAlias = await readArtifactInBoundDirectory(temporaryPath, parent);
|
|
870
|
+
if (!sameArtifact(installed, temporary)) {
|
|
871
|
+
cleanupFailures.push(new AtomicCleanupFailure(temporaryAlias !== undefined
|
|
872
|
+
&& sameSnapshot(temporaryAlias.snapshot, temporary.snapshot)
|
|
873
|
+
? temporaryPath
|
|
874
|
+
: undefined, { cause: error }));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
catch (observationError) {
|
|
878
|
+
cleanupFailures.push(new AtomicCleanupFailure(undefined, { cause: observationError }));
|
|
879
|
+
}
|
|
880
|
+
throw new AtomicCommittedMutationError({
|
|
881
|
+
installed: temporary.snapshot,
|
|
882
|
+
cleanupFailures,
|
|
883
|
+
}, error);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
const state = { temporaryLinkCount: 1n };
|
|
887
|
+
let outcome;
|
|
888
|
+
try {
|
|
889
|
+
outcome = await conditionalInstall(temporaryPath, filePath, expectation, beforeCleanup, temporary, parent, state, dependencyOverrides);
|
|
890
|
+
}
|
|
891
|
+
catch (error) {
|
|
892
|
+
const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, temporaryPath, withLinkCount(temporary, state.temporaryLinkCount), parent);
|
|
893
|
+
if (error instanceof AtomicCommittedUnlinkError) {
|
|
894
|
+
throw new AtomicCommittedUnlinkError({
|
|
895
|
+
cleanupFailures: cleanupFailure === undefined
|
|
896
|
+
? error.outcome.cleanupFailures
|
|
897
|
+
: [...error.outcome.cleanupFailures, cleanupFailure],
|
|
898
|
+
}, error.operationError);
|
|
899
|
+
}
|
|
900
|
+
if (error instanceof AtomicCommittedMutationError) {
|
|
901
|
+
throw new AtomicCommittedMutationError({
|
|
902
|
+
installed: error.outcome.installed,
|
|
903
|
+
cleanupFailures: cleanupFailure === undefined
|
|
904
|
+
? error.outcome.cleanupFailures
|
|
905
|
+
: [...error.outcome.cleanupFailures, cleanupFailure],
|
|
906
|
+
}, error.operationError);
|
|
907
|
+
}
|
|
908
|
+
if (cleanupFailure !== undefined) {
|
|
909
|
+
throw new AggregateError([error, cleanupFailure], 'Atomic file mutation failed and temporary artifact cleanup also failed');
|
|
910
|
+
}
|
|
911
|
+
throw error;
|
|
912
|
+
}
|
|
913
|
+
const cleanupFailure = await cleanupBoundArtifact(beforeCleanup, temporaryPath, withLinkCount(temporary, state.temporaryLinkCount), parent);
|
|
914
|
+
const finalOutcome = cleanupFailure === undefined
|
|
915
|
+
? outcome
|
|
916
|
+
: { installed: outcome.installed, cleanupFailures: [...outcome.cleanupFailures, cleanupFailure] };
|
|
917
|
+
try {
|
|
918
|
+
if (cleanupFailure === undefined) {
|
|
919
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot), temporary, 'Installed target did not reach its final single-link state');
|
|
920
|
+
}
|
|
921
|
+
await requireBoundDirectory(parent);
|
|
922
|
+
}
|
|
923
|
+
catch (error) {
|
|
924
|
+
throw new AtomicCommittedMutationError(finalOutcome, error);
|
|
925
|
+
}
|
|
926
|
+
return finalOutcome;
|
|
927
|
+
}
|
|
928
|
+
export async function atomicWriteText(filePath, content, mode = 0o644, dependencyOverrides = {}) {
|
|
929
|
+
return atomicWriteTextInternal(filePath, content, mode, dependencyOverrides);
|
|
930
|
+
}
|
|
931
|
+
/** Write only when the target is still the exact file observed at planning time. */
|
|
932
|
+
export async function atomicWriteTextIfUnchanged(filePath, content, expectation, mode = 0o644, dependencyOverrides = {}) {
|
|
933
|
+
return atomicWriteTextInternal(filePath, content, mode, dependencyOverrides, expectation);
|
|
934
|
+
}
|
|
935
|
+
/** Delete only when the target is still the exact planned file identity and content. */
|
|
936
|
+
export async function unlinkRegularFileIfUnchanged(filePath, expectation, dependencyOverrides = {}) {
|
|
937
|
+
const parent = await bindMutationDirectory(path.dirname(filePath), expectation.expectedParentDirectory);
|
|
938
|
+
await assertFileExpectation(filePath, expectation);
|
|
939
|
+
await dependencyOverrides.beforeCommit?.(filePath);
|
|
940
|
+
await requireBoundDirectory(parent);
|
|
941
|
+
await assertFileExpectation(filePath, expectation);
|
|
942
|
+
const expected = expectation.expected;
|
|
943
|
+
if (expected === undefined)
|
|
944
|
+
throw changedAfterPlanning();
|
|
945
|
+
const plannedTarget = await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot);
|
|
946
|
+
if (plannedTarget === undefined || !sameSnapshot(plannedTarget.snapshot, expected)) {
|
|
947
|
+
throw changedAfterPlanning(filePath);
|
|
948
|
+
}
|
|
949
|
+
const quarantinePath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${randomUUID()}.deleted`);
|
|
950
|
+
await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional unlink quarantine already exists', expectation.containmentRoot);
|
|
951
|
+
await dependencyOverrides.beforeRename?.(filePath, quarantinePath);
|
|
952
|
+
requireArtifact(await readArtifactInBoundDirectory(filePath, parent, expectation.containmentRoot), plannedTarget, 'Planned unlink target changed during the rename hook');
|
|
953
|
+
await requireAbsentInBoundDirectory(quarantinePath, parent, 'Conditional unlink hook created its quarantine path', expectation.containmentRoot);
|
|
954
|
+
await rename(filePath, quarantinePath);
|
|
955
|
+
// Built-in rename fulfillment is the commit boundary. The exact original
|
|
956
|
+
// binding is retained before any post-rename hook or observation.
|
|
957
|
+
const boundQuarantined = plannedTarget;
|
|
958
|
+
let postRenameError;
|
|
959
|
+
try {
|
|
960
|
+
await dependencyOverrides.afterRename?.(filePath, quarantinePath);
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
postRenameError = error;
|
|
964
|
+
}
|
|
965
|
+
try {
|
|
966
|
+
requireArtifact(await readArtifactInBoundDirectory(quarantinePath, parent, expectation.containmentRoot), boundQuarantined, 'Quarantined unlink target changed after rename');
|
|
967
|
+
await requireAbsentInBoundDirectory(filePath, parent, 'Conditional unlink rename left or recreated the target pathname', expectation.containmentRoot);
|
|
968
|
+
}
|
|
969
|
+
catch (error) {
|
|
970
|
+
throw new AtomicCommittedUnlinkError({
|
|
971
|
+
cleanupFailures: [await bindOwnedCleanupFailure(error, [
|
|
972
|
+
{ artifactPath: quarantinePath, expected: boundQuarantined },
|
|
973
|
+
], parent)],
|
|
974
|
+
}, postRenameError ?? error);
|
|
975
|
+
}
|
|
976
|
+
if (postRenameError !== undefined) {
|
|
977
|
+
throw new AtomicCommittedUnlinkError({
|
|
978
|
+
cleanupFailures: [await bindOwnedCleanupFailure(postRenameError, [
|
|
979
|
+
{ artifactPath: quarantinePath, expected: boundQuarantined },
|
|
980
|
+
], parent)],
|
|
981
|
+
}, postRenameError);
|
|
982
|
+
}
|
|
983
|
+
for (const absentPath of expectation.mustRemainAbsent ?? []) {
|
|
984
|
+
let conflict;
|
|
985
|
+
try {
|
|
986
|
+
await requireBoundDirectory(parent);
|
|
987
|
+
const alternate = await readRegularFile(absentPath, {
|
|
988
|
+
...(expectation.containmentRoot === undefined ? {} : { containmentRoot: expectation.containmentRoot }),
|
|
989
|
+
});
|
|
990
|
+
await requireBoundDirectory(parent);
|
|
991
|
+
if (alternate !== undefined)
|
|
992
|
+
conflict = changedAfterPlanning(absentPath);
|
|
993
|
+
}
|
|
994
|
+
catch (error) {
|
|
995
|
+
conflict = error;
|
|
996
|
+
}
|
|
997
|
+
if (conflict !== undefined) {
|
|
998
|
+
const failures = await restoreQuarantinedTarget(quarantinePath, filePath, dependencyOverrides.beforeCleanup, boundQuarantined, parent);
|
|
999
|
+
await throwAfterOriginalRestoreAttempt(conflict, failures, filePath, expected, parent, 'Conditional unlink conflicted and rollback left an artifact');
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const cleanupFailure = await cleanupBoundArtifact(dependencyOverrides.beforeCleanup, quarantinePath, boundQuarantined, parent);
|
|
1003
|
+
const outcome = { cleanupFailures: cleanupFailure === undefined ? [] : [cleanupFailure] };
|
|
1004
|
+
try {
|
|
1005
|
+
await requireBoundDirectory(parent);
|
|
1006
|
+
}
|
|
1007
|
+
catch (error) {
|
|
1008
|
+
throw new AtomicCommittedUnlinkError(outcome, error);
|
|
1009
|
+
}
|
|
1010
|
+
return outcome;
|
|
35
1011
|
}
|
|
36
1012
|
//# sourceMappingURL=atomic-write.js.map
|