@stravica-ai/digital-operator-pa 0.1.0
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/LICENSE +21 -0
- package/README.md +170 -0
- package/bin/desk +9 -0
- package/bin/distill +2 -0
- package/bin/estate-sync +2 -0
- package/bin/launch +2 -0
- package/bin/memory-sync +2 -0
- package/bin/migrate-inspect +2 -0
- package/bin/pa +2 -0
- package/bin/pa-coordinator-hook +2 -0
- package/bin/pa-init +2 -0
- package/bin/research +2 -0
- package/bin/scheduler +2 -0
- package/bin/secrets +2 -0
- package/bin/slice +2 -0
- package/bin/smoke +9 -0
- package/bin/sync +2 -0
- package/dist/engineering-pa-cli/README.md +205 -0
- package/dist/engineering-pa-cli/bin/estate-sync +2 -0
- package/dist/engineering-pa-cli/bin/launch +2 -0
- package/dist/engineering-pa-cli/bin/pa +2 -0
- package/dist/engineering-pa-cli/bin/pa-coordinator-hook +12 -0
- package/dist/engineering-pa-cli/bin/research +2 -0
- package/dist/engineering-pa-cli/bin/scheduler +2 -0
- package/dist/engineering-pa-cli/bin/secrets +2 -0
- package/dist/engineering-pa-cli/bin/smoke +9 -0
- package/dist/engineering-pa-cli/bin/sync +2 -0
- package/dist/engineering-pa-cli/config/coordinator-allowlist.json +118 -0
- package/dist/engineering-pa-cli/docs/coordinator-hook.md +211 -0
- package/dist/engineering-pa-cli/docs/estate-sync.md +412 -0
- package/dist/engineering-pa-cli/docs/gsc.md +175 -0
- package/dist/engineering-pa-cli/docs/lint-linkedin.md +139 -0
- package/dist/engineering-pa-cli/docs/secrets.md +221 -0
- package/dist/engineering-pa-cli/docs/smoke.md +113 -0
- package/dist/engineering-pa-cli/docs/staging-alias.md +114 -0
- package/dist/engineering-pa-cli/index.mjs +17 -0
- package/dist/engineering-pa-cli/package.json +37 -0
- package/dist/engineering-pa-cli/src/autonomy/cli.mjs +329 -0
- package/dist/engineering-pa-cli/src/autonomy/envelope.mjs +301 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/cli.mjs +140 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/config.mjs +134 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/message.mjs +47 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/policy.mjs +211 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/repo-root.mjs +44 -0
- package/dist/engineering-pa-cli/src/coordinator-hook/self.mjs +30 -0
- package/dist/engineering-pa-cli/src/environment/cli.mjs +287 -0
- package/dist/engineering-pa-cli/src/environment/probes.mjs +302 -0
- package/dist/engineering-pa-cli/src/gsc/auth.mjs +148 -0
- package/dist/engineering-pa-cli/src/gsc/cli.mjs +93 -0
- package/dist/engineering-pa-cli/src/gsc/cloudflare.mjs +318 -0
- package/dist/engineering-pa-cli/src/gsc/commands/add-property.mjs +70 -0
- package/dist/engineering-pa-cli/src/gsc/commands/list-properties.mjs +33 -0
- package/dist/engineering-pa-cli/src/gsc/commands/pull-analytics.mjs +130 -0
- package/dist/engineering-pa-cli/src/gsc/commands/submit-sitemap.mjs +41 -0
- package/dist/engineering-pa-cli/src/gsc/commands/verify-via-cf-dns.mjs +189 -0
- package/dist/engineering-pa-cli/src/knowledge/call-log.mjs +59 -0
- package/dist/engineering-pa-cli/src/knowledge/cli.mjs +242 -0
- package/dist/engineering-pa-cli/src/knowledge/eval.mjs +446 -0
- package/dist/engineering-pa-cli/src/knowledge/formatters.mjs +331 -0
- package/dist/engineering-pa-cli/src/knowledge/graph.mjs +710 -0
- package/dist/engineering-pa-cli/src/launch/README.md +214 -0
- package/dist/engineering-pa-cli/src/launch/cli.mjs +51 -0
- package/dist/engineering-pa-cli/src/launch/commands/launch.mjs +106 -0
- package/dist/engineering-pa-cli/src/launch/identity.mjs +43 -0
- package/dist/engineering-pa-cli/src/launch/planner.mjs +44 -0
- package/dist/engineering-pa-cli/src/launch/repo-root.mjs +53 -0
- package/dist/engineering-pa-cli/src/launch/runner.mjs +147 -0
- package/dist/engineering-pa-cli/src/launch/task-loader.mjs +85 -0
- package/dist/engineering-pa-cli/src/lint/cli.mjs +61 -0
- package/dist/engineering-pa-cli/src/lint/commands/linkedin.mjs +133 -0
- package/dist/engineering-pa-cli/src/lint/detectors.mjs +90 -0
- package/dist/engineering-pa-cli/src/lint/linter.mjs +69 -0
- package/dist/engineering-pa-cli/src/lint/reporter.mjs +32 -0
- package/dist/engineering-pa-cli/src/lint/rules.mjs +132 -0
- package/dist/engineering-pa-cli/src/pa/cli.mjs +369 -0
- package/dist/engineering-pa-cli/src/pa/doc-groups.mjs +164 -0
- package/dist/engineering-pa-cli/src/pa/io.mjs +80 -0
- package/dist/engineering-pa-cli/src/pa/project-cli.mjs +385 -0
- package/dist/engineering-pa-cli/src/pa/projects.mjs +352 -0
- package/dist/engineering-pa-cli/src/pa/repo-root.mjs +64 -0
- package/dist/engineering-pa-cli/src/pa/session.mjs +254 -0
- package/dist/engineering-pa-cli/src/pa/work-cli.mjs +164 -0
- package/dist/engineering-pa-cli/src/pa/work.mjs +196 -0
- package/dist/engineering-pa-cli/src/pa/workstreams.mjs +201 -0
- package/dist/engineering-pa-cli/src/research/README.md +360 -0
- package/dist/engineering-pa-cli/src/research/briefs/extractor.md +103 -0
- package/dist/engineering-pa-cli/src/research/briefs/searcher.md +113 -0
- package/dist/engineering-pa-cli/src/research/briefs/synthesizer.md +124 -0
- package/dist/engineering-pa-cli/src/research/briefs/verifier.md +86 -0
- package/dist/engineering-pa-cli/src/research/cli.mjs +141 -0
- package/dist/engineering-pa-cli/src/research/commands/gates.mjs +176 -0
- package/dist/engineering-pa-cli/src/research/commands/list.mjs +56 -0
- package/dist/engineering-pa-cli/src/research/commands/mark.mjs +97 -0
- package/dist/engineering-pa-cli/src/research/commands/paths.mjs +69 -0
- package/dist/engineering-pa-cli/src/research/commands/prune.mjs +86 -0
- package/dist/engineering-pa-cli/src/research/commands/run.mjs +94 -0
- package/dist/engineering-pa-cli/src/research/commands/show.mjs +43 -0
- package/dist/engineering-pa-cli/src/research/docs/operator-present-flow.md +120 -0
- package/dist/engineering-pa-cli/src/research/format.mjs +59 -0
- package/dist/engineering-pa-cli/src/research/promote.mjs +260 -0
- package/dist/engineering-pa-cli/src/research/schema/finding.schema.json +55 -0
- package/dist/engineering-pa-cli/src/research/schema/gates.schema.json +34 -0
- package/dist/engineering-pa-cli/src/research/schema/manifest.schema.json +59 -0
- package/dist/engineering-pa-cli/src/research/schema/plan.schema.json +41 -0
- package/dist/engineering-pa-cli/src/research/schema/sources.schema.json +31 -0
- package/dist/engineering-pa-cli/src/research/store/findings.mjs +61 -0
- package/dist/engineering-pa-cli/src/research/store/gates.mjs +73 -0
- package/dist/engineering-pa-cli/src/research/store/index.mjs +312 -0
- package/dist/engineering-pa-cli/src/research/store/manifest.mjs +56 -0
- package/dist/engineering-pa-cli/src/research/store/plan.mjs +59 -0
- package/dist/engineering-pa-cli/src/research/store/sources.mjs +44 -0
- package/dist/engineering-pa-cli/src/research/store/validate.mjs +92 -0
- package/dist/engineering-pa-cli/src/research/workflows/research-normal.js +573 -0
- package/dist/engineering-pa-cli/src/scheduler/cli.mjs +197 -0
- package/dist/engineering-pa-cli/src/secrets/cli.mjs +254 -0
- package/dist/engineering-pa-cli/src/secrets/commands/backup-template.mjs +67 -0
- package/dist/engineering-pa-cli/src/secrets/commands/backup.mjs +85 -0
- package/dist/engineering-pa-cli/src/secrets/commands/delete.mjs +58 -0
- package/dist/engineering-pa-cli/src/secrets/commands/find.mjs +36 -0
- package/dist/engineering-pa-cli/src/secrets/commands/get.mjs +49 -0
- package/dist/engineering-pa-cli/src/secrets/commands/init.mjs +69 -0
- package/dist/engineering-pa-cli/src/secrets/commands/list.mjs +37 -0
- package/dist/engineering-pa-cli/src/secrets/commands/rotate.mjs +46 -0
- package/dist/engineering-pa-cli/src/secrets/commands/set.mjs +123 -0
- package/dist/engineering-pa-cli/src/secrets/commands/status.mjs +146 -0
- package/dist/engineering-pa-cli/src/secrets/commands/vaults.mjs +57 -0
- package/dist/engineering-pa-cli/src/secrets/commands/verify.mjs +143 -0
- package/dist/engineering-pa-cli/src/secrets/vault.mjs +766 -0
- package/dist/engineering-pa-cli/src/smoke/checks.mjs +199 -0
- package/dist/engineering-pa-cli/src/smoke/cli.mjs +108 -0
- package/dist/engineering-pa-cli/src/smoke/commands/run.mjs +237 -0
- package/dist/engineering-pa-cli/src/smoke/runner.mjs +191 -0
- package/dist/engineering-pa-cli/src/staging-alias/cli.mjs +91 -0
- package/dist/engineering-pa-cli/src/staging-alias/cloudflare.mjs +380 -0
- package/dist/engineering-pa-cli/src/staging-alias/commands/list.mjs +36 -0
- package/dist/engineering-pa-cli/src/staging-alias/commands/set.mjs +188 -0
- package/dist/engineering-pa-cli/src/staging-alias/commands/status.mjs +113 -0
- package/dist/engineering-pa-cli/src/staging-alias/commands/unset.mjs +95 -0
- package/dist/engineering-pa-cli/src/staging-alias/sites.mjs +105 -0
- package/dist/engineering-pa-cli/src/sync/cli.mjs +151 -0
- package/dist/engineering-pa-cli/src/sync/git.mjs +149 -0
- package/dist/engineering-pa-cli/src/sync/lock.mjs +30 -0
- package/dist/engineering-pa-cli/src/sync/regenerated-artifacts.mjs +114 -0
- package/dist/engineering-pa-cli/src/sync/registry-scope.mjs +260 -0
- package/dist/engineering-pa-cli/src/sync/run.mjs +531 -0
- package/dist/engineering-pa-cli/src/sync/runner.mjs +62 -0
- package/dist/engineering-pa-cli/src/sync/runs-digest.mjs +138 -0
- package/dist/engineering-pa-core/README.md +60 -0
- package/dist/engineering-pa-core/bin/desk +9 -0
- package/dist/engineering-pa-core/bin/distill +2 -0
- package/dist/engineering-pa-core/bin/memory-sync +2 -0
- package/dist/engineering-pa-core/bin/slice +2 -0
- package/dist/engineering-pa-core/docs/autonomy-envelope-tracker.md +111 -0
- package/dist/engineering-pa-core/docs/blueprints/README.md +45 -0
- package/dist/engineering-pa-core/docs/blueprints/docker-services/blueprint.md +168 -0
- package/dist/engineering-pa-core/docs/blueprints/git-identity/blueprint.md +116 -0
- package/dist/engineering-pa-core/docs/blueprints/ideas/custom-workspace-views.md +9 -0
- package/dist/engineering-pa-core/docs/blueprints/ideas/knowledge-domain-patterns.md +15 -0
- package/dist/engineering-pa-core/docs/blueprints/ideas/office-integration.md +11 -0
- package/dist/engineering-pa-core/docs/blueprints/ideas/team-scaling.md +9 -0
- package/dist/engineering-pa-core/docs/blueprints/ideas/work-personal-separation.md +9 -0
- package/dist/engineering-pa-core/docs/blueprints/scheduled-distillation/blueprint.md +129 -0
- package/dist/engineering-pa-core/docs/blueprints/tool-integration/blueprint.md +110 -0
- package/dist/engineering-pa-core/docs/commands/init.md +159 -0
- package/dist/engineering-pa-core/docs/commands/schedule.md +98 -0
- package/dist/engineering-pa-core/docs/commands/upgrade.md +67 -0
- package/dist/engineering-pa-core/docs/entity-location-map.md +93 -0
- package/dist/engineering-pa-core/docs/knowledge-scaffold.md +121 -0
- package/dist/engineering-pa-core/docs/lock.md +155 -0
- package/dist/engineering-pa-core/docs/memory-sync.md +28 -0
- package/dist/engineering-pa-core/docs/pa-operating-model.md +457 -0
- package/dist/engineering-pa-core/docs/scheduling.md +35 -0
- package/dist/engineering-pa-core/docs/standards/README.md +24 -0
- package/dist/engineering-pa-core/docs/standards/briefing-contract.md +161 -0
- package/dist/engineering-pa-core/docs/standards/tooling.md +240 -0
- package/dist/engineering-pa-core/docs/tools/distill.md +230 -0
- package/dist/engineering-pa-core/docs/tools/memory-sync.md +98 -0
- package/dist/engineering-pa-core/docs/tools/scheduler.md +243 -0
- package/dist/engineering-pa-core/docs/tools/slice.md +150 -0
- package/dist/engineering-pa-core/docs/upstream-issue-filing.md +88 -0
- package/dist/engineering-pa-core/docs/working-directory.md +29 -0
- package/dist/engineering-pa-core/docs/workspace-operations.md +46 -0
- package/dist/engineering-pa-core/index.mjs +21 -0
- package/dist/engineering-pa-core/knowledge/README.md +53 -0
- package/dist/engineering-pa-core/knowledge/canonical/.gitkeep +0 -0
- package/dist/engineering-pa-core/knowledge/content-voice/README.md +64 -0
- package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-baseline.md +133 -0
- package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-shape.md +58 -0
- package/dist/engineering-pa-core/knowledge/content-voice/banned-ai-tells-taxonomy.md +33 -0
- package/dist/engineering-pa-core/knowledge/content-voice/content-pipeline-routing.md +58 -0
- package/dist/engineering-pa-core/knowledge/content-voice/new-tell-capture-procedure.md +40 -0
- package/dist/engineering-pa-core/knowledge/content-voice/voice-guide-shape.md +43 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/README.md +34 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/briefing-contract.md +77 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/delegation-modes.md +84 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/dispatch-envelope.md +238 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/task-groups-and-orchestrators.md +62 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/team-improvement-loop.md +48 -0
- package/dist/engineering-pa-core/knowledge/delegation-dispatch/tool-development-policy.md +48 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/README.md +33 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/blast-radius-and-safety.md +50 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/investigate-root-causes.md +48 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/role-ladder.md +57 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/verify-runtime-behaviour.md +53 -0
- package/dist/engineering-pa-core/knowledge/engineering-discipline/worktree-isolation.md +50 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/README.md +33 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/claude-md-hygiene.md +47 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/journal-and-self-improvement.md +53 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/knowledge-retrieval-discipline.md +62 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/librarian-synthesis-mandate.md +56 -0
- package/dist/engineering-pa-core/knowledge/knowledge-memory/staging-and-observations.md +58 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/README.md +32 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/communication-style.md +33 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/escalation-and-correction.md +45 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/push-back-discipline.md +42 -0
- package/dist/engineering-pa-core/knowledge/operator-interface/session-boot-and-end.md +56 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/README.md +37 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/desk-cli-reference.md +52 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/environment-awareness.md +91 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/mcp-servers-catalogue.md +52 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/pa-cli-reference.md +55 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/scheduling-and-launchd.md +56 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/tool-discovery-discipline.md +49 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/working-directory-vs-project-root.md +43 -0
- package/dist/engineering-pa-core/knowledge/platform-tooling/workspace-server.md +51 -0
- package/dist/engineering-pa-core/knowledge/raw/.gitkeep +0 -0
- package/dist/engineering-pa-core/knowledge/scaffold/domains.json +5 -0
- package/dist/engineering-pa-core/knowledge/schema/.gitkeep +0 -0
- package/dist/engineering-pa-core/knowledge/work-management/README.md +33 -0
- package/dist/engineering-pa-core/knowledge/work-management/desk-cli.md +84 -0
- package/dist/engineering-pa-core/knowledge/work-management/progress-md.md +48 -0
- package/dist/engineering-pa-core/knowledge/work-management/projects.md +64 -0
- package/dist/engineering-pa-core/knowledge/work-management/spawn-log.md +48 -0
- package/dist/engineering-pa-core/knowledge/work-management/work-items.md +67 -0
- package/dist/engineering-pa-core/package.json +26 -0
- package/dist/engineering-pa-core/roles/agent-ops/definition.md +79 -0
- package/dist/engineering-pa-core/roles/dispatch-briefer/definition.md +195 -0
- package/dist/engineering-pa-core/roles/librarian/definition.md +176 -0
- package/dist/engineering-pa-core/roles/pa/definition.md +93 -0
- package/dist/engineering-pa-core/roles/system-admin/definition.md +106 -0
- package/dist/engineering-pa-core/roles/tool-developer/definition.md +116 -0
- package/dist/engineering-pa-core/roles/writer/definition.md +130 -0
- package/dist/engineering-pa-core/schema/autonomy-envelope.schema.json +61 -0
- package/dist/engineering-pa-core/schema/project.json +114 -0
- package/dist/engineering-pa-core/schema/schedule.json +63 -0
- package/dist/engineering-pa-core/schema/work-item.json +92 -0
- package/dist/engineering-pa-core/src/config/pa-package-name.mjs +30 -0
- package/dist/engineering-pa-core/src/desk/cli.mjs +95 -0
- package/dist/engineering-pa-core/src/desk/commands.mjs +398 -0
- package/dist/engineering-pa-core/src/desk/format.mjs +84 -0
- package/dist/engineering-pa-core/src/desk/repo.mjs +172 -0
- package/dist/engineering-pa-core/src/desk/team-scope.mjs +142 -0
- package/dist/engineering-pa-core/src/distill/cli.mjs +112 -0
- package/dist/engineering-pa-core/src/distill/prereqs.mjs +171 -0
- package/dist/engineering-pa-core/src/distill/prompt.mjs +74 -0
- package/dist/engineering-pa-core/src/distill/run.mjs +230 -0
- package/dist/engineering-pa-core/src/distill/runner.mjs +86 -0
- package/dist/engineering-pa-core/src/distill/staging.mjs +59 -0
- package/dist/engineering-pa-core/src/knowledge/retrieval/backlink-indexer.mjs +154 -0
- package/dist/engineering-pa-core/src/knowledge/retrieval/graph-builder.mjs +186 -0
- package/dist/engineering-pa-core/src/knowledge/retrieval/index-builder.mjs +225 -0
- package/dist/engineering-pa-core/src/knowledge/retrieval/query.mjs +263 -0
- package/dist/engineering-pa-core/src/lock/lock.mjs +202 -0
- package/dist/engineering-pa-core/src/memory-sync/cli-support.mjs +12 -0
- package/dist/engineering-pa-core/src/memory-sync/cli.mjs +56 -0
- package/dist/engineering-pa-core/src/memory-sync/commands/pull.mjs +44 -0
- package/dist/engineering-pa-core/src/memory-sync/commands/push.mjs +46 -0
- package/dist/engineering-pa-core/src/memory-sync/mirror.mjs +114 -0
- package/dist/engineering-pa-core/src/memory-sync/repo.mjs +65 -0
- package/dist/engineering-pa-core/src/scheduler/detect.mjs +111 -0
- package/dist/engineering-pa-core/src/scheduler/host.mjs +38 -0
- package/dist/engineering-pa-core/src/scheduler/index.mjs +13 -0
- package/dist/engineering-pa-core/src/scheduler/install.mjs +854 -0
- package/dist/engineering-pa-core/src/scheduler/list.mjs +168 -0
- package/dist/engineering-pa-core/src/scheduler/remove.mjs +176 -0
- package/dist/engineering-pa-core/src/scheduler/verify.mjs +435 -0
- package/dist/engineering-pa-core/src/session-boot/autonomy-injection.mjs +84 -0
- package/dist/engineering-pa-core/src/slice/cli.mjs +139 -0
- package/dist/engineering-pa-core/src/slice/inputs.mjs +174 -0
- package/dist/engineering-pa-core/src/slice/prompt.mjs +94 -0
- package/dist/engineering-pa-core/src/slice/prompts/dave-boot.md +91 -0
- package/dist/engineering-pa-core/src/slice/run.mjs +334 -0
- package/dist/engineering-pa-core-extensions-atlassian/README.md +31 -0
- package/dist/engineering-pa-core-extensions-atlassian/index.mjs +9 -0
- package/dist/engineering-pa-core-extensions-atlassian/package.json +19 -0
- package/dist/engineering-pa-core-extensions-n8n/README.md +29 -0
- package/dist/engineering-pa-core-extensions-n8n/index.mjs +8 -0
- package/dist/engineering-pa-core-extensions-n8n/package.json +19 -0
- package/dist/engineering-pa-librarian/README.md +38 -0
- package/dist/engineering-pa-librarian/index.mjs +12 -0
- package/dist/engineering-pa-librarian/package.json +19 -0
- package/dist/engineering-pa-migrate-inspect/README.md +131 -0
- package/dist/engineering-pa-migrate-inspect/bin/migrate-inspect +2 -0
- package/dist/engineering-pa-migrate-inspect/package.json +21 -0
- package/dist/engineering-pa-migrate-inspect/src/age-key-migrate.mjs +83 -0
- package/dist/engineering-pa-migrate-inspect/src/cli.mjs +97 -0
- package/dist/engineering-pa-migrate-inspect/src/inspector.mjs +237 -0
- package/dist/engineering-pa-migrate-inspect/src/platforms/linux.mjs +87 -0
- package/dist/engineering-pa-migrate-inspect/src/platforms/macos.mjs +105 -0
- package/dist/engineering-pa-migrate-inspect/src/runner.mjs +87 -0
- package/dist/engineering-pa-runbook/README.md +103 -0
- package/dist/engineering-pa-runbook/package.json +18 -0
- package/dist/engineering-pa-runbook/reference/dex-template.runbook.json +66 -0
- package/dist/engineering-pa-runbook/reference/hq-dave.runbook.json +69 -0
- package/dist/engineering-pa-runbook/reference/smarthome01.runbook.json +92 -0
- package/dist/engineering-pa-runbook/schema/inspection.schema.json +113 -0
- package/dist/engineering-pa-runbook/schema/runbook.schema.json +67 -0
- package/dist/engineering-pa-runbook/src/validator.mjs +167 -0
- package/dist/engineering-pa-scaffold/README.md +164 -0
- package/dist/engineering-pa-scaffold/bin/pa-init +2 -0
- package/dist/engineering-pa-scaffold/index.mjs +42 -0
- package/dist/engineering-pa-scaffold/package.json +22 -0
- package/dist/engineering-pa-scaffold/src/cli.mjs +115 -0
- package/dist/engineering-pa-scaffold/src/commands/init.mjs +169 -0
- package/dist/engineering-pa-scaffold/src/generator.mjs +354 -0
- package/dist/engineering-pa-scaffold/src/prompts.mjs +125 -0
- package/dist/engineering-pa-scaffold/src/runner.mjs +142 -0
- package/dist/engineering-pa-scaffold/templates/CLAUDE.md.template +196 -0
- package/dist/engineering-pa-scaffold/templates/operator-README.md.template +44 -0
- package/dist/engineering-pa-types-engineering/README.md +35 -0
- package/dist/engineering-pa-types-engineering/docs/commands/product-build.md +120 -0
- package/dist/engineering-pa-types-engineering/index.mjs +9 -0
- package/dist/engineering-pa-types-engineering/package.json +19 -0
- package/dist/engineering-pa-types-engineering/roles/product-engineer/definition.md +650 -0
- package/dist/engineering-pa-types-engineering/roles/product-engineer/templates/build-spec.md +82 -0
- package/dist/engineering-pa-types-engineering/roles/product-engineer/templates/preflight-checklist.md +158 -0
- package/dist/engineering-pa-types-engineering/roles/product-engineer/templates/worker-brief.md +130 -0
- package/dist/engineering-pa-workspace/README.md +197 -0
- package/dist/engineering-pa-workspace/app/assets/md.js +187 -0
- package/dist/engineering-pa-workspace/app/assets/modal.js +120 -0
- package/dist/engineering-pa-workspace/app/assets/theme.css +132 -0
- package/dist/engineering-pa-workspace/app/assets/theme.js +159 -0
- package/dist/engineering-pa-workspace/app/banner.js +320 -0
- package/dist/engineering-pa-workspace/app/delegations.html +345 -0
- package/dist/engineering-pa-workspace/app/home.html +1060 -0
- package/dist/engineering-pa-workspace/app/index.html +361 -0
- package/dist/engineering-pa-workspace/app/knowledge-map.html +163 -0
- package/dist/engineering-pa-workspace/app/knowledge.html +304 -0
- package/dist/engineering-pa-workspace/app/nav.json +75 -0
- package/dist/engineering-pa-workspace/app/notes.html +199 -0
- package/dist/engineering-pa-workspace/app/onboarding.html +592 -0
- package/dist/engineering-pa-workspace/app/profile.html +243 -0
- package/dist/engineering-pa-workspace/app/project-detail.html +432 -0
- package/dist/engineering-pa-workspace/app/projects.html +297 -0
- package/dist/engineering-pa-workspace/app/repos.html +598 -0
- package/dist/engineering-pa-workspace/app/schedules.html +486 -0
- package/dist/engineering-pa-workspace/app/secrets.html +164 -0
- package/dist/engineering-pa-workspace/app/staging.html +319 -0
- package/dist/engineering-pa-workspace/app/tasks.html +465 -0
- package/dist/engineering-pa-workspace/app/vendor/chart.umd.min.js +21 -0
- package/dist/engineering-pa-workspace/app/viewer.html +116 -0
- package/dist/engineering-pa-workspace/docs/DAEMON.md +104 -0
- package/dist/engineering-pa-workspace/docs/page-blueprint.md +286 -0
- package/dist/engineering-pa-workspace/docs/status-mon.md +168 -0
- package/dist/engineering-pa-workspace/index.mjs +12 -0
- package/dist/engineering-pa-workspace/nav.json.example +8 -0
- package/dist/engineering-pa-workspace/package.json +27 -0
- package/dist/engineering-pa-workspace/src/api.mjs +2086 -0
- package/dist/engineering-pa-workspace/src/install-daemon.mjs +573 -0
- package/dist/engineering-pa-workspace/src/nav.mjs +190 -0
- package/dist/engineering-pa-workspace/src/secrets-api.mjs +175 -0
- package/dist/engineering-pa-workspace/src/server.mjs +516 -0
- package/dist/engineering-pa-workspace/src/status-mon/poll.mjs +220 -0
- package/dist/engineering-pa-workspace/src/status-mon/poller.mjs +113 -0
- package/dist/engineering-pa-workspace/src/status-mon/run.mjs +133 -0
- package/dist/engineering-pa-workspace/src/supervisor.mjs +264 -0
- package/dist/engineering-pa-workspace/src/ws.mjs +633 -0
- package/index.mjs +60 -0
- package/package.json +58 -0
- package/templates/banned-ai-tells.md +102 -0
- package/templates/purpose.md +105 -0
- package/templates/voice-guide.md +98 -0
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* install.mjs -- Install a Dave schedule definition using the specified backend
|
|
4
|
+
*
|
|
5
|
+
* Usage: node node_modules/@stravica-ai/engineering-pa-core/src/scheduler/install.mjs <schedule.json> <backend>
|
|
6
|
+
*
|
|
7
|
+
* Reads a schedule JSON file and installs it using the specified backend
|
|
8
|
+
* (cron, launchd, systemd, schtasks). The schedule field uses cron syntax
|
|
9
|
+
* (minute hour day month weekday). This script translates to the native
|
|
10
|
+
* format for each backend.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
readFileSync, writeFileSync, mkdirSync, chmodSync, realpathSync,
|
|
15
|
+
existsSync, statSync, readdirSync,
|
|
16
|
+
} from 'node:fs';
|
|
17
|
+
import { join, dirname, resolve } from 'node:path';
|
|
18
|
+
import { homedir, hostname } from 'node:os';
|
|
19
|
+
import { execSync } from 'node:child_process';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
import { hostMatches } from './host.mjs';
|
|
23
|
+
|
|
24
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
25
|
+
const __dirname = dirname(__filename);
|
|
26
|
+
|
|
27
|
+
/** Default agent id used when a schedule does not declare one. Backward-compat anchor. */
|
|
28
|
+
export const DEFAULT_AGENT = 'dave';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compute the launchd label prefix for an agent. Centralised so install,
|
|
32
|
+
* remove, and list all agree on the naming scheme. Defaults to the
|
|
33
|
+
* pre-existing `com.dave.` behaviour when no agent is given.
|
|
34
|
+
* @param {string|undefined} agent
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
export function launchdLabelPrefix(agent) {
|
|
38
|
+
return `com.${agent || DEFAULT_AGENT}.`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Resolve the full launchd label for a schedule. Agent-derived: reads
|
|
43
|
+
* `sched.agent` (optional field on the schedule JSON), falling back to
|
|
44
|
+
* DEFAULT_AGENT so existing schedule files with no `agent` field keep
|
|
45
|
+
* producing exactly `com.dave.<id>`, unchanged from pre-feature behaviour.
|
|
46
|
+
* @param {object} sched
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function resolveLaunchdLabel(sched) {
|
|
50
|
+
return `${launchdLabelPrefix(sched.agent)}${sched.id}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse a cron expression into its five fields.
|
|
55
|
+
* @param {string} expr - Cron expression (minute hour dom month dow)
|
|
56
|
+
* @returns {{ minute: string, hour: string, dom: string, month: string, dow: string }}
|
|
57
|
+
*/
|
|
58
|
+
function parseCron(expr) {
|
|
59
|
+
const parts = expr.trim().split(/\s+/);
|
|
60
|
+
if (parts.length < 5) {
|
|
61
|
+
throw new Error(`Invalid cron expression (need 5 fields): ${expr}`);
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
minute: parts[0],
|
|
65
|
+
hour: parts[1],
|
|
66
|
+
dom: parts[2],
|
|
67
|
+
month: parts[3],
|
|
68
|
+
dow: parts[4],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Read and parse a schedule JSON file.
|
|
74
|
+
* @param {string} filePath
|
|
75
|
+
* @returns {object}
|
|
76
|
+
*/
|
|
77
|
+
function readSchedule(filePath) {
|
|
78
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
79
|
+
return JSON.parse(raw);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Compute the resolved DAVE_ROOT for the current install.
|
|
84
|
+
* @returns {string}
|
|
85
|
+
*/
|
|
86
|
+
function computeDaveRoot() {
|
|
87
|
+
return process.env.DAVE_ROOT || resolve(__dirname, '..', '..', '..');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the DAVE_ROOT variable and working directory.
|
|
92
|
+
* @param {string|undefined} workDir
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
function resolveWorkDir(workDir) {
|
|
96
|
+
const daveRoot = computeDaveRoot();
|
|
97
|
+
if (!workDir) return daveRoot;
|
|
98
|
+
return workDir.replace(/\$\{DAVE_ROOT\}/g, daveRoot);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Expand ${DAVE_ROOT}, ${HOME}, ${USER} inside a string value.
|
|
103
|
+
* Unknown ${...} tokens are left as-is (so users can reference
|
|
104
|
+
* environment variables that exist at execution time on the target backend).
|
|
105
|
+
* @param {string} value
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
function substituteVars(value) {
|
|
109
|
+
if (typeof value !== 'string') return value;
|
|
110
|
+
const daveRoot = computeDaveRoot();
|
|
111
|
+
const home = homedir();
|
|
112
|
+
const user = process.env.USER || process.env.USERNAME || '';
|
|
113
|
+
return value
|
|
114
|
+
.replace(/\$\{DAVE_ROOT\}/g, daveRoot)
|
|
115
|
+
.replace(/\$\{HOME\}/g, home)
|
|
116
|
+
.replace(/\$\{USER\}/g, user);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolve the environment map on a schedule, performing variable substitution
|
|
121
|
+
* on all values. Returns an empty object if no environment is set.
|
|
122
|
+
* @param {object} sched
|
|
123
|
+
* @returns {Record<string,string>}
|
|
124
|
+
*/
|
|
125
|
+
function resolveEnvironment(sched) {
|
|
126
|
+
const env = sched.environment || {};
|
|
127
|
+
const out = {};
|
|
128
|
+
for (const [k, v] of Object.entries(env)) {
|
|
129
|
+
out[k] = substituteVars(String(v));
|
|
130
|
+
}
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Validate a RESOLVED (post-substitution) working directory before it is
|
|
136
|
+
* baked into an installed schedule. Three refusals, all exit-2 (never a
|
|
137
|
+
* silent empty/wrong path):
|
|
138
|
+
*
|
|
139
|
+
* (a) any `${...}` token still present -> an unresolvable variable
|
|
140
|
+
* (unknown template var, or a var that substituted to an empty
|
|
141
|
+
* string leaving a dangling `${}`-shaped remainder is caught by the
|
|
142
|
+
* same regex — "silently empty" never reaches the backend).
|
|
143
|
+
* (b) the resolved path has a `node_modules` path segment -> the
|
|
144
|
+
* versioned-dist-path trap: DAVE_ROOT unset falls back to
|
|
145
|
+
* `resolve(__dirname, '..','..','..')`, which under an INSTALLED
|
|
146
|
+
* package resolves inside `node_modules/.pnpm/@stravica-ai+digital-operator-pa@X.Y.Z/...`
|
|
147
|
+
* — a version-pinned store path that silently breaks on the next
|
|
148
|
+
* upgrade. Proven live (w-2026-07-15-003).
|
|
149
|
+
* (c) the resolved path does not exist, or exists but is not a
|
|
150
|
+
* directory.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} resolved
|
|
153
|
+
* @throws {Error & { exitCode: 2 }}
|
|
154
|
+
*/
|
|
155
|
+
export function validateWorkDir(resolved) {
|
|
156
|
+
if (/\$\{[^}]*\}/.test(resolved)) {
|
|
157
|
+
const e = new Error(
|
|
158
|
+
`workingDirectory resolves to "${resolved}", which still contains an unresolved `
|
|
159
|
+
+ 'variable — refusing to install with a partially-substituted path. '
|
|
160
|
+
+ 'Known vars: ${DAVE_ROOT}, ${HOME}, ${USER}.',
|
|
161
|
+
);
|
|
162
|
+
e.exitCode = 2;
|
|
163
|
+
throw e;
|
|
164
|
+
}
|
|
165
|
+
const segments = resolved.split(/[\\/]/);
|
|
166
|
+
if (segments.includes('node_modules')) {
|
|
167
|
+
const e = new Error(
|
|
168
|
+
`workingDirectory resolves to "${resolved}", which is under a node_modules directory — `
|
|
169
|
+
+ 'this is almost always the versioned-dist-path trap (DAVE_ROOT unset, falling back to '
|
|
170
|
+
+ 'the installed package\'s own directory, which is version-pinned and breaks on the next '
|
|
171
|
+
+ 'upgrade). Set DAVE_ROOT explicitly, or set workingDirectory to the real operator repo root.',
|
|
172
|
+
);
|
|
173
|
+
e.exitCode = 2;
|
|
174
|
+
throw e;
|
|
175
|
+
}
|
|
176
|
+
let stat;
|
|
177
|
+
try {
|
|
178
|
+
stat = statSync(resolved);
|
|
179
|
+
} catch {
|
|
180
|
+
const e = new Error(`workingDirectory "${resolved}" does not exist.`);
|
|
181
|
+
e.exitCode = 2;
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
if (!stat.isDirectory()) {
|
|
185
|
+
const e = new Error(`workingDirectory "${resolved}" exists but is not a directory.`);
|
|
186
|
+
e.exitCode = 2;
|
|
187
|
+
throw e;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Host-enforcement decision for `scheduler install`. A schedule with a
|
|
193
|
+
* `host` field declaring a machine other than the current one refuses to
|
|
194
|
+
* install here unless `forceHost` overrides it (still with a warning — the
|
|
195
|
+
* override is for deliberate cross-provisioning, not silent mismatch).
|
|
196
|
+
* A schedule with NO `host` field at all is a warning-only pass — host
|
|
197
|
+
* data backfill onto existing schedule files is estate-side, out of this
|
|
198
|
+
* package's scope.
|
|
199
|
+
*
|
|
200
|
+
* @param {object} sched
|
|
201
|
+
* @param {object} [opts]
|
|
202
|
+
* @param {string} [opts.currentHost] - defaults to the real os.hostname()
|
|
203
|
+
* @param {boolean} [opts.forceHost]
|
|
204
|
+
* @returns {{ ok: boolean, warning?: string, reason?: string }}
|
|
205
|
+
*/
|
|
206
|
+
export function evaluateHostGate(sched, { currentHost = hostname(), forceHost = false } = {}) {
|
|
207
|
+
if (!sched.host) {
|
|
208
|
+
return {
|
|
209
|
+
ok: true,
|
|
210
|
+
warning: `schedule "${sched.id}" has no "host" field — installing without host verification `
|
|
211
|
+
+ '(host data backfill onto existing schedule files is estate-side).',
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
if (hostMatches(sched.host, currentHost)) {
|
|
215
|
+
return { ok: true };
|
|
216
|
+
}
|
|
217
|
+
if (forceHost) {
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
warning: `schedule "${sched.id}" declares host "${sched.host}" but this machine is `
|
|
221
|
+
+ `"${currentHost}" — proceeding anyway due to --force-host.`,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
reason: `schedule "${sched.id}" declares host "${sched.host}" but this machine is `
|
|
227
|
+
+ `"${currentHost}" — refusing to install here. Pass --force-host to override.`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Refuse when another schedule file in the same schedules directory
|
|
233
|
+
* resolves to the same task id (and therefore the same backend tag —
|
|
234
|
+
* `dave-<id>` for cron, `com.<agent>.<id>` for launchd, etc.). Two
|
|
235
|
+
* different files sharing an id is exactly the namespace-clobber class
|
|
236
|
+
* (`namespace-clobber-flat-shared-dirs`): whichever installs last silently
|
|
237
|
+
* owns the backend entry, and the other file's association is lost with
|
|
238
|
+
* no error anywhere.
|
|
239
|
+
*
|
|
240
|
+
* Malformed sibling files are skipped (not this tool's job to validate
|
|
241
|
+
* every other file in the directory) rather than aborting the whole
|
|
242
|
+
* install over an unrelated broken file.
|
|
243
|
+
*
|
|
244
|
+
* @param {string} scheduleFile
|
|
245
|
+
* @param {object} sched
|
|
246
|
+
* @param {object} [opts]
|
|
247
|
+
* @param {string} [opts.schedulesDir] - defaults to dirname(scheduleFile)
|
|
248
|
+
* @throws {Error & { exitCode: 2 }}
|
|
249
|
+
*/
|
|
250
|
+
export function checkNameCollision(scheduleFile, sched, opts = {}) {
|
|
251
|
+
const schedulesDir = opts.schedulesDir ?? dirname(resolve(scheduleFile));
|
|
252
|
+
const selfAbs = resolve(scheduleFile);
|
|
253
|
+
let entries;
|
|
254
|
+
try {
|
|
255
|
+
entries = readdirSync(schedulesDir);
|
|
256
|
+
} catch {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
for (const f of entries) {
|
|
260
|
+
if (!f.endsWith('.json')) continue;
|
|
261
|
+
const candidate = join(schedulesDir, f);
|
|
262
|
+
if (resolve(candidate) === selfAbs) continue;
|
|
263
|
+
let other;
|
|
264
|
+
try {
|
|
265
|
+
other = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
266
|
+
} catch {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (other && other.id === sched.id) {
|
|
270
|
+
const e = new Error(
|
|
271
|
+
`schedule id "${sched.id}" collides with ${candidate} — two different schedule files `
|
|
272
|
+
+ `resolve to the same task id/tag ("dave-${sched.id}"). Rename one of the ids before installing.`,
|
|
273
|
+
);
|
|
274
|
+
e.exitCode = 2;
|
|
275
|
+
throw e;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* XML-escape a string for safe embedding in plist values.
|
|
282
|
+
* @param {string} s
|
|
283
|
+
*/
|
|
284
|
+
function xmlEscape(s) {
|
|
285
|
+
return String(s)
|
|
286
|
+
.replace(/&/g, '&')
|
|
287
|
+
.replace(/</g, '<')
|
|
288
|
+
.replace(/>/g, '>')
|
|
289
|
+
.replace(/"/g, '"');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Backend: cron ────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Detect whether an nvm installation exists at the target's
|
|
296
|
+
* `$HOME/.nvm/nvm.sh` — tested AT INSTALL TIME against the real
|
|
297
|
+
* filesystem (not assumed). When present, the generated cron line sources
|
|
298
|
+
* it before running the command, so `pnpm`/`node` resolve exactly the way
|
|
299
|
+
* they do in an interactive nvm-managed shell. Proven necessary
|
|
300
|
+
* (w-2026-07-15-003): cron's own minimal environment has no nvm wrapper,
|
|
301
|
+
* so `pnpm` is otherwise invisible.
|
|
302
|
+
* @param {string} [homeDir]
|
|
303
|
+
* @returns {boolean}
|
|
304
|
+
*/
|
|
305
|
+
export function detectNvm(homeDir = homedir()) {
|
|
306
|
+
return existsSync(join(homeDir, '.nvm', 'nvm.sh'));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Default per-run log path for a schedule's cron entry, matching the
|
|
311
|
+
* real house pattern (`/tmp/dave-knowledge-eval-daily.log` for
|
|
312
|
+
* agent=dave, id=knowledge-eval-daily).
|
|
313
|
+
* @param {object} sched
|
|
314
|
+
* @returns {string}
|
|
315
|
+
*/
|
|
316
|
+
export function defaultCronLogPath(sched) {
|
|
317
|
+
return `/tmp/${sched.agent || DEFAULT_AGENT}-${sched.id}.log`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Build the INNER shell script a cron entry (or a `scheduler verify` run
|
|
322
|
+
* simulating one) actually executes, plus the `bash -c "<inner>"`
|
|
323
|
+
* wrapper string. Kept separate from the inner content so `verify` can
|
|
324
|
+
* run the exact same inner script via `spawn('bash', ['-c', inner], ...)`
|
|
325
|
+
* — an argv array, never a re-parsed/re-split string — while `install`
|
|
326
|
+
* embeds the wrapper form into the crontab line.
|
|
327
|
+
*
|
|
328
|
+
* Shape (matches the real house pattern, e.g.
|
|
329
|
+
* `bash -c ". $HOME/.nvm/nvm.sh && cd /home/thefoot/dave-v2 && pnpm exec
|
|
330
|
+
* pa knowledge eval --format line >> /tmp/dave-knowledge-eval-daily.log
|
|
331
|
+
* 2>&1" # dave-knowledge-eval-daily`):
|
|
332
|
+
*
|
|
333
|
+
* [. $HOME/.nvm/nvm.sh && ]<export VAR='val'; >*cd <workDir> && <command> >> <logPath> 2>&1
|
|
334
|
+
*
|
|
335
|
+
* Env vars are emitted as `export VAR='val';` statements (not bare
|
|
336
|
+
* `VAR='val' cd ...` prefix-assignment, which in POSIX shell only scopes
|
|
337
|
+
* to the single following simple command — `cd` — and therefore never
|
|
338
|
+
* actually reaches `command` after the `&&`). `export` persists them for
|
|
339
|
+
* the rest of the script, which is what a schedule author setting
|
|
340
|
+
* `environment` actually wants.
|
|
341
|
+
*
|
|
342
|
+
* @param {object} sched
|
|
343
|
+
* @param {string} workDir
|
|
344
|
+
* @param {Record<string,string>} env
|
|
345
|
+
* @param {object} [opts]
|
|
346
|
+
* @param {boolean} [opts.hasNvm]
|
|
347
|
+
* @param {string} [opts.logPath]
|
|
348
|
+
* @returns {{ inner: string, wrapped: string }}
|
|
349
|
+
*/
|
|
350
|
+
export function buildCronCommand(sched, workDir, env, opts = {}) {
|
|
351
|
+
const hasNvm = !!opts.hasNvm;
|
|
352
|
+
const logPath = opts.logPath || defaultCronLogPath(sched);
|
|
353
|
+
const preamble = hasNvm ? '. $HOME/.nvm/nvm.sh && ' : '';
|
|
354
|
+
const exportStmts = Object.entries(env)
|
|
355
|
+
.map(([k, v]) => `export ${k}='${String(v).replace(/'/g, `'\\''`)}'; `)
|
|
356
|
+
.join('');
|
|
357
|
+
const inner = `${preamble}${exportStmts}cd ${workDir} && ${sched.command} >> ${logPath} 2>&1`;
|
|
358
|
+
const wrapped = `bash -c "${inner.replace(/"/g, '\\"')}"`;
|
|
359
|
+
return { inner, wrapped };
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Build the full cron line for a schedule (schedule expression + wrapped
|
|
364
|
+
* command + trailing tag comment).
|
|
365
|
+
* @param {object} sched
|
|
366
|
+
* @param {string} workDir
|
|
367
|
+
* @param {Record<string,string>} env
|
|
368
|
+
* @param {object} [opts] - see buildCronCommand
|
|
369
|
+
* @returns {string}
|
|
370
|
+
*/
|
|
371
|
+
export function buildCronLine(sched, workDir, env, opts = {}) {
|
|
372
|
+
const tag = `dave-${sched.id}`;
|
|
373
|
+
const { wrapped } = buildCronCommand(sched, workDir, env, opts);
|
|
374
|
+
return `${sched.schedule} ${wrapped} # ${tag}`;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function installCron(sched, workDir) {
|
|
378
|
+
const env = resolveEnvironment(sched);
|
|
379
|
+
const tag = `# dave-${sched.id}`;
|
|
380
|
+
const hasNvm = detectNvm();
|
|
381
|
+
const entry = buildCronLine(sched, workDir, env, { hasNvm });
|
|
382
|
+
|
|
383
|
+
let existing = '';
|
|
384
|
+
try {
|
|
385
|
+
existing = execSync('crontab -l 2>/dev/null', { encoding: 'utf8' });
|
|
386
|
+
} catch {
|
|
387
|
+
// no crontab yet
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Remove any existing entry for this schedule
|
|
391
|
+
const filtered = existing
|
|
392
|
+
.split('\n')
|
|
393
|
+
.filter(line => !line.includes(tag))
|
|
394
|
+
.join('\n')
|
|
395
|
+
.replace(/^\n+|\n+$/g, '');
|
|
396
|
+
|
|
397
|
+
const newCrontab = filtered ? `${filtered}\n${entry}\n` : `${entry}\n`;
|
|
398
|
+
execSync('crontab -', { input: newCrontab, encoding: 'utf8' });
|
|
399
|
+
|
|
400
|
+
console.log(`Installed cron schedule: dave-${sched.id}`);
|
|
401
|
+
console.log(` ${sched.schedule} -- ${sched.description || ''}`);
|
|
402
|
+
|
|
403
|
+
return { tag: `dave-${sched.id}` };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── Backend: launchd ─────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Merge explicit environment with launchd defaults. Explicit values win.
|
|
410
|
+
* Defaults: PATH (current process PATH), HOME (os.homedir()),
|
|
411
|
+
* DAVE_ROOT (resolved). These guard against launchd's minimal agent
|
|
412
|
+
* environment (/usr/bin:/bin:/usr/sbin:/sbin, no HOME) which silently
|
|
413
|
+
* breaks anything installed via Homebrew.
|
|
414
|
+
* @param {Record<string,string>} explicitEnv
|
|
415
|
+
* @returns {Record<string,string>}
|
|
416
|
+
*/
|
|
417
|
+
export function withLaunchdDefaults(explicitEnv) {
|
|
418
|
+
const defaults = {
|
|
419
|
+
PATH: process.env.PATH || '/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin',
|
|
420
|
+
HOME: homedir(),
|
|
421
|
+
DAVE_ROOT: computeDaveRoot(),
|
|
422
|
+
};
|
|
423
|
+
return { ...defaults, ...explicitEnv };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Build the EnvironmentVariables <dict> block (indented 2 spaces, matching
|
|
428
|
+
* the surrounding plist indentation). Returns an empty string when env is
|
|
429
|
+
* empty so the block is omitted entirely.
|
|
430
|
+
* @param {Record<string,string>} env
|
|
431
|
+
* @returns {string}
|
|
432
|
+
*/
|
|
433
|
+
function buildLaunchdEnvBlock(env) {
|
|
434
|
+
const entries = Object.entries(env);
|
|
435
|
+
if (entries.length === 0) return '';
|
|
436
|
+
const inner = entries
|
|
437
|
+
.map(([k, v]) => ` <key>${xmlEscape(k)}</key>\n <string>${xmlEscape(v)}</string>`)
|
|
438
|
+
.join('\n');
|
|
439
|
+
return ` <key>EnvironmentVariables</key>\n <dict>\n${inner}\n </dict>\n`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Expand a single cron field into either "wildcard" (no constraint — the
|
|
444
|
+
* launchd key is omitted, same as historical behaviour) or a sorted,
|
|
445
|
+
* deduped list of concrete integer values. Handles plain values, comma
|
|
446
|
+
* lists, ranges (`a-b`), stepped ranges (`a-b` over `n`), and step-wildcards
|
|
447
|
+
* (star over `n`) — the step-wildcard form is what BUG 4 could not express
|
|
448
|
+
* at all (launchd has no native step syntax; it needs the concrete value set).
|
|
449
|
+
* @param {string} field - Raw cron field text (e.g. a bare star, a stepped star, '3,6,9', '1-5')
|
|
450
|
+
* @param {number} min
|
|
451
|
+
* @param {number} max
|
|
452
|
+
* @returns {{ kind: 'wildcard' } | { kind: 'values', values: number[] }}
|
|
453
|
+
*/
|
|
454
|
+
export function expandCronField(field, min, max) {
|
|
455
|
+
if (field === '*') return { kind: 'wildcard' };
|
|
456
|
+
|
|
457
|
+
const values = new Set();
|
|
458
|
+
for (const token of field.split(',')) {
|
|
459
|
+
const stepWildcard = token.match(/^\*\/(\d+)$/);
|
|
460
|
+
const stepRange = token.match(/^(\d+)-(\d+)\/(\d+)$/);
|
|
461
|
+
const plainRange = token.match(/^(\d+)-(\d+)$/);
|
|
462
|
+
const single = token.match(/^(\d+)$/);
|
|
463
|
+
|
|
464
|
+
if (stepWildcard) {
|
|
465
|
+
const step = Number(stepWildcard[1]);
|
|
466
|
+
if (step <= 0) throw new Error(`Invalid cron step (must be > 0): "${token}"`);
|
|
467
|
+
for (let v = min; v <= max; v += step) values.add(v);
|
|
468
|
+
} else if (stepRange) {
|
|
469
|
+
const a = Number(stepRange[1]);
|
|
470
|
+
const b = Number(stepRange[2]);
|
|
471
|
+
const step = Number(stepRange[3]);
|
|
472
|
+
if (step <= 0) throw new Error(`Invalid cron step (must be > 0): "${token}"`);
|
|
473
|
+
for (let v = a; v <= b; v += step) values.add(v);
|
|
474
|
+
} else if (plainRange) {
|
|
475
|
+
const a = Number(plainRange[1]);
|
|
476
|
+
const b = Number(plainRange[2]);
|
|
477
|
+
for (let v = a; v <= b; v++) values.add(v);
|
|
478
|
+
} else if (single) {
|
|
479
|
+
values.add(Number(single[1]));
|
|
480
|
+
} else {
|
|
481
|
+
throw new Error(`Unsupported cron field token for launchd translation: "${token}"`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return { kind: 'values', values: [...values].sort((a, b) => a - b) };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const CRON_FIELD_SPECS = [
|
|
489
|
+
{ key: 'Minute', field: 'minute', min: 0, max: 59 },
|
|
490
|
+
{ key: 'Hour', field: 'hour', min: 0, max: 23 },
|
|
491
|
+
{ key: 'Day', field: 'dom', min: 1, max: 31 },
|
|
492
|
+
{ key: 'Month', field: 'month', min: 1, max: 12 },
|
|
493
|
+
{ key: 'Weekday', field: 'dow', min: 0, max: 7 },
|
|
494
|
+
];
|
|
495
|
+
|
|
496
|
+
// Sanity cap on the StartCalendarInterval array launchd will accept. A
|
|
497
|
+
// pathological step (e.g. every minute across an unconstrained day/month)
|
|
498
|
+
// would otherwise silently generate thousands of dict entries.
|
|
499
|
+
const MAX_CALENDAR_COMBINATIONS = 366;
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Build the StartCalendarInterval block (BUG 4 fix). launchd has no cron
|
|
503
|
+
* step syntax — a stepped field (or a list/range) must be expressed as
|
|
504
|
+
* multiple concrete StartCalendarInterval dicts, one per combination of
|
|
505
|
+
* the non-wildcard fields' expanded values (cartesian product), inside an
|
|
506
|
+
* <array>. Schedules where every field is a plain single value or a
|
|
507
|
+
* wildcard collapse to exactly the old single <dict> shape, so existing
|
|
508
|
+
* plain-cron installs are byte-for-byte unaffected.
|
|
509
|
+
* @param {object} cron
|
|
510
|
+
* @returns {string}
|
|
511
|
+
*/
|
|
512
|
+
export function buildStartCalendarIntervalBlock(cron) {
|
|
513
|
+
const expanded = CRON_FIELD_SPECS
|
|
514
|
+
.map((spec) => ({ key: spec.key, expansion: expandCronField(cron[spec.field], spec.min, spec.max) }))
|
|
515
|
+
.filter((e) => e.expansion.kind !== 'wildcard');
|
|
516
|
+
|
|
517
|
+
let combos = [[]];
|
|
518
|
+
for (const { key, expansion } of expanded) {
|
|
519
|
+
const next = [];
|
|
520
|
+
for (const combo of combos) {
|
|
521
|
+
for (const value of expansion.values) {
|
|
522
|
+
next.push([...combo, { key, value }]);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
combos = next;
|
|
526
|
+
if (combos.length > MAX_CALENDAR_COMBINATIONS) {
|
|
527
|
+
throw new Error(
|
|
528
|
+
`Cron expression expands to more than ${MAX_CALENDAR_COMBINATIONS} launchd StartCalendarInterval entries — too fine-grained to express as a step schedule on launchd. Narrow the schedule or use the cron/systemd backend instead.`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const dictFor = (combo) => {
|
|
534
|
+
const inner = combo.map(({ key, value }) => ` <key>${key}</key>\n <integer>${value}</integer>`).join('\n');
|
|
535
|
+
return ` <dict>\n${inner}\n </dict>`;
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
if (combos.length <= 1) {
|
|
539
|
+
const combo = combos[0] || [];
|
|
540
|
+
const inner = combo.map(({ key, value }) => ` <key>${key}</key>\n <integer>${value}</integer>`).join('\n');
|
|
541
|
+
return ` <key>StartCalendarInterval</key>\n <dict>\n${inner}\n </dict>\n`;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
const dicts = combos.map(dictFor).join('\n');
|
|
545
|
+
return ` <key>StartCalendarInterval</key>\n <array>\n${dicts}\n </array>\n`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Generate the full plist content for a schedule. Pure function — safe to
|
|
550
|
+
* call in tests. Always emits an EnvironmentVariables block on launchd
|
|
551
|
+
* (auto-injected defaults when the schedule has no explicit environment).
|
|
552
|
+
* The launchd Label is agent-derived (`com.<sched.agent||'dave'>.<id>`) —
|
|
553
|
+
* see resolveLaunchdLabel.
|
|
554
|
+
* @param {object} sched
|
|
555
|
+
* @param {string} workDir
|
|
556
|
+
* @param {object} cron
|
|
557
|
+
* @returns {string}
|
|
558
|
+
*/
|
|
559
|
+
export function buildLaunchdPlist(sched, workDir, cron) {
|
|
560
|
+
const label = resolveLaunchdLabel(sched);
|
|
561
|
+
const calendarBlock = buildStartCalendarIntervalBlock(cron);
|
|
562
|
+
|
|
563
|
+
const cmdParts = sched.command.split(/\s+/);
|
|
564
|
+
const programArgs = cmdParts.map(p => ` <string>${xmlEscape(p)}</string>`).join('\n');
|
|
565
|
+
|
|
566
|
+
const mergedEnv = withLaunchdDefaults(resolveEnvironment(sched));
|
|
567
|
+
const envBlock = buildLaunchdEnvBlock(mergedEnv);
|
|
568
|
+
|
|
569
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
570
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
571
|
+
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
572
|
+
<plist version="1.0">
|
|
573
|
+
<dict>
|
|
574
|
+
<key>Label</key>
|
|
575
|
+
<string>${label}</string>
|
|
576
|
+
<key>ProgramArguments</key>
|
|
577
|
+
<array>
|
|
578
|
+
${programArgs}
|
|
579
|
+
</array>
|
|
580
|
+
<key>WorkingDirectory</key>
|
|
581
|
+
<string>${xmlEscape(workDir)}</string>
|
|
582
|
+
${envBlock}${calendarBlock} <key>StandardOutPath</key>
|
|
583
|
+
<string>/tmp/dave-${sched.id}-stdout.log</string>
|
|
584
|
+
<key>StandardErrorPath</key>
|
|
585
|
+
<string>/tmp/dave-${sched.id}-stderr.log</string>
|
|
586
|
+
<key>RunAtLoad</key>
|
|
587
|
+
<false/>
|
|
588
|
+
</dict>
|
|
589
|
+
</plist>
|
|
590
|
+
`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function installLaunchd(sched, workDir, cron) {
|
|
594
|
+
const label = resolveLaunchdLabel(sched);
|
|
595
|
+
const plistDir = join(homedir(), 'Library', 'LaunchAgents');
|
|
596
|
+
const plistPath = join(plistDir, `${label}.plist`);
|
|
597
|
+
|
|
598
|
+
mkdirSync(plistDir, { recursive: true });
|
|
599
|
+
const plist = buildLaunchdPlist(sched, workDir, cron);
|
|
600
|
+
writeFileSync(plistPath, plist, 'utf8');
|
|
601
|
+
|
|
602
|
+
// Unload if already loaded, then load
|
|
603
|
+
const uid = execSync('id -u', { encoding: 'utf8' }).trim();
|
|
604
|
+
try { execSync(`launchctl bootout gui/${uid}/${label}`, { stdio: 'pipe' }); } catch { /* not loaded */ }
|
|
605
|
+
execSync(`launchctl bootstrap gui/${uid} ${plistPath}`);
|
|
606
|
+
|
|
607
|
+
console.log(`Installed launchd schedule: ${label}`);
|
|
608
|
+
console.log(` Plist: ${plistPath}`);
|
|
609
|
+
console.log(` ${sched.schedule} -- ${sched.description || ''}`);
|
|
610
|
+
|
|
611
|
+
return { label, plistPath };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ── Backend: systemd ─────────────────────────────────────────────────
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Build the .service unit contents.
|
|
618
|
+
* @param {object} sched
|
|
619
|
+
* @param {string} workDir
|
|
620
|
+
* @param {Record<string,string>} env
|
|
621
|
+
* @returns {string}
|
|
622
|
+
*/
|
|
623
|
+
export function buildSystemdService(sched, workDir, env) {
|
|
624
|
+
const description = sched.description || `Dave scheduled task: ${sched.id}`;
|
|
625
|
+
const envLines = Object.entries(env)
|
|
626
|
+
.map(([k, v]) => `Environment=${k}=${v}`)
|
|
627
|
+
.join('\n');
|
|
628
|
+
const envBlock = envLines ? `${envLines}\n` : '';
|
|
629
|
+
return `[Unit]
|
|
630
|
+
Description=${description}
|
|
631
|
+
|
|
632
|
+
[Service]
|
|
633
|
+
Type=oneshot
|
|
634
|
+
WorkingDirectory=${workDir}
|
|
635
|
+
${envBlock}ExecStart=/usr/bin/env bash -c '${sched.command.replace(/'/g, `'\\''`)}'
|
|
636
|
+
|
|
637
|
+
[Install]
|
|
638
|
+
WantedBy=default.target
|
|
639
|
+
`;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Build the .timer unit contents.
|
|
644
|
+
* @param {object} sched
|
|
645
|
+
* @param {object} cron
|
|
646
|
+
* @returns {string}
|
|
647
|
+
*/
|
|
648
|
+
export function buildSystemdTimer(sched, cron) {
|
|
649
|
+
const description = sched.description || `Dave scheduled task: ${sched.id}`;
|
|
650
|
+
const dowNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
651
|
+
const dowPart = cron.dow !== '*' ? (dowNames[Number(cron.dow)] || '*') : '';
|
|
652
|
+
let onCalendar = `*-${cron.month}-${cron.dom} ${cron.hour}:${cron.minute}:00`;
|
|
653
|
+
if (dowPart) onCalendar = `${dowPart} ${onCalendar}`;
|
|
654
|
+
return `[Unit]
|
|
655
|
+
Description=Timer for ${description}
|
|
656
|
+
|
|
657
|
+
[Timer]
|
|
658
|
+
OnCalendar=${onCalendar}
|
|
659
|
+
Persistent=true
|
|
660
|
+
|
|
661
|
+
[Install]
|
|
662
|
+
WantedBy=timers.target
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function installSystemd(sched, workDir, cron) {
|
|
667
|
+
const unitName = `dave-${sched.id}`;
|
|
668
|
+
const unitDir = join(homedir(), '.config', 'systemd', 'user');
|
|
669
|
+
mkdirSync(unitDir, { recursive: true });
|
|
670
|
+
|
|
671
|
+
const env = resolveEnvironment(sched);
|
|
672
|
+
const service = buildSystemdService(sched, workDir, env);
|
|
673
|
+
const timer = buildSystemdTimer(sched, cron);
|
|
674
|
+
writeFileSync(join(unitDir, `${unitName}.service`), service, 'utf8');
|
|
675
|
+
writeFileSync(join(unitDir, `${unitName}.timer`), timer, 'utf8');
|
|
676
|
+
|
|
677
|
+
execSync('systemctl --user daemon-reload');
|
|
678
|
+
execSync(`systemctl --user enable --now ${unitName}.timer`);
|
|
679
|
+
|
|
680
|
+
const description = sched.description || `Dave scheduled task: ${sched.id}`;
|
|
681
|
+
console.log(`Installed systemd schedule: ${unitName}`);
|
|
682
|
+
console.log(` Service: ${join(unitDir, unitName + '.service')}`);
|
|
683
|
+
console.log(` Timer: ${join(unitDir, unitName + '.timer')}`);
|
|
684
|
+
console.log(` ${sched.schedule} -- ${description}`);
|
|
685
|
+
|
|
686
|
+
return { unitName };
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// ── Backend: schtasks (Windows) ──────────────────────────────────────
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* Build the cmd.exe wrapper string used as the /TR argument to schtasks.
|
|
693
|
+
*
|
|
694
|
+
* schtasks has no native env-var support, so env vars are emitted as
|
|
695
|
+
* `set VAR=val &&` pairs before `cd /d workDir && command`. Callers
|
|
696
|
+
* that need stable cross-process env should instead write a .cmd wrapper
|
|
697
|
+
* to disk and point schtasks at it — we keep the inline form for
|
|
698
|
+
* parity with the other backends on simple schedules.
|
|
699
|
+
* @param {object} sched
|
|
700
|
+
* @param {string} workDir
|
|
701
|
+
* @param {Record<string,string>} env
|
|
702
|
+
* @returns {string}
|
|
703
|
+
*/
|
|
704
|
+
export function buildSchtasksCommand(sched, workDir, env) {
|
|
705
|
+
const envPrefix = Object.entries(env)
|
|
706
|
+
.map(([k, v]) => `set "${k}=${String(v).replace(/"/g, '""')}" &&`)
|
|
707
|
+
.join(' ');
|
|
708
|
+
const inner = envPrefix
|
|
709
|
+
? `${envPrefix} cd /d ${workDir} && ${sched.command}`
|
|
710
|
+
: `cd /d ${workDir} && ${sched.command}`;
|
|
711
|
+
return `cmd /c "${inner}"`;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function installSchtasks(sched, workDir, cron) {
|
|
715
|
+
const taskName = `dave-${sched.id}`;
|
|
716
|
+
const env = resolveEnvironment(sched);
|
|
717
|
+
|
|
718
|
+
const fmtHour = cron.hour === '*' ? '00' : cron.hour.padStart(2, '0');
|
|
719
|
+
const fmtMin = cron.minute === '*' ? '00' : cron.minute.padStart(2, '0');
|
|
720
|
+
const startTime = `${fmtHour}:${fmtMin}`;
|
|
721
|
+
|
|
722
|
+
let scType = 'DAILY';
|
|
723
|
+
let scModifier = '';
|
|
724
|
+
const dowNames = ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT'];
|
|
725
|
+
|
|
726
|
+
if (cron.dom === '*' && cron.month === '*' && cron.dow === '*') {
|
|
727
|
+
scType = 'DAILY';
|
|
728
|
+
} else if (cron.dow !== '*') {
|
|
729
|
+
scType = 'WEEKLY';
|
|
730
|
+
scModifier = `/D ${dowNames[Number(cron.dow)] || 'MON'}`;
|
|
731
|
+
} else if (cron.dom !== '*') {
|
|
732
|
+
scType = 'MONTHLY';
|
|
733
|
+
scModifier = `/D ${cron.dom}`;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
try {
|
|
737
|
+
execSync(`schtasks.exe /Delete /TN "${taskName}" /F`, { stdio: 'pipe' });
|
|
738
|
+
} catch { /* not found */ }
|
|
739
|
+
|
|
740
|
+
const tr = buildSchtasksCommand(sched, workDir, env);
|
|
741
|
+
let cmd = `schtasks.exe /Create /TN "${taskName}" /TR "${tr}" /SC ${scType} /ST ${startTime}`;
|
|
742
|
+
if (scModifier) cmd += ` ${scModifier}`;
|
|
743
|
+
|
|
744
|
+
execSync(cmd);
|
|
745
|
+
|
|
746
|
+
console.log(`Installed schtasks schedule: ${taskName}`);
|
|
747
|
+
console.log(` ${sched.schedule} -- ${sched.description || ''}`);
|
|
748
|
+
|
|
749
|
+
return { taskName };
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// ── Main ─────────────────────────────────────────────────────────────
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Install a schedule definition using the specified backend.
|
|
756
|
+
*
|
|
757
|
+
* BUG 2 fix: this used to fall off the end of the function with no return
|
|
758
|
+
* statement, so every caller checking a result's `.ok` field (the CLI
|
|
759
|
+
* wrapper included) was reading `undefined.ok`. Every path now returns a
|
|
760
|
+
* result object; callers can rely on `.ok` being present.
|
|
761
|
+
* @param {string} scheduleFile - Path to the schedule JSON file
|
|
762
|
+
* @param {string} backend - Backend name (cron, launchd, systemd, schtasks)
|
|
763
|
+
* @param {object} [opts]
|
|
764
|
+
* @param {boolean} [opts.forceHost] - override a foreign-host refusal (with a warning)
|
|
765
|
+
* @param {string} [opts.schedulesDir] - collision-check dir; defaults to dirname(scheduleFile)
|
|
766
|
+
* @param {string} [opts.currentHost] - test-only override for the host-gate check
|
|
767
|
+
* @returns {{ ok: boolean, id: string, backend: string, skipped?: boolean, reason?: string, warnings?: string[] }}
|
|
768
|
+
*/
|
|
769
|
+
export function install(scheduleFile, backend, opts = {}) {
|
|
770
|
+
const sched = readSchedule(scheduleFile);
|
|
771
|
+
|
|
772
|
+
if (!sched.id || !sched.schedule || !sched.command) {
|
|
773
|
+
throw new Error('Schedule file must have id, schedule, and command fields');
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
const warnings = [];
|
|
777
|
+
|
|
778
|
+
// Name-collision check — refuse before touching any backend if another
|
|
779
|
+
// schedule file in the same directory resolves to the same task id.
|
|
780
|
+
checkNameCollision(scheduleFile, sched, { schedulesDir: opts.schedulesDir });
|
|
781
|
+
|
|
782
|
+
// Host-enforcement gate — refuse (exit 2) a schedule declaring a foreign
|
|
783
|
+
// host unless --force-host overrides it. Missing `host` field is a
|
|
784
|
+
// warning, not a refusal (data backfill is estate-side).
|
|
785
|
+
const hostGate = evaluateHostGate(sched, { currentHost: opts.currentHost, forceHost: opts.forceHost });
|
|
786
|
+
if (!hostGate.ok) {
|
|
787
|
+
const e = new Error(hostGate.reason);
|
|
788
|
+
e.exitCode = 2;
|
|
789
|
+
throw e;
|
|
790
|
+
}
|
|
791
|
+
if (hostGate.warning) {
|
|
792
|
+
console.warn(`Warning: ${hostGate.warning}`);
|
|
793
|
+
warnings.push(hostGate.warning);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (sched.enabled === false) {
|
|
797
|
+
console.log(`Schedule '${sched.id}' is disabled. Set enabled=true to install.`);
|
|
798
|
+
return { ok: true, id: sched.id, backend, skipped: true, reason: 'disabled', ...(warnings.length ? { warnings } : {}) };
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const workDir = resolveWorkDir(sched.workingDirectory);
|
|
802
|
+
validateWorkDir(workDir);
|
|
803
|
+
const cron = parseCron(sched.schedule);
|
|
804
|
+
|
|
805
|
+
let detail;
|
|
806
|
+
switch (backend) {
|
|
807
|
+
case 'cron': detail = installCron(sched, workDir); break;
|
|
808
|
+
case 'launchd': detail = installLaunchd(sched, workDir, cron); break;
|
|
809
|
+
case 'systemd': detail = installSystemd(sched, workDir, cron); break;
|
|
810
|
+
case 'schtasks': detail = installSchtasks(sched, workDir, cron); break;
|
|
811
|
+
default:
|
|
812
|
+
throw new Error(`Unknown backend: ${backend}. Supported: cron, launchd, systemd, schtasks`);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
return { ok: true, id: sched.id, backend, ...detail, ...(warnings.length ? { warnings } : {}) };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Exported for tests — pure helpers.
|
|
819
|
+
export { parseCron, resolveWorkDir, resolveEnvironment, substituteVars };
|
|
820
|
+
|
|
821
|
+
// ── CLI entry point ──────────────────────────────────────────────────
|
|
822
|
+
//
|
|
823
|
+
// BUG 3 fix: the previous check compared the realpath-resolved
|
|
824
|
+
// import.meta.url against the raw (possibly symlinked) process.argv[1] via
|
|
825
|
+
// String#endsWith. Under pnpm, a direct invocation's argv[1] is frequently
|
|
826
|
+
// a symlink (workspace link or .pnpm store link) while import.meta.url is
|
|
827
|
+
// always the fully-resolved real path, so the comparison silently failed
|
|
828
|
+
// and this entrypoint became a no-op that still exited 0. Fix: realpath
|
|
829
|
+
// BOTH sides before comparing (matches the pattern already proven correct
|
|
830
|
+
// in packages/workspace/src/server.mjs and install-daemon.mjs).
|
|
831
|
+
|
|
832
|
+
function isDirectlyInvoked() {
|
|
833
|
+
if (!process.argv[1]) return false;
|
|
834
|
+
try {
|
|
835
|
+
return fileURLToPath(import.meta.url) === realpathSync(resolve(process.argv[1]));
|
|
836
|
+
} catch {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (isDirectlyInvoked()) {
|
|
842
|
+
const args = process.argv.slice(2);
|
|
843
|
+
if (args.length < 2) {
|
|
844
|
+
console.error('Usage: node install.mjs <schedule.json> <backend>');
|
|
845
|
+
console.error('Backends: cron, launchd, systemd, schtasks');
|
|
846
|
+
process.exit(1);
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
install(args[0], args[1]);
|
|
850
|
+
} catch (err) {
|
|
851
|
+
console.error(`Error: ${err.message}`);
|
|
852
|
+
process.exit(1);
|
|
853
|
+
}
|
|
854
|
+
}
|