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:
@@ -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
2
.github/CODEOWNERS
vendored
@@ -1 +1 @@
|
|||||||
* @tw93
|
* @CSZHK
|
||||||
|
|||||||
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@@ -1,2 +1 @@
|
|||||||
github: ['tw93']
|
github: ['CSZHK']
|
||||||
custom: ['https://miaoyan.app/cats.html?name=Mole']
|
|
||||||
|
|||||||
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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.
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||||
|
|||||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -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: ''
|
||||||
|
|||||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -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
|
||||||
|
|||||||
4
.github/workflows/check.yml
vendored
4
.github/workflows/check.yml
vendored
@@ -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"
|
||||||
|
|||||||
50
.github/workflows/release.yml
vendored
50
.github/workflows/release.yml
vendored
@@ -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)"
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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: ""]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
27
CHANGELOG.md
Normal 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
217
CODE_OF_CONDUCT.md
Normal 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
|
||||||
242
CONTRIBUTING.md
242
CONTRIBUTING.md
@@ -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).
|
||||||
|
|||||||
1
LICENSE
1
LICENSE
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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.";
|
||||||
|
|||||||
@@ -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 更新服务器。";
|
||||||
|
|||||||
@@ -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")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 |
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "qrcode-xiaohongshu.jpg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
33
README.md
33
README.md
@@ -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`
|
||||||
|
|||||||
12
SECURITY.md
12
SECURITY.md
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user