@huaqiu/dsh-kicad 0.4.2 → 0.4.4

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/lib/index.d.mts CHANGED
@@ -267,6 +267,33 @@ declare function readBundledSkill(moduleUrl: string, override?: string): {
267
267
  description: string;
268
268
  content: string;
269
269
  };
270
+ /**
271
+ * Read the bundled skill, or `null` when the installed package does not carry
272
+ * one.
273
+ *
274
+ * ── Why this degrades instead of throwing ──────────────────────────────────
275
+ * A missing `skills/` tree is a PACKAGING failure, and the honest signal for a
276
+ * packaging failure is a loud error — but not a dead server. `apply()` runs
277
+ * inside the DSH plugin tree: throwing here aborts the whole loader
278
+ * (`plugin tree failed to load`), kills the DSH process, and takes every other
279
+ * plugin and the entire EDA session down with it, for one missing asset.
280
+ *
281
+ * That actually happened: HQ Edge's builtin-plugin staging copied only
282
+ * `package.json` + `lib/`, so `skills/` never reached the bundle and the
283
+ * server could not start at all. The correct blast radius for "this plugin's
284
+ * skill is missing" is "this plugin is degraded", so we log at error level
285
+ * with the exact path and remedy, skip skill registration, and keep the tools
286
+ * (which fail per-call with a typed `FAILED_PRECONDITION` rather than at load).
287
+ *
288
+ * @param moduleUrl - `import.meta.url` of the calling module.
289
+ * @param override - explicit skill directory (plugin config or env var).
290
+ */
291
+ declare function tryReadBundledSkill(moduleUrl: string, override?: string): {
292
+ dir: string;
293
+ name: string;
294
+ description: string;
295
+ content: string;
296
+ } | null;
270
297
  /**
271
298
  * Host plugin body — register the `kicad-ipc` skill and the KiCad tools.
272
299
  *
@@ -281,4 +308,4 @@ declare function readBundledSkill(moduleUrl: string, override?: string): {
281
308
  */
282
309
  declare function apply(ctx: Context, config?: KicadConfigInput): () => void;
283
310
  //#endregion
284
- export { KICAD_SCRIPTS, KICAD_SCRIPT_IDS, KICAD_SKILL_NAME, type KicadConfig, type KicadConfigInput, type KicadError, type KicadErrorKind, type KicadScript, type ScriptEffect, type ScriptRun, SkillRegistration, apply, classifyRun, createKicadTools, inject, invokeKicadScript, kicadScript, kicadToolNames, name, readBundledSkill, requireSkillDir, resolveSkillDir, runKicadScript, scriptsDir, skillDescription };
311
+ export { KICAD_SCRIPTS, KICAD_SCRIPT_IDS, KICAD_SKILL_NAME, type KicadConfig, type KicadConfigInput, type KicadError, type KicadErrorKind, type KicadScript, type ScriptEffect, type ScriptRun, SkillRegistration, apply, classifyRun, createKicadTools, inject, invokeKicadScript, kicadScript, kicadToolNames, name, readBundledSkill, requireSkillDir, resolveSkillDir, runKicadScript, scriptsDir, skillDescription, tryReadBundledSkill };
package/lib/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { getLogger } from "@huaqiu/dsh-plugin-log";
2
2
  import { existsSync, readFileSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { defineTool } from "@deepseek-ai/dsh-tools";
5
6
  import { spawn } from "node:child_process";
6
7
  const DEFAULT_TIMEOUT_MS = 3e4;
@@ -160,7 +161,7 @@ function resolveSkillDir(moduleUrl, override) {
160
161
  if (override && override.trim().length > 0) return resolve(override);
161
162
  const envOverride = process.env["DSH_KICAD_SKILLS_DIR"];
162
163
  if (envOverride && envOverride.trim().length > 0) return resolve(envOverride);
163
- const here = dirname(new URL(moduleUrl).pathname);
164
+ const here = dirname(fileURLToPath(moduleUrl));
164
165
  return resolve(here, "..", "skills", KICAD_SKILL_NAME);
165
166
  }
166
167
  /**
@@ -902,6 +903,38 @@ function readBundledSkill(moduleUrl, override) {
902
903
  };
903
904
  }
904
905
  /**
906
+ * Read the bundled skill, or `null` when the installed package does not carry
907
+ * one.
908
+ *
909
+ * ── Why this degrades instead of throwing ──────────────────────────────────
910
+ * A missing `skills/` tree is a PACKAGING failure, and the honest signal for a
911
+ * packaging failure is a loud error — but not a dead server. `apply()` runs
912
+ * inside the DSH plugin tree: throwing here aborts the whole loader
913
+ * (`plugin tree failed to load`), kills the DSH process, and takes every other
914
+ * plugin and the entire EDA session down with it, for one missing asset.
915
+ *
916
+ * That actually happened: HQ Edge's builtin-plugin staging copied only
917
+ * `package.json` + `lib/`, so `skills/` never reached the bundle and the
918
+ * server could not start at all. The correct blast radius for "this plugin's
919
+ * skill is missing" is "this plugin is degraded", so we log at error level
920
+ * with the exact path and remedy, skip skill registration, and keep the tools
921
+ * (which fail per-call with a typed `FAILED_PRECONDITION` rather than at load).
922
+ *
923
+ * @param moduleUrl - `import.meta.url` of the calling module.
924
+ * @param override - explicit skill directory (plugin config or env var).
925
+ */
926
+ function tryReadBundledSkill(moduleUrl, override) {
927
+ try {
928
+ return readBundledSkill(moduleUrl, override);
929
+ } catch (err) {
930
+ log.error("dsh-kicad: bundled skill unavailable — continuing degraded, the KiCad tools are registered but will fail until the package is reinstalled", {
931
+ expectedDir: resolveSkillDir(moduleUrl, override),
932
+ error: String(err?.message ?? err)
933
+ });
934
+ return null;
935
+ }
936
+ }
937
+ /**
905
938
  * Host plugin body — register the `kicad-ipc` skill and the KiCad tools.
906
939
  *
907
940
  * Both halves are registered here so that one installation delivers both. The
@@ -917,15 +950,17 @@ function apply(ctx, config = {}) {
917
950
  if (!ctx.tools || typeof ctx.tools.register !== "function") throw new Error("@huaqiu/dsh-kicad requires the DSH `tools` service (ctx.tools.register).");
918
951
  if (!ctx.skills || typeof ctx.skills.register !== "function") throw new Error("@huaqiu/dsh-kicad requires the DSH `skills` service (ctx.skills.register).");
919
952
  const resolved = resolveKicadConfig(config);
920
- const skill = readBundledSkill(import.meta.url, config.skillsDir);
953
+ const skill = tryReadBundledSkill(import.meta.url, config.skillsDir);
954
+ const skillDir = skill?.dir ?? resolveSkillDir(import.meta.url, config.skillsDir);
921
955
  log.info("applying dsh-kicad node half", {
922
956
  hasConfigHost: hasHostConfig(config),
923
957
  pythonPath: resolved.pythonPath,
924
- skillDir: skill.dir,
958
+ skillDir,
959
+ skillPresent: skill !== null,
925
960
  timeoutMs: resolved.timeoutMs
926
961
  });
927
962
  const disposers = [];
928
- disposers.push(ctx.skills.register({
963
+ if (skill) disposers.push(ctx.skills.register({
929
964
  name: skill.name,
930
965
  description: skill.description,
931
966
  content: skill.content,
@@ -935,13 +970,14 @@ function apply(ctx, config = {}) {
935
970
  }
936
971
  }));
937
972
  const tools = createKicadTools({
938
- scriptsDir: scriptsDir(skill.dir),
973
+ scriptsDir: scriptsDir(skillDir),
939
974
  pythonPath: resolved.pythonPath,
940
975
  config: resolved
941
976
  });
942
977
  for (const tool of tools) disposers.push(ctx.tools.register(tool));
943
978
  log.info("dsh-kicad node half ready", {
944
- skill: skill.name,
979
+ skill: skill?.name ?? null,
980
+ degraded: skill === null,
945
981
  tools: tools.length,
946
982
  expectedTools: kicadToolNames().length
947
983
  });
@@ -954,4 +990,4 @@ function apply(ctx, config = {}) {
954
990
  };
955
991
  }
956
992
  //#endregion
957
- export { KICAD_SCRIPTS, KICAD_SCRIPT_IDS, KICAD_SKILL_NAME, apply, classifyRun, createKicadTools, inject, invokeKicadScript, kicadScript, kicadToolNames, name, readBundledSkill, requireSkillDir, resolveSkillDir, runKicadScript, scriptsDir, skillDescription };
993
+ export { KICAD_SCRIPTS, KICAD_SCRIPT_IDS, KICAD_SKILL_NAME, apply, classifyRun, createKicadTools, inject, invokeKicadScript, kicadScript, kicadToolNames, name, readBundledSkill, requireSkillDir, resolveSkillDir, runKicadScript, scriptsDir, skillDescription, tryReadBundledSkill };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@huaqiu/dsh-kicad",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
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.4.2"
25
+ "@huaqiu/dsh-plugin-log": "0.4.4"
26
26
  },
27
27
  "files": [
28
28
  "lib",
package/src/index.ts CHANGED
@@ -157,6 +157,46 @@ export function readBundledSkill(moduleUrl: string, override?: string): {
157
157
  }
158
158
  }
159
159
 
160
+ /**
161
+ * Read the bundled skill, or `null` when the installed package does not carry
162
+ * one.
163
+ *
164
+ * ── Why this degrades instead of throwing ──────────────────────────────────
165
+ * A missing `skills/` tree is a PACKAGING failure, and the honest signal for a
166
+ * packaging failure is a loud error — but not a dead server. `apply()` runs
167
+ * inside the DSH plugin tree: throwing here aborts the whole loader
168
+ * (`plugin tree failed to load`), kills the DSH process, and takes every other
169
+ * plugin and the entire EDA session down with it, for one missing asset.
170
+ *
171
+ * That actually happened: HQ Edge's builtin-plugin staging copied only
172
+ * `package.json` + `lib/`, so `skills/` never reached the bundle and the
173
+ * server could not start at all. The correct blast radius for "this plugin's
174
+ * skill is missing" is "this plugin is degraded", so we log at error level
175
+ * with the exact path and remedy, skip skill registration, and keep the tools
176
+ * (which fail per-call with a typed `FAILED_PRECONDITION` rather than at load).
177
+ *
178
+ * @param moduleUrl - `import.meta.url` of the calling module.
179
+ * @param override - explicit skill directory (plugin config or env var).
180
+ */
181
+ export function tryReadBundledSkill(
182
+ moduleUrl: string,
183
+ override?: string,
184
+ ): { dir: string; name: string; description: string; content: string } | null {
185
+ try {
186
+ return readBundledSkill(moduleUrl, override)
187
+ } catch (err) {
188
+ log.error(
189
+ 'dsh-kicad: bundled skill unavailable — continuing degraded, the KiCad ' +
190
+ 'tools are registered but will fail until the package is reinstalled',
191
+ {
192
+ expectedDir: resolveSkillDir(moduleUrl, override),
193
+ error: String((err as Error)?.message ?? err),
194
+ },
195
+ )
196
+ return null
197
+ }
198
+ }
199
+
160
200
  /**
161
201
  * Host plugin body — register the `kicad-ipc` skill and the KiCad tools.
162
202
  *
@@ -179,32 +219,39 @@ export function apply(ctx: Context, config: KicadConfigInput = {}): () => void {
179
219
 
180
220
  const resolved = resolveKicadConfig(config)
181
221
 
182
- // Throws when the installed package is missing its skill — a packaging
183
- // failure must be loud, not silently degraded (§15).
184
- const skill = readBundledSkill(import.meta.url, config.skillsDir)
222
+ // Degraded rather than fatal: see tryReadBundledSkill. A null skill means
223
+ // the installed package is incomplete, not that the host is unusable.
224
+ const skill = tryReadBundledSkill(import.meta.url, config.skillsDir)
225
+ // Tools resolve their Python scripts under the skill directory, so they use
226
+ // the same path even when the skill itself is missing — a call then fails
227
+ // with a typed FAILED_PRECONDITION naming the exact script it needed.
228
+ const skillDir = skill?.dir ?? resolveSkillDir(import.meta.url, config.skillsDir)
185
229
 
186
230
  log.info('applying dsh-kicad node half', {
187
231
  hasConfigHost: hasHostConfig(config),
188
232
  pythonPath: resolved.pythonPath,
189
- skillDir: skill.dir,
233
+ skillDir,
234
+ skillPresent: skill !== null,
190
235
  timeoutMs: resolved.timeoutMs,
191
236
  })
192
237
 
193
238
  const disposers: Array<() => void> = []
194
239
 
195
240
  // ── Skill (bundled, no separate installation) ────────────────────────────
196
- disposers.push(
197
- ctx.skills.register({
198
- name: skill.name,
199
- description: skill.description,
200
- content: skill.content,
201
- resourceBase: { kind: 'directory', path: skill.dir },
202
- }),
203
- )
241
+ if (skill) {
242
+ disposers.push(
243
+ ctx.skills.register({
244
+ name: skill.name,
245
+ description: skill.description,
246
+ content: skill.content,
247
+ resourceBase: { kind: 'directory', path: skill.dir },
248
+ }),
249
+ )
250
+ }
204
251
 
205
252
  // ── Tools ────────────────────────────────────────────────────────────────
206
253
  const tools = createKicadTools({
207
- scriptsDir: scriptsDir(skill.dir),
254
+ scriptsDir: scriptsDir(skillDir),
208
255
  pythonPath: resolved.pythonPath,
209
256
  config: resolved,
210
257
  })
@@ -213,7 +260,8 @@ export function apply(ctx: Context, config: KicadConfigInput = {}): () => void {
213
260
  }
214
261
 
215
262
  log.info('dsh-kicad node half ready', {
216
- skill: skill.name,
263
+ skill: skill?.name ?? null,
264
+ degraded: skill === null,
217
265
  tools: tools.length,
218
266
  expectedTools: kicadToolNames().length,
219
267
  })
package/src/paths.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { existsSync } from 'node:fs'
19
19
  import { dirname, join, resolve } from 'node:path'
20
+ import { fileURLToPath } from 'node:url'
20
21
 
21
22
  import { KICAD_SKILL_NAME } from './scripts.js'
22
23
 
@@ -35,7 +36,12 @@ export function resolveSkillDir(moduleUrl: string, override?: string): string {
35
36
  const envOverride = process.env['DSH_KICAD_SKILLS_DIR']
36
37
  if (envOverride && envOverride.trim().length > 0) return resolve(envOverride)
37
38
 
38
- const here = dirname(new URL(moduleUrl).pathname)
39
+ // `new URL(moduleUrl).pathname` keeps a leading slash on Windows
40
+ // (`/C:/Users/...`), which `resolve()` then roots at the drive root and turns
41
+ // into `C:\C:\Users\...` — a doubled drive prefix. `fileURLToPath` decodes the
42
+ // file URL into a native path on every platform, so it is the correct input
43
+ // for `dirname`/`resolve`.
44
+ const here = dirname(fileURLToPath(moduleUrl))
39
45
  // One level up is right for both `lib/` (built) and `src/` (vitest) because
40
46
  // both sit directly under the package root next to `skills/`.
41
47
  return resolve(here, '..', 'skills', KICAD_SKILL_NAME)