@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 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 key presses. Add `--json` to
83
- `tv info` for a structured TV capability snapshot. See
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
- if (!remoteOptions.token) return yield* MissingTvRemoteToken.make({});
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: Math.max(0, pressOptions?.delayMs ?? 250),
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, { delayMs });
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 send key presses only. They do not install widgets, launch
70
- applications, or capture screenshots. Use the regular `taizn install` and Tizen
71
- CLI commands for app lifecycle work. Use logs, app-level probes, Samsung Web
72
- Inspector, Remote Test Lab, or external capture when visual proof is needed.
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@putdotio/taizn",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "A tiny CLI companion for interacting with Tizen ecosystem.",
5
5
  "keywords": [
6
6
  "cli",