akm-cli 0.9.16 → 0.9.17-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2101 -0
- package/STABILITY.md +11 -10
- package/dist/akm +124 -193
- package/dist/akm-migrate +38 -19
- package/dist/assets/hints/cli-hints-full.md +6 -7
- package/dist/assets/improve-strategies/catchup.json +0 -3
- package/dist/assets/improve-strategies/consolidate.json +0 -1
- package/dist/assets/improve-strategies/default.json +1 -2
- package/dist/assets/improve-strategies/proactive-maintenance.json +1 -2
- package/dist/assets/improve-strategies/quick.json +1 -2
- package/dist/assets/improve-strategies/reflect-distill.json +1 -2
- package/dist/assets/improve-strategies/thorough.json +0 -3
- package/dist/assets/prompts/consolidate-pair.md +20 -0
- package/dist/assets/prompts/consolidate-system.md +4 -11
- package/dist/assets/prompts/retrieval-relevance-judge.md +6 -0
- package/dist/assets/stash-skeleton/facts/conventions/backlinks.md +20 -20
- package/dist/assets/stash-skeleton/facts/conventions/domains.md +2 -2
- package/dist/assets/templates/html/health.html +3 -5
- package/dist/cli/retired-commands.js +1 -1
- package/dist/cli/shared.js +6 -2
- package/dist/cli/unknown-flags.js +24 -1
- package/dist/cli.js +68 -10
- package/dist/commands/agent/agent-dispatch.js +1 -1
- package/dist/commands/command/command-execution.js +24 -62
- package/dist/commands/feedback-cli.js +0 -1
- package/dist/commands/health/accept-rate.js +6 -0
- package/dist/commands/health/archive-usage.js +92 -0
- package/dist/commands/health/checks.js +83 -74
- package/dist/commands/health/config-skew.js +38 -0
- package/dist/commands/health/data-dir-usage.js +25 -13
- package/dist/commands/health/egress.js +54 -0
- package/dist/commands/health/html-report.js +1 -42
- package/dist/commands/health/improve-metrics.js +136 -591
- package/dist/commands/health/md-report.js +1 -6
- package/dist/commands/health/plugin-staleness.js +53 -3
- package/dist/commands/health/renderers.js +12 -4
- package/dist/commands/health/report-view-model.js +14 -120
- package/dist/commands/health/types-improve.js +4 -19
- package/dist/commands/health/windows.js +64 -74
- package/dist/commands/health.js +145 -143
- package/dist/commands/improve/consolidate/chunking.js +26 -117
- package/dist/commands/improve/consolidate/continuity-check.js +137 -0
- package/dist/commands/improve/consolidate/pair-pass.js +791 -0
- package/dist/commands/improve/consolidate/sanitize.js +54 -149
- package/dist/commands/improve/consolidate.js +589 -1127
- package/dist/commands/improve/content-hash.js +16 -24
- package/dist/commands/improve/distill/content-repair.js +18 -100
- package/dist/commands/improve/distill-guards.js +20 -81
- package/dist/commands/improve/distill-promotion-policy.js +23 -243
- package/dist/commands/improve/distill.js +608 -1041
- package/dist/commands/improve/eligibility.js +126 -390
- package/dist/commands/improve/execution.js +8 -10
- package/dist/commands/improve/extract-prompt.js +1 -2
- package/dist/commands/improve/extract.js +487 -1046
- package/dist/commands/improve/feedback-valence.js +0 -25
- package/dist/commands/improve/improve-cli.js +75 -169
- package/dist/commands/improve/improve-result-file.js +10 -66
- package/dist/commands/improve/improve-strategies.js +52 -4
- package/dist/commands/improve/improve-usage-report.js +18 -64
- package/dist/commands/improve/improve.js +480 -1074
- package/dist/commands/improve/ledger.js +119 -0
- package/dist/commands/improve/locks.js +2 -8
- package/dist/commands/improve/loop-stages.js +415 -1073
- package/dist/commands/improve/memory/derived-ref.js +12 -77
- package/dist/commands/improve/memory/memory-belief.js +16 -118
- package/dist/commands/improve/memory/memory-improve.js +266 -14
- package/dist/commands/improve/outcome-loop.js +28 -156
- package/dist/commands/improve/planner.js +5 -15
- package/dist/commands/improve/preparation.js +779 -2319
- package/dist/commands/improve/proactive-maintenance.js +34 -101
- package/dist/commands/improve/reflect-noise.js +104 -280
- package/dist/commands/improve/reflect.js +642 -1353
- package/dist/commands/improve/retrieval-gate.js +127 -0
- package/dist/commands/improve/retrieval-scope.js +92 -0
- package/dist/commands/improve/salience.js +41 -240
- package/dist/commands/improve/session-asset.js +19 -100
- package/dist/commands/improve/stage.js +322 -0
- package/dist/commands/lint/base-linter.js +37 -15
- package/dist/commands/proposal/drain.js +261 -578
- package/dist/commands/proposal/proposal-cli.js +19 -20
- package/dist/commands/proposal/proposal-types.js +31 -24
- package/dist/commands/proposal/proposal.js +38 -8
- package/dist/commands/proposal/propose.js +134 -160
- package/dist/commands/proposal/repository.js +1097 -1394
- package/dist/commands/proposal/validators/proposal-quality-validators.js +71 -174
- package/dist/commands/proposal/validators/proposal-validators.js +1 -1
- package/dist/commands/proposal/validators/proposals.js +22 -89
- package/dist/commands/read/curate.js +105 -462
- package/dist/commands/read/knowledge.js +3 -2
- package/dist/commands/read/search-cli.js +16 -33
- package/dist/commands/read/search.js +17 -23
- package/dist/commands/read/show.js +57 -108
- package/dist/commands/sources/bundle-cli.js +25 -2
- package/dist/commands/sources/bundle-config-ops.js +4 -0
- package/dist/commands/sources/dangerous-env-audit.js +1 -2
- package/dist/commands/sources/info.js +127 -29
- package/dist/commands/sources/installed-stashes.js +197 -746
- package/dist/commands/sources/schema-repair.js +98 -129
- package/dist/commands/sources/source-add.js +62 -12
- package/dist/commands/sources/source-manage.js +9 -2
- package/dist/commands/sources/stash-cli.js +24 -4
- package/dist/commands/tasks/explain.js +10 -13
- package/dist/commands/tasks/tasks-cli.js +12 -13
- package/dist/commands/tasks/tasks.js +350 -936
- package/dist/commands/tasks/validate.js +26 -24
- package/dist/commands/workflow/plan.js +22 -29
- package/dist/commands/workflow-cli.js +4 -4
- package/dist/core/adapter/adapters/akm-adapter.js +2 -1
- package/dist/core/adapter/adapters/akm-lint.js +2 -3
- package/dist/core/adapter/adapters/akm-metadata.js +42 -12
- package/dist/core/adapter/adapters/akm-task-adapter.js +29 -8
- package/dist/core/adapter/adapters/akm-workflow-adapter.js +1 -1
- package/dist/core/adapter/execution-source.js +17 -29
- package/dist/core/asset/asset-placement.js +4 -13
- package/dist/core/asset/frontmatter.js +106 -1
- package/dist/core/asset/resolve-ref.js +1 -1
- package/dist/core/bundle-id.js +42 -5
- package/dist/core/bundle-rename.js +285 -0
- package/dist/core/config/config-io.js +1 -2
- package/dist/core/config/config-schema.js +9 -34
- package/dist/core/config/config-walker.js +1 -1
- package/dist/core/config/config.js +184 -111
- package/dist/core/config/engine-semantics.js +0 -2
- package/dist/core/config/legacy-source-shape-shim.js +38 -9
- package/dist/core/config/schema/embedding.js +20 -5
- package/dist/core/config/schema/engines.js +5 -0
- package/dist/core/config/schema/execution.js +1 -1
- package/dist/core/config/schema/experimental.js +1 -1
- package/dist/core/config/schema/improve-processes.js +54 -125
- package/dist/core/config/schema/improve.js +4 -42
- package/dist/core/config/schema/index-config.js +9 -48
- package/dist/core/config/schema/scheduler.js +12 -12
- package/dist/core/config/schema/search.js +6 -22
- package/dist/core/env-secret-ref.js +0 -1
- package/dist/core/errors.js +8 -9
- package/dist/core/file-change.js +13 -5
- package/dist/core/file-lock.js +76 -173
- package/dist/core/improve-result.js +35 -7
- package/dist/core/improve-types.js +0 -1
- package/dist/core/logs-db.js +2 -2
- package/dist/core/loopback.js +7 -12
- package/dist/core/non-task-input.js +20 -0
- package/dist/core/parse.js +13 -16
- package/dist/core/paths.js +0 -24
- package/dist/core/redaction.js +109 -2
- package/dist/core/run-lock.js +2 -5
- package/dist/core/spawn-env.js +1 -1
- package/dist/core/state/migrations.js +123 -61
- package/dist/core/state-db-scope.js +2 -4
- package/dist/core/state-db.js +126 -692
- package/dist/core/time.js +0 -20
- package/dist/core/type-presentation.js +1 -9
- package/dist/core/write-source.js +294 -1005
- package/dist/execution/input-contract.js +1 -1
- package/dist/execution/resolved-request.js +135 -689
- package/dist/execution/source.js +63 -257
- package/dist/execution/target-ref.js +1 -1
- package/dist/indexer/bundle-identity-guard.js +2 -2
- package/dist/indexer/db/llm-cache.js +2 -2
- package/dist/indexer/ensure-index.js +77 -73
- package/dist/indexer/index-rebuild-lock.js +3 -11
- package/dist/indexer/index-writer-lock.js +8 -17
- package/dist/indexer/index-written-assets.js +141 -154
- package/dist/indexer/indexer.js +400 -1124
- package/dist/indexer/links/declared-links.js +90 -0
- package/dist/indexer/materialize-embeddings.js +60 -397
- package/dist/indexer/passes/memory-inference.js +96 -90
- package/dist/indexer/passes/metadata.js +132 -219
- package/dist/indexer/read-preflight.js +0 -7
- package/dist/indexer/scan/doc-to-entry.js +2 -3
- package/dist/indexer/scan/drain-dir.js +1 -1
- package/dist/indexer/search/db-search.js +190 -590
- package/dist/indexer/search/fts-query.js +30 -41
- package/dist/indexer/search/ranking.js +28 -154
- package/dist/indexer/search/search-attribution.js +12 -32
- package/dist/indexer/search/search-fields.js +11 -15
- package/dist/indexer/search/search-hit-enrichers.js +54 -85
- package/dist/indexer/search/search-source.js +1 -4
- package/dist/indexer/usage/usage-events.js +36 -7
- package/dist/indexer/walk/walker.js +3 -4
- package/dist/integrations/agent/engine-fallback.js +23 -40
- package/dist/integrations/agent/engine-resolution.js +93 -183
- package/dist/integrations/agent/execution.js +507 -0
- package/dist/integrations/agent/model-map.js +28 -156
- package/dist/integrations/agent/request-lowering.js +66 -141
- package/dist/integrations/agent/runner-dispatch.js +143 -321
- package/dist/integrations/agent/runner.js +54 -14
- package/dist/integrations/lockfile.js +53 -101
- package/dist/llm/client.js +18 -6
- package/dist/llm/embedders/deterministic.js +2 -3
- package/dist/llm/embedders/profile.js +71 -0
- package/dist/llm/embedders/remote.js +11 -17
- package/dist/llm/feature-gate.js +0 -8
- package/dist/llm/index-passes.js +3 -5
- package/dist/llm/memory-infer.js +1 -2
- package/dist/llm/structured-call.js +5 -24
- package/dist/output/generic-render.js +23 -11
- package/dist/output/html-render.js +13 -10
- package/dist/output/render-registry.js +3 -32
- package/dist/output/shapes/helpers.js +25 -38
- package/dist/output/shapes/passthrough.js +1 -9
- package/dist/{indexer/graph/graph-types.js → output/text/bundle-rename.js} +4 -1
- package/dist/output/text/command-format.js +69 -31
- package/dist/output/text/helpers.js +1 -1
- package/dist/output/text/migrate.js +5 -14
- package/dist/output/text/proposal-format.js +48 -3
- package/dist/output/text/show-format.js +13 -17
- package/dist/output/text/workflow-format.js +0 -32
- package/dist/output/text.js +2 -0
- package/dist/registry/factory.js +4 -19
- package/dist/registry/network.js +66 -220
- package/dist/registry/providers/index.js +0 -2
- package/dist/registry/providers/skills-sh.js +3 -14
- package/dist/registry/providers/static-index.js +24 -26
- package/dist/registry/resolve.js +55 -131
- package/dist/scripts/akm-migrate-node.js +42948 -92369
- package/dist/scripts/akm-migrate.js +42935 -92354
- package/dist/setup/registry-stash-loader.js +4 -13
- package/dist/setup/semantic-assets.js +3 -44
- package/dist/setup/setup.js +1 -1
- package/dist/setup/steps/connection.js +5 -6
- package/dist/setup/steps/platforms.js +2 -2
- package/dist/setup/steps/tasks.js +25 -15
- package/dist/sources/provider-factory.js +17 -18
- package/dist/sources/providers/filesystem.js +2 -3
- package/dist/sources/providers/git-install.js +7 -1
- package/dist/sources/providers/git-provider.js +0 -3
- package/dist/sources/providers/git-stash.js +83 -21
- package/dist/sources/providers/npm.js +2 -4
- package/dist/sources/providers/provider-utils.js +5 -10
- package/dist/sources/providers/website.js +0 -2
- package/dist/sources/snapshot-fetchers/website-ingest.js +1 -1
- package/dist/sources/website-url.js +2 -2
- package/dist/storage/database.js +9 -35
- package/dist/storage/repositories/improve-ledger-repository.js +209 -0
- package/dist/storage/repositories/index-connection.js +39 -72
- package/dist/storage/repositories/index-entries-repository.js +131 -129
- package/dist/storage/repositories/index-entry-mapper.js +1 -2
- package/dist/storage/repositories/index-entry-schema.js +101 -268
- package/dist/storage/repositories/index-fts-repository.js +86 -256
- package/dist/storage/repositories/index-links-repository.js +143 -0
- package/dist/storage/repositories/index-llm-cache-repository.js +7 -9
- package/dist/storage/repositories/index-meta-repository.js +6 -4
- package/dist/storage/repositories/index-schema.js +257 -325
- package/dist/storage/repositories/index-utility-repository.js +8 -29
- package/dist/storage/repositories/index-vec-repository.js +133 -414
- package/dist/storage/repositories/outcome-repository.js +2 -1
- package/dist/storage/repositories/proposals-repository.js +104 -1
- package/dist/storage/repositories/registry-index-cache-repository.js +100 -0
- package/dist/storage/repositories/salience-repository.js +1 -19
- package/dist/storage/repositories/task-history-repository.js +26 -4
- package/dist/storage/repositories/workflow-runs-repository.js +53 -244
- package/dist/storage/sqlite-migrations.js +136 -0
- package/dist/storage/sqlite-pragmas.js +11 -9
- package/dist/storage/sqlite-transaction.js +170 -0
- package/dist/storage/state-db-integrity.js +130 -0
- package/dist/tasks/activation-config.js +134 -62
- package/dist/tasks/backends/cron.js +191 -302
- package/dist/tasks/backends/exec-utils.js +2 -5
- package/dist/tasks/backends/launchd.js +141 -748
- package/dist/tasks/backends/schtasks.js +119 -623
- package/dist/tasks/prepare/prepare-support.js +5 -15
- package/dist/tasks/prepare/prepare.js +0 -2
- package/dist/tasks/resolve-akm-bin.js +20 -79
- package/dist/tasks/run/attempt-lifecycle.js +0 -1
- package/dist/tasks/run/load-task.js +1 -1
- package/dist/tasks/scheduler-binding.js +20 -238
- package/dist/tasks/scheduler-invocation.js +136 -244
- package/dist/tasks/scheduler-lock.js +53 -0
- package/dist/tasks/scheduler-sync.js +368 -679
- package/dist/tasks/source/parse-task-source.js +55 -9
- package/dist/tasks/source/task-source-v3-frozen.js +3 -4
- package/dist/tasks/source/task-to-v4.js +464 -88
- package/dist/workflows/authoring/authoring.js +3 -12
- package/dist/workflows/compile.js +211 -0
- package/dist/workflows/concurrency-policy.js +13 -74
- package/dist/workflows/exec/child-invocation.js +3 -17
- package/dist/workflows/exec/child-workflow.js +32 -141
- package/dist/workflows/exec/dispatch-redaction.js +13 -53
- package/dist/workflows/exec/environment.js +98 -0
- package/dist/workflows/exec/exec-unit.js +33 -140
- package/dist/workflows/exec/frozen-judge.js +7 -59
- package/dist/workflows/exec/native-executor.js +82 -341
- package/dist/workflows/exec/param-secrets.js +29 -47
- package/dist/workflows/exec/run-workflow.js +154 -387
- package/dist/workflows/exec/scheduler.js +9 -36
- package/dist/workflows/exec/step-work.js +127 -430
- package/dist/workflows/exec/unit-dispatch.js +11 -63
- package/dist/workflows/exec/unit-writer.js +8 -52
- package/dist/workflows/exec/worktree.js +39 -273
- package/dist/workflows/freeze/child-output-references.js +4 -15
- package/dist/workflows/freeze/environment.js +99 -92
- package/dist/workflows/freeze/freeze.js +172 -0
- package/dist/workflows/freeze/step-values.js +19 -21
- package/dist/workflows/freeze/targets/child-workflow.js +23 -92
- package/dist/workflows/freeze/targets/command.js +10 -33
- package/dist/workflows/freeze/targets/script.js +5 -12
- package/dist/workflows/freeze/targets/shell.js +3 -6
- package/dist/workflows/freeze/targets/task.js +25 -80
- package/dist/workflows/freeze/task-bindings.js +20 -67
- package/dist/workflows/{source-ir/github-yaml.js → github-yaml.js} +88 -206
- package/dist/workflows/ir/params.js +6 -51
- package/dist/workflows/ir/plan-hash.js +2 -34
- package/dist/workflows/parser.js +140 -43
- package/dist/{commands/improve/consolidate/types.js → workflows/plan.js} +2 -1
- package/dist/workflows/renderer.js +36 -69
- package/dist/workflows/resource-limits.js +12 -120
- package/dist/workflows/runtime/agent-identity.js +8 -40
- package/dist/workflows/runtime/run-outputs.js +3 -6
- package/dist/workflows/runtime/run-plan.js +316 -0
- package/dist/workflows/runtime/runs.js +48 -200
- package/dist/workflows/runtime/workflow-asset-loader.js +24 -57
- package/dist/workflows/{source-ir/semantics.js → source-semantics.js} +16 -20
- package/dist/workflows/validate-summary.js +2 -7
- package/docs/integration/bundling-akm.md +49 -42
- package/docs/migration/README.md +1 -0
- package/docs/migration/release-notes/0.9.17.md +43 -0
- package/docs/migration/v0.9.1-to-v0.9.2.md +23 -7
- package/docs/reference/cli.md +232 -135
- package/docs/reference/configuration.md +71 -57
- package/docs/reference/data-and-telemetry.md +20 -21
- package/docs/reference/tasks.md +105 -39
- package/docs/reference/workflow-schema.md +14 -18
- package/docs/reference/workflows.md +6 -9
- package/package.json +1 -1
- package/schemas/akm-config.json +115 -738
- package/schemas/akm-workflow.json +1 -0
- package/dist/assets/improve-strategies/graph-refresh.json +0 -15
- package/dist/assets/prompts/contradiction-judge.md +0 -33
- package/dist/assets/prompts/graph-extract-system.md +0 -1
- package/dist/assets/prompts/graph-extract-user-prompt.md +0 -35
- package/dist/assets/prompts/metadata-enhance-system.md +0 -1
- package/dist/assets/tasks/improve/akm-graph-refresh-weekly.yml +0 -4
- package/dist/commands/health/advisories.js +0 -150
- package/dist/commands/health/metrics.js +0 -329
- package/dist/commands/health/surfaces.js +0 -102
- package/dist/commands/improve/anti-collapse.js +0 -83
- package/dist/commands/improve/collapse-detector.js +0 -432
- package/dist/commands/improve/consolidate/eligibility.js +0 -48
- package/dist/commands/improve/consolidate/merge.js +0 -149
- package/dist/commands/improve/distill/promote-memory.js +0 -291
- package/dist/commands/improve/distill/quality-gate.js +0 -337
- package/dist/commands/improve/eval-cases.js +0 -52
- package/dist/commands/improve/memory/memory-contradiction-detect.js +0 -291
- package/dist/commands/improve/proposal-envelope.js +0 -31
- package/dist/commands/improve/run-context.js +0 -123
- package/dist/commands/improve/shared.js +0 -31
- package/dist/commands/improve/source-identity.js +0 -28
- package/dist/commands/improve/triage.js +0 -96
- package/dist/commands/proposal/drain-policies.js +0 -151
- package/dist/commands/sources/update-transaction.js +0 -220
- package/dist/core/action-contributors.js +0 -28
- package/dist/core/config/config-version-shim.js +0 -101
- package/dist/core/fs-txn.js +0 -405
- package/dist/core/lexical-score.js +0 -25
- package/dist/core/maintenance-barrier.js +0 -167
- package/dist/execution/executable-identity.js +0 -105
- package/dist/execution/guarded-source.js +0 -427
- package/dist/indexer/db/graph-db.js +0 -444
- package/dist/indexer/graph/graph-boost.js +0 -427
- package/dist/indexer/graph/graph-dedup.js +0 -95
- package/dist/indexer/graph/graph-extraction.js +0 -1108
- package/dist/indexer/search/name-match.js +0 -35
- package/dist/indexer/search/ranking-contributors.js +0 -515
- package/dist/indexer/search/ranking-types.js +0 -4
- package/dist/indexer/walk/project-context.js +0 -192
- package/dist/integrations/agent/execution-cascade.js +0 -566
- package/dist/integrations/agent/execution-definitions.js +0 -202
- package/dist/integrations/agent/execution-lowering.js +0 -841
- package/dist/integrations/agent/execution-preparation.js +0 -98
- package/dist/integrations/agent/inline-execution.js +0 -74
- package/dist/llm/graph-extract.js +0 -728
- package/dist/llm/metadata-enhance.js +0 -96
- package/dist/registry/create-provider-registry.js +0 -29
- package/dist/registry/pinned-request-helper.js +0 -247
- package/dist/registry/pinned-transport.js +0 -717
- package/dist/sources/providers/index.js +0 -14
- package/dist/storage/engines/sqlite-migrations.js +0 -271
- package/dist/storage/repositories/canaries-repository.js +0 -107
- package/dist/storage/repositories/embedding-salvage-repository.js +0 -184
- package/dist/storage/repositories/registry-cache.js +0 -113
- package/dist/tasks/scheduler-sync-preview.js +0 -52
- package/dist/tasks/source/task-to-v3.js +0 -507
- package/dist/workflows/freeze/resolve-steps.js +0 -86
- package/dist/workflows/freeze/source-freeze.js +0 -64
- package/dist/workflows/ir/compile.js +0 -321
- package/dist/workflows/ir/environment-v4.js +0 -330
- package/dist/workflows/ir/freeze-v4.js +0 -153
- package/dist/workflows/ir/schema-v4.js +0 -745
- package/dist/workflows/ir/schema.js +0 -354
- package/dist/workflows/program/schema.js +0 -77
- package/dist/workflows/runtime/checkin.js +0 -57
- package/dist/workflows/runtime/plan-classifier.js +0 -196
- package/dist/workflows/runtime/unit-checkin.js +0 -45
- package/dist/workflows/runtime/unit-phases.js +0 -20
- package/dist/workflows/schema.js +0 -4
- package/dist/workflows/source-ir/compile.js +0 -200
- package/dist/workflows/source-ir/program.js +0 -50
- package/dist/workflows/source-ir/result.js +0 -26
- package/dist/workflows/source-ir/schema.js +0 -786
- package/dist/workflows/source-ir/triggers.js +0 -79
- package/dist/workflows/source-ir/uses.js +0 -40
- package/dist/workflows/validator.js +0 -60
|
@@ -6,25 +6,28 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Each task is registered under the `\akm\` Task Scheduler folder so the
|
|
8
8
|
* backend never touches user-managed tasks. The full task definition is
|
|
9
|
-
* sent through `schtasks /Create /TN \akm\<id> /XML <path
|
|
9
|
+
* sent through `schtasks /Create /TN \akm\<id> /XML <path> /F` so we can
|
|
10
10
|
* express triggers/principals/actions without quoting hell.
|
|
11
11
|
*
|
|
12
12
|
* Platform notes:
|
|
13
13
|
* • `LogonType=InteractiveToken` means the task runs in the context of
|
|
14
14
|
* the registering user only when they are logged in — there is no
|
|
15
15
|
* stored password and the task will not fire at the lock screen.
|
|
16
|
-
* • `<Principal>` records the current user SID
|
|
17
|
-
*
|
|
16
|
+
* • `<Principal>` records the current user SID; it is part of the
|
|
17
|
+
* fingerprint in `<Source>`, so a sync run by another user re-registers
|
|
18
|
+
* the task under that user.
|
|
18
19
|
* • `<DisallowStartIfOnBatteries>false</…>` and `<StopIfGoingOnBatteries>
|
|
19
|
-
* false</…>` allow the task to run on battery
|
|
20
|
-
*
|
|
21
|
-
* • `MultipleInstancesPolicy=IgnoreNew` makes overlapping triggers safe:
|
|
22
|
-
* while a task is still running, a new fire is dropped rather than
|
|
23
|
-
* queued or run in parallel.
|
|
20
|
+
* false</…>` allow the task to run on battery.
|
|
21
|
+
* • `MultipleInstancesPolicy=IgnoreNew` makes overlapping triggers safe.
|
|
24
22
|
* • `/Query /FO CSV /NH` (without `/V`) outputs three columns:
|
|
25
23
|
* `TaskName,Next Run Time,Status` — so the regex anchors on the task
|
|
26
|
-
* name as the leading quoted field.
|
|
27
|
-
*
|
|
24
|
+
* name as the leading quoted field.
|
|
25
|
+
* • Task Scheduler runs a task with the account's own environment, so
|
|
26
|
+
* PATH is not carried anywhere. The Exec action has no environment of
|
|
27
|
+
* its own: the AKM directory environment a row sets (see
|
|
28
|
+
* `src/tasks/scheduler-invocation.ts`) is a `$env:NAME='value';`
|
|
29
|
+
* assignment ahead of the call operator in the PowerShell command,
|
|
30
|
+
* inherited by the akm process it starts.
|
|
28
31
|
*
|
|
29
32
|
* Tests inject a fake exec + filesystem.
|
|
30
33
|
*/
|
|
@@ -37,143 +40,54 @@ import { ConfigError } from "../../core/errors.js";
|
|
|
37
40
|
import { getTaskLogDir } from "../../core/paths.js";
|
|
38
41
|
import { resolveAkmInvocation } from "../resolve-akm-bin.js";
|
|
39
42
|
import { parseSchedule, translateToSchtasks } from "../schedule.js";
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
43
|
+
import { schedulerBindingNativeId, schedulerLogicalBindingId } from "../scheduler-binding.js";
|
|
44
|
+
import { buildScheduledInvocation, parseScheduledInvocationArgv, scheduledRowEnvironmentEntries, scheduledRowEnvironmentFrom, } from "../scheduler-invocation.js";
|
|
42
45
|
import { escapeXml, nodeExec, nodeFs, normalizeXmlForUtf16File, runOrThrow, } from "./exec-utils.js";
|
|
43
46
|
export const DEFAULT_FOLDER_PREFIX = "\\akm\\";
|
|
44
47
|
const SIGNATURE_PREFIX = "akm:v1:";
|
|
45
|
-
const SCHTASKS_SNAPSHOT = Symbol("akm-schtasks-binding-snapshot");
|
|
46
48
|
export function SCHTASKS_BACKEND(options = {}) {
|
|
47
|
-
const exec = options.exec ??
|
|
49
|
+
const exec = options.exec ?? nodeExec();
|
|
48
50
|
const fsLike = options.fs ?? defaultSchtasksFs();
|
|
49
51
|
const akmArgv = options.akmArgv ?? resolveAkmInvocation().argv;
|
|
50
52
|
const logDir = options.logDir ?? getTaskLogDir();
|
|
51
53
|
const folder = options.folderPrefix ?? DEFAULT_FOLDER_PREFIX;
|
|
52
|
-
const scheduledContext = options.scheduledContext ?? resolveScheduledTaskContext();
|
|
53
|
-
const defaultContextPath = schedulerContextPath(schedulerContextDescriptor(scheduledContext, process.env.PATH ?? ""));
|
|
54
54
|
const userSid = options.userSid ?? resolveCurrentUserSid(exec);
|
|
55
55
|
const taskName = (nativeId) => `${folder}${nativeId}`;
|
|
56
|
+
const xmlFor = (task, opts) => buildSchtasksXml(task, akmArgv, logDir, {
|
|
57
|
+
folderPrefix: folder,
|
|
58
|
+
userSid,
|
|
59
|
+
binding: [...(opts?.binding ?? akmArgv)],
|
|
60
|
+
...(opts?.environment ? { environment: opts.environment } : {}),
|
|
61
|
+
});
|
|
62
|
+
const queryXml = (nativeId) => runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(nativeId), "/XML"], {
|
|
63
|
+
message: (result) => `schtasks /Query /XML for "${taskName(nativeId)}" failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
|
|
64
|
+
}).stdout;
|
|
56
65
|
return {
|
|
57
66
|
name: "schtasks",
|
|
58
|
-
install(task, opts
|
|
59
|
-
|
|
60
|
-
assertSchedulerExpectationIdentity(expected, task);
|
|
67
|
+
install(task, opts) {
|
|
68
|
+
const xml = normalizeXmlForUtf16File(xmlFor(task, opts));
|
|
61
69
|
const nativeId = schedulerBindingNativeId(task);
|
|
62
|
-
const xml = normalizeXmlForUtf16File(buildSchtasksXml(task, akmArgv, logDir, {
|
|
63
|
-
folderPrefix: folder,
|
|
64
|
-
contextPath: opts?.contextPath ?? defaultContextPath,
|
|
65
|
-
userSid,
|
|
66
|
-
binding: [...(opts?.binding ?? akmArgv)],
|
|
67
|
-
...(opts?.target !== undefined ? { target: opts.target } : {}),
|
|
68
|
-
}));
|
|
69
|
-
const initialArtifact = expected
|
|
70
|
-
? assertSchedulerNativeArtifactCardinality(inspectSchtasksState(exec, folder, taskName).artifacts, nativeId, expected.state === "absent" ? 0 : 1)
|
|
71
|
-
: undefined;
|
|
72
|
-
const existingNativeId = expected ? initialArtifact?.nativeId : nativeId;
|
|
73
|
-
const query = existingNativeId
|
|
74
|
-
? runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(existingNativeId), "/XML"], {
|
|
75
|
-
isOk: (r) => r.status === 0 || isMissingTaskResult(r),
|
|
76
|
-
message: (r) => `schtasks /Query failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
77
|
-
})
|
|
78
|
-
: { status: 1, stdout: "", stderr: "missing" };
|
|
79
|
-
if (expected) {
|
|
80
|
-
assertSchedulerMutationArtifact(query.status === 0 && existingNativeId ? schtasksArtifact(existingNativeId, query.stdout) : undefined, expected);
|
|
81
|
-
}
|
|
82
|
-
let previous;
|
|
83
|
-
if (query.status === 0) {
|
|
84
|
-
if (!expected) {
|
|
85
|
-
assertSchedulerNativeArtifactOwner(nativeId, task, extractSchtasksInvocation(query.stdout)?.invocation);
|
|
86
|
-
}
|
|
87
|
-
const enabled = taskXmlEnabled(query.stdout);
|
|
88
|
-
if (enabled === undefined) {
|
|
89
|
-
throw new ConfigError(`schtasks /Query returned an unreadable definition for "${taskName(nativeId)}"; refusing to replace it.`, "INVALID_CONFIG_FILE");
|
|
90
|
-
}
|
|
91
|
-
previous = { xml: normalizeXmlForUtf16File(query.stdout), enabled };
|
|
92
|
-
}
|
|
93
70
|
fsLike.ensureDir(logDir);
|
|
94
71
|
const tmpFile = path.join(fsLike.tmpdir(), `akm-task-${nativeId}-${Date.now()}.xml`);
|
|
95
72
|
fsLike.writeFile(tmpFile, xml);
|
|
96
73
|
try {
|
|
97
|
-
if
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
})
|
|
105
|
-
: undefined;
|
|
106
|
-
assertSchedulerMutationArtifact(current?.status === 0 && finalNativeId ? schtasksArtifact(finalNativeId, current.stdout) : undefined, expected);
|
|
107
|
-
}
|
|
108
|
-
else {
|
|
109
|
-
assertSchtasksArtifactUnchanged(exec, taskName(nativeId), nativeId, task);
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
// /F forces overwrite if a task with the same name exists.
|
|
113
|
-
runOrThrow(exec, ["schtasks", "/Create", "/TN", taskName(nativeId), "/XML", tmpFile, "/F"], {
|
|
114
|
-
message: (r) => `schtasks /Create failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
74
|
+
// /F forces overwrite if a task with the same name exists.
|
|
75
|
+
runOrThrow(exec, ["schtasks", "/Create", "/TN", taskName(nativeId), "/XML", tmpFile, "/F"], {
|
|
76
|
+
message: (r) => `schtasks /Create failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
77
|
+
});
|
|
78
|
+
if (!task.enabled) {
|
|
79
|
+
runOrThrow(exec, ["schtasks", "/Change", "/TN", taskName(nativeId), "/DISABLE"], {
|
|
80
|
+
message: (r) => `schtasks /Change /DISABLE failed: ${r.stderr || r.stdout || "no output"}.`,
|
|
115
81
|
});
|
|
116
|
-
if (!task.enabled) {
|
|
117
|
-
runOrThrow(exec, ["schtasks", "/Change", "/TN", taskName(nativeId), "/DISABLE"], {
|
|
118
|
-
message: (r) => `schtasks /Change /DISABLE failed: ${r.stderr || r.stdout || "no output"}.`,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
catch (err) {
|
|
123
|
-
const rollbackErrors = [];
|
|
124
|
-
if (previous === undefined) {
|
|
125
|
-
try {
|
|
126
|
-
const remove = exec.run(["schtasks", "/Delete", "/TN", taskName(nativeId), "/F"]);
|
|
127
|
-
if (remove.status !== 0 && !isMissingTaskResult(remove)) {
|
|
128
|
-
rollbackErrors.push(new ConfigError(`schtasks /Delete during rollback failed: ${remove.stderr || remove.stdout || "no output"}.`, "INVALID_CONFIG_FILE"));
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
catch (rollbackError) {
|
|
132
|
-
rollbackErrors.push(rollbackError);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
else {
|
|
136
|
-
try {
|
|
137
|
-
fsLike.writeFile(tmpFile, previous.xml);
|
|
138
|
-
runOrThrow(exec, ["schtasks", "/Create", "/TN", taskName(nativeId), "/XML", tmpFile, "/F"], {
|
|
139
|
-
message: (r) => `schtasks /Create during rollback failed: ${r.stderr || r.stdout || "no output"}.`,
|
|
140
|
-
});
|
|
141
|
-
const stateFlag = previous.enabled ? "/ENABLE" : "/DISABLE";
|
|
142
|
-
runOrThrow(exec, ["schtasks", "/Change", "/TN", taskName(nativeId), stateFlag], {
|
|
143
|
-
message: (r) => `schtasks /Change ${stateFlag} during rollback failed: ${r.stderr || r.stdout || "no output"}.`,
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
catch (rollbackError) {
|
|
147
|
-
rollbackErrors.push(rollbackError);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
if (rollbackErrors.length > 0) {
|
|
151
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
152
|
-
throw new AggregateError([err, ...rollbackErrors], `${message}; rollback for Task Scheduler task "${task.id}" was incomplete.`);
|
|
153
|
-
}
|
|
154
|
-
throw err;
|
|
155
82
|
}
|
|
156
83
|
}
|
|
157
84
|
finally {
|
|
158
85
|
fsLike.removeFile(tmpFile);
|
|
159
86
|
}
|
|
160
87
|
},
|
|
161
|
-
uninstall(nativeId
|
|
162
|
-
if (expected) {
|
|
163
|
-
assertSchedulerExpectationIdentity({ ...expected, state: "present" });
|
|
164
|
-
const artifact = assertSchedulerNativeArtifactCardinality(inspectSchtasksState(exec, folder, taskName).artifacts, nativeId, 1);
|
|
165
|
-
const currentNativeId = artifact?.nativeId ?? nativeId;
|
|
166
|
-
const current = runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(currentNativeId), "/XML"], {
|
|
167
|
-
isOk: (result) => result.status === 0 || isMissingTaskResult(result),
|
|
168
|
-
message: (result) => `schtasks /Query failed during final removal ownership check (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
|
|
169
|
-
});
|
|
170
|
-
if (current.status !== 0) {
|
|
171
|
-
throw new ConfigError(`Task Scheduler artifact ${JSON.stringify(nativeId)} disappeared after coherent inspection.`, "INVALID_CONFIG_FILE");
|
|
172
|
-
}
|
|
173
|
-
assertSchedulerRemovalArtifact(currentNativeId, expected, extractSchtasksInvocation(current.stdout)?.invocation, installedSignature(current.stdout));
|
|
174
|
-
}
|
|
88
|
+
uninstall(nativeId) {
|
|
175
89
|
runOrThrow(exec, ["schtasks", "/Delete", "/TN", taskName(nativeId), "/F"], {
|
|
176
|
-
isOk: (r) => r.status === 0 ||
|
|
90
|
+
isOk: (r) => r.status === 0 || isMissingTaskResult(r),
|
|
177
91
|
message: (r) => `schtasks /Delete failed: ${r.stderr || r.stdout || "no output"}.`,
|
|
178
92
|
});
|
|
179
93
|
},
|
|
@@ -184,219 +98,39 @@ export function SCHTASKS_BACKEND(options = {}) {
|
|
|
184
98
|
});
|
|
185
99
|
},
|
|
186
100
|
list() {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
101
|
+
const listing = runOrThrow(exec, ["schtasks", "/Query", "/FO", "CSV", "/NH"], {
|
|
102
|
+
message: (result) => `schtasks /Query failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
|
|
103
|
+
});
|
|
104
|
+
const rows = [];
|
|
105
|
+
for (const line of (listing.stdout ?? "").split(/\r?\n/)) {
|
|
106
|
+
const name = line.match(/^"([^"]+)",/)?.[1];
|
|
107
|
+
if (!name?.startsWith(folder))
|
|
108
|
+
continue;
|
|
109
|
+
const nativeId = name.slice(folder.length);
|
|
110
|
+
const xml = queryXml(nativeId);
|
|
111
|
+
const parsed = extractSchtasksInvocation(xml);
|
|
112
|
+
if (!parsed)
|
|
113
|
+
continue;
|
|
114
|
+
rows.push({
|
|
115
|
+
id: schedulerLogicalBindingId(nativeId, parsed.invocation),
|
|
116
|
+
nativeId,
|
|
117
|
+
enabled: taskXmlEnabled(xml),
|
|
118
|
+
signature: taskXmlSignature(xml),
|
|
119
|
+
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
120
|
+
binding: parsed.binding,
|
|
121
|
+
...(parsed.contextPath !== undefined ? { contextPath: parsed.contextPath } : {}),
|
|
122
|
+
...(parsed.environment !== undefined ? { environment: parsed.environment } : {}),
|
|
123
|
+
invocation: parsed.invocation,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return rows;
|
|
203
127
|
},
|
|
204
128
|
expectedSignature(task, opts) {
|
|
205
|
-
|
|
206
|
-
folderPrefix: folder,
|
|
207
|
-
contextPath: opts?.contextPath ?? defaultContextPath,
|
|
208
|
-
userSid,
|
|
209
|
-
binding: [...(opts?.binding ?? akmArgv)],
|
|
210
|
-
...(opts?.target !== undefined ? { target: opts.target } : {}),
|
|
211
|
-
}));
|
|
212
|
-
if (signature === undefined)
|
|
213
|
-
throw new Error("Failed to fingerprint generated Task Scheduler XML.");
|
|
214
|
-
return signature;
|
|
129
|
+
return taskXmlSignature(xmlFor(task, opts));
|
|
215
130
|
},
|
|
216
131
|
};
|
|
217
132
|
}
|
|
218
|
-
|
|
219
|
-
const result = runOrThrow(exec, ["schtasks", "/Query", "/FO", "CSV", "/NH"], {
|
|
220
|
-
message: (query) => `schtasks /Query failed (exit ${query.status}): ${query.stderr || query.stdout || "no output"}.`,
|
|
221
|
-
});
|
|
222
|
-
const refs = [];
|
|
223
|
-
for (const line of (result.stdout ?? "").split(/\r?\n/)) {
|
|
224
|
-
const name = line.match(/^"([^"]+)",/)?.[1];
|
|
225
|
-
if (!name?.startsWith(folder))
|
|
226
|
-
continue;
|
|
227
|
-
const id = name.slice(folder.length);
|
|
228
|
-
const query = runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(id), "/XML"], {
|
|
229
|
-
message: (current) => `schtasks /Query /XML for "${taskName(id)}" failed (exit ${current.status}): ${current.stderr || current.stdout || "no output"}.`,
|
|
230
|
-
});
|
|
231
|
-
const signature = installedSignature(query.stdout);
|
|
232
|
-
const installed = extractSchtasksInvocation(query.stdout);
|
|
233
|
-
const ref = {
|
|
234
|
-
id: installed ? schedulerLogicalBindingId(id, installed.invocation) : id,
|
|
235
|
-
...(signature !== undefined ? { signature } : {}),
|
|
236
|
-
...(installed?.target !== undefined ? { target: installed.target } : {}),
|
|
237
|
-
};
|
|
238
|
-
Object.defineProperty(ref, "nativeId", { value: id });
|
|
239
|
-
if (installed)
|
|
240
|
-
Object.defineProperty(ref, "invocation", { value: Object.freeze([...installed.invocation]) });
|
|
241
|
-
refs.push(ref);
|
|
242
|
-
}
|
|
243
|
-
return refs;
|
|
244
|
-
}
|
|
245
|
-
function inspectSchtasksState(exec, folder, taskName) {
|
|
246
|
-
const listing = runOrThrow(exec, ["schtasks", "/Query", "/FO", "CSV", "/NH"], {
|
|
247
|
-
message: (result) => `schtasks /Query failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
|
|
248
|
-
});
|
|
249
|
-
const installed = [];
|
|
250
|
-
const artifacts = [];
|
|
251
|
-
for (const nativeId of schtasksNativeIds(listing.stdout, folder)) {
|
|
252
|
-
const query = runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(nativeId), "/XML"], {
|
|
253
|
-
message: (result) => `schtasks /Query /XML for "${taskName(nativeId)}" failed (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
|
|
254
|
-
});
|
|
255
|
-
const artifact = schtasksArtifact(nativeId, query.stdout);
|
|
256
|
-
artifacts.push(artifact);
|
|
257
|
-
const parsed = extractSchtasksInvocation(query.stdout);
|
|
258
|
-
if (!parsed)
|
|
259
|
-
continue;
|
|
260
|
-
const enabled = taskXmlEnabled(query.stdout);
|
|
261
|
-
const ref = {
|
|
262
|
-
id: schedulerLogicalBindingId(nativeId, parsed.invocation),
|
|
263
|
-
...(enabled !== undefined ? { enabled } : {}),
|
|
264
|
-
...(artifact.fingerprint !== undefined ? { signature: artifact.fingerprint } : {}),
|
|
265
|
-
...(parsed.target !== undefined ? { target: parsed.target } : {}),
|
|
266
|
-
binding: parsed.binding,
|
|
267
|
-
contextPath: parsed.contextPath,
|
|
268
|
-
};
|
|
269
|
-
Object.defineProperty(ref, "nativeId", { value: nativeId });
|
|
270
|
-
Object.defineProperty(ref, "invocation", { value: Object.freeze([...parsed.invocation]) });
|
|
271
|
-
installed.push(ref);
|
|
272
|
-
}
|
|
273
|
-
return Object.freeze({ installed: Object.freeze(installed), artifacts: Object.freeze(artifacts) });
|
|
274
|
-
}
|
|
275
|
-
function schtasksNativeIds(stdout, folder) {
|
|
276
|
-
const ids = [];
|
|
277
|
-
for (const line of (stdout ?? "").split(/\r?\n/)) {
|
|
278
|
-
const match = line.match(/^"([^"]+)",/);
|
|
279
|
-
const name = match?.[1];
|
|
280
|
-
if (name?.startsWith(folder))
|
|
281
|
-
ids.push(name.slice(folder.length));
|
|
282
|
-
}
|
|
283
|
-
return ids;
|
|
284
|
-
}
|
|
285
|
-
function schtasksArtifact(nativeId, xml) {
|
|
286
|
-
const parsed = extractSchtasksInvocation(xml);
|
|
287
|
-
const owner = parsed ? schedulerLogicalBindingOwner(nativeId, parsed.invocation) : undefined;
|
|
288
|
-
const artifact = parsed
|
|
289
|
-
? {
|
|
290
|
-
nativeId,
|
|
291
|
-
...(owner !== undefined ? { bindingId: owner } : {}),
|
|
292
|
-
invocation: Object.freeze([...parsed.invocation]),
|
|
293
|
-
}
|
|
294
|
-
: { nativeId };
|
|
295
|
-
const fingerprint = installedSignature(xml);
|
|
296
|
-
if (fingerprint !== undefined)
|
|
297
|
-
Object.defineProperty(artifact, "fingerprint", { value: fingerprint });
|
|
298
|
-
return artifact;
|
|
299
|
-
}
|
|
300
|
-
function snapshotSchtasksBindings(ids, exec, folder, taskName) {
|
|
301
|
-
const listing = runOrThrow(exec, ["schtasks", "/Query", "/FO", "CSV", "/NH"], {
|
|
302
|
-
message: (result) => `schtasks /Query failed while snapshotting bindings (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
|
|
303
|
-
});
|
|
304
|
-
const requestedKeys = new Set(ids.map(schedulerNativeArtifactKey));
|
|
305
|
-
const enumeratedIds = new Set(ids);
|
|
306
|
-
for (const nativeId of schtasksNativeIds(listing.stdout, folder)) {
|
|
307
|
-
if (requestedKeys.has(schedulerNativeArtifactKey(nativeId)))
|
|
308
|
-
enumeratedIds.add(nativeId);
|
|
309
|
-
}
|
|
310
|
-
const entries = [...enumeratedIds].map((id) => {
|
|
311
|
-
const query = runOrThrow(exec, ["schtasks", "/Query", "/TN", taskName(id), "/XML"], {
|
|
312
|
-
isOk: (result) => result.status === 0 || isMissingTaskResult(result),
|
|
313
|
-
message: (result) => `schtasks /Query failed while snapshotting "${taskName(id)}": ${result.stderr || result.stdout || "no output"}.`,
|
|
314
|
-
});
|
|
315
|
-
if (query.status !== 0)
|
|
316
|
-
return Object.freeze({ id });
|
|
317
|
-
const enabled = taskXmlEnabled(query.stdout);
|
|
318
|
-
if (enabled === undefined) {
|
|
319
|
-
throw new ConfigError(`schtasks /Query returned an unreadable definition while snapshotting "${taskName(id)}".`, "INVALID_CONFIG_FILE");
|
|
320
|
-
}
|
|
321
|
-
return Object.freeze({ id, xml: normalizeXmlForUtf16File(query.stdout), enabled });
|
|
322
|
-
});
|
|
323
|
-
const artifacts = entries.flatMap((entry) => entry.xml === undefined ? [] : [schtasksArtifact(entry.id, entry.xml)]);
|
|
324
|
-
return Object.freeze({
|
|
325
|
-
kind: SCHTASKS_SNAPSHOT,
|
|
326
|
-
nativeIds: Object.freeze([...ids]),
|
|
327
|
-
artifacts: Object.freeze(artifacts),
|
|
328
|
-
entries: Object.freeze(entries),
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
function restoreSchtasksBindings(snapshot, context, expectedCurrent) {
|
|
332
|
-
if (!isSchtasksBindingSnapshot(snapshot)) {
|
|
333
|
-
throw new ConfigError("Invalid Task Scheduler snapshot.", "INVALID_CONFIG_FILE");
|
|
334
|
-
}
|
|
335
|
-
const errors = [];
|
|
336
|
-
let rollbackInventory;
|
|
337
|
-
if (expectedCurrent) {
|
|
338
|
-
try {
|
|
339
|
-
rollbackInventory = inspectSchtasksState(context.exec, context.folder, context.taskName);
|
|
340
|
-
}
|
|
341
|
-
catch (error) {
|
|
342
|
-
errors.push(error);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
for (const entry of snapshot.entries) {
|
|
346
|
-
try {
|
|
347
|
-
if (expectedCurrent) {
|
|
348
|
-
if (!rollbackInventory)
|
|
349
|
-
continue;
|
|
350
|
-
const expected = expectedCurrent.find((candidate) => candidate.nativeId === entry.id);
|
|
351
|
-
if (!expected) {
|
|
352
|
-
throw new ConfigError(`Missing Task Scheduler rollback expectation for ${JSON.stringify(entry.id)}.`, "INVALID_CONFIG_FILE");
|
|
353
|
-
}
|
|
354
|
-
assertSchedulerRollbackArtifactCardinality(rollbackInventory.artifacts, expected);
|
|
355
|
-
}
|
|
356
|
-
restoreSchtasksEntry(entry, context);
|
|
357
|
-
}
|
|
358
|
-
catch (error) {
|
|
359
|
-
errors.push(error);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
if (errors.length > 0) {
|
|
363
|
-
throw new AggregateError(errors, `Failed to completely restore ${errors.length} Task Scheduler operation(s).`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
function restoreSchtasksEntry(entry, context) {
|
|
367
|
-
if (entry.xml === undefined) {
|
|
368
|
-
runOrThrow(context.exec, ["schtasks", "/Delete", "/TN", context.taskName(entry.id), "/F"], {
|
|
369
|
-
isOk: (result) => result.status === 0 || isMissingTaskResult(result),
|
|
370
|
-
message: (result) => `schtasks /Delete failed while restoring "${context.taskName(entry.id)}": ${result.stderr || result.stdout || "no output"}.`,
|
|
371
|
-
});
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
const tmpFile = path.join(context.fsLike.tmpdir(), `akm-task-restore-${schedulerNativeBindingId(entry.id)}-${Date.now()}.xml`);
|
|
375
|
-
context.fsLike.writeFile(tmpFile, entry.xml);
|
|
376
|
-
try {
|
|
377
|
-
runOrThrow(context.exec, ["schtasks", "/Create", "/TN", context.taskName(entry.id), "/XML", tmpFile, "/F"], {
|
|
378
|
-
message: (result) => `schtasks /Create failed while restoring "${context.taskName(entry.id)}": ${result.stderr || result.stdout || "no output"}.`,
|
|
379
|
-
});
|
|
380
|
-
const stateFlag = entry.enabled === false ? "/DISABLE" : "/ENABLE";
|
|
381
|
-
runOrThrow(context.exec, ["schtasks", "/Change", "/TN", context.taskName(entry.id), stateFlag], {
|
|
382
|
-
message: (result) => `schtasks /Change ${stateFlag} failed while restoring "${context.taskName(entry.id)}": ${result.stderr || result.stdout || "no output"}.`,
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
finally {
|
|
386
|
-
context.fsLike.removeFile(tmpFile);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
function isSchtasksBindingSnapshot(value) {
|
|
390
|
-
return (typeof value === "object" &&
|
|
391
|
-
value !== null &&
|
|
392
|
-
value.kind === SCHTASKS_SNAPSHOT &&
|
|
393
|
-
Array.isArray(value.entries));
|
|
394
|
-
}
|
|
395
|
-
/**
|
|
396
|
-
* Recover the bundle name embedded as a `--bundle <bundle>` pair in the
|
|
397
|
-
* PowerShell `<Arguments>` of an installed Task Scheduler definition. Returns
|
|
398
|
-
* undefined for the primary/default form.
|
|
399
|
-
*/
|
|
133
|
+
/** The `--bundle <bundle>` token of an installed Task Scheduler definition; undefined for the primary form. */
|
|
400
134
|
export function extractSchtasksTarget(xml) {
|
|
401
135
|
return extractSchtasksInvocation(xml)?.target;
|
|
402
136
|
}
|
|
@@ -408,19 +142,26 @@ export function extractSchtasksInvocation(xml) {
|
|
|
408
142
|
const invocationStart = findPowerShellInvocationOperator(commandLine);
|
|
409
143
|
if (invocationStart === undefined)
|
|
410
144
|
return undefined;
|
|
411
|
-
|
|
145
|
+
const parsed = parseScheduledInvocationArgv(parsePowerShellSingleQuotedArgs(commandLine, invocationStart + 1));
|
|
146
|
+
if (!parsed)
|
|
147
|
+
return undefined;
|
|
148
|
+
const variables = {};
|
|
149
|
+
const assignments = commandLine.slice(0, invocationStart);
|
|
150
|
+
for (const assignment of assignments.matchAll(/\$env:([A-Za-z_][A-Za-z0-9_]*)='((?:[^']|'')*)'\s*;/g)) {
|
|
151
|
+
variables[assignment[1]] = assignment[2].replaceAll("''", "'");
|
|
152
|
+
}
|
|
153
|
+
const environment = scheduledRowEnvironmentFrom(variables);
|
|
154
|
+
return environment ? { ...parsed, environment } : parsed;
|
|
412
155
|
}
|
|
413
156
|
function findPowerShellInvocationOperator(script) {
|
|
414
157
|
let inSingleQuote = false;
|
|
415
158
|
for (let index = 0; index < script.length; index += 1) {
|
|
416
159
|
const char = script[index];
|
|
417
160
|
if (char === "'") {
|
|
418
|
-
if (inSingleQuote && script[index + 1] === "'")
|
|
161
|
+
if (inSingleQuote && script[index + 1] === "'")
|
|
419
162
|
index += 1;
|
|
420
|
-
|
|
421
|
-
else {
|
|
163
|
+
else
|
|
422
164
|
inSingleQuote = !inSingleQuote;
|
|
423
|
-
}
|
|
424
165
|
continue;
|
|
425
166
|
}
|
|
426
167
|
if (!inSingleQuote && char === "&" && /\s/.test(script[index + 1] ?? ""))
|
|
@@ -463,70 +204,35 @@ function parsePowerShellSingleQuotedArgs(script, start) {
|
|
|
463
204
|
}
|
|
464
205
|
return argv;
|
|
465
206
|
}
|
|
466
|
-
/**
|
|
467
|
-
* Re-verify ownership of the native artifact immediately before `/Create /F`
|
|
468
|
-
* overwrites it (the read/prepare-to-/Create ownership race at the native
|
|
469
|
-
* boundary).
|
|
470
|
-
*
|
|
471
|
-
* This used to also compare the fresh query against the first read taken
|
|
472
|
-
* earlier in `install` and refuse the whole operation if anything about the
|
|
473
|
-
* XML had changed in between -- a freshness re-check on top of the identity
|
|
474
|
-
* re-check right below it. That extra comparison never caught anything the
|
|
475
|
-
* owner check didn't already cover (a foreign/changed owner still fails
|
|
476
|
-
* `assertSchedulerNativeArtifactOwner`), and `task sync` is idempotent, so a
|
|
477
|
-
* spurious refusal here only cost the user a rerun of a command they'd
|
|
478
|
-
* already asked for. Dropped; the owner re-check (the actual corruption/
|
|
479
|
-
* clobber guard -- no fallback exists if a foreign task got silently
|
|
480
|
-
* overwritten) stays.
|
|
481
|
-
*/
|
|
482
|
-
function assertSchtasksArtifactUnchanged(exec, nativeTaskName, nativeId, task) {
|
|
483
|
-
// This runs outside install's rollback region: no scheduler mutation has
|
|
484
|
-
// occurred, so a raced owner must remain untouched rather than be restored
|
|
485
|
-
// from the stale first read.
|
|
486
|
-
const current = runOrThrow(exec, ["schtasks", "/Query", "/TN", nativeTaskName, "/XML"], {
|
|
487
|
-
isOk: (result) => result.status === 0 || isMissingTaskResult(result),
|
|
488
|
-
message: (result) => `schtasks /Query failed during final ownership check (exit ${result.status}): ${result.stderr || result.stdout || "no output"}.`,
|
|
489
|
-
});
|
|
490
|
-
if (current.status === 0) {
|
|
491
|
-
assertSchedulerNativeArtifactOwner(nativeId, task, extractSchtasksInvocation(current.stdout)?.invocation);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
207
|
export function buildSchtasksXml(task, akmArgv, logDir, options) {
|
|
495
208
|
const folder = options.folderPrefix ?? DEFAULT_FOLDER_PREFIX;
|
|
496
209
|
const now = options.now ? options.now() : new Date();
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
.
|
|
502
|
-
|
|
503
|
-
.replace("{{SIGNATURE}}", definition.signature)
|
|
504
|
-
.replace("{{TRIGGER_XML}}", triggerXml)
|
|
505
|
-
.replace('<Principal id="Author">', `<Principal id="Author">\n <UserId>${escapeXml(options.userSid)}</UserId>`)
|
|
506
|
-
.replace("{{ENABLED}}", task.enabled ? "true" : "false")
|
|
507
|
-
.replace("{{COMMAND}}", escapeXml(definition.command))
|
|
508
|
-
.replace("{{ARGS}}", escapeXml(definition.args))
|
|
509
|
-
.replace("{{LOG_PATH}}", escapeXml(definition.logPath));
|
|
510
|
-
}
|
|
511
|
-
function buildSchtasksDefinition(task, akmArgv, logDir, folder, contextPath, userSid, _target) {
|
|
512
|
-
const spec = parseSchedule(task.cron, "schtasks");
|
|
513
|
-
const trigger = translateToSchtasks(spec);
|
|
514
|
-
const invocation = buildScheduledBindingInvocation(akmArgv, contextPath, task.invocation);
|
|
515
|
-
const invoke = `& ${invocation.argv.map((arg) => quotePowerShell(arg)).join(" ")}`;
|
|
516
|
-
const executionEvidence = task.executionEvidenceDigest === undefined
|
|
517
|
-
? ""
|
|
518
|
-
: `$null=${quotePowerShell(`akm-workflow-evidence:${assertSchedulerExecutionEvidenceDigest(task.executionEvidenceDigest)}`)}; `;
|
|
519
|
-
const script = `${executionEvidence}${invoke}; exit $LASTEXITCODE`;
|
|
210
|
+
const trigger = translateToSchtasks(parseSchedule(task.cron, "schtasks"));
|
|
211
|
+
const argv = buildScheduledInvocation(options.binding ?? akmArgv, task.invocation);
|
|
212
|
+
const assignments = scheduledRowEnvironmentEntries(options.environment)
|
|
213
|
+
.map(([name, value]) => `$env:${name}=${quotePowerShell(value)}; `)
|
|
214
|
+
.join("");
|
|
215
|
+
const script = `${assignments}& ${argv.map((arg) => quotePowerShell(arg)).join(" ")}; exit $LASTEXITCODE`;
|
|
520
216
|
const command = "powershell.exe";
|
|
521
217
|
const args = ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script].map(quoteArg).join(" ");
|
|
522
218
|
const nativeId = schedulerBindingNativeId(task);
|
|
523
219
|
const logPath = path.join(logDir, `${nativeId}.log`);
|
|
524
|
-
// The boundary changes on reinstall, and enabled state can change via
|
|
525
|
-
// Keep both outside the stored definition fingerprint so no-op
|
|
220
|
+
// The boundary changes on reinstall, and enabled state can change via
|
|
221
|
+
// /Change. Keep both outside the stored definition fingerprint so no-op
|
|
222
|
+
// sync stays stable.
|
|
526
223
|
const fingerprint = createHash("sha256")
|
|
527
|
-
.update(JSON.stringify({ folder, id: nativeId, trigger, command, args, logPath, userSid }))
|
|
224
|
+
.update(JSON.stringify({ folder, id: nativeId, trigger, command, args, logPath, userSid: options.userSid }))
|
|
528
225
|
.digest("hex");
|
|
529
|
-
return
|
|
226
|
+
return schtasksTemplate
|
|
227
|
+
.replaceAll("{{TASK_ID}}", escapeXml(nativeId))
|
|
228
|
+
.replaceAll("{{FOLDER}}", escapeXml(folder))
|
|
229
|
+
.replace("{{SIGNATURE}}", `${SIGNATURE_PREFIX}${fingerprint}`)
|
|
230
|
+
.replace("{{TRIGGER_XML}}", renderSchtasksTrigger(trigger, now))
|
|
231
|
+
.replace('<Principal id="Author">', `<Principal id="Author">\n <UserId>${escapeXml(options.userSid)}</UserId>`)
|
|
232
|
+
.replace("{{ENABLED}}", task.enabled ? "true" : "false")
|
|
233
|
+
.replace("{{COMMAND}}", escapeXml(command))
|
|
234
|
+
.replace("{{ARGS}}", () => escapeXml(args))
|
|
235
|
+
.replace("{{LOG_PATH}}", escapeXml(logPath));
|
|
530
236
|
}
|
|
531
237
|
function renderSchtasksTrigger(trigger, now) {
|
|
532
238
|
return expandNativeTriggers(trigger)
|
|
@@ -540,14 +246,7 @@ function expandNativeTriggers(trigger) {
|
|
|
540
246
|
case "minuteValues":
|
|
541
247
|
return trigger.minutes.map((atMinute) => ({ kind: "daily", atHour: 0, atMinute, repeatEveryMinutes: 60 }));
|
|
542
248
|
case "hour":
|
|
543
|
-
return [
|
|
544
|
-
{
|
|
545
|
-
kind: "daily",
|
|
546
|
-
atHour: 0,
|
|
547
|
-
atMinute: trigger.atMinute,
|
|
548
|
-
repeatEveryMinutes: trigger.everyHours * 60,
|
|
549
|
-
},
|
|
550
|
-
];
|
|
249
|
+
return [{ kind: "daily", atHour: 0, atMinute: trigger.atMinute, repeatEveryMinutes: trigger.everyHours * 60 }];
|
|
551
250
|
case "hourValues":
|
|
552
251
|
return trigger.hours.map((atHour) => ({ kind: "daily", atHour, atMinute: trigger.atMinute }));
|
|
553
252
|
case "daily":
|
|
@@ -563,8 +262,8 @@ function renderNativeTrigger(trigger, startBoundary) {
|
|
|
563
262
|
const repetition = trigger.repeatEveryMinutes === undefined
|
|
564
263
|
? ""
|
|
565
264
|
: ` <Repetition>
|
|
566
|
-
<Interval>${
|
|
567
|
-
<Duration>${
|
|
265
|
+
<Interval>${formatMinuteDuration(trigger.repeatEveryMinutes)}</Interval>
|
|
266
|
+
<Duration>${formatMinuteDuration(24 * 60 - trigger.repeatEveryMinutes)}</Duration>
|
|
568
267
|
<StopAtDurationEnd>false</StopAtDurationEnd>
|
|
569
268
|
</Repetition>
|
|
570
269
|
`;
|
|
@@ -617,18 +316,10 @@ ${months}
|
|
|
617
316
|
</ScheduleByMonth>
|
|
618
317
|
</CalendarTrigger>`;
|
|
619
318
|
}
|
|
620
|
-
function formatRepetitionInterval(minutes) {
|
|
621
|
-
return formatMinuteDuration(minutes);
|
|
622
|
-
}
|
|
623
|
-
function formatRepetitionDuration(intervalMinutes) {
|
|
624
|
-
return formatMinuteDuration(24 * 60 - intervalMinutes);
|
|
625
|
-
}
|
|
626
319
|
function formatMinuteDuration(minutes) {
|
|
627
320
|
const hours = Math.floor(minutes / 60);
|
|
628
321
|
const remainingMinutes = minutes % 60;
|
|
629
|
-
|
|
630
|
-
const minutePart = remainingMinutes > 0 ? `${remainingMinutes}M` : "";
|
|
631
|
-
return `PT${hourPart}${minutePart}`;
|
|
322
|
+
return `PT${hours > 0 ? `${hours}H` : ""}${remainingMinutes > 0 ? `${remainingMinutes}M` : ""}`;
|
|
632
323
|
}
|
|
633
324
|
function nextStartBoundary(trigger, now) {
|
|
634
325
|
const boundary = new Date(now.getTime());
|
|
@@ -669,16 +360,11 @@ function nextStartBoundary(trigger, now) {
|
|
|
669
360
|
}
|
|
670
361
|
}
|
|
671
362
|
function formatStartBoundary(d) {
|
|
672
|
-
// Local-time ISO-8601 (no zone suffix) — Task Scheduler interprets a
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
const dd = String(d.getDate()).padStart(2, "0");
|
|
678
|
-
const hh = String(d.getHours()).padStart(2, "0");
|
|
679
|
-
const mi = String(d.getMinutes()).padStart(2, "0");
|
|
680
|
-
const ss = String(d.getSeconds()).padStart(2, "0");
|
|
681
|
-
return `${yyyy}-${mm}-${dd}T${hh}:${mi}:${ss}`;
|
|
363
|
+
// Local-time ISO-8601 (no zone suffix) — Task Scheduler interprets a bare
|
|
364
|
+
// boundary in the registering user's timezone, which matches what a user
|
|
365
|
+
// typing "0 9 * * *" means ("9am local").
|
|
366
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
367
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
682
368
|
}
|
|
683
369
|
function quoteArg(s) {
|
|
684
370
|
if (/^[A-Za-z0-9_\-./@:%=+,\\]+$/.test(s))
|
|
@@ -690,110 +376,24 @@ function quoteArg(s) {
|
|
|
690
376
|
function quotePowerShell(value) {
|
|
691
377
|
return `'${value.replaceAll("'", "''")}'`;
|
|
692
378
|
}
|
|
693
|
-
function installedSignature(xml) {
|
|
694
|
-
return taskXmlSignature(xml);
|
|
695
|
-
}
|
|
696
379
|
function isMissingTaskResult(result) {
|
|
697
380
|
return /cannot find|not found/i.test(`${result.stderr ?? ""}\n${result.stdout ?? ""}`);
|
|
698
381
|
}
|
|
382
|
+
// ── Installed-definition signature ──────────────────────────────────────────
|
|
383
|
+
//
|
|
384
|
+
// Task Scheduler rewrites a registered definition (schema defaults filled in,
|
|
385
|
+
// StartBoundary moved on), so a row is compared by the fingerprint akm stamps
|
|
386
|
+
// into `<Source>` — everything it renders except the enabled state — plus the
|
|
387
|
+
// enabled state `/Change` toggles. A task edited in Task Scheduler that keeps
|
|
388
|
+
// that fingerprint is left as it is.
|
|
699
389
|
function taskXmlSignature(xml) {
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const task = findChild(document, "Task");
|
|
703
|
-
const triggers = findChild(task, "Triggers");
|
|
704
|
-
const principals = findChild(task, "Principals");
|
|
705
|
-
const settings = findChild(task, "Settings");
|
|
706
|
-
const actions = findChild(task, "Actions");
|
|
707
|
-
if (!triggers || !principals || !settings || !actions)
|
|
708
|
-
return undefined;
|
|
709
|
-
normalizeTriggerBoundaries(triggers);
|
|
710
|
-
normalizeNativeDefaults(triggers, principals, settings);
|
|
711
|
-
const enabledElement = findChild(settings, "Enabled");
|
|
712
|
-
const enabledValue = enabledElement ? elementText(enabledElement).toLowerCase() : undefined;
|
|
713
|
-
const enabled = enabledValue === undefined || enabledValue === "true" || enabledValue === "1";
|
|
714
|
-
// Enabled is represented explicitly in the signature suffix. Removing it
|
|
715
|
-
// here also treats an omitted Enabled element as its schema default, true.
|
|
716
|
-
settings.children = settings.children.filter((child) => typeof child === "string" || child.name.toLowerCase() !== "enabled");
|
|
717
|
-
const canonical = [triggers, principals, settings, actions].map(canonicalXmlElement).join("\n");
|
|
718
|
-
const fingerprint = createHash("sha256").update(canonical).digest("hex");
|
|
719
|
-
return signatureWithEnabled(`${SIGNATURE_PREFIX}${fingerprint}`, enabled);
|
|
720
|
-
}
|
|
721
|
-
catch {
|
|
722
|
-
return undefined;
|
|
723
|
-
}
|
|
390
|
+
const source = xml.match(/<(?:[\w.-]+:)?Source>([^<]*)<\//i)?.[1]?.trim() ?? "";
|
|
391
|
+
return `${decodeXml(source)}|enabled=${taskXmlEnabled(xml)}`;
|
|
724
392
|
}
|
|
725
393
|
function taskXmlEnabled(xml) {
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if (!settings)
|
|
730
|
-
return undefined;
|
|
731
|
-
const enabledElement = findChild(settings, "Enabled");
|
|
732
|
-
if (!enabledElement)
|
|
733
|
-
return true;
|
|
734
|
-
const enabled = elementText(enabledElement).toLowerCase();
|
|
735
|
-
if (enabled === "true" || enabled === "1")
|
|
736
|
-
return true;
|
|
737
|
-
if (enabled === "false" || enabled === "0")
|
|
738
|
-
return false;
|
|
739
|
-
return undefined;
|
|
740
|
-
}
|
|
741
|
-
catch {
|
|
742
|
-
return undefined;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
function parseXml(xml) {
|
|
746
|
-
const document = { name: "#document", attributes: {}, children: [] };
|
|
747
|
-
const stack = [document];
|
|
748
|
-
const tokens = xml.match(/<!--[\s\S]*?-->|<\?[\s\S]*?\?>|<[^>]+>|[^<]+/g) ?? [];
|
|
749
|
-
for (const token of tokens) {
|
|
750
|
-
if (token.startsWith("<!--") || token.startsWith("<?"))
|
|
751
|
-
continue;
|
|
752
|
-
if (token.startsWith("</")) {
|
|
753
|
-
if (stack.length === 1)
|
|
754
|
-
throw new Error("Unexpected XML closing tag.");
|
|
755
|
-
const closingName = localXmlName(token.slice(2, -1).trim());
|
|
756
|
-
const current = stack.pop();
|
|
757
|
-
if (current?.name !== closingName)
|
|
758
|
-
throw new Error("Mismatched XML closing tag.");
|
|
759
|
-
continue;
|
|
760
|
-
}
|
|
761
|
-
if (token.startsWith("<")) {
|
|
762
|
-
const selfClosing = /\/\s*>$/.test(token);
|
|
763
|
-
const match = token.match(/^<\s*([^\s/>]+)([\s\S]*?)\/?\s*>$/);
|
|
764
|
-
if (!match)
|
|
765
|
-
throw new Error("Invalid XML opening tag.");
|
|
766
|
-
const element = {
|
|
767
|
-
name: localXmlName(match[1]),
|
|
768
|
-
attributes: parseXmlAttributes(match[2]),
|
|
769
|
-
children: [],
|
|
770
|
-
};
|
|
771
|
-
stack[stack.length - 1].children.push(element);
|
|
772
|
-
if (!selfClosing)
|
|
773
|
-
stack.push(element);
|
|
774
|
-
continue;
|
|
775
|
-
}
|
|
776
|
-
const text = decodeXml(token.trim());
|
|
777
|
-
if (text.length > 0)
|
|
778
|
-
stack[stack.length - 1].children.push(text);
|
|
779
|
-
}
|
|
780
|
-
if (stack.length !== 1)
|
|
781
|
-
throw new Error("Unclosed XML tag.");
|
|
782
|
-
return document;
|
|
783
|
-
}
|
|
784
|
-
function parseXmlAttributes(raw) {
|
|
785
|
-
const attributes = {};
|
|
786
|
-
const pattern = /([^\s=]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
|
|
787
|
-
for (const match of raw.matchAll(pattern)) {
|
|
788
|
-
const name = localXmlName(match[1]);
|
|
789
|
-
if (match[1] === "xmlns" || match[1].startsWith("xmlns:"))
|
|
790
|
-
continue;
|
|
791
|
-
attributes[name] = decodeXml(match[2] ?? match[3] ?? "");
|
|
792
|
-
}
|
|
793
|
-
return attributes;
|
|
794
|
-
}
|
|
795
|
-
function localXmlName(name) {
|
|
796
|
-
return name.slice(name.lastIndexOf(":") + 1);
|
|
394
|
+
const settings = xml.match(/<(?:[\w.-]+:)?Settings>([\s\S]*?)<\/(?:[\w.-]+:)?Settings>/i)?.[1] ?? "";
|
|
395
|
+
const value = settings.match(/<(?:[\w.-]+:)?Enabled>\s*([^<]*?)\s*<\//i)?.[1]?.toLowerCase();
|
|
396
|
+
return value !== "false" && value !== "0";
|
|
797
397
|
}
|
|
798
398
|
function decodeXml(value) {
|
|
799
399
|
return value.replace(/&(?:#(\d+)|#x([\da-f]+)|amp|lt|gt|quot|apos);/gi, (entity, decimal, hex) => {
|
|
@@ -817,124 +417,20 @@ function decodeXml(value) {
|
|
|
817
417
|
}
|
|
818
418
|
});
|
|
819
419
|
}
|
|
820
|
-
function findChild(parent, name) {
|
|
821
|
-
return parent?.children.find((child) => typeof child !== "string" && child.name.toLowerCase() === name.toLowerCase());
|
|
822
|
-
}
|
|
823
|
-
function elementText(element) {
|
|
824
|
-
return element.children.filter((child) => typeof child === "string").join("");
|
|
825
|
-
}
|
|
826
|
-
function normalizeTriggerBoundaries(triggers) {
|
|
827
|
-
for (const trigger of triggers.children) {
|
|
828
|
-
if (typeof trigger === "string")
|
|
829
|
-
continue;
|
|
830
|
-
const boundary = findChild(trigger, "StartBoundary");
|
|
831
|
-
if (!boundary)
|
|
832
|
-
continue;
|
|
833
|
-
const time = elementText(boundary).match(/T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?/);
|
|
834
|
-
if (!time)
|
|
835
|
-
continue;
|
|
836
|
-
const secondsSinceMidnight = Number(time[1]) * 3600 + Number(time[2]) * 60 + Number(time[3]);
|
|
837
|
-
const interval = findChild(findChild(trigger, "Repetition"), "Interval");
|
|
838
|
-
const intervalSeconds = interval ? parseIsoDurationSeconds(elementText(interval)) : undefined;
|
|
839
|
-
boundary.children = [
|
|
840
|
-
intervalSeconds === undefined
|
|
841
|
-
? `dynamic-date;time=${time[1]}:${time[2]}:${time[3]}`
|
|
842
|
-
: `dynamic-cycle;phase-seconds=${secondsSinceMidnight % intervalSeconds}`,
|
|
843
|
-
];
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
const MATERIALIZED_SETTING_DEFAULTS = {
|
|
847
|
-
allowstartondemand: "true",
|
|
848
|
-
allowhardterminate: "true",
|
|
849
|
-
startwhenavailable: "false",
|
|
850
|
-
runonlyifnetworkavailable: "false",
|
|
851
|
-
hidden: "false",
|
|
852
|
-
runonlyifidle: "false",
|
|
853
|
-
waketorun: "false",
|
|
854
|
-
executiontimelimit: "PT72H",
|
|
855
|
-
priority: "7",
|
|
856
|
-
compatibility: "Vista",
|
|
857
|
-
useunifiedschedulingengine: "true",
|
|
858
|
-
disallowstartonremoteappsession: "false",
|
|
859
|
-
volatile: "false",
|
|
860
|
-
};
|
|
861
|
-
const MATERIALIZED_IDLE_DEFAULTS = {
|
|
862
|
-
duration: "PT10M",
|
|
863
|
-
waittimeout: "PT1H",
|
|
864
|
-
stoponidleend: "true",
|
|
865
|
-
restartonidle: "false",
|
|
866
|
-
};
|
|
867
|
-
function normalizeNativeDefaults(triggers, principals, settings) {
|
|
868
|
-
for (const principal of elementChildren(principals)) {
|
|
869
|
-
principal.children = principal.children.filter((child) => {
|
|
870
|
-
if (typeof child === "string")
|
|
871
|
-
return true;
|
|
872
|
-
return child.name.toLowerCase() !== "runlevel" || elementText(child).toLowerCase() !== "leastprivilege";
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
settings.children = settings.children.filter((child) => {
|
|
876
|
-
if (typeof child === "string")
|
|
877
|
-
return true;
|
|
878
|
-
const name = child.name.toLowerCase();
|
|
879
|
-
if (name === "idlesettings")
|
|
880
|
-
return !containsOnlyDefaults(child, MATERIALIZED_IDLE_DEFAULTS);
|
|
881
|
-
const defaultValue = MATERIALIZED_SETTING_DEFAULTS[name];
|
|
882
|
-
return defaultValue === undefined || elementText(child) !== defaultValue;
|
|
883
|
-
});
|
|
884
|
-
for (const trigger of elementChildren(triggers)) {
|
|
885
|
-
trigger.children = trigger.children.filter((child) => {
|
|
886
|
-
if (typeof child === "string")
|
|
887
|
-
return true;
|
|
888
|
-
const name = child.name.toLowerCase();
|
|
889
|
-
if (name === "enabled")
|
|
890
|
-
return elementText(child).toLowerCase() !== "true";
|
|
891
|
-
return name !== "executiontimelimit" || elementText(child) !== "PT72H";
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
}
|
|
895
420
|
function resolveCurrentUserSid(exec) {
|
|
896
421
|
const result = runOrThrow(exec, ["whoami", "/user", "/fo", "csv", "/nh"], {
|
|
897
422
|
message: (r) => `whoami /user failed (exit ${r.status}): ${r.stderr || r.stdout || "no output"}.`,
|
|
898
423
|
});
|
|
899
424
|
const match = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.match(/\bS-\d+(?:-\d+){2,}\b/i);
|
|
900
|
-
if (!match)
|
|
425
|
+
if (!match)
|
|
901
426
|
throw new ConfigError("whoami /user returned no Windows user SID.", "INVALID_CONFIG_FILE");
|
|
902
|
-
}
|
|
903
427
|
return `S${match[0].slice(1)}`;
|
|
904
428
|
}
|
|
905
|
-
function elementChildren(element) {
|
|
906
|
-
return element.children.filter((child) => typeof child !== "string");
|
|
907
|
-
}
|
|
908
|
-
function containsOnlyDefaults(element, defaults) {
|
|
909
|
-
const children = elementChildren(element);
|
|
910
|
-
return (children.length > 0 &&
|
|
911
|
-
children.length === element.children.length &&
|
|
912
|
-
children.every((child) => defaults[child.name.toLowerCase()] === elementText(child)));
|
|
913
|
-
}
|
|
914
|
-
function parseIsoDurationSeconds(value) {
|
|
915
|
-
const match = value.match(/^PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?$/i);
|
|
916
|
-
if (!match)
|
|
917
|
-
return undefined;
|
|
918
|
-
const seconds = Number(match[1] ?? 0) * 3600 + Number(match[2] ?? 0) * 60 + Number(match[3] ?? 0);
|
|
919
|
-
return seconds > 0 ? seconds : undefined;
|
|
920
|
-
}
|
|
921
|
-
function canonicalXmlElement(element) {
|
|
922
|
-
const attributes = Object.entries(element.attributes).sort(([a], [b]) => a.localeCompare(b));
|
|
923
|
-
const children = element.children.map((child) => typeof child === "string" ? ["text", child] : ["element", canonicalXmlElement(child)]);
|
|
924
|
-
children.sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
|
|
925
|
-
return JSON.stringify([element.name, attributes, children]);
|
|
926
|
-
}
|
|
927
|
-
function signatureWithEnabled(signature, enabled) {
|
|
928
|
-
return `${signature}|enabled=${enabled ? "true" : "false"}`;
|
|
929
|
-
}
|
|
930
|
-
function defaultSchtasksExec() {
|
|
931
|
-
return nodeExec();
|
|
932
|
-
}
|
|
933
429
|
function defaultSchtasksFs() {
|
|
934
430
|
return {
|
|
935
431
|
...nodeFs(),
|
|
936
432
|
writeFile(file, content) {
|
|
937
|
-
fs.writeFileSync(file,
|
|
433
|
+
fs.writeFileSync(file, `${content}`, { encoding: "utf16le" });
|
|
938
434
|
},
|
|
939
435
|
removeFile(file) {
|
|
940
436
|
try {
|