@shepherdjerred/home-assistant 0.1.0-dev.14474
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/CHANGELOG.md +8 -0
- package/LICENSE +674 -0
- package/README.md +126 -0
- package/demos/codegen-fixture.cast +40 -0
- package/demos/home-assistant-fixture.json +33 -0
- package/demos/record-codegen-demo.sh +119 -0
- package/dist/codegen/cli.d.ts +3 -0
- package/dist/codegen/cli.d.ts.map +1 -0
- package/dist/codegen/cli.js +118 -0
- package/dist/codegen/cli.js.map +1 -0
- package/dist/codegen/emit.d.ts +8 -0
- package/dist/codegen/emit.d.ts.map +1 -0
- package/dist/codegen/emit.js +143 -0
- package/dist/codegen/emit.js.map +1 -0
- package/dist/codegen/introspect.d.ts +193 -0
- package/dist/codegen/introspect.d.ts.map +1 -0
- package/dist/codegen/introspect.js +131 -0
- package/dist/codegen/introspect.js.map +1 -0
- package/dist/codegen/known-events.d.ts +10 -0
- package/dist/codegen/known-events.d.ts.map +1 -0
- package/dist/codegen/known-events.js +25 -0
- package/dist/codegen/known-events.js.map +1 -0
- package/dist/codegen/selector-to-ts.d.ts +15 -0
- package/dist/codegen/selector-to-ts.d.ts.map +1 -0
- package/dist/codegen/selector-to-ts.js +110 -0
- package/dist/codegen/selector-to-ts.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/rest/client.d.ts +34 -0
- package/dist/rest/client.d.ts.map +1 -0
- package/dist/rest/client.js +112 -0
- package/dist/rest/client.js.map +1 -0
- package/dist/rest/errors.d.ts +12 -0
- package/dist/rest/errors.d.ts.map +1 -0
- package/dist/rest/errors.js +23 -0
- package/dist/rest/errors.js.map +1 -0
- package/dist/rest/schemas.d.ts +124 -0
- package/dist/rest/schemas.d.ts.map +1 -0
- package/dist/rest/schemas.js +64 -0
- package/dist/rest/schemas.js.map +1 -0
- package/dist/schema/types.d.ts +138 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +12 -0
- package/dist/schema/types.js.map +1 -0
- package/dist/shared/config.d.ts +6 -0
- package/dist/shared/config.d.ts.map +1 -0
- package/dist/shared/config.js +4 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/ws/client.d.ts +58 -0
- package/dist/ws/client.d.ts.map +1 -0
- package/dist/ws/client.js +343 -0
- package/dist/ws/client.js.map +1 -0
- package/dist/ws/errors.d.ts +14 -0
- package/dist/ws/errors.d.ts.map +1 -0
- package/dist/ws/errors.js +27 -0
- package/dist/ws/errors.js.map +1 -0
- package/dist/ws/messages.d.ts +130 -0
- package/dist/ws/messages.d.ts.map +1 -0
- package/dist/ws/messages.js +79 -0
- package/dist/ws/messages.js.map +1 -0
- package/dist/ws/socket-helpers.d.ts +23 -0
- package/dist/ws/socket-helpers.d.ts.map +1 -0
- package/dist/ws/socket-helpers.js +73 -0
- package/dist/ws/socket-helpers.js.map +1 -0
- package/dist/ws/subscriptions.d.ts +37 -0
- package/dist/ws/subscriptions.d.ts.map +1 -0
- package/dist/ws/subscriptions.js +58 -0
- package/dist/ws/subscriptions.js.map +1 -0
- package/examples/basic-client.mjs +18 -0
- package/examples/codegen-fixture.mjs +95 -0
- package/package.json +69 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { normalizeBaseUrl } from "../shared/config.js";
|
|
3
|
+
import { HaWebSocketClosedError, HaWebSocketError } from "./errors.js";
|
|
4
|
+
export function wait(ms) {
|
|
5
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
}
|
|
7
|
+
export function waitForOpen(socket) {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
if (socket.readyState === socket.OPEN) {
|
|
10
|
+
resolve();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const onOpen = () => {
|
|
14
|
+
cleanup();
|
|
15
|
+
resolve();
|
|
16
|
+
};
|
|
17
|
+
const onError = () => {
|
|
18
|
+
cleanup();
|
|
19
|
+
reject(new HaWebSocketError("WebSocket failed to open"));
|
|
20
|
+
};
|
|
21
|
+
const onClose = () => {
|
|
22
|
+
// If close() is called (or the server rejects the TCP/WS upgrade)
|
|
23
|
+
// while still CONNECTING, the real WebSocket emits `close` without
|
|
24
|
+
// ever firing `open`. Without this listener the promise would leak.
|
|
25
|
+
cleanup();
|
|
26
|
+
reject(new HaWebSocketClosedError());
|
|
27
|
+
};
|
|
28
|
+
const cleanup = () => {
|
|
29
|
+
socket.removeEventListener("open", onOpen);
|
|
30
|
+
socket.removeEventListener("error", onError);
|
|
31
|
+
socket.removeEventListener("close", onClose);
|
|
32
|
+
};
|
|
33
|
+
socket.addEventListener("open", onOpen);
|
|
34
|
+
socket.addEventListener("error", onError);
|
|
35
|
+
socket.addEventListener("close", onClose);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
export const MessageDataString = z.string();
|
|
39
|
+
const WebSocketEventShape = z.looseObject({ data: z.unknown() });
|
|
40
|
+
export function extractEventData(event) {
|
|
41
|
+
const parsed = WebSocketEventShape.safeParse(event);
|
|
42
|
+
return parsed.success ? parsed.data.data : undefined;
|
|
43
|
+
}
|
|
44
|
+
export function waitForFirstMessage(socket) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const onMessage = (event) => {
|
|
47
|
+
cleanup();
|
|
48
|
+
const parsed = MessageDataString.safeParse(extractEventData(event));
|
|
49
|
+
resolve(parsed.success ? parsed.data : "");
|
|
50
|
+
};
|
|
51
|
+
const onClose = () => {
|
|
52
|
+
cleanup();
|
|
53
|
+
reject(new HaWebSocketClosedError());
|
|
54
|
+
};
|
|
55
|
+
const cleanup = () => {
|
|
56
|
+
socket.removeEventListener("message", onMessage);
|
|
57
|
+
socket.removeEventListener("close", onClose);
|
|
58
|
+
};
|
|
59
|
+
socket.addEventListener("message", onMessage);
|
|
60
|
+
socket.addEventListener("close", onClose);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export function buildWsUrl(baseUrl) {
|
|
64
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
65
|
+
if (normalized.startsWith("https://")) {
|
|
66
|
+
return `${normalized.replace(/^https:\/\//u, "wss://")}/api/websocket`;
|
|
67
|
+
}
|
|
68
|
+
if (normalized.startsWith("http://")) {
|
|
69
|
+
return `${normalized.replace(/^http:\/\//u, "ws://")}/api/websocket`;
|
|
70
|
+
}
|
|
71
|
+
return `${normalized}/api/websocket`;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=socket-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"socket-helpers.js","sourceRoot":"","sources":["../../src/ws/socket-helpers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { EventEnvelope } from "./messages.js";
|
|
2
|
+
export type EventHandler = (event: EventEnvelope) => void | Promise<void>;
|
|
3
|
+
export type Subscription = {
|
|
4
|
+
kind: "event";
|
|
5
|
+
eventType: string;
|
|
6
|
+
handler: EventHandler;
|
|
7
|
+
} | {
|
|
8
|
+
kind: "trigger";
|
|
9
|
+
trigger: Record<string, unknown>;
|
|
10
|
+
handler: EventHandler;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Tracks subscriptions with two independent identifiers:
|
|
14
|
+
*
|
|
15
|
+
* - `clientKey`: stable for the lifetime of the caller-facing subscription,
|
|
16
|
+
* generated on registration. Closures returned by subscribe* capture this
|
|
17
|
+
* key so they still resolve to the right subscription after a reconnect
|
|
18
|
+
* assigns a new server-side id.
|
|
19
|
+
* - `serverId`: the HA-assigned numeric id the server uses in event and
|
|
20
|
+
* result messages. Rebound on every (re)subscribe.
|
|
21
|
+
*/
|
|
22
|
+
export declare class SubscriptionRegistry {
|
|
23
|
+
private readonly byClientKey;
|
|
24
|
+
private readonly clientKeyToServerId;
|
|
25
|
+
private readonly serverIdToClientKey;
|
|
26
|
+
private nextClientKey;
|
|
27
|
+
register(subscription: Subscription): number;
|
|
28
|
+
bindServerId(clientKey: number, serverId: number): void;
|
|
29
|
+
unregister(clientKey: number): {
|
|
30
|
+
subscription: Subscription;
|
|
31
|
+
serverId: number | undefined;
|
|
32
|
+
} | undefined;
|
|
33
|
+
getByServerId(serverId: number): Subscription | undefined;
|
|
34
|
+
clearServerIds(): void;
|
|
35
|
+
snapshot(): [number, Subscription][];
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=subscriptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriptions.d.ts","sourceRoot":"","sources":["../../src/ws/subscriptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,MAAM,MAAM,YAAY,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE1E,MAAM,MAAM,YAAY,GACpB;IACE,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,YAAY,CAAC;CACvB,GACD;IACE,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,OAAO,EAAE,YAAY,CAAC;CACvB,CAAC;AAEN;;;;;;;;;GASG;AACH,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmC;IAC/D,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA6B;IACjE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA6B;IACjE,OAAO,CAAC,aAAa,CAAK;IAEnB,QAAQ,CAAC,YAAY,EAAE,YAAY,GAAG,MAAM,CAKlD;IAEM,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAO7D;IAEM,UAAU,CAAC,SAAS,EAAE,MAAM,GAC/B;QACE,YAAY,EAAE,YAAY,CAAC;QAC3B,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;KAC9B,GACD,SAAS,CAYZ;IAEM,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAM/D;IAEM,cAAc,IAAI,IAAI,CAG5B;IAEM,QAAQ,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,EAAE,CAE1C;CACF"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tracks subscriptions with two independent identifiers:
|
|
3
|
+
*
|
|
4
|
+
* - `clientKey`: stable for the lifetime of the caller-facing subscription,
|
|
5
|
+
* generated on registration. Closures returned by subscribe* capture this
|
|
6
|
+
* key so they still resolve to the right subscription after a reconnect
|
|
7
|
+
* assigns a new server-side id.
|
|
8
|
+
* - `serverId`: the HA-assigned numeric id the server uses in event and
|
|
9
|
+
* result messages. Rebound on every (re)subscribe.
|
|
10
|
+
*/
|
|
11
|
+
export class SubscriptionRegistry {
|
|
12
|
+
byClientKey = new Map();
|
|
13
|
+
clientKeyToServerId = new Map();
|
|
14
|
+
serverIdToClientKey = new Map();
|
|
15
|
+
nextClientKey = 1;
|
|
16
|
+
register(subscription) {
|
|
17
|
+
const clientKey = this.nextClientKey;
|
|
18
|
+
this.nextClientKey += 1;
|
|
19
|
+
this.byClientKey.set(clientKey, subscription);
|
|
20
|
+
return clientKey;
|
|
21
|
+
}
|
|
22
|
+
bindServerId(clientKey, serverId) {
|
|
23
|
+
const previous = this.clientKeyToServerId.get(clientKey);
|
|
24
|
+
if (previous !== undefined) {
|
|
25
|
+
this.serverIdToClientKey.delete(previous);
|
|
26
|
+
}
|
|
27
|
+
this.clientKeyToServerId.set(clientKey, serverId);
|
|
28
|
+
this.serverIdToClientKey.set(serverId, clientKey);
|
|
29
|
+
}
|
|
30
|
+
unregister(clientKey) {
|
|
31
|
+
const subscription = this.byClientKey.get(clientKey);
|
|
32
|
+
if (subscription === undefined) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
this.byClientKey.delete(clientKey);
|
|
36
|
+
const serverId = this.clientKeyToServerId.get(clientKey);
|
|
37
|
+
this.clientKeyToServerId.delete(clientKey);
|
|
38
|
+
if (serverId !== undefined) {
|
|
39
|
+
this.serverIdToClientKey.delete(serverId);
|
|
40
|
+
}
|
|
41
|
+
return { subscription, serverId };
|
|
42
|
+
}
|
|
43
|
+
getByServerId(serverId) {
|
|
44
|
+
const clientKey = this.serverIdToClientKey.get(serverId);
|
|
45
|
+
if (clientKey === undefined) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
return this.byClientKey.get(clientKey);
|
|
49
|
+
}
|
|
50
|
+
clearServerIds() {
|
|
51
|
+
this.clientKeyToServerId.clear();
|
|
52
|
+
this.serverIdToClientKey.clear();
|
|
53
|
+
}
|
|
54
|
+
snapshot() {
|
|
55
|
+
return [...this.byClientKey.entries()];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=subscriptions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriptions.js","sourceRoot":"","sources":["../../src/ws/subscriptions.ts"],"names":[],"mappings":"AAgBA;;;;;;;;;GASG;AACH,MAAM,OAAO,oBAAoB;IACd,WAAW,GAAG,IAAI,GAAG,EAAwB,CAAC;IAC9C,mBAAmB,GAAG,IAAI,GAAG,EAAkB,CAAC;IAChD,mBAAmB,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzD,aAAa,GAAG,CAAC,CAAC;IAEnB,QAAQ,CAAC,YAA0B;QACxC,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC;QACrC,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC;QACxB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC;QAC9C,OAAO,SAAS,CAAC;IACnB,CAAC;IAEM,YAAY,CAAC,SAAiB,EAAE,QAAgB;QACrD,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACzD,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC5C,CAAC;QACD,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAClD,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;IACpD,CAAC;IAEM,UAAU,CAAC,SAAiB;QAMjC,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACrD,IAAI,YAAY,KAAK,SAAS,EAAE,CAAC;YAC/B,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACzD,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC3C,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC;IACpC,CAAC;IAEM,aAAa,CAAC,QAAgB;QACnC,MAAM,SAAS,GAAG,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACzD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;IACzC,CAAC;IAEM,cAAc;QACnB,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,CAAC;QACjC,IAAI,CAAC,mBAAmB,CAAC,KAAK,EAAE,CAAC;IACnC,CAAC;IAEM,QAAQ;QACb,OAAO,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IACzC,CAAC;CACF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { HomeAssistantRestClient } from "@shepherdjerred/home-assistant";
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
HA_ENTITY_ID: entityId,
|
|
5
|
+
HA_TOKEN: token,
|
|
6
|
+
HA_URL: baseUrl,
|
|
7
|
+
} = process.env;
|
|
8
|
+
|
|
9
|
+
if (baseUrl === undefined || token === undefined || entityId === undefined) {
|
|
10
|
+
throw new Error(
|
|
11
|
+
"Set HA_URL, HA_TOKEN, and HA_ENTITY_ID before running this example.",
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const client = new HomeAssistantRestClient({ baseUrl, token });
|
|
16
|
+
const state = await client.getState(entityId);
|
|
17
|
+
|
|
18
|
+
console.log(`${state.entity_id}: ${state.state}`);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const responses = new Map([
|
|
10
|
+
[
|
|
11
|
+
"/api/states",
|
|
12
|
+
[
|
|
13
|
+
{
|
|
14
|
+
entity_id: "light.demo_lamp",
|
|
15
|
+
state: "off",
|
|
16
|
+
attributes: { friendly_name: "Demo lamp" },
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
"/api/services",
|
|
22
|
+
[
|
|
23
|
+
{
|
|
24
|
+
domain: "light",
|
|
25
|
+
services: {
|
|
26
|
+
turn_on: {
|
|
27
|
+
fields: {
|
|
28
|
+
brightness: {
|
|
29
|
+
required: false,
|
|
30
|
+
selector: { number: {} },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
],
|
|
38
|
+
["/api/events", [{ event: "state_changed" }]],
|
|
39
|
+
["/api/config", { version: "demo", components: ["light"] }],
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const server = createServer((request, response) => {
|
|
43
|
+
const path = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
44
|
+
const body = responses.get(path);
|
|
45
|
+
if (body === undefined) {
|
|
46
|
+
response.writeHead(404);
|
|
47
|
+
response.end();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
response.setHeader("content-type", "application/json");
|
|
51
|
+
response.end(JSON.stringify(body));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const outputDirectory = await mkdtemp(join(tmpdir(), "ha-codegen-example-"));
|
|
55
|
+
const outputPath = join(outputDirectory, "ha-schema.ts");
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
server.listen(0, "127.0.0.1");
|
|
59
|
+
await once(server, "listening");
|
|
60
|
+
const address = server.address();
|
|
61
|
+
if (address === null || typeof address === "string") {
|
|
62
|
+
throw new Error("The fixture server did not expose a TCP address.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const packageDirectory = fileURLToPath(new URL("../", import.meta.url));
|
|
66
|
+
const child = spawn(
|
|
67
|
+
process.execPath,
|
|
68
|
+
[
|
|
69
|
+
"dist/codegen/cli.js",
|
|
70
|
+
"--url",
|
|
71
|
+
`http://127.0.0.1:${address.port}`,
|
|
72
|
+
"--out",
|
|
73
|
+
outputPath,
|
|
74
|
+
],
|
|
75
|
+
{
|
|
76
|
+
cwd: packageDirectory,
|
|
77
|
+
env: { ...process.env, HA_TOKEN: "fixture-token" },
|
|
78
|
+
stdio: "ignore",
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
const [code] = await once(child, "close");
|
|
82
|
+
if (code !== 0) {
|
|
83
|
+
throw new Error(`ha-codegen exited with status ${String(code)}.`);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const generated = await readFile(outputPath, "utf8");
|
|
87
|
+
process.stdout.write(
|
|
88
|
+
generated
|
|
89
|
+
.replace(/^\/\/ Source host: .*$/mu, "// Source host: fixture.local")
|
|
90
|
+
.replace(/^\/\/ Generated at: .*$/mu, "// Generated at: fixture time"),
|
|
91
|
+
);
|
|
92
|
+
} finally {
|
|
93
|
+
server.close();
|
|
94
|
+
await rm(outputDirectory, { recursive: true, force: true });
|
|
95
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shepherdjerred/home-assistant",
|
|
3
|
+
"version": "0.1.0-dev.14474",
|
|
4
|
+
"description": "Type-safe Home Assistant REST and WebSocket client for Node.js and Bun",
|
|
5
|
+
"author": "Jerred Shepherd",
|
|
6
|
+
"license": "GPL-3.0-only",
|
|
7
|
+
"homepage": "https://github.com/shepherdjerred/monorepo/tree/main/packages/home-assistant#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/shepherdjerred/monorepo.git",
|
|
11
|
+
"directory": "packages/home-assistant"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/shepherdjerred/monorepo/issues"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"bin": {
|
|
26
|
+
"ha-codegen": "./dist/codegen/cli.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"examples",
|
|
31
|
+
"demos",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"CHANGELOG.md"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=24"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "PATH=node_modules/@typescript/native/bin:$PATH tsc -p tsconfig.build.json",
|
|
44
|
+
"typecheck": "PATH=node_modules/@typescript/native/bin:$PATH tsc --noEmit",
|
|
45
|
+
"lint": "bunx eslint --cache .",
|
|
46
|
+
"test": "bun --no-install --bun vitest --config ../../vitest.config.ts run",
|
|
47
|
+
"prepublishOnly": "bun run build",
|
|
48
|
+
"publish:npm": "bun ../../scripts/publish-npm.ts .",
|
|
49
|
+
"test:ci": "bun ../../scripts/run-ci-test.ts",
|
|
50
|
+
"test:report": "CI_TEST_COVERAGE=1 bun ../../scripts/run-ci-test.ts"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"zod": "^4.4.3"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@shepherdjerred/eslint-config": "0.3.0",
|
|
57
|
+
"@tsconfig/node-lts": "^24.0.0",
|
|
58
|
+
"@tsconfig/recommended": "^1.0.13",
|
|
59
|
+
"@tsconfig/strictest": "^2.0.8",
|
|
60
|
+
"@types/bun": "1.4.0",
|
|
61
|
+
"@types/node": "^26.1.1",
|
|
62
|
+
"eslint": "^10.3.0",
|
|
63
|
+
"jiti": "^2.7.0",
|
|
64
|
+
"typescript": "^6.0.3",
|
|
65
|
+
"@typescript/native": "npm:typescript@7.0.2",
|
|
66
|
+
"vitest": "4.1.10",
|
|
67
|
+
"@vitest/coverage-istanbul": "4.1.10"
|
|
68
|
+
}
|
|
69
|
+
}
|