@playgenx/components 0.2.0 → 0.2.1

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.
Files changed (2) hide show
  1. package/bin/state-cli.mjs +196 -0
  2. package/package.json +6 -2
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: MIT
3
+ /**
4
+ * playgenx-state — CLI for inspecting and operating on
5
+ * PlaygroundState snapshots.
6
+ *
7
+ * Pure ESM, no extra deps. Reads JSON from stdin (or argv),
8
+ * writes JSON to stdout.
9
+ *
10
+ * Subcommands:
11
+ *
12
+ * snapshot Pretty-print a snapshot in the v1 envelope format.
13
+ * Accepts a JSON object on stdin (the snapshot you
14
+ * captured from `state.snapshot()` or a DebugSurface).
15
+ *
16
+ * validate Validate one or more stateKey strings from stdin.
17
+ * Reads a JSON array of strings, returns the per-key
18
+ * verdict.
19
+ *
20
+ * diff Diff two snapshots. Reads `{prev, next}` from stdin
21
+ * and returns the structured {added, changed, removed}
22
+ * diff (or null when equal).
23
+ *
24
+ * keys List the sorted keys of a snapshot from stdin.
25
+ *
26
+ * count Count the keys in a snapshot from stdin.
27
+ *
28
+ * Usage examples (run from the components package root):
29
+ *
30
+ * echo '{"volume": 5}' | node bin/state-cli.mjs snapshot
31
+ * echo '["volume","user.name","bad key"]' | node bin/state-cli.mjs validate
32
+ * echo '{"prev":{"a":1},"next":{"a":2,"b":3}}' | node bin/state-cli.mjs diff
33
+ *
34
+ * Exit codes:
35
+ *
36
+ * 0 success
37
+ * 1 bad subcommand
38
+ * 2 bad input (not parseable as JSON, wrong shape, etc.)
39
+ * 3 validation failure (one or more keys rejected)
40
+ *
41
+ * @packageDocumentation
42
+ */
43
+
44
+ import {
45
+ dumpState,
46
+ diffSnapshots,
47
+ validateStateKey,
48
+ } from '../dist/index.mjs';
49
+
50
+ /**
51
+ * Read all of stdin into a string. Node-only.
52
+ */
53
+ async function readStdin() {
54
+ if (process.stdin.isTTY) {
55
+ // Interactive / no pipe — return empty; the caller will treat
56
+ // this as "no input" and print usage.
57
+ return '';
58
+ }
59
+ const chunks = [];
60
+ for await (const chunk of process.stdin) {
61
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
62
+ }
63
+ return Buffer.concat(chunks).toString('utf8');
64
+ }
65
+
66
+ function usage() {
67
+ return [
68
+ 'Usage: playgenx-state <subcommand> [input]',
69
+ '',
70
+ 'Subcommands:',
71
+ ' snapshot Pretty-print a state snapshot (JSON on stdin)',
72
+ ' validate Validate an array of stateKey strings (JSON array on stdin)',
73
+ ' diff Diff two snapshots ({prev, next} on stdin)',
74
+ ' keys List sorted keys of a snapshot (JSON on stdin)',
75
+ ' count Count keys in a snapshot (JSON on stdin)',
76
+ '',
77
+ 'Reads from stdin when no arg is given.',
78
+ ].join('\n');
79
+ }
80
+
81
+ function ok(payload) {
82
+ process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
83
+ }
84
+
85
+ function fail(code, msg) {
86
+ process.stderr.write(`error: ${msg}\n`);
87
+ process.exit(code);
88
+ }
89
+
90
+ /**
91
+ * Convert an arbitrary snapshot to the StateEnvelope shape. The
92
+ * input is whatever the caller passed; we expect a plain object.
93
+ */
94
+ function envelope(input) {
95
+ const keys = Object.keys(input).sort();
96
+ return { version: 1, keys, values: { ...input } };
97
+ }
98
+
99
+ async function main(argv) {
100
+ const sub = argv[0];
101
+ if (!sub || sub === '-h' || sub === '--help') {
102
+ process.stdout.write(usage() + '\n');
103
+ return 0;
104
+ }
105
+ const stdinPayload = (await readStdin()).trim();
106
+ const argPayload = argv[1];
107
+
108
+ switch (sub) {
109
+ case 'snapshot': {
110
+ const input = parseObject(argPayload ?? stdinPayload);
111
+ // Re-use the library's envelope so the CLI output matches the
112
+ // canonical dumpState shape. (We don't have a real store to
113
+ // pass to dumpState; envelope() builds the same structure.)
114
+ const env = envelope(input);
115
+ process.stdout.write(JSON.stringify(env, null, 2) + '\n');
116
+ // Reference dumpState so it's not tree-shaken — the import
117
+ // is documented as the canonical envelope producer.
118
+ void dumpState;
119
+ return 0;
120
+ }
121
+
122
+ case 'validate': {
123
+ const input = parseArray(argPayload ?? stdinPayload);
124
+ const results = input.map((k) => {
125
+ if (typeof k !== 'string') {
126
+ return { key: String(k), ok: false, error: 'must be a string' };
127
+ }
128
+ const err = validateStateKey(k);
129
+ return err === null
130
+ ? { key: k, ok: true }
131
+ : { key: k, ok: false, error: err };
132
+ });
133
+ const anyFailed = results.some((r) => !r.ok);
134
+ ok(results);
135
+ return anyFailed ? 3 : 0;
136
+ }
137
+
138
+ case 'diff': {
139
+ const input = parseObject(argPayload ?? stdinPayload);
140
+ if (!('prev' in input) || !('next' in input)) {
141
+ fail(2, 'diff expects {prev, next}');
142
+ }
143
+ const prev = input.prev;
144
+ const next = input.next;
145
+ if (typeof prev !== 'object' || prev === null) fail(2, 'prev must be an object');
146
+ if (typeof next !== 'object' || next === null) fail(2, 'next must be an object');
147
+ const d = diffSnapshots(prev, next);
148
+ ok(d);
149
+ return 0;
150
+ }
151
+
152
+ case 'keys': {
153
+ const input = parseObject(argPayload ?? stdinPayload);
154
+ ok(Object.keys(input).sort());
155
+ return 0;
156
+ }
157
+
158
+ case 'count': {
159
+ const input = parseObject(argPayload ?? stdinPayload);
160
+ ok({ keys: Object.keys(input).length });
161
+ return 0;
162
+ }
163
+
164
+ default:
165
+ process.stderr.write(usage() + '\n');
166
+ return 1;
167
+ }
168
+ }
169
+
170
+ function parseObject(raw) {
171
+ if (raw === '') fail(2, 'expected a JSON object on stdin or as argv[1]');
172
+ try {
173
+ const v = JSON.parse(raw);
174
+ if (typeof v !== 'object' || v === null || Array.isArray(v)) {
175
+ fail(2, 'expected a JSON object');
176
+ }
177
+ return v;
178
+ } catch (err) {
179
+ fail(2, `JSON parse error: ${err.message}`);
180
+ }
181
+ }
182
+
183
+ function parseArray(raw) {
184
+ if (raw === '') fail(2, 'expected a JSON array on stdin or as argv[1]');
185
+ try {
186
+ const v = JSON.parse(raw);
187
+ if (!Array.isArray(v)) fail(2, 'expected a JSON array');
188
+ return v;
189
+ } catch (err) {
190
+ fail(2, `JSON parse error: ${err.message}`);
191
+ }
192
+ }
193
+
194
+ main(process.argv.slice(2))
195
+ .then((code) => process.exit(code))
196
+ .catch((err) => fail(2, err.message ?? String(err)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playgenx/components",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Default React 19 implementations for every entry in DEFAULT_REGISTRY. Pair with @playgenx/registry and the parser/validator pipeline.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -11,8 +11,12 @@
11
11
  "import": "./dist/index.mjs"
12
12
  }
13
13
  },
14
+ "bin": {
15
+ "playgenx-state": "./bin/state-cli.mjs"
16
+ },
14
17
  "files": [
15
- "dist"
18
+ "dist",
19
+ "bin"
16
20
  ],
17
21
  "scripts": {
18
22
  "build": "tsdown",