@dmytro-prototypes/cadbridge-mcp 0.1.13 → 0.1.15
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/bin/plugin.js +105 -45
- package/package.json +3 -3
package/bin/plugin.js
CHANGED
|
@@ -18,16 +18,25 @@ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync,
|
|
|
18
18
|
import { homedir } from "node:os"
|
|
19
19
|
import { dirname, join, sep } from "node:path"
|
|
20
20
|
|
|
21
|
-
// Autodesk's per-user auto-load location
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// Autodesk's per-user auto-load location — and it is not the same mechanism on
|
|
22
|
+
// the two platforms.
|
|
23
|
+
//
|
|
24
|
+
// Windows reads ApplicationPlugins: a container folder holding
|
|
25
|
+
// PackageContents.xml plus the modules it names. That is the AutoLoader, and
|
|
26
|
+
// the folder is trusted, so SECURELOAD does not prompt.
|
|
27
|
+
//
|
|
28
|
+
// macOS does not consume PackageContents.xml at all. Observed on AutoCAD 2026
|
|
29
|
+
// for Mac: a container in ApplicationPlugins never loads — not ours, and not
|
|
30
|
+
// Autodesk's own sample plug-in — while a plain .bundle placed directly in
|
|
31
|
+
// ApplicationAddins loads at startup with no prompt. So macOS gets
|
|
32
|
+
// ApplicationAddins, and installPlugin puts the modules there unwrapped.
|
|
24
33
|
export function pluginRoot({
|
|
25
34
|
platform = process.platform,
|
|
26
35
|
home = homedir(),
|
|
27
36
|
appData = process.env.APPDATA || join(home, "AppData", "Roaming"),
|
|
28
37
|
} = {}) {
|
|
29
38
|
if (platform === "win32") return join(appData, "Autodesk", "ApplicationPlugins")
|
|
30
|
-
if (platform === "darwin") return join(home, "Library", "Application Support", "Autodesk", "
|
|
39
|
+
if (platform === "darwin") return join(home, "Library", "Application Support", "Autodesk", "ApplicationAddins")
|
|
31
40
|
return null
|
|
32
41
|
}
|
|
33
42
|
|
|
@@ -110,9 +119,9 @@ export function coveredReleases(manifest) {
|
|
|
110
119
|
// seal that Gatekeeper and AutoCAD both check.
|
|
111
120
|
const STAMP = ".cadbridge-npm-install"
|
|
112
121
|
|
|
113
|
-
function
|
|
122
|
+
function installedStamp(stamp) {
|
|
114
123
|
try {
|
|
115
|
-
return JSON.parse(readFileSync(
|
|
124
|
+
return JSON.parse(readFileSync(stamp, "utf8"))
|
|
116
125
|
} catch {
|
|
117
126
|
// Missing, unreadable or hand-edited all mean the same thing: this is not
|
|
118
127
|
// a copy we can prove we put there, so replace it.
|
|
@@ -120,6 +129,39 @@ function installed(destination) {
|
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
// The modules to place, and under what names.
|
|
133
|
+
//
|
|
134
|
+
// Windows installs the container itself: AutoCAD reads PackageContents.xml and
|
|
135
|
+
// picks the Components block matching its release.
|
|
136
|
+
//
|
|
137
|
+
// macOS has no manifest to pick with, so the selection has to be made here —
|
|
138
|
+
// and it is made per ObjectARX SERIES, not per release. 2025 and 2026 both link
|
|
139
|
+
// acdb25, so both binaries load into either one: installing them side by side
|
|
140
|
+
// gives AutoCAD two live CADBridge instances that fight over the same command
|
|
141
|
+
// names. One binary per series is what the old single Components block
|
|
142
|
+
// (R25.0-R25.99) always asserted; this makes it true on disk.
|
|
143
|
+
export function pluginModules(source, { platform = process.platform } = {}) {
|
|
144
|
+
if (platform !== "darwin") return [{ name: "CADBridge.bundle", from: source }]
|
|
145
|
+
|
|
146
|
+
const osx = join(source, "Contents", "Resources", "osx")
|
|
147
|
+
const bySeries = new Map()
|
|
148
|
+
for (const entry of readdirSync(osx, { withFileTypes: true })) {
|
|
149
|
+
// CADBridge-R25.1.bundle -> series R25, minor 1.
|
|
150
|
+
const match = /^CADBridge-R(\d+)\.(\d+)\.bundle$/.exec(entry.name)
|
|
151
|
+
if (!entry.isDirectory() || !match) continue
|
|
152
|
+
const [, series, minor] = match
|
|
153
|
+
const previous = bySeries.get(series)
|
|
154
|
+
// Highest minor wins: it is built against the newest SDK of the series and
|
|
155
|
+
// serves every release in it.
|
|
156
|
+
if (!previous || Number(minor) > previous.minor) {
|
|
157
|
+
bySeries.set(series, { minor: Number(minor), from: join(osx, entry.name) })
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [...bySeries]
|
|
161
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
162
|
+
.map(([series, { from }]) => ({ name: `CADBridge-R${series}.bundle`, from }))
|
|
163
|
+
}
|
|
164
|
+
|
|
123
165
|
// A directory cannot be swapped the way installBinary swaps a file: rename
|
|
124
166
|
// onto a non-empty directory fails on both platforms. So stage a complete
|
|
125
167
|
// copy, move the old tree aside, move the new one in, and only then delete —
|
|
@@ -142,8 +184,9 @@ export function installPlugin(binary, {
|
|
|
142
184
|
let version
|
|
143
185
|
let manifest
|
|
144
186
|
try {
|
|
145
|
-
// PackageContents.xml is the file AutoCAD itself reads,
|
|
146
|
-
//
|
|
187
|
+
// PackageContents.xml is the file AutoCAD itself reads on Windows, and on
|
|
188
|
+
// both platforms it is what states the version and the releases served, so
|
|
189
|
+
// its absence means there is no loadable bundle here whatever else shipped.
|
|
147
190
|
manifest = readFileSync(join(source, "PackageContents.xml"), "utf8")
|
|
148
191
|
version = /AppVersion="([^"]*)"/.exec(manifest)?.[1] ?? "unknown"
|
|
149
192
|
} catch {
|
|
@@ -152,57 +195,74 @@ export function installPlugin(binary, {
|
|
|
152
195
|
return { status: "absent" }
|
|
153
196
|
}
|
|
154
197
|
|
|
155
|
-
|
|
198
|
+
let modules
|
|
199
|
+
try {
|
|
200
|
+
modules = pluginModules(source, { platform })
|
|
201
|
+
} catch {
|
|
202
|
+
return { status: "absent" }
|
|
203
|
+
}
|
|
204
|
+
if (!modules.length) return { status: "absent" }
|
|
205
|
+
|
|
206
|
+
const destination = platform === "darwin" ? root : join(root, "CADBridge.bundle")
|
|
207
|
+
// Windows keeps its stamp inside the container, beside PackageContents.xml.
|
|
208
|
+
// macOS cannot: there is no container, and a file added inside a module would
|
|
209
|
+
// invalidate the signature Gatekeeper and AutoCAD both check — so the stamp
|
|
210
|
+
// sits beside the modules, where AutoCAD ignores it for not being a .bundle.
|
|
211
|
+
const stamp = platform === "darwin" ? join(root, STAMP) : join(destination, STAMP)
|
|
156
212
|
const digest = hashTree(source)
|
|
157
213
|
const covers = { manifest, installedAutoCAD: autocad ?? installedAutoCAD({ platform }) }
|
|
158
|
-
if (
|
|
214
|
+
if (installedStamp(stamp)?.sha256 === digest) return { status: "current", version, destination, ...covers }
|
|
159
215
|
|
|
160
216
|
mkdirSync(root, { recursive: true })
|
|
161
|
-
// One temp directory holds both the new
|
|
162
|
-
// moved out of the way, so a single cleanup covers both and no name has
|
|
163
|
-
// derived from the pid — a pid-derived name collides with the leftovers
|
|
164
|
-
// run that was killed mid-swap, and a rename onto that stale directory
|
|
165
|
-
// with exactly the codes that mean "AutoCAD has it open", wedging every
|
|
166
|
-
// launch behind advice that cannot help.
|
|
217
|
+
// One temp directory holds both the new trees being assembled and the old
|
|
218
|
+
// ones moved out of the way, so a single cleanup covers both and no name has
|
|
219
|
+
// to be derived from the pid — a pid-derived name collides with the leftovers
|
|
220
|
+
// of a run that was killed mid-swap, and a rename onto that stale directory
|
|
221
|
+
// fails with exactly the codes that mean "AutoCAD has it open", wedging every
|
|
222
|
+
// later launch behind advice that cannot help.
|
|
167
223
|
const staging = mkdtempSync(join(root, ".cadbridge-staging-"))
|
|
168
|
-
|
|
169
|
-
// Set only when the previous bundle could NOT be put back, in which case this
|
|
224
|
+
// Set only when a previous bundle could NOT be put back, in which case this
|
|
170
225
|
// directory holds the user's sole copy and must survive the cleanup below.
|
|
171
226
|
let stranded = null
|
|
172
227
|
try {
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
228
|
+
for (const [index, module] of modules.entries()) {
|
|
229
|
+
const staged = join(staging, module.name)
|
|
230
|
+
const displaced = join(staging, `displaced-${index}`)
|
|
231
|
+
const target = join(root, module.name)
|
|
232
|
+
cpSync(module.from, staged, { recursive: true })
|
|
176
233
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
234
|
+
let moved = false
|
|
235
|
+
try {
|
|
236
|
+
renameSync(target, displaced)
|
|
237
|
+
moved = true
|
|
238
|
+
} catch (error) {
|
|
239
|
+
if (error.code !== "ENOENT") throw error
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
renameSync(staged, target)
|
|
243
|
+
} catch (error) {
|
|
244
|
+
// Leave the working plugin in place rather than a half-installed one.
|
|
245
|
+
if (moved) {
|
|
246
|
+
try {
|
|
247
|
+
renameSync(displaced, target)
|
|
248
|
+
} catch {
|
|
249
|
+
// Both moves failed, so the old bundle is still in the temp
|
|
250
|
+
// directory and is now the only copy there is. Keep it and say
|
|
251
|
+
// where, rather than deleting the user's working plugin.
|
|
252
|
+
stranded = displaced
|
|
253
|
+
}
|
|
196
254
|
}
|
|
255
|
+
throw error
|
|
197
256
|
}
|
|
198
|
-
throw error
|
|
199
257
|
}
|
|
258
|
+
// Written last: a stamp is a claim that every module above is in place.
|
|
259
|
+
writeFileSync(stamp, `${JSON.stringify({ sha256: digest, version }, null, 2)}\n`)
|
|
200
260
|
return { status: "installed", version, destination, ...covers }
|
|
201
261
|
} catch (error) {
|
|
202
|
-
// AutoCAD maps
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
262
|
+
// AutoCAD maps the plugin for the whole session, and Windows will not let a
|
|
263
|
+
// mapped module be renamed or deleted. That is the ordinary case of "the
|
|
264
|
+
// user started AutoCAD first", not a failure worth refusing to start the
|
|
265
|
+
// server over.
|
|
206
266
|
const status = !stranded && ["EBUSY", "EPERM", "EACCES", "ENOTEMPTY"].includes(error.code) ? "locked" : "failed"
|
|
207
267
|
return { status, version, destination, stranded, error, ...covers }
|
|
208
268
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dmytro-prototypes/cadbridge-mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
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,8 +30,8 @@
|
|
|
30
30
|
"LICENSE.txt"
|
|
31
31
|
],
|
|
32
32
|
"optionalDependencies": {
|
|
33
|
-
"@dmytro-prototypes/cadbridge-mcp-darwin-arm64": "0.1.
|
|
34
|
-
"@dmytro-prototypes/cadbridge-mcp-win32-x64": "0.1.
|
|
33
|
+
"@dmytro-prototypes/cadbridge-mcp-darwin-arm64": "0.1.15",
|
|
34
|
+
"@dmytro-prototypes/cadbridge-mcp-win32-x64": "0.1.15"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|
|
37
37
|
"access": "public"
|