@agentdeck/bridge 1.0.3 → 1.0.4

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/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2LpC,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAM,CAAC;AAOtF;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElE;AAoBD,QAAA,MAAM,OAAO,SAAgB,CAAC;AA4/D9B,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+MpC,eAAO,MAAM,mBAAmB,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAM,CAAC;AAOtF;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAElE;AAoBD,QAAA,MAAM,OAAO,SAAgB,CAAC;AA6iE9B,wBAAsB,MAAM,CAAC,IAAI,GAAE,MAAM,EAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
package/dist/cli.js CHANGED
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url';
8
8
  import { createRequire } from 'module';
9
9
  import { request } from 'http';
10
10
  import { BRIDGE_WS_PORT } from './types.js';
11
+ import { ensureBleRuntime, getBleRuntimeStatus } from './python-ble-runtime.js';
11
12
  import { TASK_NAME, installWindowsTask, taskExists, runWindowsTask, endWindowsTask, deleteWindowsTask, } from './windows-service.js';
12
13
  const require = createRequire(import.meta.url);
13
14
  const packageJson = require('../package.json');
@@ -135,19 +136,30 @@ async function isDaemonPort(port) {
135
136
  return false;
136
137
  }
137
138
  }
138
- function resolveTimeboxSyncPaths() {
139
- const distPath = dirname(fileURLToPath(import.meta.url));
140
- const projectRoot = join(distPath, '..', '..');
141
- const timeboxDir = join(projectRoot, 'bridge', 'src', 'timebox');
142
- return {
143
- python: join(projectRoot, '.venv', 'bin', 'python'),
144
- bleScript: join(timeboxDir, 'sync_ble.py'),
145
- scanScript: join(timeboxDir, 'scan_ble.py'),
146
- };
147
- }
148
139
  function projectRootPath() {
149
140
  return join(dirname(fileURLToPath(import.meta.url)), '..', '..');
150
141
  }
142
+ async function runBlePython(python, args, captureStdout = false) {
143
+ return new Promise((resolve, reject) => {
144
+ const child = spawn(python, args, captureStdout ? { stdio: ['ignore', 'pipe', 'inherit'] } : { stdio: 'inherit' });
145
+ let stdout = '';
146
+ child.stdout?.on('data', (data) => {
147
+ stdout += data.toString();
148
+ });
149
+ child.once('error', reject);
150
+ child.once('close', (code) => resolve({ code, stdout }));
151
+ });
152
+ }
153
+ function prepareBleRuntime() {
154
+ try {
155
+ return ensureBleRuntime({ log });
156
+ }
157
+ catch (err) {
158
+ log(`BLE support unavailable: ${err instanceof Error ? err.message : String(err)}`);
159
+ process.exitCode = 1;
160
+ return null;
161
+ }
162
+ }
151
163
  const ESP32_OTA_BOARDS = [
152
164
  { board: 'inkdeck', env: 'inkdeck', aliases: [] },
153
165
  { board: 'ulanzi_tc001', env: 'led8x32', aliases: ['led8x32'] },
@@ -1149,35 +1161,51 @@ task
1149
1161
  .action(async (opts) => {
1150
1162
  await postTaskClose({ signal: 'manual', outcome: 'abandoned', sessionId: opts.session });
1151
1163
  });
1164
+ // ===== Optional Python BLE runtime =====
1165
+ const ble = program.command('ble').description('Prepare optional Python support for BLE display devices');
1166
+ ble
1167
+ .command('setup')
1168
+ .description('Create the private BLE environment and install its Python dependencies')
1169
+ .action(() => {
1170
+ const runtime = prepareBleRuntime();
1171
+ if (runtime)
1172
+ log(`BLE support ready (${runtime.python}).`);
1173
+ });
1174
+ ble
1175
+ .command('status')
1176
+ .description('Show whether optional BLE support is ready')
1177
+ .action(() => {
1178
+ const status = getBleRuntimeStatus();
1179
+ if (status.ready) {
1180
+ log(`BLE support ready (${status.python}).`);
1181
+ return;
1182
+ }
1183
+ log(`BLE support is not ready: ${status.reason}. Run \`agentdeck ble setup\`.`);
1184
+ process.exitCode = 1;
1185
+ });
1152
1186
  // ===== iDotMatrix commands =====
1153
1187
  const idotmatrix = program.command('idotmatrix').description('Manage iDotMatrix BLE pixel display devices');
1154
1188
  idotmatrix
1155
1189
  .command('scan')
1156
1190
  .description('Discover iDotMatrix devices via BLE')
1157
1191
  .action(async () => {
1158
- const { dirname } = await import('path');
1159
- const { fileURLToPath } = await import('url');
1160
- const { spawn } = await import('child_process');
1161
- const distPath = dirname(fileURLToPath(import.meta.url));
1162
- const projectRoot = join(distPath, '..', '..');
1163
- const venvPython = join(projectRoot, '.venv', 'bin', 'python');
1164
- const scanScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'scan.py');
1192
+ const runtime = prepareBleRuntime();
1193
+ if (!runtime)
1194
+ return;
1165
1195
  log('Scanning for BLE devices (5 seconds)...');
1166
- const py = spawn(venvPython, [scanScript]);
1167
- let stdoutData = '';
1168
- py.stdout.on('data', (data) => {
1169
- stdoutData += data.toString();
1170
- });
1171
- py.on('close', (code) => {
1196
+ try {
1197
+ const { code, stdout } = await runBlePython(runtime.python, [runtime.paths.scripts.idotmatrixScan], true);
1172
1198
  if (code !== 0) {
1173
1199
  log('Failed to run scan script.');
1174
- process.exit(1);
1200
+ process.exitCode = 1;
1201
+ return;
1175
1202
  }
1176
1203
  try {
1177
- const devices = JSON.parse(stdoutData.trim());
1204
+ const devices = JSON.parse(stdout.trim());
1178
1205
  if (devices.error) {
1179
1206
  log(`Scan error: ${devices.error}`);
1180
- process.exit(1);
1207
+ process.exitCode = 1;
1208
+ return;
1181
1209
  }
1182
1210
  if (!Array.isArray(devices) || devices.length === 0) {
1183
1211
  log('No BLE devices found.');
@@ -1189,10 +1217,15 @@ idotmatrix
1189
1217
  log(`${prefix}${d.name} (${d.address})`);
1190
1218
  }
1191
1219
  }
1192
- catch (e) {
1193
- log(`Failed to parse scan output: ${stdoutData}`);
1220
+ catch {
1221
+ log(`Failed to parse scan output: ${stdout}`);
1222
+ process.exitCode = 1;
1194
1223
  }
1195
- });
1224
+ }
1225
+ catch (err) {
1226
+ log(`Failed to start BLE scan: ${err instanceof Error ? err.message : String(err)}`);
1227
+ process.exitCode = 1;
1228
+ }
1196
1229
  });
1197
1230
  idotmatrix
1198
1231
  .command('add <address>')
@@ -1243,9 +1276,6 @@ idotmatrix
1243
1276
  .option('-a, --address <address>', 'BLE Address (defaults to first configured device)')
1244
1277
  .action(async (valueStr, opts) => {
1245
1278
  const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
1246
- const { dirname } = await import('path');
1247
- const { fileURLToPath } = await import('url');
1248
- const { spawn } = await import('child_process');
1249
1279
  const value = parseInt(valueStr, 10);
1250
1280
  if (isNaN(value) || value < 5 || value > 100) {
1251
1281
  log('Brightness value must be between 5 and 100.');
@@ -1260,22 +1290,30 @@ idotmatrix
1260
1290
  }
1261
1291
  targetAddress = devices[0].address;
1262
1292
  }
1263
- const distPath = dirname(fileURLToPath(import.meta.url));
1264
- const projectRoot = join(distPath, '..', '..');
1265
- const venvPython = join(projectRoot, '.venv', 'bin', 'python');
1266
- const brightnessScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'brightness.py');
1293
+ const runtime = prepareBleRuntime();
1294
+ if (!runtime)
1295
+ return;
1267
1296
  log(`Setting brightness of ${targetAddress} to ${value}%...`);
1268
- const py = spawn(venvPython, [brightnessScript, '-a', targetAddress, '-b', String(value)], {
1269
- stdio: 'inherit'
1270
- });
1271
- py.on('close', (code) => {
1297
+ try {
1298
+ const { code } = await runBlePython(runtime.python, [
1299
+ runtime.paths.scripts.idotmatrixBrightness,
1300
+ '-a',
1301
+ targetAddress,
1302
+ '-b',
1303
+ String(value),
1304
+ ]);
1272
1305
  if (code === 0) {
1273
1306
  log('Brightness updated successfully.');
1274
1307
  }
1275
1308
  else {
1276
1309
  log(`Failed to update brightness, process exited with code ${code}`);
1310
+ process.exitCode = code ?? 1;
1277
1311
  }
1278
- });
1312
+ }
1313
+ catch (err) {
1314
+ log(`Failed to start brightness command: ${err instanceof Error ? err.message : String(err)}`);
1315
+ process.exitCode = 1;
1316
+ }
1279
1317
  });
1280
1318
  idotmatrix
1281
1319
  .command('sync [address]')
@@ -1286,9 +1324,6 @@ idotmatrix
1286
1324
  .action(async (addressOpt, opts) => {
1287
1325
  const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
1288
1326
  const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
1289
- const { dirname } = await import('path');
1290
- const { fileURLToPath } = await import('url');
1291
- const { spawn } = await import('child_process');
1292
1327
  let targetAddress = addressOpt;
1293
1328
  let defaultBrightness = 100;
1294
1329
  const devices = loadIDotMatrixDevices();
@@ -1314,17 +1349,30 @@ idotmatrix
1314
1349
  ? parseInt(opts.port, 10)
1315
1350
  : (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
1316
1351
  const url = `http://127.0.0.1:${port}`;
1317
- const distPath = dirname(fileURLToPath(import.meta.url));
1318
- const projectRoot = join(distPath, '..', '..');
1319
- const venvPython = join(projectRoot, '.venv', 'bin', 'python');
1320
- const syncScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'sync.py');
1352
+ const runtime = prepareBleRuntime();
1353
+ if (!runtime)
1354
+ return;
1321
1355
  log(`Starting BLE sync client linking to bridge at ${url} (brightness ${brightness}%, boost ${boost}x)...`);
1322
- const py = spawn(venvPython, [syncScript, '-a', targetAddress, '-u', url, '-b', String(brightness), '--boost', String(boost)], {
1323
- stdio: 'inherit'
1324
- });
1325
- py.on('close', (code) => {
1356
+ try {
1357
+ const { code } = await runBlePython(runtime.python, [
1358
+ runtime.paths.scripts.idotmatrixSync,
1359
+ '-a',
1360
+ targetAddress,
1361
+ '-u',
1362
+ url,
1363
+ '-b',
1364
+ String(brightness),
1365
+ '--boost',
1366
+ String(boost),
1367
+ ]);
1326
1368
  log(`Sync process exited with code ${code}`);
1327
- });
1369
+ if (code !== 0)
1370
+ process.exitCode = code ?? 1;
1371
+ }
1372
+ catch (err) {
1373
+ log(`Failed to start BLE sync: ${err instanceof Error ? err.message : String(err)}`);
1374
+ process.exitCode = 1;
1375
+ }
1328
1376
  });
1329
1377
  // ===== Divoom Timebox Mini Light commands =====
1330
1378
  const timebox = program.command('timebox').description('Manage Divoom Timebox Mini devices (Bluetooth LE)');
@@ -1332,22 +1380,22 @@ timebox
1332
1380
  .command('scan')
1333
1381
  .description('Discover BLE TimeBox-mini-light peripherals')
1334
1382
  .action(async () => {
1335
- const paths = resolveTimeboxSyncPaths();
1383
+ const runtime = prepareBleRuntime();
1384
+ if (!runtime)
1385
+ return;
1336
1386
  log('Scanning for BLE devices (5 seconds)...');
1337
- const py = spawn(paths.python, [paths.scanScript]);
1338
- let stdoutData = '';
1339
- py.stdout.on('data', (data) => {
1340
- stdoutData += data.toString();
1341
- });
1342
- py.on('close', (code) => {
1387
+ try {
1388
+ const { code, stdout } = await runBlePython(runtime.python, [runtime.paths.scripts.timeboxScan], true);
1343
1389
  if (code !== 0) {
1344
- log('BLE scan failed (is bleak installed in .venv?).');
1390
+ log('BLE scan failed.');
1391
+ process.exitCode = 1;
1345
1392
  return;
1346
1393
  }
1347
1394
  try {
1348
- const devices = JSON.parse(stdoutData.trim());
1395
+ const devices = JSON.parse(stdout.trim());
1349
1396
  if (devices.error) {
1350
1397
  log(`BLE scan error: ${devices.error}`);
1398
+ process.exitCode = 1;
1351
1399
  return;
1352
1400
  }
1353
1401
  if (!Array.isArray(devices) || devices.length === 0) {
@@ -1363,9 +1411,14 @@ timebox
1363
1411
  log('\nAdd the starred BLE device with: agentdeck timebox add <address>');
1364
1412
  }
1365
1413
  catch {
1366
- log(`Failed to parse BLE scan output: ${stdoutData}`);
1414
+ log(`Failed to parse BLE scan output: ${stdout}`);
1415
+ process.exitCode = 1;
1367
1416
  }
1368
- });
1417
+ }
1418
+ catch (err) {
1419
+ log(`Failed to start BLE scan: ${err instanceof Error ? err.message : String(err)}`);
1420
+ process.exitCode = 1;
1421
+ }
1369
1422
  });
1370
1423
  timebox
1371
1424
  .command('add <address>')
@@ -1442,11 +1495,22 @@ async function runTimeboxSync(targetOpt, opts, once) {
1442
1495
  ? parseInt(opts.bridgePort, 10)
1443
1496
  : (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
1444
1497
  const url = `http://127.0.0.1:${bridgePort}`;
1445
- const paths = resolveTimeboxSyncPaths();
1446
- const args = [paths.bleScript, '--address', id, '--url', url, '--brightness', String(brightness), ...(once ? ['--once'] : [])];
1498
+ const runtime = prepareBleRuntime();
1499
+ if (!runtime)
1500
+ return;
1501
+ const args = [
1502
+ runtime.paths.scripts.timeboxSync,
1503
+ '--address',
1504
+ id,
1505
+ '--url',
1506
+ url,
1507
+ '--brightness',
1508
+ String(brightness),
1509
+ ...(once ? ['--once'] : []),
1510
+ ];
1447
1511
  log(`${once ? 'Sending one Timebox frame' : 'Starting Timebox sync'} via BLE ${id} from ${url} (brightness ${brightness}%)...`);
1448
- const py = spawn(paths.python, args, { stdio: 'inherit' });
1449
- py.on('close', (code) => {
1512
+ try {
1513
+ const { code } = await runBlePython(runtime.python, args);
1450
1514
  if (once) {
1451
1515
  if (code === 0) {
1452
1516
  log('Timebox test frame sent.');
@@ -1458,8 +1522,14 @@ async function runTimeboxSync(targetOpt, opts, once) {
1458
1522
  }
1459
1523
  else {
1460
1524
  log(`Timebox sync process exited with code ${code}`);
1525
+ if (code !== 0)
1526
+ process.exitCode = code ?? 1;
1461
1527
  }
1462
- });
1528
+ }
1529
+ catch (err) {
1530
+ log(`Failed to start Timebox BLE command: ${err instanceof Error ? err.message : String(err)}`);
1531
+ process.exitCode = 1;
1532
+ }
1463
1533
  }
1464
1534
  timebox
1465
1535
  .command('test [target]')