akm-cli 0.9.17-alpha.3 → 0.9.17-alpha.4
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 +731 -0
- package/dist/akm +94 -196
- package/dist/cli/shared.js +6 -2
- package/dist/cli.js +22 -9
- 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 +2 -2
- package/dist/commands/health/checks.js +30 -75
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +0 -38
- package/dist/commands/health/improve-metrics.js +123 -562
- 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 +11 -106
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -73
- package/dist/commands/health.js +122 -143
- package/dist/commands/improve/consolidate/chunking.js +25 -100
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +538 -1075
- 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 -1075
- package/dist/commands/improve/eligibility.js +126 -400
- package/dist/commands/improve/execution.js +3 -5
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +29 -166
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +12 -7
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +443 -1063
- package/dist/commands/improve/ledger.js +114 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +459 -1172
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +14 -118
- package/dist/commands/improve/memory/memory-improve.js +4 -3
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -10
- package/dist/commands/improve/preparation.js +851 -2339
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +621 -1367
- package/dist/commands/improve/salience.js +46 -232
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +323 -0
- package/dist/commands/proposal/drain.js +251 -644
- package/dist/commands/proposal/proposal-cli.js +3 -18
- package/dist/commands/proposal/proposal-types.js +20 -41
- package/dist/commands/proposal/proposal.js +1 -2
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +502 -1487
- 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 +13 -89
- package/dist/commands/read/curate.js +63 -413
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +2 -13
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +7 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +2 -11
- 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/stash-cli.js +1 -1
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +9 -8
- package/dist/commands/tasks/tasks.js +326 -930
- package/dist/commands/tasks/validate.js +42 -21
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +0 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +11 -12
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +291 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +1 -33
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +163 -68
- 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 +21 -95
- package/dist/core/config/schema/improve.js +4 -42
- 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-lock.js +76 -173
- package/dist/core/logs-db.js +2 -2
- package/dist/core/paths.js +0 -27
- 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 +108 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +293 -1012
- 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/graph-db.js +106 -46
- package/dist/indexer/ensure-index.js +44 -85
- package/dist/indexer/graph/graph-extraction.js +340 -562
- package/dist/indexer/graph/graph-related.js +130 -0
- 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 +139 -151
- package/dist/indexer/indexer.js +524 -846
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +81 -90
- package/dist/indexer/passes/metadata.js +132 -200
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +1 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +181 -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 +2 -7
- 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/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +10 -15
- package/dist/llm/graph-extract.js +3 -12
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/metadata-enhance.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 +2 -34
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +60 -23
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +1 -2
- 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 +43937 -93313
- package/dist/scripts/akm-migrate.js +43697 -93071
- 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/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 +0 -17
- 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 +168 -0
- package/dist/storage/repositories/index-connection.js +34 -70
- package/dist/storage/repositories/index-entries-repository.js +69 -111
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +83 -269
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +192 -220
- 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 +35 -0
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- 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 +34 -27
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +129 -277
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +125 -745
- package/dist/tasks/backends/schtasks.js +101 -620
- 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/scheduler-binding.js +18 -238
- package/dist/tasks/scheduler-invocation.js +52 -52
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +361 -751
- package/dist/tasks/source/parse-task-source.js +160 -10
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +2 -2
- 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 +41 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
- package/docs/reference/cli.md +182 -125
- package/docs/reference/configuration.md +49 -56
- package/docs/reference/data-and-telemetry.md +19 -20
- package/docs/reference/tasks.md +86 -38
- 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 +87 -406
- 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 -146
- package/dist/commands/improve/distill/promote-memory.js +0 -329
- package/dist/commands/improve/distill/quality-gate.js +0 -500
- 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 -21
- 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/config/retired-experimental-keys-shim.js +0 -62
- 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 -441
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- 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/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/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 -78
- 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,13 @@
|
|
|
7
7
|
// its other lines untouched:
|
|
8
8
|
//
|
|
9
9
|
// # akm:task <id> BEGIN
|
|
10
|
-
// [SCHED] /abs/akm task run <id> >
|
|
10
|
+
// [SCHED] /abs/akm --scheduler-context <descriptor> task run <id> ... > <log> 2>&1
|
|
11
11
|
// # akm:task <id> END
|
|
12
12
|
//
|
|
13
13
|
// The backend reads/writes the user's crontab via `crontab -l` and
|
|
14
|
-
// `crontab -`.
|
|
14
|
+
// `crontab -`. Every mutation is one read → modify the akm blocks (and the
|
|
15
|
+
// `# akm:env` PATH header) → one write, with the prior crontab restored when
|
|
16
|
+
// the write fails. Disabling a task comments the entry with `# akm:disabled `
|
|
15
17
|
// rather than removing it, so re-enabling preserves the original schedule.
|
|
16
18
|
//
|
|
17
19
|
// Platform notes:
|
|
@@ -35,17 +37,18 @@ import { ConfigError } from "../../core/errors.js";
|
|
|
35
37
|
import { getTaskLogDir } from "../../core/paths.js";
|
|
36
38
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
37
39
|
import { parseSchedule, translateToCron } from "../schedule.js";
|
|
38
|
-
import {
|
|
40
|
+
import { schedulerBindingNativeId, schedulerLogicalBindingId } from "../scheduler-binding.js";
|
|
39
41
|
import { buildScheduledBindingInvocation, parsePublicSchedulerInvocation, parseScheduledBindingArgv, resolveScheduledTaskContext, SCHEDULER_CONTEXT_ARG, schedulerContextDescriptor, schedulerContextPath, } from "../scheduler-invocation.js";
|
|
40
42
|
import { nodeFs, throwIfNotOk } from "./exec-utils.js";
|
|
41
43
|
const BEGIN = (id) => `# akm:task ${assertCronValue(id)} BEGIN`;
|
|
42
44
|
const END = (id) => `# akm:task ${assertCronValue(id)} END`;
|
|
43
45
|
const DISABLED_PREFIX = "# akm:disabled ";
|
|
44
|
-
const SOURCE_ENABLED_PREFIX = "# akm:source-enabled ";
|
|
45
46
|
const BLOCK_RE = /^# akm:task ([\w.@:_-]+) BEGIN$/;
|
|
46
47
|
const BLOCK_END_RE = /^# akm:task ([\w.@:_-]+) END$/;
|
|
48
|
+
const ENV_BEGIN = "# akm:env BEGIN";
|
|
49
|
+
const ENV_END = "# akm:env END";
|
|
47
50
|
export const PORTABLE_CRON_LINE_LIMIT = 1000;
|
|
48
|
-
const
|
|
51
|
+
const CRON_WRAPPER_PREFIX = ".akm-cron-wrapper-";
|
|
49
52
|
export function CRON_BACKEND(options = {}) {
|
|
50
53
|
const exec = options.exec ?? defaultCronExec();
|
|
51
54
|
const fsLike = options.fs ?? nodeFs();
|
|
@@ -53,259 +56,103 @@ export function CRON_BACKEND(options = {}) {
|
|
|
53
56
|
const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
|
|
54
57
|
const envPath = options.envPath === false ? undefined : (options.envPath ?? process.env.PATH);
|
|
55
58
|
const scheduledContext = options.scheduledContext ?? resolveScheduledTaskContext();
|
|
56
|
-
const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext
|
|
59
|
+
const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext));
|
|
60
|
+
const lineFor = (task, opts) => buildCronLineParts(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath);
|
|
57
61
|
return {
|
|
58
62
|
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);
|
|
63
|
+
install(task, opts) {
|
|
64
|
+
const parts = lineFor(task, opts);
|
|
65
|
+
assertPortableCronLine(parts.line);
|
|
66
|
+
// The redirect target's parent directory must exist or the surrounding
|
|
67
|
+
// shell fails the entire entry.
|
|
68
68
|
fsLike.ensureDir(logDir);
|
|
69
|
-
if (
|
|
69
|
+
if (parts.wrapper) {
|
|
70
70
|
if (!fsLike.writeFile) {
|
|
71
71
|
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
72
|
}
|
|
73
|
-
fsLike.writeFile(
|
|
73
|
+
fsLike.writeFile(parts.wrapper.path, parts.wrapper.content);
|
|
74
74
|
}
|
|
75
75
|
const existing = readCrontab(exec);
|
|
76
76
|
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);
|
|
77
|
+
const block = renderBlock(nativeId, parts.line, task.enabled);
|
|
78
|
+
replaceCrontab(exec, existing, upsertEnvBlock(upsertBlock(existing, nativeId, block), envPath));
|
|
95
79
|
},
|
|
96
|
-
uninstall(nativeId
|
|
97
|
-
if (expected)
|
|
98
|
-
assertSchedulerExpectationIdentity({ ...expected, state: "present" });
|
|
80
|
+
uninstall(nativeId) {
|
|
99
81
|
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)
|
|
82
|
+
if (!listBlocks(existing).some((block) => block.id === nativeId))
|
|
108
83
|
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);
|
|
84
|
+
replaceCrontab(exec, existing, upsertEnvBlock(removeBlock(existing, nativeId), envPath));
|
|
114
85
|
},
|
|
115
86
|
setEnabled(nativeId, enabled) {
|
|
116
87
|
const existing = readCrontab(exec);
|
|
117
88
|
const next = toggleBlock(existing, nativeId, enabled);
|
|
118
|
-
|
|
89
|
+
if (next !== existing)
|
|
90
|
+
replaceCrontab(exec, existing, next);
|
|
119
91
|
},
|
|
120
92
|
list() {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
93
|
+
const rows = [];
|
|
94
|
+
for (const { id, body } of listBlocks(readCrontab(exec))) {
|
|
95
|
+
const parsed = extractCronInvocation(body);
|
|
96
|
+
if (!parsed)
|
|
97
|
+
continue;
|
|
98
|
+
rows.push({
|
|
99
|
+
id: schedulerLogicalBindingId(id, parsed.invocation),
|
|
100
|
+
nativeId: id,
|
|
101
|
+
enabled: !cronIsDisabled(body),
|
|
129
102
|
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.");
|
|
103
|
+
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
104
|
+
binding: parsed.binding,
|
|
105
|
+
// A pre-0.9.2 row has no descriptor of its own (#881); it reads as
|
|
106
|
+
// the current one so ownership resolves and sync rewrites it.
|
|
107
|
+
contextPath: parsed.contextPath || defaultContextPath,
|
|
108
|
+
invocation: parsed.invocation,
|
|
109
|
+
});
|
|
199
110
|
}
|
|
111
|
+
return rows;
|
|
200
112
|
},
|
|
201
113
|
expectedSignature(task, opts) {
|
|
202
|
-
const
|
|
203
|
-
assertPortableCronLine(
|
|
204
|
-
return normalizeSignature(cronBlockBody(
|
|
114
|
+
const line = lineFor(task, opts).line;
|
|
115
|
+
assertPortableCronLine(line);
|
|
116
|
+
return normalizeSignature(cronBlockBody(line, task.enabled));
|
|
205
117
|
},
|
|
206
118
|
};
|
|
207
119
|
}
|
|
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);
|
|
120
|
+
function buildCronLineParts(task, akmArgv, logDir, contextPath) {
|
|
121
|
+
const cronExpr = translateToCron(parseSchedule(task.cron, "cron"));
|
|
261
122
|
const nativeId = schedulerBindingNativeId(task);
|
|
262
123
|
const logPath = path.join(logDir, `${nativeId}.log`);
|
|
263
124
|
const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
|
|
264
125
|
const cmd = invocation.argv.map((part) => quoteForCron(part)).join(" ");
|
|
265
126
|
// #951: truncate (not append) so this bootstrap safety-net file always
|
|
266
|
-
// holds exactly the latest run's raw output
|
|
267
|
-
//
|
|
127
|
+
// holds exactly the latest run's raw output. akm's own per-run log
|
|
128
|
+
// (src/tasks/run/task-log.ts) keeps history.
|
|
268
129
|
const directLine = `${cronExpr} ${cmd} > ${quoteForCron(logPath)} 2>&1`;
|
|
269
|
-
if (Buffer.byteLength(directLine, "utf8") <= PORTABLE_CRON_LINE_LIMIT)
|
|
130
|
+
if (Buffer.byteLength(directLine, "utf8") <= PORTABLE_CRON_LINE_LIMIT)
|
|
270
131
|
return { line: directLine };
|
|
271
|
-
|
|
272
|
-
|
|
132
|
+
// A command over vixie-cron's MAX_COMMAND is spilled into a short wrapper
|
|
133
|
+
// script under logDir instead of being truncated or refused.
|
|
134
|
+
const content = `#!/bin/sh\nexec ${invocation.argv.map(quoteForShellScript).join(" ")}\n`;
|
|
273
135
|
const contentHash = createHash("sha256").update(content).digest("hex").slice(0, 16);
|
|
274
136
|
const wrapperPath = path.join(logDir, `${CRON_WRAPPER_PREFIX}${nativeId}-${contentHash}.sh`);
|
|
275
137
|
const line = `${cronExpr} sh ${quoteForCron(wrapperPath)} > ${quoteForCron(logPath)} 2>&1`;
|
|
276
138
|
return { line, wrapper: { path: wrapperPath, content } };
|
|
277
139
|
}
|
|
278
|
-
const CRON_WRAPPER_PREFIX = ".akm-cron-wrapper-";
|
|
279
140
|
function quoteForShellScript(part) {
|
|
280
141
|
if (/^[A-Za-z0-9_\-./@:=+,]+$/.test(part))
|
|
281
142
|
return part;
|
|
282
143
|
return `'${part.replace(/'/g, `'\\''`)}'`;
|
|
283
144
|
}
|
|
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;
|
|
145
|
+
export function buildCronLine(task, akmArgv, logDir, contextPath) {
|
|
146
|
+
return buildCronLineParts(task, akmArgv, logDir, contextPath).line;
|
|
290
147
|
}
|
|
291
148
|
/** 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");
|
|
149
|
+
export function cronBlockBody(cronLine, enabled) {
|
|
150
|
+
return enabled ? cronLine : `${DISABLED_PREFIX}${cronLine}`;
|
|
300
151
|
}
|
|
301
|
-
export function renderBlock(id, cronLine, enabled
|
|
302
|
-
return [BEGIN(id), cronBlockBody(cronLine, enabled
|
|
152
|
+
export function renderBlock(id, cronLine, enabled) {
|
|
153
|
+
return [BEGIN(id), cronBlockBody(cronLine, enabled), END(id)].join("\n");
|
|
303
154
|
}
|
|
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
|
-
*/
|
|
155
|
+
/** The akm-owned blocks of a crontab: each id with the raw body between its BEGIN/END markers. */
|
|
309
156
|
export function listBlocks(existing) {
|
|
310
157
|
return parseBlocks(existing).map(({ id, body }) => ({ id, body }));
|
|
311
158
|
}
|
|
@@ -345,15 +192,7 @@ function parseBlocks(existing) {
|
|
|
345
192
|
function malformedBlockError(id) {
|
|
346
193
|
return new ConfigError(`Crontab contains a malformed akm task block for "${id}"; refusing to modify it.`, "INVALID_CONFIG_FILE");
|
|
347
194
|
}
|
|
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
|
-
*/
|
|
195
|
+
/** The `--bundle <bundle>` token of an installed cron body; undefined for the primary/default form. */
|
|
357
196
|
export function extractInstalledTarget(body) {
|
|
358
197
|
return extractCronInvocation(body)?.target;
|
|
359
198
|
}
|
|
@@ -365,10 +204,7 @@ export function extractCronInvocation(body) {
|
|
|
365
204
|
let commandStart = 5;
|
|
366
205
|
while (commandStart < fields.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(fields[commandStart]))
|
|
367
206
|
commandStart += 1;
|
|
368
|
-
// #951:
|
|
369
|
-
// (append) too, since that's what every row written before this change —
|
|
370
|
-
// including ones a still-running older akm binary installs during a
|
|
371
|
-
// rolling upgrade — looks like on disk.
|
|
207
|
+
// #951: new rows redirect with `>`; rows written before that use `>>`.
|
|
372
208
|
const redirectIndex = fields.findIndex((field, index) => index >= commandStart && (field === ">" || field === ">>"));
|
|
373
209
|
if (redirectIndex === -1)
|
|
374
210
|
return undefined;
|
|
@@ -376,22 +212,14 @@ export function extractCronInvocation(body) {
|
|
|
376
212
|
const parsed = parseScheduledBindingArgv(tail);
|
|
377
213
|
if (parsed)
|
|
378
214
|
return parsed;
|
|
379
|
-
// Rows written by akm < 0.9.2
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
//
|
|
383
|
-
//
|
|
384
|
-
//
|
|
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.
|
|
215
|
+
// Rows written by akm < 0.9.2 have no `--scheduler-context` argument at
|
|
216
|
+
// all — the akm argv is followed directly by the public `task run …` /
|
|
217
|
+
// `workflow run …` tail (#881). This only ever runs on a body already
|
|
218
|
+
// isolated between akm's own BEGIN/END markers, so recognizing the older
|
|
219
|
+
// shape extends no trust to unmarked crontab lines. A row that DOES carry
|
|
220
|
+
// the marker but fails to parse is never reinterpreted as legacy.
|
|
390
221
|
if (tail.includes(SCHEDULER_CONTEXT_ARG))
|
|
391
222
|
return undefined;
|
|
392
|
-
return extractLegacyCronInvocation(tail);
|
|
393
|
-
}
|
|
394
|
-
function extractLegacyCronInvocation(tail) {
|
|
395
223
|
for (let index = 0; index < tail.length - 1; index += 1) {
|
|
396
224
|
if ((tail[index] === "task" || tail[index] === "workflow") && tail[index + 1] === "run") {
|
|
397
225
|
const publicInvocation = parsePublicSchedulerInvocation(tail.slice(index));
|
|
@@ -439,14 +267,10 @@ function splitCronShellWords(value) {
|
|
|
439
267
|
function normalizeSignature(body) {
|
|
440
268
|
return body
|
|
441
269
|
.split(/\r?\n/)
|
|
442
|
-
.map((
|
|
443
|
-
.filter((
|
|
444
|
-
.filter((l) => l.length > 0)
|
|
270
|
+
.map((line) => line.trim())
|
|
271
|
+
.filter((line) => line.length > 0)
|
|
445
272
|
.join("\n");
|
|
446
273
|
}
|
|
447
|
-
function stripDisabledPrefix(line) {
|
|
448
|
-
return line.startsWith(DISABLED_PREFIX) ? line.slice(DISABLED_PREFIX.length) : line;
|
|
449
|
-
}
|
|
450
274
|
function cronIsDisabled(body) {
|
|
451
275
|
return body.trimStart().startsWith(DISABLED_PREFIX);
|
|
452
276
|
}
|
|
@@ -462,17 +286,55 @@ export function removeBlock(existing, id) {
|
|
|
462
286
|
if (blocks.length === 0)
|
|
463
287
|
return existing;
|
|
464
288
|
const out = lines.filter((_, index) => !blocks.some((block) => index >= block.start && index <= block.end));
|
|
465
|
-
// Collapse trailing blank lines.
|
|
466
289
|
while (out.length > 0 && out[out.length - 1] === "")
|
|
467
290
|
out.pop();
|
|
468
291
|
return out.join("\n");
|
|
469
292
|
}
|
|
293
|
+
/**
|
|
294
|
+
* akm's managed environment section: one `PATH=` line directly above the
|
|
295
|
+
* first akm task block. Cron applies an environment assignment to every
|
|
296
|
+
* command line after it, which is why the line sits immediately above akm's
|
|
297
|
+
* own rows rather than at the top of the crontab; it is plain text a person
|
|
298
|
+
* can edit with `crontab -e`, rewritten on every akm crontab write and
|
|
299
|
+
* removed together with the last akm task block.
|
|
300
|
+
*/
|
|
301
|
+
export function renderEnvBlock(envPath) {
|
|
302
|
+
return [ENV_BEGIN, `PATH=${assertCronValue(dedupeSearchPath(envPath))}`, ENV_END].join("\n");
|
|
303
|
+
}
|
|
304
|
+
export function upsertEnvBlock(existing, envPath) {
|
|
305
|
+
const lines = removeEnvBlock(existing).split(/\r?\n/);
|
|
306
|
+
const firstBlock = lines.findIndex((line) => BLOCK_RE.test(line));
|
|
307
|
+
if (!envPath || firstBlock === -1)
|
|
308
|
+
return lines.join("\n");
|
|
309
|
+
return [...lines.slice(0, firstBlock), renderEnvBlock(envPath), ...lines.slice(firstBlock)].join("\n");
|
|
310
|
+
}
|
|
311
|
+
export function removeEnvBlock(existing) {
|
|
312
|
+
const lines = existing.split(/\r?\n/);
|
|
313
|
+
const start = lines.indexOf(ENV_BEGIN);
|
|
314
|
+
if (start === -1)
|
|
315
|
+
return existing;
|
|
316
|
+
const end = lines.indexOf(ENV_END, start);
|
|
317
|
+
if (end === -1) {
|
|
318
|
+
throw new ConfigError("Crontab contains a malformed akm environment section; refusing to modify it.", "INVALID_CONFIG_FILE");
|
|
319
|
+
}
|
|
320
|
+
return [...lines.slice(0, start), ...lines.slice(end + 1)].join("\n");
|
|
321
|
+
}
|
|
322
|
+
function dedupeSearchPath(value) {
|
|
323
|
+
const seen = new Set();
|
|
324
|
+
const entries = [];
|
|
325
|
+
for (const entry of value.split(path.delimiter)) {
|
|
326
|
+
if (entry.length === 0 || seen.has(entry))
|
|
327
|
+
continue;
|
|
328
|
+
seen.add(entry);
|
|
329
|
+
entries.push(entry);
|
|
330
|
+
}
|
|
331
|
+
return entries.join(path.delimiter);
|
|
332
|
+
}
|
|
470
333
|
export function toggleBlock(existing, id, enabled) {
|
|
471
334
|
parseBlocks(existing);
|
|
472
|
-
const lines = existing.split(/\r?\n/);
|
|
473
335
|
const out = [];
|
|
474
336
|
let inBlock = false;
|
|
475
|
-
for (const line of
|
|
337
|
+
for (const line of existing.split(/\r?\n/)) {
|
|
476
338
|
if (!inBlock && line === BEGIN(id)) {
|
|
477
339
|
inBlock = true;
|
|
478
340
|
out.push(line);
|
|
@@ -485,15 +347,12 @@ export function toggleBlock(existing, id, enabled) {
|
|
|
485
347
|
}
|
|
486
348
|
if (inBlock) {
|
|
487
349
|
const isComment = line.startsWith(DISABLED_PREFIX);
|
|
488
|
-
if (enabled && isComment)
|
|
350
|
+
if (enabled && isComment)
|
|
489
351
|
out.push(line.slice(DISABLED_PREFIX.length));
|
|
490
|
-
|
|
491
|
-
else if (!enabled && !isComment) {
|
|
352
|
+
else if (!enabled && !isComment)
|
|
492
353
|
out.push(`${DISABLED_PREFIX}${line}`);
|
|
493
|
-
|
|
494
|
-
else {
|
|
354
|
+
else
|
|
495
355
|
out.push(line);
|
|
496
|
-
}
|
|
497
356
|
continue;
|
|
498
357
|
}
|
|
499
358
|
out.push(line);
|
|
@@ -529,17 +388,16 @@ function readCrontab(exec) {
|
|
|
529
388
|
const result = exec.read();
|
|
530
389
|
if (result.status === 0)
|
|
531
390
|
return result.stdout ?? "";
|
|
532
|
-
// The spawn itself failed to find the binary (ENOENT) —
|
|
533
|
-
//
|
|
391
|
+
// The spawn itself failed to find the binary (ENOENT) — the only case
|
|
392
|
+
// where "install/PATH the crontab binary" is the correct remedy.
|
|
534
393
|
if (result.enoent) {
|
|
535
394
|
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
395
|
}
|
|
537
396
|
// #910: a nonzero exit that says nothing at all, or whose stderr says
|
|
538
397
|
// "no crontab" (BSD's "no crontab for <user>"; a supercronic-managed PATH
|
|
539
398
|
// 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.
|
|
399
|
+
// for "empty crontab". A nonzero exit that DID say something else (a
|
|
400
|
+
// permission refusal) is reported as what it said.
|
|
543
401
|
const stderr = (result.stderr ?? "").trim();
|
|
544
402
|
if (((result.stdout ?? "").trim() === "" && stderr === "") || /no crontab/i.test(stderr))
|
|
545
403
|
return "";
|
|
@@ -552,6 +410,7 @@ function writeCrontab(exec, content) {
|
|
|
552
410
|
hint: "Ensure the `crontab` binary is on PATH and your shell can write the user crontab.",
|
|
553
411
|
});
|
|
554
412
|
}
|
|
413
|
+
/** Write the next crontab; when that fails, put the prior one back before rethrowing. */
|
|
555
414
|
function replaceCrontab(exec, existing, next) {
|
|
556
415
|
try {
|
|
557
416
|
writeCrontab(exec, next);
|
|
@@ -568,24 +427,17 @@ function replaceCrontab(exec, existing, next) {
|
|
|
568
427
|
}
|
|
569
428
|
}
|
|
570
429
|
function defaultCronExec() {
|
|
430
|
+
const run = (args, input) => {
|
|
431
|
+
const r = spawnSync("crontab", args, { encoding: "utf8", ...(input !== undefined ? { input } : {}) });
|
|
432
|
+
return {
|
|
433
|
+
status: r.status ?? 1,
|
|
434
|
+
stdout: r.stdout ?? "",
|
|
435
|
+
stderr: r.stderr ?? "",
|
|
436
|
+
enoent: r.error?.code === "ENOENT",
|
|
437
|
+
};
|
|
438
|
+
};
|
|
571
439
|
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
|
-
},
|
|
440
|
+
read: () => run(["-l"]),
|
|
441
|
+
write: (content) => run(["-"], content),
|
|
590
442
|
};
|
|
591
443
|
}
|
|
@@ -51,11 +51,8 @@ export function spawnCommand(args) {
|
|
|
51
51
|
/**
|
|
52
52
|
* Throw a `ConfigError` (code `INVALID_CONFIG_FILE`) when `result` doesn't
|
|
53
53
|
* satisfy `isOk` (default: `status === 0`); otherwise return `result`
|
|
54
|
-
* unchanged
|
|
55
|
-
*
|
|
56
|
-
* schtasks backends — NOT the accumulate-into-`rollbackErrors`-and-continue
|
|
57
|
-
* idiom used during rollback, which stays inline (throwing there would
|
|
58
|
-
* change control flow: rollback needs to keep trying its remaining steps).
|
|
54
|
+
* unchanged: the "run a native scheduler command, throw a formatted error on
|
|
55
|
+
* a bad exit" idiom shared by the cron/launchd/schtasks backends.
|
|
59
56
|
*/
|
|
60
57
|
export function throwIfNotOk(result, options) {
|
|
61
58
|
const ok = options.isOk ? options.isOk(result) : result.status === 0;
|