@starci/hfs 4.2.17 → 4.4.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/CHANGELOG.md +28 -0
- package/README.md +7 -0
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/db-types.mjs +10 -1
- package/lint/run.mjs +1 -1
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +3437 -160
- package/runtime/knowledge/hfs/slots.yaml +430 -92
- package/runtime/knowledge/patterns/be/api.yaml +92 -2
- package/runtime/knowledge/patterns/be/cli.yaml +37 -9
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
- package/runtime/knowledge/patterns/be/projections.yaml +4 -2
- package/runtime/knowledge/patterns/be/queues.yaml +11 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
- package/runtime/knowledge/patterns/be/saga.yaml +20 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
- package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3128 -47
- package/runtime/modules/models/runtimes.yaml +1 -1
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
- package/runtime/packages/cli/src/validate-args.mjs +213 -0
- package/runtime/scripts/api/git/add.mjs +5 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +3 -0
- package/runtime/scripts/api/git/tag.mjs +5 -0
- package/runtime/scripts/api/node/lib.mjs +20 -0
- package/runtime/scripts/api/node/run-node.mjs +6 -0
- package/runtime/scripts/api/sops/lib.mjs +41 -0
- package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
- package/runtime/scripts/api/sops/seal.mjs +1 -1
- package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
- package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
- package/runtime/scripts/api/supabase/database-query.mjs +5 -0
- package/runtime/scripts/api/supabase/lib.mjs +44 -0
- package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
- package/runtime/scripts/hfs/app-wiring.mjs +201 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +63 -72
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
- package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/doc-commands.mjs +367 -0
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/example-secrets.mjs +158 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
- package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
- package/runtime/scripts/hfs/rules/assets.mjs +79 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
- package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
- package/runtime/scripts/hfs/rules/contract.mjs +66 -5
- package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
- package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
- package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
- package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
- package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
- package/runtime/scripts/hfs/rules/database.mjs +11 -2
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
- package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/edition.mjs +11 -10
- package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
- package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
- package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
- package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
- package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
- package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
- package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
- package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
- package/runtime/scripts/hfs/rules/saga.mjs +8 -16
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
- package/runtime/scripts/hfs/rules/services.mjs +4 -4
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
- package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
- package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
- package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
- package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
- package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
- package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
- package/runtime/scripts/hfs/sealed-members.mjs +101 -0
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
- package/runtime/scripts/hfs/secret-document.mjs +97 -0
- package/runtime/scripts/hfs/secret-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-errors.mjs +20 -0
- package/runtime/scripts/hfs/secret-files.mjs +124 -0
- package/runtime/scripts/hfs/secret-init.mjs +300 -0
- package/runtime/scripts/hfs/secret-output.mjs +100 -0
- package/runtime/scripts/hfs/secret-sops.mjs +130 -0
- package/runtime/scripts/hfs/secret-store.mjs +163 -0
- package/runtime/scripts/hfs/secret.mjs +162 -193
- package/runtime/scripts/hfs/slot-imports.mjs +18 -3
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
- package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
- package/runtime/scripts/hfs/tree-findings.mjs +22 -0
- package/runtime/scripts/lib/command-tokens.mjs +126 -0
- package/runtime/scripts/lib/compose-values.mjs +11 -0
- package/runtime/scripts/lib/deployment-findings.mjs +7 -0
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +59 -0
- package/runtime/scripts/lib/runtime-example.mjs +13 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/sql-ast.mjs +32 -0
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +126 -0
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +23 -0
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/runtime/scripts/lib/workflow-events.mjs +8 -0
- package/scaffold/add-app.mjs +77 -59
- package/scaffold/add-cli-lite.mjs +24 -17
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +122 -0
- package/scaffold/add-fe-page.mjs +269 -0
- package/scaffold/add-inbox-lite.mjs +87 -124
- package/scaffold/add-plan.mjs +183 -0
- package/scaffold/add-table.mjs +132 -70
- package/scaffold/add.mjs +186 -204
- package/scaffold/app.mjs +128 -43
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/image.mjs +2 -2
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/src/main.mjs +238 -119
- package/stack/age-install.mjs +37 -0
- package/stack/backup.mjs +100 -0
- package/stack/build-args.mjs +42 -0
- package/stack/build.mjs +173 -0
- package/stack/cli.mjs +130 -0
- package/stack/config.mjs +57 -0
- package/stack/context.mjs +57 -0
- package/stack/converge.mjs +107 -0
- package/stack/deploy.mjs +301 -0
- package/stack/digest.mjs +16 -0
- package/stack/docker-transport.mjs +53 -0
- package/stack/edge-alias.mjs +118 -0
- package/stack/errors.mjs +21 -0
- package/stack/image-scan.mjs +152 -0
- package/stack/ingress-create.mjs +114 -0
- package/stack/ingress-publish.mjs +273 -0
- package/stack/ingress.mjs +706 -0
- package/stack/jobs.mjs +51 -0
- package/stack/live-restore.mjs +177 -0
- package/stack/prepare.mjs +266 -0
- package/stack/private-file.mjs +22 -0
- package/stack/provision.mjs +90 -0
- package/stack/prune.mjs +46 -0
- package/stack/release-store.mjs +87 -0
- package/stack/remote.mjs +205 -0
- package/stack/render.mjs +149 -0
- package/stack/retire.mjs +47 -0
- package/stack/sealed.mjs +149 -0
- package/stack/task.mjs +42 -0
- package/stack/verify.mjs +99 -0
- package/sync/ci-commands.mjs +13 -0
- package/sync/index.mjs +127 -64
- package/sync/stack-images.mjs +127 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
- package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/codegen-graphql.mjs.partial +13 -2
- package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
- package/templates/app/codegen-prelude.mjs.partial +8 -0
- package/templates/app/gitignore +4 -0
- package/templates/app/gitignore-lite +4 -0
- package/templates/app/hooks/husky/pre-commit +7 -12
- package/templates/app/package-scripts/package.json +1 -0
- package/templates/app/package-scripts/supabase.json +7 -0
- package/templates/app/package-scripts-lite/package.json +1 -7
- package/templates/app/skeleton/.editorconfig +1 -0
- package/templates/app/skeleton/.gitattributes +7 -1
- package/templates/app/skeleton/.github/dependabot.yml +17 -0
- package/templates/app/skeleton/.nvmrc +1 -1
- package/templates/app/skeleton/.sops.yaml +1 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
- package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
- package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
- package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/README.md +6 -6
- package/templates/app/skeleton/scripts/codegen.mjs +1 -2
- package/templates/app/skeleton-lite/.editorconfig +16 -0
- package/templates/app/skeleton-lite/.gitattributes +8 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
- package/templates/app/skeleton-lite/.nvmrc +1 -0
- package/templates/app/skeleton-lite/.sops.yaml +3 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
- package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
- package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton-lite/README.md +8 -4
- package/templates/app/skeleton-lite/supabase/config.toml +8 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
- package/templates/app/starciwork.gitignore-lite +6 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
- package/templates/be/inbox/database-client.ts.tpl +14 -28
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
- package/templates/be/inbox/event-index.ts.tpl +1 -0
- package/templates/be/inbox/events.json.tpl +10 -0
- package/templates/be/inbox/migration.sql.tpl +1 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
- package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
- package/templates/be/inbox/platform/migration.sql.tpl +48 -0
- package/templates/be/inbox/service.spec.ts.tpl +49 -20
- package/templates/be/inbox/service.ts.tpl +27 -16
- package/templates/be/inbox/sql.ts.tpl +2 -2
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
- package/templates/be/inbox/webhook-door.ts.tpl +4 -3
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
- package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
- package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
- package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
- package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
- package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
- package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
- package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
- package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
- package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
- package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
- package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
- package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
- package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
- package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
- package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
- package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
- package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
- package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
- package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
- package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
- package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/feature.module.ts.tpl +2 -1
- package/templates/be/table/api/feature.options.ts.tpl +2 -0
- package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/graphql.module.ts.tpl +3 -2
- package/templates/be/table/api/graphql.options.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +1 -0
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
- package/templates/fe/auth/supabase-client.ts.tpl +34 -0
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
- package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
- package/upgrade/check.mjs +3 -1
- package/upgrade/file-state.mjs +68 -0
- package/upgrade/index.mjs +58 -94
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
package/src/main.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The @starci/hfs implementation behind `starci app`: one repository, `<app>/`, with one package.json, lockfile and node_modules
|
|
2
2
|
// at its root and two sides, be/ and fe/, declared by the one hfs.json of kind app. Every verb runs at the app root.
|
|
3
|
-
// starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>]
|
|
3
|
+
// starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--wiring] [--coverage-from <lcov>]
|
|
4
4
|
// every tracked path has a slot; required files exist; nothing forbidden or
|
|
5
5
|
// tracked-that-must-be-ignored; pins match; every managed file equals its render
|
|
6
6
|
// (sync/managed.mjs, the .gitignore block and sonar-project.properties included); no empty or ghost
|
|
@@ -33,12 +33,18 @@
|
|
|
33
33
|
// a back-end `<name>.service.ts` and its `<name>.service.spec.ts` skeleton (scaffold/service.mjs; <dir> is
|
|
34
34
|
// relative to be/): the spec is built
|
|
35
35
|
// with Test.createTestingModule, one provider per constructor dependency (kit doubles from @starci/jest-preset),
|
|
36
|
-
// one placeholder it per public method. Never overwrites a file. Both editions: a lite back end keeps full's unit specs (
|
|
36
|
+
// one placeholder it per public method. Never overwrites a file. Both editions: a lite back end keeps full's unit specs (ruling:R212/case-1 (owner:lite-production-swarm), amended by ruling:R212/case-4).
|
|
37
37
|
// starci app new image [--cwd <dir>] the Dockerfile of every declared app that has none, from the image canon (scaffold/image.mjs); never overwrites
|
|
38
38
|
// starci app new spec <file>.service.ts [--cwd <dir>] the spec skeleton of an existing service, read from its constructor with the repository's TypeScript
|
|
39
|
-
// starci app
|
|
40
|
-
// the
|
|
41
|
-
//
|
|
39
|
+
// starci app stack prepare|doctor|connect|disconnect|build-args|render|deploy|converge|verify|rollback|prune|backup|jobs --env <env> [--execute]
|
|
40
|
+
// the Swarm deploy tooling of stack/ (dry run unless --execute; never prints a secret value; see docs/application-stacks.md).
|
|
41
|
+
// starci app secret init | set <env> <slug> <KEY> [--generate <bytes>] | get <env> <slug> [KEY] --to-file [--reveal]
|
|
42
|
+
// starci app secret unset <env> <slug> <KEY> [--execute] | rename <env> <slug> <OLD> <NEW> [--execute] | remove <env> <slug> [--execute] [--force-nonempty]
|
|
43
|
+
// starci app secret materialize <env> | list <env> | verify <env>
|
|
44
|
+
// dotenv custody in .starcistacks, public age rule initialization, private KEY files outside Git;
|
|
45
|
+
// values reach stdout only when get explicitly receives --reveal.
|
|
46
|
+
// starci app db baseline | restamp --map <restamp.yaml> | plan
|
|
47
|
+
// preview the database cutover or compare its state with the repo; baseline and restamp mutate only with --apply.
|
|
42
48
|
// starci app add <api|webhook|realtime|saga|job|reactor|queue|projection> <name> [--event <event> --from <service> --service <Class>=<module>] [--owner <service> --failed <event> --done <event>] [--connection <name>] [--cwd <dir>]
|
|
43
49
|
// exactly that kind's file tree, generated FROM the files: tree of its pattern topic (knowledge/patterns/be) with the
|
|
44
50
|
// one template body of each entry (templates/be/patterns), plus the platform capabilities it needs when they are missing;
|
|
@@ -50,8 +56,12 @@ import { createRequire } from 'node:module';
|
|
|
50
56
|
import path from 'node:path';
|
|
51
57
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
52
58
|
import { isMain } from '../runtime/scripts/lib/is-main.mjs';
|
|
53
|
-
import { checkDatabase, checkRepository, explainPath, trackedFiles } from '../runtime/scripts/hfs/check.mjs';
|
|
59
|
+
import { checkDatabase, checkRepository, explainPath, trackedFiles, withCheckProofs } from '../runtime/scripts/hfs/check.mjs';
|
|
54
60
|
import { secretMain } from '../runtime/scripts/hfs/secret.mjs';
|
|
61
|
+
import { dbBaselineMain } from '../runtime/scripts/hfs/db-baseline-verb.mjs';
|
|
62
|
+
import { dbRestampMain } from '../runtime/scripts/hfs/db-restamp-verb.mjs';
|
|
63
|
+
import { dbPlanMain } from '../runtime/scripts/hfs/db-plan-verb.mjs';
|
|
64
|
+
import { formatCoverageReport } from '../runtime/scripts/hfs/coverage-from.mjs';
|
|
55
65
|
import { HFS_DECLARATION_FILE, HfsSlotsError, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
|
|
56
66
|
import { formatFindings } from '../sync/format.mjs';
|
|
57
67
|
import { main as syncMain } from '../sync/cli.mjs';
|
|
@@ -69,38 +79,76 @@ import { lintRepository, parseLintArgs, printLintText } from '../lint/run.mjs';
|
|
|
69
79
|
import { writeReport } from '../report/sonar.mjs';
|
|
70
80
|
import { checkEdition } from '../upgrade/check.mjs';
|
|
71
81
|
import { UpgradeError, upgradeMain } from '../upgrade/index.mjs';
|
|
82
|
+
import { ACTIONS as STACK_ACTIONS, stackMain as deployStackMain } from '../stack/cli.mjs';
|
|
83
|
+
import { releaseMain } from '../release/index.mjs';
|
|
84
|
+
import { dbMigrateMain } from '../db/migrate.mjs';
|
|
85
|
+
import { dbPostCutoverMain } from '../db/post-cutover.mjs';
|
|
72
86
|
|
|
73
|
-
export const VERBS = Object.freeze(['scaffold', 'add', 'lint', 'sync', 'check', 'upgrade', 'stack', 'explain', 'emit', 'new', 'secret', 'hygiene']);
|
|
87
|
+
export const VERBS = Object.freeze(['scaffold', 'add', 'lint', 'sync', 'check', 'upgrade', 'stack', 'release', 'explain', 'emit', 'new', 'secret', 'db', 'hygiene']);
|
|
88
|
+
const DATABASE_VERBS = new Map([['baseline', dbBaselineMain], ['restamp', dbRestampMain], ['plan', dbPlanMain], ['migrate', dbMigrateMain], ['post-cutover', dbPostCutoverMain]]);
|
|
74
89
|
|
|
75
|
-
const USAGE = `starci app scaffold <name> [--into <dir>] [--cwd <dir>]
|
|
90
|
+
const USAGE = `starci app scaffold <name> [--into <dir>] [--edition full|lite] [--registry-visibility public|private] [--cwd <dir>]
|
|
76
91
|
starci app add <api|webhook|realtime|saga|job|reactor|queue|projection|cli|table|app> <name> [options] [--cwd <dir>]
|
|
77
92
|
starci app lint [--cwd <dir>] [--changed <file>...] [--workspace <dir>] [--fix] [--format text|json] [--sonar <file>]
|
|
78
93
|
starci app sync (--check | --write) [--cwd <dir>]
|
|
79
|
-
starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--edition full] [--db-types]
|
|
94
|
+
starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--edition full] [--db-types] [--wiring] [--coverage-from <lcov>]
|
|
95
|
+
starci app db baseline [--stamp <version>] [--local] [--apply] [--cwd <dir>]
|
|
96
|
+
starci app db restamp --map <restamp.yaml> [--local] [--apply] [--cwd <dir>]
|
|
97
|
+
starci app db plan [--local] [--json] [--cwd <dir>]
|
|
98
|
+
starci app db migrate [--execute] [--linked | --url-env <NAME>] [--json] [--cwd <dir>]
|
|
99
|
+
starci app db post-cutover [--execute] [--green-at <ISO> | --owner-confirms-old-app-gone] [--linked | --url-env <NAME>] [--json] [--cwd <dir>]
|
|
100
|
+
starci app release gate [--execute] [--source <ref>] [--tag-prefix <prefix>] [--json] [--cwd <dir>]
|
|
101
|
+
starci app release push [--execute] [--remote <name>] [--json] [--cwd <dir>]
|
|
80
102
|
starci app upgrade --edition full [--plan] [--cwd <dir>]
|
|
81
103
|
starci app stack <up|down|status> [--stack <dir>] [--services <list>] [--k3d] [--force] [--json] [--cwd <dir>]
|
|
104
|
+
starci app stack <prepare|doctor|connect|disconnect|build-args|render|deploy|converge|verify|rollback|prune|backup|jobs> --env <env> [--execute] [--json] [--cwd <dir>]
|
|
82
105
|
starci app explain <path> [--cwd <dir>] [--json]
|
|
83
106
|
starci app emit [--cwd <dir>]
|
|
84
107
|
starci app new service <dir> <name> [--inject <binding>...] [--cwd <dir>]
|
|
85
108
|
starci app new spec <file>.service.ts [--cwd <dir>]
|
|
86
109
|
starci app new image [--cwd <dir>]
|
|
87
|
-
starci app secret
|
|
110
|
+
starci app secret init [--cwd <dir>]
|
|
111
|
+
starci app secret set <env> <slug> <KEY> [--generate <bytes>] [--cwd <dir>]
|
|
112
|
+
starci app secret get <env> <slug> [KEY] --to-file [--reveal] [--cwd <dir>]
|
|
113
|
+
starci app secret unset <env> <slug> <KEY> [--execute] [--cwd <dir>]
|
|
114
|
+
starci app secret rename <env> <slug> <OLD> <NEW> [--execute] [--cwd <dir>]
|
|
115
|
+
starci app secret remove <env> <slug> [--execute] [--force-nonempty] [--cwd <dir>]
|
|
116
|
+
starci app secret materialize <env> [--cwd <dir>]
|
|
117
|
+
starci app secret list <env> [--cwd <dir>]
|
|
118
|
+
starci app secret verify <env> [--cwd <dir>]
|
|
88
119
|
starci app hygiene [--cwd <dir>]
|
|
89
120
|
`;
|
|
90
121
|
const PER_CODE_LIMIT = 25;
|
|
91
|
-
const VALUE_FLAGS = new Set(['--base', '--inject', '--into', '--event', '--from', '--service', '--connection', '--owner', '--failed', '--done']);
|
|
122
|
+
const VALUE_FLAGS = new Set(['--base', '--inject', '--into', '--event', '--from', '--service', '--connection', '--owner', '--failed', '--done', '--coverage-from', '--registry-visibility']);
|
|
92
123
|
/** Flags that may repeat: their values are collected in order. */
|
|
93
124
|
const LIST_FLAGS = new Set(['--inject']);
|
|
94
|
-
const BOOL_FLAGS = new Set(['--json', '--fast', '--fe', '--no-types', '--db-types']);
|
|
125
|
+
const BOOL_FLAGS = new Set(['--json', '--fast', '--fe', '--no-types', '--db-types', '--wiring']);
|
|
126
|
+
|
|
127
|
+
function coverageArgument(arg, args, opts) {
|
|
128
|
+
const inline = arg.startsWith('--coverage-from=');
|
|
129
|
+
const value = inline ? arg.slice('--coverage-from='.length) : args.next().value;
|
|
130
|
+
if (!value || value.startsWith('--')) throw new Error('--coverage-from needs a value');
|
|
131
|
+
if (opts['coverage-from'] !== undefined) throw new Error('--coverage-from may be given only once');
|
|
132
|
+
opts['coverage-from'] = value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function valueArgument(arg, args, opts) {
|
|
136
|
+
const value = args.next().value;
|
|
137
|
+
if (value === undefined) throw new Error(`${arg} needs a value`);
|
|
138
|
+
const key = arg.slice(2);
|
|
139
|
+
opts[key] = LIST_FLAGS.has(arg) ? [...(opts[key] ?? []), value] : value;
|
|
140
|
+
}
|
|
95
141
|
|
|
96
142
|
function parse(argv) {
|
|
97
143
|
const opts = { positional: [] };
|
|
98
|
-
|
|
99
|
-
|
|
144
|
+
const args = argv.values();
|
|
145
|
+
for (const arg of args) {
|
|
146
|
+
if (arg === '--coverage-from' || arg.startsWith('--coverage-from=')) {
|
|
147
|
+
coverageArgument(arg, args, opts);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
100
150
|
if (VALUE_FLAGS.has(arg)) {
|
|
101
|
-
|
|
102
|
-
opts[arg.slice(2)] = LIST_FLAGS.has(arg) ? [...(opts[arg.slice(2)] ?? []), argv[i + 1]] : argv[i + 1];
|
|
103
|
-
i += 1;
|
|
151
|
+
valueArgument(arg, args, opts);
|
|
104
152
|
}
|
|
105
153
|
else if (BOOL_FLAGS.has(arg)) opts[arg.slice(2)] = true;
|
|
106
154
|
else if (arg.startsWith('--')) throw new Error(`unknown flag ${arg}`);
|
|
@@ -109,34 +157,57 @@ function parse(argv) {
|
|
|
109
157
|
return opts;
|
|
110
158
|
}
|
|
111
159
|
|
|
112
|
-
function
|
|
113
|
-
|
|
160
|
+
function machineDescription(machine) {
|
|
161
|
+
if (machine.status !== 'ran') return `skipped, ${machine.reason}`;
|
|
162
|
+
const owners = machine.paths ? ` (owners ${machine.paths.join(', ')})` : '';
|
|
163
|
+
return `ran over ${machine.files} source files${owners}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function contractDescription(contracts) {
|
|
167
|
+
if (contracts.status !== 'checked') return `skipped, ${contracts.reason}`;
|
|
168
|
+
const apps = contracts.apps.map((app) => `${app.app}/${app.artifact} ${app.status}`).join(', ') || 'no api app';
|
|
169
|
+
return `emitted and compared, ${apps}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function printCheckOverview(result, out) {
|
|
114
173
|
out(`starci app check${result.fast ? ' --fast' : ''} ${result.repoRoot} (profile ${result.profile ?? 'unknown'}, manifest ${result.manifest}, ${result.tracked} tracked paths)\n`);
|
|
115
174
|
if (result.fast) out(` ${result.fast.changed} path${result.fast.changed === 1 ? '' : 's'} changed since ${result.fast.base.slice(0, 12)}\n`);
|
|
116
|
-
out(` architecture machine: ${
|
|
117
|
-
if (result.contracts) out(` contract snapshots: ${result.contracts
|
|
118
|
-
|
|
175
|
+
out(` architecture machine: ${machineDescription(result.machine)}\n`);
|
|
176
|
+
if (result.contracts) out(` contract snapshots: ${contractDescription(result.contracts)}\n`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function printFindingGroup(code, list, out) {
|
|
180
|
+
const first = list[0];
|
|
181
|
+
out(`\n[${first.level.toUpperCase()}] ${code} x${list.length}: ${first.titleVi}\n ${first.whyVi}\n -> ${first.nextStepVi}\n`);
|
|
182
|
+
for (const finding of list.slice(0, PER_CODE_LIMIT)) out(` - ${finding.message}\n`);
|
|
183
|
+
if (list.length > PER_CODE_LIMIT) out(` ... and ${list.length - PER_CODE_LIMIT} more (use --json for all)\n`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function printCheck(result, out) {
|
|
187
|
+
const { counts } = result;
|
|
188
|
+
printCheckOverview(result, out);
|
|
119
189
|
const byCode = new Map();
|
|
120
190
|
for (const f of result.findings) byCode.set(f.code, [...(byCode.get(f.code) ?? []), f]);
|
|
121
|
-
for (const [code, list] of byCode)
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
out(
|
|
191
|
+
for (const [code, list] of byCode) printFindingGroup(code, list, out);
|
|
192
|
+
out(`\n${counts.error} error finding${counts.error === 1 ? '' : 's'}, ${counts.warning} warning${counts.warning === 1 ? '' : 's'}, ${counts.info} report-only\n`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function printNoSlot(e, out) {
|
|
196
|
+
out(` no slot owns this path (${e.code}): ${e.titleVi}\n ${e.whyVi}\n`);
|
|
197
|
+
if (e.nearest) out(` nearest slot ${e.nearest.slot}, pattern ${e.nearest.pattern}; matched ${e.nearest.matchedPrefix || '.'}, then expected ${e.nearest.expectedNext ?? 'nothing'}\n`);
|
|
128
198
|
}
|
|
129
199
|
|
|
130
200
|
function printExplain(e, out) {
|
|
131
201
|
out(`${e.path}\n`);
|
|
132
202
|
if (e.status === 'no-slot') {
|
|
133
|
-
|
|
134
|
-
if (e.nearest) out(` nearest slot ${e.nearest.slot}, pattern ${e.nearest.pattern}; matched ${e.nearest.matchedPrefix || '.'}, then expected ${e.nearest.expectedNext ?? 'nothing'}\n`);
|
|
203
|
+
printNoSlot(e, out);
|
|
135
204
|
return;
|
|
136
205
|
}
|
|
137
206
|
if (e.status === 'ambiguous') { out(` owned equally by ${e.candidates.join(', ')} (${e.code}): ${e.titleVi}\n`); return; }
|
|
138
|
-
|
|
139
|
-
|
|
207
|
+
const owner = e.owner ? ` owner ${e.owner.root} (${e.owner.slot})` : '';
|
|
208
|
+
const imports = e.allowedImports ? `may import ${e.allowedImports.join(', ')}; ${e.importRule}` : (e.importRule ?? 'not applicable');
|
|
209
|
+
out(` slot ${e.slot} (${e.pattern})\n presence ${e.presence}, ${e.tracking}\n tier ${e.tier}${owner}\n`);
|
|
210
|
+
out(` imports ${imports}\n`);
|
|
140
211
|
out(` tests ${e.tests}: ${e.testsMeaning}\n`);
|
|
141
212
|
if (e.requiredFiles.length) out(` requires ${e.requiredFiles.join(', ')}\n`);
|
|
142
213
|
if (e.goesTo) out(` belongs at ${e.goesTo}\n`);
|
|
@@ -207,27 +278,29 @@ async function scaffoldPresets() {
|
|
|
207
278
|
}
|
|
208
279
|
}
|
|
209
280
|
|
|
281
|
+
function globalValueArgument(arg, args) {
|
|
282
|
+
const inline = arg.startsWith('--cwd=') || arg.startsWith('--edition=');
|
|
283
|
+
const flag = inline ? arg.slice(0, arg.indexOf('=')) : arg;
|
|
284
|
+
if (flag !== '--cwd' && flag !== '--edition') return null;
|
|
285
|
+
const value = inline ? arg.slice(arg.indexOf('=') + 1) : args.next().value;
|
|
286
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} needs a value`);
|
|
287
|
+
if (flag === '--edition' && !['full', 'lite'].includes(value)) throw new Error('--edition accepts full or lite');
|
|
288
|
+
return { flag, value };
|
|
289
|
+
}
|
|
290
|
+
|
|
210
291
|
function globalOptions(argv, baseCwd) {
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const value = inline ?? argv[++i];
|
|
222
|
-
if (!value || value.startsWith('--')) throw new Error(`${flag} needs a value`);
|
|
223
|
-
if (flag === '--edition' && !['full', 'lite'].includes(value)) throw new Error('--edition accepts full or lite');
|
|
224
|
-
if (flag === '--edition') edition = value;
|
|
225
|
-
if (flag === '--cwd') cwd = path.resolve(baseCwd, value);
|
|
226
|
-
} else if (flag === '--quiet') quiet = true;
|
|
227
|
-
else if (flag === '--help') help = true;
|
|
228
|
-
else rest.push(arg);
|
|
292
|
+
const options = { argv: [], cwd: baseCwd, quiet: false, help: false, edition: undefined };
|
|
293
|
+
const args = argv.values();
|
|
294
|
+
for (const arg of args) {
|
|
295
|
+
const valued = globalValueArgument(arg, args);
|
|
296
|
+
if (valued) {
|
|
297
|
+
if (valued.flag === '--cwd') options.cwd = path.resolve(baseCwd, valued.value);
|
|
298
|
+
else options.edition = valued.value;
|
|
299
|
+
} else if (arg === '--quiet') options.quiet = true;
|
|
300
|
+
else if (arg === '--help') options.help = true;
|
|
301
|
+
else options.argv.push(arg);
|
|
229
302
|
}
|
|
230
|
-
return
|
|
303
|
+
return options;
|
|
231
304
|
}
|
|
232
305
|
|
|
233
306
|
async function installedStackMain(cwd) {
|
|
@@ -242,9 +315,119 @@ async function installedStackMain(cwd) {
|
|
|
242
315
|
return candidate;
|
|
243
316
|
}
|
|
244
317
|
|
|
245
|
-
|
|
318
|
+
function databaseMain(argv, io) {
|
|
319
|
+
const [verb, ...rest] = argv;
|
|
320
|
+
const dispatch = DATABASE_VERBS.get(verb);
|
|
321
|
+
if (!dispatch) throw new Error('starci app db takes baseline, restamp, plan, migrate or post-cutover');
|
|
322
|
+
return dispatch(rest, io);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function printCheckProofs(result, out) {
|
|
326
|
+
if (result.wiring) out(`boot wiring: ${result.wiring.status}, ${result.wiring.apps.length} app(s), ${result.wiring.findings.length} failure(s)\n`);
|
|
327
|
+
if (result.coverage) out(formatCoverageReport(result.coverage));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function checkMain(opts, { cwd: repoRoot, edition, stdout: out, presets, prettier }) {
|
|
331
|
+
if (opts.positional.length) throw new Error('starci app check takes no path');
|
|
332
|
+
if (opts.base !== undefined && opts.fast !== true) throw new Error('--base names the ref --fast compares with; it needs --fast');
|
|
333
|
+
const checked = await checkEdition({ repoRoot, edition, normal: () => runCheck({ repoRoot, fast: opts.fast === true, base: opts.base, presets, prettier, dbTypes: opts['db-types'] === true }), presets, prettier });
|
|
334
|
+
const result = withCheckProofs(checked, { repoRoot, edition, wiring: opts.wiring === true, coverageFrom: opts['coverage-from'] });
|
|
335
|
+
if (opts.json) out(`${JSON.stringify(result, null, 2)}\n`);
|
|
336
|
+
else { printCheck(result, out); printCheckProofs(result, out); }
|
|
337
|
+
return result.ok ? 0 : 1;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function emitMain(opts, { cwd: repoRoot, stdout: out, run }) {
|
|
341
|
+
if (opts.positional.length) throw new Error('starci app emit takes no path');
|
|
342
|
+
const be = readRepoDeclaration(loadSlotManifest(), repoRoot).sides?.be;
|
|
343
|
+
if (!be) throw new Error('starci app emit runs at the app root (the folder of hfs.json)');
|
|
344
|
+
const { written, skipped, standIns } = emitContracts({ repoRoot: path.join(repoRoot, 'be'), declaration: be, run });
|
|
345
|
+
for (const file of written) out(`wrote be/${file}\n`);
|
|
346
|
+
const skipSummary = skipped.length ? `, ${skipped.join(', ')} serve no GraphQL` : '';
|
|
347
|
+
out(`starci app emit: ${written.length} written${skipSummary}\n`);
|
|
348
|
+
for (const [app, lines] of Object.entries(standIns)) {
|
|
349
|
+
out(`${app}: ${lines.length} dependenc${lines.length === 1 ? 'y' : 'ies'} of the resolvers could not load here and stood in (no GraphQL type is affected):\n`);
|
|
350
|
+
for (const line of lines) out(` ${line}\n`);
|
|
351
|
+
}
|
|
352
|
+
return 0;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function newMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
356
|
+
const [kind, ...args] = opts.positional;
|
|
357
|
+
let written;
|
|
358
|
+
if (kind === 'service' && args.length === 2) written = newService({ repoRoot, dir: args[0], name: args[1], inject: opts.inject ?? [] });
|
|
359
|
+
else if (kind === 'spec' && args.length === 1 && opts.inject === undefined) written = newSpec({ repoRoot, file: args[0] });
|
|
360
|
+
else if (kind === 'image' && args.length === 0 && opts.inject === undefined) written = newImages({ repoRoot });
|
|
361
|
+
else throw new Error('starci app new takes `service <dir> <name> [--inject ...]`, `spec <file>.service.ts` or `image`');
|
|
362
|
+
for (const file of written) out(`created ${file}\n`);
|
|
363
|
+
return 0;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function addMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
367
|
+
const [noun, name, ...extra] = opts.positional;
|
|
368
|
+
if (!noun || !name || extra.length) throw new Error('starci app add takes `<noun> <name>` and the options of the noun');
|
|
369
|
+
const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: opts.fe, noTypes: opts['no-types'] } });
|
|
370
|
+
for (const file of created) out(`created ${file}\n`);
|
|
371
|
+
const kinds = registered.kinds.length ? `; kinds ${registered.kinds.join(', ')}` : '';
|
|
372
|
+
out(`starci app add ${noun}: registered patterns ${registered.patterns.join(', ')}${kinds}\n`);
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function scaffoldMain(opts, { cwd, edition = 'full', presets, lock, stdout: out }) {
|
|
377
|
+
const [name, ...extra] = opts.positional;
|
|
378
|
+
if (!name || extra.length) throw new Error('starci app scaffold takes `<name> [--into <dir>] [--edition full|lite] [--registry-visibility public|private]`');
|
|
379
|
+
const { root: created, files } = scaffoldApp({ name, into: path.resolve(cwd, opts.into ?? '.'), edition, presets: presets ?? await scaffoldPresets(), ...(opts['registry-visibility'] === undefined ? {} : { registryVisibility: opts['registry-visibility'] }), ...(lock ? { lock } : {}) });
|
|
380
|
+
out(`starci app scaffold: created ${created} (${files.length} files); next: npm ci, then starci app lint\n`);
|
|
381
|
+
return 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function explainMain(opts, { cwd: repoRoot, stdout: out }) {
|
|
385
|
+
if (opts.positional.length !== 1) throw new Error('starci app explain takes exactly one path');
|
|
386
|
+
const explained = explainPath({ repoRoot, input: opts.positional[0] });
|
|
387
|
+
if (opts.json) out(`${JSON.stringify(explained, null, 2)}\n`); else printExplain(explained, out);
|
|
388
|
+
return explained.status === 'no-slot' || explained.status === 'ambiguous' ? 1 : 0;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function upgradeAppMain(argv, { cwd, edition, stdout, presets }) {
|
|
392
|
+
return upgradeMain(['--repo', cwd, ...(edition ? ['--edition', edition] : []), ...argv], { stdout, presets });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** The edition `hfs.json` declares, or undefined when it cannot be read (the test-world verbs then decide on their own). */
|
|
396
|
+
function declaredEdition(cwd) {
|
|
397
|
+
try { return JSON.parse(fs.readFileSync(path.join(cwd, HFS_DECLARATION_FILE), 'utf8')).edition; } catch { return undefined; }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* `starci app stack`: the deploy actions (prepare, doctor, render, deploy, converge, verify, rollback, prune, backup, jobs, ...) are the Swarm
|
|
402
|
+
* tooling of stack/cli.mjs; up, down and status are the full edition's test-world infrastructure.
|
|
403
|
+
*/
|
|
404
|
+
async function stackAppMain(argv, { cwd, edition, stackMain, stdout: out, stderr: err, env, sops, stackIo }) {
|
|
405
|
+
if (STACK_ACTIONS.includes(argv[0])) return deployStackMain(argv, { ...stackIo, cwd, stdout: out, stderr: err, env, ...(sops ? { sops } : {}) });
|
|
406
|
+
if ((edition ?? declaredEdition(cwd)) === 'lite') throw new Error('starci app stack up|down|status manage the local test world of the full edition; a lite app starts its local infrastructure with the Supabase CLI');
|
|
407
|
+
const run = stackMain ?? await installedStackMain(cwd);
|
|
408
|
+
return run(argv, { cwd, out, err });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const DIRECT_VERBS = new Map([['lint', lintMain], ['secret', secretMain], ['db', databaseMain], ['upgrade', upgradeAppMain], ['stack', stackAppMain], ['release', releaseMain]]);
|
|
412
|
+
const PARSED_VERBS = new Map([['check', checkMain], ['emit', emitMain], ['new', newMain], ['add', addMain], ['scaffold', scaffoldMain], ['explain', explainMain]]);
|
|
413
|
+
|
|
414
|
+
function dispatchMain(verb, argv, io) {
|
|
415
|
+
if (verb === 'sync' || verb === 'hygiene') return syncMain([verb, ...argv], io);
|
|
416
|
+
const direct = DIRECT_VERBS.get(verb);
|
|
417
|
+
if (direct) return direct(argv, io);
|
|
418
|
+
return PARSED_VERBS.get(verb)(parse(argv), io);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function errorMessage(error) {
|
|
422
|
+
if (error instanceof EditionRefusal || error instanceof UpgradeError || error instanceof HfsSlotsError || error instanceof SyncError) return `${error.message}\n`;
|
|
423
|
+
if (error instanceof ScaffoldError) return `${error.code}: ${error.message}\n`;
|
|
424
|
+
return `starci app: ${error.message}\n${USAGE}`;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** `run` injects the Supabase command runner and `stackIo` the spawn, fetch, sleep and clock seams of the Swarm tooling; IO and tool seams keep every verb independently testable. */
|
|
246
428
|
export async function main(argv, {
|
|
247
429
|
cwd: baseCwd = process.cwd(), stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain,
|
|
430
|
+
env = process.env, stdin, sops, tools, platform, stackIo,
|
|
248
431
|
} = {}) {
|
|
249
432
|
try {
|
|
250
433
|
const global = globalOptions(argv, path.resolve(baseCwd));
|
|
@@ -252,73 +435,9 @@ export async function main(argv, {
|
|
|
252
435
|
const out = global.quiet ? () => {} : stdout;
|
|
253
436
|
if (global.help) { out(USAGE); return 0; }
|
|
254
437
|
if (!VERBS.includes(verb)) { stderr(USAGE); return 2; }
|
|
255
|
-
|
|
256
|
-
if (verb === 'lint') return await lintMain(rest, { cwd: global.cwd, stdout: out, presets, prettier });
|
|
257
|
-
if (verb === 'secret') return secretMain(rest, { stdout: out, stderr, cwd: global.cwd });
|
|
258
|
-
if (verb === 'upgrade') return await upgradeMain(['--repo', global.cwd, ...(global.edition ? ['--edition', global.edition] : []), ...rest], { stdout: out, presets });
|
|
259
|
-
if (verb === 'stack') {
|
|
260
|
-
const run = stackMain ?? await installedStackMain(global.cwd);
|
|
261
|
-
return await run(rest, { cwd: global.cwd, out, err: stderr });
|
|
262
|
-
}
|
|
263
|
-
const opts = parse(rest);
|
|
264
|
-
const repoRoot = global.cwd;
|
|
265
|
-
if (verb === 'check') {
|
|
266
|
-
if (opts.positional.length) throw new Error('starci app check takes no path');
|
|
267
|
-
if (opts.base !== undefined && opts.fast !== true) throw new Error('--base names the ref --fast compares with; it needs --fast');
|
|
268
|
-
const result = await checkEdition({ repoRoot, edition: global.edition, normal: () => runCheck({ repoRoot, fast: opts.fast === true, base: opts.base, presets, prettier, dbTypes: opts['db-types'] === true }), presets, prettier });
|
|
269
|
-
if (opts.json) out(`${JSON.stringify(result, null, 2)}\n`); else printCheck(result, out);
|
|
270
|
-
return result.ok ? 0 : 1;
|
|
271
|
-
}
|
|
272
|
-
if (verb === 'emit') {
|
|
273
|
-
if (opts.positional.length) throw new Error('starci app emit takes no path');
|
|
274
|
-
const be = readRepoDeclaration(loadSlotManifest(), repoRoot).sides?.be;
|
|
275
|
-
if (!be) throw new Error('starci app emit runs at the app root (the folder of hfs.json)');
|
|
276
|
-
const { written, skipped, standIns } = emitContracts({ repoRoot: path.join(repoRoot, 'be'), declaration: be, run });
|
|
277
|
-
for (const file of written) out(`wrote be/${file}
|
|
278
|
-
`);
|
|
279
|
-
out(`starci app emit: ${written.length} written${skipped.length ? `, ${skipped.join(', ')} serve no GraphQL` : ''}
|
|
280
|
-
`);
|
|
281
|
-
for (const [app, lines] of Object.entries(standIns)) {
|
|
282
|
-
out(`${app}: ${lines.length} dependenc${lines.length === 1 ? 'y' : 'ies'} of the resolvers could not load here and stood in (no GraphQL type is affected):\n`);
|
|
283
|
-
for (const line of lines) out(` ${line}\n`);
|
|
284
|
-
}
|
|
285
|
-
return 0;
|
|
286
|
-
}
|
|
287
|
-
if (verb === 'new') {
|
|
288
|
-
const [kind, ...args] = opts.positional;
|
|
289
|
-
let written;
|
|
290
|
-
if (kind === 'service' && args.length === 2) written = newService({ repoRoot, dir: args[0], name: args[1], inject: opts.inject ?? [] });
|
|
291
|
-
else if (kind === 'spec' && args.length === 1 && opts.inject === undefined) written = newSpec({ repoRoot, file: args[0] });
|
|
292
|
-
else if (kind === 'image' && args.length === 0 && opts.inject === undefined) written = newImages({ repoRoot });
|
|
293
|
-
else throw new Error('starci app new takes `service <dir> <name> [--inject ...]`, `spec <file>.service.ts` or `image`');
|
|
294
|
-
for (const file of written) out(`created ${file}
|
|
295
|
-
`);
|
|
296
|
-
return 0;
|
|
297
|
-
}
|
|
298
|
-
if (verb === 'add') {
|
|
299
|
-
const [noun, name, ...extra] = opts.positional;
|
|
300
|
-
if (!noun || !name || extra.length) throw new Error('starci app add takes `<noun> <name>` and the options of the noun');
|
|
301
|
-
const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: opts.fe, noTypes: opts['no-types'] } });
|
|
302
|
-
for (const file of created) out(`created ${file}
|
|
303
|
-
`);
|
|
304
|
-
out(`starci app add ${noun}: registered patterns ${registered.patterns.join(', ')}${registered.kinds.length ? `; kinds ${registered.kinds.join(', ')}` : ''}
|
|
305
|
-
`);
|
|
306
|
-
return 0;
|
|
307
|
-
}
|
|
308
|
-
if (verb === 'scaffold') {
|
|
309
|
-
const [name, ...extra] = opts.positional;
|
|
310
|
-
if (!name || extra.length) throw new Error('starci app scaffold takes `<name> [--into <dir>] [--edition full|lite]`');
|
|
311
|
-
const edition = global.edition ?? 'full';
|
|
312
|
-
const { root: created, files } = scaffoldApp({ name, into: path.resolve(global.cwd, opts.into ?? '.'), edition, presets: presets ?? await scaffoldPresets(), ...(lock ? { lock } : {}) });
|
|
313
|
-
out(`starci app scaffold: created ${created} (${files.length} files); next: npm ci, then starci app lint\n`);
|
|
314
|
-
return 0;
|
|
315
|
-
}
|
|
316
|
-
if (opts.positional.length !== 1) throw new Error('starci app explain takes exactly one path');
|
|
317
|
-
const explained = explainPath({ repoRoot, input: opts.positional[0] });
|
|
318
|
-
if (opts.json) out(`${JSON.stringify(explained, null, 2)}\n`); else printExplain(explained, out);
|
|
319
|
-
return explained.status === 'no-slot' || explained.status === 'ambiguous' ? 1 : 0;
|
|
438
|
+
return await dispatchMain(verb, rest, { cwd: global.cwd, edition: global.edition, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, sops, tools, platform, stackIo });
|
|
320
439
|
} catch (error) {
|
|
321
|
-
stderr(error
|
|
440
|
+
stderr(errorMessage(error));
|
|
322
441
|
return 2;
|
|
323
442
|
}
|
|
324
443
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// age-install.mjs - installs `age` (the backup encryptor) on the manager over passwordless sudo: the host's own package manager first
|
|
2
|
+
// (dnf, yum, apk or apt-get, detected, never assumed), then the official static binary with its sha256 verified before anything is installed.
|
|
3
|
+
import { firstLine } from './context.mjs';
|
|
4
|
+
|
|
5
|
+
const AGE_VERSION = 'v1.2.1';
|
|
6
|
+
const AGE_URL = `https://github.com/FiloSottile/age/releases/download/${AGE_VERSION}/age-${AGE_VERSION}-linux-amd64.tar.gz`;
|
|
7
|
+
/** sha256 of the official linux/amd64 archive of AGE_VERSION. */
|
|
8
|
+
export const AGE_SHA256 = '7df45a6cc87d4da11cc03a539a7470c15b1041ab2b396af088fe9990f7c79d50';
|
|
9
|
+
const INSTALL = { dnf: 'sudo -n dnf install -y age', yum: 'sudo -n yum install -y age', apk: 'sudo -n apk add age', 'apt-get': 'sudo -n apt-get install -y age' };
|
|
10
|
+
|
|
11
|
+
const STATIC_INSTALL = [
|
|
12
|
+
'set -e',
|
|
13
|
+
'tmp="$(mktemp -d)"',
|
|
14
|
+
'trap \'rm -rf "$tmp"\' EXIT',
|
|
15
|
+
`curl -fsSL -o "$tmp/age.tgz" ${AGE_URL}`,
|
|
16
|
+
`echo "${AGE_SHA256} $tmp/age.tgz" | sha256sum -c -`,
|
|
17
|
+
'tar -xzf "$tmp/age.tgz" -C "$tmp"',
|
|
18
|
+
'sudo -n install -m 0755 "$tmp/age/age" "$tmp/age/age-keygen" /usr/local/bin/',
|
|
19
|
+
].join('; ');
|
|
20
|
+
|
|
21
|
+
const packageManager = (run) => run('for m in dnf yum apk apt-get; do command -v $m >/dev/null 2>&1 && echo $m && break; done').stdout.trim();
|
|
22
|
+
const present = (run) => run('command -v age').status === 0;
|
|
23
|
+
|
|
24
|
+
/** Installs age when it is missing; reports through `report`. */
|
|
25
|
+
export function installAge(run, report) {
|
|
26
|
+
if (present(run)) return;
|
|
27
|
+
const manager = packageManager(run);
|
|
28
|
+
let failure = 'no package manager (dnf, yum, apk, apt-get) found';
|
|
29
|
+
if (Object.hasOwn(INSTALL, manager)) {
|
|
30
|
+
const packaged = run(INSTALL[manager]);
|
|
31
|
+
if (packaged.status === 0 && present(run)) { report.note(`installed age with ${manager}`); return; }
|
|
32
|
+
failure = `${manager} could not install age: ${firstLine(packaged.stderr)}`;
|
|
33
|
+
}
|
|
34
|
+
const fetched = run(STATIC_INSTALL);
|
|
35
|
+
if (fetched.status === 0 && present(run)) report.note(`installed the official static age ${AGE_VERSION} (sha256 verified) into /usr/local/bin; ${failure}`);
|
|
36
|
+
else report.bad(`cannot install age (${failure}); the static install failed too: ${firstLine(fetched.stderr)} (needs passwordless sudo once, or install age by hand)`);
|
|
37
|
+
}
|
package/stack/backup.mjs
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// backup.mjs - `starci app stack backup`: age-encrypted tarballs of the declared volumes, produced ON the manager.
|
|
2
|
+
// Per volume ONE remote command streams `tar` straight through `age` into <name>-<stamp>.tgz.age.partial and renames it to its final name only
|
|
3
|
+
// when the whole pipeline succeeded, so no plaintext file ever exists and a failed dump never leaves a truncated backup that looks valid.
|
|
4
|
+
// The age recipient is the public recipient of the app's .sops.yaml (the custody's own recipient). --swarm-state also archives
|
|
5
|
+
// /var/lib/docker/swarm (orchestration state; NOT a database backup). Backups older than keepDays are deleted after a fully successful run.
|
|
6
|
+
// The files are NOT moved off the machine: copying them and proving a restore on an isolated host is the owner's job.
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { firstLine } from './context.mjs';
|
|
10
|
+
import { failure, refusal } from './errors.mjs';
|
|
11
|
+
import { shellQuote, withRunner } from './remote.mjs';
|
|
12
|
+
import { parseYaml } from '../runtime/engine/yaml.mjs';
|
|
13
|
+
|
|
14
|
+
const RECIPIENT = /age1[0-9a-z]{50,}/u;
|
|
15
|
+
|
|
16
|
+
/** The first age recipient of the creation_rules of <repoRoot>/.sops.yaml. */
|
|
17
|
+
function readRecipient(repoRoot) {
|
|
18
|
+
let rules;
|
|
19
|
+
try { rules = parseYaml(fs.readFileSync(path.join(repoRoot, '.sops.yaml'), 'utf8'))?.creation_rules; } catch { rules = null; }
|
|
20
|
+
const found = RECIPIENT.exec(JSON.stringify(rules ?? null));
|
|
21
|
+
if (!found) throw refusal('STACK_BACKUP_NO_RECIPIENT', 'no age recipient found in the creation_rules of .sops.yaml');
|
|
22
|
+
return found[0];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** YYYYMMDD-HHMMSS in UTC. */
|
|
26
|
+
const stampOf = (epochMs) => new Date(epochMs).toISOString().slice(0, 19).replaceAll('-', '').replaceAll(':', '').replace('T', '-');
|
|
27
|
+
|
|
28
|
+
function archiveJob(name, producer, { dir, stamp, recipient }) {
|
|
29
|
+
const final = `${dir}/${name}-${stamp}.tgz.age`;
|
|
30
|
+
const partial = `${final}.partial`;
|
|
31
|
+
const script = [
|
|
32
|
+
'set -o pipefail', 'umask 077', `mkdir -p ${shellQuote(dir)}`,
|
|
33
|
+
`${producer} | age -r ${shellQuote(recipient)} -o ${shellQuote(partial)} && mv ${shellQuote(partial)} ${shellQuote(final)} || { rm -f ${shellQuote(partial)}; exit 1; }`,
|
|
34
|
+
].join('; ');
|
|
35
|
+
return { name: `${name}-${stamp}.tgz.age`, command: `bash -c ${shellQuote(script)}` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function planJobs(config, { swarmState, stamp, recipient }) {
|
|
39
|
+
const dir = config.backup.dir ?? `/var/backups/${config.stack}`;
|
|
40
|
+
const shared = { dir, stamp, recipient };
|
|
41
|
+
const jobs = config.backup.volumes.map((entry) => {
|
|
42
|
+
const mount = shellQuote(`${entry.volume}:/src:ro`);
|
|
43
|
+
return archiveJob(entry.name, `docker run --rm -v ${mount} ${shellQuote(config.backup.image)} tar czf - -C /src .`, shared);
|
|
44
|
+
});
|
|
45
|
+
if (swarmState) jobs.push(archiveJob('swarm-state', 'sudo tar czf - -C /var/lib/docker swarm', shared));
|
|
46
|
+
return { dir, jobs };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const pruneCommand = (dir, keepDays) => String.raw`find ${shellQuote(dir)} -type f \( -name '*.age' -o -name '*.age.partial' \) -mtime +${Number(keepDays)} -delete`;
|
|
50
|
+
|
|
51
|
+
function describePlan(config, plan, recipient, swarmState) {
|
|
52
|
+
const names = config.backup.volumes.map((entry) => entry.name);
|
|
53
|
+
return [
|
|
54
|
+
`volumes: ${names.join(', ')}${swarmState ? ' and the swarm state' : ''}`,
|
|
55
|
+
`target on the manager: ${plan.dir} (keep ${config.backup.keepDays} days)`,
|
|
56
|
+
`age recipient: ${recipient.slice(0, 12)}... (public recipient of .sops.yaml)`,
|
|
57
|
+
`files: ${plan.jobs.map((job) => job.name).join(', ')}`,
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const REMINDERS = [
|
|
62
|
+
'the files are encrypted but still on the manager: copy them to storage in another failure domain',
|
|
63
|
+
'a restore proof on an isolated host (age -d with the owner identity) is the job of the owner',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function runArchives(run, plan) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
const files = [];
|
|
69
|
+
for (const job of plan.jobs) {
|
|
70
|
+
const result = run(job.command);
|
|
71
|
+
if (result.status === 0) {
|
|
72
|
+
files.push(job.name);
|
|
73
|
+
lines.push(`ok ${job.name}`);
|
|
74
|
+
} else lines.push(`FAIL ${job.name}: ${firstLine(result.stderr)}`);
|
|
75
|
+
}
|
|
76
|
+
return { lines, files };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** backup: the plan without execute; with execute the encrypted archives, then the retention sweep. */
|
|
80
|
+
export async function backupVolumes(ctx, config, { swarmState = false, execute = false, sshCmd = null } = {}, deps = {}) {
|
|
81
|
+
if (!config.backup) throw refusal('STACK_BACKUP_UNDECLARED', 'the environment declares no deploy.backup block');
|
|
82
|
+
const recipient = readRecipient(config.repoRoot);
|
|
83
|
+
const plan = planJobs(config, { swarmState, stamp: stampOf(ctx.now()), recipient });
|
|
84
|
+
const summary = describePlan(config, plan, recipient, swarmState);
|
|
85
|
+
if (!execute) return { ok: true, lines: ['dry run: nothing was backed up', ...summary], data: { files: [], planned: plan.jobs.map((job) => job.name) } };
|
|
86
|
+
return withRunner(ctx, config, { sshCmd }, deps, (run, notes) => {
|
|
87
|
+
if (run('command -v age >/dev/null 2>&1').status !== 0) throw refusal('STACK_BACKUP_NO_AGE', 'age is not installed on the manager (apt install age)');
|
|
88
|
+
const done = runArchives(run, plan);
|
|
89
|
+
const lines = [...notes, ...summary, ...done.lines];
|
|
90
|
+
const complete = done.files.length === plan.jobs.length;
|
|
91
|
+
if (complete) {
|
|
92
|
+
const pruned = run(pruneCommand(plan.dir, config.backup.keepDays));
|
|
93
|
+
if (pruned.status !== 0) throw failure('STACK_BACKUP_PRUNE', `deleting backups older than ${config.backup.keepDays} days failed: ${firstLine(pruned.stderr)}`);
|
|
94
|
+
lines.push(`ok removed backups older than ${config.backup.keepDays} days`);
|
|
95
|
+
} else lines.push('old backups were kept because this run did not finish');
|
|
96
|
+
lines.push(...REMINDERS);
|
|
97
|
+
if (swarmState) lines.push('the swarm state archive is orchestration state, not a database backup');
|
|
98
|
+
return { ok: complete, lines, data: { files: done.files } };
|
|
99
|
+
});
|
|
100
|
+
}
|