akm-cli 0.9.16 → 0.9.17-alpha.10
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 +2101 -0
- package/STABILITY.md +11 -10
- package/dist/akm +124 -193
- package/dist/akm-migrate +38 -19
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/prompts/consolidate-system.md +4 -11
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +6 -2
- package/dist/cli/unknown-flags.js +24 -1
- package/dist/cli.js +68 -10
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +6 -0
- package/dist/commands/health/archive-usage.js +92 -0
- package/dist/commands/health/checks.js +83 -74
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +1 -42
- package/dist/commands/health/improve-metrics.js +136 -591
- package/dist/commands/health/md-report.js +1 -6
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +14 -120
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -74
- package/dist/commands/health.js +145 -143
- package/dist/commands/improve/consolidate/chunking.js +26 -117
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +589 -1127
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1041
- package/dist/commands/improve/eligibility.js +126 -390
- package/dist/commands/improve/execution.js +8 -10
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +75 -169
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +52 -4
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +480 -1074
- package/dist/commands/improve/ledger.js +119 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +415 -1073
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +16 -118
- package/dist/commands/improve/memory/memory-improve.js +266 -14
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -15
- package/dist/commands/improve/preparation.js +779 -2319
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +642 -1353
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +92 -0
- package/dist/commands/improve/salience.js +41 -240
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +322 -0
- package/dist/commands/lint/base-linter.js +37 -15
- package/dist/commands/proposal/drain.js +261 -578
- package/dist/commands/proposal/proposal-cli.js +19 -20
- package/dist/commands/proposal/proposal-types.js +31 -24
- package/dist/commands/proposal/proposal.js +38 -8
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +1097 -1394
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +22 -89
- package/dist/commands/read/curate.js +105 -462
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +57 -108
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +4 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +127 -29
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/source-manage.js +9 -2
- package/dist/commands/sources/stash-cli.js +24 -4
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +12 -13
- package/dist/commands/tasks/tasks.js +350 -936
- package/dist/commands/tasks/validate.js +26 -24
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +2 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +42 -12
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/asset-placement.js +4 -13
- package/dist/core/asset/frontmatter.js +106 -1
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +285 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +9 -34
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +184 -111
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +54 -125
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/index-config.js +9 -48
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-change.js +13 -5
- package/dist/core/file-lock.js +76 -173
- package/dist/core/improve-result.js +35 -7
- package/dist/core/improve-types.js +0 -1
- package/dist/core/logs-db.js +2 -2
- package/dist/core/loopback.js +7 -12
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/parse.js +13 -16
- package/dist/core/paths.js +0 -24
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +123 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/time.js +0 -20
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +294 -1005
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +77 -73
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +141 -154
- package/dist/indexer/indexer.js +400 -1124
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +96 -90
- package/dist/indexer/passes/metadata.js +132 -219
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +2 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +190 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +36 -7
- package/dist/indexer/walk/walker.js +3 -4
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/client.js +18 -6
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +11 -17
- package/dist/llm/feature-gate.js +0 -8
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +25 -38
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +69 -31
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +48 -3
- package/dist/output/text/show-format.js +13 -17
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +42948 -92369
- package/dist/scripts/akm-migrate.js +42935 -92354
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +83 -21
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +209 -0
- package/dist/storage/repositories/index-connection.js +39 -72
- package/dist/storage/repositories/index-entries-repository.js +131 -129
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +101 -268
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +257 -325
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +104 -1
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +130 -0
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +191 -302
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +141 -748
- package/dist/tasks/backends/schtasks.js +119 -623
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +20 -238
- package/dist/tasks/scheduler-invocation.js +136 -244
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +368 -679
- package/dist/tasks/source/parse-task-source.js +55 -9
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +464 -88
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +43 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
- package/docs/reference/cli.md +232 -135
- package/docs/reference/configuration.md +71 -57
- package/docs/reference/data-and-telemetry.md +20 -21
- package/docs/reference/tasks.md +105 -39
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +115 -738
- package/schemas/akm-workflow.json +1 -0
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -149
- package/dist/commands/improve/distill/promote-memory.js +0 -291
- package/dist/commands/improve/distill/quality-gate.js +0 -337
- package/dist/commands/improve/eval-cases.js +0 -52
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -31
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/db/graph-db.js +0 -444
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/graph/graph-extraction.js +0 -1108
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/search/ranking-types.js +0 -4
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/llm/graph-extract.js +0 -728
- package/dist/llm/metadata-enhance.js +0 -96
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/tasks/source/task-to-v3.js +0 -507
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -77
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -7,11 +7,18 @@
|
|
|
7
7
|
// its other lines untouched:
|
|
8
8
|
//
|
|
9
9
|
// # akm:task <id> BEGIN
|
|
10
|
-
// [SCHED] /abs/akm task run <id> >
|
|
10
|
+
// [SCHED] AKM_BUNDLE_DIR=<working stash> /abs/akm task run <id> ... > <log> 2>&1
|
|
11
11
|
// # akm:task <id> END
|
|
12
12
|
//
|
|
13
|
+
// The row sets its own environment inline, as a `VAR=value` prefix (see
|
|
14
|
+
// `src/tasks/scheduler-invocation.ts`); PATH is the `# akm:env` block. A
|
|
15
|
+
// command over the portable line limit runs `sh <wrapper script>` instead,
|
|
16
|
+
// and the script holds the same environment and argv.
|
|
17
|
+
//
|
|
13
18
|
// The backend reads/writes the user's crontab via `crontab -l` and
|
|
14
|
-
// `crontab -`.
|
|
19
|
+
// `crontab -`. Every mutation is one read → modify the akm blocks (and the
|
|
20
|
+
// `# akm:env` PATH header) → one write, with the prior crontab restored when
|
|
21
|
+
// the write fails. Disabling a task comments the entry with `# akm:disabled `
|
|
15
22
|
// rather than removing it, so re-enabling preserves the original schedule.
|
|
16
23
|
//
|
|
17
24
|
// Platform notes:
|
|
@@ -30,282 +37,131 @@
|
|
|
30
37
|
// Tests inject a fake exec so unit tests don't touch the real crontab.
|
|
31
38
|
import { spawnSync } from "node:child_process";
|
|
32
39
|
import { createHash } from "node:crypto";
|
|
40
|
+
import fs from "node:fs";
|
|
33
41
|
import path from "node:path";
|
|
34
42
|
import { ConfigError } from "../../core/errors.js";
|
|
35
43
|
import { getTaskLogDir } from "../../core/paths.js";
|
|
36
44
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
37
45
|
import { parseSchedule, translateToCron } from "../schedule.js";
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
46
|
+
import { schedulerBindingNativeId, schedulerLogicalBindingId } from "../scheduler-binding.js";
|
|
47
|
+
import { buildScheduledInvocation, parseScheduledInvocationArgv, scheduledRowEnvironmentEntries, scheduledRowEnvironmentFrom, } from "../scheduler-invocation.js";
|
|
40
48
|
import { nodeFs, throwIfNotOk } from "./exec-utils.js";
|
|
41
49
|
const BEGIN = (id) => `# akm:task ${assertCronValue(id)} BEGIN`;
|
|
42
50
|
const END = (id) => `# akm:task ${assertCronValue(id)} END`;
|
|
43
51
|
const DISABLED_PREFIX = "# akm:disabled ";
|
|
44
|
-
const SOURCE_ENABLED_PREFIX = "# akm:source-enabled ";
|
|
45
52
|
const BLOCK_RE = /^# akm:task ([\w.@:_-]+) BEGIN$/;
|
|
46
53
|
const BLOCK_END_RE = /^# akm:task ([\w.@:_-]+) END$/;
|
|
54
|
+
const ENV_BEGIN = "# akm:env BEGIN";
|
|
55
|
+
const ENV_END = "# akm:env END";
|
|
47
56
|
export const PORTABLE_CRON_LINE_LIMIT = 1000;
|
|
48
|
-
const
|
|
57
|
+
const CRON_WRAPPER_PREFIX = ".akm-cron-wrapper-";
|
|
49
58
|
export function CRON_BACKEND(options = {}) {
|
|
50
59
|
const exec = options.exec ?? defaultCronExec();
|
|
51
60
|
const fsLike = options.fs ?? nodeFs();
|
|
52
61
|
const logDir = options.logDir ?? getTaskLogDir();
|
|
53
62
|
const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
|
|
54
63
|
const envPath = options.envPath === false ? undefined : (options.envPath ?? process.env.PATH);
|
|
55
|
-
const
|
|
56
|
-
const
|
|
64
|
+
const readFile = options.fs?.readFile ?? ((file) => fs.readFileSync(file, "utf8"));
|
|
65
|
+
const lineFor = (task, opts) => buildCronLineParts(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.environment);
|
|
57
66
|
return {
|
|
58
67
|
name: "cron",
|
|
59
|
-
install(task, opts
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// shell will fail the entire entry.
|
|
65
|
-
const cronLineParts = buildCronLineParts(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target);
|
|
66
|
-
const cronLine = cronLineParts.line;
|
|
67
|
-
assertPortableCronLine(cronLine);
|
|
68
|
+
install(task, opts) {
|
|
69
|
+
const parts = lineFor(task, opts);
|
|
70
|
+
assertPortableCronLine(parts.line);
|
|
71
|
+
// The redirect target's parent directory must exist or the surrounding
|
|
72
|
+
// shell fails the entire entry.
|
|
68
73
|
fsLike.ensureDir(logDir);
|
|
69
|
-
if (
|
|
74
|
+
if (parts.wrapper) {
|
|
70
75
|
if (!fsLike.writeFile) {
|
|
71
76
|
throw new ConfigError("Cron backend needs to write a wrapper script for this task's long invocation, but the configured filesystem cannot write files.", "INVALID_CONFIG_FILE");
|
|
72
77
|
}
|
|
73
|
-
fsLike.writeFile(
|
|
78
|
+
fsLike.writeFile(parts.wrapper.path, parts.wrapper.content);
|
|
74
79
|
}
|
|
75
80
|
const existing = readCrontab(exec);
|
|
76
81
|
const nativeId = schedulerBindingNativeId(task);
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
const artifacts = matching.map((block) => cronArtifact(block.id, block.body));
|
|
80
|
-
const priorArtifact = expected
|
|
81
|
-
? assertSchedulerNativeArtifactCardinality(artifacts, nativeId, expected.state === "absent" ? 0 : 1)
|
|
82
|
-
: matching.length > 1
|
|
83
|
-
? assertSchedulerNativeArtifactCardinality(artifacts, nativeId, 1)
|
|
84
|
-
: artifacts[0];
|
|
85
|
-
const prior = priorArtifact ? matching.find((block) => block.id === priorArtifact.nativeId) : undefined;
|
|
86
|
-
if (expected) {
|
|
87
|
-
assertSchedulerMutationArtifact(priorArtifact, expected);
|
|
88
|
-
}
|
|
89
|
-
else if (prior) {
|
|
90
|
-
assertSchedulerNativeArtifactOwner(prior.id, task, extractCronInvocation(prior.body)?.invocation);
|
|
91
|
-
}
|
|
92
|
-
const block = renderBlock(nativeId, cronLine, task.enabled, task.executionEvidenceDigest);
|
|
93
|
-
const next = upsertBlock(existing, nativeId, block);
|
|
94
|
-
replaceCrontab(exec, existing, next);
|
|
82
|
+
const block = renderBlock(nativeId, parts.line, task.enabled);
|
|
83
|
+
replaceCrontab(exec, existing, upsertEnvBlock(upsertBlock(existing, nativeId, block), envPath));
|
|
95
84
|
},
|
|
96
|
-
uninstall(nativeId
|
|
97
|
-
if (expected)
|
|
98
|
-
assertSchedulerExpectationIdentity({ ...expected, state: "present" });
|
|
85
|
+
uninstall(nativeId) {
|
|
99
86
|
const existing = readCrontab(exec);
|
|
100
|
-
|
|
101
|
-
const priorArtifact = expected
|
|
102
|
-
? assertSchedulerNativeArtifactCardinality(matching.map((block) => cronArtifact(block.id, block.body)), nativeId, 1)
|
|
103
|
-
: undefined;
|
|
104
|
-
const prior = expected
|
|
105
|
-
? matching.find((block) => block.id === priorArtifact?.nativeId)
|
|
106
|
-
: matching.find(({ id }) => id === nativeId);
|
|
107
|
-
if (!prior)
|
|
87
|
+
if (!listBlocks(existing).some((block) => block.id === nativeId))
|
|
108
88
|
return;
|
|
109
|
-
|
|
110
|
-
assertSchedulerRemovalArtifact(nativeId, expected, extractCronInvocation(prior.body)?.invocation, normalizeSignature(prior.body));
|
|
111
|
-
}
|
|
112
|
-
const next = removeBlock(existing, nativeId);
|
|
113
|
-
replaceCrontab(exec, existing, next);
|
|
89
|
+
replaceCrontab(exec, existing, upsertEnvBlock(removeBlock(existing, nativeId), envPath));
|
|
114
90
|
},
|
|
115
91
|
setEnabled(nativeId, enabled) {
|
|
116
92
|
const existing = readCrontab(exec);
|
|
117
93
|
const next = toggleBlock(existing, nativeId, enabled);
|
|
118
|
-
|
|
94
|
+
if (next !== existing)
|
|
95
|
+
replaceCrontab(exec, existing, next);
|
|
119
96
|
},
|
|
120
97
|
list() {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
98
|
+
const rows = [];
|
|
99
|
+
for (const { id, body } of listBlocks(readCrontab(exec))) {
|
|
100
|
+
const parsed = extractCronInvocation(body) ?? extractCronWrapperInvocation(body, readFile);
|
|
101
|
+
if (!parsed)
|
|
102
|
+
continue;
|
|
103
|
+
rows.push({
|
|
104
|
+
id: schedulerLogicalBindingId(id, parsed.invocation),
|
|
105
|
+
nativeId: id,
|
|
106
|
+
enabled: !cronIsDisabled(body),
|
|
129
107
|
signature: normalizeSignature(body),
|
|
130
|
-
...(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
});
|
|
137
|
-
},
|
|
138
|
-
listNativeArtifacts() {
|
|
139
|
-
return [...inspectCronState(readCrontab(exec), defaultContextPath).artifacts];
|
|
140
|
-
},
|
|
141
|
-
inspectBindings() {
|
|
142
|
-
return inspectCronState(readCrontab(exec), defaultContextPath);
|
|
143
|
-
},
|
|
144
|
-
snapshotBindings(ids) {
|
|
145
|
-
const crontab = readCrontab(exec);
|
|
146
|
-
const inspection = inspectCronState(crontab, defaultContextPath);
|
|
147
|
-
const keys = new Set(ids.map(schedulerNativeArtifactKey));
|
|
148
|
-
return Object.freeze({
|
|
149
|
-
kind: CRON_SNAPSHOT,
|
|
150
|
-
nativeIds: Object.freeze([...ids]),
|
|
151
|
-
artifacts: Object.freeze(inspection.artifacts.filter((artifact) => keys.has(schedulerNativeArtifactKey(artifact.nativeId)))),
|
|
152
|
-
crontab,
|
|
153
|
-
});
|
|
154
|
-
},
|
|
155
|
-
restoreBindings(snapshot, expectedCurrent) {
|
|
156
|
-
if (!isCronBindingSnapshot(snapshot)) {
|
|
157
|
-
throw new ConfigError("Invalid cron scheduler snapshot.", "INVALID_CONFIG_FILE");
|
|
158
|
-
}
|
|
159
|
-
const existing = readCrontab(exec);
|
|
160
|
-
const current = inspectCronState(existing, defaultContextPath);
|
|
161
|
-
const safeNativeIds = [];
|
|
162
|
-
const errors = [];
|
|
163
|
-
if (expectedCurrent) {
|
|
164
|
-
for (const nativeId of snapshot.nativeIds) {
|
|
165
|
-
try {
|
|
166
|
-
const expected = expectedCurrent.find((candidate) => candidate.nativeId === nativeId);
|
|
167
|
-
if (!expected) {
|
|
168
|
-
throw new ConfigError(`Missing cron rollback expectation for ${JSON.stringify(nativeId)}.`, "INVALID_CONFIG_FILE");
|
|
169
|
-
}
|
|
170
|
-
assertSchedulerRollbackArtifactCardinality(current.artifacts, expected);
|
|
171
|
-
safeNativeIds.push(nativeId);
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
errors.push(error);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
safeNativeIds.push(...snapshot.nativeIds);
|
|
180
|
-
}
|
|
181
|
-
const priorBlocks = new Map(listBlocks(snapshot.crontab).map((block) => [block.id, block]));
|
|
182
|
-
let restored = existing;
|
|
183
|
-
for (const nativeId of safeNativeIds) {
|
|
184
|
-
const prior = priorBlocks.get(nativeId);
|
|
185
|
-
restored = prior
|
|
186
|
-
? upsertBlock(restored, nativeId, [BEGIN(nativeId), prior.body, END(nativeId)].join("\n"))
|
|
187
|
-
: removeBlock(restored, nativeId);
|
|
188
|
-
}
|
|
189
|
-
if (restored !== existing) {
|
|
190
|
-
try {
|
|
191
|
-
replaceCrontab(exec, existing, restored);
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
errors.push(error);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (errors.length > 0) {
|
|
198
|
-
throw new AggregateError(errors, "Cron rollback CAS rejected one or more changed native artifacts.");
|
|
108
|
+
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
109
|
+
binding: parsed.binding,
|
|
110
|
+
...(parsed.contextPath !== undefined ? { contextPath: parsed.contextPath } : {}),
|
|
111
|
+
...(parsed.environment !== undefined ? { environment: parsed.environment } : {}),
|
|
112
|
+
invocation: parsed.invocation,
|
|
113
|
+
});
|
|
199
114
|
}
|
|
115
|
+
return rows;
|
|
200
116
|
},
|
|
201
117
|
expectedSignature(task, opts) {
|
|
202
|
-
const
|
|
203
|
-
assertPortableCronLine(
|
|
204
|
-
return normalizeSignature(cronBlockBody(
|
|
118
|
+
const line = lineFor(task, opts).line;
|
|
119
|
+
assertPortableCronLine(line);
|
|
120
|
+
return normalizeSignature(cronBlockBody(line, task.enabled));
|
|
205
121
|
},
|
|
206
122
|
};
|
|
207
123
|
}
|
|
208
|
-
function
|
|
209
|
-
const
|
|
210
|
-
const artifacts = [];
|
|
211
|
-
for (const { id, body } of listBlocks(crontab)) {
|
|
212
|
-
const parsed = extractCronInvocation(body);
|
|
213
|
-
const fingerprint = normalizeSignature(body);
|
|
214
|
-
const artifact = cronArtifact(id, body);
|
|
215
|
-
artifacts.push(artifact);
|
|
216
|
-
if (!parsed)
|
|
217
|
-
continue;
|
|
218
|
-
const ref = {
|
|
219
|
-
id: schedulerLogicalBindingId(id, parsed.invocation),
|
|
220
|
-
enabled: !cronIsDisabled(body),
|
|
221
|
-
signature: fingerprint,
|
|
222
|
-
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
223
|
-
binding: parsed.binding,
|
|
224
|
-
// A legacy (pre-`--scheduler-context`) row has no real descriptor
|
|
225
|
-
// path to report — `extractLegacyCronInvocation` leaves it "". Fall
|
|
226
|
-
// back to the current default so downstream consumers (context
|
|
227
|
-
// validation in `akm task prune`/`explain`, `sync`'s reuse of an
|
|
228
|
-
// existing binding's contextPath) see a real, resolvable descriptor
|
|
229
|
-
// rather than an empty path, since the row is about to be reconciled
|
|
230
|
-
// to a current one anyway (#881).
|
|
231
|
-
contextPath: parsed.contextPath || fallbackContextPath,
|
|
232
|
-
};
|
|
233
|
-
Object.defineProperty(ref, "nativeId", { value: id });
|
|
234
|
-
Object.defineProperty(ref, "invocation", { value: Object.freeze([...parsed.invocation]) });
|
|
235
|
-
installed.push(ref);
|
|
236
|
-
}
|
|
237
|
-
return Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) });
|
|
238
|
-
}
|
|
239
|
-
function cronArtifact(nativeId, body) {
|
|
240
|
-
const parsed = extractCronInvocation(body);
|
|
241
|
-
const owner = parsed ? schedulerLogicalBindingOwner(nativeId, parsed.invocation) : undefined;
|
|
242
|
-
const artifact = parsed
|
|
243
|
-
? {
|
|
244
|
-
nativeId,
|
|
245
|
-
...(owner !== undefined ? { bindingId: owner } : {}),
|
|
246
|
-
invocation: Object.freeze([...parsed.invocation]),
|
|
247
|
-
}
|
|
248
|
-
: { nativeId };
|
|
249
|
-
Object.defineProperty(artifact, "fingerprint", { value: normalizeSignature(body) });
|
|
250
|
-
return artifact;
|
|
251
|
-
}
|
|
252
|
-
function isCronBindingSnapshot(value) {
|
|
253
|
-
return (typeof value === "object" &&
|
|
254
|
-
value !== null &&
|
|
255
|
-
value.kind === CRON_SNAPSHOT &&
|
|
256
|
-
typeof value.crontab === "string");
|
|
257
|
-
}
|
|
258
|
-
function buildCronLineParts(task, akmArgv, logDir, contextPath, _target) {
|
|
259
|
-
const spec = parseSchedule(task.cron, "cron");
|
|
260
|
-
const cronExpr = translateToCron(spec);
|
|
124
|
+
function buildCronLineParts(task, akmArgv, logDir, environment) {
|
|
125
|
+
const cronExpr = translateToCron(parseSchedule(task.cron, "cron"));
|
|
261
126
|
const nativeId = schedulerBindingNativeId(task);
|
|
262
127
|
const logPath = path.join(logDir, `${nativeId}.log`);
|
|
263
|
-
const
|
|
264
|
-
const
|
|
128
|
+
const argv = buildScheduledInvocation(akmArgv, task.invocation);
|
|
129
|
+
const variables = scheduledRowEnvironmentEntries(environment);
|
|
130
|
+
// An assignment prefix: the shell cron hands the line to sets it for this
|
|
131
|
+
// command only. Only the value is quoted, or the word is not an assignment.
|
|
132
|
+
const assignments = variables.map(([name, value]) => `${name}=${quoteForCron(value)} `).join("");
|
|
133
|
+
const cmd = argv.map((part) => quoteForCron(part)).join(" ");
|
|
265
134
|
// #951: truncate (not append) so this bootstrap safety-net file always
|
|
266
|
-
// holds exactly the latest run's raw output
|
|
267
|
-
//
|
|
268
|
-
const directLine = `${cronExpr} ${cmd} > ${quoteForCron(logPath)} 2>&1`;
|
|
269
|
-
if (Buffer.byteLength(directLine, "utf8") <= PORTABLE_CRON_LINE_LIMIT)
|
|
135
|
+
// holds exactly the latest run's raw output. akm's own per-run log
|
|
136
|
+
// (src/tasks/run/task-log.ts) keeps history.
|
|
137
|
+
const directLine = `${cronExpr} ${assignments}${cmd} > ${quoteForCron(logPath)} 2>&1`;
|
|
138
|
+
if (Buffer.byteLength(directLine, "utf8") <= PORTABLE_CRON_LINE_LIMIT)
|
|
270
139
|
return { line: directLine };
|
|
271
|
-
|
|
272
|
-
|
|
140
|
+
// A command over vixie-cron's MAX_COMMAND is spilled into a short wrapper
|
|
141
|
+
// script under logDir instead of being truncated or refused.
|
|
142
|
+
const exports = variables.map(([name, value]) => `export ${name}=${quoteForShellScript(value)}\n`).join("");
|
|
143
|
+
const content = `#!/bin/sh\n${exports}exec ${argv.map(quoteForShellScript).join(" ")}\n`;
|
|
273
144
|
const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
274
145
|
const wrapperPath = path.join(logDir, `${CRON_WRAPPER_PREFIX}${nativeId}-${contentHash}.sh`);
|
|
275
146
|
const line = `${cronExpr} sh ${quoteForCron(wrapperPath)} > ${quoteForCron(logPath)} 2>&1`;
|
|
276
147
|
return { line, wrapper: { path: wrapperPath, content } };
|
|
277
148
|
}
|
|
278
|
-
const CRON_WRAPPER_PREFIX = ".akm-cron-wrapper-";
|
|
279
149
|
function quoteForShellScript(part) {
|
|
280
150
|
if (/^[A-Za-z0-9_\-./@:=+,]+$/.test(part))
|
|
281
151
|
return part;
|
|
282
152
|
return `'${part.replace(/'/g, `'\\''`)}'`;
|
|
283
153
|
}
|
|
284
|
-
function
|
|
285
|
-
|
|
286
|
-
return `#!/bin/sh\nexec ${cmd}\n`;
|
|
287
|
-
}
|
|
288
|
-
export function buildCronLine(task, akmArgv, logDir, contextPath, _target) {
|
|
289
|
-
return buildCronLineParts(task, akmArgv, logDir, contextPath, _target).line;
|
|
154
|
+
export function buildCronLine(task, akmArgv, logDir, environment) {
|
|
155
|
+
return buildCronLineParts(task, akmArgv, logDir, environment).line;
|
|
290
156
|
}
|
|
291
157
|
/** The crontab line as it appears inside a block — commented when disabled. */
|
|
292
|
-
export function cronBlockBody(cronLine, enabled
|
|
293
|
-
|
|
294
|
-
cronLine,
|
|
295
|
-
...(executionEvidenceDigest === undefined
|
|
296
|
-
? []
|
|
297
|
-
: [`# akm:workflow-evidence ${assertSchedulerExecutionEvidenceDigest(executionEvidenceDigest)}`]),
|
|
298
|
-
];
|
|
299
|
-
return (enabled ? lines : lines.map((line) => `${DISABLED_PREFIX}${line}`)).join("\n");
|
|
158
|
+
export function cronBlockBody(cronLine, enabled) {
|
|
159
|
+
return enabled ? cronLine : `${DISABLED_PREFIX}${cronLine}`;
|
|
300
160
|
}
|
|
301
|
-
export function renderBlock(id, cronLine, enabled
|
|
302
|
-
return [BEGIN(id), cronBlockBody(cronLine, enabled
|
|
161
|
+
export function renderBlock(id, cronLine, enabled) {
|
|
162
|
+
return [BEGIN(id), cronBlockBody(cronLine, enabled), END(id)].join("\n");
|
|
303
163
|
}
|
|
304
|
-
/**
|
|
305
|
-
* Parse the akm-owned blocks out of a crontab, returning each task id with the
|
|
306
|
-
* raw body line(s) between its BEGIN/END markers. Used by `list()` to build a
|
|
307
|
-
* drift signature, and exported for tests.
|
|
308
|
-
*/
|
|
164
|
+
/** The akm-owned blocks of a crontab: each id with the raw body between its BEGIN/END markers. */
|
|
309
165
|
export function listBlocks(existing) {
|
|
310
166
|
return parseBlocks(existing).map(({ id, body }) => ({ id, body }));
|
|
311
167
|
}
|
|
@@ -345,69 +201,78 @@ function parseBlocks(existing) {
|
|
|
345
201
|
function malformedBlockError(id) {
|
|
346
202
|
return new ConfigError(`Crontab contains a malformed akm task block for "${id}"; refusing to modify it.`, "INVALID_CONFIG_FILE");
|
|
347
203
|
}
|
|
348
|
-
/**
|
|
349
|
-
* Recover the bundle name from an installed cron block body by reading the
|
|
350
|
-
* `--bundle <bundle>` token embedded in the scheduled `akm task run …`
|
|
351
|
-
* invocation. Returns undefined for the byte-identical primary/default form
|
|
352
|
-
* (no `--bundle`). Bundle slugs never contain whitespace (config-schema
|
|
353
|
-
* `isBundleSlug`), so the quoted token is a single whitespace-delimited field
|
|
354
|
-
* even when cron-quoted — a plain field split recovers it, and
|
|
355
|
-
* the shell-word parser below reverses the quoting.
|
|
356
|
-
*/
|
|
204
|
+
/** The `--bundle <bundle>` token of an installed cron body; undefined for the primary/default form. */
|
|
357
205
|
export function extractInstalledTarget(body) {
|
|
358
206
|
return extractCronInvocation(body)?.target;
|
|
359
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Parse an installed cron body: its inline `VAR=value` environment, its
|
|
210
|
+
* launcher, and its public tail — a current row, a row naming a
|
|
211
|
+
* `--scheduler-context` descriptor (0.9.0 – 0.9.17-alpha.6), or an older
|
|
212
|
+
* row with neither (#881). This only ever runs on a body already isolated
|
|
213
|
+
* between akm's own BEGIN/END markers, so no trust extends to unmarked lines.
|
|
214
|
+
*/
|
|
360
215
|
export function extractCronInvocation(body) {
|
|
361
216
|
const line = body.startsWith(DISABLED_PREFIX) ? body.slice(DISABLED_PREFIX.length) : body;
|
|
362
217
|
const fields = splitCronShellWords(line);
|
|
363
218
|
if (fields.length < 6)
|
|
364
219
|
return undefined;
|
|
365
220
|
let commandStart = 5;
|
|
366
|
-
|
|
221
|
+
const variables = {};
|
|
222
|
+
for (let field = fields[commandStart]; field !== undefined; field = fields[commandStart]) {
|
|
223
|
+
const assignment = /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s.exec(field);
|
|
224
|
+
if (!assignment)
|
|
225
|
+
break;
|
|
226
|
+
variables[assignment[1]] = assignment[2];
|
|
367
227
|
commandStart += 1;
|
|
368
|
-
|
|
369
|
-
//
|
|
370
|
-
// including ones a still-running older akm binary installs during a
|
|
371
|
-
// rolling upgrade — looks like on disk.
|
|
228
|
+
}
|
|
229
|
+
// #951: new rows redirect with `>`; rows written before that use `>>`.
|
|
372
230
|
const redirectIndex = fields.findIndex((field, index) => index >= commandStart && (field === ">" || field === ">>"));
|
|
373
231
|
if (redirectIndex === -1)
|
|
374
232
|
return undefined;
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
if (parsed)
|
|
378
|
-
return parsed;
|
|
379
|
-
// Rows written by akm < 0.9.2 (before `--scheduler-context` existed) have
|
|
380
|
-
// no context argument at all — just the akm argv immediately followed by
|
|
381
|
-
// the public `task run …` / `workflow run …` tail. `extractCronInvocation`
|
|
382
|
-
// only ever runs on a body already isolated between this backend's own
|
|
383
|
-
// `# akm:task … BEGIN/END` sentinels (see `parseBlocks`), so recognizing
|
|
384
|
-
// this older shape here doesn't extend trust to any unmarked crontab
|
|
385
|
-
// line — it only lets sync see and reconcile a row akm already owns
|
|
386
|
-
// instead of treating it as absent and colliding with the still-present
|
|
387
|
-
// artifact (#881). Guarded on the marker's absence so a row that DOES
|
|
388
|
-
// carry `--scheduler-context` but fails to parse for some other reason
|
|
389
|
-
// is never silently reinterpreted as legacy.
|
|
390
|
-
if (tail.includes(SCHEDULER_CONTEXT_ARG))
|
|
233
|
+
const parsed = parseScheduledInvocationArgv(fields.slice(commandStart, redirectIndex));
|
|
234
|
+
if (!parsed)
|
|
391
235
|
return undefined;
|
|
392
|
-
|
|
236
|
+
const environment = scheduledRowEnvironmentFrom(variables);
|
|
237
|
+
return environment ? { ...parsed, environment } : parsed;
|
|
393
238
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
239
|
+
/**
|
|
240
|
+
* Parse a row spilled into a wrapper script (`sh <script> > <log> 2>&1`):
|
|
241
|
+
* the script's `export NAME=value` lines and its `exec` argv. Only akm's own
|
|
242
|
+
* wrapper scripts are read; one that is gone or does not parse leaves the
|
|
243
|
+
* row unlisted, as any unparsable row is.
|
|
244
|
+
*/
|
|
245
|
+
export function extractCronWrapperInvocation(body, readFile) {
|
|
246
|
+
const line = body.startsWith(DISABLED_PREFIX) ? body.slice(DISABLED_PREFIX.length) : body;
|
|
247
|
+
const fields = splitCronShellWords(line);
|
|
248
|
+
const script = fields[6];
|
|
249
|
+
if (fields[5] !== "sh" || !script || !path.basename(script).startsWith(CRON_WRAPPER_PREFIX))
|
|
250
|
+
return undefined;
|
|
251
|
+
if (fields[7] !== ">" && fields[7] !== ">>")
|
|
252
|
+
return undefined;
|
|
253
|
+
let content;
|
|
254
|
+
try {
|
|
255
|
+
content = readFile(script);
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
const variables = {};
|
|
261
|
+
let parsed;
|
|
262
|
+
for (const scriptLine of content.split("\n")) {
|
|
263
|
+
const words = splitCronShellWords(scriptLine);
|
|
264
|
+
const assignment = words[0] === "export" ? /^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/s.exec(words[1] ?? "") : null;
|
|
265
|
+
if (assignment)
|
|
266
|
+
variables[assignment[1]] = assignment[2];
|
|
267
|
+
else if (words[0] === "exec")
|
|
268
|
+
parsed = parseScheduledInvocationArgv(words.slice(1));
|
|
407
269
|
}
|
|
408
|
-
|
|
270
|
+
if (!parsed)
|
|
271
|
+
return undefined;
|
|
272
|
+
const environment = scheduledRowEnvironmentFrom(variables);
|
|
273
|
+
return environment ? { ...parsed, environment } : parsed;
|
|
409
274
|
}
|
|
410
|
-
/** Reverse {@link quoteForCron}
|
|
275
|
+
/** Reverse {@link quoteForCron}: sh word splitting, where a backslash is literal inside single quotes. */
|
|
411
276
|
function splitCronShellWords(value) {
|
|
412
277
|
const words = [];
|
|
413
278
|
let current = "";
|
|
@@ -418,7 +283,7 @@ function splitCronShellWords(value) {
|
|
|
418
283
|
quoted = !quoted;
|
|
419
284
|
continue;
|
|
420
285
|
}
|
|
421
|
-
if (char === "\\" && index + 1 < value.length) {
|
|
286
|
+
if (!quoted && char === "\\" && index + 1 < value.length) {
|
|
422
287
|
current += value[index + 1];
|
|
423
288
|
index += 1;
|
|
424
289
|
continue;
|
|
@@ -439,14 +304,10 @@ function splitCronShellWords(value) {
|
|
|
439
304
|
function normalizeSignature(body) {
|
|
440
305
|
return body
|
|
441
306
|
.split(/\r?\n/)
|
|
442
|
-
.map((
|
|
443
|
-
.filter((
|
|
444
|
-
.filter((l) => l.length > 0)
|
|
307
|
+
.map((line) => line.trim())
|
|
308
|
+
.filter((line) => line.length > 0)
|
|
445
309
|
.join("\n");
|
|
446
310
|
}
|
|
447
|
-
function stripDisabledPrefix(line) {
|
|
448
|
-
return line.startsWith(DISABLED_PREFIX) ? line.slice(DISABLED_PREFIX.length) : line;
|
|
449
|
-
}
|
|
450
311
|
function cronIsDisabled(body) {
|
|
451
312
|
return body.trimStart().startsWith(DISABLED_PREFIX);
|
|
452
313
|
}
|
|
@@ -462,17 +323,55 @@ export function removeBlock(existing, id) {
|
|
|
462
323
|
if (blocks.length === 0)
|
|
463
324
|
return existing;
|
|
464
325
|
const out = lines.filter((_, index) => !blocks.some((block) => index >= block.start && index <= block.end));
|
|
465
|
-
// Collapse trailing blank lines.
|
|
466
326
|
while (out.length > 0 && out[out.length - 1] === "")
|
|
467
327
|
out.pop();
|
|
468
328
|
return out.join("\n");
|
|
469
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* akm's managed environment section: one `PATH=` line directly above the
|
|
332
|
+
* first akm task block. Cron applies an environment assignment to every
|
|
333
|
+
* command line after it, which is why the line sits immediately above akm's
|
|
334
|
+
* own rows rather than at the top of the crontab; it is plain text a person
|
|
335
|
+
* can edit with `crontab -e`, rewritten on every akm crontab write and
|
|
336
|
+
* removed together with the last akm task block.
|
|
337
|
+
*/
|
|
338
|
+
export function renderEnvBlock(envPath) {
|
|
339
|
+
return [ENV_BEGIN, `PATH=${assertCronValue(dedupeSearchPath(envPath))}`, ENV_END].join("\n");
|
|
340
|
+
}
|
|
341
|
+
export function upsertEnvBlock(existing, envPath) {
|
|
342
|
+
const lines = removeEnvBlock(existing).split(/\r?\n/);
|
|
343
|
+
const firstBlock = lines.findIndex((line) => BLOCK_RE.test(line));
|
|
344
|
+
if (!envPath || firstBlock === -1)
|
|
345
|
+
return lines.join("\n");
|
|
346
|
+
return [...lines.slice(0, firstBlock), renderEnvBlock(envPath), ...lines.slice(firstBlock)].join("\n");
|
|
347
|
+
}
|
|
348
|
+
export function removeEnvBlock(existing) {
|
|
349
|
+
const lines = existing.split(/\r?\n/);
|
|
350
|
+
const start = lines.indexOf(ENV_BEGIN);
|
|
351
|
+
if (start === -1)
|
|
352
|
+
return existing;
|
|
353
|
+
const end = lines.indexOf(ENV_END, start);
|
|
354
|
+
if (end === -1) {
|
|
355
|
+
throw new ConfigError("Crontab contains a malformed akm environment section; refusing to modify it.", "INVALID_CONFIG_FILE");
|
|
356
|
+
}
|
|
357
|
+
return [...lines.slice(0, start), ...lines.slice(end + 1)].join("\n");
|
|
358
|
+
}
|
|
359
|
+
function dedupeSearchPath(value) {
|
|
360
|
+
const seen = new Set();
|
|
361
|
+
const entries = [];
|
|
362
|
+
for (const entry of value.split(path.delimiter)) {
|
|
363
|
+
if (entry.length === 0 || seen.has(entry))
|
|
364
|
+
continue;
|
|
365
|
+
seen.add(entry);
|
|
366
|
+
entries.push(entry);
|
|
367
|
+
}
|
|
368
|
+
return entries.join(path.delimiter);
|
|
369
|
+
}
|
|
470
370
|
export function toggleBlock(existing, id, enabled) {
|
|
471
371
|
parseBlocks(existing);
|
|
472
|
-
const lines = existing.split(/\r?\n/);
|
|
473
372
|
const out = [];
|
|
474
373
|
let inBlock = false;
|
|
475
|
-
for (const line of
|
|
374
|
+
for (const line of existing.split(/\r?\n/)) {
|
|
476
375
|
if (!inBlock && line === BEGIN(id)) {
|
|
477
376
|
inBlock = true;
|
|
478
377
|
out.push(line);
|
|
@@ -485,15 +384,12 @@ export function toggleBlock(existing, id, enabled) {
|
|
|
485
384
|
}
|
|
486
385
|
if (inBlock) {
|
|
487
386
|
const isComment = line.startsWith(DISABLED_PREFIX);
|
|
488
|
-
if (enabled && isComment)
|
|
387
|
+
if (enabled && isComment)
|
|
489
388
|
out.push(line.slice(DISABLED_PREFIX.length));
|
|
490
|
-
|
|
491
|
-
else if (!enabled && !isComment) {
|
|
389
|
+
else if (!enabled && !isComment)
|
|
492
390
|
out.push(`${DISABLED_PREFIX}${line}`);
|
|
493
|
-
|
|
494
|
-
else {
|
|
391
|
+
else
|
|
495
392
|
out.push(line);
|
|
496
|
-
}
|
|
497
393
|
continue;
|
|
498
394
|
}
|
|
499
395
|
out.push(line);
|
|
@@ -529,17 +425,16 @@ function readCrontab(exec) {
|
|
|
529
425
|
const result = exec.read();
|
|
530
426
|
if (result.status === 0)
|
|
531
427
|
return result.stdout ?? "";
|
|
532
|
-
// The spawn itself failed to find the binary (ENOENT) —
|
|
533
|
-
//
|
|
428
|
+
// The spawn itself failed to find the binary (ENOENT) — the only case
|
|
429
|
+
// where "install/PATH the crontab binary" is the correct remedy.
|
|
534
430
|
if (result.enoent) {
|
|
535
431
|
throw new ConfigError("crontab -l failed: the `crontab` binary was not found on PATH.", "INVALID_CONFIG_FILE", "Install the `crontab` binary (e.g. cron/cronie/vixie-cron) or add one to PATH.");
|
|
536
432
|
}
|
|
537
433
|
// #910: a nonzero exit that says nothing at all, or whose stderr says
|
|
538
434
|
// "no crontab" (BSD's "no crontab for <user>"; a supercronic-managed PATH
|
|
539
435
|
// shim like OpenPalm's before any spool exists), is cron's own contract
|
|
540
|
-
// for "empty crontab"
|
|
541
|
-
//
|
|
542
|
-
// reported as what it said, below.
|
|
436
|
+
// for "empty crontab". A nonzero exit that DID say something else (a
|
|
437
|
+
// permission refusal) is reported as what it said.
|
|
543
438
|
const stderr = (result.stderr ?? "").trim();
|
|
544
439
|
if (((result.stdout ?? "").trim() === "" && stderr === "") || /no crontab/i.test(stderr))
|
|
545
440
|
return "";
|
|
@@ -552,6 +447,7 @@ function writeCrontab(exec, content) {
|
|
|
552
447
|
hint: "Ensure the `crontab` binary is on PATH and your shell can write the user crontab.",
|
|
553
448
|
});
|
|
554
449
|
}
|
|
450
|
+
/** Write the next crontab; when that fails, put the prior one back before rethrowing. */
|
|
555
451
|
function replaceCrontab(exec, existing, next) {
|
|
556
452
|
try {
|
|
557
453
|
writeCrontab(exec, next);
|
|
@@ -568,24 +464,17 @@ function replaceCrontab(exec, existing, next) {
|
|
|
568
464
|
}
|
|
569
465
|
}
|
|
570
466
|
function defaultCronExec() {
|
|
467
|
+
const run = (args, input) => {
|
|
468
|
+
const r = spawnSync("crontab", args, { encoding: "utf8", ...(input !== undefined ? { input } : {}) });
|
|
469
|
+
return {
|
|
470
|
+
status: r.status ?? 1,
|
|
471
|
+
stdout: r.stdout ?? "",
|
|
472
|
+
stderr: r.stderr ?? "",
|
|
473
|
+
enoent: r.error?.code === "ENOENT",
|
|
474
|
+
};
|
|
475
|
+
};
|
|
571
476
|
return {
|
|
572
|
-
read()
|
|
573
|
-
|
|
574
|
-
return {
|
|
575
|
-
status: r.status ?? 1,
|
|
576
|
-
stdout: r.stdout ?? "",
|
|
577
|
-
stderr: r.stderr ?? "",
|
|
578
|
-
enoent: r.error?.code === "ENOENT",
|
|
579
|
-
};
|
|
580
|
-
},
|
|
581
|
-
write(content) {
|
|
582
|
-
const r = spawnSync("crontab", ["-"], { encoding: "utf8", input: content });
|
|
583
|
-
return {
|
|
584
|
-
status: r.status ?? 1,
|
|
585
|
-
stdout: r.stdout ?? "",
|
|
586
|
-
stderr: r.stderr ?? "",
|
|
587
|
-
enoent: r.error?.code === "ENOENT",
|
|
588
|
-
};
|
|
589
|
-
},
|
|
477
|
+
read: () => run(["-l"]),
|
|
478
|
+
write: (content) => run(["-"], content),
|
|
590
479
|
};
|
|
591
480
|
}
|