@starci/hfs 4.2.17 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +7 -0
- package/db/migrate.mjs +361 -0
- package/db/post-cutover.mjs +136 -0
- package/emit/db-types.mjs +10 -1
- package/lint/run.mjs +1 -1
- package/package.json +4 -1
- package/release/gate.mjs +298 -0
- package/release/index.mjs +103 -0
- package/release/push.mjs +110 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
- package/runtime/knowledge/hfs/rules.yaml +3437 -160
- package/runtime/knowledge/hfs/slots.yaml +430 -92
- package/runtime/knowledge/patterns/be/api.yaml +92 -2
- package/runtime/knowledge/patterns/be/cli.yaml +37 -9
- package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
- package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
- package/runtime/knowledge/patterns/be/projections.yaml +4 -2
- package/runtime/knowledge/patterns/be/queues.yaml +11 -3
- package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
- package/runtime/knowledge/patterns/be/saga.yaml +20 -3
- package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
- package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/failure-codes.yaml +3128 -47
- package/runtime/modules/models/runtimes.yaml +1 -1
- package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
- package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
- package/runtime/packages/cli/src/validate-args.mjs +213 -0
- package/runtime/scripts/api/git/add.mjs +5 -0
- package/runtime/scripts/api/git/cat-file.mjs +5 -0
- package/runtime/scripts/api/git/init.mjs +7 -0
- package/runtime/scripts/api/git/lib.mjs +3 -0
- package/runtime/scripts/api/git/tag.mjs +5 -0
- package/runtime/scripts/api/node/lib.mjs +20 -0
- package/runtime/scripts/api/node/run-node.mjs +6 -0
- package/runtime/scripts/api/sops/lib.mjs +41 -0
- package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
- package/runtime/scripts/api/sops/seal.mjs +1 -1
- package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
- package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
- package/runtime/scripts/api/supabase/database-query.mjs +5 -0
- package/runtime/scripts/api/supabase/lib.mjs +44 -0
- package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
- package/runtime/scripts/hfs/app-wiring.mjs +201 -0
- package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
- package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
- package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
- package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
- package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
- package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
- package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
- package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
- package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
- package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
- package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
- package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
- package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
- package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
- package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
- package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
- package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
- package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
- package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
- package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
- package/runtime/scripts/hfs/check-results.mjs +28 -0
- package/runtime/scripts/hfs/check.mjs +63 -72
- package/runtime/scripts/hfs/coverage-from.mjs +70 -0
- package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
- package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
- package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
- package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
- package/runtime/scripts/hfs/doc-commands.mjs +367 -0
- package/runtime/scripts/hfs/edition-slots.mjs +2 -2
- package/runtime/scripts/hfs/example-secrets.mjs +158 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
- package/runtime/scripts/hfs/path-findings.mjs +32 -2
- package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
- package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
- package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
- package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
- package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
- package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
- package/runtime/scripts/hfs/rules/assets.mjs +79 -0
- package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
- package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
- package/runtime/scripts/hfs/rules/contract.mjs +66 -5
- package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
- package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
- package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
- package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
- package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
- package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
- package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
- package/runtime/scripts/hfs/rules/database.mjs +11 -2
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
- package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
- package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
- package/runtime/scripts/hfs/rules/docker.mjs +24 -7
- package/runtime/scripts/hfs/rules/edition.mjs +11 -10
- package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
- package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
- package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
- package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
- package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
- package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
- package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
- package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
- package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
- package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
- package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
- package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
- package/runtime/scripts/hfs/rules/saga.mjs +8 -16
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
- package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
- package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
- package/runtime/scripts/hfs/rules/services.mjs +4 -4
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
- package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
- package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
- package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
- package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
- package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
- package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
- package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
- package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
- package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
- package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
- package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
- package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
- package/runtime/scripts/hfs/sealed-members.mjs +101 -0
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
- package/runtime/scripts/hfs/secret-document.mjs +97 -0
- package/runtime/scripts/hfs/secret-edit.mjs +170 -0
- package/runtime/scripts/hfs/secret-errors.mjs +20 -0
- package/runtime/scripts/hfs/secret-files.mjs +124 -0
- package/runtime/scripts/hfs/secret-init.mjs +300 -0
- package/runtime/scripts/hfs/secret-output.mjs +100 -0
- package/runtime/scripts/hfs/secret-sops.mjs +130 -0
- package/runtime/scripts/hfs/secret-store.mjs +163 -0
- package/runtime/scripts/hfs/secret.mjs +162 -193
- package/runtime/scripts/hfs/slot-imports.mjs +18 -3
- package/runtime/scripts/hfs/slot-required.mjs +13 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
- package/runtime/scripts/hfs/slots.mjs +1 -0
- package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
- package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
- package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
- package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
- package/runtime/scripts/hfs/tree-findings.mjs +22 -0
- package/runtime/scripts/lib/command-tokens.mjs +126 -0
- package/runtime/scripts/lib/compose-values.mjs +11 -0
- package/runtime/scripts/lib/deployment-findings.mjs +7 -0
- package/runtime/scripts/lib/json-clone.mjs +61 -0
- package/runtime/scripts/lib/list.mjs +4 -0
- package/runtime/scripts/lib/roots.mjs +59 -0
- package/runtime/scripts/lib/runtime-example.mjs +13 -0
- package/runtime/scripts/lib/secret-patterns.mjs +60 -5
- package/runtime/scripts/lib/sql-ast.mjs +32 -0
- package/runtime/scripts/lib/stack-services.mjs +1 -1
- package/runtime/scripts/lib/swarm-stack.mjs +126 -0
- package/runtime/scripts/lib/trim-end.mjs +8 -0
- package/runtime/scripts/lib/ts-analysis.mjs +23 -0
- package/runtime/scripts/lib/ts-ast.mjs +27 -2
- package/runtime/scripts/lib/workflow-events.mjs +8 -0
- package/scaffold/add-app.mjs +77 -59
- package/scaffold/add-cli-lite.mjs +24 -17
- package/scaffold/add-compose.mjs +79 -0
- package/scaffold/add-database-lite.mjs +14 -0
- package/scaffold/add-fe-overlay.mjs +122 -0
- package/scaffold/add-fe-page.mjs +269 -0
- package/scaffold/add-inbox-lite.mjs +87 -124
- package/scaffold/add-plan.mjs +183 -0
- package/scaffold/add-table.mjs +132 -70
- package/scaffold/add.mjs +186 -204
- package/scaffold/app.mjs +128 -43
- package/scaffold/edition-gate.mjs +3 -3
- package/scaffold/image.mjs +2 -2
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +2 -2
- package/src/main.mjs +238 -119
- package/stack/age-install.mjs +37 -0
- package/stack/backup.mjs +100 -0
- package/stack/build-args.mjs +42 -0
- package/stack/build.mjs +173 -0
- package/stack/cli.mjs +130 -0
- package/stack/config.mjs +57 -0
- package/stack/context.mjs +57 -0
- package/stack/converge.mjs +107 -0
- package/stack/deploy.mjs +301 -0
- package/stack/digest.mjs +16 -0
- package/stack/docker-transport.mjs +53 -0
- package/stack/edge-alias.mjs +118 -0
- package/stack/errors.mjs +21 -0
- package/stack/image-scan.mjs +152 -0
- package/stack/ingress-create.mjs +114 -0
- package/stack/ingress-publish.mjs +273 -0
- package/stack/ingress.mjs +706 -0
- package/stack/jobs.mjs +51 -0
- package/stack/live-restore.mjs +177 -0
- package/stack/prepare.mjs +266 -0
- package/stack/private-file.mjs +22 -0
- package/stack/provision.mjs +90 -0
- package/stack/prune.mjs +46 -0
- package/stack/release-store.mjs +87 -0
- package/stack/remote.mjs +205 -0
- package/stack/render.mjs +149 -0
- package/stack/retire.mjs +47 -0
- package/stack/sealed.mjs +149 -0
- package/stack/task.mjs +42 -0
- package/stack/verify.mjs +99 -0
- package/sync/ci-commands.mjs +13 -0
- package/sync/index.mjs +127 -64
- package/sync/stack-images.mjs +127 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
- package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
- package/templates/app/codegen-graphql.mjs.partial +13 -2
- package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
- package/templates/app/codegen-prelude.mjs.partial +8 -0
- package/templates/app/gitignore +4 -0
- package/templates/app/gitignore-lite +4 -0
- package/templates/app/hooks/husky/pre-commit +7 -12
- package/templates/app/package-scripts/package.json +1 -0
- package/templates/app/package-scripts/supabase.json +7 -0
- package/templates/app/package-scripts-lite/package.json +1 -7
- package/templates/app/skeleton/.editorconfig +1 -0
- package/templates/app/skeleton/.gitattributes +7 -1
- package/templates/app/skeleton/.github/dependabot.yml +17 -0
- package/templates/app/skeleton/.nvmrc +1 -1
- package/templates/app/skeleton/.sops.yaml +1 -1
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
- package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
- package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
- package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
- package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
- package/templates/app/skeleton/README.md +6 -6
- package/templates/app/skeleton/scripts/codegen.mjs +1 -2
- package/templates/app/skeleton-lite/.editorconfig +16 -0
- package/templates/app/skeleton-lite/.gitattributes +8 -0
- package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
- package/templates/app/skeleton-lite/.nvmrc +1 -0
- package/templates/app/skeleton-lite/.sops.yaml +3 -2
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
- package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
- package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
- package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton-lite/README.md +8 -4
- package/templates/app/skeleton-lite/supabase/config.toml +8 -1
- package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
- package/templates/app/starciwork.gitignore-lite +6 -0
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
- package/templates/be/inbox/database-client.ts.tpl +14 -28
- package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
- package/templates/be/inbox/event-index.ts.tpl +1 -0
- package/templates/be/inbox/events.json.tpl +10 -0
- package/templates/be/inbox/migration.sql.tpl +1 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
- package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
- package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
- package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
- package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
- package/templates/be/inbox/platform/migration.sql.tpl +48 -0
- package/templates/be/inbox/service.spec.ts.tpl +49 -20
- package/templates/be/inbox/service.ts.tpl +27 -16
- package/templates/be/inbox/sql.ts.tpl +2 -2
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
- package/templates/be/inbox/webhook-door.ts.tpl +4 -3
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
- package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
- package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
- package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
- package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
- package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
- package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
- package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
- package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
- package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
- package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
- package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
- package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
- package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
- package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
- package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
- package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
- package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
- package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
- package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
- package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
- package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
- package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
- package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
- package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
- package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
- package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
- package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
- package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
- package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
- package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
- package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
- package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
- package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
- package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
- package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
- package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
- package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
- package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
- package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
- package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
- package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
- package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
- package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
- package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
- package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
- package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
- package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/feature.module.ts.tpl +2 -1
- package/templates/be/table/api/feature.options.ts.tpl +2 -0
- package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
- package/templates/be/table/api/graphql.module.ts.tpl +3 -2
- package/templates/be/table/api/graphql.options.ts.tpl +2 -0
- package/templates/be/table/migration.sql.tpl +1 -0
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
- package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
- package/templates/fe/auth/supabase-client.ts.tpl +34 -0
- package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
- package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
- package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
- package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
- package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
- package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
- package/upgrade/check.mjs +3 -1
- package/upgrade/file-state.mjs +68 -0
- package/upgrade/index.mjs +58 -94
- package/runtime/scripts/api/git/log.mjs +0 -5
- package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// app db restamp: an explicit version map previews file renames and history repairs.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
5
|
+
import { invocationDir } from '../lib/roots.mjs';
|
|
6
|
+
import { migrationStamp } from './rules/database-migrations.mjs';
|
|
7
|
+
import { MIGRATIONS_DIR, MIGRATIONS_ARCHIVE_DIR } from './rules/database-constants.mjs';
|
|
8
|
+
import { DbVerbError, dbOptions, dbAppRoot, dbSqlFiles, runDbSupabase, repairArgs, dbFailure } from './db-baseline-verb.mjs';
|
|
9
|
+
|
|
10
|
+
/** YAML maps use quoted UTC stamps as keys and values (or a versions: mapping). */
|
|
11
|
+
function restampPlan(root, mapFile) {
|
|
12
|
+
if (!mapFile) throw new DbVerbError('restamp requires --map <restamp.yaml>');
|
|
13
|
+
let document;
|
|
14
|
+
try { document = parseYaml(fs.readFileSync(path.resolve(root, mapFile), 'utf8')); }
|
|
15
|
+
catch { throw new DbVerbError('restamp map must be readable YAML with unique version keys'); }
|
|
16
|
+
const mapping = document?.versions ?? document;
|
|
17
|
+
if (!mapping || typeof mapping !== 'object' || Array.isArray(mapping) || !Object.keys(mapping).length) throw new DbVerbError('restamp map must contain old-to-new version pairs');
|
|
18
|
+
const active = dbSqlFiles(root);
|
|
19
|
+
const archived = dbSqlFiles(root, MIGRATIONS_ARCHIVE_DIR);
|
|
20
|
+
const occupied = new Set([...active, ...archived].map((entry) => entry.stamp));
|
|
21
|
+
const targets = new Set();
|
|
22
|
+
const steps = Object.entries(mapping).map(([old, raw]) => {
|
|
23
|
+
const next = String(raw);
|
|
24
|
+
if (![old, next].every((stamp) => /^\d{14}$/.test(stamp) && migrationStamp(stamp) !== null) || old === next) throw new DbVerbError('restamp pairs require distinct valid UTC versions');
|
|
25
|
+
const sources = active.filter((entry) => entry.stamp === old);
|
|
26
|
+
if (sources.length !== 1) throw new DbVerbError('each restamp source must identify exactly one active migration; archives never change');
|
|
27
|
+
if (occupied.has(next) || targets.has(next)) throw new DbVerbError('restamp targets must be unique unoccupied versions');
|
|
28
|
+
targets.add(next);
|
|
29
|
+
const source = sources[0];
|
|
30
|
+
const name = source.name.replace(old, next);
|
|
31
|
+
return { old, next, source: source.file, target: `${MIGRATIONS_DIR}/${name}` };
|
|
32
|
+
});
|
|
33
|
+
const replacements = new Map(steps.map((step) => [step.old, step.next]));
|
|
34
|
+
const resulting = active.map((entry) => replacements.get(entry.stamp) ?? entry.stamp);
|
|
35
|
+
if (resulting.some((stamp, index) => index > 0 && stamp <= resulting[index - 1])) throw new DbVerbError('the map must cover every version needed to preserve migration order');
|
|
36
|
+
return steps.sort((left, right) => left.old < right.old ? -1 : 1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The complete map is validated before any repair or file mutation. */
|
|
40
|
+
export function dbRestampMain(argv, { cwd = invocationDir(), stdout = (text) => process.stdout.write(text), stderr = (text) => process.stderr.write(text), ...ports } = {}) {
|
|
41
|
+
try {
|
|
42
|
+
const opts = dbOptions(argv, ['cwd', 'map', 'apply', 'local']);
|
|
43
|
+
const root = dbAppRoot(cwd, opts);
|
|
44
|
+
const steps = restampPlan(root, opts.map);
|
|
45
|
+
const repairs = steps.flatMap((step) => [repairArgs(step.old, 'reverted', opts.local), repairArgs(step.next, 'applied', opts.local)]);
|
|
46
|
+
stdout(`${opts.apply ? 'apply' : 'preview'} restamp ${steps.length} migrations\n`);
|
|
47
|
+
for (const step of steps) stdout(`${step.source} -> ${step.target}\n`);
|
|
48
|
+
for (const args of repairs) stdout(`supabase ${args.join(' ')}\n`);
|
|
49
|
+
if (!opts.apply) return 0;
|
|
50
|
+
// CLI failures never echo credentials and leave the local files intact for an explicit retry.
|
|
51
|
+
for (const args of repairs) runDbSupabase(root, args, ports);
|
|
52
|
+
for (const step of steps) {
|
|
53
|
+
const source = path.join(root, step.source);
|
|
54
|
+
const text = fs.readFileSync(source, 'utf8').replace(/^\s*--\s*starci:restamp\s+\d{14}\s+\d{14}\s*\r?\n/, '');
|
|
55
|
+
fs.writeFileSync(path.join(root, step.target), `-- starci:restamp ${step.old} ${step.next}\n${text}`, { flag: 'wx' });
|
|
56
|
+
fs.unlinkSync(source);
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
} catch (error) { return dbFailure('restamp', error, stderr); }
|
|
60
|
+
}
|
|
@@ -60,26 +60,62 @@ function connectionProblems(side, value) {
|
|
|
60
60
|
/** Whether a declared list is present but not a list of distinct entries that all satisfy `valid`. */
|
|
61
61
|
const isBadUniqueList = (list, valid) => list !== undefined && (!Array.isArray(list) || !list.every((entry) => valid(entry)) || new Set(list).size !== list.length);
|
|
62
62
|
|
|
63
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
|
|
64
|
+
|
|
65
|
+
function validExtraPackage(entry) {
|
|
66
|
+
return isPlainObject(entry) && Object.keys(entry).every(key => key === 'name' || key === 'reason')
|
|
67
|
+
&& typeof entry.name === 'string' && entry.name.length <= 214 && PACKAGE_NAME.test(entry.name)
|
|
68
|
+
&& typeof entry.reason === 'string' && entry.reason.trim().length > 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function extraPackageProblems(side, entries) {
|
|
72
|
+
if (entries === undefined) return [];
|
|
73
|
+
if (side !== 'fe') return ['extraPackages belong to the fe side'];
|
|
74
|
+
if (!Array.isArray(entries) || !entries.every(validExtraPackage)) return ['sides.fe.extraPackages must be a list of {name, reason} with a package name and a nonempty reason'];
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
|
|
63
78
|
function declarationSideProblems(side, value, names) {
|
|
64
79
|
const bad = [];
|
|
65
80
|
const at = `sides.${side}`;
|
|
66
81
|
if (!isPlainObject(value)) return [`${at} must be an object`];
|
|
67
|
-
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
82
|
+
for (const key of Object.keys(value)) if (!['apps', 'optionalSlots', 'patterns', 'kinds', 'connections', 'reads', 'extraPackages'].includes(key)) bad.push(`${at} has unknown key ${key}`);
|
|
68
83
|
bad.push(...appShapeProblems(value.apps, at, side, names));
|
|
69
84
|
if (isBadUniqueList(value.optionalSlots, (slot) => SLOT_ID.test(String(slot)))) bad.push(`${at}.optionalSlots must be a unique list of slot ids`);
|
|
70
85
|
bad.push(...patternShapeProblems(value, at, NAME), ...kindShapeProblems(value, at, NAME));
|
|
71
86
|
if (isBadUniqueList(value.reads, (read) => typeof read === 'string' && read.length > 0)) bad.push(`${at}.reads must be a unique list of paths`);
|
|
72
87
|
if (value.connections !== undefined) bad.push(...connectionProblems(side, value));
|
|
88
|
+
bad.push(...extraPackageProblems(side, value.extraPackages));
|
|
89
|
+
return bad;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Product vocabulary is a declaration, never a switch for a rule. */
|
|
93
|
+
function lexiconBlockProblems(lexicon) {
|
|
94
|
+
if (lexicon === undefined) return [];
|
|
95
|
+
if (!isPlainObject(lexicon)) return ['lexicon must be an object'];
|
|
96
|
+
const bad = [];
|
|
97
|
+
for (const key of Object.keys(lexicon)) if (key !== 'removed') bad.push(`lexicon has unknown key ${key}`);
|
|
98
|
+
if (isBadUniqueList(lexicon.removed, (word) => typeof word === 'string' && word.trim().length > 0)) bad.push('lexicon.removed must be a unique list of nonempty words');
|
|
99
|
+
return bad;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Front-end error reporting remains optional in both editions. */
|
|
103
|
+
function observeBlockProblems(observe) {
|
|
104
|
+
if (observe === undefined) return [];
|
|
105
|
+
if (!isPlainObject(observe)) return ['observe must be an object'];
|
|
106
|
+
const bad = [];
|
|
107
|
+
for (const key of Object.keys(observe)) if (key !== 'reporter') bad.push(`observe has unknown key ${key}`);
|
|
108
|
+
if (observe.reporter !== undefined && (typeof observe.reporter !== 'string' || !observe.reporter.trim())) bad.push('observe.reporter must name the optional error reporter');
|
|
73
109
|
return bad;
|
|
74
110
|
}
|
|
75
111
|
|
|
76
|
-
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser']);
|
|
112
|
+
const DECLARATION_KEYS = new Set(['hfs', 'kind', 'project', 'edition', 'supabase', 'sides', 'browser', 'lexicon', 'observe']);
|
|
77
113
|
const RUNTIME_DECLARATION_KEYS = new Set(['hfs', 'kind', 'project']);
|
|
78
114
|
|
|
79
115
|
function productDeclarationProblems(declaration, profiles) {
|
|
80
116
|
const bad = [];
|
|
81
117
|
for (const key of Object.keys(declaration)) if (!DECLARATION_KEYS.has(key)) bad.push(`unknown key ${key}`);
|
|
82
|
-
bad.push(...supabaseBlockProblems(declaration.supabase));
|
|
118
|
+
bad.push(...supabaseBlockProblems(declaration.supabase), ...lexiconBlockProblems(declaration.lexicon), ...observeBlockProblems(declaration.observe));
|
|
83
119
|
if (declaration.browser !== undefined && declaration.browser !== true) bad.push('browser is `true` when the app has a browser journey (slot app.browser), and is left out otherwise');
|
|
84
120
|
if (declaration.kind !== APP_KIND) bad.push(`kind must be ${APP_KIND} (a product is one app repository with a be and an fe side) or ${RUNTIME_KIND} (the StarCi runtime repository)`);
|
|
85
121
|
if (!isPlainObject(declaration.sides) || Object.keys(declaration.sides).sort(byCodeUnit).join() !== profiles.join()) { bad.push('sides must declare exactly be and fe'); return bad; }
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { validateArgs } from '../../packages/cli/src/validate-args.mjs';
|
|
2
|
+
import { isAlternation, isPlaceholder, tokenise } from '../lib/command-tokens.mjs';
|
|
3
|
+
|
|
4
|
+
// The one owner of "a documented command exists": it finds every `starci <group> <verb> ...` a text shows to an agent or owner
|
|
5
|
+
// (extractCommands) and judges it the way the dispatcher would (checkCommand): group and verb exist, every flag is declared, enum
|
|
6
|
+
// values are valid, required flags and positionals are present unless the text is a fragment (a bare verb, a line that ends in an
|
|
7
|
+
// ellipsis, a flag mentioned on its own). The prose-commands self-check (check-prose-commands.mjs, R230) and the doc-command specs
|
|
8
|
+
// are callers: they choose the files and the reasons they enforce, never a second extractor.
|
|
9
|
+
|
|
10
|
+
const IDENTIFIER = /^[a-z][a-z0-9-]*$/;
|
|
11
|
+
// a sentence that quotes a command only to say it is gone is not an instruction to run it
|
|
12
|
+
const RETIRED_SENTENCE = /^[^.;]*?\b(?:is|was|are|were)\s+(?:now\s+)?(?:gone|removed|retired|dropped)\b/;
|
|
13
|
+
// `starci history guard: refused ...`: a log label names no command
|
|
14
|
+
const LOG_LABEL = /^[\w-]+(?:\s+[\w-]+)?:(?:\s|$)/;
|
|
15
|
+
const ASKED = /\b(?:run|Run|execute|Execute)\s*$/;
|
|
16
|
+
const LIST_BULLET = /^\s*(?:[-*>$]\s*)*$/;
|
|
17
|
+
const NOT_A_COMMAND_HEAD = new Set(['help', '--help', '-h', '--version', 'completion']);
|
|
18
|
+
const SEPARATORS = new Set(['|', '||', '&&', ';', '>', '>>', '<', '&', '2>', '2>&1', '#', '\\', 'then', 'do', '-', '—', '–', '→', '←', '=>', '->', '-->', '=', '+', '/', 'and', 'or', 'to']);
|
|
19
|
+
const WORD_VALUE = /^[\w-]+$/;
|
|
20
|
+
const PROMPT_ASSIGNMENT = /^[A-Za-z_]+=\S*\s+/;
|
|
21
|
+
const OPERATORS = ['&&', '||', ';', '|', '('];
|
|
22
|
+
const KEYWORDS = ['then', 'do', 'sudo', 'exec'];
|
|
23
|
+
|
|
24
|
+
// a flag family (`--until-*`, `--until-<type>`, `--until-...`) names no single flag
|
|
25
|
+
const isEllipsis = (t) => t === '...' || t === '…' || t.endsWith('...') || /^--[\w-]*(-\*|-<[^>]*>|-)$/.test(t) || /^[<[]?(options?|flags?|args?|arguments?|rest|more|etc)[>\]]?$/i.test(t);
|
|
26
|
+
const isOpaque = (t) => isPlaceholder(t) || isEllipsis(t);
|
|
27
|
+
const alternatives = (t) => (!/[<{$]/.test(t) && (t.includes('|') || (/^[a-z]/.test(t) && t.includes('/'))) ? t.split(/[|/]/) : [t]);
|
|
28
|
+
const inner = (t) => t.replace(/^<|>$/g, '');
|
|
29
|
+
|
|
30
|
+
// ---- extraction -----------------------------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const COMMAND_AT = /(?<![\w@./:-])(?:npx\s+starci|npm\s+run\s+starci(?:\s+--silent)?(?:\s+--)?|starci)(?![\w@./-])(?=\s|$)/g;
|
|
33
|
+
const FENCE = /^\s*(?:[-*>|]\s*)?```/;
|
|
34
|
+
const SCRIPT_COMMENT = /^\s*(?:\/\/|\*|\/\*)/;
|
|
35
|
+
const CONTINUED = /\\\s*$/;
|
|
36
|
+
const SCRIPT_LITERAL = /(['"`])((?:\\.|(?!\1).)*)\1/g;
|
|
37
|
+
const SPELLING_FIELD = /\bspelling:\s*$/;
|
|
38
|
+
|
|
39
|
+
// the text before a `starci` is a shell prompt and environment assignments only
|
|
40
|
+
function startsAtPrompt(before) {
|
|
41
|
+
let rest = before.trimStart();
|
|
42
|
+
if (rest.startsWith('PS>')) rest = rest.slice(3).trimStart();
|
|
43
|
+
else if (rest.startsWith('$') || rest.startsWith('>')) rest = rest.slice(1).trimStart();
|
|
44
|
+
while (PROMPT_ASSIGNMENT.test(rest)) rest = rest.replace(PROMPT_ASSIGNMENT, '');
|
|
45
|
+
return rest === '';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const isWordChar = (ch) => ch !== undefined && /\w/.test(ch);
|
|
49
|
+
|
|
50
|
+
// the text before a `starci` ends in a shell operator or a command keyword
|
|
51
|
+
function endsAtOperator(before) {
|
|
52
|
+
const head = before.trimEnd();
|
|
53
|
+
if (OPERATORS.some((op) => head.endsWith(op))) return true;
|
|
54
|
+
return KEYWORDS.some((word) => head.endsWith(word) && !isWordChar(head[head.length - word.length - 1]));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const isCommandPosition = (before) => startsAtPrompt(before) || endsAtOperator(before);
|
|
58
|
+
|
|
59
|
+
// {body, end} of the code span that opens at `from` (a run of backticks closed by a run of the same length), or null
|
|
60
|
+
function spanAt(line, from) {
|
|
61
|
+
let run = 0;
|
|
62
|
+
while (line[from + run] === '`') run += 1;
|
|
63
|
+
for (let n = run; n >= 1; n -= 1) {
|
|
64
|
+
const close = line.indexOf('`'.repeat(n), from + n);
|
|
65
|
+
if (close > from + n) return { body: line.slice(from + n, close), end: close + n };
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function spansOf(line) {
|
|
71
|
+
const spans = [];
|
|
72
|
+
let masked = '';
|
|
73
|
+
let i = 0;
|
|
74
|
+
while (i < line.length) {
|
|
75
|
+
const span = line[i] === '`' ? spanAt(line, i) : null;
|
|
76
|
+
if (span === null) {
|
|
77
|
+
masked += line[i];
|
|
78
|
+
i += 1;
|
|
79
|
+
} else {
|
|
80
|
+
spans.push(span.body);
|
|
81
|
+
masked += ' '.repeat(span.end - i);
|
|
82
|
+
i = span.end;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { spans, masked };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Escapes of the host language (JSON/YAML/JS string literals) are not part of the command.
|
|
89
|
+
const unescape = (s) => s.replace(/\\[nt]/g, ' ').replace(/\\(["'`])/g, '$1').replace(/\\$/, '');
|
|
90
|
+
|
|
91
|
+
// the line without its trailing ` # comment`
|
|
92
|
+
function withoutComment(code) {
|
|
93
|
+
for (let i = code.indexOf('#'); i >= 0; i = code.indexOf('#', i + 1)) {
|
|
94
|
+
if (i > 0 && /\s/.test(code[i - 1]) && /\s/.test(code[i + 1] ?? '')) {
|
|
95
|
+
let start = i - 1;
|
|
96
|
+
while (start > 0 && /\s/.test(code[start - 1])) start -= 1;
|
|
97
|
+
return code.slice(0, start);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return code;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// every `starci` of `body` as an occurrence {line, mode, preceded, text}; `modeOf(before)` names the mode
|
|
104
|
+
function occurrencesIn(body, line, modeOf, scan = body) {
|
|
105
|
+
return [...scan.matchAll(COMMAND_AT)].map((m) => {
|
|
106
|
+
const before = body.slice(0, m.index);
|
|
107
|
+
return { line, mode: modeOf(before), preceded: before, text: unescape(body.slice(m.index + m[0].length)) };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// a fenced line is a command where starci stands in command position; elsewhere it is a diagram or a sentence
|
|
112
|
+
const fencedMode = (before) => (isCommandPosition(before) ? 'bounded' : 'prose');
|
|
113
|
+
|
|
114
|
+
// the fenced line at `index` with its `\`-continued lines joined: {code, last}
|
|
115
|
+
function joinContinuations(lines, index) {
|
|
116
|
+
let code = lines[index];
|
|
117
|
+
let last = index;
|
|
118
|
+
while (CONTINUED.test(code) && last + 1 < lines.length && !/^\s*```/.test(lines[last + 1])) {
|
|
119
|
+
last += 1;
|
|
120
|
+
code = `${code.replace(CONTINUED, ' ')}${lines[last].trimStart()}`;
|
|
121
|
+
}
|
|
122
|
+
return { code, last };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// the string literals of a script line, a retired spelling (`spelling: '...'`) kept so the dispatcher can refuse it excluded
|
|
126
|
+
const scriptLiterals = (line) => [...line.matchAll(SCRIPT_LITERAL)].filter((m) => !SPELLING_FIELD.test(line.slice(0, m.index))).map((m) => m[2]);
|
|
127
|
+
|
|
128
|
+
function lineOccurrences(line, lineNo, kind) {
|
|
129
|
+
const found = [];
|
|
130
|
+
for (const segment of kind === 'script' ? scriptLiterals(line) : [line]) {
|
|
131
|
+
const { spans, masked } = spansOf(segment);
|
|
132
|
+
for (const span of spans) found.push(...occurrencesIn(span, lineNo, () => 'bounded'));
|
|
133
|
+
found.push(...occurrencesIn(masked, lineNo, () => 'prose'));
|
|
134
|
+
}
|
|
135
|
+
return found;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Every starci command occurrence in one text.
|
|
140
|
+
* `bounded` occurrences (inside a code span, a fenced line, or a script string literal span) run to the end of that
|
|
141
|
+
* span; `prose` occurrences inside running text only keep the flags and values that follow the verb.
|
|
142
|
+
*/
|
|
143
|
+
export function extractCommands(text, { kind = 'text', skipLine = null } = {}) {
|
|
144
|
+
const found = [];
|
|
145
|
+
const lines = text.split(/\r?\n/);
|
|
146
|
+
let fenced = false;
|
|
147
|
+
let index = 0;
|
|
148
|
+
while (index < lines.length) {
|
|
149
|
+
const line = lines[index];
|
|
150
|
+
if (kind !== 'script' && FENCE.test(line)) fenced = !fenced;
|
|
151
|
+
else if (!skipLine?.(line) && fenced) {
|
|
152
|
+
const { code, last } = joinContinuations(lines, index);
|
|
153
|
+
found.push(...occurrencesIn(code, index + 1, fencedMode, withoutComment(code)));
|
|
154
|
+
index = last;
|
|
155
|
+
} else if (!skipLine?.(line) && (kind !== 'script' || !SCRIPT_COMMENT.test(line))) {
|
|
156
|
+
found.push(...lineOccurrences(line, index + 1, kind));
|
|
157
|
+
}
|
|
158
|
+
index += 1;
|
|
159
|
+
}
|
|
160
|
+
return found;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---- validation -----------------------------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
const sampleFor = (flag) => {
|
|
166
|
+
if (flag.type === 'enum') return flag.enum[0];
|
|
167
|
+
if (flag.type === 'number') return '1';
|
|
168
|
+
if (flag.type === 'boolean') return 'true';
|
|
169
|
+
return 'x';
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const globalFlagsBefore = (tokens, globals) => {
|
|
173
|
+
let i = 0;
|
|
174
|
+
while (i < tokens.length && tokens[i].text.startsWith('--')) {
|
|
175
|
+
const g = globals.get(tokens[i].text.slice(2).split('=')[0]);
|
|
176
|
+
if (!g) break;
|
|
177
|
+
i += g.type === 'boolean' || tokens[i].text.includes('=') ? 1 : 2;
|
|
178
|
+
}
|
|
179
|
+
return i;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
function commandTokens(occurrence) {
|
|
183
|
+
const tokens = tokenise(occurrence.text.split(/\s{3,}/)[0].trim());
|
|
184
|
+
const cut = tokens.findIndex((t) => SEPARATORS.has(t.text) || /^\d?>/.test(t.text) || t.text === 'starci');
|
|
185
|
+
return cut >= 0 ? tokens.slice(0, cut) : tokens;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const isAsked = (occurrence, strictProse) => strictProse || ASKED.test(occurrence.preceded) || LIST_BULLET.test(occurrence.preceded);
|
|
189
|
+
|
|
190
|
+
// the findings of the group word(s) when they end the check ([] ends it silently), or null when the verb is next
|
|
191
|
+
function groupStop(occurrence, groupAlts, unknownGroups, lenient) {
|
|
192
|
+
if (!groupAlts.every((g) => IDENTIFIER.test(g))) return [];
|
|
193
|
+
if (unknownGroups.length > 0 && (lenient || LOG_LABEL.test(occurrence.text.trim()))) return [];
|
|
194
|
+
return unknownGroups.length > 0 ? unknownGroups.map((g) => `unknown group "${g}"`) : null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// {stop: findings} when the occurrence ends before its flags, else the resolved call
|
|
198
|
+
function resolveCall(occurrence, catalog, strictProse) {
|
|
199
|
+
const bounded = occurrence.mode === 'bounded';
|
|
200
|
+
const tokens = commandTokens(occurrence);
|
|
201
|
+
const globals = new Map(catalog.global.map((f) => [f.name, f]));
|
|
202
|
+
const i = globalFlagsBefore(tokens, globals);
|
|
203
|
+
const head = tokens[i];
|
|
204
|
+
if (!head || NOT_A_COMMAND_HEAD.has(head.text) || isOpaque(head.text)) return { stop: [] };
|
|
205
|
+
const groupAlts = alternatives(head.text);
|
|
206
|
+
const lenient = !bounded && !isAsked(occurrence, strictProse);
|
|
207
|
+
const stopped = groupStop(occurrence, groupAlts, groupAlts.filter((g) => !catalog.groups[g]), lenient);
|
|
208
|
+
if (stopped) return { stop: stopped };
|
|
209
|
+
const group = catalog.groups[groupAlts[0]];
|
|
210
|
+
const verbToken = tokens[i + 1];
|
|
211
|
+
if (!verbToken || verbToken.text.startsWith('-') || isOpaque(verbToken.text)) return { stop: [] };
|
|
212
|
+
const verbAlts = alternatives(verbToken.text);
|
|
213
|
+
const unknownVerbs = verbAlts.filter((v) => !group.verbs[v]);
|
|
214
|
+
if (lenient && unknownVerbs.length === verbAlts.length) return { stop: [] };
|
|
215
|
+
if (unknownVerbs.length > 0) return { stop: unknownVerbs.map((v) => `unknown verb "${groupAlts[0]} ${v}"`) };
|
|
216
|
+
return { groupAlts, verbAlts, group, rest: tokens.slice(i + 2), globals, bounded };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// alternatives of groups or verbs: only the flag names are provable
|
|
220
|
+
function alternativeErrors(call) {
|
|
221
|
+
const errors = [];
|
|
222
|
+
for (const t of call.rest) {
|
|
223
|
+
if (!t.text.startsWith('--')) continue;
|
|
224
|
+
for (const n of alternatives(t.text).map((a) => a.replace(/^--/, '').split('=')[0])) {
|
|
225
|
+
const some = call.verbAlts.some((v) => call.group.verbs[v].flags?.some((f) => f.name === n)) || call.globals.has(n);
|
|
226
|
+
if (!some) errors.push(`--${n} is not a flag of ${call.groupAlts[0]} ${call.verbAlts.join('|')}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return errors;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const enumChoices = (literal, value) => {
|
|
233
|
+
if (literal !== null) return alternatives(literal);
|
|
234
|
+
return value !== null && isAlternation(value) ? inner(value).split('|') : [];
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
function pushFlagValue(st, flag, name, value) {
|
|
238
|
+
const literal = value === null || isPlaceholder(value) ? null : value;
|
|
239
|
+
if (flag.type !== 'enum') {
|
|
240
|
+
st.argv.push(`--${name}`, flag.type === 'number' || literal === null ? sampleFor(flag) : literal);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
for (const c of enumChoices(literal, value)) {
|
|
244
|
+
if (WORD_VALUE.test(c) && !flag.enum.includes(c)) st.errors.push(`--${name} expects one of: ${flag.enum.join(', ')} (got ${c})`);
|
|
245
|
+
}
|
|
246
|
+
st.argv.push(`--${name}`, flag.enum[0]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// reads the flag at rest[k] (with its value) into `st`; returns the index of the last token it used
|
|
250
|
+
function readFlag(st, rest, k, ctx) {
|
|
251
|
+
const alts = alternatives(rest[k].text).map((a) => a.replace(/^--/, ''));
|
|
252
|
+
const eq = alts[0].indexOf('=');
|
|
253
|
+
const name = eq >= 0 ? alts[0].slice(0, eq) : alts[0];
|
|
254
|
+
const unknown = alts.map((a) => a.split('=')[0]).filter((an) => !ctx.declared.has(an));
|
|
255
|
+
for (const an of unknown) st.errors.push(`unknown option --${an}`);
|
|
256
|
+
if (unknown.length > 0) return k;
|
|
257
|
+
const flag = ctx.declared.get(name);
|
|
258
|
+
if (flag.type === 'boolean') {
|
|
259
|
+
st.argv.push(`--${name}`);
|
|
260
|
+
return k;
|
|
261
|
+
}
|
|
262
|
+
let value = eq >= 0 ? alts[0].slice(eq + 1) : undefined;
|
|
263
|
+
let last = k;
|
|
264
|
+
if (value === undefined) {
|
|
265
|
+
const next = rest[k + 1];
|
|
266
|
+
const takesNext = next && !next.text.startsWith('--') && !isEllipsis(next.text);
|
|
267
|
+
value = takesNext ? next.text : null;
|
|
268
|
+
if (takesNext) last = k + 1;
|
|
269
|
+
}
|
|
270
|
+
if (value !== null) st.valued = true;
|
|
271
|
+
pushFlagValue(st, flag, name, value);
|
|
272
|
+
return last;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// the argv entry (and findings) of one positional token against its schema entry
|
|
276
|
+
function pushPositional(st, text, entry) {
|
|
277
|
+
const alts = isAlternation(text) ? inner(text).split('|') : null;
|
|
278
|
+
if (entry?.enum && alts) {
|
|
279
|
+
for (const c of alts) {
|
|
280
|
+
if (WORD_VALUE.test(c) && !entry.enum.includes(c)) st.errors.push(`${entry.name} expects one of: ${entry.enum.join(', ')} (got ${c})`);
|
|
281
|
+
}
|
|
282
|
+
st.argv.push(entry.enum[0]);
|
|
283
|
+
} else if (isPlaceholder(text) || text.includes('|')) st.argv.push(entry?.enum ? entry.enum[0] : 'x');
|
|
284
|
+
else st.argv.push(text);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// reads the positional token `t` into `st`; false when the command ends before it
|
|
288
|
+
function readPositional(st, t, ctx) {
|
|
289
|
+
if (!ctx.bounded && !/^<.*>$/.test(t.text)) return false;
|
|
290
|
+
if (t.text.startsWith('-') && t.text.length > 1) {
|
|
291
|
+
st.errors.push(`unknown option ${t.text}`);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
const entry = ctx.schema[Math.min(st.position, ctx.schema.length - 1)];
|
|
295
|
+
st.position += 1;
|
|
296
|
+
st.valued = true;
|
|
297
|
+
pushPositional(st, t.text, entry);
|
|
298
|
+
return true;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function readArguments(rest, ctx) {
|
|
302
|
+
const st = { argv: [], errors: [], valued: false, fragment: rest.length === 0, position: 0 };
|
|
303
|
+
let k = 0;
|
|
304
|
+
while (k < rest.length) {
|
|
305
|
+
const t = rest[k];
|
|
306
|
+
if (isEllipsis(t.text)) {
|
|
307
|
+
st.fragment = true;
|
|
308
|
+
if (!ctx.bounded) break;
|
|
309
|
+
} else if (t.text === '--') {
|
|
310
|
+
st.fragment = true;
|
|
311
|
+
break;
|
|
312
|
+
} else if (t.text.startsWith('--')) k = readFlag(st, rest, k, ctx);
|
|
313
|
+
else if (!readPositional(st, t, ctx)) break;
|
|
314
|
+
k += 1;
|
|
315
|
+
}
|
|
316
|
+
return st;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// A mention that is not a whole call (prose, no value given, an ellipsis) is only held to the flags it names.
|
|
320
|
+
function supplyMissing(st, ctx, error) {
|
|
321
|
+
const missingFlag = /^missing required option --(.+)$/.exec(error);
|
|
322
|
+
if (missingFlag) {
|
|
323
|
+
st.argv.push(`--${missingFlag[1]}`, sampleFor(ctx.declared.get(missingFlag[1])));
|
|
324
|
+
return true;
|
|
325
|
+
}
|
|
326
|
+
const missingPositional = /^missing required positional (.+)$/.exec(error);
|
|
327
|
+
if (!missingPositional) return false;
|
|
328
|
+
st.argv.push(ctx.schema.find((s) => s.name === missingPositional[1])?.enum?.[0] ?? 'x');
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function proveCall(st, ctx, catalog) {
|
|
333
|
+
const whole = ctx.bounded && st.valued && !st.fragment;
|
|
334
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
335
|
+
const result = validateArgs(st.argv, ctx.verb, catalog.global);
|
|
336
|
+
if (result.ok) return [];
|
|
337
|
+
if (whole || !supplyMissing(st, ctx, result.error)) return [result.error];
|
|
338
|
+
}
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function checkSingle(call, catalog) {
|
|
343
|
+
const verbName = call.verbAlts[0];
|
|
344
|
+
const verb = { ...call.group.verbs[verbName], group: call.groupAlts[0], verb: verbName };
|
|
345
|
+
const declared = new Map([...catalog.global, ...(verb.flags ?? [])].map((f) => [f.name, f]));
|
|
346
|
+
const ctx = { bounded: call.bounded, verb, declared, schema: verb.positional ?? [] };
|
|
347
|
+
const st = readArguments(call.rest, ctx);
|
|
348
|
+
return st.errors.length > 0 ? st.errors : proveCall(st, ctx, catalog);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Returns [] when the command is a valid call (or a fragment of one), else the reasons it is not.
|
|
353
|
+
* `strictProse` holds a mention inside running text to the same group and verb as a span.
|
|
354
|
+
*/
|
|
355
|
+
export function checkCommand(occurrence, catalog, { strictProse = false } = {}) {
|
|
356
|
+
if (RETIRED_SENTENCE.test(occurrence.text)) return [];
|
|
357
|
+
const call = resolveCall(occurrence, catalog, strictProse);
|
|
358
|
+
if (call.stop) return call.stop;
|
|
359
|
+
if (call.verbAlts.length > 1 || call.groupAlts.length > 1) return alternativeErrors(call);
|
|
360
|
+
return checkSingle(call, catalog);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** The command table of a catalog loaded from modules/cli/commands (scripts/cli/catalog.mjs loadCatalog) in the shape checkCommand reads. */
|
|
364
|
+
export const commandCatalogOf = (loaded) => ({
|
|
365
|
+
global: loaded.global.flags,
|
|
366
|
+
groups: Object.fromEntries(loaded.groups.map((group) => [group.group, { verbs: Object.fromEntries(group.verbs.map((verb) => [verb.verb, verb])) }])),
|
|
367
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// edition.mjs - the one edition filter shared by the HFS slot resolver, declaration validation, rule catalog and
|
|
2
2
|
// edition-specific rule parameters. Full is the default; lite changes data views, never the checks that consume them.
|
|
3
|
-
// Lite
|
|
4
|
-
//
|
|
3
|
+
// Lite uses provider adapters and declares Docker Swarm production infrastructure (owner:lite-production-swarm, ruling:R212/case-4).
|
|
4
|
+
// Its overlays exclude the e2e world, and every slot keeps its own `tests`.
|
|
5
5
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
6
6
|
import { EDITIONS } from './manifest-shape.mjs';
|
|
7
7
|
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Runtime examples keep names only; checks receive disposable copies with fresh sealed members.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { safeRemove } from '../api/fs/safe-remove.mjs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { runProgram } from '../api/process/run-program.mjs';
|
|
7
|
+
import { init as initGit } from '../api/git/init.mjs';
|
|
8
|
+
import { add as addGit } from '../api/git/add.mjs';
|
|
9
|
+
import { stringifyYaml } from '../../engine/yaml.mjs';
|
|
10
|
+
import { isMain } from '../lib/is-main.mjs';
|
|
11
|
+
import { readEnv } from '../lib/env.mjs';
|
|
12
|
+
import { withoutGitLocalEnv } from '../lib/git.mjs';
|
|
13
|
+
import { resolveRealTool } from '../api/process/resolve-real-tool.mjs';
|
|
14
|
+
import { generateAppIdentity } from './secret-bootstrap-crypto.mjs';
|
|
15
|
+
import { declaredSealedMembers, materializeDeclaredPlaceholders } from './sealed-members.mjs';
|
|
16
|
+
|
|
17
|
+
const SKIPPED = new Set(['.git', '.claude', '.secrets', '.aws', '.codex', '.agents', 'node_modules', '.next', '.turbo', 'dist', 'coverage']);
|
|
18
|
+
const PATH_RULE = String.raw`\.starcistacks[/\\][^/\\]+[/\\]secrets[/\\][^/\\]+\.enc$`;
|
|
19
|
+
|
|
20
|
+
function writePublicRecipient(repoRoot, recipient) {
|
|
21
|
+
fs.writeFileSync(path.join(repoRoot, '.sops.yaml'), stringifyYaml({
|
|
22
|
+
creation_rules: [{ path_regex: PATH_RULE, age: recipient }],
|
|
23
|
+
}));
|
|
24
|
+
const stacks = path.join(repoRoot, '.starcistacks');
|
|
25
|
+
for (const entry of fs.readdirSync(stacks, { withFileTypes: true })) {
|
|
26
|
+
if (!entry.isDirectory()) continue;
|
|
27
|
+
const file = path.join(stacks, entry.name, 'backup.recipient');
|
|
28
|
+
if (fs.existsSync(file)) fs.writeFileSync(file, `${recipient}\n`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Generate genuine sealed members; the fresh private identity is confined to os.tmpdir and removed before return. */
|
|
33
|
+
export async function materializeExample(repoRoot, { createIdentity = generateAppIdentity, sops } = {}) {
|
|
34
|
+
const declared = declaredSealedMembers(repoRoot);
|
|
35
|
+
if (!declared.length) return { members: [] };
|
|
36
|
+
if (declared.some(({ env, slug }) => fs.existsSync(path.join(repoRoot, '.starcistacks', env, 'secrets', slug + '.enc'))))
|
|
37
|
+
throw new Error('example materialization requires a disposable copy without existing sealed members');
|
|
38
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'starci-example-age-'));
|
|
39
|
+
try {
|
|
40
|
+
const selected = createIdentity();
|
|
41
|
+
try {
|
|
42
|
+
fs.writeFileSync(path.join(directory, 'identity.txt'), selected.identity, { mode: 0o600, flag: 'wx' });
|
|
43
|
+
} finally {
|
|
44
|
+
selected.identity.fill(0);
|
|
45
|
+
}
|
|
46
|
+
writePublicRecipient(repoRoot, selected.recipient);
|
|
47
|
+
return materializeDeclaredPlaceholders(repoRoot, { recipient: selected.recipient, sops });
|
|
48
|
+
} finally {
|
|
49
|
+
safeRemove(directory, { hold: () => null });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function trackExample(repoRoot) {
|
|
54
|
+
const env = withoutGitLocalEnv(process.env);
|
|
55
|
+
if (!initGit(repoRoot, { env }).ok) throw new Error('could not initialize the isolated example git tree');
|
|
56
|
+
const staged = addGit(['-A', '--', '.'], { cwd: repoRoot, env });
|
|
57
|
+
if (staged.error || staged.status !== 0) throw new Error('could not stage the isolated example git tree');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function safeExampleSource(source, { preserveCoverage = false } = {}) {
|
|
61
|
+
const name = path.basename(source);
|
|
62
|
+
const stat = fs.lstatSync(source);
|
|
63
|
+
if (stat.isSymbolicLink()) return false;
|
|
64
|
+
if (SKIPPED.has(name) && !(preserveCoverage && name === 'coverage')) return false;
|
|
65
|
+
if (name.endsWith('.enc') || /\.(?:agekey|pem|key)$/iu.test(name)) return false;
|
|
66
|
+
if (['secret.env', 'secrets.env', 'keys.txt'].includes(name)) return false;
|
|
67
|
+
if (name === '.env.example' || name.endsWith('.env.example')) return true;
|
|
68
|
+
if (name === '.env' || name.startsWith('.env.') || name.endsWith('.env')) return false;
|
|
69
|
+
if (stat.isDirectory()) return true;
|
|
70
|
+
const parts = source.split(path.sep);
|
|
71
|
+
return !parts.includes('secrets') || ['.gitignore', 'README.md'].includes(name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function pruneEmptyDirectories(directory) {
|
|
75
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
76
|
+
if (entry.isDirectory()) pruneEmptyDirectories(path.join(directory, entry.name));
|
|
77
|
+
}
|
|
78
|
+
if (!fs.readdirSync(directory).length) fs.rmdirSync(directory);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Await callback(tempExamplePath), return its result, and remove the os.tmpdir-based copy even on failure. */
|
|
82
|
+
export async function withMaterializedExample(repoRoot, exampleRelativePath, callback, { materialize = materializeExample, linkDependencies = false, preserveCoverage = false } = {}) {
|
|
83
|
+
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'starci-example-'));
|
|
84
|
+
const source = path.resolve(repoRoot, exampleRelativePath);
|
|
85
|
+
const app = path.join(directory, 'examples', path.basename(source));
|
|
86
|
+
try {
|
|
87
|
+
fs.writeFileSync(path.join(directory, 'hfs.json'), JSON.stringify({ kind: 'runtime' }));
|
|
88
|
+
fs.cpSync(source, app, {
|
|
89
|
+
recursive: true,
|
|
90
|
+
filter: (file) => safeExampleSource(file, { preserveCoverage }),
|
|
91
|
+
});
|
|
92
|
+
pruneEmptyDirectories(app);
|
|
93
|
+
if (linkDependencies) linkExampleDependencies(source, app);
|
|
94
|
+
await materialize(app);
|
|
95
|
+
trackExample(app);
|
|
96
|
+
return await callback(app);
|
|
97
|
+
} finally {
|
|
98
|
+
safeRemove(directory, { hold: () => null });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Runtime example command execution shares the same isolated materialization as the source check.
|
|
103
|
+
function linkExampleDependencies(source, target) {
|
|
104
|
+
for (const entry of fs.readdirSync(source, { withFileTypes: true })) {
|
|
105
|
+
const from = path.join(source, entry.name);
|
|
106
|
+
const to = path.join(target, entry.name);
|
|
107
|
+
if (entry.name === 'node_modules') {
|
|
108
|
+
fs.mkdirSync(target, { recursive: true });
|
|
109
|
+
fs.symlinkSync(from, to, process.platform === 'win32' ? 'junction' : 'dir');
|
|
110
|
+
} else if (entry.isDirectory() && safeExampleSource(from)) {
|
|
111
|
+
linkExampleDependencies(from, to);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function commandLaunch(command, args) {
|
|
117
|
+
const npm = /^(npm|npx)(?:\.cmd)?$/iu.exec(command);
|
|
118
|
+
if (process.platform !== 'win32' || !npm) return { file: command, args };
|
|
119
|
+
const entry = npm[1].toLowerCase() + '-cli.js';
|
|
120
|
+
const resolved = resolveRealTool(npm[1]);
|
|
121
|
+
const npmExecPath = readEnv('npm_execpath');
|
|
122
|
+
const candidates = [
|
|
123
|
+
npmExecPath && path.join(path.dirname(npmExecPath), entry),
|
|
124
|
+
resolved && path.join(path.dirname(resolved), 'node_modules', 'npm', 'bin', entry),
|
|
125
|
+
path.join(path.dirname(process.execPath), 'node_modules', 'npm', 'bin', entry),
|
|
126
|
+
];
|
|
127
|
+
const cli = candidates.find((file) => file && fs.existsSync(file));
|
|
128
|
+
if (!cli) throw new Error('the example runner could not locate the installed npm CLI entry point');
|
|
129
|
+
return { file: process.execPath, args: [cli, ...args] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function commandEnvironment() {
|
|
133
|
+
const env = withoutGitLocalEnv(process.env);
|
|
134
|
+
return Object.fromEntries(Object.entries(env).filter(([name]) => !/^SOPS_AGE_KEY(?:_FILE)?$/iu.test(name)));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Run an installed app command in a sealed temporary copy, returning its exit code and preserving report artifacts. */
|
|
138
|
+
export async function runMaterializedCommand(repoRoot, command, args = [], { run = runProgram } = {}) {
|
|
139
|
+
return withMaterializedExample(repoRoot, '.', (app) => {
|
|
140
|
+
const launch = commandLaunch(command, args);
|
|
141
|
+
const result = run(launch.file, launch.args, { cwd: app, env: commandEnvironment(), stdio: 'inherit', windowsHide: true });
|
|
142
|
+
const reports = path.join(app, 'reports');
|
|
143
|
+
if (fs.existsSync(reports)) fs.cpSync(reports, path.join(repoRoot, 'reports'), { recursive: true });
|
|
144
|
+
if (result.error) throw new Error('the example check command could not start');
|
|
145
|
+
return result.status ?? 1;
|
|
146
|
+
}, { linkDependencies: true, preserveCoverage: true });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function main(argv) {
|
|
150
|
+
if (argv[0] === '--run' && argv[1] && argv[2] === '--' && argv[3])
|
|
151
|
+
return runMaterializedCommand(path.resolve(argv[1]), argv[3], argv.slice(4));
|
|
152
|
+
if (argv.length !== 1) throw new Error('usage: example-secrets.mjs <disposable-copy> | --run <example> -- <command> [args]');
|
|
153
|
+
const result = await materializeExample(path.resolve(argv[0]));
|
|
154
|
+
process.stdout.write(`example sealed members generated: ${result.members.length}\n`);
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isMain(import.meta.url)) process.exitCode = await main(process.argv.slice(2));
|