artnet2usb-cli 0.2.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,52 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isWritableDir = isWritableDir;
7
+ exports.resolvePortableConfigDir = resolvePortableConfigDir;
8
+ exports.isCompiledBinary = isCompiledBinary;
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ /**
12
+ * Portable (standalone) config resolution — the shared rule for GUI and CLI:
13
+ * 1. If a config.json already EXISTS next to the exe, it always wins (USB drive scenario).
14
+ * 2. Otherwise try writing next to the exe — if writable, the config is created there and stays.
15
+ * 3. If it can't be written (e.g. macOS app translocation, read-only mount), fall back
16
+ * to the OS-standard user directory (the old behavior).
17
+ * If the GUI and CLI binaries are placed in the same folder, they keep sharing the same config.json.
18
+ */
19
+ /** Is the directory actually writable — does a test write (accessSync is misleading on Windows). */
20
+ function isWritableDir(dir) {
21
+ const probe = node_path_1.default.join(dir, `.artnet2usb-write-probe-${process.pid}`);
22
+ try {
23
+ node_fs_1.default.writeFileSync(probe, '');
24
+ node_fs_1.default.unlinkSync(probe);
25
+ return true;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function resolvePortableConfigDir(exeSideDir, fallbackDir) {
32
+ if (!exeSideDir)
33
+ return fallbackDir;
34
+ try {
35
+ if (node_fs_1.default.existsSync(node_path_1.default.join(exeSideDir, 'config.json')))
36
+ return exeSideDir;
37
+ }
38
+ catch {
39
+ return fallbackDir;
40
+ }
41
+ return isWritableDir(exeSideDir) ? exeSideDir : fallbackDir;
42
+ }
43
+ /**
44
+ * Is the process running as a single-file compiled binary?
45
+ * (pkg → process.pkg; bun build --compile → globalThis.Bun)
46
+ * Returns false under a plain `node`/`tsx` run — in that case process.execPath
47
+ * is node itself, and writing a config next to it would be meaningless/wrong.
48
+ */
49
+ function isCompiledBinary() {
50
+ const proc = process;
51
+ return Boolean(proc.pkg) || 'Bun' in globalThis;
52
+ }
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Store = void 0;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const DEFAULTS = {
10
+ deviceNames: {},
11
+ routes: [],
12
+ settings: { onSignalLoss: 'hold', signalLossMs: 3000 }
13
+ };
14
+ class Store {
15
+ file;
16
+ state;
17
+ constructor(configDir) {
18
+ this.file = node_path_1.default.join(configDir, 'config.json');
19
+ this.state = this.load();
20
+ }
21
+ load() {
22
+ try {
23
+ const raw = JSON.parse(node_fs_1.default.readFileSync(this.file, 'utf8'));
24
+ return {
25
+ deviceNames: raw.deviceNames ?? {},
26
+ routes: Array.isArray(raw.routes) ? raw.routes : [],
27
+ settings: { ...DEFAULTS.settings, ...(raw.settings ?? {}) }
28
+ };
29
+ }
30
+ catch {
31
+ return structuredClone(DEFAULTS);
32
+ }
33
+ }
34
+ save() {
35
+ try {
36
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(this.file), { recursive: true });
37
+ node_fs_1.default.writeFileSync(this.file, JSON.stringify(this.state, null, 2));
38
+ }
39
+ catch (err) {
40
+ console.error('[store] failed to save', err);
41
+ }
42
+ }
43
+ get deviceNames() {
44
+ return this.state.deviceNames;
45
+ }
46
+ setDeviceName(uid, name) {
47
+ if (name.trim())
48
+ this.state.deviceNames[uid] = name.trim().slice(0, 60);
49
+ else
50
+ delete this.state.deviceNames[uid];
51
+ this.save();
52
+ }
53
+ get routes() {
54
+ return this.state.routes;
55
+ }
56
+ upsertRoute(route) {
57
+ const i = this.state.routes.findIndex((r) => r.id === route.id);
58
+ if (i >= 0)
59
+ this.state.routes[i] = route;
60
+ else
61
+ this.state.routes.push(route);
62
+ this.save();
63
+ }
64
+ removeRoute(id) {
65
+ this.state.routes = this.state.routes.filter((r) => r.id !== id);
66
+ this.save();
67
+ }
68
+ get settings() {
69
+ return this.state.settings;
70
+ }
71
+ setSettings(s) {
72
+ this.state.settings = s;
73
+ this.save();
74
+ }
75
+ }
76
+ exports.Store = Store;
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ /** Main process ↔ renderer shared types. */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,122 @@
1
+ {
2
+ "name": "artnet2usb-cli",
3
+ "version": "0.2.0",
4
+ "description": "Headless Art-Net to USB DMX bridge CLI for Enttec DMX USB Pro and Open DMX / FTDI interfaces — a desktop GUI is also available",
5
+ "main": "dist/main/main.js",
6
+ "bin": {
7
+ "artnet2usb-cli": "dist-cli/cli/index.js"
8
+ },
9
+ "files": [
10
+ "dist-cli"
11
+ ],
12
+ "author": "Remana <info@remana.tech> (https://relackout.com)",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/remanatech/artnet2usb.git"
17
+ },
18
+ "homepage": "https://relackout.com/usb-dmx",
19
+ "bugs": {
20
+ "url": "https://github.com/remanatech/artnet2usb/issues"
21
+ },
22
+ "keywords": [
23
+ "art-net",
24
+ "artnet",
25
+ "dmx",
26
+ "dmx512",
27
+ "usb-dmx",
28
+ "enttec",
29
+ "ftdi",
30
+ "open-dmx",
31
+ "lighting-control",
32
+ "stage-lighting"
33
+ ],
34
+ "scripts": {
35
+ "dev": "electron-vite dev",
36
+ "build": "electron-vite build",
37
+ "start": "electron .",
38
+ "test": "vitest run",
39
+ "typecheck": "tsc --noEmit -p tsconfig.json",
40
+ "typecheck:cli": "tsc --noEmit -p tsconfig.cli.json",
41
+ "dist": "node scripts/build-standalone.mjs",
42
+ "dist:mac": "node scripts/build-standalone.mjs --mac",
43
+ "dist:win": "node scripts/build-standalone.mjs --win",
44
+ "dist:linux": "node scripts/build-standalone.mjs --linux",
45
+ "build:cli": "tsc -p tsconfig.cli.json",
46
+ "dev:cli": "tsx cli/index.ts",
47
+ "dist:standalone": "node scripts/build-standalone.mjs",
48
+ "prepack": "npm run build:cli"
49
+ },
50
+ "dependencies": {
51
+ "@clack/prompts": "^1.7.0",
52
+ "commander": "^15.0.0",
53
+ "serialport": "^12.0.0"
54
+ },
55
+ "devDependencies": {
56
+ "@fontsource/space-grotesk": "^5.2.10",
57
+ "react": "^18.3.1",
58
+ "react-dom": "^18.3.1",
59
+ "@types/react": "^18.3.12",
60
+ "@types/react-dom": "^18.3.1",
61
+ "@vitejs/plugin-react": "^4.3.4",
62
+ "@yao-pkg/pkg": "^6.21.0",
63
+ "autoprefixer": "^10.4.20",
64
+ "electron": "^33.2.0",
65
+ "electron-builder": "^26.0.0",
66
+ "electron-vite": "^2.3.0",
67
+ "esbuild": "^0.28.1",
68
+ "postcss": "^8.4.49",
69
+ "resedit": "^1.7.2",
70
+ "tailwindcss": "^3.4.15",
71
+ "tsx": "^4.19.2",
72
+ "typescript": "^5.6.3",
73
+ "vite": "^5.4.11",
74
+ "vitest": "^2.1.8"
75
+ },
76
+ "allowScripts": {
77
+ "electron@33.4.11": true,
78
+ "esbuild@0.21.5": true,
79
+ "@serialport/bindings-cpp@12.0.1": true,
80
+ "electron-winstaller@5.4.0": true,
81
+ "esbuild@0.28.1": true
82
+ },
83
+ "build": {
84
+ "appId": "tech.remana.relackout.artnet2usb",
85
+ "productName": "Relackout ArtNet 2 USB",
86
+ "npmRebuild": false,
87
+ "files": [
88
+ "dist/**"
89
+ ],
90
+ "directories": {
91
+ "output": "release"
92
+ },
93
+ "mac": {
94
+ "category": "public.app-category.utilities",
95
+ "target": [
96
+ "dmg"
97
+ ],
98
+ "icon": "build/icon.icns"
99
+ },
100
+ "win": {
101
+ "target": [
102
+ "portable"
103
+ ],
104
+ "icon": "build/icon.ico"
105
+ },
106
+ "linux": {
107
+ "target": [
108
+ "AppImage"
109
+ ],
110
+ "icon": "build/icons"
111
+ },
112
+ "portable": {
113
+ "artifactName": "Relackout-ArtNet2USB-${version}-portable.${ext}"
114
+ },
115
+ "appImage": {
116
+ "artifactName": "Relackout-ArtNet2USB-${version}-${arch}.${ext}"
117
+ },
118
+ "dmg": {
119
+ "artifactName": "Relackout-ArtNet2USB-${version}-${arch}.${ext}"
120
+ }
121
+ }
122
+ }