@starci/hfs 5.1.0 → 5.3.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 +54 -0
- package/README.md +11 -2
- package/cache/hfs-family.d.mts +2 -3
- package/cache/hfs-family.mjs +4 -9
- package/cache/secret-records.d.mts +7 -0
- package/cache/secret-records.mjs +12 -0
- package/cache/session.mjs +4 -2
- package/codemod/account-doors.d.mts +36 -0
- package/codemod/account-doors.mjs +128 -0
- package/codemod/main.mjs +31 -1
- package/codemod/module-definition.d.mts +52 -0
- package/codemod/module-definition.mjs +242 -0
- package/codemod/quality-binding.d.mts +33 -0
- package/codemod/quality-binding.mjs +38 -0
- package/codemod/secrets-cache.d.mts +35 -0
- package/codemod/secrets-cache.mjs +70 -0
- package/codemod/sops-rules.d.mts +34 -0
- package/codemod/sops-rules.mjs +69 -0
- package/codemod/theme-tokens.mjs +48 -2
- package/db/postgres.mjs +9 -1
- package/lint/run.d.mts +6 -0
- package/lint/run.mjs +63 -10
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +65 -11
- package/runtime/knowledge/hfs/fe-stack.yaml +9 -9
- package/runtime/knowledge/hfs/fork-images.yaml +7 -0
- package/runtime/knowledge/hfs/rules.yaml +67 -15
- package/runtime/knowledge/hfs/slots.yaml +174 -30
- package/runtime/modules/kernel/failure-codes.yaml +67 -4
- package/runtime/modules/schemas/application-stacks.schema.yaml +6 -0
- package/runtime/scripts/hfs/architecture/config.d.mts +4 -0
- package/runtime/scripts/hfs/architecture/config.mjs +25 -1
- package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
- package/runtime/scripts/hfs/architecture/module-per-transport.d.mts +1 -1
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +6 -1
- package/runtime/scripts/hfs/architecture/root-packages.d.mts +26 -0
- package/runtime/scripts/hfs/architecture/root-packages.mjs +78 -0
- package/runtime/scripts/hfs/architecture/transport-owner.d.mts +1 -2
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
- package/runtime/scripts/hfs/architecture/type-context.d.mts +4 -1
- package/runtime/scripts/hfs/architecture/type-context.mjs +30 -0
- package/runtime/scripts/hfs/architecture/typescript.d.mts +7 -1
- package/runtime/scripts/hfs/architecture/typescript.mjs +21 -1
- package/runtime/scripts/hfs/explicit-surface-rule.mjs +1 -1
- package/runtime/scripts/hfs/manifest-shape.d.mts +1 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +3 -1
- package/runtime/scripts/hfs/rules/a15-findings.mjs +5 -0
- package/runtime/scripts/hfs/rules/database-access.mjs +39 -1
- package/runtime/scripts/hfs/rules/monorepo.d.mts +4 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +10 -5
- package/runtime/scripts/hfs/rules/secrets.mjs +11 -0
- package/runtime/scripts/hfs/rules/stacks-local.d.mts +7 -0
- package/runtime/scripts/hfs/rules/stacks-local.mjs +71 -0
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +6 -1
- package/runtime/scripts/hfs/rules/stacks-service-images.d.mts +12 -0
- package/runtime/scripts/hfs/rules/stacks-service-images.mjs +120 -0
- package/runtime/scripts/hfs/secret-dev-init.d.mts +11 -0
- package/runtime/scripts/hfs/secret-dev-init.mjs +79 -0
- package/runtime/scripts/hfs/secret-errors.d.mts +3 -0
- package/runtime/scripts/hfs/secret-errors.mjs +8 -0
- package/runtime/scripts/hfs/secret-init.d.mts +6 -0
- package/runtime/scripts/hfs/secret-init.mjs +23 -0
- package/runtime/scripts/hfs/secret-sops.mjs +9 -1
- package/runtime/scripts/hfs/secret-standard.d.mts +20 -0
- package/runtime/scripts/hfs/secret-standard.mjs +106 -0
- package/runtime/scripts/hfs/secret.mjs +15 -8
- package/runtime/scripts/hfs/slots.mjs +1 -1
- package/runtime/scripts/hfs/stack-local-config.d.mts +101 -0
- package/runtime/scripts/hfs/stack-local-config.mjs +320 -0
- package/runtime/scripts/hfs/stacks-gate.mjs +1 -1
- package/runtime/scripts/hfs/view.d.mts +23 -0
- package/runtime/scripts/hfs/view.mjs +68 -0
- package/runtime/scripts/lib/declaration-surface.d.mts +4 -0
- package/runtime/scripts/lib/declaration-surface.mjs +55 -1
- package/runtime/scripts/lib/secret-records.d.mts +13 -0
- package/runtime/scripts/lib/secret-records.mjs +83 -0
- package/runtime/scripts/lib/swarm-stack.d.mts +2 -1
- package/runtime/scripts/lib/swarm-stack.mjs +2 -2
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-module.d.mts +16 -0
- package/scaffold/add-module.mjs +232 -0
- package/scaffold/add-queue-lite.mjs +50 -3
- package/scaffold/add.mjs +5 -0
- package/scaffold/app.d.mts +2 -2
- package/scaffold/app.mjs +16 -18
- package/scaffold/lite-exports.mjs +1 -15
- package/scaffold/module-registry.d.mts +19 -0
- package/scaffold/module-registry.mjs +84 -0
- package/scripts/sync-bundles.d.mts +8 -0
- package/src/main.mjs +23 -2
- package/stack/cli.mjs +6 -1
- package/stack/context.d.mts +15 -0
- package/stack/context.mjs +1 -0
- package/stack/images/openclaw-runtime/Dockerfile +18 -0
- package/stack/images/openclaw-runtime/entrypoint.sh +23 -0
- package/stack/images/openclaw-runtime/openclaw-init.mjs +101 -0
- package/stack/images/openclaw-runtime/openclaw-relay.mjs +25 -0
- package/stack/local-cli.d.mts +25 -0
- package/stack/local-cli.mjs +89 -0
- package/stack/local-config.d.mts +17 -0
- package/stack/local-config.mjs +37 -0
- package/stack/local-custody.d.mts +24 -0
- package/stack/local-custody.mjs +109 -0
- package/stack/local-doctor.d.mts +6 -0
- package/stack/local-doctor.mjs +49 -0
- package/stack/local-down.d.mts +8 -0
- package/stack/local-down.mjs +20 -0
- package/stack/local-host.d.mts +42 -0
- package/stack/local-host.mjs +163 -0
- package/stack/local-logs.d.mts +8 -0
- package/stack/local-logs.mjs +32 -0
- package/stack/local-ports.d.mts +7 -0
- package/stack/local-ports.mjs +39 -0
- package/stack/local-prepare.d.mts +22 -0
- package/stack/local-prepare.mjs +62 -0
- package/stack/local-run.d.mts +71 -0
- package/stack/local-run.mjs +200 -0
- package/stack/local-state.d.mts +35 -0
- package/stack/local-state.mjs +29 -0
- package/stack/local-status.d.mts +6 -0
- package/stack/local-status.mjs +36 -0
- package/stack/local-up.d.mts +11 -0
- package/stack/local-up.mjs +169 -0
- package/stack/local-verify.d.mts +6 -0
- package/stack/local-verify.mjs +21 -0
- package/stack/runtime-layers.d.mts +21 -0
- package/stack/runtime-layers.mjs +67 -0
- package/sync/hygiene.mjs +3 -1
- package/sync/index.d.mts +15 -12
- package/sync/index.mjs +114 -28
- package/templates/app/auth-doors.sql.partial +115 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +1 -1
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -1
- package/templates/app/module/jest.config.js.tpl +8 -0
- package/templates/app/module/src/index.ts.tpl +2 -0
- package/templates/app/module/src/manifest/declaration.spec.ts.tpl +11 -0
- package/templates/app/module/src/manifest/declaration.ts.tpl +13 -0
- package/templates/app/module/src/manifest/index.ts.tpl +1 -0
- package/templates/app/module/src/public/index.ts.tpl +2 -0
- package/templates/app/module/src/template/__name__.template.spec.ts.tpl +13 -0
- package/templates/app/module/src/template/__name__.template.ts.tpl +41 -0
- package/templates/app/module/src/template/index.ts.tpl +1 -0
- package/templates/app/module/src/workbench/components/__pascal__WorkbenchView.tsx.tpl +16 -0
- package/templates/app/module/src/workbench/index.ts.tpl +2 -0
- package/templates/app/package-scripts/core.json +1 -1
- package/templates/app/package-scripts/package.json +1 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +85 -70
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +24 -12
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +43 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +21 -26
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +6 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +6 -1
- package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +76 -13
- package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +44 -0
- package/templates/be/auth-lite/src/features/api/auth/application/confirm.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/confirm.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/forgot.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/forgot.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.query.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-begin.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-begin.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-complete.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/google-complete.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/login.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/login.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/logout.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/logout.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/refresh.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/refresh.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/resend.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/resend.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/reset.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/reset.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/session.command.ts +12 -0
- package/templates/be/auth-lite/src/features/api/auth/application/session.handler.ts +23 -0
- package/templates/be/auth-lite/src/features/api/auth/application/signup.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +6 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +21 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.options.ts +3 -0
- package/templates/be/auth-lite/src/features/api/auth/index.ts +2 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +6 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module.ts +9 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.options.ts +3 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +145 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/forgot.request.ts +16 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/google-start.request.ts +17 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/login.request.ts +16 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/resend.request.ts +16 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +18 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/token.request.ts +11 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +46 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +19 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +209 -0
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +84 -0
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.log-events.ts +6 -0
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +233 -0
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +176 -0
- package/templates/be/auth-lite/src/modules/domain/identity/identity-sign-in.log-events.ts +6 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +98 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-attempt.entity.ts +18 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-email-token.entity.ts +34 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +34 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-session.entity.ts +30 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +10 -0
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +86 -0
- package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +45 -0
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +97 -0
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +81 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +95 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +93 -0
- package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +40 -0
- package/templates/be/inbox/module-definition.ts.tpl +2 -9
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +2 -11
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +3 -10
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +3 -10
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +3 -10
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +3 -9
- package/templates/be/platform/graphql/graphql.module-definition.ts +2 -9
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +3 -9
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +3 -9
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +2 -9
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +3 -9
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +3 -13
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +4 -11
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +4 -9
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +5 -11
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +4 -11
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +2 -9
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -5
- package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +3 -2
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +12 -5
- package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +40 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +22 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +50 -2
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity-mail.messages.ts +65 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +28 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/index.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.config.ts +33 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.decorators.ts +14 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module-definition.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module.ts +27 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.options.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +3 -3
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +3 -1
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +8 -4
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +5 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module.ts +29 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +6 -31
- package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +18 -9
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +35 -11
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +44 -8
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +40 -18
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +18 -6
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.spec.ts +73 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.ts +26 -0
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +24 -53
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.registration.ts +31 -0
- package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +2 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/http-security/rate-limit.guard.spec.ts +11 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +22 -4
- package/templates/be/table/api/feature.module-definition.ts.tpl +3 -9
- package/templates/be/table/api/graphql.module-definition.ts.tpl +3 -9
- package/templates/be/table/module-definition.ts.tpl +2 -9
- package/templates/be/test-world/e2e/serves-once-migrated.e2e-spec.ts.tpl +9 -9
- package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +4 -4
- package/templates/fe/auth-lite/apps/web/src/modules/auth/client.ts +26 -0
- package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +16 -0
- package/templates/fe/auth-lite/apps/web/src/modules/auth/index.ts +5 -0
- package/templates/fe/common/site-shell.tsx.partial +3 -3
- package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +33 -10
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useViewerId.ts +11 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +15 -12
- package/templates/fe/skeleton/apps/__app__/src/modules/viewer-cache/index.ts +5 -0
- package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +1 -1
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +30 -67
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +6 -21
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +10 -16
- package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +4 -7
- package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/index.tsx +2 -3
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/confirm/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/forgot/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/resend/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/reset/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/signup/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +5 -9
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +51 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +84 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +4 -7
- package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/index.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppConfirmPage/index.tsx +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppForgotPage/index.tsx +21 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +11 -3
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +4 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResendPage/index.tsx +21 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResetPage/index.tsx +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppSignupPage/index.tsx +21 -0
- package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/emailAuth.shared.ts +12 -0
- package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/index.ts +2 -0
- package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/useEmailDoor.ts +32 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +50 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +50 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +7 -30
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +13 -14
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +5 -0
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +4 -17
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +0 -16
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +0 -11
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +0 -7
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +0 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +0 -8
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +0 -6
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -186
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +0 -22
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +0 -25
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +0 -104
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +0 -34
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.spec.ts +0 -28
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +0 -11
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +0 -88
- package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +0 -69
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +0 -41
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +0 -40
- package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +0 -63
- package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/classNames.ts +0 -4
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +0 -30
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +0 -50
- package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +0 -31
- package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/classNames.ts +0 -4
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { bundledPins } from '../runtime/scripts/hfs/canon-pins.mjs';
|
|
5
|
+
import { loadTypescript } from '../runtime/scripts/lib/package-at.mjs';
|
|
6
|
+
import { filesUnder, relativePosix, sideFolders } from './source-tree.mjs';
|
|
7
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
const KIT = '@starci/nest-kit';
|
|
9
|
+
const DEFINITION_FILE = /\.module-definition\.tsx?$/;
|
|
10
|
+
const EXPORTED = new Set(['ConfigurableModuleClass', 'MODULE_OPTIONS_TOKEN', 'OPTIONS_TYPE', 'ASYNC_OPTIONS_TYPE']);
|
|
11
|
+
const PRINT_WIDTH = 120;
|
|
12
|
+
const unfit = (source, node, code, reason) => ({ reason, code, line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1 });
|
|
13
|
+
/** The builder expression `new ConfigurableModuleBuilder<X>(args).call().call()...` unwrapped to its `new` and the calls on it, in order; null when `expression` is not one. */
|
|
14
|
+
function builderOf(ts, expression) {
|
|
15
|
+
const calls = [];
|
|
16
|
+
let node = expression;
|
|
17
|
+
while (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression)) {
|
|
18
|
+
calls.unshift({ name: node.expression.name.text, call: node });
|
|
19
|
+
node = node.expression.expression;
|
|
20
|
+
}
|
|
21
|
+
return ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'ConfigurableModuleBuilder' ? { created: node, calls } : null;
|
|
22
|
+
}
|
|
23
|
+
/** The text of a statement's own JSDoc comment (without the delimiters), or an empty string. */
|
|
24
|
+
function docOf(ts, source, statement) {
|
|
25
|
+
const ranges = ts.getLeadingCommentRanges(source.text, statement.pos) ?? [];
|
|
26
|
+
const last = ranges.at(-1);
|
|
27
|
+
const text = last === undefined ? '' : source.text.slice(last.pos, last.end);
|
|
28
|
+
return text.startsWith('/**') ? text.slice(3, -2).replace(/^\s*\*\s?/gm, '').trim() : '';
|
|
29
|
+
}
|
|
30
|
+
/** Reads the builder part of a definition: options type, token, representative or not; a reason when the chain is not one the helpers stand for. */
|
|
31
|
+
function readBuilder(ts, source, node) {
|
|
32
|
+
const built = builderOf(ts, node);
|
|
33
|
+
if (built === null)
|
|
34
|
+
return unfit(source, node, 'MODULE_DEFINITION_NOT_A_BUILDER', 'the definition is not `new ConfigurableModuleBuilder<XOptions>(...)` with its calls and `.build()`');
|
|
35
|
+
const { created, calls } = built;
|
|
36
|
+
const options = created.typeArguments?.[0];
|
|
37
|
+
if (options === undefined)
|
|
38
|
+
return unfit(source, created, 'MODULE_DEFINITION_UNTYPED', 'the builder states no options type; write `ConfigurableModuleBuilder<XOptions>` and run the codemod again');
|
|
39
|
+
let token = null;
|
|
40
|
+
const argument = created.arguments?.[0];
|
|
41
|
+
if (argument !== undefined) {
|
|
42
|
+
const only = ts.isObjectLiteralExpression(argument) && argument.properties.length === 1 ? argument.properties[0] : undefined;
|
|
43
|
+
if (only === undefined || !ts.isPropertyAssignment(only) || !ts.isIdentifier(only.name) || only.name.text !== 'optionsInjectionToken' || !ts.isIdentifier(only.initializer)) {
|
|
44
|
+
return unfit(source, argument, 'MODULE_DEFINITION_BUILDER_OPTIONS', 'the helpers take `optionsInjectionToken` as an identifier and no other builder setting; keep this definition and ask for the setting in @starci/nest-kit');
|
|
45
|
+
}
|
|
46
|
+
token = only.initializer.text;
|
|
47
|
+
}
|
|
48
|
+
const names = calls.map((entry) => entry.name);
|
|
49
|
+
if (names.at(-1) !== 'build')
|
|
50
|
+
return unfit(source, node, 'MODULE_DEFINITION_NOT_BUILT', 'the chain does not end in `.build()`');
|
|
51
|
+
const extra = names.filter((name) => name !== 'build' && name !== 'setExtras' && name !== 'setClassMethodName');
|
|
52
|
+
if (extra.length > 0)
|
|
53
|
+
return unfit(source, node, 'MODULE_DEFINITION_CHAIN', `\`.${extra[0]}(...)\` has no helper; the helpers define \`register\` (and \`isGlobal\` for a capability) only`);
|
|
54
|
+
const named = calls.find((entry) => entry.name === 'setClassMethodName');
|
|
55
|
+
const method = named?.call.arguments[0];
|
|
56
|
+
if (method !== undefined && !(ts.isStringLiteral(method) && method.text === 'register'))
|
|
57
|
+
return unfit(source, method, 'MODULE_DEFINITION_CHAIN', 'the class method is not "register"; static-module-register allows `register` only');
|
|
58
|
+
const extras = calls.find((entry) => entry.name === 'setExtras');
|
|
59
|
+
if (extras !== undefined) {
|
|
60
|
+
const defaults = extras.call.arguments[0];
|
|
61
|
+
const standard = defaults !== undefined && ts.isObjectLiteralExpression(defaults) && defaults.properties.length === 1
|
|
62
|
+
&& ts.isPropertyAssignment(defaults.properties[0]) && defaults.properties[0].initializer.kind === ts.SyntaxKind.FalseKeyword
|
|
63
|
+
&& defaults.properties[0].name.getText(source) === 'isGlobal';
|
|
64
|
+
if (!standard)
|
|
65
|
+
return unfit(source, extras.call, 'MODULE_DEFINITION_CHAIN', 'the extras are not `{ isGlobal: false }`; the capability helper is the one standard shape');
|
|
66
|
+
}
|
|
67
|
+
return { options: options.getText(source), token, representative: extras !== undefined };
|
|
68
|
+
}
|
|
69
|
+
/** The reading of one definition file, in either written form, or why it is left alone (null: no definition here, nothing to do). */
|
|
70
|
+
function readDefinition(ts, source) {
|
|
71
|
+
const statements = [...source.statements];
|
|
72
|
+
// The destructured form: `export const { A, B } = new ConfigurableModuleBuilder<X>()....build()`.
|
|
73
|
+
for (const statement of statements) {
|
|
74
|
+
if (!ts.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword))
|
|
75
|
+
continue;
|
|
76
|
+
const declaration = statement.declarationList.declarations[0];
|
|
77
|
+
if (declaration === undefined || !ts.isObjectBindingPattern(declaration.name) || declaration.initializer === undefined || builderOf(ts, declaration.initializer) === null)
|
|
78
|
+
continue;
|
|
79
|
+
const read = readBuilder(ts, source, declaration.initializer);
|
|
80
|
+
if ('reason' in read)
|
|
81
|
+
return read;
|
|
82
|
+
const names = [];
|
|
83
|
+
for (const element of declaration.name.elements) {
|
|
84
|
+
if (!ts.isIdentifier(element.name) || element.propertyName !== undefined || element.dotDotDotToken !== undefined || !EXPORTED.has(element.name.text))
|
|
85
|
+
return unfit(source, element, 'MODULE_DEFINITION_NAMES', 'a name is renamed, spread or not one of ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE, ASYNC_OPTIONS_TYPE');
|
|
86
|
+
names.push(element.name.text);
|
|
87
|
+
}
|
|
88
|
+
return { ...read, names, doc: docOf(ts, source, statement), drop: new Set(), at: statement, from: 'destructured builder' };
|
|
89
|
+
}
|
|
90
|
+
// The typed form: `const built = new ConfigurableModuleBuilder<X>()....build()`, `type Host = ConfigurableModuleHost<...>`, `export const A: Host["A"] = built.A`.
|
|
91
|
+
const builtStatement = statements.find((statement) => ts.isVariableStatement(statement) && statement.declarationList.declarations.some((declaration) => declaration.initializer !== undefined && builderOf(ts, declaration.initializer) !== null));
|
|
92
|
+
if (builtStatement === undefined || !ts.isVariableStatement(builtStatement))
|
|
93
|
+
return null;
|
|
94
|
+
const declaration = builtStatement.declarationList.declarations[0];
|
|
95
|
+
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined)
|
|
96
|
+
return unfit(source, builtStatement, 'MODULE_DEFINITION_NOT_A_BUILDER', 'the builder is not bound to one name');
|
|
97
|
+
const read = readBuilder(ts, source, declaration.initializer);
|
|
98
|
+
if ('reason' in read)
|
|
99
|
+
return read;
|
|
100
|
+
const local = declaration.name.text;
|
|
101
|
+
const drop = new Set([builtStatement]);
|
|
102
|
+
const names = [];
|
|
103
|
+
let doc = '';
|
|
104
|
+
let at = null;
|
|
105
|
+
for (const statement of statements) {
|
|
106
|
+
if (ts.isTypeAliasDeclaration(statement) && ts.isTypeReferenceNode(statement.type) && ts.isIdentifier(statement.type.typeName) && statement.type.typeName.text === 'ConfigurableModuleHost') {
|
|
107
|
+
drop.add(statement);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!ts.isVariableStatement(statement) || statement === builtStatement)
|
|
111
|
+
continue;
|
|
112
|
+
const exported = statement.declarationList.declarations[0];
|
|
113
|
+
const initializer = exported?.initializer;
|
|
114
|
+
const typed = initializer !== undefined && ts.isPropertyAccessExpression(initializer) && ts.isIdentifier(initializer.expression) && initializer.expression.text === local && ts.isIdentifier(initializer.name);
|
|
115
|
+
if (exported === undefined || !typed || !ts.isIdentifier(exported.name) || initializer.name.text !== exported.name.text || !EXPORTED.has(exported.name.text))
|
|
116
|
+
continue;
|
|
117
|
+
names.push(exported.name.text);
|
|
118
|
+
drop.add(statement);
|
|
119
|
+
if (at === null) {
|
|
120
|
+
at = statement;
|
|
121
|
+
doc = docOf(ts, source, statement);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (at === null || names.length === 0)
|
|
125
|
+
return unfit(source, builtStatement, 'MODULE_DEFINITION_NAMES', 'the built definition is not re-exported by name (`export const ConfigurableModuleClass: Host["ConfigurableModuleClass"] = built.ConfigurableModuleClass`)');
|
|
126
|
+
drop.delete(at);
|
|
127
|
+
// Anything else that still reads `built` or `Host` would break once they are gone.
|
|
128
|
+
const stillUsed = statements.some((statement) => !drop.has(statement) && statement !== at && !ts.isImportDeclaration(statement) && /\b(?:built|Host)\b/.test(statement.getText(source)));
|
|
129
|
+
if (stillUsed)
|
|
130
|
+
return unfit(source, builtStatement, 'MODULE_DEFINITION_NAMES', 'another statement of the file reads the builder result or `Host`');
|
|
131
|
+
return { ...read, names: [...names].sort((a, b) => [...EXPORTED].indexOf(a) - [...EXPORTED].indexOf(b)), doc, drop, at, from: 'typed builder' };
|
|
132
|
+
}
|
|
133
|
+
/** The one export of the new definition, laid out the way Prettier prints it at the width of the canon (break the arguments of a call that takes a token, else after the `=`). */
|
|
134
|
+
function exportLine(reading, helper) {
|
|
135
|
+
const head = `export const { ${reading.names.join(', ')} } =`;
|
|
136
|
+
const call = `${helper}<${reading.options}>(${reading.token === null ? '' : `{ optionsInjectionToken: ${reading.token} }`})`;
|
|
137
|
+
if (`${head} ${call}`.length <= PRINT_WIDTH)
|
|
138
|
+
return `${head} ${call}`;
|
|
139
|
+
return reading.token === null ? `${head}\n ${call}` : `${head} ${helper}<${reading.options}>({\n optionsInjectionToken: ${reading.token},\n})`;
|
|
140
|
+
}
|
|
141
|
+
/** The import declaration of `@nestjs/common` without the builder names, or null when nothing else is imported from it. */
|
|
142
|
+
function nestImport(ts, source, statement) {
|
|
143
|
+
const bindings = statement.importClause?.namedBindings;
|
|
144
|
+
if (bindings === undefined || !ts.isNamedImports(bindings))
|
|
145
|
+
return statement.getText(source);
|
|
146
|
+
const kept = bindings.elements.filter((element) => !['ConfigurableModuleBuilder', 'ConfigurableModuleHost'].includes((element.propertyName ?? element.name).text));
|
|
147
|
+
if (kept.length === bindings.elements.length)
|
|
148
|
+
return statement.getText(source);
|
|
149
|
+
if (kept.length === 0 && statement.importClause?.name === undefined)
|
|
150
|
+
return null;
|
|
151
|
+
return `import { ${kept.map((element) => element.getText(source)).join(', ')} } from "@nestjs/common"`;
|
|
152
|
+
}
|
|
153
|
+
/** Rewrite one definition file's text to the helper form; `text` unchanged when the file holds no definition the codemod can rewrite. */
|
|
154
|
+
export function transformModuleDefinition({ ts, text, file = 'x.module-definition.ts' }) {
|
|
155
|
+
const none = { text, changes: [], findings: [] };
|
|
156
|
+
if (!text.includes('ConfigurableModuleBuilder'))
|
|
157
|
+
return none;
|
|
158
|
+
const source = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
159
|
+
const read = readDefinition(ts, source);
|
|
160
|
+
if (read === null)
|
|
161
|
+
return none;
|
|
162
|
+
if ('reason' in read)
|
|
163
|
+
return { ...none, findings: [{ file, line: read.line, code: read.code, reason: read.reason }] };
|
|
164
|
+
const helper = read.representative ? 'defineCapabilityModule' : 'defineConfigurableModule';
|
|
165
|
+
const imports = [`import { ${helper} } from "${KIT}"`];
|
|
166
|
+
const body = [];
|
|
167
|
+
for (const statement of source.statements) {
|
|
168
|
+
if (ts.isImportDeclaration(statement)) {
|
|
169
|
+
const nest = ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === '@nestjs/common' ? nestImport(ts, source, statement) : statement.getText(source);
|
|
170
|
+
if (nest !== null)
|
|
171
|
+
imports.push(nest);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (read.drop.has(statement))
|
|
175
|
+
continue;
|
|
176
|
+
if (statement === read.at) {
|
|
177
|
+
body.push(`${read.doc === '' ? '' : `/** ${read.doc} */\n`}${exportLine(read, helper)}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
body.push(text.slice(statement.pos, statement.end).replace(/^\s*\n/, '').trimEnd());
|
|
181
|
+
}
|
|
182
|
+
const output = `${imports.join('\n')}\n\n${body.join('\n\n')}\n`;
|
|
183
|
+
return { text: output, changes: [{ file, from: read.from, helper, names: read.names }], findings: [] };
|
|
184
|
+
}
|
|
185
|
+
/** The `@starci/nest-kit` requirement the codemod adds to an app's root package.json. */
|
|
186
|
+
function kitVersion() {
|
|
187
|
+
return bundledPins()[KIT]?.version ?? '0.1.0';
|
|
188
|
+
}
|
|
189
|
+
/** Preview or apply the rewrite in one app: every `*.module-definition.ts` under its sides, then the dependency in the root package.json. */
|
|
190
|
+
export function migrateModuleDefinition({ root, write = false, ts = loadTypescript(root, PACKAGE_ROOT) }) {
|
|
191
|
+
if (!ts)
|
|
192
|
+
throw new Error('module-definition needs TypeScript installed in the app or @starci/hfs development checkout');
|
|
193
|
+
const folders = sideFolders(root);
|
|
194
|
+
if (!folders.length)
|
|
195
|
+
throw new Error('module-definition runs at an app root with a be/ directory');
|
|
196
|
+
const packageFile = path.join(root, 'package.json');
|
|
197
|
+
const manifest = fs.existsSync(packageFile) ? JSON.parse(fs.readFileSync(packageFile, 'utf8')) : null; // reason: package.json parsed from disk, only dependencies is read
|
|
198
|
+
const declared = manifest?.dependencies?.[KIT] !== undefined;
|
|
199
|
+
const report = { schema: 'starci/module-definition-report@1', root, write, filesScanned: 0, filesChanged: [], changes: [], findings: [], dependency: { name: KIT, version: kitVersion(), declared, added: false } };
|
|
200
|
+
for (const target of folders.flatMap((folder) => filesUnder(folder, DEFINITION_FILE))) {
|
|
201
|
+
const file = relativePosix(root, target);
|
|
202
|
+
const original = fs.readFileSync(target, 'utf8').replace(/\r\n/g, '\n');
|
|
203
|
+
const result = transformModuleDefinition({ ts, text: original, file });
|
|
204
|
+
report.filesScanned += 1;
|
|
205
|
+
report.changes.push(...result.changes);
|
|
206
|
+
report.findings.push(...result.findings);
|
|
207
|
+
if (result.text !== original) {
|
|
208
|
+
report.filesChanged.push(file);
|
|
209
|
+
if (write)
|
|
210
|
+
fs.writeFileSync(target, result.text);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
if (report.filesChanged.length > 0 && !declared && manifest !== null) {
|
|
214
|
+
report.dependency.added = write;
|
|
215
|
+
if (write) {
|
|
216
|
+
manifest.dependencies = Object.fromEntries(Object.entries({ ...manifest.dependencies, [KIT]: report.dependency.version }).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
217
|
+
fs.writeFileSync(packageFile, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return report;
|
|
221
|
+
}
|
|
222
|
+
const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
223
|
+
/** Render the rewrite and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
224
|
+
export function moduleDefinitionMarkdown(report) {
|
|
225
|
+
const out = ['# module-definition', '', `Scanned ${report.filesScanned} definitions, ${report.filesChanged.length} ${report.write ? 'rewritten' : 'would be rewritten'}, ${report.findings.length} left for a person.`, ''];
|
|
226
|
+
if (report.findings.length) {
|
|
227
|
+
out.push('## Left for a person', '', '| location | code | reason |', '|---|---|---|');
|
|
228
|
+
for (const finding of report.findings)
|
|
229
|
+
out.push(`| ${cell(`${finding.file}:${finding.line}`)} | ${finding.code} | ${cell(finding.reason)} |`);
|
|
230
|
+
out.push('');
|
|
231
|
+
}
|
|
232
|
+
if (report.changes.length) {
|
|
233
|
+
out.push('## Rewritten', '', '| file | from | helper | names |', '|---|---|---|---|');
|
|
234
|
+
for (const item of report.changes)
|
|
235
|
+
out.push(`| ${cell(item.file)} | ${item.from} | ${item.helper} | ${cell(item.names.join(', '))} |`);
|
|
236
|
+
out.push('');
|
|
237
|
+
}
|
|
238
|
+
if (report.filesChanged.length > 0 && !report.dependency.declared) {
|
|
239
|
+
out.push('## Dependency', '', report.dependency.added ? `\`${KIT}@${report.dependency.version}\` was added to package.json: run \`npm install\`.` : `\`${KIT}@${report.dependency.version}\` is added to package.json by \`--execute\`; then run \`npm install\`.`, '');
|
|
240
|
+
}
|
|
241
|
+
return `${out.join('\n')}\n`;
|
|
242
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** One tooling member the migration bound to its service. */
|
|
2
|
+
export interface QualityBindingChange {
|
|
3
|
+
environment: string;
|
|
4
|
+
member: string;
|
|
5
|
+
service: string;
|
|
6
|
+
keys: string[];
|
|
7
|
+
}
|
|
8
|
+
/** One case left for a person: a binding the edit cannot place, or a runtime entry to delete. */
|
|
9
|
+
export interface QualityBindingFinding {
|
|
10
|
+
environment: string;
|
|
11
|
+
member: string;
|
|
12
|
+
code: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
/** The report of a migration (`starci app codemod quality-binding`). */
|
|
16
|
+
export interface QualityBindingReport {
|
|
17
|
+
schema: string;
|
|
18
|
+
root: string;
|
|
19
|
+
write: boolean;
|
|
20
|
+
changes: QualityBindingChange[];
|
|
21
|
+
findings: QualityBindingFinding[];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Bind the sealed tooling members of an app scaffolded before the binding was rendered (sonar.enc.yaml with `services.sonar.credentials: []`) to their
|
|
25
|
+
* service: the credential and the `ci.secrets` entry the template shows. A runtime `environments.<env>.secrets` entry an earlier `starci app secret set`
|
|
26
|
+
* declared for such a member is named for the owner to delete, since CI custody refuses a CI secret granted at runtime (APP_SECRET_IN_CI).
|
|
27
|
+
*/
|
|
28
|
+
export declare function migrateQualityBinding({ root, write }: {
|
|
29
|
+
root: string;
|
|
30
|
+
write?: boolean;
|
|
31
|
+
}): QualityBindingReport;
|
|
32
|
+
/** Render the binding and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
33
|
+
export declare function qualityBindingMarkdown(report: QualityBindingReport): string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { bindToolingMembers } from '../runtime/scripts/hfs/secret-standard.mjs';
|
|
2
|
+
/**
|
|
3
|
+
* Bind the sealed tooling members of an app scaffolded before the binding was rendered (sonar.enc.yaml with `services.sonar.credentials: []`) to their
|
|
4
|
+
* service: the credential and the `ci.secrets` entry the template shows. A runtime `environments.<env>.secrets` entry an earlier `starci app secret set`
|
|
5
|
+
* declared for such a member is named for the owner to delete, since CI custody refuses a CI secret granted at runtime (APP_SECRET_IN_CI).
|
|
6
|
+
*/
|
|
7
|
+
export function migrateQualityBinding({ root, write = false }) {
|
|
8
|
+
const report = { schema: 'starci/quality-binding-report@1', root, write, changes: [], findings: [] };
|
|
9
|
+
for (const item of bindToolingMembers(root, write)) {
|
|
10
|
+
if (item.outcome.bound.length > 0)
|
|
11
|
+
report.changes.push({ environment: item.environment, member: item.member, service: item.service, keys: item.outcome.bound });
|
|
12
|
+
else if (!write && item.outcome.remedy !== '')
|
|
13
|
+
report.changes.push({ environment: item.environment, member: item.member, service: item.service, keys: [] });
|
|
14
|
+
else if (item.outcome.remedy !== '')
|
|
15
|
+
report.findings.push({ environment: item.environment, member: item.member, code: 'QUALITY_BINDING_BY_HAND', reason: item.outcome.remedy });
|
|
16
|
+
if (item.runtimeEntries.length > 0)
|
|
17
|
+
report.findings.push({ environment: item.environment, member: item.member, code: 'QUALITY_RUNTIME_ENTRY', reason: `environments.${item.environment}.secrets declares ${item.runtimeEntries.join(', ')} for the tooling member ${item.member}; delete the entry, the token is read by CI through services.${item.service}.credentials` });
|
|
18
|
+
}
|
|
19
|
+
return report;
|
|
20
|
+
}
|
|
21
|
+
const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
22
|
+
/** Render the binding and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
23
|
+
export function qualityBindingMarkdown(report) {
|
|
24
|
+
const out = ['# quality-binding', '', `${report.changes.length} tooling member(s) ${report.write ? 'bound' : 'would be bound'}, ${report.findings.length} left for a person.`, ''];
|
|
25
|
+
if (report.findings.length) {
|
|
26
|
+
out.push('## Left for a person', '', '| environment | member | code | reason |', '|---|---|---|---|');
|
|
27
|
+
for (const finding of report.findings)
|
|
28
|
+
out.push(`| ${finding.environment} | ${finding.member} | ${finding.code} | ${cell(finding.reason)} |`);
|
|
29
|
+
out.push('');
|
|
30
|
+
}
|
|
31
|
+
if (report.changes.length) {
|
|
32
|
+
out.push('## Bound', '', '| environment | member | service |', '|---|---|---|');
|
|
33
|
+
for (const item of report.changes)
|
|
34
|
+
out.push(`| ${item.environment} | ${item.member} | ${item.service} |`);
|
|
35
|
+
out.push('');
|
|
36
|
+
}
|
|
37
|
+
return `${out.join('\n')}\n`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** One cache file the migration rewrites: the records whose message became a digest or whose payload was dropped. */
|
|
2
|
+
export interface SecretsCacheChange {
|
|
3
|
+
file: string;
|
|
4
|
+
records: number;
|
|
5
|
+
messagesDigested: number;
|
|
6
|
+
payloadsDropped: number;
|
|
7
|
+
}
|
|
8
|
+
/** One case left for a person: a file whose lines the migration cannot turn into the contract's records. */
|
|
9
|
+
export interface SecretsCacheFinding {
|
|
10
|
+
file: string;
|
|
11
|
+
code: string;
|
|
12
|
+
reason: string;
|
|
13
|
+
}
|
|
14
|
+
/** The report of a migration (`starci app codemod secrets-cache`). */
|
|
15
|
+
export interface SecretsCacheReport {
|
|
16
|
+
schema: string;
|
|
17
|
+
root: string;
|
|
18
|
+
write: boolean;
|
|
19
|
+
filesScanned: number;
|
|
20
|
+
filesChanged: string[];
|
|
21
|
+
changes: SecretsCacheChange[];
|
|
22
|
+
findings: SecretsCacheFinding[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Migrate the secrets cache of one app to the record contract of the secrets family (contexts/project/check-cache.md, "The secrets records"):
|
|
26
|
+
* every message becomes its digest and a record's `data` is dropped, in the canonical form the cache writes. The migration needs no run of
|
|
27
|
+
* the gate, because the digest of a message is the identity the family compares; a file it cannot read as records is left for a person, who
|
|
28
|
+
* deletes it and runs `starci app check --gate secrets`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function migrateSecretsCache({ root, write }: {
|
|
31
|
+
root: string;
|
|
32
|
+
write?: boolean;
|
|
33
|
+
}): SecretsCacheReport;
|
|
34
|
+
/** Render the migration and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
35
|
+
export declare function secretsCacheMarkdown(report: SecretsCacheReport): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseRecords, serializeRecords } from '../cache/jsonl.mjs';
|
|
4
|
+
import { safeSecretRecords } from '../cache/secret-records.mjs';
|
|
5
|
+
import { SECRETS_CACHE_FILE, secretRecordFaults } from '../runtime/scripts/lib/secret-records.mjs';
|
|
6
|
+
import { relativePosix } from './source-tree.mjs';
|
|
7
|
+
const CACHE_FOLDER = '.starcistacks/cache/check/secrets';
|
|
8
|
+
/** Every file of the secrets cache folder under `root`, app-relative and sorted. */
|
|
9
|
+
function cacheFiles(root) {
|
|
10
|
+
const folder = path.join(root, CACHE_FOLDER);
|
|
11
|
+
if (fs.statSync(folder, { throwIfNoEntry: false })?.isDirectory() !== true)
|
|
12
|
+
return [];
|
|
13
|
+
return fs.readdirSync(folder, { recursive: true, withFileTypes: true })
|
|
14
|
+
.filter((entry) => entry.isFile())
|
|
15
|
+
.map((entry) => relativePosix(root, path.join(entry.parentPath, entry.name)))
|
|
16
|
+
.filter((file) => SECRETS_CACHE_FILE.test(file))
|
|
17
|
+
.sort();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Migrate the secrets cache of one app to the record contract of the secrets family (contexts/project/check-cache.md, "The secrets records"):
|
|
21
|
+
* every message becomes its digest and a record's `data` is dropped, in the canonical form the cache writes. The migration needs no run of
|
|
22
|
+
* the gate, because the digest of a message is the identity the family compares; a file it cannot read as records is left for a person, who
|
|
23
|
+
* deletes it and runs `starci app check --gate secrets`.
|
|
24
|
+
*/
|
|
25
|
+
export function migrateSecretsCache({ root, write = false }) {
|
|
26
|
+
const report = { schema: 'starci/secrets-cache-report@1', root, write, filesScanned: 0, filesChanged: [], changes: [], findings: [] };
|
|
27
|
+
for (const file of cacheFiles(root)) {
|
|
28
|
+
report.filesScanned += 1;
|
|
29
|
+
const target = path.join(root, file);
|
|
30
|
+
const original = fs.readFileSync(target, 'utf8');
|
|
31
|
+
const parsed = parseRecords(original);
|
|
32
|
+
if (parsed.invalid > 0) {
|
|
33
|
+
report.findings.push({ file, code: 'SECRETS_CACHE_UNREADABLE', reason: `${parsed.invalid} line${parsed.invalid === 1 ? ' is' : 's are'} not a cache record; delete the file and run starci app check --gate secrets` });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const migrated = safeSecretRecords(parsed.records);
|
|
37
|
+
const text = serializeRecords(migrated);
|
|
38
|
+
const remaining = secretRecordFaults(text);
|
|
39
|
+
if (remaining.length > 0) {
|
|
40
|
+
report.findings.push({ file, code: 'SECRETS_CACHE_NOT_MIGRATABLE', reason: `${remaining[0] ?? 'a record'}; delete the file and run starci app check --gate secrets` });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (text === original)
|
|
44
|
+
continue;
|
|
45
|
+
const digested = parsed.records.reduce((count, record, index) => count + record.findings.filter((finding, at) => finding.message !== migrated[index]?.findings[at]?.message).length, 0);
|
|
46
|
+
report.filesChanged.push(file);
|
|
47
|
+
report.changes.push({ file, records: parsed.records.length, messagesDigested: digested, payloadsDropped: parsed.records.filter((record) => record.data !== undefined).length });
|
|
48
|
+
if (write)
|
|
49
|
+
fs.writeFileSync(target, text);
|
|
50
|
+
}
|
|
51
|
+
return report;
|
|
52
|
+
}
|
|
53
|
+
const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
54
|
+
/** Render the migration and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
55
|
+
export function secretsCacheMarkdown(report) {
|
|
56
|
+
const out = ['# secrets-cache', '', `Scanned ${report.filesScanned} cache files, ${report.filesChanged.length} ${report.write ? 'migrated' : 'would be migrated'}, ${report.findings.length} left for a person.`, ''];
|
|
57
|
+
if (report.findings.length) {
|
|
58
|
+
out.push('## Left for a person', '', '| file | code | reason |', '|---|---|---|');
|
|
59
|
+
for (const finding of report.findings)
|
|
60
|
+
out.push(`| ${cell(finding.file)} | ${finding.code} | ${cell(finding.reason)} |`);
|
|
61
|
+
out.push('');
|
|
62
|
+
}
|
|
63
|
+
if (report.changes.length) {
|
|
64
|
+
out.push('## Migrated', '', '| file | records | messages digested | payloads dropped |', '|---|---|---|---|');
|
|
65
|
+
for (const item of report.changes)
|
|
66
|
+
out.push(`| ${cell(item.file)} | ${item.records} | ${item.messagesDigested} | ${item.payloadsDropped} |`);
|
|
67
|
+
out.push('');
|
|
68
|
+
}
|
|
69
|
+
return `${out.join('\n')}\n`;
|
|
70
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** One creation rule the migration rewrites: the rule's index, the path_regex it had and the one it has. */
|
|
2
|
+
export interface SopsRuleChange {
|
|
3
|
+
rule: number;
|
|
4
|
+
from: string;
|
|
5
|
+
to: string;
|
|
6
|
+
}
|
|
7
|
+
/** One case left for a person. */
|
|
8
|
+
export interface SopsRuleFinding {
|
|
9
|
+
file: string;
|
|
10
|
+
code: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
}
|
|
13
|
+
/** The report of a migration (`starci app codemod sops-rules`). */
|
|
14
|
+
export interface SopsRulesReport {
|
|
15
|
+
schema: string;
|
|
16
|
+
root: string;
|
|
17
|
+
write: boolean;
|
|
18
|
+
file: string;
|
|
19
|
+
changes: SopsRuleChange[];
|
|
20
|
+
findings: SopsRuleFinding[];
|
|
21
|
+
}
|
|
22
|
+
/** The path_regex a managed `.sops.yaml` carries for a rule written with `from`, or `from` when it is already managed or is no member rule. */
|
|
23
|
+
export declare function managedPathRegex(from: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Migrate a legacy `.sops.yaml` to the form `starci app secret init --env` writes (public recipients only, a rule per member spelling): a rule whose
|
|
26
|
+
* path_regex matches the legacy `.enc` spelling alone also matches `.enc.yaml`, the spelling `secret set` writes, which sops otherwise refuses with
|
|
27
|
+
* no creation rule. Recipients and the other rules are kept; nothing private is read or written.
|
|
28
|
+
*/
|
|
29
|
+
export declare function migrateSopsRules({ root, write }: {
|
|
30
|
+
root: string;
|
|
31
|
+
write?: boolean;
|
|
32
|
+
}): SopsRulesReport;
|
|
33
|
+
/** Render the migration and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
34
|
+
export declare function sopsRulesMarkdown(report: SopsRulesReport): string;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parseYaml, stringifyYaml } from '../runtime/engine/yaml.mjs';
|
|
4
|
+
const FILE = '.sops.yaml';
|
|
5
|
+
/** The rule an earlier `secret init` wrote: it matches the legacy `.enc` spelling of a member only. */
|
|
6
|
+
const LEGACY = String.raw `\.starcistacks[/\\][^/\\]+[/\\]secrets[/\\][^/\\]+\.enc$`;
|
|
7
|
+
/** The rule `secret init` writes now: both spellings of a member (`.enc` and `.enc.yaml`), every environment. */
|
|
8
|
+
const CURRENT = String.raw `\.starcistacks[/\\][^/\\]+[/\\]secrets[/\\][^/\\]+\.enc(?:\.yaml)?$`;
|
|
9
|
+
/** A per-environment rule written for the legacy spelling alone: `.enc$` where the managed form ends `.enc(?:\.yaml)?$`. */
|
|
10
|
+
const LEGACY_END = String.raw `\.enc$`;
|
|
11
|
+
const MODERN_END = String.raw `\.enc(?:\.yaml)?$`;
|
|
12
|
+
/** The path_regex a managed `.sops.yaml` carries for a rule written with `from`, or `from` when it is already managed or is no member rule. */
|
|
13
|
+
export function managedPathRegex(from) {
|
|
14
|
+
if (from === LEGACY)
|
|
15
|
+
return CURRENT;
|
|
16
|
+
return /secrets/.test(from) && from.endsWith(LEGACY_END) ? `${from.slice(0, -LEGACY_END.length)}${MODERN_END}` : from;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Migrate a legacy `.sops.yaml` to the form `starci app secret init --env` writes (public recipients only, a rule per member spelling): a rule whose
|
|
20
|
+
* path_regex matches the legacy `.enc` spelling alone also matches `.enc.yaml`, the spelling `secret set` writes, which sops otherwise refuses with
|
|
21
|
+
* no creation rule. Recipients and the other rules are kept; nothing private is read or written.
|
|
22
|
+
*/
|
|
23
|
+
export function migrateSopsRules({ root, write = false }) {
|
|
24
|
+
const report = { schema: 'starci/sops-rules-report@1', root, write, file: FILE, changes: [], findings: [] };
|
|
25
|
+
const target = path.join(root, FILE);
|
|
26
|
+
if (!fs.existsSync(target))
|
|
27
|
+
return report;
|
|
28
|
+
let config;
|
|
29
|
+
try {
|
|
30
|
+
config = parseYaml(fs.readFileSync(target, 'utf8'));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
config = null;
|
|
34
|
+
}
|
|
35
|
+
if (config === null || typeof config !== 'object' || !Array.isArray(config.creation_rules)) {
|
|
36
|
+
report.findings.push({ file: FILE, code: 'SOPS_RULES_UNREADABLE', reason: `${FILE} holds no creation_rules list; write it with starci app secret init --env <env>` });
|
|
37
|
+
return report;
|
|
38
|
+
}
|
|
39
|
+
config.creation_rules.forEach((rule, index) => {
|
|
40
|
+
if (typeof rule?.path_regex !== 'string')
|
|
41
|
+
return;
|
|
42
|
+
const to = managedPathRegex(rule.path_regex);
|
|
43
|
+
if (to === rule.path_regex)
|
|
44
|
+
return;
|
|
45
|
+
report.changes.push({ rule: index, from: rule.path_regex, to });
|
|
46
|
+
rule.path_regex = to;
|
|
47
|
+
});
|
|
48
|
+
if (report.changes.length > 0 && write)
|
|
49
|
+
fs.writeFileSync(target, stringifyYaml(config));
|
|
50
|
+
return report;
|
|
51
|
+
}
|
|
52
|
+
const cell = (value) => String(value).replaceAll('|', '\\|').replaceAll('\n', ' ');
|
|
53
|
+
/** Render the migration and what is left for a person as Markdown; JSON consumers read the same report object. */
|
|
54
|
+
export function sopsRulesMarkdown(report) {
|
|
55
|
+
const out = ['# sops-rules', '', `${report.changes.length} creation rule(s) ${report.write ? 'migrated' : 'would be migrated'}, ${report.findings.length} left for a person.`, ''];
|
|
56
|
+
if (report.findings.length) {
|
|
57
|
+
out.push('## Left for a person', '', '| file | code | reason |', '|---|---|---|');
|
|
58
|
+
for (const finding of report.findings)
|
|
59
|
+
out.push(`| ${cell(finding.file)} | ${finding.code} | ${cell(finding.reason)} |`);
|
|
60
|
+
out.push('');
|
|
61
|
+
}
|
|
62
|
+
if (report.changes.length) {
|
|
63
|
+
out.push('## Migrated', '', '| rule | from | to |', '|---|---|---|');
|
|
64
|
+
for (const item of report.changes)
|
|
65
|
+
out.push(`| ${item.rule} | \`${cell(item.from)}\` | \`${cell(item.to)}\` |`);
|
|
66
|
+
out.push('');
|
|
67
|
+
}
|
|
68
|
+
return `${out.join('\n')}\n`;
|
|
69
|
+
}
|
package/codemod/theme-tokens.mjs
CHANGED
|
@@ -23,12 +23,51 @@ const KEPT = /^--(?:offset-pop-shadow-ink|starci-core-(?:mark-(?:danger|warning)
|
|
|
23
23
|
const PRIVATE = /--(starci-core|offset-pop)-([a-z][a-z0-9-]*)/g;
|
|
24
24
|
/** The mapped name of one private token, or null when HeroUI has no token for it. */
|
|
25
25
|
const mapped = (prefix, name) => (prefix === 'starci-core' ? CORE_TOKENS : OFFSET_POP_TOKENS)[name] ?? null;
|
|
26
|
+
/**
|
|
27
|
+
* The lines left once the self-referential aliases are dropped (`null` marks a dropped line): the blank lines that would sit just before a
|
|
28
|
+
* closing brace go, and a rule block left empty goes with the selector lines and the comment that introduced it.
|
|
29
|
+
*/
|
|
30
|
+
function tidyDropped(lines) {
|
|
31
|
+
const kept = [];
|
|
32
|
+
let dropped = false;
|
|
33
|
+
for (const line of lines) {
|
|
34
|
+
if (line === null) {
|
|
35
|
+
dropped = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (dropped && /^\s*\}\s*$/.test(line)) {
|
|
39
|
+
while ((kept[kept.length - 1] ?? 'x').trim() === '')
|
|
40
|
+
kept.pop();
|
|
41
|
+
const open = kept.length - 1;
|
|
42
|
+
if (open >= 0 && /\{\s*$/.test(kept[open] ?? '') && !/^\s*@/.test(kept[open] ?? '')) {
|
|
43
|
+
let start = open;
|
|
44
|
+
while (start > 0 && /,\s*$/.test(kept[start - 1] ?? ''))
|
|
45
|
+
start -= 1;
|
|
46
|
+
if (start > 0 && /\*\/\s*$/.test(kept[start - 1] ?? '')) {
|
|
47
|
+
let comment = start - 1;
|
|
48
|
+
while (comment > 0 && !/^\s*\/\*/.test(kept[comment] ?? ''))
|
|
49
|
+
comment -= 1;
|
|
50
|
+
start = comment;
|
|
51
|
+
}
|
|
52
|
+
kept.length = start;
|
|
53
|
+
while (kept.length > 0 && (kept[kept.length - 1] ?? '').trim() === '')
|
|
54
|
+
kept.pop();
|
|
55
|
+
dropped = false;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (line.trim() !== '' || !dropped)
|
|
60
|
+
dropped = false;
|
|
61
|
+
kept.push(line);
|
|
62
|
+
}
|
|
63
|
+
return kept;
|
|
64
|
+
}
|
|
26
65
|
/** One stylesheet or component file rewritten; `changes` and `findings` carry 1-based lines. */
|
|
27
66
|
export function transformThemeTokens({ text, file = '' }) {
|
|
28
67
|
const changes = [];
|
|
29
68
|
const findings = [];
|
|
30
69
|
const isCss = file.endsWith('.css') || file === '';
|
|
31
|
-
const
|
|
70
|
+
const mappedLines = text.split('\n').map((line, index) => {
|
|
32
71
|
const number = index + 1;
|
|
33
72
|
let out = line;
|
|
34
73
|
if (isCss) {
|
|
@@ -55,6 +94,13 @@ export function transformThemeTokens({ text, file = '' }) {
|
|
|
55
94
|
changes.push({ file, line: number, from: token, to: `--${to}` });
|
|
56
95
|
return `--${to}`;
|
|
57
96
|
});
|
|
97
|
+
// `--starci-core-accent: var(--accent)` became `--accent: var(--accent)`: a custom property that names itself is invalid at computed-value
|
|
98
|
+
// time and blanks the colour, and the grammar reads HeroUI's token directly, so the alias is dropped, not renamed.
|
|
99
|
+
const self = /^\s*(--[a-z][a-z0-9-]*)\s*:\s*var\(\s*(--[a-z][a-z0-9-]*)\s*\)\s*;?\s*$/.exec(out);
|
|
100
|
+
if (self !== null && self[1] === self[2] && out !== line) {
|
|
101
|
+
changes.push({ file, line: number, from: line.trim(), to: '(dropped: the alias names itself)' });
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
58
104
|
}
|
|
59
105
|
else {
|
|
60
106
|
const root = /<(?:Core|OffsetPop)?GrammarRoot\b[^>]*\btheme=/.exec(line);
|
|
@@ -63,7 +109,7 @@ export function transformThemeTokens({ text, file = '' }) {
|
|
|
63
109
|
}
|
|
64
110
|
return out;
|
|
65
111
|
});
|
|
66
|
-
return { text:
|
|
112
|
+
return { text: tidyDropped(mappedLines).join('\n'), changes, findings };
|
|
67
113
|
}
|
|
68
114
|
function sourceFiles(dir) {
|
|
69
115
|
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
package/db/postgres.mjs
CHANGED
|
@@ -35,7 +35,15 @@ export function dbTarget(ctx, name = DEFAULT_DB_URL_ENV) {
|
|
|
35
35
|
const parsed = parseConnection(value);
|
|
36
36
|
if (!parsed)
|
|
37
37
|
throw refusal('DB_URL_INVALID', `${name} is not a postgres connection URL`);
|
|
38
|
-
|
|
38
|
+
const target = { kind: 'url', how: `connection string from ${name}`, urlEnv: name, url: parsed.url, env: parsed.password ? { PGPASSWORD: parsed.password } : {}, secrets: [value, parsed.password].filter(Boolean) };
|
|
39
|
+
// A dev environment's database runs in its own container (starci app stack up): STARCI_DB_CONTAINER runs the client tools inside it, so the host needs no PostgreSQL client.
|
|
40
|
+
const container = ctx.env.STARCI_DB_CONTAINER;
|
|
41
|
+
if (!container)
|
|
42
|
+
return target;
|
|
43
|
+
const inside = inContainer(target, container);
|
|
44
|
+
const url = new URL(inside.url);
|
|
45
|
+
url.port = '5432';
|
|
46
|
+
return { ...target, ...inside, kind: 'url', urlEnv: name, url: url.toString() };
|
|
39
47
|
}
|
|
40
48
|
/** Where a `-f` file of an in-container target is copied before the client reads it. */
|
|
41
49
|
const CONTAINER_DIR = '/tmp/starci-db';
|
package/lint/run.d.mts
CHANGED
|
@@ -56,6 +56,12 @@ export declare const ESLINT_HEAP_ENV = "STARCI_ESLINT_HEAP_MB";
|
|
|
56
56
|
export declare function eslintHeapArgs(env?: NodeJS.ProcessEnv): string[];
|
|
57
57
|
/** A linter's json results through the app's own install, run from `cwd` (a side folder): `{ results }` or `{ error }`. The child runs while the caller goes on. */
|
|
58
58
|
export declare function runLinter({ cwd, pkg, bin, args, bound, list, files, graph }: LinterSpec, execute?: HeavyNodeRun): Promise<LinterRun>;
|
|
59
|
+
/**
|
|
60
|
+
* The root workspace packages of an app (`packages/<pkg>/src/**`) are linted by the managed `eslint.config.mjs` of the app root, as a third scope beside
|
|
61
|
+
* the two sides: its files are app-relative and its working directory is the app root. It has a scope only while the config is there.
|
|
62
|
+
*/
|
|
63
|
+
export declare const PACKAGES = "packages";
|
|
64
|
+
export declare const PACKAGES_CONFIG = "eslint.config.mjs";
|
|
59
65
|
/**
|
|
60
66
|
* The lint scope a flat config declares: `ignored(file)` for its global ignores (a block holding only `ignores` and `name`), which prune
|
|
61
67
|
* the walk, and `source(file)` for a file some block's `files` select while that block's own `ignores` do not (standard flat-config
|