@putdotio/taizn 1.12.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 +7 -3
- package/dist/taizn.mjs +192 -0
- package/docs/TV_REMOTE.md +12 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,6 +57,9 @@ 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
|
|
@@ -80,9 +83,10 @@ proof output. `profile` imports
|
|
|
80
83
|
`package` builds and signs a `.wgt`. `install` packages and sideloads it.
|
|
81
84
|
`run` launches the configured variant application on the target. `tv` commands use
|
|
82
85
|
Samsung's websocket remote-control API to inspect a TV,
|
|
83
|
-
pair for a remote token, and send remote-control
|
|
84
|
-
|
|
85
|
-
structured
|
|
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
|
|
86
90
|
[Samsung TV Remote](./docs/TV_REMOTE.md) for pairing, environment, and limits.
|
|
87
91
|
`tv press` accepts one key or a sequence of keys.
|
|
88
92
|
|
package/dist/taizn.mjs
CHANGED
|
@@ -484,6 +484,54 @@ const sendSamsungTvKeys = Effect.fn("sendSamsungTvKeys")(function* (env, keys, p
|
|
|
484
484
|
}
|
|
485
485
|
yield* Console.log(keys.length === 1 ? `Sent Samsung TV remote key: ${keys[0]}` : `Sent Samsung TV remote keys: ${keys.join(", ")}`);
|
|
486
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
|
+
});
|
|
487
535
|
const showSamsungTvInfo = Effect.fn("showSamsungTvInfo")(function* (env, infoOptions = {}) {
|
|
488
536
|
const options = yield* resolveRemoteOptions(env);
|
|
489
537
|
const info = yield* fetchSamsungTvInfo(options.host, {
|
|
@@ -582,6 +630,33 @@ const saveRemoteState = Effect.fn("saveRemoteState")(function* (options) {
|
|
|
582
630
|
path: paths.remoteStatePath
|
|
583
631
|
})));
|
|
584
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
|
+
});
|
|
585
660
|
const connectRemote = Effect.fn("connectRemote")(function* (options, sequence) {
|
|
586
661
|
return yield* Effect.tryPromise({
|
|
587
662
|
try: () => connectRemotePromise(options, sequence),
|
|
@@ -683,6 +758,66 @@ const fetchSamsungTvInfo = Effect.fn("fetchSamsungTvInfo")(function* (host, opti
|
|
|
683
758
|
});
|
|
684
759
|
return yield* Schema.decodeUnknownEffect(TvInfo)(json, { errors: "all" }).pipe(Effect.mapError((error) => TvRemoteProtocolError.make({ details: error.message })));
|
|
685
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
|
+
});
|
|
686
821
|
const isAbortError = (cause) => cause instanceof Error && (cause.name === "AbortError" || cause.name === "TimeoutError");
|
|
687
822
|
const parseRemoteEvent = (source, options) => {
|
|
688
823
|
try {
|
|
@@ -726,6 +861,53 @@ const normalizeRemoteError = (cause, options) => {
|
|
|
726
861
|
target: remoteTarget(options)
|
|
727
862
|
});
|
|
728
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
|
+
};
|
|
729
911
|
const hostFromTarget = (target) => {
|
|
730
912
|
if (!target) return;
|
|
731
913
|
return target.split(":").at(0);
|
|
@@ -1262,6 +1444,15 @@ const run = Command.make("run", {}, () => withContext((context) => runWidget(con
|
|
|
1262
1444
|
const tvPair = Command.make("pair", {}, () => Effect.gen(function* () {
|
|
1263
1445
|
yield* pairSamsungTvRemote(yield* loadEnv());
|
|
1264
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
|
+
}));
|
|
1265
1456
|
const tvPress = Command.make("press", {
|
|
1266
1457
|
delayMs: Flag.integer("delay-ms").pipe(Flag.withDefault(250)),
|
|
1267
1458
|
json: Flag.boolean("json"),
|
|
@@ -1276,6 +1467,7 @@ const tvInfo = Command.make("info", { json: Flag.boolean("json") }, ({ json }) =
|
|
|
1276
1467
|
yield* showSamsungTvInfo(yield* loadEnv(), { json });
|
|
1277
1468
|
}));
|
|
1278
1469
|
const tv = Command.make("tv", {}).pipe(Command.withSubcommands([
|
|
1470
|
+
tvDoctor,
|
|
1279
1471
|
tvPair,
|
|
1280
1472
|
tvPress,
|
|
1281
1473
|
tvInfo
|
package/docs/TV_REMOTE.md
CHANGED
|
@@ -6,6 +6,9 @@ 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
|
|
@@ -14,6 +17,10 @@ taizn tv press --json KEY_ENTER
|
|
|
14
17
|
taizn tv press --delay-ms 250 KEY_HOME KEY_DOWN KEY_ENTER
|
|
15
18
|
```
|
|
16
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.
|
|
17
24
|
- `info` reads the TV's local `/api/v2/` metadata and reports remote-control
|
|
18
25
|
support. Add `--json` to emit a structured TV capability snapshot for agents
|
|
19
26
|
and scripts.
|
|
@@ -68,7 +75,8 @@ The `.taizn/` directory is local state and must stay ignored.
|
|
|
68
75
|
|
|
69
76
|
## Scope
|
|
70
77
|
|
|
71
|
-
Remote commands
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|