@putdotio/taizn 1.11.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/taizn.mjs +216 -4
- package/docs/TV_REMOTE.md +14 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,10 +57,14 @@ taizn profile
|
|
|
57
57
|
taizn package
|
|
58
58
|
taizn install
|
|
59
59
|
taizn run
|
|
60
|
+
taizn tv doctor
|
|
61
|
+
taizn tv doctor --json
|
|
62
|
+
taizn tv doctor --connect --json
|
|
60
63
|
taizn tv info
|
|
61
64
|
taizn tv info --json
|
|
62
65
|
taizn tv pair
|
|
63
66
|
taizn tv press KEY_ENTER
|
|
67
|
+
taizn tv press --json KEY_ENTER
|
|
64
68
|
taizn tv press --delay-ms 250 KEY_HOME KEY_DOWN KEY_ENTER
|
|
65
69
|
taizn --version
|
|
66
70
|
```
|
|
@@ -79,8 +83,10 @@ proof output. `profile` imports
|
|
|
79
83
|
`package` builds and signs a `.wgt`. `install` packages and sideloads it.
|
|
80
84
|
`run` launches the configured variant application on the target. `tv` commands use
|
|
81
85
|
Samsung's websocket remote-control API to inspect a TV,
|
|
82
|
-
pair for a remote token, and send remote-control
|
|
83
|
-
`tv
|
|
86
|
+
diagnose remote-control readiness, pair for a remote token, and send remote-control
|
|
87
|
+
key presses. Add `--json` to `tv doctor` for structured host/token/connection
|
|
88
|
+
diagnostics, to `tv info` for a structured TV capability snapshot, or to
|
|
89
|
+
`tv press` for a structured key-sequence receipt. See
|
|
84
90
|
[Samsung TV Remote](./docs/TV_REMOTE.md) for pairing, environment, and limits.
|
|
85
91
|
`tv press` accepts one key or a sequence of keys.
|
|
86
92
|
|
package/dist/taizn.mjs
CHANGED
|
@@ -461,13 +461,77 @@ Effect.fn("sendSamsungTvKey")(function* (env, key) {
|
|
|
461
461
|
});
|
|
462
462
|
const sendSamsungTvKeys = Effect.fn("sendSamsungTvKeys")(function* (env, keys, pressOptions) {
|
|
463
463
|
const remoteOptions = yield* resolveRemoteOptions(env, { requireToken: true });
|
|
464
|
-
|
|
464
|
+
const token = remoteOptions.token;
|
|
465
|
+
const delayMs = Math.max(0, pressOptions?.delayMs ?? 250);
|
|
466
|
+
if (!token) return yield* MissingTvRemoteToken.make({});
|
|
465
467
|
yield* connectRemote(remoteOptions, {
|
|
466
|
-
delayMs
|
|
468
|
+
delayMs,
|
|
467
469
|
keys
|
|
468
470
|
});
|
|
471
|
+
if (pressOptions?.json) {
|
|
472
|
+
yield* Console.log(JSON.stringify({
|
|
473
|
+
delayMs,
|
|
474
|
+
keys,
|
|
475
|
+
keyCount: keys.length,
|
|
476
|
+
target: {
|
|
477
|
+
host: remoteOptions.host,
|
|
478
|
+
port: remoteOptions.port,
|
|
479
|
+
protocol: remoteOptions.protocol,
|
|
480
|
+
url: remoteTarget(remoteOptions)
|
|
481
|
+
}
|
|
482
|
+
}));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
469
485
|
yield* Console.log(keys.length === 1 ? `Sent Samsung TV remote key: ${keys[0]}` : `Sent Samsung TV remote keys: ${keys.join(", ")}`);
|
|
470
486
|
});
|
|
487
|
+
const diagnoseSamsungTvRemote = Effect.fn("diagnoseSamsungTvRemote")(function* (env, doctorOptions = {}) {
|
|
488
|
+
const stateRead = yield* readRemoteStateDiagnostic();
|
|
489
|
+
const state = stateRead.state;
|
|
490
|
+
const targetHost = hostFromTarget(env.target);
|
|
491
|
+
const host = env.tvHost ?? state?.host ?? targetHost;
|
|
492
|
+
const hostSource = valueSource(env.tvHost, state?.host, targetHost);
|
|
493
|
+
const token = env.tvToken ?? state?.token;
|
|
494
|
+
const tokenSource = valueSource(env.tvToken, state?.token, void 0);
|
|
495
|
+
const remoteOptions = host ? {
|
|
496
|
+
host,
|
|
497
|
+
name: env.tvName ?? state?.name ?? DEFAULT_REMOTE_NAME,
|
|
498
|
+
port: env.tvPort ?? state?.port ?? DEFAULT_REMOTE_PORT,
|
|
499
|
+
protocol: env.tvProtocol ?? state?.protocol ?? DEFAULT_REMOTE_PROTOCOL,
|
|
500
|
+
timeoutMs: env.tvTimeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
501
|
+
token
|
|
502
|
+
} : void 0;
|
|
503
|
+
const info = yield* readInfoDiagnostic(host, env.tvInfoPort ?? TV_INFO_PORT, remoteOptions?.timeoutMs);
|
|
504
|
+
const connection = yield* readRemoteConnectionDiagnostic(remoteOptions, doctorOptions);
|
|
505
|
+
const result = {
|
|
506
|
+
host,
|
|
507
|
+
hostSource,
|
|
508
|
+
info,
|
|
509
|
+
remote: {
|
|
510
|
+
connection,
|
|
511
|
+
name: remoteOptions?.name ?? env.tvName ?? DEFAULT_REMOTE_NAME,
|
|
512
|
+
port: remoteOptions?.port ?? env.tvPort ?? state?.port ?? DEFAULT_REMOTE_PORT,
|
|
513
|
+
protocol: remoteOptions?.protocol ?? env.tvProtocol ?? state?.protocol ?? DEFAULT_REMOTE_PROTOCOL,
|
|
514
|
+
target: remoteOptions ? remoteTarget(remoteOptions) : void 0,
|
|
515
|
+
timeoutMs: remoteOptions?.timeoutMs ?? env.tvTimeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
516
|
+
tokenConfigured: Boolean(token),
|
|
517
|
+
tokenSource
|
|
518
|
+
},
|
|
519
|
+
state: stateRead.diagnostic,
|
|
520
|
+
target: env.target
|
|
521
|
+
};
|
|
522
|
+
if (doctorOptions.json) {
|
|
523
|
+
yield* Console.log(JSON.stringify(result));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
yield* Console.log(`Samsung TV doctor: ${host ?? "missing host"}`);
|
|
527
|
+
yield* Console.log(`host_source: ${hostSource}`);
|
|
528
|
+
yield* Console.log(`info: ${info.ok ? `ok ${info.name}` : `failed ${info.error.type}`}`);
|
|
529
|
+
yield* Console.log(`remote: ${result.remote.target ?? "missing"}`);
|
|
530
|
+
yield* Console.log(`token_configured: ${result.remote.tokenConfigured ? "yes" : "no"}`);
|
|
531
|
+
yield* Console.log(`remote_state: ${result.state.status}`);
|
|
532
|
+
if (connection.tested) yield* Console.log(`remote_connect: ${connection.ok ? "ok" : `failed ${connection.error.type}`}`);
|
|
533
|
+
else yield* Console.log(`remote_connect: skipped (${connection.reason})`);
|
|
534
|
+
});
|
|
471
535
|
const showSamsungTvInfo = Effect.fn("showSamsungTvInfo")(function* (env, infoOptions = {}) {
|
|
472
536
|
const options = yield* resolveRemoteOptions(env);
|
|
473
537
|
const info = yield* fetchSamsungTvInfo(options.host, {
|
|
@@ -566,6 +630,33 @@ const saveRemoteState = Effect.fn("saveRemoteState")(function* (options) {
|
|
|
566
630
|
path: paths.remoteStatePath
|
|
567
631
|
})));
|
|
568
632
|
});
|
|
633
|
+
const readRemoteStateDiagnostic = Effect.fn("readRemoteStateDiagnostic")(function* () {
|
|
634
|
+
const paths = yield* getPaths();
|
|
635
|
+
return yield* readRemoteState().pipe(Effect.match({
|
|
636
|
+
onFailure: (error) => ({ diagnostic: {
|
|
637
|
+
error: diagnosticError(error),
|
|
638
|
+
path: paths.remoteStatePath,
|
|
639
|
+
status: "error",
|
|
640
|
+
tokenConfigured: false
|
|
641
|
+
} }),
|
|
642
|
+
onSuccess: (state) => state ? {
|
|
643
|
+
diagnostic: {
|
|
644
|
+
host: state.host,
|
|
645
|
+
name: state.name,
|
|
646
|
+
path: paths.remoteStatePath,
|
|
647
|
+
port: state.port,
|
|
648
|
+
protocol: state.protocol,
|
|
649
|
+
status: "valid",
|
|
650
|
+
tokenConfigured: true
|
|
651
|
+
},
|
|
652
|
+
state
|
|
653
|
+
} : { diagnostic: {
|
|
654
|
+
path: paths.remoteStatePath,
|
|
655
|
+
status: "missing",
|
|
656
|
+
tokenConfigured: false
|
|
657
|
+
} }
|
|
658
|
+
}));
|
|
659
|
+
});
|
|
569
660
|
const connectRemote = Effect.fn("connectRemote")(function* (options, sequence) {
|
|
570
661
|
return yield* Effect.tryPromise({
|
|
571
662
|
try: () => connectRemotePromise(options, sequence),
|
|
@@ -667,6 +758,66 @@ const fetchSamsungTvInfo = Effect.fn("fetchSamsungTvInfo")(function* (host, opti
|
|
|
667
758
|
});
|
|
668
759
|
return yield* Schema.decodeUnknownEffect(TvInfo)(json, { errors: "all" }).pipe(Effect.mapError((error) => TvRemoteProtocolError.make({ details: error.message })));
|
|
669
760
|
});
|
|
761
|
+
const readInfoDiagnostic = Effect.fn("readInfoDiagnostic")(function* (host, port, timeoutMs) {
|
|
762
|
+
if (!host) return {
|
|
763
|
+
error: diagnosticError(MissingTvRemoteHost.make({})),
|
|
764
|
+
ok: false,
|
|
765
|
+
port
|
|
766
|
+
};
|
|
767
|
+
return yield* fetchSamsungTvInfo(host, {
|
|
768
|
+
port,
|
|
769
|
+
timeoutMs
|
|
770
|
+
}).pipe(Effect.match({
|
|
771
|
+
onFailure: (error) => ({
|
|
772
|
+
error: diagnosticError(error),
|
|
773
|
+
ok: false,
|
|
774
|
+
port
|
|
775
|
+
}),
|
|
776
|
+
onSuccess: (info) => {
|
|
777
|
+
const support = info.isSupport ? parseSupport(info.isSupport) : void 0;
|
|
778
|
+
return {
|
|
779
|
+
developer: {
|
|
780
|
+
enabled: stringFlag(info.device.developerMode),
|
|
781
|
+
ip: info.device.developerIP,
|
|
782
|
+
mode: info.device.developerMode
|
|
783
|
+
},
|
|
784
|
+
ip: info.device.ip ?? host,
|
|
785
|
+
model: info.device.modelName,
|
|
786
|
+
name: decodeHtml(info.name),
|
|
787
|
+
ok: true,
|
|
788
|
+
port,
|
|
789
|
+
remote: info.remote,
|
|
790
|
+
remoteAvailable: stringFlag(support?.remote_available),
|
|
791
|
+
tokenAuth: stringFlag(info.device.TokenAuthSupport),
|
|
792
|
+
type: info.type,
|
|
793
|
+
uri: info.uri
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
}));
|
|
797
|
+
});
|
|
798
|
+
const readRemoteConnectionDiagnostic = Effect.fn("readRemoteConnectionDiagnostic")(function* (options, doctorOptions) {
|
|
799
|
+
if (!doctorOptions.connect) return {
|
|
800
|
+
reason: "pass --connect to test the websocket endpoint",
|
|
801
|
+
tested: false
|
|
802
|
+
};
|
|
803
|
+
if (!options) return {
|
|
804
|
+
error: diagnosticError(MissingTvRemoteHost.make({})),
|
|
805
|
+
ok: false,
|
|
806
|
+
tested: true
|
|
807
|
+
};
|
|
808
|
+
return yield* connectRemote(options).pipe(Effect.match({
|
|
809
|
+
onFailure: (error) => ({
|
|
810
|
+
error: diagnosticError(error),
|
|
811
|
+
ok: false,
|
|
812
|
+
tested: true
|
|
813
|
+
}),
|
|
814
|
+
onSuccess: (returnedToken) => ({
|
|
815
|
+
ok: true,
|
|
816
|
+
tested: true,
|
|
817
|
+
tokenReturned: returnedToken.length > 0
|
|
818
|
+
})
|
|
819
|
+
}));
|
|
820
|
+
});
|
|
670
821
|
const isAbortError = (cause) => cause instanceof Error && (cause.name === "AbortError" || cause.name === "TimeoutError");
|
|
671
822
|
const parseRemoteEvent = (source, options) => {
|
|
672
823
|
try {
|
|
@@ -710,6 +861,53 @@ const normalizeRemoteError = (cause, options) => {
|
|
|
710
861
|
target: remoteTarget(options)
|
|
711
862
|
});
|
|
712
863
|
};
|
|
864
|
+
const diagnosticError = (error) => {
|
|
865
|
+
if (error instanceof FileSystemFailure) return {
|
|
866
|
+
message: error.message,
|
|
867
|
+
target: error.path,
|
|
868
|
+
type: "FileSystemFailure"
|
|
869
|
+
};
|
|
870
|
+
if (error instanceof InvalidJson) return {
|
|
871
|
+
details: error.details,
|
|
872
|
+
file: error.file,
|
|
873
|
+
message: error.message,
|
|
874
|
+
type: "InvalidJson"
|
|
875
|
+
};
|
|
876
|
+
if (error instanceof MissingTvRemoteHost) return {
|
|
877
|
+
message: error.message,
|
|
878
|
+
type: "MissingTvRemoteHost"
|
|
879
|
+
};
|
|
880
|
+
if (error instanceof MissingTvRemoteToken) return {
|
|
881
|
+
message: error.message,
|
|
882
|
+
type: "MissingTvRemoteToken"
|
|
883
|
+
};
|
|
884
|
+
if (error instanceof TvRemoteConnectionFailed) return {
|
|
885
|
+
message: error.message,
|
|
886
|
+
target: error.target,
|
|
887
|
+
type: "TvRemoteConnectionFailed"
|
|
888
|
+
};
|
|
889
|
+
if (error instanceof TvRemoteProtocolError) return {
|
|
890
|
+
details: error.details,
|
|
891
|
+
message: error.message,
|
|
892
|
+
type: "TvRemoteProtocolError"
|
|
893
|
+
};
|
|
894
|
+
if (error instanceof TvRemoteTimeout) return {
|
|
895
|
+
message: error.message,
|
|
896
|
+
target: error.target,
|
|
897
|
+
type: "TvRemoteTimeout"
|
|
898
|
+
};
|
|
899
|
+
return {
|
|
900
|
+
message: error.message,
|
|
901
|
+
target: error.target,
|
|
902
|
+
type: "TvRemoteUnauthorized"
|
|
903
|
+
};
|
|
904
|
+
};
|
|
905
|
+
const valueSource = (envValue, stateValue, targetValue) => {
|
|
906
|
+
if (envValue) return "env";
|
|
907
|
+
if (stateValue) return "state";
|
|
908
|
+
if (targetValue) return "target";
|
|
909
|
+
return "none";
|
|
910
|
+
};
|
|
713
911
|
const hostFromTarget = (target) => {
|
|
714
912
|
if (!target) return;
|
|
715
913
|
return target.split(":").at(0);
|
|
@@ -1246,16 +1444,30 @@ const run = Command.make("run", {}, () => withContext((context) => runWidget(con
|
|
|
1246
1444
|
const tvPair = Command.make("pair", {}, () => Effect.gen(function* () {
|
|
1247
1445
|
yield* pairSamsungTvRemote(yield* loadEnv());
|
|
1248
1446
|
}));
|
|
1447
|
+
const tvDoctor = Command.make("doctor", {
|
|
1448
|
+
connect: Flag.boolean("connect"),
|
|
1449
|
+
json: Flag.boolean("json")
|
|
1450
|
+
}, ({ connect, json }) => Effect.gen(function* () {
|
|
1451
|
+
yield* diagnoseSamsungTvRemote(yield* loadEnv(), {
|
|
1452
|
+
connect,
|
|
1453
|
+
json
|
|
1454
|
+
});
|
|
1455
|
+
}));
|
|
1249
1456
|
const tvPress = Command.make("press", {
|
|
1250
1457
|
delayMs: Flag.integer("delay-ms").pipe(Flag.withDefault(250)),
|
|
1458
|
+
json: Flag.boolean("json"),
|
|
1251
1459
|
keys: Argument.string("key").pipe(Argument.variadic({ min: 1 }))
|
|
1252
|
-
}, ({ delayMs, keys }) => Effect.gen(function* () {
|
|
1253
|
-
yield* sendSamsungTvKeys(yield* loadEnv(), keys, {
|
|
1460
|
+
}, ({ delayMs, json, keys }) => Effect.gen(function* () {
|
|
1461
|
+
yield* sendSamsungTvKeys(yield* loadEnv(), keys, {
|
|
1462
|
+
delayMs,
|
|
1463
|
+
json
|
|
1464
|
+
});
|
|
1254
1465
|
}));
|
|
1255
1466
|
const tvInfo = Command.make("info", { json: Flag.boolean("json") }, ({ json }) => Effect.gen(function* () {
|
|
1256
1467
|
yield* showSamsungTvInfo(yield* loadEnv(), { json });
|
|
1257
1468
|
}));
|
|
1258
1469
|
const tv = Command.make("tv", {}).pipe(Command.withSubcommands([
|
|
1470
|
+
tvDoctor,
|
|
1259
1471
|
tvPair,
|
|
1260
1472
|
tvPress,
|
|
1261
1473
|
tvInfo
|
package/docs/TV_REMOTE.md
CHANGED
|
@@ -6,13 +6,21 @@ and smoke checks against a physical TV or monitor.
|
|
|
6
6
|
## Commands
|
|
7
7
|
|
|
8
8
|
```bash
|
|
9
|
+
taizn tv doctor
|
|
10
|
+
taizn tv doctor --json
|
|
11
|
+
taizn tv doctor --connect --json
|
|
9
12
|
taizn tv info
|
|
10
13
|
taizn tv info --json
|
|
11
14
|
taizn tv pair
|
|
12
15
|
taizn tv press KEY_ENTER
|
|
16
|
+
taizn tv press --json KEY_ENTER
|
|
13
17
|
taizn tv press --delay-ms 250 KEY_HOME KEY_DOWN KEY_ENTER
|
|
14
18
|
```
|
|
15
19
|
|
|
20
|
+
- `doctor` reports the resolved host, local `.taizn/remote.json` state, token
|
|
21
|
+
presence, HTTP metadata status, and websocket endpoint. Add `--json` for a
|
|
22
|
+
structured diagnostic. Add `--connect` only when you want to test the
|
|
23
|
+
websocket; without a token this can trigger the TV's allow/deny prompt.
|
|
16
24
|
- `info` reads the TV's local `/api/v2/` metadata and reports remote-control
|
|
17
25
|
support. Add `--json` to emit a structured TV capability snapshot for agents
|
|
18
26
|
and scripts.
|
|
@@ -22,6 +30,7 @@ taizn tv press --delay-ms 250 KEY_HOME KEY_DOWN KEY_ENTER
|
|
|
22
30
|
as `KEY_HOME`, `KEY_BACK`, `KEY_UP`, `KEY_DOWN`, `KEY_LEFT`, `KEY_RIGHT`, or
|
|
23
31
|
`KEY_ENTER`. Pass multiple keys to send a navigation sequence on one
|
|
24
32
|
websocket connection. `--delay-ms` controls the delay between sequence keys.
|
|
33
|
+
Add `--json` to emit a redacted receipt with the target, delay, and keys sent.
|
|
25
34
|
|
|
26
35
|
## Environment
|
|
27
36
|
|
|
@@ -66,7 +75,8 @@ The `.taizn/` directory is local state and must stay ignored.
|
|
|
66
75
|
|
|
67
76
|
## Scope
|
|
68
77
|
|
|
69
|
-
Remote commands
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
78
|
+
Remote commands only inspect metadata, diagnose remote readiness, pair, and send
|
|
79
|
+
key presses. They do not install widgets, launch applications, or capture
|
|
80
|
+
screenshots. Use the regular `taizn install` and Tizen CLI commands for app
|
|
81
|
+
lifecycle work. Use logs, app-level probes, Samsung Web Inspector, Remote Test
|
|
82
|
+
Lab, or external capture when visual proof is needed.
|