lightman-agent 1.0.17 → 1.0.20
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/agent.config.json +22 -23
- package/agent.config.template.json +30 -31
- package/bin/cms-agent.js +291 -291
- package/package.json +1 -1
- package/public/assets/index-CcBNCz6h.css +1 -1
- package/public/assets/index-D9QHMG8k.js +1 -1
- package/public/assets/index-H-8HDl46.js +1 -1
- package/public/assets/index-YodeiCia.css +1 -1
- package/public/assets/index-legacy-DWtNM8y7.js +41 -41
- package/public/assets/polyfills-legacy-DyVYWHbW.js +4 -4
- package/scripts/guardian.ps1 +50 -124
- package/scripts/install-windows.ps1 +27 -90
- package/scripts/lightman-agent.logrotate +12 -12
- package/scripts/lightman-agent.service +38 -38
- package/scripts/reinstall-windows.ps1 +26 -26
- package/scripts/restore-desktop.ps1 +32 -32
- package/scripts/setup.ps1 +17 -22
- package/scripts/sync-display.mjs +20 -20
- package/scripts/uninstall-windows.ps1 +54 -54
- package/src/commands/display.ts +177 -177
- package/src/commands/kiosk.ts +113 -113
- package/src/commands/maintenance.ts +106 -106
- package/src/commands/network.ts +129 -129
- package/src/commands/power.ts +163 -163
- package/src/commands/rpi.ts +45 -45
- package/src/commands/screenshot.ts +166 -166
- package/src/commands/serial.ts +17 -17
- package/src/commands/update.ts +124 -124
- package/src/index.ts +173 -90
- package/src/lib/config.ts +2 -3
- package/src/lib/identity.ts +40 -40
- package/src/lib/logger.ts +137 -137
- package/src/lib/platform.ts +10 -10
- package/src/lib/rpi.ts +180 -180
- package/src/lib/screenMap.ts +135 -0
- package/src/lib/screens.ts +128 -128
- package/src/lib/types.ts +176 -177
- package/src/services/commands.ts +107 -107
- package/src/services/health.ts +161 -161
- package/src/services/localEvents.ts +60 -60
- package/src/services/logForwarder.ts +72 -72
- package/src/services/multiScreenKiosk.ts +116 -83
- package/src/services/oscBridge.ts +186 -186
- package/src/services/powerScheduler.ts +260 -260
- package/src/services/provisioning.ts +120 -122
- package/src/services/serialBridge.ts +230 -230
- package/src/services/serviceLauncher.ts +183 -183
- package/src/services/staticServer.ts +226 -226
- package/src/services/updater.ts +249 -249
- package/src/services/watchdog.ts +310 -310
- package/src/services/websocket.ts +152 -152
- package/tsconfig.json +28 -28
package/bin/cms-agent.js
CHANGED
|
@@ -1,291 +1,291 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { existsSync, readFileSync } from 'fs';
|
|
4
|
-
import { networkInterfaces } from 'os';
|
|
5
|
-
import { resolve, dirname } from 'path';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
|
-
import { spawnSync } from 'child_process';
|
|
8
|
-
import { createInterface } from 'readline/promises';
|
|
9
|
-
import { stdin as input, stdout as output, cwd, platform, exit } from 'process';
|
|
10
|
-
|
|
11
|
-
const DEFAULT_SERVER = 'http://192.168.10.100:3401';
|
|
12
|
-
const INSTALL_CONFIG_PATH = 'C:\\Program Files\\Lightman\\Agent\\agent.config.json';
|
|
13
|
-
|
|
14
|
-
function printUsage() {
|
|
15
|
-
console.log(`
|
|
16
|
-
cms-agent <command> [options]
|
|
17
|
-
|
|
18
|
-
Commands:
|
|
19
|
-
install Prompt slug and server IP, install agent with ShellReplace, reboot
|
|
20
|
-
setup Alias of install
|
|
21
|
-
update Reinstall/update using installed config, reboot
|
|
22
|
-
|
|
23
|
-
Options:
|
|
24
|
-
--slug <value> Device slug (example: C-AV01)
|
|
25
|
-
--server <url> Server URL or IP (example: 192.168.10.100 or http://192.168.10.100:3401)
|
|
26
|
-
--timezone <tz> Timezone override (default: Asia/Kolkata)
|
|
27
|
-
--pair-timeout <s> Wait time for pairing in seconds (default: 900, 0 = no timeout)
|
|
28
|
-
--no-restart Skip reboot after successful install/update
|
|
29
|
-
-h, --help Show help
|
|
30
|
-
`);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function parseArgs(argv) {
|
|
34
|
-
const args = {};
|
|
35
|
-
for (let i = 0; i < argv.length; i += 1) {
|
|
36
|
-
const part = argv[i];
|
|
37
|
-
if (!part.startsWith('-')) {
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (part === '--no-restart') {
|
|
41
|
-
args.noRestart = true;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
if (part === '--help' || part === '-h') {
|
|
45
|
-
args.help = true;
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
const next = argv[i + 1];
|
|
49
|
-
if (!next || next.startsWith('-')) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (part === '--slug') {
|
|
53
|
-
args.slug = next.trim();
|
|
54
|
-
i += 1;
|
|
55
|
-
} else if (part === '--server') {
|
|
56
|
-
args.server = next.trim();
|
|
57
|
-
i += 1;
|
|
58
|
-
} else if (part === '--timezone') {
|
|
59
|
-
args.timezone = next.trim();
|
|
60
|
-
i += 1;
|
|
61
|
-
} else if (part === '--pair-timeout') {
|
|
62
|
-
args.pairTimeout = next.trim();
|
|
63
|
-
i += 1;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return args;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function safeReadJson(path) {
|
|
70
|
-
try {
|
|
71
|
-
if (!existsSync(path)) return null;
|
|
72
|
-
return JSON.parse(readFileSync(path, 'utf8'));
|
|
73
|
-
} catch {
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function isValidSlug(slug) {
|
|
79
|
-
return /^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(slug);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function normalizeServer(server) {
|
|
83
|
-
const value = (server || '').trim();
|
|
84
|
-
if (!value) {
|
|
85
|
-
return '';
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const hasProtocol = /^[a-z]+:\/\//i.test(value);
|
|
89
|
-
const candidate = hasProtocol ? value : `http://${value}`;
|
|
90
|
-
|
|
91
|
-
let parsed;
|
|
92
|
-
try {
|
|
93
|
-
parsed = new URL(candidate);
|
|
94
|
-
} catch {
|
|
95
|
-
throw new Error('Invalid server IP/URL. Use an IP like 192.168.10.100 or a URL like http://192.168.10.100:3401.');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!parsed.port) {
|
|
99
|
-
parsed.port = '3401';
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
if (parsed.pathname === '/') {
|
|
103
|
-
parsed.pathname = '';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return parsed.toString().replace(/\/$/, '');
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function collectMacAddresses() {
|
|
110
|
-
const nets = networkInterfaces();
|
|
111
|
-
const macs = new Set();
|
|
112
|
-
for (const netName of Object.keys(nets)) {
|
|
113
|
-
const ifaceList = nets[netName] || [];
|
|
114
|
-
for (const iface of ifaceList) {
|
|
115
|
-
const mac = (iface.mac || '').trim().toUpperCase();
|
|
116
|
-
if (!iface.internal && mac && mac !== '00:00:00:00:00:00') {
|
|
117
|
-
macs.add(`${netName}: ${mac}`);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
return [...macs];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function isWindowsAdmin() {
|
|
125
|
-
const cmd = '[bool]([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)';
|
|
126
|
-
const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', cmd], { encoding: 'utf8' });
|
|
127
|
-
return result.status === 0 && result.stdout.trim().toLowerCase() === 'true';
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function runOrFail(command, args) {
|
|
131
|
-
const result = spawnSync(command, args, { stdio: 'inherit', shell: false });
|
|
132
|
-
if (result.error) {
|
|
133
|
-
throw result.error;
|
|
134
|
-
}
|
|
135
|
-
if (result.status !== 0) {
|
|
136
|
-
throw new Error(`Command failed with exit code ${result.status}: ${command} ${args.join(' ')}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function promptSlug(defaultSlug) {
|
|
141
|
-
const rl = createInterface({ input, output });
|
|
142
|
-
try {
|
|
143
|
-
while (true) {
|
|
144
|
-
const prompt = defaultSlug
|
|
145
|
-
? `Enter device slug [${defaultSlug}]: `
|
|
146
|
-
: 'Enter device slug (example C-AV01): ';
|
|
147
|
-
const answer = (await rl.question(prompt)).trim();
|
|
148
|
-
const slug = answer || defaultSlug || '';
|
|
149
|
-
if (isValidSlug(slug)) {
|
|
150
|
-
return slug;
|
|
151
|
-
}
|
|
152
|
-
console.error('Invalid slug. Use letters, numbers, and hyphens only.');
|
|
153
|
-
}
|
|
154
|
-
} finally {
|
|
155
|
-
rl.close();
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function promptServer(defaultServer) {
|
|
160
|
-
const rl = createInterface({ input, output });
|
|
161
|
-
try {
|
|
162
|
-
while (true) {
|
|
163
|
-
const prompt = defaultServer
|
|
164
|
-
? `Enter server IP or URL [${defaultServer}]: `
|
|
165
|
-
: 'Enter server IP or URL (example 192.168.10.100 or http://192.168.10.100:3401): ';
|
|
166
|
-
const answer = (await rl.question(prompt)).trim();
|
|
167
|
-
const server = answer || defaultServer || '';
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
return normalizeServer(server);
|
|
171
|
-
} catch (error) {
|
|
172
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
} finally {
|
|
176
|
-
rl.close();
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function resolveInstallScript() {
|
|
181
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
182
|
-
const packaged = resolve(here, '../scripts/install-windows.ps1');
|
|
183
|
-
const localRepo = resolve(cwd(), 'scripts/install-windows.ps1');
|
|
184
|
-
if (existsSync(packaged)) return packaged;
|
|
185
|
-
if (existsSync(localRepo)) return localRepo;
|
|
186
|
-
throw new Error('install-windows.ps1 not found. Expected in package scripts/ or current folder scripts/.');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function installUsingPowerShell({ scriptPath, slug, server, timezone, pairingTimeoutSeconds, noRestart }) {
|
|
190
|
-
console.log(`powershell -ExecutionPolicy Bypass -File scripts\\install-windows.ps1 -Slug "${slug}" -Server "${server}" -ShellReplace`);
|
|
191
|
-
const args = ['-ExecutionPolicy', 'Bypass', '-File', scriptPath, '-Slug', slug, '-Server', server, '-ShellReplace'];
|
|
192
|
-
if (timezone) {
|
|
193
|
-
args.push('-Timezone', timezone);
|
|
194
|
-
}
|
|
195
|
-
if (Number.isInteger(pairingTimeoutSeconds) && pairingTimeoutSeconds >= 0) {
|
|
196
|
-
args.push('-PairingTimeoutSeconds', String(pairingTimeoutSeconds));
|
|
197
|
-
}
|
|
198
|
-
runOrFail('powershell.exe', args);
|
|
199
|
-
|
|
200
|
-
if (!noRestart) {
|
|
201
|
-
console.log('Installation completed. Rebooting now...');
|
|
202
|
-
runOrFail('shutdown.exe', ['/r', '/t', '0', '/c', 'CMS Agent installation complete']);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function runInstall(opts) {
|
|
207
|
-
if (platform !== 'win32') {
|
|
208
|
-
throw new Error('cms-agent install is currently supported on Windows only.');
|
|
209
|
-
}
|
|
210
|
-
if (!isWindowsAdmin()) {
|
|
211
|
-
throw new Error('Please run this command from an Administrator PowerShell or CMD.');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const localConfig = safeReadJson(resolve(cwd(), 'agent.config.json')) || {};
|
|
215
|
-
const installedConfig = safeReadJson(INSTALL_CONFIG_PATH) || {};
|
|
216
|
-
const defaultSlug = opts.slug || localConfig.deviceSlug || installedConfig.deviceSlug || '';
|
|
217
|
-
const defaultServer = localConfig.serverUrl || installedConfig.serverUrl || DEFAULT_SERVER;
|
|
218
|
-
const timezone = opts.timezone || localConfig?.powerSchedule?.timezone || installedConfig?.powerSchedule?.timezone || 'Asia/Kolkata';
|
|
219
|
-
const pairingTimeoutSeconds = Number.isFinite(Number(opts.pairTimeout)) ? Number.parseInt(String(opts.pairTimeout), 10) : 900;
|
|
220
|
-
const noRestart = Boolean(opts.noRestart);
|
|
221
|
-
|
|
222
|
-
console.log('Detected MAC addresses:');
|
|
223
|
-
const macs = collectMacAddresses();
|
|
224
|
-
if (macs.length === 0) {
|
|
225
|
-
console.log(' (no active external interface found)');
|
|
226
|
-
} else {
|
|
227
|
-
for (const item of macs) {
|
|
228
|
-
console.log(` ${item}`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
console.log('');
|
|
232
|
-
|
|
233
|
-
const slug = opts.slug || await promptSlug(defaultSlug);
|
|
234
|
-
const server = opts.server ? normalizeServer(opts.server) : await promptServer(defaultServer);
|
|
235
|
-
const scriptPath = resolveInstallScript();
|
|
236
|
-
|
|
237
|
-
console.log(`Installing with slug=${slug}, server=${server}, shellReplace=true`);
|
|
238
|
-
installUsingPowerShell({ scriptPath, slug, server, timezone, pairingTimeoutSeconds, noRestart });
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function runUpdate(opts) {
|
|
242
|
-
if (platform !== 'win32') {
|
|
243
|
-
throw new Error('cms-agent update is currently supported on Windows only.');
|
|
244
|
-
}
|
|
245
|
-
if (!isWindowsAdmin()) {
|
|
246
|
-
throw new Error('Please run this command from an Administrator PowerShell or CMD.');
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const installedConfig = safeReadJson(INSTALL_CONFIG_PATH) || {};
|
|
250
|
-
const slug = opts.slug || installedConfig.deviceSlug;
|
|
251
|
-
const server = normalizeServer(opts.server || installedConfig.serverUrl || DEFAULT_SERVER);
|
|
252
|
-
const timezone = opts.timezone || installedConfig?.powerSchedule?.timezone || 'Asia/Kolkata';
|
|
253
|
-
const pairingTimeoutSeconds = Number.isFinite(Number(opts.pairTimeout)) ? Number.parseInt(String(opts.pairTimeout), 10) : 900;
|
|
254
|
-
const noRestart = Boolean(opts.noRestart);
|
|
255
|
-
const scriptPath = resolveInstallScript();
|
|
256
|
-
|
|
257
|
-
if (!slug) {
|
|
258
|
-
throw new Error('No installed slug found. Use cms-agent install first or pass --slug.');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
console.log(`Updating with slug=${slug}, server=${server}, shellReplace=true`);
|
|
262
|
-
installUsingPowerShell({ scriptPath, slug, server, timezone, pairingTimeoutSeconds, noRestart });
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function main() {
|
|
266
|
-
const [, , commandRaw, ...rest] = process.argv;
|
|
267
|
-
const command = commandRaw || '';
|
|
268
|
-
const opts = parseArgs(rest);
|
|
269
|
-
|
|
270
|
-
if (!command || opts.help || command === 'help' || command === '--help' || command === '-h') {
|
|
271
|
-
printUsage();
|
|
272
|
-
return;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (command === 'install' || command === 'setup') {
|
|
276
|
-
await runInstall(opts);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (command === 'update') {
|
|
280
|
-
await runUpdate(opts);
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
printUsage();
|
|
285
|
-
throw new Error(`Unknown command: ${command}`);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
main().catch((err) => {
|
|
289
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
290
|
-
exit(1);
|
|
291
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from 'fs';
|
|
4
|
+
import { networkInterfaces } from 'os';
|
|
5
|
+
import { resolve, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { spawnSync } from 'child_process';
|
|
8
|
+
import { createInterface } from 'readline/promises';
|
|
9
|
+
import { stdin as input, stdout as output, cwd, platform, exit } from 'process';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_SERVER = 'http://192.168.10.100:3401';
|
|
12
|
+
const INSTALL_CONFIG_PATH = 'C:\\Program Files\\Lightman\\Agent\\agent.config.json';
|
|
13
|
+
|
|
14
|
+
function printUsage() {
|
|
15
|
+
console.log(`
|
|
16
|
+
cms-agent <command> [options]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
install Prompt slug and server IP, install agent with ShellReplace, reboot
|
|
20
|
+
setup Alias of install
|
|
21
|
+
update Reinstall/update using installed config, reboot
|
|
22
|
+
|
|
23
|
+
Options:
|
|
24
|
+
--slug <value> Device slug (example: C-AV01)
|
|
25
|
+
--server <url> Server URL or IP (example: 192.168.10.100 or http://192.168.10.100:3401)
|
|
26
|
+
--timezone <tz> Timezone override (default: Asia/Kolkata)
|
|
27
|
+
--pair-timeout <s> Wait time for pairing in seconds (default: 900, 0 = no timeout)
|
|
28
|
+
--no-restart Skip reboot after successful install/update
|
|
29
|
+
-h, --help Show help
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseArgs(argv) {
|
|
34
|
+
const args = {};
|
|
35
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
36
|
+
const part = argv[i];
|
|
37
|
+
if (!part.startsWith('-')) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (part === '--no-restart') {
|
|
41
|
+
args.noRestart = true;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (part === '--help' || part === '-h') {
|
|
45
|
+
args.help = true;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const next = argv[i + 1];
|
|
49
|
+
if (!next || next.startsWith('-')) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (part === '--slug') {
|
|
53
|
+
args.slug = next.trim();
|
|
54
|
+
i += 1;
|
|
55
|
+
} else if (part === '--server') {
|
|
56
|
+
args.server = next.trim();
|
|
57
|
+
i += 1;
|
|
58
|
+
} else if (part === '--timezone') {
|
|
59
|
+
args.timezone = next.trim();
|
|
60
|
+
i += 1;
|
|
61
|
+
} else if (part === '--pair-timeout') {
|
|
62
|
+
args.pairTimeout = next.trim();
|
|
63
|
+
i += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return args;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function safeReadJson(path) {
|
|
70
|
+
try {
|
|
71
|
+
if (!existsSync(path)) return null;
|
|
72
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isValidSlug(slug) {
|
|
79
|
+
return /^[A-Za-z0-9][A-Za-z0-9-]{0,62}$/.test(slug);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeServer(server) {
|
|
83
|
+
const value = (server || '').trim();
|
|
84
|
+
if (!value) {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const hasProtocol = /^[a-z]+:\/\//i.test(value);
|
|
89
|
+
const candidate = hasProtocol ? value : `http://${value}`;
|
|
90
|
+
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = new URL(candidate);
|
|
94
|
+
} catch {
|
|
95
|
+
throw new Error('Invalid server IP/URL. Use an IP like 192.168.10.100 or a URL like http://192.168.10.100:3401.');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!parsed.port) {
|
|
99
|
+
parsed.port = '3401';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (parsed.pathname === '/') {
|
|
103
|
+
parsed.pathname = '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parsed.toString().replace(/\/$/, '');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function collectMacAddresses() {
|
|
110
|
+
const nets = networkInterfaces();
|
|
111
|
+
const macs = new Set();
|
|
112
|
+
for (const netName of Object.keys(nets)) {
|
|
113
|
+
const ifaceList = nets[netName] || [];
|
|
114
|
+
for (const iface of ifaceList) {
|
|
115
|
+
const mac = (iface.mac || '').trim().toUpperCase();
|
|
116
|
+
if (!iface.internal && mac && mac !== '00:00:00:00:00:00') {
|
|
117
|
+
macs.add(`${netName}: ${mac}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return [...macs];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function isWindowsAdmin() {
|
|
125
|
+
const cmd = '[bool]([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)';
|
|
126
|
+
const result = spawnSync('powershell.exe', ['-NoProfile', '-Command', cmd], { encoding: 'utf8' });
|
|
127
|
+
return result.status === 0 && result.stdout.trim().toLowerCase() === 'true';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runOrFail(command, args) {
|
|
131
|
+
const result = spawnSync(command, args, { stdio: 'inherit', shell: false });
|
|
132
|
+
if (result.error) {
|
|
133
|
+
throw result.error;
|
|
134
|
+
}
|
|
135
|
+
if (result.status !== 0) {
|
|
136
|
+
throw new Error(`Command failed with exit code ${result.status}: ${command} ${args.join(' ')}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function promptSlug(defaultSlug) {
|
|
141
|
+
const rl = createInterface({ input, output });
|
|
142
|
+
try {
|
|
143
|
+
while (true) {
|
|
144
|
+
const prompt = defaultSlug
|
|
145
|
+
? `Enter device slug [${defaultSlug}]: `
|
|
146
|
+
: 'Enter device slug (example C-AV01): ';
|
|
147
|
+
const answer = (await rl.question(prompt)).trim();
|
|
148
|
+
const slug = answer || defaultSlug || '';
|
|
149
|
+
if (isValidSlug(slug)) {
|
|
150
|
+
return slug;
|
|
151
|
+
}
|
|
152
|
+
console.error('Invalid slug. Use letters, numbers, and hyphens only.');
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
rl.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function promptServer(defaultServer) {
|
|
160
|
+
const rl = createInterface({ input, output });
|
|
161
|
+
try {
|
|
162
|
+
while (true) {
|
|
163
|
+
const prompt = defaultServer
|
|
164
|
+
? `Enter server IP or URL [${defaultServer}]: `
|
|
165
|
+
: 'Enter server IP or URL (example 192.168.10.100 or http://192.168.10.100:3401): ';
|
|
166
|
+
const answer = (await rl.question(prompt)).trim();
|
|
167
|
+
const server = answer || defaultServer || '';
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
return normalizeServer(server);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
} finally {
|
|
176
|
+
rl.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function resolveInstallScript() {
|
|
181
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
182
|
+
const packaged = resolve(here, '../scripts/install-windows.ps1');
|
|
183
|
+
const localRepo = resolve(cwd(), 'scripts/install-windows.ps1');
|
|
184
|
+
if (existsSync(packaged)) return packaged;
|
|
185
|
+
if (existsSync(localRepo)) return localRepo;
|
|
186
|
+
throw new Error('install-windows.ps1 not found. Expected in package scripts/ or current folder scripts/.');
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function installUsingPowerShell({ scriptPath, slug, server, timezone, pairingTimeoutSeconds, noRestart }) {
|
|
190
|
+
console.log(`powershell -ExecutionPolicy Bypass -File scripts\\install-windows.ps1 -Slug "${slug}" -Server "${server}" -ShellReplace`);
|
|
191
|
+
const args = ['-ExecutionPolicy', 'Bypass', '-File', scriptPath, '-Slug', slug, '-Server', server, '-ShellReplace'];
|
|
192
|
+
if (timezone) {
|
|
193
|
+
args.push('-Timezone', timezone);
|
|
194
|
+
}
|
|
195
|
+
if (Number.isInteger(pairingTimeoutSeconds) && pairingTimeoutSeconds >= 0) {
|
|
196
|
+
args.push('-PairingTimeoutSeconds', String(pairingTimeoutSeconds));
|
|
197
|
+
}
|
|
198
|
+
runOrFail('powershell.exe', args);
|
|
199
|
+
|
|
200
|
+
if (!noRestart) {
|
|
201
|
+
console.log('Installation completed. Rebooting now...');
|
|
202
|
+
runOrFail('shutdown.exe', ['/r', '/t', '0', '/c', 'CMS Agent installation complete']);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function runInstall(opts) {
|
|
207
|
+
if (platform !== 'win32') {
|
|
208
|
+
throw new Error('cms-agent install is currently supported on Windows only.');
|
|
209
|
+
}
|
|
210
|
+
if (!isWindowsAdmin()) {
|
|
211
|
+
throw new Error('Please run this command from an Administrator PowerShell or CMD.');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const localConfig = safeReadJson(resolve(cwd(), 'agent.config.json')) || {};
|
|
215
|
+
const installedConfig = safeReadJson(INSTALL_CONFIG_PATH) || {};
|
|
216
|
+
const defaultSlug = opts.slug || localConfig.deviceSlug || installedConfig.deviceSlug || '';
|
|
217
|
+
const defaultServer = localConfig.serverUrl || installedConfig.serverUrl || DEFAULT_SERVER;
|
|
218
|
+
const timezone = opts.timezone || localConfig?.powerSchedule?.timezone || installedConfig?.powerSchedule?.timezone || 'Asia/Kolkata';
|
|
219
|
+
const pairingTimeoutSeconds = Number.isFinite(Number(opts.pairTimeout)) ? Number.parseInt(String(opts.pairTimeout), 10) : 900;
|
|
220
|
+
const noRestart = Boolean(opts.noRestart);
|
|
221
|
+
|
|
222
|
+
console.log('Detected MAC addresses:');
|
|
223
|
+
const macs = collectMacAddresses();
|
|
224
|
+
if (macs.length === 0) {
|
|
225
|
+
console.log(' (no active external interface found)');
|
|
226
|
+
} else {
|
|
227
|
+
for (const item of macs) {
|
|
228
|
+
console.log(` ${item}`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
console.log('');
|
|
232
|
+
|
|
233
|
+
const slug = opts.slug || await promptSlug(defaultSlug);
|
|
234
|
+
const server = opts.server ? normalizeServer(opts.server) : await promptServer(defaultServer);
|
|
235
|
+
const scriptPath = resolveInstallScript();
|
|
236
|
+
|
|
237
|
+
console.log(`Installing with slug=${slug}, server=${server}, shellReplace=true`);
|
|
238
|
+
installUsingPowerShell({ scriptPath, slug, server, timezone, pairingTimeoutSeconds, noRestart });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function runUpdate(opts) {
|
|
242
|
+
if (platform !== 'win32') {
|
|
243
|
+
throw new Error('cms-agent update is currently supported on Windows only.');
|
|
244
|
+
}
|
|
245
|
+
if (!isWindowsAdmin()) {
|
|
246
|
+
throw new Error('Please run this command from an Administrator PowerShell or CMD.');
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const installedConfig = safeReadJson(INSTALL_CONFIG_PATH) || {};
|
|
250
|
+
const slug = opts.slug || installedConfig.deviceSlug;
|
|
251
|
+
const server = normalizeServer(opts.server || installedConfig.serverUrl || DEFAULT_SERVER);
|
|
252
|
+
const timezone = opts.timezone || installedConfig?.powerSchedule?.timezone || 'Asia/Kolkata';
|
|
253
|
+
const pairingTimeoutSeconds = Number.isFinite(Number(opts.pairTimeout)) ? Number.parseInt(String(opts.pairTimeout), 10) : 900;
|
|
254
|
+
const noRestart = Boolean(opts.noRestart);
|
|
255
|
+
const scriptPath = resolveInstallScript();
|
|
256
|
+
|
|
257
|
+
if (!slug) {
|
|
258
|
+
throw new Error('No installed slug found. Use cms-agent install first or pass --slug.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
console.log(`Updating with slug=${slug}, server=${server}, shellReplace=true`);
|
|
262
|
+
installUsingPowerShell({ scriptPath, slug, server, timezone, pairingTimeoutSeconds, noRestart });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async function main() {
|
|
266
|
+
const [, , commandRaw, ...rest] = process.argv;
|
|
267
|
+
const command = commandRaw || '';
|
|
268
|
+
const opts = parseArgs(rest);
|
|
269
|
+
|
|
270
|
+
if (!command || opts.help || command === 'help' || command === '--help' || command === '-h') {
|
|
271
|
+
printUsage();
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (command === 'install' || command === 'setup') {
|
|
276
|
+
await runInstall(opts);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (command === 'update') {
|
|
280
|
+
await runUpdate(opts);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
printUsage();
|
|
285
|
+
throw new Error(`Unknown command: ${command}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
main().catch((err) => {
|
|
289
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
290
|
+
exit(1);
|
|
291
|
+
});
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.resize{resize:both}.border{border-style:var(--tw-border-style);border-width:1px}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}}html,body,#root{color:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#000;width:100%;height:100%;margin:0;padding:0;font-family:system-ui,-apple-system,sans-serif;overflow:hidden}body{overscroll-behavior:none;-webkit-overflow-scrolling:touch;touch-action:manipulation}::-webkit-scrollbar{display:none}*{scrollbar-width:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
|
|
1
|
+
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--font-mono:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--ease-out:cubic-bezier(0,0,.2,1);--ease-in-out:cubic-bezier(.4,0,.2,1);--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4,0,.2,1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.block{display:block}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.transform{transform:var(--tw-rotate-x,)var(--tw-rotate-y,)var(--tw-rotate-z,)var(--tw-skew-x,)var(--tw-skew-y,)}.resize{resize:both}.border{border-style:var(--tw-border-style);border-width:1px}.uppercase{text-transform:uppercase}.italic{font-style:italic}.underline{text-decoration-line:underline}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}}html,body,#root{color:#fff;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-color:#000;width:100%;height:100%;margin:0;padding:0;font-family:system-ui,-apple-system,sans-serif;overflow:hidden}body{overscroll-behavior:none;-webkit-overflow-scrolling:touch;touch-action:manipulation}::-webkit-scrollbar{display:none}*{scrollbar-width:none}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}
|