From ca61bc011eec794daf00c4b1bceebe7dbdadc23d Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 9 May 2026 00:21:15 +0000 Subject: [PATCH 1/7] Bundle mdsmith binary from npm into VS Code extension - Add @mdsmith/cli as optional dependency in package.json - Create binary.ts module to resolve bundled binary path - Update extension.ts to use bundled binary as fallback when mdsmith.path is the default 'mdsmith' string - Update .vscodeignore to ship bundled binary in .vsix - Update README and package.json descriptions to document bundling - Add unit tests for binary resolution logic The extension now bundles the mdsmith binary from npm, eliminating the need for manual installation in most cases. When the optional dependency install fails (proxies, offline), the extension falls back to PATH resolution. Co-Authored-By: Claude Sonnet 4.5 Co-authored-by: jeduden <1117699+jeduden@users.noreply.github.com> --- editors/vscode/.vscodeignore | 3 ++ editors/vscode/README.md | 31 +++++++----- editors/vscode/package.json | 5 +- editors/vscode/src/binary.test.ts | 84 +++++++++++++++++++++++++++++++ editors/vscode/src/binary.ts | 46 +++++++++++++++++ editors/vscode/src/extension.ts | 4 +- 6 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 editors/vscode/src/binary.test.ts create mode 100644 editors/vscode/src/binary.ts diff --git a/editors/vscode/.vscodeignore b/editors/vscode/.vscodeignore index b393729de..ebfbdcaad 100644 --- a/editors/vscode/.vscodeignore +++ b/editors/vscode/.vscodeignore @@ -5,5 +5,8 @@ tsconfig.json build.ts bun.lock node_modules/** +!node_modules/.bin/mdsmith +!node_modules/.bin/mdsmith.cmd +!node_modules/@mdsmith/** **/*.map **/*.ts diff --git a/editors/vscode/README.md b/editors/vscode/README.md index cb775548a..74caa0eb6 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -9,12 +9,17 @@ runs `mdsmith fix` on the whole buffer. ## Prerequisites -- The `mdsmith` binary on `$PATH`, or a path you supply via - the `mdsmith.path` setting. Install with - `go install github.com/jeduden/mdsmith/cmd/mdsmith@latest` - or download from the - [releases page](https://github.com/jeduden/mdsmith/releases). -- VS Code 1.85 or later. +- **VS Code 1.85 or later.** +- **The `mdsmith` binary** — the extension bundles the binary + from npm as an optional dependency, so no separate install + is required in most cases. If the bundled binary fails to + install (corporate proxies, offline environments), you can + still install `mdsmith` manually: + - `npm install -g @mdsmith/cli` + - `go install github.com/jeduden/mdsmith/cmd/mdsmith@latest` + - Download from the + [releases page](https://github.com/jeduden/mdsmith/releases) + - Then configure `mdsmith.path` to point to the binary. ## Install @@ -24,13 +29,13 @@ code --install-extension mdsmith-.vsix ## Settings -| Setting | Default | Purpose | -|------------------------|-------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `mdsmith.path` | `"mdsmith"` | Binary path; resolved against the extension-host PATH (use an absolute path if `which mdsmith` works in your terminal but the extension reports `spawn ENOENT` — `~/.bashrc`/`~/.zshrc` are not sourced) | -| `mdsmith.config` | `""` | Override `-c` config path | -| `mdsmith.run` | `"onSave"` | When to lint: `onType`, `onSave`, or `off` | -| `mdsmith.fixOnSave` | `false` | Wires `source.fixAll.mdsmith` on save | -| `mdsmith.trace.server` | `"off"` | LSP trace verbosity | +| Setting | Default | Purpose | +|------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------| +| `mdsmith.path` | `"mdsmith"` | Binary path; defaults to bundled binary. Falls back to PATH resolution. Set absolute path if needed (e.g. `/go/bin/mdsmith`) | +| `mdsmith.config` | `""` | Override `-c` config path | +| `mdsmith.run` | `"onSave"` | When to lint: `onType`, `onSave`, or `off` | +| `mdsmith.fixOnSave` | `false` | Wires `source.fixAll.mdsmith` on save | +| `mdsmith.trace.server` | `"off"` | LSP trace verbosity | See the [full guide](https://github.com/jeduden/mdsmith/blob/main/docs/guides/editors/vscode.md) diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 5a168d099..82a3089cb 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -47,7 +47,7 @@ "mdsmith.path": { "type": "string", "default": "mdsmith", - "description": "Path to the mdsmith binary. A bare name is resolved against VS Code's extension-host PATH, which is the container/login-shell environment — interactive-only files like ~/.bashrc and ~/.zshrc are NOT sourced. If `which mdsmith` works in a terminal but the extension reports `spawn ENOENT`, set this to an absolute path (e.g. /go/bin/mdsmith) or symlink the binary into /usr/local/bin." + "description": "Path to the mdsmith binary. Defaults to the bundled binary from @mdsmith/cli. If the bundled binary is unavailable, falls back to resolving 'mdsmith' against PATH. Set to an absolute path if you installed mdsmith elsewhere (e.g. /go/bin/mdsmith)." }, "mdsmith.config": { "type": "string", @@ -84,6 +84,9 @@ "dependencies": { "vscode-languageclient": "^9.0.1" }, + "optionalDependencies": { + "@mdsmith/cli": "0.0.0-dev" + }, "devDependencies": { "@types/bun": "^1.1.0", "@types/node": "^20.11.0", diff --git a/editors/vscode/src/binary.test.ts b/editors/vscode/src/binary.test.ts new file mode 100644 index 000000000..09b7780b5 --- /dev/null +++ b/editors/vscode/src/binary.test.ts @@ -0,0 +1,84 @@ +// Unit tests for binary resolution logic. + +import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; +import { resolveBinary } from "./binary"; + +// Mock existsSync so tests can inject arbitrary "file exists" results +// without touching the real filesystem. beforeEach saves the original, +// tests replace it with a mock, afterEach restores it. +const originalExistsSync = existsSync; +let mockExistsSync: typeof existsSync; + +beforeEach(() => { + mockExistsSync = mock(() => false); + (global as any).existsSync = mockExistsSync; +}); + +afterEach(() => { + (global as any).existsSync = originalExistsSync; +}); + +describe("resolveBinary", () => { + test("returns custom path unchanged when user specifies non-default", () => { + const result = resolveBinary("/custom/path/to/mdsmith", "/ext"); + expect(result).toBe("/custom/path/to/mdsmith"); + // Should not even attempt to check for bundled binary + expect(mockExistsSync).not.toHaveBeenCalled(); + }); + + test("returns bundled binary when default path and bundled exists (Unix)", () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "linux", writable: true }); + + const extensionPath = "/ext"; + const bundledPath = join(extensionPath, "node_modules", ".bin", "mdsmith"); + + // Mock: bundled binary exists + mockExistsSync = mock((path) => path === bundledPath) as any; + (global as any).existsSync = mockExistsSync; + + const result = resolveBinary("mdsmith", extensionPath); + expect(result).toBe(bundledPath); + expect(mockExistsSync).toHaveBeenCalledWith(bundledPath); + + Object.defineProperty(process, "platform", { value: originalPlatform, writable: true }); + }); + + test("returns bundled binary when default path and bundled exists (Windows)", () => { + const originalPlatform = process.platform; + Object.defineProperty(process, "platform", { value: "win32", writable: true }); + + const extensionPath = "/ext"; + const bundledPath = join(extensionPath, "node_modules", ".bin", "mdsmith.cmd"); + + // Mock: bundled binary exists + mockExistsSync = mock((path) => path === bundledPath) as any; + (global as any).existsSync = mockExistsSync; + + const result = resolveBinary("mdsmith", extensionPath); + expect(result).toBe(bundledPath); + expect(mockExistsSync).toHaveBeenCalledWith(bundledPath); + + Object.defineProperty(process, "platform", { value: originalPlatform, writable: true }); + }); + + test("falls back to default path when bundled binary does not exist", () => { + // Mock: no bundled binary + mockExistsSync = mock(() => false); + (global as any).existsSync = mockExistsSync; + + const result = resolveBinary("mdsmith", "/ext"); + expect(result).toBe("mdsmith"); + // Should have checked for bundled binary + expect(mockExistsSync).toHaveBeenCalled(); + }); + + test("returns custom bare name unchanged", () => { + const result = resolveBinary("my-mdsmith-fork", "/ext"); + expect(result).toBe("my-mdsmith-fork"); + // Should not check for bundled binary when not the default + expect(mockExistsSync).not.toHaveBeenCalled(); + }); +}); diff --git a/editors/vscode/src/binary.ts b/editors/vscode/src/binary.ts new file mode 100644 index 000000000..481f9ce47 --- /dev/null +++ b/editors/vscode/src/binary.ts @@ -0,0 +1,46 @@ +// Binary resolution logic for the mdsmith extension. +// Resolves the mdsmith binary path, trying the bundled version as a +// fallback when the user-configured path is the bare "mdsmith" string. + +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +// resolveBinary returns the path to the mdsmith binary. When the +// configured path is the bare string "mdsmith", it first checks +// whether the optional @mdsmith/cli dependency bundled a binary into +// node_modules/.bin/mdsmith (or the platform-specific wrapper). If +// that file exists, return its absolute path. Otherwise return the +// configured path unchanged so the LanguageClient spawns it and +// lets the shell resolve it against PATH (the original behavior). +// +// The extensionPath should be the vscode.ExtensionContext.extensionPath +// (the directory containing package.json and node_modules/). +export function resolveBinary(configuredPath: string, extensionPath: string): string { + // If the user specified a custom path (not the bare "mdsmith"), + // honor it exactly — they know what they want. + if (configuredPath !== "mdsmith") { + return configuredPath; + } + + // The user left the default "mdsmith". Check whether the optional + // dependency installed a bundled binary. @mdsmith/cli's bin wrapper + // lives at node_modules/.bin/mdsmith (Unix) or + // node_modules/.bin/mdsmith.cmd (Windows). Node package managers + // (npm, pnpm, yarn, bun) all populate .bin/ symlinks/wrappers for + // bin entries; we can rely on that convention. + const binDir = join(extensionPath, "node_modules", ".bin"); + const unixBin = join(binDir, "mdsmith"); + const winBin = join(binDir, "mdsmith.cmd"); + + // Prefer the platform-appropriate wrapper if it exists. + const candidate = process.platform === "win32" ? winBin : unixBin; + if (existsSync(candidate)) { + return candidate; + } + + // The bundled binary does not exist (optional dependency install + // failed, or this is a dev build without `bun install`). Fall back + // to the bare "mdsmith" string so the LanguageClient resolves it + // against the shell PATH (same as before bundling). + return configuredPath; +} diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index 0dac06d0d..05f4d707c 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -18,6 +18,7 @@ import { collectFixAllEdits, startupErrorMessage } from "./wiring"; +import { resolveBinary } from "./binary"; let client: LanguageClient | undefined; // Track the .mdsmith.yml file watcher across the activate / @@ -74,7 +75,8 @@ export async function activate(context: vscode.ExtensionContext): Promise // must remain usable so the user can retry. async function startServer(context: vscode.ExtensionContext): Promise { const cfg = vscode.workspace.getConfiguration("mdsmith"); - const binary = cfg.get("path", "mdsmith"); + const configuredPath = cfg.get("path", "mdsmith"); + const binary = resolveBinary(configuredPath, context.extensionPath); const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; const serverOptions: ServerOptions = buildServerOptions( From af51d07ee79bfd0e7f668e2447e57e38a30aee50 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 9 May 2026 00:46:05 +0000 Subject: [PATCH 2/7] Fix comments to reflect single cross-platform extension install Agent-Logs-Url: https://github.com/jeduden/mdsmith/sessions/17cbae91-624a-47cc-b72a-9787298ed65d Co-authored-by: jeduden <1117699+jeduden@users.noreply.github.com> --- editors/vscode/README.md | 12 +++++------ editors/vscode/package.json | 2 +- editors/vscode/src/binary.test.ts | 3 +++ editors/vscode/src/binary.ts | 34 ++++++++++++++++++------------- 4 files changed, 30 insertions(+), 21 deletions(-) diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 74caa0eb6..2765c235d 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -10,11 +10,11 @@ runs `mdsmith fix` on the whole buffer. ## Prerequisites - **VS Code 1.85 or later.** -- **The `mdsmith` binary** — the extension bundles the binary - from npm as an optional dependency, so no separate install - is required in most cases. If the bundled binary fails to - install (corporate proxies, offline environments), you can - still install `mdsmith` manually: +- **The `mdsmith` binary** — the extension bundles a cross-platform + binary from npm that works on Linux, macOS, and Windows from a + single extension install. No separate binary install is required in + most cases. If the bundled binary fails to install (corporate + proxies, offline environments), you can install `mdsmith` manually: - `npm install -g @mdsmith/cli` - `go install github.com/jeduden/mdsmith/cmd/mdsmith@latest` - Download from the @@ -31,7 +31,7 @@ code --install-extension mdsmith-.vsix | Setting | Default | Purpose | |------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------| -| `mdsmith.path` | `"mdsmith"` | Binary path; defaults to bundled binary. Falls back to PATH resolution. Set absolute path if needed (e.g. `/go/bin/mdsmith`) | +| `mdsmith.path` | `"mdsmith"` | Binary path; defaults to bundled cross-platform binary. Falls back to PATH resolution. Set absolute path if needed (e.g. `/go/bin/mdsmith`) | | `mdsmith.config` | `""` | Override `-c` config path | | `mdsmith.run` | `"onSave"` | When to lint: `onType`, `onSave`, or `off` | | `mdsmith.fixOnSave` | `false` | Wires `source.fixAll.mdsmith` on save | diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 82a3089cb..43a566443 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -47,7 +47,7 @@ "mdsmith.path": { "type": "string", "default": "mdsmith", - "description": "Path to the mdsmith binary. Defaults to the bundled binary from @mdsmith/cli. If the bundled binary is unavailable, falls back to resolving 'mdsmith' against PATH. Set to an absolute path if you installed mdsmith elsewhere (e.g. /go/bin/mdsmith)." + "description": "Path to the mdsmith binary. Defaults to the bundled cross-platform binary from @mdsmith/cli (works on Linux, macOS, Windows from a single install). If the bundled binary is unavailable, falls back to resolving 'mdsmith' against PATH. Set to an absolute path if you installed mdsmith elsewhere (e.g. /go/bin/mdsmith)." }, "mdsmith.config": { "type": "string", diff --git a/editors/vscode/src/binary.test.ts b/editors/vscode/src/binary.test.ts index 09b7780b5..6e49708ef 100644 --- a/editors/vscode/src/binary.test.ts +++ b/editors/vscode/src/binary.test.ts @@ -1,4 +1,7 @@ // Unit tests for binary resolution logic. +// The extension bundles a cross-platform mdsmith binary that works on +// all platforms from a single .vsix install; these tests verify the +// fallback behavior when bundling fails. import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import { existsSync } from "node:fs"; diff --git a/editors/vscode/src/binary.ts b/editors/vscode/src/binary.ts index 481f9ce47..2b503d976 100644 --- a/editors/vscode/src/binary.ts +++ b/editors/vscode/src/binary.ts @@ -1,17 +1,22 @@ // Binary resolution logic for the mdsmith extension. -// Resolves the mdsmith binary path, trying the bundled version as a -// fallback when the user-configured path is the bare "mdsmith" string. +// The extension bundles a cross-platform mdsmith binary from npm that +// works on all supported platforms (Linux, macOS, Windows) via a single +// .vsix install. This module resolves the bundled binary when the user +// leaves the default "mdsmith" path, falling back to PATH if bundling +// failed. import { existsSync } from "node:fs"; import { join } from "node:path"; // resolveBinary returns the path to the mdsmith binary. When the -// configured path is the bare string "mdsmith", it first checks -// whether the optional @mdsmith/cli dependency bundled a binary into -// node_modules/.bin/mdsmith (or the platform-specific wrapper). If -// that file exists, return its absolute path. Otherwise return the -// configured path unchanged so the LanguageClient spawns it and -// lets the shell resolve it against PATH (the original behavior). +// configured path is the bare string "mdsmith", it first checks for +// the bundled binary at node_modules/.bin/mdsmith (Unix) or +// node_modules/.bin/mdsmith.cmd (Windows). The @mdsmith/cli npm package +// ships with platform-specific binaries as optional dependencies, so +// a single extension install works on all platforms. If the bundled +// binary exists, return its absolute path. Otherwise return the +// configured path unchanged so the LanguageClient resolves it against +// PATH (fallback for proxy/offline install failures). // // The extensionPath should be the vscode.ExtensionContext.extensionPath // (the directory containing package.json and node_modules/). @@ -22,12 +27,13 @@ export function resolveBinary(configuredPath: string, extensionPath: string): st return configuredPath; } - // The user left the default "mdsmith". Check whether the optional - // dependency installed a bundled binary. @mdsmith/cli's bin wrapper - // lives at node_modules/.bin/mdsmith (Unix) or - // node_modules/.bin/mdsmith.cmd (Windows). Node package managers - // (npm, pnpm, yarn, bun) all populate .bin/ symlinks/wrappers for - // bin entries; we can rely on that convention. + // The user left the default "mdsmith". Check for the bundled binary + // from @mdsmith/cli. The npm package ships with platform-specific + // binaries (linux-x64, darwin-arm64, win32-x64, etc.) as optional + // dependencies; npm installs the correct one for the current platform + // during extension install. The bin wrapper lives at + // node_modules/.bin/mdsmith (Unix) or node_modules/.bin/mdsmith.cmd + // (Windows). This works across all platforms with a single .vsix. const binDir = join(extensionPath, "node_modules", ".bin"); const unixBin = join(binDir, "mdsmith"); const winBin = join(binDir, "mdsmith.cmd"); From 05b8198a191de016e98a463ac680bba4dfb79c81 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 9 May 2026 07:40:58 +0000 Subject: [PATCH 3/7] Refactor binary resolution to be testable without mocking globals - Add optional platform and fileExists parameters to resolveBinary - Update tests to pass parameters instead of mocking process.platform and global.existsSync - Clarify cross-platform bundling behavior in comments (npm installs ALL optional deps during packaging) Agent-Logs-Url: https://github.com/jeduden/mdsmith/sessions/cc332da0-56a7-4c58-8486-2cbcf0a9db86 Co-authored-by: jeduden <1117699+jeduden@users.noreply.github.com> --- editors/vscode/src/binary.test.ts | 59 +++++++++---------------------- editors/vscode/src/binary.ts | 25 ++++++++----- 2 files changed, 33 insertions(+), 51 deletions(-) diff --git a/editors/vscode/src/binary.test.ts b/editors/vscode/src/binary.test.ts index 6e49708ef..e3fc16651 100644 --- a/editors/vscode/src/binary.test.ts +++ b/editors/vscode/src/binary.test.ts @@ -3,85 +3,58 @@ // all platforms from a single .vsix install; these tests verify the // fallback behavior when bundling fails. -import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; -import { existsSync } from "node:fs"; +import { describe, expect, mock, test } from "bun:test"; import { join } from "node:path"; import { resolveBinary } from "./binary"; -// Mock existsSync so tests can inject arbitrary "file exists" results -// without touching the real filesystem. beforeEach saves the original, -// tests replace it with a mock, afterEach restores it. -const originalExistsSync = existsSync; -let mockExistsSync: typeof existsSync; - -beforeEach(() => { - mockExistsSync = mock(() => false); - (global as any).existsSync = mockExistsSync; -}); - -afterEach(() => { - (global as any).existsSync = originalExistsSync; -}); - describe("resolveBinary", () => { test("returns custom path unchanged when user specifies non-default", () => { - const result = resolveBinary("/custom/path/to/mdsmith", "/ext"); + const fileExists = mock(() => false); + const result = resolveBinary("/custom/path/to/mdsmith", "/ext", "linux", fileExists); expect(result).toBe("/custom/path/to/mdsmith"); // Should not even attempt to check for bundled binary - expect(mockExistsSync).not.toHaveBeenCalled(); + expect(fileExists).not.toHaveBeenCalled(); }); test("returns bundled binary when default path and bundled exists (Unix)", () => { - const originalPlatform = process.platform; - Object.defineProperty(process, "platform", { value: "linux", writable: true }); - const extensionPath = "/ext"; const bundledPath = join(extensionPath, "node_modules", ".bin", "mdsmith"); // Mock: bundled binary exists - mockExistsSync = mock((path) => path === bundledPath) as any; - (global as any).existsSync = mockExistsSync; + const fileExists = mock((path) => path === bundledPath); - const result = resolveBinary("mdsmith", extensionPath); + const result = resolveBinary("mdsmith", extensionPath, "linux", fileExists); expect(result).toBe(bundledPath); - expect(mockExistsSync).toHaveBeenCalledWith(bundledPath); - - Object.defineProperty(process, "platform", { value: originalPlatform, writable: true }); + expect(fileExists).toHaveBeenCalledWith(bundledPath); }); test("returns bundled binary when default path and bundled exists (Windows)", () => { - const originalPlatform = process.platform; - Object.defineProperty(process, "platform", { value: "win32", writable: true }); - const extensionPath = "/ext"; const bundledPath = join(extensionPath, "node_modules", ".bin", "mdsmith.cmd"); // Mock: bundled binary exists - mockExistsSync = mock((path) => path === bundledPath) as any; - (global as any).existsSync = mockExistsSync; + const fileExists = mock((path) => path === bundledPath); - const result = resolveBinary("mdsmith", extensionPath); + const result = resolveBinary("mdsmith", extensionPath, "win32", fileExists); expect(result).toBe(bundledPath); - expect(mockExistsSync).toHaveBeenCalledWith(bundledPath); - - Object.defineProperty(process, "platform", { value: originalPlatform, writable: true }); + expect(fileExists).toHaveBeenCalledWith(bundledPath); }); test("falls back to default path when bundled binary does not exist", () => { // Mock: no bundled binary - mockExistsSync = mock(() => false); - (global as any).existsSync = mockExistsSync; + const fileExists = mock(() => false); - const result = resolveBinary("mdsmith", "/ext"); + const result = resolveBinary("mdsmith", "/ext", "linux", fileExists); expect(result).toBe("mdsmith"); // Should have checked for bundled binary - expect(mockExistsSync).toHaveBeenCalled(); + expect(fileExists).toHaveBeenCalled(); }); test("returns custom bare name unchanged", () => { - const result = resolveBinary("my-mdsmith-fork", "/ext"); + const fileExists = mock(() => false); + const result = resolveBinary("my-mdsmith-fork", "/ext", "linux", fileExists); expect(result).toBe("my-mdsmith-fork"); // Should not check for bundled binary when not the default - expect(mockExistsSync).not.toHaveBeenCalled(); + expect(fileExists).not.toHaveBeenCalled(); }); }); diff --git a/editors/vscode/src/binary.ts b/editors/vscode/src/binary.ts index 2b503d976..777271bd9 100644 --- a/editors/vscode/src/binary.ts +++ b/editors/vscode/src/binary.ts @@ -20,7 +20,15 @@ import { join } from "node:path"; // // The extensionPath should be the vscode.ExtensionContext.extensionPath // (the directory containing package.json and node_modules/). -export function resolveBinary(configuredPath: string, extensionPath: string): string { +// +// The optional platform and fileExists parameters are for testing; in +// production they default to process.platform and fs.existsSync. +export function resolveBinary( + configuredPath: string, + extensionPath: string, + platform: string = process.platform, + fileExists: (path: string) => boolean = existsSync +): string { // If the user specified a custom path (not the bare "mdsmith"), // honor it exactly — they know what they want. if (configuredPath !== "mdsmith") { @@ -30,22 +38,23 @@ export function resolveBinary(configuredPath: string, extensionPath: string): st // The user left the default "mdsmith". Check for the bundled binary // from @mdsmith/cli. The npm package ships with platform-specific // binaries (linux-x64, darwin-arm64, win32-x64, etc.) as optional - // dependencies; npm installs the correct one for the current platform - // during extension install. The bin wrapper lives at - // node_modules/.bin/mdsmith (Unix) or node_modules/.bin/mdsmith.cmd - // (Windows). This works across all platforms with a single .vsix. + // dependencies; npm installs ALL of them during packaging (regardless + // of build platform), so a single .vsix works on Linux, macOS, and + // Windows. The bin wrapper (mdsmith.js) selects the correct binary + // at runtime. The wrapper lives at node_modules/.bin/mdsmith (Unix) + // or node_modules/.bin/mdsmith.cmd (Windows). const binDir = join(extensionPath, "node_modules", ".bin"); const unixBin = join(binDir, "mdsmith"); const winBin = join(binDir, "mdsmith.cmd"); // Prefer the platform-appropriate wrapper if it exists. - const candidate = process.platform === "win32" ? winBin : unixBin; - if (existsSync(candidate)) { + const candidate = platform === "win32" ? winBin : unixBin; + if (fileExists(candidate)) { return candidate; } // The bundled binary does not exist (optional dependency install - // failed, or this is a dev build without `bun install`). Fall back + // failed, or this is a dev build without npm install). Fall back // to the bare "mdsmith" string so the LanguageClient resolves it // against the shell PATH (same as before bundling). return configuredPath; From ba928f002ff56a1d0e55cc46112fac2389a98c7c Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 9 May 2026 08:29:17 +0000 Subject: [PATCH 4/7] Implement proper binary bundling via build-time copy to dist/bin/ - Update build.ts to copy platform binaries from @mdsmith/* to dist/bin/ - Update binary.ts to resolve from dist/bin/{platform}-{arch}-{binary} - Update binary.test.ts to test new resolution with platform+arch params - Update .vscodeignore to remove node_modules inclusions (dist/ is included by default) - Binaries now ship in .vsix even with --no-dependencies flag Agent-Logs-Url: https://github.com/jeduden/mdsmith/sessions/9b3811b3-360e-4209-97ad-c9d8af603b87 Co-authored-by: jeduden <1117699+jeduden@users.noreply.github.com> --- editors/vscode/.vscodeignore | 3 -- editors/vscode/build.ts | 45 +++++++++++++++++++- editors/vscode/src/binary.test.ts | 36 ++++++++++------ editors/vscode/src/binary.ts | 68 ++++++++++++++++--------------- 4 files changed, 104 insertions(+), 48 deletions(-) diff --git a/editors/vscode/.vscodeignore b/editors/vscode/.vscodeignore index ebfbdcaad..b393729de 100644 --- a/editors/vscode/.vscodeignore +++ b/editors/vscode/.vscodeignore @@ -5,8 +5,5 @@ tsconfig.json build.ts bun.lock node_modules/** -!node_modules/.bin/mdsmith -!node_modules/.bin/mdsmith.cmd -!node_modules/@mdsmith/** **/*.map **/*.ts diff --git a/editors/vscode/build.ts b/editors/vscode/build.ts index 145016dc5..ca766eac2 100644 --- a/editors/vscode/build.ts +++ b/editors/vscode/build.ts @@ -2,8 +2,10 @@ // Bundles src/extension.ts into dist/extension.js as a single CJS // file consumed by VS Code, marking `vscode` as external because // the host supplies it at runtime. +// Also copies platform binaries from @mdsmith/* packages into dist/bin/ +// so they can be bundled in the .vsix (even with --no-dependencies). -import { copyFileSync, existsSync } from "node:fs"; +import { copyFileSync, existsSync, mkdirSync, readdirSync } from "node:fs"; import { join } from "node:path"; const args = Bun.argv.slice(2); @@ -22,6 +24,47 @@ if (existsSync(repoLicense)) { copyFileSync(repoLicense, stagedLicense); } +// Copy platform binaries from @mdsmith/* packages into dist/bin/ +// so they ship in the .vsix even with vsce package --no-dependencies. +// The npm packages install as optional dependencies; when present, +// bundle them. When absent (offline install, proxy), the extension +// falls back to PATH resolution. +function copyPlatformBinaries() { + const distBin = join(import.meta.dir, "dist", "bin"); + mkdirSync(distBin, { recursive: true }); + + // Platform packages that @mdsmith/cli declares as optionalDependencies + const platforms = [ + { pkg: "@mdsmith/linux-x64", binary: "mdsmith" }, + { pkg: "@mdsmith/linux-arm64", binary: "mdsmith" }, + { pkg: "@mdsmith/darwin-x64", binary: "mdsmith" }, + { pkg: "@mdsmith/darwin-arm64", binary: "mdsmith" }, + { pkg: "@mdsmith/win32-x64", binary: "mdsmith.exe" }, + ]; + + let copied = 0; + for (const { pkg, binary } of platforms) { + const srcBin = join(import.meta.dir, "node_modules", pkg, "bin", binary); + if (existsSync(srcBin)) { + const destBin = join(distBin, `${pkg.replace("@mdsmith/", "")}-${binary}`); + copyFileSync(srcBin, destBin); + copied++; + } + } + + if (copied > 0) { + console.log(`copied ${copied} platform binary/binaries → dist/bin/`); + } else { + console.warn( + "warning: no platform binaries found in node_modules/@mdsmith/; " + + "extension will fall back to PATH resolution. Run `npm install` " + + "to bundle binaries." + ); + } +} + +copyPlatformBinaries(); + const config: Parameters[0] = { entrypoints: ["src/extension.ts"], outdir: "dist", diff --git a/editors/vscode/src/binary.test.ts b/editors/vscode/src/binary.test.ts index e3fc16651..fd1b9b5e7 100644 --- a/editors/vscode/src/binary.test.ts +++ b/editors/vscode/src/binary.test.ts @@ -1,7 +1,7 @@ // Unit tests for binary resolution logic. -// The extension bundles a cross-platform mdsmith binary that works on -// all platforms from a single .vsix install; these tests verify the -// fallback behavior when bundling fails. +// The extension bundles cross-platform mdsmith binaries into dist/bin/ +// during the build step; these tests verify the resolution logic and +// fallback behavior. import { describe, expect, mock, test } from "bun:test"; import { join } from "node:path"; @@ -10,32 +10,44 @@ import { resolveBinary } from "./binary"; describe("resolveBinary", () => { test("returns custom path unchanged when user specifies non-default", () => { const fileExists = mock(() => false); - const result = resolveBinary("/custom/path/to/mdsmith", "/ext", "linux", fileExists); + const result = resolveBinary("/custom/path/to/mdsmith", "/ext", "linux", "x64", fileExists); expect(result).toBe("/custom/path/to/mdsmith"); // Should not even attempt to check for bundled binary expect(fileExists).not.toHaveBeenCalled(); }); - test("returns bundled binary when default path and bundled exists (Unix)", () => { + test("returns bundled binary when default path and bundled exists (Linux x64)", () => { const extensionPath = "/ext"; - const bundledPath = join(extensionPath, "node_modules", ".bin", "mdsmith"); + const bundledPath = join(extensionPath, "dist", "bin", "linux-x64-mdsmith"); // Mock: bundled binary exists const fileExists = mock((path) => path === bundledPath); - const result = resolveBinary("mdsmith", extensionPath, "linux", fileExists); + const result = resolveBinary("mdsmith", extensionPath, "linux", "x64", fileExists); expect(result).toBe(bundledPath); expect(fileExists).toHaveBeenCalledWith(bundledPath); }); - test("returns bundled binary when default path and bundled exists (Windows)", () => { + test("returns bundled binary when default path and bundled exists (macOS arm64)", () => { const extensionPath = "/ext"; - const bundledPath = join(extensionPath, "node_modules", ".bin", "mdsmith.cmd"); + const bundledPath = join(extensionPath, "dist", "bin", "darwin-arm64-mdsmith"); // Mock: bundled binary exists const fileExists = mock((path) => path === bundledPath); - const result = resolveBinary("mdsmith", extensionPath, "win32", fileExists); + const result = resolveBinary("mdsmith", extensionPath, "darwin", "arm64", fileExists); + expect(result).toBe(bundledPath); + expect(fileExists).toHaveBeenCalledWith(bundledPath); + }); + + test("returns bundled binary when default path and bundled exists (Windows x64)", () => { + const extensionPath = "/ext"; + const bundledPath = join(extensionPath, "dist", "bin", "win32-x64-mdsmith.exe"); + + // Mock: bundled binary exists + const fileExists = mock((path) => path === bundledPath); + + const result = resolveBinary("mdsmith", extensionPath, "win32", "x64", fileExists); expect(result).toBe(bundledPath); expect(fileExists).toHaveBeenCalledWith(bundledPath); }); @@ -44,7 +56,7 @@ describe("resolveBinary", () => { // Mock: no bundled binary const fileExists = mock(() => false); - const result = resolveBinary("mdsmith", "/ext", "linux", fileExists); + const result = resolveBinary("mdsmith", "/ext", "linux", "x64", fileExists); expect(result).toBe("mdsmith"); // Should have checked for bundled binary expect(fileExists).toHaveBeenCalled(); @@ -52,7 +64,7 @@ describe("resolveBinary", () => { test("returns custom bare name unchanged", () => { const fileExists = mock(() => false); - const result = resolveBinary("my-mdsmith-fork", "/ext", "linux", fileExists); + const result = resolveBinary("my-mdsmith-fork", "/ext", "linux", "x64", fileExists); expect(result).toBe("my-mdsmith-fork"); // Should not check for bundled binary when not the default expect(fileExists).not.toHaveBeenCalled(); diff --git a/editors/vscode/src/binary.ts b/editors/vscode/src/binary.ts index 777271bd9..1e43a09f0 100644 --- a/editors/vscode/src/binary.ts +++ b/editors/vscode/src/binary.ts @@ -1,32 +1,37 @@ // Binary resolution logic for the mdsmith extension. -// The extension bundles a cross-platform mdsmith binary from npm that -// works on all supported platforms (Linux, macOS, Windows) via a single -// .vsix install. This module resolves the bundled binary when the user -// leaves the default "mdsmith" path, falling back to PATH if bundling -// failed. +// The extension bundles cross-platform mdsmith binaries from npm packages +// into dist/bin/ during the build step. This module resolves the correct +// platform binary when the user leaves the default "mdsmith" path, falling +// back to PATH if bundling failed or binaries are unavailable. import { existsSync } from "node:fs"; import { join } from "node:path"; // resolveBinary returns the path to the mdsmith binary. When the // configured path is the bare string "mdsmith", it first checks for -// the bundled binary at node_modules/.bin/mdsmith (Unix) or -// node_modules/.bin/mdsmith.cmd (Windows). The @mdsmith/cli npm package -// ships with platform-specific binaries as optional dependencies, so -// a single extension install works on all platforms. If the bundled +// bundled binaries in dist/bin/ (copied there by build.ts from the +// @mdsmith/* npm packages). Platform-specific binaries are named like +// "linux-x64-mdsmith", "win32-x64-mdsmith.exe", etc. If the bundled // binary exists, return its absolute path. Otherwise return the // configured path unchanged so the LanguageClient resolves it against -// PATH (fallback for proxy/offline install failures). +// PATH (fallback for dev builds or when optional deps failed to install). +// +// Cross-platform bundling: The build script copies binaries from ALL +// @mdsmith/* platform packages (linux-x64, darwin-arm64, win32-x64, etc.) +// into dist/bin/. This works even with `vsce package --no-dependencies` +// because dist/ is included in the .vsix. At runtime, this function +// selects the binary matching the user's OS+arch. // // The extensionPath should be the vscode.ExtensionContext.extensionPath -// (the directory containing package.json and node_modules/). +// (the directory containing package.json and dist/). // -// The optional platform and fileExists parameters are for testing; in -// production they default to process.platform and fs.existsSync. +// The optional platform, arch, and fileExists parameters are for testing; +// in production they default to process.platform, process.arch, and fs.existsSync. export function resolveBinary( configuredPath: string, extensionPath: string, platform: string = process.platform, + arch: string = process.arch, fileExists: (path: string) => boolean = existsSync ): string { // If the user specified a custom path (not the bare "mdsmith"), @@ -35,27 +40,26 @@ export function resolveBinary( return configuredPath; } - // The user left the default "mdsmith". Check for the bundled binary - // from @mdsmith/cli. The npm package ships with platform-specific - // binaries (linux-x64, darwin-arm64, win32-x64, etc.) as optional - // dependencies; npm installs ALL of them during packaging (regardless - // of build platform), so a single .vsix works on Linux, macOS, and - // Windows. The bin wrapper (mdsmith.js) selects the correct binary - // at runtime. The wrapper lives at node_modules/.bin/mdsmith (Unix) - // or node_modules/.bin/mdsmith.cmd (Windows). - const binDir = join(extensionPath, "node_modules", ".bin"); - const unixBin = join(binDir, "mdsmith"); - const winBin = join(binDir, "mdsmith.cmd"); + // The user left the default "mdsmith". Check for bundled binaries + // in dist/bin/. The build script copies them there with names like + // "linux-x64-mdsmith", "win32-x64-mdsmith.exe". + + // Map Node's process.platform and process.arch to our package names + const platformArch = `${platform}-${arch}`; + const binaryName = platform === "win32" ? "mdsmith.exe" : "mdsmith"; + const bundledBinary = join( + extensionPath, + "dist", + "bin", + `${platformArch}-${binaryName}` + ); - // Prefer the platform-appropriate wrapper if it exists. - const candidate = platform === "win32" ? winBin : unixBin; - if (fileExists(candidate)) { - return candidate; + if (fileExists(bundledBinary)) { + return bundledBinary; } - // The bundled binary does not exist (optional dependency install - // failed, or this is a dev build without npm install). Fall back - // to the bare "mdsmith" string so the LanguageClient resolves it - // against the shell PATH (same as before bundling). + // The bundled binary does not exist (build step didn't run, or + // optional dependencies weren't installed). Fall back to the bare + // "mdsmith" string so the LanguageClient resolves it against PATH. return configuredPath; } From 1646960e9fbd259b8df1cfa516b07aa44db8f3de Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 9 May 2026 08:30:12 +0000 Subject: [PATCH 5/7] Update documentation to reflect build-time binary bundling - Update README to explain binaries are copied to dist/bin/ during build - Update package.json setting description with accurate bundling details - Clarify that single .vsix works on all platforms via build-time copy Agent-Logs-Url: https://github.com/jeduden/mdsmith/sessions/9b3811b3-360e-4209-97ad-c9d8af603b87 Co-authored-by: jeduden <1117699+jeduden@users.noreply.github.com> --- editors/vscode/README.md | 16 ++++++++++------ editors/vscode/package.json | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 2765c235d..969a04270 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -10,11 +10,15 @@ runs `mdsmith fix` on the whole buffer. ## Prerequisites - **VS Code 1.85 or later.** -- **The `mdsmith` binary** — the extension bundles a cross-platform - binary from npm that works on Linux, macOS, and Windows from a - single extension install. No separate binary install is required in - most cases. If the bundled binary fails to install (corporate - proxies, offline environments), you can install `mdsmith` manually: +- **The `mdsmith` binary** — the extension bundles pre-built binaries + for all platforms (Linux, macOS, Windows) from npm. The build step + copies platform binaries from the `@mdsmith/*` npm packages into + `dist/bin/`, so they ship in the .vsix and work on all platforms + from a single install. No separate binary install is required in + most cases. + + If the bundled binary is unavailable or you prefer a custom build, + you can install `mdsmith` manually: - `npm install -g @mdsmith/cli` - `go install github.com/jeduden/mdsmith/cmd/mdsmith@latest` - Download from the @@ -31,7 +35,7 @@ code --install-extension mdsmith-.vsix | Setting | Default | Purpose | |------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------| -| `mdsmith.path` | `"mdsmith"` | Binary path; defaults to bundled cross-platform binary. Falls back to PATH resolution. Set absolute path if needed (e.g. `/go/bin/mdsmith`) | +| `mdsmith.path` | `"mdsmith"` | Binary path; defaults to bundled binary in dist/bin/. Falls back to PATH resolution if bundled binary unavailable. Set absolute path if needed (e.g. `/go/bin/mdsmith`) | | `mdsmith.config` | `""` | Override `-c` config path | | `mdsmith.run` | `"onSave"` | When to lint: `onType`, `onSave`, or `off` | | `mdsmith.fixOnSave` | `false` | Wires `source.fixAll.mdsmith` on save | diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 43a566443..eeddf90be 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -47,7 +47,7 @@ "mdsmith.path": { "type": "string", "default": "mdsmith", - "description": "Path to the mdsmith binary. Defaults to the bundled cross-platform binary from @mdsmith/cli (works on Linux, macOS, Windows from a single install). If the bundled binary is unavailable, falls back to resolving 'mdsmith' against PATH. Set to an absolute path if you installed mdsmith elsewhere (e.g. /go/bin/mdsmith)." + "description": "Path to the mdsmith binary. Defaults to the bundled binary in dist/bin/ (copied from @mdsmith/* npm packages during build). Works on Linux, macOS, and Windows from a single .vsix. If the bundled binary is unavailable, falls back to resolving 'mdsmith' against PATH. Set to an absolute path if you installed mdsmith elsewhere (e.g. /go/bin/mdsmith)." }, "mdsmith.config": { "type": "string", From 3054e34e1f7478f20d9d355c09ee535c2073fba1 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 9 May 2026 08:54:46 +0000 Subject: [PATCH 6/7] Fix CI linting issues in VS Code extension README - Remove bold formatting from "VS Code 1.85 or later" to fix MDS018 - Auto-fix table formatting to fix MDS025 Agent-Logs-Url: https://github.com/jeduden/mdsmith/sessions/c36bb1ca-3452-44b2-8c07-7f4ca32aebea Co-authored-by: jeduden <1117699+jeduden@users.noreply.github.com> --- editors/vscode/README.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 969a04270..48ff66b6e 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -9,7 +9,7 @@ runs `mdsmith fix` on the whole buffer. ## Prerequisites -- **VS Code 1.85 or later.** +- VS Code 1.85 or later. - **The `mdsmith` binary** — the extension bundles pre-built binaries for all platforms (Linux, macOS, Windows) from npm. The build step copies platform binaries from the `@mdsmith/*` npm packages into @@ -33,13 +33,13 @@ code --install-extension mdsmith-.vsix ## Settings -| Setting | Default | Purpose | -|------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------| +| Setting | Default | Purpose | +|------------------------|-------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `mdsmith.path` | `"mdsmith"` | Binary path; defaults to bundled binary in dist/bin/. Falls back to PATH resolution if bundled binary unavailable. Set absolute path if needed (e.g. `/go/bin/mdsmith`) | -| `mdsmith.config` | `""` | Override `-c` config path | -| `mdsmith.run` | `"onSave"` | When to lint: `onType`, `onSave`, or `off` | -| `mdsmith.fixOnSave` | `false` | Wires `source.fixAll.mdsmith` on save | -| `mdsmith.trace.server` | `"off"` | LSP trace verbosity | +| `mdsmith.config` | `""` | Override `-c` config path | +| `mdsmith.run` | `"onSave"` | When to lint: `onType`, `onSave`, or `off` | +| `mdsmith.fixOnSave` | `false` | Wires `source.fixAll.mdsmith` on save | +| `mdsmith.trace.server` | `"off"` | LSP trace verbosity | See the [full guide](https://github.com/jeduden/mdsmith/blob/main/docs/guides/editors/vscode.md) From 41fd6de98c21d82e886cb85b552992a3e5dad333 Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Sat, 9 May 2026 09:04:53 +0000 Subject: [PATCH 7/7] Update docs and comments to reflect platform bundling limitation - Remove unused readdirSync import from build.ts - Update comments to clarify only host platform binary is bundled due to npm os/cpu constraints - Update warning message to reference bun instead of npm - Update README to accurately describe bundling (host platform only) - Update package.json setting description to reflect reality The @mdsmith/* platform packages have os/cpu constraints, so npm only installs the package matching the build host (typically linux-x64 in CI). Other platforms fall back to PATH and require manual installation. Agent-Logs-Url: https://github.com/jeduden/mdsmith/sessions/815aaf18-5770-4db2-b451-1f1430644ed4 Co-authored-by: jeduden <1117699+jeduden@users.noreply.github.com> --- editors/vscode/README.md | 17 +++++++---------- editors/vscode/build.ts | 20 ++++++++++++-------- editors/vscode/package.json | 2 +- editors/vscode/src/binary.ts | 24 +++++++++++++----------- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 48ff66b6e..b2acbe5d3 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -10,20 +10,17 @@ runs `mdsmith fix` on the whole buffer. ## Prerequisites - VS Code 1.85 or later. -- **The `mdsmith` binary** — the extension bundles pre-built binaries - for all platforms (Linux, macOS, Windows) from npm. The build step - copies platform binaries from the `@mdsmith/*` npm packages into - `dist/bin/`, so they ship in the .vsix and work on all platforms - from a single install. No separate binary install is required in - most cases. - - If the bundled binary is unavailable or you prefer a custom build, - you can install `mdsmith` manually: +- **The `mdsmith` binary** — the extension includes a bundled binary + for the host platform (typically Linux from CI builds). If you're on + the same platform as the build host, no separate install is required. + + For other platforms or if the bundled binary is unavailable, install + `mdsmith` manually: - `npm install -g @mdsmith/cli` - `go install github.com/jeduden/mdsmith/cmd/mdsmith@latest` - Download from the [releases page](https://github.com/jeduden/mdsmith/releases) - - Then configure `mdsmith.path` to point to the binary. + - Then optionally configure `mdsmith.path` to point to the binary. ## Install diff --git a/editors/vscode/build.ts b/editors/vscode/build.ts index ca766eac2..26c4ac969 100644 --- a/editors/vscode/build.ts +++ b/editors/vscode/build.ts @@ -3,9 +3,10 @@ // file consumed by VS Code, marking `vscode` as external because // the host supplies it at runtime. // Also copies platform binaries from @mdsmith/* packages into dist/bin/ -// so they can be bundled in the .vsix (even with --no-dependencies). +// when available. Note: npm platform packages have os/cpu constraints, +// so only the host platform binary will be bundled. -import { copyFileSync, existsSync, mkdirSync, readdirSync } from "node:fs"; +import { copyFileSync, existsSync, mkdirSync } from "node:fs"; import { join } from "node:path"; const args = Bun.argv.slice(2); @@ -25,15 +26,18 @@ if (existsSync(repoLicense)) { } // Copy platform binaries from @mdsmith/* packages into dist/bin/ -// so they ship in the .vsix even with vsce package --no-dependencies. -// The npm packages install as optional dependencies; when present, -// bundle them. When absent (offline install, proxy), the extension -// falls back to PATH resolution. +// when available. Note: The @mdsmith/* platform packages have os/cpu +// constraints, so npm only installs the package matching the build +// host's platform. This means only the host platform binary will be +// bundled (typically linux-x64 in CI). Other platforms fall back to +// PATH resolution. function copyPlatformBinaries() { const distBin = join(import.meta.dir, "dist", "bin"); mkdirSync(distBin, { recursive: true }); - // Platform packages that @mdsmith/cli declares as optionalDependencies + // Platform packages that @mdsmith/cli declares as optionalDependencies. + // Only the host platform package will actually be installed due to + // os/cpu constraints. const platforms = [ { pkg: "@mdsmith/linux-x64", binary: "mdsmith" }, { pkg: "@mdsmith/linux-arm64", binary: "mdsmith" }, @@ -57,7 +61,7 @@ function copyPlatformBinaries() { } else { console.warn( "warning: no platform binaries found in node_modules/@mdsmith/; " + - "extension will fall back to PATH resolution. Run `npm install` " + + "extension will fall back to PATH resolution. Run `bun install` " + "to bundle binaries." ); } diff --git a/editors/vscode/package.json b/editors/vscode/package.json index eeddf90be..e7f65f13b 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -47,7 +47,7 @@ "mdsmith.path": { "type": "string", "default": "mdsmith", - "description": "Path to the mdsmith binary. Defaults to the bundled binary in dist/bin/ (copied from @mdsmith/* npm packages during build). Works on Linux, macOS, and Windows from a single .vsix. If the bundled binary is unavailable, falls back to resolving 'mdsmith' against PATH. Set to an absolute path if you installed mdsmith elsewhere (e.g. /go/bin/mdsmith)." + "description": "Path to the mdsmith binary. Defaults to the bundled binary in dist/bin/ when available (host platform only, typically linux-x64 from CI). Falls back to resolving 'mdsmith' against PATH if bundled binary unavailable or on non-host platforms. Set to an absolute path if you installed mdsmith elsewhere (e.g. /go/bin/mdsmith)." }, "mdsmith.config": { "type": "string", diff --git a/editors/vscode/src/binary.ts b/editors/vscode/src/binary.ts index 1e43a09f0..d8ef7fe98 100644 --- a/editors/vscode/src/binary.ts +++ b/editors/vscode/src/binary.ts @@ -1,26 +1,28 @@ // Binary resolution logic for the mdsmith extension. -// The extension bundles cross-platform mdsmith binaries from npm packages -// into dist/bin/ during the build step. This module resolves the correct -// platform binary when the user leaves the default "mdsmith" path, falling -// back to PATH if bundling failed or binaries are unavailable. +// The extension bundles platform binaries from npm packages into dist/bin/ +// during the build step when available. Due to npm os/cpu constraints on +// the @mdsmith/* packages, only the host platform binary is bundled (typically +// linux-x64 in CI). This module resolves the bundled binary when present, +// falling back to PATH for other platforms or when bundling failed. import { existsSync } from "node:fs"; import { join } from "node:path"; // resolveBinary returns the path to the mdsmith binary. When the // configured path is the bare string "mdsmith", it first checks for -// bundled binaries in dist/bin/ (copied there by build.ts from the +// a bundled binary in dist/bin/ (copied there by build.ts from the // @mdsmith/* npm packages). Platform-specific binaries are named like // "linux-x64-mdsmith", "win32-x64-mdsmith.exe", etc. If the bundled // binary exists, return its absolute path. Otherwise return the // configured path unchanged so the LanguageClient resolves it against -// PATH (fallback for dev builds or when optional deps failed to install). +// PATH (fallback for dev builds, non-host platforms, or when optional +// deps failed to install). // -// Cross-platform bundling: The build script copies binaries from ALL -// @mdsmith/* platform packages (linux-x64, darwin-arm64, win32-x64, etc.) -// into dist/bin/. This works even with `vsce package --no-dependencies` -// because dist/ is included in the .vsix. At runtime, this function -// selects the binary matching the user's OS+arch. +// Platform bundling limitation: The @mdsmith/* platform packages have +// os/cpu constraints, so npm only installs the package matching the +// build host. This means only one platform binary is bundled per .vsix +// (typically linux-x64 from CI). Other platforms fall back to PATH and +// require manual mdsmith installation. // // The extensionPath should be the vscode.ExtensionContext.extensionPath // (the directory containing package.json and dist/).