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:
@@ -9080,3 +9080,8 @@ COLOR (2026-06-28, folded into payload spec): add an OPTIONAL per-entry ''color'
|
||||
DONE (2026-06-29): Phosphor glyph card landed end-to-end + tests green. icon.show (Flutter-free, dart-tested): variadic stringList positionals (clide icon show gear folder — required a schema enhancement so a trailing stringList positional is variadic) OR --file JSON [{icon,label,description,color}]; resolves by name (injected kPhosphorGlyphs resolver) or 0xNNNN; color via parseSvgColor (hex/CSS); publishes on ''icon'' bus; honest userError on unknown glyph/bad color/malformed payload/no UI. Card: per entry a hero (52) + continuous sample strip 10..48, optional label/description, per-entry/card color; display-only D-78. Commits 9a2b4e9e (handler+schema) + 281fb2a8 (card+wiring).', NULL, '2026-06-29 11:11:17', '2026-06-29 11:11:17.477', '2026-06-29 11:11:17.477', NULL, 'dc19f7fc9798ca56d6ec7db1779744e4', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'status', 'in_progress', 'done', NULL, '2026-06-29 11:11:17', '2026-06-29 11:11:17.514', '2026-06-29 11:11:17.514', NULL, 'b051374bed839b26002105c4febe1574', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ESCR4V6V07CRH18FBCDN8', 'status', 'backlog', 'in_progress', NULL, '2026-06-29 11:11:28', '2026-06-29 11:11:28.762', '2026-06-29 11:11:28.762', NULL, '490b259653d17d1643ebb9e8a17f0574', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ESCR4V6V07CRH18FBCDN8', 'description', 'Template for the unified drawing card (T-317, D-91): compare-images / before-after. The JSON carries two (or more) image paths, each with an optional label + description; the card renders them side by side for visual comparison, each with a lightbox-on-click (shared with the image template). Lowers onto the canvas primitives + the shared per-object label/description widget (T-318). Display-only (D-78). Depends on the core engine (T-318).', 'Template for the unified drawing card (T-317, D-91): compare-images / before-after. The JSON carries two (or more) image paths, each with an optional label + description; the card renders them side by side for visual comparison, each with a lightbox-on-click (shared with the image template). Lowers onto the canvas primitives + the shared per-object label/description widget (T-318). Display-only (D-78). Depends on the core engine (T-318).
|
||||
|
||||
DONE (2026-06-29): SVG-substrate compare card landed (approach chosen by user) + tests green. compareTemplateHandler lowers an images array [{path,label,description}] to side-by-side <image> cells, each with the T-318 data-label/description caption + data-lightbox; paths resolved to absolute up front (injected, honest DrawErr on a miss). _paintImage now aspect-fits (contain, centered) so differing shapes don''t distort. Draw card path loads <image> hrefs -> ui.Image via loadDrawingImages (decoder injectable for tests); renderer + lightbox both paint through the resolver. Commits a651c9e0 (template+painter) + 1f6d31ac (image-loading+register).', NULL, '2026-06-29 13:02:44', '2026-06-29 13:02:44.687', '2026-06-29 13:02:44.687', NULL, '61cd7ded0058d2727055ab9805712180', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ESCR4V6V07CRH18FBCDN8', 'status', 'in_progress', 'done', NULL, '2026-06-29 13:02:44', '2026-06-29 13:02:44.726', '2026-06-29 13:02:44.726', NULL, 'a03f0481fb84e46fa45077355a64a47b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ACSDBDZARV3NNGYD9NYYR', 'status', 'backlog', 'in_progress', NULL, '2026-06-29 16:44:18', '2026-06-29 16:44:18.342', '2026-06-29 16:44:18.342', NULL, '0d5c93289262d7527ea7471e96f8d02b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -10959,3 +10959,37 @@ COLOR (2026-06-28, folded into payload spec): add an OPTIONAL per-entry ''color'
|
||||
|
||||
DONE (2026-06-29): Phosphor glyph card landed end-to-end + tests green. icon.show (Flutter-free, dart-tested): variadic stringList positionals (clide icon show gear folder — required a schema enhancement so a trailing stringList positional is variadic) OR --file JSON [{icon,label,description,color}]; resolves by name (injected kPhosphorGlyphs resolver) or 0xNNNN; color via parseSvgColor (hex/CSS); publishes on ''icon'' bus; honest userError on unknown glyph/bad color/malformed payload/no UI. Card: per entry a hero (52) + continuous sample strip 10..48, optional label/description, per-entry/card color; display-only D-78. Commits 9a2b4e9e (handler+schema) + 281fb2a8 (card+wiring).', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 10:20:54', '2026-06-29 11:11:17.514', NULL, '0952c384083c3bd521c8b693bdbf1bf7', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ESCR4V6V07CRH18FBCDN8', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card template: compare-images / before-after', 'Template for the unified drawing card (T-317, D-91): compare-images / before-after. The JSON carries two (or more) image paths, each with an optional label + description; the card renders them side by side for visual comparison, each with a lightbox-on-click (shared with the image template). Lowers onto the canvas primitives + the shared per-object label/description widget (T-318). Display-only (D-78). Depends on the core engine (T-318).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:45', '2026-06-29 11:11:28.761', NULL, 'd0854a069e7eb5a42e250a9b7ef134cb', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ESCR4V6V07CRH18FBCDN8', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card template: compare-images / before-after', 'Template for the unified drawing card (T-317, D-91): compare-images / before-after. The JSON carries two (or more) image paths, each with an optional label + description; the card renders them side by side for visual comparison, each with a lightbox-on-click (shared with the image template). Lowers onto the canvas primitives + the shared per-object label/description widget (T-318). Display-only (D-78). Depends on the core engine (T-318).
|
||||
|
||||
DONE (2026-06-29): SVG-substrate compare card landed (approach chosen by user) + tests green. compareTemplateHandler lowers an images array [{path,label,description}] to side-by-side <image> cells, each with the T-318 data-label/description caption + data-lightbox; paths resolved to absolute up front (injected, honest DrawErr on a miss). _paintImage now aspect-fits (contain, centered) so differing shapes don''t distort. Draw card path loads <image> hrefs -> ui.Image via loadDrawingImages (decoder injectable for tests); renderer + lightbox both paint through the resolver. Commits a651c9e0 (template+painter) + 1f6d31ac (image-loading+register).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:45', '2026-06-29 13:02:44.687', NULL, 'c2e33736d8dcbe31102fd5f12495c53e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ESCR4V6V07CRH18FBCDN8', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'Drawing card template: compare-images / before-after', 'Template for the unified drawing card (T-317, D-91): compare-images / before-after. The JSON carries two (or more) image paths, each with an optional label + description; the card renders them side by side for visual comparison, each with a lightbox-on-click (shared with the image template). Lowers onto the canvas primitives + the shared per-object label/description widget (T-318). Display-only (D-78). Depends on the core engine (T-318).
|
||||
|
||||
DONE (2026-06-29): SVG-substrate compare card landed (approach chosen by user) + tests green. compareTemplateHandler lowers an images array [{path,label,description}] to side-by-side <image> cells, each with the T-318 data-label/description caption + data-lightbox; paths resolved to absolute up front (injected, honest DrawErr on a miss). _paintImage now aspect-fits (contain, centered) so differing shapes don''t distort. Draw card path loads <image> hrefs -> ui.Image via loadDrawingImages (decoder injectable for tests); renderer + lightbox both paint through the resolver. Commits a651c9e0 (template+painter) + 1f6d31ac (image-loading+register).', 'done', 'medium', NULL, NULL, NULL, '2026-06-10 11:11:45', '2026-06-29 13:02:44.726', NULL, '0c8fca4b7ab4f3d23364e389373a35b0', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ACSDBDZARV3NNGYD9NYYR', 'task', '06FB2EDCBYRBDSV9V1PJ1KE3CM', 'clide CLI: accept a JSON payload on stdin for structured commands', 'Add a stdin path to clide''s CLI so a command can receive a JSON payload piped in — `… | clide icon show --stdin`, `cat meta.json | clide image show foo.png --stdin` — instead of only positionals/flags or a `--file`.
|
||||
|
||||
## Why
|
||||
|
||||
Structured commands (the labelled icon-card entries in T-313, image annotation metadata in T-316) want a JSON payload that''s awkward to express as flags. Today clide''s CLI argv parser (lib/src/cli/argv_to_request.dart) only produces positionals, --flags, and `-- passthrough`; there is no stdin path. T-313 therefore falls back to a `--file <path.json>` flag. A `--stdin` convention is the ergonomic peer of `--file` for piping, and is shared infra both icon.show and image.show consume.
|
||||
|
||||
## Where the work lives
|
||||
|
||||
clide''s IPC server runs in-process and the `clide` CLI is a thin client that serialises argv into an IpcRequest over CLIDE_SOCK. So stdin must be slurped CLIENT-SIDE (in lib/src/cli/, around argv_to_request.dart / argv_dispatch.dart) and folded into the request before it is sent — the in-process handler never sees the real stdin. Decide how it surfaces in the envelope: e.g. a reserved `stdin`/`payload` field on IpcRequest, or a synthesised arg the CommandSchema can opt into (an ArgSpec flag like `acceptsStdin`, mirroring how ArgType.stringList is declared in lib/src/ipc/command_schema.dart).
|
||||
|
||||
## Scope / decisions
|
||||
|
||||
- Generic infra, not icon/image specific — once landed, any command opts in via its CommandSchema.
|
||||
- Keep `--file` working; --stdin and --file should be mutually exclusive (error if both given) or layered with a defined precedence.
|
||||
- Text/JSON payloads only to start; define a size cap and a clear error when --stdin is passed but stdin is empty/not a pipe (don''t hang waiting on a TTY).
|
||||
- Honest IpcError (userError) on malformed JSON, surfaced like image.show''s other validation failures.
|
||||
- D-6 parity: document the stdin convention alongside the other CLI verbs.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- A command can declare (via CommandSchema) that it accepts a stdin payload; piping JSON in populates the IpcRequest with that payload.
|
||||
- `clide icon show --stdin` (T-313) and `clide image show <path> --stdin` (T-316) both consume it.
|
||||
- --stdin + --file together is a clear user error; --stdin with no piped input fails fast, never hangs on a TTY.
|
||||
- Malformed JSON returns a userError with a helpful message.
|
||||
|
||||
Unblocks the piped-JSON variants of T-313 (icon entries) and T-316 (image annotations); both can also ship with --file independently of this.
|
||||
|
||||
Deferred 2026-06-28 (user): blocked on its consumers T-316 + T-313 so it only resurfaces if one actually wants the piping UX. Standalone --stdin infra isn''t worth a build cycle now — both consumers can ship with --file (per this ticket), and structured JSON also flows natively through the MCP tool surface (D-86). Build it lazily inside whichever consumer first needs piping, if ever.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-10 10:52:34', '2026-06-29 16:44:18.342', NULL, 'c6331517a674a7e66a02bac032bc6cff', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -18,6 +18,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Added
|
||||
|
||||
- **Piped `--stdin` payloads.** `cat icons.json | clide icon show --stdin` (and
|
||||
`image show`) accept a JSON payload on stdin — the ergonomic peer of `--file`
|
||||
for structured commands. (T-315)
|
||||
- **Before/after compare cards.** `clide draw --file compare.json` (template
|
||||
`compare`) renders two or more images side by side, each with its own
|
||||
label/description and tap-to-zoom; images aspect-fit so differing shapes
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -279,19 +279,41 @@ static void json_escape(const char *s, char *out, size_t out_size) {
|
||||
|
||||
/* Build the request envelope and write it to `out`. Returns 0 on
|
||||
* success, -1 if any input was too large. */
|
||||
static int build_request(int argc, char **argv, long long pid, char *out, size_t out_size) {
|
||||
/* Slurp stdin (a piped JSON payload, T-315) into buf, NUL-terminated. Bounded:
|
||||
* reads at most size-1 bytes — a larger payload is truncated, and the envelope's
|
||||
* own size guard then rejects it loudly rather than corrupting the wire. */
|
||||
static int slurp_stdin(char *buf, size_t size) {
|
||||
size_t n = fread(buf, 1, size - 1, stdin);
|
||||
buf[n] = '\0';
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
static int build_request(int argc, char **argv, long long pid, char *out, size_t out_size, const char *stdin_data) {
|
||||
/* Compute argv array size: each arg gets its own escaped JSON. */
|
||||
int n = snprintf(out, out_size,
|
||||
"{\"type\":\"request\",\"v\":1,\"id\":\"c%lld\",\"cmd\":\"_argv\",\"args\":{\"argv\":[",
|
||||
pid);
|
||||
if (n < 0 || (size_t)n >= out_size) return -1;
|
||||
int first = 1;
|
||||
for (int i = 0; i < argc; i++) {
|
||||
/* The --stdin flag is a slurp signal, not a value — strip it from argv;
|
||||
* the payload rides alongside in the `stdin` field (T-315). */
|
||||
if (strcmp(argv[i], "--stdin") == 0) continue;
|
||||
char esc[4096];
|
||||
json_escape(argv[i], esc, sizeof(esc));
|
||||
n += snprintf(out + n, out_size - n, "%s%s", i ? "," : "", esc);
|
||||
n += snprintf(out + n, out_size - n, "%s%s", first ? "" : ",", esc);
|
||||
if (n < 0 || (size_t)n >= out_size) return -1;
|
||||
first = 0;
|
||||
}
|
||||
n += snprintf(out + n, out_size - n, "]");
|
||||
if (n < 0 || (size_t)n >= out_size) return -1;
|
||||
if (stdin_data && *stdin_data) {
|
||||
char esc[40000];
|
||||
json_escape(stdin_data, esc, sizeof(esc));
|
||||
n += snprintf(out + n, out_size - n, ",\"stdin\":%s", esc);
|
||||
if (n < 0 || (size_t)n >= out_size) return -1;
|
||||
}
|
||||
n += snprintf(out + n, out_size - n, "]}}\n");
|
||||
n += snprintf(out + n, out_size - n, "}}\n");
|
||||
return (n < 0 || (size_t)n >= out_size) ? -1 : 0;
|
||||
}
|
||||
|
||||
@@ -386,7 +408,7 @@ static int list_instances(void) {
|
||||
sock_t fd = connect_unix(path);
|
||||
if (fd == NET_INVALID) continue; /* dead socket — skip */
|
||||
char req[1024];
|
||||
if (build_request(1, qargv, (long long)clide_getpid(), req, sizeof(req)) == 0 &&
|
||||
if (build_request(1, qargv, (long long)clide_getpid(), req, sizeof(req), NULL) == 0 &&
|
||||
net_write(fd, req, (int)strlen(req)) == (int)strlen(req)) {
|
||||
char resp[65536];
|
||||
if (read_line(fd, resp, sizeof(resp)) == 0) {
|
||||
@@ -460,10 +482,22 @@ int main(int argc, char **argv) {
|
||||
return EX_UNAVAILABLE;
|
||||
}
|
||||
|
||||
/* A `--stdin` flag anywhere in argv means slurp a piped JSON payload and
|
||||
* ship it alongside (T-315) — the structured peer of `--file`. */
|
||||
char stdin_buf[32768];
|
||||
const char *stdin_data = NULL;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (strcmp(argv[i], "--stdin") == 0) {
|
||||
slurp_stdin(stdin_buf, sizeof(stdin_buf));
|
||||
stdin_data = stdin_buf;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Build + send request. Worst-case envelope sizing: argv totals
|
||||
* plus JSON overhead. 64 KB envelope handles 4 KB args * 16. */
|
||||
char req[65536];
|
||||
if (build_request(argc - 1, argv + 1, (long long)clide_getpid(), req, sizeof(req)) != 0) {
|
||||
if (build_request(argc - 1, argv + 1, (long long)clide_getpid(), req, sizeof(req), stdin_data) != 0) {
|
||||
fprintf(stderr, "clide: request payload too large\n");
|
||||
net_close(fd);
|
||||
return EX_USAGE;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
library;
|
||||
|
||||
import 'package:clide/src/cli/argv_dispatch.dart';
|
||||
import 'package:clide/src/cli/argv_to_request.dart' show ArgvParsed;
|
||||
import 'package:clide/src/daemon/dispatcher.dart';
|
||||
import 'package:clide/src/ipc/envelope.dart';
|
||||
import 'package:clide/src/ipc/schema_v1.dart';
|
||||
@@ -34,6 +35,23 @@ void main() {
|
||||
expect(res.data['pong'], isTrue);
|
||||
});
|
||||
|
||||
test('a stdin payload folds into the inner request as a flag (T-315)', () {
|
||||
final result = unwrapArgvRequest(
|
||||
IpcRequest(
|
||||
id: 'x',
|
||||
cmd: argvSentinelCmd,
|
||||
args: {
|
||||
'argv': ['icon', 'show'],
|
||||
'stdin': '[{"icon":"gear"}]',
|
||||
},
|
||||
),
|
||||
);
|
||||
expect(result, isA<ArgvParsed>());
|
||||
final req = (result as ArgvParsed).request;
|
||||
expect(req.cmd, 'icon.show');
|
||||
expect((req.args['flags'] as Map)['stdin'], '[{"icon":"gear"}]');
|
||||
});
|
||||
|
||||
test('args.argv missing → userError', () async {
|
||||
final res = await d.dispatch(IpcRequest(id: 'y', cmd: argvSentinelCmd, args: const {}));
|
||||
expect(res.ok, isFalse);
|
||||
|
||||
@@ -63,6 +63,20 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
test('a --stdin payload is parsed like --file (T-315)', () async {
|
||||
wire();
|
||||
final r = await show([], flags: {'stdin': '[{"icon":"gear","label":"Settings"}]'});
|
||||
expect(r.ok, isTrue, reason: r.error?.message);
|
||||
expect((published.single.data['entries'] as List).single, {'codepoint': 0xe2a4, 'name': 'gear', 'label': 'Settings'});
|
||||
});
|
||||
|
||||
test('--stdin wins over --file', () async {
|
||||
wire(files: {'i.json': '[{"icon":"folder"}]'});
|
||||
final r = await show([], flags: {'stdin': '[{"icon":"gear"}]'});
|
||||
expect(r.ok, isTrue, reason: r.error?.message);
|
||||
expect((published.single.data['entries'] as List).single['name'], 'gear');
|
||||
});
|
||||
|
||||
test('a card-level --color rides along', () async {
|
||||
wire();
|
||||
final r = await show(['gear'], flags: {'color': 'red'});
|
||||
|
||||
@@ -56,6 +56,13 @@ void main() {
|
||||
expect(published.single.data, {'path': '/abs/docs/shot.png', 'caption': 'before', 'label': 'HUD v3', 'description': 'cramped status row'});
|
||||
});
|
||||
|
||||
test('a --stdin payload carries metadata like --file (T-315)', () async {
|
||||
wire(found: {'docs/shot.png'});
|
||||
final r = await show([], flags: {'stdin': '{"path":"docs/shot.png","label":"HUD"}'});
|
||||
expect(r.ok, isTrue, reason: r.error?.message);
|
||||
expect(published.single.data, {'path': '/abs/docs/shot.png', 'label': 'HUD'});
|
||||
});
|
||||
|
||||
test('a malformed --file payload is an honest userError, nothing published', () async {
|
||||
wire(files: {'meta.json': 'not json'});
|
||||
final r = await show([], flags: {'file': 'meta.json'});
|
||||
|
||||
Reference in New Issue
Block a user