feat: add in-app update checker, enhance About page and project metadata

- Add AtlasUpdateChecker with GitHub Releases API integration
- Add AtlasVersionComparator for semantic version comparison
- Add AboutUpdateToolbarButton with popover update UI
- Enhance AboutFeatureView with social QR codes and layout refinements
- Add CHANGELOG.md and CODE_OF_CONDUCT.md
- Rebrand project files from Mole to Atlas for Mac
- Update build script to support version/build number injection
- Add installation guide to README
- Add bilingual localization strings for update feature
- Add unit tests for update checker and version comparator
This commit is contained in:
zhukang
2026-03-11 20:07:26 +08:00
parent 0a9d47027b
commit d3ca6d18dc
34 changed files with 1143 additions and 327 deletions

View File

@@ -1,4 +1,4 @@
# EditorConfig for Mole project # EditorConfig for Atlas for Mac project
# https://editorconfig.org # https://editorconfig.org
root = true root = true

2
.github/CODEOWNERS vendored
View File

@@ -1 +1 @@
* @tw93 * @CSZHK

3
.github/FUNDING.yml vendored
View File

@@ -1,2 +1 @@
github: ['tw93'] github: ['CSZHK']
custom: ['https://miaoyan.app/cats.html?name=Mole']

View File

@@ -1,6 +1,6 @@
--- ---
name: Bug Report name: Bug Report
about: Report a bug or issue with Mole about: Report a bug or issue with Atlas for Mac
title: '[BUG] ' title: '[BUG] '
labels: bug labels: bug
assignees: '' assignees: ''
@@ -14,40 +14,25 @@ If you believe the issue may allow unsafe deletion, path validation bypass, priv
## Steps to reproduce ## Steps to reproduce
1. Run command: `mo ...` 1. Open Atlas for Mac
2. ... 2. Navigate to the relevant module (e.g., Smart Clean, Apps, Overview)
3. See error 3. Perform the action that triggers the bug
4. See error
## Expected behavior ## Expected behavior
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
## Debug logs ## Screenshots
Please run the command with `--debug` flag and paste the output here: If applicable, add screenshots to help explain your problem.
```bash
mo <command> --debug
# Example: mo clean --debug
```
<details>
<summary>Debug output</summary>
```text
Paste the debug output here
```
</details>
## Environment ## Environment
Please run `mo update` to ensure you are on the latest version, then paste the output of `mo --version` below: - macOS version:
- Atlas for Mac version:
```text - Chip: Apple Silicon / Intel
Paste mo --version output here
```
## Additional context ## Additional context
Add any other context about the problem here, such as screenshots or related issues. Add any other context about the problem here, such as console logs or related issues.

View File

@@ -1,11 +1,8 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Private Security Report - name: Private Security Report
url: mailto:hitw93@gmail.com?subject=Mole%20security%20report url: mailto:cszhk0310@gmail.com?subject=Atlas%20for%20Mac%20security%20report
about: Report a suspected vulnerability privately instead of opening a public issue about: Report a suspected vulnerability privately instead of opening a public issue
- name: Telegram Community
url: https://t.me/+GclQS9ZnxyI2ODQ1
about: Join our Telegram group for questions and discussions
- name: GitHub Discussions - name: GitHub Discussions
url: https://github.com/tw93/mole/discussions url: https://github.com/CSZHK/CleanMyPc/discussions
about: Ask questions and share ideas with the community about: Ask questions and share ideas with the community

View File

@@ -1,6 +1,6 @@
--- ---
name: Feature Request name: Feature Request
about: Suggest an idea for Mole about: Suggest an idea for Atlas for Mac
title: '[FEATURE] ' title: '[FEATURE] '
labels: enhancement labels: enhancement
assignees: '' assignees: ''

View File

@@ -7,7 +7,7 @@ updates:
labels: labels:
- "dependencies" - "dependencies"
reviewers: reviewers:
- "tw93" - "CSZHK"
open-pull-requests-limit: 10 open-pull-requests-limit: 10
- package-ecosystem: "gomod" - package-ecosystem: "gomod"
@@ -17,5 +17,5 @@ updates:
labels: labels:
- "dependencies" - "dependencies"
reviewers: reviewers:
- "tw93" - "CSZHK"
open-pull-requests-limit: 10 open-pull-requests-limit: 10

View File

@@ -51,8 +51,8 @@ jobs:
- name: Commit formatting changes - name: Commit formatting changes
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
run: | run: |
git config user.name "Tw93" git config user.name "github-actions[bot]"
git config user.email "tw93@qq.com" git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
if [[ -n $(git status --porcelain) ]]; then if [[ -n $(git status --porcelain) ]]; then
git add . git add .
git commit -m "chore: auto format code" git commit -m "chore: auto format code"

View File

@@ -102,53 +102,3 @@ jobs:
generate_release_notes: false generate_release_notes: false
draft: false draft: false
prerelease: false prerelease: false
update-formula:
runs-on: ubuntu-latest
needs: release
steps:
- name: Extract version from tag
id: tag_version
run: |
TAG=${GITHUB_REF#refs/tags/}
VERSION=${TAG#V}
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Releasing version: $VERSION (tag: $TAG)"
- name: Update Homebrew formula (Personal Tap)
uses: mislav/bump-homebrew-formula-action@56a283fa15557e9abaa4bdb63b8212abc68e655c # v3.6
with:
formula-name: mole
formula-path: Formula/mole.rb
homebrew-tap: tw93/homebrew-tap
tag-name: ${{ steps.tag_version.outputs.tag }}
commit-message: |
mole ${{ steps.tag_version.outputs.version }}
Automated release via GitHub Actions
env:
COMMITTER_TOKEN: ${{ secrets.PAT_TOKEN }}
- name: Update Homebrew formula (Official Core)
uses: mislav/bump-homebrew-formula-action@56a283fa15557e9abaa4bdb63b8212abc68e655c # v3.6
with:
formula-name: mole
homebrew-tap: Homebrew/homebrew-core
tag-name: ${{ steps.tag_version.outputs.tag }}
commit-message: |
mole ${{ steps.tag_version.outputs.version }}
Automated release via GitHub Actions
env:
COMMITTER_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }}
continue-on-error: true
- name: Verify formula updates
if: success()
run: |
echo "✓ Homebrew formulae updated successfully"
echo " Version: ${{ steps.tag_version.outputs.version }}"
echo " Tag: ${{ steps.tag_version.outputs.tag }}"
echo " Personal tap: tw93/homebrew-tap"
echo " Official core: Homebrew/homebrew-core (PR created)"

View File

@@ -1,4 +1,4 @@
# golangci-lint configuration for Mole # golangci-lint configuration for Atlas for Mac
# https://golangci-lint.run/usage/configuration/ # https://golangci-lint.run/usage/configuration/
version: "2" version: "2"

View File

@@ -40,38 +40,29 @@ struct AppShellView: View {
detailContent(for: route) detailContent(for: route)
.toolbar { .toolbar {
ToolbarItemGroup { ToolbarItem {
Button { taskCenterToolbarButton
model.openTaskCenter()
} label: {
Label {
Text(AtlasL10n.string("toolbar.taskcenter"))
} icon: {
ZStack(alignment: .topTrailing) {
Image(systemName: AtlasIcon.taskCenter)
.symbolRenderingMode(.hierarchical)
if activeTaskCount > 0 {
Text(activeTaskCount > 99 ? "99+" : "\(activeTaskCount)")
.font(.caption2.weight(.bold))
.foregroundStyle(.white)
.padding(.horizontal, activeTaskCount > 9 ? AtlasSpacing.xxs : AtlasSpacing.xs)
.padding(.vertical, 2)
.background(Capsule(style: .continuous).fill(AtlasColor.accent))
.offset(x: 10, y: -8)
}
}
}
}
.help(AtlasL10n.string("toolbar.taskcenter.help"))
.accessibilityIdentifier("toolbar.taskCenter")
.accessibilityLabel(AtlasL10n.string("toolbar.taskcenter.accessibilityLabel"))
.accessibilityHint(AtlasL10n.string("toolbar.taskcenter.accessibilityHint"))
} }
} }
.animation(AtlasMotion.slow, value: model.selection) .animation(AtlasMotion.slow, value: model.selection)
} }
.navigationSplitViewStyle(.balanced) .navigationSplitViewStyle(.balanced)
.overlay(alignment: .topTrailing) {
AboutUpdateToolbarButton(
appVersion: model.appVersion,
appBuild: model.appBuild,
updateResult: model.latestUpdateResult,
isCheckingForUpdate: model.isCheckingForUpdate,
updateCheckNotice: model.updateCheckNotice,
updateCheckError: model.updateCheckError,
onCheckForUpdate: {
Task { await model.checkForUpdate() }
}
)
.padding(.top, 10)
.padding(.trailing, 24)
.ignoresSafeArea(.container, edges: .top)
}
.task { .task {
await model.refreshHealthSnapshotIfNeeded() await model.refreshHealthSnapshotIfNeeded()
await model.refreshPermissionsIfNeeded() await model.refreshPermissionsIfNeeded()
@@ -226,6 +217,35 @@ struct AppShellView: View {
taskRun.status == .queued || taskRun.status == .running taskRun.status == .queued || taskRun.status == .running
}.count }.count
} }
private var taskCenterToolbarButton: some View {
Button {
model.openTaskCenter()
} label: {
Label {
Text(AtlasL10n.string("toolbar.taskcenter"))
} icon: {
ZStack(alignment: .topTrailing) {
Image(systemName: AtlasIcon.taskCenter)
.symbolRenderingMode(.hierarchical)
if activeTaskCount > 0 {
Text(activeTaskCount > 99 ? "99+" : "\(activeTaskCount)")
.font(.caption2.weight(.bold))
.foregroundStyle(.white)
.padding(.horizontal, activeTaskCount > 9 ? AtlasSpacing.xxs : AtlasSpacing.xs)
.padding(.vertical, 2)
.background(Capsule(style: .continuous).fill(AtlasColor.accent))
.offset(x: 10, y: -8)
}
}
}
}
.help(AtlasL10n.string("toolbar.taskcenter.help"))
.accessibilityIdentifier("toolbar.taskCenter")
.accessibilityLabel(AtlasL10n.string("toolbar.taskcenter.accessibilityLabel"))
.accessibilityHint(AtlasL10n.string("toolbar.taskcenter.accessibilityHint"))
}
} }
private struct SidebarRouteRow: View { private struct SidebarRouteRow: View {

View File

@@ -31,8 +31,13 @@ final class AtlasAppModel: ObservableObject {
@Published private(set) var latestScanProgress: Double = 0 @Published private(set) var latestScanProgress: Double = 0
@Published private(set) var isCurrentSmartCleanPlanFresh: Bool @Published private(set) var isCurrentSmartCleanPlanFresh: Bool
@Published private(set) var smartCleanPlanIssue: String? @Published private(set) var smartCleanPlanIssue: String?
@Published private(set) var latestUpdateResult: AtlasAppUpdate?
@Published private(set) var isCheckingForUpdate = false
@Published private(set) var updateCheckNotice: String?
@Published private(set) var updateCheckError: String?
private let workspaceController: AtlasWorkspaceController private let workspaceController: AtlasWorkspaceController
private let updateChecker = AtlasUpdateChecker()
private let notificationPermissionRequester: @Sendable () async -> Bool private let notificationPermissionRequester: @Sendable () async -> Bool
private var didRequestInitialHealthSnapshot = false private var didRequestInitialHealthSnapshot = false
private var didRequestInitialPermissionSnapshot = false private var didRequestInitialPermissionSnapshot = false
@@ -82,6 +87,46 @@ final class AtlasAppModel: ObservableObject {
settings.language settings.language
} }
var appVersion: String {
Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
}
var appBuild: String {
Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "1"
}
func checkForUpdate() async {
guard !isCheckingForUpdate else { return }
isCheckingForUpdate = true
defer { isCheckingForUpdate = false }
updateCheckNotice = nil
updateCheckError = nil
do {
let result = try await updateChecker.checkForUpdate(currentVersion: appVersion)
withAnimation(.snappy(duration: 0.24)) {
latestUpdateResult = result
}
} catch let error as AtlasUpdateCheckerError {
withAnimation(.snappy(duration: 0.24)) {
latestUpdateResult = nil
}
switch error {
case .noPublishedRelease:
updateCheckNotice = error.localizedDescription
case .requestFailed:
updateCheckError = error.localizedDescription
}
} catch {
withAnimation(.snappy(duration: 0.24)) {
latestUpdateResult = nil
}
updateCheckError = error.localizedDescription
}
}
func searchText(for route: AtlasRoute) -> String { func searchText(for route: AtlasRoute) -> String {
searchTextByRoute[route, default: ""] searchTextByRoute[route, default: ""]
} }

View File

@@ -398,12 +398,16 @@
buildSettings = { buildSettings = {
AD_HOC_CODE_SIGNING_ALLOWED = YES; AD_HOC_CODE_SIGNING_ALLOWED = YES;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleShortVersionString = "$(MARKETING_VERSION)";
INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app.worker; PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app.worker;
PRODUCT_NAME = AtlasWorkerXPC; PRODUCT_NAME = AtlasWorkerXPC;
SDKROOT = macosx; SDKROOT = macosx;
@@ -471,12 +475,16 @@
buildSettings = { buildSettings = {
AD_HOC_CODE_SIGNING_ALLOWED = YES; AD_HOC_CODE_SIGNING_ALLOWED = YES;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleShortVersionString = "$(MARKETING_VERSION)";
INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app.worker; PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app.worker;
PRODUCT_NAME = AtlasWorkerXPC; PRODUCT_NAME = AtlasWorkerXPC;
SDKROOT = macosx; SDKROOT = macosx;
@@ -489,8 +497,11 @@
AD_HOC_CODE_SIGNING_ALLOWED = YES; AD_HOC_CODE_SIGNING_ALLOWED = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Atlas for Mac"; INFOPLIST_KEY_CFBundleDisplayName = "Atlas for Mac";
INFOPLIST_KEY_CFBundleShortVersionString = "$(MARKETING_VERSION)";
INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSPrincipalClass = NSApplication; INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -498,6 +509,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app; PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app;
PRODUCT_NAME = "Atlas for Mac"; PRODUCT_NAME = "Atlas for Mac";
SDKROOT = macosx; SDKROOT = macosx;
@@ -573,8 +585,11 @@
AD_HOC_CODE_SIGNING_ALLOWED = YES; AD_HOC_CODE_SIGNING_ALLOWED = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_CFBundleDisplayName = "Atlas for Mac"; INFOPLIST_KEY_CFBundleDisplayName = "Atlas for Mac";
INFOPLIST_KEY_CFBundleShortVersionString = "$(MARKETING_VERSION)";
INFOPLIST_KEY_CFBundleVersion = "$(CURRENT_PROJECT_VERSION)";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSPrincipalClass = NSApplication; INFOPLIST_KEY_NSPrincipalClass = NSApplication;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -582,6 +597,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 14.0; MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app; PRODUCT_BUNDLE_IDENTIFIER = com.atlasformac.app;
PRODUCT_NAME = "Atlas for Mac"; PRODUCT_NAME = "Atlas for Mac";
SDKROOT = macosx; SDKROOT = macosx;

27
CHANGELOG.md Normal file
View File

@@ -0,0 +1,27 @@
# Changelog
All notable changes to Atlas for Mac will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
### Added
- Native macOS app with 7 MVP modules: Overview, Smart Clean, Apps, History, Recovery, Permissions, Settings
- Recovery-first cleanup workflow — actions are reversible via Trash before permanent deletion
- Explainable recommendations — every suggestion shows reasoning before execution
- Bilingual UI: Simplified Chinese (default) and English, with persistent language preference
- AtlasDesignSystem shared design tokens: brand colors (teal/mint), typography, 4pt spacing grid, continuous corner radius
- Layered Swift Package architecture with strict dependency direction
- XPC worker architecture for sandboxed operations
- Privileged helper for elevated operations requiring administrator access
- Keyboard navigation and command shortcuts for the main shell
- Accessibility semantics and stable UI-automation identifiers
- Native packaging: `.app`, `.zip`, `.dmg`, `.pkg` artifact generation
- Go-based TUI tools inherited from upstream Mole: disk analyzer (`analyze`) and system monitor (`status`)
- CI/CD: GitHub Actions for formatting, linting, testing, CodeQL scanning, and release packaging
### Attribution
- Built in part on the open-source [Mole](https://github.com/tw93/mole) project (MIT) by tw93 and contributors

217
CODE_OF_CONDUCT.md Normal file
View File

@@ -0,0 +1,217 @@
# Contributor Covenant Code of Conduct
Atlas for Mac adopts the Contributor Covenant, version 2.1.
English is the authoritative version of this Code of Conduct. A Chinese translation is included below for convenience.
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances
of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for
moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official email address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
cszhk0310@gmail.com. All complaints will be reviewed and investigated promptly
and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
Community Impact: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
Consequence: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
Community Impact: A violation through a single incident or series of actions.
Consequence: A warning with consequences for continued behavior. No interaction
with the people involved, including unsolicited interaction with those
enforcing the Code of Conduct, for a specified period of time. This includes
avoiding interactions in community spaces as well as external channels like
social media. Violating these terms may lead to a temporary or permanent ban.
### 3. Temporary Ban
Community Impact: A serious violation of community standards, including
sustained inappropriate behavior.
Consequence: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
Community Impact: Demonstrating a pattern of violation of community standards,
including sustained inappropriate behavior, harassment of an individual, or
aggression toward or disparagement of classes of individuals.
Consequence: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version 2.1,
available at
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
Community Impact Guidelines were inspired by Mozilla's code of conduct
enforcement ladder.
For answers to common questions about this Code of Conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
---
# 贡献者公约行为准则
Atlas for Mac 采用 Contributor Covenant 2.1 版本。
以下中文内容为便于阅读而提供;如有歧义,以英文版为准。
## 我们的承诺
身为社区成员、贡献者和维护者,我们承诺让每一位参与者都能在社区中获得免于骚扰的体验,而不论其年龄、体型、显性或隐性的身心障碍、族裔、性征、性别认同和表达、经验水平、教育程度、社会经济地位、国籍、外貌、种族、种姓、肤色、宗教信仰,或性身份与性取向如何。
我们承诺以有助于建设开放、友善、多元、包容且健康社区的方式行事和互动。
## 我们的准则
有助于营造积极社区环境的行为包括:
- 对他人表现出同理心和善意
- 尊重不同的意见、观点和经历
- 提供并坦然接受建设性反馈
- 对自己的错误承担责任,向受影响的人道歉,并从经历中学习
- 关注整体社区的最佳利益,而不仅仅是个人利益
不可接受的行为包括:
- 使用带有性意味的语言或图像,或任何形式的性关注与示好
- 嘲讽、侮辱、贬损性评论,以及人身或政治攻击
- 公开或私下骚扰
- 未经明确许可公开他人的私人信息,例如住址或电子邮箱地址
- 其他在专业环境中可被合理认为不当的行为
## 执行责任
社区领导者有责任解释并执行我们关于可接受行为的标准,并将针对其认定为不当、威胁性、冒犯性或有害的行为采取适当且公正的纠正措施。
社区领导者有权删除、编辑或拒绝与本行为准则不一致的评论、提交、代码、Wiki 编辑、议题及其他贡献,并在适当情况下说明作出审核决定的原因。
## 适用范围
本行为准则适用于所有社区空间,也适用于个人在公共场合正式代表社区时的行为。代表社区的情形包括使用官方邮箱、通过官方社交媒体账号发布内容,或在在线或线下活动中担任指定代表。
## 举报与执行
辱骂、骚扰或其他不可接受的行为,可通过 cszhk0310@gmail.com 向负责执行的社区领导者报告。所有投诉都将被及时、公正地审查和调查。
所有社区领导者都有义务尊重事件报告者的隐私与安全。
## 执行指南
社区领导者将在判定违反本行为准则的后果时参考以下社区影响指南:
### 1. 纠正
社区影响:在社区中使用不当语言,或其他被认为不专业或不受欢迎的行为。
后果:社区领导者将发出私下书面警告,明确说明违规性质以及该行为为何不当;在某些情况下,可能会要求公开道歉。
### 2. 警告
社区影响:单次事件或一系列行为构成违规。
后果:发出警告,并说明继续违规的后果。在规定时间内,不得与相关人员互动,包括主动联系执行本准则的人员。这同样适用于社区空间以及社交媒体等外部渠道。违反这些限制可能导致临时或永久封禁。
### 3. 临时封禁
社区影响:严重违反社区准则,包括持续性的不当行为。
后果:在规定时间内,禁止与社区发生任何形式的互动或公开交流。在此期间,不得与相关人员进行任何公开或私下互动,也不得主动联系执行本准则的人员。违反这些限制可能导致永久封禁。
### 4. 永久封禁
社区影响:表现出持续违反社区准则的模式,包括持续性的不当行为、骚扰个人,或对特定群体表现出攻击性或贬损行为。
后果:永久禁止在社区内进行任何形式的公开互动。
## 归属说明
本行为准则改编自 Contributor Covenant 2.1 版本:
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
社区影响指南参考了 Mozilla 的行为准则执行阶梯。
常见问题可参见:
https://www.contributor-covenant.org/faq
更多翻译可参见:
https://www.contributor-covenant.org/translations

View File

@@ -1,181 +1,133 @@
# Contributing to Mole # Contributing to Atlas for Mac
## Setup Thank you for your interest in contributing to Atlas for Mac! This guide covers everything you need to get started.
## Prerequisites
- macOS 14.0 (Sonoma) or later
- Xcode 16+ (Swift 6.0)
- [xcodegen](https://github.com/yonaskolb/XcodeGen): `brew install xcodegen`
- Go 1.24+ (only for legacy CLI components)
## Getting Started
```bash ```bash
# Install development tools # Clone the repository
brew install shfmt shellcheck bats-core golangci-lint git clone https://github.com/CSZHK/CleanMyPc.git
cd CleanMyPc
# Install goimports for better Go formatting # Option A: Run directly
go install golang.org/x/tools/cmd/goimports@latest swift run --package-path Apps AtlasApp
# Option B: Open in Xcode
xcodegen generate
open Atlas.xcodeproj
``` ```
## Development ## Architecture Overview
Run quality checks before committing (auto-formats code): Atlas uses a layered Swift Package architecture with strict top-down dependency direction:
```
Apps/AtlasApp ← App entry point, state management, routing
Feature Packages ← One package per module (Overview, SmartClean, Apps, etc.)
AtlasDesignSystem ← Brand tokens, reusable UI components
AtlasDomain ← Core models, localization (AtlasL10n)
AtlasApplication ← Workspace controller, repository layer
AtlasInfrastructure ← Worker management, XPC communication
XPC/AtlasWorkerXPC ← Sandboxed worker service
Helpers/ ← Privileged helper for elevated operations
```
Each feature package depends only on `AtlasDesignSystem` + `AtlasDomain` and receives callbacks for parent coordination.
## Running Tests
```bash
# Domain, design system, adapters, and shared packages
swift test --package-path Packages
# App-level tests
swift test --package-path Apps
# Run a single test target
swift test --package-path Packages --filter AtlasDomainTests
# Full test suite (includes Go and shell tests)
./scripts/test.sh
```
## Code Quality
Run formatting and linting before committing:
```bash ```bash
./scripts/check.sh ./scripts/check.sh
``` ```
Run tests: CI will also run these checks automatically on every push and pull request.
```bash
./scripts/test.sh
```
## Code Style ## Code Style
### Basic Rules ### Swift
- Bash 3.2+ compatible (macOS default) - Swift 6.0 with strict concurrency enabled
- 4 spaces indent - Follow existing patterns in the codebase
- Use `set -euo pipefail` in all scripts - Use `AtlasL10n` for all user-facing strings — never hardcode display text
- Quote all variables: `"$variable"`
- Use `[[ ]]` not `[ ]` for tests
- Use `local` for function variables, `readonly` for constants
- Function names: `snake_case`
- BSD commands not GNU (e.g., `stat -f%z` not `stat --format`)
Config: `.editorconfig` and `.shellcheckrc` ### Design System
All UI should use the shared design tokens from `AtlasDesignSystem`:
- **Colors**: `AtlasColor` — brand (teal), accent (mint), semantic (success/warning/danger/info)
- **Typography**: `AtlasTypography` — screenTitle, heroMetric, sectionTitle, label, body, caption
- **Spacing**: `AtlasSpacing` — 4pt grid (xxs=4, xs=8, sm=12, md=16, lg=20, xl=24, section=32)
- **Radius**: `AtlasRadius` — continuous corners (sm=8, md=12, lg=16)
### File Operations ### File Operations
**Always use safe wrappers, never `rm -rf` directly:** All cleanup and deletion logic must use safe wrappers. Never use raw `rm -rf` or unguarded file removal. See `SECURITY_AUDIT.md` for details on path validation and deletion boundaries.
```bash ## Localization
# Single file/directory
safe_remove "/path/to/file"
# Purge files older than 7 days Atlas supports Simplified Chinese (default) and English.
safe_find_delete "$dir" "*.log" 7 "f"
# With sudo String files are located at:
safe_sudo_remove "/Library/Caches/com.example"
```
Packages/AtlasDomain/Sources/AtlasDomain/Resources/zh-Hans.lproj/Localizable.strings
Packages/AtlasDomain/Sources/AtlasDomain/Resources/en.lproj/Localizable.strings
``` ```
See `lib/core/file_ops.sh` for all safe functions. When adding user-facing text:
### Pipefail Safety 1. Add entries to **both** `.strings` files
2. Access strings via the `AtlasL10n` enum
All commands that might fail must be handled:
```bash
# Correct: handle failure
find /nonexistent -name "*.cache" 2>/dev/null || true
# Correct: check array before use
if [[ ${#array[@]} -gt 0 ]]; then
for item in "${array[@]}"; do
process "$item"
done
fi
# Correct: arithmetic operations
((count++)) || true
```
### Error Handling
```bash
# Network requests with timeout
result=$(curl -fsSL --connect-timeout 2 --max-time 3 "$url" 2>/dev/null || echo "")
# Command existence check
if ! command -v brew >/dev/null 2>&1; then
log_warning "Homebrew not installed"
return 0
fi
```
### UI and Logging
```bash
# Logging
log_info "Starting cleanup"
log_success "Cache cleaned"
log_warning "Some files skipped"
log_error "Operation failed"
# Spinners
with_spinner "Cleaning cache" rm -rf "$cache_dir"
# Or inline
start_inline_spinner "Processing..."
# ... work ...
stop_inline_spinner "Complete"
```
### Debug Mode
Enable debug output with `--debug`:
```bash
mo --debug clean
./bin/clean.sh --debug
```
Modules check the internal `MO_DEBUG` variable:
```bash
if [[ "${MO_DEBUG:-0}" == "1" ]]; then
echo "[MODULE] Debug message" >&2
fi
```
Format: `[MODULE_NAME] message` output to stderr.
## Requirements
- macOS 10.14 or newer, works on Intel and Apple Silicon
- Default macOS Bash 3.2+ plus administrator privileges for cleanup tasks
- Install Command Line Tools with `xcode-select --install` for curl, tar, and related utilities
- Go 1.24+ is required to build the `mo status` or `mo analyze` TUI binaries locally.
## Go Components ## Go Components
`mo status` and `mo analyze` use Go with Bubble Tea for interactive dashboards. The `cmd/analyze/` and `cmd/status/` directories contain Go-based TUI tools inherited from the upstream Mole project. These are built separately:
**Code organization:**
- Each module split into focused files by responsibility
- `cmd/analyze/` - Disk analyzer with 7 files under 500 lines each
- `cmd/status/` - System monitor with metrics split into 11 domain files
**Development workflow:**
- Format code with `gofmt -w ./cmd/...`
- Run `go vet ./cmd/...` to check for issues
- Build with `go build ./...` to verify all packages compile
**Building Go Binaries:**
For local development:
```bash ```bash
# Build binaries for current architecture make build # Build for current architecture
make build go run ./cmd/analyze # Run disk analyzer directly
go run ./cmd/status # Run system monitor directly
# Or run directly without building
go run ./cmd/analyze
go run ./cmd/status
``` ```
For releases, GitHub Actions builds architecture-specific binaries automatically.
**Guidelines:**
- Keep files focused on single responsibility
- Extract constants instead of magic numbers
- Use context for timeout control on external commands
- Add comments explaining **why** something is done, not just **what** is being done.
## Pull Requests ## Pull Requests
1. Fork and create branch from `main` 1. Fork the repository and create a branch from `main`
2. Make changes 2. Make your changes
3. Run checks: `./scripts/check.sh` 3. Run tests: `swift test --package-path Packages && swift test --package-path Apps`
4. Commit and push 4. Run quality checks: `./scripts/check.sh`
5. Open PR targeting `main` 5. Open a PR targeting `main`
CI will verify formatting, linting, and tests. CI will verify formatting, linting, and tests automatically.
## Security
If you discover a security vulnerability, **do not** open a public issue. Please report it privately following the instructions in [SECURITY.md](SECURITY.md).

View File

@@ -1,6 +1,7 @@
MIT License MIT License
Copyright (c) 2025 tw93 Copyright (c) 2025 tw93
Copyright (c) 2026 CSZHK
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -0,0 +1,84 @@
import AtlasDomain
import Foundation
public actor AtlasUpdateChecker {
public typealias DataLoader = @Sendable (URLRequest) async throws -> (Data, URLResponse)
private let releaseURL: URL
private let dataLoader: DataLoader
public init() {
self.releaseURL = URL(string: "https://api.github.com/repos/CSZHK/CleanMyPc/releases/latest")!
self.dataLoader = { request in
try await URLSession.shared.data(for: request)
}
}
init(
releaseURL: URL = URL(string: "https://api.github.com/repos/CSZHK/CleanMyPc/releases/latest")!,
dataLoader: @escaping DataLoader
) {
self.releaseURL = releaseURL
self.dataLoader = dataLoader
}
public func checkForUpdate(currentVersion: String) async throws -> AtlasAppUpdate {
var request = URLRequest(url: releaseURL)
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
request.timeoutInterval = 15
let (data, response) = try await dataLoader(request)
guard let httpResponse = response as? HTTPURLResponse else {
throw AtlasUpdateCheckerError.requestFailed
}
switch httpResponse.statusCode {
case 200..<300:
break
case 404:
throw AtlasUpdateCheckerError.noPublishedRelease
default:
throw AtlasUpdateCheckerError.requestFailed
}
let release = try JSONDecoder().decode(GitHubRelease.self, from: data)
let latestVersion = release.tagName
let isNewer = AtlasVersionComparator.isNewer(latestVersion, than: currentVersion)
return AtlasAppUpdate(
currentVersion: currentVersion,
latestVersion: latestVersion,
releaseURL: URL(string: release.htmlURL),
releaseNotes: release.body,
isUpdateAvailable: isNewer
)
}
}
public enum AtlasUpdateCheckerError: LocalizedError, Sendable, Equatable {
case requestFailed
case noPublishedRelease
public var errorDescription: String? {
switch self {
case .requestFailed:
return AtlasL10n.string("update.error.requestFailed")
case .noPublishedRelease:
return AtlasL10n.string("update.notice.noPublishedRelease")
}
}
}
private struct GitHubRelease: Decodable, Sendable {
let tagName: String
let htmlURL: String
let body: String?
enum CodingKeys: String, CodingKey {
case tagName = "tag_name"
case htmlURL = "html_url"
case body
}
}

View File

@@ -0,0 +1,69 @@
import Foundation
import XCTest
@testable import AtlasApplication
final class AtlasUpdateCheckerTests: XCTestCase {
func testCheckForUpdateReturnsAvailableRelease() async throws {
let checker = AtlasUpdateChecker { request in
XCTAssertEqual(
request.url?.absoluteString,
"https://api.github.com/repos/CSZHK/CleanMyPc/releases/latest"
)
let body = """
{
"tag_name": "V1.2.3",
"html_url": "https://github.com/CSZHK/CleanMyPc/releases/tag/V1.2.3",
"body": "Release notes"
}
"""
return (
Data(body.utf8),
HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!
)
}
let result = try await checker.checkForUpdate(currentVersion: "1.0.0")
XCTAssertEqual(result.currentVersion, "1.0.0")
XCTAssertEqual(result.latestVersion, "V1.2.3")
XCTAssertEqual(result.releaseURL?.absoluteString, "https://github.com/CSZHK/CleanMyPc/releases/tag/V1.2.3")
XCTAssertEqual(result.releaseNotes, "Release notes")
XCTAssertTrue(result.isUpdateAvailable)
}
func testCheckForUpdateTreatsMissingReleaseAsUnavailable() async {
let checker = AtlasUpdateChecker { request in
(
Data(),
HTTPURLResponse(url: request.url!, statusCode: 404, httpVersion: nil, headerFields: nil)!
)
}
do {
_ = try await checker.checkForUpdate(currentVersion: "1.0.0")
XCTFail("Expected missing releases to be reported explicitly")
} catch let error as AtlasUpdateCheckerError {
XCTAssertEqual(error, .noPublishedRelease)
} catch {
XCTFail("Unexpected error: \(error)")
}
}
func testCheckForUpdateMapsUnexpectedStatusToRequestFailure() async {
let checker = AtlasUpdateChecker { request in
(
Data(),
HTTPURLResponse(url: request.url!, statusCode: 500, httpVersion: nil, headerFields: nil)!
)
}
do {
_ = try await checker.checkForUpdate(currentVersion: "1.0.0")
XCTFail("Expected a request failure")
} catch let error as AtlasUpdateCheckerError {
XCTAssertEqual(error, .requestFailed)
} catch {
XCTFail("Unexpected error: \(error)")
}
}
}

View File

@@ -0,0 +1,53 @@
import Foundation
public struct AtlasAppUpdate: Sendable, Equatable {
public let currentVersion: String
public let latestVersion: String
public let releaseURL: URL?
public let releaseNotes: String?
public let isUpdateAvailable: Bool
public init(
currentVersion: String,
latestVersion: String,
releaseURL: URL?,
releaseNotes: String?,
isUpdateAvailable: Bool
) {
self.currentVersion = currentVersion
self.latestVersion = latestVersion
self.releaseURL = releaseURL
self.releaseNotes = releaseNotes
self.isUpdateAvailable = isUpdateAvailable
}
}
public enum AtlasVersionComparator {
public static func compare(_ lhs: String, _ rhs: String) -> ComparisonResult {
let lhsParts = parse(lhs)
let rhsParts = parse(rhs)
let maxCount = max(lhsParts.count, rhsParts.count)
for index in 0..<maxCount {
let left = index < lhsParts.count ? lhsParts[index] : 0
let right = index < rhsParts.count ? rhsParts[index] : 0
if left < right { return .orderedAscending }
if left > right { return .orderedDescending }
}
return .orderedSame
}
public static func isNewer(_ candidate: String, than current: String) -> Bool {
compare(current, candidate) == .orderedAscending
}
private static func parse(_ version: String) -> [Int] {
var cleaned = version
if cleaned.hasPrefix("v") || cleaned.hasPrefix("V") {
cleaned = String(cleaned.dropFirst())
}
return cleaned
.split(separator: ".")
.compactMap { Int($0) }
}
}

View File

@@ -611,3 +611,19 @@
"storage.screen.title" = "Storage"; "storage.screen.title" = "Storage";
"storage.screen.subtitle" = "Reserved list-based storage views for a future scope decision."; "storage.screen.subtitle" = "Reserved list-based storage views for a future scope decision.";
"storage.largeItems.title" = "Large Items"; "storage.largeItems.title" = "Large Items";
"update.version.title" = "Version";
"update.version.current" = "Current Version";
"update.version.build" = "Build";
"update.check.action" = "Check for Updates";
"update.check.checking" = "Checking…";
"update.available.title" = "New version %@ available";
"update.available.detail" = "Current version %@, latest version %@. Download the update from GitHub.";
"update.available.download" = "Download";
"update.upToDate.title" = "You're up to date";
"update.upToDate.detail" = "Version %@ is the latest release.";
"update.notice.title" = "Updates unavailable";
"update.notice.noPublishedRelease" = "Atlas for Mac has not published a downloadable release yet.";
"update.error.title" = "Update check failed";
"update.error.detail" = "Could not reach the update server. Please try again later.";
"update.error.requestFailed" = "Could not connect to the GitHub release server.";

View File

@@ -611,3 +611,19 @@
"storage.screen.title" = "存储"; "storage.screen.title" = "存储";
"storage.screen.subtitle" = "为未来版本预留的基于列表的存储视图。"; "storage.screen.subtitle" = "为未来版本预留的基于列表的存储视图。";
"storage.largeItems.title" = "大文件"; "storage.largeItems.title" = "大文件";
"update.version.title" = "版本信息";
"update.version.current" = "当前版本";
"update.version.build" = "构建号";
"update.check.action" = "检查更新";
"update.check.checking" = "正在检查…";
"update.available.title" = "发现新版本 %@";
"update.available.detail" = "当前版本 %@,最新版本 %@。建议前往 GitHub 下载更新。";
"update.available.download" = "前往下载";
"update.upToDate.title" = "已是最新版本";
"update.upToDate.detail" = "当前版本 %@ 已是最新,无需更新。";
"update.notice.title" = "暂时无法提供更新";
"update.notice.noPublishedRelease" = "Atlas for Mac 目前还没有发布可下载安装的版本。";
"update.error.title" = "检查更新失败";
"update.error.detail" = "无法连接到更新服务器,请稍后再试。";
"update.error.requestFailed" = "无法连接到 GitHub 更新服务器。";

View File

@@ -0,0 +1,80 @@
import XCTest
@testable import AtlasDomain
final class AtlasVersionComparatorTests: XCTestCase {
func testEqualVersions() {
XCTAssertEqual(
AtlasVersionComparator.compare("1.0.0", "1.0.0"),
.orderedSame
)
}
func testPrefixVIsStripped() {
XCTAssertEqual(
AtlasVersionComparator.compare("V1.0.0", "1.0.0"),
.orderedSame
)
}
func testLowercaseVPrefix() {
XCTAssertEqual(
AtlasVersionComparator.compare("v1.0.0", "1.0.0"),
.orderedSame
)
}
func testNewerPatchVersion() {
XCTAssertTrue(
AtlasVersionComparator.isNewer("1.0.1", than: "1.0.0")
)
}
func testNewerMinorVersion() {
XCTAssertTrue(
AtlasVersionComparator.isNewer("1.1.0", than: "1.0.0")
)
}
func testNewerMajorVersion() {
XCTAssertTrue(
AtlasVersionComparator.isNewer("2.0.0", than: "1.99.99")
)
}
func testVPrefixNewerThanCurrent() {
XCTAssertTrue(
AtlasVersionComparator.isNewer("V1.30.0", than: "1.0.0")
)
}
func testVPrefixOlderThanCurrent() {
XCTAssertFalse(
AtlasVersionComparator.isNewer("V1.0.0", than: "1.0.1")
)
}
func testBothVPrefixed() {
XCTAssertTrue(
AtlasVersionComparator.isNewer("V2.0.0", than: "V1.99.99")
)
}
func testSameVersionIsNotNewer() {
XCTAssertFalse(
AtlasVersionComparator.isNewer("1.0.0", than: "1.0.0")
)
}
func testTwoComponentVersion() {
XCTAssertTrue(
AtlasVersionComparator.isNewer("1.1", than: "1.0")
)
}
func testMismatchedComponentCount() {
XCTAssertTrue(
AtlasVersionComparator.isNewer("1.0.1", than: "1.0")
)
}
}

View File

@@ -36,31 +36,10 @@ public struct AboutFeatureView: View {
.font(AtlasTypography.body) .font(AtlasTypography.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
Divider()
.padding(.vertical, AtlasSpacing.xs)
HStack(spacing: AtlasSpacing.md) {
SocialBadge(
assetName: "icon-wechat",
label: AtlasL10n.string("about.social.wechat")
)
SocialBadge(
assetName: "icon-xiaohongshu",
label: AtlasL10n.string("about.social.xiaohongshu")
)
SocialBadge(
assetName: "icon-x",
label: AtlasL10n.string("about.social.x"),
url: "https://x.com/lizikk_zhu"
)
SocialBadge(
assetName: "icon-discord",
label: AtlasL10n.string("about.social.discord"),
url: "https://discord.gg"
)
}
} }
SocialGrid()
AtlasCallout( AtlasCallout(
title: AtlasL10n.string("about.author.quote"), title: AtlasL10n.string("about.author.quote"),
detail: AtlasL10n.string("about.author.name"), detail: AtlasL10n.string("about.author.name"),
@@ -107,33 +86,110 @@ public struct AboutFeatureView: View {
} }
} }
private struct SocialBadge: View { // MARK: - Social Grid
let assetName: String
private struct SocialGrid: View {
var body: some View {
HStack(spacing: AtlasSpacing.md) {
SocialCard(
iconAsset: "icon-wechat",
label: AtlasL10n.string("about.social.wechat"),
qrCodeAsset: "qrcode-wechat"
)
SocialCard(
iconAsset: "icon-xiaohongshu",
label: AtlasL10n.string("about.social.xiaohongshu"),
qrCodeAsset: "qrcode-xiaohongshu"
)
SocialCard(
iconAsset: "icon-x",
label: AtlasL10n.string("about.social.x"),
url: "https://x.com/lizikk_zhu"
)
SocialCard(
iconAsset: "icon-discord",
label: AtlasL10n.string("about.social.discord"),
url: "https://discord.gg/aR2kF8Xman"
)
}
}
}
private struct SocialCard: View {
let iconAsset: String
let label: String let label: String
var qrCodeAsset: String? = nil
var url: String? = nil var url: String? = nil
var body: some View { @State private var isHovering = false
let content = VStack(spacing: AtlasSpacing.xs) {
Image(assetName, bundle: .module)
.resizable()
.scaledToFit()
.frame(width: 28, height: 28)
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
Text(label) var body: some View {
.font(.caption2) Group {
.foregroundStyle(.secondary) if let url, let destination = URL(string: url) {
.lineLimit(1) Link(destination: destination) { cardContent }
} else {
cardContent
}
} }
.frame(maxWidth: .infinity) .onHover { isHovering = $0 }
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)
.accessibilityLabel(label) .accessibilityLabel(label)
}
if let url, let destination = URL(string: url) { private var cardContent: some View {
Link(destination: destination) { content } VStack(spacing: AtlasSpacing.sm) {
} else { if let qrCodeAsset {
content Image(qrCodeAsset, bundle: .module)
.resizable()
.interpolation(.high)
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 4, style: .continuous))
.padding(.horizontal, AtlasSpacing.section)
.padding(.top, AtlasSpacing.sm)
} else {
Spacer()
Image(iconAsset, bundle: .module)
.resizable()
.scaledToFit()
.frame(width: 52, height: 52)
Spacer()
}
HStack(spacing: AtlasSpacing.xxs) {
Image(iconAsset, bundle: .module)
.resizable()
.scaledToFit()
.frame(width: 12, height: 12)
Text(label)
.font(AtlasTypography.captionSmall)
.foregroundStyle(.secondary)
.lineLimit(1)
if url != nil {
Image(systemName: "arrow.up.right")
.font(.system(size: 8, weight: .bold))
.foregroundStyle(.tertiary)
}
}
.padding(.bottom, AtlasSpacing.xs)
} }
.frame(maxWidth: .infinity, minHeight: 120)
.background(
RoundedRectangle(cornerRadius: AtlasRadius.lg, style: .continuous)
.fill(AtlasColor.cardRaised)
)
.overlay(
RoundedRectangle(cornerRadius: AtlasRadius.lg, style: .continuous)
.strokeBorder(
isHovering ? AtlasColor.borderEmphasis : AtlasColor.border,
lineWidth: 1
)
)
.scaleEffect(isHovering ? 1.02 : 1.0)
.animation(.easeOut(duration: 0.15), value: isHovering)
} }
} }

View File

@@ -0,0 +1,154 @@
import AtlasDesignSystem
import AtlasDomain
import SwiftUI
public struct AboutUpdateToolbarButton: View {
private let appVersion: String
private let appBuild: String
private let updateResult: AtlasAppUpdate?
private let isCheckingForUpdate: Bool
private let updateCheckNotice: String?
private let updateCheckError: String?
private let onCheckForUpdate: () -> Void
@State private var isPopoverPresented = false
public init(
appVersion: String,
appBuild: String,
updateResult: AtlasAppUpdate?,
isCheckingForUpdate: Bool,
updateCheckNotice: String?,
updateCheckError: String?,
onCheckForUpdate: @escaping () -> Void
) {
self.appVersion = appVersion
self.appBuild = appBuild
self.updateResult = updateResult
self.isCheckingForUpdate = isCheckingForUpdate
self.updateCheckNotice = updateCheckNotice
self.updateCheckError = updateCheckError
self.onCheckForUpdate = onCheckForUpdate
}
public var body: some View {
Button {
isPopoverPresented.toggle()
} label: {
HStack(spacing: AtlasSpacing.xs) {
if isCheckingForUpdate {
ProgressView()
.controlSize(.small)
}
Text("v\(appVersion)")
.font(AtlasTypography.caption)
.foregroundStyle(AtlasColor.textPrimary)
if updateResult?.isUpdateAvailable == true {
Circle()
.fill(AtlasColor.danger)
.frame(width: 8, height: 8)
}
}
.padding(.horizontal, AtlasSpacing.md)
.padding(.vertical, AtlasSpacing.sm)
.background(
Capsule(style: .continuous)
.fill(AtlasColor.cardRaised)
)
.overlay(
Capsule(style: .continuous)
.stroke(AtlasColor.borderEmphasis, lineWidth: 1)
)
}
.buttonStyle(.plain)
.popover(isPresented: $isPopoverPresented, arrowEdge: .top) {
popoverContent
}
}
private var popoverContent: some View {
VStack(alignment: .leading, spacing: AtlasSpacing.lg) {
AtlasInfoCard(
title: AtlasL10n.string("update.version.title")
) {
AtlasDetailRow(
title: AtlasL10n.string("update.version.current"),
subtitle: appVersion,
systemImage: "tag"
)
AtlasDetailRow(
title: AtlasL10n.string("update.version.build"),
subtitle: appBuild,
systemImage: "hammer"
)
Button {
onCheckForUpdate()
} label: {
HStack(spacing: AtlasSpacing.xs) {
if isCheckingForUpdate {
ProgressView()
.controlSize(.small)
}
Text(isCheckingForUpdate
? AtlasL10n.string("update.check.checking")
: AtlasL10n.string("update.check.action"))
.frame(maxWidth: .infinity)
}
}
.buttonStyle(.atlasSecondary)
.disabled(isCheckingForUpdate)
.padding(.top, AtlasSpacing.sm)
}
if let result = updateResult {
if result.isUpdateAvailable {
AtlasCallout(
title: AtlasL10n.string("update.available.title", result.latestVersion),
detail: AtlasL10n.string("update.available.detail", result.currentVersion, result.latestVersion),
tone: .warning,
systemImage: "arrow.down.circle"
)
if let url = result.releaseURL {
Link(destination: url) {
Text(AtlasL10n.string("update.available.download"))
.frame(maxWidth: .infinity)
}
.buttonStyle(.atlasPrimary)
}
} else {
AtlasCallout(
title: AtlasL10n.string("update.upToDate.title"),
detail: AtlasL10n.string("update.upToDate.detail", result.currentVersion),
tone: .success,
systemImage: "checkmark.circle"
)
}
}
if let notice = updateCheckNotice {
AtlasCallout(
title: AtlasL10n.string("update.notice.title"),
detail: notice,
tone: .neutral,
systemImage: "info.circle"
)
}
if let error = updateCheckError {
AtlasCallout(
title: AtlasL10n.string("update.error.title"),
detail: error,
tone: .danger,
systemImage: "exclamationmark.triangle"
)
}
}
.padding(AtlasSpacing.xl)
.frame(width: 360)
}
}

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "qrcode-wechat.jpg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "qrcode-xiaohongshu.jpg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -12,6 +12,39 @@ Atlas for Mac is a native macOS application for people who need to understand wh
This repository is the working source for the new Atlas for Mac product. Atlas for Mac itself is open source under the MIT License. It remains an independent project and may reuse selected upstream Mole capabilities under the MIT License, but user-facing naming, release materials, and product direction are Atlas-first. This repository is the working source for the new Atlas for Mac product. Atlas for Mac itself is open source under the MIT License. It remains an independent project and may reuse selected upstream Mole capabilities under the MIT License, but user-facing naming, release materials, and product direction are Atlas-first.
## Installation
### Download
Download the latest release from the [Releases](https://github.com/CSZHK/CleanMyPc/releases) page:
- **`.dmg`** — Recommended. Open the disk image and drag Atlas to your Applications folder.
- **`.zip`** — Extract and move Atlas.app to your Applications folder.
- **`.pkg`** — Run the installer package for guided installation.
### Requirements
- macOS 14.0 (Sonoma) or later
- Apple Silicon or Intel Mac
### Build from Source
```bash
git clone https://github.com/CSZHK/CleanMyPc.git
cd CleanMyPc
swift run --package-path Apps AtlasApp
```
Or open in Xcode:
```bash
brew install xcodegen
xcodegen generate
open Atlas.xcodeproj
```
> **Note**: The app is currently unsigned. On first launch, you may need to right-click and select "Open" to bypass Gatekeeper, or go to System Settings > Privacy & Security to allow it.
## MVP Modules ## MVP Modules
- `Overview` - `Overview`

View File

@@ -1,13 +1,13 @@
# Security Policy # Security Policy
Mole is a local system maintenance tool. It includes high-risk operations such as cleanup, uninstall, optimization, and artifact removal. We treat safety boundaries, deletion logic, and release integrity as security-sensitive areas. Atlas for Mac is a local system maintenance tool. It includes high-risk operations such as cleanup, uninstall, optimization, and artifact removal. We treat safety boundaries, deletion logic, and release integrity as security-sensitive areas.
## Reporting a Vulnerability ## Reporting a Vulnerability
Please report suspected security issues privately. Please report suspected security issues privately.
- Email: `hitw93@gmail.com` - Email: `cszhk0310@gmail.com`
- Subject line: `Mole security report` - Subject line: `Atlas for Mac security report`
Do not open a public GitHub issue for an unpatched vulnerability. Do not open a public GitHub issue for an unpatched vulnerability.
@@ -15,7 +15,7 @@ If GitHub Security Advisories private reporting is enabled for the repository, y
Include as much of the following as possible: Include as much of the following as possible:
- Mole version and install method - Atlas for Mac version and install method
- macOS version - macOS version
- Exact command or workflow involved - Exact command or workflow involved
- Reproduction steps or proof of concept - Reproduction steps or proof of concept
@@ -55,14 +55,14 @@ Examples of security-relevant issues include:
The following are usually normal bugs, feature requests, or documentation issues rather than security issues: The following are usually normal bugs, feature requests, or documentation issues rather than security issues:
- Cleanup misses that leave recoverable junk behind - Cleanup misses that leave recoverable junk behind
- False negatives where Mole refuses to clean something - False negatives where Atlas for Mac refuses to clean something
- Cosmetic UI problems - Cosmetic UI problems
- Requests for broader or more aggressive cleanup behavior - Requests for broader or more aggressive cleanup behavior
- Compatibility issues without a plausible security impact - Compatibility issues without a plausible security impact
If you are unsure whether something is security-relevant, report it privately first. If you are unsure whether something is security-relevant, report it privately first.
## Security-Focused Areas in Mole ## Security-Focused Areas in Atlas for Mac
The project pays particular attention to: The project pays particular attention to:

View File

@@ -1,10 +1,10 @@
# Mole Security Audit # Atlas for Mac Security Audit
This document describes the security-relevant behavior of the current `main` branch. It is intended as a public description of Mole's safety boundaries, destructive-operation controls, release integrity signals, and known limitations. This document describes the security-relevant behavior of the current `main` branch. It is intended as a public description of Atlas for Mac's safety boundaries, destructive-operation controls, release integrity signals, and known limitations.
## Executive Summary ## Executive Summary
Mole is a local system maintenance tool. Its main risk surface is not remote code execution; it is unintended local damage caused by cleanup, uninstall, optimize, purge, installer cleanup, or other destructive operations. Atlas for Mac is a local system maintenance tool. Its main risk surface is not remote code execution; it is unintended local damage caused by cleanup, uninstall, optimize, purge, installer cleanup, or other destructive operations.
The project is designed around safety-first defaults: The project is designed around safety-first defaults:
@@ -14,7 +14,7 @@ The project is designed around safety-first defaults:
- symlink handling is conservative - symlink handling is conservative
- preview, confirmation, timeout, and operation logging are used to make destructive behavior more visible and auditable - preview, confirmation, timeout, and operation logging are used to make destructive behavior more visible and auditable
Mole prioritizes bounded cleanup over aggressive cleanup. When uncertainty exists, the tool should refuse, skip, or require stronger confirmation instead of widening deletion scope. Atlas for Mac prioritizes bounded cleanup over aggressive cleanup. When uncertainty exists, the tool should refuse, skip, or require stronger confirmation instead of widening deletion scope.
The project continues to strengthen: The project continues to strengthen:
@@ -24,7 +24,7 @@ The project continues to strengthen:
## Threat Surface ## Threat Surface
The highest-risk areas in Mole are: The highest-risk areas in Atlas for Mac are:
- direct file and directory deletion - direct file and directory deletion
- recursive cleanup across common user and system cache locations - recursive cleanup across common user and system cache locations
@@ -133,7 +133,7 @@ See [`journal/2026-03-11-safe-remove-design.md`](journal/2026-03-11-safe-remove-
## Protected Directories and Categories ## Protected Directories and Categories
Mole has explicit protected-path and protected-category logic in addition to root-path blocking. Atlas for Mac has explicit protected-path and protected-category logic in addition to root-path blocking.
Protected or conservatively handled categories include: Protected or conservatively handled categories include:
@@ -181,7 +181,7 @@ Path traversal handling is also explicit:
## Privilege Escalation and Sudo Boundaries ## Privilege Escalation and Sudo Boundaries
Mole uses sudo for a subset of system-maintenance paths, but elevated behavior is still bounded by validation and protected-path rules. Atlas for Mac uses sudo for a subset of system-maintenance paths, but elevated behavior is still bounded by validation and protected-path rules.
Key properties: Key properties:
@@ -192,11 +192,11 @@ Key properties:
- sudo cleanup skips or reports denied operations instead of widening scope - sudo cleanup skips or reports denied operations instead of widening scope
- authentication, SIP/MDM, and read-only filesystem failures are classified separately in file-operation results - authentication, SIP/MDM, and read-only filesystem failures are classified separately in file-operation results
When sudo is denied or unavailable, Mole prefers skipping privileged cleanup to forcing execution through unsafe fallback behavior. When sudo is denied or unavailable, Atlas for Mac prefers skipping privileged cleanup to forcing execution through unsafe fallback behavior.
## Sensitive Data Exclusions ## Sensitive Data Exclusions
Mole is not intended to aggressively delete high-value user data. Atlas for Mac is not intended to aggressively delete high-value user data.
Examples of conservative handling include: Examples of conservative handling include:
@@ -218,7 +218,7 @@ This reduces the risk of incorrectly classifying active software as orphaned dat
## Dry-Run, Confirmation, and Audit Logging ## Dry-Run, Confirmation, and Audit Logging
Mole exposes multiple safety controls before and during destructive actions: Atlas for Mac exposes multiple safety controls before and during destructive actions:
- `--dry-run` previews are available for major destructive commands - `--dry-run` previews are available for major destructive commands
- interactive high-risk flows require explicit confirmation before deletion - interactive high-risk flows require explicit confirmation before deletion
@@ -236,7 +236,7 @@ Relevant timeout behavior includes:
## Release Integrity and Continuous Security Signals ## Release Integrity and Continuous Security Signals
Mole treats release trust as part of its security posture, not just a packaging detail. Atlas for Mac treats release trust as part of its security posture, not just a packaging detail.
Repository-level signals include: Repository-level signals include:

View File

@@ -37,7 +37,11 @@ targets:
base: base:
PRODUCT_BUNDLE_IDENTIFIER: com.atlasformac.app.worker PRODUCT_BUNDLE_IDENTIFIER: com.atlasformac.app.worker
PRODUCT_NAME: AtlasWorkerXPC PRODUCT_NAME: AtlasWorkerXPC
MARKETING_VERSION: "1.0.0"
CURRENT_PROJECT_VERSION: 1
GENERATE_INFOPLIST_FILE: YES GENERATE_INFOPLIST_FILE: YES
INFOPLIST_KEY_CFBundleShortVersionString: $(MARKETING_VERSION)
INFOPLIST_KEY_CFBundleVersion: $(CURRENT_PROJECT_VERSION)
AD_HOC_CODE_SIGNING_ALLOWED: YES AD_HOC_CODE_SIGNING_ALLOWED: YES
dependencies: dependencies:
- package: Packages - package: Packages
@@ -58,7 +62,11 @@ targets:
base: base:
PRODUCT_BUNDLE_IDENTIFIER: com.atlasformac.app PRODUCT_BUNDLE_IDENTIFIER: com.atlasformac.app
PRODUCT_NAME: Atlas for Mac PRODUCT_NAME: Atlas for Mac
MARKETING_VERSION: "1.0.0"
CURRENT_PROJECT_VERSION: 1
GENERATE_INFOPLIST_FILE: YES GENERATE_INFOPLIST_FILE: YES
INFOPLIST_KEY_CFBundleShortVersionString: $(MARKETING_VERSION)
INFOPLIST_KEY_CFBundleVersion: $(CURRENT_PROJECT_VERSION)
AD_HOC_CODE_SIGNING_ALLOWED: YES AD_HOC_CODE_SIGNING_ALLOWED: YES
INFOPLIST_KEY_CFBundleDisplayName: Atlas for Mac INFOPLIST_KEY_CFBundleDisplayName: Atlas for Mac
INFOPLIST_KEY_LSApplicationCategoryType: public.app-category.utilities INFOPLIST_KEY_LSApplicationCategoryType: public.app-category.utilities

View File

@@ -16,9 +16,23 @@ if [[ -f "$ROOT_DIR/project.yml" ]]; then
fi fi
fi fi
xcodebuild \ VERSION_OVERRIDES=()
-project "$PROJECT_PATH" \ if [[ -n "${ATLAS_VERSION:-}" ]]; then
-scheme "$SCHEME" \ VERSION_OVERRIDES+=(MARKETING_VERSION="$ATLAS_VERSION")
-configuration "$CONFIGURATION" \ fi
-derivedDataPath "$DERIVED_DATA_PATH" \ if [[ -n "${ATLAS_BUILD_NUMBER:-}" ]]; then
build VERSION_OVERRIDES+=(CURRENT_PROJECT_VERSION="$ATLAS_BUILD_NUMBER")
fi
xcodebuild_args=(
-project "$PROJECT_PATH"
-scheme "$SCHEME"
-configuration "$CONFIGURATION"
-derivedDataPath "$DERIVED_DATA_PATH"
)
if (( ${#VERSION_OVERRIDES[@]} > 0 )); then
xcodebuild_args+=("${VERSION_OVERRIDES[@]}")
fi
xcodebuild "${xcodebuild_args[@]}" build