@dmytro-prototypes/cadbridge-mcp 0.1.12 → 0.1.14

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 (2) hide show
  1. package/bin/plugin.js +67 -5
  2. package/package.json +4 -4
package/bin/plugin.js CHANGED
@@ -14,7 +14,7 @@
14
14
  // com.apple.quarantine nor Mark-of-the-Web, so nothing here is ever gated by
15
15
  // Gatekeeper or SmartScreen.
16
16
  import { createHash } from "node:crypto"
17
- import { cpSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs"
17
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs"
18
18
  import { homedir } from "node:os"
19
19
  import { dirname, join, sep } from "node:path"
20
20
 
@@ -65,6 +65,46 @@ export function hashTree(root) {
65
65
  return digest.digest("hex")
66
66
  }
67
67
 
68
+ // AutoCAD releases installed on this machine, newest first. LT is excluded
69
+ // deliberately: it shares the naming but cannot load ObjectARX modules at all,
70
+ // so counting it would report coverage that can never happen.
71
+ //
72
+ // Detection only ever adds information. The bundle is installed either way —
73
+ // it is inert without AutoCAD, and a false negative here (an install in a
74
+ // non-standard location) must not be able to withhold the plugin from someone
75
+ // who does have AutoCAD.
76
+ export function installedAutoCAD({ platform = process.platform, roots } = {}) {
77
+ const search = roots ?? (platform === "win32"
78
+ ? [process.env["ProgramFiles"] || "C:\\Program Files", process.env["ProgramW6432"]].filter(Boolean).map(p => join(p, "Autodesk"))
79
+ : ["/Applications/Autodesk"])
80
+
81
+ const found = new Set()
82
+ for (const root of search) {
83
+ let entries
84
+ try {
85
+ entries = readdirSync(root, { withFileTypes: true })
86
+ } catch {
87
+ continue
88
+ }
89
+ for (const entry of entries) {
90
+ // "AutoCAD 2026" yes, "AutoCAD LT 2026" no.
91
+ const match = /^AutoCAD (\d{4})$/.exec(entry.name)
92
+ if (match) found.add(match[1])
93
+ }
94
+ }
95
+ return [...found].sort().reverse()
96
+ }
97
+
98
+ // Which AutoCAD releases the bundle about to be installed actually serves.
99
+ // Read out of the manifest itself rather than from plugin/series.json, which
100
+ // is a build-time file and is not shipped: the manifest is both what AutoCAD
101
+ // obeys and the only copy that reaches a user's machine. Its per-release
102
+ // Components blocks are generated by scripts/package-contents.mjs, which names
103
+ // the release in each Description.
104
+ export function coveredReleases(manifest) {
105
+ return [...manifest.matchAll(/<Components Description="CADBridge for AutoCAD (\d{4})"/g)].map(([, release]) => release)
106
+ }
107
+
68
108
  // Beside PackageContents.xml rather than inside Contents/: the signed bundle
69
109
  // is nested one level down, and adding a file inside it would invalidate the
70
110
  // seal that Gatekeeper and AutoCAD both check.
@@ -89,6 +129,9 @@ export function installPlugin(binary, {
89
129
  home = homedir(),
90
130
  appData = process.env.APPDATA || join(home, "AppData", "Roaming"),
91
131
  env = process.env,
132
+ // Injectable so a test does not depend on what AutoCAD happens to be
133
+ // installed on the machine running it.
134
+ autocad,
92
135
  } = {}) {
93
136
  if (env.CADBRIDGE_SKIP_PLUGIN_INSTALL) return { status: "skipped" }
94
137
 
@@ -97,10 +140,12 @@ export function installPlugin(binary, {
97
140
 
98
141
  const source = pluginSource(binary)
99
142
  let version
143
+ let manifest
100
144
  try {
101
145
  // PackageContents.xml is the file AutoCAD itself reads, so its absence
102
146
  // means there is no loadable bundle here whatever else was shipped.
103
- version = /AppVersion="([^"]*)"/.exec(readFileSync(join(source, "PackageContents.xml"), "utf8"))?.[1] ?? "unknown"
147
+ manifest = readFileSync(join(source, "PackageContents.xml"), "utf8")
148
+ version = /AppVersion="([^"]*)"/.exec(manifest)?.[1] ?? "unknown"
104
149
  } catch {
105
150
  // Older platform packages shipped the server alone. Nothing to install,
106
151
  // and nothing worth saying about it.
@@ -109,7 +154,8 @@ export function installPlugin(binary, {
109
154
 
110
155
  const destination = join(root, "CADBridge.bundle")
111
156
  const digest = hashTree(source)
112
- if (installed(destination)?.sha256 === digest) return { status: "current", version, destination }
157
+ const covers = { manifest, installedAutoCAD: autocad ?? installedAutoCAD({ platform }) }
158
+ if (installed(destination)?.sha256 === digest) return { status: "current", version, destination, ...covers }
113
159
 
114
160
  mkdirSync(root, { recursive: true })
115
161
  // One temp directory holds both the new tree being assembled and the old one
@@ -151,14 +197,14 @@ export function installPlugin(binary, {
151
197
  }
152
198
  throw error
153
199
  }
154
- return { status: "installed", version, destination }
200
+ return { status: "installed", version, destination, ...covers }
155
201
  } catch (error) {
156
202
  // AutoCAD maps CADBridge.arx for the whole session (PackageContents.xml
157
203
  // sets LoadOnAutoCADStartup), and Windows will not let a mapped module be
158
204
  // renamed or deleted. That is the ordinary case of "the user started
159
205
  // AutoCAD first", not a failure worth refusing to start the server over.
160
206
  const status = !stranded && ["EBUSY", "EPERM", "EACCES", "ENOTEMPTY"].includes(error.code) ? "locked" : "failed"
161
- return { status, version, destination, stranded, error }
207
+ return { status, version, destination, stranded, error, ...covers }
162
208
  } finally {
163
209
  if (!stranded) rmSync(staging, { recursive: true, force: true })
164
210
  }
@@ -167,6 +213,22 @@ export function installPlugin(binary, {
167
213
  // stdout belongs to the MCP protocol; every byte here goes to stderr, which
168
214
  // clients surface in their server logs.
169
215
  export function reportPluginInstall(result, write = message => process.stderr.write(`${message}\n`)) {
216
+ // An AutoCAD that is installed but that the bundle does not serve is the one
217
+ // thing the user cannot discover for themselves: AutoCAD ignores a
218
+ // non-matching Components block in silence, so CADBridge would simply never
219
+ // appear there with no clue why. Reported on any status, because the bundle
220
+ // being already current does not make the gap go away.
221
+ if (result.manifest && result.installedAutoCAD?.length) {
222
+ const covered = coveredReleases(result.manifest)
223
+ const missing = result.installedAutoCAD.filter(release => !covered.includes(release))
224
+ if (missing.length) {
225
+ write(
226
+ `CADBridge: no plugin build for AutoCAD ${missing.join(", ")} — ` +
227
+ `this bundle serves ${covered.join(", ") || "no installed release"}. ` +
228
+ `CADBridge will not load there.`,
229
+ )
230
+ }
231
+ }
170
232
  if (result.status === "installed") {
171
233
  // The plugin's own version, which is not the server's and not this npm
172
234
  // package's — the three are released independently and only
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmytro-prototypes/cadbridge-mcp",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "mcpName": "net.dmytro-prototypes/cadbridge-mcp-autocad",
5
5
  "description": "Drive a live AutoCAD session from an AI client through MCP",
6
6
  "type": "module",
@@ -30,14 +30,14 @@
30
30
  "LICENSE.txt"
31
31
  ],
32
32
  "optionalDependencies": {
33
- "@dmytro-prototypes/cadbridge-mcp-darwin-arm64": "0.1.12",
34
- "@dmytro-prototypes/cadbridge-mcp-win32-x64": "0.1.12"
33
+ "@dmytro-prototypes/cadbridge-mcp-darwin-arm64": "0.1.14",
34
+ "@dmytro-prototypes/cadbridge-mcp-win32-x64": "0.1.14"
35
35
  },
36
36
  "publishConfig": {
37
37
  "access": "public"
38
38
  },
39
39
  "cadbridge": {
40
40
  "server": "0.1.11",
41
- "plugin": "0.1.11"
41
+ "plugin": "0.1.12"
42
42
  }
43
43
  }