Compare commits

..

6 Commits

Author SHA1 Message Date
Spidy123222
d0424fe0a5 Merge branch 'develop' into Sidekit-jit-implementation 2024-12-11 02:54:40 -08:00
Spidy123222
a29cdf0323 Merge branch 'develop' into Sidekit-jit-implementation
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-05-17 02:34:48 -07:00
naturecodevoid
68db11d8bf Add Attach error
Signed-off-by: naturecodevoid <44983869+naturecodevoid@users.noreply.github.com>
2023-03-04 08:55:04 -08:00
naturecodevoid
7cf4101130 Merge branch 'develop' into Sidekit-jit-implementation 2023-03-04 08:19:54 -08:00
Spidy123222
13a7991481 Merge branch 'develop' into Sidekit-jit-implementation 2023-03-03 22:17:30 -08:00
Spidy123222
efcf557e44 make url scheme with bid and pid endings
Signed-off-by: Spidy123222 <64176728+Spidy123222@users.noreply.github.com>
2023-02-28 15:19:22 -08:00
19 changed files with 96 additions and 171 deletions

View File

@@ -27,9 +27,6 @@ jobs:
- name: Install dependencies
run: brew install ldid
- name: Install xcbeautify
run: brew install xcbeautify
- name: Cache .nightly-build-num
uses: actions/cache@v4
with:
@@ -56,12 +53,9 @@ jobs:
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
- name: Build SideStore
run: NSUnbufferedIO=YES make build 2>&1 | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
run: make build | xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: make fakesign

View File

@@ -46,7 +46,7 @@ jobs:
restore-keys: xcode-cache-deriveddata-
- name: Build SideStore
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
run: make build | xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: make fakesign

View File

@@ -3,7 +3,6 @@ on:
push:
tags:
- '[0-9]+.[0-9]+.[0-9]+' # example: 1.0.0
workflow_dispatch:
jobs:
build:
@@ -45,12 +44,9 @@ jobs:
with:
key: xcode-cache-deriveddata-${{ github.sha }}
restore-keys: xcode-cache-deriveddata-
swiftpm-cache-key: xcode-cache-sourcedata-${{ github.sha }}
swiftpm-cache-restore-keys: |
xcode-cache-sourcedata-
- name: Build SideStore
run: NSUnbufferedIO=YES make build | xcbeautify --renderer github-actions && exit ${PIPESTATUS[0]}
run: make build | xcpretty && exit ${PIPESTATUS[0]}
- name: Fakesign app
run: make fakesign

View File

@@ -590,8 +590,6 @@
B3C3960F284F53E900DA9E2F /* AltBackup.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AltBackup.xcconfig; sourceTree = "<group>"; };
B3EE16B52925E27D00B3B1F5 /* AnisetteManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnisetteManager.swift; sourceTree = "<group>"; };
BD4513AA2C6FA98C0052BCC0 /* AppExtensionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppExtensionView.swift; sourceTree = "<group>"; };
BD7FFE492D1D2B7F00A9623D /* ptrace.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ptrace.m; sourceTree = "<group>"; };
BD7FFE4A2D1D2B8F00A9623D /* ptrace.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ptrace.h; sourceTree = "<group>"; };
BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAttemptsViewController.swift; sourceTree = "<group>"; };
BF08858222DE795100DE9F1E /* MyAppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewController.swift; sourceTree = "<group>"; };
BF08858422DE7EC800DE9F1E /* UpdateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateCollectionViewCell.swift; sourceTree = "<group>"; };
@@ -1309,7 +1307,6 @@
BF66EE7F2501AE50007EE018 /* AltStoreCore */ = {
isa = PBXGroup;
children = (
BD7FFE492D1D2B7F00A9623D /* ptrace.m */,
BF66EE802501AE50007EE018 /* AltStoreCore.h */,
BF66EE8A2501AEB1007EE018 /* Components */,
BF66EEE32501AED0007EE018 /* Extensions */,
@@ -1320,7 +1317,6 @@
BF66EE8D2501AEBC007EE018 /* Types */,
BF66EE812501AE50007EE018 /* Info.plist */,
BFCB9205250AB1FF0057B44E /* Resources */,
BD7FFE4A2D1D2B8F00A9623D /* ptrace.h */,
);
path = AltStoreCore;
sourceTree = "<group>";

View File

@@ -7,7 +7,7 @@
"location" : "https://github.com/SideStore/AltSign",
"state" : {
"branch" : "master",
"revision" : "4323ff794e600ce1759cb6ea57275e13b7ea72f2"
"revision" : "cc6189f0f7cd8e5bd24943af9322e0ff9420e9f4"
}
},
{

View File

@@ -233,13 +233,6 @@ private extension AppDelegate
return true
case "jit":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
guard let pidstr = queryItems["pid"], let pid = Int32(pidstr) else { return false }
return ptrace(14, pid, nil, 0) == 0
default: return false
}
}

View File

@@ -1141,7 +1141,7 @@ private extension AppManager
}
}
private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any] = [.increasedDebuggingMemoryLimit: ALTEntitlement.increasedDebuggingMemoryLimit, .increasedMemoryLimit: ALTEntitlement.increasedMemoryLimit, .extendedVirtualAddressing: ALTEntitlement.extendedVirtualAddressing], cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
private func _install(_ app: AppProtocol, operation appOperation: AppOperation, group: RefreshGroup, context: InstallAppOperationContext? = nil, additionalEntitlements: [ALTEntitlement: Any]? = nil, cacheApp: Bool = true, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
{
let progress = Progress.discreteProgress(totalUnitCount: 100)

View File

@@ -299,19 +299,6 @@ extension FetchProvisioningProfilesOperation
}
}
}
catch ALTAppleAPIError.bundleIdentifierUnavailable {
ALTAppleAPI.shared.fetchAppIDs(for: team, session: session) {res, err in
if let err = err {
return completionHandler(.failure(err))
}
guard let res = res else {return completionHandler(.failure(ALTError(.unknown)))}
for appid in res {
if appid.bundleIdentifier == bundleIdentifier {
completionHandler(.success(appid))
}
}
}
}
catch
{
completionHandler(.failure(error))
@@ -376,9 +363,7 @@ extension FetchProvisioningProfilesOperation
}
}
appID.entitlements = entitlements
if updateFeatures || true
if updateFeatures
{
let appID = appID.copy() as! ALTAppID
appID.features = features

View File

@@ -238,7 +238,15 @@ struct OperationError: ALTLocalizedError {
}
}
}
return OperationError.profileInstall
case 19:
return OperationError.profileInstall
case 20:
return OperationError.noConnection
case 21:
return OperationError.attach
default:
return OperationError.unknown
extension MinimuxerError: LocalizedError {
public var failureReason: String? {
switch self {

View File

@@ -9,6 +9,8 @@
import UIKit
import AltStoreCore
import EmotionalDamage
import minimuxer
@available(iOS 13, *)
final class SceneDelegate: UIResponder, UIWindowSceneDelegate
@@ -140,6 +142,45 @@ private extension SceneDelegate
NotificationCenter.default.post(name: AppDelegate.addSourceDeepLinkNotification, object: nil, userInfo: [AppDelegate.addSourceDeepLinkURLKey: sourceURL])
}
case "sidejit-enable":
let queryItems = components.queryItems?.reduce(into: [String: String]()) { $0[$1.name.lowercased()] = $1.value } ?? [:]
if let jitdebugURLString = queryItems["bid"] {
DispatchQueue.main.async {
let v = minimuxer_to_operation(code: 1)
do {
var x = try debug_app(app_id: jitdebugURLString)
switch x {
case .Good: print(jitdebugURLString)
case .Bad(let code): minimuxer_to_operation(code: code)
}
} catch Uhoh.Bad(let code) {
minimuxer_to_operation(code: code)
} catch {
print(OperationError.unknown)
}
} }
else if let jitdebugURLString = queryItems["pid"] {
DispatchQueue.main.async {
let v = minimuxer_to_operation(code: 1)
do {
var x = try debug_app(app_id: jitdebugURLString)
switch x {
case .Good: print(jitdebugURLString)
case .Bad(let code): minimuxer_to_operation(code: code)
}
} catch Uhoh.Bad(let code) {
minimuxer_to_operation(code: code)
} catch {
print(OperationError.unknown)
}
} }
else { return }
default: break
}
}

View File

@@ -272,34 +272,6 @@
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="546" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="amC-sE-8O0" id="GEO-2e-E4k">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Allow Siri To Refresh Apps…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr">
<rect key="frame" x="30" y="15.5" width="228.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="c6K-fI-CVr" firstAttribute="centerY" secondItem="GEO-2e-E4k" secondAttribute="centerY" id="IGB-ox-RAM"/>
<constraint firstItem="c6K-fI-CVr" firstAttribute="leading" secondItem="GEO-2e-E4k" secondAttribute="leadingMargin" id="xoI-eB-1TH"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="7PQ-AW-GcV" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="495" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
@@ -329,6 +301,34 @@
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="2"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="boolean" keyPath="isSelectable" value="YES"/>
</userDefinedRuntimeAttributes>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" rowHeight="51" id="amC-sE-8O0" customClass="InsetGroupTableViewCell" customModule="SideStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="546" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="amC-sE-8O0" id="GEO-2e-E4k">
<rect key="frame" x="0.0" y="0.0" width="375" height="51"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Allow Siri To Refresh Apps…" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="c6K-fI-CVr">
<rect key="frame" x="30" y="15.5" width="228.5" height="20.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="c6K-fI-CVr" firstAttribute="centerY" secondItem="GEO-2e-E4k" secondAttribute="centerY" id="IGB-ox-RAM"/>
<constraint firstItem="c6K-fI-CVr" firstAttribute="leading" secondItem="GEO-2e-E4k" secondAttribute="leadingMargin" id="xoI-eB-1TH"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="1" alpha="0.14999999999999999" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="8" left="30" bottom="8" right="30"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="style">
<integer key="value" value="3"/>

View File

@@ -32,17 +32,16 @@ extension SettingsViewController
{
case backgroundRefresh
case noIdleTimeout
case disableAppLimit
@available(iOS 14, *)
case addToSiri
case disableAppLimit
static var allCases: [AppRefreshRow] {
var c: [AppRefreshRow] = [.backgroundRefresh, .noIdleTimeout]
guard #available(iOS 14, *) else { return c }
c.append(.addToSiri)
// conditional entries go at the last to preserve ordering
if !ProcessInfo().sparseRestorePatched { c.append(.disableAppLimit) }
c.append(.addToSiri)
return c
}
}
@@ -496,13 +495,6 @@ extension SettingsViewController
}
}
if let cell = cell as? InsetGroupTableViewCell,
indexPath.section == Section.appRefresh.rawValue,
indexPath.row == AppRefreshRow.allCases.count-1 // last row
{
cell.setValue(3, forKey: "style")
}
return cell
}

View File

@@ -26,4 +26,3 @@ FOUNDATION_EXPORT const unsigned char AltStoreCoreVersionString[];
#import <AltStoreCore/ALTWrappedError.h>
#import <AltStoreCore/NSError+ALTServerError.h>
#import <AltStoreCore/CFNotificationName+AltStore.h>
#import "ptrace.h"

View File

@@ -240,7 +240,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
self.iconURL = try container.decode(URL.self, forKey: .iconURL)
self.screenshotURLs = try container.decodeIfPresent([URL].self, forKey: .screenshotURLs) ?? []
var downloadURL = try container.decodeIfPresent(URL.self, forKey: .downloadURL)
let downloadURL = try container.decodeIfPresent(URL.self, forKey: .downloadURL)
let platformURLs = try container.decodeIfPresent(PlatformURLs.self.self, forKey: .platformURLs)
if let platformURLs = platformURLs {
self.platformURLs = platformURLs
@@ -255,22 +255,7 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
} else if let downloadURL = downloadURL {
self._downloadURL = downloadURL
} else {
let version = try container.decode(String.self, forKey: .version)
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions){
for ver in versions {
if ver.version == version {
self._downloadURL = ver.downloadURL
downloadURL = ver.downloadURL // not sure if this is needed
}
}
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
} else {
throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
}
// else {
// throw DecodingError.dataCorruptedError(forKey: .downloadURL, in: container, debugDescription: "E downloadURL:String or downloadURLs:[[Platform:URL]] key required.")
// }
}
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
@@ -290,9 +275,6 @@ public class StoreApp: NSManagedObject, Decodable, Fetchable
if let versions = try container.decodeIfPresent([AppVersion].self, forKey: .versions)
{
//TODO: Throw error if there isn't at least one version.
if (versions.count == 0){
throw DecodingError.dataCorruptedError(forKey: .versions, in: container, debugDescription: "At least one version is required in key: versions")
}
for version in versions
{

View File

@@ -1,14 +0,0 @@
//
// ptrace.h
// AltStore
//
// Created by June P on 12/26/24.
// Copyright © 2024 SideStore. All rights reserved.
//
#ifndef ptrace_h
#define ptrace_h
int ptrace(int request, pid_t pid, caddr_t addr, int data);
#endif /* ptrace_h */

View File

@@ -1,27 +0,0 @@
//
// ptrace.m
// AltStore
//
// Created by June P on 12/26/24.
// Copyright © 2024 SideStore. All rights reserved.
//
int ptrace(int request, pid_t pid, caddr_t addr, int data) {
int result = 0;
__asm__ (
"MOV x16, #26 \n" // Syscall number for ptrace
"MOV x0, %[request] \n" // Pass request to x0
"MOV x1, %[pid] \n" // Pass pid to x1
"MOV x2, %[addr] \n" // Pass addr to x2
"MOV x3, %[data] \n" // Pass data to x3
"SVC 0 \n" // Make the syscall (0 for ARM64)
: [result] "=r" (result) // No output
: [request] "r" (request), // Input constraints
[pid] "r" (pid),
[addr] "r" (addr),
[data] "r" (data)
: "x0", "x1", "x2", "x3", "x16" // Clobber list
);
return result;
}

View File

@@ -1,8 +1,8 @@
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
MARKETING_VERSION = 0.5.10
CURRENT_PROJECT_VERSION = 5100
MARKETING_VERSION = 0.5.9
CURRENT_PROJECT_VERSION = 5090
// Vars to be overwritten by `CodeSigning.xcconfig` if exists
DEVELOPMENT_TEAM = S32Z3HMYVQ

View File

@@ -44,36 +44,16 @@ check_for_update() {
LAST_FETCH=`cat .last-prebuilt-fetch-$1 | perl -n -e '/([0-9]*),([^ ]*)$/ && print $1'`
LAST_COMMIT=`cat .last-prebuilt-fetch-$1 | perl -n -e '/([0-9]*),([^ ]*)$/ && print $2'`
# Check if required library files exist
FORCE_DOWNLOAD=false
if [ ! -f "$1/lib$1-sim.a" ] || [ ! -f "$1/lib$1-ios.a" ]; then
echo "Required libraries missing for $1, forcing download..."
FORCE_DOWNLOAD=true
fi
# Download if:
# 1. Libraries are missing (FORCE_DOWNLOAD), or
# 2. Last fetch was over 1 hour ago, or
# 3. Force flag was passed
if [ "$FORCE_DOWNLOAD" = true ] || [[ $LAST_FETCH -lt $(expr $(date +%s) - 3600) ]] || [[ "$2" == "force" ]]; then
# fetch if last fetch was over 1 hour ago
if [[ $LAST_FETCH -lt $(expr $(date +%s) - 3600) ]] || [[ "$2" == "force" ]]; then
echo "Checking $1 for update"
echo
LATEST_COMMIT=`curl https://api.github.com/repos/SideStore/$1/releases/latest | perl -n -e '/Commit: https:\\/\\/github\\.com\\/[^\\/]*\\/[^\\/]*\\/commit\\/([^"]*)/ && print $1'`
echo
echo "Last commit: $LAST_COMMIT"
echo "Latest commit: $LATEST_COMMIT"
NOT_UPTODATE=false
if [[ "$LAST_COMMIT" != "$LATEST_COMMIT" ]]; then
echo "Found update on the remote: https://api.github.com/repos/SideStore/$1/releases/latest"
NOT_UPTODATE=true
fi
# Download if:
# 1. Libraries are missing (FORCE_DOWNLOAD), or
# 2. New commit is available
if [ "$FORCE_DOWNLOAD" = true ] || [ "$NOT_UPTODATE" = true ] ;then
echo "downloading binaries"
echo "Found update, downloading binaries"
echo
wget -O "$1/lib$1-sim.a" "https://github.com/SideStore/$1/releases/latest/download/lib$1-sim.a"
if [[ "$1" != "minimuxer" ]]; then

View File

@@ -6,7 +6,7 @@
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://makeapullrequest.com)
[![Nightly SideStore build](https://github.com/SideStore/SideStore/actions/workflows/nightly.yml/badge.svg)](https://github.com/SideStore/SideStore/actions/workflows/nightly.yml)
[![.github/workflows/beta.yml](https://github.com/SideStore/SideStore/actions/workflows/beta.yml/badge.svg)](https://github.com/SideStore/SideStore/actions/workflows/beta.yml)
[![Discord](https://img.shields.io/discord/949183273383395328?label=Discord)](https://discord.gg/sidestore)
[![Discord](https://img.shields.io/discord/949183273383395328?label=Discord)](https://discord.gg/sidestore-949183273383395328)
![Alt](https://repobeats.axiom.co/api/embed/3a329ce95955690b9a9366f8d5598626a847d96c.svg "Repobeats analytics image")