@starci/hfs 4.3.0 → 4.4.1
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 +14 -0
- package/README.md +1 -1
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/contracts.mjs +2 -2
- package/emit/db-types.mjs +12 -3
- package/lint/run.mjs +3 -3
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/report/sonar.mjs +1 -1
- package/runtime/config.example.yaml +7 -6
- package/runtime/engine/config.mjs +10 -18
- package/runtime/engine/removed-vocabulary.mjs +0 -2
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +2855 -163
- package/runtime/knowledge/hfs/slots.yaml +333 -62
- package/runtime/knowledge/patterns/be/api.yaml +32 -5
- package/runtime/knowledge/patterns/be/cli.yaml +14 -4
- 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 +23 -4
- package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3091 -43
- package/runtime/modules/kernel/removed-vocabulary.yaml +30 -0
- package/runtime/modules/models/runtimes.yaml +5 -7
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +13769 -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/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +4 -1
- 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 +42 -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 +1 -8
- package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/config.mjs +1 -1
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- 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/index.mjs +1 -1
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
- 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 +2 -2
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
- 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 +4 -4
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
- package/runtime/scripts/hfs/architecture.mjs +1 -1
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +46 -47
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/coverage-scope.mjs +1 -1
- 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 +69 -9
- package/runtime/scripts/hfs/param-names.mjs +1 -1
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +158 -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/cli.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +1 -1
- package/runtime/scripts/hfs/rules/contract.mjs +67 -6
- 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 +286 -7
- 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 +9 -0
- 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 +9 -8
- 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/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 +13 -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 +41 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +2 -2
- 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-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-init.mjs +127 -19
- package/runtime/scripts/hfs/secret-sops.mjs +38 -6
- package/runtime/scripts/hfs/secret-store.mjs +56 -54
- package/runtime/scripts/hfs/secret.mjs +34 -5
- package/runtime/scripts/hfs/slot-imports.mjs +18 -5
- package/runtime/scripts/hfs/slots.mjs +3 -2
- 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/typescript-programs.mjs +0 -8
- 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/env.mjs +2 -2
- package/runtime/scripts/lib/git.mjs +1 -1
- package/runtime/scripts/lib/i18n.mjs +0 -2
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/language.mjs +1 -1
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +2 -4
- 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 +5 -18
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +5 -2
- 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 +14 -6
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +1 -0
- package/scaffold/add-fe-page.mjs +1 -0
- package/scaffold/add-inbox-lite.mjs +74 -42
- package/scaffold/add-table.mjs +110 -87
- package/scaffold/add.mjs +159 -44
- package/scaffold/app.mjs +124 -39
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/scaffold/supabase-ports.mjs +1 -1
- package/src/main.mjs +229 -116
- 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/format.mjs +1 -1
- package/sync/hygiene.mjs +1 -1
- package/sync/index.mjs +110 -54
- 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 +3 -0
- package/templates/app/gitignore-lite +3 -0
- 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/.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 +178 -1
- 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/README.md +5 -3
- 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/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 +17 -18
- package/templates/be/inbox/database-client.ts.tpl +12 -11
- 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 +42 -9
- package/templates/be/inbox/service.ts.tpl +23 -8
- package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
- package/templates/be/inbox/webhook-door.ts.tpl +2 -2
- 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.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.module.ts +11 -9
- 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 +2 -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 +15 -1
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
- 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/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/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/app.module.ts +14 -18
- 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 +2 -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 +2 -3
- 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/index.mjs +54 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.4.1 - 2026-10-17
|
|
4
|
+
- Changed: the bundled runtime copies follow the runtime of 1.0.0-alpha.19 (owner ruling no-runtime-specs, 2026-10-17): the failure codes, the slot manifest (`exampleNa`) and the rule catalog (`specNa`, `exampleNa`) carry the removal of the runtime's own specs; package behavior is unchanged. The managed codecov.yml, the CI coverage upload and the hfs sync render stay (Codecov is kept for apps and examples); internal exports that only the deleted runtime specs read are no longer exported.
|
|
5
|
+
|
|
6
|
+
## 4.4.0 - 2026-10-15
|
|
7
|
+
- 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).
|
|
8
|
+
- 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({})`.
|
|
9
|
+
- 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.
|
|
10
|
+
|
|
3
11
|
## 4.3.0 - 2026-10-14
|
|
4
12
|
- 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.
|
|
5
13
|
- 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.
|
|
@@ -116,6 +124,12 @@
|
|
|
116
124
|
- 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.
|
|
117
125
|
- 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.
|
|
118
126
|
- 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.
|
|
127
|
+
## Unreleased (a18 R281)
|
|
128
|
+
|
|
129
|
+
- 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`.
|
|
130
|
+
- 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.
|
|
131
|
+
- 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.
|
|
132
|
+
|
|
119
133
|
## Unreleased (C0 batch)
|
|
120
134
|
|
|
121
135
|
- 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
|
@@ -185,7 +185,7 @@ The upload authenticates with GitHub's OIDC token: the managed `ci.yml` job has
|
|
|
185
185
|
editing a list), plus the catalog slice of every code `starci app check` can emit: its own and the machine's (`ARCHITECTURE_RULE_IDS`,
|
|
186
186
|
derived from the machine's rule id lists). After changing any of those files, `knowledge/hfs/slots.yaml`,
|
|
187
187
|
`knowledge/hfs/canon-pins.yaml`, `knowledge/patterns/fe/folder.yaml` or the catalog entries of those codes, run `starci release sync-runtime`;
|
|
188
|
-
`
|
|
188
|
+
`starci release sync-runtime --check` fails on a stale copy. Bump `version` here and in the pin when the behaviour changes.
|
|
189
189
|
|
|
190
190
|
The examples gate `starci runtime check --only example-architecture` runs `starci app lint` of this CLI at the root of every `examples/*`
|
|
191
191
|
app with an `hfs.json` (each `examples/<app>` root) and fails on any finding or any tool that could not run;
|
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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/contracts.mjs
CHANGED
|
@@ -41,7 +41,7 @@ const OPERATIONS_WORKER = path.join(path.dirname(fileURLToPath(import.meta.url))
|
|
|
41
41
|
export const appModulePath = (app) => `apps/${app}/src/app.module.ts`;
|
|
42
42
|
|
|
43
43
|
/** The repository-relative path of an app's snapshot. */
|
|
44
|
-
|
|
44
|
+
const snapshotPath = (app) => `contracts/${app}/schema.graphql`;
|
|
45
45
|
|
|
46
46
|
/** The `paths` aliases of a tsconfig as [{ prefix, targets }], `@modules/*` giving prefix `@modules/`. Pure. */
|
|
47
47
|
export function aliasesOf(paths, baseDir) {
|
|
@@ -57,7 +57,7 @@ export function aliasTarget(aliases, specifier) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
/** The api apps of a declaration that can serve GraphQL: kind `api`. Pure. */
|
|
60
|
-
|
|
60
|
+
const apiApps = (declaration) => (declaration.apps ?? []).filter((app) => app.kind === 'api').map((app) => app.name);
|
|
61
61
|
|
|
62
62
|
/** The text written to a snapshot file: the printed schema and one final newline. Pure. */
|
|
63
63
|
export const snapshotText = (printed) => `${printed.replace(/\n+$/, '')}\n`;
|
package/emit/db-types.mjs
CHANGED
|
@@ -22,13 +22,13 @@ export const dbTypesPath = 'supabase/types/database.types.ts';
|
|
|
22
22
|
|
|
23
23
|
/** The folder of the app's migrations, and the first line of the committed types: the digest of the migrations they were generated from. */
|
|
24
24
|
const MIGRATIONS_DIR = 'supabase/migrations';
|
|
25
|
-
|
|
25
|
+
const TYPES_STAMP = '// migrations-sha256: ';
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* The sha256 of the app's migrations, hex: every `supabase/migrations/*.sql` by name, each as name, NUL, its text with LF line ends,
|
|
29
29
|
* NUL. The offline codegen hook of a lite app (templates/app/codegen-lite) computes the same digest and refuses types stamped with another.
|
|
30
30
|
*/
|
|
31
|
-
|
|
31
|
+
function migrationsDigest(root) {
|
|
32
32
|
const dir = path.join(root, MIGRATIONS_DIR);
|
|
33
33
|
const hash = createHash('sha256');
|
|
34
34
|
for (const name of (fs.existsSync(dir) ? fs.readdirSync(dir) : []).filter((file) => file.endsWith('.sql')).sort()) {
|
|
@@ -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
|
@@ -63,8 +63,8 @@ export function parseLintArgs(argv) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/** The typed ESLint run reads nothing above the app root: its TypeScript `sys` is bound by this preload (3.3, bound-sys.cjs). */
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
const BOUND_ENV = 'STARCI_LINT_BOUND';
|
|
67
|
+
const linterBoundArgs = () => ['--require', fileURLToPath(new URL('./bound-sys.cjs', import.meta.url))];
|
|
68
68
|
|
|
69
69
|
/** The child `node` ran with `args` from `cwd`: its exit status, signal, spawn error and the text of both streams, once it closes. */
|
|
70
70
|
function runNode(args, options) {
|
|
@@ -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));
|