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