@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,766 @@
|
|
|
1
|
+
// vault.mjs — domain + chokepoint for SOPS/age shell-outs.
|
|
2
|
+
//
|
|
3
|
+
// Every external-effect call (spawning sops, spawning age, touching the
|
|
4
|
+
// filesystem in the vault/key paths) routes through a function in this
|
|
5
|
+
// module. Commands in src/commands/ consume this module; they never call
|
|
6
|
+
// child_process themselves.
|
|
7
|
+
//
|
|
8
|
+
// Path resolution is deliberately injectable via `paths()` so tests can
|
|
9
|
+
// operate against a temp vault and a temp key dir without ever touching
|
|
10
|
+
// the real ~/.config/dave or repo .vault.
|
|
11
|
+
|
|
12
|
+
import { spawn } from 'node:child_process';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, chmodSync } from 'node:fs';
|
|
15
|
+
import { join, dirname, resolve, basename, sep } from 'node:path';
|
|
16
|
+
import { fileURLToPath } from 'node:url';
|
|
17
|
+
import { mkdtempSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
|
|
20
|
+
// ── Path resolution ──────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
// Workspace root resolution, in priority order:
|
|
23
|
+
// 1. Upward walk from cwd for a VAULT-BEARING directory — one containing
|
|
24
|
+
// a `.vault/` dir, or a `vault/` dir with at least one *.enc.json
|
|
25
|
+
// (canonical-clone layout). A bare git repo WITHOUT a vault does not
|
|
26
|
+
// stop resolution: it falls through to the next tiers, LOUDLY — a
|
|
27
|
+
// stderr notice names the store actually targeted on every such
|
|
28
|
+
// invocation. Visible redirects are a feature; invisible ones are
|
|
29
|
+
// the wrong-store incident class this exists to prevent.
|
|
30
|
+
// (v1 history: root was `resolve(__dirname, '../../../..')`, which
|
|
31
|
+
// landed at the operator repo root by accident in v1's
|
|
32
|
+
// `core/tools/secrets/src/` layout; in v2 the module lives in
|
|
33
|
+
// `node_modules/` and __dirname-navigation lands outside the tree.)
|
|
34
|
+
// 2. DAVE_ROOT / DAVE_REPO_ROOT / PA_REPO_ROOT env.
|
|
35
|
+
// 3. `workspaceRoot` key in ~/.config/stravica-pa/defaults.json
|
|
36
|
+
// (written by the user or provisioning — this CLI never creates it).
|
|
37
|
+
// 4. Error listing exactly what was tried, in order.
|
|
38
|
+
//
|
|
39
|
+
// PRECEDENCE CHANGE vs the previously-shipped behaviour: env used to be
|
|
40
|
+
// checked BEFORE the cwd walk. Now the walk wins — standing inside
|
|
41
|
+
// workspace A with DAVE_ROOT pointing at workspace B targets A. And the
|
|
42
|
+
// old ultimate fallback of bare `cwd` (which invented a phantom
|
|
43
|
+
// `<cwd>/.vault/` and the misleading "No recipients file" error whenever
|
|
44
|
+
// the CLI ran outside a workspace) is gone entirely: tiers 2-4 replace it.
|
|
45
|
+
//
|
|
46
|
+
// `secrets init` is the one deliberate exception (allowGitRootWithoutVault):
|
|
47
|
+
// init CREATES a vault, so a vault-less git repo you are standing in IS its
|
|
48
|
+
// legitimate target — same bootstrap behaviour as before this change.
|
|
49
|
+
|
|
50
|
+
function hasVaultMarker(d) {
|
|
51
|
+
if (existsSync(join(d, '.vault'))) return true;
|
|
52
|
+
const canon = join(d, 'vault');
|
|
53
|
+
if (existsSync(canon)) {
|
|
54
|
+
try {
|
|
55
|
+
return readdirSync(canon).some((f) => f.endsWith('.enc.json'));
|
|
56
|
+
} catch { /* unreadable — not a marker */ }
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function walkForWorkspace(start) {
|
|
62
|
+
let d = start;
|
|
63
|
+
let prev;
|
|
64
|
+
let gitRootWithoutVault = null;
|
|
65
|
+
while (d && d !== prev) {
|
|
66
|
+
if (hasVaultMarker(d)) return { root: d, gitRootWithoutVault };
|
|
67
|
+
if (!gitRootWithoutVault && existsSync(join(d, '.git'))) gitRootWithoutVault = d;
|
|
68
|
+
prev = d;
|
|
69
|
+
d = dirname(d);
|
|
70
|
+
}
|
|
71
|
+
return { root: null, gitRootWithoutVault };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function defaultsFilePath(home) {
|
|
75
|
+
return join(home, '.config', 'stravica-pa', 'defaults.json');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readDefaultsWorkspaceRoot(home) {
|
|
79
|
+
const file = defaultsFilePath(home);
|
|
80
|
+
if (!existsSync(file)) return { file, root: null, reason: 'file not present' };
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(readFileSync(file, 'utf-8'));
|
|
83
|
+
if (parsed && typeof parsed.workspaceRoot === 'string' && parsed.workspaceRoot.trim() !== '') {
|
|
84
|
+
return { file, root: resolve(parsed.workspaceRoot), reason: null };
|
|
85
|
+
}
|
|
86
|
+
return { file, root: null, reason: 'workspaceRoot key not set' };
|
|
87
|
+
} catch {
|
|
88
|
+
return { file, root: null, reason: 'file is not valid JSON' };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the workspace root using the tiered fallback above. Returns
|
|
94
|
+
* { root, source } where source is 'cwd-walk' | 'env' | 'defaults'.
|
|
95
|
+
* Prints a mandatory stderr notice whenever the nearest git repo has no
|
|
96
|
+
* vault and resolution redirected elsewhere. Throws VaultError (exit 1,
|
|
97
|
+
* message already printed to stderr) listing every tier tried when
|
|
98
|
+
* nothing matches.
|
|
99
|
+
*/
|
|
100
|
+
export function resolveWorkspaceRoot(opts = {}) {
|
|
101
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
102
|
+
const env = opts.env ?? process.env;
|
|
103
|
+
const home = opts.home ?? homedir();
|
|
104
|
+
const tried = [];
|
|
105
|
+
|
|
106
|
+
const { root: walked, gitRootWithoutVault } = walkForWorkspace(cwd);
|
|
107
|
+
|
|
108
|
+
// init bootstrap: a vault-less git repo you're standing in is init's
|
|
109
|
+
// legitimate creation target, not a redirect case.
|
|
110
|
+
if (opts.allowGitRootWithoutVault && !walked && gitRootWithoutVault) {
|
|
111
|
+
return { root: gitRootWithoutVault, source: 'cwd-walk' };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sourceLabel = {
|
|
115
|
+
'cwd-walk': (r) => `an enclosing workspace at ${r}`,
|
|
116
|
+
env: (r) => `workspace ${r} (from $DAVE_ROOT/$DAVE_REPO_ROOT/$PA_REPO_ROOT)`,
|
|
117
|
+
defaults: (r, file) => `workspace ${r} (from "workspaceRoot" in ${file})`,
|
|
118
|
+
};
|
|
119
|
+
const finish = (root, source, defaultsFile) => {
|
|
120
|
+
if (gitRootWithoutVault && root !== gitRootWithoutVault) {
|
|
121
|
+
console.error(`notice: no vault in ${gitRootWithoutVault} — using ${sourceLabel[source](root, defaultsFile)}`);
|
|
122
|
+
}
|
|
123
|
+
return { root, source };
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
if (walked) return finish(walked, 'cwd-walk');
|
|
127
|
+
tried.push(
|
|
128
|
+
`walked up from ${cwd} looking for a workspace (.vault/ or vault/*.enc.json) — none found`
|
|
129
|
+
+ (gitRootWithoutVault ? ` (nearest git repo ${gitRootWithoutVault} has no vault)` : ''),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const envRoot = env.DAVE_ROOT || env.DAVE_REPO_ROOT || env.PA_REPO_ROOT;
|
|
133
|
+
if (envRoot) return finish(resolve(envRoot), 'env');
|
|
134
|
+
tried.push('$DAVE_ROOT / $DAVE_REPO_ROOT / $PA_REPO_ROOT — not set');
|
|
135
|
+
|
|
136
|
+
const defaults = readDefaultsWorkspaceRoot(home);
|
|
137
|
+
if (defaults.root) return finish(defaults.root, 'defaults', defaults.file);
|
|
138
|
+
tried.push(`"workspaceRoot" in ${defaults.file} — ${defaults.reason}`);
|
|
139
|
+
|
|
140
|
+
const message = [
|
|
141
|
+
'Error: Could not resolve a workspace root. Tried, in order:',
|
|
142
|
+
...tried.map((t, i) => ` ${i + 1}. ${t}`),
|
|
143
|
+
`Run from inside a workspace, set DAVE_ROOT, or write {"workspaceRoot": "/path/to/workspace"} to ${defaults.file}.`,
|
|
144
|
+
].join('\n');
|
|
145
|
+
console.error(message);
|
|
146
|
+
throw new VaultError(message, 1);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function defaultPaths(overrides = {}) {
|
|
150
|
+
const root = overrides.root
|
|
151
|
+
?? resolveWorkspaceRoot({
|
|
152
|
+
cwd: overrides.cwd,
|
|
153
|
+
env: overrides.env,
|
|
154
|
+
home: overrides.home,
|
|
155
|
+
allowGitRootWithoutVault: overrides.allowGitRootWithoutVault,
|
|
156
|
+
}).root;
|
|
157
|
+
const home = overrides.home ?? homedir();
|
|
158
|
+
const vaultDir = overrides.vaultDir ?? join(root, '.vault');
|
|
159
|
+
const keyDir = overrides.keyDir ?? join(home, '.config', 'stravica-pa');
|
|
160
|
+
return {
|
|
161
|
+
root,
|
|
162
|
+
vaultDir,
|
|
163
|
+
secretsFile: join(vaultDir, 'secrets.enc.json'),
|
|
164
|
+
recipientsFile: join(vaultDir, 'recipients.txt'),
|
|
165
|
+
sopsConfig: join(vaultDir, '.sops.yaml'),
|
|
166
|
+
keyDir,
|
|
167
|
+
keyFile: join(keyDir, 'age-key.txt'),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Scoped vault resolution ──────────────────────────────────────────
|
|
172
|
+
//
|
|
173
|
+
// Two on-disk layouts, both sops-encrypted JSON with per-scope age
|
|
174
|
+
// recipients declared in a shared .sops.yaml (creation_rules keyed by
|
|
175
|
+
// filename, NOT a single recipients.txt — that's the whole point of
|
|
176
|
+
// scoping: each scope can have a different recipient set):
|
|
177
|
+
//
|
|
178
|
+
// workspace layout (estate hosts): <root>/.vault/scopes/<scope>.enc.json
|
|
179
|
+
// <root>/.vault/scopes/.sops.yaml
|
|
180
|
+
// canonical layout (operator repos): <root>/vault/<scope>.enc.json
|
|
181
|
+
// <root>/vault/.sops.yaml
|
|
182
|
+
//
|
|
183
|
+
// `root` is resolved the same way as the legacy vault: the tiered
|
|
184
|
+
// resolveWorkspaceRoot fallback (cwd walk → env → defaults.json).
|
|
185
|
+
|
|
186
|
+
const SCOPE_NAME_RE = /^[A-Za-z0-9_-]+$/;
|
|
187
|
+
|
|
188
|
+
// Print-then-throw, matching the command modules' convention — main()'s
|
|
189
|
+
// catch deliberately does NOT print messages for errors that carry an
|
|
190
|
+
// exitCode (it assumes the throw site already reported), so a bare
|
|
191
|
+
// `throw new VaultError(...)` here would exit silently through the real
|
|
192
|
+
// binary. That silent-exit was a live bug in the first --scope cut.
|
|
193
|
+
function scopeError(message, exitCode = 1) {
|
|
194
|
+
console.error(message);
|
|
195
|
+
return new VaultError(message, exitCode);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Resolve paths for `--scope <name>` or `--file <path>`. Throws VaultError
|
|
200
|
+
* (exit 1) for an invalid scope name, an ambiguous scope (present in both
|
|
201
|
+
* layouts), or a scope found in neither layout. `--file` bypasses all of
|
|
202
|
+
* this and points directly at the given encrypted file.
|
|
203
|
+
*/
|
|
204
|
+
export function resolveScopePaths(opts = {}) {
|
|
205
|
+
const home = opts.home ?? homedir();
|
|
206
|
+
const keyDir = opts.keyDir ?? join(home, '.config', 'stravica-pa');
|
|
207
|
+
const keyFile = opts.keyFile ?? join(keyDir, 'age-key.txt');
|
|
208
|
+
|
|
209
|
+
if (opts.file) {
|
|
210
|
+
const secretsFile = resolve(opts.file);
|
|
211
|
+
const vaultDir = dirname(secretsFile);
|
|
212
|
+
return {
|
|
213
|
+
root: vaultDir,
|
|
214
|
+
vaultDir,
|
|
215
|
+
secretsFile,
|
|
216
|
+
recipientsFile: null,
|
|
217
|
+
sopsConfig: opts.sopsConfig ? resolve(opts.sopsConfig) : join(vaultDir, '.sops.yaml'),
|
|
218
|
+
keyDir,
|
|
219
|
+
keyFile,
|
|
220
|
+
scope: opts.scope ?? null,
|
|
221
|
+
scoped: true,
|
|
222
|
+
layout: 'file',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!opts.scope) {
|
|
227
|
+
throw scopeError('Error: resolveScopePaths requires --scope or --file');
|
|
228
|
+
}
|
|
229
|
+
if (!SCOPE_NAME_RE.test(opts.scope)) {
|
|
230
|
+
throw scopeError(`Error: Invalid scope name "${opts.scope}". Scope names must match ${SCOPE_NAME_RE}.`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const { root } = resolveWorkspaceRoot({ cwd: opts.cwd, env: opts.env, home: opts.home });
|
|
234
|
+
const workspaceDir = join(root, '.vault', 'scopes');
|
|
235
|
+
const workspaceFile = join(workspaceDir, `${opts.scope}.enc.json`);
|
|
236
|
+
const canonicalDir = join(root, 'vault');
|
|
237
|
+
const canonicalFile = join(canonicalDir, `${opts.scope}.enc.json`);
|
|
238
|
+
|
|
239
|
+
const workspaceExists = existsSync(workspaceFile);
|
|
240
|
+
const canonicalExists = existsSync(canonicalFile);
|
|
241
|
+
|
|
242
|
+
if (workspaceExists && canonicalExists) {
|
|
243
|
+
throw scopeError([
|
|
244
|
+
`Error: Scope "${opts.scope}" is ambiguous — found in both layouts:`,
|
|
245
|
+
` workspace: ${workspaceFile}`,
|
|
246
|
+
` canonical: ${canonicalFile}`,
|
|
247
|
+
'Use --file <path> to pick one explicitly.',
|
|
248
|
+
].join('\n'));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (workspaceExists) {
|
|
252
|
+
return {
|
|
253
|
+
root,
|
|
254
|
+
vaultDir: workspaceDir,
|
|
255
|
+
secretsFile: workspaceFile,
|
|
256
|
+
recipientsFile: null,
|
|
257
|
+
sopsConfig: join(workspaceDir, '.sops.yaml'),
|
|
258
|
+
keyDir,
|
|
259
|
+
keyFile,
|
|
260
|
+
scope: opts.scope,
|
|
261
|
+
scoped: true,
|
|
262
|
+
layout: 'workspace',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (canonicalExists) {
|
|
267
|
+
return {
|
|
268
|
+
root,
|
|
269
|
+
vaultDir: canonicalDir,
|
|
270
|
+
secretsFile: canonicalFile,
|
|
271
|
+
recipientsFile: null,
|
|
272
|
+
sopsConfig: join(canonicalDir, '.sops.yaml'),
|
|
273
|
+
keyDir,
|
|
274
|
+
keyFile,
|
|
275
|
+
scope: opts.scope,
|
|
276
|
+
scoped: true,
|
|
277
|
+
layout: 'canonical',
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
throw scopeError([
|
|
282
|
+
`Error: No vault found for scope "${opts.scope}". Checked:`,
|
|
283
|
+
` ${workspaceFile}`,
|
|
284
|
+
` ${canonicalFile}`,
|
|
285
|
+
'Use --file <path> to point at an explicit vault file.',
|
|
286
|
+
].join('\n'));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Default-scope resolution (get/set only) ──────────────────────────
|
|
290
|
+
//
|
|
291
|
+
// When `get`/`set` run WITHOUT --scope/--file, the target is no longer
|
|
292
|
+
// silently "the legacy single-file vault" — a default scope can now come
|
|
293
|
+
// from, in priority order:
|
|
294
|
+
// 1. $SECRETS_DEFAULT_SCOPE env var.
|
|
295
|
+
// 2. "defaultScope" key in ~/.config/stravica-pa/defaults.json — same
|
|
296
|
+
// file/convention as "workspaceRoot" (readDefaultsWorkspaceRoot
|
|
297
|
+
// above), additive key, this CLI never writes it.
|
|
298
|
+
// 3. The calling PA's own identity file: agents/<name>/identity.json's
|
|
299
|
+
// "defaultScope" field. Located by walking up from cwd for the
|
|
300
|
+
// nearest ancestor directory that both (a) sits under an "agents"
|
|
301
|
+
// path segment and (b) contains identity.json — mirrors how a PA's
|
|
302
|
+
// own cwd is conventionally agents/<name>/... (Dave) or nested
|
|
303
|
+
// deeper, e.g. agents/barry/dex/... (Dex). $DAVE_ROOT is checked
|
|
304
|
+
// too, but ONLY as the single well-known path
|
|
305
|
+
// $DAVE_ROOT/agents/dave/identity.json — the one PA with an
|
|
306
|
+
// established root env var; no other agent name is ever guessed
|
|
307
|
+
// from an env var.
|
|
308
|
+
// 4. null — the caller falls through to the legacy single-file vault,
|
|
309
|
+
// with the mandatory stderr notice below (resolveCommandPaths)
|
|
310
|
+
// making that fallback visible instead of silent.
|
|
311
|
+
//
|
|
312
|
+
// This convention is NOT yet backed by any identity.json files on disk
|
|
313
|
+
// (none exist anywhere in the estate as of this change) — the resolver
|
|
314
|
+
// only reads them if present. Creating Dave's and Dex's identity.json is
|
|
315
|
+
// tracked as a follow-up.
|
|
316
|
+
//
|
|
317
|
+
// A malformed identity.json is a stderr WARNING and a fall-through, not
|
|
318
|
+
// a crash — this is a convenience default, never a hard dependency the
|
|
319
|
+
// CLI can refuse to run without.
|
|
320
|
+
|
|
321
|
+
function readDefaultsDefaultScope(home) {
|
|
322
|
+
const file = defaultsFilePath(home);
|
|
323
|
+
if (!existsSync(file)) return { file, scope: null, reason: 'file not present' };
|
|
324
|
+
try {
|
|
325
|
+
const parsed = JSON.parse(readFileSync(file, 'utf-8'));
|
|
326
|
+
if (parsed && typeof parsed.defaultScope === 'string' && parsed.defaultScope.trim() !== '') {
|
|
327
|
+
return { file, scope: parsed.defaultScope.trim(), reason: null };
|
|
328
|
+
}
|
|
329
|
+
return { file, scope: null, reason: 'defaultScope key not set' };
|
|
330
|
+
} catch {
|
|
331
|
+
return { file, scope: null, reason: 'file is not valid JSON' };
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Walk up from `cwd` for the nearest ancestor directory that sits under
|
|
337
|
+
* an "agents" path segment AND contains identity.json. Returns the file
|
|
338
|
+
* path, or null if none found before the filesystem root.
|
|
339
|
+
*/
|
|
340
|
+
function findIdentityFile(cwd) {
|
|
341
|
+
let d = resolve(cwd);
|
|
342
|
+
let prev;
|
|
343
|
+
while (d && d !== prev) {
|
|
344
|
+
if (d.split(sep).includes('agents') && existsSync(join(d, 'identity.json'))) {
|
|
345
|
+
return join(d, 'identity.json');
|
|
346
|
+
}
|
|
347
|
+
prev = d;
|
|
348
|
+
d = dirname(d);
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function readIdentityDefaultScope(cwd, env) {
|
|
354
|
+
const walked = findIdentityFile(cwd);
|
|
355
|
+
const daveFallback = env.DAVE_ROOT ? join(resolve(env.DAVE_ROOT), 'agents', 'dave', 'identity.json') : null;
|
|
356
|
+
const file = walked || (daveFallback && existsSync(daveFallback) ? daveFallback : null);
|
|
357
|
+
if (!file) {
|
|
358
|
+
return { file: null, scope: null, reason: 'no agents/<name>/identity.json found (cwd walk or $DAVE_ROOT)' };
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const parsed = JSON.parse(readFileSync(file, 'utf-8'));
|
|
362
|
+
if (parsed && typeof parsed.defaultScope === 'string' && parsed.defaultScope.trim() !== '') {
|
|
363
|
+
return { file, scope: parsed.defaultScope.trim(), reason: null };
|
|
364
|
+
}
|
|
365
|
+
return { file, scope: null, reason: 'defaultScope key not set' };
|
|
366
|
+
} catch {
|
|
367
|
+
console.error(`warning: ${file} is malformed JSON — ignoring its "defaultScope" default`);
|
|
368
|
+
return { file, scope: null, reason: 'file is not valid JSON' };
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Resolve the default scope (source-tagged) for get/set when neither
|
|
374
|
+
* --scope nor --file was passed. Returns { scope, source, detail }, with
|
|
375
|
+
* scope: null meaning nothing is configured anywhere — the legacy
|
|
376
|
+
* fallback case.
|
|
377
|
+
*/
|
|
378
|
+
export function resolveDefaultScope(opts = {}) {
|
|
379
|
+
const env = opts.env ?? process.env;
|
|
380
|
+
const home = opts.home ?? homedir();
|
|
381
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
382
|
+
|
|
383
|
+
if (typeof env.SECRETS_DEFAULT_SCOPE === 'string' && env.SECRETS_DEFAULT_SCOPE.trim() !== '') {
|
|
384
|
+
return { scope: env.SECRETS_DEFAULT_SCOPE.trim(), source: 'env', detail: 'SECRETS_DEFAULT_SCOPE env var' };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const defaults = readDefaultsDefaultScope(home);
|
|
388
|
+
if (defaults.scope) {
|
|
389
|
+
return { scope: defaults.scope, source: 'defaults', detail: defaults.file };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const identity = readIdentityDefaultScope(cwd, env);
|
|
393
|
+
if (identity.scope) {
|
|
394
|
+
return { scope: identity.scope, source: 'identity', detail: identity.file };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { scope: null, source: null, detail: null };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Resolve the paths get/set should operate on, printing the interim
|
|
402
|
+
* stderr notice (change A) naming the target vault BEFORE the operation
|
|
403
|
+
* runs. If overrides.paths is already set — an explicit --scope/--file
|
|
404
|
+
* resolved upstream in cli.mjs, or a test-only DI injection — this is a
|
|
405
|
+
* pure pass-through: no default-scope resolution, no notice. An explicit
|
|
406
|
+
* caller choice is never second-guessed.
|
|
407
|
+
*/
|
|
408
|
+
export function resolveCommandPaths(overrides = {}) {
|
|
409
|
+
if (overrides.paths) return overrides.paths;
|
|
410
|
+
|
|
411
|
+
const { scope, detail } = resolveDefaultScope(overrides);
|
|
412
|
+
if (scope) {
|
|
413
|
+
const paths = resolveScopePaths({
|
|
414
|
+
scope,
|
|
415
|
+
cwd: overrides.cwd,
|
|
416
|
+
env: overrides.env,
|
|
417
|
+
home: overrides.home,
|
|
418
|
+
keyDir: overrides.keyDir,
|
|
419
|
+
keyFile: overrides.keyFile,
|
|
420
|
+
});
|
|
421
|
+
console.error(`note: writing to '${scope}' pot (default from ${detail})`);
|
|
422
|
+
return paths;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
console.error("note: writing to '(legacy)' pot — no scope specified and no default configured. Use --scope <name> or configure a default.");
|
|
426
|
+
return defaultPaths(overrides);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ── Vault discovery (no decryption) ──────────────────────────────────
|
|
430
|
+
//
|
|
431
|
+
// sops's JSON mode encrypts VALUES but keeps the top-level structure —
|
|
432
|
+
// including key NAMES — in plaintext (values become "ENC[AES256_GCM,...]"
|
|
433
|
+
// strings; a `sops` metadata key is appended). That means enumeration and
|
|
434
|
+
// key-name search never need the age private key and never touch secret
|
|
435
|
+
// material. Verified against real fixtures in the test suite; if a vault
|
|
436
|
+
// were ever created with encrypted_regex hiding key names, the reads
|
|
437
|
+
// below would simply not match and the count degrades to null ('?').
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Read the plaintext top-level key names of a sops-encrypted JSON file
|
|
441
|
+
* WITHOUT decrypting. Returns null when the file is missing/unparseable.
|
|
442
|
+
*/
|
|
443
|
+
export function readVaultKeyNames(filePath) {
|
|
444
|
+
try {
|
|
445
|
+
const parsed = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
446
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
|
|
447
|
+
return Object.keys(parsed).filter((k) => k !== 'sops').sort();
|
|
448
|
+
} catch {
|
|
449
|
+
return null;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Enumerate every vault visible from a workspace root:
|
|
455
|
+
* legacy .vault/secrets.enc.json, workspace scopes .vault/scopes/*.enc.json,
|
|
456
|
+
* canonical vault/*.enc.json. Returns [{ scope, layout, path }].
|
|
457
|
+
*/
|
|
458
|
+
export function enumerateVaults(root) {
|
|
459
|
+
const found = [];
|
|
460
|
+
const legacy = join(root, '.vault', 'secrets.enc.json');
|
|
461
|
+
if (existsSync(legacy)) {
|
|
462
|
+
found.push({ scope: '(legacy)', layout: 'legacy', path: legacy });
|
|
463
|
+
}
|
|
464
|
+
const scopesDir = join(root, '.vault', 'scopes');
|
|
465
|
+
if (existsSync(scopesDir)) {
|
|
466
|
+
try {
|
|
467
|
+
for (const f of readdirSync(scopesDir).sort()) {
|
|
468
|
+
if (f.endsWith('.enc.json')) {
|
|
469
|
+
found.push({ scope: f.slice(0, -'.enc.json'.length), layout: 'workspace-scoped', path: join(scopesDir, f) });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} catch { /* unreadable dir — skip */ }
|
|
473
|
+
}
|
|
474
|
+
const canonDir = join(root, 'vault');
|
|
475
|
+
if (existsSync(canonDir)) {
|
|
476
|
+
try {
|
|
477
|
+
for (const f of readdirSync(canonDir).sort()) {
|
|
478
|
+
if (f.endsWith('.enc.json')) {
|
|
479
|
+
found.push({ scope: f.slice(0, -'.enc.json'.length), layout: 'canonical', path: join(canonDir, f) });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch { /* unreadable dir — skip */ }
|
|
483
|
+
}
|
|
484
|
+
return found;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// ── Spawn chokepoint ─────────────────────────────────────────────────
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Run a command and return { code, stdout, stderr }. Args passed as an
|
|
491
|
+
* array — never string-joined — so there is no shell interpolation
|
|
492
|
+
* surface. Optional env vars (e.g. SOPS_AGE_KEY_FILE) are merged into
|
|
493
|
+
* process.env for the child.
|
|
494
|
+
*/
|
|
495
|
+
export function runCmd(cmd, args, options = {}) {
|
|
496
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
497
|
+
const child = spawn(cmd, args, {
|
|
498
|
+
env: { ...process.env, ...(options.env || {}) },
|
|
499
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
500
|
+
});
|
|
501
|
+
let stdout = '';
|
|
502
|
+
let stderr = '';
|
|
503
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
504
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
505
|
+
child.on('error', (err) => rejectPromise(err));
|
|
506
|
+
child.on('close', (code) => resolvePromise({ code, stdout, stderr }));
|
|
507
|
+
if (options.stdin != null) {
|
|
508
|
+
child.stdin.write(options.stdin);
|
|
509
|
+
child.stdin.end();
|
|
510
|
+
} else {
|
|
511
|
+
child.stdin.end();
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ── Dependency checks ────────────────────────────────────────────────
|
|
517
|
+
|
|
518
|
+
export async function hasBinary(name) {
|
|
519
|
+
try {
|
|
520
|
+
const { code } = await runCmd('/bin/sh', ['-c', `command -v ${name}`]);
|
|
521
|
+
return code === 0;
|
|
522
|
+
} catch {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export async function checkDeps() {
|
|
528
|
+
const missing = [];
|
|
529
|
+
if (!(await hasBinary('sops'))) missing.push('sops');
|
|
530
|
+
if (!(await hasBinary('age'))) missing.push('age');
|
|
531
|
+
return missing;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function depInstallMessage(name) {
|
|
535
|
+
if (name === 'sops') {
|
|
536
|
+
return [
|
|
537
|
+
'Error: sops is not installed.',
|
|
538
|
+
'',
|
|
539
|
+
'Install options:',
|
|
540
|
+
' macOS: brew install sops',
|
|
541
|
+
' Debian/Ubuntu: Download from https://github.com/getsops/sops/releases',
|
|
542
|
+
' Manual: https://github.com/getsops/sops/releases/latest',
|
|
543
|
+
].join('\n');
|
|
544
|
+
}
|
|
545
|
+
if (name === 'age') {
|
|
546
|
+
return [
|
|
547
|
+
'Error: age is not installed.',
|
|
548
|
+
'',
|
|
549
|
+
'Install options:',
|
|
550
|
+
' macOS: brew install age',
|
|
551
|
+
' Debian/Ubuntu: sudo apt install age',
|
|
552
|
+
' Manual: https://github.com/FiloSottile/age/releases/latest',
|
|
553
|
+
].join('\n');
|
|
554
|
+
}
|
|
555
|
+
return `Error: ${name} is not installed.`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ── Vault state checks ───────────────────────────────────────────────
|
|
559
|
+
|
|
560
|
+
export function checkVault(paths) {
|
|
561
|
+
// A missing private key is treated as an operational failure (exit 2):
|
|
562
|
+
// the operator either lost their key or never installed it on this host,
|
|
563
|
+
// and remediation requires backup-restore or key transfer — not just
|
|
564
|
+
// running `secrets init` (which would generate a NEW key and orphan the
|
|
565
|
+
// encrypted store). A missing recipients file, by contrast, is a clean
|
|
566
|
+
// "vault not initialised" state (exit 1) — `secrets init` is the right
|
|
567
|
+
// next step.
|
|
568
|
+
if (!existsSync(paths.keyFile)) {
|
|
569
|
+
return {
|
|
570
|
+
ok: false,
|
|
571
|
+
reason: 'missing-key',
|
|
572
|
+
exitCode: 2,
|
|
573
|
+
message: `Error: No age private key found at ${paths.keyFile}\nRestore your key from backup, or run 'secrets init' to start a fresh vault.`,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
// Scoped vaults have no recipients.txt — recipients live in the scope's
|
|
577
|
+
// own .sops.yaml creation_rules — so the equivalent "not set up" signal
|
|
578
|
+
// is a missing sops config rather than a missing recipients file.
|
|
579
|
+
if (paths.scoped) {
|
|
580
|
+
if (!existsSync(paths.sopsConfig)) {
|
|
581
|
+
return {
|
|
582
|
+
ok: false,
|
|
583
|
+
reason: 'missing-sops-config',
|
|
584
|
+
exitCode: 1,
|
|
585
|
+
message: `Error: No SOPS config found at ${paths.sopsConfig}\nScoped vaults are provisioned by the estate's scope tooling, not 'secrets init'.`,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return { ok: true };
|
|
589
|
+
}
|
|
590
|
+
if (!existsSync(paths.recipientsFile)) {
|
|
591
|
+
return {
|
|
592
|
+
ok: false,
|
|
593
|
+
reason: 'missing-recipients',
|
|
594
|
+
exitCode: 1,
|
|
595
|
+
message: `Error: No recipients file at ${paths.recipientsFile}\nRun 'secrets init' to set up the vault.`,
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
return { ok: true };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function getPublicKey(paths) {
|
|
602
|
+
const contents = readFileSync(paths.recipientsFile, 'utf-8');
|
|
603
|
+
const first = contents.split('\n')[0];
|
|
604
|
+
return (first || '').trim();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// ── SOPS/age helpers ─────────────────────────────────────────────────
|
|
608
|
+
|
|
609
|
+
/** age-keygen -o <keyFile> */
|
|
610
|
+
export async function ageKeygen(paths) {
|
|
611
|
+
const { code, stderr } = await runCmd('age-keygen', ['-o', paths.keyFile]);
|
|
612
|
+
if (code !== 0) {
|
|
613
|
+
throw Object.assign(new Error(`age-keygen failed: ${stderr.trim()}`), { exitCode: 3 });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** Extract the public key line from an age key file. */
|
|
618
|
+
export function publicKeyFromKeyFile(keyFile) {
|
|
619
|
+
const contents = readFileSync(keyFile, 'utf-8');
|
|
620
|
+
const line = contents.split('\n').find((l) => l.includes('public key:'));
|
|
621
|
+
if (!line) return null;
|
|
622
|
+
const parts = line.trim().split(/\s+/);
|
|
623
|
+
return parts[parts.length - 1];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/** Extract the AGE-SECRET-KEY- line from an age key file. */
|
|
627
|
+
export function secretKeyFromKeyFile(keyFile) {
|
|
628
|
+
const contents = readFileSync(keyFile, 'utf-8');
|
|
629
|
+
const line = contents.split('\n').find((l) => l.startsWith('AGE-SECRET-KEY-'));
|
|
630
|
+
return line || null;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/** Decrypt an encrypted file, return the JSON object it contains. */
|
|
634
|
+
export async function sopsDecrypt(paths, filePath) {
|
|
635
|
+
const { code, stdout, stderr } = await runCmd('sops', ['--config', paths.sopsConfig, '--decrypt', filePath], {
|
|
636
|
+
env: { SOPS_AGE_KEY_FILE: paths.keyFile },
|
|
637
|
+
});
|
|
638
|
+
if (code !== 0) {
|
|
639
|
+
throw Object.assign(new Error(`sops decrypt failed: ${stderr.trim()}`), { exitCode: 2 });
|
|
640
|
+
}
|
|
641
|
+
return JSON.parse(stdout);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Encrypt a JSON object to the secrets file with the given public key recipient. */
|
|
645
|
+
export async function sopsEncrypt(paths, pubKey, obj) {
|
|
646
|
+
const tmp = mkdtempSync(join(tmpdir(), 'vault-'));
|
|
647
|
+
// Name the tmpfile with the .enc.json suffix so the creation_rule in
|
|
648
|
+
// the vault's own .sops.yaml matches (path_regex: \.enc\.json$). If the
|
|
649
|
+
// suffix doesn't match, sops errors with "no matching creation rules".
|
|
650
|
+
const tmpFile = join(tmp, 'secrets.enc.json');
|
|
651
|
+
writeFileSync(tmpFile, JSON.stringify(obj, null, 2) + '\n');
|
|
652
|
+
try {
|
|
653
|
+
// --config pins the sops config to the vault's own .sops.yaml so sops
|
|
654
|
+
// doesn't walk up from CWD and hit an unrelated ambient .sops.yaml
|
|
655
|
+
// (e.g. an older vault's config at a higher directory).
|
|
656
|
+
const { code, stdout, stderr } = await runCmd('sops', ['--config', paths.sopsConfig, '--encrypt', '--age', pubKey, tmpFile], {
|
|
657
|
+
env: { SOPS_AGE_KEY_FILE: paths.keyFile },
|
|
658
|
+
});
|
|
659
|
+
if (code !== 0) {
|
|
660
|
+
throw Object.assign(new Error(`sops encrypt failed: ${stderr.trim()}`), { exitCode: 2 });
|
|
661
|
+
}
|
|
662
|
+
writeFileSync(paths.secretsFile, stdout);
|
|
663
|
+
} finally {
|
|
664
|
+
try { unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Decrypt an EXISTING store as the precondition for a write (set/delete/
|
|
670
|
+
* rotate). A failed decrypt REFUSES the operation — proceeding to encrypt
|
|
671
|
+
* would replace the store's entire contents with whatever the caller was
|
|
672
|
+
* about to write (the vault-clobber/data-loss class: any host whose age key
|
|
673
|
+
* is not a recipient of this vault would wipe it with a single `set`).
|
|
674
|
+
* Prints the refusal (main() does not print messages for errors carrying an
|
|
675
|
+
* exitCode) naming the store and the key resolution attempted, then throws
|
|
676
|
+
* VaultError exit 2.
|
|
677
|
+
*/
|
|
678
|
+
export async function decryptStoreForWrite(paths) {
|
|
679
|
+
try {
|
|
680
|
+
return await sopsDecrypt(paths, paths.secretsFile);
|
|
681
|
+
} catch (err) {
|
|
682
|
+
const detail = err && err.message ? err.message : String(err);
|
|
683
|
+
const message = [
|
|
684
|
+
`Error: refusing to modify ${paths.secretsFile}`,
|
|
685
|
+
` the existing store could not be decrypted with the age key at ${paths.keyFile} (via SOPS_AGE_KEY_FILE)`,
|
|
686
|
+
` cause: ${detail}`,
|
|
687
|
+
"Proceeding would re-encrypt and REPLACE the store's contents. Fix key access first — is this host's key a recipient of this vault?",
|
|
688
|
+
].join('\n');
|
|
689
|
+
console.error(message);
|
|
690
|
+
throw new VaultError(message, 2);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Encrypt a JSON object to a SCOPED secrets file. Unlike sopsEncrypt(), no
|
|
696
|
+
* `--age` recipient is passed on the command line — recipients come purely
|
|
697
|
+
* from the scope's own .sops.yaml creation_rule, matched by filename. The
|
|
698
|
+
* tmp file is named to match paths.secretsFile's basename (not the fixed
|
|
699
|
+
* 'secrets.enc.json' sopsEncrypt() uses) so the creation_rule's path_regex
|
|
700
|
+
* (e.g. `wsd\.enc\.json$`) actually matches.
|
|
701
|
+
*/
|
|
702
|
+
export async function sopsEncryptScoped(paths, obj) {
|
|
703
|
+
const tmp = mkdtempSync(join(tmpdir(), 'vault-'));
|
|
704
|
+
const tmpFile = join(tmp, basename(paths.secretsFile));
|
|
705
|
+
writeFileSync(tmpFile, JSON.stringify(obj, null, 2) + '\n');
|
|
706
|
+
try {
|
|
707
|
+
const { code, stdout, stderr } = await runCmd('sops', ['--config', paths.sopsConfig, '--encrypt', tmpFile], {
|
|
708
|
+
env: { SOPS_AGE_KEY_FILE: paths.keyFile },
|
|
709
|
+
});
|
|
710
|
+
if (code !== 0) {
|
|
711
|
+
throw Object.assign(new Error(`sops encrypt failed: ${stderr.trim()}`), { exitCode: 2 });
|
|
712
|
+
}
|
|
713
|
+
writeFileSync(paths.secretsFile, stdout);
|
|
714
|
+
} finally {
|
|
715
|
+
try { unlinkSync(tmpFile); } catch { /* ignore */ }
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// ── Filesystem helpers ───────────────────────────────────────────────
|
|
720
|
+
|
|
721
|
+
export function ensureKeyDir(paths) {
|
|
722
|
+
if (!existsSync(paths.keyDir)) {
|
|
723
|
+
mkdirSync(paths.keyDir, { recursive: true, mode: 0o700 });
|
|
724
|
+
} else {
|
|
725
|
+
try { chmodSync(paths.keyDir, 0o700); } catch { /* non-fatal */ }
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export function ensureVaultDir(paths) {
|
|
730
|
+
if (!existsSync(paths.vaultDir)) {
|
|
731
|
+
mkdirSync(paths.vaultDir, { recursive: true });
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export function chmodKeyFile(paths) {
|
|
736
|
+
try { chmodSync(paths.keyFile, 0o600); } catch { /* non-fatal */ }
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export function writeRecipients(paths, pubKey) {
|
|
740
|
+
writeFileSync(paths.recipientsFile, `${pubKey}\n`);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export function writeSopsConfig(paths, pubKey) {
|
|
744
|
+
const body = [
|
|
745
|
+
'creation_rules:',
|
|
746
|
+
' - path_regex: \\.enc\\.json$',
|
|
747
|
+
' age: >-',
|
|
748
|
+
` ${pubKey}`,
|
|
749
|
+
'',
|
|
750
|
+
].join('\n');
|
|
751
|
+
writeFileSync(paths.sopsConfig, body);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
export function keyFileExists(paths) { return existsSync(paths.keyFile); }
|
|
755
|
+
export function secretsFileExists(paths) { return existsSync(paths.secretsFile); }
|
|
756
|
+
export function recipientsFileExists(paths) { return existsSync(paths.recipientsFile); }
|
|
757
|
+
export function sopsConfigExists(paths) { return existsSync(paths.sopsConfig); }
|
|
758
|
+
|
|
759
|
+
// ── Error with exit code ─────────────────────────────────────────────
|
|
760
|
+
|
|
761
|
+
export class VaultError extends Error {
|
|
762
|
+
constructor(message, exitCode = 1) {
|
|
763
|
+
super(message);
|
|
764
|
+
this.exitCode = exitCode;
|
|
765
|
+
}
|
|
766
|
+
}
|