akm-cli 0.9.17-alpha.2 → 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.
Files changed (343) hide show
  1. package/CHANGELOG.md +756 -0
  2. package/dist/akm +94 -196
  3. package/dist/cli/shared.js +6 -2
  4. package/dist/cli.js +22 -9
  5. package/dist/commands/agent/agent-dispatch.js +1 -1
  6. package/dist/commands/command/command-execution.js +24 -62
  7. package/dist/commands/feedback-cli.js +0 -1
  8. package/dist/commands/health/accept-rate.js +2 -2
  9. package/dist/commands/health/checks.js +30 -75
  10. package/dist/commands/health/config-skew.js +38 -0
  11. package/dist/commands/health/egress.js +54 -0
  12. package/dist/commands/health/html-report.js +0 -38
  13. package/dist/commands/health/improve-metrics.js +123 -562
  14. package/dist/commands/health/plugin-staleness.js +53 -3
  15. package/dist/commands/health/renderers.js +12 -4
  16. package/dist/commands/health/report-view-model.js +11 -106
  17. package/dist/commands/health/types-improve.js +4 -19
  18. package/dist/commands/health/windows.js +64 -73
  19. package/dist/commands/health.js +122 -143
  20. package/dist/commands/improve/consolidate/chunking.js +25 -100
  21. package/dist/commands/improve/consolidate/sanitize.js +54 -149
  22. package/dist/commands/improve/consolidate.js +538 -1075
  23. package/dist/commands/improve/content-hash.js +16 -24
  24. package/dist/commands/improve/distill/content-repair.js +18 -100
  25. package/dist/commands/improve/distill-guards.js +20 -81
  26. package/dist/commands/improve/distill-promotion-policy.js +23 -243
  27. package/dist/commands/improve/distill.js +608 -1075
  28. package/dist/commands/improve/eligibility.js +126 -400
  29. package/dist/commands/improve/execution.js +3 -5
  30. package/dist/commands/improve/extract.js +487 -1046
  31. package/dist/commands/improve/feedback-valence.js +0 -25
  32. package/dist/commands/improve/improve-cli.js +29 -166
  33. package/dist/commands/improve/improve-result-file.js +10 -66
  34. package/dist/commands/improve/improve-strategies.js +12 -7
  35. package/dist/commands/improve/improve-usage-report.js +18 -64
  36. package/dist/commands/improve/improve.js +443 -1063
  37. package/dist/commands/improve/ledger.js +114 -0
  38. package/dist/commands/improve/locks.js +2 -8
  39. package/dist/commands/improve/loop-stages.js +459 -1172
  40. package/dist/commands/improve/memory/derived-ref.js +12 -77
  41. package/dist/commands/improve/memory/memory-belief.js +14 -118
  42. package/dist/commands/improve/memory/memory-improve.js +4 -3
  43. package/dist/commands/improve/outcome-loop.js +28 -156
  44. package/dist/commands/improve/planner.js +5 -10
  45. package/dist/commands/improve/preparation.js +851 -2339
  46. package/dist/commands/improve/proactive-maintenance.js +34 -101
  47. package/dist/commands/improve/reflect-noise.js +104 -280
  48. package/dist/commands/improve/reflect.js +621 -1367
  49. package/dist/commands/improve/salience.js +46 -232
  50. package/dist/commands/improve/session-asset.js +19 -100
  51. package/dist/commands/improve/stage.js +323 -0
  52. package/dist/commands/proposal/drain.js +251 -644
  53. package/dist/commands/proposal/proposal-cli.js +3 -18
  54. package/dist/commands/proposal/proposal-types.js +20 -41
  55. package/dist/commands/proposal/proposal.js +1 -2
  56. package/dist/commands/proposal/propose.js +134 -160
  57. package/dist/commands/proposal/repository.js +502 -1487
  58. package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
  59. package/dist/commands/proposal/validators/proposal-validators.js +1 -1
  60. package/dist/commands/proposal/validators/proposals.js +13 -89
  61. package/dist/commands/read/curate.js +63 -413
  62. package/dist/commands/read/search-cli.js +16 -33
  63. package/dist/commands/read/search.js +17 -23
  64. package/dist/commands/read/show.js +2 -13
  65. package/dist/commands/sources/bundle-cli.js +25 -2
  66. package/dist/commands/sources/bundle-config-ops.js +7 -0
  67. package/dist/commands/sources/dangerous-env-audit.js +1 -2
  68. package/dist/commands/sources/info.js +2 -11
  69. package/dist/commands/sources/installed-stashes.js +197 -746
  70. package/dist/commands/sources/schema-repair.js +98 -129
  71. package/dist/commands/sources/source-add.js +62 -12
  72. package/dist/commands/sources/stash-cli.js +1 -1
  73. package/dist/commands/tasks/explain.js +10 -13
  74. package/dist/commands/tasks/tasks-cli.js +9 -8
  75. package/dist/commands/tasks/tasks.js +326 -930
  76. package/dist/commands/tasks/validate.js +42 -21
  77. package/dist/commands/workflow/plan.js +22 -29
  78. package/dist/commands/workflow-cli.js +4 -4
  79. package/dist/core/adapter/adapters/akm-adapter.js +0 -1
  80. package/dist/core/adapter/adapters/akm-lint.js +2 -3
  81. package/dist/core/adapter/adapters/akm-metadata.js +11 -12
  82. package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
  83. package/dist/core/adapter/execution-source.js +17 -29
  84. package/dist/core/asset/resolve-ref.js +1 -1
  85. package/dist/core/bundle-id.js +42 -5
  86. package/dist/core/bundle-rename.js +291 -0
  87. package/dist/core/config/config-io.js +1 -2
  88. package/dist/core/config/config-schema.js +1 -33
  89. package/dist/core/config/config-walker.js +1 -1
  90. package/dist/core/config/config.js +163 -68
  91. package/dist/core/config/legacy-source-shape-shim.js +38 -9
  92. package/dist/core/config/schema/embedding.js +20 -5
  93. package/dist/core/config/schema/engines.js +5 -0
  94. package/dist/core/config/schema/execution.js +1 -1
  95. package/dist/core/config/schema/experimental.js +1 -1
  96. package/dist/core/config/schema/improve-processes.js +21 -95
  97. package/dist/core/config/schema/improve.js +4 -42
  98. package/dist/core/config/schema/scheduler.js +12 -12
  99. package/dist/core/config/schema/search.js +6 -22
  100. package/dist/core/env-secret-ref.js +0 -1
  101. package/dist/core/errors.js +8 -9
  102. package/dist/core/file-lock.js +76 -173
  103. package/dist/core/logs-db.js +2 -2
  104. package/dist/core/paths.js +0 -27
  105. package/dist/core/redaction.js +109 -2
  106. package/dist/core/run-lock.js +2 -5
  107. package/dist/core/spawn-env.js +1 -1
  108. package/dist/core/state/migrations.js +108 -61
  109. package/dist/core/state-db-scope.js +2 -4
  110. package/dist/core/state-db.js +126 -692
  111. package/dist/core/type-presentation.js +1 -9
  112. package/dist/core/write-source.js +293 -1012
  113. package/dist/execution/input-contract.js +1 -1
  114. package/dist/execution/resolved-request.js +135 -689
  115. package/dist/execution/source.js +63 -257
  116. package/dist/execution/target-ref.js +1 -1
  117. package/dist/indexer/bundle-identity-guard.js +2 -2
  118. package/dist/indexer/db/graph-db.js +106 -46
  119. package/dist/indexer/ensure-index.js +44 -85
  120. package/dist/indexer/graph/graph-extraction.js +340 -562
  121. package/dist/indexer/graph/graph-related.js +130 -0
  122. package/dist/indexer/index-rebuild-lock.js +3 -11
  123. package/dist/indexer/index-writer-lock.js +8 -17
  124. package/dist/indexer/index-written-assets.js +139 -151
  125. package/dist/indexer/indexer.js +524 -846
  126. package/dist/indexer/materialize-embeddings.js +60 -397
  127. package/dist/indexer/passes/memory-inference.js +81 -90
  128. package/dist/indexer/passes/metadata.js +132 -200
  129. package/dist/indexer/read-preflight.js +0 -7
  130. package/dist/indexer/scan/doc-to-entry.js +1 -3
  131. package/dist/indexer/scan/drain-dir.js +1 -1
  132. package/dist/indexer/search/db-search.js +181 -590
  133. package/dist/indexer/search/fts-query.js +30 -41
  134. package/dist/indexer/search/ranking.js +28 -154
  135. package/dist/indexer/search/search-attribution.js +12 -32
  136. package/dist/indexer/search/search-fields.js +11 -15
  137. package/dist/indexer/search/search-hit-enrichers.js +54 -85
  138. package/dist/indexer/search/search-source.js +1 -4
  139. package/dist/indexer/usage/usage-events.js +2 -7
  140. package/dist/integrations/agent/engine-fallback.js +23 -40
  141. package/dist/integrations/agent/engine-resolution.js +93 -183
  142. package/dist/integrations/agent/execution.js +507 -0
  143. package/dist/integrations/agent/model-map.js +28 -156
  144. package/dist/integrations/agent/request-lowering.js +66 -141
  145. package/dist/integrations/agent/runner-dispatch.js +143 -321
  146. package/dist/integrations/agent/runner.js +54 -14
  147. package/dist/integrations/lockfile.js +53 -101
  148. package/dist/llm/embedders/deterministic.js +2 -3
  149. package/dist/llm/embedders/profile.js +71 -0
  150. package/dist/llm/embedders/remote.js +10 -15
  151. package/dist/llm/graph-extract.js +3 -12
  152. package/dist/llm/index-passes.js +3 -5
  153. package/dist/llm/memory-infer.js +1 -2
  154. package/dist/llm/metadata-enhance.js +1 -2
  155. package/dist/llm/structured-call.js +5 -24
  156. package/dist/output/generic-render.js +23 -11
  157. package/dist/output/html-render.js +13 -10
  158. package/dist/output/render-registry.js +3 -32
  159. package/dist/output/shapes/helpers.js +2 -34
  160. package/dist/output/shapes/passthrough.js +1 -9
  161. package/dist/{indexer/search/ranking-types.js → output/text/bundle-rename.js} +4 -1
  162. package/dist/output/text/command-format.js +60 -23
  163. package/dist/output/text/helpers.js +1 -1
  164. package/dist/output/text/migrate.js +5 -14
  165. package/dist/output/text/proposal-format.js +1 -2
  166. package/dist/output/text/workflow-format.js +0 -32
  167. package/dist/output/text.js +2 -0
  168. package/dist/registry/factory.js +4 -19
  169. package/dist/registry/network.js +66 -220
  170. package/dist/registry/providers/index.js +0 -2
  171. package/dist/registry/providers/skills-sh.js +3 -14
  172. package/dist/registry/providers/static-index.js +24 -26
  173. package/dist/registry/resolve.js +55 -131
  174. package/dist/scripts/akm-migrate-node.js +43937 -93313
  175. package/dist/scripts/akm-migrate.js +43697 -93071
  176. package/dist/setup/registry-stash-loader.js +4 -13
  177. package/dist/setup/semantic-assets.js +3 -44
  178. package/dist/setup/setup.js +1 -1
  179. package/dist/setup/steps/tasks.js +25 -15
  180. package/dist/sources/provider-factory.js +17 -18
  181. package/dist/sources/providers/filesystem.js +2 -3
  182. package/dist/sources/providers/git-install.js +7 -1
  183. package/dist/sources/providers/git-provider.js +0 -3
  184. package/dist/sources/providers/git-stash.js +0 -17
  185. package/dist/sources/providers/npm.js +2 -4
  186. package/dist/sources/providers/provider-utils.js +5 -10
  187. package/dist/sources/providers/website.js +0 -2
  188. package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
  189. package/dist/sources/website-url.js +2 -2
  190. package/dist/storage/database.js +9 -35
  191. package/dist/storage/repositories/improve-ledger-repository.js +168 -0
  192. package/dist/storage/repositories/index-connection.js +34 -70
  193. package/dist/storage/repositories/index-entries-repository.js +69 -111
  194. package/dist/storage/repositories/index-entry-mapper.js +1 -2
  195. package/dist/storage/repositories/index-entry-schema.js +83 -269
  196. package/dist/storage/repositories/index-fts-repository.js +86 -256
  197. package/dist/storage/repositories/index-llm-cache-repository.js +17 -0
  198. package/dist/storage/repositories/index-meta-repository.js +6 -4
  199. package/dist/storage/repositories/index-schema.js +192 -220
  200. package/dist/storage/repositories/index-utility-repository.js +8 -29
  201. package/dist/storage/repositories/index-vec-repository.js +133 -414
  202. package/dist/storage/repositories/outcome-repository.js +2 -1
  203. package/dist/storage/repositories/proposals-repository.js +35 -0
  204. package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
  205. package/dist/storage/repositories/task-history-repository.js +26 -4
  206. package/dist/storage/repositories/workflow-runs-repository.js +53 -244
  207. package/dist/storage/sqlite-migrations.js +136 -0
  208. package/dist/storage/sqlite-pragmas.js +11 -9
  209. package/dist/storage/sqlite-transaction.js +170 -0
  210. package/dist/storage/state-db-integrity.js +34 -27
  211. package/dist/tasks/activation-config.js +134 -62
  212. package/dist/tasks/backends/cron.js +129 -277
  213. package/dist/tasks/backends/exec-utils.js +2 -5
  214. package/dist/tasks/backends/launchd.js +125 -745
  215. package/dist/tasks/backends/schtasks.js +101 -620
  216. package/dist/tasks/prepare/prepare-support.js +5 -15
  217. package/dist/tasks/prepare/prepare.js +0 -2
  218. package/dist/tasks/resolve-akm-bin.js +20 -79
  219. package/dist/tasks/run/attempt-lifecycle.js +0 -1
  220. package/dist/tasks/scheduler-binding.js +18 -238
  221. package/dist/tasks/scheduler-invocation.js +52 -52
  222. package/dist/tasks/scheduler-lock.js +53 -0
  223. package/dist/tasks/scheduler-sync.js +363 -679
  224. package/dist/tasks/source/parse-task-source.js +160 -10
  225. package/dist/tasks/source/task-source-v3-frozen.js +3 -4
  226. package/dist/tasks/source/task-to-v4.js +2 -2
  227. package/dist/workflows/authoring/authoring.js +3 -12
  228. package/dist/workflows/compile.js +211 -0
  229. package/dist/workflows/concurrency-policy.js +13 -74
  230. package/dist/workflows/exec/child-invocation.js +3 -17
  231. package/dist/workflows/exec/child-workflow.js +32 -141
  232. package/dist/workflows/exec/dispatch-redaction.js +13 -53
  233. package/dist/workflows/exec/environment.js +98 -0
  234. package/dist/workflows/exec/exec-unit.js +33 -140
  235. package/dist/workflows/exec/frozen-judge.js +7 -59
  236. package/dist/workflows/exec/native-executor.js +82 -341
  237. package/dist/workflows/exec/param-secrets.js +29 -47
  238. package/dist/workflows/exec/run-workflow.js +154 -387
  239. package/dist/workflows/exec/scheduler.js +9 -36
  240. package/dist/workflows/exec/step-work.js +127 -430
  241. package/dist/workflows/exec/unit-dispatch.js +11 -63
  242. package/dist/workflows/exec/unit-writer.js +8 -52
  243. package/dist/workflows/exec/worktree.js +39 -273
  244. package/dist/workflows/freeze/child-output-references.js +4 -15
  245. package/dist/workflows/freeze/environment.js +99 -92
  246. package/dist/workflows/freeze/freeze.js +172 -0
  247. package/dist/workflows/freeze/step-values.js +19 -21
  248. package/dist/workflows/freeze/targets/child-workflow.js +23 -92
  249. package/dist/workflows/freeze/targets/command.js +10 -33
  250. package/dist/workflows/freeze/targets/script.js +5 -12
  251. package/dist/workflows/freeze/targets/shell.js +3 -6
  252. package/dist/workflows/freeze/targets/task.js +25 -80
  253. package/dist/workflows/freeze/task-bindings.js +20 -67
  254. package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
  255. package/dist/workflows/ir/params.js +6 -51
  256. package/dist/workflows/ir/plan-hash.js +2 -34
  257. package/dist/workflows/parser.js +140 -43
  258. package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
  259. package/dist/workflows/renderer.js +36 -69
  260. package/dist/workflows/resource-limits.js +12 -120
  261. package/dist/workflows/runtime/agent-identity.js +8 -40
  262. package/dist/workflows/runtime/run-outputs.js +3 -6
  263. package/dist/workflows/runtime/run-plan.js +316 -0
  264. package/dist/workflows/runtime/runs.js +48 -200
  265. package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
  266. package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
  267. package/dist/workflows/validate-summary.js +2 -7
  268. package/docs/integration/bundling-akm.md +49 -42
  269. package/docs/migration/README.md +1 -0
  270. package/docs/migration/release-notes/0.9.17.md +41 -0
  271. package/docs/migration/v0.9.1-to-v0.9.2.md +19 -7
  272. package/docs/reference/cli.md +182 -125
  273. package/docs/reference/configuration.md +49 -56
  274. package/docs/reference/data-and-telemetry.md +19 -20
  275. package/docs/reference/tasks.md +86 -38
  276. package/docs/reference/workflow-schema.md +14 -18
  277. package/docs/reference/workflows.md +6 -9
  278. package/package.json +1 -1
  279. package/schemas/akm-config.json +87 -406
  280. package/dist/commands/health/advisories.js +0 -150
  281. package/dist/commands/health/metrics.js +0 -329
  282. package/dist/commands/health/surfaces.js +0 -102
  283. package/dist/commands/improve/anti-collapse.js +0 -83
  284. package/dist/commands/improve/collapse-detector.js +0 -432
  285. package/dist/commands/improve/consolidate/eligibility.js +0 -48
  286. package/dist/commands/improve/consolidate/merge.js +0 -146
  287. package/dist/commands/improve/distill/promote-memory.js +0 -329
  288. package/dist/commands/improve/distill/quality-gate.js +0 -500
  289. package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
  290. package/dist/commands/improve/proposal-envelope.js +0 -31
  291. package/dist/commands/improve/run-context.js +0 -123
  292. package/dist/commands/improve/shared.js +0 -21
  293. package/dist/commands/improve/source-identity.js +0 -28
  294. package/dist/commands/improve/triage.js +0 -96
  295. package/dist/commands/proposal/drain-policies.js +0 -151
  296. package/dist/commands/sources/update-transaction.js +0 -220
  297. package/dist/core/action-contributors.js +0 -28
  298. package/dist/core/config/config-version-shim.js +0 -101
  299. package/dist/core/config/retired-experimental-keys-shim.js +0 -62
  300. package/dist/core/fs-txn.js +0 -405
  301. package/dist/core/lexical-score.js +0 -25
  302. package/dist/core/maintenance-barrier.js +0 -167
  303. package/dist/execution/executable-identity.js +0 -105
  304. package/dist/execution/guarded-source.js +0 -427
  305. package/dist/indexer/graph/graph-boost.js +0 -427
  306. package/dist/indexer/graph/graph-dedup.js +0 -95
  307. package/dist/indexer/search/name-match.js +0 -35
  308. package/dist/indexer/search/ranking-contributors.js +0 -515
  309. package/dist/indexer/walk/project-context.js +0 -192
  310. package/dist/integrations/agent/execution-cascade.js +0 -566
  311. package/dist/integrations/agent/execution-definitions.js +0 -202
  312. package/dist/integrations/agent/execution-lowering.js +0 -841
  313. package/dist/integrations/agent/execution-preparation.js +0 -98
  314. package/dist/integrations/agent/inline-execution.js +0 -74
  315. package/dist/registry/create-provider-registry.js +0 -29
  316. package/dist/registry/pinned-request-helper.js +0 -247
  317. package/dist/registry/pinned-transport.js +0 -717
  318. package/dist/sources/providers/index.js +0 -14
  319. package/dist/storage/engines/sqlite-migrations.js +0 -271
  320. package/dist/storage/repositories/canaries-repository.js +0 -107
  321. package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
  322. package/dist/storage/repositories/registry-cache.js +0 -113
  323. package/dist/tasks/scheduler-sync-preview.js +0 -52
  324. package/dist/workflows/freeze/resolve-steps.js +0 -86
  325. package/dist/workflows/freeze/source-freeze.js +0 -64
  326. package/dist/workflows/ir/compile.js +0 -321
  327. package/dist/workflows/ir/environment-v4.js +0 -330
  328. package/dist/workflows/ir/freeze-v4.js +0 -153
  329. package/dist/workflows/ir/schema-v4.js +0 -745
  330. package/dist/workflows/ir/schema.js +0 -354
  331. package/dist/workflows/program/schema.js +0 -78
  332. package/dist/workflows/runtime/checkin.js +0 -57
  333. package/dist/workflows/runtime/plan-classifier.js +0 -196
  334. package/dist/workflows/runtime/unit-checkin.js +0 -45
  335. package/dist/workflows/runtime/unit-phases.js +0 -20
  336. package/dist/workflows/schema.js +0 -4
  337. package/dist/workflows/source-ir/compile.js +0 -200
  338. package/dist/workflows/source-ir/program.js +0 -50
  339. package/dist/workflows/source-ir/result.js +0 -26
  340. package/dist/workflows/source-ir/schema.js +0 -786
  341. package/dist/workflows/source-ir/triggers.js +0 -79
  342. package/dist/workflows/source-ir/uses.js +0 -40
  343. 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> > /home/.../tasks/logs/<id>.log 2>&1
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 -`. Disabling a task comments the entry with `# akm:disabled `
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 { assertSchedulerExecutionEvidenceDigest, assertSchedulerExpectationIdentity, assertSchedulerMutationArtifact, assertSchedulerNativeArtifactCardinality, assertSchedulerNativeArtifactOwner, assertSchedulerRemovalArtifact, assertSchedulerRollbackArtifactCardinality, schedulerBindingNativeId, schedulerLogicalBindingId, schedulerLogicalBindingOwner, schedulerNativeArtifactKey, } from "../scheduler-binding.js";
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 CRON_SNAPSHOT = Symbol("akm-cron-binding-snapshot");
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, envPath ?? ""));
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, expected) {
60
- if (expected)
61
- assertSchedulerExpectationIdentity(expected, task);
62
- // Create the log directory before writing the crontab line — the
63
- // redirect target's parent directory must exist or the surrounding
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 (cronLineParts.wrapper) {
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(cronLineParts.wrapper.path, cronLineParts.wrapper.content);
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 blocks = listBlocks(existing);
78
- const matching = blocks.filter(({ id }) => schedulerNativeArtifactKey(id) === schedulerNativeArtifactKey(nativeId));
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, expected) {
97
- if (expected)
98
- assertSchedulerExpectationIdentity({ ...expected, state: "present" });
80
+ uninstall(nativeId) {
99
81
  const existing = readCrontab(exec);
100
- const matching = listBlocks(existing).filter(({ id }) => schedulerNativeArtifactKey(id) === schedulerNativeArtifactKey(nativeId));
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
- if (expected) {
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
- replaceCrontab(exec, existing, next);
89
+ if (next !== existing)
90
+ replaceCrontab(exec, existing, next);
119
91
  },
120
92
  list() {
121
- return [...inspectCronState(readCrontab(exec), defaultContextPath).installed];
122
- },
123
- listForRebind() {
124
- const existing = readCrontab(exec);
125
- return listBlocks(existing).map(({ id, body }) => {
126
- const installed = extractCronInvocation(body);
127
- const ref = {
128
- id: installed ? schedulerLogicalBindingId(id, installed.invocation) : id,
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
- ...(installed?.target !== undefined ? { target: installed.target } : {}),
131
- };
132
- Object.defineProperty(ref, "nativeId", { value: id });
133
- if (installed)
134
- Object.defineProperty(ref, "invocation", { value: Object.freeze([...installed.invocation]) });
135
- return ref;
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 cronLine = buildCronLine(task, [...(opts?.binding ?? akmArgv)], logDir, opts?.contextPath ?? defaultContextPath, opts?.target);
203
- assertPortableCronLine(cronLine);
204
- return normalizeSignature(cronBlockBody(cronLine, task.enabled, task.executionEvidenceDigest));
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 inspectCronState(crontab, fallbackContextPath) {
209
- const installed = [];
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 rather than growing forever.
267
- // akm's own per-run log (src/tasks/run/task-log.ts) already keeps history.
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
- const content = cronWrapperScriptContent(invocation.argv);
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 cronWrapperScriptContent(argv) {
285
- const cmd = argv.map(quoteForShellScript).join(" ");
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, executionEvidenceDigest) {
293
- const lines = [
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, executionEvidenceDigest) {
302
- return [BEGIN(id), cronBlockBody(cronLine, enabled, executionEvidenceDigest), END(id)].join("\n");
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: newly-installed rows redirect with `>` (truncate); tolerate `>>`
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 (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.
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((l) => l.trim())
443
- .filter((l) => !stripDisabledPrefix(l).startsWith(SOURCE_ENABLED_PREFIX))
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 lines) {
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) — this is the only
533
- // case where "install/PATH the crontab binary" is the correct remedy.
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" — not evidence the binary is missing or broken. A
541
- // nonzero exit that DID say something else (a permission refusal) is
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
- const r = spawnSync("crontab", ["-l"], { encoding: "utf8" });
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. Factors out the "run a native scheduler command, throw a
55
- * formatted error on a bad exit" idiom repeated across the cron/launchd/
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;