@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/stack/task.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// task.mjs - `starci app stack task <name>`: runs ONE declared one-off command (deploy.tasks.<name>.command) inside the running task of
|
|
2
|
+
// deploy.tasks.<name>.service on the manager, through the ssh runner, and relays its stdout. Which command runs is the app's declaration, never a
|
|
3
|
+
// flag: the only argument is the task name. Without --execute a dry-run line says what would run. Output is relayed as the command prints it; this
|
|
4
|
+
// module never reads or prints environment values.
|
|
5
|
+
import { firstLine, lines as splitLines } from './context.mjs';
|
|
6
|
+
import { failure, refusal } from './errors.mjs';
|
|
7
|
+
import { remoteDocker, withRunner } from './remote.mjs';
|
|
8
|
+
|
|
9
|
+
const TASK_NAME = /^[a-z][a-z0-9-]{0,40}$/u;
|
|
10
|
+
|
|
11
|
+
function undeclared(config, why) {
|
|
12
|
+
const names = Object.keys(config.tasks ?? {});
|
|
13
|
+
const listing = names.length > 0 ? `declared tasks: ${names.join(', ')}` : 'the environment declares no deploy.tasks';
|
|
14
|
+
return refusal('STACK_TASK_UNDECLARED', `${why}; ${listing}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function runningTask(run, service) {
|
|
18
|
+
const result = remoteDocker(run, ['ps', '-q', '--filter', `label=com.docker.swarm.service.name=${service}`, '--filter', 'status=running']);
|
|
19
|
+
const task = result.status === 0 ? splitLines(result.stdout)[0] : undefined;
|
|
20
|
+
if (task) return task;
|
|
21
|
+
const reason = result.status === 0 ? 'none is running' : firstLine(result.stderr);
|
|
22
|
+
throw failure('STACK_TASK_NO_TASK', `no running task of service ${service} on the manager (${reason})`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** task: runs the declared one-off command `name`. */
|
|
26
|
+
export async function runTask(ctx, config, { name, args = [], execute = false, sshCmd = null } = {}, deps = {}) {
|
|
27
|
+
if (name === undefined || !TASK_NAME.test(String(name)) || !Object.hasOwn(config.tasks ?? {}, name)) {
|
|
28
|
+
throw undeclared(config, name === undefined ? 'task takes the name of a declared task' : `no task named ${String(name).slice(0, 41)} is declared`);
|
|
29
|
+
}
|
|
30
|
+
if (args.length > 0) throw refusal('STACK_USAGE', 'task takes only the task name; the command is the app\'s declaration');
|
|
31
|
+
const declared = config.tasks[name];
|
|
32
|
+
if (!execute) {
|
|
33
|
+
return { ok: true, lines: [`dry run: would run task ${name} (${declared.command.join(' ')}) in the running ${declared.service} task (add --execute)`], data: { name, status: null } };
|
|
34
|
+
}
|
|
35
|
+
return withRunner(ctx, config, { sshCmd }, deps, (run, notes) => {
|
|
36
|
+
const task = runningTask(run, `${config.stack}_${declared.service}`);
|
|
37
|
+
const result = remoteDocker(run, ['exec', task, ...declared.command]);
|
|
38
|
+
const lines = [...notes, ...splitLines(result.stdout)];
|
|
39
|
+
if (result.status !== 0) lines.push(`FAIL task ${name} exited ${result.status}: ${firstLine(result.stderr)}`);
|
|
40
|
+
return { ok: result.status === 0, lines, data: { name, status: result.status } };
|
|
41
|
+
});
|
|
42
|
+
}
|
package/stack/verify.mjs
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// verify.mjs - functional verification after convergence: the declared HTTP journeys (config.verify.http) and the declared checks inside a
|
|
2
|
+
// running task (config.verify.exec). Probes are bounded and read-only; a POST body comes only from the declared config and nothing here
|
|
3
|
+
// mutates business data. HTTP goes through ctx.fetch, docker through ctx.spawn, waits through ctx.sleep and time through ctx.now.
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
|
+
import { eachInOrder, repeatInOrder } from '../runtime/scripts/lib/in-order.mjs';
|
|
6
|
+
import { lines as splitLines, tool } from './context.mjs';
|
|
7
|
+
import { edgeAttachmentsOf, verifyEdgeAttachment } from './edge-alias.mjs';
|
|
8
|
+
|
|
9
|
+
const RETRY_MS = 6000;
|
|
10
|
+
const HTTP_TIMEOUT_MS = 8000;
|
|
11
|
+
|
|
12
|
+
const expect = (condition, message) => {
|
|
13
|
+
if (!condition) throw new Error(message);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Runs one check, retrying every 6 s up to `retrySeconds` when `retry` is set; records { name, ok, detail }. */
|
|
17
|
+
async function runCheck(ctx, results, name, run, { retry = false, retrySeconds = 0 } = {}) {
|
|
18
|
+
const deadline = ctx.now() + (retry ? retrySeconds * 1000 : 0);
|
|
19
|
+
const outcome = await repeatInOrder(async () => {
|
|
20
|
+
try {
|
|
21
|
+
return { ok: true, detail: await run() };
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (ctx.now() >= deadline) return { ok: false, detail: String(error.message) };
|
|
24
|
+
await ctx.sleep(RETRY_MS);
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
results.push({ name, ...outcome });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function checkJson(text, expected) {
|
|
32
|
+
let body;
|
|
33
|
+
try { body = JSON.parse(text); } catch { throw new Error('body is not JSON'); }
|
|
34
|
+
for (const [key, value] of Object.entries(expected)) expect(isDeepStrictEqual(body?.[key], value), `body.${key} is not the declared value`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One declared HTTP journey; throws a message when the answer is not the declared one. */
|
|
38
|
+
async function probeHttp(ctx, check) {
|
|
39
|
+
const init = { method: check.method, signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) };
|
|
40
|
+
if (check.body !== null) Object.assign(init, { body: check.body, headers: { 'content-type': 'application/json' } });
|
|
41
|
+
const response = await ctx.fetch(check.url, init);
|
|
42
|
+
expect(response.status === check.status, `HTTP ${response.status}`);
|
|
43
|
+
const text = check.json === null && check.contains === null ? '' : await response.text();
|
|
44
|
+
if (check.json !== null) checkJson(text, check.json);
|
|
45
|
+
if (check.contains !== null) expect(text.includes(check.contains), `no ${check.contains} in the answer`);
|
|
46
|
+
return String(response.status);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `docker exec` of a declared command in the running container; exit 0 and, when declared, stdout matching the regex. */
|
|
50
|
+
function probeExec(ctx, container, check) {
|
|
51
|
+
const run = tool(ctx, 'docker', ['exec', container, ...check.command]);
|
|
52
|
+
expect(run.status === 0, `exit ${run.status}`);
|
|
53
|
+
if (check.stdout === null) return 'ok';
|
|
54
|
+
expect(new RegExp(check.stdout, 'u').test(run.stdout), `stdout does not match ${check.stdout}`);
|
|
55
|
+
return run.stdout.trim();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The running-task check of one service, then its declared commands (skipped when no task runs). */
|
|
59
|
+
async function verifyService(ctx, config, results, service) {
|
|
60
|
+
const found = tool(ctx, 'docker', ['ps', '-q', '--filter', `label=com.docker.swarm.service.name=${config.stack}_${service}`, '--filter', 'status=running']);
|
|
61
|
+
const container = splitLines(found.stdout)[0];
|
|
62
|
+
await runCheck(ctx, results, `a ${service} task is running on this manager`, () => {
|
|
63
|
+
expect(container, `no running container of ${config.stack}_${service} on the daemon`);
|
|
64
|
+
return container.slice(0, 12);
|
|
65
|
+
});
|
|
66
|
+
if (!container) return;
|
|
67
|
+
await eachInOrder(config.verify.exec.filter((entry) => entry.service === service), (check) => runCheck(ctx, results, check.name, () => probeExec(ctx, container, check)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Every edge-attached service carries exactly its declared aliases (never the implicit short name) on the shared external network. */
|
|
71
|
+
async function verifyEdge(ctx, config, results, file) {
|
|
72
|
+
let attachments;
|
|
73
|
+
try {
|
|
74
|
+
attachments = edgeAttachmentsOf(file);
|
|
75
|
+
} catch (error) {
|
|
76
|
+
results.push({ name: 'edge aliases: the stack file is readable', ok: false, detail: String(error.message) });
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await eachInOrder(attachments, (item) => runCheck(ctx, results, `edge aliases of ${item.service} on ${item.network} are exactly the declared ones`, () => verifyEdgeAttachment(ctx, config, item)));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const verifyExec = (ctx, config, results) => eachInOrder([...new Set(config.verify.exec.map((check) => check.service))], (service) => verifyService(ctx, config, results, service));
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Runs the declared verification. Returns { ok, lines, data: { checks: [{ name, ok }] } }; the lines read `PASS|FAIL name detail`.
|
|
86
|
+
* `skipPublic` skips the HTTP journeys, `skipInternal` the checks inside a task. `stackFile` (default: the stack source) is read for the services
|
|
87
|
+
* attached to an external network: each must carry exactly its declared aliases, never the implicit short name.
|
|
88
|
+
*/
|
|
89
|
+
export async function verifyStack(ctx, config, { retrySeconds = 180, skipPublic = false, skipInternal = false, stackFile = config.stackFile } = {}) {
|
|
90
|
+
const results = [];
|
|
91
|
+
if (!skipPublic) await eachInOrder(config.verify.http, (check) => runCheck(ctx, results, check.name, () => probeHttp(ctx, check), { retry: check.retry, retrySeconds }));
|
|
92
|
+
if (!skipInternal && stackFile) await verifyEdge(ctx, config, results, stackFile);
|
|
93
|
+
if (!skipInternal) await verifyExec(ctx, config, results);
|
|
94
|
+
return {
|
|
95
|
+
ok: results.every((entry) => entry.ok),
|
|
96
|
+
lines: results.map((entry) => `${entry.ok ? 'PASS' : 'FAIL'} ${entry.name} ${entry.detail}`),
|
|
97
|
+
data: { checks: results.map(({ name, ok }) => ({ name, ok })) },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// One command vocabulary for managed CI rendering and the runtime coverage gate.
|
|
2
|
+
const exampleCommand = (source, command) => source?.runtimeExample
|
|
3
|
+
? 'node node_modules/@starci/hfs/runtime/scripts/hfs/example-secrets.mjs --run . -- ' + command
|
|
4
|
+
: command;
|
|
5
|
+
|
|
6
|
+
export function ciWorkflowCommands(source, lcovReport = 'be/coverage/lcov.info') {
|
|
7
|
+
return {
|
|
8
|
+
ciLint: exampleCommand(source, 'npm run lint'),
|
|
9
|
+
ciLintSonar: exampleCommand(source, 'npm run lint -- --sonar reports/lint.sonar.json'),
|
|
10
|
+
ciCoverageProof: exampleCommand(source, 'npx starci app check --coverage-from ' + lcovReport),
|
|
11
|
+
ciDatabaseLint: exampleCommand(source, 'npm run db:lint'),
|
|
12
|
+
};
|
|
13
|
+
}
|
package/sync/index.mjs
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// kind app, CI, the hooks, the formatter and Sonar configuration and .starciwork; its sides be/ and fe/ hold each side's tool
|
|
5
5
|
// configuration. The managed set is read from the slot manifest (knowledge/hfs/slots.yaml): every slot that names `managedBy`
|
|
6
6
|
// lists, in its `path`, the whole files sync renders, and `managedBy` names the template directory they are rendered from:
|
|
7
|
-
// templates/<scope or common>/<managedBy>/<file path with each leading dot dropped>,
|
|
7
|
+
// templates/<scope or common>/<managedBy>/<file path with each leading dot dropped>, with dotted skeleton paths tried when no
|
|
8
|
+
// undotted template exists. The scope is app for a root slot and the
|
|
8
9
|
// side (be, fe) for a side slot, whose files land under that side's folder. Two targets are not whole files of a managedBy slot
|
|
9
10
|
// and are listed in this module: the marked block of the root .gitignore, and .starciwork/.gitignore, which lives inside the
|
|
10
11
|
// .starciwork directory slot. Every file is rendered with the app's hfs.json (both sides and their apps) and, for the Sonar
|
|
@@ -24,8 +25,12 @@ import { APP_SCOPE, SIDES, litePresenceOf, loadSlotManifest, resolveRepoDeclarat
|
|
|
24
25
|
import { managedGroupOf } from '../runtime/scripts/hfs/edition-slots.mjs';
|
|
25
26
|
import { feAppPackageName } from '../runtime/scripts/hfs/rules/monorepo.mjs';
|
|
26
27
|
import { DEFAULT_PORT, NODE_IMAGE, dockerfilePath } from '../runtime/scripts/hfs/rules/docker.mjs';
|
|
28
|
+
import { isFile } from '../runtime/scripts/lib/fs-kind.mjs';
|
|
27
29
|
import { readDeclaredSonarKey } from './sonar-key.mjs';
|
|
30
|
+
import { readDeclaredStack, stackWorkflowVariables } from './stack-images.mjs';
|
|
28
31
|
import { byCodeUnit } from '../report/order.mjs';
|
|
32
|
+
import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
|
|
33
|
+
import { ciWorkflowCommands } from './ci-commands.mjs';
|
|
29
34
|
|
|
30
35
|
export const TEMPLATES_DIR = path.join(import.meta.dirname, '..', 'templates');
|
|
31
36
|
const NODE_MAJOR = 22;
|
|
@@ -34,7 +39,7 @@ const APP_BINARY = 'starci app';
|
|
|
34
39
|
export const SCOPES = Object.freeze([APP_SCOPE, ...SIDES]);
|
|
35
40
|
export const BLOCK_BEGIN = '# >>> starci app sync (managed block; do not edit) >>>';
|
|
36
41
|
export const BLOCK_END = '# <<< starci app sync <<<';
|
|
37
|
-
const HEADER = scope => `# Generated by ${APP_BINARY} sync (${scope === APP_SCOPE ? 'app root' :
|
|
42
|
+
const HEADER = scope => `# Generated by ${APP_BINARY} sync (${scope === APP_SCOPE ? 'app root' : scope + ' side'}). Do not edit: run "npx ${APP_BINARY} sync --write".`;
|
|
38
43
|
|
|
39
44
|
export class SyncError extends Error {
|
|
40
45
|
constructor(code, message) {
|
|
@@ -47,7 +52,8 @@ export class SyncError extends Error {
|
|
|
47
52
|
// package.json. A template that can hold a comment opens with {{header}}; a file whose exact content is specified (JSON, the
|
|
48
53
|
// eslint one-liner) has no header.
|
|
49
54
|
const MODE_OF_GROUP = Object.freeze({ 'package-scripts': 'scripts', 'package-scripts-lite': 'scripts' });
|
|
50
|
-
const
|
|
55
|
+
const GLOB_CHARACTERS = new Set('*?<>{}[]');
|
|
56
|
+
const literalFile = file => file.length >= 2 && !file.endsWith('/') && ![...file].some(character => GLOB_CHARACTERS.has(character));
|
|
51
57
|
|
|
52
58
|
/** The targets no managedBy slot lists as a whole file (see the header of this module); both belong to the app root. */
|
|
53
59
|
const UNLISTED_TARGETS = Object.freeze([
|
|
@@ -58,32 +64,44 @@ const UNLISTED_TARGETS = Object.freeze([
|
|
|
58
64
|
const templateName = (scope, group, file) => `${scope}/${group}/${file.split('/').map(segment => segment.replace(/^\./, '')).join('/')}`;
|
|
59
65
|
const onSide = (scope, file) => (scope === APP_SCOPE ? file : `${scope}/${file}`);
|
|
60
66
|
|
|
67
|
+
function managedTemplate(scope, group, file, hasTemplate) {
|
|
68
|
+
const directories = [scope, 'common'];
|
|
69
|
+
const undotted = directories.map(dir => templateName(dir, group, file)).find(name => hasTemplate(name));
|
|
70
|
+
if (undotted || !['skeleton', 'skeleton-lite'].includes(group)) return undotted;
|
|
71
|
+
return directories.map(dir => `${dir}/${group}/${file}`).find(name => hasTemplate(name));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function slotTargets(slot, scope, edition, manifest, hasTemplate) {
|
|
75
|
+
if (!slot.profiles.includes(scope) || !slotInEdition(manifest, slot, edition)) return [];
|
|
76
|
+
if (edition === 'lite' && litePresenceOf(slot, scope) === 'forbidden') return [];
|
|
77
|
+
const group = managedGroupOf(slot, edition);
|
|
78
|
+
if (group === undefined) return [];
|
|
79
|
+
const effectivePath = edition === 'lite' ? (slot.lite?.path ?? slot.path) : slot.path;
|
|
80
|
+
return braceVariants(effectivePath).map(file => {
|
|
81
|
+
const managedKey = edition === 'lite' && slot.liteManagedBy !== undefined ? 'liteManagedBy' : 'managedBy';
|
|
82
|
+
if (!literalFile(file)) throw new SyncError('HFS_SYNC_MANIFEST_MANAGED', `slot ${slot.id} names ${managedKey} but its path ${effectivePath} is not a list of literal files (${file})`);
|
|
83
|
+
const template = managedTemplate(scope, group, file, hasTemplate);
|
|
84
|
+
if (!template) throw new SyncError('HFS_SYNC_TEMPLATE_MISSING', `slot ${slot.id} manages ${file} for ${scope}, but templates/{${scope},common}/${group} has no template for it`);
|
|
85
|
+
return { path: file, template, mode: MODE_OF_GROUP[group] ?? 'file', slot: slot.id };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function unlistedTargets(scope, edition) {
|
|
90
|
+
if (scope !== APP_SCOPE) return [];
|
|
91
|
+
return UNLISTED_TARGETS.map(target => {
|
|
92
|
+
if (edition !== 'lite') return { ...target };
|
|
93
|
+
return { ...target, template: target.path === '.gitignore' ? 'app/gitignore-lite' : 'app/starciwork.gitignore-lite' };
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
61
97
|
/**
|
|
62
98
|
* Every target of `scope` (app, be or fe), its path relative to that scope's folder: the files the manifest's managedBy slots
|
|
63
99
|
* list, then (app root) the unlisted ones. A slot that names managedBy lists literal files only, and each has a template under
|
|
64
100
|
* its scope or under common.
|
|
65
101
|
*/
|
|
66
102
|
export function targetsOf(scope, { edition = 'full', manifest = loadSlotManifest(), hasTemplate = name => fs.existsSync(path.join(TEMPLATES_DIR, name)) } = {}) {
|
|
67
|
-
const listed =
|
|
68
|
-
|
|
69
|
-
if (!slot.profiles.includes(scope) || !slotInEdition(manifest, slot, edition)) continue;
|
|
70
|
-
if (edition === 'lite' && litePresenceOf(slot, scope) === 'forbidden') continue;
|
|
71
|
-
const group = managedGroupOf(slot, edition);
|
|
72
|
-
if (group === undefined) continue;
|
|
73
|
-
const effectivePath = edition === 'lite' ? (slot.lite?.path ?? slot.path) : slot.path;
|
|
74
|
-
for (const file of braceVariants(effectivePath)) {
|
|
75
|
-
if (!LITERAL_FILE.test(file)) throw new SyncError('HFS_SYNC_MANIFEST_MANAGED', `slot ${slot.id} names ${edition === 'lite' && slot.liteManagedBy !== undefined ? 'liteManagedBy' : 'managedBy'} but its path ${effectivePath} is not a list of literal files (${file})`);
|
|
76
|
-
const template = [scope, 'common'].map(dir => templateName(dir, group, file)).find(hasTemplate);
|
|
77
|
-
if (!template) throw new SyncError('HFS_SYNC_TEMPLATE_MISSING', `slot ${slot.id} manages ${file} for ${scope}, but templates/{${scope},common}/${group} has no template for it`);
|
|
78
|
-
listed.push({ path: file, template, mode: MODE_OF_GROUP[group] ?? 'file', slot: slot.id });
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const unlisted = scope === APP_SCOPE
|
|
82
|
-
? UNLISTED_TARGETS.map(target => edition === 'lite' && target.path === '.gitignore'
|
|
83
|
-
? { ...target, template: 'app/gitignore-lite' }
|
|
84
|
-
: { ...target })
|
|
85
|
-
: [];
|
|
86
|
-
return [...listed, ...unlisted];
|
|
103
|
+
const listed = manifest.slots.flatMap(slot => slotTargets(slot, scope, edition, manifest, hasTemplate));
|
|
104
|
+
return [...listed, ...unlistedTargets(scope, edition)];
|
|
87
105
|
}
|
|
88
106
|
|
|
89
107
|
const lf = text => text.replace(/\r\n/g, '\n');
|
|
@@ -111,14 +129,20 @@ function readBundled(name) {
|
|
|
111
129
|
* so a JavaScript template stays valid source before rendering. An unknown name is an error, never an empty string.
|
|
112
130
|
*/
|
|
113
131
|
export function render(text, vars, readTemplate = readBundled) {
|
|
114
|
-
const withPartials = text.replace(/^(?:\/\/ )?\{\{> ([\w./-]+)\}\}\n/gm, (_, name) => render(readTemplate(name), vars, readTemplate)
|
|
132
|
+
const withPartials = text.replace(/^(?:\/\/ )?\{\{> ([\w./-]+)\}\}\n/gm, (_, name) => trailingNewline(render(readTemplate(name), vars, readTemplate)));
|
|
115
133
|
return withPartials.replace(/\{\{([A-Za-z]\w*)\}\}/g, (_, key) => {
|
|
116
134
|
if (!Object.hasOwn(vars, key)) throw new SyncError('HFS_SYNC_TEMPLATE_VARIABLE', `template names {{${key}}}, which sync does not provide`);
|
|
117
135
|
return vars[key];
|
|
118
136
|
});
|
|
119
137
|
}
|
|
120
138
|
|
|
121
|
-
|
|
139
|
+
function trailingNewline(text) {
|
|
140
|
+
let end = text.length;
|
|
141
|
+
while (end > 0 && text[end - 1] === '\n') end -= 1;
|
|
142
|
+
return text.slice(0, end) + '\n';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** What the app's sync reads from the jest preset it installs for its be side: the Sonar exclusions ({ sonarExclusions }); both editions install it (a lite back end keeps full's unit run, ruling:R212/case-1, amended by ruling:R212/case-4). */
|
|
122
146
|
export async function loadPresets(root) {
|
|
123
147
|
const name = '@starci/jest-preset';
|
|
124
148
|
const require = createRequire(path.join(root, 'package.json'));
|
|
@@ -139,7 +163,9 @@ const SOURCE_SKIPPED = new Set(['node_modules', 'dist', 'coverage']);
|
|
|
139
163
|
|
|
140
164
|
/**
|
|
141
165
|
* The be source of the app at `root` the coverage components read: { files, read }, `files` be-relative posix paths of every `*.ts` file under
|
|
142
|
-
* be/apps and be/src (build output and dependencies skipped) and `read` their text.
|
|
166
|
+
* be/apps and be/src (build output and dependencies skipped) and `read` their text. `workPresent` records whether the optional
|
|
167
|
+
* lite brand tree exists, so sync can preserve its absence. `hasImage` identifies authored optional Dockerfiles without reading
|
|
168
|
+
* their content. An app with no be folder has no source files.
|
|
143
169
|
*/
|
|
144
170
|
export function appSource(root) {
|
|
145
171
|
const files = [];
|
|
@@ -152,7 +178,9 @@ export function appSource(root) {
|
|
|
152
178
|
}
|
|
153
179
|
};
|
|
154
180
|
for (const dir of ['apps', 'src']) walk(dir);
|
|
155
|
-
return { files: files.sort(byCodeUnit), read: file => fs.readFileSync(path.join(root, 'be', file), 'utf8')
|
|
181
|
+
return { runtimeExample: isRuntimeExampleApp(root), files: files.sort(byCodeUnit), read: file => fs.readFileSync(path.join(root, 'be', file), 'utf8'),
|
|
182
|
+
workPresent: fs.existsSync(path.join(root, '.starciwork')), hasImage: file => isFile(path.join(root, file), { followLinks: false }),
|
|
183
|
+
stack: readDeclaredStack(root) };
|
|
156
184
|
}
|
|
157
185
|
|
|
158
186
|
/**
|
|
@@ -183,7 +211,7 @@ export function jestCoverageSource(coverage) {
|
|
|
183
211
|
* `docker:build:<app>` builds the image of one app (the exact command its Dockerfile header states, tag `<project>/<app>:dev`) and
|
|
184
212
|
* `docker:build` builds every image, one after the other, never pushing.
|
|
185
213
|
*/
|
|
186
|
-
export function appScripts(app) {
|
|
214
|
+
export function appScripts(app, source) {
|
|
187
215
|
const line = (name, command) => `${JSON.stringify(name)}: ${JSON.stringify(command)},`;
|
|
188
216
|
const be = app.sides.be.apps;
|
|
189
217
|
const fe = app.sides.fe.apps;
|
|
@@ -194,7 +222,7 @@ export function appScripts(app) {
|
|
|
194
222
|
const workspace = entry => feAppPackageName(app.project, entry.name);
|
|
195
223
|
// Both editions generate before they run (owner ruling 2026-10-13): the fe's GraphQL wire types, and under lite the Supabase types check.
|
|
196
224
|
const dev = entry => `npm run codegen --silent && turbo run dev --filter=${workspace(entry)}`;
|
|
197
|
-
const images = imageEntries(app);
|
|
225
|
+
const images = imageEntries(app, source);
|
|
198
226
|
const imageCommand = ({ side, entry }) => `docker build -f ${dockerfilePath(side, entry.name)} -t ${app.project}/${entry.name}:dev .`;
|
|
199
227
|
return [
|
|
200
228
|
...(apis.length === 1 ? [line('dev:be', watch(apis[0]))] : apis.map(entry => line(`dev:be:${entry.name}`, watch(entry)))),
|
|
@@ -219,9 +247,10 @@ const opensPackages = side => (side.optionalSlots ?? []).some(id => id === 'repo
|
|
|
219
247
|
/** The stylesheets of the fe side the CSS canon judges, relative to fe/: every app and every workspace package (slot fe.route, fe.package.*). */
|
|
220
248
|
export const STYLE_GLOB = '{apps,packages}/*/src/**/*.css';
|
|
221
249
|
|
|
222
|
-
/**
|
|
223
|
-
function imageEntries(app) {
|
|
224
|
-
|
|
250
|
+
/** Required app images plus lite front ends whose optional Dockerfile the source holds. */
|
|
251
|
+
function imageEntries(app, source) {
|
|
252
|
+
const frontend = app.sides.fe.apps.filter(entry => app.edition !== 'lite' || source?.hasImage?.(dockerfilePath('fe', entry.name)));
|
|
253
|
+
return [...app.sides.be.apps.map(entry => ({ side: 'be', entry })), ...frontend.map(entry => ({ side: 'fe', entry }))];
|
|
225
254
|
}
|
|
226
255
|
|
|
227
256
|
/**
|
|
@@ -229,9 +258,16 @@ function imageEntries(app) {
|
|
|
229
258
|
* runs on the release tag, so every image is built there; no path filter narrows it. A YAML block mapping per entry, indented for
|
|
230
259
|
* `strategy.matrix.include`.
|
|
231
260
|
*/
|
|
232
|
-
export function imageMatrix(app) {
|
|
261
|
+
export function imageMatrix(app, source) {
|
|
233
262
|
const entry = (side, item) => ` - name: ${item.name}\n file: ${dockerfilePath(side, item.name)}`;
|
|
234
|
-
return imageEntries(app).map(image => entry(image.side, image.entry)).join('\n');
|
|
263
|
+
return imageEntries(app, source).map(image => entry(image.side, image.entry)).join('\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The Dockerfile of the app a deploy image names (be first, then fe); an app hfs.json does not declare cannot be built by the managed workflow. */
|
|
267
|
+
function stackDockerfile(app, name) {
|
|
268
|
+
const side = ['be', 'fe'].find(which => app.sides[which].apps.some(entry => entry.name === name));
|
|
269
|
+
if (!side) throw new SyncError('HFS_SYNC_STACK_IMAGE', `deploy.images names the app ${name}, which hfs.json does not declare under be or fe`);
|
|
270
|
+
return dockerfilePath(side, name);
|
|
235
271
|
}
|
|
236
272
|
|
|
237
273
|
const IMAGE_TEMPLATE = Object.freeze({ api: 'be/image/api', worker: 'be/image/worker', cli: 'be/image/cli', next: 'fe/image/next' });
|
|
@@ -241,14 +277,14 @@ const IMAGE_TEMPLATE = Object.freeze({ api: 'be/image/api', worker: 'be/image/wo
|
|
|
241
277
|
* and then the app's own, judged by the docker rules (R187-R191). A Next image copies the fe packages only when the fe side opts into
|
|
242
278
|
* them and the be contracts only when the fe side reads them, so each COPY names a folder the app has.
|
|
243
279
|
*/
|
|
244
|
-
export function imageFiles(app) {
|
|
280
|
+
export function imageFiles(app, source) {
|
|
245
281
|
const side = app.sides.fe;
|
|
246
282
|
const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...((side.reads ?? []).includes('be/contracts/') ? ['be/contracts'] : [])];
|
|
247
283
|
const copyInputs = inputs.map(folder => `COPY ${folder} ${folder}\n`).join('');
|
|
248
284
|
// The folders outside be/ that the back end declares it reads (the generated Supabase types of a lite back end, which an upgraded app keeps reading) are in every one of its images.
|
|
249
285
|
const beFolders = (app.sides.be.reads ?? []).map(folder => folder.replace(/\/$/, ''));
|
|
250
286
|
const copyBeInputs = beFolders.map(folder => `COPY ${folder} ${folder}\n`).join('');
|
|
251
|
-
const entries = imageEntries(app).map(({ side: which, entry: item }) => ({ side: which, item }));
|
|
287
|
+
const entries = imageEntries(app, source).map(({ side: which, entry: item }) => ({ side: which, item }));
|
|
252
288
|
return entries.map(({ side: which, item }) => {
|
|
253
289
|
const template = IMAGE_TEMPLATE[item.kind];
|
|
254
290
|
const vars = { project: app.project, app: item.name, nodeImage: NODE_IMAGE, port: DEFAULT_PORT, copyInputs, copyBeInputs };
|
|
@@ -258,14 +294,18 @@ export function imageFiles(app) {
|
|
|
258
294
|
|
|
259
295
|
/** Every value a template of `scope` can name, derived from hfs.json and the presets. */
|
|
260
296
|
export function variables(app, scope, presets, sonarKey, manifest = loadSlotManifest(), source = { files: [], read: () => '' }) {
|
|
297
|
+
const stack = source.stack ?? { images: [], passEnv: [] };
|
|
261
298
|
const fe = app.sides.fe;
|
|
262
299
|
const packages = opensPackages(fe);
|
|
263
300
|
const lite = app.edition === 'lite';
|
|
264
301
|
const providers = (app.sides.be.connections ?? []).map(connection => connection.provider);
|
|
265
302
|
return {
|
|
266
303
|
header: HEADER(scope),
|
|
304
|
+
project: app.project,
|
|
267
305
|
appBinary: APP_BINARY,
|
|
268
|
-
appScripts: appScripts(app),
|
|
306
|
+
appScripts: appScripts(app, source),
|
|
307
|
+
// A full upgrade keeps the app's declared Supabase connection and its local database commands.
|
|
308
|
+
supabaseScripts: providers.includes('supabase') ? render(readBundled('app/package-scripts/supabase.json'), { appBinary: APP_BINARY }) : '',
|
|
269
309
|
// turbo builds every fe app workspace; ^build (turbo.json) builds the packages each app imports first, and caches both.
|
|
270
310
|
buildFe: 'npm run codegen --silent && turbo run build --filter=./fe/apps/*',
|
|
271
311
|
// The be side with its tsc; every fe workspace with its own typecheck script through turbo (the packages are built first: ^build).
|
|
@@ -276,20 +316,23 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
|
|
|
276
316
|
sonarExclusions: [presets?.sonarExclusions, '**/.next/**', '**/node_modules/**', '**/src/messages/**', ...(lite ? ['supabase/types/**'] : [])].filter(Boolean).join(','),
|
|
277
317
|
sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : [])].join(','),
|
|
278
318
|
lcovReport: LCOV_REPORT,
|
|
279
|
-
|
|
319
|
+
...ciWorkflowCommands(source, LCOV_REPORT),
|
|
320
|
+
// The coverage scope is the same in both editions: lite measures exactly what full measures (ruling:R212/case-1, amended by ruling:R212/case-4).
|
|
280
321
|
jestCoverage: scope === 'be' ? jestCoverageSource(jestCoverage(manifest, providers)) : '',
|
|
281
322
|
coverageExclusions: scope === APP_SCOPE ? sonarCoverageExclusions(manifest, providers).join(',') : '',
|
|
282
323
|
codecovPaths: scope === APP_SCOPE ? codecovPaths(manifest, providers).map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
|
|
283
324
|
codecovComponents: scope === APP_SCOPE ? componentsYaml(coverageComponents(manifest, { ...source, apps: app.sides.be.apps, providers })) : '',
|
|
284
325
|
tsconfigPaths: ['be/tsconfig.json', ...fe.apps.map(entry => `fe/apps/${entry.name}/tsconfig.json`), ...(packages ? ['fe/packages/*/tsconfig.json'] : [])].join(','),
|
|
285
326
|
styleGlob: STYLE_GLOB,
|
|
286
|
-
imageMatrix: imageMatrix(app),
|
|
327
|
+
imageMatrix: imageMatrix(app, source),
|
|
287
328
|
imageProject: app.project,
|
|
329
|
+
...stackWorkflowVariables(stack, app.project, name => stackDockerfile(app, name)),
|
|
288
330
|
};
|
|
289
331
|
}
|
|
290
332
|
|
|
291
333
|
// The scripts of a package.json as one comparable text: a line per script, sorted by name (order is not drift).
|
|
292
|
-
const scriptsText = scripts =>
|
|
334
|
+
const scriptsText = scripts => Object.keys(scripts).sort(byCodeUnit)
|
|
335
|
+
.map(name => `${name}: ${typeof scripts[name] === 'string' ? scripts[name] : JSON.stringify(scripts[name])}`).join('\n') + '\n';
|
|
293
336
|
|
|
294
337
|
function parseScripts(text) {
|
|
295
338
|
try {
|
|
@@ -310,7 +353,9 @@ export function renderTargets(hfs, presets, { sonarKey, readTemplate = readBundl
|
|
|
310
353
|
const hasTemplate = name => { try { return readTemplate(name) !== undefined; } catch { return false; } };
|
|
311
354
|
return SCOPES.flatMap(scope => {
|
|
312
355
|
const vars = variables(app, scope, presets, sonarKey, manifest, source);
|
|
313
|
-
|
|
356
|
+
const targets = targetsOf(scope, { edition: app.edition, manifest, hasTemplate })
|
|
357
|
+
.filter(target => !(app.edition === 'lite' && source?.workPresent === false && target.path === '.starciwork/.gitignore'));
|
|
358
|
+
return targets.map(target => {
|
|
314
359
|
const body = render(readTemplate(target.template), vars, readTemplate);
|
|
315
360
|
const where = onSide(scope, target.path);
|
|
316
361
|
if (target.mode === 'scripts') {
|
|
@@ -319,7 +364,7 @@ export function renderTargets(hfs, presets, { sonarKey, readTemplate = readBundl
|
|
|
319
364
|
const content = scriptsText(scripts);
|
|
320
365
|
return { path: where, scope, mode: 'scripts', content, hash: hashOf(content), scripts };
|
|
321
366
|
}
|
|
322
|
-
const managed = target.mode === 'block' ? `${BLOCK_BEGIN}\n${body
|
|
367
|
+
const managed = target.mode === 'block' ? `${BLOCK_BEGIN}\n${trailingNewline(body)}${BLOCK_END}\n` : body;
|
|
323
368
|
return { path: where, scope, mode: target.mode, content: managed, hash: hashOf(managed) };
|
|
324
369
|
});
|
|
325
370
|
});
|
|
@@ -361,6 +406,21 @@ export function checkTargets(root, targets) {
|
|
|
361
406
|
}
|
|
362
407
|
|
|
363
408
|
/** Rewrite the missing or drifted targets; returns the paths written. */
|
|
409
|
+
function writtenContent(target, existing) {
|
|
410
|
+
if (target.mode === 'scripts') {
|
|
411
|
+
let pkg = {};
|
|
412
|
+
if (existing) {
|
|
413
|
+
try { pkg = JSON.parse(existing); } catch { throw new SyncError('HFS_SYNC_HFS_INVALID', `${target.path} is not valid JSON; fix it by hand before sync can set its scripts`); }
|
|
414
|
+
}
|
|
415
|
+
return `${JSON.stringify({ ...pkg, scripts: target.scripts }, null, 2)}\n`;
|
|
416
|
+
}
|
|
417
|
+
if (target.mode === 'block' && existing) {
|
|
418
|
+
const current = managedPart('block', existing);
|
|
419
|
+
return current === null ? `${target.content}\n${existing}` : existing.replace(current, () => target.content);
|
|
420
|
+
}
|
|
421
|
+
return target.content;
|
|
422
|
+
}
|
|
423
|
+
|
|
364
424
|
export function writeTargets(root, targets) {
|
|
365
425
|
const written = [];
|
|
366
426
|
for (const result of checkTargets(root, targets)) {
|
|
@@ -368,17 +428,7 @@ export function writeTargets(root, targets) {
|
|
|
368
428
|
const target = targets.find(candidate => candidate.path === result.path);
|
|
369
429
|
const file = path.join(root, target.path);
|
|
370
430
|
const existing = fs.existsSync(file) ? lf(fs.readFileSync(file, 'utf8')) : '';
|
|
371
|
-
|
|
372
|
-
if (target.mode === 'scripts') {
|
|
373
|
-
let pkg = {};
|
|
374
|
-
if (existing) {
|
|
375
|
-
try { pkg = JSON.parse(existing); } catch { throw new SyncError('HFS_SYNC_HFS_INVALID', `${target.path} is not valid JSON; fix it by hand before sync can set its scripts`); }
|
|
376
|
-
}
|
|
377
|
-
next = `${JSON.stringify({ ...pkg, scripts: target.scripts }, null, 2)}\n`;
|
|
378
|
-
} else if (target.mode === 'block' && existing) {
|
|
379
|
-
const current = managedPart('block', existing);
|
|
380
|
-
next = current === null ? `${target.content}\n${existing}` : existing.replace(current, () => target.content);
|
|
381
|
-
}
|
|
431
|
+
const next = writtenContent(target, existing);
|
|
382
432
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
383
433
|
fs.writeFileSync(file, next);
|
|
384
434
|
written.push(target.path);
|
|
@@ -404,19 +454,40 @@ export function loadHfs(root, manifest = loadSlotManifest()) {
|
|
|
404
454
|
export async function renderRepo(root, { presets, parseYaml, manifest = loadSlotManifest() } = {}) {
|
|
405
455
|
const hfs = loadHfs(root, manifest);
|
|
406
456
|
const sonarKey = await readDeclaredSonarKey(root, { parseYaml, fail: message => { throw new SyncError('HFS_SYNC_SONAR_KEY', message); } });
|
|
407
|
-
return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root), { sonarKey, manifest, source: appSource(root) }) };
|
|
457
|
+
return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root), { sonarKey, manifest, source: { ...appSource(root), stack: readDeclaredStack(root, { parseYaml }) } }) };
|
|
408
458
|
}
|
|
409
459
|
|
|
410
460
|
/** `starci app sync --check | --write [--cwd <dir>]` at the app root; returns 0 clean, 1 drift, 2 usage. */
|
|
411
|
-
|
|
461
|
+
function syncOptions(argv) {
|
|
412
462
|
const check = argv.includes('--check'), write = argv.includes('--write');
|
|
413
463
|
const cwdAt = argv.indexOf('--cwd');
|
|
414
464
|
const known = new Set(['--check', '--write', '--cwd']);
|
|
415
465
|
const unknown = argv.find((arg, index) => !known.has(arg) && index !== cwdAt + 1);
|
|
416
466
|
if ([check, write].filter(Boolean).length !== 1 || unknown !== undefined || (cwdAt >= 0 && argv[cwdAt + 1] === undefined)) {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
return { write, cwdAt };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function reportSyncCheck(root, targets, out) {
|
|
473
|
+
const results = checkTargets(root, targets);
|
|
474
|
+
const bad = results.filter(item => item.status !== 'ok');
|
|
475
|
+
for (const result of bad) {
|
|
476
|
+
const found = result.actualHash ? `, found ${result.actualHash.slice(0, 12)}` : '';
|
|
477
|
+
const where = result.difference ? ` (line ${result.difference.line}: expected ${JSON.stringify(result.difference.expected)}, found ${JSON.stringify(result.difference.actual)})` : '';
|
|
478
|
+
out(`HFS_SYNC_DRIFT ${result.path}: ${result.status}, expected sha256 ${result.expectedHash.slice(0, 12)}${found}${where}`);
|
|
479
|
+
}
|
|
480
|
+
out(`starci app sync --check: ${results.length - bad.length} of ${results.length} in sync${bad.length ? '; run "npx starci app sync --write"' : ''}`);
|
|
481
|
+
return bad.length ? 1 : 0;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export async function runSync(argv, { cwd = process.cwd(), out = line => process.stdout.write(`${line}\n`), presets, parseYaml } = {}) {
|
|
485
|
+
const options = syncOptions(argv);
|
|
486
|
+
if (!options) {
|
|
417
487
|
out('usage: starci app sync (--check | --write) [--cwd <dir>]');
|
|
418
488
|
return 2;
|
|
419
489
|
}
|
|
490
|
+
const { write, cwdAt } = options;
|
|
420
491
|
const root = cwdAt >= 0 ? path.resolve(cwd, argv[cwdAt + 1]) : cwd;
|
|
421
492
|
try {
|
|
422
493
|
const { targets } = await renderRepo(root, { presets, parseYaml });
|
|
@@ -426,15 +497,7 @@ export async function runSync(argv, { cwd = process.cwd(), out = line => process
|
|
|
426
497
|
out(`starci app sync: ${written.length} written, ${targets.length - written.length} already in sync`);
|
|
427
498
|
return 0;
|
|
428
499
|
}
|
|
429
|
-
|
|
430
|
-
const bad = results.filter(item => item.status !== 'ok');
|
|
431
|
-
for (const result of bad) {
|
|
432
|
-
const found = result.actualHash ? `, found ${result.actualHash.slice(0, 12)}` : '';
|
|
433
|
-
const where = result.difference ? ` (line ${result.difference.line}: expected ${JSON.stringify(result.difference.expected)}, found ${JSON.stringify(result.difference.actual)})` : '';
|
|
434
|
-
out(`HFS_SYNC_DRIFT ${result.path}: ${result.status}, expected sha256 ${result.expectedHash.slice(0, 12)}${found}${where}`);
|
|
435
|
-
}
|
|
436
|
-
out(`starci app sync --check: ${results.length - bad.length} of ${results.length} in sync${bad.length ? '; run "npx starci app sync --write"' : ''}`);
|
|
437
|
-
return bad.length ? 1 : 0;
|
|
500
|
+
return reportSyncCheck(root, targets, out);
|
|
438
501
|
} catch (error) {
|
|
439
502
|
if (!(error instanceof SyncError)) throw error;
|
|
440
503
|
out(error.message);
|