@shetty4l/core 0.1.3

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,36 @@
1
+ name: CI (shared)
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ extra-validate-command:
7
+ description: "Optional extra command to run after validate (e.g. a full test suite)"
8
+ required: false
9
+ type: string
10
+ default: ""
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ validate:
17
+ runs-on: ubuntu-latest
18
+ steps:
19
+ - name: Checkout
20
+ uses: actions/checkout@v4
21
+
22
+ - name: Setup Bun
23
+ uses: oven-sh/setup-bun@v2
24
+ with:
25
+ bun-version: latest
26
+
27
+ - name: Install dependencies
28
+ run: bun install --frozen-lockfile
29
+
30
+ - name: Validate
31
+ run: bun run validate
32
+
33
+ - name: Extra validation
34
+ if: ${{ inputs.extra-validate-command != '' }}
35
+ shell: bash
36
+ run: ${{ inputs.extra-validate-command }}
@@ -0,0 +1,14 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ validate:
14
+ uses: ./.github/workflows/ci-shared.yml
@@ -0,0 +1,150 @@
1
+ name: Release (shared)
2
+
3
+ on:
4
+ workflow_call:
5
+ inputs:
6
+ service-name:
7
+ description: "Service name for tarball prefix and release title (e.g. engram, synapse)"
8
+ required: true
9
+ type: string
10
+ extra-tarball-paths:
11
+ description: "Space-separated additional paths to include in tarball (e.g. 'opencode/ scripts/install.sh')"
12
+ required: false
13
+ type: string
14
+ default: ""
15
+ publish-npm:
16
+ description: "Whether to publish to GitHub Packages via npm publish"
17
+ required: false
18
+ type: boolean
19
+ default: false
20
+ attach-install-sh:
21
+ description: "Whether to attach scripts/install.sh as a release asset"
22
+ required: false
23
+ type: boolean
24
+ default: false
25
+
26
+ concurrency:
27
+ group: release
28
+ cancel-in-progress: false
29
+
30
+ jobs:
31
+ release:
32
+ runs-on: ubuntu-latest
33
+ permissions:
34
+ contents: write
35
+ packages: write
36
+ steps:
37
+ - name: Checkout
38
+ uses: actions/checkout@v4
39
+ with:
40
+ ref: ${{ github.event.workflow_run.head_sha }}
41
+ fetch-depth: 0
42
+
43
+ - name: Setup Bun
44
+ uses: oven-sh/setup-bun@v2
45
+ with:
46
+ bun-version: latest
47
+
48
+ - name: Install dependencies
49
+ run: bun install --frozen-lockfile
50
+
51
+ - name: Compute next version
52
+ id: version
53
+ run: |
54
+ LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.1.0")
55
+ echo "latest_tag=${LATEST_TAG}" >> "$GITHUB_OUTPUT"
56
+
57
+ VERSION="${LATEST_TAG#v}"
58
+ IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
59
+
60
+ COMMITS_SINCE="$(git log "${LATEST_TAG}..HEAD" --format='%s' 2>/dev/null || echo "")"
61
+ if echo "$COMMITS_SINCE" | grep -q '\[major\]'; then
62
+ NEXT_VERSION="$((MAJOR + 1)).0.0"
63
+ BUMP_LEVEL="major"
64
+ elif echo "$COMMITS_SINCE" | grep -q '\[minor\]'; then
65
+ NEXT_VERSION="${MAJOR}.$((MINOR + 1)).0"
66
+ BUMP_LEVEL="minor"
67
+ else
68
+ NEXT_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
69
+ BUMP_LEVEL="patch"
70
+ fi
71
+
72
+ echo "bump_level=${BUMP_LEVEL}" >> "$GITHUB_OUTPUT"
73
+ echo "next_version=${NEXT_VERSION}" >> "$GITHUB_OUTPUT"
74
+ echo "next_tag=v${NEXT_VERSION}" >> "$GITHUB_OUTPUT"
75
+ echo "Releasing: v${NEXT_VERSION} (previous: ${LATEST_TAG}, bump: ${BUMP_LEVEL})"
76
+
77
+ - name: Write VERSION file
78
+ run: echo "${{ steps.version.outputs.next_version }}" > VERSION
79
+
80
+ - name: Update package.json version
81
+ run: |
82
+ jq --arg v "${{ steps.version.outputs.next_version }}" '.version = $v' package.json > tmp.json
83
+ mv tmp.json package.json
84
+
85
+ - name: Write BUILD_META.json
86
+ run: |
87
+ SHA="${{ github.event.workflow_run.head_sha }}"
88
+ SHORT_SHA="${SHA:0:7}"
89
+ BRANCH="${{ github.event.workflow_run.head_branch }}"
90
+ TITLE="$(git log -1 --format='%s' "$SHA")"
91
+ BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
92
+ jq -n \
93
+ --arg sha "$SHA" \
94
+ --arg shortSha "$SHORT_SHA" \
95
+ --arg branch "$BRANCH" \
96
+ --arg title "$TITLE" \
97
+ --arg buildTime "$BUILD_TIME" \
98
+ '{
99
+ gitSha: $sha,
100
+ gitShortSha: $shortSha,
101
+ gitBranch: $branch,
102
+ commitTitle: $title,
103
+ buildTimeUtc: $buildTime
104
+ }' > BUILD_META.json
105
+
106
+ - name: Publish to GitHub Packages
107
+ if: ${{ inputs.publish-npm }}
108
+ run: |
109
+ echo "//npm.pkg.github.com/:_authToken=${{ github.token }}" > .npmrc
110
+ echo "@shetty4l:registry=https://npm.pkg.github.com" >> .npmrc
111
+ npm publish --access public
112
+
113
+ - name: Create source tarball
114
+ run: |
115
+ TAG="${{ steps.version.outputs.next_tag }}"
116
+ EXTRA_PATHS="${{ inputs.extra-tarball-paths }}"
117
+ tar czf "${{ inputs.service-name }}-${TAG}.tar.gz" \
118
+ --exclude='node_modules' \
119
+ --exclude='.git' \
120
+ --exclude='test' \
121
+ --exclude='.husky' \
122
+ --exclude='.DS_Store' \
123
+ --exclude='dist' \
124
+ src/ \
125
+ package.json \
126
+ bun.lock \
127
+ tsconfig.json \
128
+ biome.json \
129
+ VERSION \
130
+ BUILD_META.json \
131
+ $EXTRA_PATHS
132
+
133
+ - name: Create tag and release
134
+ env:
135
+ GH_TOKEN: ${{ github.token }}
136
+ run: |
137
+ TAG="${{ steps.version.outputs.next_tag }}"
138
+ ASSETS=("${{ inputs.service-name }}-${TAG}.tar.gz")
139
+
140
+ if [ "${{ inputs.attach-install-sh }}" = "true" ] && [ -f "scripts/install.sh" ]; then
141
+ ASSETS+=("scripts/install.sh")
142
+ fi
143
+
144
+ git tag "${TAG}"
145
+ git push origin "${TAG}"
146
+
147
+ gh release create "${TAG}" \
148
+ --title "Release ${TAG}" \
149
+ --notes "Automated release ${TAG}" \
150
+ "${ASSETS[@]}"
@@ -0,0 +1,19 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_run:
5
+ workflows: ["CI"]
6
+ types: [completed]
7
+ branches: [main]
8
+
9
+ permissions:
10
+ contents: write
11
+ packages: write
12
+
13
+ jobs:
14
+ release:
15
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
16
+ uses: ./.github/workflows/release-shared.yml
17
+ with:
18
+ service-name: core
19
+ publish-npm: true
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ bun run validate || exit 1
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # @shetty4l/core
2
+
3
+ Shared infrastructure primitives for Bun/TypeScript services. Zero external dependencies.
4
+
5
+ ## Modules
6
+
7
+ | Module | Purpose |
8
+ |--------|---------|
9
+ | `result` | `Result<T, E>` type, `ok`/`err` constructors, `Port` branded type |
10
+ | `version` | Read VERSION file from project root with fallback |
11
+ | `config` | XDG directory resolution, path expansion, env var interpolation, JSON config loading |
12
+ | `cli` | Argument parsing, command dispatch, uptime formatting |
13
+ | `daemon` | PID-file daemon management (start/stop/restart/status) |
14
+ | `http` | Bun.serve wrapper with CORS, health endpoint, JSON response helpers |
15
+ | `signals` | Graceful shutdown handler (SIGINT/SIGTERM) |
16
+
17
+ ## Install
18
+
19
+ Configure GitHub Packages registry:
20
+
21
+ ```toml
22
+ # bunfig.toml
23
+ [install.scopes]
24
+ "@shetty4l" = "https://npm.pkg.github.com"
25
+ ```
26
+
27
+ Then:
28
+
29
+ ```bash
30
+ bun add @shetty4l/core
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### Namespace imports (recommended)
36
+
37
+ ```typescript
38
+ import { config, http, cli, daemon, readVersion, onShutdown } from "@shetty4l/core";
39
+
40
+ const version = readVersion(import.meta.dir);
41
+ const port = config.parsePort(process.env.PORT!, "PORT");
42
+ const cfg = config.loadJsonConfig({ name: "myservice", defaults: { port: 3000 } });
43
+
44
+ const server = http.createServer({
45
+ port: 3000,
46
+ version,
47
+ onRequest: (req, url) => {
48
+ if (url.pathname === "/echo") return http.jsonOk({ ok: true });
49
+ return null; // 404
50
+ },
51
+ });
52
+
53
+ onShutdown(() => server.stop());
54
+ ```
55
+
56
+ ### Sub-path imports
57
+
58
+ ```typescript
59
+ import { parsePort, loadJsonConfig } from "@shetty4l/core/config";
60
+ import { createServer, jsonOk } from "@shetty4l/core/http";
61
+ import { createDaemonManager } from "@shetty4l/core/daemon";
62
+ ```
63
+
64
+ ## Result type
65
+
66
+ Functions that can fail with expected errors return `Result<T, E>` instead of throwing:
67
+
68
+ ```typescript
69
+ import { config } from "@shetty4l/core";
70
+
71
+ const result = config.parsePort("abc", "PORT");
72
+ if (!result.ok) {
73
+ console.error(result.error);
74
+ process.exit(1);
75
+ }
76
+ // result.value is a branded Port type — validated once, trusted downstream
77
+ ```
78
+
79
+ **Convention:** `Result` for expected failures (invalid input, missing files). `throw` for programmer errors (bugs, invariant violations).
80
+
81
+ ## Development
82
+
83
+ ```bash
84
+ bun install
85
+ bun run validate # typecheck + lint + format:check + test
86
+ bun run format # auto-fix formatting
87
+ bun test # run tests only
88
+ ```
package/biome.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json",
3
+ "formatter": {
4
+ "enabled": true,
5
+ "indentStyle": "space",
6
+ "indentWidth": 2
7
+ },
8
+ "linter": {
9
+ "enabled": false
10
+ },
11
+ "assist": { "actions": { "source": { "organizeImports": "on" } } }
12
+ }
package/bun.lock ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "configVersion": 1,
4
+ "workspaces": {
5
+ "": {
6
+ "name": "@shetty4l/core",
7
+ "devDependencies": {
8
+ "@biomejs/biome": "^2.3.13",
9
+ "@types/bun": "latest",
10
+ "husky": "^9.0.0",
11
+ "oxlint": "^0.12.0",
12
+ "typescript": "^5.0.0",
13
+ },
14
+ },
15
+ },
16
+ "packages": {
17
+ "@biomejs/biome": ["@biomejs/biome@2.4.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.2", "@biomejs/cli-darwin-x64": "2.4.2", "@biomejs/cli-linux-arm64": "2.4.2", "@biomejs/cli-linux-arm64-musl": "2.4.2", "@biomejs/cli-linux-x64": "2.4.2", "@biomejs/cli-linux-x64-musl": "2.4.2", "@biomejs/cli-win32-arm64": "2.4.2", "@biomejs/cli-win32-x64": "2.4.2" }, "bin": { "biome": "bin/biome" } }, "sha512-vVE/FqLxNLbvYnFDYg3Xfrh1UdFhmPT5i+yPT9GE2nTUgI4rkqo5krw5wK19YHBd7aE7J6r91RRmb8RWwkjy6w=="],
18
+
19
+ "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3pEcKCP/1POKyaZZhXcxFl3+d9njmeAihZ17k8lL/1vk+6e0Cbf0yPzKItFiT+5Yh6TQA4uKvnlqe0oVZwRxCA=="],
20
+
21
+ "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-P7hK1jLVny+0R9UwyGcECxO6sjETxfPyBm/1dmFjnDOHgdDPjPqozByunrwh4xPKld8sxOr5eAsSqal5uKgeBg=="],
22
+
23
+ "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-DI3Mi7GT2zYNgUTDEbSjl3e1KhoP76OjQdm8JpvZYZWtVDRyLd3w8llSr2TWk1z+U3P44kUBWY3X7H9MD1/DGQ=="],
24
+
25
+ "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-/x04YK9+7erw6tYEcJv9WXoBHcULI/wMOvNdAyE9S3JStZZ9yJyV67sWAI+90UHuDo/BDhq0d96LDqGlSVv7WA=="],
26
+
27
+ "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-GK2ErnrKpWFigYP68cXiCHK4RTL4IUWhK92AFS3U28X/nuAL5+hTuy6hyobc8JZRSt+upXt1nXChK+tuHHx4mA=="],
28
+
29
+ "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.2", "", { "os": "linux", "cpu": "x64" }, "sha512-wbBmTkeAoAYbOQ33f6sfKG7pcRSydQiF+dTYOBjJsnXO2mWEOQHllKlC2YVnedqZFERp2WZhFUoO7TNRwnwEHQ=="],
30
+
31
+ "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-k2uqwLYrNNxnaoiW3RJxoMGnbKda8FuCmtYG3cOtVljs3CzWxaTR+AoXwKGHscC9thax9R4kOrtWqWN0+KdPTw=="],
32
+
33
+ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.2", "", { "os": "win32", "cpu": "x64" }, "sha512-9ma7C4g8Sq3cBlRJD2yrsHXB1mnnEBdpy7PhvFrylQWQb4PoyCmPucdX7frvsSBQuFtIiKCrolPl/8tCZrKvgQ=="],
34
+
35
+ "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@0.12.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UydkjcAImpmBn8JYaMPg0zJrwgWJMGvJagvCnyPfyiBRWAN83Kq+BDgJZgIq+2Te6kvlnoiHWNJKVJmpy0f0BA=="],
36
+
37
+ "@oxlint/darwin-x64": ["@oxlint/darwin-x64@0.12.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-bxLyiAiHzXB56w7cf50YNPpZlK+PMxA8GgHutRSoNK/Z/BR/xsibNLs/9YNUnjHB+PF19+EbIRtJxoHjmbRr8g=="],
38
+
39
+ "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@0.12.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jVkmfoMjPKFDIZySmpykwrCmx5xhpLJdMpUAR8ycEkFRJFp5qKLWZd6cEjiMb7gxmWN6qcCvDVTF/zEs3aRpyQ=="],
40
+
41
+ "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@0.12.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-8VdV1nKYDj7AFaw1a03Ih43/+pUS/hhMZbTFLRMpvlVp1cPtdB77c+bl/OdiJ/BwNTzLIzr/GrospwCoEJkQKg=="],
42
+
43
+ "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@0.12.0", "", { "os": "linux", "cpu": "x64" }, "sha512-MacAt8N4XU5DeoHcseXLom/z+B0seecCz8vGAH4ppF2EH49o7NbN7VvFsw2nZ2QNO/4vw+pdS1BHXLTr9lY6zQ=="],
44
+
45
+ "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@0.12.0", "", { "os": "linux", "cpu": "x64" }, "sha512-/ZBDJ9wpUE6bB05nniQl29kD5vJUMg6n75LdHD8F6ThXfsHGI/n7Je3gzggnXokgf9UQpTUPWrWlfEuWVCBMag=="],
46
+
47
+ "@oxlint/win32-arm64": ["@oxlint/win32-arm64@0.12.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-hY1ya9dv8VY8113YSSDfMs/989aFmoA2fIZco8uxTxIEVl9nGY6tDtpgKZqUIiGrrMbDO8BBb1G5jsekmfexbA=="],
48
+
49
+ "@oxlint/win32-x64": ["@oxlint/win32-x64@0.12.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NHLJolo4sZk3nu/bPNuaJ+6p5DdHoRuZAjyuSO6CnLgpmZcYqx7LgngA/x2oB/bLgi4Hv9twjHjODc5Ce5o14g=="],
50
+
51
+ "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="],
52
+
53
+ "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
54
+
55
+ "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="],
56
+
57
+ "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
58
+
59
+ "oxlint": ["oxlint@0.12.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "0.12.0", "@oxlint/darwin-x64": "0.12.0", "@oxlint/linux-arm64-gnu": "0.12.0", "@oxlint/linux-arm64-musl": "0.12.0", "@oxlint/linux-x64-gnu": "0.12.0", "@oxlint/linux-x64-musl": "0.12.0", "@oxlint/win32-arm64": "0.12.0", "@oxlint/win32-x64": "0.12.0" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-M0vWq8KYtp4vpweRxcdCiVO8QFwzoRyp5bWTMrEL/0Z+GDKCMJltac7H3T3T09FIiktOZLvID733d7OcKk/caw=="],
60
+
61
+ "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
62
+
63
+ "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
64
+ }
65
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@shetty4l/core",
3
+ "version": "0.1.3",
4
+ "description": "Shared infrastructure primitives for Bun/TypeScript services",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./result": "./src/result.ts",
10
+ "./version": "./src/version.ts",
11
+ "./config": "./src/config.ts",
12
+ "./signals": "./src/signals.ts",
13
+ "./cli": "./src/cli.ts",
14
+ "./daemon": "./src/daemon.ts",
15
+ "./http": "./src/http.ts"
16
+ },
17
+ "bin": {
18
+ "version-bump": "./src/scripts/version-bump.ts"
19
+ },
20
+ "scripts": {
21
+ "typecheck": "tsc --noEmit",
22
+ "lint": "oxlint src/",
23
+ "format": "biome check --write src/ test/",
24
+ "format:check": "biome check src/ test/",
25
+ "test": "bun test",
26
+ "validate": "bun run typecheck && bun run lint && bun run format:check && bun run test",
27
+ "version:bump": "bun run src/scripts/version-bump.ts",
28
+ "prepare": "husky"
29
+ },
30
+ "devDependencies": {
31
+ "@biomejs/biome": "^2.3.13",
32
+ "@types/bun": "latest",
33
+ "husky": "^9.0.0",
34
+ "oxlint": "^0.12.0",
35
+ "typescript": "^5.0.0"
36
+ }
37
+ }
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env bash
2
+ # install-lib.sh — Shared install functions for shetty4l services.
3
+ #
4
+ # Source this file from a per-service install.sh, then call the functions.
5
+ # Required variables before sourcing:
6
+ # SERVICE_NAME — e.g. "engram"
7
+ # REPO — e.g. "shetty4l/engram"
8
+ # INSTALL_BASE — e.g. "$HOME/srv/engram"
9
+ #
10
+ # Optional variables:
11
+ # BIN_DIR — CLI symlink directory (default: $HOME/.local/bin)
12
+ # MAX_VERSIONS — versions to keep (default: 5)
13
+ #
14
+ # After sourcing, these variables are set by fetch_latest_release:
15
+ # RELEASE_TAG — e.g. "v0.2.3"
16
+ # TARBALL_URL — download URL for the release tarball
17
+
18
+ set -euo pipefail
19
+
20
+ BIN_DIR="${BIN_DIR:-${HOME}/.local/bin}"
21
+ MAX_VERSIONS="${MAX_VERSIONS:-5}"
22
+
23
+ # --- color helpers ---
24
+
25
+ info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
26
+ ok() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
27
+ warn() { printf '\033[1;33m==>\033[0m %s\n' "$*"; }
28
+ err() { printf '\033[1;31m==>\033[0m %s\n' "$*" >&2; }
29
+ die() { err "$@"; exit 1; }
30
+
31
+ # --- prereqs ---
32
+
33
+ check_prereqs() {
34
+ local missing=()
35
+ for cmd in bun curl tar jq; do
36
+ if ! command -v "$cmd" &>/dev/null; then
37
+ missing+=("$cmd")
38
+ fi
39
+ done
40
+ if [ ${#missing[@]} -gt 0 ]; then
41
+ die "Missing required tools: ${missing[*]}"
42
+ fi
43
+ }
44
+
45
+ # --- fetch latest release ---
46
+
47
+ fetch_latest_release() {
48
+ info "Fetching latest release from GitHub..."
49
+ local release_json
50
+ release_json=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest")
51
+
52
+ RELEASE_TAG=$(echo "$release_json" | jq -r '.tag_name')
53
+ TARBALL_URL=$(echo "$release_json" | jq -r ".assets[] | select(.name | startswith(\"${SERVICE_NAME}-\")) | .browser_download_url")
54
+
55
+ if [ -z "$RELEASE_TAG" ] || [ "$RELEASE_TAG" = "null" ]; then
56
+ die "No releases found for ${REPO}"
57
+ fi
58
+ if [ -z "$TARBALL_URL" ] || [ "$TARBALL_URL" = "null" ]; then
59
+ die "No tarball asset found in release ${RELEASE_TAG}"
60
+ fi
61
+
62
+ info "Latest release: ${RELEASE_TAG}"
63
+ }
64
+
65
+ # --- download and extract ---
66
+
67
+ download_and_extract() {
68
+ local version_dir="${INSTALL_BASE}/${RELEASE_TAG}"
69
+
70
+ if [ -d "$version_dir" ]; then
71
+ warn "Version ${RELEASE_TAG} already exists at ${version_dir}, reinstalling..."
72
+ rm -rf "$version_dir"
73
+ fi
74
+
75
+ mkdir -p "$version_dir"
76
+
77
+ info "Downloading ${RELEASE_TAG}..."
78
+ local tmpfile
79
+ tmpfile=$(mktemp)
80
+ curl -fsSL -o "$tmpfile" "$TARBALL_URL"
81
+
82
+ info "Extracting to ${version_dir}..."
83
+ tar xzf "$tmpfile" -C "$version_dir"
84
+ rm -f "$tmpfile"
85
+
86
+ info "Installing dependencies..."
87
+ (cd "$version_dir" && bun install --frozen-lockfile)
88
+
89
+ info "Creating CLI wrapper..."
90
+ cat > "$version_dir/${SERVICE_NAME}" <<WRAPPER
91
+ #!/usr/bin/env bash
92
+ SCRIPT_DIR="\$(cd "\$(dirname "\$(readlink "\$0" || echo "\$0")")" && pwd)"
93
+ exec bun run "\$SCRIPT_DIR/src/cli.ts" "\$@"
94
+ WRAPPER
95
+ chmod +x "$version_dir/${SERVICE_NAME}"
96
+
97
+ ok "Installed ${RELEASE_TAG} to ${version_dir}"
98
+ }
99
+
100
+ # --- symlink management (atomic) ---
101
+
102
+ update_symlink() {
103
+ local version_dir="${INSTALL_BASE}/${RELEASE_TAG}"
104
+ local latest_link="${INSTALL_BASE}/latest"
105
+
106
+ ln -sfn "$version_dir" "$latest_link"
107
+ echo "$RELEASE_TAG" > "${INSTALL_BASE}/current-version"
108
+
109
+ ok "Symlinked latest -> ${RELEASE_TAG}"
110
+ }
111
+
112
+ # --- prune old versions ---
113
+
114
+ prune_versions() {
115
+ local versions=()
116
+ for d in "${INSTALL_BASE}"/v*; do
117
+ [ -d "$d" ] && versions+=("$(basename "$d")")
118
+ done
119
+
120
+ if [ ${#versions[@]} -eq 0 ]; then
121
+ return
122
+ fi
123
+
124
+ IFS=$'\n' sorted=($(printf '%s\n' "${versions[@]}" | sed 's/^v//' | sort -t. -k1,1n -k2,2n -k3,3n | sed 's/^/v/'))
125
+ unset IFS
126
+
127
+ local count=${#sorted[@]}
128
+ if [ "$count" -gt "$MAX_VERSIONS" ]; then
129
+ local remove_count=$((count - MAX_VERSIONS))
130
+ for ((i = 0; i < remove_count; i++)); do
131
+ local old_version="${sorted[$i]}"
132
+ info "Removing old version: ${old_version}"
133
+ rm -rf "${INSTALL_BASE}/${old_version}"
134
+ done
135
+ fi
136
+ }
137
+
138
+ # --- CLI binary ---
139
+
140
+ install_cli() {
141
+ mkdir -p "$BIN_DIR"
142
+ ln -sf "${INSTALL_BASE}/latest/${SERVICE_NAME}" "${BIN_DIR}/${SERVICE_NAME}"
143
+ ok "CLI linked: ${BIN_DIR}/${SERVICE_NAME}"
144
+
145
+ if [[ ":$PATH:" != *":${BIN_DIR}:"* ]]; then
146
+ warn "~/.local/bin is not in your PATH. Add it to your shell profile:"
147
+ warn " export PATH=\"\$HOME/.local/bin:\$PATH\""
148
+ fi
149
+ }