mkit-cli 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mkit/secrets.py ADDED
@@ -0,0 +1,148 @@
1
+ """Credentials, from two .env files, never from the repo tree.
2
+
3
+ ~/.mkit/.env shared by every app: the Play service account (one
4
+ developer account), Telegram, the reports bucket
5
+ <project>/.env this app only: its Firebase service account, overrides
6
+
7
+ The shell environment wins over both, then the project file, then the user
8
+ file. A credential is JSON, stored one of two ways:
9
+
10
+ MKIT_<NAME>_JSON_BASE64=eyJ0eXBlIjoi... base64, survives .env quoting
11
+ MKIT_<NAME>_FILE=C:/keys/play.json a path outside the repo
12
+
13
+ The unprefixed spelling (<NAME>_JSON_BASE64) is accepted too, because the
14
+ older per-project kits and CI used it.
15
+
16
+ Every secret value that is read is registered with the output layer, so it is
17
+ masked if it ever reaches a log line, an error, or a JSON result.
18
+ """
19
+ import base64
20
+ import binascii
21
+ import json
22
+ import os
23
+ from pathlib import Path
24
+
25
+ from . import config
26
+ from .errors import CredentialMissing
27
+
28
+ # name -> what it is for; doctor and error hints use the description.
29
+ KNOWN = {
30
+ 'PLAY_SERVICE_ACCOUNT': 'Google Play Android Publisher service account (JSON)',
31
+ 'FIREBASE_SERVICE_ACCOUNT': "Firebase/Google Cloud service account for this app's project (JSON)",
32
+ 'TELEGRAM': 'Telegram bot, JSON {"bot_token": "...", "chat_id": "..."}',
33
+ }
34
+
35
+ _registered = set()
36
+
37
+
38
+ def register(value):
39
+ """Mark a string as secret so the output layer masks it."""
40
+ if isinstance(value, str) and len(value) >= 8:
41
+ _registered.add(value)
42
+
43
+
44
+ def redact(text):
45
+ if not isinstance(text, str) or not _registered:
46
+ return text
47
+ for secret in sorted(_registered, key=len, reverse=True):
48
+ if secret in text:
49
+ text = text.replace(secret, '***')
50
+ return text
51
+
52
+
53
+ def parse_env_file(path):
54
+ """KEY=VALUE lines; # comments; optional quotes. No interpolation."""
55
+ values = {}
56
+ try:
57
+ with open(path, encoding='utf-8-sig') as f:
58
+ lines = f.read().splitlines()
59
+ except FileNotFoundError:
60
+ return values
61
+ for raw in lines:
62
+ line = raw.strip()
63
+ if not line or line.startswith('#') or '=' not in line:
64
+ continue
65
+ key, value = line.split('=', 1)
66
+ key = key.strip()
67
+ if key.startswith('export '):
68
+ key = key[7:].strip()
69
+ value = value.strip()
70
+ if len(value) >= 2 and value[0] == value[-1] and value[0] in '"\'':
71
+ value = value[1:-1]
72
+ values[key] = value
73
+ return values
74
+
75
+
76
+ class Secrets:
77
+ def __init__(self, project_root=None, user_home=None):
78
+ self.user_file = Path(user_home or config.home()) / '.env'
79
+ self.project_file = Path(project_root) / '.env' if project_root else None
80
+ self._user = parse_env_file(self.user_file)
81
+ self._project = parse_env_file(self.project_file) if self.project_file else {}
82
+
83
+ def lookup(self, key):
84
+ """(value, source) for a plain key, or (None, None)."""
85
+ if os.environ.get(key):
86
+ return os.environ[key], 'environment'
87
+ if self._project.get(key):
88
+ return self._project[key], str(self.project_file)
89
+ if self._user.get(key):
90
+ return self._user[key], str(self.user_file)
91
+ return None, None
92
+
93
+ def value(self, key, default=None):
94
+ """A plain setting such as MKIT_PLAY_REPORTS_BUCKET."""
95
+ for candidate in (key, key[5:] if key.startswith('MKIT_') else None):
96
+ if candidate:
97
+ found, _ = self.lookup(candidate)
98
+ if found:
99
+ return found
100
+ return default
101
+
102
+ def _raw(self, name):
103
+ for prefix in ('MKIT_', ''):
104
+ b64, source = self.lookup(f'{prefix}{name}_JSON_BASE64')
105
+ if b64:
106
+ return 'base64', b64, source
107
+ path, source = self.lookup(f'{prefix}{name}_FILE')
108
+ if path:
109
+ return 'file', path, source
110
+ return None, None, None
111
+
112
+ def source(self, name):
113
+ """Where a credential would come from, for doctor. None if absent."""
114
+ _, _, source = self._raw(name)
115
+ return source
116
+
117
+ def has(self, name):
118
+ return self._raw(name)[0] is not None
119
+
120
+ def json(self, name, required=True):
121
+ """The credential as a dict. Missing and `required` raises exit 3 with
122
+ a hint naming the exact key and file to put it in."""
123
+ form, raw, source = self._raw(name)
124
+ if form is None:
125
+ if not required:
126
+ return None
127
+ raise CredentialMissing(
128
+ f'{KNOWN.get(name, name)} is not set',
129
+ hint=(f'Add MKIT_{name}_JSON_BASE64=<base64 of the JSON> to '
130
+ f'{self.project_file or "<project>/.env"} (this app) or '
131
+ f'{self.user_file} (all apps). `mkit doctor` shows what is set.'),
132
+ code=f'MISSING_{name}')
133
+ register(raw)
134
+ try:
135
+ if form == 'base64':
136
+ text = base64.b64decode(''.join(raw.split()), validate=True).decode('utf-8')
137
+ else:
138
+ text = Path(raw).expanduser().read_text(encoding='utf-8-sig')
139
+ data = json.loads(text)
140
+ except (binascii.Error, UnicodeDecodeError, ValueError, OSError) as e:
141
+ raise CredentialMissing(
142
+ f'{name} from {source} could not be read as JSON ({type(e).__name__})',
143
+ hint='Re-encode it: PowerShell [Convert]::ToBase64String([IO.File]::'
144
+ 'ReadAllBytes("key.json")), macOS/Linux base64 -w 0 key.json',
145
+ code=f'BAD_{name}') from e
146
+ for field in ('private_key', 'private_key_id', 'bot_token'):
147
+ register(data.get(field))
148
+ return data
mkit/toolchain.py ADDED
@@ -0,0 +1,210 @@
1
+ """Finding the tools a build needs: Android SDK, adb, emulator, a JDK, Gradle.
2
+
3
+ Nothing here is per project except where a project may pin a value in
4
+ mkit.toml ([app].java_home) or the environment (MKIT_JAVA_HOME, JAVA_HOME,
5
+ ANDROID_HOME).
6
+ """
7
+ import glob
8
+ import os
9
+ import re
10
+ import shutil
11
+ import subprocess
12
+ from pathlib import Path
13
+
14
+ from .errors import MkitError
15
+
16
+ EXE = '.exe' if os.name == 'nt' else ''
17
+
18
+ # Gradle 8 runs on JDK 17-24. Android Studio's bundled jbr is tried last: recent
19
+ # Studio builds ship a JDK newer than Gradle 8 accepts.
20
+ JDK_MIN, JDK_MAX = 17, 24
21
+ JDK_CANDIDATES = (
22
+ r'C:\Program Files\Microsoft\jdk-17*',
23
+ r'C:\Program Files\Microsoft\jdk-21*',
24
+ r'C:\Program Files\Eclipse Adoptium\jdk-17*',
25
+ r'C:\Program Files\Eclipse Adoptium\jdk-21*',
26
+ r'C:\Program Files\Java\jdk-17*',
27
+ r'C:\Program Files\Java\jdk-21*',
28
+ os.path.join(str(Path.home()), '.jdks', 'jbr-17*'),
29
+ os.path.join(str(Path.home()), '.jdks', '*-17*'),
30
+ os.path.join(str(Path.home()), '.jdks', '*-21*'),
31
+ '/usr/lib/jvm/*-17*',
32
+ '/usr/lib/jvm/*-21*',
33
+ '/Library/Java/JavaVirtualMachines/*17*/Contents/Home',
34
+ '/Library/Java/JavaVirtualMachines/*21*/Contents/Home',
35
+ r'C:\Program Files\Android\Android Studio\jbr',
36
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Android Studio', 'jbr'),
37
+ '/Applications/Android Studio.app/Contents/jbr/Contents/Home',
38
+ '/opt/android-studio/jbr',
39
+ )
40
+
41
+
42
+ # Where Android Studio lives, newest first within a pattern. The Toolbox keeps
43
+ # every build in its own folder, so those are globbed and sorted.
44
+ STUDIO_CANDIDATES = (
45
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Android Studio*', 'bin', 'studio64.exe'),
46
+ r'C:\Program Files\Android\Android Studio*\bin\studio64.exe',
47
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'JetBrains', 'Toolbox', 'apps', '*ndroid*',
48
+ '**', 'bin', 'studio64.exe'),
49
+ os.path.join(os.environ.get('LOCALAPPDATA', ''), 'Programs', 'Android Studio*', 'bin', 'studio.exe'),
50
+ '/Applications/Android Studio*.app/Contents/MacOS/studio',
51
+ os.path.join(str(Path.home()), 'Applications', 'Android Studio*.app', 'Contents', 'MacOS', 'studio'),
52
+ '/opt/android-studio*/bin/studio.sh',
53
+ '/usr/local/android-studio*/bin/studio.sh',
54
+ os.path.join(str(Path.home()), '.local', 'share', 'JetBrains', 'Toolbox', 'apps', '*ndroid*',
55
+ '**', 'bin', 'studio.sh'),
56
+ )
57
+
58
+ STUDIO_EXES = ('studio64.exe', 'studio.exe') if os.name == 'nt' else ('studio.sh', 'studio')
59
+
60
+
61
+ def _studio_inside(path):
62
+ """The launcher inside a folder somebody pinned: an install dir, a macOS
63
+ bundle, or the bin/ folder itself."""
64
+ path = Path(path)
65
+ if path.is_file():
66
+ return str(path)
67
+ for relative in ('Contents/MacOS/studio', *(f'bin/{e}' for e in STUDIO_EXES), *STUDIO_EXES):
68
+ found = path / relative
69
+ if found.is_file():
70
+ return str(found)
71
+ return None
72
+
73
+
74
+ def studio(project=None):
75
+ """Android Studio's launcher: [app].studio or MKIT_STUDIO taken as given,
76
+ then the usual install locations, then PATH."""
77
+ pinned = (project.get('app.studio') if project else None) or os.environ.get('MKIT_STUDIO')
78
+ if pinned:
79
+ found = _studio_inside(pinned)
80
+ if not found:
81
+ raise MkitError(f'studio {pinned} is not an Android Studio launcher', code='TOOL_MISSING',
82
+ hint='Point it at studio64.exe / studio.sh, or at the install folder.')
83
+ return found
84
+ for pattern in STUDIO_CANDIDATES:
85
+ for candidate in sorted(glob.glob(pattern, recursive=True), reverse=True):
86
+ if Path(candidate).is_file():
87
+ return candidate
88
+ for name in STUDIO_EXES:
89
+ found = shutil.which(name)
90
+ if found:
91
+ return found
92
+ raise MkitError('Android Studio not found', code='TOOL_MISSING',
93
+ hint='Install it, or set studio under [app] in mkit.toml (or MKIT_STUDIO) '
94
+ 'to the studio64.exe / studio.sh path.')
95
+
96
+
97
+ def tool(name):
98
+ """A PATH tool's full path. shutil.which follows PATHEXT, so firebase.cmd
99
+ and flutter.bat launch without shell=True."""
100
+ path = shutil.which(name)
101
+ if not path:
102
+ raise MkitError(f'{name} not found on PATH', code='TOOL_MISSING',
103
+ hint=f'Install {name} or add it to PATH, then re-run `mkit doctor`.')
104
+ return path
105
+
106
+
107
+ def _local_properties(root):
108
+ for props in (root / 'local.properties', root / 'android' / 'local.properties'):
109
+ if props.is_file():
110
+ for line in props.read_text(encoding='utf-8', errors='replace').splitlines():
111
+ m = re.match(r'\s*sdk\.dir\s*=\s*(.+?)\s*$', line)
112
+ if m:
113
+ return Path(m.group(1).replace('\\\\', '\\').replace('\\:', ':'))
114
+ return None
115
+
116
+
117
+ def sdk_dir(root=None):
118
+ if root is not None:
119
+ found = _local_properties(Path(root))
120
+ if found and found.is_dir():
121
+ return found
122
+ for key in ('ANDROID_HOME', 'ANDROID_SDK_ROOT'):
123
+ if os.environ.get(key) and Path(os.environ[key]).is_dir():
124
+ return Path(os.environ[key])
125
+ guess = (Path(os.environ.get('LOCALAPPDATA', '')) / 'Android' / 'Sdk' if os.name == 'nt'
126
+ else Path.home() / ('Library/Android/sdk' if os.uname().sysname == 'Darwin' else 'Android/Sdk'))
127
+ return guess if guess.is_dir() else None
128
+
129
+
130
+ def adb(root=None):
131
+ """The SDK's adb before any on PATH: other tools (scrcpy, phone suites)
132
+ ship their own, and two adb versions keep killing each other's server."""
133
+ sdk = sdk_dir(root)
134
+ if sdk:
135
+ exe = sdk / 'platform-tools' / f'adb{EXE}'
136
+ if exe.is_file():
137
+ return str(exe)
138
+ found = shutil.which('adb')
139
+ if found:
140
+ return found
141
+ raise MkitError('adb not found', code='TOOL_MISSING',
142
+ hint='Install the Android SDK platform-tools, or set ANDROID_HOME.')
143
+
144
+
145
+ def emulator(root=None):
146
+ sdk = sdk_dir(root)
147
+ exe = sdk / 'emulator' / f'emulator{EXE}' if sdk else None
148
+ if not exe or not exe.is_file():
149
+ raise MkitError('Android emulator not found', code='TOOL_MISSING',
150
+ hint='Install it from the SDK Manager, or set sdk.dir in local.properties.')
151
+ return str(exe)
152
+
153
+
154
+ def java_major(home):
155
+ java = Path(home) / 'bin' / f'java{EXE}'
156
+ if not java.is_file():
157
+ return 0
158
+ try:
159
+ out = subprocess.run([str(java), '-version'], capture_output=True, text=True, timeout=30).stderr
160
+ except Exception:
161
+ return 0
162
+ m = re.search(r'version "(\d+)', out)
163
+ return int(m.group(1)) if m else 0
164
+
165
+
166
+ def find_jdk(project=None):
167
+ """A JDK for Gradle (JAVA_HOME) and the Android SDK (ANDROID_HOME), both
168
+ exported for the processes we spawn. See _find_jdk for the JDK order."""
169
+ jdk = _find_jdk(project)
170
+ if not os.environ.get('ANDROID_HOME') and not os.environ.get('ANDROID_SDK_ROOT'):
171
+ # A checkout without local.properties (it is gitignored) still builds.
172
+ sdk = sdk_dir(project.root if project else None)
173
+ if sdk:
174
+ os.environ['ANDROID_HOME'] = str(sdk)
175
+ return jdk
176
+
177
+
178
+ def _find_jdk(project=None):
179
+ """A JDK Gradle accepts, exported as JAVA_HOME for the processes we spawn.
180
+
181
+ Order: [app].java_home / MKIT_JAVA_HOME (taken as given), JAVA_HOME if it is
182
+ in range, then the first candidate in range. Deliberately not left to
183
+ `flutter build`, which prefers Android Studio's bundled JDK -- the one that
184
+ is often too new.
185
+ """
186
+ pinned = project.get('app.java_home') if project else os.environ.get('MKIT_APP_JAVA_HOME')
187
+ pinned = pinned or os.environ.get('MKIT_JAVA_HOME')
188
+ if pinned:
189
+ if not java_major(pinned):
190
+ raise MkitError(f'java_home {pinned} has no bin/java', code='TOOL_MISSING')
191
+ os.environ['JAVA_HOME'] = str(pinned)
192
+ return str(pinned)
193
+ current = os.environ.get('JAVA_HOME', '').rstrip('\\/')
194
+ if current and JDK_MIN <= java_major(current) <= JDK_MAX:
195
+ return current
196
+ for pattern in JDK_CANDIDATES:
197
+ for candidate in sorted(glob.glob(pattern), reverse=True):
198
+ if JDK_MIN <= java_major(candidate) <= JDK_MAX:
199
+ os.environ['JAVA_HOME'] = candidate
200
+ return candidate
201
+ raise MkitError(f'no JDK {JDK_MIN}-{JDK_MAX} found', code='TOOL_MISSING',
202
+ hint='Install JDK 17 or 21, or set java_home under [app] in mkit.toml.')
203
+
204
+
205
+ def gradlew(android_dir):
206
+ path = Path(android_dir) / ('gradlew.bat' if os.name == 'nt' else 'gradlew')
207
+ if not path.is_file():
208
+ raise MkitError(f'no Gradle wrapper at {path}', code='TOOL_MISSING',
209
+ hint='Commit the Gradle wrapper (gradlew, gradlew.bat, gradle/wrapper).')
210
+ return str(path)
@@ -0,0 +1,179 @@
1
+ Metadata-Version: 2.5
2
+ Name: mkit-cli
3
+ Version: 0.3.1
4
+ Summary: Release Android apps to Google Play from the command line: build, upload, listing, screenshots, data safety, in-app products, vitals and Firebase, in one tool built for humans and AI agents.
5
+ Project-URL: Homepage, https://github.com/almasumdev/mkit-cli
6
+ Project-URL: Documentation, https://github.com/almasumdev/mkit-cli/blob/main/docs/commands.md
7
+ Project-URL: Repository, https://github.com/almasumdev/mkit-cli
8
+ Project-URL: Issues, https://github.com/almasumdev/mkit-cli/issues
9
+ Project-URL: Changelog, https://github.com/almasumdev/mkit-cli/blob/main/CHANGELOG.md
10
+ Author: Nurullah Al-Masum
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: android,aso,firebase,flutter,google-play,release
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: Developers
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Software Development :: Build Tools
24
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
25
+ Classifier: Topic :: System :: Software Distribution
26
+ Requires-Python: >=3.11
27
+ Requires-Dist: google-api-python-client>=2.150
28
+ Requires-Dist: google-auth>=2.35
29
+ Requires-Dist: pillow>=11
30
+ Requires-Dist: requests>=2.32
31
+ Description-Content-Type: text/markdown
32
+
33
+ # mkit
34
+
35
+ [![tests](https://github.com/almasumdev/mkit-cli/actions/workflows/tests.yml/badge.svg)](https://github.com/almasumdev/mkit-cli/actions/workflows/tests.yml)
36
+ [![licence: MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](https://github.com/almasumdev/mkit-cli/blob/main/LICENSE)
37
+
38
+ One command-line tool for everything around an Android app that is not writing
39
+ it: build and install on a device, release to Google Play, keep the store
40
+ listing, screenshots, data safety and in-app products in your repo, read
41
+ installs, vitals, crashes and reviews, check search rank, and drive the
42
+ Firebase side (Firestore rules, Remote Config, an in-app update document).
43
+
44
+ Flutter and native Kotlin apps, including apps with a Wear OS module.
45
+
46
+ ```bash
47
+ mkit deploy # bump, build, upload to Play, publish, notify, push
48
+ mkit run -p # build and install on the phone
49
+ mkit status # what every track holds right now
50
+ mkit crashes # what is breaking in production
51
+ ```
52
+
53
+ A release is one command. `mkit deploy` bumps the version, commits it, builds
54
+ the bundle, uploads it with its mapping file and release notes, stages it on a
55
+ track, writes the update document, sends a message, and pushes — and if
56
+ anything fails before Play accepts the build, the version bump undoes itself so
57
+ the repo never claims a release that did not happen.
58
+
59
+ ## Why it exists
60
+
61
+ Shipping an Android app means a dozen small jobs that live in different places:
62
+ a Gradle command, the Play Console, the Firebase console, a screenshot folder
63
+ somebody renamed, a changelog nobody updated. Each is easy; together they are a
64
+ release day. mkit puts them behind one tool, with the app's settings in one
65
+ file, so the same release happens the same way every time — and so an AI agent
66
+ can run the safe parts without being handed your Play Console password.
67
+
68
+ It was built to ship a small studio's apps and has been doing that for a while.
69
+ It is opinionated where opinions save time and configurable where apps really
70
+ differ.
71
+
72
+ ## Install
73
+
74
+ Requires Python 3.11 or newer. [uv](https://docs.astral.sh/uv/) is the easiest
75
+ way in, and installs mkit as a command of its own rather than into a project:
76
+
77
+ ```bash
78
+ uv tool install mkit-cli
79
+ mkit --version
80
+ ```
81
+
82
+ With pipx: `pipx install mkit-cli`. With pip: `pip install mkit-cli`.
83
+
84
+ The latest from `main`, between releases:
85
+ `uv tool install git+https://github.com/almasumdev/mkit-cli`
86
+
87
+ `mkit self status` says whether a newer release is out, and `mkit self update`
88
+ gives the upgrade command for whichever of these installed it.
89
+
90
+ ## Getting started
91
+
92
+ In an app's folder:
93
+
94
+ ```bash
95
+ mkit init # writes mkit.toml, asking what it cannot work out
96
+ mkit doctor # what is configured, what is missing, and what to add
97
+ ```
98
+
99
+ `mkit.toml` is the whole of an app's configuration:
100
+
101
+ ```toml
102
+ [app]
103
+ name = "My App"
104
+ kind = "flutter" # flutter | android | android-wear
105
+ package = "com.example.app"
106
+
107
+ [play] # switches Play features on
108
+
109
+ [store] # store listing, art and data safety in the repo
110
+ listing_dir = "store/listing"
111
+ ```
112
+
113
+ Credentials never live in that file. A Google Play service account goes in
114
+ `.env` (per app) or `~/.mkit/.env` (shared); `mkit doctor` names the exact key
115
+ when one is missing, and never prints a secret. Every setting is documented in
116
+ [docs/config.md](https://github.com/almasumdev/mkit-cli/blob/main/docs/config.md).
117
+
118
+ ## What it does
119
+
120
+ | | |
121
+ |---|---|
122
+ | **Develop** | `run`, `devices`, `logs`, `emu`, `studio`, `gradle`, `version`, `bump` |
123
+ | **Release** | `deploy`, `status`, `promote`, `rollout`, `share` |
124
+ | **Store** | `listing`, `whatsnew`, `datasafety`, `testers`, `art` |
125
+ | **Money** | `products`, `prices` |
126
+ | **Insight** | `stats`, `vitals`, `crashes`, `reviews`, `aso` |
127
+ | **Firebase** | `rules`, `config`, `remoteconfig`, `firebase` |
128
+
129
+ Every command, flag and example: [docs/commands.md](https://github.com/almasumdev/mkit-cli/blob/main/docs/commands.md),
130
+ generated from the code so it cannot describe a flag that does not exist.
131
+
132
+ Commands are classed by what they can change — `read`, `plan`, `write`,
133
+ `publish` — and anything in the `publish` class refuses to run without an
134
+ explicit confirmation naming the package.
135
+
136
+ Some things have no API and must be done in the Play Console. `mkit manual`
137
+ lists them, so you know what is left rather than discovering it at release
138
+ time.
139
+
140
+ ## Working with AI agents
141
+
142
+ mkit answers in JSON whenever it is not attached to a terminal, with one
143
+ envelope on stdout and a documented exit code per outcome, so an agent can
144
+ branch on a result instead of reading prose. [AGENTS.md](https://github.com/almasumdev/mkit-cli/blob/main/AGENTS.md) is the
145
+ contract: which commands are safe to run unasked, which need a human to say so,
146
+ and what an agent must never do.
147
+
148
+ ## Documentation
149
+
150
+ | | |
151
+ |---|---|
152
+ | [docs/commands.md](https://github.com/almasumdev/mkit-cli/blob/main/docs/commands.md) | every command and flag |
153
+ | [docs/config.md](https://github.com/almasumdev/mkit-cli/blob/main/docs/config.md) | `mkit.toml`, the store and keyword files, credentials |
154
+ | [docs/store.md](https://github.com/almasumdev/mkit-cli/blob/main/docs/store.md) | the `store/` folder: what lives where, in every language |
155
+ | [docs/store-art.md](https://github.com/almasumdev/mkit-cli/blob/main/docs/store-art.md) | screenshots and the feature graphic, built from real captures |
156
+ | [docs/in-app-update.md](https://github.com/almasumdev/mkit-cli/blob/main/docs/in-app-update.md) | how an app should check for updates: Play first, your policy second |
157
+ | [docs/news-topics.md](https://github.com/almasumdev/mkit-cli/blob/main/docs/news-topics.md) | news and offer notifications over FCM topics |
158
+ | [AGENTS.md](https://github.com/almasumdev/mkit-cli/blob/main/AGENTS.md) | the agent contract |
159
+ | [CONTRIBUTING.md](https://github.com/almasumdev/mkit-cli/blob/main/CONTRIBUTING.md) | changing mkit, tests, versioning |
160
+ | [SECURITY.md](https://github.com/almasumdev/mkit-cli/blob/main/SECURITY.md) | what counts as a security bug here, and how to report it |
161
+
162
+ ## Status and scope
163
+
164
+ Developed on Windows against real apps on Google Play; the code paths for
165
+ macOS and Linux exist but are less travelled, and there is no CI yet. The test
166
+ suite runs against a fake Play service — nothing in the tests touches a real
167
+ account.
168
+
169
+ It does not: create an app in the Play Console, answer content rating or target
170
+ audience questionnaires, manage tester email lists (the API only takes Google
171
+ Groups), or publish for you when managed publishing is on. `mkit manual` is the
172
+ full list.
173
+
174
+ Issues and pull requests are welcome. If you use it for your own apps, I would
175
+ like to hear what broke.
176
+
177
+ ## Licence
178
+
179
+ MIT. See [LICENSE](https://github.com/almasumdev/mkit-cli/blob/main/LICENSE).
@@ -0,0 +1,48 @@
1
+ mkit/__init__.py,sha256=iUQkWEnJBDh_PVSwZzs2sDbiKY0IYTDKqeV75nFY0-4,227
2
+ mkit/art.py,sha256=2Nkgxzwql02wTQfSeVmgfmufryjjx_Q55Tulh0C-T70,59074
3
+ mkit/aso.py,sha256=XMWDkzAgt40Vg9INSE6War9AEwflNGuznGpCc-H_MzE,15291
4
+ mkit/cli.py,sha256=exINjh598DDfRHhI9VuUCTyaE0fLhdPANBxMpD2sK7M,7246
5
+ mkit/config.py,sha256=mGTMeC90WEVv_LZh8xby9HnniXA2eb7laLSRrv6sfVY,5252
6
+ mkit/datasafety.py,sha256=afHlHx90nV4niOM1qBoZOVK0TR0dH9iPCtFOifAv_18,13066
7
+ mkit/device.py,sha256=8Ooe_MjELX9E4rw4kmBx_aDyE7BFTZj5eRLGNZty9IY,10643
8
+ mkit/docgen.py,sha256=xAZaulKML9j-1xJxuj0EAcWh4G1FcyPgHLoVxeO2Xog,4518
9
+ mkit/errors.py,sha256=1x067e8l288UMeIpG3s6SdxY03pC-7OJLiCpecapnyk,1889
10
+ mkit/git.py,sha256=ofCbNL8Knyz3Sf2msXEQsVSiq-syXVV0zHjgExpnkkk,3705
11
+ mkit/hooks.py,sha256=csh1TPRvL3v2lhNTHUGPj89cNHWHKnj-JxI61m_Gws8,1083
12
+ mkit/kinds.py,sha256=PuQeu4O7mzVV_9VCksY0V3D5tbXDkTfMF43ylsGkhYc,13277
13
+ mkit/notify.py,sha256=fM653ABGjapZd8vdr2IcixAOltPCwpgRxXbdbPPzKEI,1676
14
+ mkit/output.py,sha256=HQLDywJdGfT2TMwiEm46q4qeiCm9UhbnbZVjtQVHGO4,13782
15
+ mkit/proc.py,sha256=-dD8_KKnq4yFaVDr7Nzy5P0EZscaGykt0-Hr-W1OlT4,6179
16
+ mkit/registry.py,sha256=rAL7MwvQPSEhtdUchiEP7W-3druQR552Q1E-GqRD9K4,3610
17
+ mkit/safety.py,sha256=jwShFLnxVHxKyRcAqfSW-gOLMuH4Z3mtOmlk22adQLE,2721
18
+ mkit/secrets.py,sha256=RpTLMzY8096Vvx1Uikp_vUu-9gu7Jfot9W_J6VBz2Ek,5693
19
+ mkit/toolchain.py,sha256=UlPMQ4P0guEGBroG-qAJw8yBeOS6vugGHcvl4OV_WwU,8770
20
+ mkit/cloud/__init__.py,sha256=lcRs1Xr8vS4gTfG1gcHpqiNJDs2wkivGkWZ6cp6LY9s,85
21
+ mkit/cloud/auth.py,sha256=6WPNauTQ9fPiX9QFmhaaVhRnIjYqiN-TzQnIPneB8AQ,5364
22
+ mkit/cloud/fbproject.py,sha256=pGQdS6Q4ycnq0XKmYAo31oz0u-NQno0crJPHmqhE5dM,7731
23
+ mkit/cloud/firebase.py,sha256=KP6IIzfmnnojBWWtNrW_UHNtpUUpk90lh5Pzns8SBos,7453
24
+ mkit/cloud/insight.py,sha256=JtqYB3LNsEPfWJOZTUwuMyDzWFIKkpujpWtAJs3QFME,12947
25
+ mkit/cloud/money.py,sha256=YY_6uGrmGvkOOeCrlblJXlbX128j5uD6op1lHC52Mtk,32107
26
+ mkit/cloud/play.py,sha256=PVbctsOnVzpKdtu8FiL3X1FzrXG0-q8_qqMbsubTvlo,23263
27
+ mkit/cloud/store.py,sha256=g-7Dgmd1B56RLWT5kppdWNXuQLffz0VqjrG0k3niQ-s,17639
28
+ mkit/commands/__init__.py,sha256=99Fc-7Nz1XPlqr55vvnrTg3M3YmxKmCkLuqLjH4Lep8,172
29
+ mkit/commands/art.py,sha256=9hf8sHVRBU8VUwu_9eKT6iWjHZ-QsI9ogTORHTkWIB0,5360
30
+ mkit/commands/aso.py,sha256=pJs8CPQNRQqvkzDcNtv_v8ILS3sZ88QSlo9zbv0_h-Q,7090
31
+ mkit/commands/dev.py,sha256=aH-BtTGQA0-QJndRjejfwtdX8EaingcTCHwIrzb46XI,13042
32
+ mkit/commands/doctor.py,sha256=PU6YRZ_ZC-WU5jJxyz6Ue1NoZW2qkJUsAu2zHvYeXBA,8103
33
+ mkit/commands/firebase_cmds.py,sha256=Q7MriBDRI4A-mtRgF333uLuO_z-n36_bYhpka3V0IPM,9633
34
+ mkit/commands/init.py,sha256=e-p2tQIX9XGDStivRR2UNDp_Nlh-Vt_5yuHcE2TO_LA,8227
35
+ mkit/commands/insight.py,sha256=53FFD6cua2O9OzR3Yud1pptStGSdJccjf5QQGyD9jns,5523
36
+ mkit/commands/meta.py,sha256=6mtY6AKLj0eQg2eU95cJFXUwFuGcvSOWIFc_Plmspug,5010
37
+ mkit/commands/money.py,sha256=K5-ZT2I8Yc-MDi21QPev0ZQBrS0s9j5xhKslTFOfyuY,6325
38
+ mkit/commands/release.py,sha256=XrHv67WWXLYYYqL0k3nT9_Zi688lC3iG9C5SvoyaT88,20888
39
+ mkit/commands/selfcmd.py,sha256=hIjJNJIFfadc4yoyeQurBuGKvbsSBqdDOujU75gGQnE,7463
40
+ mkit/commands/store.py,sha256=jGrm8Ty3LFF0SDFxCUiz4YzKKTrs2vJ5X4QQjT70t24,13070
41
+ mkit/data/datasafety-questions.csv,sha256=FrvFSRxZW99rOqj5JEXVtRkm0mp2quKnCP0SqUWI9yw,178026
42
+ mkit/data/art/art.toml,sha256=nJzDLlj-mKsEbE4uwI_4r6dhG6Tq17DT4wLy1tV-XvA,4885
43
+ mkit/data/art/en.json,sha256=xuEPUT_MzCdP2UOKHShTiCTDzjBgof7fzsOJ1SDi6pI,1115
44
+ mkit_cli-0.3.1.dist-info/METADATA,sha256=1b-FmEsLwE1QXX5_nE4d-W9hJd6Io44asiDaH1dG6eo,8349
45
+ mkit_cli-0.3.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
46
+ mkit_cli-0.3.1.dist-info/entry_points.txt,sha256=niTcFqsV7EFp871rsGdY6zWQcHxkzaox5Mj7Fa1KSKs,39
47
+ mkit_cli-0.3.1.dist-info/licenses/LICENSE,sha256=3DBcB7wESKA4pgutHnCDuOq55myaF_ftXfhLcE2muC8,1074
48
+ mkit_cli-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mkit = mkit.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nurullah Al-Masum
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.