gentle-shell-bridge 0.3.0 → 0.3.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 (75) hide show
  1. package/LICENSE +202 -0
  2. package/docs/contract.md +1 -1
  3. package/package.json +13 -1
  4. package/src/api.ts +1 -1
  5. package/.codegraph/.gitignore +0 -5
  6. package/PRD.md +0 -144
  7. package/examples/bash-card.ts +0 -1694
  8. package/gentle-shell-bridge-0.3.0.tgz +0 -0
  9. package/odd/tasks/palette-sections.md +0 -30
  10. package/odd/tasks/plugin-keymap.md +0 -32
  11. package/odd/tasks/plugin-surface-contract.md +0 -55
  12. package/test/api.test.ts +0 -395
  13. package/test/apply-on-move.test.ts +0 -477
  14. package/test/arm.test.ts +0 -220
  15. package/test/bridge-ready.test.ts +0 -309
  16. package/test/bus.test.ts +0 -165
  17. package/test/button-rows.test.ts +0 -409
  18. package/test/capture.test.ts +0 -356
  19. package/test/card-style-rows.test.ts +0 -510
  20. package/test/card-style-transform.test.ts +0 -379
  21. package/test/card-style-wiring.test.ts +0 -258
  22. package/test/card-styles-e2e.test.ts +0 -491
  23. package/test/card-styles.test.ts +0 -324
  24. package/test/card-transcript-path.test.ts +0 -178
  25. package/test/config.test.ts +0 -93
  26. package/test/e2e.test.ts +0 -443
  27. package/test/example-bash-identity.test.ts +0 -372
  28. package/test/example-bash-matrix.test.ts +0 -856
  29. package/test/example-designs.test.ts +0 -734
  30. package/test/example-neon-transform.test.ts +0 -283
  31. package/test/example-plain-style.test.ts +0 -1268
  32. package/test/extension.test.ts +0 -160
  33. package/test/fake-events.ts +0 -58
  34. package/test/fake-host.ts +0 -211
  35. package/test/fake-sidebar.ts +0 -516
  36. package/test/fake-transcript.ts +0 -486
  37. package/test/frame-telemetry.test.ts +0 -417
  38. package/test/hook.test.ts +0 -196
  39. package/test/manifest.test.ts +0 -105
  40. package/test/new-category.test.ts +0 -979
  41. package/test/notices.test.ts +0 -328
  42. package/test/plugin-integration-lines.test.ts +0 -979
  43. package/test/plugin-surface.test.ts +0 -152
  44. package/test/presence.test.ts +0 -459
  45. package/test/rail-surfaces.test.ts +0 -457
  46. package/test/rail-width.test.ts +0 -423
  47. package/test/regression.test.ts +0 -483
  48. package/test/repaint-api.test.ts +0 -200
  49. package/test/reserved-categories.test.ts +0 -361
  50. package/test/row-labels.test.ts +0 -284
  51. package/test/row-names.test.ts +0 -473
  52. package/test/row-origin.test.ts +0 -395
  53. package/test/section-interaction.test.ts +0 -364
  54. package/test/section-panel.test.ts +0 -403
  55. package/test/sections-rows.test.ts +0 -291
  56. package/test/sidebar-backend.test.ts +0 -471
  57. package/test/sidebar-e2e.test.ts +0 -331
  58. package/test/sidebar-hover.test.ts +0 -276
  59. package/test/sidebar-independent.test.ts +0 -349
  60. package/test/sidebar-render.test.ts +0 -281
  61. package/test/sidebar-sections.test.ts +0 -245
  62. package/test/status-card-styles.test.ts +0 -201
  63. package/test/status-section-frame.test.ts +0 -181
  64. package/test/status-sections.test.ts +0 -243
  65. package/test/status.test.ts +0 -209
  66. package/test/store.test.ts +0 -192
  67. package/test/surface-repaint-cost.test.ts +0 -520
  68. package/test/tool-design-chain.test.ts +0 -483
  69. package/test/tool-design-cost.test.ts +0 -473
  70. package/test/tool-design-rows.test.ts +0 -359
  71. package/test/tool-designs.test.ts +0 -745
  72. package/test/transform-pipeline.test.ts +0 -465
  73. package/test/transforms-bus.test.ts +0 -233
  74. package/test/verifier-width-regressions.test.ts +0 -164
  75. package/tsconfig.json +0 -18
@@ -1,309 +0,0 @@
1
- /**
2
- * The announcement: a plugin loaded BEFORE this package can still be served.
3
- *
4
- * The old rule — "the symbol's presence at `session_start` is the complete answer" — is wrong for
5
- * a plugin Pi loads earlier than the package. The example's real TUI report showed it: the global
6
- * extension's `session_start` handler ran first, found no symbol, and said "not installed" while
7
- * the bridge went on to publish. So the bridge now tells the process it published, on Pi's own
8
- * cross-extension bus, exactly once, and only when this copy really owns a compatible slot.
9
- *
10
- * These tests pin the event name and payload, the once-per-process rule across package copies, the
11
- * silence for an incompatible slot, and `onBridgeApi` — the helper that gives an importing plugin
12
- * both forms: immediate when the API is here, subscribed when it is coming.
13
- */
14
-
15
- import assert from "node:assert/strict";
16
- import { mkdtempSync, rmSync } from "node:fs";
17
- import { tmpdir } from "node:os";
18
- import { join } from "node:path";
19
- import test from "node:test";
20
-
21
- import {
22
- API_SCHEMA,
23
- API_VERSION,
24
- BRIDGE_API_KEY,
25
- BRIDGE_READY_EVENT,
26
- VERSION,
27
- announceBridgeReady,
28
- bridgeApi,
29
- onBridgeApi,
30
- publishBridgeApi,
31
- resetBridgeApiForTests,
32
- type BridgeApi,
33
- } from "../src/api.ts";
34
- import {
35
- resetBusForTests,
36
- resetSectionsForTests,
37
- snapshotProviders,
38
- } from "../src/bus.ts";
39
- import gentleShellBridge, { type BridgeExtensionApi } from "../src/extension.ts";
40
- import { resetNoticesForTests } from "../src/notices.ts";
41
- import { resetStoreStateForTests } from "../src/store.ts";
42
- import type { RowProvider } from "../src/types.ts";
43
- import { createFakeEvents, type FakeEvents } from "./fake-events.ts";
44
-
45
- const hostSlot = (): Record<symbol, unknown> => globalThis as unknown as Record<symbol, unknown>;
46
-
47
- const provider = (id: string): RowProvider => ({ id, category: "Cards", rows: () => [] });
48
-
49
- /** Every terminal write the process would show; nothing here may produce one. */
50
- const captureConsole = (): { writes: string[]; restore: () => void } => {
51
- const writes: string[] = [];
52
- const sink = (globalThis as unknown as { console: { log: unknown; warn: unknown; error: unknown } })
53
- .console;
54
- const original = { log: sink.log, warn: sink.warn, error: sink.error };
55
- const record =
56
- (method: string) =>
57
- (...args: unknown[]): void => {
58
- writes.push(`${method}: ${args.map((arg) => String(arg)).join(" ")}`);
59
- };
60
- sink.log = record("log");
61
- sink.warn = record("warn");
62
- sink.error = record("error");
63
- return {
64
- writes,
65
- restore: () => {
66
- sink.log = original.log;
67
- sink.warn = original.warn;
68
- sink.error = original.error;
69
- },
70
- };
71
- };
72
-
73
- let configHome: string;
74
- let previousConfigHome: string | undefined;
75
-
76
- test.before(() => {
77
- configHome = mkdtempSync(join(tmpdir(), "gsb-ready-"));
78
- previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
79
- process.env.GENTLE_PI_CONFIG_HOME = configHome;
80
- });
81
-
82
- test.after(() => {
83
- if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
84
- else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
85
- rmSync(configHome, { recursive: true, force: true });
86
- });
87
-
88
- test.beforeEach(() => {
89
- rmSync(join(configHome, "gentle-shell-bridge"), { recursive: true, force: true });
90
- resetBridgeApiForTests();
91
- resetNoticesForTests();
92
- resetBusForTests();
93
- resetSectionsForTests();
94
- resetStoreStateForTests();
95
- });
96
-
97
- // ---------------------------------------------------------------------------------------------
98
- // The event: name, payload, once per process
99
- // ---------------------------------------------------------------------------------------------
100
-
101
- test("the bridge announces itself with the schema, the major and the publishing release", () => {
102
- const bus = createFakeEvents();
103
- publishBridgeApi();
104
-
105
- assert.equal(announceBridgeReady(bus), true);
106
-
107
- assert.deepEqual(
108
- bus.emitted().map((entry) => entry.channel),
109
- [BRIDGE_READY_EVENT],
110
- "one event, on the channel plugins listen to",
111
- );
112
- assert.equal(BRIDGE_READY_EVENT, "gentle-shell-bridge:ready", "the name is the contract");
113
-
114
- const payload = bus.emitted()[0].payload as Record<string, unknown>;
115
- assert.deepEqual(payload, { schema: API_SCHEMA, version: API_VERSION, VERSION });
116
- assert.equal(payload.schema, "gentle-shell-bridge/api");
117
- assert.equal(payload.version, 1);
118
- assert.equal(payload.VERSION, "0.3.0");
119
- assert.ok(Object.isFrozen(payload), "a listener cannot rewrite the announcement in place");
120
- });
121
-
122
- test("the announcement happens once per process, however often this copy is asked", () => {
123
- const bus = createFakeEvents();
124
- publishBridgeApi();
125
-
126
- assert.equal(announceBridgeReady(bus), true);
127
- assert.equal(announceBridgeReady(bus), false, "the second ask is not an announcement");
128
- assert.equal(announceBridgeReady(bus), false);
129
-
130
- assert.equal(bus.emitted().length, 1, "one event for the process, not one per call");
131
- });
132
-
133
- test("two loaded copies of the package announce once between them", async () => {
134
- const first = createFakeEvents();
135
- const second = createFakeEvents();
136
- publishBridgeApi();
137
-
138
- // Pi loads every extension with `moduleCache: false`, so the package really exists twice.
139
- const copyPath = new URL("../src/api.ts?second-copy", import.meta.url).href;
140
- const copy = (await import(copyPath)) as typeof import("../src/api.ts");
141
-
142
- assert.equal(announceBridgeReady(first), true, "the copy that owns the slot announces");
143
- assert.equal(copy.announceBridgeReady(second), false, "and the other one stays quiet");
144
- assert.equal(first.emitted().length, 1);
145
- assert.deepEqual(second.emitted(), [], "no second announcement, on any bus");
146
- });
147
-
148
- test("an incompatible slot is never announced", () => {
149
- const bus = createFakeEvents();
150
- const incompatible = { schema: "someone-else/api", version: 1 };
151
- hostSlot()[BRIDGE_API_KEY] = incompatible;
152
-
153
- publishBridgeApi();
154
- assert.equal(announceBridgeReady(bus), false);
155
-
156
- assert.deepEqual(bus.emitted(), [], "no plugin is told to look at a slot it cannot use");
157
- assert.equal(hostSlot()[BRIDGE_API_KEY], incompatible, "and the slot is left exactly as it was");
158
- });
159
-
160
- test("a Pi without an event bus is skipped silently: the symbol still works", () => {
161
- publishBridgeApi();
162
- const captured = captureConsole();
163
- let announced: boolean | undefined;
164
- try {
165
- assert.doesNotThrow(() => {
166
- announced = announceBridgeReady(undefined);
167
- });
168
- } finally {
169
- captured.restore();
170
- }
171
-
172
- assert.equal(announced, false);
173
- assert.deepEqual(captured.writes, [], "nothing lands in the terminal");
174
- assert.equal(bridgeApi()?.VERSION, VERSION, "a plugin that reads the symbol is unaffected");
175
- });
176
-
177
- // ---------------------------------------------------------------------------------------------
178
- // onBridgeApi: immediate when it is here, subscribed when it is coming
179
- // ---------------------------------------------------------------------------------------------
180
-
181
- const apiHandler = (): { calls: BridgeApi[]; handler: (api: BridgeApi) => void } => {
182
- const calls: BridgeApi[] = [];
183
- return { calls, handler: (api) => calls.push(api) };
184
- };
185
-
186
- test("onBridgeApi calls the handler immediately when the API is already published, without listening", () => {
187
- const bus = createFakeEvents();
188
- const api = publishBridgeApi();
189
- const { calls, handler } = apiHandler();
190
-
191
- const dispose = onBridgeApi(handler, bus);
192
-
193
- assert.deepEqual(calls, [api], "the plugin is served in the same turn, before any event");
194
- assert.equal(bus.listenerCount(), 0, "and nothing is left subscribed: no listener leak");
195
- assert.doesNotThrow(() => dispose());
196
- assert.equal(calls.length, 1, "disposing after the fact cannot call the handler again");
197
- });
198
-
199
- test("onBridgeApi serves a plugin loaded first: one delivery when the announcement arrives", () => {
200
- const bus = createFakeEvents();
201
- const { calls, handler } = apiHandler();
202
-
203
- const dispose = onBridgeApi(handler, bus);
204
- assert.deepEqual(calls, [], "absent at subscription time: nothing to hand over yet");
205
- assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 1);
206
-
207
- const api = publishBridgeApi();
208
- assert.equal(announceBridgeReady(bus), true);
209
-
210
- assert.deepEqual(calls, [api], "the announcement delivered the published object, once");
211
- assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 0, "and the subscription cleaned itself up");
212
-
213
- assert.equal(announceBridgeReady(bus), false, "a second announcement is never emitted");
214
- assert.equal(calls.length, 1, "so the handler cannot run twice for one process");
215
-
216
- dispose();
217
- });
218
-
219
- test("an unsubscribe before the announcement means the handler never runs", () => {
220
- const bus = createFakeEvents();
221
- const { calls, handler } = apiHandler();
222
-
223
- const dispose = onBridgeApi(handler, bus);
224
- dispose();
225
-
226
- publishBridgeApi();
227
- announceBridgeReady(bus);
228
-
229
- assert.deepEqual(calls, [], "the plugin withdrew its interest and hears nothing");
230
- assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 0);
231
- });
232
-
233
- test("onBridgeApi without a bus does not pretend to wait: a safe disposer, no call", () => {
234
- const { calls, handler } = apiHandler();
235
-
236
- const dispose = onBridgeApi(handler);
237
-
238
- assert.deepEqual(calls, []);
239
- assert.doesNotThrow(() => dispose());
240
- assert.equal(bridgeApi(), undefined, "and nothing was published behind its back");
241
- });
242
-
243
- test("onBridgeApi ignores an announcement that is not backed by a published object", () => {
244
- const bus = createFakeEvents();
245
- const { calls, handler } = apiHandler();
246
- onBridgeApi(handler, bus);
247
-
248
- bus.emit(BRIDGE_READY_EVENT, { schema: "someone-else/api", version: 1 });
249
-
250
- assert.deepEqual(calls, [], "the symbol is the authority, not the payload");
251
- assert.equal(bus.listenerCount(BRIDGE_READY_EVENT), 1, "it keeps waiting for the real one");
252
- });
253
-
254
- test("a handler that throws cannot break the announcement for the next listener", () => {
255
- const bus = createFakeEvents();
256
- const { calls, handler } = apiHandler();
257
- const captured = captureConsole();
258
- try {
259
- onBridgeApi(() => {
260
- throw new Error("plugin exploded");
261
- }, bus);
262
- onBridgeApi(handler, bus);
263
- publishBridgeApi();
264
- assert.doesNotThrow(() => announceBridgeReady(bus));
265
- } finally {
266
- captured.restore();
267
- }
268
-
269
- assert.equal(calls.length, 1, "the second plugin was still served");
270
- assert.deepEqual(captured.writes, [], "and the failure was not printed to the terminal");
271
- });
272
-
273
- // ---------------------------------------------------------------------------------------------
274
- // Through the entry: the extension is the only place `pi` exists
275
- // ---------------------------------------------------------------------------------------------
276
-
277
- const bridgePi = (events: FakeEvents | undefined): BridgeExtensionApi => ({
278
- on() {},
279
- registerCommand() {},
280
- ...(events === undefined ? {} : { events }),
281
- });
282
-
283
- test("the extension entry announces through the Pi it was handed, once", () => {
284
- const bus = createFakeEvents();
285
-
286
- gentleShellBridge(bridgePi(bus));
287
- gentleShellBridge(bridgePi(bus));
288
-
289
- assert.deepEqual(
290
- bus.emitted().map((entry) => entry.channel),
291
- [BRIDGE_READY_EVENT],
292
- "loading the package makes the process hear about it exactly once",
293
- );
294
- });
295
-
296
- test("the extension entry still publishes when it is handed no event bus", () => {
297
- const captured = captureConsole();
298
- try {
299
- assert.doesNotThrow(() => gentleShellBridge(bridgePi(undefined)));
300
- } finally {
301
- captured.restore();
302
- }
303
-
304
- assert.deepEqual(captured.writes, []);
305
- const api = bridgeApi();
306
- assert.ok(api !== undefined, "the symbol is the handshake, with or without a bus");
307
- api.registerRows(provider("after-entry"));
308
- assert.deepEqual(snapshotProviders().map((registered) => registered.id), ["after-entry"]);
309
- });
package/test/bus.test.ts DELETED
@@ -1,165 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
-
4
- import type { RowProvider } from "../src/types.ts";
5
-
6
- const withId = (id: string, category = "Layout"): RowProvider => ({
7
- id,
8
- category,
9
- rows: () => [],
10
- });
11
-
12
- // One shared bus for the whole file; every test resets it through the internal seam.
13
- // `src/index.ts` deliberately does not re-export the seam (asserted below).
14
- const bus = await import("../src/bus.ts");
15
- const { registerRows, snapshotProviders, resetBusForTests } = bus;
16
-
17
- const ids = () => snapshotProviders().map((provider: RowProvider) => provider.id);
18
-
19
- test.beforeEach(() => {
20
- resetBusForTests();
21
- });
22
-
23
- test("providers are returned in registration order", () => {
24
- registerRows(withId("a"));
25
- registerRows(withId("b"));
26
- registerRows(withId("c"));
27
- assert.deepEqual(ids(), ["a", "b", "c"]);
28
- });
29
-
30
- test("an empty bus returns an empty snapshot", () => {
31
- assert.deepEqual(snapshotProviders(), []);
32
- });
33
-
34
- test("the disposer removes only its own provider", () => {
35
- registerRows(withId("a"));
36
- const disposeB = registerRows(withId("b"));
37
- registerRows(withId("c"));
38
-
39
- disposeB();
40
-
41
- assert.deepEqual(ids(), ["a", "c"]);
42
- });
43
-
44
- test("the disposer keeps the remaining providers' relative order", () => {
45
- const disposeA = registerRows(withId("a"));
46
- registerRows(withId("b"));
47
- registerRows(withId("c"));
48
-
49
- disposeA();
50
-
51
- assert.deepEqual(ids(), ["b", "c"]);
52
- });
53
-
54
- test("registering an existing id replaces in place and keeps its position", () => {
55
- registerRows(withId("a", "Layout"));
56
- registerRows(withId("b", "Banner"));
57
- registerRows(withId("c", "Cards"));
58
-
59
- const replacement = withId("b", "Layout");
60
- registerRows(replacement);
61
-
62
- assert.deepEqual(ids(), ["a", "b", "c"], "one entry, original position");
63
- assert.equal(snapshotProviders().length, 3);
64
- assert.equal(snapshotProviders()[1], replacement, "the new provider object is the live one");
65
- assert.equal(snapshotProviders()[1].category, "Layout");
66
- });
67
-
68
- test("the replaced provider's disposer does not remove its replacement", () => {
69
- const disposeFirst = registerRows(withId("a"));
70
- const replacement = withId("a", "Banner");
71
- registerRows(replacement);
72
-
73
- disposeFirst();
74
-
75
- assert.deepEqual(ids(), ["a"], "the stale disposer is a no-op, not a removal of the replacement");
76
- assert.equal(snapshotProviders()[0], replacement);
77
- });
78
-
79
- test("a disposer called twice is a no-op the second time", () => {
80
- registerRows(withId("a"));
81
- const disposeB = registerRows(withId("b"));
82
- registerRows(withId("c"));
83
-
84
- disposeB();
85
- disposeB();
86
-
87
- assert.deepEqual(ids(), ["a", "c"]);
88
- });
89
-
90
- test("a disposer after a reset cannot remove a later provider", () => {
91
- const disposeA = registerRows(withId("a"));
92
- resetBusForTests();
93
- registerRows(withId("a"));
94
-
95
- disposeA();
96
-
97
- assert.deepEqual(ids(), ["a"], "the pre-reset disposer must not touch the new registration");
98
- });
99
-
100
- test("a snapshot is a stable copy: registering during iteration does not change it", () => {
101
- registerRows(withId("a"));
102
-
103
- const during = snapshotProviders();
104
- const seen: string[] = [];
105
- for (const provider of during) {
106
- seen.push(provider.id);
107
- registerRows(withId("late"));
108
- }
109
-
110
- assert.deepEqual(seen, ["a"]);
111
- assert.deepEqual(during.map((provider: RowProvider) => provider.id), ["a"]);
112
- assert.deepEqual(ids(), ["a", "late"]);
113
- });
114
-
115
- test("two independent module copies in one process share one bus", async () => {
116
- // Node's ESM loader keys modules by resolved URL, so a query string yields a second instance
117
- // of the same source with its own module scope — the closest local model of jiti's
118
- // `moduleCache: false` loading of two package copies. The specifier is computed so `tsc`
119
- // types it as a dynamic import instead of trying to resolve a URL query on disk.
120
- const secondCopySpecifier = "../src/bus.ts?copy=2";
121
- const copyTwo: typeof bus = await import(secondCopySpecifier);
122
- assert.notEqual(copyTwo, bus, "the second import must be a distinct module instance");
123
-
124
- resetBusForTests();
125
- registerRows(withId("from-copy-one"));
126
- copyTwo.registerRows(withId("from-copy-two"));
127
-
128
- assert.deepEqual(ids(), ["from-copy-one", "from-copy-two"]);
129
- assert.deepEqual(copyTwo.snapshotProviders().map((p: RowProvider) => p.id), [
130
- "from-copy-one",
131
- "from-copy-two",
132
- ]);
133
- });
134
-
135
- test("a disposer from another module copy removes its provider", async () => {
136
- const secondCopySpecifier = "../src/bus.ts?copy=2";
137
- const copyTwo: typeof bus = await import(secondCopySpecifier);
138
-
139
- const dispose = copyTwo.registerRows(withId("shared"));
140
- registerRows(withId("local"));
141
- assert.deepEqual(ids(), ["shared", "local"]);
142
-
143
- dispose();
144
-
145
- assert.deepEqual(ids(), ["local"], "the visible registry is the process-global one");
146
- });
147
-
148
- test("the bus lives on globalThis behind a stable Symbol.for key", () => {
149
- registerRows(withId("a"));
150
- const slot = (globalThis as unknown as Record<symbol, unknown>)[
151
- Symbol.for("gentle-shell-bridge.providers")
152
- ];
153
- assert.ok(slot !== undefined, "the registration state must sit on globalThis");
154
- assert.notEqual(
155
- Symbol.for("gentle-shell-bridge.providers"),
156
- Symbol.for("gentle-pi.customize-row-providers"),
157
- "the bridge bus must not reuse the hook registry key",
158
- );
159
- });
160
-
161
- test("the public entry does not expose the test-only reset seam", async () => {
162
- const entry = await import("../src/index.ts");
163
- assert.equal("resetBusForTests" in entry, false);
164
- assert.equal("registerRows" in entry, true);
165
- });