@cueloop/client 0.1.0-alpha.18 → 0.1.0-alpha.19

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.
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@cueloop/client",
3
- "version": "0.1.0-alpha.18",
3
+ "version": "0.1.0-alpha.19",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.ts"
8
8
  },
9
9
  "dependencies": {
10
- "@cueloop/daemon": "0.1.0-alpha.18",
11
- "@cueloop/extension-api": "0.1.0-alpha.18",
12
- "@cueloop/integration-obsidian": "0.1.0-alpha.18",
13
- "@cueloop/schema": "0.1.0-alpha.18",
10
+ "@cueloop/daemon": "0.1.0-alpha.19",
11
+ "@cueloop/extension-api": "0.1.0-alpha.19",
12
+ "@cueloop/integration-obsidian": "0.1.0-alpha.19",
13
+ "@cueloop/schema": "0.1.0-alpha.19",
14
14
  "@opentui/core": "^0.5.1",
15
15
  "@opentui/keymap": "0.5.1",
16
16
  "@opentui/react": "^0.5.1",
@@ -0,0 +1,29 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { OBSIDIAN_DEFAULTS } from "@cueloop/integration-obsidian";
3
+ import { loadBundledExporters } from "./integrations";
4
+ import type { IntegrationsConfig } from "./config";
5
+
6
+ function integrations(overrides: Partial<typeof OBSIDIAN_DEFAULTS> = {}): IntegrationsConfig {
7
+ return { obsidian: { ...OBSIDIAN_DEFAULTS, ...overrides } };
8
+ }
9
+
10
+ describe("loadBundledExporters", () => {
11
+ test("composes the obsidian integration into one named exporter", async () => {
12
+ const exporters = await loadBundledExporters(integrations());
13
+ expect(exporters.map((exporter) => exporter.name)).toEqual(["obsidian"]);
14
+ });
15
+
16
+ test("runsOn mirrors the configured export policy", async () => {
17
+ const onApprove = (await loadBundledExporters(integrations({ exportOn: "approve" })))[0]!;
18
+ expect(onApprove.runsOn("approve")).toBeTrue();
19
+ expect(onApprove.runsOn("request_changes")).toBeFalse();
20
+
21
+ const onResolve = (await loadBundledExporters(integrations({ exportOn: "resolve" })))[0]!;
22
+ expect(onResolve.runsOn("approve")).toBeTrue();
23
+ expect(onResolve.runsOn("request_changes")).toBeTrue();
24
+
25
+ const manual = (await loadBundledExporters(integrations({ exportOn: "manual" })))[0]!;
26
+ expect(manual.runsOn("approve")).toBeFalse();
27
+ expect(manual.runsOn("request_changes")).toBeFalse();
28
+ });
29
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Bundled integration wiring: composes the configured integrations into
3
+ * generic exporters plus their run policy, so the session controller depends
4
+ * only on the extension seam, never on an integration's own config type. This
5
+ * and config.ts are the only client modules that name a concrete integration.
6
+ */
7
+
8
+ import { Registry, type Exporter } from "@cueloop/extension-api";
9
+ import { createObsidianExtension, shouldExport } from "@cueloop/integration-obsidian";
10
+ import type { VerdictKind } from "@cueloop/schema";
11
+ import type { IntegrationsConfig } from "./config";
12
+
13
+ /** An exporter plus the policy deciding which verdicts trigger it. */
14
+ export interface BundledExporter {
15
+ name: string;
16
+ run: Exporter;
17
+ runsOn(verdict: VerdictKind): boolean;
18
+ }
19
+
20
+ /** Load the bundled integrations from config as generic exporters. */
21
+ export async function loadBundledExporters(integrations: IntegrationsConfig): Promise<BundledExporter[]> {
22
+ const registry = new Registry();
23
+ const obsidian = integrations.obsidian;
24
+ const record = await registry.load("obsidian", createObsidianExtension(obsidian));
25
+ const run = record.exporters.get("obsidian");
26
+ if (!run) return [];
27
+ return [{ name: "obsidian", run, runsOn: (verdict) => shouldExport(obsidian.exportOn, verdict) }];
28
+ }
@@ -22,8 +22,7 @@ import {
22
22
  type ReviewSession,
23
23
  type VerdictKind,
24
24
  } from "@cueloop/schema";
25
- import { Registry, type Exporter } from "@cueloop/extension-api";
26
- import { createObsidianExtension, shouldExport, type ObsidianConfig } from "@cueloop/integration-obsidian";
25
+ import { loadBundledExporters, type BundledExporter } from "./integrations";
27
26
  import { buildDisplay, nextWorkBlock, type DisplayBlock } from "./view-plan";
28
27
  import { diffRowAnchor, diffRows, type DiffRow } from "./view-diff";
29
28
  import { firstUnviewedIndex, walkFiles, type WalkFile } from "./walk";
@@ -141,8 +140,7 @@ class Controller implements ReviewController {
141
140
  };
142
141
  private listeners = new Set<() => void>();
143
142
  private autoClose: AutoClose = "off";
144
- private obsidian: ObsidianConfig | null = null;
145
- private exporters = new Map<string, Exporter>();
143
+ private exporters: BundledExporter[] = [];
146
144
  private readonly clock: Clock;
147
145
  private countdown: TimerHandle | undefined;
148
146
  /** Projections keyed by session identity so renders reuse one computation. */
@@ -207,11 +205,8 @@ class Controller implements ReviewController {
207
205
 
208
206
  applyConfig(config: CueloopConfig): void {
209
207
  this.autoClose = config.ui.autoClose;
210
- this.obsidian = config.integrations.obsidian;
211
- // bundled integrations register through the public extension API
212
- const registry = new Registry();
213
- void registry.load("obsidian", createObsidianExtension(config.integrations.obsidian)).then((record) => {
214
- this.exporters = record.exporters;
208
+ void loadBundledExporters(config.integrations).then((exporters) => {
209
+ this.exporters = exporters;
215
210
  });
216
211
  }
217
212
 
@@ -448,11 +443,10 @@ class Controller implements ReviewController {
448
443
  // The completion overlay heading already states the verdict, so the
449
444
  // status line stays empty here - only export/error messages fill it.
450
445
  this.update({ session: resolved, status: "" });
451
- // notes-vault export: guarded by config, default is manual (no-op)
452
- const obsidian = this.obsidian;
453
- const exporter = this.exporters.get("obsidian");
454
- if (obsidian && exporter && shouldExport(obsidian.exportOn, verdict)) {
455
- void exporter(resolved).then((exportResult) => {
446
+ // notes-vault export: guarded by each exporter's policy (default manual = no-op)
447
+ for (const exporter of this.exporters) {
448
+ if (!exporter.runsOn(verdict)) continue;
449
+ void exporter.run(resolved).then((exportResult) => {
456
450
  this.setStatus(exportResult.success && exportResult.path ? `exported to ${exportResult.path}` : `export failed: ${exportResult.error ?? "unknown"}`);
457
451
  });
458
452
  }