@agentdeck/bridge 1.0.3 → 1.0.5
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/assets/fm-helper/agentdeck-fm-helper +0 -0
- package/dist/adapters/claude-code.d.ts.map +1 -1
- package/dist/adapters/claude-code.js +3 -0
- package/dist/adapters/claude-code.js.map +1 -1
- package/dist/adb-reverse.d.ts.map +1 -1
- package/dist/adb-reverse.js +4 -2
- package/dist/adb-reverse.js.map +1 -1
- package/dist/apme/collector.js +2 -2
- package/dist/apme/collector.js.map +1 -1
- package/dist/apme/outcome.js +2 -2
- package/dist/apme/outcome.js.map +1 -1
- package/dist/apme/runner.js +3 -3
- package/dist/apme/runner.js.map +1 -1
- package/dist/ble-sync-spawn.js +1 -1
- package/dist/ble-sync-spawn.js.map +1 -1
- package/dist/calendar.d.ts +82 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +324 -0
- package/dist/calendar.js.map +1 -0
- package/dist/card-feed.d.ts +63 -5
- package/dist/card-feed.d.ts.map +1 -1
- package/dist/card-feed.js +218 -14
- package/dist/card-feed.js.map +1 -1
- package/dist/card-modules.d.ts +4 -0
- package/dist/card-modules.d.ts.map +1 -1
- package/dist/card-modules.js +5 -3
- package/dist/card-modules.js.map +1 -1
- package/dist/check-deps.js +4 -4
- package/dist/check-deps.js.map +1 -1
- package/dist/cli.d.ts +35 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +406 -76
- package/dist/cli.js.map +1 -1
- package/dist/codex-otel.d.ts +89 -0
- package/dist/codex-otel.d.ts.map +1 -0
- package/dist/codex-otel.js +487 -0
- package/dist/codex-otel.js.map +1 -0
- package/dist/codex-rate-limits.d.ts.map +1 -1
- package/dist/codex-rate-limits.js +15 -3
- package/dist/codex-rate-limits.js.map +1 -1
- package/dist/daemon-server.d.ts +4 -0
- package/dist/daemon-server.d.ts.map +1 -1
- package/dist/daemon-server.js +797 -54
- package/dist/daemon-server.js.map +1 -1
- package/dist/daemon-ws-client.d.ts +108 -24
- package/dist/daemon-ws-client.d.ts.map +1 -1
- package/dist/daemon-ws-client.js +175 -39
- package/dist/daemon-ws-client.js.map +1 -1
- package/dist/daemon.js +1 -1
- package/dist/device-voice-reply.d.ts.map +1 -1
- package/dist/device-voice-reply.js +15 -3
- package/dist/device-voice-reply.js.map +1 -1
- package/dist/device-voice.d.ts +37 -0
- package/dist/device-voice.d.ts.map +1 -1
- package/dist/device-voice.js +130 -1
- package/dist/device-voice.js.map +1 -1
- package/dist/diag-analyzer.d.ts.map +1 -1
- package/dist/diag-analyzer.js +1 -0
- package/dist/diag-analyzer.js.map +1 -1
- package/dist/display-monitor.d.ts.map +1 -1
- package/dist/display-monitor.js +1 -0
- package/dist/display-monitor.js.map +1 -1
- package/dist/esp32-serial.d.ts +4 -0
- package/dist/esp32-serial.d.ts.map +1 -1
- package/dist/esp32-serial.js +6 -3
- package/dist/esp32-serial.js.map +1 -1
- package/dist/foundation-models-helper.d.ts +23 -0
- package/dist/foundation-models-helper.d.ts.map +1 -1
- package/dist/foundation-models-helper.js +32 -0
- package/dist/foundation-models-helper.js.map +1 -1
- package/dist/gateway-probe.js +1 -1
- package/dist/gateway-probe.js.map +1 -1
- package/dist/glance-frame.d.ts +64 -0
- package/dist/glance-frame.d.ts.map +1 -0
- package/dist/glance-frame.js +364 -0
- package/dist/glance-frame.js.map +1 -0
- package/dist/hook-codex-sessions.d.ts +40 -0
- package/dist/hook-codex-sessions.d.ts.map +1 -0
- package/dist/hook-codex-sessions.js +174 -0
- package/dist/hook-codex-sessions.js.map +1 -0
- package/dist/idotmatrix/idotmatrix-daemon-sync.d.ts.map +1 -1
- package/dist/idotmatrix/idotmatrix-daemon-sync.js +5 -18
- package/dist/idotmatrix/idotmatrix-daemon-sync.js.map +1 -1
- package/dist/idotmatrix/idotmatrix-discover.d.ts.map +1 -1
- package/dist/idotmatrix/idotmatrix-discover.js +12 -17
- package/dist/idotmatrix/idotmatrix-discover.js.map +1 -1
- package/dist/idotmatrix/idotmatrix-settings.d.ts +15 -0
- package/dist/idotmatrix/idotmatrix-settings.d.ts.map +1 -1
- package/dist/idotmatrix/idotmatrix-settings.js +21 -0
- package/dist/idotmatrix/idotmatrix-settings.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +88 -7
- package/dist/index.js.map +1 -1
- package/dist/linux-service.d.ts +52 -0
- package/dist/linux-service.d.ts.map +1 -0
- package/dist/linux-service.js +206 -0
- package/dist/linux-service.js.map +1 -0
- package/dist/logger.d.ts +6 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +15 -0
- package/dist/logger.js.map +1 -1
- package/dist/mdns-discover.d.ts +19 -0
- package/dist/mdns-discover.d.ts.map +1 -0
- package/dist/mdns-discover.js +109 -0
- package/dist/mdns-discover.js.map +1 -0
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +1 -0
- package/dist/model-catalog.js.map +1 -1
- package/dist/modules/adb-module.js +1 -1
- package/dist/modules/adb-module.js.map +1 -1
- package/dist/output-parser.d.ts +9 -0
- package/dist/output-parser.d.ts.map +1 -1
- package/dist/output-parser.js +67 -16
- package/dist/output-parser.js.map +1 -1
- package/dist/passive-observer.d.ts +57 -8
- package/dist/passive-observer.d.ts.map +1 -1
- package/dist/passive-observer.js +210 -74
- package/dist/passive-observer.js.map +1 -1
- package/dist/pixoo/pixoo-bridge.d.ts.map +1 -1
- package/dist/pixoo/pixoo-bridge.js +47 -6
- package/dist/pixoo/pixoo-bridge.js.map +1 -1
- package/dist/pixoo/pixoo-renderer.d.ts +2 -1
- package/dist/pixoo/pixoo-renderer.d.ts.map +1 -1
- package/dist/pixoo/pixoo-renderer.js +137 -8
- package/dist/pixoo/pixoo-renderer.js.map +1 -1
- package/dist/pocket-autonomy.d.ts +78 -0
- package/dist/pocket-autonomy.d.ts.map +1 -0
- package/dist/pocket-autonomy.js +529 -0
- package/dist/pocket-autonomy.js.map +1 -0
- package/dist/pty-manager.d.ts.map +1 -1
- package/dist/pty-manager.js +3 -2
- package/dist/pty-manager.js.map +1 -1
- package/dist/python-ble-runtime.d.ts +64 -0
- package/dist/python-ble-runtime.d.ts.map +1 -0
- package/dist/python-ble-runtime.js +216 -0
- package/dist/python-ble-runtime.js.map +1 -0
- package/dist/remote-sessions.d.ts +64 -0
- package/dist/remote-sessions.d.ts.map +1 -0
- package/dist/remote-sessions.js +81 -0
- package/dist/remote-sessions.js.map +1 -0
- package/dist/review-runner.js +2 -2
- package/dist/review-runner.js.map +1 -1
- package/dist/session-aggregator.d.ts +2 -0
- package/dist/session-aggregator.d.ts.map +1 -1
- package/dist/session-aggregator.js +6 -1
- package/dist/session-aggregator.js.map +1 -1
- package/dist/session-focus-relay.d.ts +47 -1
- package/dist/session-focus-relay.d.ts.map +1 -1
- package/dist/session-focus-relay.js +130 -15
- package/dist/session-focus-relay.js.map +1 -1
- package/dist/session-push-channel.d.ts +31 -0
- package/dist/session-push-channel.d.ts.map +1 -0
- package/dist/session-push-channel.js +94 -0
- package/dist/session-push-channel.js.map +1 -0
- package/dist/session-registry.d.ts +126 -0
- package/dist/session-registry.d.ts.map +1 -1
- package/dist/session-registry.js +180 -3
- package/dist/session-registry.js.map +1 -1
- package/dist/subagent-timeline.d.ts +34 -0
- package/dist/subagent-timeline.d.ts.map +1 -0
- package/dist/subagent-timeline.js +156 -0
- package/dist/subagent-timeline.js.map +1 -0
- package/dist/timebox/timebox-daemon-sync.d.ts.map +1 -1
- package/dist/timebox/timebox-daemon-sync.js +5 -20
- package/dist/timebox/timebox-daemon-sync.js.map +1 -1
- package/dist/timebox/timebox-discover.d.ts.map +1 -1
- package/dist/timebox/timebox-discover.js +7 -16
- package/dist/timebox/timebox-discover.js.map +1 -1
- package/dist/tui/gauge.d.ts +4 -2
- package/dist/tui/gauge.d.ts.map +1 -1
- package/dist/tui/gauge.js +8 -4
- package/dist/tui/gauge.js.map +1 -1
- package/dist/tui/renderer.d.ts.map +1 -1
- package/dist/tui/renderer.js +28 -0
- package/dist/tui/renderer.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/usage-api.d.ts +43 -0
- package/dist/usage-api.d.ts.map +1 -1
- package/dist/usage-api.js +114 -9
- package/dist/usage-api.js.map +1 -1
- package/dist/usage-event.d.ts +19 -0
- package/dist/usage-event.d.ts.map +1 -1
- package/dist/usage-event.js +35 -0
- package/dist/usage-event.js.map +1 -1
- package/dist/utils/project-name.d.ts.map +1 -1
- package/dist/utils/project-name.js +1 -0
- package/dist/utils/project-name.js.map +1 -1
- package/dist/version-check.d.ts.map +1 -1
- package/dist/version-check.js +3 -2
- package/dist/version-check.js.map +1 -1
- package/dist/voice-assistant.d.ts.map +1 -1
- package/dist/voice-assistant.js +2 -1
- package/dist/voice-assistant.js.map +1 -1
- package/dist/voice.d.ts.map +1 -1
- package/dist/voice.js +1 -0
- package/dist/voice.js.map +1 -1
- package/dist/weather.d.ts +72 -0
- package/dist/weather.d.ts.map +1 -0
- package/dist/weather.js +206 -0
- package/dist/weather.js.map +1 -0
- package/dist/windows-service.js +5 -5
- package/dist/windows-service.js.map +1 -1
- package/fm-helper/AgentDeckFMHelper.swift +200 -0
- package/package.json +20 -14
- package/python/requirements-ble.txt +5 -0
- package/src/idotmatrix/brightness.py +40 -0
- package/src/idotmatrix/identity_generated.py +50 -0
- package/src/idotmatrix/scan.py +55 -0
- package/src/idotmatrix/sync.py +400 -0
- package/src/pysync/matrix_sync_common.py +89 -0
- package/src/timebox/scan_ble.py +52 -0
- package/src/timebox/sync_ble.py +394 -0
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from 'commander';
|
|
2
|
+
import { Command, InvalidArgumentError } from 'commander';
|
|
3
3
|
import { writeFileSync, unlinkSync, existsSync, realpathSync } from 'fs';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { dirname, join } from 'path';
|
|
@@ -8,12 +8,42 @@ 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 { SESSION_WEIGHT_MIN, SESSION_WEIGHT_MAX } from '@agentdeck/shared';
|
|
12
|
+
import { ensureBleRuntime, getBleRuntimeStatus } from './python-ble-runtime.js';
|
|
11
13
|
import { TASK_NAME, installWindowsTask, taskExists, runWindowsTask, endWindowsTask, deleteWindowsTask, } from './windows-service.js';
|
|
14
|
+
import { deriveRemoteAttachOpts } from './session-registry.js';
|
|
15
|
+
import { SERVICE_NAME, hasSystemctl, unitExists, installUnit, startUnit, disableUnit, getUnitPath, getDataDir, } from './linux-service.js';
|
|
12
16
|
const require = createRequire(import.meta.url);
|
|
13
17
|
const packageJson = require('../package.json');
|
|
18
|
+
/**
|
|
19
|
+
* Pre-PTY warning for a host hint given without the opt-in switch. Must run in
|
|
20
|
+
* the CLI action handler BEFORE the bridge starts: once the PTY takes the
|
|
21
|
+
* terminal, `log()` output is swallowed and the user would never see it.
|
|
22
|
+
*/
|
|
23
|
+
function warnIfDaemonHostIgnored(opts) {
|
|
24
|
+
if (deriveRemoteAttachOpts(opts).ignoredHostHint) {
|
|
25
|
+
console.error('[agentdeck] --daemon-host / AGENTDECK_DAEMON_HOST is set but ignored: remote attach requires --remote-daemon (or AGENTDECK_REMOTE_DAEMON=1). Session stays local-only.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
14
28
|
function log(msg) {
|
|
15
29
|
process.stderr.write(msg + '\n');
|
|
16
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Parse a `--weight <n>` value into an integer sort override. Rejects
|
|
33
|
+
* non-integers so a typo (`--weight foo`) fails loudly instead of silently
|
|
34
|
+
* collapsing to 0, and rejects values outside the documented cross-platform
|
|
35
|
+
* range (shared SESSION_WEIGHT_MIN/MAX) — the wire value must fit every
|
|
36
|
+
* consumer's integer type (Kotlin `Int` in particular), and comparators on
|
|
37
|
+
* fixed-width platforms must never see values that could overflow. Lower
|
|
38
|
+
* sorts first (see shared sortSessions).
|
|
39
|
+
*/
|
|
40
|
+
function parseWeight(value) {
|
|
41
|
+
const n = Number(value);
|
|
42
|
+
if (!Number.isInteger(n) || n < SESSION_WEIGHT_MIN || n > SESSION_WEIGHT_MAX) {
|
|
43
|
+
throw new InvalidArgumentError(`weight must be an integer between ${SESSION_WEIGHT_MIN} and ${SESSION_WEIGHT_MAX} (e.g. --weight 1, --weight -5)`);
|
|
44
|
+
}
|
|
45
|
+
return n;
|
|
46
|
+
}
|
|
17
47
|
function formatBytes(value) {
|
|
18
48
|
const bytes = typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
|
19
49
|
if (bytes >= 1024 * 1024)
|
|
@@ -135,19 +165,30 @@ async function isDaemonPort(port) {
|
|
|
135
165
|
return false;
|
|
136
166
|
}
|
|
137
167
|
}
|
|
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
168
|
function projectRootPath() {
|
|
149
169
|
return join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
150
170
|
}
|
|
171
|
+
async function runBlePython(python, args, captureStdout = false) {
|
|
172
|
+
return new Promise((resolve, reject) => {
|
|
173
|
+
const child = spawn(python, args, captureStdout ? { stdio: ['ignore', 'pipe', 'inherit'], windowsHide: true } : { stdio: 'inherit', windowsHide: true });
|
|
174
|
+
let stdout = '';
|
|
175
|
+
child.stdout?.on('data', (data) => {
|
|
176
|
+
stdout += data.toString();
|
|
177
|
+
});
|
|
178
|
+
child.once('error', reject);
|
|
179
|
+
child.once('close', (code) => resolve({ code, stdout }));
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function prepareBleRuntime() {
|
|
183
|
+
try {
|
|
184
|
+
return ensureBleRuntime({ log });
|
|
185
|
+
}
|
|
186
|
+
catch (err) {
|
|
187
|
+
log(`BLE support unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
188
|
+
process.exitCode = 1;
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
151
192
|
const ESP32_OTA_BOARDS = [
|
|
152
193
|
{ board: 'inkdeck', env: 'inkdeck', aliases: [] },
|
|
153
194
|
{ board: 'ulanzi_tc001', env: 'led8x32', aliases: ['led8x32'] },
|
|
@@ -192,6 +233,117 @@ function platformioBin() {
|
|
|
192
233
|
return '/opt/homebrew/bin/pio';
|
|
193
234
|
return 'pio';
|
|
194
235
|
}
|
|
236
|
+
// ===== Env-var default CLI args =====
|
|
237
|
+
//
|
|
238
|
+
// Two layers of environment-variable defaults, so a shell profile can set
|
|
239
|
+
// repeated flags once:
|
|
240
|
+
// AGENTDECK_COMMANDER_ARGS → args for the `agentdeck` (commander) layer,
|
|
241
|
+
// spliced into argv after the session subcommand.
|
|
242
|
+
// AGENTDECK_<AGENT>_ARGS → args appended to the spawned agent command,
|
|
243
|
+
// weaving into an explicit `-c` when present.
|
|
244
|
+
// See docs and cli.test.ts for behavior.
|
|
245
|
+
/** Session subcommands the global env-args var applies to. */
|
|
246
|
+
const SESSION_COMMANDS = ['claude', 'codex', 'opencode', 'monitor'];
|
|
247
|
+
/** Env var holding extra args for the spawned agent command, per agent type. */
|
|
248
|
+
const AGENT_ARGS_ENV = {
|
|
249
|
+
'claude-code': 'AGENTDECK_CLAUDE_ARGS',
|
|
250
|
+
'codex-cli': 'AGENTDECK_CODEX_ARGS',
|
|
251
|
+
opencode: 'AGENTDECK_OPENCODE_ARGS',
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Split an env-provided arg string into tokens, honoring single/double quotes
|
|
255
|
+
* so values like `--flag "a b"` survive. No shell expansion (no $VAR,
|
|
256
|
+
* globbing, or escapes beyond quote grouping) — this only groups whitespace.
|
|
257
|
+
* Cross-platform: pure JS, never invokes a shell.
|
|
258
|
+
*/
|
|
259
|
+
export function tokenizeArgString(s) {
|
|
260
|
+
if (!s)
|
|
261
|
+
return [];
|
|
262
|
+
const tokens = [];
|
|
263
|
+
// Match: double-quoted run | single-quoted run | bare non-space run.
|
|
264
|
+
const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
|
|
265
|
+
let cur = '';
|
|
266
|
+
let curActive = false;
|
|
267
|
+
let lastIndex = 0;
|
|
268
|
+
let m;
|
|
269
|
+
while ((m = re.exec(s)) !== null) {
|
|
270
|
+
// A gap of whitespace between matches ends the current token.
|
|
271
|
+
if (curActive && m.index > lastIndex) {
|
|
272
|
+
tokens.push(cur);
|
|
273
|
+
cur = '';
|
|
274
|
+
curActive = false;
|
|
275
|
+
}
|
|
276
|
+
cur += m[1] ?? m[2] ?? m[3] ?? '';
|
|
277
|
+
curActive = true;
|
|
278
|
+
lastIndex = re.lastIndex;
|
|
279
|
+
}
|
|
280
|
+
if (curActive)
|
|
281
|
+
tokens.push(cur);
|
|
282
|
+
return tokens;
|
|
283
|
+
}
|
|
284
|
+
/** Per-invocation escape hatch: disables BOTH env-default layers when typed. */
|
|
285
|
+
const NO_ENV_ARGS_FLAG = '--no-env-args';
|
|
286
|
+
/**
|
|
287
|
+
* If argv's subcommand is a session command and AGENTDECK_COMMANDER_ARGS is set,
|
|
288
|
+
* splice its tokens in immediately after the subcommand name. Env tokens land
|
|
289
|
+
* before any user-typed tokens, giving last-write override for SCALAR options
|
|
290
|
+
* (retype `--weight`/`--daemon-host` to win); boolean flags have no inverse
|
|
291
|
+
* spelling, so the per-invocation override is the typed `--no-env-args` hatch,
|
|
292
|
+
* which skips the splice entirely (and, parsed by commander, turns the
|
|
293
|
+
* per-agent append off in the session actions too).
|
|
294
|
+
* Returns a new argv; no-op (returns input) when unset or not a session command.
|
|
295
|
+
*/
|
|
296
|
+
export function applyGlobalEnvArgs(argv, env = process.env) {
|
|
297
|
+
// The hatch itself is stripped from the env tokens: an env-smuggled
|
|
298
|
+
// --no-env-args would otherwise splice normally and then parse to
|
|
299
|
+
// envArgs=false, silently disabling only the per-agent layer — an
|
|
300
|
+
// undocumented half-state. The env var cannot turn the hatch on or off;
|
|
301
|
+
// only a typed flag can.
|
|
302
|
+
const extra = tokenizeArgString(env.AGENTDECK_COMMANDER_ARGS)
|
|
303
|
+
.filter((tok) => tok !== NO_ENV_ARGS_FLAG);
|
|
304
|
+
if (extra.length === 0)
|
|
305
|
+
return argv;
|
|
306
|
+
// The CLI has a fixed top-level command position: argv[0]=node,
|
|
307
|
+
// argv[1]=script, argv[2]=subcommand. Keying the decision to argv[2] (not a
|
|
308
|
+
// token scan) keeps non-session commands a true no-op even when a
|
|
309
|
+
// positional VALUE equals a session command name (`agentdeck speak b claude`).
|
|
310
|
+
const sub = argv[2];
|
|
311
|
+
if (sub === undefined || !SESSION_COMMANDS.includes(sub))
|
|
312
|
+
return argv;
|
|
313
|
+
// Typed escape hatch — checked on the RAW argv before splicing, because
|
|
314
|
+
// post-parse is too late (env tokens would already have parsed into opts).
|
|
315
|
+
// Exact-token match: a flag VALUE lexically equal to --no-env-args would
|
|
316
|
+
// false-positive, but producing that requires a deliberately pathological
|
|
317
|
+
// invocation (`-c --no-env-args`), and the argv[2] gate above bounds the
|
|
318
|
+
// scan to session commands.
|
|
319
|
+
if (argv.slice(3).includes(NO_ENV_ARGS_FLAG))
|
|
320
|
+
return argv;
|
|
321
|
+
const next = argv.slice();
|
|
322
|
+
next.splice(3, 0, ...extra);
|
|
323
|
+
return next;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Append the per-agent env var to the resolved `-c` command string. Raw append
|
|
327
|
+
* (single space + trim) — the PTY shell re-parses the whole string at spawn, so
|
|
328
|
+
* this rides the same platform shell path (cmd.exe / POSIX) that already handles
|
|
329
|
+
* a user's `-c`. No-op (returns `command`) when the var is unset/empty.
|
|
330
|
+
*/
|
|
331
|
+
export function weaveAgentCommand(agentType, command, env = process.env) {
|
|
332
|
+
const extra = (env[AGENT_ARGS_ENV[agentType]] ?? '').trim();
|
|
333
|
+
if (!extra)
|
|
334
|
+
return command;
|
|
335
|
+
return `${command} ${extra}`.trim();
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* The session actions' one-call selection for the spawned agent command:
|
|
339
|
+
* weave the per-agent env args unless the user typed `--no-env-args`
|
|
340
|
+
* (commander parses it as `opts.envArgs === false`; the hatch covers BOTH
|
|
341
|
+
* env layers, and the commander-layer half is handled pre-parse in
|
|
342
|
+
* `applyGlobalEnvArgs`).
|
|
343
|
+
*/
|
|
344
|
+
export function resolveAgentCommand(agentType, command, envArgsEnabled, env = process.env) {
|
|
345
|
+
return envArgsEnabled ? weaveAgentCommand(agentType, command, env) : command;
|
|
346
|
+
}
|
|
195
347
|
// ===== Program =====
|
|
196
348
|
const program = new Command();
|
|
197
349
|
program
|
|
@@ -211,16 +363,24 @@ program
|
|
|
211
363
|
.option('--no-adb', 'Disable ADB reverse setup')
|
|
212
364
|
.option('--no-postit', 'Disable terminal tab title updates')
|
|
213
365
|
.option('--wake-word', 'Enable wake word voice assistant ("오픈클로")')
|
|
366
|
+
.option('--remote-daemon', 'Opt in to attaching this session to a daemon on another machine (gates --daemon-host and mDNS discovery)')
|
|
367
|
+
.option('--daemon-host <host>', 'Explicit remote daemon endpoint (host or host:port); requires --remote-daemon')
|
|
368
|
+
.option('--no-env-args', 'Ignore AGENTDECK_COMMANDER_ARGS and AGENTDECK_<AGENT>_ARGS for this invocation')
|
|
369
|
+
.option('--weight <n>', 'Deck/tab sort order override (integer, lower first; default 0)', parseWeight)
|
|
214
370
|
.action(async (opts) => {
|
|
371
|
+
warnIfDaemonHostIgnored(opts);
|
|
215
372
|
const { startSession } = await import('./index.js');
|
|
216
373
|
await startSession({
|
|
217
374
|
agentType: 'claude-code',
|
|
218
375
|
port: parseInt(opts.port, 10),
|
|
219
|
-
command: opts.command,
|
|
376
|
+
command: resolveAgentCommand('claude-code', opts.command, opts.envArgs !== false),
|
|
220
377
|
debug: opts.debug,
|
|
221
378
|
noUpdateCheck: opts.updateCheck === false,
|
|
222
379
|
postit: opts.postit !== false,
|
|
223
380
|
wakeWord: !!opts.wakeWord,
|
|
381
|
+
remoteDaemon: !!opts.remoteDaemon,
|
|
382
|
+
daemonHost: opts.daemonHost,
|
|
383
|
+
weight: opts.weight,
|
|
224
384
|
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
|
|
225
385
|
mdns: false, // daemon-only — session bridges never advertise mDNS
|
|
226
386
|
adb: opts.adb !== false ? 'auto' : false,
|
|
@@ -240,7 +400,12 @@ program
|
|
|
240
400
|
.option('--no-adb', 'Disable ADB reverse setup')
|
|
241
401
|
.option('--no-postit', 'Disable terminal tab title updates')
|
|
242
402
|
.option('--no-codex-hooks', 'Skip ~/.codex/config.toml hook install')
|
|
403
|
+
.option('--remote-daemon', 'Opt in to attaching this session to a daemon on another machine (gates --daemon-host and mDNS discovery)')
|
|
404
|
+
.option('--daemon-host <host>', 'Explicit remote daemon endpoint (host or host:port); requires --remote-daemon')
|
|
405
|
+
.option('--no-env-args', 'Ignore AGENTDECK_COMMANDER_ARGS and AGENTDECK_<AGENT>_ARGS for this invocation')
|
|
406
|
+
.option('--weight <n>', 'Deck/tab sort order override (integer, lower first; default 0)', parseWeight)
|
|
243
407
|
.action(async (opts) => {
|
|
408
|
+
warnIfDaemonHostIgnored(opts);
|
|
244
409
|
// Install Codex lifecycle hooks before starting the session so the
|
|
245
410
|
// first prompt's UserPromptSubmit / Stop events reach the daemon.
|
|
246
411
|
// Idempotent: re-running with the same daemon port is a no-op.
|
|
@@ -269,9 +434,12 @@ program
|
|
|
269
434
|
await startSession({
|
|
270
435
|
agentType: 'codex-cli',
|
|
271
436
|
port: parseInt(opts.port, 10),
|
|
272
|
-
command: opts.command,
|
|
437
|
+
command: resolveAgentCommand('codex-cli', opts.command, opts.envArgs !== false),
|
|
273
438
|
debug: opts.debug,
|
|
274
439
|
postit: opts.postit !== false,
|
|
440
|
+
remoteDaemon: !!opts.remoteDaemon,
|
|
441
|
+
daemonHost: opts.daemonHost,
|
|
442
|
+
weight: opts.weight,
|
|
275
443
|
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
|
|
276
444
|
mdns: false, // daemon-only
|
|
277
445
|
adb: opts.adb !== false ? 'auto' : false,
|
|
@@ -291,7 +459,12 @@ program
|
|
|
291
459
|
.option('--no-adb', 'Disable ADB reverse setup')
|
|
292
460
|
.option('--no-postit', 'Disable terminal tab title updates')
|
|
293
461
|
.option('--no-opencode-hooks', 'Skip OpenCode observer plugin install')
|
|
462
|
+
.option('--remote-daemon', 'Opt in to attaching this session to a daemon on another machine (gates --daemon-host and mDNS discovery)')
|
|
463
|
+
.option('--daemon-host <host>', 'Explicit remote daemon endpoint (host or host:port); requires --remote-daemon')
|
|
464
|
+
.option('--no-env-args', 'Ignore AGENTDECK_COMMANDER_ARGS and AGENTDECK_<AGENT>_ARGS for this invocation')
|
|
465
|
+
.option('--weight <n>', 'Deck/tab sort order override (integer, lower first; default 0)', parseWeight)
|
|
294
466
|
.action(async (opts) => {
|
|
467
|
+
warnIfDaemonHostIgnored(opts);
|
|
295
468
|
// Install the OpenCode observer plugin so standalone `opencode` runs
|
|
296
469
|
// (outside this managed session) also reach the daemon timeline. The
|
|
297
470
|
// plugin self-disables inside managed PTYs via AGENTDECK_PORT, so this
|
|
@@ -319,9 +492,12 @@ program
|
|
|
319
492
|
await startSession({
|
|
320
493
|
agentType: 'opencode',
|
|
321
494
|
port: parseInt(opts.port, 10),
|
|
322
|
-
command: opts.command,
|
|
495
|
+
command: resolveAgentCommand('opencode', opts.command, opts.envArgs !== false),
|
|
323
496
|
debug: opts.debug,
|
|
324
497
|
postit: opts.postit !== false,
|
|
498
|
+
remoteDaemon: !!opts.remoteDaemon,
|
|
499
|
+
daemonHost: opts.daemonHost,
|
|
500
|
+
weight: opts.weight,
|
|
325
501
|
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : {
|
|
326
502
|
mdns: false,
|
|
327
503
|
adb: opts.adb !== false ? 'auto' : false,
|
|
@@ -337,7 +513,13 @@ program
|
|
|
337
513
|
.option('-p, --port <port>', 'Bridge server port', String(BRIDGE_WS_PORT))
|
|
338
514
|
.option('-d, --debug', 'Enable debug logging')
|
|
339
515
|
.option('--local', 'Disable all device modules')
|
|
516
|
+
.option('--remote-daemon', 'Opt in to attaching this session to a daemon on another machine (gates --daemon-host and mDNS discovery)')
|
|
517
|
+
.option('--daemon-host <host>', 'Explicit remote daemon endpoint (host or host:port); requires --remote-daemon')
|
|
518
|
+
// Commander-layer half only: monitor spawns no agent command to weave into.
|
|
519
|
+
.option('--no-env-args', 'Ignore AGENTDECK_COMMANDER_ARGS for this invocation')
|
|
520
|
+
.option('--weight <n>', 'Deck/tab sort order override (integer, lower first; default 0)', parseWeight)
|
|
340
521
|
.action(async (opts) => {
|
|
522
|
+
warnIfDaemonHostIgnored(opts);
|
|
341
523
|
const { startSession } = await import('./index.js');
|
|
342
524
|
const port = parseInt(opts.port, 10);
|
|
343
525
|
log(`\nMonitor mode: hook server on port ${port}`);
|
|
@@ -346,6 +528,9 @@ program
|
|
|
346
528
|
agentType: 'monitor',
|
|
347
529
|
port,
|
|
348
530
|
debug: opts.debug,
|
|
531
|
+
remoteDaemon: !!opts.remoteDaemon,
|
|
532
|
+
daemonHost: opts.daemonHost,
|
|
533
|
+
weight: opts.weight,
|
|
349
534
|
modules: opts.local ? { mdns: false, adb: false, serial: false, pixoo: false, timebox: false } : undefined,
|
|
350
535
|
});
|
|
351
536
|
});
|
|
@@ -453,6 +638,7 @@ daemon
|
|
|
453
638
|
const child = spawn(process.execPath, args, {
|
|
454
639
|
detached: true,
|
|
455
640
|
stdio: ['ignore', out, err],
|
|
641
|
+
windowsHide: true,
|
|
456
642
|
});
|
|
457
643
|
child.unref();
|
|
458
644
|
log(`Daemon started (PID ${child.pid})`);
|
|
@@ -490,6 +676,7 @@ daemon
|
|
|
490
676
|
const child = spawn(process.execPath, args, {
|
|
491
677
|
detached: true,
|
|
492
678
|
stdio: 'ignore',
|
|
679
|
+
windowsHide: true,
|
|
493
680
|
});
|
|
494
681
|
child.unref();
|
|
495
682
|
log(`Daemon restarted (PID ${child.pid})`);
|
|
@@ -522,7 +709,7 @@ daemon
|
|
|
522
709
|
});
|
|
523
710
|
daemon
|
|
524
711
|
.command('install')
|
|
525
|
-
.description('Install daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows)')
|
|
712
|
+
.description('Install daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows, systemd --user unit on Linux)')
|
|
526
713
|
.action(async () => {
|
|
527
714
|
if (process.platform === 'win32') {
|
|
528
715
|
try {
|
|
@@ -576,6 +763,62 @@ daemon
|
|
|
576
763
|
catch { /* hooks package not built yet — task install still succeeds */ }
|
|
577
764
|
process.exit(0);
|
|
578
765
|
}
|
|
766
|
+
if (process.platform === 'linux') {
|
|
767
|
+
if (!hasSystemctl()) {
|
|
768
|
+
log('systemd --user is not available on this host.');
|
|
769
|
+
log('Run the daemon manually with: agentdeck daemon start');
|
|
770
|
+
log('(or add your own autostart entry).');
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
try {
|
|
774
|
+
installUnit();
|
|
775
|
+
startUnit();
|
|
776
|
+
log(`systemd user unit '${SERVICE_NAME}' installed and started.`);
|
|
777
|
+
log(`Unit file: ${getUnitPath()}`);
|
|
778
|
+
if (process.env.AGENTDECK_DATA_DIR) {
|
|
779
|
+
log(`Data dir: ${getDataDir()} (AGENTDECK_DATA_DIR persisted into the unit — re-run 'agentdeck daemon install' to change it)`);
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
log(`Data dir: ${getDataDir()}`);
|
|
783
|
+
}
|
|
784
|
+
log('For boot-without-login on a headless host, run once: loginctl enable-linger $USER');
|
|
785
|
+
}
|
|
786
|
+
catch (e) {
|
|
787
|
+
const detail = e.stderr?.toString().trim() || e.message;
|
|
788
|
+
log('Failed to install the systemd user unit.');
|
|
789
|
+
if (detail)
|
|
790
|
+
log(detail);
|
|
791
|
+
log('You can still run the daemon manually with: agentdeck daemon start');
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
// Install Codex + OpenCode hooks for parity with the macOS/Windows paths.
|
|
796
|
+
try {
|
|
797
|
+
const { installCodexHooksIfNeeded } = await import('@agentdeck/hooks');
|
|
798
|
+
const result = installCodexHooksIfNeeded();
|
|
799
|
+
if (result.installed) {
|
|
800
|
+
log('Codex lifecycle hooks installed in ~/.codex/config.toml');
|
|
801
|
+
if (result.warning)
|
|
802
|
+
log(`Codex hooks degraded: ${result.warning}`);
|
|
803
|
+
}
|
|
804
|
+
else if (result.reason) {
|
|
805
|
+
log(`Codex hooks skipped: ${result.reason}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
catch { /* hooks package not built yet — install still succeeds */ }
|
|
809
|
+
try {
|
|
810
|
+
const { installOpenCodeHooksIfNeeded, opencodePluginPath } = await import('@agentdeck/hooks');
|
|
811
|
+
const result = installOpenCodeHooksIfNeeded();
|
|
812
|
+
if (result.installed) {
|
|
813
|
+
log(`OpenCode observer plugin installed at ${opencodePluginPath()}`);
|
|
814
|
+
}
|
|
815
|
+
else if (result.reason) {
|
|
816
|
+
log(`OpenCode observer plugin skipped: ${result.reason}`);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
catch { /* hooks package not built yet — install still succeeds */ }
|
|
820
|
+
process.exit(0);
|
|
821
|
+
}
|
|
579
822
|
if (process.platform !== 'darwin') {
|
|
580
823
|
log('LaunchAgent is macOS-only');
|
|
581
824
|
process.exit(1);
|
|
@@ -618,7 +861,7 @@ daemon
|
|
|
618
861
|
});
|
|
619
862
|
daemon
|
|
620
863
|
.command('uninstall')
|
|
621
|
-
.description('Remove daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows)')
|
|
864
|
+
.description('Remove daemon auto-start (LaunchAgent on macOS, Scheduled Task on Windows, systemd --user unit on Linux)')
|
|
622
865
|
.action(async () => {
|
|
623
866
|
if (process.platform === 'win32') {
|
|
624
867
|
if (!taskExists()) {
|
|
@@ -644,6 +887,17 @@ daemon
|
|
|
644
887
|
}
|
|
645
888
|
process.exit(0);
|
|
646
889
|
}
|
|
890
|
+
if (process.platform === 'linux') {
|
|
891
|
+
if (!unitExists()) {
|
|
892
|
+
log(`systemd user unit '${SERVICE_NAME}' is not installed.`);
|
|
893
|
+
process.exit(0);
|
|
894
|
+
}
|
|
895
|
+
// Graceful daemon shutdown first, then disable + remove the unit.
|
|
896
|
+
await stopDaemon(BRIDGE_WS_PORT);
|
|
897
|
+
disableUnit();
|
|
898
|
+
log(`systemd user unit '${SERVICE_NAME}' removed.`);
|
|
899
|
+
process.exit(0);
|
|
900
|
+
}
|
|
647
901
|
if (!existsSync(PLIST_PATH)) {
|
|
648
902
|
log('LaunchAgent not installed');
|
|
649
903
|
return;
|
|
@@ -949,6 +1203,7 @@ program
|
|
|
949
1203
|
.option('-e, --env <env>', 'PlatformIO environment for default firmware path')
|
|
950
1204
|
.option('-f, --firmware <path>', 'Firmware .bin path')
|
|
951
1205
|
.option('--build', 'Build the PlatformIO environment before upload')
|
|
1206
|
+
.option('--stage', 'Stage for pull OTA: the board installs it on its next feed pull (no live WS needed)')
|
|
952
1207
|
.action(async (target, opts) => {
|
|
953
1208
|
const { readDaemonInfo, findDaemonPort } = await import('./session-registry.js');
|
|
954
1209
|
const info = readDaemonInfo();
|
|
@@ -962,12 +1217,25 @@ program
|
|
|
962
1217
|
execFileSync(pio, ['run', '-e', env], {
|
|
963
1218
|
cwd: join(projectRootPath(), 'esp32'),
|
|
964
1219
|
stdio: 'inherit',
|
|
1220
|
+
windowsHide: true,
|
|
965
1221
|
});
|
|
966
1222
|
}
|
|
967
1223
|
// Send the canonical board string, not the raw alias — the daemon matches
|
|
968
1224
|
// `target` against device_info.board, so `ttgo` must become `ttgo_t_display`.
|
|
969
1225
|
const daemonTarget = resolveEsp32OtaDaemonTarget(target);
|
|
970
1226
|
const firmwarePath = resolveEsp32FirmwarePath(target, opts.env, opts.firmware);
|
|
1227
|
+
if (opts.stage) {
|
|
1228
|
+
// Pull-OTA staging: for wake-sync-sleep boards (XTeink X3/X4) that never
|
|
1229
|
+
// hold a live WS. The daemon adverts the build on the board's feed pulls;
|
|
1230
|
+
// the board downloads + flashes itself on its next wake.
|
|
1231
|
+
const { statusCode, body } = await postJsonWithTimeout(`http://127.0.0.1:${port}/esp32/ota`, { target: daemonTarget, firmwarePath, stage: true }, 30_000);
|
|
1232
|
+
if (statusCode < 200 || statusCode >= 300 || body.ok === false) {
|
|
1233
|
+
throw new Error(String(body.error ?? `HTTP ${statusCode}`));
|
|
1234
|
+
}
|
|
1235
|
+
log(`Staged for ${body.board}: ${formatBytes(body.bytes)} md5=${body.md5}`);
|
|
1236
|
+
log('The board installs it on its next feed pull (battery cadence: typically within 15-60 min).');
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
971
1239
|
log(`Uploading ${firmwarePath} to ${daemonTarget} via daemon :${port}...`);
|
|
972
1240
|
let { statusCode, body } = await postJsonWithTimeout(`http://127.0.0.1:${port}/esp32/ota`, { target: daemonTarget, firmwarePath }, 15 * 60_000);
|
|
973
1241
|
if (body.ok === false && String(body.error ?? '').startsWith('firmware_unreadable')) {
|
|
@@ -1149,35 +1417,55 @@ task
|
|
|
1149
1417
|
.action(async (opts) => {
|
|
1150
1418
|
await postTaskClose({ signal: 'manual', outcome: 'abandoned', sessionId: opts.session });
|
|
1151
1419
|
});
|
|
1420
|
+
// ===== Optional Python BLE runtime =====
|
|
1421
|
+
const ble = program.command('ble').description('Prepare optional Python support for BLE display devices');
|
|
1422
|
+
ble
|
|
1423
|
+
.command('setup')
|
|
1424
|
+
.description('Create the private BLE environment and install its Python dependencies')
|
|
1425
|
+
.action(() => {
|
|
1426
|
+
const runtime = prepareBleRuntime();
|
|
1427
|
+
if (runtime)
|
|
1428
|
+
log(`BLE support ready (${runtime.python}).`);
|
|
1429
|
+
});
|
|
1430
|
+
ble
|
|
1431
|
+
.command('status')
|
|
1432
|
+
.description('Show whether optional BLE support is ready')
|
|
1433
|
+
.action(() => {
|
|
1434
|
+
const status = getBleRuntimeStatus();
|
|
1435
|
+
if (status.ready) {
|
|
1436
|
+
log(`BLE support ready (${status.python}).`);
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
log(`BLE support is not ready: ${status.reason}. Run \`agentdeck ble setup\`.`);
|
|
1440
|
+
process.exitCode = 1;
|
|
1441
|
+
});
|
|
1152
1442
|
// ===== iDotMatrix commands =====
|
|
1153
1443
|
const idotmatrix = program.command('idotmatrix').description('Manage iDotMatrix BLE pixel display devices');
|
|
1154
1444
|
idotmatrix
|
|
1155
1445
|
.command('scan')
|
|
1156
1446
|
.description('Discover iDotMatrix devices via BLE')
|
|
1157
1447
|
.action(async () => {
|
|
1158
|
-
const
|
|
1159
|
-
|
|
1160
|
-
|
|
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');
|
|
1448
|
+
const runtime = prepareBleRuntime();
|
|
1449
|
+
if (!runtime)
|
|
1450
|
+
return;
|
|
1165
1451
|
log('Scanning for BLE devices (5 seconds)...');
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1452
|
+
try {
|
|
1453
|
+
// Panels are identified by advertised service UUID first and known name
|
|
1454
|
+
// families second; settings.json `idotmatrixNamePrefixes` widens the
|
|
1455
|
+
// latter for a rebranded panel that hides its service.
|
|
1456
|
+
const { loadIDotMatrixNamePrefixes } = await import('./idotmatrix/idotmatrix-settings.js');
|
|
1457
|
+
const { code, stdout } = await runBlePython(runtime.python, [runtime.paths.scripts.idotmatrixScan, ...loadIDotMatrixNamePrefixes()], true);
|
|
1172
1458
|
if (code !== 0) {
|
|
1173
1459
|
log('Failed to run scan script.');
|
|
1174
|
-
process.
|
|
1460
|
+
process.exitCode = 1;
|
|
1461
|
+
return;
|
|
1175
1462
|
}
|
|
1176
1463
|
try {
|
|
1177
|
-
const devices = JSON.parse(
|
|
1464
|
+
const devices = JSON.parse(stdout.trim());
|
|
1178
1465
|
if (devices.error) {
|
|
1179
1466
|
log(`Scan error: ${devices.error}`);
|
|
1180
|
-
process.
|
|
1467
|
+
process.exitCode = 1;
|
|
1468
|
+
return;
|
|
1181
1469
|
}
|
|
1182
1470
|
if (!Array.isArray(devices) || devices.length === 0) {
|
|
1183
1471
|
log('No BLE devices found.');
|
|
@@ -1189,10 +1477,15 @@ idotmatrix
|
|
|
1189
1477
|
log(`${prefix}${d.name} (${d.address})`);
|
|
1190
1478
|
}
|
|
1191
1479
|
}
|
|
1192
|
-
catch
|
|
1193
|
-
log(`Failed to parse scan output: ${
|
|
1480
|
+
catch {
|
|
1481
|
+
log(`Failed to parse scan output: ${stdout}`);
|
|
1482
|
+
process.exitCode = 1;
|
|
1194
1483
|
}
|
|
1195
|
-
}
|
|
1484
|
+
}
|
|
1485
|
+
catch (err) {
|
|
1486
|
+
log(`Failed to start BLE scan: ${err instanceof Error ? err.message : String(err)}`);
|
|
1487
|
+
process.exitCode = 1;
|
|
1488
|
+
}
|
|
1196
1489
|
});
|
|
1197
1490
|
idotmatrix
|
|
1198
1491
|
.command('add <address>')
|
|
@@ -1243,9 +1536,6 @@ idotmatrix
|
|
|
1243
1536
|
.option('-a, --address <address>', 'BLE Address (defaults to first configured device)')
|
|
1244
1537
|
.action(async (valueStr, opts) => {
|
|
1245
1538
|
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
1539
|
const value = parseInt(valueStr, 10);
|
|
1250
1540
|
if (isNaN(value) || value < 5 || value > 100) {
|
|
1251
1541
|
log('Brightness value must be between 5 and 100.');
|
|
@@ -1260,22 +1550,30 @@ idotmatrix
|
|
|
1260
1550
|
}
|
|
1261
1551
|
targetAddress = devices[0].address;
|
|
1262
1552
|
}
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
const brightnessScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'brightness.py');
|
|
1553
|
+
const runtime = prepareBleRuntime();
|
|
1554
|
+
if (!runtime)
|
|
1555
|
+
return;
|
|
1267
1556
|
log(`Setting brightness of ${targetAddress} to ${value}%...`);
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1557
|
+
try {
|
|
1558
|
+
const { code } = await runBlePython(runtime.python, [
|
|
1559
|
+
runtime.paths.scripts.idotmatrixBrightness,
|
|
1560
|
+
'-a',
|
|
1561
|
+
targetAddress,
|
|
1562
|
+
'-b',
|
|
1563
|
+
String(value),
|
|
1564
|
+
]);
|
|
1272
1565
|
if (code === 0) {
|
|
1273
1566
|
log('Brightness updated successfully.');
|
|
1274
1567
|
}
|
|
1275
1568
|
else {
|
|
1276
1569
|
log(`Failed to update brightness, process exited with code ${code}`);
|
|
1570
|
+
process.exitCode = code ?? 1;
|
|
1277
1571
|
}
|
|
1278
|
-
}
|
|
1572
|
+
}
|
|
1573
|
+
catch (err) {
|
|
1574
|
+
log(`Failed to start brightness command: ${err instanceof Error ? err.message : String(err)}`);
|
|
1575
|
+
process.exitCode = 1;
|
|
1576
|
+
}
|
|
1279
1577
|
});
|
|
1280
1578
|
idotmatrix
|
|
1281
1579
|
.command('sync [address]')
|
|
@@ -1286,9 +1584,6 @@ idotmatrix
|
|
|
1286
1584
|
.action(async (addressOpt, opts) => {
|
|
1287
1585
|
const { loadIDotMatrixDevices } = await import('./idotmatrix/idotmatrix-settings.js');
|
|
1288
1586
|
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
1587
|
let targetAddress = addressOpt;
|
|
1293
1588
|
let defaultBrightness = 100;
|
|
1294
1589
|
const devices = loadIDotMatrixDevices();
|
|
@@ -1314,17 +1609,30 @@ idotmatrix
|
|
|
1314
1609
|
? parseInt(opts.port, 10)
|
|
1315
1610
|
: (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
|
|
1316
1611
|
const url = `http://127.0.0.1:${port}`;
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
const syncScript = join(projectRoot, 'bridge', 'src', 'idotmatrix', 'sync.py');
|
|
1612
|
+
const runtime = prepareBleRuntime();
|
|
1613
|
+
if (!runtime)
|
|
1614
|
+
return;
|
|
1321
1615
|
log(`Starting BLE sync client linking to bridge at ${url} (brightness ${brightness}%, boost ${boost}x)...`);
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1616
|
+
try {
|
|
1617
|
+
const { code } = await runBlePython(runtime.python, [
|
|
1618
|
+
runtime.paths.scripts.idotmatrixSync,
|
|
1619
|
+
'-a',
|
|
1620
|
+
targetAddress,
|
|
1621
|
+
'-u',
|
|
1622
|
+
url,
|
|
1623
|
+
'-b',
|
|
1624
|
+
String(brightness),
|
|
1625
|
+
'--boost',
|
|
1626
|
+
String(boost),
|
|
1627
|
+
]);
|
|
1326
1628
|
log(`Sync process exited with code ${code}`);
|
|
1327
|
-
|
|
1629
|
+
if (code !== 0)
|
|
1630
|
+
process.exitCode = code ?? 1;
|
|
1631
|
+
}
|
|
1632
|
+
catch (err) {
|
|
1633
|
+
log(`Failed to start BLE sync: ${err instanceof Error ? err.message : String(err)}`);
|
|
1634
|
+
process.exitCode = 1;
|
|
1635
|
+
}
|
|
1328
1636
|
});
|
|
1329
1637
|
// ===== Divoom Timebox Mini Light commands =====
|
|
1330
1638
|
const timebox = program.command('timebox').description('Manage Divoom Timebox Mini devices (Bluetooth LE)');
|
|
@@ -1332,22 +1640,22 @@ timebox
|
|
|
1332
1640
|
.command('scan')
|
|
1333
1641
|
.description('Discover BLE TimeBox-mini-light peripherals')
|
|
1334
1642
|
.action(async () => {
|
|
1335
|
-
const
|
|
1643
|
+
const runtime = prepareBleRuntime();
|
|
1644
|
+
if (!runtime)
|
|
1645
|
+
return;
|
|
1336
1646
|
log('Scanning for BLE devices (5 seconds)...');
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
py.stdout.on('data', (data) => {
|
|
1340
|
-
stdoutData += data.toString();
|
|
1341
|
-
});
|
|
1342
|
-
py.on('close', (code) => {
|
|
1647
|
+
try {
|
|
1648
|
+
const { code, stdout } = await runBlePython(runtime.python, [runtime.paths.scripts.timeboxScan], true);
|
|
1343
1649
|
if (code !== 0) {
|
|
1344
|
-
log('BLE scan failed
|
|
1650
|
+
log('BLE scan failed.');
|
|
1651
|
+
process.exitCode = 1;
|
|
1345
1652
|
return;
|
|
1346
1653
|
}
|
|
1347
1654
|
try {
|
|
1348
|
-
const devices = JSON.parse(
|
|
1655
|
+
const devices = JSON.parse(stdout.trim());
|
|
1349
1656
|
if (devices.error) {
|
|
1350
1657
|
log(`BLE scan error: ${devices.error}`);
|
|
1658
|
+
process.exitCode = 1;
|
|
1351
1659
|
return;
|
|
1352
1660
|
}
|
|
1353
1661
|
if (!Array.isArray(devices) || devices.length === 0) {
|
|
@@ -1363,9 +1671,14 @@ timebox
|
|
|
1363
1671
|
log('\nAdd the starred BLE device with: agentdeck timebox add <address>');
|
|
1364
1672
|
}
|
|
1365
1673
|
catch {
|
|
1366
|
-
log(`Failed to parse BLE scan output: ${
|
|
1674
|
+
log(`Failed to parse BLE scan output: ${stdout}`);
|
|
1675
|
+
process.exitCode = 1;
|
|
1367
1676
|
}
|
|
1368
|
-
}
|
|
1677
|
+
}
|
|
1678
|
+
catch (err) {
|
|
1679
|
+
log(`Failed to start BLE scan: ${err instanceof Error ? err.message : String(err)}`);
|
|
1680
|
+
process.exitCode = 1;
|
|
1681
|
+
}
|
|
1369
1682
|
});
|
|
1370
1683
|
timebox
|
|
1371
1684
|
.command('add <address>')
|
|
@@ -1442,11 +1755,22 @@ async function runTimeboxSync(targetOpt, opts, once) {
|
|
|
1442
1755
|
? parseInt(opts.bridgePort, 10)
|
|
1443
1756
|
: (info?.httpPort ?? info?.port ?? findDaemonPort() ?? BRIDGE_WS_PORT);
|
|
1444
1757
|
const url = `http://127.0.0.1:${bridgePort}`;
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1758
|
+
const runtime = prepareBleRuntime();
|
|
1759
|
+
if (!runtime)
|
|
1760
|
+
return;
|
|
1761
|
+
const args = [
|
|
1762
|
+
runtime.paths.scripts.timeboxSync,
|
|
1763
|
+
'--address',
|
|
1764
|
+
id,
|
|
1765
|
+
'--url',
|
|
1766
|
+
url,
|
|
1767
|
+
'--brightness',
|
|
1768
|
+
String(brightness),
|
|
1769
|
+
...(once ? ['--once'] : []),
|
|
1770
|
+
];
|
|
1447
1771
|
log(`${once ? 'Sending one Timebox frame' : 'Starting Timebox sync'} via BLE ${id} from ${url} (brightness ${brightness}%)...`);
|
|
1448
|
-
|
|
1449
|
-
|
|
1772
|
+
try {
|
|
1773
|
+
const { code } = await runBlePython(runtime.python, args);
|
|
1450
1774
|
if (once) {
|
|
1451
1775
|
if (code === 0) {
|
|
1452
1776
|
log('Timebox test frame sent.');
|
|
@@ -1458,8 +1782,14 @@ async function runTimeboxSync(targetOpt, opts, once) {
|
|
|
1458
1782
|
}
|
|
1459
1783
|
else {
|
|
1460
1784
|
log(`Timebox sync process exited with code ${code}`);
|
|
1785
|
+
if (code !== 0)
|
|
1786
|
+
process.exitCode = code ?? 1;
|
|
1461
1787
|
}
|
|
1462
|
-
}
|
|
1788
|
+
}
|
|
1789
|
+
catch (err) {
|
|
1790
|
+
log(`Failed to start Timebox BLE command: ${err instanceof Error ? err.message : String(err)}`);
|
|
1791
|
+
process.exitCode = 1;
|
|
1792
|
+
}
|
|
1463
1793
|
}
|
|
1464
1794
|
timebox
|
|
1465
1795
|
.command('test [target]')
|
|
@@ -2259,7 +2589,7 @@ export async function runCli(argv = process.argv) {
|
|
|
2259
2589
|
await showDefaultStatusOrHelp();
|
|
2260
2590
|
return;
|
|
2261
2591
|
}
|
|
2262
|
-
program.parse(argv);
|
|
2592
|
+
program.parse(applyGlobalEnvArgs(argv));
|
|
2263
2593
|
}
|
|
2264
2594
|
export { program };
|
|
2265
2595
|
if (isMainModule()) {
|