@tridha643/hestia 1.0.0

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,117 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/engine/src/proc/proc-relay.ts
5
+ import {
6
+ chmodSync,
7
+ closeSync,
8
+ existsSync,
9
+ openSync,
10
+ renameSync,
11
+ rmSync,
12
+ statSync,
13
+ writeSync
14
+ } from "fs";
15
+ import { spawn } from "child_process";
16
+ var MAX_LOG_BYTES = 25 * 1024 * 1024;
17
+ var ARCHIVE_COUNT = 3;
18
+ var RELAY_SPEC_ENV = "HESTIA_PROC_RELAY_SPEC";
19
+ function relaySpec() {
20
+ const encoded = process.env[RELAY_SPEC_ENV];
21
+ delete process.env[RELAY_SPEC_ENV];
22
+ if (encoded === undefined)
23
+ throw new Error(`${RELAY_SPEC_ENV} is missing`);
24
+ const value = JSON.parse(Buffer.from(encoded, "base64").toString("utf8"));
25
+ if (!Array.isArray(value.argv) || value.argv.length === 0 || typeof value.cwd !== "string" || typeof value.logPath !== "string") {
26
+ throw new Error("invalid proc relay spec");
27
+ }
28
+ return value;
29
+ }
30
+
31
+ class RotatingLogWriter {
32
+ path;
33
+ maxLogBytes;
34
+ archiveCount;
35
+ #fd;
36
+ #bytes;
37
+ constructor(path, maxLogBytes = MAX_LOG_BYTES, archiveCount = ARCHIVE_COUNT) {
38
+ this.path = path;
39
+ this.maxLogBytes = maxLogBytes;
40
+ this.archiveCount = archiveCount;
41
+ this.#fd = openSync(path, "a", 384);
42
+ chmodSync(path, 384);
43
+ this.#bytes = statSync(path).size;
44
+ }
45
+ write(chunk) {
46
+ let offset = 0;
47
+ while (offset < chunk.byteLength) {
48
+ if (this.#bytes >= this.maxLogBytes)
49
+ this.rotate();
50
+ const length = Math.min(chunk.byteLength - offset, this.maxLogBytes - this.#bytes);
51
+ writeSync(this.#fd, chunk, offset, length);
52
+ offset += length;
53
+ this.#bytes += length;
54
+ }
55
+ }
56
+ close() {
57
+ closeSync(this.#fd);
58
+ }
59
+ rotate() {
60
+ closeSync(this.#fd);
61
+ rmSync(`${this.path}.${this.archiveCount}`, { force: true });
62
+ for (let index = this.archiveCount - 1;index >= 1; index -= 1) {
63
+ const source = `${this.path}.${index}`;
64
+ if (existsSync(source))
65
+ renameSync(source, `${this.path}.${index + 1}`);
66
+ }
67
+ if (existsSync(this.path))
68
+ renameSync(this.path, `${this.path}.1`);
69
+ this.#fd = openSync(this.path, "a", 384);
70
+ chmodSync(this.path, 384);
71
+ this.#bytes = 0;
72
+ }
73
+ }
74
+ function forwardSignal(child, signal) {
75
+ if (child.exitCode !== null || child.signalCode !== null)
76
+ return;
77
+ try {
78
+ child.kill(signal);
79
+ } catch {}
80
+ }
81
+ async function main() {
82
+ const spec = relaySpec();
83
+ const writer = new RotatingLogWriter(spec.logPath);
84
+ const child = spawn(spec.argv[0], spec.argv.slice(1), {
85
+ cwd: spec.cwd,
86
+ env: spec.env,
87
+ stdio: ["ignore", "pipe", "pipe"]
88
+ });
89
+ child.stdout?.on("data", (chunk) => writer.write(chunk));
90
+ child.stderr?.on("data", (chunk) => writer.write(chunk));
91
+ process.on("SIGTERM", () => forwardSignal(child, "SIGTERM"));
92
+ process.on("SIGINT", () => forwardSignal(child, "SIGINT"));
93
+ const result = await new Promise((resolve) => {
94
+ child.once("error", (error) => {
95
+ writer.write(Buffer.from(`hestia proc relay: ${error.message}
96
+ `));
97
+ resolve({ code: 127, signal: null });
98
+ });
99
+ child.once("exit", (code, signal) => resolve({ code, signal }));
100
+ });
101
+ writer.close();
102
+ if (result.signal !== null)
103
+ process.exit(1);
104
+ process.exit(result.code ?? 1);
105
+ }
106
+ if (import.meta.main) {
107
+ main().catch((error) => {
108
+ process.stderr.write(`hestia proc relay failed: ${error.message}
109
+ `);
110
+ process.exit(1);
111
+ });
112
+ }
113
+ export {
114
+ RotatingLogWriter
115
+ };
116
+
117
+ //# debugId=D0D371B26D9A99F264756E2164756E21
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../packages/engine/src/proc/proc-relay.ts"],
4
+ "sourcesContent": [
5
+ "#!/usr/bin/env bun\nimport {\n chmodSync,\n closeSync,\n existsSync,\n openSync,\n renameSync,\n rmSync,\n statSync,\n writeSync,\n} from \"node:fs\";\nimport { spawn, type ChildProcess } from \"node:child_process\";\n\nconst MAX_LOG_BYTES = 25 * 1024 * 1024;\nconst ARCHIVE_COUNT = 3;\nconst RELAY_SPEC_ENV = \"HESTIA_PROC_RELAY_SPEC\";\n\ninterface RelaySpec {\n argv: string[];\n cwd: string;\n env: Record<string, string | undefined>;\n logPath: string;\n}\n\nfunction relaySpec(): RelaySpec {\n const encoded = process.env[RELAY_SPEC_ENV];\n delete process.env[RELAY_SPEC_ENV];\n if (encoded === undefined) throw new Error(`${RELAY_SPEC_ENV} is missing`);\n const value = JSON.parse(Buffer.from(encoded, \"base64\").toString(\"utf8\")) as RelaySpec;\n if (!Array.isArray(value.argv) || value.argv.length === 0 ||\n typeof value.cwd !== \"string\" || typeof value.logPath !== \"string\") {\n throw new Error(\"invalid proc relay spec\");\n }\n return value;\n}\n\nexport class RotatingLogWriter {\n #fd: number;\n #bytes: number;\n\n constructor(\n readonly path: string,\n readonly maxLogBytes = MAX_LOG_BYTES,\n readonly archiveCount = ARCHIVE_COUNT,\n ) {\n this.#fd = openSync(path, \"a\", 0o600);\n chmodSync(path, 0o600);\n this.#bytes = statSync(path).size;\n }\n\n write(chunk: Buffer): void {\n let offset = 0;\n while (offset < chunk.byteLength) {\n if (this.#bytes >= this.maxLogBytes) this.rotate();\n const length = Math.min(chunk.byteLength - offset, this.maxLogBytes - this.#bytes);\n writeSync(this.#fd, chunk, offset, length);\n offset += length;\n this.#bytes += length;\n }\n }\n\n close(): void {\n closeSync(this.#fd);\n }\n\n private rotate(): void {\n closeSync(this.#fd);\n rmSync(`${this.path}.${this.archiveCount}`, { force: true });\n for (let index = this.archiveCount - 1; index >= 1; index -= 1) {\n const source = `${this.path}.${index}`;\n if (existsSync(source)) renameSync(source, `${this.path}.${index + 1}`);\n }\n if (existsSync(this.path)) renameSync(this.path, `${this.path}.1`);\n this.#fd = openSync(this.path, \"a\", 0o600);\n chmodSync(this.path, 0o600);\n this.#bytes = 0;\n }\n}\n\nfunction forwardSignal(child: ChildProcess, signal: NodeJS.Signals): void {\n if (child.exitCode !== null || child.signalCode !== null) return;\n try {\n child.kill(signal);\n } catch {}\n}\n\nasync function main(): Promise<void> {\n const spec = relaySpec();\n const writer = new RotatingLogWriter(spec.logPath);\n const child = spawn(spec.argv[0]!, spec.argv.slice(1), {\n cwd: spec.cwd,\n env: spec.env as NodeJS.ProcessEnv,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n });\n child.stdout?.on(\"data\", (chunk: Buffer) => writer.write(chunk));\n child.stderr?.on(\"data\", (chunk: Buffer) => writer.write(chunk));\n process.on(\"SIGTERM\", () => forwardSignal(child, \"SIGTERM\"));\n process.on(\"SIGINT\", () => forwardSignal(child, \"SIGINT\"));\n const result = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>((resolve) => {\n child.once(\"error\", (error) => {\n writer.write(Buffer.from(`hestia proc relay: ${error.message}\\n`));\n resolve({ code: 127, signal: null });\n });\n child.once(\"exit\", (code, signal) => resolve({ code, signal }));\n });\n writer.close();\n if (result.signal !== null) process.exit(1);\n process.exit(result.code ?? 1);\n}\n\nif (import.meta.main) {\n void main().catch((error) => {\n process.stderr.write(`hestia proc relay failed: ${(error as Error).message}\\n`);\n process.exit(1);\n });\n}\n"
6
+ ],
7
+ "mappings": "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",
8
+ "debugId": "D0D371B26D9A99F264756E2164756E21",
9
+ "names": []
10
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@tridha643/hestia",
3
+ "version": "1.0.0",
4
+ "description": "Per-worktree isolated development stacks for humans and coding agents",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/tridha643/hestia"
8
+ },
9
+ "type": "module",
10
+ "workspaces": ["packages/*"],
11
+ "bin": {
12
+ "hestia": "./dist/cli.js"
13
+ },
14
+ "scripts": {
15
+ "hestia": "bun run packages/cli/src/index.ts",
16
+ "test": "bun test",
17
+ "test:tui": "bun test packages/tui/test test/pty",
18
+ "build": "bun run scripts/build-dist.ts",
19
+ "prepack": "bun run build"
20
+ },
21
+ "devDependencies": {
22
+ "@types/bun": "latest",
23
+ "@types/react": "19.2.14",
24
+ "portless": "0.15.1",
25
+ "tuistory": "0.0.16",
26
+ "typescript": "^5.9.0"
27
+ },
28
+ "dependencies": {
29
+ "@opentui/core": "0.4.3",
30
+ "@opentui/react": "0.4.3",
31
+ "react": "19.2.4",
32
+ "yaml": "^2.6.0"
33
+ },
34
+ "engines": {
35
+ "bun": ">=1.3.0"
36
+ },
37
+ "os": ["darwin"],
38
+ "cpu": ["arm64", "x64"],
39
+ "files": [
40
+ "dist",
41
+ "skills/hestia",
42
+ "README.md",
43
+ "NOTICES.md"
44
+ ],
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "provenance": true
48
+ }
49
+ }
@@ -0,0 +1,148 @@
1
+ ---
2
+ name: hestia
3
+ description: Discover, configure, run, inspect, expose, and tear down isolated macOS/Bun development workloads in parallel Git worktrees.
4
+ ---
5
+
6
+ # Hestia agent workflow
7
+
8
+ Hestia owns development workload lifecycle and reachability for this worktree.
9
+ Do not choose ports, start configured dev servers directly, or run a second
10
+ Cloudflare connector beside Hestia.
11
+
12
+ ## Start with discovery
13
+
14
+ ```bash
15
+ hestia version --json
16
+ hestia discover --json
17
+ hestia doctor --json
18
+ ```
19
+
20
+ Discovery is read-only. Inspect:
21
+
22
+ - `repository`: exact repo, `repoId`, branch, and absolute worktree.
23
+ - `runnableWorkloads` and `candidateWorkloads`.
24
+ - `bindings` and configured endpoint aliases.
25
+ - `decisionSource`: `discovery`, `repository`, `machine`, or `worktree`.
26
+ - `missingDecisions`, `conflicts`, and `suggestions`.
27
+
28
+ Do not guess when discovery reports a missing decision. If repository changes
29
+ are authorized, use a suggested `hestia init ... --scope repository --write`.
30
+ For personal/non-portable setup use `--scope machine --write`. Without
31
+ `--write`, init prints the complete proposed TOML and changes nothing. Hestia
32
+ never commits.
33
+
34
+ Examples:
35
+
36
+ ```bash
37
+ hestia init dockerfile web Dockerfile --scope repository
38
+ hestia init proc consumer --scope machine --no-port -- bun run consume
39
+ hestia init endpoint dashboard web 3000/tcp http --scope repository --write
40
+ hestia init wrangler slack-worker apps/slack/wrangler.toml --scope repository --write
41
+ hestia discover --json
42
+ ```
43
+
44
+ Configuration layers are worktree runtime intent, machine repository overlay
45
+ at `~/.hestia/repositories/<repoId>.toml`, optional committed `hestia.toml`,
46
+ then automatic discovery. Conflicts fail rather than silently overriding a
47
+ workload source. `.hestia/` must remain ignored.
48
+
49
+ ## Run and consume structured endpoints
50
+
51
+ ```bash
52
+ hestia up --json
53
+ hestia endpoint list --json
54
+ hestia endpoint get dashboard --json
55
+ hestia logs web -f --json
56
+ ```
57
+
58
+ A workload is a lifecycle/logging unit. A binding is an owned socket such as
59
+ `api:8080/tcp`. An endpoint alias assigns protocol meaning to a binding.
60
+ Resolution order is exact alias, canonical `workload:target/protocol`, then a
61
+ uniquely-bound workload. Handle `service-port-ambiguous` by using a reported
62
+ selector or configured alias.
63
+
64
+ Canonical port variables look like `HESTIA_API_9090_TCP_PORT`. Only a unique
65
+ binding receives `HESTIA_API_PORT`. HTTP aliases also receive direct/local URL
66
+ variables. Never invent an HTTP URL for a raw TCP/UDP endpoint.
67
+
68
+ Use `hestia run` only for explicit ad-hoc processes not already declared as a
69
+ workload:
70
+
71
+ ```bash
72
+ hestia run --name web -- bun run dev
73
+ hestia run --name consumer --no-port -- bun run consume
74
+ ```
75
+
76
+ Hestia injects `$PORT` and substitutes `{port}`. Raw `--env` values remain in
77
+ memory and are not persisted. Logs are bounded by the rotating relay.
78
+
79
+ ## Routes and exposure
80
+
81
+ ```bash
82
+ hestia route add dashboard --json
83
+ hestia route disable dashboard --json
84
+ hestia route reset dashboard --json
85
+ hestia open dashboard --local --json
86
+ hestia expose dashboard --json
87
+ hestia expose dashboard --tunnel tri --zone example.dev --json
88
+ ```
89
+
90
+ Local route overrides are per worktree. `disable` suppresses repository or
91
+ machine defaults; `reset` removes the override and restores defaults.
92
+
93
+ Public ingress is fail-closed but unauthenticated. Hestiad verifies the current
94
+ process/container identity and port ownership before every origin connection.
95
+ If a port is recycled, the foreign listener receives zero bytes.
96
+
97
+ Named mode never writes DNS. It requires:
98
+
99
+ ```text
100
+ *.<zone> CNAME <tunnel-uuid>.cfargotunnel.com
101
+ ```
102
+
103
+ Handle `dns-route-required` by reporting its `wildcardTarget` to the human.
104
+ Never use the removed `--overwrite-dns`. Named mode may use
105
+ `--keep-host-header`; quick mode rejects it. Never start another
106
+ `cloudflared tunnel run` for an adopted tunnel.
107
+
108
+ ## Inspect and finish
109
+
110
+ ```bash
111
+ hestia status --json
112
+ hestia env --json
113
+ hestia doctor --json
114
+ hestia tui
115
+ hestia down
116
+ ```
117
+
118
+ Fleet shows the repository, selected branch, absolute worktree, project,
119
+ workloads, endpoints, and logs. It is for human operation; agents should use
120
+ JSON/NDJSON commands.
121
+
122
+ Run `down` before switching or deleting the branch. After deletion, use the
123
+ recorded project with `hestia down --project <project>`. Docker workloads
124
+ cannot be stopped individually (`backend-not-stoppable`); proc/Wrangler
125
+ workloads can. Named volumes are retained unless `--destroy` is explicit.
126
+
127
+ ## Stable error remedies
128
+
129
+ | code | remedy |
130
+ |---|---|
131
+ | `setup-required` | follow `discover --json` suggestions with explicit authorization |
132
+ | `config-conflict` / `config-invalid` | fix the reported layer/path; do not guess precedence |
133
+ | `state-not-ignored` | add the exact `.hestia/` line reported by `doctor` |
134
+ | `state-corrupt` | inspect the path, then use the reported `down --project` recovery |
135
+ | `migration-required` | inspect if needed, then down the legacy stack |
136
+ | `stack-identity-changed` | down the recorded project before continuing on this checkout |
137
+ | `env-key-conflict` | rename one workload/alias so normalized env keys differ |
138
+ | `compose-unsupported` | replace the unsupported global/shared construct or use a supported topology |
139
+ | `service-port-ambiguous` | use a canonical selector or endpoint alias |
140
+ | `proc-ready-timeout` | inspect logs; use `--no-port` only for non-servers |
141
+ | `stack-limit` | down an owned stack or retry with `--wait=120` |
142
+ | `dns-route-required` | configure its wildcard CNAME target |
143
+ | `tunnel-busy` | stop the foreign connector; do not kill processes Hestia did not start |
144
+ | `route-origin-unavailable` | restart the workload through Hestia |
145
+ | `backend-not-stoppable` | use `hestia down` for Docker workloads |
146
+
147
+ `--json` failures are `{ "error": { "code", "message", "details"? } }`.
148
+ `hestia logs --json` is NDJSON, one `LogLine` per line.