Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
TablePro is a native macOS database client (SwiftUI + AppKit) — a fast, lightweight alternative to TablePlus. macOS 14.0+, Swift 5.9, Universal Binary (arm64 + x86_64).

- **Source**: `TablePro/` — `Core/` (business logic, services), `Views/` (UI), `Models/` (data structures), `ViewModels/`, `Extensions/`, `Theme/`
- **Plugins**: `Plugins/` — `.tableplugin` bundles + `TableProPluginKit` shared framework. Built-in (bundled in app): MySQL, PostgreSQL, SQLite, CSV, JSON, SQL export. Separately distributed via plugin registry: ClickHouse, MSSQL, MongoDB, Redis, Oracle, DuckDB, XLSX, MQL, SQLImport
- **Plugins**: `Plugins/` — `.tableplugin` bundles + `TableProPluginKit` shared framework. Built-in (bundled in app): MySQL, PostgreSQL, SQLite, ClickHouse, MSSQL, Redis, CSV, JSON, SQL export, XLSX, MQL, SQLImport, DynamoDB. Separately distributed via plugin registry: MongoDB, Oracle, DuckDB, Cassandra, Etcd, CloudflareD1
- **C bridges**: Each plugin contains its own C bridge module (e.g., `Plugins/MySQLDriverPlugin/CMariaDB/`, `Plugins/PostgreSQLDriverPlugin/CLibPQ/`)
- **Static libs**: `Libs/` — pre-built `libmariadb*.a`, `libpq*.a`, etc. Downloaded from GitHub Releases via `scripts/download-libs.sh` (not in git)
- **SPM deps**: CodeEditSourceEditor (`main` branch, tree-sitter editor), Sparkle (2.8.1, auto-update), OracleNIO. Managed via Xcode, no `Package.swift`.
Expand Down Expand Up @@ -79,10 +79,10 @@ Plugin bundles under `Plugins/`:
| MySQLDriverPlugin | MySQL, MariaDB | CMariaDB | Built-in |
| PostgreSQLDriverPlugin | PostgreSQL, Redshift | CLibPQ | Built-in |
| SQLiteDriverPlugin | SQLite | (Foundation sqlite3) | Built-in |
| ClickHouseDriverPlugin | ClickHouse | (URLSession HTTP) | Registry |
| MSSQLDriverPlugin | SQL Server | CFreeTDS | Registry |
| ClickHouseDriverPlugin | ClickHouse | (URLSession HTTP) | Built-in |
| MSSQLDriverPlugin | SQL Server | CFreeTDS | Built-in |
| RedisDriverPlugin | Redis | CRedis | Built-in |
| MongoDBDriverPlugin | MongoDB | CLibMongoc | Registry |
| RedisDriverPlugin | Redis | CRedis | Registry |
| DuckDBDriverPlugin | DuckDB | CDuckDB | Registry |
| OracleDriverPlugin | Oracle | OracleNIO (SPM) | Registry |

Expand Down
77 changes: 77 additions & 0 deletions TablePro.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
5A86B000D00000000 /* JSONExport.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A86B000100000000 /* JSONExport.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A86C000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
5A86C000D00000000 /* SQLExport.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A86C000100000000 /* SQLExport.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A863000D00000000 /* ClickHouseDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A863000100000000 /* ClickHouseDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A864000D00000000 /* MSSQLDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A864000100000000 /* MSSQLDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A867000D00000000 /* RedisDriver.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A867000100000000 /* RedisDriver.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A86D000D00000000 /* XLSXExport.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A86D000100000000 /* XLSXExport.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A86E000D00000000 /* MQLExport.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A86E000100000000 /* MQLExport.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A86F000D00000000 /* SQLImport.tableplugin in Copy Plug-Ins */ = {isa = PBXBuildFile; fileRef = 5A86F000100000000 /* SQLImport.tableplugin */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5A86D000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
5A86E000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
5A86F000A00000000 /* TableProPluginKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A860000100000000 /* TableProPluginKit.framework */; };
Expand Down Expand Up @@ -126,6 +132,41 @@
remoteGlobalIDString = 5A86C000000000000;
remoteInfo = SQLExport;
};
5A864000B00000000 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5A864000000000000;
remoteInfo = MSSQLDriver;
};
5A867000B00000000 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5A867000000000000;
remoteInfo = RedisDriver;
};
5A86D000B00000000 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5A86D000000000000;
remoteInfo = XLSXExport;
};
5A86E000B00000000 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5A86E000000000000;
remoteInfo = MQLExport;
};
5A86F000B00000000 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */;
proxyType = 1;
remoteGlobalIDString = 5A86F000000000000;
remoteInfo = SQLImport;
};
5ABCC5AB2F43856700EAF3FC /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 5A1091BF2EF17EDC0055EA7C /* Project object */;
Expand All @@ -152,9 +193,15 @@
5A865000D00000000 /* MySQLDriver.tableplugin in Copy Plug-Ins */,
5A868000D00000000 /* PostgreSQLDriver.tableplugin in Copy Plug-Ins */,
5A862000D00000000 /* SQLiteDriver.tableplugin in Copy Plug-Ins */,
5A863000D00000000 /* ClickHouseDriver.tableplugin in Copy Plug-Ins */,
5A864000D00000000 /* MSSQLDriver.tableplugin in Copy Plug-Ins */,
5A867000D00000000 /* RedisDriver.tableplugin in Copy Plug-Ins */,
5A86A000D00000000 /* CSVExport.tableplugin in Copy Plug-Ins */,
5A86B000D00000000 /* JSONExport.tableplugin in Copy Plug-Ins */,
5A86C000D00000000 /* SQLExport.tableplugin in Copy Plug-Ins */,
5A86D000D00000000 /* XLSXExport.tableplugin in Copy Plug-Ins */,
5A86E000D00000000 /* MQLExport.tableplugin in Copy Plug-Ins */,
5A86F000D00000000 /* SQLImport.tableplugin in Copy Plug-Ins */,
5ADDB00000000000000000D0 /* DynamoDBDriverPlugin.tableplugin in Copy Plug-Ins */,
);
name = "Copy Plug-Ins";
Expand Down Expand Up @@ -823,12 +870,17 @@
5A861000C00000000 /* PBXTargetDependency */,
5A862000C00000000 /* PBXTargetDependency */,
5A863000C00000000 /* PBXTargetDependency */,
5A864000C00000000 /* PBXTargetDependency */,
5A865000C00000000 /* PBXTargetDependency */,
5A867000C00000000 /* PBXTargetDependency */,
5A868000C00000000 /* PBXTargetDependency */,
5A869000C00000000 /* PBXTargetDependency */,
5A86A000C00000000 /* PBXTargetDependency */,
5A86B000C00000000 /* PBXTargetDependency */,
5A86C000C00000000 /* PBXTargetDependency */,
5A86D000C00000000 /* PBXTargetDependency */,
5A86E000C00000000 /* PBXTargetDependency */,
5A86F000C00000000 /* PBXTargetDependency */,
5ADDB00000000000000000C1 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
Expand Down Expand Up @@ -1771,6 +1823,31 @@
target = 5A86C000000000000 /* SQLExport */;
targetProxy = 5A86C000B00000000 /* PBXContainerItemProxy */;
};
5A864000C00000000 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5A864000000000000 /* MSSQLDriver */;
targetProxy = 5A864000B00000000 /* PBXContainerItemProxy */;
};
5A867000C00000000 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5A867000000000000 /* RedisDriver */;
targetProxy = 5A867000B00000000 /* PBXContainerItemProxy */;
};
5A86D000C00000000 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5A86D000000000000 /* XLSXExport */;
targetProxy = 5A86D000B00000000 /* PBXContainerItemProxy */;
};
5A86E000C00000000 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5A86E000000000000 /* MQLExport */;
targetProxy = 5A86E000B00000000 /* PBXContainerItemProxy */;
};
5A86F000C00000000 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5A86F000000000000 /* SQLImport */;
targetProxy = 5A86F000B00000000 /* PBXContainerItemProxy */;
};
5ABCC5AC2F43856700EAF3FC /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 5A1091C62EF17EDC0055EA7C /* TablePro */;
Expand Down
36 changes: 36 additions & 0 deletions TablePro/Core/Plugins/PluginManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ final class PluginManager {

if let builtInDir = builtInPluginsDir {
discoverPlugins(from: builtInDir, source: .builtIn)
removeUserInstalledDuplicates(builtInDir: builtInDir)
}

discoverPlugins(from: userPluginsDir, source: .userInstalled)
Expand Down Expand Up @@ -320,6 +321,41 @@ final class PluginManager {
}
}

/// Remove user-installed plugins that now ship as built-in to avoid dead weight.
private func removeUserInstalledDuplicates(builtInDir: URL) {
let fm = FileManager.default
guard let builtInBundles = try? fm.contentsOfDirectory(
at: builtInDir,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
) else { return }

var builtInBundleIds = Set<String>()
for url in builtInBundles where url.pathExtension == "tableplugin" {
if let bundle = Bundle(url: url), let id = bundle.bundleIdentifier {
builtInBundleIds.insert(id)
}
}

guard let userPlugins = try? fm.contentsOfDirectory(
at: userPluginsDir,
includingPropertiesForKeys: nil,
options: [.skipsHiddenFiles]
) else { return }

for url in userPlugins where url.pathExtension == "tableplugin" {
guard let bundle = Bundle(url: url), let id = bundle.bundleIdentifier else { continue }
if builtInBundleIds.contains(id) {
do {
try fm.removeItem(at: url)
Self.logger.info("Removed user-installed '\(id)' — now ships as built-in")
} catch {
Self.logger.warning("Failed to remove duplicate plugin '\(id)': \(error.localizedDescription)")
}
}
}
}

private func discoverPlugin(at url: URL, source: PluginSource) throws {
guard let bundle = Bundle(url: url) else {
throw PluginError.invalidBundle("Cannot create bundle from \(url.lastPathComponent)")
Expand Down
Loading