@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.
Files changed (527) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +157 -2
  3. package/bin/rer.mjs +3 -0
  4. package/cli/README.md +243 -0
  5. package/cli/adapter.mjs +10 -0
  6. package/cli/ai-local.mjs +65 -0
  7. package/cli/archive-path.mjs +52 -0
  8. package/cli/archive-vite.mjs +55 -0
  9. package/cli/archive.mjs +8 -0
  10. package/cli/auth-copy.mjs +80 -0
  11. package/cli/bounded.mjs +70 -0
  12. package/cli/cache-doctor.mjs +63 -0
  13. package/cli/container-transfer.mjs +35 -0
  14. package/cli/container.mjs +62 -0
  15. package/cli/contract.mjs +25 -0
  16. package/cli/dependencies.mjs +45 -0
  17. package/cli/dependency-approval.mjs +33 -0
  18. package/cli/dependency-doctor.mjs +34 -0
  19. package/cli/dependency-input.mjs +43 -0
  20. package/cli/deploy-config.mjs +118 -0
  21. package/cli/deploy.mjs +37 -0
  22. package/cli/dev-identity.mjs +80 -0
  23. package/cli/errors.mjs +14 -0
  24. package/cli/export.mjs +42 -0
  25. package/cli/font-cache.mjs +29 -0
  26. package/cli/hosted-output.mjs +110 -0
  27. package/cli/independent.mjs +29 -0
  28. package/cli/inspect.mjs +174 -0
  29. package/cli/jsonc.mjs +111 -0
  30. package/cli/lifecycle.mjs +148 -0
  31. package/cli/local.mjs +100 -0
  32. package/cli/main.mjs +57 -0
  33. package/cli/managed-server.mjs +143 -0
  34. package/cli/node-runtime-cache.mjs +68 -0
  35. package/cli/node-runtime-discovery.mjs +56 -0
  36. package/cli/node-runtime-download.mjs +59 -0
  37. package/cli/node-runtime.mjs +50 -0
  38. package/cli/output.mjs +22 -0
  39. package/cli/parse.mjs +77 -0
  40. package/cli/port.mjs +52 -0
  41. package/cli/portability-worker.mjs +56 -0
  42. package/cli/portability.mjs +66 -0
  43. package/cli/preview-cache.mjs +99 -0
  44. package/cli/project.mjs +48 -0
  45. package/cli/release.mjs +24 -0
  46. package/cli/runtime-front-door.mjs +46 -0
  47. package/cli/scheduled.mjs +81 -0
  48. package/cli/server-activity.mjs +12 -0
  49. package/cli/server-proxy.mjs +93 -0
  50. package/cli/server-registry.mjs +182 -0
  51. package/cli/server-resources.mjs +22 -0
  52. package/cli/settings.mjs +15 -0
  53. package/cli/setup-answers.mjs +29 -0
  54. package/cli/setup-input.mjs +113 -0
  55. package/cli/shared.mjs +7 -0
  56. package/cli/transfers.mjs +33 -0
  57. package/cli/version.mjs +36 -0
  58. package/composition/git-source.mjs +111 -0
  59. package/edition-authoring/files.mjs +81 -0
  60. package/edition-authoring/manifest.mjs +65 -0
  61. package/global/README.md +158 -0
  62. package/global/checkout.mjs +72 -0
  63. package/global/cli/command.mjs +13 -0
  64. package/global/cli/install.mjs +75 -0
  65. package/global/cli/layout.mjs +45 -0
  66. package/global/cli/status.mjs +54 -0
  67. package/global/cli/uninstall.mjs +36 -0
  68. package/global/delegate.mjs +13 -0
  69. package/global/launch/capacity.mjs +72 -0
  70. package/global/launch/dev.mjs +117 -0
  71. package/global/launch/facts.mjs +94 -0
  72. package/global/launch/failure.mjs +5 -0
  73. package/global/launch/local-defaults.mjs +4 -0
  74. package/global/launch/prepare.mjs +63 -0
  75. package/global/launch/render.mjs +44 -0
  76. package/global/launch/state.mjs +106 -0
  77. package/global/launch/steps.mjs +90 -0
  78. package/global/launch/stop.mjs +89 -0
  79. package/global/launch/worker.mjs +62 -0
  80. package/global/list.mjs +59 -0
  81. package/global/main.mjs +53 -0
  82. package/global/package-info.mjs +38 -0
  83. package/global/registry.mjs +24 -0
  84. package/global/rer.mjs +29 -0
  85. package/global/update/archive.mjs +89 -0
  86. package/global/update/command.mjs +90 -0
  87. package/global/update/latest.mjs +6 -0
  88. package/global/update/notice.mjs +58 -0
  89. package/global/update/registry.mjs +47 -0
  90. package/global/version.mjs +29 -0
  91. package/install.sh +144 -0
  92. package/modules/ai/LICENSE +21 -0
  93. package/modules/ai/README.md +90 -0
  94. package/modules/ai/client.mjs +71 -0
  95. package/modules/ai/config.mjs +39 -0
  96. package/modules/ai/dev/index.d.mts +6 -0
  97. package/modules/ai/dev/index.mjs +64 -0
  98. package/modules/ai/dev/outbox.mjs +33 -0
  99. package/modules/ai/errors.mjs +28 -0
  100. package/modules/ai/fake/index.d.mts +3 -0
  101. package/modules/ai/fake/index.mjs +82 -0
  102. package/modules/ai/index.d.mts +10 -0
  103. package/modules/ai/index.mjs +3 -0
  104. package/modules/ai/package.json +6 -0
  105. package/modules/container/README.md +156 -0
  106. package/modules/container/adapters/d1.mjs +34 -0
  107. package/modules/container/adapters/egress.mjs +40 -0
  108. package/modules/container/adapters/jobs.mjs +47 -0
  109. package/modules/container/adapters/objects.mjs +84 -0
  110. package/modules/container/adapters/retained.mjs +14 -0
  111. package/modules/container/adapters/sqlite-error.mjs +7 -0
  112. package/modules/container/adapters/sqlite-worker.mjs +26 -0
  113. package/modules/container/assets.mjs +14 -0
  114. package/modules/container/bindings.mjs +1 -0
  115. package/modules/container/boundary.mjs +38 -0
  116. package/modules/container/build.mjs +65 -0
  117. package/modules/container/deploy-input.mjs +17 -0
  118. package/modules/container/entrypoint.sh +5 -0
  119. package/modules/container/host.mjs +43 -0
  120. package/modules/container/ingress.mjs +23 -0
  121. package/modules/container/lock.mjs +11 -0
  122. package/modules/container/migrations.mjs +16 -0
  123. package/modules/container/node-engine.mjs +39 -0
  124. package/modules/container/portability-schema.mjs +14 -0
  125. package/modules/container/remote/deploy.py +76 -0
  126. package/modules/container/remote/files.py +275 -0
  127. package/modules/container/remote/resources.py +157 -0
  128. package/modules/container/runner.mjs +10 -0
  129. package/modules/container/ssh-plan.mjs +44 -0
  130. package/modules/container/ssh.mjs +63 -0
  131. package/modules/container/transfer.mjs +27 -0
  132. package/modules/container/worker-engine.mjs +32 -0
  133. package/modules/dependencies/README.md +211 -0
  134. package/modules/dependencies/approval.mjs +51 -0
  135. package/modules/dependencies/auth.mjs +40 -0
  136. package/modules/dependencies/authority.mjs +14 -0
  137. package/modules/dependencies/crypto.mjs +13 -0
  138. package/modules/dependencies/index.d.mts +23 -0
  139. package/modules/dependencies/index.mjs +113 -0
  140. package/modules/dependencies/logout.mjs +42 -0
  141. package/modules/dependencies/native.mjs +9 -0
  142. package/modules/dependencies/protocol.mjs +23 -0
  143. package/modules/dependencies/schema/approval-attempt.sql +3 -0
  144. package/modules/dependencies/schema/dependencies.sql +15 -0
  145. package/modules/dependencies/store.mjs +19 -0
  146. package/modules/deploy-kit/README.md +308 -0
  147. package/modules/deploy-kit/artifact.mjs +47 -0
  148. package/modules/deploy-kit/build-modules.mjs +21 -0
  149. package/modules/deploy-kit/check.mjs +13 -0
  150. package/modules/deploy-kit/cli.mjs +50 -0
  151. package/modules/deploy-kit/container-config.mjs +8 -0
  152. package/modules/deploy-kit/default-stage.mjs +14 -0
  153. package/modules/deploy-kit/deploy.mjs +80 -0
  154. package/modules/deploy-kit/health-request.mjs +86 -0
  155. package/modules/deploy-kit/health.mjs +99 -0
  156. package/modules/deploy-kit/index.mjs +12 -0
  157. package/modules/deploy-kit/local-only.mjs +10 -0
  158. package/modules/deploy-kit/migrations.mjs +48 -0
  159. package/modules/deploy-kit/owner-cli.mjs +63 -0
  160. package/modules/deploy-kit/owner.mjs +297 -0
  161. package/modules/deploy-kit/prepare.mjs +62 -0
  162. package/modules/deploy-kit/provider.mjs +99 -0
  163. package/modules/deploy-kit/queue-consumer.mjs +10 -0
  164. package/modules/deploy-kit/receipt.mjs +16 -0
  165. package/modules/deploy-kit/render.mjs +47 -0
  166. package/modules/deploy-kit/resources.mjs +106 -0
  167. package/modules/deploy-kit/service-grants.mjs +25 -0
  168. package/modules/deploy-kit/sql-safety.mjs +94 -0
  169. package/modules/deploy-kit/sql-tokens.mjs +49 -0
  170. package/modules/deploy-kit/teardown.mjs +100 -0
  171. package/modules/deploy-kit/templates.mjs +31 -0
  172. package/modules/deploy-kit/validation.mjs +58 -0
  173. package/modules/deploy-kit/worker.mjs +105 -0
  174. package/modules/erasure/CONTRACT.md +90 -0
  175. package/modules/erasure/README.md +221 -0
  176. package/modules/erasure/audit.mjs +23 -0
  177. package/modules/erasure/blobs.mjs +45 -0
  178. package/modules/erasure/cascade.mjs +91 -0
  179. package/modules/erasure/completion.mjs +62 -0
  180. package/modules/erasure/declaration.mjs +51 -0
  181. package/modules/erasure/fence.mjs +19 -0
  182. package/modules/erasure/index.d.mts +62 -0
  183. package/modules/erasure/index.mjs +85 -0
  184. package/modules/erasure/order.mjs +56 -0
  185. package/modules/erasure/plan.mjs +121 -0
  186. package/modules/erasure/rate.mjs +57 -0
  187. package/modules/erasure/receipt.mjs +32 -0
  188. package/modules/erasure/root.mjs +21 -0
  189. package/modules/erasure/schema/erasure-rates.sql +5 -0
  190. package/modules/erasure/schema/erasure.sql +27 -0
  191. package/modules/erasure/scope.mjs +29 -0
  192. package/modules/erasure/sql.mjs +23 -0
  193. package/modules/erasure/verification.mjs +69 -0
  194. package/modules/integrations/core/CONTRACT.md +50 -0
  195. package/modules/integrations/core/README.md +27 -0
  196. package/modules/integrations/core/crypto.ts +19 -0
  197. package/modules/integrations/core/database.ts +21 -0
  198. package/modules/integrations/core/delivery.ts +97 -0
  199. package/modules/integrations/core/destination.ts +13 -0
  200. package/modules/integrations/core/index.ts +7 -0
  201. package/modules/integrations/core/node-transport.ts +72 -0
  202. package/modules/integrations/core/public-address.ts +57 -0
  203. package/modules/integrations/core/repository.ts +74 -0
  204. package/modules/integrations/core/schema.sql +36 -0
  205. package/modules/integrations/core/sqlite.ts +20 -0
  206. package/modules/integrations/core/timestamped-body-signature.ts +38 -0
  207. package/modules/integrations/core/tokens.ts +71 -0
  208. package/modules/integrations/core/types.ts +52 -0
  209. package/modules/integrations/core/upgrade-token-usage-context.sql +5 -0
  210. package/modules/integrations/core/webhook.ts +73 -0
  211. package/modules/job-queue/README.md +173 -0
  212. package/modules/job-queue/cli.mjs +64 -0
  213. package/modules/job-queue/command.mjs +64 -0
  214. package/modules/job-queue/config.mjs +48 -0
  215. package/modules/job-queue/identity.mjs +48 -0
  216. package/modules/job-queue/index.mjs +135 -0
  217. package/modules/job-queue/membership.mjs +30 -0
  218. package/modules/job-queue/module.json +10 -0
  219. package/modules/job-queue/policy.mjs +29 -0
  220. package/modules/job-queue/processes.mjs +80 -0
  221. package/modules/job-queue/resources.mjs +37 -0
  222. package/modules/job-queue/storage.mjs +64 -0
  223. package/modules/job-queue/supervisor.mjs +30 -0
  224. package/modules/job-queue/waiting.mjs +16 -0
  225. package/modules/mail/LICENSE +21 -0
  226. package/modules/mail/README.md +336 -0
  227. package/modules/mail/doctor.mjs +81 -0
  228. package/modules/mail/index.mjs +8 -0
  229. package/modules/mail/live-proof/README.md +113 -0
  230. package/modules/mail/live-proof/run.mjs +55 -0
  231. package/modules/mail/package.json +19 -0
  232. package/modules/mail/pnpm-lock.yaml +537 -0
  233. package/modules/mail/src/adapters/cloudflare/README.md +84 -0
  234. package/modules/mail/src/adapters/cloudflare/headers.mjs +63 -0
  235. package/modules/mail/src/adapters/cloudflare/index.mjs +47 -0
  236. package/modules/mail/src/adapters/cloudflare/response.mjs +99 -0
  237. package/modules/mail/src/adapters/messaging/README.md +92 -0
  238. package/modules/mail/src/adapters/messaging/contract.mjs +66 -0
  239. package/modules/mail/src/adapters/messaging/event-schema.mjs +28 -0
  240. package/modules/mail/src/adapters/messaging/index.mjs +61 -0
  241. package/modules/mail/src/adapters/messaging/jws.mjs +68 -0
  242. package/modules/mail/src/adapters/messaging/request.mjs +52 -0
  243. package/modules/mail/src/adapters/messaging/serialize.mjs +21 -0
  244. package/modules/mail/src/adapters/resend/README.md +120 -0
  245. package/modules/mail/src/adapters/resend/index.mjs +22 -0
  246. package/modules/mail/src/adapters/resend/response.mjs +54 -0
  247. package/modules/mail/src/adapters/resend/serialize.mjs +46 -0
  248. package/modules/mail/src/adapters/resend/size.mjs +99 -0
  249. package/modules/mail/src/adapters/sendgrid/README.md +75 -0
  250. package/modules/mail/src/adapters/sendgrid/index.mjs +35 -0
  251. package/modules/mail/src/adapters/sendgrid/serialize.mjs +104 -0
  252. package/modules/mail/src/core/errors.mjs +9 -0
  253. package/modules/mail/src/core/transport.mjs +33 -0
  254. package/modules/mail/src/core/validation.mjs +172 -0
  255. package/modules/mail/src/development/index.d.mts +37 -0
  256. package/modules/mail/src/development/index.mjs +35 -0
  257. package/modules/mail/src/development/outbox.mjs +78 -0
  258. package/modules/mail/src/development/schema.sql +9 -0
  259. package/modules/mail/src/development/viewer-html.mjs +42 -0
  260. package/modules/mail/src/development/viewer.mjs +65 -0
  261. package/modules/mail/src/doctor/README.md +107 -0
  262. package/modules/mail/src/doctor/common.mjs +64 -0
  263. package/modules/mail/src/doctor/dns.mjs +102 -0
  264. package/modules/mail/src/doctor/index.mjs +28 -0
  265. package/modules/mail/src/doctor/probe.mjs +66 -0
  266. package/modules/mail/src/doctor/receive.mjs +81 -0
  267. package/modules/mail/src/doctor/records.mjs +45 -0
  268. package/modules/mail/src/events/README.md +268 -0
  269. package/modules/mail/src/events/d1-schema.sql +25 -0
  270. package/modules/mail/src/events/d1-storage.mjs +76 -0
  271. package/modules/mail/src/events/index.mjs +3 -0
  272. package/modules/mail/src/events/intake.mjs +201 -0
  273. package/modules/mail/src/events/storage.mjs +52 -0
  274. package/modules/mail/src/events/verify.mjs +92 -0
  275. package/modules/mail/src/inbound/README.md +137 -0
  276. package/modules/mail/src/inbound/bytes.mjs +91 -0
  277. package/modules/mail/src/inbound/headers.mjs +143 -0
  278. package/modules/mail/src/inbound/index.mjs +29 -0
  279. package/modules/mail/src/inbound/mime.mjs +36 -0
  280. package/modules/mail/src/inbound/multipart.mjs +27 -0
  281. package/modules/mail/src/inbound/resend-attachment.mjs +43 -0
  282. package/modules/mail/src/inbound/resend.mjs +54 -0
  283. package/modules/mail/src/inbound/sendgrid.mjs +60 -0
  284. package/modules/mail/src/sender.mjs +33 -0
  285. package/modules/mail/src/sendgrid-email/index.d.mts +33 -0
  286. package/modules/mail/src/sendgrid-email/index.mjs +86 -0
  287. package/modules/mail/src/unsubscribe/index.mjs +142 -0
  288. package/modules/operator-auth/README.md +599 -0
  289. package/modules/operator-auth/accounts/adapter.mjs +41 -0
  290. package/modules/operator-auth/accounts/admin.mjs +159 -0
  291. package/modules/operator-auth/accounts/credentials.mjs +27 -0
  292. package/modules/operator-auth/accounts/editor-owner.mjs +13 -0
  293. package/modules/operator-auth/accounts/index.d.mts +9 -0
  294. package/modules/operator-auth/accounts/index.mjs +9 -0
  295. package/modules/operator-auth/accounts/native.d.mts +38 -0
  296. package/modules/operator-auth/accounts/native.mjs +171 -0
  297. package/modules/operator-auth/accounts/oa.mjs +90 -0
  298. package/modules/operator-auth/accounts/provider-control.mjs +50 -0
  299. package/modules/operator-auth/accounts/provider-pin.mjs +14 -0
  300. package/modules/operator-auth/accounts/roles.mjs +26 -0
  301. package/modules/operator-auth/accounts/schema/dependency-setup.sql +17 -0
  302. package/modules/operator-auth/accounts/schema/dev-sessions.sql +22 -0
  303. package/modules/operator-auth/accounts/schema/domain-rules-step-up.sql +20 -0
  304. package/modules/operator-auth/accounts/schema/invitation-cancellation.sql +17 -0
  305. package/modules/operator-auth/accounts/schema/operator-auth.sql +142 -0
  306. package/modules/operator-auth/accounts/schema/provider-control.sql +30 -0
  307. package/modules/operator-auth/accounts/schema/provider-pins.sql +6 -0
  308. package/modules/operator-auth/accounts/settings-page.mjs +4 -0
  309. package/modules/operator-auth/accounts/store.mjs +246 -0
  310. package/modules/operator-auth/branding.mjs +66 -0
  311. package/modules/operator-auth/config.mjs +35 -0
  312. package/modules/operator-auth/conformance/README.md +81 -0
  313. package/modules/operator-auth/conformance/index.d.mts +43 -0
  314. package/modules/operator-auth/conformance/index.mjs +300 -0
  315. package/modules/operator-auth/conformance/presentation.mjs +57 -0
  316. package/modules/operator-auth/crypto.mjs +17 -0
  317. package/modules/operator-auth/dev/badge.mjs +62 -0
  318. package/modules/operator-auth/dev/index.d.mts +14 -0
  319. package/modules/operator-auth/dev/index.mjs +137 -0
  320. package/modules/operator-auth/dev/personas.mjs +17 -0
  321. package/modules/operator-auth/dev/sign-in-as.d.mts +3 -0
  322. package/modules/operator-auth/dev/sign-in-as.mjs +25 -0
  323. package/modules/operator-auth/dev-admission.d.mts +4 -0
  324. package/modules/operator-auth/dev-admission.mjs +10 -0
  325. package/modules/operator-auth/dev-gate.mjs +86 -0
  326. package/modules/operator-auth/docs/README.md +78 -0
  327. package/modules/operator-auth/docs/native-accounts.md +313 -0
  328. package/modules/operator-auth/docs/tanstack-start.md +218 -0
  329. package/modules/operator-auth/docs/vite-spa.md +144 -0
  330. package/modules/operator-auth/errors.mjs +9 -0
  331. package/modules/operator-auth/http.mjs +42 -0
  332. package/modules/operator-auth/identity/admission.mjs +105 -0
  333. package/modules/operator-auth/identity/after-confirm.mjs +18 -0
  334. package/modules/operator-auth/identity/email-link/confirmation.mjs +14 -0
  335. package/modules/operator-auth/identity/email-link/index.d.mts +48 -0
  336. package/modules/operator-auth/identity/email-link/index.mjs +128 -0
  337. package/modules/operator-auth/identity/email-link/message.mjs +16 -0
  338. package/modules/operator-auth/identity/index.d.mts +7 -0
  339. package/modules/operator-auth/identity/index.mjs +5 -0
  340. package/modules/operator-auth/identity/oidc/README.md +171 -0
  341. package/modules/operator-auth/identity/oidc/client-auth.mjs +16 -0
  342. package/modules/operator-auth/identity/oidc/config.mjs +102 -0
  343. package/modules/operator-auth/identity/oidc/crypto.mjs +29 -0
  344. package/modules/operator-auth/identity/oidc/errors.mjs +5 -0
  345. package/modules/operator-auth/identity/oidc/index.d.mts +33 -0
  346. package/modules/operator-auth/identity/oidc/index.mjs +287 -0
  347. package/modules/operator-auth/identity/oidc/jwt.mjs +102 -0
  348. package/modules/operator-auth/identity/oidc/network.mjs +93 -0
  349. package/modules/operator-auth/identity/oidc/presets.mjs +26 -0
  350. package/modules/operator-auth/identity/oidc/schema/oidc.sql +50 -0
  351. package/modules/operator-auth/identity/oidc/store.mjs +68 -0
  352. package/modules/operator-auth/identity/principal.mjs +28 -0
  353. package/modules/operator-auth/index.d.mts +223 -0
  354. package/modules/operator-auth/index.mjs +11 -0
  355. package/modules/operator-auth/input.mjs +55 -0
  356. package/modules/operator-auth/local-cli.mjs +16 -0
  357. package/modules/operator-auth/page.mjs +34 -0
  358. package/modules/operator-auth/policy.mjs +20 -0
  359. package/modules/operator-auth/routes.mjs +91 -0
  360. package/modules/operator-auth/service.mjs +152 -0
  361. package/modules/operator-auth/signin/index.mjs +67 -0
  362. package/modules/owner-claim/README.md +137 -0
  363. package/modules/owner-claim/idp.mjs +40 -0
  364. package/modules/owner-claim/index.d.mts +27 -0
  365. package/modules/owner-claim/index.mjs +39 -0
  366. package/modules/owner-claim/page.mjs +11 -0
  367. package/modules/owner-claim/schema/idp.sql +4 -0
  368. package/modules/owner-claim/schema/owner-claim.sql +18 -0
  369. package/modules/owner-claim/transaction.mjs +87 -0
  370. package/modules/owner-claim/verify.mjs +80 -0
  371. package/modules/portability/README.md +444 -0
  372. package/modules/portability/bridge.mjs +102 -0
  373. package/modules/portability/catalogue.mjs +118 -0
  374. package/modules/portability/cli-options.mjs +38 -0
  375. package/modules/portability/cli.mjs +56 -0
  376. package/modules/portability/copies.mjs +68 -0
  377. package/modules/portability/dev-data.mjs +32 -0
  378. package/modules/portability/diff.mjs +33 -0
  379. package/modules/portability/export.mjs +129 -0
  380. package/modules/portability/files.mjs +65 -0
  381. package/modules/portability/format.mjs +122 -0
  382. package/modules/portability/generated.mjs +60 -0
  383. package/modules/portability/hash.mjs +79 -0
  384. package/modules/portability/http.mjs +42 -0
  385. package/modules/portability/import.mjs +185 -0
  386. package/modules/portability/index.d.mts +58 -0
  387. package/modules/portability/index.mjs +22 -0
  388. package/modules/portability/journal.mjs +49 -0
  389. package/modules/portability/legacy-files.mjs +43 -0
  390. package/modules/portability/legacy-safety.mjs +27 -0
  391. package/modules/portability/lines.mjs +36 -0
  392. package/modules/portability/migration-history.mjs +18 -0
  393. package/modules/portability/migration-statements.mjs +23 -0
  394. package/modules/portability/payloads.mjs +52 -0
  395. package/modules/portability/provider-data.mjs +25 -0
  396. package/modules/portability/restore-safety.mjs +30 -0
  397. package/modules/portability/schema/portability.sql +25 -0
  398. package/modules/portability/storage.mjs +49 -0
  399. package/modules/release/README.md +212 -0
  400. package/modules/release/catalogue.mjs +79 -0
  401. package/modules/release/export.mjs +51 -0
  402. package/modules/release/produce.mjs +136 -0
  403. package/modules/release/public-child-transport-state.cjs +29 -0
  404. package/modules/release/public-child.mjs +71 -0
  405. package/modules/release/public-git.mjs +21 -0
  406. package/modules/release/publication-scan.mjs +22 -0
  407. package/modules/release/publish-handoff.mjs +37 -0
  408. package/modules/release/source.mjs +71 -0
  409. package/modules/release/test-summary.mjs +50 -0
  410. package/onboarding/README.md +163 -0
  411. package/onboarding/activity.mjs +31 -0
  412. package/onboarding/adapter-auth.mjs +88 -0
  413. package/onboarding/adapter-hosting.mjs +35 -0
  414. package/onboarding/adapter-labels.mjs +13 -0
  415. package/onboarding/adapter-settings.mjs +129 -0
  416. package/onboarding/adapter.mjs +238 -0
  417. package/onboarding/agent-handoff.mjs +9 -0
  418. package/onboarding/app-processes.mjs +21 -0
  419. package/onboarding/archive-output.d.mts +3 -0
  420. package/onboarding/archive-output.mjs +54 -0
  421. package/onboarding/command-custody.mjs +24 -0
  422. package/onboarding/command-recovery.mjs +8 -0
  423. package/onboarding/commands.mjs +166 -0
  424. package/onboarding/dependency-approval.mjs +36 -0
  425. package/onboarding/dev-identity.mjs +23 -0
  426. package/onboarding/email.mjs +24 -0
  427. package/onboarding/failures.mjs +42 -0
  428. package/onboarding/input-limits.mjs +7 -0
  429. package/onboarding/installer-inputs.mjs +24 -0
  430. package/onboarding/launcher.mjs +34 -0
  431. package/onboarding/local/build-files.mjs +64 -0
  432. package/onboarding/local/build.mjs +49 -0
  433. package/onboarding/local/environment.mjs +30 -0
  434. package/onboarding/local/files.mjs +35 -0
  435. package/onboarding/local/operation.mjs +166 -0
  436. package/onboarding/local/owner.mjs +34 -0
  437. package/onboarding/local/process-table.mjs +114 -0
  438. package/onboarding/local/recovery.mjs +79 -0
  439. package/onboarding/local/state.mjs +31 -0
  440. package/onboarding/metadata.mjs +7 -0
  441. package/onboarding/operator-install.mjs +45 -0
  442. package/onboarding/operator-installer.mjs +39 -0
  443. package/onboarding/operator-prerequisites.mjs +27 -0
  444. package/onboarding/package.json +6 -0
  445. package/onboarding/prefill.mjs +16 -0
  446. package/onboarding/private-files.mjs +97 -0
  447. package/onboarding/private.mjs +35 -0
  448. package/onboarding/process-cleanup.mjs +134 -0
  449. package/onboarding/process-diagnostics.mjs +91 -0
  450. package/onboarding/process-scope.mjs +118 -0
  451. package/onboarding/process-table.mjs +118 -0
  452. package/onboarding/process-tree.mjs +36 -0
  453. package/onboarding/production-modules.mjs +22 -0
  454. package/onboarding/production-options.mjs +40 -0
  455. package/onboarding/production-plan.mjs +8 -0
  456. package/onboarding/production-runtime.mjs +30 -0
  457. package/onboarding/project-vite.mjs +14 -0
  458. package/onboarding/providers.mjs +289 -0
  459. package/onboarding/queue.mjs +53 -0
  460. package/onboarding/queued-entry.mjs +34 -0
  461. package/onboarding/readiness.mjs +59 -0
  462. package/onboarding/redaction-jwt.mjs +32 -0
  463. package/onboarding/redaction.mjs +298 -0
  464. package/onboarding/schema/SETUP.schema.json +514 -0
  465. package/onboarding/schema/dependencies.mjs +35 -0
  466. package/onboarding/schema/json-schema.mjs +202 -0
  467. package/onboarding/schema/setup-validation.mjs +325 -0
  468. package/onboarding/secrets.mjs +21 -0
  469. package/onboarding/server.mjs +188 -0
  470. package/onboarding/setup-manifest.mjs +116 -0
  471. package/onboarding/state.mjs +46 -0
  472. package/onboarding/theme-files.mjs +37 -0
  473. package/onboarding/theme.mjs +118 -0
  474. package/onboarding/ui/activity.js +98 -0
  475. package/onboarding/ui/address-client.mjs +33 -0
  476. package/onboarding/ui/app.js +377 -0
  477. package/onboarding/ui/index.html +1 -0
  478. package/onboarding/ui/model-core.mjs +127 -0
  479. package/onboarding/ui/model.mjs +5 -0
  480. package/onboarding/ui/style.css +63 -0
  481. package/onboarding/ui/theme.css +37 -0
  482. package/onboarding/ui/workspace-art.js +30 -0
  483. package/onboarding/wrangler-auth.mjs +43 -0
  484. package/operations/README.md +246 -0
  485. package/operations/operations.mjs +168 -0
  486. package/operations/project-operations.mjs +157 -0
  487. package/package.json +11 -4
  488. package/scripts/publish-check/README.md +72 -0
  489. package/scripts/publish-check/checksum_maps.py +52 -0
  490. package/scripts/publish-check/cli.py +34 -0
  491. package/scripts/publish-check/command_arguments.py +51 -0
  492. package/scripts/publish-check/command_shell_quotes.py +41 -0
  493. package/scripts/publish-check/command_source_quotes.py +120 -0
  494. package/scripts/publish-check/content_identities.py +250 -0
  495. package/scripts/publish-check/copy_identities.py +41 -0
  496. package/scripts/publish-check/der.py +61 -0
  497. package/scripts/publish-check/destination.py +117 -0
  498. package/scripts/publish-check/env_values.py +74 -0
  499. package/scripts/publish-check/font_licence.py +23 -0
  500. package/scripts/publish-check/git_history.py +115 -0
  501. package/scripts/publish-check/github_refs.py +217 -0
  502. package/scripts/publish-check/gitleaks.py +72 -0
  503. package/scripts/publish-check/gitleaks.toml +40 -0
  504. package/scripts/publish-check/history_scan.py +176 -0
  505. package/scripts/publish-check/hit_location.py +18 -0
  506. package/scripts/publish-check/hygiene_config.py +42 -0
  507. package/scripts/publish-check/hygiene_text.py +11 -0
  508. package/scripts/publish-check/identity-recipe.mjs +54 -0
  509. package/scripts/publish-check/json_identity.py +79 -0
  510. package/scripts/publish-check/license-exceptions.json +11 -0
  511. package/scripts/publish-check/licenses.py +49 -0
  512. package/scripts/publish-check/policy.py +18 -0
  513. package/scripts/publish-check/project_identity.py +89 -0
  514. package/scripts/publish-check/provenance.py +91 -0
  515. package/scripts/publish-check/provenance_pins.py +33 -0
  516. package/scripts/publish-check/public_base.py +63 -0
  517. package/scripts/publish-check/public_hygiene.py +80 -0
  518. package/scripts/publish-check/publication-config.json +12 -0
  519. package/scripts/publish-check/publication_config.py +34 -0
  520. package/scripts/publish-check/report.py +52 -0
  521. package/scripts/publish-check/repository_commands.py +75 -0
  522. package/scripts/publish-check/repository_intent.py +52 -0
  523. package/scripts/publish-check/reviewed.py +41 -0
  524. package/scripts/publish-check/source_hashes.mjs +6 -0
  525. package/scripts/publish-check/tree_scan.py +88 -0
  526. package/scripts/publish-check/trust_bundle.py +87 -0
  527. package/scripts/publish-check.mjs +58 -0
@@ -0,0 +1,79 @@
1
+ import { readdir, rmdir, unlink } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+ import { inspect } from './files.mjs'
4
+ import { buildDestination, inspectBuildFile, restoreBuildFile } from './build-files.mjs'
5
+ import { localOperation } from './operation.mjs'
6
+
7
+ // Legacy setup/build names and encoded build inventory names retain private
8
+ // originals across a hard kill. The next local operation recovers them first.
9
+ const hiddenOriginals = [['local-build', 'original.dev.vars'], ['local-setup', 'previous.dev.vars']]
10
+
11
+ export async function strandedOriginals(root) {
12
+ const found = []
13
+ // Never follow a symlinked output directory; setup refuses those itself.
14
+ const realDirectory = async (path) => { const status = await inspect(path); return status?.isDirectory() && !status.isSymbolicLink() }
15
+ if (!await realDirectory(join(root, '.wrangler'))) return found
16
+ for (const [folder, name] of hiddenOriginals) {
17
+ const directory = join(root, '.wrangler', folder)
18
+ if (!await realDirectory(directory)) continue
19
+ let runs
20
+ try { runs = await readdir(directory, { withFileTypes: true }) } catch (error) {
21
+ if (error.code === 'ENOENT') continue
22
+ throw error
23
+ }
24
+ for (const run of runs) {
25
+ if (!run.isDirectory() || !run.name.startsWith('run-')) continue
26
+ const path = join(directory, run.name, name)
27
+ const status = await inspect(path)
28
+ if (status?.isFile()) found.push({ path, status, folder, file: '.dev.vars' })
29
+ if (folder === 'local-build') {
30
+ for (const entry of await readdir(join(directory, run.name), { withFileTypes: true })) {
31
+ const file = buildDestination(entry.name)
32
+ if (!file) continue
33
+ const hidden = join(directory, run.name, entry.name)
34
+ const status = await inspect(hidden)
35
+ if (!status?.isFile() || status.isSymbolicLink()) throw new Error('Unsafe private build recovery file.')
36
+ found.push({ path: hidden, status, folder, file })
37
+ }
38
+ }
39
+ }
40
+ }
41
+ return found
42
+ }
43
+
44
+ // Restores stranded private originals before anything reads or replaces them.
45
+ // Recovery runs under the operation lock, so a live build's hidden file is
46
+ // never taken: that build owns the lock and the caller is told to wait. Returns
47
+ // the operation the caller now holds (the one passed in, or a new one).
48
+ export async function recoverInterrupted(root, operation) {
49
+ if ((await strandedOriginals(root)).length === 0) return operation
50
+ const held = operation ?? await localOperation(root)
51
+ try { await restore(root) } catch (error) {
52
+ if (!operation) await held.release()
53
+ throw error
54
+ }
55
+ return held
56
+ }
57
+
58
+ async function restore(root) {
59
+ const stranded = await strandedOriginals(root)
60
+ for (const file of new Set(stranded.map(item => item.file))) {
61
+ const items = stranded.filter(item => item.file === file)
62
+ const current = (await inspectBuildFile(root, file, { publication: true })).status
63
+ const pending = []
64
+ for (const item of items) {
65
+ if (current && item.status.ino === current.ino && item.status.dev === current.dev) await unlink(item.path)
66
+ else pending.push(item)
67
+ }
68
+ if (current) {
69
+ for (const item of pending) console.error(`An interrupted build or setup left an earlier private copy of ${file} at ${item.path}. The current file was kept; delete that copy once you no longer need it.`)
70
+ continue
71
+ }
72
+ if (pending.length > 1) throw new Error(`${file} is missing and interrupted runs left several private originals:\n${pending.map(item => ` ${item.path}`).join('\n')}\nMove the one to keep to ${file}, then run again.`)
73
+ if (!pending.length) continue
74
+ const [item] = pending
75
+ await restoreBuildFile(root, item)
76
+ if (item.folder === 'local-build') await rmdir(dirname(item.path)).catch(() => {})
77
+ console.error(`Restored ${file} from an interrupted build or setup (${item.path}).`)
78
+ }
79
+ }
@@ -0,0 +1,31 @@
1
+ import { lstat, realpath } from 'node:fs/promises';
2
+ import { isAbsolute, relative, resolve, sep } from 'node:path';
3
+ export class LocalStateSafetyError extends Error {}
4
+ async function physicalPath(path) {
5
+ const absolute = resolve(path), parent = resolve(absolute, '..');
6
+ if (parent === absolute) return realpath(absolute);
7
+ const physicalParent = await physicalPath(parent), candidate = resolve(physicalParent, relative(parent, absolute));
8
+ let status; try { status = await lstat(candidate); } catch (error) { if (error.code === 'ENOENT') return candidate; throw error; }
9
+ if (status.isSymbolicLink()) { try { return await realpath(candidate); } catch { throw new LocalStateSafetyError('Local state safety: unresolved symlink. Preserve it and resolve placement before retrying.'); } }
10
+ return candidate;
11
+ }
12
+ function contains(parent, path) { const suffix = relative(parent, path); return suffix === '' || (!isAbsolute(suffix) && suffix !== '..' && !suffix.startsWith('..' + sep)); }
13
+ export async function selectLocalState(root, persistTo = process.env.RER_LOCAL_STATE_PATH ?? '.wrangler/state') {
14
+ if (typeof persistTo !== 'string' || !persistTo.trim()) throw new LocalStateSafetyError('Local state safety: choose an explicit nonempty persistence directory.');
15
+ const checkout = await realpath(root), lexical = resolve(checkout, persistTo), state = await physicalPath(lexical);
16
+ for (const name of ['dist', '.output', '.tanstack', 'node_modules', '.wrangler/deploy']) {
17
+ const output = resolve(checkout, name), physical = await physicalPath(output);
18
+ for (const [a, b] of [[lexical, output], [state, physical]]) if (contains(a, b) || contains(b, a)) throw new LocalStateSafetyError(`Local state safety: persistence overlaps generated ${name}. Preserve data and resolve placement.`);
19
+ }
20
+ if (lexical !== resolve(checkout, '.wrangler/state') || state !== lexical) throw new LocalStateSafetyError('Local state safety: canonical persistence must be contained at .wrangler/state without symlinks. Preserve existing data and recover it explicitly.');
21
+ for (const name of ['.wrangler/reference', 'dist/server/.wrangler/state', 'dist/.wrangler/state', '.output/server/.wrangler/state']) {
22
+ const legacy = resolve(checkout, name); await physicalPath(legacy);
23
+ try { await lstat(legacy); } catch (error) { if (error.code === 'ENOENT') continue; throw error; }
24
+ throw new LocalStateSafetyError(`Local state safety: legacy state exists at ${name}. Preserve it and explicitly recover its data; nothing was moved or deleted.`);
25
+ }
26
+ for (const name of ['.wrangler', '.wrangler/state']) {
27
+ let status; try { status = await lstat(resolve(checkout, name)); } catch (error) { if (error.code !== 'ENOENT') throw error; }
28
+ if (status && (!status.isDirectory() || status.isSymbolicLink())) throw new LocalStateSafetyError('Local state safety: state must be a directory without symlinks. Preserve it and resolve placement.');
29
+ }
30
+ return state;
31
+ }
@@ -0,0 +1,7 @@
1
+ import { execFile } from 'node:child_process';
2
+ const command=(root,tool,args)=>new Promise(resolve=>execFile(tool,args,{cwd:root,timeout:8000,maxBuffer:10000},(error,stdout)=>resolve(error?null:stdout.trim())));
3
+ export async function metadata(root){
4
+ const [name,email,remote,github]=await Promise.all([command(root,'git',['config','user.name']),command(root,'git',['config','user.email']),command(root,'git',['remote','get-url','origin']),command(root,'gh',['api','user','--jq','.login'])]);
5
+ let repository=null;if(remote){const match=remote.match(/github\.com[:/]([^\s?#]+?)(?:\.git)?$/);if(match)repository=match[1]}
6
+ return {name,email,repository,githubAccount:github,cloudflareAccount:process.env.CLOUDFLARE_ACCOUNT_ID||null,cloudflareAuthentication:process.env.CLOUDFLARE_API_TOKEN?'environment-token':'not-checked',nodeWarning:process.versions.node.split('.').map(Number).some(Number.isNaN)?null:(Number(process.versions.node.split('.')[0])===26&&Number(process.versions.node.split('.')[1])<8?'This checkout recommends Node 26.8.1 or later.':null)};
7
+ }
@@ -0,0 +1,45 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { readFile, unlink } from 'node:fs/promises';
3
+ import { join, relative } from 'node:path';
4
+ import { chooseOrigin } from './adapter-settings.mjs';
5
+ import { privateDirectory, writePrivateFile } from './private-files.mjs';
6
+ import { installationFailure, installationFailureMessage } from './failures.mjs';
7
+ import { labels } from './adapter-labels.mjs';
8
+
9
+ // The browser and terminal use the same bounded setup journey. Its temporary
10
+ // runtime is gone before that lease releases. The foreground preview belongs
11
+ // to the installer process, outside the bounded job's descendant tree.
12
+ export async function installOperator(root,input,store,onApp,signal,hooks){
13
+ let answer,app,step='setup';
14
+ try{
15
+ await store.configureQueue?.({verb:'installer'});
16
+ const origin=await chooseOrigin({root,input,store},hooks),address=new URL(origin);
17
+ const directory=await privateDirectory(root,['.seed','installer']);
18
+ answer=join(directory,`answers-${randomUUID()}.json`);
19
+ await writePrivateFile(answer,JSON.stringify(input));
20
+ // The browser owns no local operation. A verifier may have launched it while
21
+ // holding a different checkout's token; each public command takes its own.
22
+ const env={...process.env};delete env.RER_LOCAL_OPERATION;
23
+ const run=(label,args)=>store.run(label,process.execPath,['scripts/rer.mjs',...args,'--non-interactive','--json'],{cwd:root,signal,env,heavy:false});
24
+ step='dependencies';await store.step(labels.dependencies);await run(labels.dependencies,['install']);
25
+ step='setup';await store.step(labels.build);await run(labels.build,['setup','--answers',relative(root,answer),'--host',address.hostname.replace(/^\[|\]$/g,''),'--port',address.port]);
26
+ await unlink(answer);answer=undefined;
27
+ // Preview's ready event carries RER_OPEN; keep the checkout's dev key out of this log.
28
+ const devKey=/^RER_DEV_KEY=(\S+)$/m.exec(await readFile(join(root,'.rer/local/dev-key.env'),'utf8').catch(()=>''))?.[1];if(devKey)store.addSecrets?.([devKey]);
29
+ step='start';await store.step(labels.start);
30
+ app=store.spawn(labels.start,process.execPath,['scripts/rer.mjs','preview','--non-interactive','--json'],{cwd:root,signal,env});onApp(app);
31
+ let buffer='',ready=false,openUrl;
32
+ app.stdout.on('data',chunk=>{buffer+=chunk;let end;while((end=buffer.indexOf('\n'))>=0){const line=buffer.slice(0,end);buffer=buffer.slice(end+1);try{const event=JSON.parse(line);if(event.version===1&&event.verb==='preview'&&event.event==='ready'&&event.url===origin){ready=true;openUrl=event.openUrl;}}catch{}}});
33
+ await store.waitForReady(labels.start,app,async probeSignal=>{
34
+ if(!ready)return {ready:false,message:'Waiting for managed application readiness'};
35
+ const response=await fetch(origin+'/health',{signal:probeSignal});
36
+ return {ready:response.ok&&(await response.json()).status==='ok'};
37
+ },{signal,timeoutMs:1200000});
38
+ // A dev-identity seed's outbox needs the key exchange RER_OPEN carries. State
39
+ // never holds the key: the installer page asks /api/open-app when it shows the link.
40
+ store.state.appUrl=origin+'/__dev/outbox';store.state.devOpen=Boolean(openUrl?.startsWith(origin+'/'));await store.step(labels.complete);await store.phase('complete');
41
+ }catch(error){
42
+ if(app)await store.stop(app);await store.stopCommands?.(signal);
43
+ store.state.error=installationFailureMessage(installationFailure('command_failed',{step,diagnostic:error}));await store.phase('failed');
44
+ }finally{if(answer)await unlink(answer);}
45
+ }
@@ -0,0 +1,39 @@
1
+ import {fileURLToPath} from 'node:url';
2
+ import { privateDirectory } from './private-files.mjs';
3
+ import { localInstaller } from './adapter.mjs';
4
+ import { selectLocalState, LocalStateSafetyError } from './local/state.mjs';
5
+ import {operatorPrerequisites} from './operator-prerequisites.mjs';
6
+ import { installOperator } from './operator-install.mjs';
7
+ import { projectViteBinary } from './project-vite.mjs';
8
+ import { join, delimiter } from 'node:path';
9
+
10
+ export function operatorInstaller({ database, features, prepare, prepareData }) {
11
+ const lifecycle=localInstaller({
12
+ features, node: '22.20.0',
13
+ failure: (error,{store})=>error instanceof LocalStateSafetyError ? store.redact(error.message) : undefined,
14
+ async prepare(context) { context.localState = await selectLocalState(context.root); await privateDirectory(context.root, ['.wrangler', 'state']); await prepare?.(context); },
15
+ prepareData,
16
+ guard: async ({ root }) => { await selectLocalState(root); },
17
+ secrets: {
18
+ keepExisting: true, originKey: 'AUTH_ORIGIN',
19
+ values: ({ origin }) => ({ AUTH_LOCAL: 'true', AUTH_ORIGIN: origin, MAIL_PROVIDER: 'development', MAIL_FROM: 'auth@localhost.test' }),
20
+ },
21
+ build: ['pnpm', ['run', 'build:app']],
22
+ migrate: ['pnpm', ['exec', 'wrangler', 'd1', 'migrations', 'apply', database, '--local', '--persist-to', '.wrangler/state', '--config', 'dist/server/wrangler.json']],
23
+ // Setup uses preview's production runtime behind the same public proxy.
24
+ start: previewTarget,
25
+ owner: { existingOwnerViaSignIn: true, displayName: input => input.displayName || 'Owner' },
26
+ signIn: { emailKey: 'OWNER_EMAIL', returnTo: '/setup' },
27
+ });
28
+ return {...lifecycle,localPrerequisites:async({root,vars,email})=>vars.AUTH_LOCAL==='true'&&vars.MAIL_PROVIDER==='development'&&Boolean(vars.SESSION_SECRET)&&await operatorPrerequisites(root,email),install:(root,input,store,onApp,signal)=>installOperator(root,input,store,onApp,signal,lifecycle.hooks)};
29
+ }
30
+
31
+ // Only local runtimes receive the dev key; vite.config.ts passes it to `vite dev` alone.
32
+ // No runtime rer starts serves the Local Explorer: it reads and writes the store
33
+ // without the key. trafficProxy also refuses /cdn-cgi/* on the public listener.
34
+ const devBindings = (devKey, devLogin) => devKey ? { RER_DEV_KEY: devKey, RER_DEV_IDENTITY: devLogin ? 'off' : 'on' } : {};
35
+ const internalPort = port => { if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('A local runtime requires the traffic proxy internal port.'); return String(port); };
36
+ const developmentEnvironment = () => { const { RER_MODELS_SERVICE, RER_MODELS_KEY, ...env } = process.env; return env; };
37
+ export const developmentTarget = ({ root, origin, runtimePort, devKey, devLogin, modelsLive }) => [process.execPath, [projectViteBinary(root), 'dev', '--host', '127.0.0.1', '--port', internalPort(runtimePort), '--strictPort'], { env: { ...developmentEnvironment(), PATH: join(root, 'node_modules', '.bin') + delimiter + (process.env.PATH ?? ''), RER_DEV_ORIGIN: origin, WRANGLER_SEND_METRICS: 'false', X_LOCAL_EXPLORER: 'false', ...devBindings(devKey, devLogin), ...(modelsLive ? { RER_MODELS_SERVICE: modelsLive.baseUrl, RER_MODELS_KEY: modelsLive.apiKey } : {}) } }];
38
+
39
+ export const previewTarget = ({ origin, runtimePort, devKey, scheduled }) => [process.execPath, [fileURLToPath(new URL('./production-runtime.mjs', import.meta.url))], { env: { ...process.env, RER_DEV_ORIGIN: origin, RER_RUNTIME_HOST: '127.0.0.1', RER_RUNTIME_PORT: internalPort(runtimePort), WRANGLER_SEND_METRICS: 'false', CLOUDFLARE_INCLUDE_PROCESS_ENV: 'false', X_LOCAL_EXPLORER: 'false', RER_RUNTIME_TRIGGERS: scheduled ? '1' : '0', ...(devKey ? { RER_DEV_KEY: devKey } : {}) } }];
@@ -0,0 +1,27 @@
1
+ import { readdir,lstat } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+
5
+ // A read-only preflight prevents an empty resource directory from being treated
6
+ // as completed setup. Ordinary application sign-in then verifies the actual
7
+ // configured binding before readiness; no database or owner is created here.
8
+ export async function operatorPrerequisites(root,email){
9
+ const migrations=(await readdir(join(root,'migrations/core'))).filter(name=>name.endsWith('.sql'));
10
+ const directory=join(root,'.wrangler/state/v3/d1/miniflare-D1DatabaseObject');
11
+ let files;try{
12
+ let path=root;for(const part of ['.wrangler','state','v3','d1','miniflare-D1DatabaseObject']){path=join(path,part);const info=await lstat(path);if(!info.isDirectory()||info.isSymbolicLink()||info.uid!==process.getuid())return false;}
13
+ files=await readdir(directory,{withFileTypes:true});}catch{return false;}
14
+ for(const file of files){
15
+ if(!file.isFile()||!file.name.endsWith('.sqlite'))continue;
16
+ let db;
17
+ try{
18
+ db=new DatabaseSync(join(directory,file.name),{readOnly:true});
19
+ const applied=new Set(db.prepare('SELECT name FROM d1_migrations').all().map(row=>row.name));
20
+ const claimed=db.prepare('SELECT COUNT(*) AS count FROM oa_bootstrap').get().count;
21
+ const owner=db.prepare("SELECT COUNT(*) AS count FROM oa_operators WHERE email=? AND role='owner' AND status='active'").get(email).count;
22
+ if(claimed===1&&owner===1&&migrations.every(name=>applied.has(name)))return true;
23
+ }catch{/* A missing/corrupt/unrelated database cannot prove prior setup. */}
24
+ finally{db?.close();}
25
+ }
26
+ return false;
27
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@run-edit-run/onboarding",
3
+ "private": true,
4
+ "type": "module",
5
+ "description": "Dependency-free Base seed installer and local runner; seeds copy its runtime source without Base tests."
6
+ }
@@ -0,0 +1,16 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { SETUP_VALUE_LIMIT, setupInputLimit } from './input-limits.mjs';
3
+ export function validateAnswers(input,{fields,safeKeys}) {
4
+ if(!input||typeof input!=='object'||Array.isArray(input))throw Error('Provide a JSON object of known nonsecret answers.');
5
+ const answers={};
6
+ for(const [key,value] of Object.entries(input)){
7
+ if(!safeKeys.includes(key))throw Error('Only supported nonsecret answer fields are accepted. Use the secure stdin path for credentials.');
8
+ if(typeof value!=='string'||value.length>SETUP_VALUE_LIMIT)throw Error('Answers must be strings within the field limits.');
9
+ if(value!==''&&!fields[key].valid(value))throw Error(`Invalid ${(fields[key].label||key).toLowerCase()}.`);
10
+ if(value&&fields[key].type==='url')answers[key]=new URL(value).origin;else answers[key]=value;
11
+ }
12
+ for(const [key,mode] of [['prefix','prefixMode'],['appOrigin','addressMode']])if(safeKeys.includes(mode)&&answers[key]&&!Object.hasOwn(answers,mode))answers[mode]='custom';
13
+ return answers;
14
+ }
15
+ export async function readAnswers(path,model){const text=await readFile(path,'utf8');if(Buffer.byteLength(text)>setupInputLimit(model))throw Error('The answers file is too large.');let value;try{value=JSON.parse(text)}catch{throw Error('The answers file must contain valid JSON.')}return validateAnswers(value,model)}
16
+ export async function readSecrets(stream,model){const limit=setupInputLimit(model);let text='';for await(const chunk of stream){text+=chunk;if(Buffer.byteLength(text)>limit)throw Error('Secure input is too large.')}try{return JSON.parse(text)}catch{throw Error('Secure stdin must contain a JSON object.')}}
@@ -0,0 +1,97 @@
1
+ import { constants, linkSync, unlinkSync, writeSync, openSync, closeSync, fstatSync, fchmodSync, ftruncateSync } from 'node:fs';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { mkdir, open, rename, unlink } from 'node:fs/promises';
4
+ import { join } from 'node:path';
5
+
6
+ function requireOwner(stat, path, directory = false) {
7
+ if (!(directory ? stat.isDirectory() : stat.isFile()) || (!directory && stat.nlink > 1 && (stat.mode & 0o7777) !== 0o600) || (process.getuid && stat.uid !== process.getuid())) {
8
+ throw Error(`Installer private storage must be an owned ${directory ? 'directory' : 'regular private file (no permissive hard links)'}: ${path}`);
9
+ }
10
+ }
11
+
12
+ // Refuse links and foreign-owned storage before changing permissions or bytes.
13
+ export async function privateDirectory(root, parts) {
14
+ let path = root;
15
+ for (const part of parts) {
16
+ path = join(path, part);
17
+ try { await mkdir(path, { mode: 0o700 }); } catch (error) { if (error.code !== 'EEXIST') throw error; }
18
+ const handle = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
19
+ try { const stat = await handle.stat(); requireOwner(stat, path, true); if ((stat.mode & 0o7777) !== 0o700) await handle.chmod(0o700); } finally { await handle.close(); }
20
+ // Self-ignore on both creation and resume. Atomic private-file replacement
21
+ // validates existing ownership/links before changing any bytes.
22
+ if (part === '.seed') await writePrivateFile(join(path, '.gitignore'), '*\n');
23
+ }
24
+ return path;
25
+ }
26
+
27
+ async function privateFile(path, flags, action) {
28
+ const handle = await open(path, flags | constants.O_NOFOLLOW, 0o600);
29
+ try {
30
+ const stat = await handle.stat();
31
+ requireOwner(stat, path);
32
+ // Already-private hard links are safe to read or replace. Never chmod their
33
+ // shared inode; this also permits a complete exclusive publication in flight.
34
+ if (stat.nlink <= 1 && (stat.mode & 0o7777) !== 0o600) await handle.chmod(0o600);
35
+ return await action(handle);
36
+ } finally { await handle.close(); }
37
+ }
38
+ export const readPrivateFile = path => privateFile(path, constants.O_RDONLY, handle => handle.readFile('utf8'));
39
+ export async function writePrivateFile(path, content, exclusive = false) {
40
+ if (!exclusive) {
41
+ try { await privateFile(path, constants.O_RDONLY, () => {}); }
42
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
43
+ }
44
+ const temporary = `${path}.${randomBytes(16).toString('hex')}.tmp`;
45
+ try {
46
+ await privateFile(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
47
+ async handle => { await handle.writeFile(content); });
48
+ if (exclusive) {
49
+ // Publish complete bytes without replacing another process's credential.
50
+ // Readers can safely use the fully written, already-private linked inode.
51
+ // A process interruption before unlink does not invalidate the receipt.
52
+ linkSync(temporary, path);
53
+ unlinkSync(temporary);
54
+ } else await rename(temporary, path);
55
+ } finally {
56
+ try { await unlink(temporary); } catch (error) { if (error.code !== 'ENOENT') throw error; }
57
+ }
58
+ }
59
+
60
+ // Validate each append, so replacement with a link is refused as well. Short
61
+ // synchronous writes cannot queue unbounded command output or leak descriptors.
62
+ export const PRIVATE_LOG_LIMIT = 16 * 1024 * 1024;
63
+ const TRUNCATED = Buffer.from('\n[log truncated]\n');
64
+
65
+ export async function openPrivateLog(path) {
66
+ let written=0, truncated=false, closed=false;
67
+ const withFile = action => {
68
+ const fd=openSync(path,constants.O_WRONLY|constants.O_APPEND|constants.O_CREAT|constants.O_NOFOLLOW,0o600);
69
+ try {
70
+ const stat=fstatSync(fd);requireOwner(stat,path);
71
+ if(stat.nlink!==1)throw Error('Installer append logs must not have hard links.');
72
+ if((stat.mode&0o7777)!==0o600)fchmodSync(fd,0o600);
73
+ action(fd,stat);
74
+ } finally {closeSync(fd);}
75
+ };
76
+ const writeAll = (fd,bytes) => { let offset=0;while(offset<bytes.length)offset+=writeSync(fd,bytes,offset,bytes.length-offset); };
77
+ // O_TRUNC would destroy linked targets before validation. Truncate only after
78
+ // the opened inode passes the same ownership/link checks as every append.
79
+ withFile(fd=>ftruncateSync(fd,0));
80
+ const append = text => {
81
+ if(closed||truncated||!text)return;
82
+ withFile((fd,stat)=>{
83
+ written=Math.max(written,stat.size);
84
+ const available=Math.max(0,PRIVATE_LOG_LIMIT-TRUNCATED.length-written);
85
+ // The caller normally emits small chunks. Bound conversion here too so a
86
+ // direct huge write cannot allocate a second copy of the entire input.
87
+ const source=String(text), length=Buffer.byteLength(source);
88
+ const bytes=Buffer.from(source.slice(0,available));
89
+ let end=Math.min(bytes.length,available);
90
+ // Do not leave half of a UTF-8 code point before the truncation marker.
91
+ if(end<bytes.length)while(end>0&&(bytes[end]&0xc0)===0x80)end--;
92
+ writeAll(fd,bytes.subarray(0,end));written+=end;
93
+ if(length>available){writeAll(fd,TRUNCATED);written+=TRUNCATED.length;truncated=true;}
94
+ });
95
+ };
96
+ return {write:append,close:async()=>{closed=true;}};
97
+ }
@@ -0,0 +1,35 @@
1
+ import { unlink } from 'node:fs/promises';
2
+ import { privateDirectory, readPrivateFile, writePrivateFile } from './private-files.mjs';
3
+ import { join } from 'node:path';
4
+ import { randomBytes, createHash } from 'node:crypto';
5
+ export async function installationReceipt(root,origin,scope){
6
+ await privateDirectory(root,['.seed','installer','private']);
7
+ const path=join(root,'.seed/installer/private',createHash('sha256').update(origin).digest('hex')+'.json');
8
+ let saved;try{saved=JSON.parse(await readPrivateFile(path))}catch(error){if(error.code==='ENOENT')return null;throw error}
9
+ if(saved.origin!==origin||typeof saved.token!=='string'||!saved.token)throw Error('The saved installation receipt is invalid. Preserve it and inspect this installation before continuing.');
10
+ if(scope&&(saved.accountId!==scope.accountId||saved.prefix!==scope.prefix))throw Error('The saved installation receipt does not identify this Cloudflare account and Worker. Preserve it and verify the existing installation before continuing.');
11
+ return saved;
12
+ }
13
+ export async function installationCredential(root,origin,scope){
14
+ const directory=await privateDirectory(root,['.seed','installer','private']);
15
+ const path=join(directory,createHash('sha256').update(origin).digest('hex')+'.json');
16
+ let saved=await installationReceipt(root,origin,scope);let token=saved?.token,deployed=saved?.deployed===true;
17
+ const record=()=>({...saved,origin,token,...(scope?{accountId:scope.accountId,prefix:scope.prefix}:{})});
18
+ if(!token){token=randomBytes(32).toString('hex');try{await writePrivateFile(path,JSON.stringify(record()),true)}catch(error){if(error.code!=='EEXIST')throw error;saved=await installationReceipt(root,origin,scope);token=saved.token;deployed=saved.deployed===true}}
19
+ return {token,deployed,markDeployed:()=>writePrivateFile(path,JSON.stringify({...record(),deployed:true})),clear:()=>unlink(path)};
20
+ }
21
+ export async function localSetupToken(root,key){
22
+ try{const text=await readPrivateFile(join(root,'.dev.vars'));const line=text.split(/\r?\n/).find(value=>value.slice(0,value.indexOf('=')).trim()===key);if(!line)return undefined;const value=line.slice(line.indexOf('=')+1).trim();return /^['"].*['"]$/.test(value)?value.slice(1,-1):value}catch(error){if(error.code==='ENOENT')return undefined;throw error}
23
+ }
24
+ export function stopChild(child){if(!child||!child.pid||child.exitCode!==null||child.signalCode!==null)return;try{process.kill(-child.pid,'SIGTERM')}catch{child.kill('SIGTERM')}}
25
+ export async function persistLocalEmail(root,variables){
26
+ const path=join(root,'.dev.vars');let content='';try{content=await readPrivateFile(path)}catch(error){if(error.code!=='ENOENT')throw error}
27
+ const lines=content.split(/\r?\n/).filter(line=>!Object.keys(variables).some(key=>line.startsWith(key+'=')));
28
+ await writePrivateFile(path,lines.join('\n').replace(/\n*$/,'\n')+Object.entries(variables).map(([key,value])=>key+'='+JSON.stringify(value)).join('\n')+'\n');
29
+ }
30
+ export async function localEmailBindings(root){
31
+ let text;try{text=await readPrivateFile(join(root,'.dev.vars'))}catch(error){if(error.code==='ENOENT')return {};throw error}
32
+ const allowed=new Set(['EMAIL_PROVIDER','EMAIL_FROM','EMAIL_ACCOUNT_ID','EMAIL_API_KEY']);const values={};
33
+ for(const line of text.split(/\r?\n/)){const index=line.indexOf('=');const key=line.slice(0,index).trim();if(index<0||!allowed.has(key))continue;let value=line.slice(index+1).trim();if(value.startsWith('"')){try{value=JSON.parse(value)}catch{continue}}else if(value.startsWith("'")&&value.endsWith("'"))value=value.slice(1,-1);values[key]=value}
34
+ return values;
35
+ }
@@ -0,0 +1,134 @@
1
+ import { setTimeout as delay } from 'node:timers/promises'
2
+ import { inspectionMs, readProcessIdentity, readProcessTable, sameBirth, sameProcess, validPid } from './process-table.mjs'
3
+ import { readOwnedTree } from './process-tree.mjs'
4
+
5
+ const graceMs = 1000
6
+ const cleanupMs = 30000
7
+ // macOS ps already spaces observations by tens of milliseconds.
8
+ const pollMs = process.platform === 'darwin' ? 5 : 25
9
+ const isZombie = (member) => member.state.startsWith('Z')
10
+ const isStopped = (member) => /^[Tt]/.test(member.state)
11
+ const key = (member) => `${member.pid}:${member.birth}:${member.command}`
12
+ const isLeaf = (member, group) => !group.some((other) => other.ppid === member.pid)
13
+
14
+ // Ask every owned process to shut down gracefully, including supervisors that
15
+ // would restart a killed worker. Forced escalation is leaf-first so surviving
16
+ // parents can reap children. Every signal rechecks numeric birth identity.
17
+ export async function stopOwnedGroup(child, signal, ownership) {
18
+ const pgid = child.pid
19
+ if (!validPid(pgid)) throw new Error('Invalid owned process group.')
20
+ const deadline = Date.now() + cleanupMs
21
+ // Even callers without a prior capture must establish actual ancestry. A
22
+ // same-PGID sibling is not ours, and a lost launcher cannot establish custody.
23
+ ownership ??= await readOwnedTree(child)
24
+ const inspect = async (timeout) => {
25
+ const current = await readOwnedTree(child, ownership, timeout)
26
+ // Retain departed group leaders as provenance, not as signal targets.
27
+ for (const member of current) if (!ownership.some(saved => sameProcess(saved, member))) ownership.push(member)
28
+ return current
29
+ }
30
+ let group
31
+ const signalled = new Map()
32
+ const stopped = new Map()
33
+ let lastGroup = []
34
+
35
+ const snapshot = async (limit = deadline) => {
36
+ const current = await inspect(Math.min(inspectionMs, limit - Date.now()))
37
+ if (Date.now() >= limit) throw new Error('Process inspection deadline expired.')
38
+ lastGroup = current
39
+ return current
40
+ }
41
+ const send = async (member, name, requireLeaf = true, limit = deadline) => {
42
+ // Recheck both identity and children immediately before each numeric PID
43
+ // signal. Never send a negative/group signal that can kill parents first.
44
+ const timeout = Math.min(inspectionMs, limit - Date.now())
45
+ const current = requireLeaf ? await readProcessTable(timeout) : []
46
+ const target = await readProcessIdentity(member.pid, Math.min(inspectionMs, limit - Date.now()))
47
+ if (Date.now() >= limit) throw new Error('Process inspection deadline expired.')
48
+ if (!sameProcess(member, target) || isZombie(target)) return false
49
+ if (requireLeaf && !isLeaf(target, current)) return false
50
+ if (name === 'SIGKILL' && !isStopped(target)) return false
51
+ if (requireLeaf && target.pid === pgid && current.some(other => other.pid !== pgid
52
+ && ownership.some(saved => sameBirth(saved, other)))) return false
53
+ // The full identity was read immediately before this numeric signal. A
54
+ // stopped leaf cannot fork across its final SIGKILL inspection. Discovery
55
+ // continues on every outer scan, including after a graceful late fork.
56
+ try { process.kill(target.pid, name) } catch (error) {
57
+ if (error.code !== 'ESRCH') throw new Error(`Could not send ${name} to owned process ${target.pid}.`)
58
+ return false
59
+ }
60
+ return true
61
+ }
62
+
63
+ // A captured direct supervisor can receive its graceful shutdown signal
64
+ // promptly. Identity is rechecked first; no group or descendant is signalled
65
+ // until scoped/ancestry discovery establishes its own full identity.
66
+ const leader = ownership.find(member => member.pid === pgid)
67
+ if (leader && child.exitCode == null && child.signalCode == null) {
68
+ const live = await readProcessIdentity(pgid, inspectionMs)
69
+ if (sameProcess(leader, live) && live.ppid === process.pid && !isZombie(live)) {
70
+ try { process.kill(pgid, signal); signalled.set(key(live), Date.now()) }
71
+ catch (error) { if (error.code !== 'ESRCH') throw new Error(`Could not send ${signal} to owned process ${pgid}.`) }
72
+ }
73
+ }
74
+
75
+ const unfinished = (cause) => new Error(`Owned processes remain after cancellation (last observed ${lastGroup.map((member) => `${member.pid}:${isZombie(member) ? 'zombie' : 'live'}`).join(', ')}).`, { cause })
76
+ let cleanupError
77
+ try {
78
+ while (Date.now() < deadline) {
79
+ group = await snapshot()
80
+ if (!group.length) {
81
+ await delay(pollMs)
82
+ group = await snapshot()
83
+ if (!group.length && (child.exitCode !== null || child.signalCode !== null)) return
84
+ if (!group.length) continue
85
+ }
86
+ let resuming = false
87
+ // Resume raced parents before considering any leaves, regardless of the
88
+ // order ps returns them. Confirm SIGCONT took effect on the next scan.
89
+ for (const member of group) {
90
+ if (!stopped.has(key(member)) || isLeaf(member, group)) continue
91
+ // SIGCONT also cancels a still-pending SIGSTOP; a running snapshot
92
+ // alone is not evidence that our stop can no longer suspend a parent.
93
+ if (await send(member, 'SIGCONT', false)) stopped.delete(key(member))
94
+ resuming = true
95
+ }
96
+ if (resuming) { await delay(pollMs); continue }
97
+ for (const member of group) {
98
+ if (Date.now() >= deadline) break
99
+ const identity = key(member)
100
+ if (isZombie(member)) continue
101
+ if (!signalled.has(identity)) {
102
+ // A supervisor needs its shutdown signal before its worker disappears;
103
+ // otherwise normal restart behavior can keep this tree alive forever.
104
+ if (await send(member, signal, false)) signalled.set(identity, Date.now())
105
+ } else if (Date.now() - signalled.get(identity) >= graceMs) {
106
+ if (!isLeaf(member, group) || (member.pid === pgid && group.length !== 1)) continue
107
+ if (!stopped.has(identity)) {
108
+ // Freeze only a childless process before its final inspection. It
109
+ // cannot fork between that inspection and SIGKILL once stopped.
110
+ if (await send(member, 'SIGSTOP')) stopped.set(identity, member)
111
+ } else if (isStopped(member)) {
112
+ if (await send(member, 'SIGKILL')) stopped.delete(identity)
113
+ }
114
+ }
115
+ }
116
+ await delay(pollMs)
117
+ }
118
+ throw unfinished()
119
+ } catch (error) {
120
+ cleanupError = Date.now() >= deadline ? unfinished(error) : error
121
+ throw cleanupError
122
+ } finally {
123
+ // Inspection/signalling failures must not leave a process suspended.
124
+ const errors = []
125
+ const resumeDeadline = Math.min(deadline + 1000, Date.now() + 1000)
126
+ for (const member of stopped.values()) {
127
+ try { await send(member, 'SIGCONT', false, resumeDeadline) } catch (error) { errors.push(error) }
128
+ }
129
+ // Preserve the original failure while reporting a failure to resume an owned process.
130
+ // eslint-disable-next-line no-unsafe-finally
131
+ if (errors.length) throw new AggregateError([cleanupError, ...errors].filter(Boolean),
132
+ [cleanupError?.message, 'Could not resume every suspended owned process.', ...errors.map((error) => error.message)].filter(Boolean).join(' '))
133
+ }
134
+ }
@@ -0,0 +1,91 @@
1
+ import {execFile} from 'node:child_process';
2
+ import {promisify} from 'node:util';
3
+ import {realpath,readFile,readdir,readlink} from 'node:fs/promises';
4
+ import {basename,join} from 'node:path';
5
+ const exec=promisify(execFile);
6
+ // macOS supplies lsof in /usr/sbin, which a command PATH may omit.
7
+ // Diagnose sockets with the system utility even when that PATH is constrained.
8
+ const lsof=process.platform==='darwin'?'/usr/sbin/lsof':'lsof';
9
+ const run=async(command,args)=>{
10
+ try{return (await exec(command,args,{timeout:3000,maxBuffer:1024*1024})).stdout;}
11
+ catch(error){
12
+ // lsof exits 1 when a selected PID has exited,
13
+ // while still returning the live listeners of other selected PIDs.
14
+ return command===lsof&&error.code===1&&typeof error.stdout==='string'?error.stdout:'';
15
+ }
16
+ };
17
+ export async function processName(pid){
18
+ if(process.platform==='linux'){const name=await readFile(`/proc/${pid}/comm`,'utf8').catch(()=>'');if(name)return name.trim().replace(/[^a-zA-Z0-9._-]/g,'').slice(0,80)||'process';}
19
+ const text=await run('/bin/ps',['-p',String(pid),'-o','comm=']);
20
+ return basename(text.trim()).replace(/[^a-zA-Z0-9._-]/g,'').slice(0,80)||'process';
21
+ }
22
+ function records(text){let record;const rows=[];for(const line of text.split('\n')){if(line[0]==='p'){record={pid:Number(line.slice(1)),names:[]};rows.push(record);}else if(line[0]==='n'&&record)record.names.push(line.slice(1));}return rows;}
23
+ export async function listeners(pids){
24
+ // -a is essential: otherwise lsof ORs the PID and network selections and
25
+ // still walks every process's sockets. Read only the owned tree on failure.
26
+ if(process.platform!=='linux')return records(await run(lsof,['-nP',...(pids?['-a','-p',pids.join(',')]:[]),'-iTCP','-sTCP:LISTEN','-Fpn']));
27
+ const sockets=new Map();
28
+ for(const file of ['tcp','tcp6'])for(const line of (await readFile('/proc/net/'+file,'utf8').catch(()=>'' )).trim().split('\n').slice(1)){
29
+ const parts=line.trim().split(/\s+/);if(parts[3]==='0A')sockets.set(parts[9],parseInt(parts[1].split(':')[1],16));
30
+ }
31
+ const found=[];
32
+ for(const pid of pids??await linuxPids()){
33
+ const names=[];for(const target of await descriptors(pid)){const inode=/^socket:\[(\d+)\]$/.exec(target)?.[1];if(sockets.has(inode))names.push('127.0.0.1:'+sockets.get(inode));}
34
+ if(names.length)found.push({pid,names});
35
+ }return found;
36
+ }
37
+ async function linuxPids(){return (await readdir('/proc')).filter(name=>/^\d+$/.test(name)).map(Number);}
38
+ async function descriptors(pid){return Promise.all((await readdir(`/proc/${pid}/fd`).catch(()=>[])).map(fd=>readlink(`/proc/${pid}/fd/${fd}`).catch(()=>'')));}
39
+
40
+ const portOf=name=>Number(/:(\d+)$/.exec(name)?.[1]);
41
+ export async function processCwd(pid){
42
+ if(process.platform==='linux')return readlink(`/proc/${pid}/cwd`).catch(()=>undefined);
43
+ return records(await run(lsof,['-a','-p',String(pid),'-d','cwd','-Fpn'])).flatMap(row=>row.names)[0];
44
+ }
45
+ // Every TCP port some process listens on, on any address: a wildcard listener
46
+ // does not stop a bind to 127.0.0.1, yet it still serves that port's traffic.
47
+ export async function listeningPorts(){return new Set((await listeners()).flatMap(row=>row.names.map(portOf)).filter(Number.isInteger));}
48
+ // The process that listens on a port on the given host's address or on every
49
+ // address, with its command and working directory.
50
+ export async function portListener(port,host){
51
+ const bare=host?.replace(/^\[|\]$/g,''),serves=name=>{const address=name.slice(0,name.lastIndexOf(':')).replace(/^\[|\]$/g,'');return portOf(name)===port&&(!host||address==='*'||address===bare);};
52
+ const row=(await listeners()).find(item=>item.names.some(serves));
53
+ return row&&{pid:row.pid,command:await processName(row.pid),cwd:await processCwd(row.pid)};
54
+ }
55
+
56
+ export async function listenerPorts(pid){
57
+ // Include child processes: pnpm commonly delegates the actual Vite listener.
58
+ const rows=(await run('/bin/ps',['-axo','pid=,ppid='])).trim().split('\n').map(line=>line.trim().split(/\s+/).map(Number));
59
+ const owned=new Set([pid]);for(let changed=true;changed;){changed=false;for(const [child,parent]of rows)if(owned.has(parent)&&!owned.has(child)){owned.add(child);changed=true;}}
60
+ return [...new Set((await listeners([...owned])).filter(row=>owned.has(row.pid)).flatMap(row=>row.names.map(name=>Number(/:(\d+)$/.exec(name)?.[1])).filter(port=>Number.isInteger(port)&&port>0&&port<=65535)))].sort((a,b)=>a-b);
61
+ }
62
+ // The caller and the chain that launched it (an installer server running setup,
63
+ // pnpm, a shell) act for this command, never against it.
64
+ async function callerLineage(){
65
+ const parents=new Map((await run('/bin/ps',['-axo','pid=,ppid='])).trim().split('\n').map(line=>line.trim().split(/\s+/).map(Number)));
66
+ const lineage=new Set([process.pid,process.ppid]);
67
+ for(let pid=process.ppid;pid>1&&parents.has(pid);){pid=parents.get(pid);if(lineage.has(pid))break;lineage.add(pid);}
68
+ return lineage;
69
+ }
70
+ export async function localProcessConflict(root,savedOrigin){
71
+ root=await realpath(root);const all=await listeners(),lineage=await callerLineage();
72
+ const cwdRecords=process.platform!=='linux'&&all.length?records(await run(lsof,['-a','-p',all.map(row=>row.pid).join(','),'-d','cwd','-Fpn'])):[];
73
+ let port;try{const url=new URL(savedOrigin);port=Number(url.port||80);}catch{}
74
+ for(const row of all){
75
+ if(lineage.has(row.pid))continue;
76
+ const onSavedPort=port&&row.names.some(name=>Number(/:(\d+)$/.exec(name)?.[1])===port);
77
+ const cwd=process.platform==='linux'?[await readlink(`/proc/${row.pid}/cwd`).catch(()=>undefined)].filter(Boolean):cwdRecords.filter(item=>item.pid===row.pid).flatMap(item=>item.names);
78
+ const places=(await Promise.all(cwd.map(path=>realpath(path).catch(()=>undefined)))).filter(Boolean),inCheckout=places.includes(root);
79
+ // A listener running somewhere else is another program's or another checkout's
80
+ // server: it does not use this checkout's state, so it only blocks the port.
81
+ if(onSavedPort&&places.length&&!places.some(place=>place===root||place.startsWith(root+'/')))continue;
82
+ if(onSavedPort||inCheckout)return {pid:row.pid,command:await processName(row.pid),reason:onSavedPort?`saved local port ${port}`:'this checkout'};
83
+ }
84
+ // A process may hold the canonical database without listening. Inspect only
85
+ // actual state files, never broad checkout contents or a process environment.
86
+ const state=join(root,'.wrangler/state');
87
+ let held;
88
+ if(process.platform==='linux'){for(const pid of await linuxPids())if(!lineage.has(pid)&&(await descriptors(pid)).some(path=>path.startsWith(state+'/'))){held={pid};break;}}
89
+ else held=records(await run(lsof,['-nP','+D',state,'-Fpn'])).find(row=>!lineage.has(row.pid));
90
+ if(held)return {pid:held.pid,command:await processName(held.pid),reason:'the canonical local state'};
91
+ }