@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.4.0 - 2026-10-15
|
|
4
|
+
- Fixed: a swarm service on a shared external edge network no longer keeps the implicit short service name as an alias (`docker stack deploy` adds `api` for nivo_api on every network, so another tenant resolving `http://api` reached the wrong stack). `starci app stack deploy` and rollback reconcile the edge attachment to exactly the declared aliases right after `docker stack deploy` (`STACK_DEPLOY_EDGE_ALIAS` when they still differ), `stack verify` refuses an undeclared alias on an edge-attached service, and the deploy preflight and `stack doctor` extend `STACK_DEPLOY_ALIAS_SHARED` to a short name that another container or service of the edge network already resolves (`STACK_DEPLOY_EDGE_UNREADABLE` when the network cannot be inspected).
|
|
5
|
+
- Added: R295 `BE_MODULE_DEFINITION_MISSING`: every Nest module has a module definition (owner ruling 2026-10-15 `owner:module-definition-everywhere`). The slot manifest allows `<x>.module-definition.ts` and `<x>.options.ts` beside every feature, transport, cli group, queue, reactor and job module; the generators of `starci app add` and `starci app new` write the three files and register the module as `X.register({})`.
|
|
6
|
+
- Changed: `module-per-transport` accepts a module-definition beside the application module, the transport module of its folder and a cli group, and refuses one anywhere else.
|
|
7
|
+
|
|
8
|
+
## 4.3.0 - 2026-10-14
|
|
9
|
+
- Added: the app file role `<app>.env.ts` (`apps/<app>/src/`, every `be.app.*` slot, budget 150): a typed `<App>Env` interface and fail-fast `read<App>Env` reader (secrets `_FILE` first), imported by `main.ts` so the entry stays within its 80 lines. Only `main.ts` and the app's own env file read the environment; the env file lists no module and imports no feature. The api, lite api and cli scaffolds write it.
|
|
10
|
+
- Added: `starci app secret` custody verbs `init`, `set <env> <slug> <KEY>` (stdin or `--generate <bytes>`), `get <env> <slug> [KEY] --to-file [--reveal]`, `materialize <env>` and `list <env>` over sops dotenv files under `.starcistacks/<env>/secrets/`; the age identity lives outside the repository, materialized values go under the local StarCi secrets directory for `*_FILE` reading, and no value reaches stdout unless `--reveal`. The scaffolded `.sops.yaml` carries the `init` instruction and the be config template reads `<NAME>_FILE` before `<NAME>`. An untracked root `secrets.env` of a lite app is an `HFS_PLAINTEXT_SECRET` warning.
|
|
11
|
+
- Added: lite delivery slots `app.deploy` (`deploy/`), `app.ci-deploy`, `app.ci-authorize-deploy-key` and `app.scripts.delivery`; R06 scans all of them.
|
|
12
|
+
- Added: fe tier edge `feature -> feature` narrowed by `mayImportKinds` (pages compose overlays and blocks) and render props in the pure half (`render*` callbacks in `props` of a non-block component); `FE_CONNECTED_BLOCK_RENDER_PAIR` reads the exact sibling `XBase` twin.
|
|
13
|
+
- Changed: a lite app's `.starciwork/` holds only `.gitignore`, `workspace.yaml` and `brand/**` and is optional (index.yaml, shell, features, resources, UAT and br are forbidden); the work readers (validate, kernel, uat, example refs) skip the removed workspace, index, shell and feature records under lite, and the lite scaffold and example no longer write them.
|
|
14
|
+
- Changed: a pure re-export `index.ts` gets the 400-line `barrel` budget (hooks slots); an `fe.modules` instance with no `.ts`/`.tsx` file needs no `index.ts` (`HFS_REQUIRED_FILE_MISSING`, `HFS_SLOT_REQUIRED_MISSING`).
|
|
15
|
+
- Changed: `BE_BACKGROUND_UNOWNED` decides by context (job processor, scheduler, saga, event handler and what they call), never by a `retry*`/`replay*` method name; resolvers and controllers are never background.
|
|
16
|
+
- Changed: `HFS_DUPLICATE_CODE` drops canon scaffold shapes; `HFS_UNUSED_EXPORT` counts staged test consumers; `FE_I18N_KEYS` reads in-segment template keys (`t(`stage_${x}`)`) as globs.
|
|
17
|
+
- Changed: the scaffolded pre-commit hook passes staged files to eslint and prettier in chunks of 50 (Windows command-line limit); `DB_MIGRATION_SHAPE` orders a new migration against HEAD's active migrations, not the clock or the base branch.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
- Added: HFS manifest 3.1.0 (rule catalog 3.1.0, owner ruling 2026-10-14, R245 and R246): every `be.app.*` slot allows the composition groups `compose/<area>.ts` and budgets its composition files (`app.module.ts` 250, `<app>.options.ts` 200, `compose/<area>.ts` 120, `main.ts` 80); `<app>.options.ts` holds env-derived options and port bindings only (`BE_APP_OPTIONS_COMPOSITION`, `BE_APP_COMPOSITION_BUDGET`).
|
|
21
|
+
- Changed: `starci app scaffold` writes `apps/<api app>/src/compose/platform.ts` (the platform capabilities as one group, `platformModules(options)`) in both editions, and the scaffolded `app.module.ts` spreads it before the domain modules and the guards.
|
|
22
|
+
- Changed: `add table` lists the table's error kinds where the app keeps its error kinds array: the root `app.module.ts`, or else the `compose/` group that holds it.
|
|
23
|
+
- Changed: the architecture machine reads an app's root and its `compose/` groups as one composition (register-once, module-per-transport, the connection and context maps).
|
|
24
|
+
|
|
3
25
|
## 4.2.17 - 2026-10-14
|
|
4
26
|
|
|
5
27
|
- Fixed: the webhook door spec template signs with a per-run key (`randomBytes`) instead of a literal, so a scaffolded spec holds no plaintext secret-shaped value the release secret scan could flag.
|
|
@@ -99,6 +121,12 @@
|
|
|
99
121
|
- New: `hfs secret list|show|set|gen` (alpha.5 item 6.4), the canon command over the sealed secrets of `.starcistacks/<env>/secrets/<slug>.enc`, on the runtime's sops api (a new `seal` call: the value travels on stdin, never on an argument list). Replaces the plain `sops` commands of runbooks and KEYS.md files.
|
|
100
122
|
- Changed: HFS_REQUIRED_FILE_MISSING now also carries what BE_REQUIRED_MODULE_MISSING reported, and BE_TEST_TOPOLOGY what HFS_TEST_KIND_RETIRED reported (one code per obligation); the bundled architecture machine follows `APP_GUARD` and `APP_FILTER` providers built by a helper outside the app root file.
|
|
101
123
|
- Changed: the managed fe scripts (`dev:fe`, `start:<app>`, `build:fe`) run `next` from the app directory (`cd fe/apps/<app> && next ...`) so next-intl finds its request config; the fe skeleton reads the request locale through `next/root-params` (`experimental.rootParams`), pins the workspace root three levels above the app and uses the `.*[.].*` proxy matcher; the bundled runtime carries the `&&`/`||`-aware FE_SWR_KEY_IDENTITY check.
|
|
124
|
+
## Unreleased (a18 R281)
|
|
125
|
+
|
|
126
|
+
- Fixed: the bundled architecture machine (`function-spec-providers.mjs`, `unit-spec-providers.mjs`) takes the callable subset of a function-only subject's exports (constants, enums, re-exports and overload signatures are ignored) and lets the spec of a value-object class (undecorated, no constructor injection token) construct it with `new`.
|
|
127
|
+
- Security (canon pins, npm audit of 2026-10-16): `next` 16.1.6 to 16.4.2 (critical, with the `postcss` and `sharp` highs it brings), `axios` 1.15.2 to 1.20.0 and `@nestjs/platform-express` 11.2.5 to 11.2.7 (with `multer`) in `knowledge/hfs/canon-pins.yaml`; the example apps (`ecommerce-app`, `lite-app`, `shape-slot`) and `packages/test-world` carry the new pins and lockfiles.
|
|
128
|
+
- Known advisory, accepted (owner ruling `graphql-13-advisory-accepted-until-a19`, 2026-10-16): `@nestjs/graphql` and `@nestjs/apollo` stay on 13.4.5. `npm audit --omit=dev --audit-level=high` still reports their highs (and `@graphql-tools/utils` through them); the fix is 14.0.3, a semver major, deferred to alpha.19.
|
|
129
|
+
|
|
102
130
|
## Unreleased (C0 batch)
|
|
103
131
|
|
|
104
132
|
- Fixed (contract change `sonar-coverage-exclusions`): SonarQube has no `sonar.coverage.inclusions` property, so Sonar ignored it and counted every executable be/ file at 0% (overall coverage 65.8% and 45.2% on the examples, although every service was at 100). The managed `sonar-project.properties` now renders `sonar.coverage.exclusions` as the complement of the services. `coverageExclusions(presets, manifest)` in `sync/index.mjs` derives it from the preset's `COVERAGE_SOURCES` and the slot manifest, and lists:
|
package/README.md
CHANGED
|
@@ -34,6 +34,12 @@ out the test world and event machinery.
|
|
|
34
34
|
`app.supabase.types` artifact from the app's local Supabase stack; if that generation cannot run, scaffolding fails and removes
|
|
35
35
|
the app it began instead of leaving a partial tree.
|
|
36
36
|
|
|
37
|
+
The lite `app.starciwork` slot is optional when the app has no brand records. When present, it holds only `.gitignore`,
|
|
38
|
+
optional `workspace.yaml` metadata and `brand/**` (R212): the existing brand record, drawing records, references and assets.
|
|
39
|
+
Workspace metadata supplies the project id and repo roles for app-root detection and front-end role mapping. Lite readers
|
|
40
|
+
read workspace metadata normally and skip the feature catalog, shell and feature records. The scaffold writes `.gitignore`
|
|
41
|
+
and `workspace.yaml` and retains `brand/` only when present.
|
|
42
|
+
|
|
37
43
|
The lite additions are deliberately narrow:
|
|
38
44
|
|
|
39
45
|
- `starci app add table <name> [--fe] [--no-types]` adds the migration and matching back-end capability. `--fe` also adds the table's
|
|
@@ -77,6 +83,7 @@ Its own checks (`scripts/hfs/check.mjs`, `scripts/hfs/rules/`; the rendered-file
|
|
|
77
83
|
| `HFS_SLOT_AMBIGUOUS` | error | two slots own the path equally (a manifest gap) |
|
|
78
84
|
| `HFS_TRACKED_MUST_BE_IGNORED` | error | a tracked path in an `ignored` slot (build output, generated) |
|
|
79
85
|
| `HFS_FORBIDDEN_PRESENT` | error | a tracked path in a forbidden / `external` slot |
|
|
86
|
+
| `HFS_STARCIWORK_LITE_FORBIDDEN` | error | content outside the lite `app.starciwork` boundary: "lite .starciwork holds only workspace.yaml and brand/ (plus .gitignore)" (R212) |
|
|
80
87
|
| `HFS_SLOT_REQUIRED_MISSING` | error | a file or directory a required slot, app or instance must contain |
|
|
81
88
|
| `HFS_MIN_INSTANCES` | error | fewer instances of a slot than `minInstances` |
|
|
82
89
|
| `HFS_CANON_PIN_DRIFT` | error | a dependency not at the exact version of `knowledge/hfs/canon-pins.yaml` |
|
package/db/migrate.mjs
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// migrate.mjs - `starci app db migrate`: puts a database's migration history in the state the app's repository declares, idempotently.
|
|
2
|
+
// The optional `database:` block of .starcistacks/application-stacks.yaml names the baseline migration, the optional second baseline (standard),
|
|
3
|
+
// the held-back migrations (applied later by `db post-cutover`), the restamp pairs and the archive directory. The plan, from the remote history:
|
|
4
|
+
// repair revert each restamped old version still on the remote and mark its new version applied
|
|
5
|
+
// baseline revert the archived rows, mark the baseline applied (it is NOT run)
|
|
6
|
+
// forward push every other migration, EXCEPT the held-back ones (they are never copied into the push project)
|
|
7
|
+
// When the remote already is baseline + forward (held-back ones may or may not be applied) it prints `nothing to do` and makes no mutating call.
|
|
8
|
+
// The Supabase CLI runs through ctx.spawn with scratch workdirs; a password travels only in the child environment (SUPABASE_DB_PASSWORD from the
|
|
9
|
+
// environment or the sealed custody, PGPASSWORD split off a --url-env connection string), never in argv, a file, a line or an error.
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { isPlainObject as plain } from '../runtime/engine/plain-object.mjs';
|
|
14
|
+
import { byCodeUnit } from '../runtime/engine/by-code-unit.mjs';
|
|
15
|
+
import { readMap } from '../runtime/scripts/hfs/secret-store.mjs';
|
|
16
|
+
import { STACKS_DIR } from '../stack/config.mjs';
|
|
17
|
+
import { firstLine, tool } from '../stack/context.mjs';
|
|
18
|
+
import { approvalMissing, failure, refusal } from '../stack/errors.mjs';
|
|
19
|
+
import { readDeclaration, runVerb } from '../release/index.mjs';
|
|
20
|
+
|
|
21
|
+
const DATABASE_CODE = 'DB_CONFIG_INVALID';
|
|
22
|
+
|
|
23
|
+
const SEGMENT = /^[A-Za-z0-9._-]+$/u;
|
|
24
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/u;
|
|
25
|
+
const IDENT = /^[a-z][a-z0-9_]*$/u;
|
|
26
|
+
const SLUG = /^[a-z][a-z0-9-]*$/u;
|
|
27
|
+
const KEY = /^[A-Z][A-Z0-9_]*$/u;
|
|
28
|
+
const STAMP = /^\d{14}$/u;
|
|
29
|
+
const PROJECT_REF = /^[a-z0-9]{10,40}$/u;
|
|
30
|
+
const SIGNATURE = /^[A-Za-z0-9_."(), ]+$/u;
|
|
31
|
+
const MIGRATION = /^(\d{14})_([A-Za-z0-9_-]+)\.sql$/u;
|
|
32
|
+
const PENDING = /(\d{14})_[^\s"]+?\.sql/gu;
|
|
33
|
+
|
|
34
|
+
const matches = (pattern) => (value) => typeof value === 'string' && pattern.test(value);
|
|
35
|
+
const listOf = (item) => (value) => Array.isArray(value) && value.every((entry) => item(entry));
|
|
36
|
+
const dirOk = (value) => typeof value === 'string' && value.split('/').every((part) => SEGMENT.test(part) && part !== '..');
|
|
37
|
+
const pairOk = (value) => plain(value) && Object.keys(value).length === 2 && matches(STAMP)(value.from) && matches(STAMP)(value.to);
|
|
38
|
+
const passwordOk = (value) => plain(value) && Object.keys(value).length === 3 && matches(SLUG)(value.env) && matches(SLUG)(value.member) && matches(KEY)(value.key);
|
|
39
|
+
const POST_FIELDS = ['schema', 'functionPrefixes', 'functionNames', 'functions', 'minAgeHours'];
|
|
40
|
+
const POST_CHECKS = {
|
|
41
|
+
schema: matches(IDENT), functionPrefixes: listOf(matches(IDENT)), functionNames: listOf(matches(IDENT)),
|
|
42
|
+
functions: listOf(matches(SIGNATURE)), minAgeHours: (value) => Number.isFinite(value) && value >= 0,
|
|
43
|
+
};
|
|
44
|
+
const postOk = (value) => plain(value) && Object.keys(value).every((key) => POST_FIELDS.includes(key) && POST_CHECKS[key](value[key]));
|
|
45
|
+
|
|
46
|
+
const CHECKS = {
|
|
47
|
+
migrationsDir: [dirOk, 'must be a relative directory inside the app'],
|
|
48
|
+
archiveDir: [dirOk, 'must be a relative directory inside the app'],
|
|
49
|
+
baseline: [matches(NAME), `must match ${NAME} (the name after the stamp)`],
|
|
50
|
+
standard: [matches(NAME), `must match ${NAME} (the name after the stamp)`],
|
|
51
|
+
heldBack: [listOf(matches(NAME)), 'must be a list of migration names'],
|
|
52
|
+
projectRef: [matches(PROJECT_REF), `must match ${PROJECT_REF}`],
|
|
53
|
+
restamp: [listOf(pairOk), 'must be a list of { from, to } fourteen-digit stamps'],
|
|
54
|
+
password: [passwordOk, 'must be { env, member, key } naming a sealed value'],
|
|
55
|
+
postCutover: [postOk, `must use only ${POST_FIELDS.join(', ')} with valid values`],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** Judges the `database:` block. Returns { config } or { problems }; `baseline` is the one required field. */
|
|
59
|
+
export function normalizeDatabase(block) {
|
|
60
|
+
if (!plain(block)) return { problems: ['database: must be a mapping'] };
|
|
61
|
+
const problems = [];
|
|
62
|
+
for (const [key, value] of Object.entries(block)) {
|
|
63
|
+
if (!Object.hasOwn(CHECKS, key)) problems.push(`database.${key}: is not a field of the database block`);
|
|
64
|
+
else if (!CHECKS[key][0](value)) problems.push(`database.${key}: ${CHECKS[key][1]}`);
|
|
65
|
+
}
|
|
66
|
+
if (block.baseline === undefined) problems.push('database.baseline: is required');
|
|
67
|
+
if (problems.length > 0) return { problems };
|
|
68
|
+
const config = {
|
|
69
|
+
migrationsDir: 'supabase/migrations', archiveDir: null, standard: null, heldBack: [], projectRef: null, restamp: [], password: null, postCutover: null, ...block,
|
|
70
|
+
};
|
|
71
|
+
return { config };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The normalised `database:` block of the app at `root`; absent or invalid is a refusal (these verbs need the declaration). */
|
|
75
|
+
export function loadDatabase(root) {
|
|
76
|
+
const block = readDeclaration(root).database;
|
|
77
|
+
if (block === undefined) throw refusal('DB_UNDECLARED', 'the app declares no database: block in .starcistacks/application-stacks.yaml');
|
|
78
|
+
const { config, problems } = normalizeDatabase(block);
|
|
79
|
+
if (!config) throw refusal(DATABASE_CODE, `${problems.length} problem(s) in database: ${problems.slice(0, 6).join('; ')}`);
|
|
80
|
+
return config;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* ---- the repository model */
|
|
84
|
+
|
|
85
|
+
const sqlNames = (dir, what) => {
|
|
86
|
+
try { return fs.readdirSync(dir).filter((name) => name.endsWith('.sql')).sort(byCodeUnit); } catch { throw refusal('DB_MIGRATIONS_MISSING', `${what} (${dir}) is not readable`); }
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const stampOf = (file) => file.split('_')[0];
|
|
90
|
+
|
|
91
|
+
function archiveOf(root, decl) {
|
|
92
|
+
if (decl.archiveDir === null || !fs.existsSync(path.join(root, decl.archiveDir))) return [];
|
|
93
|
+
const moved = new Map(decl.restamp.map((pair) => [pair.from, pair.to]));
|
|
94
|
+
return sqlNames(path.join(root, decl.archiveDir), 'the archive directory').map((file) => {
|
|
95
|
+
const version = moved.get(stampOf(file)) ?? stampOf(file);
|
|
96
|
+
return { from: path.join(root, decl.archiveDir, file), name: `${version}_${file.slice(file.indexOf('_') + 1)}`, version };
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function stampProblems(model, decl) {
|
|
101
|
+
const { files, baselineFile, standardFile, baseline, standard } = model;
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
const version = stampOf(file);
|
|
104
|
+
if (file !== baselineFile && version <= baseline) throw refusal('DB_STAMP_ORDER', `${file} sorts at or before the baseline: it would run before the baseline on a fresh database`);
|
|
105
|
+
if (standardFile && file !== baselineFile && file !== standardFile && version <= standard) throw refusal('DB_STAMP_ORDER', `${file} sorts before the second baseline`);
|
|
106
|
+
if (decl.restamp.some((pair) => pair.to === version)) throw refusal('DB_STAMP_COLLISION', `${file} collides with a restamp target`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The migration files of the app and its baseline pair; refuses badly named, duplicate-stamped or missing files. */
|
|
111
|
+
function checkedFiles(root, decl) {
|
|
112
|
+
const files = sqlNames(path.join(root, decl.migrationsDir), 'the migrations directory');
|
|
113
|
+
const bad = files.filter((file) => !MIGRATION.test(file));
|
|
114
|
+
if (bad.length > 0) throw refusal('DB_MIGRATION_NAME', `migration files must be <14-digit stamp>_<name>.sql (got ${bad[0]})`);
|
|
115
|
+
if (new Set(files.map(stampOf)).size !== files.length) throw refusal('DB_STAMP_DUPLICATE', 'two migrations share one stamp');
|
|
116
|
+
const baselineFile = files.find((file) => file.endsWith(`_${decl.baseline}.sql`));
|
|
117
|
+
const standardFile = decl.standard === null ? undefined : files.find((file) => file.endsWith(`_${decl.standard}.sql`));
|
|
118
|
+
if (!baselineFile) throw refusal('DB_BASELINE_MISSING', `no migration named ${decl.baseline} in ${decl.migrationsDir}`);
|
|
119
|
+
if (decl.standard !== null && !standardFile) throw refusal('DB_BASELINE_MISSING', `no migration named ${decl.standard} in ${decl.migrationsDir}`);
|
|
120
|
+
return { files, baselineFile, standardFile };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Classifies the migration files of the app: baseline pair, held-back, forward, restamp pairs and archive. Refuses an inconsistent repository. */
|
|
124
|
+
export function buildModel(root, decl) {
|
|
125
|
+
const { files, baselineFile, standardFile } = checkedFiles(root, decl);
|
|
126
|
+
const held = files.filter((file) => decl.heldBack.some((name) => file.endsWith(`_${name}.sql`)));
|
|
127
|
+
const push = files.filter((file) => !held.includes(file));
|
|
128
|
+
const baseline = stampOf(baselineFile);
|
|
129
|
+
const model = {
|
|
130
|
+
root, decl, files, baselineFile, standardFile, baseline, standard: standardFile ? stampOf(standardFile) : null, held, push,
|
|
131
|
+
heldVersions: held.map(stampOf), pushPending: push.filter((file) => file !== baselineFile).map(stampOf), archive: archiveOf(root, decl),
|
|
132
|
+
};
|
|
133
|
+
stampProblems(model, decl);
|
|
134
|
+
return { ...model, pushVersions: new Set([baseline, ...model.pushPending]), allowed: new Set([baseline, ...model.pushPending, ...model.heldVersions]) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* ---- reading the CLI */
|
|
138
|
+
|
|
139
|
+
/** Versions from the JSON form of `supabase migration list`, or null when the text holds no readable JSON. */
|
|
140
|
+
function jsonVersions(text) {
|
|
141
|
+
const start = text.indexOf('{"migrations"');
|
|
142
|
+
if (start < 0) return null;
|
|
143
|
+
const end = text.indexOf('\n', start);
|
|
144
|
+
try {
|
|
145
|
+
const items = JSON.parse(text.slice(start, end > 0 ? end : undefined)).migrations;
|
|
146
|
+
return { remote: new Set(items.filter((item) => item.remote).map((item) => item.remote)), local: new Set(items.filter((item) => item.local).map((item) => item.local)) };
|
|
147
|
+
} catch { return null; }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Versions from the older table form: rows of `local | remote | time`. */
|
|
151
|
+
function tableVersions(text) {
|
|
152
|
+
const remote = new Set();
|
|
153
|
+
const local = new Set();
|
|
154
|
+
for (const cells of text.split('\n').map((line) => line.split('|').map((cell) => cell.trim())).filter((row) => row.length >= 3)) {
|
|
155
|
+
if (STAMP.test(cells[0])) local.add(cells[0]);
|
|
156
|
+
if (STAMP.test(cells[1])) remote.add(cells[1]);
|
|
157
|
+
}
|
|
158
|
+
return { remote, local };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Remote (and local) versions from `supabase migration list`: the JSON form, or the older table. */
|
|
162
|
+
export const parseMigrationList = (text) => jsonVersions(text) ?? tableVersions(text);
|
|
163
|
+
|
|
164
|
+
/** The versions a `db push --dry-run` would apply. */
|
|
165
|
+
export const pendingVersions = (text) => [...new Set([...text.matchAll(PENDING)].map((match) => match[1]))];
|
|
166
|
+
|
|
167
|
+
/* ---- the target */
|
|
168
|
+
|
|
169
|
+
const scrub = (text, secrets) => secrets.reduce((out, secret) => out.replaceAll(secret, '[value]'), String(text));
|
|
170
|
+
|
|
171
|
+
function parseConnection(value) {
|
|
172
|
+
try {
|
|
173
|
+
const url = new URL(value);
|
|
174
|
+
if (!/^postgres(?:ql)?:$/u.test(url.protocol)) return null;
|
|
175
|
+
const password = decodeURIComponent(url.password);
|
|
176
|
+
url.password = '';
|
|
177
|
+
return { url: url.toString(), password };
|
|
178
|
+
} catch { return null; }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function urlTarget(ctx, name) {
|
|
182
|
+
if (!KEY.test(name)) throw refusal('DB_USAGE', '--url-env must name an UPPER_CASE environment variable');
|
|
183
|
+
const value = ctx.env[name];
|
|
184
|
+
if (typeof value !== 'string' || value === '') throw refusal('DB_URL_ENV_MISSING', `${name} is not set`);
|
|
185
|
+
const parsed = parseConnection(value);
|
|
186
|
+
if (!parsed) throw refusal('DB_URL_INVALID', `${name} is not a postgres connection URL`);
|
|
187
|
+
const secrets = [value, parsed.password].filter(Boolean);
|
|
188
|
+
return { kind: 'url', how: `connection string from ${name}`, url: parsed.url, env: parsed.password ? { PGPASSWORD: parsed.password } : {}, secrets };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function sealedPassword(ctx, spec) {
|
|
192
|
+
let map;
|
|
193
|
+
try { map = readMap(path.join(ctx.cwd, STACKS_DIR, spec.env, 'secrets', `${spec.member}.enc`), ctx.sops); } catch { throw refusal('DB_PASSWORD_UNREADABLE', `the sealed member ${spec.member} cannot be opened`); }
|
|
194
|
+
if (typeof map[spec.key] !== 'string' || map[spec.key] === '') throw refusal('DB_PASSWORD_UNREADABLE', `the sealed member ${spec.member} has no value for ${spec.key}`);
|
|
195
|
+
return map[spec.key];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The database password for the linked CLI: SUPABASE_DB_PASSWORD from the environment, else the declared sealed value, else none (the CLI mints a login). */
|
|
199
|
+
function linkedPassword(ctx, decl) {
|
|
200
|
+
const given = ctx.env.SUPABASE_DB_PASSWORD;
|
|
201
|
+
if (typeof given === 'string' && given !== '') return given;
|
|
202
|
+
return decl.password === null ? null : sealedPassword(ctx, decl.password);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function linkedTarget(ctx, decl) {
|
|
206
|
+
let ref;
|
|
207
|
+
try { ref = fs.readFileSync(path.join(ctx.cwd, 'supabase', '.temp', 'project-ref'), 'utf8').trim(); } catch { throw refusal('DB_NOT_LINKED', 'no linked project (supabase/.temp/project-ref): run supabase link first'); }
|
|
208
|
+
if (decl.projectRef !== null && ref !== decl.projectRef) throw refusal('DB_WRONG_PROJECT', `the app is linked to ${ref}, not the declared project ${decl.projectRef}`);
|
|
209
|
+
const password = linkedPassword(ctx, decl);
|
|
210
|
+
return { kind: 'linked', how: `linked project ${ref}`, ref, url: null, env: password ? { SUPABASE_DB_PASSWORD: password } : {}, secrets: password ? [password] : [] };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** { kind, how, env, secrets, url }: the linked project of the app (default) or the connection string in the named environment variable. */
|
|
214
|
+
export function resolveTarget(ctx, decl, options) {
|
|
215
|
+
if (options.linked === true && options.urlEnv !== undefined) throw refusal('DB_USAGE', '--linked and --url-env exclude each other');
|
|
216
|
+
return options.urlEnv === undefined ? linkedTarget(ctx, decl) : urlTarget(ctx, options.urlEnv);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** CLI arguments that select the database and the local supabase folder. */
|
|
220
|
+
export const targetArgs = (target, dir) => (target.kind === 'url' ? ['--db-url', target.url, '--workdir', dir] : ['--linked', '--workdir', dir]);
|
|
221
|
+
|
|
222
|
+
/** Refuses a mutation unless the operator armed it with STARCI_DB_ARMED=1. */
|
|
223
|
+
export function requireArmed(ctx) {
|
|
224
|
+
if (ctx.env.STARCI_DB_ARMED !== '1') throw approvalMissing('DB_NOT_ARMED', 'set STARCI_DB_ARMED=1 to apply database changes');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const supabaseCommand = (root) => {
|
|
228
|
+
const script = path.join(root, 'node_modules', 'supabase', 'dist', 'supabase.js');
|
|
229
|
+
return fs.existsSync(script) ? [process.execPath, script] : ['supabase'];
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/** Runs the Supabase CLI through the seam; a failure names the first stderr line with every secret scrubbed. Returns { stdout, text } (text = stdout plus stderr). */
|
|
233
|
+
export function runCli(session, args) {
|
|
234
|
+
const { ctx, target } = session;
|
|
235
|
+
const [command, ...lead] = supabaseCommand(ctx.cwd);
|
|
236
|
+
const run = tool(ctx, command, [...lead, ...args], { cwd: ctx.cwd, env: { ...ctx.env, ...target.env } });
|
|
237
|
+
if (run.error) throw failure('DB_CLI_MISSING', `cannot run the Supabase CLI (${scrub(run.error.message, target.secrets)})`);
|
|
238
|
+
if (run.status !== 0) throw failure('DB_CLI_FAILED', `supabase ${args.slice(0, 2).join(' ')} failed: ${scrub(firstLine(run.stderr), target.secrets)}`);
|
|
239
|
+
return { stdout: run.stdout ?? '', text: `${run.stdout ?? ''}${run.stderr ?? ''}` };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The remote history as a Set of versions (read-only). */
|
|
243
|
+
export const listRemote = (session) => parseMigrationList(runCli(session, ['migration', 'list', ...targetArgs(session.target, session.ctx.cwd)]).text).remote;
|
|
244
|
+
|
|
245
|
+
/* ---- the plan */
|
|
246
|
+
|
|
247
|
+
/** Pure: what to do given the remote history. `done` means baseline + forward are present and nothing else is. */
|
|
248
|
+
export function planMigration(model, remote) {
|
|
249
|
+
const done = [...model.pushVersions].every((version) => remote.has(version)) && [...remote].every((version) => model.allowed.has(version));
|
|
250
|
+
const sim = new Set(remote);
|
|
251
|
+
const repairs = model.decl.restamp.filter((pair) => sim.has(pair.from));
|
|
252
|
+
for (const pair of repairs) {
|
|
253
|
+
sim.delete(pair.from);
|
|
254
|
+
sim.add(pair.to);
|
|
255
|
+
}
|
|
256
|
+
const reverts = sim.has(model.baseline) ? [] : model.archive.map((item) => item.version).filter((version) => sim.has(version));
|
|
257
|
+
for (const version of reverts) sim.delete(version);
|
|
258
|
+
const mark = !sim.has(model.baseline);
|
|
259
|
+
sim.add(model.baseline);
|
|
260
|
+
const push = model.pushPending.filter((version) => !sim.has(version));
|
|
261
|
+
for (const version of push) sim.add(version);
|
|
262
|
+
const archived = new Set(model.archive.map((item) => item.version));
|
|
263
|
+
const missingFiles = repairs.map((pair) => pair.to).filter((version) => !archived.has(version));
|
|
264
|
+
return { done, repairs, reverts, mark, push, missingFiles, unexpected: [...sim].filter((version) => !model.allowed.has(version)), final: sim.size };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function planLines(model, session, remote, plan) {
|
|
268
|
+
const nameOf = (version) => model.files.find((file) => file.startsWith(`${version}_`));
|
|
269
|
+
return [
|
|
270
|
+
`target ${session.target.how}`,
|
|
271
|
+
`remote history ${remote.size} version(s)`,
|
|
272
|
+
[`repairs ${plan.repairs.length} restamped version(s) to move`, ...plan.repairs.map((pair) => ` ${pair.from} -> ${pair.to}`)].join('\n'),
|
|
273
|
+
`archive revert ${plan.reverts.length} archived version(s) present on the remote`,
|
|
274
|
+
`baseline ${model.baseline} ${plan.mark ? 'will be marked applied (not run)' : 'already applied'}`,
|
|
275
|
+
`forward push ${plan.push.length} migration(s): ${plan.push.map(nameOf).join(', ') || 'none'}`,
|
|
276
|
+
`held back ${model.held.join(', ') || 'none'} (never applied here; starci app db post-cutover)`,
|
|
277
|
+
];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const dataOf = (model, plan, remote, extra) => ({
|
|
281
|
+
remoteCount: remote.size, repairs: plan.repairs.length, reverts: plan.reverts, baseline: model.baseline, markBaseline: plan.mark, push: plan.push,
|
|
282
|
+
heldBack: model.heldVersions, done: plan.done, ...extra,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
/* ---- applying */
|
|
286
|
+
|
|
287
|
+
/** A scratch supabase project holding exactly the given migration files; the link state of the app is copied so --linked works from it. */
|
|
288
|
+
function buildScratch(session, dir, copies) {
|
|
289
|
+
fs.mkdirSync(path.join(dir, 'supabase', 'migrations'), { recursive: true });
|
|
290
|
+
fs.writeFileSync(path.join(dir, 'supabase', 'config.toml'), 'project_id = "starci-db-scratch"\n');
|
|
291
|
+
for (const copy of copies) fs.copyFileSync(copy.from, path.join(dir, 'supabase', 'migrations', copy.name));
|
|
292
|
+
if (session.target.kind === 'linked') fs.cpSync(path.join(session.ctx.cwd, 'supabase', '.temp'), path.join(dir, 'supabase', '.temp'), { recursive: true });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const repair = (session, status, versions, dir) => runCli(session, ['migration', 'repair', '--status', status, ...versions, ...targetArgs(session.target, dir)]);
|
|
296
|
+
|
|
297
|
+
function applyRepairs(session, plan, scratch) {
|
|
298
|
+
const dir = path.join(scratch, 'archive');
|
|
299
|
+
buildScratch(session, dir, session.model.archive.map((item) => ({ from: item.from, name: item.name })));
|
|
300
|
+
repair(session, 'reverted', plan.repairs.map((pair) => pair.from), dir);
|
|
301
|
+
repair(session, 'applied', plan.repairs.map((pair) => pair.to), dir);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function applyForward(session, plan, scratch) {
|
|
305
|
+
const { model } = session;
|
|
306
|
+
const dir = path.join(scratch, 'baseline');
|
|
307
|
+
buildScratch(session, dir, model.push.map((file) => ({ from: path.join(model.root, model.decl.migrationsDir, file), name: file })));
|
|
308
|
+
if (plan.reverts.length > 0) repair(session, 'reverted', plan.reverts, dir);
|
|
309
|
+
if (plan.mark) repair(session, 'applied', [model.baseline], dir);
|
|
310
|
+
const pending = pendingVersions(runCli(session, ['db', 'push', '--dry-run', ...targetArgs(session.target, dir)]).text);
|
|
311
|
+
const stray = pending.filter((version) => !model.pushPending.includes(version));
|
|
312
|
+
if (stray.length > 0) throw failure('DB_PUSH_STRAY', `the dry run lists versions outside the push list: ${stray.join(', ')}. Stopped before pushing.`);
|
|
313
|
+
if (pending.length > 0) runCli(session, ['db', 'push', '--yes', ...targetArgs(session.target, dir)]);
|
|
314
|
+
return pending;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function applyPlan(session, plan) {
|
|
318
|
+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'starci-db-'));
|
|
319
|
+
try {
|
|
320
|
+
if (plan.repairs.length > 0) applyRepairs(session, plan, scratch);
|
|
321
|
+
const after = planMigration(session.model, listRemote(session));
|
|
322
|
+
return applyForward(session, after, scratch);
|
|
323
|
+
} finally {
|
|
324
|
+
fs.rmSync(scratch, { recursive: true, force: true });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* `starci app db migrate`. options: { execute, linked, urlEnv }. Dry run reads the remote history once and prints the plan; --execute (with
|
|
330
|
+
* STARCI_DB_ARMED=1) applies it. Already baseline + forward: `nothing to do`, no mutating call, ok.
|
|
331
|
+
*/
|
|
332
|
+
export async function migrateDatabase(ctx, decl, options = {}) {
|
|
333
|
+
const model = buildModel(ctx.cwd, decl);
|
|
334
|
+
const session = { ctx, model, target: resolveTarget(ctx, decl, options) };
|
|
335
|
+
const remote = listRemote(session);
|
|
336
|
+
const plan = planMigration(model, remote);
|
|
337
|
+
if (plan.done) return { ok: true, lines: [`target ${session.target.how}`, 'remote history already is the baseline plus the forward migrations (held-back ones may or may not be applied). Nothing to do.'], data: dataOf(model, plan, remote, { executed: false }) };
|
|
338
|
+
const lines = planLines(model, session, remote, plan);
|
|
339
|
+
const red = [
|
|
340
|
+
...plan.missingFiles.map((version) => `no archived migration file for the restamped version ${version}`),
|
|
341
|
+
...plan.unexpected.map((version) => `remote version ${version} is outside baseline + forward + held-back and no restamp or archive row explains it`),
|
|
342
|
+
];
|
|
343
|
+
if (red.length > 0) return { ok: false, lines: [...lines, ...red.map((item) => `RED: ${item}`), 'nothing applied.'], data: dataOf(model, plan, remote, { red, executed: false }) };
|
|
344
|
+
if (options.execute !== true) return { ok: true, lines: [...lines, `dry run: read-only (one migration list); the final history would be ${plan.final} version(s). Nothing applied.`], data: dataOf(model, plan, remote, { executed: false }) };
|
|
345
|
+
requireArmed(ctx);
|
|
346
|
+
const pushed = applyPlan(session, plan);
|
|
347
|
+
const final = listRemote(session);
|
|
348
|
+
const verdict = planMigration(model, final);
|
|
349
|
+
if (!verdict.done) throw failure('DB_FINAL_MISMATCH', `the remote history is not baseline + forward migrations (${[...final].join(', ')})`);
|
|
350
|
+
return { ok: true, lines: [...lines, `applied: ${pushed.length} forward migration(s) pushed; the remote history now holds ${final.size} version(s). Held back: ${model.held.join(', ') || 'nothing'}.`], data: dataOf(model, plan, remote, { executed: true, pushed, remoteCountAfter: final.size }) };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const SPEC = { boolean: ['--execute', '--json', '--linked'], value: ['--url-env'] };
|
|
354
|
+
|
|
355
|
+
/** Entry of the verb: `starci app db migrate [--execute] [--linked | --url-env NAME] [--json]`. `io` is the app IO of src/main.mjs. */
|
|
356
|
+
export function dbMigrateMain(argv, io = {}) {
|
|
357
|
+
return runVerb({
|
|
358
|
+
label: 'starci app db migrate', name: 'db migrate', code: 'DB_USAGE', io, argv, spec: SPEC,
|
|
359
|
+
run: (ctx, flags) => migrateDatabase(ctx, loadDatabase(ctx.cwd), { execute: flags.execute === true, linked: flags.linked === true, urlEnv: flags['url-env'] }),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// post-cutover.mjs - `starci app db post-cutover`: applies the held-back migrations (database.heldBack) that `db migrate` skipped, e.g. the one that
|
|
2
|
+
// drops the old engine's tables. After it a rollback to the old app needs a database restore, so it is guarded three ways:
|
|
3
|
+
// 1. a READ-ONLY pg_proc preflight (also in the dry run): every function of the declared families (database.postCutover.functionPrefixes and
|
|
4
|
+
// functionNames in database.postCutover.schema) that exists on the database must be covered by the DROP FUNCTION statements of the held-back
|
|
5
|
+
// migration (or database.postCutover.functions); a function nobody drops is a function something may still need, so nothing is applied;
|
|
6
|
+
// 2. a time gate: --green-at <ISO> at least minAgeHours (48) ago, or --owner-confirms-old-app-gone;
|
|
7
|
+
// 3. STARCI_DB_ARMED=1.
|
|
8
|
+
// Idempotent: when every held-back version is already on the remote it prints `nothing to do`. The pg_proc query goes through `options.query(sql)`
|
|
9
|
+
// (specs inject one) or, by default, `supabase db query` of the linked project or the --url-env connection.
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { failure, refusal } from '../stack/errors.mjs';
|
|
13
|
+
import { runVerb } from '../release/index.mjs';
|
|
14
|
+
import { buildModel, listRemote, loadDatabase, pendingVersions, requireArmed, resolveTarget, runCli, targetArgs } from './migrate.mjs';
|
|
15
|
+
|
|
16
|
+
const DROP_FUNCTION = /^drop\s+function\s+/iu;
|
|
17
|
+
const IF_EXISTS = /^if\s+exists\s+/iu;
|
|
18
|
+
const MS_PER_HOUR = 3.6e6;
|
|
19
|
+
const DEFAULT_MIN_AGE_HOURS = 48;
|
|
20
|
+
|
|
21
|
+
/** Makes a DROP signature comparable with pg_proc's regprocedure text. */
|
|
22
|
+
export function normalizeSignature(signature, schema) {
|
|
23
|
+
const lowered = signature.trim().toLowerCase();
|
|
24
|
+
const bare = lowered.startsWith(`${schema}.`) ? lowered.slice(schema.length + 1) : lowered;
|
|
25
|
+
const words = bare
|
|
26
|
+
.replace(/\bint\b/gu, 'integer')
|
|
27
|
+
.replace(/\btimestamptz\b/gu, 'timestamp with time zone')
|
|
28
|
+
.replace(/\s+/gu, ' ');
|
|
29
|
+
return [...'(),'].reduce((out, mark) => out.replaceAll(` ${mark}`, mark).replaceAll(`${mark} `, mark), words);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The signature a `DROP FUNCTION [IF EXISTS] <signature>` statement drops, or null for any other statement. */
|
|
33
|
+
function droppedSignature(statement) {
|
|
34
|
+
const head = DROP_FUNCTION.exec(statement);
|
|
35
|
+
return head ? statement.slice(head[0].length).replace(IF_EXISTS, '') : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The signatures a migration drops: every `DROP FUNCTION [IF EXISTS] <signature>;` statement. */
|
|
39
|
+
export function dropSignatures(sql, schema) {
|
|
40
|
+
return sql.replace(/--[^\n]*/gu, '').split(';').flatMap((statement) => {
|
|
41
|
+
const signature = droppedSignature(statement.trim());
|
|
42
|
+
return signature === null ? [] : [normalizeSignature(signature, schema)];
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const settingsOf = (post) => ({
|
|
47
|
+
schema: post.schema ?? 'public', prefixes: post.functionPrefixes ?? [], names: post.functionNames ?? [], extra: post.functions ?? [],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** The read-only pg_proc query of the declared function families (identifiers are validated, so nothing free-form reaches the SQL). */
|
|
51
|
+
export function preflightSql({ schema, prefixes, names }) {
|
|
52
|
+
const escaped = (prefix) => prefix.replaceAll('_', String.raw`\_`);
|
|
53
|
+
const clauses = [...names.map((name) => `proname = '${name}'`), ...prefixes.map((prefix) => `proname like '${escaped(prefix)}%'`)];
|
|
54
|
+
return `select oid::regprocedure::text as signature from pg_proc where pronamespace = '${schema}'::regnamespace and (${clauses.join(' or ')})`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cliQuery = (session) => (sql) => JSON.parse(runCli(session, ['db', 'query', sql, '--output', 'json', '--agent', 'no', ...targetArgs(session.target, session.ctx.cwd)]).stdout);
|
|
58
|
+
|
|
59
|
+
async function queryRows(session, sql, options) {
|
|
60
|
+
try {
|
|
61
|
+
const rows = await (options.query ?? cliQuery(session))(sql);
|
|
62
|
+
if (Array.isArray(rows) && rows.every((row) => typeof row?.signature === 'string')) return rows;
|
|
63
|
+
} catch { /* the cause may hold a connection detail: it is withheld */ }
|
|
64
|
+
throw failure('DB_PREFLIGHT_UNREADABLE', 'the read-only function preflight could not query the database; nothing applied');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The pg_proc preflight: refuses when a function of the declared families is not dropped by the held-back migration(s). */
|
|
68
|
+
async function functionPreflight(session, options) {
|
|
69
|
+
const { model, decl } = session;
|
|
70
|
+
const settings = settingsOf(decl.postCutover);
|
|
71
|
+
if (settings.prefixes.length + settings.names.length === 0) return { count: 0, line: 'function preflight: no function family declared; nothing to check' };
|
|
72
|
+
const dropped = model.held.flatMap((file) => dropSignatures(fs.readFileSync(path.join(model.root, decl.migrationsDir, file), 'utf8'), settings.schema));
|
|
73
|
+
const allowed = new Set([...dropped, ...settings.extra.map((item) => normalizeSignature(item, settings.schema))]);
|
|
74
|
+
const rows = await queryRows(session, preflightSql(settings), options);
|
|
75
|
+
const uncovered = rows.map((row) => normalizeSignature(row.signature, settings.schema)).filter((item) => !allowed.has(item));
|
|
76
|
+
if (uncovered.length > 0) throw refusal('DB_PREFLIGHT_UNCOVERED', `function signatures not covered by the held-back migration:\n${uncovered.join('\n')}`);
|
|
77
|
+
return { count: rows.length, line: `read-only function preflight: ${rows.length} signature(s), all covered by the held-back migration` };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The time gate: { open, line }. Dry run only reports it; --execute refuses while it is closed. */
|
|
81
|
+
function timeGate(ctx, post, options) {
|
|
82
|
+
if (options.ownerConfirms === true) return { open: true, line: 'gate: the owner confirms the old app is gone: OPEN' };
|
|
83
|
+
if (options.greenAt === undefined) return { open: false, line: 'gate: CLOSED (needs --green-at <ISO> old enough, or --owner-confirms-old-app-gone)' };
|
|
84
|
+
const at = Date.parse(options.greenAt);
|
|
85
|
+
if (Number.isNaN(at)) throw refusal('DB_USAGE', '--green-at must be an ISO time');
|
|
86
|
+
const hours = (ctx.now() - at) / MS_PER_HOUR;
|
|
87
|
+
const need = post.minAgeHours ?? DEFAULT_MIN_AGE_HOURS;
|
|
88
|
+
return { open: hours >= need, line: `gate: last green ${options.greenAt} (${hours.toFixed(1)} h ago, ${need} h needed): ${hours >= need ? 'OPEN' : 'CLOSED'}` };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function applyHeld(session, remote) {
|
|
92
|
+
const { model, ctx } = session;
|
|
93
|
+
const lacking = [...model.pushVersions].filter((version) => !remote.has(version));
|
|
94
|
+
if (lacking.length > 0) throw refusal('DB_BASELINE_NOT_APPLIED', `the remote history lacks ${lacking.join(', ')}: run starci app db migrate first`);
|
|
95
|
+
const args = ['db', 'push', '--include-all'];
|
|
96
|
+
const pending = pendingVersions(runCli(session, [...args, '--dry-run', ...targetArgs(session.target, ctx.cwd)]).text);
|
|
97
|
+
const stray = pending.filter((version) => !model.heldVersions.includes(version));
|
|
98
|
+
if (stray.length > 0) throw failure('DB_PUSH_STRAY', `the dry run lists more than the held-back migrations: ${pending.join(', ')}. Stopped.`);
|
|
99
|
+
if (pending.length > 0) runCli(session, [...args, '--yes', ...targetArgs(session.target, ctx.cwd)]);
|
|
100
|
+
return pending;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* `starci app db post-cutover`. options: { execute, linked, urlEnv, greenAt, ownerConfirms, query }. Dry run: remote read plus the read-only preflight.
|
|
105
|
+
* --execute: gate open and STARCI_DB_ARMED=1, then `db push --include-all` of exactly the held-back versions.
|
|
106
|
+
*/
|
|
107
|
+
export async function postCutoverDatabase(ctx, decl, options = {}) {
|
|
108
|
+
const model = buildModel(ctx.cwd, decl);
|
|
109
|
+
if (model.held.length === 0) return { ok: true, lines: ['no held-back migration in the repository. Nothing to do.'], data: { held: [], done: true, executed: false } };
|
|
110
|
+
if (decl.postCutover === null) throw refusal('DB_POSTCUTOVER_UNDECLARED', 'database.postCutover is not declared: the function preflight needs it (an empty mapping declares no family)');
|
|
111
|
+
const session = { ctx, decl, model, target: resolveTarget(ctx, decl, options) };
|
|
112
|
+
const remote = listRemote(session);
|
|
113
|
+
const data = { held: model.heldVersions, done: false, executed: false };
|
|
114
|
+
if (model.heldVersions.every((version) => remote.has(version))) return { ok: true, lines: [`held-back migrations ${model.held.join(', ')} are already applied. Nothing to do.`], data: { ...data, done: true } };
|
|
115
|
+
const gate = timeGate(ctx, decl.postCutover, options);
|
|
116
|
+
const preflight = await functionPreflight(session, options);
|
|
117
|
+
const lines = [`held-back migrations: ${model.held.join(', ')}`, preflight.line, gate.line, 'WARNING: after this step a rollback to the old app needs a database restore.'];
|
|
118
|
+
if (options.execute !== true) return { ok: true, lines: [...lines, 'dry run: only read-only calls were made; nothing applied.'], data: { ...data, gateOpen: gate.open, signatures: preflight.count } };
|
|
119
|
+
if (!gate.open) throw refusal('DB_POSTCUTOVER_GATE_CLOSED', 'the time gate is closed; nothing applied');
|
|
120
|
+
requireArmed(ctx);
|
|
121
|
+
const pending = applyHeld(session, remote);
|
|
122
|
+
if (!model.heldVersions.every((version) => listRemote(session).has(version))) throw failure('DB_HELD_MISSING', 'a held-back migration is still missing from the remote history');
|
|
123
|
+
return { ok: true, lines: [...lines, pending.length > 0 ? `applied: ${pending.join(', ')}` : 'nothing pending: already applied.'], data: { ...data, done: true, executed: true, pushed: pending, signatures: preflight.count } };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const SPEC = { boolean: ['--execute', '--json', '--linked', '--owner-confirms-old-app-gone'], value: ['--url-env', '--green-at'] };
|
|
127
|
+
|
|
128
|
+
/** Entry of the verb: `starci app db post-cutover [--execute] [--green-at <ISO> | --owner-confirms-old-app-gone] [--linked | --url-env NAME] [--json]`. */
|
|
129
|
+
export function dbPostCutoverMain(argv, io = {}) {
|
|
130
|
+
return runVerb({
|
|
131
|
+
label: 'starci app db post-cutover', name: 'db post-cutover', code: 'DB_USAGE', io, argv, spec: SPEC,
|
|
132
|
+
run: (ctx, flags) => postCutoverDatabase(ctx, loadDatabase(ctx.cwd), {
|
|
133
|
+
execute: flags.execute === true, linked: flags.linked === true, urlEnv: flags['url-env'], greenAt: flags['green-at'], ownerConfirms: flags['owner-confirms-old-app-gone'] === true,
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
}
|
package/emit/db-types.mjs
CHANGED
|
@@ -74,6 +74,15 @@ const withNpxFallback = (run, platform) => (file, args, options = {}) => {
|
|
|
74
74
|
/** The first meaningful line of a failure output, for the error a human acts on. */
|
|
75
75
|
const firstLine = (text) => String(text ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0];
|
|
76
76
|
|
|
77
|
+
const START_TAIL_LINES = 12;
|
|
78
|
+
const START_LINE_CHARS = 300;
|
|
79
|
+
/** The last lines (bounded) of a failed `supabase start`: its cause (42501 permission denied to alter role) sits after the first progress lines, so the first line alone hid it. */
|
|
80
|
+
const startDetail = (started) => {
|
|
81
|
+
const text = [started?.stderr, started?.stdout, started?.error?.message ?? started?.error].map((part) => String(part ?? '')).find((part) => part.trim()) ?? '';
|
|
82
|
+
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => (line.length > START_LINE_CHARS ? `${line.slice(0, START_LINE_CHARS)}...` : line));
|
|
83
|
+
return lines.length ? lines.slice(-START_TAIL_LINES).join(' | ') : 'no output';
|
|
84
|
+
};
|
|
85
|
+
|
|
77
86
|
/**
|
|
78
87
|
* The schemas `supabase/config.toml` exposes under [api] (`[]` when the file or the list is absent - the CLI then reads its
|
|
79
88
|
* own defaults). A config that does not parse is a refusal: L08 reads the same file and the emit must not guess.
|
|
@@ -127,7 +136,7 @@ export function generateDbTypes({ root, run = spawn, platform = process.platform
|
|
|
127
136
|
const started = execute(SUPABASE_CLI, ['start', '-x', 'studio,mailpit,logflare,vector,edge-runtime,imgproxy,supavisor'], { cwd: root });
|
|
128
137
|
const already = /already (?:running|started)/i.test(`${started?.stderr ?? ''}${started?.stdout ?? ''}`);
|
|
129
138
|
if (started?.error || (started?.status !== 0 && !already)) {
|
|
130
|
-
throw new DbTypesError(`\`${SUPABASE_CLI} start\` failed${started?.status != null ? ` (exit ${started.status})` : ''}: ${
|
|
139
|
+
throw new DbTypesError(`\`${SUPABASE_CLI} start\` failed${started?.status != null ? ` (exit ${started.status})` : ''}: ${startDetail(started)} (the last ${START_TAIL_LINES} output lines at most; with no cause among them, Docker must be running)`, { file: dbTypesPath });
|
|
131
140
|
}
|
|
132
141
|
try {
|
|
133
142
|
return emitDbTypesWithRunner({ root, run: execute });
|
package/lint/run.mjs
CHANGED
|
@@ -94,7 +94,7 @@ async function runLinter({ cwd, pkg, bin, args, bound = null }) {
|
|
|
94
94
|
// ESLint prints its report on stdout; stylelint uses stderr. JSON does not prove a completed child: only exits 0/1
|
|
95
95
|
// without a spawn error or signal are measurable, and exit 1 must carry findings.
|
|
96
96
|
let results;
|
|
97
|
-
try { results = JSON.parse(run.stdout.trim() || run.stderr); } catch { return { error: `${pkg} produced no json report in ${path.basename(cwd)}/ (exit ${run.status}): ${String(run.stderr || run.stdout).trim().split('\n')
|
|
97
|
+
try { results = JSON.parse(run.stdout.trim() || run.stderr); } catch { return { error: `${pkg} produced no json report in ${path.basename(cwd)}/ (exit ${run.status}): ${String(run.stderr || run.stdout).trim().split('\n').slice(0, 8).join(' | ')}` }; }
|
|
98
98
|
if (!Array.isArray(results)) return { error: `${pkg} produced a report that is not a result list` };
|
|
99
99
|
const completed = !run.error && !run.signal && [0, 1].includes(run.status)
|
|
100
100
|
&& (run.status === 0 || results.some((result) => result?.messages?.length || result?.warnings?.length));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starci/hfs",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "The StarCi app command implementation for scaffolding, linting, checking and synchronizing one product repository.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,9 @@
|
|
|
18
18
|
"LICENSE",
|
|
19
19
|
"src/main.mjs",
|
|
20
20
|
"emit/**",
|
|
21
|
+
"stack/**",
|
|
22
|
+
"release/**",
|
|
23
|
+
"db/**",
|
|
21
24
|
"lint/**",
|
|
22
25
|
"scaffold/**",
|
|
23
26
|
"report/**",
|