@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,64 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { constants } from 'node:os';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { unlink } from 'node:fs/promises';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { stopGroup } from './processes.mjs';
7
+
8
+ export function signalExit(signal) { return 128 + (constants.signals[signal] ?? 1); }
9
+
10
+ export async function execute(command, job, config, adopt, signal, onStarted, options = {}) {
11
+ const child = (options.spawn ?? spawn)(process.execPath, [fileURLToPath(new URL('./supervisor.mjs', import.meta.url)), randomUUID(), String(config.killGraceMs)],
12
+ { cwd: job.repository, env: options.env ?? process.env, detached: true, stdio: ['inherit', 'inherit', 'inherit', 'ipc'] });
13
+ let owner, finish, outcome, reported = false;
14
+ const done = new Promise(resolve => { finish = resolve; });
15
+ const exited = new Promise(resolve => child.once('close', (code, signame) => {
16
+ finish({ code: code ?? signalExit(signame), signal: signame }); resolve();
17
+ }));
18
+ child.once('error', error => finish({ code: 127, error: error.message }));
19
+ child.on('message', message => {
20
+ if (message.done) { reported = true; finish({ ...message.done,
21
+ code: message.done.code ?? signalExit(message.done.signal) }); }
22
+ });
23
+ // The supervisor is this process's own child, so its exit always arrives. An
24
+ // exit without its done report means the job lost its supervisor; close alone
25
+ // can wait forever, because descendants still hold the inherited output pipes.
26
+ child.once('exit', (code, signame) => {
27
+ if (owner && !reported) finish({ code: code ?? signalExit(signame), signal: signame, supervisorEnded: true,
28
+ error: `${job.label}: the job's supervisor ended before the job finished; what remained of the job was stopped and its slot released.` });
29
+ });
30
+ const ended = () => child.exitCode !== null || child.signalCode !== null;
31
+ const abort = () => finish({ code: signalExit(signal.reason || 'SIGTERM'), signal: signal.reason });
32
+ signal.addEventListener('abort', abort, { once: true });
33
+ try {
34
+ const ready = await new Promise((resolve, reject) => {
35
+ const timer = setTimeout(() => reject(Error('Job supervisor did not become ready')), 10000);
36
+ child.once('message', message => { clearTimeout(timer); resolve(message.ready); });
37
+ child.once('error', error => { clearTimeout(timer); reject(error); });
38
+ child.once('exit', () => { clearTimeout(timer); reject(Error('Job supervisor exited before readiness')); });
39
+ });
40
+ owner = ready;
41
+ if (!owner || owner.pid !== child.pid) throw Error('Invalid job supervisor identity');
42
+ // Commit the group lease before allowing the supervisor to spawn any work.
43
+ await adopt(owner);
44
+ if (signal.aborted) abort();
45
+ else if (!ended()) {
46
+ await onStarted?.(job);
47
+ if (signal.aborted) abort();
48
+ else if (!ended()) child.send({ command, cwd: job.repository, nice: config.nice, membership: JSON.stringify({ directory: config.directory, id: job.id, token: job.membership }) });
49
+ }
50
+ outcome=await done;return outcome;
51
+ } finally {
52
+ signal.removeEventListener('abort', abort);
53
+ try {
54
+ if (owner) await stopGroup(owner, config.killGraceMs);
55
+ else child.kill('SIGKILL'); // The supervisor has not been given GO.
56
+ // Its group is stopped; output still arriving on a dead supervisor's pipes
57
+ // comes from escaped descendants, which the caller's cleanup stops.
58
+ if (outcome?.supervisorEnded) { child.stdout?.destroy(); child.stderr?.destroy(); }
59
+ await exited;
60
+ await options.cleanup?.(child,outcome);
61
+ if (owner?.socket) await unlink(owner.socket).catch(error => { if (error.code !== 'ENOENT') throw error; });
62
+ } catch (error) { error.keepLease = true; throw error; }
63
+ }
64
+ }
@@ -0,0 +1,48 @@
1
+ import { cpus, totalmem, userInfo } from 'node:os';
2
+ import { readFile } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+
5
+ export const GIB = 1024 ** 3;
6
+ export const TIMEOUT_EXIT = 75;
7
+ export const priorities = { owner: 0, normal: 1, background: 2 };
8
+ const names = { slots: 'SLOTS', loadThreshold: 'LOAD_THRESHOLD',
9
+ minFreeMemoryBytes: 'MIN_FREE_MEMORY_BYTES', pollMs: 'POLL_MS',
10
+ nice: 'NICE', killGraceMs: 'KILL_GRACE_MS' };
11
+
12
+ export async function configuration({ env = process.env, ...overrides } = {}) {
13
+ const directory = resolve(overrides.directory ?? env.RER_JOBS_DIR ?? `${userInfo().homedir}/.rer/jobs`);
14
+ let disk = {};
15
+ try { disk = JSON.parse(await readFile(resolve(directory, 'config.json'), 'utf8')); }
16
+ catch (error) { if (error.code !== 'ENOENT') throw error; }
17
+ const config = { slots: Math.max(1, Math.min(3, Math.floor(cpus().length / 4),
18
+ Math.floor(totalmem() / (8 * GIB)))), loadThreshold: Infinity,
19
+ minFreeMemoryBytes: 2 * GIB, pollMs: 3000, nice: 15, killGraceMs: 1000, ...disk };
20
+ for (const [key, suffix] of Object.entries(names)) {
21
+ if (env[`RER_JOBS_${suffix}`] !== undefined) config[key] = Number(env[`RER_JOBS_${suffix}`]);
22
+ if (overrides[key] !== undefined) config[key] = overrides[key];
23
+ if (!(Number.isFinite(config[key]) || key === 'loadThreshold' && config[key] === Infinity) || config[key] < 0) throw Error(`Invalid job queue ${key}`);
24
+ }
25
+ if (!Number.isInteger(config.slots) || config.slots < 1 || config.pollMs < 10
26
+ || !Number.isInteger(config.nice) || config.nice > 19) throw Error('Invalid slots, pollMs or nice');
27
+ return { ...config, directory };
28
+ }
29
+
30
+ export function ordered(jobs) {
31
+ return [...jobs].sort((a, b) => priorities[a.priority] - priorities[b.priority] || a.sequence - b.sequence);
32
+ }
33
+
34
+ // Each reason says what is running, what the limit is and how to change it.
35
+ export function admissionDetails(running, config, sample) {
36
+ if (running >= config.slots) return { code: 'slots', reason: `all job slots are in use (${running} of ${config.slots}; change with RER_JOBS_SLOTS)` };
37
+ if (!Number.isFinite(sample.load) || !Number.isFinite(sample.availableMemoryBytes))
38
+ return { code: 'resources_unavailable', reason: 'this computer\'s load or available memory could not be read' };
39
+ if (sample.load > config.loadThreshold) return { code: 'load',
40
+ reason: `the 1-minute load is ${sample.load.toFixed(1)} and jobs start at ${config.loadThreshold} or below (change with RER_JOBS_LOAD_THRESHOLD)` };
41
+ if (sample.availableMemoryBytes <= config.minFreeMemoryBytes) return { code: 'free_memory',
42
+ reason: `available memory is at or below the floor (change with RER_JOBS_MIN_FREE_MEMORY_BYTES)` };
43
+ return null;
44
+ }
45
+
46
+ export function admission(running, config, sample) {
47
+ return admissionDetails(running, config, sample)?.reason ?? null;
48
+ }
@@ -0,0 +1,48 @@
1
+ import { createServer, createConnection } from 'node:net';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { chmod, unlink } from 'node:fs/promises';
6
+ import { processInfo } from './processes.mjs';
7
+
8
+ // A lifetime nonce supplements Darwin's second-resolution ps start time.
9
+ // It also distinguishes two library clients in the same Node process.
10
+ export async function identity() {
11
+ const token = randomUUID(), socket = join(tmpdir(), `rer-job-${token}.sock`);
12
+ // Darwin's TMPDIR can be long; use the short system temporary directory then.
13
+ const path = Buffer.byteLength(socket) < 100 ? socket : `/tmp/rer-job-${token}.sock`;
14
+ const server = createServer(client => { client.end(token); });
15
+ await new Promise((resolve, reject) => { server.once('error', reject); server.listen(path, resolve); });
16
+ await chmod(path, 0o600);
17
+ const { title, ...processIdentity } = await processInfo(process.pid);
18
+ const info = { ...processIdentity, token, socket: path };
19
+ return { info, async close() {
20
+ await new Promise(resolve => server.close(resolve));
21
+ await unlink(path).catch(error => { if (error.code !== 'ENOENT') throw error; });
22
+ } };
23
+ }
24
+
25
+ export async function alive(owner) {
26
+ const current = await processInfo(owner.pid);
27
+ if (!current || current.zombie || current.start !== owner.start) return false;
28
+ if (!owner.socket || !owner.token) throw Error('Lease lacks lifetime identity');
29
+ return new Promise((resolve, reject) => {
30
+ const connection = createConnection(owner.socket);
31
+ let body = '';
32
+ connection.setTimeout(3000, () => connection.destroy(Error('Owner identity probe timed out; refusing admission')));
33
+ connection.on('data', data => { body += data; if (body.length > 128) connection.destroy(Error('Invalid owner identity')); });
34
+ connection.on('end', () => resolve(body === owner.token));
35
+ connection.on('error', async error => {
36
+ if (['ENOENT', 'ECONNREFUSED'].includes(error.code)) resolve(false);
37
+ else if (error.code === 'ECONNRESET') {
38
+ // Listener shutdown can reset a probe after the initial PID check.
39
+ // Reclaim proven death; otherwise retain the owner until the next sample.
40
+ try {
41
+ const after = await processInfo(owner.pid);
42
+ resolve(Boolean(after && !after.zombie && after.start === owner.start));
43
+ } catch (failure) { reject(failure); }
44
+ }
45
+ else reject(error);
46
+ });
47
+ });
48
+ }
@@ -0,0 +1,135 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { realpath, unlink } from 'node:fs/promises';
3
+ import { performance } from 'node:perf_hooks';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { configuration, admissionDetails, ordered, priorities, TIMEOUT_EXIT } from './config.mjs';
6
+ import { identity, alive } from './identity.mjs';
7
+ import { transaction } from './storage.mjs';
8
+ import { sampleResources } from './resources.mjs';
9
+ import { execute, signalExit } from './command.mjs';
10
+ import { stopGroup } from './processes.mjs';
11
+
12
+ const WAIT_REPORT_INTERVAL_MS = 15_000;
13
+
14
+ export async function createQueue(options = {}) {
15
+ const config = await configuration(options), lifetime = await identity();
16
+ const sampler = options.sampler ?? sampleResources;
17
+ let active = 0, closed = false;
18
+ const mutate = action => {
19
+ if (closed) throw Error('Job queue is closed');
20
+ return transaction(config.directory, lifetime.info, action);
21
+ };
22
+ async function reclaim(state) {
23
+ for (const key of ['queued', 'running']) {
24
+ const kept = [];
25
+ for (const job of state[key]) {
26
+ if (await alive(job.owner)) kept.push(job);
27
+ else {
28
+ if (key === 'running') await stopGroup(job.owner, config.killGraceMs);
29
+ if (job.owner.socket) await unlink(job.owner.socket).catch(error => { if (error.code !== 'ENOENT') throw error; });
30
+ }
31
+ }
32
+ state[key] = kept;
33
+ }
34
+ }
35
+ async function status() {
36
+ return mutate(async state => {
37
+ await reclaim(state);
38
+ const resources = await sampler();
39
+ const describe = ({ command, membership, owner: { token, socket, ...owner }, ...job }) => ({ ...job, owner,
40
+ elapsedSeconds: Math.floor((Date.now() - (job.startedAt ?? job.enqueuedAt)) / 1000) });
41
+ return { running: state.running.map(describe), queued: ordered(state.queued).map(describe), resources, config,
42
+ admissionReason: state.queued.length ? admissionDetails(state.running.length, config, resources)?.reason ?? 'ready' : 'queue empty' };
43
+ });
44
+ }
45
+ async function run(command, opts = {}) {
46
+ if (!Array.isArray(command) || !command.length || command.some(arg => typeof arg !== 'string')) throw Error('Command must be a nonempty string array');
47
+ if ((opts.kind ?? 'heavy') !== 'heavy' || !Object.hasOwn(priorities, opts.priority ?? 'normal')) throw Error('Invalid job kind or priority');
48
+ if (opts.maxWait !== undefined && (!Number.isFinite(opts.maxWait) || opts.maxWait < 0)) throw Error('Invalid maxWait');
49
+ const repository = await realpath(opts.cwd ?? process.cwd());
50
+ const controller = new AbortController(), start = performance.now();
51
+ const interrupt = () => controller.abort('SIGINT'), terminate = () => controller.abort('SIGTERM');
52
+ const external = () => controller.abort(opts.signal.reason === 'SIGINT' ? 'SIGINT' : 'SIGTERM');
53
+ process.on('SIGINT', interrupt); process.on('SIGTERM', terminate);
54
+ opts.signal?.addEventListener('abort', external, { once: true });
55
+ if (opts.signal?.aborted) external();
56
+ const job = { id: randomUUID(), membership: randomUUID(), label: opts.label ?? command[0], command,
57
+ repository, priority: opts.priority ?? 'normal',
58
+ enqueuedAt: Date.now(), owner: lifetime.info };
59
+ active++;
60
+ let keepLease = false, lastWaitReportAt = 0, lastWaitSignature;
61
+ try {
62
+ await mutate(state => { job.sequence = ++state.sequence; state.queued.push(job); });
63
+ await opts.onQueued?.(job);
64
+ for (;;) {
65
+ if (controller.signal.aborted) return { code: signalExit(controller.signal.reason), signal: controller.signal.reason };
66
+ const result = await mutate(async state => {
67
+ await reclaim(state);
68
+ const resources = await sampler();
69
+ const admission = admissionDetails(state.running.length, config, resources);
70
+ const waiting = ordered(state.queued), position = waiting.findIndex(item => item.id === job.id) + 1;
71
+ const blockers = [], reasonCodes = [];
72
+ if (position > 1) {
73
+ blockers.push(`waiting behind ${position - 1} earlier queued ${position === 2 ? 'job' : 'jobs'}`);
74
+ reasonCodes.push('queue_order');
75
+ }
76
+ if (admission) { blockers.push(admission.reason); reasonCodes.push(admission.code); }
77
+ const ahead = [...state.running.map(item => ({ state: 'running', label: item.label, repository: item.repository })),
78
+ ...waiting.slice(0, Math.max(0, position - 1)).map(item => ({ state: 'queued', label: item.label, repository: item.repository }))];
79
+ if (blockers.length) return { reason: blockers.join('; '), blockers, reasonCodes, position, ahead,
80
+ queued: waiting.length, running: state.running.length, resources,
81
+ limits: { slots: config.slots, loadThreshold: config.loadThreshold, minFreeMemoryBytes: config.minFreeMemoryBytes } };
82
+ if (opts.maxWait > 0 && performance.now() - start >= opts.maxWait * 1000)
83
+ return { reason: 'max-wait elapsed before admission', blockers: ['max-wait elapsed before admission'],
84
+ reasonCodes: ['max_wait'], position, queued: waiting.length, running: state.running.length, resources,
85
+ limits: { slots: config.slots, loadThreshold: config.loadThreshold, minFreeMemoryBytes: config.minFreeMemoryBytes } };
86
+ state.queued = state.queued.filter(item => item.id !== job.id);
87
+ job.startedAt = Date.now(); state.running.push(job);
88
+ return null;
89
+ });
90
+ if (!result) break;
91
+ const elapsed = performance.now() - start;
92
+ if (opts.maxWait !== undefined && elapsed >= opts.maxWait * 1000)
93
+ return { code: TIMEOUT_EXIT, timedOut: true, reason: result.reason };
94
+ const signature = `${result.position}:${result.reasonCodes.join(',')}`;
95
+ const now = performance.now();
96
+ if (!lastWaitReportAt || signature !== lastWaitSignature || now - lastWaitReportAt >= WAIT_REPORT_INTERVAL_MS) {
97
+ await opts.onWaiting?.(result.reason, result);
98
+ lastWaitReportAt = now;
99
+ lastWaitSignature = signature;
100
+ }
101
+ const remaining = opts.maxWait === undefined ? config.pollMs : Math.min(config.pollMs, Math.max(1, opts.maxWait * 1000 - elapsed));
102
+ await delay(remaining, undefined, { signal: controller.signal }).catch(error => { if (error.name !== 'AbortError') throw error; });
103
+ }
104
+ return await execute(command, job, config, owner => mutate(state => {
105
+ const lease = state.running.find(item => item.id === job.id);
106
+ if (!lease) throw Error('Job reservation disappeared');
107
+ lease.owner = owner;
108
+ }), controller.signal, opts.onStarted, opts);
109
+ } catch (error) { keepLease = Boolean(error.keepLease); throw error; }
110
+ finally {
111
+ try {
112
+ await mutate(state => { state.queued = state.queued.filter(item => item.id !== job.id);
113
+ if (!keepLease) state.running = state.running.filter(item => item.id !== job.id); });
114
+ } finally {
115
+ active--;
116
+ process.off('SIGINT', interrupt); process.off('SIGTERM', terminate);
117
+ opts.signal?.removeEventListener('abort', external);
118
+ }
119
+ }
120
+ }
121
+ async function drain({ maxWait } = {}) {
122
+ if (maxWait !== undefined && (!Number.isFinite(maxWait) || maxWait < 0)) throw Error('Invalid maxWait');
123
+ const start = performance.now();
124
+ for (;;) {
125
+ const view = await status();
126
+ if (!view.running.length && !view.queued.length) return { code: 0, ...view };
127
+ if (maxWait !== undefined && performance.now() - start >= maxWait * 1000) return { code: TIMEOUT_EXIT, timedOut: true, ...view };
128
+ await delay(maxWait === undefined ? config.pollMs : Math.min(config.pollMs, Math.max(1, maxWait * 1000 - (performance.now() - start))));
129
+ }
130
+ }
131
+ return { config, run, status, drain, async close() {
132
+ if (active) throw Error('Cannot close a queue with active jobs');
133
+ closed = true; await lifetime.close();
134
+ } };
135
+ }
@@ -0,0 +1,30 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+ import { join } from 'node:path';
4
+ import { readFile } from 'node:fs/promises';
5
+ import { alive } from './identity.mjs';
6
+ const execute = promisify(execFile);
7
+
8
+ // Environment conveys a capability, never a boolean exemption. Validate both
9
+ // the live lease and the calling process's actual ancestry before sharing it.
10
+ export async function liveMembership(value = process.env.RER_JOB_MEMBERSHIP) {
11
+ if (!value) return null;
12
+ try {
13
+ const claim = JSON.parse(value);
14
+ if (typeof claim.directory !== 'string' || typeof claim.id !== 'string' || typeof claim.token !== 'string') return null;
15
+ const state = JSON.parse(await readFile(join(claim.directory, 'state.json'), 'utf8'));
16
+ const job = state.running.find(job => job.id === claim.id && job.membership === claim.token);
17
+ if (!job || !await alive(job.owner)) return null;
18
+ if(await descendantOf(job.owner.pid))return {id:job.id,label:job.label,repository:job.repository};
19
+ } catch { /* Invalid, stale or unrelated inheritance requires normal admission. */ }
20
+ return null;
21
+ }
22
+
23
+ export async function descendantOf(owner) {
24
+ const {stdout}=await execute('/bin/ps',['-A','-o','pid=','-o','ppid=']);
25
+ const parents=new Map(stdout.trim().split('\n').map(line=>line.trim().split(/\s+/).map(Number))),visited=new Set();
26
+ for(let pid=process.pid;pid>1&&!visited.has(pid);pid=parents.get(pid)){
27
+ if(pid===owner)return true;visited.add(pid);
28
+ }
29
+ return false;
30
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "schema": 1,
3
+ "id": "job-queue",
4
+ "kind": "optional-source",
5
+ "entry": "index.mjs",
6
+ "cli": "cli.mjs",
7
+ "copy": ["config.mjs", "resources.mjs", "processes.mjs", "identity.mjs", "storage.mjs", "command.mjs", "supervisor.mjs", "index.mjs", "cli.mjs"],
8
+ "requires": ["node-22-or-newer", "macos-or-linux", "local-atomic-filesystem", "shared-owner-state-directory"],
9
+ "status": "Machine-wide development admission and process-group cleanup; rer verbs can adopt the queue independently"
10
+ }
@@ -0,0 +1,29 @@
1
+ import { createServer,createConnection } from 'node:net';
2
+ import { chmod,unlink } from 'node:fs/promises';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { identity,alive } from './identity.mjs';
5
+ import { descendantOf } from './membership.mjs';
6
+
7
+ async function inherited(value) {
8
+ if(!value)return null;
9
+ try {
10
+ const claim=JSON.parse(value);
11
+ if(!await alive(claim.owner)||!await descendantOf(claim.owner.pid))return null;
12
+ return await new Promise((resolve,reject)=>{
13
+ const socket=createConnection(claim.path);let text='';
14
+ socket.setTimeout(3000,()=>socket.destroy(Error('Policy probe timed out')));
15
+ socket.on('data',data=>{text+=data;if(text.length>4096)socket.destroy();});
16
+ socket.on('error',reject);socket.on('end',()=>{try{const policy=JSON.parse(text);resolve(policy.token===claim.token?policy:null);}catch{resolve(null);}});
17
+ });
18
+ }catch{return null;}
19
+ }
20
+ export async function callerPolicy(env=process.env) {
21
+ const value=env.RER_JOB_POLICY, policy=await inherited(value);
22
+ if(policy)return {ci:policy.ci,environment:{RER_JOB_POLICY:value},close:async()=>{}};
23
+ const lifetime=await identity(),token=randomUUID(),path=`/tmp/rer-policy-${token}.sock`;
24
+ // An invalid inherited claim fails closed even when the child changes CI.
25
+ const ci=!value&&env.CI==='true';
26
+ const server=createServer(socket=>socket.end(JSON.stringify({token,ci})));
27
+ await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(path,resolve);});await chmod(path,0o600);
28
+ return {ci,environment:{RER_JOB_POLICY:JSON.stringify({path,token,owner:lifetime.info})},async close(){await new Promise(resolve=>server.close(resolve));await unlink(path).catch(error=>{if(error.code!=='ENOENT')throw error;});await lifetime.close();}};
29
+ }
@@ -0,0 +1,80 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { execFile } from 'node:child_process';
3
+ import { promisify } from 'node:util';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ const execute = promisify(execFile);
6
+
7
+ export async function processInfo(pid) {
8
+ if (!Number.isInteger(pid) || pid < 1) throw Error('Invalid process PID');
9
+ if (process.platform === 'linux') {
10
+ try {
11
+ const [stat, boot, command] = await Promise.all([readFile(`/proc/${pid}/stat`, 'utf8'),
12
+ readFile('/proc/sys/kernel/random/boot_id', 'utf8'), readFile(`/proc/${pid}/cmdline`, 'utf8')]);
13
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
14
+ return { pid, start: `${boot.trim()}:${fields[19]}`, zombie: fields[0] === 'Z',
15
+ title: command.replace(/\0/g, ' ').trim() };
16
+ } catch (error) { if (error.code === 'ENOENT' || error.code === 'ESRCH') return null; throw error; }
17
+ }
18
+ if (process.platform !== 'darwin') throw Error('Job queue supports macOS and Linux');
19
+ try {
20
+ const { stdout } = await execute('/bin/ps', ['-p', String(pid), '-o', 'lstart=', '-o', 'stat=', '-o', 'command='],
21
+ { env: { ...process.env, LC_ALL: 'C', TZ: 'UTC' } });
22
+ const match = stdout.trim().match(/^(.{24})\s+(\S+)\s+(.*)$/);
23
+ if (!match) throw Error(`Cannot inspect process ${pid}`);
24
+ return { pid, start: match[1], zombie: match[2].startsWith('Z'), title: match[3] };
25
+ } catch (error) { if (error.code === 1) return null; throw error; }
26
+ }
27
+
28
+ export function signalGroup(pgid, signal) {
29
+ try { process.kill(-pgid, signal); return true; }
30
+ catch (error) { if (error.code === 'ESRCH') return false; throw error; }
31
+ }
32
+
33
+ async function groupLive(pgid) {
34
+ if (process.platform === 'darwin') {
35
+ const { stdout } = await execute('/bin/ps', ['-A', '-o', 'pgid=', '-o', 'stat=']);
36
+ // A member already exiting (stat flag E) cannot be signalled and is ending,
37
+ // like a zombie; killpg answers EPERM for a group of only such members.
38
+ return stdout.trim().split('\n').some(line => {
39
+ const [group, state] = line.trim().split(/\s+/);
40
+ return Number(group) === pgid && !state.startsWith('Z') && !state.includes('E');
41
+ });
42
+ }
43
+ for (const name of (await readdir('/proc')).filter(name => /^\d+$/.test(name))) {
44
+ try {
45
+ const stat = await readFile(`/proc/${name}/stat`, 'utf8');
46
+ const fields = stat.slice(stat.lastIndexOf(')') + 2).split(' ');
47
+ if (Number(fields[2]) === pgid && fields[0] !== 'Z') return true;
48
+ } catch (error) { if (!['ENOENT', 'ESRCH'].includes(error.code)) throw error; }
49
+ }
50
+ return false;
51
+ }
52
+
53
+ export async function deliverGroupSignal(pgid, signal, { send = signalGroup, live = groupLive } = {}) {
54
+ try { return send(pgid, signal); }
55
+ catch (error) {
56
+ // Darwin excludes zombies in killpg and can return EPERM for a stopped group.
57
+ // Completion requires independent evidence; a live/unknown group stays held.
58
+ if (error.code === 'EPERM' && !await live(pgid)) return false;
59
+ throw error;
60
+ }
61
+ }
62
+
63
+ // A group ID cannot be reused while the old group has surviving members (POSIX).
64
+ // A live replacement leader is never signalled; both platforms check its unique title.
65
+ export async function stopGroup(owner, graceMs) {
66
+ if (!owner?.pgid) return;
67
+ if (!Number.isInteger(owner.pgid) || owner.pgid < 2 || owner.pgid !== owner.pid)
68
+ throw Error('Invalid lease process group');
69
+ const current = await processInfo(owner.pid);
70
+ if (current && !current.zombie && (current.start !== owner.start
71
+ || (owner.marker && current.title && !current.title.includes(owner.marker)))) return;
72
+ if (await deliverGroupSignal(owner.pgid, 'SIGTERM')) {
73
+ await delay(graceMs);
74
+ // Check again immediately before escalation, including the recycled PID case.
75
+ const after = await processInfo(owner.pid);
76
+ if (after && !after.zombie && (after.start !== owner.start
77
+ || (owner.marker && after.title && !after.title.includes(owner.marker)))) return;
78
+ await deliverGroupSignal(owner.pgid, 'SIGKILL');
79
+ }
80
+ }
@@ -0,0 +1,37 @@
1
+ import { loadavg, freemem } from 'node:os';
2
+ import { readFileSync } from 'node:fs';
3
+ import { execFileSync } from 'node:child_process';
4
+
5
+ // macOS keeps little memory "free" on purpose: free pages are the last thing it
6
+ // holds. Inactive, speculative and purgeable pages are reclaimed on demand, so
7
+ // the memory a new job can actually use is their sum with the free pages.
8
+ // (Purgeable pages can already sit in the inactive count; they are a small
9
+ // share, so the overlap only makes the reading slightly generous.)
10
+ export function parseVmStat(text) {
11
+ const size = Number(/page size of (\d+) bytes/.exec(text)?.[1]);
12
+ const pages = name => Number(new RegExp(`^Pages ${name}:\\s+(\\d+)\\.`, 'm').exec(text)?.[1]);
13
+ const total = ['free', 'inactive', 'speculative', 'purgeable'].map(pages);
14
+ if (!Number.isInteger(size) || total.some(value => !Number.isFinite(value))) return NaN;
15
+ return total.reduce((sum, value) => sum + value, 0) * size;
16
+ }
17
+
18
+ // Linux reports the same idea directly as MemAvailable, in kibibytes.
19
+ export function parseMemAvailable(text) {
20
+ const kib = Number(/^MemAvailable:\s+(\d+) kB/m.exec(text)?.[1]);
21
+ return Number.isFinite(kib) ? kib * 1024 : NaN;
22
+ }
23
+
24
+ export function availableMemory() {
25
+ try {
26
+ if (process.platform === 'darwin') return parseVmStat(execFileSync('/usr/bin/vm_stat', { encoding: 'utf8', timeout: 3000 }));
27
+ if (process.platform === 'linux') return parseMemAvailable(readFileSync('/proc/meminfo', 'utf8'));
28
+ return freemem();
29
+ } catch { return NaN; }
30
+ }
31
+
32
+ // Built-in OS counters; injection belongs to library tests, never CLI admission.
33
+ // freeMemoryBytes is the raw OS free counter, kept for display only: admission
34
+ // uses availableMemoryBytes.
35
+ export function sampleResources() {
36
+ return { load: loadavg()[0], freeMemoryBytes: freemem(), availableMemoryBytes: availableMemory() };
37
+ }
@@ -0,0 +1,64 @@
1
+ import { mkdir, readFile, writeFile, rename, readdir, unlink, link } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { alive } from './identity.mjs';
6
+
7
+ export async function atomicJson(path, value) {
8
+ const temporary = `${path}.${randomUUID()}.tmp`;
9
+ try {
10
+ await writeFile(temporary, JSON.stringify(value), { flag: 'wx', mode: 0o600 });
11
+ await rename(temporary, path);
12
+ } finally { await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error; }); }
13
+ }
14
+
15
+ export async function readJson(path, fallback) {
16
+ try { return JSON.parse(await readFile(path, 'utf8')); }
17
+ catch (error) { if (error.code === 'ENOENT') return fallback; throw error; }
18
+ }
19
+
20
+ // Lamport's bakery lock: publish choosing before reading tickets, then wait for
21
+ // every earlier (number,id). Unique records avoid the stale-unlink/new-lock race
22
+ // of a single reclaimable lock file. Every publication is atomic and complete.
23
+ export async function transaction(directory, owner, action) {
24
+ const locks = join(directory, 'locks');
25
+ await mkdir(locks, { recursive: true, mode: 0o700 });
26
+ const id = randomUUID(), path = join(locks, `${id}.json`);
27
+ const candidate = `${path}.tmp`;
28
+ await writeFile(candidate, JSON.stringify({ id, owner, choosing: true, number: 0 }), { flag: 'wx', mode: 0o600 });
29
+ try { await link(candidate, path); } finally { await unlink(candidate); }
30
+ const proofs = new Map();
31
+ async function liveOwner(record) {
32
+ const key = `${record.owner.token}:${record.owner.pid}:${record.owner.start}`;
33
+ const previous = proofs.get(key);
34
+ // A briefly cached live proof only delays reclamation; it cannot admit an
35
+ // extra writer. Unique lifetime tokens cannot become live again after death.
36
+ if (previous && Date.now() - previous.at < 200) return previous.live;
37
+ const live = await alive(record.owner);
38
+ proofs.set(key, { live, at: Date.now() });
39
+ return live;
40
+ }
41
+ async function participants() {
42
+ const records = await Promise.all((await readdir(locks)).filter(name => name.endsWith('.json'))
43
+ .map(name => readJson(join(locks, name), null)));
44
+ const live = [];
45
+ for (const record of records) {
46
+ if (!record) continue;
47
+ if (record.id === id || await liveOwner(record)) live.push(record);
48
+ else await unlink(join(locks, `${record.id}.json`)).catch(error => { if (error.code !== 'ENOENT') throw error; });
49
+ }
50
+ return live;
51
+ }
52
+ try {
53
+ const number = Math.max(0, ...(await participants()).map(record => record.number)) + 1;
54
+ await atomicJson(path, { id, owner, choosing: false, number });
55
+ while ((await participants()).some(record => record.id !== id && (record.choosing
56
+ || record.number < number || (record.number === number && record.id < id)))) await delay(20);
57
+ const statePath = join(directory, 'state.json');
58
+ const state = await readJson(statePath, { version: 1, sequence: 0, queued: [], running: [] });
59
+ if (state.version !== 1 || !Array.isArray(state.queued) || !Array.isArray(state.running)) throw Error('Invalid job queue state');
60
+ const result = await action(state);
61
+ await atomicJson(statePath, state);
62
+ return result;
63
+ } finally { await unlink(path).catch(error => { if (error.code !== 'ENOENT') throw error; }); }
64
+ }
@@ -0,0 +1,30 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { identity } from './identity.mjs';
3
+ import { signalGroup } from './processes.mjs';
4
+
5
+ const marker = `rer-job-supervisor-${process.argv[2]}`;
6
+ process.title = marker;
7
+ let stopping = false, started = false;
8
+ function stop() {
9
+ if (stopping) return;
10
+ stopping = true;
11
+ // Remain group leader until escalation: this prevents group-ID reuse while
12
+ // cleanup is in progress and allows the caller to safely finish cancellation.
13
+ signalGroup(process.pid, 'SIGTERM');
14
+ setTimeout(() => signalGroup(process.pid, 'SIGKILL'), Math.max(20, Number(process.argv[3])));
15
+ }
16
+ process.on('SIGINT', stop);
17
+ process.on('SIGTERM', stop);
18
+ process.on('disconnect', stop);
19
+ const lifetime = await identity();
20
+ process.send({ ready: { ...lifetime.info, marker, pgid: process.pid } });
21
+ process.on('message', message => {
22
+ if (started || stopping || !message.command) return;
23
+ started = true;
24
+ const child = spawn('/usr/bin/nice', ['-n', String(message.nice), ...message.command],
25
+ { cwd: message.cwd, env: { ...process.env, RER_JOB_MEMBERSHIP: message.membership }, stdio: 'inherit' });
26
+ child.once('error', error => process.send?.({ done: { code: 127, error: error.message } }));
27
+ child.once('exit', (code, signal) => {
28
+ if (process.connected) process.send({ done: { code, signal } });
29
+ });
30
+ });
@@ -0,0 +1,16 @@
1
+ import { GIB } from './config.mjs';
2
+
3
+ const gib = bytes => `${(bytes / GIB).toFixed(1)} GiB`;
4
+
5
+ // What a queued job tells the person: its place, who is ahead of it (with their
6
+ // checkout paths), memory against the floor, and how to change that floor.
7
+ export function waitingMessage(wait) {
8
+ const lines = [`Waiting to start (queue position ${wait.position}/${wait.queued}): ${wait.reason}.`];
9
+ if (wait.ahead?.length) {
10
+ lines.push(`Ahead of this job:`);
11
+ for (const job of wait.ahead) lines.push(` ${job.state}: ${job.label} in ${job.repository}`);
12
+ }
13
+ if (Number.isFinite(wait.resources?.availableMemoryBytes))
14
+ lines.push(`Memory: ${gib(wait.resources.availableMemoryBytes)} available, floor ${gib(wait.limits.minFreeMemoryBytes)}. Change the floor with RER_JOBS_MIN_FREE_MEMORY_BYTES (bytes; 0 for no floor).`);
15
+ return lines.join('\n');
16
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Run Edit Run
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.