rollbridge 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.
@@ -0,0 +1,39 @@
1
+ # AI Prompt For Rollbridge TODO Work
2
+
3
+ Use this prompt when asking an AI agent to implement one or more isolated Rollbridge TODO items.
4
+
5
+ ```text
6
+ You are working in the Rollbridge repository.
7
+
8
+ Goal:
9
+ Pick one or more isolated unchecked items from TODO.md and implement them end to end. Prefer one focused item unless several TODOs are tightly coupled and can be completed cleanly in the same change.
10
+
11
+ Before changing files:
12
+ 1. Read README.md, TODO.md, package.json, and the source files related to the chosen TODO item.
13
+ 2. State the exact TODO item(s) you are taking.
14
+ 3. Confirm the item is isolated. Do not combine unrelated roadmap items.
15
+
16
+ Scope rules:
17
+ - Keep the diff focused on the selected TODO item(s).
18
+ - Do not add Capistrano plugins, Capistrano tasks, or Capistrano-specific integration code. Deploy tools should call Rollbridge through CLI commands.
19
+ - Capistrano documentation examples are allowed only as shell-command recipes that invoke Rollbridge CLI commands.
20
+ - Do not change runtime behavior for documentation-only TODOs.
21
+ - Do not mark a TODO checkbox complete unless the feature or document is actually finished and validated.
22
+ - If the work changes public config, CLI output, command names, or operational behavior, update README.md or dedicated docs in the same change.
23
+
24
+ Implementation expectations:
25
+ - Follow existing Rollbridge patterns and JSDoc style.
26
+ - Keep config validation at the boundary and trust normalized config downstream.
27
+ - Add behavior-focused tests for new process, daemon, CLI, config, or proxy behavior.
28
+ - For worker or memory supervision work, include status output and tests covering failure/restart behavior.
29
+ - For docs work, include concrete commands and examples that an operator can use.
30
+
31
+ Suggested acceptance criteria:
32
+ - The selected TODO item(s) are implemented or documented fully.
33
+ - Relevant TODO.md checkbox(es) are updated.
34
+ - npm run all-checks passes.
35
+ - The final response summarizes changed behavior, docs, tests, and any remaining follow-up work.
36
+
37
+ If the TODO item is too large:
38
+ Split it into a smaller independently shippable subtask, document the boundary in TODO.md, and implement only that subtask.
39
+ ```
package/README.md ADDED
@@ -0,0 +1,200 @@
1
+ # Rollbridge
2
+
3
+ Rollbridge is a Node.js process supervisor and local traffic switcher for zero-downtime deploys.
4
+
5
+ Nginx points at one stable Rollbridge proxy port. Deploy tooling asks Rollbridge to start a new release, health-check it, switch new traffic to it, and drain old HTTP/WebSocket connections before stopping the previous release.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install rollbridge
11
+ ```
12
+
13
+ For local development in this repository:
14
+
15
+ ```bash
16
+ npm install
17
+ npm run all-checks
18
+ ```
19
+
20
+ ## Config
21
+
22
+ A Rollbridge config is a JavaScript module that `export default`s a config
23
+ object. It can also export a function (sync or async) that returns the object,
24
+ which is handy for computing values from the environment. Write it in your
25
+ project's module system — `export default` for ESM (`"type": "module"`) or
26
+ `module.exports` for CommonJS.
27
+
28
+ ```js
29
+ // rollbridge.js
30
+ export default {
31
+ application: "ticket-server",
32
+
33
+ control: {
34
+ path: "/tmp/rollbridge-ticket-server.sock"
35
+ },
36
+
37
+ proxy: {
38
+ host: "127.0.0.1",
39
+ port: 8182,
40
+ healthPath: "/ping",
41
+ healthTimeoutMs: 30000,
42
+ drainTimeoutMs: 60000,
43
+ forceStopTimeoutMs: 10000
44
+ },
45
+
46
+ processes: [
47
+ {
48
+ id: "beacon",
49
+ policy: "companion",
50
+ cwd: "{{releasePath}}",
51
+ command: "env VELOCIOUS_BEACON_PORT={{port}} npx velocious beacon",
52
+ port: {from: 17330, to: 17399}
53
+ },
54
+ {
55
+ id: "background-jobs-worker",
56
+ policy: "companion",
57
+ cwd: "{{releasePath}}",
58
+ command: "npx velocious background-jobs-worker"
59
+ },
60
+ {
61
+ id: "background-jobs-main",
62
+ policy: "service",
63
+ cwd: "{{releasePath}}",
64
+ command: "npx velocious background-jobs-main"
65
+ },
66
+ {
67
+ id: "web",
68
+ policy: "proxied",
69
+ cwd: "{{releasePath}}",
70
+ command: "npx velocious server --host 127.0.0.1 --port {{port}}",
71
+ port: {from: 18182, to: 18299},
72
+ health: {path: "/ping", timeoutMs: 30000}
73
+ }
74
+ ]
75
+ }
76
+ ```
77
+
78
+ A function export receives no arguments and lets you build the config at load
79
+ time:
80
+
81
+ ```js
82
+ // rollbridge.js
83
+ export default () => ({
84
+ application: process.env.APP_NAME || "ticket-server",
85
+ control: {path: `/tmp/rollbridge-${process.env.APP_NAME || "ticket-server"}.sock`},
86
+ proxy: {host: "127.0.0.1", port: 8182},
87
+ processes: [
88
+ {id: "web", policy: "proxied", cwd: "{{releasePath}}", command: "npx velocious server --port {{port}}", port: {from: 18182, to: 18299}}
89
+ ]
90
+ })
91
+ ```
92
+
93
+ Production-ready examples live in `examples/`, including
94
+ `examples/tensorbuzz.com.js` for the current TensorBuzz backend deployment.
95
+
96
+ ## Process Policies
97
+
98
+ - `proxied`: the web/API process. Rollbridge forwards HTTP and WebSocket traffic to the active release and tracks connections for draining.
99
+ - `companion`: a release-scoped support process. It starts with the release and stops after that release drains.
100
+ - `singleton`: a one-at-a-time support process. Rollbridge stops the old singleton before starting the new one, so duplicate-unsafe schedulers or job dispatchers do not overlap.
101
+ - `service`: a daemon-wide support process. Rollbridge starts it before release processes need it, leaves it running across deploys, and updates its restart template after a successful deploy.
102
+
103
+ ## Commands
104
+
105
+ `--config` is optional for every command. When omitted, Rollbridge looks for
106
+ `rollbridge.js` in the current directory. The examples below pass `--config`
107
+ explicitly, but `rollbridge validate` (or any command) works with no flag when a
108
+ `rollbridge.js` is present.
109
+
110
+ Validate a config without starting the daemon:
111
+
112
+ ```bash
113
+ rollbridge validate --config rollbridge.js
114
+ ```
115
+
116
+ `validate` reports every config error at once with an example fix and exits
117
+ non-zero when issues are found, so deploy tooling can gate on it. It checks
118
+ required fields and types, duplicate process IDs, port ranges, that exactly one
119
+ process is `proxied`, and that the proxied process defines a port range. Example
120
+ output for a misconfigured file:
121
+
122
+ ```text
123
+ Found 2 configuration issues in rollbridge.js:
124
+
125
+ 1. Config must define exactly one proxied process; found 0
126
+ Fix: Mark exactly one process with policy: proxied so Rollbridge knows where to forward traffic.
127
+
128
+ 2. Duplicate process id: web
129
+ Fix: Give each process a unique id; "web" is used more than once.
130
+ ```
131
+
132
+ Start the daemon:
133
+
134
+ ```bash
135
+ rollbridge daemon --config rollbridge.js
136
+ ```
137
+
138
+ Start the daemon only when it is not already running:
139
+
140
+ ```bash
141
+ rollbridge ensure-daemon --config rollbridge.js --daemon-log-path log/rollbridge.log --daemon-pid-path tmp/pids/rollbridge.pid
142
+ ```
143
+
144
+ Deploy a prepared release:
145
+
146
+ ```bash
147
+ rollbridge deploy --config rollbridge.js --release-path /home/dev/ticket-server/releases/20260521073000/ticket-server --revision abc123
148
+ ```
149
+
150
+ Deploy and start the daemon first when needed:
151
+
152
+ ```bash
153
+ rollbridge deploy --ensure-daemon --config rollbridge.js --release-path /home/dev/ticket-server/releases/20260521073000/ticket-server --revision abc123
154
+ ```
155
+
156
+ Inspect state:
157
+
158
+ ```bash
159
+ rollbridge status --config rollbridge.js
160
+ ```
161
+
162
+ Stop the active release:
163
+
164
+ ```bash
165
+ rollbridge stop --config rollbridge.js
166
+ ```
167
+
168
+ Shut down the daemon and managed processes:
169
+
170
+ ```bash
171
+ rollbridge shutdown --config rollbridge.js
172
+ ```
173
+
174
+ ## Nginx
175
+
176
+ Nginx should proxy to Rollbridge, not directly to Velocious:
177
+
178
+ ```nginx
179
+ location / {
180
+ proxy_pass http://127.0.0.1:8182;
181
+ proxy_http_version 1.1;
182
+ proxy_set_header Upgrade $http_upgrade;
183
+ proxy_set_header Connection "upgrade";
184
+ proxy_set_header Host $host;
185
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
186
+ proxy_set_header X-Forwarded-Proto $scheme;
187
+ }
188
+ ```
189
+
190
+ ## Deployment Notes
191
+
192
+ Run migrations before `rollbridge deploy`, and keep migrations backwards-compatible while old and new web releases overlap. For stable local brokers such as Velocious Beacon or `background-jobs-main`, use `service` when the process should survive deploys and restart from the latest successful release if it crashes.
193
+
194
+ ## Releasing
195
+
196
+ Maintainers can publish a patch release from the latest default branch:
197
+
198
+ ```bash
199
+ npm run release:patch
200
+ ```
package/TODO.md ADDED
@@ -0,0 +1,96 @@
1
+ # Rollbridge TODO
2
+
3
+ This roadmap tracks planned Rollbridge features and documentation. Rollbridge should stay deploy-tool agnostic: Capistrano, shell scripts, CI, or other deploy systems should call Rollbridge through CLI commands instead of Rollbridge shipping Capistrano tasks or plugins.
4
+
5
+ ## Current Baseline
6
+
7
+ - [x] Stable local HTTP/WebSocket proxy in front of one active release.
8
+ - [x] Unix socket control API used by the CLI.
9
+ - [x] `daemon`, `ensure-daemon`, `deploy`, `status`, `stop`, and `shutdown` commands.
10
+ - [x] Per-release process startup with templated `releasePath`, `releaseId`, `revision`, and ports.
11
+ - [x] Process policies for `proxied`, `companion`, `singleton`, and `service`.
12
+ - [x] HTTP health check before switching traffic to a new web process.
13
+ - [x] Drain old HTTP/WebSocket connections before stopping previous release processes.
14
+ - [x] Restart crashed active release processes after `restartDelayMs`.
15
+ - [x] Restart crashed daemon-wide service processes from the latest successful release template.
16
+ - [x] Graceful `SIGTERM` followed by `SIGKILL` after timeout.
17
+ - [x] TensorBuzz production example config.
18
+ - [x] ESLint, TypeScript `checkJs`, TensorBuzz CI, and `release:patch`.
19
+
20
+ ## Major Features
21
+
22
+ - [ ] Memory supervision.
23
+ - [ ] Add per-process memory config with an RSS limit, check interval, warning threshold, and restart policy.
24
+ - [ ] Measure the managed process tree, not only the shell wrapper PID.
25
+ - [ ] Report memory stats and last memory-triggered restart in `status`.
26
+ - [ ] Restart memory-heavy workers gracefully when possible, with a forced stop timeout.
27
+ - [ ] Add tests with a fixture process that allocates memory above the configured limit.
28
+ - [ ] Worker auto-restart and restart policy controls.
29
+ - [ ] Add config for max restarts, restart window, exponential backoff, and disabled restart behavior.
30
+ - [ ] Distinguish crash restarts, deploy replacements, manual restarts, and memory restarts in status/events.
31
+ - [ ] Add a `restart` CLI command for a single process, a policy group, or all non-proxied workers.
32
+ - [ ] Keep restart behavior safe for job workers by using lifecycle hooks before termination.
33
+ - [ ] Graceful job-worker lifecycle.
34
+ - [ ] Add generic lifecycle hooks such as `quietCommand`, `drainCommand`, `drainTimeoutMs`, and `stopCommand`.
35
+ - [ ] Support signal-only lifecycle steps for workers that can quiet on a Unix signal.
36
+ - [ ] Add a non-blocking drain mode so new workers can start while old workers finish running jobs.
37
+ - [ ] Document a Velocious background-jobs-worker recipe once the lifecycle contract is implemented.
38
+ - [ ] Replicas and stable worker indexes.
39
+ - [ ] Allow one process config to start multiple replicas.
40
+ - [ ] Expose `ROLLBRIDGE_REPLICA_INDEX`, replica count, and per-replica template context.
41
+ - [ ] Restart or stop one replica without affecting the rest.
42
+ - [ ] Preserve readable status output for replica groups.
43
+ - [ ] Persistent daemon state and recovery.
44
+ - [ ] Persist active release, draining releases, process metadata, counters, and recent events.
45
+ - [ ] Reconnect status to still-running child processes after daemon restart where possible.
46
+ - [ ] Detect and report orphaned Rollbridge-managed processes.
47
+ - [ ] Add a recovery mode for safe startup after daemon crash or machine reboot.
48
+ - [ ] Rollback support.
49
+ - [ ] Keep enough release metadata to switch traffic back to a previous healthy release.
50
+ - [ ] Add a `rollback` CLI command that health-checks the target before switching.
51
+ - [ ] Define how rollback interacts with singleton workers and draining releases.
52
+ - [ ] Document migration constraints for rollback.
53
+ - [ ] Observability and diagnostics.
54
+ - [ ] Add structured event history for deploys, switches, stops, crashes, memory restarts, and failed commands.
55
+ - [ ] Add restart counters, uptime, exit reasons, memory stats, and child-process-tree details to status.
56
+ - [ ] Add `logs` and `events` CLI commands.
57
+ - [ ] Add optional file logging with rotation guidance.
58
+ - [ ] Add machine-readable JSON output flags for all CLI commands.
59
+ - [ ] Config validation and doctoring.
60
+ - [x] Add `validate` to parse config and report all config errors without starting the daemon.
61
+ - [ ] Add `doctor` to check control socket reachability, proxy port availability, process commands, release path, and writable log/state paths.
62
+ - [x] Validate duplicate process IDs, missing ports on proxied processes, invalid ranges, and the single-proxied-process policy rule.
63
+ - [ ] Validate unsupported lifecycle-hook combinations once worker lifecycle hooks land.
64
+ - [x] Include example fixes in validation output.
65
+
66
+ ## Minor Features
67
+
68
+ - [ ] Add socket permission and socket owner/group options for shared deploy users.
69
+ - [x] Make stale control socket diagnostics clearer when another daemon is still alive.
70
+ - [ ] Add old-release cleanup policies by age, count, and stopped state.
71
+ - [x] Add port allocation diagnostics when a range is exhausted.
72
+ - [ ] Add optional startup command timeout before health checks begin.
73
+ - [ ] Add process output retention config instead of a fixed recent-log count.
74
+ - [ ] Add environment variable interpolation from the daemon environment.
75
+ - [x] Add `--config` default lookup resolving to `rollbridge.js` when no path is given.
76
+ - [ ] Add shell completion generation for common shells.
77
+ - [ ] Add npm package metadata such as repository, license, bugs, and homepage.
78
+ - [ ] Add systemd service examples for the Rollbridge daemon.
79
+ - [ ] Add tests for malformed control socket JSON and unknown control commands.
80
+ - [ ] Add tests for duplicate IDs and singleton replacement failure behavior.
81
+ - [ ] Add tests for proxy behavior when the active release exits unexpectedly.
82
+
83
+ ## Documentation TODO
84
+
85
+ - [ ] Write a full config reference covering every field, default, and template variable.
86
+ - [ ] Write a CLI reference for `daemon`, `ensure-daemon`, `deploy`, `status`, `stop`, `shutdown`, and future commands.
87
+ - [ ] Expand process policy docs with deployment examples for `proxied`, `companion`, `singleton`, and `service`.
88
+ - [ ] Document memory checks and auto-restart behavior after the feature lands.
89
+ - [ ] Document worker lifecycle hooks and safe background-job deployment patterns after the feature lands.
90
+ - [ ] Add a Velocious deployment guide with Beacon, background-jobs-main, background-jobs-worker, and web process examples.
91
+ - [ ] Add an Nginx guide with WebSocket headers, timeouts, and common failure modes.
92
+ - [ ] Add deploy-tool recipes that call Rollbridge CLI commands directly.
93
+ - [ ] Add a Capistrano recipe showing shell commands only; do not add a Capistrano plugin or Rollbridge-specific Capistrano tasks.
94
+ - [ ] Add a TensorBuzz-specific runbook for current production ports, external services, deploy ordering, and rollback constraints.
95
+ - [ ] Add troubleshooting docs for health-check failures, port conflicts, stale sockets, crash loops, and stuck draining releases.
96
+ - [ ] Add a release checklist for maintainers using `npm run release:patch`.
package/bin/rollbridge ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import {runCli} from "../src/cli.js"
3
+
4
+ await runCli(process.argv)
@@ -0,0 +1,99 @@
1
+ import js from "@eslint/js"
2
+ import {jsdoc} from 'eslint-plugin-jsdoc'
3
+ import globals from "globals"
4
+ import { defineConfig } from "eslint/config"
5
+
6
+ /**
7
+ * Extracts the brace-balanced type expressions that follow JSDoc tags in a comment body.
8
+ * @param {string} commentValue The raw comment text between the comment delimiters.
9
+ * @returns {string[]} Each `{...}` type expression, without its surrounding braces.
10
+ */
11
+ function jsdocTypeExpressions(commentValue) {
12
+ const types = []
13
+ const tagWithType = /@\w+\s*\{/g
14
+ let match
15
+
16
+ while ((match = tagWithType.exec(commentValue))) {
17
+ let depth = 0
18
+ let start = -1
19
+
20
+ for (let index = match.index + match[0].length - 1; index < commentValue.length; index++) {
21
+ const character = commentValue[index]
22
+
23
+ if (character === "{") {
24
+ if (depth === 0) start = index
25
+ depth++
26
+ } else if (character === "}") {
27
+ depth--
28
+
29
+ if (depth === 0) {
30
+ types.push(commentValue.slice(start + 1, index))
31
+ break
32
+ }
33
+ }
34
+ }
35
+ }
36
+
37
+ return types
38
+ }
39
+
40
+ // eslint-plugin-jsdoc rejects `any` everywhere (jsdoc/reject-any-type) but only catches `unknown`
41
+ // at the top level, so this local rule rejects `unknown` anywhere inside a JSDoc type expression.
42
+ const localPlugin = {
43
+ rules: {
44
+ "no-unknown-jsdoc-type": {
45
+ meta: {
46
+ type: "problem",
47
+ docs: {description: "Disallow the `unknown` type anywhere in JSDoc type annotations."},
48
+ messages: {noUnknown: "Avoid the unknown type in JSDoc; use a specific type such as JsonValue (src/json.d.ts) instead."},
49
+ schema: []
50
+ },
51
+ create(context) {
52
+ return {
53
+ Program() {
54
+ for (const comment of context.sourceCode.getAllComments()) {
55
+ if (comment.type !== "Block" || !comment.value.startsWith("*")) continue
56
+
57
+ const hasUnknown = jsdocTypeExpressions(comment.value).some((type) => /\bunknown\b/.test(type))
58
+
59
+ if (hasUnknown) context.report({loc: comment.loc, messageId: "noUnknown"})
60
+ }
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+ }
67
+
68
+ export default defineConfig([
69
+ {
70
+ name: "global ignores",
71
+ ignores: ["build/**", "dist/**"],
72
+ },
73
+ {
74
+ files: ["**/*.{js,mjs,cjs}", "**/bin/rollbridge"],
75
+ plugins: {js},
76
+ extends: ["js/recommended"],
77
+ languageOptions: {
78
+ globals: {...globals.browser, ...globals.node}
79
+ },
80
+ rules: {
81
+ "no-unused-vars": ["error", {argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_"}]
82
+ }
83
+ },
84
+ jsdoc({
85
+ config: "flat/recommended",
86
+ files: ["**/*.{js,mjs,cjs}", "**/bin/rollbridge"],
87
+ rules: {
88
+ "jsdoc/reject-any-type": "error"
89
+ }
90
+ }),
91
+ {
92
+ name: "local jsdoc type rules",
93
+ files: ["**/*.{js,mjs,cjs}", "**/bin/rollbridge"],
94
+ plugins: {local: localPlugin},
95
+ rules: {
96
+ "local/no-unknown-jsdoc-type": "error"
97
+ }
98
+ }
99
+ ])
@@ -0,0 +1,85 @@
1
+ // Rollbridge config for the TensorBuzz production backend.
2
+ //
3
+ // Nginx should keep proxying the backend host to 127.0.0.1:4500. Rollbridge
4
+ // binds that stable HTTP port, forwards to the active release's internal web
5
+ // port, keeps Beacon/jobs-main as daemon-wide services, and starts one
6
+ // background worker per active release.
7
+
8
+ export default {
9
+ application: "tensorbuzz",
10
+
11
+ control: {
12
+ path: "/tmp/rollbridge-tensorbuzz.sock"
13
+ },
14
+
15
+ proxy: {
16
+ host: "127.0.0.1",
17
+ port: 4500,
18
+ healthPath: "/ping",
19
+ healthTimeoutMs: 30000,
20
+ drainTimeoutMs: 60000,
21
+ forceStopTimeoutMs: 10000
22
+ },
23
+
24
+ processes: [
25
+ {
26
+ id: "beacon",
27
+ policy: "service",
28
+ cwd: "{{releasePath}}/backend",
29
+ env: {
30
+ NODE_ENV: "production",
31
+ VELOCIOUS_ENV: "production",
32
+ VELOCIOUS_BEACON_PORT: "{{port}}"
33
+ },
34
+ command: "npx velocious beacon",
35
+ port: 7330
36
+ },
37
+ {
38
+ id: "background-jobs-main",
39
+ policy: "service",
40
+ cwd: "{{releasePath}}/backend",
41
+ env: {
42
+ NODE_ENV: "production",
43
+ VELOCIOUS_ENV: "production",
44
+ VELOCIOUS_BEACON_PORT: "{{ports.beacon}}",
45
+ VELOCIOUS_BACKGROUND_JOBS_PORT: "{{port}}"
46
+ },
47
+ command: "wait-for-it 127.0.0.1:{{ports.beacon}} --strict -- npx velocious background-jobs-main",
48
+ port: 7331
49
+ },
50
+ {
51
+ id: "background-jobs-worker",
52
+ policy: "companion",
53
+ cwd: "{{releasePath}}/backend",
54
+ env: {
55
+ NODE_ENV: "production",
56
+ VELOCIOUS_ENV: "production",
57
+ VELOCIOUS_BEACON_PORT: "{{ports.beacon}}",
58
+ VELOCIOUS_BACKGROUND_JOBS_PORT: "{{ports.background-jobs-main}}"
59
+ },
60
+ command: "wait-for-it 127.0.0.1:{{ports.beacon}} --strict -- wait-for-it 127.0.0.1:{{ports.background-jobs-main}} --strict -- npx velocious background-jobs-worker",
61
+ gracefulStopMs: 60000
62
+ },
63
+ {
64
+ id: "web",
65
+ policy: "proxied",
66
+ cwd: "{{releasePath}}/backend",
67
+ env: {
68
+ NODE_ENV: "production",
69
+ VELOCIOUS_ENV: "production",
70
+ VELOCIOUS_BEACON_PORT: "{{ports.beacon}}",
71
+ VELOCIOUS_BACKGROUND_JOBS_PORT: "{{ports.background-jobs-main}}"
72
+ },
73
+ command: "wait-for-it 127.0.0.1:{{ports.beacon}} --strict -- wait-for-it 127.0.0.1:{{ports.background-jobs-main}} --strict -- npx velocious server --host 127.0.0.1 --port {{port}}",
74
+ port: {
75
+ from: 14500,
76
+ to: 14599
77
+ },
78
+ health: {
79
+ path: "/ping",
80
+ timeoutMs: 30000,
81
+ intervalMs: 500
82
+ }
83
+ }
84
+ ]
85
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "rollbridge",
3
+ "version": "0.1.1",
4
+ "description": "Zero-downtime process supervisor and local traffic switcher for deploy-managed apps.",
5
+ "type": "module",
6
+ "bin": {
7
+ "rollbridge": "./bin/rollbridge"
8
+ },
9
+ "scripts": {
10
+ "all-checks": "npm run typecheck && npm run lint && npm test",
11
+ "lint": "eslint",
12
+ "release:patch": "node scripts/release-patch.js",
13
+ "test": "node --test test/*.test.js",
14
+ "typecheck": "tsc --noEmit"
15
+ },
16
+ "engines": {
17
+ "node": ">=20"
18
+ },
19
+ "dependencies": {
20
+ "commander": "^12.1.0",
21
+ "http-proxy": "^1.18.1"
22
+ },
23
+ "devDependencies": {
24
+ "@eslint/js": "^10.0.1",
25
+ "@types/http-proxy": "^1.17.17",
26
+ "@types/node": "^25.9.1",
27
+ "eslint": "^10.4.0",
28
+ "eslint-plugin-jsdoc": "^62.9.0",
29
+ "globals": "^17.6.0",
30
+ "typescript": "^6.0.3"
31
+ }
32
+ }
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ import {execFileSync} from "node:child_process"
3
+
4
+ /**
5
+ * Runs a command and inherits stdio.
6
+ * @param {string} command - Command to run.
7
+ * @param {string[]} [args] - Command arguments.
8
+ * @returns {void}
9
+ */
10
+ function run(command, args = []) {
11
+ execFileSync(command, args, {
12
+ env: {
13
+ ...process.env,
14
+ GIT_EDITOR: "true",
15
+ GIT_MERGE_AUTOEDIT: "no"
16
+ },
17
+ stdio: "inherit"
18
+ })
19
+ }
20
+
21
+ /**
22
+ * Runs a command and returns trimmed stdout.
23
+ * @param {string} command - Command to run.
24
+ * @param {string[]} [args] - Command arguments.
25
+ * @returns {string} Trimmed stdout.
26
+ */
27
+ function output(command, args = []) {
28
+ return execFileSync(command, args, {encoding: "utf8"}).trim()
29
+ }
30
+
31
+ /** @returns {string} GitHub remote default branch name. */
32
+ function defaultBranch() {
33
+ const remoteHead = output("git", ["ls-remote", "--symref", "origin", "HEAD"])
34
+ const match = remoteHead.match(/^ref: refs\/heads\/(.+)\s+HEAD$/m)
35
+
36
+ if (!match) throw new Error("Unable to determine origin default branch")
37
+
38
+ return match[1]
39
+ }
40
+
41
+ /**
42
+ * @param {string} branch - Branch name.
43
+ * @returns {boolean} True when the local branch exists.
44
+ */
45
+ function localBranchExists(branch) {
46
+ try {
47
+ output("git", ["rev-parse", "--verify", `refs/heads/${branch}`])
48
+ return true
49
+ } catch (_error) {
50
+ return false
51
+ }
52
+ }
53
+
54
+ /** @returns {string} Updated default branch name. */
55
+ function updateLocalDefaultBranch() {
56
+ run("git", ["fetch", "origin"])
57
+ const branch = defaultBranch()
58
+
59
+ if (localBranchExists(branch)) {
60
+ run("git", ["checkout", branch])
61
+ } else {
62
+ run("git", ["checkout", "-b", branch, `origin/${branch}`])
63
+ }
64
+
65
+ run("git", ["merge", "--ff-only", `origin/${branch}`])
66
+
67
+ return branch
68
+ }
69
+
70
+ try {
71
+ execFileSync("npm", ["whoami"], {stdio: "ignore"})
72
+ } catch {
73
+ run("npm", ["login"])
74
+ }
75
+
76
+ const branch = updateLocalDefaultBranch()
77
+
78
+ run("npm", ["version", "patch", "--no-git-tag-version"])
79
+ run("npm", ["install"])
80
+ run("git", ["add", "package.json", "package-lock.json"])
81
+ run("git", ["commit", "-m", "chore: bump patch version"])
82
+ run("git", ["push", "origin", branch])
83
+ run("npm", ["publish"])