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
42 changes: 42 additions & 0 deletions .github/workflows/release-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Release Developer Preview

on:
push:
tags:
- "v0.1.0-preview.*"

permissions:
contents: write

jobs:
release:
name: Build Apple Silicon DMG
runs-on: macos-14
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version-file: .node-version
cache: npm
cache-dependency-path: adapters/mcp/package-lock.json

- name: Build preview package
env:
NEXUS_RUNTIME_CACHE: ${{ runner.temp }}/nexus-runtime-cache
run: |
scripts/build-preview.sh
scripts/verify-preview.sh

- name: Publish GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
dist/preview/Nexus-*.dmg \
dist/preview/SHA256SUMS \
--title "Nexus $GITHUB_REF_NAME" \
--prerelease \
--generate-notes
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to Nexus will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.1.0-preview.1] - 2026-08-12

### Added
- Apple Silicon Developer Preview DMG with a real `Nexus.app` bundle
- Bundled Node.js 26.5.0 Runtime and MCP Helper with first-launch installation
- Stable MCP shim, runtime diagnostics, and reproducible package verification

## [Unreleased]

### Added
Expand Down
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Nexus is a local-first trusted work-context layer for coding assistants on macOS

It turns requirements, API examples, SQL, logs, source files, and human notes into a concise, reviewable, source-backed **Context Pack**, then carries later code changes forward as separate evidence. Once approved, that context is available to coding assistants such as Codex and Claude through MCP without injecting every raw material by default.

> Status: pre-alpha local prototype suitable for dogfooding. A production App Bundle, private Helper runtime, signing, notarization, and DMG are not complete yet.
> Status: pre-alpha Apple Silicon Developer Preview. The preview DMG includes a private Node Runtime and MCP Helper, but it is not Developer ID signed or notarized.

## Why Nexus

Expand Down Expand Up @@ -60,7 +60,9 @@ Nexus does not run coding agents or decide implementation strategy. It never swi

## Requirements

Current development baseline:
For the Developer Preview, use an Apple Silicon Mac running macOS 14 or later. Node.js is bundled with the DMG and is not required for normal use.

Source-development baseline:

- macOS 14 or later;
- Xcode 26.5 / Swift 6.3.2;
Expand All @@ -71,6 +73,17 @@ The MCP Helper uses `node:sqlite`; it does not depend on the system SQLite CLI o

## Build and Run

Build the Apple Silicon Developer Preview DMG:

```bash
scripts/build-preview.sh
scripts/verify-preview.sh
```

The DMG is written to `dist/preview/`. The build downloads the pinned official Node Runtime, verifies its SHA-256 checksum, and embeds it in `Nexus.app`; the runtime is not committed to this repository.

The preview is not Developer ID signed or notarized. macOS may require opening it from Finder with the usual local preview confirmation.

Build and test Swift:

```bash
Expand Down Expand Up @@ -117,7 +130,9 @@ An approved Context Pack can be expanded in the main workspace to inspect its fu

## MCP Helper

Install the development helper shim:
When Nexus is installed from the Developer Preview DMG, the first app launch automatically installs the bundled Helper into the user's Nexus Application Support directory and creates the stable command path below. No Node.js installation or manual Helper script is required.

For source development only, install the development helper shim:

```bash
scripts/install-helper.sh
Expand All @@ -136,6 +151,8 @@ Check the Helper:
"$HOME/Library/Application Support/Nexus/bin/nexus-mcp" --doctor
```

`--doctor` reports the Helper version, MCP SDK version, private Node version, SQLite adapter, database, app runtime, and projection compatibility.

Example Codex configuration:

```bash
Expand Down Expand Up @@ -181,6 +198,7 @@ Archiving or deleting a Work does not delete a Nexus-created directory or Git br

- [Architecture](docs/architecture.md)
- [Development guide](docs/development.md)
- [Developer Preview release](docs/release.md)
- [Contributing](CONTRIBUTING.md)
- [Security policy](SECURITY.md)
- [Assistant rules example](docs/agent-rules.md)
Expand Down
24 changes: 21 additions & 3 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Nexus 是 Mac 上面向编码助手的本地优先可信工作上下文层。

它把需求说明、接口样例、SQL、日志、代码文件和人的补充说明,整理成一份短小、可审核、带来源的 **Context Pack**,并把后续代码变化作为独立证据持续交接。确认后的上下文可通过 MCP 供 Codex、Claude 等编码助手按需读取,原始材料不会被默认全部塞进模型。

> 当前状态:pre-alpha 本地原型,可用于 dogfooding。正式 App Bundle、私有 Helper runtime、签名、公证和 DMG 尚未完成。
> 当前状态:pre-alpha Apple Silicon Developer Preview。预览版 DMG 已包含私有 Node Runtime 和 MCP Helper,但暂未进行 Developer ID 签名和公证。

## 为什么使用 Nexus

Expand Down Expand Up @@ -60,7 +60,9 @@ Nexus 不运行编码 Agent,不替用户决定实现方案,也不自动切

## 环境要求

当前开发基线:
Developer Preview 仅支持 Apple Silicon Mac,系统要求 macOS 14 或更高版本。DMG 已内置 Node.js,普通用户不需要自行安装 Node。

源码开发基线:

- macOS 14 或更高版本;
- Xcode 26.5 / Swift 6.3.2;
Expand All @@ -71,6 +73,17 @@ MCP Helper 使用 `node:sqlite`,不依赖系统 SQLite CLI 或第三方 native

## 构建与运行

构建 Apple Silicon Developer Preview DMG:

```bash
scripts/build-preview.sh
scripts/verify-preview.sh
```

DMG 输出在 `dist/preview/`。构建过程会下载固定版本的官方 Node Runtime,校验 SHA-256 后嵌入 `Nexus.app`;Runtime 不会提交到仓库。

预览版暂未进行 Developer ID 签名和公证。macOS 可能需要在 Finder 中按系统提示确认打开本地预览应用。

构建并测试 Swift:

```bash
Expand Down Expand Up @@ -117,7 +130,9 @@ Nexus 默认把数据保存到:

## MCP Helper

安装开发用 helper shim:
通过 Developer Preview DMG 安装后,首次启动 Nexus 会自动把内置 Helper 安装到本机 Nexus Application Support 目录,并创建下面的稳定命令路径。普通用户不需要安装 Node,也不需要手动执行 Helper 安装脚本。

源码开发时才需要安装开发用 helper shim:

```bash
scripts/install-helper.sh
Expand All @@ -136,6 +151,8 @@ scripts/install-helper.sh
"$HOME/Library/Application Support/Nexus/bin/nexus-mcp" --doctor
```

`--doctor` 会报告 Helper 版本、MCP SDK 版本、私有 Node 版本、SQLite 适配器、数据库、App 运行状态和投影兼容性。

Codex 配置示例:

```bash
Expand Down Expand Up @@ -181,6 +198,7 @@ codex mcp add nexus-worktree \

- [架构说明](docs/architecture.zh.md)
- [开发指南](docs/development.zh.md)
- [Developer Preview 发布说明](docs/release.zh.md)
- [参与贡献](CONTRIBUTING.md)
- [安全策略](SECURITY.md)
- [助手规则示例](docs/agent-rules.zh.md)
Expand Down
140 changes: 140 additions & 0 deletions Sources/NexusCore/BundledHelperInstaller.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Foundation

public struct BundledHelperInstallation: Sendable {
public let helperVersion: String
public let nodeVersion: String
public let helperDirectory: URL
public let stableShim: URL

public init(helperVersion: String, nodeVersion: String, helperDirectory: URL, stableShim: URL) {
self.helperVersion = helperVersion
self.nodeVersion = nodeVersion
self.helperDirectory = helperDirectory
self.stableShim = stableShim
}
}

public enum BundledHelperInstallError: LocalizedError, Sendable {
case resourceMissing(URL)
case manifestInvalid(String)
case helperNotExecutable(URL)
case entrypointMissing(URL)
case installationFailed(String)

public var errorDescription: String? {
switch self {
case .resourceMissing(let url):
return "Bundled MCP Helper resource is missing: \(url.path)"
case .manifestInvalid(let reason):
return "Bundled MCP Helper manifest is invalid: \(reason)"
case .helperNotExecutable(let url):
return "Bundled Node Runtime is not executable: \(url.path)"
case .entrypointMissing(let url):
return "Bundled MCP Helper entrypoint is missing: \(url.path)"
case .installationFailed(let reason):
return "Could not install the bundled MCP Helper: \(reason)"
}
}
}

public enum BundledHelperInstaller {
private struct Manifest: Decodable {
let helperVersion: String
let nodeVersion: String
let entrypoint: String
}

public static func install(
from resourceDirectory: URL,
applicationSupportDirectory: URL = NexusPaths.applicationSupportDirectory
) throws -> BundledHelperInstallation {
let fileManager = FileManager.default
var isDirectory: ObjCBool = false
guard fileManager.fileExists(atPath: resourceDirectory.path, isDirectory: &isDirectory), isDirectory.boolValue
else {
throw BundledHelperInstallError.resourceMissing(resourceDirectory)
}

let manifestURL = resourceDirectory.appendingPathComponent("manifest.json")
let manifest: Manifest
do {
manifest = try JSONDecoder().decode(Manifest.self, from: Data(contentsOf: manifestURL))
} catch {
throw BundledHelperInstallError.manifestInvalid(error.localizedDescription)
}

let resourceNode = resourceDirectory.appendingPathComponent("node")
guard fileManager.isExecutableFile(atPath: resourceNode.path) else {
throw BundledHelperInstallError.helperNotExecutable(resourceNode)
}
let resourceEntrypoint = resourceDirectory.appendingPathComponent(manifest.entrypoint)
guard fileManager.fileExists(atPath: resourceEntrypoint.path) else {
throw BundledHelperInstallError.entrypointMissing(resourceEntrypoint)
}

let helpersDirectory = applicationSupportDirectory.appendingPathComponent("helpers", isDirectory: true)
let installedDirectory = helpersDirectory.appendingPathComponent(manifest.helperVersion, isDirectory: true)
let stableBinDirectory = applicationSupportDirectory.appendingPathComponent("bin", isDirectory: true)
let stableShim = stableBinDirectory.appendingPathComponent("nexus-mcp")
do {
try fileManager.createDirectory(at: helpersDirectory, withIntermediateDirectories: true)
try fileManager.createDirectory(at: stableBinDirectory, withIntermediateDirectories: true)

if !isValidInstalledHelper(installedDirectory, entrypoint: manifest.entrypoint) {
let stagingDirectory = helpersDirectory.appendingPathComponent(".install-\(UUID().uuidString)")
try? fileManager.removeItem(at: stagingDirectory)
try fileManager.copyItem(at: resourceDirectory, to: stagingDirectory)
try? fileManager.removeItem(at: installedDirectory)
try fileManager.moveItem(at: stagingDirectory, to: installedDirectory)
}

let installedNode = installedDirectory.appendingPathComponent("node")
let installedEntrypoint = installedDirectory.appendingPathComponent(manifest.entrypoint)
try writeStableShim(
at: stableShim,
node: installedNode,
entrypoint: installedEntrypoint,
applicationSupportDirectory: applicationSupportDirectory
)
return BundledHelperInstallation(
helperVersion: manifest.helperVersion,
nodeVersion: manifest.nodeVersion,
helperDirectory: installedDirectory,
stableShim: stableShim
)
} catch let error as BundledHelperInstallError {
throw error
} catch {
throw BundledHelperInstallError.installationFailed(error.localizedDescription)
}
}

private static func isValidInstalledHelper(_ directory: URL, entrypoint: String) -> Bool {
let fileManager = FileManager.default
return fileManager.isExecutableFile(atPath: directory.appendingPathComponent("node").path)
&& fileManager.fileExists(atPath: directory.appendingPathComponent(entrypoint).path)
}

private static func writeStableShim(
at url: URL,
node: URL,
entrypoint: URL,
applicationSupportDirectory: URL
) throws {
let script = """
#!/bin/sh
set -eu
if [ -z "${NEXUS_HOME:-}" ]; then
NEXUS_HOME=\(shellQuote(applicationSupportDirectory.path))
fi
export NEXUS_HOME
exec \(shellQuote(node.path)) \(shellQuote(entrypoint.path)) "$@"
"""
try Data(script.utf8).write(to: url, options: [.atomic])
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: url.path)
}

private static func shellQuote(_ value: String) -> String {
"'" + value.replacingOccurrences(of: "'", with: "'\\''") + "'"
}
}
16 changes: 16 additions & 0 deletions Sources/NexusMac/AppModel+AssistantConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ import NexusCore

@MainActor
extension AppModel {
func installBundledHelperIfAvailable() async {
guard let resourceDirectory = Bundle.main.resourceURL?.appendingPathComponent("MCPHelper") else {
return
}
do {
_ = try await NexusBackgroundWork.run(priority: .utility) {
try BundledHelperInstaller.install(from: resourceDirectory)
}
} catch {
assistantConnectionReady = false
assistantConnectionStatus = l10n.helperMissing
assistantConnectionDetail = l10n.installHelperFirst
assistantConnectionDoctor = error.localizedDescription
}
}

func copyContextSummary() {
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(contextSummaryText(), forType: .string)
Expand Down
1 change: 1 addition & 0 deletions Sources/NexusMac/AppModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ final class AppModel: ObservableObject {
guard let self else { return }
defer { bootstrapTask = nil }
do {
await installBundledHelperIfAvailable()
try await performStoreOperation { store in
try store.bootstrap()
}
Expand Down
9 changes: 5 additions & 4 deletions Sources/NexusMac/L10n.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,9 @@ struct L10n {
var textKind: String { zh ? "文本" : "Text" }
var updatedPrefix: String { zh ? "更新" : "Updated" }
var dropLocalFilesHere: String { zh ? "拖入本地文件" : "Drop local files here" }
var originalFilesStayOnDisk: String { zh ? "Nexus 只保留引用,不会删除原文件。" : "Original files stay on disk." }
var dropFiles: String { zh ? "拖入文件" : "Drop files" }
var filePickerHint: String { zh ? "点击选择或拖入,Nexus 只保留引用" : "Click to choose or drop; Nexus keeps a reference only" }
var dropFiles: String { zh ? "点击选择或拖入文件" : "Click to choose or drop files" }
var chooseFiles: String { zh ? "选择文件..." : "Choose Files..." }
var snapshotSaved: String { zh ? "快照已保存" : "Snapshot saved" }
var prepareCurrentWork: String { zh ? "整理当前工作" : "Prepare Current Work" }
var contextPreparationTitle: String { zh ? "整理当前工作" : "Prepare Current Work" }
Expand Down Expand Up @@ -487,9 +488,9 @@ struct L10n {
var openNexusToConnect: String { zh ? "打开 Nexus 以连接" : "Open Nexus to connect" }
var connectionFailed: String { zh ? "连接失败" : "Connection failed" }
var helperInstalledButNotReady: String { zh ? "本地连接组件已安装,但还没有准备好。" : "The helper is installed but not ready." }
var runtimeStatusMissing: String { zh ? "Nexus 还没有发布运行状态。" : "Nexus has not published a running status yet." }
var runtimeStatusMissing: String { zh ? "连接状态还没有准备好,请稍后重试。" : "The connection is not ready yet. Please try again shortly." }
var assistantAccessPausedReason: String { zh ? "助手读取已暂停。" : "Assistant reading is paused." }
var heartbeatStale: String { zh ? "Nexus 运行状态已过期。" : "Nexus heartbeat is stale." }
var heartbeatStale: String { zh ? "Nexus 暂时没有响应,请重新打开后重试。" : "Nexus is not responding. Reopen it and try again." }
var appNotRunning: String { zh ? "Nexus 没有运行。" : "Nexus is not running." }
var appProcessNotAlive: String { zh ? "Nexus 进程已退出。" : "Nexus process is no longer alive." }
var chooseGitRepository: String { zh ? "选择 Git 主目录或已有代码目录" : "Choose a Git checkout or existing code directory" }
Expand Down
Loading
Loading