teslacode.dev 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,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultOsControl = void 0;
4
+ exports.execOsControl = execOsControl;
5
+ const node_child_process_1 = require("node:child_process");
6
+ function execOsControl(msg) {
7
+ if (!msg || typeof msg !== "object")
8
+ return;
9
+ const m = msg;
10
+ if (m.type === "key") {
11
+ if (m.key === "space") {
12
+ try {
13
+ (0, node_child_process_1.execSync)("osascript", { input: 'tell application "System Events" to keystroke " "' });
14
+ }
15
+ catch (err) {
16
+ console.log("[teslacode.dev] osascript failed:", "Accessibility permission required? Check System Settings → Privacy & Security → Accessibility. Error: " + String(err));
17
+ }
18
+ }
19
+ else if (m.key === "enter") {
20
+ try {
21
+ (0, node_child_process_1.execSync)("osascript", { input: 'tell application "System Events" to keystroke return' });
22
+ }
23
+ catch (err) {
24
+ console.log("[teslacode.dev] osascript failed:", "Accessibility permission required? Check System Settings → Privacy & Security → Accessibility. Error: " + String(err));
25
+ }
26
+ }
27
+ }
28
+ }
29
+ exports.defaultOsControl = {
30
+ execute: execOsControl,
31
+ };
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const node_child_process_1 = require("node:child_process");
5
+ vitest_1.vi.mock("node:child_process", () => ({
6
+ execSync: vitest_1.vi.fn(),
7
+ }));
8
+ const control_js_1 = require("./control.js");
9
+ (0, vitest_1.beforeEach)(() => {
10
+ vitest_1.vi.mocked(node_child_process_1.execSync).mockReset();
11
+ });
12
+ (0, vitest_1.describe)("execOsControl — key", () => {
13
+ (0, vitest_1.it)("dispatches osascript for space key", () => {
14
+ (0, control_js_1.execOsControl)({ type: "key", key: "space" });
15
+ (0, vitest_1.expect)(node_child_process_1.execSync).toHaveBeenCalledOnce();
16
+ const [cmd] = vitest_1.vi.mocked(node_child_process_1.execSync).mock.calls[0];
17
+ (0, vitest_1.expect)(cmd).toBe("osascript");
18
+ const opts = vitest_1.vi.mocked(node_child_process_1.execSync).mock.calls[0][1];
19
+ (0, vitest_1.expect)(opts.input).toContain('keystroke " "');
20
+ });
21
+ (0, vitest_1.it)("dispatches osascript for enter key", () => {
22
+ (0, control_js_1.execOsControl)({ type: "key", key: "enter" });
23
+ (0, vitest_1.expect)(node_child_process_1.execSync).toHaveBeenCalledOnce();
24
+ const [cmd] = vitest_1.vi.mocked(node_child_process_1.execSync).mock.calls[0];
25
+ (0, vitest_1.expect)(cmd).toBe("osascript");
26
+ const opts = vitest_1.vi.mocked(node_child_process_1.execSync).mock.calls[0][1];
27
+ (0, vitest_1.expect)(opts.input).toContain("keystroke return");
28
+ });
29
+ });
30
+ (0, vitest_1.describe)("execOsControl — no-ops", () => {
31
+ (0, vitest_1.it)("unknown key is a no-op", () => {
32
+ (0, control_js_1.execOsControl)({ type: "key", key: "tab" });
33
+ (0, vitest_1.expect)(node_child_process_1.execSync).not.toHaveBeenCalled();
34
+ });
35
+ (0, vitest_1.it)("non-object input is a no-op", () => {
36
+ (0, control_js_1.execOsControl)("space");
37
+ (0, vitest_1.expect)(node_child_process_1.execSync).not.toHaveBeenCalled();
38
+ });
39
+ (0, vitest_1.it)("null is a no-op", () => {
40
+ (0, control_js_1.execOsControl)(null);
41
+ (0, vitest_1.expect)(node_child_process_1.execSync).not.toHaveBeenCalled();
42
+ });
43
+ (0, vitest_1.it)("unknown type is a no-op", () => {
44
+ (0, control_js_1.execOsControl)({ type: "click", x: 100, y: 200 });
45
+ (0, vitest_1.expect)(node_child_process_1.execSync).not.toHaveBeenCalled();
46
+ });
47
+ });
48
+ (0, vitest_1.describe)("execOsControl — error handling", () => {
49
+ (0, vitest_1.it)("logs human-readable error when osascript exits non-zero", () => {
50
+ vitest_1.vi.mocked(node_child_process_1.execSync).mockImplementation(() => {
51
+ throw new Error("Command failed: osascript exit code 1");
52
+ });
53
+ const consoleSpy = vitest_1.vi.spyOn(console, "log").mockImplementation(() => { });
54
+ (0, control_js_1.execOsControl)({ type: "key", key: "space" });
55
+ (0, vitest_1.expect)(consoleSpy).toHaveBeenCalledWith(vitest_1.expect.stringContaining("[teslacode.dev]"), vitest_1.expect.stringContaining("Accessibility"));
56
+ consoleSpy.mockRestore();
57
+ });
58
+ });
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const server_js_1 = require("./server.js");
5
+ const control_js_1 = require("./control.js");
6
+ const PORT = parseInt(process.env["TESLACODE_PORT"] ?? "27184", 10);
7
+ const server = (0, server_js_1.createDaemonServer)(PORT, control_js_1.defaultOsControl);
8
+ server.on("error", (err) => {
9
+ if (err.code === "EADDRINUSE") {
10
+ console.error(`[teslacode.dev] daemon already running on port ${PORT} — exiting`);
11
+ }
12
+ else {
13
+ console.error("[teslacode.dev] server error:", err.message);
14
+ }
15
+ process.exit(1);
16
+ });
17
+ server.listen(PORT, "127.0.0.1", () => {
18
+ console.log(`[teslacode.dev] running on http://localhost:${PORT}`);
19
+ console.log("[teslacode.dev] Reminder: grant Accessibility permission in System Settings → Privacy & Security → Accessibility");
20
+ });
package/dist/server.js ADDED
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.createDaemonServer = createDaemonServer;
37
+ const http = __importStar(require("node:http"));
38
+ const VERSION = "1.0.0";
39
+ function setCors(res) {
40
+ res.setHeader("Access-Control-Allow-Origin", "*");
41
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
42
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
43
+ }
44
+ function json(res, status, body) {
45
+ setCors(res);
46
+ res.writeHead(status, { "Content-Type": "application/json" });
47
+ res.end(JSON.stringify(body));
48
+ }
49
+ function readBody(req) {
50
+ return new Promise((resolve, reject) => {
51
+ const chunks = [];
52
+ req.on("data", (chunk) => chunks.push(chunk));
53
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
54
+ req.on("error", reject);
55
+ });
56
+ }
57
+ function createDaemonServer(port, osControl) {
58
+ const server = http.createServer(async (req, res) => {
59
+ if (req.method === "OPTIONS") {
60
+ setCors(res);
61
+ res.writeHead(204);
62
+ res.end();
63
+ return;
64
+ }
65
+ if (req.method === "GET" && req.url === "/health") {
66
+ json(res, 200, { ok: true, version: VERSION });
67
+ return;
68
+ }
69
+ if (req.method === "POST" && req.url === "/control") {
70
+ let body;
71
+ try {
72
+ body = await readBody(req);
73
+ }
74
+ catch {
75
+ json(res, 500, { ok: false, error: "failed to read body" });
76
+ return;
77
+ }
78
+ let msg;
79
+ try {
80
+ msg = JSON.parse(body);
81
+ }
82
+ catch {
83
+ json(res, 400, { ok: false, error: "invalid message" });
84
+ return;
85
+ }
86
+ if (msg && typeof msg === "object" && msg.type === "key") {
87
+ osControl.execute(msg);
88
+ }
89
+ json(res, 200, { ok: true });
90
+ return;
91
+ }
92
+ json(res, 404, { ok: false, error: "not found" });
93
+ });
94
+ return server;
95
+ }
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ // Import server after mocking (stubs replaced during T009)
38
+ let createDaemonServer;
39
+ async function loadServer() {
40
+ const mod = await Promise.resolve().then(() => __importStar(require("./server.js")));
41
+ createDaemonServer = mod.createDaemonServer;
42
+ }
43
+ function makeOsControl() {
44
+ return { execute: vitest_1.vi.fn() };
45
+ }
46
+ async function startServer(osControl) {
47
+ await loadServer();
48
+ const server = createDaemonServer(0, osControl);
49
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
50
+ const addr = server.address();
51
+ return { server, port: addr.port };
52
+ }
53
+ async function stopServer(server) {
54
+ await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
55
+ }
56
+ async function req(port, method, path, body) {
57
+ const bodyStr = body !== undefined ? JSON.stringify(body) : undefined;
58
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
59
+ method,
60
+ headers: body !== undefined ? { "Content-Type": "application/json" } : {},
61
+ body: bodyStr,
62
+ });
63
+ let json;
64
+ try {
65
+ json = await res.json();
66
+ }
67
+ catch {
68
+ json = null;
69
+ }
70
+ return {
71
+ status: res.status,
72
+ headers: Object.fromEntries(res.headers.entries()),
73
+ json,
74
+ };
75
+ }
76
+ (0, vitest_1.describe)("GET /health", () => {
77
+ let server;
78
+ let port;
79
+ (0, vitest_1.beforeEach)(async () => {
80
+ const result = await startServer(makeOsControl());
81
+ server = result.server;
82
+ port = result.port;
83
+ });
84
+ (0, vitest_1.afterEach)(async () => {
85
+ await stopServer(server);
86
+ });
87
+ (0, vitest_1.it)("returns 200 with { ok: true, version }", async () => {
88
+ const r = await req(port, "GET", "/health");
89
+ (0, vitest_1.expect)(r.status).toBe(200);
90
+ (0, vitest_1.expect)(r.json.ok).toBe(true);
91
+ (0, vitest_1.expect)(typeof r.json.version).toBe("string");
92
+ });
93
+ (0, vitest_1.it)("includes Access-Control-Allow-Origin: * header", async () => {
94
+ const r = await req(port, "GET", "/health");
95
+ (0, vitest_1.expect)(r.headers["access-control-allow-origin"]).toBe("*");
96
+ });
97
+ });
98
+ (0, vitest_1.describe)("POST /control", () => {
99
+ let server;
100
+ let port;
101
+ let osControl;
102
+ (0, vitest_1.beforeEach)(async () => {
103
+ osControl = makeOsControl();
104
+ const result = await startServer(osControl);
105
+ server = result.server;
106
+ port = result.port;
107
+ });
108
+ (0, vitest_1.afterEach)(async () => {
109
+ await stopServer(server);
110
+ });
111
+ (0, vitest_1.it)("calls OsControl.execute once for { type: 'key', key: 'space' }", async () => {
112
+ const r = await req(port, "POST", "/control", { type: "key", key: "space" });
113
+ (0, vitest_1.expect)(r.status).toBe(200);
114
+ (0, vitest_1.expect)(osControl.execute).toHaveBeenCalledOnce();
115
+ (0, vitest_1.expect)(osControl.execute).toHaveBeenCalledWith({ type: "key", key: "space" });
116
+ });
117
+ (0, vitest_1.it)("calls OsControl.execute once for { type: 'key', key: 'enter' }", async () => {
118
+ const r = await req(port, "POST", "/control", { type: "key", key: "enter" });
119
+ (0, vitest_1.expect)(r.status).toBe(200);
120
+ (0, vitest_1.expect)(osControl.execute).toHaveBeenCalledOnce();
121
+ });
122
+ (0, vitest_1.it)("returns 400 for malformed body", async () => {
123
+ const res = await fetch(`http://127.0.0.1:${port}/control`, {
124
+ method: "POST",
125
+ headers: { "Content-Type": "application/json" },
126
+ body: "not-json{",
127
+ });
128
+ (0, vitest_1.expect)(res.status).toBe(400);
129
+ const json = await res.json();
130
+ (0, vitest_1.expect)(json.ok).toBe(false);
131
+ (0, vitest_1.expect)(typeof json.error).toBe("string");
132
+ });
133
+ (0, vitest_1.it)("returns 200 and silently drops unknown type", async () => {
134
+ const r = await req(port, "POST", "/control", { type: "hover", x: 100, y: 200 });
135
+ (0, vitest_1.expect)(r.status).toBe(200);
136
+ (0, vitest_1.expect)(osControl.execute).not.toHaveBeenCalled();
137
+ });
138
+ (0, vitest_1.it)("includes Access-Control-Allow-Origin: * on all responses", async () => {
139
+ const r = await req(port, "POST", "/control", { type: "key", key: "space" });
140
+ (0, vitest_1.expect)(r.headers["access-control-allow-origin"]).toBe("*");
141
+ });
142
+ });
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "teslacode.dev",
3
+ "version": "1.0.0",
4
+ "description": "Local control daemon for Tesla Screen Share",
5
+ "bin": {
6
+ "teslacode.dev": "./dist/index.js"
7
+ },
8
+ "engines": {
9
+ "node": ">=18"
10
+ },
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "start": "node dist/index.js",
14
+ "test": "vitest run"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20.0.0",
18
+ "typescript": "^5.7.3",
19
+ "vitest": "^2.1.8"
20
+ }
21
+ }
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { execSync } from "node:child_process";
3
+
4
+ vi.mock("node:child_process", () => ({
5
+ execSync: vi.fn(),
6
+ }));
7
+
8
+ import { execOsControl } from "./control.js";
9
+
10
+ beforeEach(() => {
11
+ vi.mocked(execSync).mockReset();
12
+ });
13
+
14
+ describe("execOsControl — key", () => {
15
+ it("dispatches osascript for space key", () => {
16
+ execOsControl({ type: "key", key: "space" });
17
+ expect(execSync).toHaveBeenCalledOnce();
18
+ const [cmd] = vi.mocked(execSync).mock.calls[0];
19
+ expect(cmd).toBe("osascript");
20
+ const opts = vi.mocked(execSync).mock.calls[0][1] as { input: string };
21
+ expect(opts.input).toContain('keystroke " "');
22
+ });
23
+
24
+ it("dispatches osascript for enter key", () => {
25
+ execOsControl({ type: "key", key: "enter" });
26
+ expect(execSync).toHaveBeenCalledOnce();
27
+ const [cmd] = vi.mocked(execSync).mock.calls[0];
28
+ expect(cmd).toBe("osascript");
29
+ const opts = vi.mocked(execSync).mock.calls[0][1] as { input: string };
30
+ expect(opts.input).toContain("keystroke return");
31
+ });
32
+ });
33
+
34
+ describe("execOsControl — no-ops", () => {
35
+ it("unknown key is a no-op", () => {
36
+ execOsControl({ type: "key", key: "tab" });
37
+ expect(execSync).not.toHaveBeenCalled();
38
+ });
39
+
40
+ it("non-object input is a no-op", () => {
41
+ execOsControl("space");
42
+ expect(execSync).not.toHaveBeenCalled();
43
+ });
44
+
45
+ it("null is a no-op", () => {
46
+ execOsControl(null);
47
+ expect(execSync).not.toHaveBeenCalled();
48
+ });
49
+
50
+ it("unknown type is a no-op", () => {
51
+ execOsControl({ type: "click", x: 100, y: 200 });
52
+ expect(execSync).not.toHaveBeenCalled();
53
+ });
54
+ });
55
+
56
+ describe("execOsControl — error handling", () => {
57
+ it("logs human-readable error when osascript exits non-zero", () => {
58
+ vi.mocked(execSync).mockImplementation(() => {
59
+ throw new Error("Command failed: osascript exit code 1");
60
+ });
61
+ const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
62
+
63
+ execOsControl({ type: "key", key: "space" });
64
+
65
+ expect(consoleSpy).toHaveBeenCalledWith(
66
+ expect.stringContaining("[teslacode.dev]"),
67
+ expect.stringContaining("Accessibility"),
68
+ );
69
+ consoleSpy.mockRestore();
70
+ });
71
+ });
package/src/control.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { execSync } from "node:child_process";
2
+
3
+ export type ControlMessage = { type: "key"; key: "space" | "enter" };
4
+
5
+ export interface OsControl {
6
+ execute(msg: unknown): void;
7
+ }
8
+
9
+ export function execOsControl(msg: unknown): void {
10
+ if (!msg || typeof msg !== "object") return;
11
+ const m = msg as Record<string, unknown>;
12
+
13
+ if (m.type === "key") {
14
+ if (m.key === "space") {
15
+ try {
16
+ execSync("osascript", { input: 'tell application "System Events" to keystroke " "' });
17
+ } catch (err) {
18
+ console.log("[teslacode.dev] osascript failed:", "Accessibility permission required? Check System Settings → Privacy & Security → Accessibility. Error: " + String(err));
19
+ }
20
+ } else if (m.key === "enter") {
21
+ try {
22
+ execSync("osascript", { input: 'tell application "System Events" to keystroke return' });
23
+ } catch (err) {
24
+ console.log("[teslacode.dev] osascript failed:", "Accessibility permission required? Check System Settings → Privacy & Security → Accessibility. Error: " + String(err));
25
+ }
26
+ }
27
+ }
28
+ }
29
+
30
+ export const defaultOsControl: OsControl = {
31
+ execute: execOsControl,
32
+ };
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { createDaemonServer } from "./server.js";
3
+ import { defaultOsControl } from "./control.js";
4
+
5
+ const PORT = parseInt(process.env["TESLACODE_PORT"] ?? "27184", 10);
6
+
7
+ const server = createDaemonServer(PORT, defaultOsControl);
8
+
9
+ server.on("error", (err: NodeJS.ErrnoException) => {
10
+ if (err.code === "EADDRINUSE") {
11
+ console.error(`[teslacode.dev] daemon already running on port ${PORT} — exiting`);
12
+ } else {
13
+ console.error("[teslacode.dev] server error:", err.message);
14
+ }
15
+ process.exit(1);
16
+ });
17
+
18
+ server.listen(PORT, "127.0.0.1", () => {
19
+ console.log(`[teslacode.dev] running on http://localhost:${PORT}`);
20
+ console.log("[teslacode.dev] Reminder: grant Accessibility permission in System Settings → Privacy & Security → Accessibility");
21
+ });
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ import * as http from "node:http";
3
+ import type { OsControl } from "./control.js";
4
+
5
+ // Import server after mocking (stubs replaced during T009)
6
+ let createDaemonServer: (port: number, osControl: OsControl) => http.Server;
7
+
8
+ async function loadServer() {
9
+ const mod = await import("./server.js");
10
+ createDaemonServer = mod.createDaemonServer;
11
+ }
12
+
13
+ function makeOsControl(): OsControl & { execute: ReturnType<typeof vi.fn> } {
14
+ return { execute: vi.fn() };
15
+ }
16
+
17
+ async function startServer(osControl: OsControl): Promise<{ server: http.Server; port: number }> {
18
+ await loadServer();
19
+ const server = createDaemonServer(0, osControl);
20
+ await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
21
+ const addr = server.address() as { port: number };
22
+ return { server, port: addr.port };
23
+ }
24
+
25
+ async function stopServer(server: http.Server): Promise<void> {
26
+ await new Promise<void>((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
27
+ }
28
+
29
+ async function req(port: number, method: string, path: string, body?: unknown): Promise<{ status: number; headers: Record<string, string | string[] | undefined>; json: unknown }> {
30
+ const bodyStr = body !== undefined ? JSON.stringify(body) : undefined;
31
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
32
+ method,
33
+ headers: body !== undefined ? { "Content-Type": "application/json" } : {},
34
+ body: bodyStr,
35
+ });
36
+ let json: unknown;
37
+ try { json = await res.json(); } catch { json = null; }
38
+ return {
39
+ status: res.status,
40
+ headers: Object.fromEntries(res.headers.entries()),
41
+ json,
42
+ };
43
+ }
44
+
45
+ describe("GET /health", () => {
46
+ let server: http.Server;
47
+ let port: number;
48
+
49
+ beforeEach(async () => {
50
+ const result = await startServer(makeOsControl());
51
+ server = result.server;
52
+ port = result.port;
53
+ });
54
+
55
+ afterEach(async () => {
56
+ await stopServer(server);
57
+ });
58
+
59
+ it("returns 200 with { ok: true, version }", async () => {
60
+ const r = await req(port, "GET", "/health");
61
+ expect(r.status).toBe(200);
62
+ expect((r.json as Record<string, unknown>).ok).toBe(true);
63
+ expect(typeof (r.json as Record<string, unknown>).version).toBe("string");
64
+ });
65
+
66
+ it("includes Access-Control-Allow-Origin: * header", async () => {
67
+ const r = await req(port, "GET", "/health");
68
+ expect(r.headers["access-control-allow-origin"]).toBe("*");
69
+ });
70
+ });
71
+
72
+ describe("POST /control", () => {
73
+ let server: http.Server;
74
+ let port: number;
75
+ let osControl: OsControl & { execute: ReturnType<typeof vi.fn> };
76
+
77
+ beforeEach(async () => {
78
+ osControl = makeOsControl();
79
+ const result = await startServer(osControl);
80
+ server = result.server;
81
+ port = result.port;
82
+ });
83
+
84
+ afterEach(async () => {
85
+ await stopServer(server);
86
+ });
87
+
88
+ it("calls OsControl.execute once for { type: 'key', key: 'space' }", async () => {
89
+ const r = await req(port, "POST", "/control", { type: "key", key: "space" });
90
+ expect(r.status).toBe(200);
91
+ expect(osControl.execute).toHaveBeenCalledOnce();
92
+ expect(osControl.execute).toHaveBeenCalledWith({ type: "key", key: "space" });
93
+ });
94
+
95
+ it("calls OsControl.execute once for { type: 'key', key: 'enter' }", async () => {
96
+ const r = await req(port, "POST", "/control", { type: "key", key: "enter" });
97
+ expect(r.status).toBe(200);
98
+ expect(osControl.execute).toHaveBeenCalledOnce();
99
+ });
100
+
101
+ it("returns 400 for malformed body", async () => {
102
+ const res = await fetch(`http://127.0.0.1:${port}/control`, {
103
+ method: "POST",
104
+ headers: { "Content-Type": "application/json" },
105
+ body: "not-json{",
106
+ });
107
+ expect(res.status).toBe(400);
108
+ const json = await res.json() as Record<string, unknown>;
109
+ expect(json.ok).toBe(false);
110
+ expect(typeof json.error).toBe("string");
111
+ });
112
+
113
+ it("returns 200 and silently drops unknown type", async () => {
114
+ const r = await req(port, "POST", "/control", { type: "hover", x: 100, y: 200 });
115
+ expect(r.status).toBe(200);
116
+ expect(osControl.execute).not.toHaveBeenCalled();
117
+ });
118
+
119
+ it("includes Access-Control-Allow-Origin: * on all responses", async () => {
120
+ const r = await req(port, "POST", "/control", { type: "key", key: "space" });
121
+ expect(r.headers["access-control-allow-origin"]).toBe("*");
122
+ });
123
+ });
package/src/server.ts ADDED
@@ -0,0 +1,70 @@
1
+ import * as http from "node:http";
2
+ import type { OsControl } from "./control.js";
3
+
4
+ const VERSION = "1.0.0";
5
+
6
+ function setCors(res: http.ServerResponse): void {
7
+ res.setHeader("Access-Control-Allow-Origin", "*");
8
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
9
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
10
+ }
11
+
12
+ function json(res: http.ServerResponse, status: number, body: unknown): void {
13
+ setCors(res);
14
+ res.writeHead(status, { "Content-Type": "application/json" });
15
+ res.end(JSON.stringify(body));
16
+ }
17
+
18
+ function readBody(req: http.IncomingMessage): Promise<string> {
19
+ return new Promise((resolve, reject) => {
20
+ const chunks: Buffer[] = [];
21
+ req.on("data", (chunk: Buffer) => chunks.push(chunk));
22
+ req.on("end", () => resolve(Buffer.concat(chunks).toString()));
23
+ req.on("error", reject);
24
+ });
25
+ }
26
+
27
+ export function createDaemonServer(port: number, osControl: OsControl): http.Server {
28
+ const server = http.createServer(async (req, res) => {
29
+ if (req.method === "OPTIONS") {
30
+ setCors(res);
31
+ res.writeHead(204);
32
+ res.end();
33
+ return;
34
+ }
35
+
36
+ if (req.method === "GET" && req.url === "/health") {
37
+ json(res, 200, { ok: true, version: VERSION });
38
+ return;
39
+ }
40
+
41
+ if (req.method === "POST" && req.url === "/control") {
42
+ let body: string;
43
+ try {
44
+ body = await readBody(req);
45
+ } catch {
46
+ json(res, 500, { ok: false, error: "failed to read body" });
47
+ return;
48
+ }
49
+
50
+ let msg: unknown;
51
+ try {
52
+ msg = JSON.parse(body);
53
+ } catch {
54
+ json(res, 400, { ok: false, error: "invalid message" });
55
+ return;
56
+ }
57
+
58
+ if (msg && typeof msg === "object" && (msg as Record<string, unknown>).type === "key") {
59
+ osControl.execute(msg);
60
+ }
61
+
62
+ json(res, 200, { ok: true });
63
+ return;
64
+ }
65
+
66
+ json(res, 404, { ok: false, error: "not found" });
67
+ });
68
+
69
+ return server;
70
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "moduleResolution": "node",
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true
12
+ },
13
+ "include": ["src/**/*"]
14
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ environment: "node",
6
+ },
7
+ });