@huaqiu/dsh-eda-host 0.3.20 → 0.3.22

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/cordis.patch.yml CHANGED
@@ -1,8 +1,11 @@
1
1
  # DSH bundle patch: inserts the Huaqiu EDA host netlist tool plugin.
2
- # The entry inject must match the node-half module contract (`src/index.ts`
3
- # export const inject). The tool's apply() reads the `tools` service; the
4
- # hq-edge supervisor delivers the base URL as overlay config.
2
+ # The entry `inject` MUST match the node-half module contract (`src/index.ts`
3
+ # `export const inject`): `hqEdge` + `tools`. `hqEdge` is REQUIRED — the
4
+ # edge-bridge (HOST) provides it and its `baseUrl` is the loopback HQ Edge
5
+ # endpoint the netlist tools fetch from (`createNodeHqEdge(upstreamRoot,…)` →
6
+ # `get baseUrl()`). Omitting `hqEdge` from inject makes `apply()`'s
7
+ # `ctx.hqEdge` access throw `cannot get property "hqEdge" without inject`.
5
8
  - insert:
6
9
  - id: huaqiu-dsh-eda-host
7
10
  name: '@huaqiu/dsh-eda-host'
8
- inject: ['tools']
11
+ inject: ['hqEdge', 'tools']
package/lib/index.d.mts CHANGED
@@ -89,20 +89,58 @@ interface EdaHostClient {
89
89
  //#region src/index.d.ts
90
90
  /** Plugin id — matches package.json. */
91
91
  declare const name = "@huaqiu/dsh-eda-host";
92
- /** Cordis services this half depends on. */
93
- declare const inject: readonly ["tools"];
92
+ /**
93
+ * Cordis services this half depends on.
94
+ *
95
+ * `hqEdge` is REQUIRED: the edge-bridge (the HOST) provides the node-side
96
+ * `hqEdge` service whose `baseUrl` is the loopback HQ Edge endpoint
97
+ * (`createNodeHqEdge(upstreamRoot, …)` → `get baseUrl()`). Without it the
98
+ * plugin cannot reach hq-edge at all — by design it cannot work standalone
99
+ * (the user-confirmed contract is "eda-host cannot work without hqEdge").
100
+ * Declaring it here is what lets `apply()` read `ctx.hqEdge` without Cordis
101
+ * throwing `cannot get property "hqEdge" without inject` (its context proxy
102
+ * walks the fiber tree and throws at the root fiber when the service was
103
+ * never injected into this plugin's fiber).
104
+ *
105
+ * `tools` is the DSH node runtime tool registry used to register the netlist
106
+ * tools.
107
+ */
108
+ declare const inject: readonly ["hqEdge", "tools"];
94
109
  declare module '@deepseek-ai/cordis' {
95
110
  interface Context {
96
111
  edaHost: EdaHostClient;
112
+ /**
113
+ * Provided by the hq-edge `edge-bridge` plugin (the HOST). `baseUrl` is the
114
+ * loopback HQ Edge endpoint, e.g. "http://localhost:18080". We read it
115
+ * lazily so this plugin does not hard-depend on the bridge and still works
116
+ * in standalone DSH installs (where the service is absent).
117
+ */
118
+ /**
119
+ * Provided by the hq-edge `edge-bridge` plugin (the HOST). `baseUrl` is the
120
+ * loopback HQ Edge endpoint, e.g. "http://localhost:18080". This is a
121
+ * REQUIRED inject (see `export const inject` above): `apply()` reads
122
+ * `ctx.hqEdge` to resolve the endpoint, so the service must be present or
123
+ * Cordis throws `cannot get property "hqEdge" without inject`. When present
124
+ * but its `baseUrl` is empty, the netlist tools degrade to a clear
125
+ * FAILED_PRECONDITION rather than throwing at load time.
126
+ */
127
+ hqEdge: {
128
+ baseUrl?: string;
129
+ };
97
130
  }
98
131
  }
99
132
  /**
100
133
  * Host plugin body — provide `edaHost` and register the three netlist tools.
101
134
  *
102
- * When no host base URL is configured, tools return ok:false with
103
- * error.kind "FAILED_PRECONDITION" instead of throwing at load time: the
104
- * plugin can be installed in standalone DSH where hq-edge is absent, and the
105
- * tools degrade to a clear semantic message.
135
+ * The HQ Edge endpoint is resolved **lazily at request time** (see
136
+ * `getHqEdgeBaseUrl`): the edge-bridge plugin provides `ctx.hqEdge.baseUrl`,
137
+ * which wins over the static `config.hqEdgeBaseUrl` / `HQ_EDGE_BASE_URL` env
138
+ * fallback. This matches how `@huaqiu/dsh-artifacts` and `@huaqiu/dsh-tool-
139
+ * symbol-footprint` reach hq-edge, and means the URL is correct even when this
140
+ * plugin is applied before the bridge. When no host URL is available at call
141
+ * time, the tools degrade to a clear FAILED_PRECONDITION instead of throwing at
142
+ * load time — so the plugin still installs in standalone DSH where hq-edge is
143
+ * absent.
106
144
  *
107
145
  * @param ctx - real cordis context (node side).
108
146
  * @returns disposer — unregisters the tools on plugin dispose.
package/lib/index.mjs CHANGED
@@ -51,7 +51,12 @@ function statusToKind(status) {
51
51
  function createEdaHostClient(config, deps = {}) {
52
52
  const fetchImpl = deps.fetchImpl ?? globalThis.fetch;
53
53
  async function fetchScope(scope) {
54
- const url = netlistUrlOf(config, scope);
54
+ const baseUrl = deps.baseUrlResolver?.()?.trim() ?? config.hqEdgeBaseUrl?.trim() ?? "";
55
+ if (baseUrl.length === 0) throw new NetlistError("FAILED_PRECONDITION", "eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — netlist tools require the hq-edge EDA host bridge.");
56
+ const url = netlistUrlOf({
57
+ ...config,
58
+ hqEdgeBaseUrl: baseUrl
59
+ }, scope);
55
60
  let response;
56
61
  try {
57
62
  response = await fetchImpl(url, {
@@ -159,16 +164,36 @@ function createNetListTools(env) {
159
164
  //#region src/index.ts
160
165
  /** Plugin id — matches package.json. */
161
166
  const name = "@huaqiu/dsh-eda-host";
162
- /** Cordis services this half depends on. */
163
- const inject = ["tools"];
167
+ /**
168
+ * Cordis services this half depends on.
169
+ *
170
+ * `hqEdge` is REQUIRED: the edge-bridge (the HOST) provides the node-side
171
+ * `hqEdge` service whose `baseUrl` is the loopback HQ Edge endpoint
172
+ * (`createNodeHqEdge(upstreamRoot, …)` → `get baseUrl()`). Without it the
173
+ * plugin cannot reach hq-edge at all — by design it cannot work standalone
174
+ * (the user-confirmed contract is "eda-host cannot work without hqEdge").
175
+ * Declaring it here is what lets `apply()` read `ctx.hqEdge` without Cordis
176
+ * throwing `cannot get property "hqEdge" without inject` (its context proxy
177
+ * walks the fiber tree and throws at the root fiber when the service was
178
+ * never injected into this plugin's fiber).
179
+ *
180
+ * `tools` is the DSH node runtime tool registry used to register the netlist
181
+ * tools.
182
+ */
183
+ const inject = ["hqEdge", "tools"];
164
184
  const log = getLogger("dsh-eda-host");
165
185
  /**
166
186
  * Host plugin body — provide `edaHost` and register the three netlist tools.
167
187
  *
168
- * When no host base URL is configured, tools return ok:false with
169
- * error.kind "FAILED_PRECONDITION" instead of throwing at load time: the
170
- * plugin can be installed in standalone DSH where hq-edge is absent, and the
171
- * tools degrade to a clear semantic message.
188
+ * The HQ Edge endpoint is resolved **lazily at request time** (see
189
+ * `getHqEdgeBaseUrl`): the edge-bridge plugin provides `ctx.hqEdge.baseUrl`,
190
+ * which wins over the static `config.hqEdgeBaseUrl` / `HQ_EDGE_BASE_URL` env
191
+ * fallback. This matches how `@huaqiu/dsh-artifacts` and `@huaqiu/dsh-tool-
192
+ * symbol-footprint` reach hq-edge, and means the URL is correct even when this
193
+ * plugin is applied before the bridge. When no host URL is available at call
194
+ * time, the tools degrade to a clear FAILED_PRECONDITION instead of throwing at
195
+ * load time — so the plugin still installs in standalone DSH where hq-edge is
196
+ * absent.
172
197
  *
173
198
  * @param ctx - real cordis context (node side).
174
199
  * @returns disposer — unregisters the tools on plugin dispose.
@@ -176,31 +201,23 @@ const log = getLogger("dsh-eda-host");
176
201
  function apply(ctx, config = {}) {
177
202
  if (!ctx.tools || typeof ctx.tools.register !== "function") throw new Error("@huaqiu/dsh-eda-host requires the DSH `tools` service (ctx.tools.register).");
178
203
  const resolved = resolveEdaHostConfig(config);
204
+ const getHqEdgeBaseUrl = () => {
205
+ const hq = ctx.hqEdge;
206
+ return hq?.baseUrl && hq.baseUrl.trim().length > 0 ? hq.baseUrl : void 0;
207
+ };
179
208
  log.info("applying dsh-eda-host node half", {
180
- hasHost: hasHost(resolved),
181
- hqEdgeBaseUrl: resolved.hqEdgeBaseUrl ?? null,
209
+ hasConfigHost: hasHost(resolved),
210
+ hqEdgeBaseUrlFromConfig: resolved.hqEdgeBaseUrl ?? null,
182
211
  netlistPathPrefix: resolved.netlistPathPrefix
183
212
  });
184
- let client;
185
- if (hasHost(resolved)) client = createEdaHostClient(resolved);
186
- else client = {
187
- getProjectNetlist: async () => {
188
- throw new NetlistError("FAILED_PRECONDITION", "eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — netlist tools require the hq-edge EDA host bridge.");
189
- },
190
- getSelectionNetlist: async () => {
191
- throw new NetlistError("FAILED_PRECONDITION", "eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — netlist tools require the hq-edge EDA host bridge.");
192
- },
193
- getActivePageNetlist: async () => {
194
- throw new NetlistError("FAILED_PRECONDITION", "eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — netlist tools require the hq-edge EDA host bridge.");
195
- }
196
- };
213
+ const client = createEdaHostClient(resolved, { baseUrlResolver: getHqEdgeBaseUrl });
197
214
  ctx.effect(() => ctx.provide("edaHost", client));
198
215
  const tools = createNetListTools({ client });
199
216
  const disposers = [];
200
217
  for (const tool of tools) disposers.push(ctx.tools.register(tool));
201
218
  log.info("dsh-eda-host node half ready", {
202
219
  tools: 3,
203
- hostMode: hasHost(resolved)
220
+ configHostMode: hasHost(resolved)
204
221
  });
205
222
  return () => {
206
223
  for (const dispose of disposers) try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-eda-host",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "type": "module",
5
5
  "main": "./lib/index.mjs",
6
6
  "types": "./lib/index.d.mts",
@@ -22,7 +22,7 @@
22
22
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.0"
23
23
  },
24
24
  "dependencies": {
25
- "@huaqiu/dsh-plugin-log": "0.3.20"
25
+ "@huaqiu/dsh-plugin-log": "0.3.22"
26
26
  },
27
27
  "files": [
28
28
  "lib",
package/src/client.ts CHANGED
@@ -17,6 +17,16 @@ import {
17
17
 
18
18
  export interface EdaHostClientDeps {
19
19
  fetchImpl?: typeof fetch
20
+ /**
21
+ * Optional late-bound resolver for the HQ Edge base URL. When supplied it is
22
+ * consulted on EVERY request and takes priority over the static `config`
23
+ * value. This lets the node half read the endpoint from the `ctx.hqEdge`
24
+ * service the edge-bridge plugin provides — the same pattern `@huaqiu/
25
+ * dsh-artifacts` and `@huaqiu/dsh-tool-symbol-footprint` use — so the URL is
26
+ * picked up even if this plugin is applied before the bridge, and stays
27
+ * correct in standalone installs where no host is present.
28
+ */
29
+ baseUrlResolver?: () => string | undefined
20
30
  }
21
31
 
22
32
  export interface EdaHostClient {
@@ -43,7 +53,20 @@ export function createEdaHostClient(
43
53
  const fetchImpl = deps.fetchImpl ?? globalThis.fetch
44
54
 
45
55
  async function fetchScope(scope: NetlistScope): Promise<SchematicNetlist> {
46
- const url = netlistUrlOf(config, scope)
56
+ // Resolve the host endpoint per request. A resolver (ctx.hqEdge) wins over
57
+ // the static config/env value; if neither yields a URL we degrade to the
58
+ // same clear FAILED_PRECONDITION the standalone install path uses.
59
+ const baseUrl = deps.baseUrlResolver?.()?.trim()
60
+ ?? config.hqEdgeBaseUrl?.trim()
61
+ ?? ''
62
+ if (baseUrl.length === 0) {
63
+ throw new NetlistError(
64
+ 'FAILED_PRECONDITION',
65
+ 'eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — ' +
66
+ 'netlist tools require the hq-edge EDA host bridge.',
67
+ )
68
+ }
69
+ const url = netlistUrlOf({ ...config, hqEdgeBaseUrl: baseUrl }, scope)
47
70
 
48
71
  let response: Response
49
72
  try {
package/src/index.ts CHANGED
@@ -13,9 +13,12 @@
13
13
  * This plugin owns DSH integration only: it translates tool calls into hq-edge
14
14
  * requests and returns the semantic `SchematicNetlist`. It contains no
15
15
  * KiCad-specific logic, no schematic parsing, and no host IPC. The plugin is
16
- * self-contained — no `@hqedge/*` dependency; the base URL is delivered by the
17
- * hq-edge supervisor as overlay config (`hqEdgeBaseUrl`), with
18
- * `HQ_EDGE_BASE_URL` as env fallback (same convention as `@huaqiu/dsh-auth`).
16
+ * self-contained — no `@hqedge/*` dependency. The base URL is resolved at
17
+ * request time in this order: (1) the `ctx.hqEdge.baseUrl` service provided by
18
+ * the edge-bridge HOST (the loopback HQ Edge endpoint), then (2) the
19
+ * supervisor's overlay config (`hqEdgeBaseUrl`), then (3) `HQ_EDGE_BASE_URL`
20
+ * env fallback (same convention as `@huaqiu/dsh-auth`). `hqEdge` is a REQUIRED
21
+ * inject — the plugin cannot reach hq-edge without the bridge.
19
22
  *
20
23
  * @module @huaqiu/dsh-eda-host
21
24
  */
@@ -24,13 +27,27 @@ import { getLogger } from '@huaqiu/dsh-plugin-log'
24
27
  import { createEdaHostClient, type EdaHostClient } from './client.js'
25
28
  import { hasHost, resolveEdaHostConfig, type EdaHostConfig } from './config.js'
26
29
  import { createNetListTools } from './tools.js'
27
- import { NetlistError } from './types.js'
28
30
 
29
31
  /** Plugin id — matches package.json. */
30
32
  export const name = '@huaqiu/dsh-eda-host'
31
33
 
32
- /** Cordis services this half depends on. */
33
- export const inject = ['tools'] as const
34
+ /**
35
+ * Cordis services this half depends on.
36
+ *
37
+ * `hqEdge` is REQUIRED: the edge-bridge (the HOST) provides the node-side
38
+ * `hqEdge` service whose `baseUrl` is the loopback HQ Edge endpoint
39
+ * (`createNodeHqEdge(upstreamRoot, …)` → `get baseUrl()`). Without it the
40
+ * plugin cannot reach hq-edge at all — by design it cannot work standalone
41
+ * (the user-confirmed contract is "eda-host cannot work without hqEdge").
42
+ * Declaring it here is what lets `apply()` read `ctx.hqEdge` without Cordis
43
+ * throwing `cannot get property "hqEdge" without inject` (its context proxy
44
+ * walks the fiber tree and throws at the root fiber when the service was
45
+ * never injected into this plugin's fiber).
46
+ *
47
+ * `tools` is the DSH node runtime tool registry used to register the netlist
48
+ * tools.
49
+ */
50
+ export const inject = ['hqEdge', 'tools'] as const
34
51
 
35
52
  export type { EdaHostConfig } from './config.js'
36
53
  export type { EdaHostClient } from './client.js'
@@ -48,6 +65,22 @@ export { NetlistError } from './types.js'
48
65
  declare module '@deepseek-ai/cordis' {
49
66
  interface Context {
50
67
  edaHost: EdaHostClient
68
+ /**
69
+ * Provided by the hq-edge `edge-bridge` plugin (the HOST). `baseUrl` is the
70
+ * loopback HQ Edge endpoint, e.g. "http://localhost:18080". We read it
71
+ * lazily so this plugin does not hard-depend on the bridge and still works
72
+ * in standalone DSH installs (where the service is absent).
73
+ */
74
+ /**
75
+ * Provided by the hq-edge `edge-bridge` plugin (the HOST). `baseUrl` is the
76
+ * loopback HQ Edge endpoint, e.g. "http://localhost:18080". This is a
77
+ * REQUIRED inject (see `export const inject` above): `apply()` reads
78
+ * `ctx.hqEdge` to resolve the endpoint, so the service must be present or
79
+ * Cordis throws `cannot get property "hqEdge" without inject`. When present
80
+ * but its `baseUrl` is empty, the netlist tools degrade to a clear
81
+ * FAILED_PRECONDITION rather than throwing at load time.
82
+ */
83
+ hqEdge: { baseUrl?: string }
51
84
  }
52
85
  }
53
86
 
@@ -58,10 +91,15 @@ const log = getLogger(COMPONENT)
58
91
  /**
59
92
  * Host plugin body — provide `edaHost` and register the three netlist tools.
60
93
  *
61
- * When no host base URL is configured, tools return ok:false with
62
- * error.kind "FAILED_PRECONDITION" instead of throwing at load time: the
63
- * plugin can be installed in standalone DSH where hq-edge is absent, and the
64
- * tools degrade to a clear semantic message.
94
+ * The HQ Edge endpoint is resolved **lazily at request time** (see
95
+ * `getHqEdgeBaseUrl`): the edge-bridge plugin provides `ctx.hqEdge.baseUrl`,
96
+ * which wins over the static `config.hqEdgeBaseUrl` / `HQ_EDGE_BASE_URL` env
97
+ * fallback. This matches how `@huaqiu/dsh-artifacts` and `@huaqiu/dsh-tool-
98
+ * symbol-footprint` reach hq-edge, and means the URL is correct even when this
99
+ * plugin is applied before the bridge. When no host URL is available at call
100
+ * time, the tools degrade to a clear FAILED_PRECONDITION instead of throwing at
101
+ * load time — so the plugin still installs in standalone DSH where hq-edge is
102
+ * absent.
65
103
  *
66
104
  * @param ctx - real cordis context (node side).
67
105
  * @returns disposer — unregisters the tools on plugin dispose.
@@ -74,44 +112,21 @@ export function apply(ctx: Context, config: Partial<EdaHostConfig> = {}): () =>
74
112
  }
75
113
 
76
114
  const resolved = resolveEdaHostConfig(config)
115
+
116
+ // Late-bound host endpoint: prefer the edge-bridge service, then the overlay
117
+ // config / env value. Consulted on every request (see client.ts).
118
+ const getHqEdgeBaseUrl = (): string | undefined => {
119
+ const hq = ctx.hqEdge
120
+ return hq?.baseUrl && hq.baseUrl.trim().length > 0 ? hq.baseUrl : undefined
121
+ }
122
+
77
123
  log.info('applying dsh-eda-host node half', {
78
- hasHost: hasHost(resolved),
79
- hqEdgeBaseUrl: resolved.hqEdgeBaseUrl ?? null,
124
+ hasConfigHost: hasHost(resolved),
125
+ hqEdgeBaseUrlFromConfig: resolved.hqEdgeBaseUrl ?? null,
80
126
  netlistPathPrefix: resolved.netlistPathPrefix,
81
127
  })
82
128
 
83
- let client: EdaHostClient
84
-
85
- if (hasHost(resolved)) {
86
- client = createEdaHostClient(resolved)
87
- } else {
88
- // Standalone install (no hq-edge supervisor): every scope degrades to
89
- // FAILED_PRECONDITION with a clear message.
90
- const unavailable: EdaHostClient = {
91
- getProjectNetlist: async () => {
92
- throw new NetlistError(
93
- 'FAILED_PRECONDITION',
94
- 'eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — ' +
95
- 'netlist tools require the hq-edge EDA host bridge.',
96
- )
97
- },
98
- getSelectionNetlist: async () => {
99
- throw new NetlistError(
100
- 'FAILED_PRECONDITION',
101
- 'eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — ' +
102
- 'netlist tools require the hq-edge EDA host bridge.',
103
- )
104
- },
105
- getActivePageNetlist: async () => {
106
- throw new NetlistError(
107
- 'FAILED_PRECONDITION',
108
- 'eda-host: no hq-edge base URL configured (hqEdgeBaseUrl / HQ_EDGE_BASE_URL) — ' +
109
- 'netlist tools require the hq-edge EDA host bridge.',
110
- )
111
- },
112
- }
113
- client = unavailable
114
- }
129
+ const client = createEdaHostClient(resolved, { baseUrlResolver: getHqEdgeBaseUrl })
115
130
 
116
131
  ctx.effect(() => ctx.provide('edaHost', client))
117
132
 
@@ -121,7 +136,7 @@ export function apply(ctx: Context, config: Partial<EdaHostConfig> = {}): () =>
121
136
  disposers.push(ctx.tools.register(tool))
122
137
  }
123
138
 
124
- log.info('dsh-eda-host node half ready', { tools: 3, hostMode: hasHost(resolved) })
139
+ log.info('dsh-eda-host node half ready', { tools: 3, configHostMode: hasHost(resolved) })
125
140
 
126
141
  return () => {
127
142
  for (const dispose of disposers) {