Skip to content

Commit 653ce58

Browse files
authored
Merge pull request #10 from git-stunts/release/v0.10.0
Release/v0.10.0–add JSONL command channel
2 parents c34820a + 46f731e commit 653ce58

19 files changed

Lines changed: 797 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ All notable changes to this project will be documented in this file.
1010
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1111
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1212

13+
## [0.10.0] - 2026-02-04 (@git-stunts/alfred)
14+
15+
### Changed
16+
17+
- Version bump to keep lockstep alignment with the Alfred package family (no API changes).
18+
1319
## [0.9.1] - 2026-02-04 (@git-stunts/alfred)
1420

1521
### Changed
@@ -162,6 +168,15 @@ All notable changes to this project will be documented in this file.
162168
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
163169
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
164170

171+
## [0.10.0] - 2026-02-04 (@git-stunts/alfred-live)
172+
173+
### Added
174+
175+
- Canonical JSONL command envelope with strict validation helpers.
176+
- Result envelope helpers plus JSONL encode/decode utilities.
177+
- `alfredctl` CLI for emitting JSONL commands.
178+
- JSONL command channel example and tests.
179+
165180
## [0.9.1] - 2026-02-04 (@git-stunts/alfred-live)
166181

167182
### Changed

COOKBOOK.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,18 @@ router.execute({ type: 'list_config', prefix: 'bulkhead' });
104104

105105
---
106106

107-
## Recipe: Control plane CLI (planned)
107+
## Recipe: Control plane CLI (JSONL)
108108

109109
**Goal**
110110
Send control plane commands from a CLI.
111111

112112
**Packages**
113113
- `@git-stunts/alfred-live`
114114

115-
**Status**
116-
Planned for v0.10 as `alfredctl`.
115+
**Example**
116+
117+
```bash
118+
alfredctl list retry
119+
alfredctl read retry/count
120+
alfredctl write retry/count 5
121+
```

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Alfred is a **policy engine** for async resilience: composable, observable, test
2525
## Packages
2626

2727
- `@git-stunts/alfred` — Resilience policies and composition utilities for async operations.
28-
- `@git-stunts/alfred-live`In-memory control plane primitives plus live policy plans.
28+
- `@git-stunts/alfred-live`Control plane primitives, live policy plans, and the `alfredctl` CLI.
2929

3030
## Versioning Policy
3131

@@ -73,6 +73,7 @@ The publish flow runs `release:preflight`, which verifies:
7373
- `jsr.json` versions match `package.json`.
7474
- `package.json` exports exist on disk and are included in `files`.
7575
- `jsr.json` exports match `package.json` exports.
76+
- `pnpm install --frozen-lockfile` succeeds (lockfile matches specs).
7677
- `npm pack --dry-run` succeeds for each published package.
7778

7879
## Repo Layout

alfred-live/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.10.0] - 2026-02-04
9+
10+
### Added
11+
12+
- Canonical JSONL command envelope with strict validation helpers.
13+
- Result envelope helpers plus JSONL encode/decode utilities.
14+
- `alfredctl` CLI for emitting JSONL commands.
15+
- JSONL command channel example and tests.
16+
817
## [0.9.1] - 2026-02-04
918

1019
### Changed

alfred-live/README.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,55 @@ router.execute({ type: 'write_config', path: 'retry/count', value: '5' });
6565
router.execute({ type: 'list_config', prefix: 'retry' });
6666
```
6767

68+
## Command Channel (JSONL)
69+
70+
Alfred Live exposes a canonical JSONL envelope for sending commands over
71+
stdin/stdout. Use the helper functions to decode, validate, and execute.
72+
73+
```javascript
74+
import {
75+
CommandRouter,
76+
ConfigRegistry,
77+
executeCommandLine,
78+
decodeCommandEnvelope,
79+
encodeCommandEnvelope,
80+
} from '@git-stunts/alfred-live';
81+
82+
const registry = new ConfigRegistry();
83+
const router = new CommandRouter(registry);
84+
85+
const line = JSON.stringify({
86+
id: 'cmd-1',
87+
cmd: 'list_config',
88+
args: { prefix: 'retry' },
89+
});
90+
91+
const decoded = decodeCommandEnvelope(line);
92+
if (!decoded.ok) throw new Error(decoded.error.message);
93+
94+
const resultLine = executeCommandLine(router, line);
95+
if (!resultLine.ok) throw new Error(resultLine.error.message);
96+
console.log(resultLine.data);
97+
98+
const outgoing = encodeCommandEnvelope({
99+
id: 'cmd-2',
100+
cmd: 'read_config',
101+
args: { path: 'retry/count' },
102+
});
103+
if (outgoing.ok) console.log(outgoing.data);
104+
```
105+
106+
## CLI (`alfredctl`)
107+
108+
`alfredctl` emits JSONL commands to stdout. Pipe its output into your control
109+
plane transport (stdin/stdout, ssh, etc.).
110+
111+
```bash
112+
alfredctl list retry
113+
alfredctl read retry/count
114+
alfredctl write retry/count 5
115+
```
116+
68117
## Live Policies
69118

70119
Live policies are described with a `LivePolicyPlan` and then bound to a registry
@@ -121,13 +170,16 @@ registry.write('gateway/api/retry/retries', '5');
121170
## Examples
122171

123172
- `alfred-live/examples/control-plane/basic.js` — in-process registry + command router usage.
173+
- `alfred-live/examples/control-plane/jsonl-channel.js` — JSONL command envelope execution.
124174
- `alfred-live/examples/control-plane/live-policies.js` — live policy wrappers driven by registry state.
125175

126176
## Status
127177

128-
v0.9.0 live policies implemented:
178+
v0.10.0 control plane primitives implemented:
129179

130180
- `Adaptive<T>` live values with version + updatedAt.
131181
- `ConfigRegistry` for typed config and validation.
132182
- Command router for `read_config`, `write_config`, `list_config`.
133183
- `LivePolicyPlan` + `ControlPlane.registerLivePolicy` for live policy stacks.
184+
- Canonical JSONL command envelope + helpers.
185+
- `alfredctl` CLI for emitting JSONL commands.

alfred-live/bin/alfredctl.js

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
#!/usr/bin/env node
2+
3+
import { randomUUID } from 'node:crypto';
4+
import { encodeCommandEnvelope } from '../src/command-envelope.js';
5+
6+
const usage = `alfredctl - emit Alfred Live control-plane commands (JSONL)
7+
8+
Usage:
9+
alfredctl list [prefix] [--id <id>] [--auth <token>]
10+
alfredctl read <path> [--id <id>] [--auth <token>]
11+
alfredctl write <path> <value> [--id <id>] [--auth <token>]
12+
13+
Options:
14+
--id <id> Command id (default: random UUID)
15+
--auth <token> Optional auth token
16+
-h, --help Show this help
17+
`;
18+
19+
function fail(message) {
20+
process.stderr.write(`${message}\n\n${usage.trim()}\n`);
21+
process.exit(1);
22+
}
23+
24+
function redactSensitive(value) {
25+
if (Array.isArray(value)) {
26+
return value.map((entry) => redactSensitive(entry));
27+
}
28+
if (!value || typeof value !== 'object') {
29+
return value;
30+
}
31+
32+
const redacted = {};
33+
for (const [key, entry] of Object.entries(value)) {
34+
if (/auth|token|password/i.test(key)) {
35+
redacted[key] = '[REDACTED]';
36+
} else {
37+
redacted[key] = redactSensitive(entry);
38+
}
39+
}
40+
return redacted;
41+
}
42+
43+
function parseArgs(argv) {
44+
const options = { id: undefined, auth: undefined };
45+
const positionals = [];
46+
47+
for (let i = 0; i < argv.length; i += 1) {
48+
const arg = argv[i];
49+
50+
if (arg === '--') {
51+
positionals.push(...argv.slice(i + 1));
52+
break;
53+
}
54+
55+
if (arg === '-h' || arg === '--help') {
56+
return { help: true, options, positionals };
57+
}
58+
59+
if (arg === '--id' || arg === '--auth') {
60+
const value = argv[i + 1];
61+
if (!value) {
62+
fail(`Missing value for ${arg}.`);
63+
}
64+
if (arg === '--id') {
65+
options.id = value;
66+
} else {
67+
options.auth = value;
68+
}
69+
i += 1;
70+
continue;
71+
}
72+
73+
if (arg.startsWith('--')) {
74+
fail(`Unknown option: ${arg}`);
75+
}
76+
77+
positionals.push(arg);
78+
}
79+
80+
return { help: false, options, positionals };
81+
}
82+
83+
function buildEnvelope(positionals, options) {
84+
if (positionals.length === 0) {
85+
fail('Missing command.');
86+
}
87+
88+
const [command, ...rest] = positionals;
89+
const id = options.id ?? randomUUID();
90+
const auth = options.auth;
91+
92+
switch (command) {
93+
case 'list': {
94+
if (rest.length > 1) {
95+
fail('list accepts at most one prefix argument.');
96+
}
97+
const args = rest[0] ? { prefix: rest[0] } : {};
98+
return { id, cmd: 'list_config', args, auth };
99+
}
100+
case 'read': {
101+
if (rest.length !== 1) {
102+
fail('read requires exactly one path argument.');
103+
}
104+
return { id, cmd: 'read_config', args: { path: rest[0] }, auth };
105+
}
106+
case 'write': {
107+
if (rest.length !== 2) {
108+
fail('write requires a path and value argument.');
109+
}
110+
return { id, cmd: 'write_config', args: { path: rest[0], value: rest[1] }, auth };
111+
}
112+
default:
113+
fail(`Unknown command: ${command}`);
114+
return null;
115+
}
116+
}
117+
118+
const parsed = parseArgs(process.argv.slice(2));
119+
if (parsed.help) {
120+
process.stdout.write(`${usage.trim()}\n`);
121+
process.exit(0);
122+
}
123+
124+
const envelope = buildEnvelope(parsed.positionals, parsed.options);
125+
if (!envelope) {
126+
process.exit(1);
127+
}
128+
const encoded = encodeCommandEnvelope(envelope);
129+
if (!encoded.ok) {
130+
process.stderr.write(`${encoded.error.code}: ${encoded.error.message}\n`);
131+
if (encoded.error.details) {
132+
const redacted = redactSensitive(encoded.error.details);
133+
process.stderr.write(`${JSON.stringify(redacted, null, 2)}\n`);
134+
}
135+
process.exit(1);
136+
}
137+
138+
process.stdout.write(`${encoded.data}\n`);
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {
2+
Adaptive,
3+
CommandRouter,
4+
ConfigRegistry,
5+
executeCommandLine,
6+
} from '@git-stunts/alfred-live';
7+
8+
const registry = new ConfigRegistry();
9+
const retryCount = new Adaptive(3);
10+
11+
registry.register('retry/count', retryCount, {
12+
parse: (value) => {
13+
const parsed = Number(value);
14+
if (!Number.isFinite(parsed)) {
15+
throw new Error('retry/count must be a number');
16+
}
17+
return parsed;
18+
},
19+
format: (value) => value.toString(),
20+
});
21+
22+
const router = new CommandRouter(registry);
23+
24+
const line = JSON.stringify({
25+
id: 'cmd-1',
26+
cmd: 'write_config',
27+
args: { path: 'retry/count', value: '5' },
28+
});
29+
30+
const resultLine = executeCommandLine(router, line);
31+
if (!resultLine.ok) {
32+
throw new Error(resultLine.error.message);
33+
}
34+
35+
console.log(resultLine.data);

alfred-live/jsr.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@git-stunts/alfred-live",
3-
"version": "0.9.1",
3+
"version": "0.10.0",
44
"description": "In-memory control plane for Alfred: adaptive values, config registry, command router.",
55
"license": "Apache-2.0",
66
"exports": {

alfred-live/package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@git-stunts/alfred-live",
3-
"version": "0.9.1",
3+
"version": "0.10.0",
44
"description": "In-memory control plane for Alfred: adaptive values, config registry, command router.",
55
"type": "module",
66
"sideEffects": false,
@@ -12,8 +12,11 @@
1212
"default": "./src/index.js"
1313
}
1414
},
15+
"bin": {
16+
"alfredctl": "./bin/alfredctl.js"
17+
},
1518
"dependencies": {
16-
"@git-stunts/alfred": "0.9.1"
19+
"@git-stunts/alfred": "0.10.0"
1720
},
1821
"engines": {
1922
"node": ">=20.0.0"
@@ -31,6 +34,7 @@
3134
"url": "git+https://github.com/git-stunts/alfred.git"
3235
},
3336
"files": [
37+
"bin",
3438
"src",
3539
"README.md",
3640
"LICENSE",

0 commit comments

Comments
 (0)