@shetty4l/core 0.1.3 → 0.1.9

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.
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@shetty4l/core",
3
- "version": "0.1.3",
3
+ "version": "0.1.9",
4
4
  "description": "Shared infrastructure primitives for Bun/TypeScript services",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/shetty4l/core"
8
+ },
5
9
  "type": "module",
6
10
  "main": "src/index.ts",
7
11
  "exports": {
@@ -14,6 +18,13 @@
14
18
  "./daemon": "./src/daemon.ts",
15
19
  "./http": "./src/http.ts"
16
20
  },
21
+ "files": [
22
+ "src/",
23
+ "scripts/install-lib.sh"
24
+ ],
25
+ "publishConfig": {
26
+ "registry": "https://registry.npmjs.org"
27
+ },
17
28
  "bin": {
18
29
  "version-bump": "./src/scripts/version-bump.ts"
19
30
  },
package/src/cli.ts CHANGED
@@ -45,7 +45,7 @@ export function formatUptime(seconds: number): string {
45
45
  export type CommandHandler = (
46
46
  args: string[],
47
47
  json: boolean,
48
- ) => void | Promise<void>;
48
+ ) => void | number | Promise<void | number>;
49
49
 
50
50
  export interface RunCliOpts {
51
51
  /** Service name, used in error messages. */
@@ -104,6 +104,6 @@ export async function runCli(opts: RunCliOpts): Promise<void> {
104
104
  process.exit(1);
105
105
  }
106
106
 
107
- await handler(args, json);
108
- process.exit(0);
107
+ const exitCode = (await handler(args, json)) ?? 0;
108
+ process.exit(exitCode);
109
109
  }
@@ -1,36 +0,0 @@
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 }}
@@ -1,14 +0,0 @@
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
@@ -1,150 +0,0 @@
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[@]}"
@@ -1,19 +0,0 @@
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
package/.husky/pre-commit DELETED
@@ -1,3 +0,0 @@
1
- #!/bin/sh
2
-
3
- bun run validate || exit 1
package/biome.json DELETED
@@ -1,12 +0,0 @@
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 DELETED
@@ -1,65 +0,0 @@
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/test/cli.test.ts DELETED
@@ -1,61 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { formatUptime, parseArgs } from "../src/cli";
3
-
4
- // --- parseArgs ---
5
-
6
- describe("parseArgs", () => {
7
- test("extracts command and args", () => {
8
- const result = parseArgs(["start", "--port", "8080"]);
9
- expect(result.command).toBe("start");
10
- expect(result.args).toEqual(["--port", "8080"]);
11
- expect(result.json).toBe(false);
12
- });
13
-
14
- test("strips --json flag", () => {
15
- const result = parseArgs(["status", "--json"]);
16
- expect(result.command).toBe("status");
17
- expect(result.args).toEqual([]);
18
- expect(result.json).toBe(true);
19
- });
20
-
21
- test("--json can appear anywhere", () => {
22
- const result = parseArgs(["--json", "health"]);
23
- expect(result.command).toBe("health");
24
- expect(result.json).toBe(true);
25
- });
26
-
27
- test("defaults to help when empty", () => {
28
- const result = parseArgs([]);
29
- expect(result.command).toBe("help");
30
- expect(result.args).toEqual([]);
31
- expect(result.json).toBe(false);
32
- });
33
- });
34
-
35
- // --- formatUptime ---
36
-
37
- describe("formatUptime", () => {
38
- test("formats seconds", () => {
39
- expect(formatUptime(45)).toBe("45s");
40
- });
41
-
42
- test("formats minutes and seconds", () => {
43
- expect(formatUptime(192)).toBe("3m 12s");
44
- });
45
-
46
- test("formats hours and minutes", () => {
47
- expect(formatUptime(8100)).toBe("2h 15m");
48
- });
49
-
50
- test("edge case: exactly 60 seconds", () => {
51
- expect(formatUptime(60)).toBe("1m 0s");
52
- });
53
-
54
- test("edge case: exactly 1 hour", () => {
55
- expect(formatUptime(3600)).toBe("1h 0m");
56
- });
57
-
58
- test("zero seconds", () => {
59
- expect(formatUptime(0)).toBe("0s");
60
- });
61
- });
@@ -1,263 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
- import { homedir } from "os";
4
- import { join } from "path";
5
- import {
6
- expandPath,
7
- getConfigDir,
8
- getDataDir,
9
- interpolateDeep,
10
- interpolateEnvVars,
11
- loadJsonConfig,
12
- parsePort,
13
- } from "../src/config";
14
-
15
- const TMP = join(import.meta.dir, ".tmp-config");
16
-
17
- beforeEach(() => {
18
- if (existsSync(TMP)) rmSync(TMP, { recursive: true });
19
- mkdirSync(TMP, { recursive: true });
20
- });
21
-
22
- afterEach(() => {
23
- if (existsSync(TMP)) rmSync(TMP, { recursive: true });
24
- // Clean up env vars
25
- delete process.env.XDG_DATA_HOME;
26
- delete process.env.XDG_CONFIG_HOME;
27
- delete process.env.CORE_TEST_VAR;
28
- });
29
-
30
- // --- getDataDir ---
31
-
32
- describe("getDataDir", () => {
33
- test("uses XDG_DATA_HOME when set", () => {
34
- process.env.XDG_DATA_HOME = "/custom/data";
35
- expect(getDataDir("engram")).toBe("/custom/data/engram");
36
- });
37
-
38
- test("falls back to ~/.local/share/{name}", () => {
39
- delete process.env.XDG_DATA_HOME;
40
- expect(getDataDir("engram")).toBe(
41
- join(homedir(), ".local", "share", "engram"),
42
- );
43
- });
44
- });
45
-
46
- // --- getConfigDir ---
47
-
48
- describe("getConfigDir", () => {
49
- test("uses XDG_CONFIG_HOME when set", () => {
50
- process.env.XDG_CONFIG_HOME = "/custom/config";
51
- expect(getConfigDir("synapse")).toBe("/custom/config/synapse");
52
- });
53
-
54
- test("falls back to ~/.config/{name}", () => {
55
- delete process.env.XDG_CONFIG_HOME;
56
- expect(getConfigDir("synapse")).toBe(join(homedir(), ".config", "synapse"));
57
- });
58
- });
59
-
60
- // --- expandPath ---
61
-
62
- describe("expandPath", () => {
63
- test("expands ~ to homedir", () => {
64
- const result = expandPath("~/foo/bar");
65
- expect(result).toBe(join(homedir(), "foo/bar"));
66
- });
67
-
68
- test("leaves absolute paths unchanged", () => {
69
- expect(expandPath("/usr/bin")).toBe("/usr/bin");
70
- });
71
-
72
- test("leaves relative paths unchanged", () => {
73
- expect(expandPath("foo/bar")).toBe("foo/bar");
74
- });
75
- });
76
-
77
- // --- interpolateEnvVars ---
78
-
79
- describe("interpolateEnvVars", () => {
80
- test("replaces env var references", () => {
81
- process.env.CORE_TEST_VAR = "hello";
82
- const result = interpolateEnvVars("prefix-${CORE_TEST_VAR}-suffix");
83
- expect(result.ok).toBe(true);
84
- if (result.ok) expect(result.value).toBe("prefix-hello-suffix");
85
- });
86
-
87
- test("returns err for missing env var", () => {
88
- delete process.env.CORE_TEST_VAR;
89
- const result = interpolateEnvVars("${CORE_TEST_VAR}");
90
- expect(result.ok).toBe(false);
91
- if (!result.ok) expect(result.error).toContain("CORE_TEST_VAR");
92
- });
93
-
94
- test("returns string unchanged when no vars present", () => {
95
- const result = interpolateEnvVars("plain string");
96
- expect(result.ok).toBe(true);
97
- if (result.ok) expect(result.value).toBe("plain string");
98
- });
99
-
100
- test("ignores malformed env var references", () => {
101
- const result = interpolateEnvVars("${not valid}");
102
- expect(result.ok).toBe(true);
103
- if (result.ok) expect(result.value).toBe("${not valid}");
104
- });
105
-
106
- test("ignores empty braces", () => {
107
- const result = interpolateEnvVars("${}");
108
- expect(result.ok).toBe(true);
109
- if (result.ok) expect(result.value).toBe("${}");
110
- });
111
- });
112
-
113
- // --- interpolateDeep ---
114
-
115
- describe("interpolateDeep", () => {
116
- test("interpolates strings in nested objects", () => {
117
- process.env.CORE_TEST_VAR = "world";
118
- const result = interpolateDeep({
119
- greeting: "hello ${CORE_TEST_VAR}",
120
- nested: { value: "${CORE_TEST_VAR}" },
121
- });
122
- expect(result.ok).toBe(true);
123
- if (result.ok) {
124
- expect(result.value).toEqual({
125
- greeting: "hello world",
126
- nested: { value: "world" },
127
- });
128
- }
129
- });
130
-
131
- test("interpolates strings in arrays", () => {
132
- process.env.CORE_TEST_VAR = "item";
133
- const result = interpolateDeep(["${CORE_TEST_VAR}", 42, true]);
134
- expect(result.ok).toBe(true);
135
- if (result.ok) {
136
- expect(result.value).toEqual(["item", 42, true]);
137
- }
138
- });
139
-
140
- test("passes through non-string primitives", () => {
141
- const result = interpolateDeep({ num: 42, bool: true, nil: null });
142
- expect(result.ok).toBe(true);
143
- if (result.ok) {
144
- expect(result.value).toEqual({ num: 42, bool: true, nil: null });
145
- }
146
- });
147
-
148
- test("returns err on missing env var in nested value", () => {
149
- delete process.env.CORE_TEST_VAR;
150
- const result = interpolateDeep({ deep: { value: "${CORE_TEST_VAR}" } });
151
- expect(result.ok).toBe(false);
152
- });
153
- });
154
-
155
- // --- parsePort ---
156
-
157
- describe("parsePort", () => {
158
- test("valid port returns Ok with branded Port", () => {
159
- const result = parsePort("8080", "TEST");
160
- expect(result.ok).toBe(true);
161
- if (result.ok) expect(result.value as number).toBe(8080);
162
- });
163
-
164
- test("port 1 is valid", () => {
165
- const result = parsePort("1", "TEST");
166
- expect(result.ok).toBe(true);
167
- });
168
-
169
- test("port 65535 is valid", () => {
170
- const result = parsePort("65535", "TEST");
171
- expect(result.ok).toBe(true);
172
- });
173
-
174
- test("port 0 is invalid", () => {
175
- const result = parsePort("0", "TEST");
176
- expect(result.ok).toBe(false);
177
- });
178
-
179
- test("port 65536 is invalid", () => {
180
- const result = parsePort("65536", "TEST");
181
- expect(result.ok).toBe(false);
182
- });
183
-
184
- test("non-numeric returns error", () => {
185
- const result = parsePort("abc", "SOURCE");
186
- expect(result.ok).toBe(false);
187
- if (!result.ok) {
188
- expect(result.error).toContain("abc");
189
- expect(result.error).toContain("SOURCE");
190
- }
191
- });
192
- });
193
-
194
- // --- loadJsonConfig ---
195
-
196
- describe("loadJsonConfig", () => {
197
- const defaults = { host: "localhost", port: 3000 };
198
-
199
- test("returns defaults when config file missing", () => {
200
- const result = loadJsonConfig({
201
- name: "test",
202
- defaults,
203
- configPath: join(TMP, "nonexistent.json"),
204
- });
205
- expect(result.ok).toBe(true);
206
- if (result.ok) {
207
- expect(result.value.config).toEqual(defaults);
208
- expect(result.value.source).toBe("defaults");
209
- }
210
- });
211
-
212
- test("merges file config with defaults", () => {
213
- const configPath = join(TMP, "config.json");
214
- writeFileSync(configPath, JSON.stringify({ port: 9999 }));
215
-
216
- const result = loadJsonConfig({ name: "test", defaults, configPath });
217
- expect(result.ok).toBe(true);
218
- if (result.ok) {
219
- expect(result.value.config).toEqual({ host: "localhost", port: 9999 });
220
- expect(result.value.source).toBe("file");
221
- }
222
- });
223
-
224
- test("interpolates env vars in config file", () => {
225
- process.env.CORE_TEST_VAR = "from-env";
226
- const configPath = join(TMP, "config.json");
227
- writeFileSync(configPath, JSON.stringify({ host: "${CORE_TEST_VAR}" }));
228
-
229
- const result = loadJsonConfig({ name: "test", defaults, configPath });
230
- expect(result.ok).toBe(true);
231
- if (result.ok) {
232
- expect(result.value.config.host).toBe("from-env");
233
- }
234
- });
235
-
236
- test("returns err for invalid JSON", () => {
237
- const configPath = join(TMP, "config.json");
238
- writeFileSync(configPath, "not json{{{");
239
-
240
- const result = loadJsonConfig({ name: "test", defaults, configPath });
241
- expect(result.ok).toBe(false);
242
- if (!result.ok) expect(result.error).toContain("invalid JSON");
243
- });
244
-
245
- test("returns err for non-object JSON", () => {
246
- const configPath = join(TMP, "config.json");
247
- writeFileSync(configPath, "[1, 2, 3]");
248
-
249
- const result = loadJsonConfig({ name: "test", defaults, configPath });
250
- expect(result.ok).toBe(false);
251
- if (!result.ok) expect(result.error).toContain("must be a JSON object");
252
- });
253
-
254
- test("returns err for missing env var in config", () => {
255
- delete process.env.CORE_TEST_VAR;
256
- const configPath = join(TMP, "config.json");
257
- writeFileSync(configPath, JSON.stringify({ host: "${CORE_TEST_VAR}" }));
258
-
259
- const result = loadJsonConfig({ name: "test", defaults, configPath });
260
- expect(result.ok).toBe(false);
261
- if (!result.ok) expect(result.error).toContain("CORE_TEST_VAR");
262
- });
263
- });
@@ -1,89 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
- import { join } from "path";
4
- import type { DaemonManager } from "../src/daemon";
5
- import { createDaemonManager } from "../src/daemon";
6
-
7
- const TMP = join(import.meta.dir, ".tmp-daemon");
8
-
9
- function setup(): string {
10
- if (existsSync(TMP)) rmSync(TMP, { recursive: true });
11
- mkdirSync(TMP, { recursive: true });
12
- return TMP;
13
- }
14
-
15
- function teardown() {
16
- if (existsSync(TMP)) rmSync(TMP, { recursive: true });
17
- }
18
-
19
- describe("createDaemonManager", () => {
20
- test("creates a manager with all methods", () => {
21
- const configDir = setup();
22
- try {
23
- const manager = createDaemonManager({
24
- name: "test",
25
- configDir,
26
- cliPath: "/nonexistent/cli.ts",
27
- });
28
- expect(typeof manager.start).toBe("function");
29
- expect(typeof manager.stop).toBe("function");
30
- expect(typeof manager.restart).toBe("function");
31
- expect(typeof manager.status).toBe("function");
32
- } finally {
33
- teardown();
34
- }
35
- });
36
-
37
- test("status returns not running when no PID file", async () => {
38
- const configDir = setup();
39
- try {
40
- const manager = createDaemonManager({
41
- name: "test",
42
- configDir,
43
- cliPath: "/nonexistent/cli.ts",
44
- });
45
- const status = await manager.status();
46
- expect(status.running).toBe(false);
47
- expect(status.pid).toBeUndefined();
48
- } finally {
49
- teardown();
50
- }
51
- });
52
-
53
- test("status cleans up stale PID file", async () => {
54
- const configDir = setup();
55
- try {
56
- // Write a PID that doesn't correspond to a running process
57
- writeFileSync(join(configDir, "test.pid"), "999999");
58
-
59
- const manager = createDaemonManager({
60
- name: "test",
61
- configDir,
62
- cliPath: "/nonexistent/cli.ts",
63
- });
64
- const status = await manager.status();
65
- expect(status.running).toBe(false);
66
-
67
- // PID file should be cleaned up
68
- expect(existsSync(join(configDir, "test.pid"))).toBe(false);
69
- } finally {
70
- teardown();
71
- }
72
- });
73
-
74
- test("stop returns err when not running", async () => {
75
- const configDir = setup();
76
- try {
77
- const manager = createDaemonManager({
78
- name: "test",
79
- configDir,
80
- cliPath: "/nonexistent/cli.ts",
81
- });
82
- const result = await manager.stop();
83
- expect(result.ok).toBe(false);
84
- if (!result.ok) expect(result.error).toContain("not running");
85
- } finally {
86
- teardown();
87
- }
88
- });
89
- });
package/test/http.test.ts DELETED
@@ -1,152 +0,0 @@
1
- import { afterAll, describe, expect, test } from "bun:test";
2
- import {
3
- corsHeaders,
4
- corsPreflightResponse,
5
- createServer,
6
- healthResponse,
7
- jsonError,
8
- jsonOk,
9
- } from "../src/http";
10
-
11
- // --- corsHeaders ---
12
-
13
- describe("corsHeaders", () => {
14
- test("returns CORS headers", () => {
15
- const headers = corsHeaders();
16
- expect(headers["Access-Control-Allow-Origin"]).toBe("*");
17
- expect(headers["Access-Control-Allow-Methods"]).toContain("GET");
18
- expect(headers["Access-Control-Allow-Headers"]).toContain("Content-Type");
19
- });
20
-
21
- test("returns a fresh copy each time", () => {
22
- const a = corsHeaders();
23
- const b = corsHeaders();
24
- expect(a).toEqual(b);
25
- a["X-Custom"] = "mutated";
26
- expect(b["X-Custom"]).toBeUndefined();
27
- });
28
- });
29
-
30
- // --- corsPreflightResponse ---
31
-
32
- describe("corsPreflightResponse", () => {
33
- test("returns 204 with CORS headers", () => {
34
- const res = corsPreflightResponse();
35
- expect(res.status).toBe(204);
36
- expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*");
37
- });
38
- });
39
-
40
- // --- jsonOk ---
41
-
42
- describe("jsonOk", () => {
43
- test("returns JSON with 200 by default", async () => {
44
- const res = jsonOk({ message: "hello" });
45
- expect(res.status).toBe(200);
46
- const body = await res.json();
47
- expect(body).toEqual({ message: "hello" });
48
- });
49
-
50
- test("supports custom status code", () => {
51
- const res = jsonOk({ id: 1 }, 201);
52
- expect(res.status).toBe(201);
53
- });
54
-
55
- test("includes CORS headers", () => {
56
- const res = jsonOk({});
57
- expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*");
58
- });
59
- });
60
-
61
- // --- jsonError ---
62
-
63
- describe("jsonError", () => {
64
- test("returns error JSON", async () => {
65
- const res = jsonError(400, "bad request");
66
- expect(res.status).toBe(400);
67
- const body = await res.json();
68
- expect(body).toEqual({ error: "bad request" });
69
- });
70
-
71
- test("includes CORS headers", () => {
72
- const res = jsonError(500, "oops");
73
- expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*");
74
- });
75
- });
76
-
77
- // --- healthResponse ---
78
-
79
- describe("healthResponse", () => {
80
- test("returns standard health fields", async () => {
81
- const startTime = Date.now() - 5000;
82
- const res = healthResponse("1.0.0", startTime);
83
- expect(res.status).toBe(200);
84
- const body = (await res.json()) as Record<string, unknown>;
85
- expect(body.status).toBe("healthy");
86
- expect(body.version).toBe("1.0.0");
87
- expect(typeof body.uptime).toBe("number");
88
- expect((body.uptime as number) >= 4).toBe(true);
89
- });
90
-
91
- test("includes extra fields when provided", async () => {
92
- const res = healthResponse("1.0.0", Date.now(), {
93
- memories: 42,
94
- });
95
- const body = (await res.json()) as Record<string, unknown>;
96
- expect(body.memories).toBe(42);
97
- });
98
- });
99
-
100
- // --- createServer ---
101
-
102
- describe("createServer", () => {
103
- let server: ReturnType<typeof createServer> | null = null;
104
-
105
- afterAll(() => {
106
- if (server) server.stop();
107
- });
108
-
109
- test("starts server and responds to /health", async () => {
110
- server = createServer({
111
- port: 0, // random available port
112
- version: "0.1.0-test",
113
- onRequest: () => null,
114
- });
115
-
116
- const res = await fetch(`http://localhost:${server.port}/health`);
117
- expect(res.status).toBe(200);
118
- const body = (await res.json()) as Record<string, unknown>;
119
- expect(body.status).toBe("healthy");
120
- expect(body.version).toBe("0.1.0-test");
121
- });
122
-
123
- test("handles OPTIONS preflight", async () => {
124
- const res = await fetch(`http://localhost:${server!.port}/anything`, {
125
- method: "OPTIONS",
126
- });
127
- expect(res.status).toBe(204);
128
- expect(res.headers.get("Access-Control-Allow-Origin")).toBe("*");
129
- });
130
-
131
- test("routes to onRequest for custom paths", async () => {
132
- server!.stop();
133
- server = createServer({
134
- port: 0,
135
- version: "0.1.0-test",
136
- onRequest: (_req, url) => {
137
- if (url.pathname === "/echo") return jsonOk({ path: "/echo" });
138
- return null;
139
- },
140
- });
141
-
142
- const res = await fetch(`http://localhost:${server.port}/echo`);
143
- expect(res.status).toBe(200);
144
- const body = (await res.json()) as Record<string, unknown>;
145
- expect(body.path).toBe("/echo");
146
- });
147
-
148
- test("returns 404 when onRequest returns null", async () => {
149
- const res = await fetch(`http://localhost:${server!.port}/nonexistent`);
150
- expect(res.status).toBe(404);
151
- });
152
- });
@@ -1,58 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { err, ok } from "../src/result";
3
-
4
- describe("ok", () => {
5
- test("creates Ok result", () => {
6
- const result = ok(42);
7
- expect(result.ok).toBe(true);
8
- expect(result.value).toBe(42);
9
- });
10
-
11
- test("works with string values", () => {
12
- const result = ok("hello");
13
- expect(result.ok).toBe(true);
14
- expect(result.value).toBe("hello");
15
- });
16
-
17
- test("works with objects", () => {
18
- const result = ok({ name: "test" });
19
- expect(result.ok).toBe(true);
20
- expect(result.value).toEqual({ name: "test" });
21
- });
22
- });
23
-
24
- describe("err", () => {
25
- test("creates Err result", () => {
26
- const result = err("something went wrong");
27
- expect(result.ok).toBe(false);
28
- expect(result.error).toBe("something went wrong");
29
- });
30
-
31
- test("works with structured errors", () => {
32
- const result = err({ code: "NOT_FOUND", message: "missing" });
33
- expect(result.ok).toBe(false);
34
- expect(result.error).toEqual({ code: "NOT_FOUND", message: "missing" });
35
- });
36
- });
37
-
38
- describe("type narrowing", () => {
39
- test("narrows Ok in conditional", () => {
40
- const result = ok(42);
41
- if (result.ok) {
42
- // TypeScript knows this is Ok<number>
43
- expect(result.value).toBe(42);
44
- } else {
45
- throw new Error("should not reach");
46
- }
47
- });
48
-
49
- test("narrows Err in conditional", () => {
50
- const result = err("bad");
51
- if (!result.ok) {
52
- // TypeScript knows this is Err<string>
53
- expect(result.error).toBe("bad");
54
- } else {
55
- throw new Error("should not reach");
56
- }
57
- });
58
- });
@@ -1,25 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { onShutdown } from "../src/signals";
3
-
4
- describe("onShutdown", () => {
5
- test("registers handler without throwing", () => {
6
- // onShutdown registers process listeners — verify it doesn't throw.
7
- // We can't easily test signal delivery in unit tests, but we can
8
- // verify the function accepts valid inputs.
9
- expect(() => {
10
- onShutdown(() => {}, { signals: [] });
11
- }).not.toThrow();
12
- });
13
-
14
- test("accepts async cleanup function", () => {
15
- expect(() => {
16
- onShutdown(async () => {}, { signals: [] });
17
- }).not.toThrow();
18
- });
19
-
20
- test("accepts custom signals list", () => {
21
- expect(() => {
22
- onShutdown(() => {}, { signals: ["SIGUSR1"], timeoutMs: 1000 });
23
- }).not.toThrow();
24
- });
25
- });
@@ -1,55 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
- import { join } from "path";
4
- import { readVersion } from "../src/version";
5
-
6
- const TMP = join(import.meta.dir, ".tmp-version");
7
-
8
- function setup() {
9
- if (existsSync(TMP)) rmSync(TMP, { recursive: true });
10
- mkdirSync(TMP, { recursive: true });
11
- }
12
-
13
- function teardown() {
14
- if (existsSync(TMP)) rmSync(TMP, { recursive: true });
15
- }
16
-
17
- describe("readVersion", () => {
18
- test("returns fallback when VERSION file missing", () => {
19
- setup();
20
- try {
21
- expect(readVersion(TMP)).toBe("0.0.0-dev");
22
- } finally {
23
- teardown();
24
- }
25
- });
26
-
27
- test("returns custom fallback when VERSION file missing", () => {
28
- setup();
29
- try {
30
- expect(readVersion(TMP, "1.0.0-local")).toBe("1.0.0-local");
31
- } finally {
32
- teardown();
33
- }
34
- });
35
-
36
- test("reads VERSION file when present", () => {
37
- setup();
38
- try {
39
- writeFileSync(join(TMP, "VERSION"), "2.3.4\n");
40
- expect(readVersion(TMP)).toBe("2.3.4");
41
- } finally {
42
- teardown();
43
- }
44
- });
45
-
46
- test("trims whitespace from VERSION file", () => {
47
- setup();
48
- try {
49
- writeFileSync(join(TMP, "VERSION"), " 1.0.0 \n\n");
50
- expect(readVersion(TMP)).toBe("1.0.0");
51
- } finally {
52
- teardown();
53
- }
54
- });
55
- });
package/tsconfig.json DELETED
@@ -1,18 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "target": "ESNext",
4
- "module": "ESNext",
5
- "moduleResolution": "bundler",
6
- "strict": true,
7
- "skipLibCheck": true,
8
- "noEmit": true,
9
- "esModuleInterop": true,
10
- "allowSyntheticDefaultImports": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "resolveJsonModule": true,
13
- "isolatedModules": true,
14
- "types": ["bun"]
15
- },
16
- "include": ["src/**/*", "test/**/*"],
17
- "exclude": ["node_modules"]
18
- }