@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,101 @@
|
|
|
1
|
+
// Writes the gateway settings the StarCi runtime layer relies on into openclaw.json before the gateway starts (idempotent merge: other keys,
|
|
2
|
+
// agents and plugins that OpenClaw or an operator added are kept). Loopback bind + token auth: the gateway is reached only through the relay.
|
|
3
|
+
//
|
|
4
|
+
// The contract an app image relies on (images/README.md "The OpenClaw runtime layer"):
|
|
5
|
+
// STARCI_PLUGINS_DIR the plugin directory (default /opt/starci/plugins): every subfolder holding an openclaw.plugin.json is added to
|
|
6
|
+
// plugins.load.paths and enabled as plugins.entries.<folder name>
|
|
7
|
+
// AI_BASE_URL + STARCI_AI_KEY one OpenAI-compatible custom provider (name OPENCLAW_PROVIDER, default "starci-ai"; chat model AI_CHAT_MODEL,
|
|
8
|
+
// default "work"); the key stays a reference openclaw resolves from the gateway process environment
|
|
9
|
+
// OPENCLAW_MODEL the primary model of the agents; OPENCLAW_MODEL_REASONING=off switches the model's reasoning off
|
|
10
|
+
import fs from "node:fs"
|
|
11
|
+
import path from "node:path"
|
|
12
|
+
|
|
13
|
+
const file = "/home/node/.openclaw/openclaw.json"
|
|
14
|
+
let config = {}
|
|
15
|
+
try {
|
|
16
|
+
config = JSON.parse(fs.readFileSync(file, "utf8"))
|
|
17
|
+
} catch {
|
|
18
|
+
config = {}
|
|
19
|
+
}
|
|
20
|
+
// The app writes each agent workspace itself: no first-run ritual.
|
|
21
|
+
config.agents = config.agents || {}
|
|
22
|
+
config.agents.defaults = { ...(config.agents.defaults || {}), skipBootstrap: true }
|
|
23
|
+
// The app owns every trigger and schedule: OpenClaw runs one turn when asked and nothing in the background. `cron.enabled: false` stops its cron
|
|
24
|
+
// scheduler; a `defaults.heartbeat` block makes every agent resolve its heartbeat from it, and `every: "0m"` gives each no interval.
|
|
25
|
+
config.cron = { ...(config.cron || {}), enabled: false }
|
|
26
|
+
config.agents.defaults.heartbeat = { ...(config.agents.defaults.heartbeat || {}), every: "0m" }
|
|
27
|
+
// Plugins: every folder of the plugin directory that holds an openclaw.plugin.json is loaded and enabled; other paths and entries are kept.
|
|
28
|
+
const pluginsDir = process.env.STARCI_PLUGINS_DIR || "/opt/starci/plugins"
|
|
29
|
+
const found = fs.existsSync(pluginsDir)
|
|
30
|
+
? fs.readdirSync(pluginsDir, { withFileTypes: true })
|
|
31
|
+
.filter((entry) => entry.isDirectory() && fs.existsSync(path.join(pluginsDir, entry.name, "openclaw.plugin.json")))
|
|
32
|
+
.map((entry) => entry.name)
|
|
33
|
+
.sort()
|
|
34
|
+
: []
|
|
35
|
+
const plugins = config.plugins || {}
|
|
36
|
+
const load = plugins.load || {}
|
|
37
|
+
const paths = Array.isArray(load.paths) ? load.paths : []
|
|
38
|
+
const entries = { ...(plugins.entries || {}) }
|
|
39
|
+
for (const name of found) entries[name] = { ...(entries[name] || {}), enabled: true }
|
|
40
|
+
config.plugins = {
|
|
41
|
+
...plugins,
|
|
42
|
+
load: { ...load, paths: [...paths, ...found.map((name) => path.join(pluginsDir, name)).filter((folder) => !paths.includes(folder))] },
|
|
43
|
+
entries,
|
|
44
|
+
}
|
|
45
|
+
config.session = {
|
|
46
|
+
...(config.session || {}),
|
|
47
|
+
maintenance: {
|
|
48
|
+
...((config.session || {}).maintenance || {}),
|
|
49
|
+
mode: "enforce",
|
|
50
|
+
pruneAfter: "7d",
|
|
51
|
+
maxEntries: 400,
|
|
52
|
+
maxDiskBytes: "400mb",
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
config.gateway = {
|
|
56
|
+
...(config.gateway || {}),
|
|
57
|
+
mode: "local",
|
|
58
|
+
bind: "loopback",
|
|
59
|
+
port: 18789,
|
|
60
|
+
auth: {
|
|
61
|
+
...((config.gateway || {}).auth || {}),
|
|
62
|
+
mode: "token",
|
|
63
|
+
token: fs.readFileSync(process.env.OPENCLAW_GATEWAY_TOKEN_FILE, "utf8"),
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
// Language models come only from the AI gateway the app hosts: one OpenAI-compatible custom provider with the virtual key it shares with the app.
|
|
67
|
+
if (process.env.AI_BASE_URL && process.env.STARCI_AI_KEY) {
|
|
68
|
+
const chat = process.env.AI_CHAT_MODEL || "work"
|
|
69
|
+
const provider = process.env.OPENCLAW_PROVIDER || "starci-ai"
|
|
70
|
+
config.models = {
|
|
71
|
+
...(config.models || {}),
|
|
72
|
+
mode: "merge",
|
|
73
|
+
providers: {
|
|
74
|
+
...((config.models || {}).providers || {}),
|
|
75
|
+
[provider]: {
|
|
76
|
+
baseUrl: process.env.AI_BASE_URL,
|
|
77
|
+
apiKey: "${STARCI_AI_KEY}",
|
|
78
|
+
api: "openai-completions",
|
|
79
|
+
models: [{ id: chat, name: chat, input: ["text", "image"] }],
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const model = process.env.OPENCLAW_MODEL
|
|
85
|
+
if (model) {
|
|
86
|
+
config.agents.defaults.model = { ...(config.agents.defaults.model || {}), primary: model }
|
|
87
|
+
if (process.env.OPENCLAW_MODEL_REASONING === "off") {
|
|
88
|
+
const models = (config.agents.defaults.models = config.agents.defaults.models || {})
|
|
89
|
+
const previous = models[model] || {}
|
|
90
|
+
models[model] = {
|
|
91
|
+
...previous,
|
|
92
|
+
params: {
|
|
93
|
+
...(previous.params || {}),
|
|
94
|
+
extra_body: { ...((previous.params || {}).extra_body || {}), reasoning: { enabled: false } },
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
fs.mkdirSync("/home/node/.openclaw", { recursive: true })
|
|
100
|
+
fs.writeFileSync(file, JSON.stringify(config, null, 2), { mode: 0o600 })
|
|
101
|
+
console.log("openclaw.json ready (gateway local/loopback/token, " + found.length + " plugin" + (found.length === 1 ? "" : "s") + (model ? ", model " + model : "") + ")")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A byte-for-byte TCP relay inside the OpenClaw task: it listens on the overlay address and forwards every connection to the loopback-only
|
|
2
|
+
// gateway (127.0.0.1:18789), so the gateway sees a loopback peer and honours the scopes a client declares. It adds no header and reads no frame.
|
|
3
|
+
import net from "node:net"
|
|
4
|
+
|
|
5
|
+
const listenPort = Number(process.env.OPENCLAW_RELAY_PORT || 18790)
|
|
6
|
+
const gatewayPort = 18789
|
|
7
|
+
|
|
8
|
+
const server = net.createServer((client) => {
|
|
9
|
+
const gateway = net.connect({ host: "127.0.0.1", port: gatewayPort })
|
|
10
|
+
const close = () => {
|
|
11
|
+
client.destroy()
|
|
12
|
+
gateway.destroy()
|
|
13
|
+
}
|
|
14
|
+
client.on("error", close)
|
|
15
|
+
gateway.on("error", close)
|
|
16
|
+
client.on("close", close)
|
|
17
|
+
gateway.on("close", close)
|
|
18
|
+
client.pipe(gateway)
|
|
19
|
+
gateway.pipe(client)
|
|
20
|
+
})
|
|
21
|
+
server.on("error", (error) => {
|
|
22
|
+
console.error("openclaw relay failed: " + error.message)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
})
|
|
25
|
+
server.listen(listenPort, "0.0.0.0", () => console.log("openclaw relay " + listenPort + " -> 127.0.0.1:" + gatewayPort))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { isLocalEnvironment } from './local-config.mjs';
|
|
2
|
+
import type { StackContext } from './context.mjs';
|
|
3
|
+
import type { LocalDeps } from './local-run.mjs';
|
|
4
|
+
/** The actions that exist only for a dev environment. */
|
|
5
|
+
export declare const LOCAL_ONLY: readonly ["doctor", "up", "down", "logs"];
|
|
6
|
+
/** The actions a swarm environment already has, which a docker-compose environment answers with its dev implementation. */
|
|
7
|
+
export declare const LOCAL_SHARED: readonly ["prepare", "status", "verify"];
|
|
8
|
+
/** True when `value` is an action only a dev environment has. */
|
|
9
|
+
export declare const isLocalOnly: (value: unknown) => boolean;
|
|
10
|
+
/** True when `value` is an action both kinds have. */
|
|
11
|
+
export declare const isLocalShared: (value: unknown) => boolean;
|
|
12
|
+
/** The parsed flags of a dev action. */
|
|
13
|
+
export interface LocalFlags {
|
|
14
|
+
env?: string;
|
|
15
|
+
json: boolean;
|
|
16
|
+
execute: boolean;
|
|
17
|
+
timeout?: string;
|
|
18
|
+
service?: string;
|
|
19
|
+
lines?: string;
|
|
20
|
+
}
|
|
21
|
+
/** Parses the flags of one dev action; a flag of another action, an unknown flag and a missing value are refusals. */
|
|
22
|
+
export declare function parseLocalFlags(action: string, argv: readonly string[]): LocalFlags;
|
|
23
|
+
/** Runs one dev action and returns the exit code. */
|
|
24
|
+
export declare function runLocal(ctx: StackContext, action: string, argv: readonly string[], deps?: LocalDeps): Promise<number>;
|
|
25
|
+
export { isLocalEnvironment };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
2
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
3
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
4
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
return path;
|
|
8
|
+
};
|
|
9
|
+
// local-cli.mjs - the dev actions of `starci app stack` (PROJECT-STACKS-6): doctor, up, down and logs, and prepare, status and verify when the
|
|
10
|
+
// named environment runs docker-compose. Each takes only its own flags; effects (up, down, prepare) are dry runs until --execute.
|
|
11
|
+
// starci app stack doctor --env <env>
|
|
12
|
+
// starci app stack prepare --env <env> [--execute]
|
|
13
|
+
// starci app stack up --env <env> [--execute] [--timeout <seconds>]
|
|
14
|
+
// starci app stack status --env <env>
|
|
15
|
+
// starci app stack logs --env <env> --service <name> [--lines <n>]
|
|
16
|
+
// starci app stack verify --env <env>
|
|
17
|
+
// starci app stack down --env <env> [--execute]
|
|
18
|
+
import { loadLocalConfig, isLocalEnvironment } from './local-config.mjs';
|
|
19
|
+
import { EXIT, refusal } from './errors.mjs';
|
|
20
|
+
/** The actions that exist only for a dev environment. */
|
|
21
|
+
export const LOCAL_ONLY = Object.freeze(['doctor', 'up', 'down', 'logs']);
|
|
22
|
+
/** The actions a swarm environment already has, which a docker-compose environment answers with its dev implementation. */
|
|
23
|
+
export const LOCAL_SHARED = Object.freeze(['prepare', 'status', 'verify']);
|
|
24
|
+
/** True when `value` is an action only a dev environment has. */
|
|
25
|
+
export const isLocalOnly = (value) => LOCAL_ONLY.includes(value);
|
|
26
|
+
/** True when `value` is an action both kinds have. */
|
|
27
|
+
export const isLocalShared = (value) => LOCAL_SHARED.includes(value);
|
|
28
|
+
const FLAGS = Object.freeze({
|
|
29
|
+
doctor: [], up: ['--execute', '--timeout'], down: ['--execute'], logs: ['--service', '--lines'], prepare: ['--execute'], status: [], verify: [],
|
|
30
|
+
});
|
|
31
|
+
const VALUES = new Set(['--env', '--timeout', '--service', '--lines']);
|
|
32
|
+
/** Parses the flags of one dev action; a flag of another action, an unknown flag and a missing value are refusals. */
|
|
33
|
+
export function parseLocalFlags(action, argv) {
|
|
34
|
+
const own = new Set(['--env', '--json', ...(FLAGS[action] ?? [])]);
|
|
35
|
+
const flags = { json: false, execute: false };
|
|
36
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
37
|
+
const arg = argv[index];
|
|
38
|
+
if (!arg.startsWith('--'))
|
|
39
|
+
throw refusal('STACK_USAGE', `stack ${action} takes no operand (got ${arg})`);
|
|
40
|
+
if (!own.has(arg))
|
|
41
|
+
throw refusal('STACK_USAGE', `stack ${action} takes no ${arg}: its flags are ${[...own].join(', ')}`);
|
|
42
|
+
if (arg === '--json')
|
|
43
|
+
flags.json = true;
|
|
44
|
+
else if (arg === '--execute')
|
|
45
|
+
flags.execute = true;
|
|
46
|
+
else if (VALUES.has(arg)) {
|
|
47
|
+
const value = argv[index + 1];
|
|
48
|
+
if (value === undefined || value.startsWith('--'))
|
|
49
|
+
throw refusal('STACK_USAGE', `${arg} needs a value`);
|
|
50
|
+
index += 1;
|
|
51
|
+
flags[arg.slice(2)] = value;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (flags.env === undefined)
|
|
55
|
+
throw refusal('STACK_USAGE', `stack ${action} names its environment with --env <env>`);
|
|
56
|
+
return flags;
|
|
57
|
+
}
|
|
58
|
+
const whole = (value, name, fallback) => {
|
|
59
|
+
if (value === undefined)
|
|
60
|
+
return fallback;
|
|
61
|
+
const parsed = Number(value);
|
|
62
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
63
|
+
throw refusal('STACK_USAGE', `${name} must be a whole number`);
|
|
64
|
+
return parsed;
|
|
65
|
+
};
|
|
66
|
+
/** The module of each dev action. */
|
|
67
|
+
const MODULES = {
|
|
68
|
+
doctor: ['./local-doctor.mjs', 'doctorLocal'], prepare: ['./local-prepare.mjs', 'prepareLocal'], up: ['./local-up.mjs', 'upLocal'], status: ['./local-status.mjs', 'statusLocal'],
|
|
69
|
+
logs: ['./local-logs.mjs', 'logsLocal'], verify: ['./local-verify.mjs', 'verifyLocal'], down: ['./local-down.mjs', 'downLocal'],
|
|
70
|
+
};
|
|
71
|
+
/** Runs one dev action and returns the exit code. */
|
|
72
|
+
export async function runLocal(ctx, action, argv, deps = {}) {
|
|
73
|
+
const flags = parseLocalFlags(action, argv);
|
|
74
|
+
const local = loadLocalConfig({ repoRoot: ctx.cwd, env: flags.env });
|
|
75
|
+
const [file, name] = MODULES[action];
|
|
76
|
+
const handler = (await import(__rewriteRelativeImportExtension(file)))[name];
|
|
77
|
+
const result = await handler(ctx, local, { execute: flags.execute, timeoutSeconds: whole(flags.timeout, '--timeout', 180), service: flags.service, lines: whole(flags.lines, '--lines', 200) }, deps);
|
|
78
|
+
if (flags.json)
|
|
79
|
+
ctx.stdout(`${JSON.stringify({ verb: action, ok: result.ok, ...result.data }, null, 2)}\n`);
|
|
80
|
+
else
|
|
81
|
+
for (const line of result.lines)
|
|
82
|
+
ctx.stdout(`${line}\n`);
|
|
83
|
+
if (result.refused) {
|
|
84
|
+
ctx.stderr(`starci app stack: ${result.refused.code}: ${result.refused.message}\n`);
|
|
85
|
+
return EXIT.refused;
|
|
86
|
+
}
|
|
87
|
+
return result.ok ? EXIT.ok : EXIT.failed;
|
|
88
|
+
}
|
|
89
|
+
export { isLocalEnvironment };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { LocalBlock } from '../runtime/scripts/hfs/stack-local-config.mjs';
|
|
2
|
+
/** The dev configuration of one environment, with absolute paths. */
|
|
3
|
+
export interface LocalConfig extends LocalBlock {
|
|
4
|
+
env: string;
|
|
5
|
+
appProject: string;
|
|
6
|
+
repoRoot: string;
|
|
7
|
+
envDir: string;
|
|
8
|
+
composeFiles: string[];
|
|
9
|
+
secretsDir: string;
|
|
10
|
+
}
|
|
11
|
+
/** True when the environment is declared and runs docker-compose (the dev kind); false for a swarm or an unknown one. */
|
|
12
|
+
export declare function isLocalEnvironment(repoRoot: string, env: string): boolean;
|
|
13
|
+
/** The normalised dev configuration; a refusal names every problem when the block is missing or invalid. */
|
|
14
|
+
export declare function loadLocalConfig({ repoRoot, env }: {
|
|
15
|
+
repoRoot: string;
|
|
16
|
+
env: string;
|
|
17
|
+
}): LocalConfig;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// local-config.mjs - loads `environments.<env>.local` of .starcistacks/application-stacks.yaml for the dev verbs of `starci app stack`.
|
|
2
|
+
// The block is read and judged by the module `starci app check` uses (runtime/scripts/hfs/stack-local-config.mjs).
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { normalizeLocal, LOCAL_CONFIG_CODE } from '../runtime/scripts/hfs/stack-local-config.mjs';
|
|
6
|
+
import { readYaml, STACKS_DIR, STACKS_FILE } from './config.mjs';
|
|
7
|
+
import { refusal } from './errors.mjs';
|
|
8
|
+
/** True when the environment is declared and runs docker-compose (the dev kind); false for a swarm or an unknown one. */
|
|
9
|
+
export function isLocalEnvironment(repoRoot, env) {
|
|
10
|
+
try {
|
|
11
|
+
const declaration = readYaml(path.join(repoRoot, STACKS_DIR, STACKS_FILE), STACKS_FILE);
|
|
12
|
+
return declaration?.environments?.[env]?.runtime === 'docker-compose';
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** The normalised dev configuration; a refusal names every problem when the block is missing or invalid. */
|
|
19
|
+
export function loadLocalConfig({ repoRoot, env }) {
|
|
20
|
+
const declaration = readYaml(path.join(repoRoot, STACKS_DIR, STACKS_FILE), STACKS_FILE);
|
|
21
|
+
const environment = declaration?.environments?.[env];
|
|
22
|
+
if (!environment)
|
|
23
|
+
throw refusal('STACKS_DEPLOY_ENV_UNKNOWN', `${STACKS_FILE} declares no environment ${env}`);
|
|
24
|
+
if (environment.runtime !== 'docker-compose')
|
|
25
|
+
throw refusal('STACK_LOCAL_NOT_DEV', `environment ${env} runs ${environment.runtime}; up, down, doctor and logs run a docker-compose dev environment`);
|
|
26
|
+
if (environment.local === undefined)
|
|
27
|
+
throw refusal('STACK_LOCAL_UNDECLARED', `environment ${env} has no local block in ${STACKS_FILE}: declare the ports, services, processes and probes the dev environment runs (PROJECT-STACKS-6)`);
|
|
28
|
+
const appProject = String(JSON.parse(fs.readFileSync(path.join(repoRoot, 'hfs.json'), 'utf8')).project);
|
|
29
|
+
const { config, problems } = normalizeLocal(environment.local, { project: appProject, env });
|
|
30
|
+
if (!config) {
|
|
31
|
+
const first = problems.slice(0, 6).map((item) => `${item.at}: ${item.message}`).join('; ');
|
|
32
|
+
throw refusal(LOCAL_CONFIG_CODE, `${problems.length} problem${problems.length === 1 ? '' : 's'} in environments.${env}.local: ${first}`);
|
|
33
|
+
}
|
|
34
|
+
const envDir = path.join(repoRoot, STACKS_DIR, env);
|
|
35
|
+
const composeFiles = (environment.composeFiles ?? []).map((file) => path.join(envDir, file));
|
|
36
|
+
return { ...config, env, appProject, repoRoot, envDir, composeFiles, secretsDir: path.join(envDir, 'secrets') };
|
|
37
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { StackContext } from './context.mjs';
|
|
2
|
+
import type { LocalConfig } from './local-config.mjs';
|
|
3
|
+
/** The opened custody: the directory of the host-local `<KEY>` files and the values, in memory only. */
|
|
4
|
+
export interface Custody {
|
|
5
|
+
directory: string;
|
|
6
|
+
values: Map<string, string>;
|
|
7
|
+
}
|
|
8
|
+
/** Every key the environment's commands, processes, database and compose read. */
|
|
9
|
+
export declare function neededKeys(local: LocalConfig): string[];
|
|
10
|
+
/** Which sealed member holds each key, read from the cleartext key names (no identity needed). */
|
|
11
|
+
export declare function holders(secretsDir: string): Map<string, string>;
|
|
12
|
+
/** The keys needed that no sealed member holds. */
|
|
13
|
+
export declare const missingKeys: (local: LocalConfig) => string[];
|
|
14
|
+
/** The owner step for keys no member holds: init when the environment generates them, else set. */
|
|
15
|
+
export declare function missingStep(local: LocalConfig, missing: readonly string[]): string;
|
|
16
|
+
/**
|
|
17
|
+
* Decrypts the needed keys in memory and writes each to its own private file outside the checkout. A key no member holds, or a member the
|
|
18
|
+
* machine's identity cannot open, is a refusal that names the owner step; nothing is generated here.
|
|
19
|
+
*/
|
|
20
|
+
export declare function openCustody(ctx: StackContext, local: LocalConfig): Custody;
|
|
21
|
+
/** The directory of one least grant inside the host-local custody directory (created private; it holds only the keys the grant names). */
|
|
22
|
+
export declare function grantDirectory(directory: string, name: string): string;
|
|
23
|
+
/** The `<KEY>_FILE` environment of the keys a command reads: each points at the host-local file. */
|
|
24
|
+
export declare const fileEnvironment: (custody: Custody, keys: readonly string[]) => Record<string, string>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// local-custody.mjs - the dev custody of a dev environment: which keys the environment reads, whether they are sealed, and their host-local
|
|
2
|
+
// files (PROJECT-STACKS-5). Opening a member needs the owner identity; a machine without it is refused with the exact owner step, and a missing
|
|
3
|
+
// value is never answered by minting a replacement (only the owner-armed `starci app secret init --env <env>` creates dev values, once).
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { envelopeOf } from '../runtime/scripts/hfs/secret-document.mjs';
|
|
7
|
+
import { materializedDirectory, writeSecretFile } from '../runtime/scripts/hfs/secret-files.mjs';
|
|
8
|
+
import { isSealedName, memberOfName } from '../runtime/scripts/lib/sealed-member.mjs';
|
|
9
|
+
import { refusal } from './errors.mjs';
|
|
10
|
+
/** Every key the environment's commands, processes, database and compose read. */
|
|
11
|
+
export function neededKeys(local) {
|
|
12
|
+
const keys = new Set();
|
|
13
|
+
for (const run of [...local.setup, ...local.seed, ...local.processes])
|
|
14
|
+
for (const key of run.files)
|
|
15
|
+
if (key !== local.database?.urlEnv)
|
|
16
|
+
keys.add(key);
|
|
17
|
+
for (const item of local.composeEnv)
|
|
18
|
+
keys.add(item.key);
|
|
19
|
+
for (const grant of local.grants)
|
|
20
|
+
for (const key of grant.keys)
|
|
21
|
+
keys.add(key);
|
|
22
|
+
if (local.database) {
|
|
23
|
+
keys.add(local.database.password.key);
|
|
24
|
+
keys.add(local.database.adminPassword.key);
|
|
25
|
+
if (local.database.appPassword)
|
|
26
|
+
keys.add(local.database.appPassword.key);
|
|
27
|
+
}
|
|
28
|
+
return [...keys].sort();
|
|
29
|
+
}
|
|
30
|
+
/** Which sealed member holds each key, read from the cleartext key names (no identity needed). */
|
|
31
|
+
export function holders(secretsDir) {
|
|
32
|
+
const held = new Map();
|
|
33
|
+
let names = [];
|
|
34
|
+
try {
|
|
35
|
+
names = fs.readdirSync(secretsDir).filter(isSealedName).sort();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return held;
|
|
39
|
+
}
|
|
40
|
+
for (const name of names) {
|
|
41
|
+
try {
|
|
42
|
+
for (const key of envelopeOf(fs.readFileSync(path.join(secretsDir, name), 'utf8')).keys)
|
|
43
|
+
if (!held.has(key))
|
|
44
|
+
held.set(key, name);
|
|
45
|
+
}
|
|
46
|
+
catch { /* an unreadable member is judged when it is opened */ }
|
|
47
|
+
}
|
|
48
|
+
return held;
|
|
49
|
+
}
|
|
50
|
+
/** The keys needed that no sealed member holds. */
|
|
51
|
+
export const missingKeys = (local) => {
|
|
52
|
+
const held = holders(local.secretsDir);
|
|
53
|
+
return neededKeys(local).filter((key) => !held.has(key));
|
|
54
|
+
};
|
|
55
|
+
/** The owner step for keys no member holds: init when the environment generates them, else set. */
|
|
56
|
+
export function missingStep(local, missing) {
|
|
57
|
+
const generated = new Set(local.custody.flatMap((item) => [item.key, ...(item.publicKey ? [item.publicKey] : [])]));
|
|
58
|
+
const create = missing.filter((key) => generated.has(key));
|
|
59
|
+
const set = missing.filter((key) => !generated.has(key));
|
|
60
|
+
return [
|
|
61
|
+
create.length ? `the owner arms "starci app secret init --env ${local.env} --execute" to create ${create.join(', ')} once` : '',
|
|
62
|
+
set.length ? `the owner seals ${set.join(', ')} with "starci app secret set --env ${local.env} <slug> <KEY>"` : '',
|
|
63
|
+
].filter(Boolean).join('; ');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Decrypts the needed keys in memory and writes each to its own private file outside the checkout. A key no member holds, or a member the
|
|
67
|
+
* machine's identity cannot open, is a refusal that names the owner step; nothing is generated here.
|
|
68
|
+
*/
|
|
69
|
+
export function openCustody(ctx, local) {
|
|
70
|
+
const needed = neededKeys(local);
|
|
71
|
+
const held = holders(local.secretsDir);
|
|
72
|
+
const missing = needed.filter((key) => !held.has(key));
|
|
73
|
+
if (missing.length)
|
|
74
|
+
throw refusal('STACK_LOCAL_CUSTODY_MISSING', `no sealed member of ${local.env} holds ${missing.join(', ')}: ${missingStep(local, missing)}`);
|
|
75
|
+
const values = new Map();
|
|
76
|
+
const members = [...new Set(needed.map((key) => held.get(key)))];
|
|
77
|
+
for (const name of members) {
|
|
78
|
+
const file = path.join(local.secretsDir, name);
|
|
79
|
+
try {
|
|
80
|
+
ctx.sops.requireIdentity?.(file);
|
|
81
|
+
const map = ctx.sops.decrypt(file, envelopeOf(fs.readFileSync(file, 'utf8')).format);
|
|
82
|
+
for (const key of needed)
|
|
83
|
+
if (held.get(key) === name && typeof map[key] === 'string')
|
|
84
|
+
values.set(key, map[key]);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const why = String(error.message ?? error).split(/\r?\n/u)[0];
|
|
88
|
+
throw refusal('STACK_LOCAL_IDENTITY_MISSING', `${memberOfName(name)} (${local.env}) cannot be opened on this machine: ${why}. The owner step: put the age identity that ${local.env} is sealed to in SOPS_AGE_KEY of the harness secret.env (or .claude/secret.env of the app), obtained from the owner; a replacement is never generated for an existing sealed value (PROJECT-STACKS-5)`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const directory = materializedDirectory(local.repoRoot, local.env, ctx.env, ctx.platform);
|
|
92
|
+
for (const [key, value] of values)
|
|
93
|
+
writeSecretFile(directory, key, value);
|
|
94
|
+
for (const grant of local.grants) {
|
|
95
|
+
const target = grantDirectory(directory, grant.name);
|
|
96
|
+
for (const key of grant.keys)
|
|
97
|
+
if (values.has(key))
|
|
98
|
+
writeSecretFile(target, key, values.get(key));
|
|
99
|
+
}
|
|
100
|
+
return { directory, values };
|
|
101
|
+
}
|
|
102
|
+
/** The directory of one least grant inside the host-local custody directory (created private; it holds only the keys the grant names). */
|
|
103
|
+
export function grantDirectory(directory, name) {
|
|
104
|
+
const target = path.join(directory, 'grants', name);
|
|
105
|
+
fs.mkdirSync(target, { recursive: true, mode: 0o700 });
|
|
106
|
+
return target;
|
|
107
|
+
}
|
|
108
|
+
/** The `<KEY>_FILE` environment of the keys a command reads: each points at the host-local file. */
|
|
109
|
+
export const fileEnvironment = (custody, keys) => Object.fromEntries(keys.filter((key) => custody.values.has(key)).map((key) => [`${key}_FILE`, path.join(custody.directory, key)]));
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StackContext } from './context.mjs';
|
|
2
|
+
import type { ActionResult } from './remote.mjs';
|
|
3
|
+
import type { LocalConfig } from './local-config.mjs';
|
|
4
|
+
import type { LocalDeps } from './local-run.mjs';
|
|
5
|
+
/** `doctor` for a dev environment. */
|
|
6
|
+
export declare function doctorLocal(ctx: StackContext, local: LocalConfig, _options?: Record<string, unknown>, deps?: LocalDeps): Promise<ActionResult>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// local-doctor.mjs - `starci app stack doctor --env <dev env>` (PROJECT-STACKS-6): the read-only inspection of a dev environment and of the machine
|
|
2
|
+
// it runs on. The prerequisite table (tools, engine, resources, ports) names the owner-run install step per OS for each missing one; then
|
|
3
|
+
// the custody (which keys are sealed, whether this machine's identity opens the members), the compose render, the volumes and the recorded
|
|
4
|
+
// processes. It changes nothing and never prints a value.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { firstLine, lines as splitLines } from './context.mjs';
|
|
8
|
+
import { holders, missingKeys, missingStep, neededKeys } from './local-custody.mjs';
|
|
9
|
+
import { tableLines } from './local-host.mjs';
|
|
10
|
+
import { preflight } from './local-prepare.mjs';
|
|
11
|
+
import { compose, isAlive, stateOf } from './local-run.mjs';
|
|
12
|
+
import { readRecord } from './local-state.mjs';
|
|
13
|
+
function custodyRows(ctx, local) {
|
|
14
|
+
const needed = neededKeys(local);
|
|
15
|
+
const missing = missingKeys(local);
|
|
16
|
+
const rows = [{ id: 'custody', ok: missing.length === 0, detail: missing.length ? `${missing.join(', ')} sealed in no member of ${local.env}` : `${needed.length} key(s) sealed`, fix: missing.length ? missingStep(local, missing) : null }];
|
|
17
|
+
const held = holders(local.secretsDir);
|
|
18
|
+
const members = [...new Set(needed.map((key) => held.get(key)).filter((name) => name !== undefined))];
|
|
19
|
+
for (const name of members) {
|
|
20
|
+
try {
|
|
21
|
+
ctx.sops.requireIdentity?.(path.join(local.secretsDir, name));
|
|
22
|
+
rows.push({ id: `identity ${name}`, ok: true, detail: 'this machine holds an identity the member is sealed to', fix: null });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
rows.push({ id: `identity ${name}`, ok: false, detail: String(error.message).split(/\r?\n/u)[0] ?? 'no identity', fix: 'obtain the age identity of the environment from the owner into SOPS_AGE_KEY of the harness secret.env; a replacement is never generated for an existing sealed value' });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return rows;
|
|
29
|
+
}
|
|
30
|
+
/** `doctor` for a dev environment. */
|
|
31
|
+
export async function doctorLocal(ctx, local, _options = {}, deps = {}) {
|
|
32
|
+
const host = await preflight(ctx, local, deps);
|
|
33
|
+
const rows = [...custodyRows(ctx, local)];
|
|
34
|
+
const lines = [...host.lines, ...tableLines(rows)];
|
|
35
|
+
const dockerOk = host.rows.filter((row) => row.id === 'docker' || row.id === 'compose').every((row) => row.ok);
|
|
36
|
+
if (dockerOk) {
|
|
37
|
+
const placeholders = Object.fromEntries(local.composeEnv.map((item) => [item.name, 'placeholder']));
|
|
38
|
+
const render = await compose(ctx, local, ['config', '-q'], { values: placeholders });
|
|
39
|
+
rows.push({ id: 'compose files', ok: render.status === 0, detail: render.status === 0 ? `${local.composeFiles.length} file(s) render` : firstLine(render.stderr) || 'do not render', fix: render.status === 0 ? null : 'fix the compose files named above' });
|
|
40
|
+
lines.push(...tableLines(rows.slice(-1)));
|
|
41
|
+
const volumes = splitLines(ctx.spawn('docker', ['volume', 'ls', '--filter', `label=com.docker.compose.project=${local.project}`, '--format', '{{.Name}}']).stdout);
|
|
42
|
+
lines.push(`volumes: ${volumes.join(', ') || 'none yet'}`);
|
|
43
|
+
}
|
|
44
|
+
const record = readRecord(stateOf(ctx, local));
|
|
45
|
+
const alive = deps.alive ?? isAlive;
|
|
46
|
+
const recorded = Object.entries(record.processes).map(([name, entry]) => `${name} pid ${entry.pid} ${alive(entry.pid) ? 'alive' : 'gone'}`);
|
|
47
|
+
lines.push(`processes: ${recorded.join(', ') || 'none recorded'}`, `state: ${fs.existsSync(stateOf(ctx, local)) ? stateOf(ctx, local) : 'none yet'}`);
|
|
48
|
+
return { ok: host.ok && rows.every((row) => row.ok), lines, data: { rows: [...host.rows, ...rows] } };
|
|
49
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StackContext } from './context.mjs';
|
|
2
|
+
import type { ActionResult } from './remote.mjs';
|
|
3
|
+
import type { LocalConfig } from './local-config.mjs';
|
|
4
|
+
import type { LocalDeps } from './local-run.mjs';
|
|
5
|
+
/** `down` for a dev environment. */
|
|
6
|
+
export declare function downLocal(ctx: StackContext, local: LocalConfig, { execute }?: {
|
|
7
|
+
execute?: boolean;
|
|
8
|
+
}, deps?: LocalDeps): Promise<ActionResult>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { compose, isAlive, stateOf, stopProcess } from './local-run.mjs';
|
|
2
|
+
import { readRecord, writeRecord } from './local-state.mjs';
|
|
3
|
+
/** `down` for a dev environment. */
|
|
4
|
+
export async function downLocal(ctx, local, { execute = false } = {}, deps = {}) {
|
|
5
|
+
const dir = stateOf(ctx, local);
|
|
6
|
+
const record = readRecord(dir);
|
|
7
|
+
const alive = deps.alive ?? isAlive;
|
|
8
|
+
const running = Object.entries(record.processes).filter(([, entry]) => alive(entry.pid));
|
|
9
|
+
const lines = [`processes to end: ${running.map(([name, entry]) => `${name} (pid ${entry.pid})`).join(', ') || 'none'}`, `compose project to stop: ${local.project} (volumes and images stay)`];
|
|
10
|
+
if (!execute)
|
|
11
|
+
return { ok: true, lines: [...lines, 'dry run: nothing was stopped; arm it with --execute'], data: { processes: running.map(([name]) => name) } };
|
|
12
|
+
for (const [name, entry] of running) {
|
|
13
|
+
stopProcess(ctx, entry.pid, deps);
|
|
14
|
+
lines.push(`process ${name}: ended`);
|
|
15
|
+
}
|
|
16
|
+
writeRecord(dir, { ...record, processes: {} });
|
|
17
|
+
const stopped = await compose(ctx, local, ['down'], { label: 'compose down' });
|
|
18
|
+
lines.push(stopped.status === 0 ? `compose ${local.project}: stopped, volumes kept` : `compose ${local.project}: down failed (${(stopped.stderr.trim().split(/\r?\n/u)[0] ?? '').slice(0, 160)})`);
|
|
19
|
+
return { ok: stopped.status === 0, lines, data: { processes: running.map(([name]) => name) } };
|
|
20
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { RunResult, StackContext } from './context.mjs';
|
|
2
|
+
import type { LocalBlock } from '../runtime/scripts/hfs/stack-local-config.mjs';
|
|
3
|
+
/** One row of the prerequisite table. */
|
|
4
|
+
export interface Prerequisite {
|
|
5
|
+
id: string;
|
|
6
|
+
ok: boolean;
|
|
7
|
+
detail: string;
|
|
8
|
+
fix: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** The seams of the host probes a spec replaces. */
|
|
11
|
+
export interface HostDeps {
|
|
12
|
+
portFree?: ((port: number) => Promise<boolean>) | undefined;
|
|
13
|
+
diskFreeGb?: ((dir: string) => number | null) | undefined;
|
|
14
|
+
memoryMb?: (() => {
|
|
15
|
+
free: number;
|
|
16
|
+
total: number;
|
|
17
|
+
}) | undefined;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* How to start `command` on this platform without a shell. npm and npx are `.cmd` shims on Windows, which cannot be spawned directly: they run as
|
|
21
|
+
* `node <npm-cli.js>` from the Node install beside this node when it is there, else through `cmd.exe /d /s /c`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function launch(platform: NodeJS.Platform, command: string, args: readonly string[], exists?: (file: string) => boolean): [string, string[]];
|
|
24
|
+
/** Runs a host tool without a shell (see launch). */
|
|
25
|
+
export declare function hostTool(ctx: Pick<StackContext, 'spawn' | 'platform'>, command: string, args: readonly string[], options?: {
|
|
26
|
+
cwd?: string;
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
}): RunResult;
|
|
29
|
+
/** The Node version the app asks for: `.nvmrc`, else the first version of `engines.node`; null when it asks for none. */
|
|
30
|
+
export declare function wantedNode(repoRoot: string): string | null;
|
|
31
|
+
/** True when the environment's secrets folder holds a sealed member. */
|
|
32
|
+
export declare const hasSealedMembers: (secretsDir: string) => boolean;
|
|
33
|
+
/** One row per declared port: free, or held by this environment's own record, or held by someone else (named). */
|
|
34
|
+
export declare function portRows(ctx: StackContext, local: LocalBlock, stateDir: string, deps?: HostDeps): Promise<Prerequisite[]>;
|
|
35
|
+
/** The whole table for one environment: tools, engine, resources, then the declared ports. */
|
|
36
|
+
export declare function hostPrerequisites(ctx: StackContext, local: LocalBlock, where: {
|
|
37
|
+
repoRoot: string;
|
|
38
|
+
stateDir: string;
|
|
39
|
+
secretsDir: string;
|
|
40
|
+
}, deps?: HostDeps): Promise<Prerequisite[]>;
|
|
41
|
+
/** The table as printed lines: `ok ` and `FAIL ` rows, each failure followed by its owner-run step. */
|
|
42
|
+
export declare function tableLines(rows: readonly Prerequisite[]): string[];
|