@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
|
@@ -12,7 +12,7 @@ export const unquoteDiffPath = (value) =>
|
|
|
12
12
|
|
|
13
13
|
// git's repository-local variables (git rev-parse --local-env-vars). A hook or alias run in a linked worktree exports
|
|
14
14
|
// GIT_DIR=<main>/.git/worktrees/<wt>; a spec inheriting it pointed every fixture git at the LIVE .claude repo and re-inited
|
|
15
|
-
// it core.bare=true (2026-09-29, a6f60352c).
|
|
15
|
+
// it core.bare=true (2026-09-29, a6f60352c). e2e/setup/isolated-registry.mjs drops the same list.
|
|
16
16
|
export const GIT_LOCAL_ENV_VARS = ['GIT_DIR', 'GIT_COMMON_DIR', 'GIT_WORK_TREE', 'GIT_INDEX_FILE', 'GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES', 'GIT_IMPLICIT_WORK_TREE', 'GIT_PREFIX', 'GIT_CONFIG', 'GIT_CONFIG_PARAMETERS', 'GIT_CONFIG_COUNT', 'GIT_GRAFT_FILE', 'GIT_NO_REPLACE_OBJECTS', 'GIT_REPLACE_REF_BASE', 'GIT_SHALLOW_FILE', 'GIT_INTERNAL_SUPER_PREFIX'];
|
|
17
17
|
/** `parent` (the caller's env) without git's repository-local variables (GIT_CONFIG_KEY_n/VALUE_n go with GIT_CONFIG_COUNT). Pure. */
|
|
18
18
|
export function withoutGitLocalEnv(parent) {
|
|
@@ -49,8 +49,6 @@ export function loadCatalog(root = skillRoot, { scope = 'runtime' } = {}) {
|
|
|
49
49
|
return cache.get(key);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/** Forget the cached catalogs (a spec that rewrites a catalog file). */
|
|
53
|
-
export const resetCatalogCache = () => cache.clear();
|
|
54
52
|
|
|
55
53
|
/** The names each `pattern` match captures (group 1) in `text`, in match order (`sort` sorts them). */
|
|
56
54
|
export const captureNames = (text, pattern, { sort = false } = {}) => {
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// json-clone.mjs — a deep copy that keeps exactly what survives a JSON text and nothing else: the plain-data
|
|
2
|
+
// copy of a value, detached from every reference, frozen state and class of the original. `structuredClone` keeps
|
|
3
|
+
// what JSON drops (undefined members, Dates, Maps, sparse holes) and refuses what JSON accepts (functions,
|
|
4
|
+
// `toJSON` objects), so it is not a substitute; this walks the value once under the JSON.stringify rules.
|
|
5
|
+
|
|
6
|
+
/** The boxed primitives JSON.stringify unwraps: recognised by the internal slot a valueOf call reads, as JSON does. */
|
|
7
|
+
const BOXED = [
|
|
8
|
+
{ slot: Number.prototype.valueOf, read: Number },
|
|
9
|
+
{ slot: String.prototype.valueOf, read: String },
|
|
10
|
+
{ slot: Boolean.prototype.valueOf, read: (box) => Boolean.prototype.valueOf.call(box) },
|
|
11
|
+
{ slot: BigInt.prototype.valueOf, read: (box) => BigInt.prototype.valueOf.call(box) },
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
/** Whether `value` carries the internal slot `slot` reads (a valueOf of that boxed type does not throw on it). */
|
|
15
|
+
const hasSlot = (slot, value) => {
|
|
16
|
+
try { slot.call(value); return true; } catch { return false; }
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** What JSON.stringify takes of `holder[key]`: the toJSON result when there is one, a boxed primitive unwrapped. */
|
|
20
|
+
const serialized = (holder, key) => {
|
|
21
|
+
let value = holder[key];
|
|
22
|
+
if (value !== null && (typeof value === 'object' || typeof value === 'bigint') && typeof value.toJSON === 'function') value = value.toJSON(key);
|
|
23
|
+
if (value === null || typeof value !== 'object') return value;
|
|
24
|
+
const box = BOXED.find((candidate) => hasSlot(candidate.slot, value));
|
|
25
|
+
return box ? box.read(value) : value;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** A member JSON leaves out of an object (and writes as null in an array). */
|
|
29
|
+
const isAbsent = (value) => value === undefined || typeof value === 'function' || typeof value === 'symbol';
|
|
30
|
+
|
|
31
|
+
const copyValue = (value, ancestors) => {
|
|
32
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
33
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value + 0 : null;
|
|
34
|
+
if (typeof value === 'bigint') throw new TypeError('Do not know how to serialize a BigInt');
|
|
35
|
+
if (ancestors.includes(value)) throw new TypeError('Converting circular structure to JSON');
|
|
36
|
+
const inside = [...ancestors, value];
|
|
37
|
+
if (Array.isArray(value)) {
|
|
38
|
+
return Array.from({ length: value.length }, (_, index) => {
|
|
39
|
+
const item = serialized(value, String(index));
|
|
40
|
+
return isAbsent(item) ? null : copyValue(item, inside);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const entries = [];
|
|
44
|
+
for (const name of Object.keys(value)) {
|
|
45
|
+
const member = serialized(value, name);
|
|
46
|
+
if (!isAbsent(member)) entries.push([name, copyValue(member, inside)]);
|
|
47
|
+
}
|
|
48
|
+
return Object.fromEntries(entries);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `value` copied as JSON data: members that are undefined, functions or symbols are dropped (an array item becomes
|
|
53
|
+
* null), non-finite numbers become null and -0 becomes 0, `toJSON` results replace their objects (a Date becomes its ISO string), boxed
|
|
54
|
+
* primitives unwrap, and a cycle or a BigInt throws a TypeError. A value JSON has no text for (undefined, a function,
|
|
55
|
+
* a symbol) throws a SyntaxError, as parsing the missing text does.
|
|
56
|
+
*/
|
|
57
|
+
export function jsonClone(value) {
|
|
58
|
+
const top = serialized({ '': value }, '');
|
|
59
|
+
if (isAbsent(top)) throw new SyntaxError('"undefined" is not valid JSON');
|
|
60
|
+
return copyValue(top, []);
|
|
61
|
+
}
|
|
@@ -76,7 +76,7 @@ const I18N_CATALOG_DIR = 'modules/i18n/messages/';
|
|
|
76
76
|
const OP_MANIFEST_VIETNAMESE_FIELDS = Object.freeze({ fields: Object.freeze(['vi']) });
|
|
77
77
|
const OP_MANIFEST_DIR = 'modules/ops/ops/';
|
|
78
78
|
/** The declared Vietnamese fields of `rel`: its own entry, or the op-manifest text field for a manifest of modules/ops/ops. */
|
|
79
|
-
|
|
79
|
+
const declaredVietnameseFieldsOf = (rel) => DECLARED_VIETNAMESE_FIELDS[rel]
|
|
80
80
|
?? (rel.startsWith(I18N_CATALOG_DIR) && !rel.slice(I18N_CATALOG_DIR.length).includes('/') && /\.ya?ml$/.test(rel) ? I18N_CATALOG_VIETNAMESE_FIELDS : null)
|
|
81
81
|
?? (rel.startsWith(OP_MANIFEST_DIR) && !rel.slice(OP_MANIFEST_DIR.length).includes('/') && /\.ya?ml$/.test(rel) ? OP_MANIFEST_VIETNAMESE_FIELDS : null);
|
|
82
82
|
|
|
@@ -17,6 +17,10 @@ export const objectList = (v) => (Array.isArray(v) ? v : []).filter((item) => it
|
|
|
17
17
|
export const stringList = (v, { blank = true } = {}) =>
|
|
18
18
|
Array.isArray(v) && v.every((s) => typeof s === 'string' && s.length > 0 && (blank || s.trim().length > 0));
|
|
19
19
|
|
|
20
|
+
/** A non-empty list of distinct values, all accepted by the supplied predicate. */
|
|
21
|
+
export const nonemptyUniqueList = (values, accepts) => Array.isArray(values) && values.length > 0
|
|
22
|
+
&& values.every((entry) => accepts(entry)) && new Set(values).size === values.length;
|
|
23
|
+
|
|
20
24
|
/** The items of a `sep`-separated list: trimmed, empties dropped; `dedupe` keeps first occurrences only. */
|
|
21
25
|
export const splitList = (v, { sep = ',', dedupe = false } = {}) => {
|
|
22
26
|
const items = String(v ?? '').split(sep).map((s) => s.trim()).filter(Boolean);
|
|
@@ -17,16 +17,14 @@
|
|
|
17
17
|
// directory or from the Work directory name outside this module.
|
|
18
18
|
import fs from 'node:fs';
|
|
19
19
|
import path from 'node:path';
|
|
20
|
-
import { skillRoot,
|
|
20
|
+
import { skillRoot, starciLocalRoot, starciSourceRoot } from '../../engine/runtime-root.mjs';
|
|
21
21
|
import { tempRoot } from '../../engine/temp-root.mjs';
|
|
22
22
|
|
|
23
23
|
export { tempRoot, starciLocalRoot, starciSourceRoot };
|
|
24
24
|
|
|
25
25
|
/** The runtime tree this code runs from. */
|
|
26
|
-
|
|
26
|
+
const runtimeTree = skillRoot;
|
|
27
27
|
|
|
28
|
-
/** The state directory of a runtime tree (default: this one). */
|
|
29
|
-
export const runtimeState = (tree = runtimeTree) => runtimeStateDir(tree);
|
|
30
28
|
|
|
31
29
|
/** The name of the Work records directory inside a product tree. */
|
|
32
30
|
export const WORK_DIR_NAME = '.starciwork';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Identify runtime-owned example apps; an unrelated directory named examples grants no fixture behavior.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export function isRuntimeExampleApp(repoRoot) {
|
|
6
|
+
if (path.basename(path.dirname(repoRoot)) !== 'examples') return false;
|
|
7
|
+
try {
|
|
8
|
+
const runtime = JSON.parse(fs.readFileSync(path.resolve(repoRoot, '..', '..', 'hfs.json'), 'utf8'));
|
|
9
|
+
return runtime.kind === 'runtime';
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -12,6 +12,57 @@ export const FORBIDDEN_FILES = [
|
|
|
12
12
|
{ name: 'starcistacks-secret-plaintext', test: (f) => /(^|\/)\.starcistacks\/[^/]+\/secrets\//i.test(f) && !/(\.enc|\/\.gitkeep|\/KEYS\.md)$/i.test(f) },
|
|
13
13
|
{ name: 'credentials-json', test: (f) => /(^|\/)(credentials|service-account|client_secret)[^/]*\.json$/i.test(f) },
|
|
14
14
|
];
|
|
15
|
+
// `word` in any case (name checks are case-aware by construction, not by flag).
|
|
16
|
+
const fold = (word) => word.replace(/[a-z]/gi, (c) => `[${c.toLowerCase()}${c.toUpperCase()}]`);
|
|
17
|
+
|
|
18
|
+
// A name that holds secret material: the keyword is an identifier segment, so DB_PASSWORD, dbPassword,
|
|
19
|
+
// OPENAI_API_KEY, api_secret and accessToken are candidates while `secondary`, `tokenize`, `monkey`, `secrets.x`
|
|
20
|
+
// and `$secret` are not. A segment starts at the name edge, after `_`/`-`/`.`, or at a camel hump.
|
|
21
|
+
const SECRET_WORD = [
|
|
22
|
+
'password', 'passwd', 'passphrase', 'secret', 'credentials?', 'token',
|
|
23
|
+
'api[_-]?key', 'access[_-]?token', 'auth[_-]?token', 'refresh[_-]?token', 'id[_-]?token', 'session[_-]?token',
|
|
24
|
+
'client[_-]?secret', 'api[_-]?secret', 'session[_-]?secret', 'jwt[_-]?secret',
|
|
25
|
+
'private[_-]?key', 'secret[_-]?key', 'signing[_-]?key', 'encryption[_-]?key', 'master[_-]?key',
|
|
26
|
+
'service[_-]?role[_-]?key', 'service[_-]?key', 'session[_-]?key',
|
|
27
|
+
].map((word) => fold(word)).join('|');
|
|
28
|
+
|
|
29
|
+
// A name whose last segment is configuration, not material: SECRET_FILE and *_PATH point at the materialized
|
|
30
|
+
// secret file (the materializer's own form), PASSWORD_MIN_LENGTH is a number, *_REF/_ID a reference.
|
|
31
|
+
const BENIGN_WORD = [
|
|
32
|
+
'file', 'path', 'dir', 'url', 'uri', 'endpoint', 'host', 'refs?', 'names?', 'ids?', 'arn', 'urn',
|
|
33
|
+
'length', 'min', 'max', 'size', 'count', 'budget', 'limit', 'regex', 'pattern', 'policy',
|
|
34
|
+
'algorithm', 'scheme', 'expiry', 'expires', 'ttl', 'required', 'enabled', 'disabled', 'type', 'version',
|
|
35
|
+
].map((word) => fold(word)).join('|');
|
|
36
|
+
|
|
37
|
+
const SEGMENT_BEFORE = String.raw`(?:(?<![A-Za-z0-9_$])|(?<=[_\-.])|(?<=[a-z])(?=[A-Z]))`;
|
|
38
|
+
const NAME_REST = '[A-Za-z0-9_$.-]*?';
|
|
39
|
+
const SECRET_NAME = [
|
|
40
|
+
'(?<![A-Za-z0-9_$])',
|
|
41
|
+
String.raw`(?!${NAME_REST}${SEGMENT_BEFORE}(?:${BENIGN_WORD})(?=\s*[:=]))`,
|
|
42
|
+
String.raw`(?=${NAME_REST}${SEGMENT_BEFORE}(?:${SECRET_WORD})(?=[_\-.]|[A-Z]|[^A-Za-z0-9_$]|$))`,
|
|
43
|
+
String.raw`[A-Za-z_$][A-Za-z0-9_$.-]*\s*[:=]\s*`,
|
|
44
|
+
].join('');
|
|
45
|
+
|
|
46
|
+
// `password = "hunter2hunter2"` — the quoted literal. `env(NAME)` is a reference, never a secret.
|
|
47
|
+
const ASSIGNED_QUOTED = new RegExp(
|
|
48
|
+
SECRET_NAME + String.raw`['"\`](?!env\([A-Z_][A-Z0-9_]*\)['"\`])([^'"\`\n\s$<{]{12,})['"\`]`,
|
|
49
|
+
);
|
|
50
|
+
// `DB_PASSWORD=hunter2hunter2` — the compose list-item and dotenv form; the dotenv value has no `:` so
|
|
51
|
+
// `password: secret:db/password` and `token: ref:vault/api` stay references, and the end boundary keeps
|
|
52
|
+
// calls and member/index references such as `secret = credentialsSource.current` out of the literals.
|
|
53
|
+
const ASSIGNED_UNQUOTED = new RegExp(
|
|
54
|
+
SECRET_NAME + String.raw`(?!['"\`]|env\()([A-Za-z0-9_+=-]{12,})(?![A-Za-z0-9_+=.(\[-])`,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const NAME_WORD = (word, tail) => new RegExp(`^${NAME_REST}${SEGMENT_BEFORE}(?:${word})${tail}`);
|
|
58
|
+
const SECRET_NAME_TEST = NAME_WORD(SECRET_WORD, String.raw`(?:[_\-.]|[A-Z]|[^A-Za-z0-9_$]|$)`);
|
|
59
|
+
const BENIGN_TAIL_TEST = NAME_WORD(BENIGN_WORD, '$');
|
|
60
|
+
|
|
61
|
+
/** True when a bare variable `name` holds secret material as an identifier segment (DB_PASSWORD, dbPassword,
|
|
62
|
+
* SUPABASE_SERVICE_ROLE_KEY) and does not end in a config suffix (PASSWORD_MIN_LENGTH, SECRET_FILE,
|
|
63
|
+
* tokenBudget). The same name judgement the `assigned-secret` patterns make, for callers that read keys. */
|
|
64
|
+
export const secretNameLike = (name) => SECRET_NAME_TEST.test(name) && !BENIGN_TAIL_TEST.test(name);
|
|
65
|
+
|
|
15
66
|
export const SECRET_PATTERNS = [
|
|
16
67
|
{ name: 'private-key-block', re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
|
17
68
|
{ name: 'aws-access-key', re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
@@ -23,17 +74,20 @@ export const SECRET_PATTERNS = [
|
|
|
23
74
|
{ name: 'telegram-bot-token', re: /\b\d{8,10}:AA[0-9A-Za-z_-]{33}\b/ },
|
|
24
75
|
{ name: 'stripe-secret', re: /\b(?:sk|rk)_live_[0-9A-Za-z]{20,}\b/ },
|
|
25
76
|
{ name: 'jwt', re: /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
|
|
77
|
+
{ name: 'age-secret-key', re: /\bAGE-SECRET-KEY-1[0-9A-Z]{20,}\b/ },
|
|
78
|
+
{ name: 'password-connection-string', re: /\b(?:postgres(?:ql)?|mysql|mariadb|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:[^@\s/${}]{8,}@/iu },
|
|
26
79
|
// A value written `env(NAME)` (the Supabase config.toml reference form) names a variable, not a secret.
|
|
27
80
|
// A keyword-assigned value that names itself a stand-in (a test stub's `accessToken: "fixture-..."`) is no
|
|
28
81
|
// candidate; only this heuristic takes the exemption, never a provider-shaped token above.
|
|
29
82
|
// A spec file's keyword-assigned values are test inputs (a spec's sign-in
|
|
30
83
|
// form password and a mocked accessToken once refused a push): the heuristic skips spec files too.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
String.raw`\b\s*[:=]\s*['"](?!env\([A-Z_][A-Z0-9_]*\)['"])([^'"\s$<{]{12,})`,
|
|
34
|
-
`['"]`,
|
|
35
|
-
].join(''), 'i'), placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
|
|
84
|
+
// A SCREAMING_SNAKE quoted value in source is a public code (an enum member whose value is an upper-case code), not a credential.
|
|
85
|
+
{ name: 'assigned-secret', re: ASSIGNED_QUOTED, sourceCode: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$/, placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
|
|
36
86
|
skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$/ },
|
|
87
|
+
// An unquoted value in TypeScript or JavaScript source is an identifier or a type (`token: InjectionToken`,
|
|
88
|
+
// `MODULE_OPTIONS_TOKEN: DATABASE_OPTIONS_TOKEN`), never a literal; the unquoted form is the dotenv, compose and toml shape.
|
|
89
|
+
{ name: 'assigned-secret', re: ASSIGNED_UNQUOTED, placeholder: /fixture|stub|fake|dummy|placeholder|example|sample|changeme|redacted|mock/i,
|
|
90
|
+
skipFile: /\.(?:spec|test|e2e-spec)\.[cm]?[jt]sx?$|\.[cm]?[jt]sx?$/ },
|
|
37
91
|
];
|
|
38
92
|
|
|
39
93
|
|
|
@@ -43,6 +97,7 @@ export function secretHits(file, text) {
|
|
|
43
97
|
for (const rule of SECRET_PATTERNS) {
|
|
44
98
|
if (rule.skipFile?.test(file ?? '')) continue;
|
|
45
99
|
const hit = rule.re.exec(text);
|
|
100
|
+
if (hit && rule.sourceCode?.test(hit[1] ?? '') && /\.[cm]?[jt]sx?$/.test(file ?? '')) continue;
|
|
46
101
|
if (hit && !rule.placeholder?.test(hit[1] ?? '')) names.push(rule.name);
|
|
47
102
|
}
|
|
48
103
|
return names;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// PostgreSQL AST primitives shared by database verbs and synchronous content rules.
|
|
2
|
+
|
|
3
|
+
/** String identifier components from libpg-query's wrapped String nodes. */
|
|
4
|
+
export const sqlStringParts = (nodes) => (nodes ?? []).map((node) => node?.String?.sval).filter((value) => value !== undefined);
|
|
5
|
+
|
|
6
|
+
/** Traverse SQL/PLpgSQL objects; executable scans leave function declarations inert. */
|
|
7
|
+
export function walkSqlAst(value, visit, executable = false) {
|
|
8
|
+
if (!value || typeof value !== 'object' || (executable && value.CreateFunctionStmt)) return;
|
|
9
|
+
visit(value);
|
|
10
|
+
for (const child of Object.values(value)) walkSqlAst(child, visit, executable);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Build a synchronous parser with the caller's failure policy and initialized parser. */
|
|
14
|
+
export function sqlParser(parser, reject = () => null) {
|
|
15
|
+
return (text) => {
|
|
16
|
+
try { return parser?.parseSync(text) ?? null; }
|
|
17
|
+
catch (error) { return reject(error); }
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Resolve named arguments before positional arguments, as PostgreSQL does. */
|
|
22
|
+
export function sqlCallArgument(call, position, names = []) {
|
|
23
|
+
const args = call.args ?? [];
|
|
24
|
+
return args.find((item) => names.includes(item?.NamedArgExpr?.name))?.NamedArgExpr?.arg
|
|
25
|
+
?? args.filter((item) => !item.NamedArgExpr)[position];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Read a literal string through static casts without evaluating an expression. */
|
|
29
|
+
export function sqlStringLiteral(node) {
|
|
30
|
+
if (node?.TypeCast) return sqlStringLiteral(node.TypeCast.arg);
|
|
31
|
+
return node?.A_Const?.sval?.sval ?? null;
|
|
32
|
+
}
|
|
@@ -58,7 +58,7 @@ const KNOWN_IMAGES = Object.freeze({
|
|
|
58
58
|
export const STATEFUL_KINDS = Object.freeze(new Set(['database', 'cache', 'identity', 'storage', 'mail', 'queue', 'search']));
|
|
59
59
|
|
|
60
60
|
/** The repository name of an image reference: `quay.io/keycloak/keycloak:26.0` -> `keycloak`. */
|
|
61
|
-
const repositoryOf = image => {
|
|
61
|
+
export const repositoryOf = image => {
|
|
62
62
|
const withoutDigest = image.split('@')[0];
|
|
63
63
|
const lastSegment = withoutDigest.split('/').at(-1) ?? withoutDigest;
|
|
64
64
|
return lastSegment.split(':')[0];
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Shared static Swarm contract for app checks and rendered deployment evidence.
|
|
2
2
|
import { isPlainObject as plain } from '../../engine/plain-object.mjs';
|
|
3
|
+
import { composeEntries } from './compose-values.mjs';
|
|
4
|
+
import { repositoryOf } from './stack-services.mjs';
|
|
3
5
|
|
|
4
6
|
const PINNED_IMAGE = /@sha256:[a-f0-9]{64}$/i;
|
|
5
7
|
const VERSIONED_SECRET = /-v[1-9][0-9]*$/;
|
|
@@ -29,13 +31,6 @@ function runnableHealthcheck(value) {
|
|
|
29
31
|
return value.slice(1).some(present);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
/** Match image repository names, including registry and vendor namespaces, without matching application tags. */
|
|
33
|
-
function duplicatesManagedBacking(image) {
|
|
34
|
-
if (!present(image)) return false;
|
|
35
|
-
const repository = image.split('@')[0].split('/').at(-1).split(':')[0];
|
|
36
|
-
return BACKING_IMAGE.test(repository);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
34
|
function jobDeclarationIssues(id, component, components, add) {
|
|
40
35
|
if (component.mode !== 'replicated-job') return;
|
|
41
36
|
const scheduler = components[component.scheduler];
|
|
@@ -51,7 +46,7 @@ export function swarmInventoryIssues({ components, lite = false }) {
|
|
|
51
46
|
const add = (at, message) => errors.push({ at, message });
|
|
52
47
|
for (const [id, entry] of Object.entries(catalog)) {
|
|
53
48
|
const component = mapping(entry);
|
|
54
|
-
if (lite &&
|
|
49
|
+
if (lite && present(component.image) && BACKING_IMAGE.test(repositoryOf(component.image)))
|
|
55
50
|
add(`components.${id}.image`, 'lite must use managed Supabase backing services; this image duplicates a Supabase-provided service');
|
|
56
51
|
jobDeclarationIssues(id, component, catalog, add);
|
|
57
52
|
}
|
|
@@ -73,14 +68,6 @@ function serviceSafetyIssues(id, service, add) {
|
|
|
73
68
|
add(`${at}.deploy.restart_policy`, 'every Swarm service needs deploy.restart_policy with a condition');
|
|
74
69
|
}
|
|
75
70
|
|
|
76
|
-
function labelsOf(value) {
|
|
77
|
-
if (!Array.isArray(value)) return mapping(value);
|
|
78
|
-
return Object.fromEntries(value.map(label => {
|
|
79
|
-
const text = String(label), separator = text.indexOf('=');
|
|
80
|
-
return separator < 0 ? [text, ''] : [text.slice(0, separator), text.slice(separator + 1)];
|
|
81
|
-
}));
|
|
82
|
-
}
|
|
83
|
-
|
|
84
71
|
function jobServiceIssues(id, service, component, services, bindings, add) {
|
|
85
72
|
const deploy = mapping(service.deploy), at = `services.${id}.deploy`;
|
|
86
73
|
if (component.mode !== 'replicated-job') {
|
|
@@ -90,7 +77,7 @@ function jobServiceIssues(id, service, component, services, bindings, add) {
|
|
|
90
77
|
if (deploy.mode !== 'replicated-job') add(`${at}.mode`, 'replicated-job component must use deploy.mode: replicated-job');
|
|
91
78
|
if (deploy.replicas !== 0) add(`${at}.replicas`, 'scheduled replicated-job must keep deploy.replicas: 0 until its scheduler triggers it');
|
|
92
79
|
if (deploy.restart_policy?.condition !== 'none') add(`${at}.restart_policy.condition`, 'scheduled replicated-job must use restart_policy.condition: none');
|
|
93
|
-
const labels =
|
|
80
|
+
const labels = Object.fromEntries(composeEntries(deploy.labels));
|
|
94
81
|
if (String(labels['swarm.cronjob.enable']) !== 'true') add(`${at}.labels`, 'scheduled replicated-job needs swarm.cronjob.enable: "true"');
|
|
95
82
|
if (!present(component.schedule) || labels['swarm.cronjob.schedule'] !== component.schedule)
|
|
96
83
|
add(`${at}.labels`, 'swarm.cronjob.schedule must match the component cron schedule');
|
|
@@ -127,7 +114,7 @@ export function swarmModelIssues({ model, components, bindings, lite = false })
|
|
|
127
114
|
add(`services.${id}.ports`, 'Swarm ports must be an array of published port definitions');
|
|
128
115
|
else if (service.ports?.length && component.role !== 'ingress')
|
|
129
116
|
add(`services.${id}.ports`, 'only a component with role ingress may publish Swarm ports');
|
|
130
|
-
if (lite &&
|
|
117
|
+
if (lite && present(service.image) && BACKING_IMAGE.test(repositoryOf(service.image)))
|
|
131
118
|
add(`services.${id}.image`, 'lite stack must not run an image that duplicates a Supabase-provided backing service');
|
|
132
119
|
jobServiceIssues(id, service, component, services, bound, add);
|
|
133
120
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// trim-end.mjs — a string without the run of characters at its end that `matches` accepts.
|
|
2
|
+
|
|
3
|
+
/** `value` with its trailing characters removed while `matches(char)` holds. */
|
|
4
|
+
export const trimEndWhile = (value, matches) => {
|
|
5
|
+
let end = value.length;
|
|
6
|
+
while (end > 0 && matches(value[end - 1])) end -= 1;
|
|
7
|
+
return value.slice(0, end);
|
|
8
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** The expression under the wrappers selected by an AST check's contract. */
|
|
2
|
-
export function unwrapExpression(ts, expression, { awaitExpressions = false, typeAssertions = true, satisfiesExpressions = true } = {}) {
|
|
2
|
+
export function unwrapExpression(ts, expression, { awaitExpressions = false, typeAssertions = true, nonNullExpressions = true, satisfiesExpressions = true } = {}) {
|
|
3
3
|
while (expression && (ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression)
|
|
4
|
-
|| (typeAssertions && ts.isTypeAssertionExpression(expression)) || ts.isNonNullExpression(expression)
|
|
4
|
+
|| (typeAssertions && ts.isTypeAssertionExpression(expression)) || (nonNullExpressions && ts.isNonNullExpression(expression))
|
|
5
5
|
|| (satisfiesExpressions && (ts.isSatisfiesExpression?.(expression) ?? false))
|
|
6
6
|
|| (awaitExpressions && ts.isAwaitExpression(expression)))) expression = expression.expression;
|
|
7
7
|
return expression;
|
|
@@ -18,3 +18,6 @@ export function importsOf(ts, sourceFile, { moduleKey = 'module', stringLiterals
|
|
|
18
18
|
}
|
|
19
19
|
return imports;
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
/** Catalog values unwrap parentheses, `as`, and `satisfies`, preserving assertions and non-null nodes. */
|
|
23
|
+
export const unwrapCatalogExpression = (ts, node) => unwrapExpression(ts, node, { typeAssertions: false, nonNullExpressions: false });
|
|
@@ -11,8 +11,18 @@ export const hasAnyFlag = (flags, mask) => ((flags ?? 0) & mask) !== 0;
|
|
|
11
11
|
/** Whether the variable `declaration` belongs to a `const` list: the TypeScript node-flag test, an explicit comparison of the masked flags. */
|
|
12
12
|
export const isConstVariable = (ts, declaration) => (ts.getCombinedNodeFlags(declaration.parent) & ts.NodeFlags.Const) !== 0;
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
export
|
|
14
|
+
/** Text of a string-literal-like node or an enabled identifier/numeric name; null for other expressions. */
|
|
15
|
+
export function nameText(ts, node, { identifiers = true, numbers = false } = {}) {
|
|
16
|
+
if (!node) return null;
|
|
17
|
+
if (ts.isStringLiteralLike(node) || (identifiers && ts.isIdentifier(node)) || (numbers && ts.isNumericLiteral(node))) return node.text;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A statically selected property name, including string-literal element access; null for computed selections. */
|
|
22
|
+
export function accessName(ts, node) {
|
|
23
|
+
if (ts.isPropertyAccessExpression(node)) return node.name.text;
|
|
24
|
+
return ts.isElementAccessExpression(node) ? literalText(ts, node.argumentExpression) : null;
|
|
25
|
+
}
|
|
16
26
|
|
|
17
27
|
/** The text of a string-literal-like `node` (a string literal or a no-substitution template), or null. */
|
|
18
28
|
export const literalText = (ts, node) => (node && ts.isStringLiteralLike(node) ? node.text : null);
|
|
@@ -42,3 +52,18 @@ export function propertyText(ts, text, { file = 'source.ts', key, declaration =
|
|
|
42
52
|
visit(source);
|
|
43
53
|
return value;
|
|
44
54
|
}
|
|
55
|
+
|
|
56
|
+
/** Initializers of every identifier-named variable, grouped in traversal order across the source's scopes. */
|
|
57
|
+
export function variableInitializerGroups(ts, source) {
|
|
58
|
+
const groups = new Map();
|
|
59
|
+
const collect = (node) => {
|
|
60
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
|
|
61
|
+
const initializers = groups.get(node.name.text) ?? [];
|
|
62
|
+
initializers.push(node.initializer);
|
|
63
|
+
groups.set(node.name.text, initializers);
|
|
64
|
+
}
|
|
65
|
+
ts.forEachChild(node, collect);
|
|
66
|
+
};
|
|
67
|
+
collect(source);
|
|
68
|
+
return groups;
|
|
69
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// GitHub workflow `on` supports a scalar event, an event list or an options map.
|
|
2
|
+
|
|
3
|
+
/** Convert shorthand workflow events into an options map; mappings retain their values. */
|
|
4
|
+
export function workflowEventOptions(on, eventName = (event) => event) {
|
|
5
|
+
if (typeof on === 'string') return { [on]: null };
|
|
6
|
+
if (Array.isArray(on)) return Object.fromEntries(on.map((event) => [eventName(event), null]));
|
|
7
|
+
return on;
|
|
8
|
+
}
|
package/scaffold/add-app.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
} from "../runtime/scripts/hfs/slots.mjs";
|
|
8
8
|
import { imageFiles, render, TEMPLATES_DIR } from "../sync/index.mjs";
|
|
9
9
|
import { ScaffoldError, pascalOf } from "./service.mjs";
|
|
10
|
-
import { PARTIAL_VARIABLES, coreFeApps, jsonText, packageJsonText } from "./app.mjs";
|
|
10
|
+
import { PARTIAL_VARIABLES, bundledPins, callbackSourceFiles, coreFeApps, hasAuthCallback, jsonText, packageJsonText } from "./app.mjs";
|
|
11
11
|
|
|
12
12
|
/** The shared fe app core (modules/api, auth, toast, redux, the providers, the hooks): the base tree, then the edition overlay. */
|
|
13
13
|
const CORE_DIRS = Object.freeze({
|
|
@@ -35,37 +35,8 @@ const fill = (text, values, file) =>
|
|
|
35
35
|
return values[key];
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
* 2026-10-13: lite and full have one fe structure), then registers it in hfs.json. The root workspace glob is added only when an
|
|
41
|
-
* older app manifest does not already carry it.
|
|
42
|
-
*/
|
|
43
|
-
export function addApp({ root, name }) {
|
|
44
|
-
if (!NAME.test(String(name)))
|
|
45
|
-
throw new ScaffoldError(
|
|
46
|
-
"HFS_ADD_NAME_INVALID",
|
|
47
|
-
`app name ${name} must be kebab-case (letters, digits and single dashes)`,
|
|
48
|
-
);
|
|
49
|
-
const declarationFile = path.join(root, "hfs.json");
|
|
50
|
-
const packageFile = path.join(root, "package.json");
|
|
51
|
-
if (!fs.existsSync(declarationFile) || !fs.existsSync(packageFile))
|
|
52
|
-
throw new ScaffoldError(
|
|
53
|
-
"HFS_ADD_NOT_AN_APP",
|
|
54
|
-
"add app runs at the app root containing hfs.json and package.json",
|
|
55
|
-
);
|
|
56
|
-
const manifest = loadSlotManifest();
|
|
57
|
-
const repo = readRepoDeclaration(manifest, root);
|
|
58
|
-
if (!repo.sides?.fe)
|
|
59
|
-
throw new ScaffoldError(
|
|
60
|
-
"HFS_ADD_NO_FRONT_END",
|
|
61
|
-
"this app has no front-end side",
|
|
62
|
-
);
|
|
63
|
-
if (repo.sides.fe.apps.some((app) => app.name === name))
|
|
64
|
-
throw new ScaffoldError(
|
|
65
|
-
"HFS_ADD_EXISTS",
|
|
66
|
-
`front-end app ${name} is already declared`,
|
|
67
|
-
);
|
|
68
|
-
|
|
38
|
+
/** Render the edition skeleton followed by its shared core and edition overlay. */
|
|
39
|
+
function planAppTemplates(repo, name) {
|
|
69
40
|
const source = path.join(
|
|
70
41
|
TEMPLATES_DIR,
|
|
71
42
|
"fe",
|
|
@@ -94,8 +65,13 @@ export function addApp({ root, name }) {
|
|
|
94
65
|
if (!fs.existsSync(dir)) continue;
|
|
95
66
|
for (const relative of listFiles(dir)) byPath.set(relative, { relative: `${targetBase}/${relative}`, body: readTemplate(dir, relative) });
|
|
96
67
|
}
|
|
97
|
-
|
|
68
|
+
return callbackSourceFiles([...byPath.values()].map(file => ({ path: file.relative, content: file.body })), repo)
|
|
69
|
+
.map(file => ({ relative: file.path, body: file.content }));
|
|
70
|
+
}
|
|
98
71
|
|
|
72
|
+
/** The package and TypeScript manifests of a new Next workspace. */
|
|
73
|
+
function planWorkspaceManifests(root, repo, name) {
|
|
74
|
+
const targetBase = `fe/apps/${name}`;
|
|
99
75
|
// The dependency source is a product app (it carries the core's fe-stack packages), else the first Next app.
|
|
100
76
|
const firstApp = coreFeApps(repo.sides.fe.apps)[0] ?? repo.sides.fe.apps.find((app) => app.kind === "next");
|
|
101
77
|
const firstPackage = firstApp
|
|
@@ -108,8 +84,14 @@ export function addApp({ root, name }) {
|
|
|
108
84
|
);
|
|
109
85
|
const workspacePackage = JSON.parse(fs.readFileSync(firstPackage, "utf8"));
|
|
110
86
|
workspacePackage.name = `@${repo.project}/${name}`;
|
|
87
|
+
if (hasAuthCallback(repo)) {
|
|
88
|
+
const pins = bundledPins();
|
|
89
|
+
workspacePackage.dependencies ??= {};
|
|
90
|
+
for (const dependency of ["@supabase/ssr", "@supabase/supabase-js", "server-only"])
|
|
91
|
+
workspacePackage.dependencies[dependency] = pins[dependency].version;
|
|
92
|
+
}
|
|
111
93
|
const appTemplates = path.join(TEMPLATES_DIR, "fe", "app");
|
|
112
|
-
|
|
94
|
+
return [
|
|
113
95
|
{
|
|
114
96
|
relative: `${targetBase}/package.json`,
|
|
115
97
|
body:
|
|
@@ -121,32 +103,10 @@ export function addApp({ root, name }) {
|
|
|
121
103
|
relative: `${targetBase}/tsconfig.json`,
|
|
122
104
|
body: fs.readFileSync(path.join(appTemplates, "tsconfig.json"), "utf8"),
|
|
123
105
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
declaration.sides.fe.apps.push({ name, kind: "next" });
|
|
127
|
-
if (repo.edition === "full") {
|
|
128
|
-
const image = imageFiles(
|
|
129
|
-
resolveRepoDeclaration(manifest, declaration),
|
|
130
|
-
).find((file) => file.path === `${targetBase}/Dockerfile`);
|
|
131
|
-
if (!image)
|
|
132
|
-
throw new ScaffoldError(
|
|
133
|
-
"HFS_ADD_TEMPLATE_MISSING",
|
|
134
|
-
`the Dockerfile of front-end app ${name} did not render`,
|
|
135
|
-
);
|
|
136
|
-
planned.push({ relative: image.path, body: image.content });
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const clashes = planned
|
|
140
|
-
.filter((file) =>
|
|
141
|
-
fs.existsSync(path.join(root, ...file.relative.split("/"))),
|
|
142
|
-
)
|
|
143
|
-
.map((file) => file.relative);
|
|
144
|
-
if (clashes.length)
|
|
145
|
-
throw new ScaffoldError(
|
|
146
|
-
"HFS_ADD_EXISTS",
|
|
147
|
-
`front-end app ${name} already exists: ${clashes.join(", ")}`,
|
|
148
|
-
);
|
|
106
|
+
];
|
|
107
|
+
}
|
|
149
108
|
|
|
109
|
+
function persistAppFiles({ root, name, planned, declaration, declarationFile, packageFile }) {
|
|
150
110
|
const rootPackage = JSON.parse(fs.readFileSync(packageFile, "utf8"));
|
|
151
111
|
const oldDeclaration = fs.readFileSync(declarationFile, "utf8");
|
|
152
112
|
const oldPackage = fs.readFileSync(packageFile, "utf8");
|
|
@@ -173,6 +133,64 @@ export function addApp({ root, name }) {
|
|
|
173
133
|
fs.writeFileSync(packageFile, oldPackage);
|
|
174
134
|
throw error;
|
|
175
135
|
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Adds one Next workspace from the edition's canonical app skeleton and the shared app core (both editions, owner ruling
|
|
140
|
+
* 2026-10-13: lite and full have one fe structure), then registers it in hfs.json. The root workspace glob is added only when an
|
|
141
|
+
* older app manifest does not already carry it.
|
|
142
|
+
*/
|
|
143
|
+
export function addApp({ root, name }) {
|
|
144
|
+
if (!NAME.test(String(name)))
|
|
145
|
+
throw new ScaffoldError(
|
|
146
|
+
"HFS_ADD_NAME_INVALID",
|
|
147
|
+
`app name ${name} must be kebab-case (letters, digits and single dashes)`,
|
|
148
|
+
);
|
|
149
|
+
const declarationFile = path.join(root, "hfs.json");
|
|
150
|
+
const packageFile = path.join(root, "package.json");
|
|
151
|
+
if (!fs.existsSync(declarationFile) || !fs.existsSync(packageFile))
|
|
152
|
+
throw new ScaffoldError(
|
|
153
|
+
"HFS_ADD_NOT_AN_APP",
|
|
154
|
+
"add app runs at the app root containing hfs.json and package.json",
|
|
155
|
+
);
|
|
156
|
+
const manifest = loadSlotManifest();
|
|
157
|
+
const repo = readRepoDeclaration(manifest, root);
|
|
158
|
+
if (!repo.sides?.fe)
|
|
159
|
+
throw new ScaffoldError(
|
|
160
|
+
"HFS_ADD_NO_FRONT_END",
|
|
161
|
+
"this app has no front-end side",
|
|
162
|
+
);
|
|
163
|
+
if (repo.sides.fe.apps.some((app) => app.name === name))
|
|
164
|
+
throw new ScaffoldError(
|
|
165
|
+
"HFS_ADD_EXISTS",
|
|
166
|
+
`front-end app ${name} is already declared`,
|
|
167
|
+
);
|
|
168
|
+
const targetBase = `fe/apps/${name}`;
|
|
169
|
+
const planned = [...planAppTemplates(repo, name), ...planWorkspaceManifests(root, repo, name)];
|
|
170
|
+
const declaration = JSON.parse(fs.readFileSync(declarationFile, "utf8"));
|
|
171
|
+
declaration.sides.fe.apps.push({ name, kind: "next" });
|
|
172
|
+
const image = imageFiles(
|
|
173
|
+
resolveRepoDeclaration(manifest, declaration),
|
|
174
|
+
).find((file) => file.path === `${targetBase}/Dockerfile`);
|
|
175
|
+
if (!image && repo.edition !== "lite")
|
|
176
|
+
throw new ScaffoldError(
|
|
177
|
+
"HFS_ADD_TEMPLATE_MISSING",
|
|
178
|
+
`the Dockerfile of front-end app ${name} did not render`,
|
|
179
|
+
);
|
|
180
|
+
if (image) planned.push({ relative: image.path, body: image.content });
|
|
181
|
+
|
|
182
|
+
const clashes = planned
|
|
183
|
+
.filter((file) =>
|
|
184
|
+
fs.existsSync(path.join(root, ...file.relative.split("/"))),
|
|
185
|
+
)
|
|
186
|
+
.map((file) => file.relative);
|
|
187
|
+
if (clashes.length)
|
|
188
|
+
throw new ScaffoldError(
|
|
189
|
+
"HFS_ADD_EXISTS",
|
|
190
|
+
`front-end app ${name} already exists: ${clashes.join(", ")}`,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
persistAppFiles({ root, name, planned, declaration, declarationFile, packageFile });
|
|
176
194
|
return {
|
|
177
195
|
created: planned.map((file) => file.relative),
|
|
178
196
|
registered: { app: name, workspace: "fe/apps/*" },
|