@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,106 @@
|
|
|
1
|
+
// Per-checkout launch state, private to the account, beside the server
|
|
2
|
+
// registry: <servers>/launches/<name>-<hash>/
|
|
3
|
+
// worker.json the preparing worker (PID, start time, token) while it runs
|
|
4
|
+
// events-<token>.jsonl what that worker has done, one JSON event per line
|
|
5
|
+
// install.json whether an install this launcher started has finished
|
|
6
|
+
// setup.json whether a setup this launcher started has finished
|
|
7
|
+
// launch.log the worker's private log (child output)
|
|
8
|
+
// server.log the private log of the server rer started
|
|
9
|
+
// server.json that server's PID and start time, and the process rer spawned
|
|
10
|
+
// stop.json when rer dev --stop last ran: a launch requested before it stops
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { appendFile, mkdir, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
13
|
+
import { basename, join } from 'node:path';
|
|
14
|
+
import { processInfo } from '../../modules/job-queue/processes.mjs';
|
|
15
|
+
import { serversDirectory } from '../registry.mjs';
|
|
16
|
+
|
|
17
|
+
export function launchPath(checkout, env = process.env) {
|
|
18
|
+
const name = basename(checkout).toLowerCase().replace(/[^a-z0-9-]+/g, '-').slice(0, 40) || 'checkout';
|
|
19
|
+
return join(serversDirectory(env), 'launches', `${name}-${createHash('sha256').update(checkout).digest('hex').slice(0, 12)}`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function launchDirectory(checkout, env = process.env) {
|
|
23
|
+
const directory = launchPath(checkout, env);
|
|
24
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
25
|
+
return directory;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function readJson(path) { try { return JSON.parse(await readFile(path, 'utf8')); } catch (error) { if (error.code === 'ENOENT' || error instanceof SyntaxError) return undefined; throw error; } }
|
|
29
|
+
|
|
30
|
+
async function live(holder) {
|
|
31
|
+
if (!holder?.pid) return false;
|
|
32
|
+
const current = await processInfo(holder.pid);
|
|
33
|
+
return Boolean(current && !current.zombie && current.start === holder.start);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function liveWorker(directory) {
|
|
37
|
+
const holder = await readJson(join(directory, 'worker.json'));
|
|
38
|
+
return await live(holder) ? holder : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A port chosen for a setup stays claimed while the process that chose it
|
|
42
|
+
// lives, so two launches preparing at once never pick the same port.
|
|
43
|
+
export async function claimPort(port, env = process.env) {
|
|
44
|
+
const directory = join(serversDirectory(env), 'launches', 'ports'), path = join(directory, `${port}.json`);
|
|
45
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
46
|
+
const { start } = await processInfo(process.pid);
|
|
47
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
48
|
+
try {
|
|
49
|
+
await writeFile(path, JSON.stringify({ pid: process.pid, start }), { flag: 'wx', mode: 0o600 });
|
|
50
|
+
return { release: async () => { if ((await readJson(path))?.pid === process.pid) await rm(path, { force: true }); } };
|
|
51
|
+
} catch (error) { if (error.code !== 'EEXIST') throw error; }
|
|
52
|
+
if (await live(await readJson(path))) return undefined;
|
|
53
|
+
await rm(path, { force: true });
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// The worker claims the checkout's launch; a second worker started at the
|
|
59
|
+
// same moment finds the first and leaves.
|
|
60
|
+
export async function claimLaunch(directory, token) {
|
|
61
|
+
const path = join(directory, 'worker.json');
|
|
62
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
63
|
+
const { start } = await processInfo(process.pid);
|
|
64
|
+
try {
|
|
65
|
+
await writeFile(path, JSON.stringify({ token, pid: process.pid, start, startedAt: Date.now(), events: `events-${token}.jsonl` }), { flag: 'wx', mode: 0o600 });
|
|
66
|
+
for (const entry of await readdir(directory)) if (/^events-.*\.jsonl$/.test(entry) && entry !== `events-${token}.jsonl`) await rm(join(directory, entry), { force: true });
|
|
67
|
+
return true;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error.code !== 'EEXIST') throw error;
|
|
70
|
+
if (await liveWorker(directory)) return false;
|
|
71
|
+
await rm(path, { force: true });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function releaseLaunch(directory, token) {
|
|
78
|
+
const holder = await readJson(join(directory, 'worker.json'));
|
|
79
|
+
if (holder?.token === token) await rm(join(directory, 'worker.json'), { force: true });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function eventLog(directory, token) {
|
|
83
|
+
const path = join(directory, `events-${token}.jsonl`);
|
|
84
|
+
return { path, write: event => appendFile(path, JSON.stringify({ at: Date.now(), ...event }) + '\n', { mode: 0o600 }) };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// { state: 'running' | 'done' | 'failed', ... } for the last install or setup
|
|
88
|
+
// this launcher ran. 'running' survives only a step whose process was killed.
|
|
89
|
+
const stepRecord = name => ({
|
|
90
|
+
read: directory => readJson(join(directory, `${name}.json`)),
|
|
91
|
+
write: (directory, record) => writeFile(join(directory, `${name}.json`), JSON.stringify({ ...record, at: Date.now() }), { mode: 0o600 }),
|
|
92
|
+
});
|
|
93
|
+
export const installState = stepRecord('install'), setupState = stepRecord('setup');
|
|
94
|
+
|
|
95
|
+
// A stop asks every launch started before it to end. A worker reads this right
|
|
96
|
+
// after it claims the launch; a stop that ran before the claim found no worker
|
|
97
|
+
// to signal, so the worker ends itself here instead of starting anything.
|
|
98
|
+
export const stopRequest = {
|
|
99
|
+
write: directory => writeFile(join(directory, 'stop.json'), JSON.stringify({ at: Date.now(), pid: process.pid }), { mode: 0o600 }),
|
|
100
|
+
covers: async (directory, requestedAt) => ((await readJson(join(directory, 'stop.json')))?.at ?? -Infinity) >= requestedAt,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const serverRecord = {
|
|
104
|
+
read: directory => readJson(join(directory, 'server.json')),
|
|
105
|
+
write: (directory, record) => writeFile(join(directory, 'server.json'), JSON.stringify(record), { mode: 0o600 }),
|
|
106
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Run the pinned dispatcher's public verbs as children, reading its --json
|
|
2
|
+
// events: install and setup to completion, and dev until its `ready` event.
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { createReadStream } from 'node:fs';
|
|
5
|
+
import { open, stat } from 'node:fs/promises';
|
|
6
|
+
import { createInterface } from 'node:readline';
|
|
7
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
+
import { LaunchFailure } from './failure.mjs';
|
|
9
|
+
|
|
10
|
+
function events(line) { try { const value = JSON.parse(line); return value && typeof value === 'object' ? value : undefined; } catch { return undefined; } }
|
|
11
|
+
|
|
12
|
+
// What a child's JSON event means for the person waiting.
|
|
13
|
+
export function waitingNote(event) {
|
|
14
|
+
if (event.event === 'waiting') return `waiting in the job queue${event.reason ? `: ${event.reason}` : ''}`;
|
|
15
|
+
if (event.event === 'node-selection') return event.message;
|
|
16
|
+
if (event.event === 'port') return `using port ${event.port}`;
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function failureFrom(code, error, output) {
|
|
21
|
+
return new LaunchFailure(code || 1, error?.message ?? (output.trim().split('\n').slice(-5).join('\n') || `The command stopped with exit ${code}.`), error);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// A child joins the set a stop signals; one spawned as the stop arrives is
|
|
25
|
+
// signalled at once rather than left to finish.
|
|
26
|
+
function own(children, child, signal) {
|
|
27
|
+
children.add(child);
|
|
28
|
+
if (signal?.aborted) child.kill('SIGTERM');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// One bounded verb, never prompting: no stdin, --non-interactive, --json.
|
|
32
|
+
export async function runVerb(entry, args, { root, env, note, log, children, signal }) {
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
const child = spawn(process.execPath, [entry, ...args, '--json', '--non-interactive'], { cwd: root, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
35
|
+
own(children, child, signal);
|
|
36
|
+
let error, output = '';
|
|
37
|
+
const lines = createInterface({ input: child.stdout });
|
|
38
|
+
lines.on('line', line => { const event = events(line); if (event?.event === 'error') error = event; const text = event && waitingNote(event); if (text) note(text); log(line); });
|
|
39
|
+
child.stderr.on('data', data => { output = (output + data).slice(-8192); log(String(data).trimEnd()); });
|
|
40
|
+
const code = await new Promise((resolve, reject) => { child.once('error', reject); child.once('close', (exit, signal) => resolve(exit ?? (signal ? 128 : 1))); });
|
|
41
|
+
children.delete(child);
|
|
42
|
+
if (code !== 0) throw failureFrom(code, error, output);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Follow a growing log file from an offset, line by line.
|
|
46
|
+
async function follow(path, from, onLine, stopped) {
|
|
47
|
+
let offset = from, rest = '';
|
|
48
|
+
while (!stopped()) {
|
|
49
|
+
const size = (await stat(path)).size;
|
|
50
|
+
if (size > offset) {
|
|
51
|
+
const handle = await open(path, 'r');
|
|
52
|
+
try {
|
|
53
|
+
const { buffer, bytesRead } = await handle.read({ buffer: Buffer.alloc(size - offset), position: offset });
|
|
54
|
+
offset += bytesRead; rest += buffer.subarray(0, bytesRead).toString('utf8');
|
|
55
|
+
} finally { await handle.close(); }
|
|
56
|
+
const parts = rest.split('\n'); rest = parts.pop();
|
|
57
|
+
for (const line of parts) if (onLine(line)) return;
|
|
58
|
+
}
|
|
59
|
+
await delay(200);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Start `dev` detached into its own process group with its output in a private
|
|
64
|
+
// log, and return its ready event once it prints one. The server outlives this
|
|
65
|
+
// process and stops on rer dev --stop or its idle timeout.
|
|
66
|
+
export async function startServer(entry, args, { root, env, logPath, children, signal }) {
|
|
67
|
+
signal?.throwIfAborted();
|
|
68
|
+
const handle = await open(logPath, 'a', 0o600), from = (await handle.stat()).size;
|
|
69
|
+
let child;
|
|
70
|
+
try { child = spawn(process.execPath, [entry, 'dev', ...args, '--json'], { cwd: root, env, detached: true, stdio: ['ignore', handle.fd, handle.fd] }); }
|
|
71
|
+
finally { await handle.close(); }
|
|
72
|
+
own(children, child, signal);
|
|
73
|
+
let exit, ready, error, output = '';
|
|
74
|
+
child.once('exit', (code, name) => { exit = code ?? (name ? 128 : 1); });
|
|
75
|
+
await follow(logPath, from, line => {
|
|
76
|
+
const event = events(line);
|
|
77
|
+
if (event?.event === 'ready') { ready = event; return true; }
|
|
78
|
+
if (event?.event === 'error') error = event;
|
|
79
|
+
if (!event) output = (output + line + '\n').slice(-8192);
|
|
80
|
+
return false;
|
|
81
|
+
}, () => exit !== undefined);
|
|
82
|
+
if (!ready) {
|
|
83
|
+
// Read whatever the exiting dispatcher wrote last.
|
|
84
|
+
for await (const line of createInterface({ input: createReadStream(logPath, { start: from }) })) { const event = events(line); if (event?.event === 'ready') ready = event; if (event?.event === 'error') error = event; }
|
|
85
|
+
}
|
|
86
|
+
children.delete(child);
|
|
87
|
+
if (!ready) throw failureFrom(exit, error, output);
|
|
88
|
+
child.unref();
|
|
89
|
+
return { ready, pid: child.pid, reused: exit === 0 };
|
|
90
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// rer dev --stop for a checkout the launcher may be preparing or serving.
|
|
2
|
+
// It ends a launch still preparing (or one that has not yet claimed the
|
|
3
|
+
// checkout), has the pinned dispatcher stop the registered server, and then
|
|
4
|
+
// checks that every process it found -- the worker, the server rer started,
|
|
5
|
+
// the registered server and everything they spawned -- has exited. It says
|
|
6
|
+
// what it stopped, or exits non-zero naming what is still running.
|
|
7
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
8
|
+
import { processInfo } from '../../modules/job-queue/processes.mjs';
|
|
9
|
+
import { readProcessTable, sameBirth } from '../../onboarding/process-table.mjs';
|
|
10
|
+
import { delegate } from '../delegate.mjs';
|
|
11
|
+
import { liveServers } from '../registry.mjs';
|
|
12
|
+
import { LaunchFailure } from './failure.mjs';
|
|
13
|
+
import { liveWorker, serverRecord, stopRequest } from './state.mjs';
|
|
14
|
+
|
|
15
|
+
export async function stopLaunch(target, devArgs, { checkout, directory }) {
|
|
16
|
+
// Written before looking for a worker: one that claims the launch after this
|
|
17
|
+
// look reads it and ends itself (worker.mjs).
|
|
18
|
+
await stopRequest.write(directory);
|
|
19
|
+
const holder = await liveWorker(directory);
|
|
20
|
+
const launch = holder ? await tree([holder]) : [];
|
|
21
|
+
if (holder) await endWorker(holder);
|
|
22
|
+
// Only now: a worker stopped while its server came up has registered it.
|
|
23
|
+
const record = (await liveServers()).find(item => item.checkout === checkout);
|
|
24
|
+
const started = await serverRecord.read(directory);
|
|
25
|
+
const server = await tree([record?.owner, record?.runtimeProcess, started, started?.spawned]);
|
|
26
|
+
await delegate(target, ['dev', ...devArgs]);
|
|
27
|
+
if (process.exitCode) return process.exitCode;
|
|
28
|
+
const left = await exited([...launch, ...server], 15000);
|
|
29
|
+
if (left.length) {
|
|
30
|
+
const pids = left.map(item => item.pid).join(' ');
|
|
31
|
+
process.stderr.write(`rer dev --stop: ${left.length === 1 ? 'a process' : `${left.length} processes`} of this checkout's dev server ${left.length === 1 ? 'is' : 'are'} still running 15 seconds after the stop (PID ${pids}). Stop ${left.length === 1 ? 'it' : 'them'} with kill -TERM ${pids}, then run rer dev --stop again.\n`);
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
if (!devArgs.includes('--json')) process.stdout.write(said({ checkout, record, started, server, holder, launch }) + '\n');
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// SIGTERM the preparing worker and wait for it to exit.
|
|
39
|
+
async function endWorker(holder) {
|
|
40
|
+
try { process.kill(holder.pid, 'SIGTERM'); } catch (error) { if (error.code === 'ESRCH') return; throw error; }
|
|
41
|
+
for (let waited = 0; waited < 60000; waited += 250) {
|
|
42
|
+
const current = await processInfo(holder.pid);
|
|
43
|
+
if (!current || current.zombie || current.start !== holder.start) return;
|
|
44
|
+
await delay(250);
|
|
45
|
+
}
|
|
46
|
+
throw new LaunchFailure(1, `The launch worker (PID ${holder.pid}) did not stop within a minute.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The live processes among these recorded identities ({pid, start}), with
|
|
50
|
+
// every process descended from them, as process-table entries.
|
|
51
|
+
async function tree(identities) {
|
|
52
|
+
const roots = new Set();
|
|
53
|
+
for (const identity of identities) {
|
|
54
|
+
if (!identity?.pid || !identity.start) continue;
|
|
55
|
+
const current = await processInfo(identity.pid);
|
|
56
|
+
if (current && !current.zombie && current.start === identity.start) roots.add(identity.pid);
|
|
57
|
+
}
|
|
58
|
+
if (!roots.size) return [];
|
|
59
|
+
const table = await readProcessTable(), found = table.filter(entry => roots.has(entry.pid));
|
|
60
|
+
for (let index = 0; index < found.length; index++) {
|
|
61
|
+
for (const entry of table) if (entry.ppid === found[index].pid && !found.includes(entry)) found.push(entry);
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Wait until none of these processes is running; return those still running.
|
|
67
|
+
async function exited(processes, within) {
|
|
68
|
+
const deadline = Date.now() + within;
|
|
69
|
+
for (;;) {
|
|
70
|
+
const table = processes.length ? await readProcessTable() : [];
|
|
71
|
+
const left = processes.filter(item => table.some(entry => sameBirth(item, entry) && !entry.state.startsWith('Z')));
|
|
72
|
+
if (!left.length || Date.now() >= deadline) return left;
|
|
73
|
+
await delay(250);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function said({ checkout, record, started, server, holder, launch }) {
|
|
78
|
+
const lines = [], more = n => n > 0 ? ` with ${n} more ${n === 1 ? 'process' : 'processes'}` : '';
|
|
79
|
+
if (server.length) {
|
|
80
|
+
const pid = record?.owner?.pid ?? started?.pid ?? server[0].pid;
|
|
81
|
+
lines.push(`Stopped the dev server${record?.url ? ` at ${record.url}` : ''} for ${checkout} (PID ${pid})${more(server.length - 1)}.`);
|
|
82
|
+
}
|
|
83
|
+
if (holder) {
|
|
84
|
+
const own = launch.filter(item => !server.some(entry => sameBirth(item, entry)));
|
|
85
|
+
lines.push(`Stopped the launch still running for ${checkout} (PID ${holder.pid})${more(own.length - 1)}.`);
|
|
86
|
+
}
|
|
87
|
+
if (!lines.length) lines.push(`No dev server was running for ${checkout}.`);
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// The detached launch worker: node worker.mjs ROOT TOKEN ARGS_JSON REQUESTED_AT
|
|
2
|
+
// (REQUESTED_AT: when the `rer dev` that spawned it started, epoch ms).
|
|
3
|
+
// It runs in its own session, so killing the `rer dev` that started it (or that
|
|
4
|
+
// command's whole process group) leaves it running; a rerun attaches to it.
|
|
5
|
+
// rer dev --stop ends it: SIGTERM, or, for a stop that ran before this worker
|
|
6
|
+
// claimed the launch, the stop request it reads right after claiming. Either
|
|
7
|
+
// way no further step starts, the running one is signalled, and the launch
|
|
8
|
+
// reports that it was stopped instead of ready.
|
|
9
|
+
import { createWriteStream } from 'node:fs';
|
|
10
|
+
import { realpath } from 'node:fs/promises';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { processInfo } from '../../modules/job-queue/processes.mjs';
|
|
13
|
+
import { liveServers } from '../registry.mjs';
|
|
14
|
+
import { LaunchFailure } from './failure.mjs';
|
|
15
|
+
import { prepare } from './prepare.mjs';
|
|
16
|
+
import { claimLaunch, eventLog, launchDirectory, liveWorker, releaseLaunch, serverRecord, stopRequest } from './state.mjs';
|
|
17
|
+
import { startServer } from './steps.mjs';
|
|
18
|
+
|
|
19
|
+
const [root, token, argsJSON, requestedAtText] = process.argv.slice(2), args = JSON.parse(argsJSON), requestedAt = Number(requestedAtText);
|
|
20
|
+
const checkout = await realpath(root), directory = await launchDirectory(checkout), events = eventLog(directory, token);
|
|
21
|
+
// Listening before the claim: a stop may signal this worker as soon as it is the holder.
|
|
22
|
+
const children = new Set(), stopping = new AbortController();
|
|
23
|
+
const stop = () => { stopping.abort(); for (const child of children) child.kill('SIGTERM'); };
|
|
24
|
+
for (const signal of ['SIGTERM', 'SIGINT']) process.on(signal, stop);
|
|
25
|
+
process.on('SIGHUP', () => {});
|
|
26
|
+
if (!await claimLaunch(directory, token)) {
|
|
27
|
+
const holder = await liveWorker(directory);
|
|
28
|
+
await events.write({ event: 'redirect', token: holder?.token });
|
|
29
|
+
} else {
|
|
30
|
+
const logFile = createWriteStream(join(directory, 'launch.log'), { flags: 'a', mode: 0o600 });
|
|
31
|
+
const log = text => logFile.write(text + '\n');
|
|
32
|
+
try {
|
|
33
|
+
if (await stopRequest.covers(directory, requestedAt)) stop();
|
|
34
|
+
stopping.signal.throwIfAborted();
|
|
35
|
+
const prepared = await prepare({ root, args, env: process.env, report: fields => events.write({ event: 'phase', ...fields }), directory, log, children, signal: stopping.signal });
|
|
36
|
+
await events.write({ event: 'phase', phase: 'start', message: prepared.own ? 'the app is already running; checking it' : 'starting the app' });
|
|
37
|
+
// A server rer started logs to server.log; asking a running one for its
|
|
38
|
+
// ready lines goes to reuse.log, and names server.log only when rer started it.
|
|
39
|
+
const serverLog = join(directory, 'server.log');
|
|
40
|
+
let started;
|
|
41
|
+
try { started = await startServer(prepared.entry, args, { root, env: { ...process.env, ...prepared.added }, logPath: prepared.own ? join(directory, 'reuse.log') : serverLog, children, signal: stopping.signal }); }
|
|
42
|
+
finally { await prepared.release?.(); }
|
|
43
|
+
const record = (await liveServers(process.env)).find(item => item.checkout === checkout);
|
|
44
|
+
const pid = record?.owner?.pid ?? started.pid;
|
|
45
|
+
// The server's PID, and the process rer spawned for it (the same one unless
|
|
46
|
+
// the pin re-runs itself under its own Node), each with its start time.
|
|
47
|
+
const identity = async id => ({ pid: id, start: (await processInfo(id))?.start });
|
|
48
|
+
if (!prepared.own) await serverRecord.write(directory, { ...await identity(pid), spawned: await identity(started.pid) });
|
|
49
|
+
const ours = !prepared.own || (await serverRecord.read(directory))?.pid === pid;
|
|
50
|
+
// A stop that arrived while the server came up finds it registered and
|
|
51
|
+
// stops it; this launch reports the stop rather than a server about to end.
|
|
52
|
+
stopping.signal.throwIfAborted();
|
|
53
|
+
await events.write({ event: 'ready', ready: started.ready, pid, ...(ours ? { log: serverLog } : {}) });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
log(String(error?.stack ?? error));
|
|
56
|
+
const failure = stopping.signal.aborted ? new LaunchFailure(1, 'rer dev --stop stopped this launch before the app was ready.') : error;
|
|
57
|
+
await events.write({ event: 'failed', exitCode: failure.exitCode ?? 1, message: failure.exitCode ? failure.message : `The launch failed unexpectedly; see ${join(directory, 'launch.log')}.`, ...(failure.event ? { error: failure.event } : {}) });
|
|
58
|
+
} finally {
|
|
59
|
+
await releaseLaunch(directory, token);
|
|
60
|
+
logFile.end();
|
|
61
|
+
}
|
|
62
|
+
}
|
package/global/list.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// rer list [DIR] [--json]: the immediate subfolders of DIR that hold an
|
|
2
|
+
// OPERATIONS.json, newest change first, with what rer dev can do for each.
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { readdir, realpath, stat } from 'node:fs/promises';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { descriptorProfile, dispatcher } from './checkout.mjs';
|
|
8
|
+
import { liveServers } from './registry.mjs';
|
|
9
|
+
import { describeCheckout, setupNeeded } from './launch/facts.mjs';
|
|
10
|
+
import { launchPath, setupState } from './launch/state.mjs';
|
|
11
|
+
|
|
12
|
+
const run = promisify(execFile);
|
|
13
|
+
const reasons = {
|
|
14
|
+
independent: 'independent project: rer runs its declared commands; zero-setup rer dev applies to first-party seeds only',
|
|
15
|
+
unreadable: 'OPERATIONS.json cannot be read',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
async function changed(path) {
|
|
19
|
+
try { const { stdout } = await run('git', ['-C', path, 'log', '-1', '--format=%ct'], { timeout: 10000 }); if (/^\d+$/.test(stdout.trim())) return Number(stdout.trim()) * 1000; } catch {}
|
|
20
|
+
return (await stat(path)).mtimeMs;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function entry(path, running) {
|
|
24
|
+
const facts = descriptorProfile(join(path, 'OPERATIONS.json'));
|
|
25
|
+
const profile = ['first-party', 'independent'].includes(facts.profile) ? facts.profile : 'unsupported';
|
|
26
|
+
const result = { path, name: path.split('/').pop(), profile, launchable: profile === 'first-party' };
|
|
27
|
+
if (!result.launchable) result.reason = reasons[facts.profile] ?? `OPERATIONS.json is version ${facts.version ?? '(none)'}, which rer does not run`;
|
|
28
|
+
result.prepared = null;
|
|
29
|
+
if (profile === 'first-party') {
|
|
30
|
+
// Set up: saved settings, and no setup a launch started left unfinished.
|
|
31
|
+
try { result.prepared = !await setupNeeded(path, await describeCheckout(path, dispatcher(path).entry, process.env), await setupState.read(launchPath(path))); }
|
|
32
|
+
catch (failure) { result.launchable = false; result.reason = `rer describe fails here: ${failure.message.split('\n')[0]}`; }
|
|
33
|
+
}
|
|
34
|
+
result.url = running.get(path) ?? null;
|
|
35
|
+
result.changedAt = new Date(await changed(path)).toISOString();
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function listCommand(args) {
|
|
40
|
+
const json = args.includes('--json'), positional = args.filter(arg => arg !== '--json');
|
|
41
|
+
if (positional.length > 1 || positional.some(arg => arg.startsWith('-'))) { process.stderr.write('Usage: rer list [DIR] [--json]\n'); return 2; }
|
|
42
|
+
let directory;
|
|
43
|
+
try { directory = await realpath(resolve(positional[0] ?? '.')); } catch { process.stderr.write(`rer list: ${resolve(positional[0] ?? '.')} is not a folder.\n`); return 3; }
|
|
44
|
+
const running = new Map((await liveServers()).map(record => [record.checkout, record.url]));
|
|
45
|
+
const folders = [];
|
|
46
|
+
for (const item of await readdir(directory, { withFileTypes: true })) {
|
|
47
|
+
const path = join(directory, item.name);
|
|
48
|
+
try { if ((await stat(path)).isDirectory() && (await stat(join(path, 'OPERATIONS.json'))).isFile()) folders.push(await realpath(path)); } catch {}
|
|
49
|
+
}
|
|
50
|
+
const checkouts = [];
|
|
51
|
+
for (let i = 0; i < folders.length; i += 4) checkouts.push(...await Promise.all(folders.slice(i, i + 4).map(path => entry(path, running))));
|
|
52
|
+
checkouts.sort((a, b) => b.changedAt.localeCompare(a.changedAt) || a.name.localeCompare(b.name));
|
|
53
|
+
if (json) { process.stdout.write(JSON.stringify({ directory, checkouts }) + '\n'); return 0; }
|
|
54
|
+
if (!checkouts.length) { process.stdout.write(`No checkouts with an OPERATIONS.json in ${directory}.\n`); return 0; }
|
|
55
|
+
const width = Math.max(4, ...checkouts.map(item => item.name.length));
|
|
56
|
+
const rows = checkouts.map(item => `${item.name.padEnd(width)} ${item.profile.padEnd(11)} ${(item.launchable ? 'yes' : 'no').padEnd(6)} ${(item.prepared === null ? '-' : item.prepared ? 'yes' : 'no').padEnd(8)} ${item.url ?? (item.launchable ? '-' : item.reason)}`);
|
|
57
|
+
process.stdout.write([`${'NAME'.padEnd(width)} ${'PROFILE'.padEnd(11)} LAUNCH PREPARED URL`, ...rows, '', 'Launch one with: rer dev --cwd FOLDER'].join('\n') + '\n');
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
package/global/main.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { dispatcher, locate, unsupportedMessage } from './checkout.mjs';
|
|
2
|
+
import { delegate } from './delegate.mjs';
|
|
3
|
+
import { localDefaults } from './launch/local-defaults.mjs';
|
|
4
|
+
|
|
5
|
+
const help = `Usage: rer VERB [FLAGS]
|
|
6
|
+
|
|
7
|
+
Inside a project (the nearest OPERATIONS.json at or above this folder, or --cwd DIR),
|
|
8
|
+
every verb runs that checkout's own pinned rer, exactly as node scripts/rer.mjs VERB would.
|
|
9
|
+
|
|
10
|
+
rer dev Launch a first-party checkout with no setup steps: installs dependencies,
|
|
11
|
+
sets up a local owner and a free port when needed, starts the app and prints
|
|
12
|
+
RER_READY and a sign-in link. Without a terminal it returns once the app is
|
|
13
|
+
ready and leaves it running (--detach and --foreground override); allow up
|
|
14
|
+
to 10 minutes on a first launch, or rerun it to reattach. rer dev --stop stops it.
|
|
15
|
+
rer list [DIR] The checkouts in DIR (default: here), newest first, with whether rer dev can
|
|
16
|
+
launch them, whether they are set up, and their running URL. --json for data.
|
|
17
|
+
rer cli status This rer: version, contents digest, install channel, launcher and other rer
|
|
18
|
+
commands on PATH. --json for data.
|
|
19
|
+
rer cli install Install this package for the current user (~/.local/bin/rer).
|
|
20
|
+
rer cli uninstall Remove the launcher and the install root; ~/.rer is untouched.
|
|
21
|
+
rer update Move to the latest published rer (--version X for an exact one), the way it
|
|
22
|
+
was installed: npx and install.sh installs switch atomically, npm installs run npm.
|
|
23
|
+
rer --version Print this rer's version; inside a checkout, also the version the checkout pins.
|
|
24
|
+
|
|
25
|
+
Checkout verbs: install, setup, dev, preview, build, verify, reset, deploy, export, import,
|
|
26
|
+
outbox, doctor, describe, check, release. Run rer VERB --help in a checkout for its flags.
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
export async function main(argv) {
|
|
30
|
+
const [verb] = argv;
|
|
31
|
+
if (verb === '--version' || verb === '-v') return (await import('./version.mjs')).versionCommand(argv);
|
|
32
|
+
// Only the zero-setup launch sets this, for its own setup child; an explicit
|
|
33
|
+
// verb never inherits it.
|
|
34
|
+
delete process.env[localDefaults];
|
|
35
|
+
if (verb === 'update') return (await import('./update/command.mjs')).update(argv.slice(1));
|
|
36
|
+
const notice = await import('./update/notice.mjs');
|
|
37
|
+
if (notice.wantsNotice(argv)) await notice.updateNotice();
|
|
38
|
+
if (!verb || verb === '--help' || verb === '-h' || verb === 'help') { process.stdout.write(help); return 0; }
|
|
39
|
+
if (verb === 'cli') return (await import('./cli/command.mjs')).cliCommand(argv.slice(1));
|
|
40
|
+
if (verb === 'list') return (await import('./list.mjs')).listCommand(argv.slice(1));
|
|
41
|
+
const target = locate(argv);
|
|
42
|
+
if (target.profile === 'unsupported') { process.stderr.write(unsupportedMessage(target.root, target.version) + '\n'); return 2; }
|
|
43
|
+
// A checkout that pins its own rer package runs all of it, launching included.
|
|
44
|
+
const mode = target.profile === 'first-party' ? dispatcher(target.root).mode : undefined;
|
|
45
|
+
if (verb === 'dev' && mode && mode !== 'package' && !argv.includes('--help')) return (await import('./launch/dev.mjs')).launchDev(target);
|
|
46
|
+
// Every other server start or queued job on a vendored pin gets the person
|
|
47
|
+
// defaults too (a pinned package applies its own).
|
|
48
|
+
if (mode === 'vendored') {
|
|
49
|
+
const { personEnvironment, personVerbs, pinPolicy } = await import('./launch/capacity.mjs');
|
|
50
|
+
if (personVerbs.has(verb)) Object.assign(process.env, personEnvironment(process.env, target.args, await pinPolicy(target.root, mode)));
|
|
51
|
+
}
|
|
52
|
+
await delegate(target, target.args);
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Facts about the running rer package: where it lives, its version and the
|
|
2
|
+
// digest of its contents (which tells two builds of one version apart).
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
5
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
6
|
+
import { join, relative } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
export const packageName = '@runeditrun/rer';
|
|
10
|
+
export const ownRoot = realpathSync(fileURLToPath(new URL('../', import.meta.url)));
|
|
11
|
+
|
|
12
|
+
export function ownManifest() {
|
|
13
|
+
const packed = JSON.parse(readFileSync(join(ownRoot, 'package.json'), 'utf8'));
|
|
14
|
+
return packed.name === packageName ? packed : JSON.parse(readFileSync(join(ownRoot, 'global/package.json'), 'utf8'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function packageFileList(root) {
|
|
18
|
+
const files = [];
|
|
19
|
+
async function walk(directory) {
|
|
20
|
+
for (const entry of (await readdir(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
21
|
+
const path = join(directory, entry.name);
|
|
22
|
+
if (entry.isDirectory()) await walk(path);
|
|
23
|
+
else if (entry.isFile()) files.push(relative(root, path));
|
|
24
|
+
else throw Error(`Unexpected non-file entry in the rer package: ${relative(root, path)}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
await walk(root);
|
|
28
|
+
return files.sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// sha256 over every file's path and bytes, in a fixed order.
|
|
32
|
+
export async function packageDigest(root) {
|
|
33
|
+
const hash = createHash('sha256');
|
|
34
|
+
for (const path of await packageFileList(root)) {
|
|
35
|
+
hash.update(path).update('\0').update(await readFile(join(root, path))).update('\0');
|
|
36
|
+
}
|
|
37
|
+
return hash.digest('hex');
|
|
38
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Read-only view of the shared server registry that every pinned dispatcher
|
|
2
|
+
// writes (RER_SERVERS_DIR, else the account's ~/.rer/servers).
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { userInfo } from 'node:os';
|
|
5
|
+
import { join, resolve } from 'node:path';
|
|
6
|
+
import { alive } from '../modules/job-queue/identity.mjs';
|
|
7
|
+
|
|
8
|
+
export const serversDirectory = (env = process.env) => resolve(env.RER_SERVERS_DIR ?? join(userInfo().homedir, '.rer/servers'));
|
|
9
|
+
|
|
10
|
+
export async function liveServers(env = process.env) {
|
|
11
|
+
let state;
|
|
12
|
+
try { state = JSON.parse(await readFile(join(serversDirectory(env), 'state.json'), 'utf8')); }
|
|
13
|
+
catch (error) { if (error.code === 'ENOENT') return []; throw error; }
|
|
14
|
+
const live = [];
|
|
15
|
+
for (const record of state.running ?? []) {
|
|
16
|
+
// An identity that cannot be verified is kept, as the dispatchers keep it.
|
|
17
|
+
let running = true;
|
|
18
|
+
try { running = await alive(record.owner); } catch {}
|
|
19
|
+
if (running) live.push(record);
|
|
20
|
+
}
|
|
21
|
+
return live;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const stopCommand = record => `rer ${record.mode} --stop --cwd ${JSON.stringify(record.checkout)}`;
|
package/global/rer.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The installed `rer` command. This file avoids newer syntax so that an old
|
|
3
|
+
// Node still reaches the one-line minimum-version message.
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
function manifest() {
|
|
7
|
+
// Packed: the package's own package.json. From a Base checkout: the template.
|
|
8
|
+
var packed = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
9
|
+
return packed.name === '@runeditrun/rer' ? packed : JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function below(actual, minimum) {
|
|
13
|
+
var a = actual.split('.').map(Number), m = minimum.split('.').map(Number);
|
|
14
|
+
for (var i = 0; i < 3; i++) { if ((a[i] || 0) !== (m[i] || 0)) return (a[i] || 0) < (m[i] || 0); }
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
var minimum = manifest().engines.node.replace(/^>=\s*/, '');
|
|
19
|
+
if (process.platform === 'win32') {
|
|
20
|
+
process.stderr.write('rer runs on macOS and Linux. On Windows, install it inside WSL2.\n');
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
} else if (below(process.versions.node, minimum)) {
|
|
23
|
+
process.stderr.write('rer needs Node ' + minimum + ' or later; this is Node ' + process.versions.node + '. Install a newer Node, then run rer again.\n');
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
} else {
|
|
26
|
+
import('./main.mjs').then(function (module) { return module.main(process.argv.slice(2)); }).then(function (code) {
|
|
27
|
+
if (code !== undefined) process.exitCode = code;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Read an npm tarball for installing: check it is exactly the registry's bytes
|
|
2
|
+
// (sha512 integrity, and the entry count and unpacked size when the registry
|
|
3
|
+
// gives them) and that every entry is a plain file or folder under package/,
|
|
4
|
+
// then write it out. Absolute paths, .. entries, links and devices are refused,
|
|
5
|
+
// and so is anything another tar reader could read differently: a format other
|
|
6
|
+
// than ustar, a pax extended header with a field other than path, size or
|
|
7
|
+
// mtime, or data after the end of the archive. install.sh checks the same way.
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
10
|
+
import { dirname, join } from 'node:path';
|
|
11
|
+
import { gunzipSync } from 'node:zlib';
|
|
12
|
+
import { UpdateFailure } from './registry.mjs';
|
|
13
|
+
|
|
14
|
+
const text = (block, start, length) => { const end = block.subarray(start, start + length).indexOf(0); return block.toString('utf8', start, end === -1 ? start + length : start + end); };
|
|
15
|
+
|
|
16
|
+
// A pax extended header's records ("<length> <key>=<value>\n"): path and size
|
|
17
|
+
// replace the next entry's own, mtime is only a time; any other field is refused.
|
|
18
|
+
function paxFields(data) {
|
|
19
|
+
const fields = {};
|
|
20
|
+
for (let offset = 0; offset < data.length;) {
|
|
21
|
+
const space = data.indexOf(0x20, offset), digits = data.toString('latin1', offset, space), length = Number(digits);
|
|
22
|
+
if (space === -1 || !/^[1-9]\d*$/.test(digits) || offset + length <= space + 1 || offset + length > data.length || data[offset + length - 1] !== 0x0a) throw new UpdateFailure('The archive has a damaged extended header.');
|
|
23
|
+
const record = data.toString('utf8', space + 1, offset + length - 1), equals = record.indexOf('=');
|
|
24
|
+
const key = record.slice(0, equals), value = record.slice(equals + 1);
|
|
25
|
+
offset += length;
|
|
26
|
+
if (equals < 1) throw new UpdateFailure('The archive has a damaged extended header.');
|
|
27
|
+
if (key === 'linkpath') throw new UpdateFailure('The archive contains a link.');
|
|
28
|
+
if (key === 'size' && !/^\d+$/.test(value)) throw new UpdateFailure('The archive has a damaged extended header.');
|
|
29
|
+
if (key !== 'path' && key !== 'size' && key !== 'mtime') throw new UpdateFailure(`The archive has an extended header field rer does not read (${key}).`);
|
|
30
|
+
fields[key] = key === 'size' ? Number(value) : value;
|
|
31
|
+
}
|
|
32
|
+
return fields;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Every entry under package/, folders included (each counts toward the
|
|
36
|
+
// registry's dist.fileCount); a folder has empty data.
|
|
37
|
+
export function readPackage(bytes, dist) {
|
|
38
|
+
if (!/^sha512-[A-Za-z0-9+/]+=*$/.test(dist.integrity ?? '')) throw new UpdateFailure('The registry gives no sha512 integrity for this version.');
|
|
39
|
+
if (`sha512-${createHash('sha512').update(bytes).digest('base64')}` !== dist.integrity) throw new UpdateFailure('The download does not match the registry\'s sha512 integrity.');
|
|
40
|
+
let tar;
|
|
41
|
+
try { tar = gunzipSync(bytes); } catch { throw new UpdateFailure('The download is not a gzip archive.'); }
|
|
42
|
+
const entries = [];
|
|
43
|
+
let offset = 0, pax, ended = false;
|
|
44
|
+
while (offset + 512 <= tar.length) {
|
|
45
|
+
const block = tar.subarray(offset, offset + 512);
|
|
46
|
+
if (block.every(byte => byte === 0)) {
|
|
47
|
+
if (!tar.subarray(offset).every(byte => byte === 0)) throw new UpdateFailure('The archive has data after its end.');
|
|
48
|
+
ended = true;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
let sum = 0;
|
|
52
|
+
for (let i = 0; i < 512; i++) sum += i >= 148 && i < 156 ? 32 : block[i];
|
|
53
|
+
if (parseInt(text(block, 148, 8), 8) !== sum) throw new UpdateFailure('The archive has a damaged header.');
|
|
54
|
+
if (block.toString('latin1', 257, 265) !== 'ustar\u0000' + '00') throw new UpdateFailure('The archive is not a ustar archive.');
|
|
55
|
+
const sizeField = text(block, 124, 12).trim();
|
|
56
|
+
if (!/^[0-7]+$/.test(sizeField)) throw new UpdateFailure('The archive has a damaged header.');
|
|
57
|
+
const type = String.fromCharCode(block[156] || 48), size = type !== 'x' && pax?.size !== undefined ? pax.size : parseInt(sizeField, 8);
|
|
58
|
+
const data = tar.subarray(offset + 512, offset + 512 + size);
|
|
59
|
+
if (data.length < size) throw new UpdateFailure('The archive is truncated.');
|
|
60
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
61
|
+
if (type === 'x') {
|
|
62
|
+
if (pax) throw new UpdateFailure('The archive has two extended headers for one entry.');
|
|
63
|
+
pax = paxFields(data);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const name = pax?.path ?? (text(block, 345, 155) ? `${text(block, 345, 155)}/${text(block, 0, 100)}` : text(block, 0, 100));
|
|
67
|
+
pax = undefined;
|
|
68
|
+
if (type !== '0' && type !== '5') throw new UpdateFailure(`The archive contains a link, device or other special entry (${name}).`);
|
|
69
|
+
const parts = name.split('/');
|
|
70
|
+
if (name.startsWith('/') || parts.includes('..') || name.includes('\\') || parts[0] !== 'package') throw new UpdateFailure(`The archive contains an unsafe path (${name}).`);
|
|
71
|
+
if (type === '5' && size !== 0) throw new UpdateFailure(`The archive gives the folder ${name} contents.`);
|
|
72
|
+
if (type === '0' && name.endsWith('/')) throw new UpdateFailure(`The archive gives the file ${name} a folder's name.`);
|
|
73
|
+
entries.push({ path: parts.slice(1).filter(Boolean).join('/'), data, executable: (parseInt(text(block, 100, 8), 8) & 0o100) !== 0, directory: type === '5' });
|
|
74
|
+
}
|
|
75
|
+
if (!ended || pax) throw new UpdateFailure('The archive is truncated.');
|
|
76
|
+
if (dist.fileCount !== undefined && dist.fileCount !== entries.length) throw new UpdateFailure(`The archive holds ${entries.length} entries; the registry says ${dist.fileCount}.`);
|
|
77
|
+
const total = entries.reduce((sum, entry) => sum + entry.data.length, 0);
|
|
78
|
+
if (dist.unpackedSize !== undefined && dist.unpackedSize !== total) throw new UpdateFailure(`The archive unpacks to ${total} bytes; the registry says ${dist.unpackedSize}.`);
|
|
79
|
+
return entries;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function writePackage(entries, root) {
|
|
83
|
+
for (const entry of entries) {
|
|
84
|
+
if (entry.directory) { await mkdir(join(root, entry.path), { recursive: true }); continue; }
|
|
85
|
+
await mkdir(dirname(join(root, entry.path)), { recursive: true });
|
|
86
|
+
try { await writeFile(join(root, entry.path), entry.data, { mode: entry.executable ? 0o755 : 0o644, flag: 'wx' }); }
|
|
87
|
+
catch (error) { if (['EEXIST', 'EISDIR'].includes(error.code)) throw new UpdateFailure(`The archive holds ${entry.path} twice.`); throw error; }
|
|
88
|
+
}
|
|
89
|
+
}
|