@runeditrun/rer 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/bin/rer.mjs +3 -0
- package/cli/README.md +243 -0
- package/cli/adapter.mjs +10 -0
- package/cli/ai-local.mjs +65 -0
- package/cli/archive-path.mjs +52 -0
- package/cli/archive-vite.mjs +55 -0
- package/cli/archive.mjs +8 -0
- package/cli/auth-copy.mjs +80 -0
- package/cli/bounded.mjs +70 -0
- package/cli/cache-doctor.mjs +63 -0
- package/cli/container-transfer.mjs +35 -0
- package/cli/container.mjs +62 -0
- package/cli/contract.mjs +25 -0
- package/cli/dependencies.mjs +45 -0
- package/cli/dependency-approval.mjs +33 -0
- package/cli/dependency-doctor.mjs +34 -0
- package/cli/dependency-input.mjs +43 -0
- package/cli/deploy-config.mjs +118 -0
- package/cli/deploy.mjs +37 -0
- package/cli/dev-identity.mjs +80 -0
- package/cli/errors.mjs +14 -0
- package/cli/export.mjs +42 -0
- package/cli/font-cache.mjs +29 -0
- package/cli/hosted-output.mjs +110 -0
- package/cli/independent.mjs +29 -0
- package/cli/inspect.mjs +174 -0
- package/cli/jsonc.mjs +111 -0
- package/cli/lifecycle.mjs +148 -0
- package/cli/local.mjs +100 -0
- package/cli/main.mjs +57 -0
- package/cli/managed-server.mjs +143 -0
- package/cli/node-runtime-cache.mjs +68 -0
- package/cli/node-runtime-discovery.mjs +56 -0
- package/cli/node-runtime-download.mjs +59 -0
- package/cli/node-runtime.mjs +50 -0
- package/cli/output.mjs +22 -0
- package/cli/parse.mjs +77 -0
- package/cli/port.mjs +52 -0
- package/cli/portability-worker.mjs +56 -0
- package/cli/portability.mjs +66 -0
- package/cli/preview-cache.mjs +99 -0
- package/cli/project.mjs +48 -0
- package/cli/release.mjs +24 -0
- package/cli/runtime-front-door.mjs +46 -0
- package/cli/scheduled.mjs +81 -0
- package/cli/server-activity.mjs +12 -0
- package/cli/server-proxy.mjs +93 -0
- package/cli/server-registry.mjs +182 -0
- package/cli/server-resources.mjs +22 -0
- package/cli/settings.mjs +15 -0
- package/cli/setup-answers.mjs +29 -0
- package/cli/setup-input.mjs +113 -0
- package/cli/shared.mjs +7 -0
- package/cli/transfers.mjs +33 -0
- package/cli/version.mjs +36 -0
- package/composition/git-source.mjs +111 -0
- package/edition-authoring/files.mjs +81 -0
- package/edition-authoring/manifest.mjs +65 -0
- package/global/README.md +158 -0
- package/global/checkout.mjs +72 -0
- package/global/cli/command.mjs +13 -0
- package/global/cli/install.mjs +75 -0
- package/global/cli/layout.mjs +45 -0
- package/global/cli/status.mjs +54 -0
- package/global/cli/uninstall.mjs +36 -0
- package/global/delegate.mjs +13 -0
- package/global/launch/capacity.mjs +72 -0
- package/global/launch/dev.mjs +117 -0
- package/global/launch/facts.mjs +94 -0
- package/global/launch/failure.mjs +5 -0
- package/global/launch/local-defaults.mjs +4 -0
- package/global/launch/prepare.mjs +63 -0
- package/global/launch/render.mjs +44 -0
- package/global/launch/state.mjs +106 -0
- package/global/launch/steps.mjs +90 -0
- package/global/launch/stop.mjs +89 -0
- package/global/launch/worker.mjs +62 -0
- package/global/list.mjs +59 -0
- package/global/main.mjs +53 -0
- package/global/package-info.mjs +38 -0
- package/global/registry.mjs +24 -0
- package/global/rer.mjs +29 -0
- package/global/update/archive.mjs +89 -0
- package/global/update/command.mjs +90 -0
- package/global/update/latest.mjs +6 -0
- package/global/update/notice.mjs +58 -0
- package/global/update/registry.mjs +47 -0
- package/global/version.mjs +29 -0
- package/install.sh +144 -0
- package/modules/ai/LICENSE +21 -0
- package/modules/ai/README.md +90 -0
- package/modules/ai/client.mjs +71 -0
- package/modules/ai/config.mjs +39 -0
- package/modules/ai/dev/index.d.mts +6 -0
- package/modules/ai/dev/index.mjs +64 -0
- package/modules/ai/dev/outbox.mjs +33 -0
- package/modules/ai/errors.mjs +28 -0
- package/modules/ai/fake/index.d.mts +3 -0
- package/modules/ai/fake/index.mjs +82 -0
- package/modules/ai/index.d.mts +10 -0
- package/modules/ai/index.mjs +3 -0
- package/modules/ai/package.json +6 -0
- package/modules/container/README.md +156 -0
- package/modules/container/adapters/d1.mjs +34 -0
- package/modules/container/adapters/egress.mjs +40 -0
- package/modules/container/adapters/jobs.mjs +47 -0
- package/modules/container/adapters/objects.mjs +84 -0
- package/modules/container/adapters/retained.mjs +14 -0
- package/modules/container/adapters/sqlite-error.mjs +7 -0
- package/modules/container/adapters/sqlite-worker.mjs +26 -0
- package/modules/container/assets.mjs +14 -0
- package/modules/container/bindings.mjs +1 -0
- package/modules/container/boundary.mjs +38 -0
- package/modules/container/build.mjs +65 -0
- package/modules/container/deploy-input.mjs +17 -0
- package/modules/container/entrypoint.sh +5 -0
- package/modules/container/host.mjs +43 -0
- package/modules/container/ingress.mjs +23 -0
- package/modules/container/lock.mjs +11 -0
- package/modules/container/migrations.mjs +16 -0
- package/modules/container/node-engine.mjs +39 -0
- package/modules/container/portability-schema.mjs +14 -0
- package/modules/container/remote/deploy.py +76 -0
- package/modules/container/remote/files.py +275 -0
- package/modules/container/remote/resources.py +157 -0
- package/modules/container/runner.mjs +10 -0
- package/modules/container/ssh-plan.mjs +44 -0
- package/modules/container/ssh.mjs +63 -0
- package/modules/container/transfer.mjs +27 -0
- package/modules/container/worker-engine.mjs +32 -0
- package/modules/dependencies/README.md +211 -0
- package/modules/dependencies/approval.mjs +51 -0
- package/modules/dependencies/auth.mjs +40 -0
- package/modules/dependencies/authority.mjs +14 -0
- package/modules/dependencies/crypto.mjs +13 -0
- package/modules/dependencies/index.d.mts +23 -0
- package/modules/dependencies/index.mjs +113 -0
- package/modules/dependencies/logout.mjs +42 -0
- package/modules/dependencies/native.mjs +9 -0
- package/modules/dependencies/protocol.mjs +23 -0
- package/modules/dependencies/schema/approval-attempt.sql +3 -0
- package/modules/dependencies/schema/dependencies.sql +15 -0
- package/modules/dependencies/store.mjs +19 -0
- package/modules/deploy-kit/README.md +308 -0
- package/modules/deploy-kit/artifact.mjs +47 -0
- package/modules/deploy-kit/build-modules.mjs +21 -0
- package/modules/deploy-kit/check.mjs +13 -0
- package/modules/deploy-kit/cli.mjs +50 -0
- package/modules/deploy-kit/container-config.mjs +8 -0
- package/modules/deploy-kit/default-stage.mjs +14 -0
- package/modules/deploy-kit/deploy.mjs +80 -0
- package/modules/deploy-kit/health-request.mjs +86 -0
- package/modules/deploy-kit/health.mjs +99 -0
- package/modules/deploy-kit/index.mjs +12 -0
- package/modules/deploy-kit/local-only.mjs +10 -0
- package/modules/deploy-kit/migrations.mjs +48 -0
- package/modules/deploy-kit/owner-cli.mjs +63 -0
- package/modules/deploy-kit/owner.mjs +297 -0
- package/modules/deploy-kit/prepare.mjs +62 -0
- package/modules/deploy-kit/provider.mjs +99 -0
- package/modules/deploy-kit/queue-consumer.mjs +10 -0
- package/modules/deploy-kit/receipt.mjs +16 -0
- package/modules/deploy-kit/render.mjs +47 -0
- package/modules/deploy-kit/resources.mjs +106 -0
- package/modules/deploy-kit/service-grants.mjs +25 -0
- package/modules/deploy-kit/sql-safety.mjs +94 -0
- package/modules/deploy-kit/sql-tokens.mjs +49 -0
- package/modules/deploy-kit/teardown.mjs +100 -0
- package/modules/deploy-kit/templates.mjs +31 -0
- package/modules/deploy-kit/validation.mjs +58 -0
- package/modules/deploy-kit/worker.mjs +105 -0
- package/modules/erasure/CONTRACT.md +90 -0
- package/modules/erasure/README.md +221 -0
- package/modules/erasure/audit.mjs +23 -0
- package/modules/erasure/blobs.mjs +45 -0
- package/modules/erasure/cascade.mjs +91 -0
- package/modules/erasure/completion.mjs +62 -0
- package/modules/erasure/declaration.mjs +51 -0
- package/modules/erasure/fence.mjs +19 -0
- package/modules/erasure/index.d.mts +62 -0
- package/modules/erasure/index.mjs +85 -0
- package/modules/erasure/order.mjs +56 -0
- package/modules/erasure/plan.mjs +121 -0
- package/modules/erasure/rate.mjs +57 -0
- package/modules/erasure/receipt.mjs +32 -0
- package/modules/erasure/root.mjs +21 -0
- package/modules/erasure/schema/erasure-rates.sql +5 -0
- package/modules/erasure/schema/erasure.sql +27 -0
- package/modules/erasure/scope.mjs +29 -0
- package/modules/erasure/sql.mjs +23 -0
- package/modules/erasure/verification.mjs +69 -0
- package/modules/integrations/core/CONTRACT.md +50 -0
- package/modules/integrations/core/README.md +27 -0
- package/modules/integrations/core/crypto.ts +19 -0
- package/modules/integrations/core/database.ts +21 -0
- package/modules/integrations/core/delivery.ts +97 -0
- package/modules/integrations/core/destination.ts +13 -0
- package/modules/integrations/core/index.ts +7 -0
- package/modules/integrations/core/node-transport.ts +72 -0
- package/modules/integrations/core/public-address.ts +57 -0
- package/modules/integrations/core/repository.ts +74 -0
- package/modules/integrations/core/schema.sql +36 -0
- package/modules/integrations/core/sqlite.ts +20 -0
- package/modules/integrations/core/timestamped-body-signature.ts +38 -0
- package/modules/integrations/core/tokens.ts +71 -0
- package/modules/integrations/core/types.ts +52 -0
- package/modules/integrations/core/upgrade-token-usage-context.sql +5 -0
- package/modules/integrations/core/webhook.ts +73 -0
- package/modules/job-queue/README.md +173 -0
- package/modules/job-queue/cli.mjs +64 -0
- package/modules/job-queue/command.mjs +64 -0
- package/modules/job-queue/config.mjs +48 -0
- package/modules/job-queue/identity.mjs +48 -0
- package/modules/job-queue/index.mjs +135 -0
- package/modules/job-queue/membership.mjs +30 -0
- package/modules/job-queue/module.json +10 -0
- package/modules/job-queue/policy.mjs +29 -0
- package/modules/job-queue/processes.mjs +80 -0
- package/modules/job-queue/resources.mjs +37 -0
- package/modules/job-queue/storage.mjs +64 -0
- package/modules/job-queue/supervisor.mjs +30 -0
- package/modules/job-queue/waiting.mjs +16 -0
- package/modules/mail/LICENSE +21 -0
- package/modules/mail/README.md +336 -0
- package/modules/mail/doctor.mjs +81 -0
- package/modules/mail/index.mjs +8 -0
- package/modules/mail/live-proof/README.md +113 -0
- package/modules/mail/live-proof/run.mjs +55 -0
- package/modules/mail/package.json +19 -0
- package/modules/mail/pnpm-lock.yaml +537 -0
- package/modules/mail/src/adapters/cloudflare/README.md +84 -0
- package/modules/mail/src/adapters/cloudflare/headers.mjs +63 -0
- package/modules/mail/src/adapters/cloudflare/index.mjs +47 -0
- package/modules/mail/src/adapters/cloudflare/response.mjs +99 -0
- package/modules/mail/src/adapters/messaging/README.md +92 -0
- package/modules/mail/src/adapters/messaging/contract.mjs +66 -0
- package/modules/mail/src/adapters/messaging/event-schema.mjs +28 -0
- package/modules/mail/src/adapters/messaging/index.mjs +61 -0
- package/modules/mail/src/adapters/messaging/jws.mjs +68 -0
- package/modules/mail/src/adapters/messaging/request.mjs +52 -0
- package/modules/mail/src/adapters/messaging/serialize.mjs +21 -0
- package/modules/mail/src/adapters/resend/README.md +120 -0
- package/modules/mail/src/adapters/resend/index.mjs +22 -0
- package/modules/mail/src/adapters/resend/response.mjs +54 -0
- package/modules/mail/src/adapters/resend/serialize.mjs +46 -0
- package/modules/mail/src/adapters/resend/size.mjs +99 -0
- package/modules/mail/src/adapters/sendgrid/README.md +75 -0
- package/modules/mail/src/adapters/sendgrid/index.mjs +35 -0
- package/modules/mail/src/adapters/sendgrid/serialize.mjs +104 -0
- package/modules/mail/src/core/errors.mjs +9 -0
- package/modules/mail/src/core/transport.mjs +33 -0
- package/modules/mail/src/core/validation.mjs +172 -0
- package/modules/mail/src/development/index.d.mts +37 -0
- package/modules/mail/src/development/index.mjs +35 -0
- package/modules/mail/src/development/outbox.mjs +78 -0
- package/modules/mail/src/development/schema.sql +9 -0
- package/modules/mail/src/development/viewer-html.mjs +42 -0
- package/modules/mail/src/development/viewer.mjs +65 -0
- package/modules/mail/src/doctor/README.md +107 -0
- package/modules/mail/src/doctor/common.mjs +64 -0
- package/modules/mail/src/doctor/dns.mjs +102 -0
- package/modules/mail/src/doctor/index.mjs +28 -0
- package/modules/mail/src/doctor/probe.mjs +66 -0
- package/modules/mail/src/doctor/receive.mjs +81 -0
- package/modules/mail/src/doctor/records.mjs +45 -0
- package/modules/mail/src/events/README.md +268 -0
- package/modules/mail/src/events/d1-schema.sql +25 -0
- package/modules/mail/src/events/d1-storage.mjs +76 -0
- package/modules/mail/src/events/index.mjs +3 -0
- package/modules/mail/src/events/intake.mjs +201 -0
- package/modules/mail/src/events/storage.mjs +52 -0
- package/modules/mail/src/events/verify.mjs +92 -0
- package/modules/mail/src/inbound/README.md +137 -0
- package/modules/mail/src/inbound/bytes.mjs +91 -0
- package/modules/mail/src/inbound/headers.mjs +143 -0
- package/modules/mail/src/inbound/index.mjs +29 -0
- package/modules/mail/src/inbound/mime.mjs +36 -0
- package/modules/mail/src/inbound/multipart.mjs +27 -0
- package/modules/mail/src/inbound/resend-attachment.mjs +43 -0
- package/modules/mail/src/inbound/resend.mjs +54 -0
- package/modules/mail/src/inbound/sendgrid.mjs +60 -0
- package/modules/mail/src/sender.mjs +33 -0
- package/modules/mail/src/sendgrid-email/index.d.mts +33 -0
- package/modules/mail/src/sendgrid-email/index.mjs +86 -0
- package/modules/mail/src/unsubscribe/index.mjs +142 -0
- package/modules/operator-auth/README.md +599 -0
- package/modules/operator-auth/accounts/adapter.mjs +41 -0
- package/modules/operator-auth/accounts/admin.mjs +159 -0
- package/modules/operator-auth/accounts/credentials.mjs +27 -0
- package/modules/operator-auth/accounts/editor-owner.mjs +13 -0
- package/modules/operator-auth/accounts/index.d.mts +9 -0
- package/modules/operator-auth/accounts/index.mjs +9 -0
- package/modules/operator-auth/accounts/native.d.mts +38 -0
- package/modules/operator-auth/accounts/native.mjs +171 -0
- package/modules/operator-auth/accounts/oa.mjs +90 -0
- package/modules/operator-auth/accounts/provider-control.mjs +50 -0
- package/modules/operator-auth/accounts/provider-pin.mjs +14 -0
- package/modules/operator-auth/accounts/roles.mjs +26 -0
- package/modules/operator-auth/accounts/schema/dependency-setup.sql +17 -0
- package/modules/operator-auth/accounts/schema/dev-sessions.sql +22 -0
- package/modules/operator-auth/accounts/schema/domain-rules-step-up.sql +20 -0
- package/modules/operator-auth/accounts/schema/invitation-cancellation.sql +17 -0
- package/modules/operator-auth/accounts/schema/operator-auth.sql +142 -0
- package/modules/operator-auth/accounts/schema/provider-control.sql +30 -0
- package/modules/operator-auth/accounts/schema/provider-pins.sql +6 -0
- package/modules/operator-auth/accounts/settings-page.mjs +4 -0
- package/modules/operator-auth/accounts/store.mjs +246 -0
- package/modules/operator-auth/branding.mjs +66 -0
- package/modules/operator-auth/config.mjs +35 -0
- package/modules/operator-auth/conformance/README.md +81 -0
- package/modules/operator-auth/conformance/index.d.mts +43 -0
- package/modules/operator-auth/conformance/index.mjs +300 -0
- package/modules/operator-auth/conformance/presentation.mjs +57 -0
- package/modules/operator-auth/crypto.mjs +17 -0
- package/modules/operator-auth/dev/badge.mjs +62 -0
- package/modules/operator-auth/dev/index.d.mts +14 -0
- package/modules/operator-auth/dev/index.mjs +137 -0
- package/modules/operator-auth/dev/personas.mjs +17 -0
- package/modules/operator-auth/dev/sign-in-as.d.mts +3 -0
- package/modules/operator-auth/dev/sign-in-as.mjs +25 -0
- package/modules/operator-auth/dev-admission.d.mts +4 -0
- package/modules/operator-auth/dev-admission.mjs +10 -0
- package/modules/operator-auth/dev-gate.mjs +86 -0
- package/modules/operator-auth/docs/README.md +78 -0
- package/modules/operator-auth/docs/native-accounts.md +313 -0
- package/modules/operator-auth/docs/tanstack-start.md +218 -0
- package/modules/operator-auth/docs/vite-spa.md +144 -0
- package/modules/operator-auth/errors.mjs +9 -0
- package/modules/operator-auth/http.mjs +42 -0
- package/modules/operator-auth/identity/admission.mjs +105 -0
- package/modules/operator-auth/identity/after-confirm.mjs +18 -0
- package/modules/operator-auth/identity/email-link/confirmation.mjs +14 -0
- package/modules/operator-auth/identity/email-link/index.d.mts +48 -0
- package/modules/operator-auth/identity/email-link/index.mjs +128 -0
- package/modules/operator-auth/identity/email-link/message.mjs +16 -0
- package/modules/operator-auth/identity/index.d.mts +7 -0
- package/modules/operator-auth/identity/index.mjs +5 -0
- package/modules/operator-auth/identity/oidc/README.md +171 -0
- package/modules/operator-auth/identity/oidc/client-auth.mjs +16 -0
- package/modules/operator-auth/identity/oidc/config.mjs +102 -0
- package/modules/operator-auth/identity/oidc/crypto.mjs +29 -0
- package/modules/operator-auth/identity/oidc/errors.mjs +5 -0
- package/modules/operator-auth/identity/oidc/index.d.mts +33 -0
- package/modules/operator-auth/identity/oidc/index.mjs +287 -0
- package/modules/operator-auth/identity/oidc/jwt.mjs +102 -0
- package/modules/operator-auth/identity/oidc/network.mjs +93 -0
- package/modules/operator-auth/identity/oidc/presets.mjs +26 -0
- package/modules/operator-auth/identity/oidc/schema/oidc.sql +50 -0
- package/modules/operator-auth/identity/oidc/store.mjs +68 -0
- package/modules/operator-auth/identity/principal.mjs +28 -0
- package/modules/operator-auth/index.d.mts +223 -0
- package/modules/operator-auth/index.mjs +11 -0
- package/modules/operator-auth/input.mjs +55 -0
- package/modules/operator-auth/local-cli.mjs +16 -0
- package/modules/operator-auth/page.mjs +34 -0
- package/modules/operator-auth/policy.mjs +20 -0
- package/modules/operator-auth/routes.mjs +91 -0
- package/modules/operator-auth/service.mjs +152 -0
- package/modules/operator-auth/signin/index.mjs +67 -0
- package/modules/owner-claim/README.md +137 -0
- package/modules/owner-claim/idp.mjs +40 -0
- package/modules/owner-claim/index.d.mts +27 -0
- package/modules/owner-claim/index.mjs +39 -0
- package/modules/owner-claim/page.mjs +11 -0
- package/modules/owner-claim/schema/idp.sql +4 -0
- package/modules/owner-claim/schema/owner-claim.sql +18 -0
- package/modules/owner-claim/transaction.mjs +87 -0
- package/modules/owner-claim/verify.mjs +80 -0
- package/modules/portability/README.md +444 -0
- package/modules/portability/bridge.mjs +102 -0
- package/modules/portability/catalogue.mjs +118 -0
- package/modules/portability/cli-options.mjs +38 -0
- package/modules/portability/cli.mjs +56 -0
- package/modules/portability/copies.mjs +68 -0
- package/modules/portability/dev-data.mjs +32 -0
- package/modules/portability/diff.mjs +33 -0
- package/modules/portability/export.mjs +129 -0
- package/modules/portability/files.mjs +65 -0
- package/modules/portability/format.mjs +122 -0
- package/modules/portability/generated.mjs +60 -0
- package/modules/portability/hash.mjs +79 -0
- package/modules/portability/http.mjs +42 -0
- package/modules/portability/import.mjs +185 -0
- package/modules/portability/index.d.mts +58 -0
- package/modules/portability/index.mjs +22 -0
- package/modules/portability/journal.mjs +49 -0
- package/modules/portability/legacy-files.mjs +43 -0
- package/modules/portability/legacy-safety.mjs +27 -0
- package/modules/portability/lines.mjs +36 -0
- package/modules/portability/migration-history.mjs +18 -0
- package/modules/portability/migration-statements.mjs +23 -0
- package/modules/portability/payloads.mjs +52 -0
- package/modules/portability/provider-data.mjs +25 -0
- package/modules/portability/restore-safety.mjs +30 -0
- package/modules/portability/schema/portability.sql +25 -0
- package/modules/portability/storage.mjs +49 -0
- package/modules/release/README.md +212 -0
- package/modules/release/catalogue.mjs +79 -0
- package/modules/release/export.mjs +51 -0
- package/modules/release/produce.mjs +136 -0
- package/modules/release/public-child-transport-state.cjs +29 -0
- package/modules/release/public-child.mjs +71 -0
- package/modules/release/public-git.mjs +21 -0
- package/modules/release/publication-scan.mjs +22 -0
- package/modules/release/publish-handoff.mjs +37 -0
- package/modules/release/source.mjs +71 -0
- package/modules/release/test-summary.mjs +50 -0
- package/onboarding/README.md +163 -0
- package/onboarding/activity.mjs +31 -0
- package/onboarding/adapter-auth.mjs +88 -0
- package/onboarding/adapter-hosting.mjs +35 -0
- package/onboarding/adapter-labels.mjs +13 -0
- package/onboarding/adapter-settings.mjs +129 -0
- package/onboarding/adapter.mjs +238 -0
- package/onboarding/agent-handoff.mjs +9 -0
- package/onboarding/app-processes.mjs +21 -0
- package/onboarding/archive-output.d.mts +3 -0
- package/onboarding/archive-output.mjs +54 -0
- package/onboarding/command-custody.mjs +24 -0
- package/onboarding/command-recovery.mjs +8 -0
- package/onboarding/commands.mjs +166 -0
- package/onboarding/dependency-approval.mjs +36 -0
- package/onboarding/dev-identity.mjs +23 -0
- package/onboarding/email.mjs +24 -0
- package/onboarding/failures.mjs +42 -0
- package/onboarding/input-limits.mjs +7 -0
- package/onboarding/installer-inputs.mjs +24 -0
- package/onboarding/launcher.mjs +34 -0
- package/onboarding/local/build-files.mjs +64 -0
- package/onboarding/local/build.mjs +49 -0
- package/onboarding/local/environment.mjs +30 -0
- package/onboarding/local/files.mjs +35 -0
- package/onboarding/local/operation.mjs +166 -0
- package/onboarding/local/owner.mjs +34 -0
- package/onboarding/local/process-table.mjs +114 -0
- package/onboarding/local/recovery.mjs +79 -0
- package/onboarding/local/state.mjs +31 -0
- package/onboarding/metadata.mjs +7 -0
- package/onboarding/operator-install.mjs +45 -0
- package/onboarding/operator-installer.mjs +39 -0
- package/onboarding/operator-prerequisites.mjs +27 -0
- package/onboarding/package.json +6 -0
- package/onboarding/prefill.mjs +16 -0
- package/onboarding/private-files.mjs +97 -0
- package/onboarding/private.mjs +35 -0
- package/onboarding/process-cleanup.mjs +134 -0
- package/onboarding/process-diagnostics.mjs +91 -0
- package/onboarding/process-scope.mjs +118 -0
- package/onboarding/process-table.mjs +118 -0
- package/onboarding/process-tree.mjs +36 -0
- package/onboarding/production-modules.mjs +22 -0
- package/onboarding/production-options.mjs +40 -0
- package/onboarding/production-plan.mjs +8 -0
- package/onboarding/production-runtime.mjs +30 -0
- package/onboarding/project-vite.mjs +14 -0
- package/onboarding/providers.mjs +289 -0
- package/onboarding/queue.mjs +53 -0
- package/onboarding/queued-entry.mjs +34 -0
- package/onboarding/readiness.mjs +59 -0
- package/onboarding/redaction-jwt.mjs +32 -0
- package/onboarding/redaction.mjs +298 -0
- package/onboarding/schema/SETUP.schema.json +514 -0
- package/onboarding/schema/dependencies.mjs +35 -0
- package/onboarding/schema/json-schema.mjs +202 -0
- package/onboarding/schema/setup-validation.mjs +325 -0
- package/onboarding/secrets.mjs +21 -0
- package/onboarding/server.mjs +188 -0
- package/onboarding/setup-manifest.mjs +116 -0
- package/onboarding/state.mjs +46 -0
- package/onboarding/theme-files.mjs +37 -0
- package/onboarding/theme.mjs +118 -0
- package/onboarding/ui/activity.js +98 -0
- package/onboarding/ui/address-client.mjs +33 -0
- package/onboarding/ui/app.js +377 -0
- package/onboarding/ui/index.html +1 -0
- package/onboarding/ui/model-core.mjs +127 -0
- package/onboarding/ui/model.mjs +5 -0
- package/onboarding/ui/style.css +63 -0
- package/onboarding/ui/theme.css +37 -0
- package/onboarding/ui/workspace-art.js +30 -0
- package/onboarding/wrangler-auth.mjs +43 -0
- package/operations/README.md +246 -0
- package/operations/operations.mjs +168 -0
- package/operations/project-operations.mjs +157 -0
- package/package.json +11 -4
- package/scripts/publish-check/README.md +72 -0
- package/scripts/publish-check/checksum_maps.py +52 -0
- package/scripts/publish-check/cli.py +34 -0
- package/scripts/publish-check/command_arguments.py +51 -0
- package/scripts/publish-check/command_shell_quotes.py +41 -0
- package/scripts/publish-check/command_source_quotes.py +120 -0
- package/scripts/publish-check/content_identities.py +250 -0
- package/scripts/publish-check/copy_identities.py +41 -0
- package/scripts/publish-check/der.py +61 -0
- package/scripts/publish-check/destination.py +117 -0
- package/scripts/publish-check/env_values.py +74 -0
- package/scripts/publish-check/font_licence.py +23 -0
- package/scripts/publish-check/git_history.py +115 -0
- package/scripts/publish-check/github_refs.py +217 -0
- package/scripts/publish-check/gitleaks.py +72 -0
- package/scripts/publish-check/gitleaks.toml +40 -0
- package/scripts/publish-check/history_scan.py +176 -0
- package/scripts/publish-check/hit_location.py +18 -0
- package/scripts/publish-check/hygiene_config.py +42 -0
- package/scripts/publish-check/hygiene_text.py +11 -0
- package/scripts/publish-check/identity-recipe.mjs +54 -0
- package/scripts/publish-check/json_identity.py +79 -0
- package/scripts/publish-check/license-exceptions.json +11 -0
- package/scripts/publish-check/licenses.py +49 -0
- package/scripts/publish-check/policy.py +18 -0
- package/scripts/publish-check/project_identity.py +89 -0
- package/scripts/publish-check/provenance.py +91 -0
- package/scripts/publish-check/provenance_pins.py +33 -0
- package/scripts/publish-check/public_base.py +63 -0
- package/scripts/publish-check/public_hygiene.py +80 -0
- package/scripts/publish-check/publication-config.json +12 -0
- package/scripts/publish-check/publication_config.py +34 -0
- package/scripts/publish-check/report.py +52 -0
- package/scripts/publish-check/repository_commands.py +75 -0
- package/scripts/publish-check/repository_intent.py +52 -0
- package/scripts/publish-check/reviewed.py +41 -0
- package/scripts/publish-check/source_hashes.mjs +6 -0
- package/scripts/publish-check/tree_scan.py +88 -0
- package/scripts/publish-check/trust_bundle.py +87 -0
- package/scripts/publish-check.mjs +58 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { delimiter, join, resolve, sep } from 'node:path';
|
|
5
|
+
import { invalid, missing } from './errors.mjs';
|
|
6
|
+
import { cachedRuntime, nodeCache, tidyRuntime } from './node-runtime-cache.mjs';
|
|
7
|
+
|
|
8
|
+
const exact = value => typeof value === 'string' && /^v?\d+\.\d+\.\d+$/.test(value);
|
|
9
|
+
// Read project-owned declarations, never import an adapter under the wrong Node.
|
|
10
|
+
// Independent projects without an exact pin keep their chosen runtime.
|
|
11
|
+
export async function projectNodePin(root) {
|
|
12
|
+
let descriptor;
|
|
13
|
+
try { descriptor = JSON.parse(await readFile(resolve(root, 'OPERATIONS.json'), 'utf8')); }
|
|
14
|
+
catch { return undefined; } // Normal project validation owns descriptor errors.
|
|
15
|
+
const firstParty = Boolean(descriptor.seedContract);
|
|
16
|
+
let file, nvm, pkg = {};
|
|
17
|
+
try { file = (await readFile(resolve(root, '.node-version'), 'utf8')).trim(); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
18
|
+
try { nvm = (await readFile(resolve(root, '.nvmrc'), 'utf8')).trim(); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
19
|
+
try { pkg = JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')); } catch {}
|
|
20
|
+
if (file && !exact(file)) throw invalid('Use an exact Node release (major.minor.patch) in .node-version.');
|
|
21
|
+
const declarations = [file, nvm, pkg.engines?.node, pkg.volta?.node].filter(exact).map(value => value.replace(/^v/, ''));
|
|
22
|
+
if (new Set(declarations).size > 1) throw invalid('The exact Node pins in .node-version, .nvmrc, package.json engines.node or volta.node disagree. Make the project pins agree before running rer.');
|
|
23
|
+
const pin = declarations[0];
|
|
24
|
+
if (!pin && firstParty) throw missing('Declare the project Node release in .node-version and package.json engines.node before running rer.');
|
|
25
|
+
return pin?.replace(/^v/, '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function matchesRuntime(binary, version) {
|
|
29
|
+
const result = spawnSync(binary, ['--version'], { encoding: 'utf8', timeout: 1500, maxBuffer: 1024, windowsHide: true });
|
|
30
|
+
return result.status === 0 && result.stdout.trim() === `v${version}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function installedRuntime(version) {
|
|
34
|
+
const home = homedir(), name = process.platform === 'win32' ? 'node.exe' : 'node';
|
|
35
|
+
let cached, cacheError;
|
|
36
|
+
try { cached = await cachedRuntime(version); } catch (error) { cacheError = error; }
|
|
37
|
+
if (cached && matchesRuntime(cached, version)) { await tidyRuntime(version); return cached; }
|
|
38
|
+
const candidates = [
|
|
39
|
+
join(home, '.nvm', 'versions', 'node', `v${version}`, 'bin', name),
|
|
40
|
+
...(process.env.NVM_DIR ? [join(process.env.NVM_DIR, 'versions', 'node', `v${version}`, 'bin', name)] : []),
|
|
41
|
+
join(home, '.volta', 'tools', 'image', 'node', version, 'bin', name),
|
|
42
|
+
...['.fnm', '.local/share/fnm', 'Library/Application Support/fnm'].map(path => join(home, path, 'node-versions', `v${version}`, 'installation', 'bin', name)),
|
|
43
|
+
...(process.env.PATH ?? '').split(delimiter).filter(Boolean).map(path => join(path, name)),
|
|
44
|
+
...['/opt/homebrew/opt', '/usr/local/opt', '/usr/local/homebrew/opt'].map(path => join(path, `node@${version.split('.')[0]}`, 'bin', name)),
|
|
45
|
+
];
|
|
46
|
+
let cacheRoot; try { cacheRoot = await realpath(nodeCache()); } catch {}
|
|
47
|
+
const seen = new Set(), deadline = Date.now() + 10000;
|
|
48
|
+
for (const candidate of candidates) {
|
|
49
|
+
if (Date.now() > deadline) break;
|
|
50
|
+
let canonical; try { canonical = await realpath(candidate); } catch { continue; }
|
|
51
|
+
if (cacheRoot && canonical.startsWith(cacheRoot + sep)) continue; // Cache binaries require their verified receipt.
|
|
52
|
+
if (seen.has(canonical)) continue; seen.add(canonical);
|
|
53
|
+
if (matchesRuntime(canonical, version)) return canonical;
|
|
54
|
+
}
|
|
55
|
+
if (cacheError) throw cacheError;
|
|
56
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
3
|
+
import { chmod, mkdtemp, readFile, rename, rm } from 'node:fs/promises';
|
|
4
|
+
import { pipeline } from 'node:stream/promises';
|
|
5
|
+
import { Transform } from 'node:stream';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { matchesRuntime } from './node-runtime-discovery.mjs';
|
|
9
|
+
import { prepareNodeCache, runtimeBinary, runtimeName, recordRuntime, cachedRuntime, tidyRuntime } from './node-runtime-cache.mjs';
|
|
10
|
+
|
|
11
|
+
async function download(url, destination, limit, signal) {
|
|
12
|
+
const response = await fetch(url, { signal, redirect: 'error' });
|
|
13
|
+
if (!response.ok || !response.body) throw Error('The official Node release could not be downloaded.');
|
|
14
|
+
let bytes = 0;
|
|
15
|
+
const bounded = new Transform({ transform(chunk, encoding, callback) {
|
|
16
|
+
bytes += chunk.length; callback(bytes > limit ? Error('Node release download exceeds its size limit.') : null, chunk);
|
|
17
|
+
} });
|
|
18
|
+
await pipeline(response.body, bounded, createWriteStream(destination, { flags: 'wx', mode: 0o600 }), { signal });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function extract(archive, directory, name, signal) {
|
|
22
|
+
const child = spawn('tar', ['-xzf', archive, '-C', directory, '--exclude', name + '/include'], { stdio: 'ignore', signal });
|
|
23
|
+
await new Promise((resolve, reject) => {
|
|
24
|
+
let failure;
|
|
25
|
+
child.once('error', error => { failure = error; });
|
|
26
|
+
child.once('close', code => code === 0 && !failure ? resolve() : reject(failure ?? Error('Verified Node release extraction failed.')));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function downloadRuntime(version, cancellation) {
|
|
31
|
+
if (!['darwin', 'linux'].includes(process.platform) || !['x64', 'arm64', 'armv7l', 'ppc64', 's390x'].includes(process.arch)) throw Error('Automatic Node download is unavailable on this platform.');
|
|
32
|
+
const cache = await prepareNodeCache(), name = runtimeName(version), filename = `${name}.tar.gz`;
|
|
33
|
+
const url = `https://nodejs.org/dist/v${version}/`;
|
|
34
|
+
const temporary = await mkdtemp(join(cache, '.download-'));
|
|
35
|
+
const signal = AbortSignal.any([cancellation, AbortSignal.timeout(60000)]);
|
|
36
|
+
try {
|
|
37
|
+
const sums = join(temporary, 'SHASUMS256.txt'), archive = join(temporary, filename);
|
|
38
|
+
await download(url + 'SHASUMS256.txt', sums, 1024 * 1024, signal);
|
|
39
|
+
const expected = (await readFile(sums, 'utf8')).split(/\r?\n/).map(line => line.trim().split(/\s+/)).find(parts => parts[1] === filename)?.[0];
|
|
40
|
+
if (!/^[a-f0-9]{64}$/.test(expected ?? '')) throw Error('The official release checksum is missing.');
|
|
41
|
+
await download(url + filename, archive, 150 * 1024 * 1024, signal);
|
|
42
|
+
const hash = createHash('sha256');
|
|
43
|
+
for await (const chunk of createReadStream(archive)) hash.update(chunk);
|
|
44
|
+
if (hash.digest('hex') !== expected) throw Error('Official Node release checksum verification failed; nothing was extracted or executed.');
|
|
45
|
+
// This is the first point at which any downloaded content can be executed.
|
|
46
|
+
await extract(archive, temporary, name, signal);
|
|
47
|
+
const extracted = join(temporary, name), binary = runtimeBinary(extracted);
|
|
48
|
+
if (!matchesRuntime(binary, version)) throw Error('Verified Node release did not report the project pin.');
|
|
49
|
+
await chmod(extracted, 0o700);
|
|
50
|
+
await recordRuntime(extracted, version, expected);
|
|
51
|
+
const destination = join(cache, name);
|
|
52
|
+
try { await rename(extracted, destination); }
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (!['EEXIST', 'ENOTEMPTY'].includes(error.code) || !(await cachedRuntime(version))) throw error;
|
|
55
|
+
}
|
|
56
|
+
await tidyRuntime(version);
|
|
57
|
+
return runtimeBinary(destination);
|
|
58
|
+
} finally { await rm(temporary, { recursive: true, force: true }); }
|
|
59
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { constants } from 'node:os';
|
|
3
|
+
import { dirname, delimiter, resolve } from 'node:path';
|
|
4
|
+
import { projectNodePin, installedRuntime } from './node-runtime-discovery.mjs';
|
|
5
|
+
import { downloadRuntime } from './node-runtime-download.mjs';
|
|
6
|
+
import { missing, RerError } from './errors.mjs';
|
|
7
|
+
|
|
8
|
+
import { shellQuote } from './node-runtime-cache.mjs';
|
|
9
|
+
function repair(version, options) {
|
|
10
|
+
return `npm exec --yes --package=node@${version} -- node ${[resolve(process.argv[1]), options.verb, '--cwd', options.cwd].map(shellQuote).join(' ')}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function selectNodeRuntime(options, argv, out) {
|
|
14
|
+
// Inspection keeps its read-only contract, and stop works without tool setup.
|
|
15
|
+
if (['describe', 'check', 'doctor'].includes(options.verb) || options.stop) return undefined;
|
|
16
|
+
const version = await projectNodePin(options.cwd);
|
|
17
|
+
if (!version || process.versions.node === version) return undefined;
|
|
18
|
+
const command = repair(version, options);
|
|
19
|
+
if (process.env.RER_NODE_AUTO === '0') throw missing(`Node ${version} is required; automatic selection is disabled by RER_NODE_AUTO=0. Run: ${command} (repeat any original options privately).`);
|
|
20
|
+
const cancellation = new AbortController();
|
|
21
|
+
let termination, child;
|
|
22
|
+
const handlers = new Map(['SIGINT', 'SIGTERM'].map(signal => [signal, () => {
|
|
23
|
+
termination = signal; cancellation.abort(); if (child) child.kill(signal);
|
|
24
|
+
}]));
|
|
25
|
+
for (const [signal, handler] of handlers) process.on(signal, handler);
|
|
26
|
+
try {
|
|
27
|
+
let binary = await installedRuntime(version);
|
|
28
|
+
if (termination) return { exitCode: termination === 'SIGINT' ? 130 : 143 };
|
|
29
|
+
if (!binary) {
|
|
30
|
+
try { binary = await downloadRuntime(version, cancellation.signal); }
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (termination) return { exitCode: termination === 'SIGINT' ? 130 : 143 };
|
|
33
|
+
if (error instanceof RerError) throw error;
|
|
34
|
+
const reason = /unavailable on this platform/.test(error.message) ? error.message : /checksum/.test(error.message) ? 'Official release checksum verification failed.' : 'The exact official release could not be acquired within 60 seconds; check the connection and available disk space.';
|
|
35
|
+
throw missing(`Node ${version} is required. ${reason} With network access, run: ${command} (repeat any original options privately).`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const message = `Using project Node ${version} (current Node ${process.versions.node}).`;
|
|
39
|
+
if (options.json) out.event('node-selection', { versionSelected: version, previousVersion: process.versions.node, message });
|
|
40
|
+
else out.human(message);
|
|
41
|
+
child = spawn(binary, [...process.execArgv, process.argv[1], ...argv], {
|
|
42
|
+
cwd: process.cwd(), stdio: 'inherit', env: { ...process.env, PATH: dirname(binary) + delimiter + (process.env.PATH ?? '') },
|
|
43
|
+
});
|
|
44
|
+
const result = await new Promise((resolve, reject) => {
|
|
45
|
+
child.once('error', reject);
|
|
46
|
+
child.once('exit', (code, signal) => resolve({ exitCode: code ?? (signal && constants.signals[signal] ? 128 + constants.signals[signal] : 1) }));
|
|
47
|
+
});
|
|
48
|
+
return result;
|
|
49
|
+
} finally { for (const [signal, handler] of handlers) process.off(signal, handler); }
|
|
50
|
+
}
|
package/cli/output.mjs
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { writeSync } from 'node:fs';
|
|
2
|
+
export function output(options) {
|
|
3
|
+
let closed = false, finished = false;
|
|
4
|
+
// A bounded child retains the caller's human progress but sends its closing
|
|
5
|
+
// outcome as data, so only the drained outer workflow renders the result.
|
|
6
|
+
const structuredResult = options.json || process.env.RER_BOUNDED_OUTPUT === '1';
|
|
7
|
+
const write = (fd, text) => { if (closed && fd === 1) return; try { writeSync(fd, text); } catch (error) { if (error.code === 'EPIPE') closed = true; else if (!['EAGAIN', 'EWOULDBLOCK'].includes(error.code)) throw error; } };
|
|
8
|
+
return {
|
|
9
|
+
event(event, details = {}) { write(1, JSON.stringify({ version: 1, verb: options.verb ?? 'unknown', event, ...details }) + '\n'); },
|
|
10
|
+
human(text) { write(1, text + '\n'); },
|
|
11
|
+
progress(text) { write(2, text); },
|
|
12
|
+
result(ok, exitCode, details = {}) {
|
|
13
|
+
if (finished) throw Error('Duplicate CLI result'); finished = true;
|
|
14
|
+
if (structuredResult) this.event('result', { ok, exitCode, ...details });
|
|
15
|
+
else if (!['describe', 'check', 'dev', 'preview', 'outbox', 'setup'].includes(options.verb) || !ok) this.human(`${options.verb}: ${ok ? 'complete' : 'failed'} (exit ${exitCode})`);
|
|
16
|
+
},
|
|
17
|
+
error(error) {
|
|
18
|
+
if (structuredResult) this.event('error', { code: error.code, message: error.message, exitCode: error.exitCode });
|
|
19
|
+
else write(2, error.message + '\n');
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
package/cli/parse.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { commonFlags, serverDefaults, verbs } from './contract.mjs';
|
|
3
|
+
import { invalid, refuse } from './errors.mjs';
|
|
4
|
+
const booleans = new Set(['--help', '--json', '--non-interactive', '--yes', '--dry-run', '--open', '--stop', '--no-queue', '--login', '--legacy-v1', '--acknowledge-unverified-metadata']);
|
|
5
|
+
export const mailbox = value => typeof value === 'string' && value.length <= 254 && /^[a-zA-Z0-9.!+_%\-]+@[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)+$/.test(value);
|
|
6
|
+
export const loopback = host => ['127.0.0.1', 'localhost', '::1'].includes(host);
|
|
7
|
+
export const origin = (host, port) => `http://${host === '::1' ? '[::1]' : host}:${port}`;
|
|
8
|
+
|
|
9
|
+
export function parse(argv, cwd = process.cwd(), { independent = false, deferTail = false } = {}) {
|
|
10
|
+
const [verb, ...args] = argv;
|
|
11
|
+
if (!verb || verb === '--help') return { help: true };
|
|
12
|
+
if (verb === 'release') return { verb, releaseArgs: args };
|
|
13
|
+
if (verb === 'archive') return { verb, archiveArgs: args };
|
|
14
|
+
// Independent projects own verb names; only the common wrapper flags apply
|
|
15
|
+
// to unknown names. Profile resolution rejects undeclared commands later.
|
|
16
|
+
if (!/^[a-z][a-z0-9-]*$/.test(verb)) throw invalid('Unknown rer command. Run rer --help.');
|
|
17
|
+
const allowed = new Set(independent ? [...commonFlags,...(['install','setup','build','verify','deploy','release','export','import','dev','preview'].includes(verb)?['--no-queue']:[])] : verbs[verb]?.flags ?? commonFlags), options = { verb, cwd: resolve(cwd), forwarded: [], supplied: new Set() };
|
|
18
|
+
let separator = false;
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const flag = args[i];
|
|
21
|
+
if (flag === '--') { if (separator) throw invalid('Use only one -- separator.'); separator = true; if (independent || deferTail) { options.forwarded = args.slice(i + 1); break; } continue; }
|
|
22
|
+
if (separator && !flag.startsWith('--')) { options.forwarded.push(...args.slice(i)); break; }
|
|
23
|
+
// pnpm run VERB -- FLAGS forwards the separator before wrapper flags.
|
|
24
|
+
if (separator && !allowed.has(flag) && !flag.startsWith('--')) { options.forwarded.push(...args.slice(i)); break; }
|
|
25
|
+
if (!allowed.has(flag)) {
|
|
26
|
+
if(flag==='--strictPort')throw invalid('Unsupported --strictPort. Use --port PORT; strict port binding is automatic.');
|
|
27
|
+
const suggestion=[...allowed].sort((a,b)=>distance(flag,a)-distance(flag,b))[0];
|
|
28
|
+
throw invalid(`Unsupported flag ${/^--[a-zA-Z-]{1,40}$/.test(flag)?flag:'(invalid)'} for ${verb}.${suggestion?` Did you mean ${suggestion}?`:''} Run rer ${verb} --help.`);
|
|
29
|
+
}
|
|
30
|
+
if (options.supplied.has(flag)) throw invalid(`Repeated flag ${flag} for ${verb}. Run rer ${verb} --help.`);
|
|
31
|
+
options.supplied.add(flag);
|
|
32
|
+
const key = flag.slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
33
|
+
if (booleans.has(flag)) options[key] = true;
|
|
34
|
+
else {
|
|
35
|
+
const value = args[++i];
|
|
36
|
+
if (value === undefined || value.startsWith('--') || !value.trim()) throw invalid(`A value is required for ${flag}.`);
|
|
37
|
+
options[key] = value;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (options.help) return options;
|
|
41
|
+
if (deferTail) { options.cwd=resolve(cwd,options.cwd);return options; }
|
|
42
|
+
if(options.stop && ['--port','--host','--idle-timeout','--no-queue','--as','--login','--models','--models-service'].some(flag=>options.supplied.has(flag)))throw invalid('--stop cannot be combined with start-only flags.');
|
|
43
|
+
if(options.models!==undefined && !['fake','live'].includes(options.models))throw invalid('--models takes fake or live.');
|
|
44
|
+
if(options.modelsService!==undefined && options.models!=='live')throw invalid('--models-service requires --models live.');
|
|
45
|
+
if(options.as!==undefined && (options.login || !/^[a-z][a-z0-9-]{0,39}$/.test(options.as)))throw invalid(options.login?'--as and --login cannot be combined.':'--as takes a persona name such as owner, signed-out or admin.');
|
|
46
|
+
if(['dev','preview'].includes(verb) && !options.stop){
|
|
47
|
+
const value=options.idleTimeout??process.env.RER_SERVER_IDLE_SECONDS??String(serverDefaults.idleSeconds);
|
|
48
|
+
if(!/^[1-9][0-9]*$/.test(value)||!Number.isSafeInteger(Number(value)))throw invalid('Idle timeout must be positive integer seconds.');
|
|
49
|
+
options.idleTimeout=Number(value);
|
|
50
|
+
}
|
|
51
|
+
if (options.port !== undefined) {
|
|
52
|
+
if (!/^\d+$/.test(options.port) || Number(options.port) < 1 || Number(options.port) > 65535) throw invalid('Port must be a decimal integer from 1 to 65535.');
|
|
53
|
+
options.port = Number(options.port);
|
|
54
|
+
}
|
|
55
|
+
for(const key of ['httpPort','httpsPort'])if(options[key]!==undefined){if(!/^[0-9]+$/.test(options[key])||Number(options[key])<1||Number(options[key])>65535)throw invalid('Ingress port must be an integer from 1 to 65535.');options[key]=Number(options[key]);}
|
|
56
|
+
if(['image','bindAddress','httpPort','httpsPort'].some(key=>options[key]!==undefined)&&options.platform!=='container')throw invalid('Container image and ingress flags require --platform container.');
|
|
57
|
+
if(options.platform !== undefined && !['workers','container'].includes(options.platform))throw invalid('Platform must be workers or container.');
|
|
58
|
+
if (options.host !== undefined && !(verb === 'deploy' && options.platform === 'container') && !loopback(options.host)) throw refuse('Host must be exactly 127.0.0.1, localhost or ::1.');
|
|
59
|
+
if (options.ownerEmail !== undefined) { if (!mailbox(options.ownerEmail)) throw invalid('Provide a valid plain owner mailbox.'); options.ownerEmail = options.ownerEmail.toLowerCase(); }
|
|
60
|
+
if (options.ownerName !== undefined && (options.ownerName.length > 200 || /[\p{Cc}\p{Cf}]/u.test(options.ownerName))) throw invalid('Provide a valid owner name.');
|
|
61
|
+
if (options.target !== undefined && !['local', 'remote'].includes(options.target)) throw invalid('Target must be local or remote.');
|
|
62
|
+
if (options.profile !== undefined && options.profile !== 'first-party') throw invalid('The supported explicit profile is first-party.');
|
|
63
|
+
if (options.environment !== undefined && !/^[a-z][a-z0-9-]{0,39}$/.test(options.environment)) throw invalid('Provide a valid deployment environment name.');
|
|
64
|
+
if (options.origin !== undefined) {
|
|
65
|
+
let url; try { url = new URL(options.origin); } catch { throw invalid('Provide a valid credential-free origin.'); }
|
|
66
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash || url.pathname !== '/') throw invalid('Provide a credential-free HTTP origin without a path.');
|
|
67
|
+
}
|
|
68
|
+
if (options.cwd !== undefined) options.cwd = resolve(cwd, options.cwd);
|
|
69
|
+
return options;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function help(verb) {
|
|
73
|
+
if (!verb) return `Usage: rer VERB [FLAGS]\nCommands: ${Object.keys(verbs).join(', ')}, release\nRun rer VERB --help for flags.\n`;
|
|
74
|
+
return `Usage: rer ${verb} [FLAGS]\nFlags: ${(verbs[verb]?.flags ?? commonFlags).join(', ')}\n${verbs[verb]?.effects ?? 'Execute the declared OPERATIONS v2 argv with explicit arguments after --.'}\n`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function distance(a,b){let row=Array.from({length:b.length+1},(_,i)=>i);for(let i=1;i<=a.length;i++){const next=[i];for(let j=1;j<=b.length;j++)next[j]=Math.min(next[j-1]+1,row[j]+1,row[j-1]+(a[i-1]===b[j-1]?0:1));row=next;}return row[b.length];}
|
package/cli/port.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createServer } from 'node:net';
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { readJson } from '../modules/job-queue/storage.mjs';
|
|
5
|
+
import { serversDirectory, stopInstruction } from './server-registry.mjs';
|
|
6
|
+
import { shared } from './shared.mjs';
|
|
7
|
+
|
|
8
|
+
export const defaultPort = 8787;
|
|
9
|
+
|
|
10
|
+
async function bindable(host, port) {
|
|
11
|
+
const socket = createServer();
|
|
12
|
+
try { await new Promise((resolve, reject) => { socket.once('error', reject); socket.listen(port, host, resolve); }); return true; }
|
|
13
|
+
catch { return false; }
|
|
14
|
+
finally { if (socket.listening) await new Promise(resolve => socket.close(resolve)); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// The first port from `from` that nothing listens on, on any address, and that
|
|
18
|
+
// this host can bind. Another checkout's server may listen on every address,
|
|
19
|
+
// which a bind to 127.0.0.1 alone does not notice.
|
|
20
|
+
export async function firstFreePort(host, from = defaultPort) {
|
|
21
|
+
const { listeningPorts } = await shared('process-diagnostics.mjs');
|
|
22
|
+
const busy = await listeningPorts();
|
|
23
|
+
for (let port = from; port <= 65535; port++) if (!busy.has(port) && await bindable(host, port)) return port;
|
|
24
|
+
throw Error(`No free port from ${from} upward.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Who holds a port: a server another checkout registered (its path and the
|
|
28
|
+
// command that stops it), else the process and its working directory.
|
|
29
|
+
export async function portHolder(port, host, root, env = process.env) {
|
|
30
|
+
const { portListener } = await shared('process-diagnostics.mjs');
|
|
31
|
+
const listener = await portListener(port, host);
|
|
32
|
+
if (!listener) return undefined;
|
|
33
|
+
const state = await readJson(join(serversDirectory(env), 'state.json'), { running: [] }).catch(() => ({ running: [] }));
|
|
34
|
+
const record = (state.running ?? []).find(item => item.owner?.pid === listener.pid && item.port === port);
|
|
35
|
+
return { ...listener, ...(record ? { server: record, checkout: record.checkout, own: record.checkout === await realpath(root) } : {}) };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The refusal for a busy port. For a checkout that wants to move, it names the
|
|
39
|
+
// holder and gives the exact command that moves this checkout to a free port.
|
|
40
|
+
export async function busyPortMessage({ port, host, root, saved, env = process.env }) {
|
|
41
|
+
const holder = await portHolder(port, host, root, env);
|
|
42
|
+
const next = await firstFreePort(host, port + 1).catch(() => undefined);
|
|
43
|
+
const kind = saved ? `Port ${port} is saved for this checkout but is` : `Port ${port} is`;
|
|
44
|
+
if (!holder) return `${kind} in use by another process that this account cannot inspect. Stop it, or pick a free port${next ? ` such as ${next}` : ''}.`;
|
|
45
|
+
if (holder.own) return `${kind} held by this checkout's own ${holder.server.mode} server (PID ${holder.pid}). Stop it with: ${stopInstruction(holder.server)}`;
|
|
46
|
+
const who = holder.server
|
|
47
|
+
? `the ${holder.server.mode} server for ${holder.checkout} (PID ${holder.pid})`
|
|
48
|
+
: `PID ${holder.pid} (${holder.command})${holder.cwd ? ` running in ${holder.cwd}` : ''}`;
|
|
49
|
+
const move = next ? `\nTo move this checkout to a free port and keep its data, run this in its folder:\n rer setup --port ${next}` : '';
|
|
50
|
+
const stop = holder.server ? `\nOr stop that server first: ${stopInstruction(holder.server)}` : `\nOr stop it with: kill -TERM ${holder.pid}`;
|
|
51
|
+
return `${kind} in use by ${who}.${move}${stop}`;
|
|
52
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { portabilityPlan, ensurePortabilitySchema } from '../modules/container/portability-schema.mjs';
|
|
2
|
+
import { exportArchive, importArchive, fileRecords } from '../modules/portability/files.mjs';
|
|
3
|
+
import { readLegacyArchive } from '../modules/portability/format.mjs';
|
|
4
|
+
import { recoverLegacyArchive } from '../modules/portability/legacy-files.mjs';
|
|
5
|
+
import { legacyEmptyTables, requireLegacyEmpty } from '../modules/portability/legacy-safety.mjs';
|
|
6
|
+
import { migrationReplay } from '../modules/portability/migration-history.mjs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { createPortability, PortabilityError, readArchive } from '../modules/portability/index.mjs';
|
|
11
|
+
import { parseJsonc } from './jsonc.mjs';
|
|
12
|
+
|
|
13
|
+
// The migrations this installation applied, in Wrangler's applied order, so a
|
|
14
|
+
// restore can place an older archive within them.
|
|
15
|
+
async function appliedMigrations(root, db, binding, split) {
|
|
16
|
+
const config = parseJsonc(await readFile(join(root, 'wrangler.jsonc'), 'utf8'));
|
|
17
|
+
const directory = join(root, config.d1_databases.find(entry => entry.binding === binding).migrations_dir ?? 'migrations');
|
|
18
|
+
const names = (await db.prepare('SELECT name FROM d1_migrations ORDER BY id').all()).results.map(row => row.name);
|
|
19
|
+
return Promise.all(names.map(async name => ({ name, statements: split(await readFile(join(directory, name), 'utf8')) })));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(direction, { root, path, journal, binding, buckets, policy, transient, legacyRecovery }) {
|
|
23
|
+
// Validate all inputs before opening bindings or acquiring a restore lock.
|
|
24
|
+
if (direction === 'import') {
|
|
25
|
+
for await (const record of fileRecords(path,legacyRecovery?readLegacyArchive:readArchive)) {}
|
|
26
|
+
if(journal)for await(const record of fileRecords(journal)) {}
|
|
27
|
+
}
|
|
28
|
+
const { getPlatformProxy, unstable_splitSqlQuery } = createRequire(join(root, 'package.json'))('wrangler');
|
|
29
|
+
|
|
30
|
+
// Unlike Wrangler's --persist-to base, the API takes the versioned directory.
|
|
31
|
+
const proxy = await getPlatformProxy({ configPath: join(root, 'wrangler.jsonc'), persist: { path: join(root, '.wrangler/state/v3') }, remoteBindings: false });
|
|
32
|
+
try {
|
|
33
|
+
const migrations=async()=>migrationReplay(await appliedMigrations(root,proxy.env[binding],binding,unstable_splitSqlQuery),policy);
|
|
34
|
+
const options={ db: proxy.env[binding], buckets: Object.fromEntries(Object.entries(buckets).map(([name, binding]) => [name, proxy.env[binding]])), policy, migrations };
|
|
35
|
+
if(direction==='legacy-preflight') {
|
|
36
|
+
await requireLegacyEmpty(options,await legacyEmptyTables(options.db));
|
|
37
|
+
return {checkedEmpty:true};
|
|
38
|
+
}
|
|
39
|
+
await ensurePortabilitySchema(proxy.env[binding], await portabilityPlan(unstable_splitSqlQuery));
|
|
40
|
+
if(legacyRecovery)return await recoverLegacyArchive(options,path,legacyRecovery.output,transient,legacyRecovery.acknowledged);
|
|
41
|
+
const service = await createPortability(options);
|
|
42
|
+
return await (direction === 'export' ? exportArchive(service,path,transient) : importArchive(service,path,transient,journal));
|
|
43
|
+
} finally { await proxy.dispose(); }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try { console.log(JSON.stringify({ ok: true, ...await run(process.argv[2], JSON.parse(process.argv[3])) })); }
|
|
47
|
+
catch (error) {
|
|
48
|
+
// Erasure-fence refusals name the fence: an older archive is refused, not a failed transfer.
|
|
49
|
+
const refusal = error instanceof PortabilityError && (error.status === 409 || ['wrong_product', 'schema_mismatch', 'table_inventory', 'configuration_inventory', 'erased_row', 'erasure_epoch', 'later_journal', 'invalid_journal'].includes(error.code.split(':')[0]));
|
|
50
|
+
const exitCode = error.message === 'archive_json' ? 2 : refusal || error.code === 'EEXIST' ? 4 : 1;
|
|
51
|
+
// Report only symbolic codes; native database errors can contain private data.
|
|
52
|
+
const reason = error instanceof PortabilityError ? error.code.split(':')[0] : /^[A-Z_]+$/.test(error.code ?? '') ? error.code : error.message === 'archive_json' ? 'archive_json' : 'native_error';
|
|
53
|
+
// A later migration that changes restorable data is named: it is a file of this installation.
|
|
54
|
+
const migration = error instanceof PortabilityError && error.code.startsWith('schema_mismatch:migration_data:') ? error.code.slice('schema_mismatch:migration_data:'.length) : undefined;
|
|
55
|
+
console.log(JSON.stringify({ ok: false, code: exitCode === 2 ? 'invalid_archive' : exitCode === 4 ? 'restore_refused' : 'transfer_failed', reason, ...(migration ? { migration } : {}), exitCode })); process.exitCode = exitCode;
|
|
56
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { lstat } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { relative } from 'node:path';
|
|
4
|
+
import { readArchive, PortabilityError } from '../modules/portability/index.mjs';
|
|
5
|
+
import { readLegacyArchive } from '../modules/portability/format.mjs';
|
|
6
|
+
import { fileRecords } from '../modules/portability/files.mjs';
|
|
7
|
+
import { RerError, refuse } from './errors.mjs';
|
|
8
|
+
|
|
9
|
+
// Seeds declare their real bindings and data policy. The shared module owns the
|
|
10
|
+
// archive format and atomic restore; the shared runner owns its native children.
|
|
11
|
+
export function localPortability({ database, binding = 'DB', buckets = {}, policy, transient = [] }) {
|
|
12
|
+
async function run({ root, path, journal, store, signal, operationToken, platform, legacyRecovery }, direction) {
|
|
13
|
+
if (direction === 'export') {
|
|
14
|
+
if (await lstat(path).catch(error => { if (error.code === 'ENOENT') return null; throw error; })) throw refuse('Export requires a new archive path.');
|
|
15
|
+
}
|
|
16
|
+
if(legacyRecovery && (!legacyRecovery.acknowledged || await lstat(legacyRecovery.output).catch(error=>{if(error.code==='ENOENT')return null;throw error;})))throw refuse('Legacy recovery requires acknowledgement and a new output archive path.');
|
|
17
|
+
if (direction === 'import') {
|
|
18
|
+
// Refuse damaged or unverifiable archives before migrations open the
|
|
19
|
+
// destination database. Worker and finalization verify again before publish.
|
|
20
|
+
try {
|
|
21
|
+
for await (const record of fileRecords(path,legacyRecovery?readLegacyArchive:readArchive)) {}
|
|
22
|
+
if(journal)for await(const record of fileRecords(journal)) {}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (!(error instanceof PortabilityError)) throw error;
|
|
26
|
+
const message = error.code === 'legacy_archive_unverifiable'
|
|
27
|
+
? 'Restore refused (legacy_archive_unverifiable): version 1 metadata cannot be authenticated. Upgrade the original installation and re-export, or use rer import --legacy-v1 --input FILE --yes --acknowledge-unverified-metadata --output exports/recovered-v2.rer in an empty installation. See modules/portability/README.md#backup-only-v1-recovery.'
|
|
28
|
+
: `Restore refused (${error.code}): archive verification failed.`;
|
|
29
|
+
const refused = error.status === 409;
|
|
30
|
+
throw new RerError(refused ? 'restore_refused' : 'transfer_failed', message, refused ? 4 : 1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if(legacyRecovery)store.report('Legacy v1 recovery: manifest metadata is unverified. Restored data will be audited and exported to a new v2 backup.');
|
|
34
|
+
if(platform==='container') return (await import('./container-transfer.mjs')).containerTransfer({root,path,journal,store,signal,direction,binding,buckets,policy,transient});
|
|
35
|
+
const input={ root, path, journal, binding, buckets, policy, transient, legacyRecovery };
|
|
36
|
+
// Do not apply pending product migrations to a populated recovery target.
|
|
37
|
+
// Check real existing bindings first; finalize repeats this under the lock.
|
|
38
|
+
if(legacyRecovery)await nativeWorker(store,'legacy-preflight',input,signal);
|
|
39
|
+
await store.run('Applying local data migrations', 'pnpm', ['exec', 'wrangler', 'd1', 'migrations', 'apply', database, '--local', '--persist-to', '.wrangler/state', '--config', 'wrangler.jsonc'], {
|
|
40
|
+
cwd: root, signal, env: { ...process.env, RER_LOCAL_OPERATION: operationToken, WRANGLER_SEND_METRICS: 'false' },
|
|
41
|
+
});
|
|
42
|
+
const receipt=await nativeWorker(store,direction,input,signal);
|
|
43
|
+
if(legacyRecovery)receipt.output=relative(root,legacyRecovery.output);
|
|
44
|
+
return { target: 'local', [direction === 'export' ? 'output' : 'input']: relative(root, path), ...receipt };
|
|
45
|
+
}
|
|
46
|
+
return { legacyRecovery:true, export: context => run(context, 'export'), import: context => run(context, 'import') };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function nativeWorker(store,direction,input,signal) {
|
|
50
|
+
const worker = fileURLToPath(new URL('./portability-worker.mjs', import.meta.url));
|
|
51
|
+
const child = store.spawn(`${direction === 'legacy-preflight' ? 'Checking empty recovery destination' : direction === 'export' ? 'Exporting' : 'Restoring'} local installation data`, process.execPath,
|
|
52
|
+
[worker, direction, JSON.stringify(input)], { cwd: input.root, signal, env: { ...process.env, WRANGLER_SEND_METRICS: 'false' } });
|
|
53
|
+
let output = '';
|
|
54
|
+
child.stdout.on('data', chunk => { output += chunk; });
|
|
55
|
+
const closed = new Promise(resolve => child.once('close', (code, signal) => resolve({ code, signal })));
|
|
56
|
+
await new Promise(resolve => { child.once('exit', resolve); child.once('error', resolve); });
|
|
57
|
+
await store.stop(child);
|
|
58
|
+
const status = await closed;
|
|
59
|
+
let receipt; try { receipt = JSON.parse(output.trim().split('\n').at(-1)); } catch { throw new RerError('transfer_failed', 'Local portability did not return a complete receipt.', 1); }
|
|
60
|
+
if (status.code !== 0 || !receipt.ok) throw new RerError(receipt.code ?? 'transfer_failed', receipt.reason==='legacy_restored_export_failed'
|
|
61
|
+
? 'Legacy data was restored and audited, but its v2 export failed. Preserve this installation, resolve the output problem, then run rer export --output FILE. Do not reset it to retry recovery.'
|
|
62
|
+
: receipt.migration ? `Restore refused (schema_mismatch:migration_data): migration ${receipt.migration} changed data after this archive was made, so its rows would skip that change. Restore an archive made after it.`
|
|
63
|
+
: `The portability module refused or failed the archive operation (${receipt.reason ?? 'incomplete'}).`, receipt.exitCode ?? 1);
|
|
64
|
+
delete receipt.ok;
|
|
65
|
+
return receipt;
|
|
66
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {shared} from './shared.mjs';
|
|
2
|
+
import {invalid,refuse} from './errors.mjs';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { lstat, readFile, readdir, unlink } from 'node:fs/promises';
|
|
5
|
+
import { join, relative, resolve, dirname, extname } from 'node:path';
|
|
6
|
+
const excluded = new Set(['node_modules','.git','.wrangler','.seed','.rer','.cache','.state','dist','coverage','test-results','playwright-report']);
|
|
7
|
+
// A project declares the paths its build regenerates (`generatedOutputs` on the
|
|
8
|
+
// adapter): project-relative files or directories. They are not build inputs.
|
|
9
|
+
export function generatedOutputs(project) {
|
|
10
|
+
const declared=project.adapter.generatedOutputs??[];
|
|
11
|
+
if(!Array.isArray(declared))throw invalid('Adapter generatedOutputs must be a list of project-relative paths.');
|
|
12
|
+
return declared.map(path=>{
|
|
13
|
+
const name=typeof path==='string'?path.replace(/\/+$/,''):'';
|
|
14
|
+
if(!name||name.startsWith('/')||name.includes('\\')||name.split('/').some(part=>part===''||part==='.'||part==='..'))throw invalid(`Adapter generatedOutputs entry ${JSON.stringify(path)} must be a project-relative file or directory path.`);
|
|
15
|
+
return name;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
19
|
+
async function tree(root,{privateFiles=[],generated=[],outputs=false,metadata}={}) {
|
|
20
|
+
const files={};
|
|
21
|
+
async function walk(directory) {
|
|
22
|
+
for(const entry of (await readdir(directory,{withFileTypes:true})).sort((a,b)=>a.name.localeCompare(b.name))){
|
|
23
|
+
const path=join(directory,entry.name),name=relative(root,path);
|
|
24
|
+
if(!outputs && (((directory===root&&excluded.has(entry.name))||['node_modules','.git'].includes(entry.name))||/^\.env(?:\.|$)|^\.dev\.vars(?:\.|$)/.test(entry.name)||privateFiles.includes(name)))continue;
|
|
25
|
+
if(generated.some(item=>name===item||name.startsWith(item+'/')))continue;
|
|
26
|
+
if(entry.isSymbolicLink())throw Error('Build receipt cannot certify symbolic source/output links.');
|
|
27
|
+
if(entry.isDirectory())await walk(path);
|
|
28
|
+
else if(entry.isFile()){
|
|
29
|
+
const before=await lstat(path,{bigint:true});files[name]=digest(await readFile(path));
|
|
30
|
+
const after=await lstat(path,{bigint:true});
|
|
31
|
+
if(stamp(before)!==stamp(after))throw Error('Build input/output changed while it was read.');
|
|
32
|
+
if(metadata)metadata[name]=stamp(after);
|
|
33
|
+
}
|
|
34
|
+
else throw Error('Build receipt requires regular files.');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
await walk(root);return files;
|
|
38
|
+
}
|
|
39
|
+
const stamp=status=>[status.dev,status.ino,status.size,status.mtimeNs,status.ctimeNs].map(String).join(':');
|
|
40
|
+
export async function buildInputs(project,{revision=false}={}) {
|
|
41
|
+
const metadata={},sources=await tree(project.root,{privateFiles:project.adapter.settings,generated:generatedOutputs(project),metadata});
|
|
42
|
+
const pending=Object.keys(sources).filter(name=>/\.[cm]?[jt]sx?$/.test(name)).map(name=>resolve(project.root,name));
|
|
43
|
+
const seen=new Set(),external={};
|
|
44
|
+
while(pending.length){
|
|
45
|
+
const file=pending.pop();if(seen.has(file))continue;seen.add(file);
|
|
46
|
+
const bytes=await readFile(file);const name=relative(project.root,file);
|
|
47
|
+
if(name.startsWith('..')){external[name]=digest(bytes);metadata[name]=stamp(await lstat(file,{bigint:true}));}
|
|
48
|
+
for(const match of bytes.toString().matchAll(/(?:from\s*|import\s*\(?|require\s*\()(['"])(\.[^'"\n]+)\1/g)){
|
|
49
|
+
const path=resolve(dirname(file),match[2]);
|
|
50
|
+
for(const candidate of [path,...['.ts','.tsx','.js','.mjs','/index.ts','/index.js'].map(ext=>path+ext),...(extname(path)==='.js'?[path.slice(0,-3)+'.ts']:[])]){
|
|
51
|
+
try{if((await lstat(candidate)).isFile()){if(!/\.dev\.vars|(?:^|\/)\.env(?:\.|$)/.test(candidate))pending.push(candidate);break;}}catch{}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const tools={};
|
|
56
|
+
for(const name of ['vite','wrangler','@cloudflare/vite-plugin','typescript']){
|
|
57
|
+
try{const pkg=JSON.parse(await readFile(join(project.root,'node_modules',name,'package.json'),'utf8'));tools[name]=pkg.version;}catch{tools[name]=null;}
|
|
58
|
+
}
|
|
59
|
+
const inputs=digest(JSON.stringify({sources,external:Object.fromEntries(Object.entries(external).sort()),node:process.versions.node,tools,build:project.adapter.hooks.build}));
|
|
60
|
+
return revision?{inputs,revision:digest(JSON.stringify(metadata)),files:{...sources,...external},stamps:metadata}:inputs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Name the inputs the build touched: edited contents versus a rewrite that left
|
|
64
|
+
// the bytes alone (a generated file the project has not declared).
|
|
65
|
+
function changedInputs(before,after) {
|
|
66
|
+
const names=new Set([...Object.keys(before.files),...Object.keys(after.files)]);
|
|
67
|
+
const edited=[...names].filter(name=>before.files[name]!==after.files[name]).sort();
|
|
68
|
+
const rewritten=[...names].filter(name=>before.files[name]===after.files[name]&&before.stamps[name]!==after.stamps[name]).sort();
|
|
69
|
+
// Many files in one directory read as that directory, which is also the declaration to make.
|
|
70
|
+
const grouped=items=>{
|
|
71
|
+
const folders=new Map();for(const name of items){const folder=dirname(name);folders.set(folder,(folders.get(folder)??0)+1);}
|
|
72
|
+
return [...new Set(items.map(name=>folders.get(dirname(name))>1?`${dirname(name)}/ (${folders.get(dirname(name))} files)`:name))];
|
|
73
|
+
};
|
|
74
|
+
const list=(items,group)=>{const names=group?grouped(items):items;return names.slice(0,5).join(', ')+(names.length>5?` and ${names.length-5} more`:'');};
|
|
75
|
+
const parts=[];
|
|
76
|
+
if(edited.length)parts.push(`Source changed while the production build ran: ${list(edited)}. Leave it alone during the build, then run preview again.`);
|
|
77
|
+
if(rewritten.length){const them=rewritten.length>1?'them':'it';parts.push(`The production build rewrote ${list(rewritten,true)} with identical contents. If the build generates ${them}, declare ${them} as generatedOutputs in the project adapter; otherwise stop whatever rewrites ${them}.`);}
|
|
78
|
+
return parts.join(' ')||'Build inputs changed during the production build (tools, Node or the build command). Run preview again.';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function productionBuild(project,build,report=()=>{}) {
|
|
82
|
+
const {privateDirectory,writePrivateFile,readPrivateFile}=await shared('private-files.mjs');
|
|
83
|
+
const directory=await privateDirectory(project.root,['.rer','local']),receipt=join(directory,'production.json'),output=join(project.root,'dist');
|
|
84
|
+
try{const info=await lstat(receipt);if(!info.isFile()||info.isSymbolicLink()||info.uid!==process.getuid())throw Error('Production receipt must be an owned regular file.');}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
85
|
+
const before=await buildInputs(project,{revision:true}),inputs=before.inputs;
|
|
86
|
+
try {
|
|
87
|
+
const prior=JSON.parse(await readPrivateFile(receipt));
|
|
88
|
+
const outputs=await tree(output,{outputs:true});
|
|
89
|
+
if(prior.version===1 && prior.inputs===inputs && outputs['server/wrangler.json'] && prior.outputs===digest(JSON.stringify(outputs))){report('Reusing intact production output.');return {reused:true};}
|
|
90
|
+
} catch { /* Missing/corrupt input receipt or output cannot authorize reuse. */ }
|
|
91
|
+
await unlink(receipt).catch(error=>{if(error.code!=='ENOENT')throw error;});
|
|
92
|
+
report('Building production output.');const started=performance.now();await build();const buildMilliseconds=performance.now()-started;
|
|
93
|
+
const outputs=await tree(output,{outputs:true});
|
|
94
|
+
if(!outputs['server/wrangler.json'])throw Error('Production configuration is missing.');
|
|
95
|
+
const after=await buildInputs(project,{revision:true});
|
|
96
|
+
if(after.inputs!==inputs||after.revision!==before.revision)throw refuse(changedInputs(before,after));
|
|
97
|
+
await writePrivateFile(receipt,JSON.stringify({version:1,inputs,outputs:digest(JSON.stringify(outputs)),buildMilliseconds})+'\n');
|
|
98
|
+
return {reused:false};
|
|
99
|
+
}
|
package/cli/project.mjs
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { readFile, lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { readOperations } from '../operations/operations.mjs';
|
|
5
|
+
import { invalid, missing, refuse } from './errors.mjs';
|
|
6
|
+
|
|
7
|
+
export async function contained(root, path, { optional = false, privateFile = false } = {}) {
|
|
8
|
+
if (typeof path !== 'string' || !path || isAbsolute(path) || path.split(/[\\/]/).some(part => part === '..' || part === '')) throw refuse('Path must be a contained checkout path without traversal.');
|
|
9
|
+
const checkout = await realpath(root), destination = resolve(checkout, path), parts = relative(checkout, destination).split(sep);
|
|
10
|
+
let current = checkout;
|
|
11
|
+
for (let i = 0; i < parts.length; i++) {
|
|
12
|
+
current = resolve(current, parts[i]);
|
|
13
|
+
let status; try { status = await lstat(current); } catch (error) { if (error.code === 'ENOENT' && optional) return destination; if (error.code === 'ENOENT') throw missing(`Missing input file: ${path}. See the project operating documentation.`); throw error; }
|
|
14
|
+
if (status.isSymbolicLink()) throw refuse(`Symlink refused at ${path}; preserve it and resolve placement first.`);
|
|
15
|
+
if (i < parts.length - 1 && !status.isDirectory()) throw refuse(`Unsafe path ancestor for ${path}.`);
|
|
16
|
+
if (privateFile && i === parts.length - 1 && (!status.isFile() || status.nlink !== 1 || (status.mode & 0o077) !== 0 || (process.getuid && status.uid !== process.getuid()))) throw refuse(`Input must be an owned regular private file (0600): ${path}.`);
|
|
17
|
+
}
|
|
18
|
+
return destination;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function resolveProject(root) {
|
|
22
|
+
root = await realpath(root);
|
|
23
|
+
let loaded;
|
|
24
|
+
try { loaded = await readOperations(root); } catch (error) {
|
|
25
|
+
if (error.code === 'ENOENT') throw missing('OPERATIONS.json or a declared input is missing. See the project operating documentation.');
|
|
26
|
+
throw invalid('Malformed operations descriptor or unsafe input. Correct OPERATIONS.json and its contained inputs.');
|
|
27
|
+
}
|
|
28
|
+
const marker = loaded.descriptor.seedContract;
|
|
29
|
+
let adapter;
|
|
30
|
+
if (marker) {
|
|
31
|
+
await contained(root, marker.adapter);
|
|
32
|
+
try { adapter = (await import(pathToFileURL(resolve(root, marker.adapter)))).commandAdapter; }
|
|
33
|
+
catch { throw invalid('The declared adapter could not be loaded. Restore its portable sources.'); }
|
|
34
|
+
}
|
|
35
|
+
return { root, ...loaded, profile: marker ? 'first-party' : 'independent', adapter };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Only the descriptor marker distinguishes package forwarding from a raw
|
|
39
|
+
// independent tail. Schema validation and adapter imports happen after parsing.
|
|
40
|
+
export async function parsingProfile(root) {
|
|
41
|
+
try { const descriptor=JSON.parse(await readFile(resolve(root,'OPERATIONS.json'),'utf8'));return descriptor.version===2&&!descriptor.seedContract?'independent':'first-party'; }
|
|
42
|
+
catch { return 'first-party'; } // Help still works with absent/broken metadata.
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function readPackage(root) {
|
|
46
|
+
try { return JSON.parse(await readFile(resolve(root, 'package.json'), 'utf8')); }
|
|
47
|
+
catch { return {}; }
|
|
48
|
+
}
|
package/cli/release.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Preserve release's accepted parser, producer, receipt and validation exits.
|
|
2
|
+
export async function dispatchRelease(args) {
|
|
3
|
+
try {
|
|
4
|
+
const options = {};
|
|
5
|
+
for (let index = 0; index < args.length; index++) {
|
|
6
|
+
if(args[index]==='--no-queue'){}
|
|
7
|
+
else if (args[index] === '--tag') options.tag = args[index + 1]?.startsWith('v') ? args[++index] : true;
|
|
8
|
+
else if (args[index] === '--clean-history') options.cleanHistory = true;
|
|
9
|
+
else if (args[index] === '--dry-run') options.dryRun = true;
|
|
10
|
+
else if (['--repo', '--project', '--repository', '--output', '--ref', '--env-file', '--onto'].includes(args[index]) && args[index + 1] && !args[index + 1].startsWith('--'))
|
|
11
|
+
options[args[index] === '--env-file' ? 'envFile' : args[index].slice(2)] = args[++index];
|
|
12
|
+
else throw Error('Invalid release arguments');
|
|
13
|
+
}
|
|
14
|
+
if(args.includes('--no-queue')){const {shared}=await import('./shared.mjs');const {commandPolicy}=await shared('queue.mjs');const policy=await commandPolicy();try{if(!policy.ci)throw Error('Original CI required');}finally{await policy.close();}}
|
|
15
|
+
if (!options.repo || !options.output) throw Error('Release requires --repo and --output');
|
|
16
|
+
const { release } = await import('../modules/release/produce.mjs');
|
|
17
|
+
const receipt = await release(options);
|
|
18
|
+
console.log(JSON.stringify(receipt, null, 2));
|
|
19
|
+
return receipt.status === 'BLOCKED' ? 1 : 0;
|
|
20
|
+
} catch {
|
|
21
|
+
console.error('Release failed during argument, source or artifact validation. No release completed.');
|
|
22
|
+
return 2;
|
|
23
|
+
}
|
|
24
|
+
}
|