feat(cli): --stdin piped JSON payloads for structured commands (T-315)

The C client slurps stdin when it sees --stdin, strips the flag, and ships
the payload alongside the argv; the Dart unwrap folds it into the request
as a `stdin` arg (undeclared keys pass the schema untouched). icon.show and
image.show now read that payload as the peer of --file (stdin wins). Bounded
slurp + the envelope's existing size guard keep a huge payload from
corrupting the wire. C client builds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 23:56:33 +02:00
co-authored by Claude Opus 4.8
parent 1f6d31ac73
commit 64410e8047
10 changed files with 160 additions and 21 deletions
+15 -1
View File
@@ -36,7 +36,21 @@ ArgvParseResult unwrapArgvRequest(IpcRequest outer) {
),
);
}
return parseArgv(raw.cast<String>(), requestId: outer.id);
final result = parseArgv(raw.cast<String>(), requestId: outer.id);
// A piped `--stdin` payload (T-315): the C client slurps stdin and ships it
// alongside the argv. Fold it into the inner request as a `stdin` flag so it
// surfaces as a named arg (undeclared keys pass the schema untouched) — the
// handler reads it as the structured payload, the piped peer of `--file`.
final stdin = outer.args['stdin'];
if (result is ArgvParsed && stdin is String) {
final req = result.request;
final args = Map<String, Object?>.from(req.args);
final flags = Map<String, Object?>.from((args['flags'] as Map?) ?? const {});
flags['stdin'] = stdin;
args['flags'] = flags;
return ArgvParsed(IpcRequest(id: req.id, cmd: req.cmd, args: args));
}
return result;
}
/// Wire the `_argv` sentinel handler onto [dispatcher]. The handler
+12 -6
View File
@@ -62,11 +62,14 @@ Future<IpcResponse> _show(IpcRequest req, MessagePublisher? Function() publisher
}
final entries = <Map<String, Object?>>[];
// The entry payload comes from a piped --stdin (T-315) or a --file; --stdin
// wins. Either is a JSON array of {icon,label,description,color}.
final stdin = _str(req.args['stdin']);
final file = _str(req.args['file']);
if (file != null) {
final raw = readFile == null ? null : await readFile(file);
if (raw == null) {
String? payload = stdin;
if (payload == null && file != null) {
payload = readFile == null ? null : await readFile(file);
if (payload == null) {
return IpcResponse.err(
id: req.id,
error: IpcError(
@@ -77,11 +80,14 @@ Future<IpcResponse> _show(IpcRequest req, MessagePublisher? Function() publisher
),
);
}
}
if (payload != null) {
Object? decoded;
try {
decoded = jsonDecode(raw);
decoded = jsonDecode(payload);
} on FormatException catch (e) {
return _userErr(req.id, 'invalid JSON in $file: ${e.message}');
return _userErr(req.id, 'invalid JSON in the icon payload: ${e.message}');
}
if (decoded is! List) return _userErr(req.id, 'icon metadata must be a JSON array of entries');
for (final item in decoded) {
+13 -9
View File
@@ -68,13 +68,15 @@ Future<IpcResponse> _show(IpcRequest req, MessagePublisher? Function() publisher
String? label, description;
String? caption = req.args['caption'] as String?;
// --file <json>: an annotation payload {path,label,description,caption}
// (T-316). Additive — the bare `image show <path> [--caption]` form is
// unchanged; label/description are the new richer metadata.
final file = req.args['file'] as String?;
if (file != null && file.trim().isNotEmpty) {
final raw = readFile == null ? null : await readFile(file);
if (raw == null) {
// An annotation payload {path,label,description,caption} from a piped --stdin
// (T-315) or a --file (T-316); --stdin wins. Additive — the bare
// `image show <path> [--caption]` form is unchanged.
final stdin = _str(req.args['stdin']);
final file = _str(req.args['file']);
String? payload = stdin;
if (payload == null && file != null) {
payload = readFile == null ? null : await readFile(file);
if (payload == null) {
return IpcResponse.err(
id: req.id,
error: IpcError(
@@ -85,11 +87,13 @@ Future<IpcResponse> _show(IpcRequest req, MessagePublisher? Function() publisher
),
);
}
}
if (payload != null) {
Object? decoded;
try {
decoded = jsonDecode(raw);
decoded = jsonDecode(payload);
} on FormatException catch (e) {
return _userErr(req.id, 'invalid JSON in $file: ${e.message}');
return _userErr(req.id, 'invalid JSON in the image payload: ${e.message}');
}
if (decoded is! Map) return _userErr(req.id, 'image metadata must be a JSON object');
path = _str(decoded['path']) ?? path;