@huaqiu/dsh-eda-host 0.3.22 → 0.3.25
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/README.md +61 -6
- package/lib/index.d.mts +106 -5
- package/lib/index.mjs +235 -46
- package/package.json +2 -2
- package/src/client.ts +217 -33
- package/src/config.ts +37 -0
- package/src/index.ts +53 -7
- package/src/tools.ts +118 -27
- package/src/types.ts +92 -0
package/src/client.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Transport for `@huaqiu/dsh-eda-host`.
|
|
3
3
|
*
|
|
4
4
|
* DSH → dsh-eda-host → hq-edge → EDA Host is the ONLY production path. This
|
|
5
|
-
* module fetches the semantic netlist
|
|
6
|
-
* maps
|
|
7
|
-
*
|
|
5
|
+
* module fetches the semantic netlist and the EDA-independent host information
|
|
6
|
+
* from hq-edge, maps HTTP statuses back to the semantic gRPC error categories,
|
|
7
|
+
* and enforces the single end-to-end request budget. It never parses schematic
|
|
8
|
+
* files and never touches KiCad.
|
|
8
9
|
*
|
|
9
10
|
* @module
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import { netlistUrlOf, type EdaHostConfig, type NetlistScope } from './config.js'
|
|
13
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS, hostUrlOf, netlistUrlOf, type EdaHostConfig, type NetlistScope } from './config.js'
|
|
13
14
|
import {
|
|
14
15
|
NetlistError,
|
|
16
|
+
type EdaHostCapability,
|
|
17
|
+
type EdaHostInfo,
|
|
15
18
|
type SchematicNetlist,
|
|
16
19
|
} from './types.js'
|
|
17
20
|
|
|
@@ -29,49 +32,201 @@ export interface EdaHostClientDeps {
|
|
|
29
32
|
baseUrlResolver?: () => string | undefined
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/** Per-call options. `signal` lets DSH cancel a request (see §6 of the task). */
|
|
36
|
+
export interface EdaHostRequestOptions {
|
|
37
|
+
signal?: AbortSignal
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
export interface EdaHostClient {
|
|
33
41
|
/** Netlist of the currently selected components. */
|
|
34
|
-
getSelectionNetlist(): Promise<SchematicNetlist>
|
|
42
|
+
getSelectionNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>
|
|
35
43
|
/** Complete logical netlist for the current project. */
|
|
36
|
-
getProjectNetlist(): Promise<SchematicNetlist>
|
|
44
|
+
getProjectNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>
|
|
37
45
|
/** Netlist of the current active schematic page. */
|
|
38
|
-
getActivePageNetlist(): Promise<SchematicNetlist>
|
|
46
|
+
getActivePageNetlist(options?: EdaHostRequestOptions): Promise<SchematicNetlist>
|
|
47
|
+
/**
|
|
48
|
+
* EDA-independent identity / installation of the host. Presence of a result
|
|
49
|
+
* is the availability signal — `hq.host.v1` has no availability field.
|
|
50
|
+
*/
|
|
51
|
+
getEdaHostInfo(options?: EdaHostRequestOptions): Promise<EdaHostInfo>
|
|
52
|
+
/** Capabilities the host currently provides. */
|
|
53
|
+
getEdaHostCapabilities(options?: EdaHostRequestOptions): Promise<EdaHostCapability[]>
|
|
39
54
|
}
|
|
40
55
|
|
|
41
|
-
/** HTTP status → semantic error kind (see routes/
|
|
56
|
+
/** HTTP status → semantic error kind (see routes/edaHostStatus.ts on hq-edge). */
|
|
42
57
|
function statusToKind(status: number): NetlistError['kind'] {
|
|
43
58
|
if (status === 412) return 'FAILED_PRECONDITION'
|
|
44
59
|
if (status === 501) return 'UNIMPLEMENTED'
|
|
45
60
|
if (status === 503) return 'UNAVAILABLE'
|
|
61
|
+
if (status === 504) return 'DEADLINE_EXCEEDED'
|
|
46
62
|
return 'INTERNAL'
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
/** True when a thrown fetch error is an abort/timeout rather than a transport error. */
|
|
66
|
+
function isAbortError(err: unknown): boolean {
|
|
67
|
+
const name = (err as Error | undefined)?.name
|
|
68
|
+
return name === 'AbortError' || name === 'TimeoutError'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Combine an optional caller signal with the plugin's own budget.
|
|
73
|
+
*
|
|
74
|
+
* `AbortSignal.any` is not available on every Node version DSH may run on, so
|
|
75
|
+
* fall back to whichever signal exists — the budget is always present, which is
|
|
76
|
+
* what guarantees no request can wait indefinitely.
|
|
77
|
+
*/
|
|
78
|
+
function resolveSignal(deadlineMs: number, caller?: AbortSignal): AbortSignal | undefined {
|
|
79
|
+
const budget = deadlineMs > 0 ? AbortSignal.timeout(deadlineMs) : undefined
|
|
80
|
+
const signals = [caller, budget].filter((s): s is AbortSignal => Boolean(s))
|
|
81
|
+
if (signals.length === 0) return undefined
|
|
82
|
+
if (signals.length === 1) return signals[0]
|
|
83
|
+
const anyFn = (AbortSignal as unknown as { any?: (s: AbortSignal[]) => AbortSignal }).any
|
|
84
|
+
return typeof anyFn === 'function' ? anyFn.call(AbortSignal, signals) : signals[0]
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract the semantic `SchematicNetlist` from an hq-edge netlist body.
|
|
89
|
+
*
|
|
90
|
+
* hq-edge now emits a single-level body: `{ netlist: { components, nets } }`.
|
|
91
|
+
* Older hq-edge builds serialized the protobuf envelope
|
|
92
|
+
* (`GetNetListResponse.oneof result`), which produced a second `netlist` level
|
|
93
|
+
* and made every populated design look empty. That legacy shape is unwrapped
|
|
94
|
+
* EXPLICITLY — not silently — and anything else is a hard error, because
|
|
95
|
+
* "malformed" must never masquerade as "empty design".
|
|
96
|
+
*/
|
|
97
|
+
export function parseNetlistBody(body: unknown): SchematicNetlist {
|
|
98
|
+
if (!body || typeof body !== 'object' || Array.isArray(body)) {
|
|
99
|
+
throw new NetlistError('INTERNAL', 'eda-host: malformed netlist response from hq-edge')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let candidate: unknown = (body as { netlist?: unknown }).netlist
|
|
103
|
+
|
|
104
|
+
if (
|
|
105
|
+
candidate &&
|
|
106
|
+
typeof candidate === 'object' &&
|
|
107
|
+
!Array.isArray(candidate) &&
|
|
108
|
+
(candidate as { components?: unknown }).components === undefined &&
|
|
109
|
+
typeof (candidate as { netlist?: unknown }).netlist === 'object'
|
|
110
|
+
) {
|
|
111
|
+
// Legacy double-nested envelope — unwrap exactly one level.
|
|
112
|
+
candidate = (candidate as { netlist: unknown }).netlist
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) {
|
|
116
|
+
throw new NetlistError('INTERNAL', 'eda-host: malformed netlist response from hq-edge')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const { components, nets } = candidate as { components?: unknown; nets?: unknown }
|
|
120
|
+
|
|
121
|
+
// proto3 JSON omits empty arrays, so `undefined` is a legitimate empty list.
|
|
122
|
+
if (components !== undefined && !Array.isArray(components)) {
|
|
123
|
+
throw new NetlistError('INTERNAL', 'eda-host: netlist.components is not an array')
|
|
124
|
+
}
|
|
125
|
+
if (nets !== undefined && !Array.isArray(nets)) {
|
|
126
|
+
throw new NetlistError('INTERNAL', 'eda-host: netlist.nets is not an array')
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
components: (components ?? []) as SchematicNetlist['components'],
|
|
131
|
+
nets: (nets ?? []) as SchematicNetlist['nets'],
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function str(value: unknown): string {
|
|
136
|
+
return typeof value === 'string' ? value : ''
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extract a complete `EdaHostInfo` from an hq-edge host-info body.
|
|
141
|
+
*
|
|
142
|
+
* `hq.host.v1` has no availability flags, so the only way to distinguish "the
|
|
143
|
+
* host is here" from "the host is not" is whether this call succeeded at all.
|
|
144
|
+
* That makes the *shape* the contract: proto3 JSON omits default-valued fields,
|
|
145
|
+
* so a host that legitimately reports nothing arrives as `{}`. Rather than let
|
|
146
|
+
* a half-empty object reach the agent — where a missing field could be misread
|
|
147
|
+
* as "not available" — absent values are filled with their proto3 defaults.
|
|
148
|
+
*
|
|
149
|
+
* Values that are present are never reinterpreted; unknown `hostType` strings
|
|
150
|
+
* pass through so a newer host cannot be silently downgraded.
|
|
151
|
+
*/
|
|
152
|
+
export function parseEdaHostInfo(value: unknown): EdaHostInfo {
|
|
153
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
154
|
+
throw new NetlistError('INTERNAL', 'eda-host: malformed host info response from hq-edge')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const raw = value as {
|
|
158
|
+
identity?: { hostType?: unknown; hostName?: unknown; version?: unknown }
|
|
159
|
+
installation?: { applicationPath?: unknown; executables?: unknown }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const executables = Array.isArray(raw.installation?.executables)
|
|
163
|
+
? (raw.installation?.executables as unknown[])
|
|
164
|
+
.filter((e): e is Record<string, unknown> => Boolean(e) && typeof e === 'object')
|
|
165
|
+
.map((e) => ({ name: str(e.name), path: str(e.path) }))
|
|
166
|
+
: []
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
identity: {
|
|
170
|
+
hostType:
|
|
171
|
+
typeof raw.identity?.hostType === 'string'
|
|
172
|
+
? (raw.identity.hostType as EdaHostInfo['identity']['hostType'])
|
|
173
|
+
: 'EDA_HOST_TYPE_UNSPECIFIED',
|
|
174
|
+
hostName: str(raw.identity?.hostName),
|
|
175
|
+
version: str(raw.identity?.version),
|
|
176
|
+
},
|
|
177
|
+
installation: {
|
|
178
|
+
applicationPath: str(raw.installation?.applicationPath),
|
|
179
|
+
executables,
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
49
184
|
export function createEdaHostClient(
|
|
50
185
|
config: EdaHostConfig,
|
|
51
186
|
deps: EdaHostClientDeps = {},
|
|
52
187
|
): EdaHostClient {
|
|
53
188
|
const fetchImpl = deps.fetchImpl ?? globalThis.fetch
|
|
54
189
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
190
|
+
/**
|
|
191
|
+
* Resolve the host endpoint per request. A resolver (ctx.hqEdge) wins over
|
|
192
|
+
* the static config/env value; if neither yields a URL we degrade to the
|
|
193
|
+
* same clear FAILED_PRECONDITION the standalone install path uses.
|
|
194
|
+
*/
|
|
195
|
+
function resolveConfig(): EdaHostConfig {
|
|
196
|
+
const baseUrl = deps.baseUrlResolver?.()?.trim() ?? config.hqEdgeBaseUrl?.trim() ?? ''
|
|
62
197
|
if (baseUrl.length === 0) {
|
|
63
198
|
throw new NetlistError(
|
|
64
199
|
'FAILED_PRECONDITION',
|
|
65
|
-
'eda-host: no hq-edge base URL configured (
|
|
66
|
-
'
|
|
200
|
+
'eda-host: no hq-edge base URL configured (ctx.hqEdge.baseUrl / hqEdgeBaseUrl / ' +
|
|
201
|
+
'HQ_EDGE_BASE_URL) — EDA host tools require the hq-edge bridge.',
|
|
67
202
|
)
|
|
68
203
|
}
|
|
69
|
-
|
|
204
|
+
return { ...config, hqEdgeBaseUrl: baseUrl }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Perform one GET and return the parsed JSON body, mapping failures. */
|
|
208
|
+
async function getJson(
|
|
209
|
+
url: string,
|
|
210
|
+
options: EdaHostRequestOptions | undefined,
|
|
211
|
+
what: string,
|
|
212
|
+
): Promise<unknown> {
|
|
213
|
+
const signal = resolveSignal(config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, options?.signal)
|
|
70
214
|
|
|
71
215
|
let response: Response
|
|
72
216
|
try {
|
|
73
|
-
response = await fetchImpl(url, {
|
|
217
|
+
response = await fetchImpl(url, {
|
|
218
|
+
method: 'GET',
|
|
219
|
+
headers: { Accept: 'application/json' },
|
|
220
|
+
...(signal ? { signal } : {}),
|
|
221
|
+
})
|
|
74
222
|
} catch (err) {
|
|
223
|
+
// An aborted request is a timeout, never an empty result.
|
|
224
|
+
if (isAbortError(err)) {
|
|
225
|
+
throw new NetlistError(
|
|
226
|
+
'DEADLINE_EXCEEDED',
|
|
227
|
+
`eda-host: ${what} request exceeded its time budget at ${url}`,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
75
230
|
// Connection-level failure: host not running / unreachable.
|
|
76
231
|
throw new NetlistError(
|
|
77
232
|
'UNAVAILABLE',
|
|
@@ -89,25 +244,54 @@ export function createEdaHostClient(
|
|
|
89
244
|
}
|
|
90
245
|
throw new NetlistError(
|
|
91
246
|
statusToKind(response.status),
|
|
92
|
-
`eda-host:
|
|
247
|
+
`eda-host: ${what} request failed (${response.status}${detail ? `: ${detail}` : ''})`,
|
|
93
248
|
)
|
|
94
249
|
}
|
|
95
250
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (!body || typeof body.netlist !== 'object' || body.netlist === null) {
|
|
99
|
-
throw new NetlistError('INTERNAL', 'eda-host: malformed netlist response from hq-edge')
|
|
100
|
-
}
|
|
251
|
+
return response.json()
|
|
252
|
+
}
|
|
101
253
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
254
|
+
async function fetchScope(
|
|
255
|
+
scope: NetlistScope,
|
|
256
|
+
options?: EdaHostRequestOptions,
|
|
257
|
+
): Promise<SchematicNetlist> {
|
|
258
|
+
const resolved = resolveConfig()
|
|
259
|
+
const url = netlistUrlOf(resolved, scope)
|
|
260
|
+
return parseNetlistBody(await getJson(url, options, 'netlist'))
|
|
106
261
|
}
|
|
107
262
|
|
|
108
263
|
return {
|
|
109
|
-
getSelectionNetlist: () => fetchScope('selection'),
|
|
110
|
-
getProjectNetlist: () => fetchScope('project'),
|
|
111
|
-
getActivePageNetlist: () => fetchScope('active-page'),
|
|
264
|
+
getSelectionNetlist: (options) => fetchScope('selection', options),
|
|
265
|
+
getProjectNetlist: (options) => fetchScope('project', options),
|
|
266
|
+
getActivePageNetlist: (options) => fetchScope('active-page', options),
|
|
267
|
+
|
|
268
|
+
getEdaHostInfo: async (options) => {
|
|
269
|
+
const resolved = resolveConfig()
|
|
270
|
+
const url = hostUrlOf(resolved, 'info')
|
|
271
|
+
const body = (await getJson(url, options, 'host info')) as { info?: unknown }
|
|
272
|
+
|
|
273
|
+
if (!body || typeof body.info !== 'object' || body.info === null) {
|
|
274
|
+
throw new NetlistError('INTERNAL', 'eda-host: malformed host info response from hq-edge')
|
|
275
|
+
}
|
|
276
|
+
return parseEdaHostInfo(body.info)
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
getEdaHostCapabilities: async (options) => {
|
|
280
|
+
const resolved = resolveConfig()
|
|
281
|
+
const url = hostUrlOf(resolved, 'capabilities')
|
|
282
|
+
const body = (await getJson(url, options, 'host capabilities')) as {
|
|
283
|
+
capabilities?: unknown
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (!body || !Array.isArray(body.capabilities)) {
|
|
287
|
+
throw new NetlistError(
|
|
288
|
+
'INTERNAL',
|
|
289
|
+
'eda-host: malformed host capabilities response from hq-edge',
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
return body.capabilities.filter(
|
|
293
|
+
(c): c is EdaHostCapability => typeof c === 'string',
|
|
294
|
+
)
|
|
295
|
+
},
|
|
112
296
|
}
|
|
113
297
|
}
|
package/src/config.ts
CHANGED
|
@@ -15,10 +15,25 @@ export interface EdaHostConfig {
|
|
|
15
15
|
hqEdgeBaseUrl?: string
|
|
16
16
|
/** Path prefix on the host; default "/api/v1/netlist". */
|
|
17
17
|
netlistPathPrefix?: string
|
|
18
|
+
/** Path prefix for host discovery; default "/api/v1/host". */
|
|
19
|
+
hostPathPrefix?: string
|
|
20
|
+
/**
|
|
21
|
+
* End-to-end budget for one EDA-host request, in milliseconds.
|
|
22
|
+
*
|
|
23
|
+
* This is the SINGLE request budget for the whole chain — the plugin does
|
|
24
|
+
* not define separate per-layer timeouts. It is enforced here (outermost)
|
|
25
|
+
* and mirrored by the KiCad UI-dispatch bound, so no agent request can wait
|
|
26
|
+
* indefinitely. See docs/tasks/expose-capability.md §6.
|
|
27
|
+
*/
|
|
28
|
+
requestTimeoutMs?: number
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
export const DEFAULT_NETLIST_PATH_PREFIX = '/api/v1/netlist'
|
|
21
32
|
|
|
33
|
+
export const DEFAULT_HOST_PATH_PREFIX = '/api/v1/host'
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000
|
|
36
|
+
|
|
22
37
|
/** Scope → route suffix on the host netlist router. */
|
|
23
38
|
export type NetlistScope = 'project' | 'selection' | 'active-page'
|
|
24
39
|
|
|
@@ -35,9 +50,16 @@ export function resolveEdaHostConfig(
|
|
|
35
50
|
const baseUrl = config?.hqEdgeBaseUrl ?? env.HQ_EDGE_BASE_URL ?? ''
|
|
36
51
|
const pathPrefix =
|
|
37
52
|
config?.netlistPathPrefix ?? env.HQ_EDGE_NETLIST_PATH ?? DEFAULT_NETLIST_PATH_PREFIX
|
|
53
|
+
const hostPrefix =
|
|
54
|
+
config?.hostPathPrefix ?? env.HQ_EDGE_HOST_PATH ?? DEFAULT_HOST_PATH_PREFIX
|
|
55
|
+
const timeoutRaw = config?.requestTimeoutMs ?? env.HQ_EDGE_REQUEST_TIMEOUT_MS
|
|
56
|
+
const timeout = Number.parseInt(String(timeoutRaw ?? ''), 10)
|
|
38
57
|
return {
|
|
39
58
|
hqEdgeBaseUrl: baseUrl,
|
|
40
59
|
netlistPathPrefix: pathPrefix,
|
|
60
|
+
hostPathPrefix: hostPrefix,
|
|
61
|
+
requestTimeoutMs:
|
|
62
|
+
Number.isFinite(timeout) && timeout > 0 ? timeout : DEFAULT_REQUEST_TIMEOUT_MS,
|
|
41
63
|
}
|
|
42
64
|
}
|
|
43
65
|
|
|
@@ -52,3 +74,18 @@ export function netlistUrlOf(config: EdaHostConfig, scope: NetlistScope): string
|
|
|
52
74
|
const prefix = (config.netlistPathPrefix ?? DEFAULT_NETLIST_PATH_PREFIX).replace(/^\/+|\/+$/g, '')
|
|
53
75
|
return `${base}/${prefix}${SCOPE_ROUTE[scope]}`
|
|
54
76
|
}
|
|
77
|
+
|
|
78
|
+
/** Host discovery route suffix. */
|
|
79
|
+
export type HostRoute = 'info' | 'capabilities'
|
|
80
|
+
|
|
81
|
+
const HOST_ROUTE: Record<HostRoute, string> = {
|
|
82
|
+
info: '/info',
|
|
83
|
+
capabilities: '/capabilities',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Build the absolute URL for one host discovery route. */
|
|
87
|
+
export function hostUrlOf(config: EdaHostConfig, route: HostRoute): string {
|
|
88
|
+
const base = (config.hqEdgeBaseUrl ?? '').replace(/\/+$/, '')
|
|
89
|
+
const prefix = (config.hostPathPrefix ?? DEFAULT_HOST_PATH_PREFIX).replace(/^\/+|\/+$/g, '')
|
|
90
|
+
return `${base}/${prefix}${HOST_ROUTE[route]}`
|
|
91
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* `@huaqiu/dsh-eda-host` — node plugin entry.
|
|
3
3
|
*
|
|
4
4
|
* Provides the `edaHost` service (semantic EDA-host capability) and registers
|
|
5
|
-
*
|
|
5
|
+
* five agent tools:
|
|
6
6
|
*
|
|
7
7
|
* get_project_netlist complete project netlist
|
|
8
8
|
* get_selection_netlist currently selected components netlist
|
|
9
9
|
* get_active_page_netlist active schematic page netlist
|
|
10
|
+
* get_eda_host_info which EDA host, version, installation
|
|
11
|
+
* get_eda_host_capabilities what the current host can actually do
|
|
10
12
|
*
|
|
11
13
|
* ── Architectural boundary (task: add-dsh-eda-host) ─────────────────────────
|
|
12
14
|
* The ONLY production request path is DSH → dsh-eda-host → hq-edge → EDA host.
|
|
@@ -26,7 +28,7 @@ import type { Context } from '@deepseek-ai/cordis'
|
|
|
26
28
|
import { getLogger } from '@huaqiu/dsh-plugin-log'
|
|
27
29
|
import { createEdaHostClient, type EdaHostClient } from './client.js'
|
|
28
30
|
import { hasHost, resolveEdaHostConfig, type EdaHostConfig } from './config.js'
|
|
29
|
-
import { createNetListTools } from './tools.js'
|
|
31
|
+
import { createEdaHostTools, createNetListTools } from './tools.js'
|
|
30
32
|
|
|
31
33
|
/** Plugin id — matches package.json. */
|
|
32
34
|
export const name = '@huaqiu/dsh-eda-host'
|
|
@@ -50,8 +52,14 @@ export const name = '@huaqiu/dsh-eda-host'
|
|
|
50
52
|
export const inject = ['hqEdge', 'tools'] as const
|
|
51
53
|
|
|
52
54
|
export type { EdaHostConfig } from './config.js'
|
|
53
|
-
export type { EdaHostClient } from './client.js'
|
|
55
|
+
export type { EdaHostClient, EdaHostRequestOptions } from './client.js'
|
|
54
56
|
export type {
|
|
57
|
+
EdaHostCapability,
|
|
58
|
+
EdaHostExecutable,
|
|
59
|
+
EdaHostIdentity,
|
|
60
|
+
EdaHostInfo,
|
|
61
|
+
EdaHostInstallation,
|
|
62
|
+
EdaHostType,
|
|
55
63
|
ElectricalNet,
|
|
56
64
|
ElectricalType,
|
|
57
65
|
NetlistErrorKind,
|
|
@@ -61,6 +69,7 @@ export type {
|
|
|
61
69
|
SchematicNetlist,
|
|
62
70
|
} from './types.js'
|
|
63
71
|
export { NetlistError } from './types.js'
|
|
72
|
+
export { parseNetlistBody } from './client.js'
|
|
64
73
|
|
|
65
74
|
declare module '@deepseek-ai/cordis' {
|
|
66
75
|
interface Context {
|
|
@@ -88,6 +97,12 @@ declare module '@deepseek-ai/cordis' {
|
|
|
88
97
|
const COMPONENT = 'dsh-eda-host'
|
|
89
98
|
const log = getLogger(COMPONENT)
|
|
90
99
|
|
|
100
|
+
// Emitted on import, before any Cordis dependency is resolved. Pairs with the
|
|
101
|
+
// "node half ready" marker in apply(): if this line is logged but that one is
|
|
102
|
+
// not, the edge-bridge never provided `hqEdge` and this plugin is still
|
|
103
|
+
// pending — which is otherwise completely silent.
|
|
104
|
+
log.info('dsh-eda-host: module loaded (waiting for the hqEdge + tools services)')
|
|
105
|
+
|
|
91
106
|
/**
|
|
92
107
|
* Host plugin body — provide `edaHost` and register the three netlist tools.
|
|
93
108
|
*
|
|
@@ -113,30 +128,61 @@ export function apply(ctx: Context, config: Partial<EdaHostConfig> = {}): () =>
|
|
|
113
128
|
|
|
114
129
|
const resolved = resolveEdaHostConfig(config)
|
|
115
130
|
|
|
131
|
+
// ── Explicit bridge dependency ───────────────────────────────────────────
|
|
132
|
+
// `hqEdge` is a REQUIRED inject, so Cordis only calls `apply()` once the
|
|
133
|
+
// edge-bridge has provided it. That means a missing bridge would otherwise
|
|
134
|
+
// leave this plugin pending forever with the tools never registered and
|
|
135
|
+
// nothing logged. Two things make that diagnosable:
|
|
136
|
+
//
|
|
137
|
+
// 1. `apply()` asserts the bridge actually gave us a usable endpoint and
|
|
138
|
+
// THROWS when it did not — a loud startup failure beats five tools that
|
|
139
|
+
// fail one by one at call time.
|
|
140
|
+
// 2. The module-level marker below is emitted on import. If
|
|
141
|
+
// "dsh-eda-host: module loaded" appears in the log but
|
|
142
|
+
// "dsh-eda-host: node half ready" never does, the bridge never provided
|
|
143
|
+
// `hqEdge` and this plugin is still pending.
|
|
144
|
+
//
|
|
145
|
+
// hq-edge is NOT optional here — this plugin must never become a standalone
|
|
146
|
+
// DSH plugin (docs/tasks/expose-capability.md §3, §7).
|
|
147
|
+
const hq = ctx.hqEdge
|
|
148
|
+
if (!hq || typeof hq.baseUrl !== 'string' || hq.baseUrl.trim().length === 0) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
'@huaqiu/dsh-eda-host requires a usable hq-edge context: the edge-bridge ' +
|
|
151
|
+
'plugin did not provide ctx.hqEdge.baseUrl. EDA host tools cannot work ' +
|
|
152
|
+
'without hq-edge — check that the bridge started and that the HQ Edge ' +
|
|
153
|
+
'port is valid.',
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
116
157
|
// Late-bound host endpoint: prefer the edge-bridge service, then the overlay
|
|
117
158
|
// config / env value. Consulted on every request (see client.ts).
|
|
118
159
|
const getHqEdgeBaseUrl = (): string | undefined => {
|
|
119
|
-
const
|
|
120
|
-
return
|
|
160
|
+
const current = ctx.hqEdge
|
|
161
|
+
return current?.baseUrl && current.baseUrl.trim().length > 0 ? current.baseUrl : undefined
|
|
121
162
|
}
|
|
122
163
|
|
|
123
164
|
log.info('applying dsh-eda-host node half', {
|
|
124
165
|
hasConfigHost: hasHost(resolved),
|
|
125
166
|
hqEdgeBaseUrlFromConfig: resolved.hqEdgeBaseUrl ?? null,
|
|
126
167
|
netlistPathPrefix: resolved.netlistPathPrefix,
|
|
168
|
+
hostPathPrefix: resolved.hostPathPrefix,
|
|
169
|
+
requestTimeoutMs: resolved.requestTimeoutMs,
|
|
127
170
|
})
|
|
128
171
|
|
|
129
172
|
const client = createEdaHostClient(resolved, { baseUrlResolver: getHqEdgeBaseUrl })
|
|
130
173
|
|
|
131
174
|
ctx.effect(() => ctx.provide('edaHost', client))
|
|
132
175
|
|
|
133
|
-
const tools = createNetListTools({ client })
|
|
176
|
+
const tools = [...createNetListTools({ client }), ...createEdaHostTools({ client })]
|
|
134
177
|
const disposers: Array<() => void> = []
|
|
135
178
|
for (const tool of tools) {
|
|
136
179
|
disposers.push(ctx.tools.register(tool))
|
|
137
180
|
}
|
|
138
181
|
|
|
139
|
-
log.info('dsh-eda-host node half ready', {
|
|
182
|
+
log.info('dsh-eda-host node half ready', {
|
|
183
|
+
tools: tools.length,
|
|
184
|
+
configHostMode: hasHost(resolved),
|
|
185
|
+
})
|
|
140
186
|
|
|
141
187
|
return () => {
|
|
142
188
|
for (const dispose of disposers) {
|