mirror of
https://github.com/maxgoedjen/secretive.git
synced 2026-04-10 03:07:22 +02:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8cf0db1c5 | ||
|
|
412687467b | ||
|
|
416a7d5f40 | ||
|
|
c4605fb60e | ||
|
|
61eed5987c | ||
|
|
cbef7c6181 | ||
|
|
63a09390b8 | ||
|
|
a4e1ab9eb6 | ||
|
|
147f4d9908 | ||
|
|
ddcb2a36ec |
21
.github/workflows/nightly.yml
vendored
21
.github/workflows/nightly.yml
vendored
@@ -3,10 +3,16 @@ name: Nightly
|
|||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 8 * * *"
|
- cron: "0 8 * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
# runs-on: macOS-latest
|
# runs-on: macOS-latest
|
||||||
runs-on: macos-15
|
runs-on: macos-15
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
|
attestations: write
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
@@ -30,22 +36,23 @@ jobs:
|
|||||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
||||||
- name: Build
|
- name: Build
|
||||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||||
- name: Create ZIPs
|
- name: Create ZIP
|
||||||
run: |
|
run: |
|
||||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
||||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Archive.zip
|
|
||||||
- name: Notarize
|
- name: Notarize
|
||||||
env:
|
env:
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||||
- name: Attest
|
|
||||||
id: attest
|
|
||||||
uses: actions/attest-build-provenance@v2
|
|
||||||
with:
|
|
||||||
subject-path: 'Secretive.zip'
|
|
||||||
- name: Upload App to Artifacts
|
- name: Upload App to Artifacts
|
||||||
|
id: upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: Secretive.zip
|
name: Secretive.zip
|
||||||
path: Secretive.zip
|
path: Secretive.zip
|
||||||
|
- name: Attest
|
||||||
|
id: attest
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-name: "Secretive.zip"
|
||||||
|
subject-digest: sha256:${{ steps.upload.outputs.artifact-digest }}
|
||||||
|
|||||||
23
.github/workflows/release.yml
vendored
23
.github/workflows/release.yml
vendored
@@ -56,39 +56,34 @@ jobs:
|
|||||||
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
sed -i '' -e "s/GITHUB_BUILD_URL/https:\/\/github.com\/maxgoedjen\/secretive\/actions\/runs\/$RUN_ID/g" Sources/Secretive/Credits.rtf
|
||||||
- name: Build
|
- name: Build
|
||||||
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
run: xcrun xcodebuild -project Sources/Secretive.xcodeproj -scheme Secretive -configuration Release -archivePath Archive.xcarchive archive
|
||||||
- name: Create ZIPs
|
- name: Create ZIP
|
||||||
run: |
|
run: |
|
||||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive/Products/Applications/Secretive.app ./Secretive.zip
|
||||||
ditto -c -k --sequesterRsrc --keepParent Archive.xcarchive ./Xcode_Archive.zip
|
|
||||||
- name: Notarize
|
- name: Notarize
|
||||||
env:
|
env:
|
||||||
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
|
||||||
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
|
||||||
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
run: xcrun notarytool submit --key ~/.private_keys/AuthKey_$APPLE_API_KEY_ID.p8 --key-id $APPLE_API_KEY_ID --issuer $APPLE_API_ISSUER Secretive.zip
|
||||||
|
- name: Upload App to Artifacts
|
||||||
|
id: upload
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: Secretive.zip
|
||||||
|
path: Secretive.zip
|
||||||
- name: Attest
|
- name: Attest
|
||||||
id: attest
|
id: attest
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@v2
|
||||||
with:
|
with:
|
||||||
subject-path: 'Secretive.zip, Xcode_Archive.zip'
|
subject-name: "Secretive.zip"
|
||||||
|
subject-digest: ${{ steps.upload.outputs.artifact-digest }}
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
run: |
|
run: |
|
||||||
sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md
|
sed -i.tmp "s/RUN_ID/$RUN_ID/g" .github/templates/release.md
|
||||||
sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md
|
sed -i.tmp "s/ATTESTATION_ID/$ATTESTATION_ID/g" .github/templates/release.md
|
||||||
gh release create $TAG_NAME -d -F .github/templates/release.md
|
gh release create $TAG_NAME -d -F .github/templates/release.md
|
||||||
gh release upload Secretive.zip
|
gh release upload Secretive.zip
|
||||||
gh release upload Xcode_Archive.zip
|
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
TAG_NAME: ${{ github.ref }}
|
TAG_NAME: ${{ github.ref }}
|
||||||
RUN_ID: ${{ github.run_id }}
|
RUN_ID: ${{ github.run_id }}
|
||||||
ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }}
|
ATTESTATION_ID: ${{ steps.attest.outputs.attestation-id }}
|
||||||
- name: Upload App to Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: Secretive.zip
|
|
||||||
path: Secretive.zip
|
|
||||||
- name: Upload Archive to Artifacts
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: Xcode_Archive.zip
|
|
||||||
path: Xcode_Archive.zip
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ let package = Package(
|
|||||||
)
|
)
|
||||||
|
|
||||||
var localization: Resource {
|
var localization: Resource {
|
||||||
.process("../../Localizable.xcstrings")
|
.process("../../Resources/Localizable.xcstrings")
|
||||||
}
|
}
|
||||||
|
|
||||||
var swiftSettings: [PackageDescription.SwiftSetting] {
|
var swiftSettings: [PackageDescription.SwiftSetting] {
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ let package = Package(
|
|||||||
)
|
)
|
||||||
|
|
||||||
var localization: Resource {
|
var localization: Resource {
|
||||||
.process("../../Localizable.xcstrings")
|
.process("../../Resources/Localizable.xcstrings")
|
||||||
}
|
}
|
||||||
|
|
||||||
var swiftSettings: [PackageDescription.SwiftSetting] {
|
var swiftSettings: [PackageDescription.SwiftSetting] {
|
||||||
|
|||||||
@@ -2983,73 +2983,73 @@
|
|||||||
"localizations" : {
|
"localizations" : {
|
||||||
"ca" : {
|
"ca" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive suporta claus EC256, EC384, RSA1024 i RSA2048."
|
"value" : "Secretive suporta claus EC256, EC384, RSA1024 i RSA2048."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"de" : {
|
"de" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive unterstützt EC256, EC384, RSA1024 und RSA2048 Schlüssel."
|
"value" : "Secretive unterstützt EC256, EC384, RSA1024 und RSA2048 Schlüssel."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Secretive supports EC256, EC384, RSA1024, and RSA2048 keys."
|
"value" : "Secretive supports EC256, EC384, and RSA2048 keys."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fi" : {
|
"fi" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive tukee EC256-, EC384-, RSA1024- ja RSA2048-avaimia."
|
"value" : "Secretive tukee EC256-, EC384-, RSA1024- ja RSA2048-avaimia."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"fr" : {
|
"fr" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive prend en charge les clés EC256, EC384, RSA1024 et RSA2048."
|
"value" : "Secretive prend en charge les clés EC256, EC384, RSA1024 et RSA2048."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"it" : {
|
"it" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive supporta la cifratura EC256, EC384, RSA1024 e RSA2048."
|
"value" : "Secretive supporta la cifratura EC256, EC384, RSA1024 e RSA2048."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja" : {
|
"ja" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "SecretiveはEC256、EC384、RSA1024、またはRSA2048の鍵に対応しています。"
|
"value" : "SecretiveはEC256、EC384、RSA1024、またはRSA2048の鍵に対応しています。"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ko" : {
|
"ko" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive는 EC256, EC384, RSA1024 및 RSA2048 키를 지원합니다."
|
"value" : "Secretive는 EC256, EC384, RSA1024 및 RSA2048 키를 지원합니다."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pl" : {
|
"pl" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive wspiera klucze EC256, EC384, RSA1024 i RSA2048."
|
"value" : "Secretive wspiera klucze EC256, EC384, RSA1024 i RSA2048."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"pt-BR" : {
|
"pt-BR" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive suporta chaves EC256, EC384, RSA1024 e RSA2048."
|
"value" : "Secretive suporta chaves EC256, EC384, RSA1024 e RSA2048."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ru" : {
|
"ru" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive поддерживает ключи EC256, EC384, RSA1024, и RSA2048."
|
"value" : "Secretive поддерживает ключи EC256, EC384, RSA1024, и RSA2048."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "needs_review",
|
||||||
"value" : "Secretive 支持 EC256, EC384, RSA1024, 和RSA2048."
|
"value" : "Secretive 支持 EC256, EC384, RSA1024, 和RSA2048."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3132,6 +3132,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"export SSH_AUTH_SOCK=%@" : {
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"Host *\n\tIdentityAgent %@" : {
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
"integrations_add_this_title" : {
|
"integrations_add_this_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3176,6 +3182,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"integrations_configure_using_secret_empty_create" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "You'll need to create a Secret before configuring this action."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integrations_configure_using_secret_header" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Configure Using Secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integrations_configure_using_secret_no_secret" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "No Secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integrations_configure_using_secret_secret_title" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Secret"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"integrations_getting_started_multiple_config" : {
|
"integrations_getting_started_multiple_config" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3286,6 +3336,40 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"integrations_git_step_gitallowedsigners_description" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "~/.gitallowedsigners probably does not exist. You'll need to create it."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integrations_git_step_gitconfig_description" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "[user]\n signingkey = %1$(publicKeyPathPlaceholder)@\n[commit]\n gpgsign = true\n[gpg]\n format = ssh\n[gpg \"ssh\"]\n allowedSignersFile = ~/.gitallowedsigners"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"integrations_menu_bar_title" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Integrations…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"integrations_other_section_title" : {
|
"integrations_other_section_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3330,6 +3414,17 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"integrations_ssh_specific_key_note" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "You can tell SSH to use a specific key for a given host. See the web documentation for more details."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"integrations_system_section_title" : {
|
"integrations_system_section_title" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3341,6 +3436,65 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"integrations_tool_name_bash" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "bash"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"integrations_tool_name_fish" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "fish"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"integrations_tool_name_git_signing" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Git Signing"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"integrations_tool_name_ssh" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "SSH"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"integrations_tool_name_zsh" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "zsh"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
"integrations_view_other_github_link" : {
|
"integrations_view_other_github_link" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -3363,13 +3517,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"integrationsMenuBarTitle" : {
|
"integrationsGitStepGitconfigSectionNote" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Integrations…"
|
"value" : "If any section (like [user]) already exists, just add the entries in the existing section."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4269,16 +4423,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Setup" : {
|
"set -x SSH_AUTH_SOCK %@" : {
|
||||||
"extractionState" : "stale",
|
"shouldTranslate" : false
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Setup"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"setup_agent_activity_monitor_description" : {
|
"setup_agent_activity_monitor_description" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -5200,9 +5346,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Test" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"unnamed_secret" : {
|
"unnamed_secret" : {
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -26,7 +26,8 @@ public final class PublicKeyFileStoreController: Sendable {
|
|||||||
let untracked = Set(fullPathContents)
|
let untracked = Set(fullPathContents)
|
||||||
.subtracting(validPaths)
|
.subtracting(validPaths)
|
||||||
for path in untracked {
|
for path in untracked {
|
||||||
try? FileManager.default.removeItem(at: URL(fileURLWithPath: path))
|
// string instead of fileURLWithPath since we're already using fileURL format.
|
||||||
|
try? FileManager.default.removeItem(at: URL(string: path)!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil)
|
try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: false, attributes: nil)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ extension SecureEnclave {
|
|||||||
SecItemCopyMatching(privateAttributes, &privateUntyped)
|
SecItemCopyMatching(privateAttributes, &privateUntyped)
|
||||||
guard let privateTyped = privateUntyped as? [[CFString: Any]] else { return }
|
guard let privateTyped = privateUntyped as? [[CFString: Any]] else { return }
|
||||||
let migratedPublicKeys = Set(store.secrets.map(\.publicKey))
|
let migratedPublicKeys = Set(store.secrets.map(\.publicKey))
|
||||||
var migrated = false
|
var migratedAny = false
|
||||||
for key in privateTyped {
|
for key in privateTyped {
|
||||||
let name = key[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret)
|
let name = key[kSecAttrLabel] as? String ?? String(localized: .unnamedSecret)
|
||||||
let id = key[kSecAttrApplicationLabel] as! Data
|
let id = key[kSecAttrApplicationLabel] as! Data
|
||||||
@@ -45,6 +45,7 @@ extension SecureEnclave {
|
|||||||
// Best guess.
|
// Best guess.
|
||||||
let auth: AuthenticationRequirement = String(describing: accessControl)
|
let auth: AuthenticationRequirement = String(describing: accessControl)
|
||||||
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
|
.contains("DeviceOwnerAuthentication") ? .presenceRequired : .unknown
|
||||||
|
do {
|
||||||
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
|
let parsed = try CryptoKit.SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: tokenObjectID)
|
||||||
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
|
let secret = Secret(id: UUID().uuidString, name: name, publicKey: parsed.publicKey.x963Representation, attributes: Attributes(keyType: .init(algorithm: .ecdsa, size: 256), authentication: auth))
|
||||||
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
|
guard !migratedPublicKeys.contains(parsed.publicKey.x963Representation) else {
|
||||||
@@ -56,9 +57,12 @@ extension SecureEnclave {
|
|||||||
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
|
try store.saveKey(tokenObjectID, name: name, attributes: secret.attributes)
|
||||||
logger.log("Migrated \(name).")
|
logger.log("Migrated \(name).")
|
||||||
try markMigrated(secret: secret, oldID: id)
|
try markMigrated(secret: secret, oldID: id)
|
||||||
migrated = true
|
migratedAny = true
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to migrate \(name): \(error).")
|
||||||
}
|
}
|
||||||
if migrated {
|
}
|
||||||
|
if migratedAny {
|
||||||
store.reloadSecrets()
|
store.reloadSecrets()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ extension SecureEnclave {
|
|||||||
for await note in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
|
for await note in DistributedNotificationCenter.default().notifications(named: .secretStoreUpdated) {
|
||||||
guard Constants.notificationToken != (note.object as? String) else {
|
guard Constants.notificationToken != (note.object as? String) else {
|
||||||
// Don't reload if we're the ones triggering this by reloading.
|
// Don't reload if we're the ones triggering this by reloading.
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
reloadSecrets()
|
reloadSecrets()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,85 +6,65 @@ import SmartCardSecretKit
|
|||||||
import SecretAgentKit
|
import SecretAgentKit
|
||||||
import Brief
|
import Brief
|
||||||
import Observation
|
import Observation
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
|
|
||||||
extension EnvironmentValues {
|
|
||||||
private static let _agentStatusChecker = AgentStatusChecker()
|
|
||||||
@Entry var agentStatusChecker: any AgentStatusCheckerProtocol = _agentStatusChecker
|
|
||||||
}
|
|
||||||
|
|
||||||
@main
|
@main
|
||||||
struct SecretAgent: App {
|
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||||
|
|
||||||
@SceneBuilder var body: some Scene {
|
@MainActor private let storeList: SecretStoreList = {
|
||||||
MenuBarExtra("Test", systemImage: "lock") {
|
let list = SecretStoreList()
|
||||||
AgentStatusView()
|
let cryptoKit = SecureEnclave.Store()
|
||||||
.fixedSize()
|
let migrator = SecureEnclave.CryptoKitMigrator()
|
||||||
|
try? migrator.migrate(to: cryptoKit)
|
||||||
|
list.add(store: cryptoKit)
|
||||||
|
list.add(store: SmartCard.Store())
|
||||||
|
return list
|
||||||
|
}()
|
||||||
|
private let updater = Updater(checkOnLaunch: true)
|
||||||
|
private let notifier = Notifier()
|
||||||
|
private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
|
||||||
|
private lazy var agent: Agent = {
|
||||||
|
Agent(storeList: storeList, witness: notifier)
|
||||||
|
}()
|
||||||
|
private lazy var socketController: SocketController = {
|
||||||
|
let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
||||||
|
return SocketController(path: path)
|
||||||
|
}()
|
||||||
|
private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
|
||||||
|
|
||||||
|
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||||
|
logger.debug("SecretAgent finished launching")
|
||||||
|
Task {
|
||||||
|
for await session in socketController.sessions {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
for await message in session.messages {
|
||||||
|
let agentResponse = try await agent.handle(data: message, provenance: session.provenance)
|
||||||
|
try await session.write(agentResponse)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
try session.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task {
|
||||||
|
for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
|
||||||
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
||||||
|
notifier.prompt()
|
||||||
|
_ = withObservationTracking {
|
||||||
|
updater.update
|
||||||
|
} onChange: { [updater, notifier] in
|
||||||
|
Task {
|
||||||
|
guard !updater.testBuild else { return }
|
||||||
|
await notifier.notify(update: updater.update!) { release in
|
||||||
|
await updater.ignore(release: release)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.menuBarExtraStyle(.window)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//@main
|
|
||||||
//class AppDelegate: NSObject, NSApplicationDelegate {
|
|
||||||
//
|
|
||||||
// @MainActor private let storeList: SecretStoreList = {
|
|
||||||
// let list = SecretStoreList()
|
|
||||||
// let cryptoKit = SecureEnclave.Store()
|
|
||||||
// let migrator = SecureEnclave.CryptoKitMigrator()
|
|
||||||
// try? migrator.migrate(to: cryptoKit)
|
|
||||||
// list.add(store: cryptoKit)
|
|
||||||
// list.add(store: SmartCard.Store())
|
|
||||||
// return list
|
|
||||||
// }()
|
|
||||||
// private let updater = Updater(checkOnLaunch: true)
|
|
||||||
// private let notifier = Notifier()
|
|
||||||
// private let publicKeyFileStoreController = PublicKeyFileStoreController(homeDirectory: URL.homeDirectory)
|
|
||||||
// private lazy var agent: Agent = {
|
|
||||||
// Agent(storeList: storeList, witness: notifier)
|
|
||||||
// }()
|
|
||||||
// private lazy var socketController: SocketController = {
|
|
||||||
// let path = (NSHomeDirectory() as NSString).appendingPathComponent("socket.ssh") as String
|
|
||||||
// return SocketController(path: path)
|
|
||||||
// }()
|
|
||||||
// private let logger = Logger(subsystem: "com.maxgoedjen.secretive.secretagent", category: "AppDelegate")
|
|
||||||
//
|
|
||||||
// func applicationDidFinishLaunching(_ aNotification: Notification) {
|
|
||||||
// logger.debug("SecretAgent finished launching")
|
|
||||||
// Task {
|
|
||||||
// for await session in socketController.sessions {
|
|
||||||
// Task {
|
|
||||||
// do {
|
|
||||||
// for await message in session.messages {
|
|
||||||
// let agentResponse = try await agent.handle(data: message, provenance: session.provenance)
|
|
||||||
// try await session.write(agentResponse)
|
|
||||||
// }
|
|
||||||
// } catch {
|
|
||||||
// try session.close()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// Task {
|
|
||||||
// for await _ in NotificationCenter.default.notifications(named: .secretStoreReloaded) {
|
|
||||||
// try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// try? publicKeyFileStoreController.generatePublicKeys(for: storeList.allSecrets, clear: true)
|
|
||||||
// notifier.prompt()
|
|
||||||
// _ = withObservationTracking {
|
|
||||||
// updater.update
|
|
||||||
// } onChange: { [updater, notifier] in
|
|
||||||
// Task {
|
|
||||||
// guard !updater.testBuild else { return }
|
|
||||||
// await notifier.notify(update: updater.update!) { release in
|
|
||||||
// await updater.ignore(release: release)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
|
|||||||
@@ -26,6 +26,10 @@
|
|||||||
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
50153E20250AFCB200525160 /* UpdateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E1F250AFCB200525160 /* UpdateView.swift */; };
|
||||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50153E21250DECA300525160 /* SecretListItemView.swift */; };
|
||||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
5018F54F24064786002EB505 /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5018F54E24064786002EB505 /* Notifier.swift */; };
|
||||||
|
504788EC2E680DC800B4556F /* URLs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788EB2E680DC400B4556F /* URLs.swift */; };
|
||||||
|
504788F22E681F3A00B4556F /* Instructions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F12E681F3A00B4556F /* Instructions.swift */; };
|
||||||
|
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F32E681F6900B4556F /* ToolConfigurationView.swift */; };
|
||||||
|
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504788F52E68206F00B4556F /* GettingStartedView.swift */; };
|
||||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
|
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */; };
|
||||||
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
50571E0524393D1500F76F6C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
||||||
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
|
50617D8323FCE48E0099B055 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617D8223FCE48E0099B055 /* App.swift */; };
|
||||||
@@ -33,11 +37,6 @@
|
|||||||
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
|
50617D8723FCE48E0099B055 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8623FCE48E0099B055 /* Assets.xcassets */; };
|
||||||
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
50617D8A23FCE48E0099B055 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50617D8923FCE48E0099B055 /* Preview Assets.xcassets */; };
|
||||||
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
50617DD223FCEFA90099B055 /* PreviewStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50617DD123FCEFA90099B055 /* PreviewStore.swift */; };
|
||||||
5064ADD32E669B1100B1382C /* AgentStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */; };
|
|
||||||
5064ADD42E669B2300B1382C /* AgentStatusChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */; };
|
|
||||||
5064ADD52E669B3000B1382C /* BundleIDs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50033AC227813F1700253856 /* BundleIDs.swift */; };
|
|
||||||
5064ADD62E669B5F00B1382C /* LaunchAgentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50571E0424393D1500F76F6C /* LaunchAgentController.swift */; };
|
|
||||||
5064ADD72E669B7E00B1382C /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
|
|
||||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5065E312295517C500E16645 /* ToolbarButtonStyle.swift */; };
|
||||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C12516F303004B5A36 /* SetupView.swift */; };
|
||||||
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
5066A6C82516FE6E004B5A36 /* CopyableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5066A6C72516FE6E004B5A36 /* CopyableView.swift */; };
|
||||||
@@ -111,10 +110,14 @@
|
|||||||
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
50020BAF24064869003D4025 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
|
50033AC227813F1700253856 /* BundleIDs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIDs.swift; sourceTree = "<group>"; };
|
||||||
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
|
5003EF39278005C800DF2006 /* Packages */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Packages; sourceTree = "<group>"; };
|
||||||
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Localizable.xcstrings; sourceTree = SOURCE_ROOT; };
|
5008C23D2E525D8200507AC2 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = Localizable.xcstrings; path = Packages/Resources/Localizable.xcstrings; sourceTree = SOURCE_ROOT; };
|
||||||
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
50153E1F250AFCB200525160 /* UpdateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateView.swift; sourceTree = "<group>"; };
|
||||||
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
50153E21250DECA300525160 /* SecretListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretListItemView.swift; sourceTree = "<group>"; };
|
||||||
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
5018F54E24064786002EB505 /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||||
|
504788EB2E680DC400B4556F /* URLs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLs.swift; sourceTree = "<group>"; };
|
||||||
|
504788F12E681F3A00B4556F /* Instructions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Instructions.swift; sourceTree = "<group>"; };
|
||||||
|
504788F32E681F6900B4556F /* ToolConfigurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolConfigurationView.swift; sourceTree = "<group>"; };
|
||||||
|
504788F52E68206F00B4556F /* GettingStartedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GettingStartedView.swift; sourceTree = "<group>"; };
|
||||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
|
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JustUpdatedChecker.swift; sourceTree = "<group>"; };
|
||||||
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
|
50571E0424393D1500F76F6C /* LaunchAgentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchAgentController.swift; sourceTree = "<group>"; };
|
||||||
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
50617D7F23FCE48E0099B055 /* Secretive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Secretive.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -190,6 +193,55 @@
|
|||||||
path = Helpers;
|
path = Helpers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
504788ED2E681EB200B4556F /* Styles */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
|
||||||
|
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
|
||||||
|
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
|
||||||
|
);
|
||||||
|
path = Styles;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504788EE2E681EC300B4556F /* Secrets */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
|
||||||
|
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
|
||||||
|
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
|
||||||
|
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
|
||||||
|
506772C82425BB8500034DED /* NoStoresView.swift */,
|
||||||
|
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
|
||||||
|
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
||||||
|
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
||||||
|
);
|
||||||
|
path = Secrets;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504788EF2E681ED700B4556F /* Configuration */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */,
|
||||||
|
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */,
|
||||||
|
504788F12E681F3A00B4556F /* Instructions.swift */,
|
||||||
|
504788F32E681F6900B4556F /* ToolConfigurationView.swift */,
|
||||||
|
5066A6C12516F303004B5A36 /* SetupView.swift */,
|
||||||
|
504788F52E68206F00B4556F /* GettingStartedView.swift */,
|
||||||
|
);
|
||||||
|
path = Configuration;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
504788F02E681F0100B4556F /* Views */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
||||||
|
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
||||||
|
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
||||||
|
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
||||||
|
);
|
||||||
|
path = Views;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
50617D7623FCE48D0099B055 = {
|
50617D7623FCE48D0099B055 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -252,24 +304,10 @@
|
|||||||
508A58B0241ED1C40069DC07 /* Views */ = {
|
508A58B0241ED1C40069DC07 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
50617D8423FCE48E0099B055 /* ContentView.swift */,
|
504788EF2E681ED700B4556F /* Configuration */,
|
||||||
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
|
504788EE2E681EC300B4556F /* Secrets */,
|
||||||
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
|
504788ED2E681EB200B4556F /* Styles */,
|
||||||
5079BA0E250F29BF00EA86F4 /* StoreListView.swift */,
|
504788F02E681F0100B4556F /* Views */,
|
||||||
50153E21250DECA300525160 /* SecretListItemView.swift */,
|
|
||||||
50C385A42407A76D00AF2719 /* SecretDetailView.swift */,
|
|
||||||
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */,
|
|
||||||
50B8550C24138C4F009958AC /* DeleteSecretView.swift */,
|
|
||||||
2C4A9D2E2636FFD3008CC8E2 /* EditSecretView.swift */,
|
|
||||||
50BB046A2418AAAE00D6E079 /* EmptyStoreView.swift */,
|
|
||||||
506772C82425BB8500034DED /* NoStoresView.swift */,
|
|
||||||
50153E1F250AFCB200525160 /* UpdateView.swift */,
|
|
||||||
5066A6C12516F303004B5A36 /* SetupView.swift */,
|
|
||||||
50BDCB712E63BAF20072D2E7 /* AgentStatusView.swift */,
|
|
||||||
50AE96FF2E5C1A420018C710 /* IntegrationsView.swift */,
|
|
||||||
5066A6C72516FE6E004B5A36 /* CopyableView.swift */,
|
|
||||||
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
|
|
||||||
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */,
|
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -277,6 +315,7 @@
|
|||||||
508A58B1241ED1EA0069DC07 /* Controllers */ = {
|
508A58B1241ED1EA0069DC07 /* Controllers */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
504788EB2E680DC400B4556F /* URLs.swift */,
|
||||||
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
|
508A58B2241ED2180069DC07 /* AgentStatusChecker.swift */,
|
||||||
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
|
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */,
|
||||||
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
|
50571E0224393C2600F76F6C /* JustUpdatedChecker.swift */,
|
||||||
@@ -447,12 +486,15 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
504788F22E681F3A00B4556F /* Instructions.swift in Sources */,
|
||||||
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
|
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */,
|
||||||
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
2C4A9D2F2636FFD3008CC8E2 /* EditSecretView.swift in Sources */,
|
||||||
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */,
|
||||||
|
504788EC2E680DC800B4556F /* URLs.swift in Sources */,
|
||||||
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
5066A6C22516F303004B5A36 /* SetupView.swift in Sources */,
|
||||||
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
|
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
|
||||||
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
|
||||||
|
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
|
||||||
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
|
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
|
||||||
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
|
||||||
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
|
||||||
@@ -470,6 +512,7 @@
|
|||||||
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
50BB046B2418AAAE00D6E079 /* EmptyStoreView.swift in Sources */,
|
||||||
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
|
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */,
|
||||||
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
50617D8323FCE48E0099B055 /* App.swift in Sources */,
|
||||||
|
504788F42E681F6900B4556F /* ToolConfigurationView.swift in Sources */,
|
||||||
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
506772C92425BB8500034DED /* NoStoresView.swift in Sources */,
|
||||||
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
50153E22250DECA300525160 /* SecretListItemView.swift in Sources */,
|
||||||
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
|
508A58B5241ED48F0069DC07 /* PreviewAgentStatusChecker.swift in Sources */,
|
||||||
@@ -481,13 +524,8 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
5064ADD32E669B1100B1382C /* AgentStatusView.swift in Sources */,
|
|
||||||
5064ADD72E669B7E00B1382C /* ConfigurationItemView.swift in Sources */,
|
|
||||||
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
50020BB024064869003D4025 /* AppDelegate.swift in Sources */,
|
||||||
5064ADD52E669B3000B1382C /* BundleIDs.swift in Sources */,
|
|
||||||
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
5018F54F24064786002EB505 /* Notifier.swift in Sources */,
|
||||||
5064ADD62E669B5F00B1382C /* LaunchAgentController.swift in Sources */,
|
|
||||||
5064ADD42E669B2300B1382C /* AgentStatusChecker.swift in Sources */,
|
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
12
Sources/Secretive/Controllers/URLs.swift
Normal file
12
Sources/Secretive/Controllers/URLs.swift
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension URL {
|
||||||
|
|
||||||
|
static var agentHomeURL: URL {
|
||||||
|
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
|
||||||
|
}
|
||||||
|
|
||||||
|
static var socketPath: String {
|
||||||
|
URL.agentHomeURL.appendingPathComponent("socket.ssh").path()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct GettingStartedView: View {
|
||||||
|
|
||||||
|
private let instructions = Instructions()
|
||||||
|
|
||||||
|
@Binding var selectedInstruction: ConfigurationFileInstructions?
|
||||||
|
|
||||||
|
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
|
||||||
|
_selectedInstruction = selectedInstruction
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section(.integrationsGettingStartedTitle) {
|
||||||
|
Text(.integrationsGettingStartedTitleDescription)
|
||||||
|
}
|
||||||
|
Section {
|
||||||
|
Group {
|
||||||
|
Text(.integrationsGettingStartedSuggestionSsh)
|
||||||
|
.onTapGesture {
|
||||||
|
self.selectedInstruction = instructions.ssh
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text(.integrationsGettingStartedSuggestionShell)
|
||||||
|
Text(.integrationsGettingStartedSuggestionShellDefault(shellName: String(localized: instructions.defaultShell.tool)))
|
||||||
|
.font(.caption2)
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
self.selectedInstruction = instructions.defaultShell
|
||||||
|
}
|
||||||
|
Text(.integrationsGettingStartedSuggestionGit)
|
||||||
|
.onTapGesture {
|
||||||
|
self.selectedInstruction = instructions.git
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundStyle(.link)
|
||||||
|
|
||||||
|
} header: {
|
||||||
|
Text(.integrationsGettingStartedWhatShouldIConfigureTitle)
|
||||||
|
}
|
||||||
|
footer: {
|
||||||
|
Text(.integrationsGettingStartedMultipleConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
179
Sources/Secretive/Views/Configuration/Instructions.swift
Normal file
179
Sources/Secretive/Views/Configuration/Instructions.swift
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct Instructions {
|
||||||
|
|
||||||
|
enum Constants {
|
||||||
|
static let publicKeyPathPlaceholder = "_PUBLIC_KEY_PATH_PLACEHOLDER_"
|
||||||
|
static let publicKeyPlaceholder = "_PUBLIC_KEY_PLACEHOLDER_"
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultShell: ConfigurationFileInstructions {
|
||||||
|
zsh
|
||||||
|
}
|
||||||
|
|
||||||
|
var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted)
|
||||||
|
|
||||||
|
var ssh: ConfigurationFileInstructions {
|
||||||
|
ConfigurationFileInstructions(
|
||||||
|
tool: LocalizedStringResource.integrationsToolNameSsh,
|
||||||
|
configPath: "~/.ssh/config",
|
||||||
|
configText: "Host *\n\tIdentityAgent \(URL.socketPath)",
|
||||||
|
website: URL(string: "https://man.openbsd.org/ssh_config.5")!,
|
||||||
|
note: .integrationsSshSpecificKeyNote,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var git: ConfigurationFileInstructions {
|
||||||
|
ConfigurationFileInstructions(
|
||||||
|
tool: .integrationsToolNameGitSigning,
|
||||||
|
steps: [
|
||||||
|
.init(path: "~/.gitconfig", steps: [
|
||||||
|
.integrationsGitStepGitconfigDescription(publicKeyPathPlaceholder: Constants.publicKeyPathPlaceholder)
|
||||||
|
],
|
||||||
|
note: .integrationsGitStepGitconfigSectionNote
|
||||||
|
),
|
||||||
|
.init(
|
||||||
|
path: "~/.gitallowedsigners",
|
||||||
|
steps: [
|
||||||
|
LocalizedStringResource(stringLiteral: Constants.publicKeyPlaceholder)
|
||||||
|
],
|
||||||
|
note: .integrationsGitStepGitallowedsignersDescription
|
||||||
|
),
|
||||||
|
],
|
||||||
|
website: URL(string: "https://git-scm.com/docs/git-config")!,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var zsh: ConfigurationFileInstructions {
|
||||||
|
ConfigurationFileInstructions(
|
||||||
|
tool: .integrationsToolNameZsh,
|
||||||
|
configPath: "~/.zshrc",
|
||||||
|
configText: "export SSH_AUTH_SOCK=\(URL.socketPath)"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var instructions: [ConfigurationGroup] {
|
||||||
|
[
|
||||||
|
ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [
|
||||||
|
gettingStarted
|
||||||
|
]),
|
||||||
|
ConfigurationGroup(
|
||||||
|
name: .integrationsSystemSectionTitle,
|
||||||
|
instructions: [
|
||||||
|
ssh,
|
||||||
|
git,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [
|
||||||
|
zsh,
|
||||||
|
ConfigurationFileInstructions(
|
||||||
|
tool: .integrationsToolNameBash,
|
||||||
|
configPath: "~/.bashrc",
|
||||||
|
configText: "export SSH_AUTH_SOCK=\(URL.socketPath)"
|
||||||
|
),
|
||||||
|
ConfigurationFileInstructions(
|
||||||
|
tool: .integrationsToolNameFish,
|
||||||
|
configPath: "~/.config/fish/config.fish",
|
||||||
|
configText: "set -x SSH_AUTH_SOCK \(URL.socketPath)"
|
||||||
|
),
|
||||||
|
ConfigurationFileInstructions(.integrationsOtherShellRowTitle, id: .otherShell),
|
||||||
|
]),
|
||||||
|
ConfigurationGroup(name: .integrationsOtherSectionTitle, instructions: [
|
||||||
|
ConfigurationFileInstructions(.integrationsAppsRowTitle, id: .otherApp),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfigurationGroup: Identifiable {
|
||||||
|
let id = UUID()
|
||||||
|
var name: LocalizedStringResource
|
||||||
|
var instructions: [ConfigurationFileInstructions] = []
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ConfigurationFileInstructions: Hashable, Identifiable {
|
||||||
|
|
||||||
|
struct StepGroup: Hashable, Identifiable {
|
||||||
|
let path: String
|
||||||
|
let steps: [LocalizedStringResource]
|
||||||
|
let note: LocalizedStringResource?
|
||||||
|
var id: String { path }
|
||||||
|
|
||||||
|
init(path: String, steps: [LocalizedStringResource], note: LocalizedStringResource? = nil) {
|
||||||
|
self.path = path
|
||||||
|
self.steps = steps
|
||||||
|
self.note = note
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
id.hash(into: &hasher)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var id: ID
|
||||||
|
var tool: LocalizedStringResource
|
||||||
|
var steps: [StepGroup]
|
||||||
|
var requiresSecret: Bool
|
||||||
|
var website: URL?
|
||||||
|
|
||||||
|
init(
|
||||||
|
tool: LocalizedStringResource,
|
||||||
|
configPath: String,
|
||||||
|
configText: LocalizedStringResource,
|
||||||
|
requiresSecret: Bool = false,
|
||||||
|
website: URL? = nil,
|
||||||
|
note: LocalizedStringResource? = nil
|
||||||
|
) {
|
||||||
|
self.id = .tool(String(localized: tool))
|
||||||
|
self.tool = tool
|
||||||
|
self.steps = [StepGroup(path: configPath, steps: [configText], note: note)]
|
||||||
|
self.requiresSecret = requiresSecret
|
||||||
|
self.website = website
|
||||||
|
}
|
||||||
|
|
||||||
|
init(
|
||||||
|
tool: LocalizedStringResource,
|
||||||
|
steps: [StepGroup],
|
||||||
|
requiresSecret: Bool = false,
|
||||||
|
website: URL? = nil
|
||||||
|
) {
|
||||||
|
self.id = .tool(String(localized: tool))
|
||||||
|
self.tool = tool
|
||||||
|
self.steps = steps
|
||||||
|
self.requiresSecret = true
|
||||||
|
self.website = website
|
||||||
|
}
|
||||||
|
|
||||||
|
init(_ name: LocalizedStringResource, id: ID) {
|
||||||
|
self.id = id
|
||||||
|
tool = name
|
||||||
|
steps = []
|
||||||
|
requiresSecret = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hash(into hasher: inout Hasher) {
|
||||||
|
id.hash(into: &hasher)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ID: Identifiable, Hashable {
|
||||||
|
case gettingStarted
|
||||||
|
case tool(String)
|
||||||
|
case otherShell
|
||||||
|
case otherApp
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
switch self {
|
||||||
|
case .gettingStarted:
|
||||||
|
"getting_started"
|
||||||
|
case .tool(let name):
|
||||||
|
name
|
||||||
|
case .otherShell:
|
||||||
|
"other_shell"
|
||||||
|
case .otherApp:
|
||||||
|
"other_app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
115
Sources/Secretive/Views/Configuration/IntegrationsView.swift
Normal file
115
Sources/Secretive/Views/Configuration/IntegrationsView.swift
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct IntegrationsView: View {
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@State private var selectedInstruction: ConfigurationFileInstructions?
|
||||||
|
private let instructions = Instructions()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationSplitView {
|
||||||
|
List(selection: $selectedInstruction) {
|
||||||
|
ForEach(instructions.instructions) { group in
|
||||||
|
Section(group.name) {
|
||||||
|
ForEach(group.instructions) { instruction in
|
||||||
|
Text(instruction.tool)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.tag(instruction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} detail: {
|
||||||
|
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
|
||||||
|
.fauxToolbar {
|
||||||
|
Button(.setupDoneButton) {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
.normalButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
selectedInstruction = instructions.gettingStarted
|
||||||
|
}
|
||||||
|
.frame(minHeight: 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
|
||||||
|
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
|
||||||
|
modifier(FauxToolbarModifier(toolbarContent: content()))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FauxToolbarModifier<ToolbarContent: View>: ViewModifier {
|
||||||
|
|
||||||
|
var toolbarContent: ToolbarContent
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
content
|
||||||
|
Divider()
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
toolbarContent
|
||||||
|
.padding(.top, 8)
|
||||||
|
.padding(.trailing, 16)
|
||||||
|
.padding(.bottom, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct IntegrationsDetailView: View {
|
||||||
|
|
||||||
|
@Binding private var selectedInstruction: ConfigurationFileInstructions?
|
||||||
|
|
||||||
|
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
|
||||||
|
_selectedInstruction = selectedInstruction
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
if let selectedInstruction {
|
||||||
|
switch selectedInstruction.id {
|
||||||
|
case .gettingStarted:
|
||||||
|
GettingStartedView(selectedInstruction: $selectedInstruction)
|
||||||
|
case .tool:
|
||||||
|
ToolConfigurationView(selectedInstruction: selectedInstruction)
|
||||||
|
case .otherShell:
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!)
|
||||||
|
} header: {
|
||||||
|
Text(.integrationsCommunityShellListDescription)
|
||||||
|
.font(.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
|
||||||
|
case .otherApp:
|
||||||
|
Form {
|
||||||
|
Section {
|
||||||
|
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!)
|
||||||
|
} header: {
|
||||||
|
Text(.integrationsCommunityAppsListDescription)
|
||||||
|
.font(.body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
IntegrationsView()
|
||||||
|
.frame(height: 500)
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ struct SetupView: View {
|
|||||||
buttonWidth = width
|
buttonWidth = width
|
||||||
}
|
}
|
||||||
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
.background(.white.opacity(0.1), in: RoundedRectangle(cornerRadius: 10))
|
||||||
.frame(minWidth: 700, maxWidth: .infinity)
|
.frame(minWidth: 600, maxWidth: .infinity)
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
Button(.setupDoneButton) {
|
Button(.setupDoneButton) {
|
||||||
@@ -154,17 +154,19 @@ struct StepView<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 20) {
|
HStack(spacing: 0) {
|
||||||
icon
|
icon
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: 24)
|
.frame(width: 24)
|
||||||
VStack(alignment: .leading, spacing: 6) {
|
Spacer()
|
||||||
|
.frame(width: 20)
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.bold()
|
.bold()
|
||||||
Text(description)
|
Text(description)
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer(minLength: 20)
|
||||||
actions
|
actions
|
||||||
}
|
}
|
||||||
.padding(20)
|
.padding(20)
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import SecretKit
|
||||||
|
|
||||||
|
struct ToolConfigurationView: View {
|
||||||
|
|
||||||
|
private let instructions = Instructions()
|
||||||
|
let selectedInstruction: ConfigurationFileInstructions
|
||||||
|
|
||||||
|
@Environment(\.secretStoreList) private var secretStoreList
|
||||||
|
|
||||||
|
@State var creating = false
|
||||||
|
@State var selectedSecret: AnySecret?
|
||||||
|
|
||||||
|
init(selectedInstruction: ConfigurationFileInstructions) {
|
||||||
|
self.selectedInstruction = selectedInstruction
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
if selectedInstruction.requiresSecret {
|
||||||
|
if secretStoreList.allSecrets.isEmpty {
|
||||||
|
Section {
|
||||||
|
Text(.integrationsConfigureUsingSecretEmptyCreate)
|
||||||
|
if let store = secretStoreList.modifiableStore {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Button(.createSecretTitle) {
|
||||||
|
creating = true
|
||||||
|
}
|
||||||
|
.sheet(isPresented: $creating) {
|
||||||
|
CreateSecretView(store: store) { created in
|
||||||
|
selectedSecret = created
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Section {
|
||||||
|
Picker(.integrationsConfigureUsingSecretSecretTitle, selection: $selectedSecret) {
|
||||||
|
if selectedSecret == nil {
|
||||||
|
Text(.integrationsConfigureUsingSecretNoSecret)
|
||||||
|
.tag(nil as (AnySecret?))
|
||||||
|
}
|
||||||
|
ForEach(secretStoreList.allSecrets) { secret in
|
||||||
|
Text(secret.name)
|
||||||
|
.tag(secret)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text(.integrationsConfigureUsingSecretHeader)
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
selectedSecret = secretStoreList.allSecrets.first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ForEach(selectedInstruction.steps) { stepGroup in
|
||||||
|
Section {
|
||||||
|
ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
|
||||||
|
ForEach(stepGroup.steps, id: \.self.key) { step in
|
||||||
|
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(String(localized: step))) {
|
||||||
|
HStack {
|
||||||
|
Text(placeholdersReplaced(text: String(localized: step)))
|
||||||
|
.padding(8)
|
||||||
|
.font(.system(.subheadline, design: .monospaced))
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.fill(.black.opacity(0.05))
|
||||||
|
.stroke(.separator, lineWidth: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} footer: {
|
||||||
|
if let note = stepGroup.note {
|
||||||
|
Text(note)
|
||||||
|
.font(.caption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let url = selectedInstruction.website {
|
||||||
|
Section {
|
||||||
|
Link(destination: url) {
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text(.integrationsWebLink)
|
||||||
|
.font(.headline)
|
||||||
|
Text(url.absoluteString)
|
||||||
|
.font(.caption2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func placeholdersReplaced(text: String) -> String {
|
||||||
|
guard let selectedSecret else { return text }
|
||||||
|
let writer = OpenSSHPublicKeyWriter()
|
||||||
|
let fileController = PublicKeyFileStoreController(homeDirectory: URL.agentHomeURL)
|
||||||
|
return text
|
||||||
|
.replacingOccurrences(of: Instructions.Constants.publicKeyPlaceholder, with: writer.openSSHString(secret: selectedSecret))
|
||||||
|
.replacingOccurrences(of: Instructions.Constants.publicKeyPathPlaceholder, with: fileController.publicKeyPath(for: selectedSecret))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct IntegrationsView: View {
|
|
||||||
|
|
||||||
@Environment(\.dismiss) private var dismiss
|
|
||||||
|
|
||||||
@State private var selectedInstruction: ConfigurationFileInstructions?
|
|
||||||
private let instructions = Instructions()
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
NavigationSplitView {
|
|
||||||
List(selection: $selectedInstruction) {
|
|
||||||
ForEach(instructions.instructions) { group in
|
|
||||||
Section(group.name) {
|
|
||||||
ForEach(group.instructions) { instruction in
|
|
||||||
Text(instruction.tool)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.tag(instruction)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} detail: {
|
|
||||||
IntegrationsDetailView(selectedInstruction: $selectedInstruction)
|
|
||||||
.fauxToolbar {
|
|
||||||
Button(.setupDoneButton) {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
.normalButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
selectedInstruction = instructions.gettingStarted
|
|
||||||
}
|
|
||||||
.frame(minHeight: 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension View {
|
|
||||||
|
|
||||||
func fauxToolbar<Content: View>(content: () -> Content) -> some View {
|
|
||||||
modifier(FauxToolbarModifier(toolbarContent: content()))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FauxToolbarModifier<ToolbarContent: View>: ViewModifier {
|
|
||||||
|
|
||||||
var toolbarContent: ToolbarContent
|
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
|
||||||
VStack(alignment: .leading) {
|
|
||||||
content
|
|
||||||
Divider()
|
|
||||||
HStack {
|
|
||||||
Spacer()
|
|
||||||
toolbarContent
|
|
||||||
.padding(.top, 8)
|
|
||||||
.padding(.trailing, 16)
|
|
||||||
.padding(.bottom, 16)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct IntegrationsDetailView: View {
|
|
||||||
|
|
||||||
@Binding private var selectedInstruction: ConfigurationFileInstructions?
|
|
||||||
private let instructions = Instructions()
|
|
||||||
|
|
||||||
init(selectedInstruction: Binding<ConfigurationFileInstructions?>) {
|
|
||||||
_selectedInstruction = selectedInstruction
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
if let selectedInstruction {
|
|
||||||
switch selectedInstruction.id {
|
|
||||||
case .gettingStarted:
|
|
||||||
Form {
|
|
||||||
Section(.integrationsGettingStartedTitle) {
|
|
||||||
Text(.integrationsGettingStartedTitleDescription)
|
|
||||||
}
|
|
||||||
Section {
|
|
||||||
Group {
|
|
||||||
Text(.integrationsGettingStartedSuggestionSsh)
|
|
||||||
.onTapGesture {
|
|
||||||
self.selectedInstruction = instructions.ssh
|
|
||||||
}
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
|
||||||
Text(.integrationsGettingStartedSuggestionShell)
|
|
||||||
Text(.integrationsGettingStartedSuggestionShellDefault(shellName: instructions.defaultShell.tool))
|
|
||||||
.font(.caption2)
|
|
||||||
}
|
|
||||||
.onTapGesture {
|
|
||||||
self.selectedInstruction = instructions.defaultShell
|
|
||||||
}
|
|
||||||
Text(.integrationsGettingStartedSuggestionGit)
|
|
||||||
.onTapGesture {
|
|
||||||
self.selectedInstruction = instructions.git
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.foregroundStyle(.link)
|
|
||||||
|
|
||||||
} header: {
|
|
||||||
Text(.integrationsGettingStartedWhatShouldIConfigureTitle)
|
|
||||||
}
|
|
||||||
footer: {
|
|
||||||
Text(.integrationsGettingStartedMultipleConfig)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
case .tool:
|
|
||||||
Form {
|
|
||||||
ForEach(selectedInstruction.steps) { stepGroup in
|
|
||||||
Section {
|
|
||||||
ConfigurationItemView(title: .integrationsPathTitle, value: stepGroup.path, action: .revealInFinder(stepGroup.path))
|
|
||||||
ForEach(stepGroup.steps, id: \.self) { step in
|
|
||||||
ConfigurationItemView(title: .integrationsAddThisTitle, action: .copy(step)) {
|
|
||||||
HStack {
|
|
||||||
Text(step)
|
|
||||||
.padding(8)
|
|
||||||
.font(.system(.subheadline, design: .monospaced))
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background {
|
|
||||||
RoundedRectangle(cornerRadius: 6)
|
|
||||||
.fill(.black.opacity(0.05))
|
|
||||||
.stroke(.separator, lineWidth: 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} footer: {
|
|
||||||
if let note = stepGroup.note {
|
|
||||||
Text(note)
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let url = selectedInstruction.website {
|
|
||||||
Section {
|
|
||||||
Link(destination: url) {
|
|
||||||
VStack(alignment: .leading, spacing: 5) {
|
|
||||||
Text(.integrationsWebLink)
|
|
||||||
.font(.headline)
|
|
||||||
Text(url.absoluteString)
|
|
||||||
.font(.caption2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
case .otherShell:
|
|
||||||
Form {
|
|
||||||
Section {
|
|
||||||
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/shells")!)
|
|
||||||
} header: {
|
|
||||||
Text(.integrationsCommunityShellListDescription)
|
|
||||||
.font(.body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
|
|
||||||
case .otherApp:
|
|
||||||
Form {
|
|
||||||
Section {
|
|
||||||
Link(.integrationsViewOtherGithubLink, destination: URL(string: "https://github.com/maxgoedjen/secretive-config-instructions/tree/main/apps")!)
|
|
||||||
} header: {
|
|
||||||
Text(.integrationsCommunityAppsListDescription)
|
|
||||||
.font(.body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.formStyle(.grouped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct Instructions {
|
|
||||||
|
|
||||||
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
|
|
||||||
|
|
||||||
|
|
||||||
var defaultShell: ConfigurationFileInstructions {
|
|
||||||
zsh
|
|
||||||
}
|
|
||||||
|
|
||||||
var gettingStarted: ConfigurationFileInstructions = ConfigurationFileInstructions(.integrationsGettingStartedRowTitle, id: .gettingStarted)
|
|
||||||
|
|
||||||
var ssh: ConfigurationFileInstructions {
|
|
||||||
ConfigurationFileInstructions(
|
|
||||||
tool: "SSH",
|
|
||||||
configPath: "~/.ssh/config",
|
|
||||||
configText: "Host *\n\tIdentityAgent \(socketPath)",
|
|
||||||
website: URL(string: "https://man.openbsd.org/ssh_config.5")!,
|
|
||||||
note: "You can tell SSH to use a specific key for a given host. See the web documentation for more details.",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var git: ConfigurationFileInstructions {
|
|
||||||
ConfigurationFileInstructions(
|
|
||||||
tool: "Git Signing",
|
|
||||||
steps: [
|
|
||||||
.init(path: "~/.gitconfig", steps: [
|
|
||||||
"""
|
|
||||||
[user]
|
|
||||||
signingkey = YOUR_PUBLIC_KEY_PATH
|
|
||||||
[commit]
|
|
||||||
gpgsign = true
|
|
||||||
[gpg]
|
|
||||||
format = ssh
|
|
||||||
[gpg "ssh"]
|
|
||||||
allowedSignersFile = ~/.gitallowedsigners
|
|
||||||
"""
|
|
||||||
],
|
|
||||||
note: "If any section (like [user]) already exists, just add the entries in the existing section."
|
|
||||||
|
|
||||||
),
|
|
||||||
.init(
|
|
||||||
path: "~/.gitallowedsigners",
|
|
||||||
steps: [
|
|
||||||
"YOUR_PUBLIC_KEY"
|
|
||||||
],
|
|
||||||
note: "~/.gitallowedsigners probably does not exist. You'll need to create it."
|
|
||||||
),
|
|
||||||
],
|
|
||||||
website: URL(string: "https://git-scm.com/docs/git-config")!,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var zsh: ConfigurationFileInstructions {
|
|
||||||
ConfigurationFileInstructions(
|
|
||||||
tool: "zsh",
|
|
||||||
configPath: "~/.zshrc",
|
|
||||||
configText: "export SSH_AUTH_SOCK=\(socketPath)"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var instructions: [ConfigurationGroup] {
|
|
||||||
[
|
|
||||||
ConfigurationGroup(name: .integrationsGettingStartedSectionTitle, instructions: [
|
|
||||||
gettingStarted
|
|
||||||
]),
|
|
||||||
ConfigurationGroup(
|
|
||||||
name: .integrationsSystemSectionTitle,
|
|
||||||
instructions: [
|
|
||||||
ssh,
|
|
||||||
git,
|
|
||||||
]
|
|
||||||
),
|
|
||||||
ConfigurationGroup(name: .integrationsShellSectionTitle, instructions: [
|
|
||||||
zsh,
|
|
||||||
ConfigurationFileInstructions(
|
|
||||||
tool: "bash",
|
|
||||||
configPath: "~/.bashrc",
|
|
||||||
configText: "export SSH_AUTH_SOCK=\(socketPath)"
|
|
||||||
),
|
|
||||||
ConfigurationFileInstructions(
|
|
||||||
tool: "fish",
|
|
||||||
configPath: "~/.config/fish/config.fish",
|
|
||||||
configText: "set -x SSH_AUTH_SOCK \(socketPath)"
|
|
||||||
),
|
|
||||||
ConfigurationFileInstructions(.integrationsOtherShellRowTitle, id: .otherShell),
|
|
||||||
]),
|
|
||||||
ConfigurationGroup(name: .integrationsOtherSectionTitle, instructions: [
|
|
||||||
ConfigurationFileInstructions(.integrationsAppsRowTitle, id: .otherApp),
|
|
||||||
]),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ConfigurationGroup: Identifiable {
|
|
||||||
let id = UUID()
|
|
||||||
var name: LocalizedStringResource
|
|
||||||
var instructions: [ConfigurationFileInstructions] = []
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ConfigurationFileInstructions: Hashable, Identifiable {
|
|
||||||
|
|
||||||
struct StepGroup: Hashable, Identifiable {
|
|
||||||
let path: String
|
|
||||||
let steps: [String]
|
|
||||||
let note: String?
|
|
||||||
var id: String { path }
|
|
||||||
|
|
||||||
init(path: String, steps: [String], note: String? = nil) {
|
|
||||||
self.path = path
|
|
||||||
self.steps = steps
|
|
||||||
self.note = note
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var id: ID
|
|
||||||
var tool: String
|
|
||||||
var steps: [StepGroup]
|
|
||||||
var website: URL?
|
|
||||||
|
|
||||||
init(tool: String, configPath: String, configText: String, website: URL? = nil, note: String? = nil) {
|
|
||||||
self.id = .tool(tool)
|
|
||||||
self.tool = tool
|
|
||||||
self.steps = [StepGroup(path: configPath, steps: [configText], note: note)]
|
|
||||||
self.website = website
|
|
||||||
}
|
|
||||||
|
|
||||||
init(tool: String, steps: [StepGroup], website: URL? = nil) {
|
|
||||||
self.id = .tool(tool)
|
|
||||||
self.tool = tool
|
|
||||||
self.steps = steps
|
|
||||||
self.website = website
|
|
||||||
}
|
|
||||||
|
|
||||||
init(_ name: LocalizedStringResource, id: ID) {
|
|
||||||
self.id = id
|
|
||||||
tool = String(localized: name)
|
|
||||||
self.steps = []
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ID: Identifiable, Hashable {
|
|
||||||
case gettingStarted
|
|
||||||
case tool(String)
|
|
||||||
case otherShell
|
|
||||||
case otherApp
|
|
||||||
|
|
||||||
var id: String {
|
|
||||||
switch self {
|
|
||||||
case .gettingStarted:
|
|
||||||
"getting_started"
|
|
||||||
case .tool(let name):
|
|
||||||
name
|
|
||||||
case .otherShell:
|
|
||||||
"other_shell"
|
|
||||||
case .otherApp:
|
|
||||||
"other_app"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
IntegrationsView()
|
|
||||||
.frame(height: 500)
|
|
||||||
}
|
|
||||||
@@ -146,6 +146,6 @@ struct CreateSecretView<StoreType: SecretStoreModifiable>: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
//#Preview {
|
||||||
CreateSecretView(store: Preview.StoreModifiable()) { _ in }
|
// CreateSecretView(store: Preview.StoreModifiable()) { _ in }
|
||||||
}
|
//}
|
||||||
@@ -57,15 +57,10 @@ struct EmptyStoreModifiableView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
|
|
||||||
struct EmptyStoreModifiableView_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
EmptyStoreImmutableView()
|
EmptyStoreImmutableView()
|
||||||
EmptyStoreModifiableView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
#Preview {
|
||||||
#endif
|
EmptyStoreModifiableView()
|
||||||
|
}
|
||||||
@@ -13,12 +13,7 @@ struct NoStoresView: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#Preview {
|
||||||
|
|
||||||
struct NoStoresView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
NoStoresView()
|
NoStoresView()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
|
||||||
@@ -37,14 +37,6 @@ struct SecretDetailView<SecretType: Secret>: View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension URL {
|
//#Preview {
|
||||||
|
// SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
|
||||||
static var agentHomeURL: URL {
|
//}
|
||||||
URL(fileURLWithPath: URL.homeDirectory.path().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
SecretDetailView(secret: Preview.Secret(name: "Demonstration Secret"))
|
|
||||||
}
|
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
import Brief
|
|
||||||
|
|
||||||
struct UpdateDetailView: View {
|
|
||||||
|
|
||||||
@Environment(\.updater) var updater: any UpdaterProtocol
|
|
||||||
|
|
||||||
let update: Release
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack {
|
|
||||||
Text(.updateVersionName(updateName: update.name)).font(.title)
|
|
||||||
GroupBox(label: Text(.updateReleaseNotesTitle)) {
|
|
||||||
ScrollView {
|
|
||||||
Text(attributedBody)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HStack {
|
|
||||||
if !update.critical {
|
|
||||||
Button(.updateIgnoreButton) {
|
|
||||||
Task {
|
|
||||||
await updater.ignore(release: update)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
Button(.updateUpdateButton) {
|
|
||||||
NSWorkspace.shared.open(update.html_url)
|
|
||||||
}
|
|
||||||
.keyboardShortcut(.defaultAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.frame(maxWidth: 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
var attributedBody: AttributedString {
|
|
||||||
do {
|
|
||||||
var text = try AttributedString(
|
|
||||||
markdown: update.body,
|
|
||||||
options: .init(
|
|
||||||
allowsExtendedAttributes: true,
|
|
||||||
interpretedSyntax: .full,
|
|
||||||
),
|
|
||||||
baseURL: URL(string: "https://github.com/maxgoedjen/secretive")!
|
|
||||||
)
|
|
||||||
.transformingAttributes(AttributeScopes.FoundationAttributes.PresentationIntentAttribute.self) { key in
|
|
||||||
let font: Font? = switch key.value?.components.first?.kind {
|
|
||||||
case .header(level: 1):
|
|
||||||
Font.title
|
|
||||||
case .header(level: 2):
|
|
||||||
Font.title2
|
|
||||||
case .header(level: 3):
|
|
||||||
Font.title3
|
|
||||||
default:
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
if let font {
|
|
||||||
key.replace(with: AttributeScopes.SwiftUIAttributes.FontAttribute.self, value: font)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let lineBreak = AttributedString("\n\n")
|
|
||||||
for run in text.runs.reversed() {
|
|
||||||
text.insert(lineBreak, at: run.range.lowerBound)
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
} catch {
|
|
||||||
var text = AttributedString()
|
|
||||||
for line in update.body.split(whereSeparator: \.isNewline) {
|
|
||||||
let attributed: AttributedString
|
|
||||||
let split = line.split(separator: " ")
|
|
||||||
let unprefixed = split.dropFirst().joined(separator: " ")
|
|
||||||
if let prefix = split.first {
|
|
||||||
var container = AttributeContainer()
|
|
||||||
switch prefix {
|
|
||||||
case "#":
|
|
||||||
container.font = .title
|
|
||||||
case "##":
|
|
||||||
container.font = .title2
|
|
||||||
case "###":
|
|
||||||
container.font = .title3
|
|
||||||
default:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
attributed = AttributedString(unprefixed, attributes: container)
|
|
||||||
} else {
|
|
||||||
attributed = AttributedString(line + "\n\n")
|
|
||||||
}
|
|
||||||
text = text + attributed
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -15,7 +15,6 @@ struct AgentStatusView: View {
|
|||||||
struct AgentRunningView: View {
|
struct AgentRunningView: View {
|
||||||
|
|
||||||
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
|
||||||
private let socketPath = (NSHomeDirectory().replacingOccurrences(of: Bundle.hostBundleID, with: Bundle.agentBundleID) as NSString).appendingPathComponent("socket.ssh") as String
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Form {
|
Form {
|
||||||
@@ -28,8 +27,8 @@ struct AgentRunningView: View {
|
|||||||
)
|
)
|
||||||
ConfigurationItemView(
|
ConfigurationItemView(
|
||||||
title: .agentDetailsSocketPathTitle,
|
title: .agentDetailsSocketPathTitle,
|
||||||
value: socketPath,
|
value: URL.socketPath,
|
||||||
action: .copy(socketPath),
|
action: .copy(URL.socketPath),
|
||||||
)
|
)
|
||||||
ConfigurationItemView(
|
ConfigurationItemView(
|
||||||
title: .agentDetailsVersionTitle,
|
title: .agentDetailsVersionTitle,
|
||||||
@@ -127,7 +126,7 @@ struct AgentNotRunningView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// .primaryButton()
|
.primaryButton()
|
||||||
} else {
|
} else {
|
||||||
Text(.agentDetailsCouldNotStartError)
|
Text(.agentDetailsCouldNotStartError)
|
||||||
.bold()
|
.bold()
|
||||||
@@ -198,25 +198,17 @@ extension ContentView {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
|
|
||||||
struct ContentView_Previews: PreviewProvider {
|
|
||||||
|
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
// Empty on modifiable and nonmodifiable
|
|
||||||
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
|
|
||||||
.environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
|
|
||||||
.environment(PreviewUpdater())
|
|
||||||
|
|
||||||
// 5 items on modifiable and nonmodifiable
|
|
||||||
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
|
|
||||||
.environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
|
|
||||||
.environment(PreviewUpdater())
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
//#Preview {
|
||||||
|
// // Empty on modifiable and nonmodifiable
|
||||||
|
// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
|
||||||
|
// .environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
|
||||||
|
// .environment(PreviewUpdater())
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//#Preview {
|
||||||
|
// // 5 items on modifiable and nonmodifiable
|
||||||
|
// ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
|
||||||
|
// .environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
|
||||||
|
// .environment(PreviewUpdater())
|
||||||
|
//}
|
||||||
@@ -163,17 +163,12 @@ fileprivate struct BackgroundViewModifier: ViewModifier {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
#Preview {
|
||||||
|
|
||||||
struct CopyableView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
Group {
|
|
||||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
|
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
|
||||||
.padding()
|
.padding()
|
||||||
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
|
|
||||||
.padding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#Preview {
|
||||||
|
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
63
Sources/Secretive/Views/Views/UpdateView.swift
Normal file
63
Sources/Secretive/Views/Views/UpdateView.swift
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import SwiftUI
|
||||||
|
import Brief
|
||||||
|
|
||||||
|
struct UpdateDetailView: View {
|
||||||
|
|
||||||
|
@Environment(\.updater) var updater: any UpdaterProtocol
|
||||||
|
|
||||||
|
let update: Release
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack {
|
||||||
|
Text(.updateVersionName(updateName: update.name)).font(.title)
|
||||||
|
GroupBox(label: Text(.updateReleaseNotesTitle)) {
|
||||||
|
ScrollView {
|
||||||
|
attributedBody
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
if !update.critical {
|
||||||
|
Button(.updateIgnoreButton) {
|
||||||
|
Task {
|
||||||
|
await updater.ignore(release: update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Button(.updateUpdateButton) {
|
||||||
|
NSWorkspace.shared.open(update.html_url)
|
||||||
|
}
|
||||||
|
.keyboardShortcut(.defaultAction)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
.frame(maxWidth: 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
var attributedBody: Text {
|
||||||
|
var text = Text(verbatim: "")
|
||||||
|
for line in update.body.split(whereSeparator: \.isNewline) {
|
||||||
|
let attributed: Text
|
||||||
|
let split = line.split(separator: " ")
|
||||||
|
let unprefixed = split.dropFirst().joined(separator: " ")
|
||||||
|
if let prefix = split.first {
|
||||||
|
switch prefix {
|
||||||
|
case "#":
|
||||||
|
attributed = Text(unprefixed).font(.title) + Text(verbatim: "\n")
|
||||||
|
case "##":
|
||||||
|
attributed = Text(unprefixed).font(.title2) + Text(verbatim: "\n")
|
||||||
|
case "###":
|
||||||
|
attributed = Text(unprefixed).font(.title3) + Text(verbatim: "\n")
|
||||||
|
default:
|
||||||
|
attributed = Text(line) + Text(verbatim: "\n\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attributed = Text(line) + Text(verbatim: "\n\n")
|
||||||
|
}
|
||||||
|
text = text + attributed
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user