nowrun 2.0.0__tar.gz → 2.0.2__tar.gz
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.
- {nowrun-2.0.0 → nowrun-2.0.2}/PKG-INFO +13 -12
- {nowrun-2.0.0 → nowrun-2.0.2}/README.md +12 -11
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/cli.py +33 -18
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/admin.py +117 -68
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/deploy.py +67 -23
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/inspect.py +1 -2
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/config.py +23 -19
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/output.py +6 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/skill/SKILL.md +48 -32
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun.egg-info/PKG-INFO +13 -12
- {nowrun-2.0.0 → nowrun-2.0.2}/setup.py +1 -1
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/__init__.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/__main__.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/apk.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/appmeta.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/client.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/__init__.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/app.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/legacy.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/login.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/mcp.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/mode.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/setup.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/skill.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/update.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/commands/validate.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/fields.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/passthrough.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun/upload.py +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun.egg-info/SOURCES.txt +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun.egg-info/dependency_links.txt +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun.egg-info/entry_points.txt +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun.egg-info/requires.txt +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/nowrun.egg-info/top_level.txt +0 -0
- {nowrun-2.0.0 → nowrun-2.0.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: nowrun
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: CLI to deploy and manage Android and HTML5 apps on nowrun.
|
|
5
5
|
Home-page: https://nowrun.io
|
|
6
6
|
Author: nowrun
|
|
@@ -14,12 +14,12 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
|
|
15
15
|
# nowrun CLI
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Publish Android apps so people can run them inside ChatGPT - driven by your AI coding agent.
|
|
18
18
|
|
|
19
|
-
`nowrun` publishes Android
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(native, Unity, Godot, Unreal)
|
|
19
|
+
`nowrun` publishes Android apps to [nowrun](https://nowrun.io) so people can run them inside
|
|
20
|
+
ChatGPT (and other AI assistants that connect to nowrun) - streamed into the conversation, no
|
|
21
|
+
install and no app store. You don't run deploy commands by hand: you set the CLI up once, then
|
|
22
|
+
tell your agent to ship. Works with any engine that builds an APK (native, Unity, Godot, Unreal).
|
|
23
23
|
|
|
24
24
|
## Quickstart
|
|
25
25
|
|
|
@@ -38,10 +38,10 @@ copy or paste. (`nowrun update` refreshes it later; `nowrun logout` signs out.)
|
|
|
38
38
|
|
|
39
39
|
**3. Tell your agent.** Open your AI coding tool in your project and give it this prompt:
|
|
40
40
|
|
|
41
|
-
> Deploy my app to nowrun
|
|
41
|
+
> Deploy my app to nowrun so people can run it in ChatGPT using the nowrun CLI. If you need the steps, read https://nowrun.io/docs/skill.md
|
|
42
42
|
|
|
43
|
-
Your agent creates (or reuses) the app, uploads the build, deploys it
|
|
44
|
-
|
|
43
|
+
Your agent creates (or reuses) the app, uploads the build, and deploys it - and once it's live,
|
|
44
|
+
people can run it inside ChatGPT. That's the whole flow.
|
|
45
45
|
|
|
46
46
|
## What your agent runs for you
|
|
47
47
|
|
|
@@ -56,13 +56,14 @@ They're here for reference (and the occasional manual run):
|
|
|
56
56
|
| `nowrun app list` / `nowrun app show` | list your apps / show one, with its visibility and readiness |
|
|
57
57
|
| `nowrun inspect app.apk` | read a build's package + version, no Android SDK needed |
|
|
58
58
|
| `nowrun validate` | check what's missing before deploying |
|
|
59
|
-
| `nowrun deploy -f app.apk` | deploy a build
|
|
59
|
+
| `nowrun deploy -f app.apk` | deploy a build so it's runnable in ChatGPT (version read from the APK; `--wait` blocks until live) |
|
|
60
60
|
| `nowrun status -s <submission_id>` | poll until it's live |
|
|
61
61
|
| `nowrun help --json` | full command list + workflow (what the agent reads) |
|
|
62
62
|
|
|
63
63
|
`deploy` reads the build's package/version and **refuses to deploy a build whose package
|
|
64
|
-
doesn't match the app**, so you never publish to the wrong place.
|
|
65
|
-
|
|
64
|
+
doesn't match the app**, so you never publish to the wrong place. It also **refuses a version
|
|
65
|
+
code that isn't higher than the one currently live** (like the Play Store), so you can't ship a
|
|
66
|
+
downgrade. Add `--json` (in any position) to any command for machine-readable output.
|
|
66
67
|
|
|
67
68
|
New apps are private; a nowrun admin decides which deployed apps are listed in ChatGPT.
|
|
68
69
|
You stay signed in: the CLI refreshes its session automatically (for 30 days after last use).
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# nowrun CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Publish Android apps so people can run them inside ChatGPT - driven by your AI coding agent.
|
|
4
4
|
|
|
5
|
-
`nowrun` publishes Android
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
(native, Unity, Godot, Unreal)
|
|
5
|
+
`nowrun` publishes Android apps to [nowrun](https://nowrun.io) so people can run them inside
|
|
6
|
+
ChatGPT (and other AI assistants that connect to nowrun) - streamed into the conversation, no
|
|
7
|
+
install and no app store. You don't run deploy commands by hand: you set the CLI up once, then
|
|
8
|
+
tell your agent to ship. Works with any engine that builds an APK (native, Unity, Godot, Unreal).
|
|
9
9
|
|
|
10
10
|
## Quickstart
|
|
11
11
|
|
|
@@ -24,10 +24,10 @@ copy or paste. (`nowrun update` refreshes it later; `nowrun logout` signs out.)
|
|
|
24
24
|
|
|
25
25
|
**3. Tell your agent.** Open your AI coding tool in your project and give it this prompt:
|
|
26
26
|
|
|
27
|
-
> Deploy my app to nowrun
|
|
27
|
+
> Deploy my app to nowrun so people can run it in ChatGPT using the nowrun CLI. If you need the steps, read https://nowrun.io/docs/skill.md
|
|
28
28
|
|
|
29
|
-
Your agent creates (or reuses) the app, uploads the build, deploys it
|
|
30
|
-
|
|
29
|
+
Your agent creates (or reuses) the app, uploads the build, and deploys it - and once it's live,
|
|
30
|
+
people can run it inside ChatGPT. That's the whole flow.
|
|
31
31
|
|
|
32
32
|
## What your agent runs for you
|
|
33
33
|
|
|
@@ -42,13 +42,14 @@ They're here for reference (and the occasional manual run):
|
|
|
42
42
|
| `nowrun app list` / `nowrun app show` | list your apps / show one, with its visibility and readiness |
|
|
43
43
|
| `nowrun inspect app.apk` | read a build's package + version, no Android SDK needed |
|
|
44
44
|
| `nowrun validate` | check what's missing before deploying |
|
|
45
|
-
| `nowrun deploy -f app.apk` | deploy a build
|
|
45
|
+
| `nowrun deploy -f app.apk` | deploy a build so it's runnable in ChatGPT (version read from the APK; `--wait` blocks until live) |
|
|
46
46
|
| `nowrun status -s <submission_id>` | poll until it's live |
|
|
47
47
|
| `nowrun help --json` | full command list + workflow (what the agent reads) |
|
|
48
48
|
|
|
49
49
|
`deploy` reads the build's package/version and **refuses to deploy a build whose package
|
|
50
|
-
doesn't match the app**, so you never publish to the wrong place.
|
|
51
|
-
|
|
50
|
+
doesn't match the app**, so you never publish to the wrong place. It also **refuses a version
|
|
51
|
+
code that isn't higher than the one currently live** (like the Play Store), so you can't ship a
|
|
52
|
+
downgrade. Add `--json` (in any position) to any command for machine-readable output.
|
|
52
53
|
|
|
53
54
|
New apps are private; a nowrun admin decides which deployed apps are listed in ChatGPT.
|
|
54
55
|
You stay signed in: the CLI refreshes its session automatically (for 30 days after last use).
|
|
@@ -57,7 +57,7 @@ WORKFLOW = [
|
|
|
57
57
|
"app create --title <t> ... # create an app with all its metadata (or 'app list' to reuse one)",
|
|
58
58
|
"use <app_id> # bind this workspace to an app (optional)",
|
|
59
59
|
"validate # check what's missing before deploying",
|
|
60
|
-
"deploy -f app.apk ... # upload + deploy a build
|
|
60
|
+
"deploy -f app.apk ... # upload + deploy a build (runnable in ChatGPT once live)",
|
|
61
61
|
"status # poll until it's live",
|
|
62
62
|
"mode [admin|personal] # nowrun admins: show/switch mode (admin = nowrun-managed apps, audited)",
|
|
63
63
|
"admin apps list ... # nowrun admins (admin mode): review apps, set-public/disable/..., users, audit",
|
|
@@ -112,8 +112,9 @@ def build_manifest(parser):
|
|
|
112
112
|
tree = _describe(parser, 'nowrun', 'nowrun publishing CLI')
|
|
113
113
|
return {
|
|
114
114
|
"tool": "nowrun",
|
|
115
|
-
"description": "Deploy Android
|
|
116
|
-
"Every command supports --json for
|
|
115
|
+
"description": "Deploy Android apps to nowrun so people can run them inside ChatGPT. "
|
|
116
|
+
"Sign in once with `nowrun login`. Every command supports --json for "
|
|
117
|
+
"machine-readable output.",
|
|
117
118
|
"workflow": WORKFLOW,
|
|
118
119
|
"commands": tree.get("subcommands", []),
|
|
119
120
|
"global_options": tree.get("args", []),
|
|
@@ -248,7 +249,7 @@ def build_parser():
|
|
|
248
249
|
'flags override it. In admin mode this creates a nowrun-managed app.')
|
|
249
250
|
app_create_parser.add_argument('-t', '--title', type=str, help='App title (required)')
|
|
250
251
|
app_create_parser.add_argument('--type', dest='game_type', choices=['android', 'h5'],
|
|
251
|
-
help='App type
|
|
252
|
+
help='App type (default: android)')
|
|
252
253
|
app_create_parser.add_argument('-p', '--package', dest='app_pkg_name', type=str,
|
|
253
254
|
help='Android package name (required for --type android; or use --from-apk)')
|
|
254
255
|
app_create_parser.add_argument('-f', '--from-apk', dest='from_apk', type=str,
|
|
@@ -279,24 +280,29 @@ def build_parser():
|
|
|
279
280
|
app_delete_parser.add_argument('-y', '--yes', action='store_true', help='Skip the confirmation prompt (for scripts/agents)')
|
|
280
281
|
|
|
281
282
|
# deploy
|
|
282
|
-
deploy_parser = subparsers.add_parser('deploy', help='Deploy a build
|
|
283
|
+
deploy_parser = subparsers.add_parser('deploy', help='Deploy a build (runnable in ChatGPT once live)', formatter_class=fmt)
|
|
283
284
|
deploy_parser.add_argument('-a', '--app-id', '--app_id', dest='app_id', type=str, help="The app's app_id from `app create` / `app list` (default: workspace app)", required=False)
|
|
284
285
|
deploy_parser.add_argument('-f', '--file_path', type=str,
|
|
285
|
-
help='Path to the build: an .apk
|
|
286
|
+
help='Path to the build: an .apk or .zip for Android',
|
|
286
287
|
required=True)
|
|
287
288
|
deploy_parser.add_argument('-av', '--apk_version', type=str,
|
|
288
289
|
help='Version name (Android; auto-read from the .apk if omitted)', required=False)
|
|
289
290
|
deploy_parser.add_argument('-vc', '--version_code', type=int,
|
|
290
291
|
help='Version code (Android; auto-read from the .apk if omitted)', required=False)
|
|
291
292
|
deploy_parser.add_argument('--wait', action='store_true',
|
|
292
|
-
help='Block until the deployment is live (or fails)
|
|
293
|
+
help='Block until the deployment is live (or fails)')
|
|
293
294
|
deploy_parser.add_argument('--timeout', type=int, default=900,
|
|
294
295
|
help='With --wait, give up after this many seconds (default: 900)')
|
|
295
296
|
|
|
296
297
|
# status
|
|
297
|
-
status_parser = subparsers.add_parser('status', help='
|
|
298
|
+
status_parser = subparsers.add_parser('status', help='Show what is live and in progress, or one submission with -s', formatter_class=fmt)
|
|
298
299
|
status_parser.add_argument('-a', '--app-id', '--app_id', dest='app_id', type=str, help="The app's app_id from `app create` / `app list` (default: workspace app)", required=False)
|
|
299
|
-
status_parser.add_argument('-s', '--submission_id', type=str, help='
|
|
300
|
+
status_parser.add_argument('-s', '--submission_id', type=str, help='A specific submission (from deploy); omit for the deployment overview', required=False)
|
|
301
|
+
|
|
302
|
+
# cancel
|
|
303
|
+
cancel_parser = subparsers.add_parser('cancel', help='Cancel the deployment in progress for an app', formatter_class=fmt)
|
|
304
|
+
cancel_parser.add_argument('-a', '--app-id', '--app_id', dest='app_id', type=str, help="The app's app_id from `app create` / `app list` (default: workspace app)", required=False)
|
|
305
|
+
cancel_parser.add_argument('-s', '--submission_id', type=str, help='A specific in-progress submission to cancel (default: the app\'s current one)', required=False)
|
|
300
306
|
|
|
301
307
|
# validate (publish-readiness)
|
|
302
308
|
validate_parser = subparsers.add_parser('validate', help='Check publish-readiness of an app', formatter_class=fmt)
|
|
@@ -373,7 +379,7 @@ def _admin_parsers(subparsers):
|
|
|
373
379
|
'set-private': 'Remove apps from the catalog listing',
|
|
374
380
|
'disable': 'Disable apps: out of the catalog, read-only, no deploys (asks to confirm)',
|
|
375
381
|
'enable': 'Re-enable disabled apps',
|
|
376
|
-
'unpublish': 'Take the live deployment down (private
|
|
382
|
+
'unpublish': 'Take the live deployment down (private; removed from ChatGPT; asks to confirm)',
|
|
377
383
|
'remove': 'Remove any apps: out of every list and the catalog, package freed, record kept (asks to confirm)',
|
|
378
384
|
'delete': 'PERMANENTLY erase nowrun-managed apps and their builds - cannot be undone (asks to confirm)',
|
|
379
385
|
}
|
|
@@ -384,14 +390,16 @@ def _admin_parsers(subparsers):
|
|
|
384
390
|
p.add_argument('-y', '--yes', action='store_true', help='Skip the confirmation prompt')
|
|
385
391
|
|
|
386
392
|
fetch = apps_sub.add_parser(
|
|
387
|
-
'fetch-apk', help='Download
|
|
393
|
+
'fetch-apk', help='Download a build from a link to this machine and report it (does not deploy)',
|
|
388
394
|
formatter_class=fmt,
|
|
389
|
-
description="Download an APK from a link
|
|
390
|
-
"version, size, SHA-256)
|
|
391
|
-
"
|
|
395
|
+
description="Download an APK/ZIP from a link onto this machine, read its metadata "
|
|
396
|
+
"locally (package, version, size, SHA-256), and report it. The download and, "
|
|
397
|
+
"on --deploy, the upload run from here, not through nowrun's servers. The "
|
|
398
|
+
"build is an untrusted third-party one: it is not deployed until you review "
|
|
399
|
+
"it and pass --deploy, which deploys it via the normal upload flow.")
|
|
392
400
|
fetch.add_argument('-a', '--app-id', '--app_id', dest='app_id', type=str,
|
|
393
401
|
help="The app to provision (default: workspace app)", required=False)
|
|
394
|
-
fetch.add_argument('--url', type=str, required=True, help='Direct http(s) link to the .apk')
|
|
402
|
+
fetch.add_argument('--url', type=str, required=True, help='Direct http(s) link to the .apk or .zip')
|
|
395
403
|
fetch.add_argument('--expected-package', dest='expected_package', type=str,
|
|
396
404
|
help='Fail unless the fetched build has this package name')
|
|
397
405
|
fetch.add_argument('--deploy', action='store_true',
|
|
@@ -518,8 +526,8 @@ def main():
|
|
|
518
526
|
"publisher tokens are no longer used - ignoring -t/--token and signing in with "
|
|
519
527
|
"`nowrun login` (browser). Update this command to `nowrun login`.")
|
|
520
528
|
# login is strict about the target environment: with --host, use it; WITHOUT --host,
|
|
521
|
-
# always reset to prod - never inherit a host left over from an earlier
|
|
522
|
-
# (which would silently sign you into the wrong environment).
|
|
529
|
+
# always reset to prod - never inherit a host left over from an earlier
|
|
530
|
+
# `--host https://qa.nowrun.io` (which would silently sign you into the wrong environment).
|
|
523
531
|
if getattr(args, 'host', None):
|
|
524
532
|
config.set_host(config.resolve_host(args.host))
|
|
525
533
|
else:
|
|
@@ -598,7 +606,14 @@ def main():
|
|
|
598
606
|
elif args.command == 'status':
|
|
599
607
|
app_id = resolve_app_id(args)
|
|
600
608
|
if app_id:
|
|
601
|
-
|
|
609
|
+
if args.submission_id:
|
|
610
|
+
deploy_cmd.status(app_id, args.submission_id)
|
|
611
|
+
else:
|
|
612
|
+
deploy_cmd.deployment_overview(app_id)
|
|
613
|
+
elif args.command == 'cancel':
|
|
614
|
+
app_id = resolve_app_id(args)
|
|
615
|
+
if app_id:
|
|
616
|
+
deploy_cmd.cancel(app_id, args.submission_id)
|
|
602
617
|
elif args.command == 'validate':
|
|
603
618
|
app_id = resolve_app_id(args)
|
|
604
619
|
if app_id:
|
|
@@ -6,8 +6,15 @@ suspend|reactivate; audit. All under /v2/admin/*, which the server refuses outsi
|
|
|
6
6
|
is given before any request. Every change is audited server-side, one entry per item.
|
|
7
7
|
"""
|
|
8
8
|
import datetime
|
|
9
|
+
import hashlib
|
|
10
|
+
import os
|
|
9
11
|
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import urllib.parse
|
|
10
14
|
|
|
15
|
+
import requests
|
|
16
|
+
|
|
17
|
+
from nowrun import apk
|
|
11
18
|
from nowrun import config
|
|
12
19
|
from nowrun import fields as fieldmap
|
|
13
20
|
from nowrun import output
|
|
@@ -60,10 +67,6 @@ def require_admin_mode():
|
|
|
60
67
|
return False
|
|
61
68
|
|
|
62
69
|
|
|
63
|
-
def _error_code(body):
|
|
64
|
-
return (body.get('error_code') or body.get('code')) if isinstance(body, dict) else None
|
|
65
|
-
|
|
66
|
-
|
|
67
70
|
def _bool_param(value):
|
|
68
71
|
return None if value is None else ('true' if value else 'false')
|
|
69
72
|
|
|
@@ -257,62 +260,136 @@ def apps_action(command, app_ids, reason=None, assume_yes=False):
|
|
|
257
260
|
_bulk('/v2/admin/apps/bulk', 'app_ids', app_ids, action, reason, 'app')
|
|
258
261
|
|
|
259
262
|
|
|
260
|
-
# --- apk sourcing (
|
|
263
|
+
# --- apk sourcing (download a build from a link and deploy it from this machine) -------
|
|
264
|
+
|
|
265
|
+
# Streamed and capped so a bad link cannot fill the disk; matches the size the deploy
|
|
266
|
+
# pipeline accepts.
|
|
267
|
+
_MAX_APK_BYTES = 400 * 1024 * 1024
|
|
268
|
+
_DOWNLOAD_TIMEOUT = 120
|
|
269
|
+
_TOO_LARGE = "That build is larger than nowrun allows (400 MB)."
|
|
270
|
+
|
|
261
271
|
|
|
262
272
|
def fetch_apk(app_id, url, expected_package=None, deploy=False, assume_yes=False,
|
|
263
273
|
wait=False, timeout=900):
|
|
264
|
-
"""Download an APK from a link
|
|
265
|
-
|
|
266
|
-
|
|
274
|
+
"""Download an APK/ZIP from a link ON THIS MACHINE, read its metadata locally, and - with
|
|
275
|
+
--deploy - deploy it through the normal upload flow (the same path as `nowrun deploy -f`).
|
|
276
|
+
|
|
277
|
+
The build is fetched and uploaded from here, not routed through nowrun's servers: a local
|
|
278
|
+
machine is faster for the download + upload, and the metadata is read locally rather than by
|
|
279
|
+
the backend."""
|
|
267
280
|
if not require_admin_mode():
|
|
268
281
|
return
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
ok, resp, status = StudioClient().request('POST', '/v2/admin/apps/fetch-apk', json_body=body)
|
|
273
|
-
if not ok:
|
|
274
|
-
_fetch_apk_error(resp, status)
|
|
275
|
-
return
|
|
276
|
-
data = resp.get('data') or {}
|
|
277
|
-
if deploy and not data.get('package_matches', True):
|
|
278
|
-
# A build whose package doesn't match would deploy to the wrong app.
|
|
279
|
-
emit_error(f"The fetched build's package '{data.get('package')}' does not match this app.",
|
|
280
|
-
code='PACKAGE_MISMATCH',
|
|
281
|
-
hint="Fetch the right build, or omit --deploy to stage and review it first.")
|
|
282
|
-
return
|
|
283
|
-
if not deploy:
|
|
284
|
-
emit_success({**data, "staged": True, "deployed": False}, human=_print_fetched)
|
|
282
|
+
if urllib.parse.urlparse(url).scheme not in ("http", "https"):
|
|
283
|
+
emit_error("The build link must be an http(s) URL.", code="INVALID_URL",
|
|
284
|
+
hint="Pass a direct link to an .apk or .zip.")
|
|
285
285
|
return
|
|
286
286
|
|
|
287
|
-
if not _confirm_fetch_deploy(app_id, data, assume_yes):
|
|
288
|
-
return
|
|
289
|
-
from nowrun.commands import deploy as deploy_cmd
|
|
290
287
|
if not output.is_json():
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
288
|
+
logger.info(color("Downloading the build to this machine…", 'dim'))
|
|
289
|
+
path, err = _download_build(url)
|
|
290
|
+
if err:
|
|
291
|
+
emit_error(err[0], code=err[1], hint=err[2])
|
|
292
|
+
return
|
|
293
|
+
try:
|
|
294
|
+
meta = apk.inspect(path) # local read: package, version_name, version_code
|
|
295
|
+
package = meta.get("package")
|
|
296
|
+
if expected_package and package and package != expected_package:
|
|
297
|
+
emit_error(f"The build is '{package}', not '{expected_package}'.",
|
|
298
|
+
code="PACKAGE_MISMATCH",
|
|
299
|
+
hint="Fetch the build whose package matches, or drop --expected-package.")
|
|
300
|
+
return
|
|
301
|
+
data = {"package": package, "version_name": meta.get("version_name"),
|
|
302
|
+
"version_code": meta.get("version_code"), "size": os.path.getsize(path),
|
|
303
|
+
"sha256": _sha256_file(path), "deployed": bool(deploy)}
|
|
304
|
+
if not deploy:
|
|
305
|
+
emit_success(data, human=_print_fetched)
|
|
306
|
+
return
|
|
307
|
+
if not _confirm_fetch_deploy(app_id, data, assume_yes):
|
|
308
|
+
return
|
|
309
|
+
from nowrun.commands import deploy as deploy_cmd
|
|
310
|
+
if not output.is_json():
|
|
311
|
+
_print_fetched(data)
|
|
312
|
+
# Deploy via the normal local upload path (multipart upload + trigger), exactly like
|
|
313
|
+
# `nowrun deploy -f`: it re-reads the package/version from the file and refuses a build
|
|
314
|
+
# whose package does not match the bound app.
|
|
315
|
+
deploy_cmd.deploy(app_id, path, meta.get("version_name"), meta.get("version_code"),
|
|
316
|
+
wait=wait, timeout=timeout)
|
|
317
|
+
finally:
|
|
318
|
+
_safe_remove(path)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _download_build(url):
|
|
322
|
+
"""Stream a build to a temp file on this machine. Returns (path, None) on success, or
|
|
323
|
+
(None, (message, code, hint)) on failure."""
|
|
324
|
+
# Keep the extension so apk.inspect() and deploy() route .zip vs .apk correctly.
|
|
325
|
+
suffix = ".zip" if urllib.parse.urlparse(url).path.lower().endswith(".zip") else ".apk"
|
|
326
|
+
tmp = tempfile.NamedTemporaryFile(prefix="nowrun-fetch-", suffix=suffix, delete=False)
|
|
327
|
+
path = tmp.name
|
|
328
|
+
total = 0
|
|
329
|
+
try:
|
|
330
|
+
with requests.get(url, stream=True, timeout=_DOWNLOAD_TIMEOUT,
|
|
331
|
+
allow_redirects=True) as resp:
|
|
332
|
+
if resp.status_code != 200:
|
|
333
|
+
tmp.close(); _safe_remove(path)
|
|
334
|
+
return None, (f"The link returned HTTP {resp.status_code}.", "FETCH_FAILED",
|
|
335
|
+
"Check the link and retry.")
|
|
336
|
+
declared = int(resp.headers.get("content-length") or 0)
|
|
337
|
+
if declared and declared > _MAX_APK_BYTES:
|
|
338
|
+
tmp.close(); _safe_remove(path)
|
|
339
|
+
return None, (_TOO_LARGE, "APK_TOO_LARGE", None)
|
|
340
|
+
for chunk in resp.iter_content(chunk_size=256 * 1024):
|
|
341
|
+
if not chunk:
|
|
342
|
+
continue
|
|
343
|
+
total += len(chunk)
|
|
344
|
+
if total > _MAX_APK_BYTES:
|
|
345
|
+
tmp.close(); _safe_remove(path)
|
|
346
|
+
return None, (_TOO_LARGE, "APK_TOO_LARGE", None)
|
|
347
|
+
tmp.write(chunk)
|
|
348
|
+
tmp.close()
|
|
349
|
+
return path, None
|
|
350
|
+
except requests.Timeout:
|
|
351
|
+
tmp.close(); _safe_remove(path)
|
|
352
|
+
return None, ("The download timed out.", "FETCH_TIMEOUT", "Try a faster or direct link.")
|
|
353
|
+
except requests.RequestException as exc:
|
|
354
|
+
tmp.close(); _safe_remove(path)
|
|
355
|
+
return None, (f"Could not download the build: {exc}", "FETCH_FAILED",
|
|
356
|
+
"Check the link and retry.")
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _sha256_file(path):
|
|
360
|
+
h = hashlib.sha256()
|
|
361
|
+
with open(path, "rb") as f:
|
|
362
|
+
for block in iter(lambda: f.read(1024 * 1024), b""):
|
|
363
|
+
h.update(block)
|
|
364
|
+
return h.hexdigest()
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _safe_remove(path):
|
|
368
|
+
try:
|
|
369
|
+
os.remove(path)
|
|
370
|
+
except OSError:
|
|
371
|
+
pass
|
|
295
372
|
|
|
296
373
|
|
|
297
374
|
def _print_fetched(d):
|
|
298
|
-
logger.info(color("
|
|
299
|
-
|
|
300
|
-
|
|
375
|
+
logger.info(color("Downloaded an unreviewed third-party build to this machine. "
|
|
376
|
+
"Review it before deploying.", 'yellow'))
|
|
377
|
+
logger.info(f"Package : {d.get('package', '')}")
|
|
301
378
|
logger.info(f"Version : {d.get('version_name', '')} (code {d.get('version_code', '')})")
|
|
302
379
|
size = d.get('size')
|
|
303
380
|
if size:
|
|
304
381
|
logger.info(f"Size : {round(int(size) / (1024 * 1024), 2)} MB")
|
|
305
382
|
logger.info(f"SHA-256 : {d.get('sha256', '')}")
|
|
306
|
-
if not d.get('deployed', True)
|
|
307
|
-
logger.info(color("
|
|
308
|
-
"
|
|
383
|
+
if not d.get('deployed', True):
|
|
384
|
+
logger.info(color("Not deployed. Deploy it with: nowrun admin apps fetch-apk "
|
|
385
|
+
"-a <app_id> --url <same url> --deploy", 'dim'))
|
|
309
386
|
|
|
310
387
|
|
|
311
388
|
def _confirm_fetch_deploy(app_id, data, assume_yes):
|
|
312
389
|
if assume_yes:
|
|
313
390
|
return True
|
|
314
391
|
if sys.stdin.isatty() and not output.is_json():
|
|
315
|
-
logger.info(color(f"[ADMIN MODE] Deploy the
|
|
392
|
+
logger.info(color(f"[ADMIN MODE] Deploy the downloaded build '{data.get('package')}' "
|
|
316
393
|
f"v{data.get('version_name')} to {app_id}? It is an unreviewed "
|
|
317
394
|
"third-party build. This is audited.", 'yellow'))
|
|
318
395
|
try:
|
|
@@ -321,42 +398,14 @@ def _confirm_fetch_deploy(app_id, data, assume_yes):
|
|
|
321
398
|
resp = ''
|
|
322
399
|
if resp.strip().lower() == 'yes':
|
|
323
400
|
return True
|
|
324
|
-
emit_error("Aborted - the build
|
|
401
|
+
emit_error("Aborted - the build was downloaded but not deployed.", code='ABORTED')
|
|
325
402
|
return False
|
|
326
403
|
emit_error(f"Refusing to deploy an unreviewed build to {app_id} without confirmation.",
|
|
327
404
|
code='CONFIRMATION_REQUIRED',
|
|
328
|
-
hint="Re-run with --yes to confirm, or omit --deploy to
|
|
405
|
+
hint="Re-run with --yes to confirm, or omit --deploy to download and review first.")
|
|
329
406
|
return False
|
|
330
407
|
|
|
331
408
|
|
|
332
|
-
_FETCH_APK_GUIDANCE = {
|
|
333
|
-
'NOT_AN_APK': "The link did not point to a valid Android APK. Use a direct link to an .apk file.",
|
|
334
|
-
'INVALID_URL': "The URL is not valid. Pass a direct http(s) link to the build.",
|
|
335
|
-
'FETCH_FAILED': "nowrun couldn't download the build from that link. Check the link and retry.",
|
|
336
|
-
'FETCH_TIMEOUT': "The download timed out. Try a faster mirror or a direct link.",
|
|
337
|
-
'APK_TOO_LARGE': "The build is larger than nowrun allows. Use a smaller build.",
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
def _fetch_apk_error(body, status):
|
|
342
|
-
code = _error_code(body)
|
|
343
|
-
if code == 'PACKAGE_MISMATCH':
|
|
344
|
-
fetched = ((body.get('details') or {}).get('fetched_package')
|
|
345
|
-
if isinstance(body, dict) else None)
|
|
346
|
-
got = f" (got '{fetched}')" if fetched else ""
|
|
347
|
-
emit_error(f"The fetched build's package doesn't match --expected-package{got}.",
|
|
348
|
-
http_status=status, code=code,
|
|
349
|
-
hint="Fetch the build whose package matches, or drop --expected-package to "
|
|
350
|
-
"stage it anyway and review.")
|
|
351
|
-
return
|
|
352
|
-
hint = _FETCH_APK_GUIDANCE.get(code)
|
|
353
|
-
if hint:
|
|
354
|
-
message = body.get('message') if isinstance(body, dict) else None
|
|
355
|
-
emit_error(message or hint, http_status=status, code=code, hint=hint)
|
|
356
|
-
return
|
|
357
|
-
api_error(body, status, fallback='Failed to fetch the APK')
|
|
358
|
-
|
|
359
|
-
|
|
360
409
|
# --- users ----------------------------------------------------------------------------
|
|
361
410
|
|
|
362
411
|
def users_list(email=None, status=None, created_from=None, created_to=None, limit=None,
|
|
@@ -146,29 +146,6 @@ def trigger_deploy(app_id, upload_url, upload_type, version=None, version_code=N
|
|
|
146
146
|
})
|
|
147
147
|
|
|
148
148
|
|
|
149
|
-
def deploy_staged(app_id, upload_url, upload_type, version=None, version_code=None, wait=False,
|
|
150
|
-
timeout=900):
|
|
151
|
-
"""Deploy a build that was already staged into the bucket (e.g. by fetch-apk), then report
|
|
152
|
-
the submission the same way `deploy` does. Emits its own error on failure."""
|
|
153
|
-
ok, body, status = trigger_deploy(app_id, upload_url, upload_type, version, version_code)
|
|
154
|
-
if not ok:
|
|
155
|
-
api_error(body, status, fallback='Deployment failed')
|
|
156
|
-
return
|
|
157
|
-
submission = body.get('data', {}) or {}
|
|
158
|
-
submission_id = submission.get('id')
|
|
159
|
-
if wait and submission_id:
|
|
160
|
-
_wait_until_done(app_id, submission_id, timeout, [])
|
|
161
|
-
return
|
|
162
|
-
result = {"submission_id": submission_id,
|
|
163
|
-
"submission_state": submission.get('submission_state'),
|
|
164
|
-
"job_status": submission.get('job_status')}
|
|
165
|
-
|
|
166
|
-
def _triggered(d):
|
|
167
|
-
logger.info(f"Deploy started for {scope_label()} {app_id} (submission {d['submission_id']}).")
|
|
168
|
-
logger.info(color(f" Check back: nowrun status -a {app_id} -s {d['submission_id']}", 'dim'))
|
|
169
|
-
emit_success(result, human=_triggered)
|
|
170
|
-
|
|
171
|
-
|
|
172
149
|
def _fetch_status(app_id, submission_id):
|
|
173
150
|
return StudioClient().request(
|
|
174
151
|
'GET', '/v2/console/app/jenkins_job_status',
|
|
@@ -242,6 +219,73 @@ def _wait_until_done(app_id, submission_id, timeout, notes):
|
|
|
242
219
|
f"nowrun status -a {app_id} -s {submission_id}")
|
|
243
220
|
|
|
244
221
|
|
|
222
|
+
def cancel(app_id, submission_id=None):
|
|
223
|
+
"""Cancel the deployment in progress for an app so another can be started. Optional
|
|
224
|
+
submission_id targets a specific build; otherwise the app's current in-progress one."""
|
|
225
|
+
params = {'app_id': app_id}
|
|
226
|
+
if submission_id:
|
|
227
|
+
params['submission_id'] = submission_id
|
|
228
|
+
ok, body, http_status = StudioClient().request(
|
|
229
|
+
'POST', '/v2/console/app/deploy/cancel', params=params)
|
|
230
|
+
if not ok:
|
|
231
|
+
api_error(body, http_status, fallback='Failed to cancel the deployment')
|
|
232
|
+
return
|
|
233
|
+
data = body.get('data', {}) or {}
|
|
234
|
+
result = {"submission_id": data.get('id'), "job_status": data.get('job_status')}
|
|
235
|
+
|
|
236
|
+
def _done(d):
|
|
237
|
+
logger.info(color(f"✓ Cancelled the deployment for {scope_label()} {app_id}.", 'yellow'))
|
|
238
|
+
logger.info(color(" Start a new one when ready: nowrun deploy -f <build>", 'dim'))
|
|
239
|
+
emit_success(result, human=_done)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def deployment_overview(app_id):
|
|
243
|
+
"""Show what is live and what is building for an app: the live version, any in-progress
|
|
244
|
+
build, and recent deployment history - the same view as the dashboard's deploy panel."""
|
|
245
|
+
ok, body, http_status = StudioClient().request(
|
|
246
|
+
'GET', '/v2/console/app/deploy/status', params={'app_id': app_id})
|
|
247
|
+
if not ok:
|
|
248
|
+
api_error(body, http_status, fallback='Failed to fetch deployment status')
|
|
249
|
+
return
|
|
250
|
+
data = body.get('data', {}) or {}
|
|
251
|
+
|
|
252
|
+
def _print(d):
|
|
253
|
+
live = d.get('live') or {}
|
|
254
|
+
active = d.get('active')
|
|
255
|
+
recent = d.get('recent') or []
|
|
256
|
+
# Live
|
|
257
|
+
if live.get('url'):
|
|
258
|
+
vc = live.get('version_code')
|
|
259
|
+
head = f"● Live: v{vc}" if vc is not None else "● Live"
|
|
260
|
+
logger.info(color(head, 'green'))
|
|
261
|
+
logger.info(f" {live.get('url')}")
|
|
262
|
+
if live.get('published_at'):
|
|
263
|
+
logger.info(color(f" since {live.get('published_at')}", 'dim'))
|
|
264
|
+
else:
|
|
265
|
+
logger.info(color("● Live: not deployed yet", 'dim'))
|
|
266
|
+
# In progress
|
|
267
|
+
if active:
|
|
268
|
+
vc = active.get('app_version_code')
|
|
269
|
+
ver = f"v{vc}" if vc is not None else (active.get('app_version') or '?')
|
|
270
|
+
logger.info(color(f"⠿ In progress: {ver} ({active.get('job_status')})", 'cyan'))
|
|
271
|
+
logger.info(color(
|
|
272
|
+
f" submission {active.get('submission_id')}"
|
|
273
|
+
+ (f" · started {active.get('triggered_at')}" if active.get('triggered_at') else ""),
|
|
274
|
+
'dim'))
|
|
275
|
+
logger.info(color(f" Cancel: nowrun cancel -a {app_id}", 'dim'))
|
|
276
|
+
# Recent history
|
|
277
|
+
if recent:
|
|
278
|
+
glyphs = {'success': '✓', 'failed': '✗', 'cancelled': '∅', 'progress': '⠿'}
|
|
279
|
+
logger.info("Recent:")
|
|
280
|
+
for s in recent[:5]:
|
|
281
|
+
vc = s.get('app_version_code')
|
|
282
|
+
ver = f"v{vc}" if vc is not None else (s.get('app_version') or '-')
|
|
283
|
+
glyph = glyphs.get(s.get('job_status'), '·')
|
|
284
|
+
created = s.get('created_at') or ''
|
|
285
|
+
logger.info(f" {glyph} {ver:<8} {str(s.get('job_status') or ''):<9} {created}")
|
|
286
|
+
emit_success(data, human=_print)
|
|
287
|
+
|
|
288
|
+
|
|
245
289
|
def status(app_id, submission_id):
|
|
246
290
|
"""Fetch the test-link / cloud build job status for a submission."""
|
|
247
291
|
ok, body, http_status = _fetch_status(app_id, submission_id)
|
|
@@ -18,8 +18,7 @@ def inspect(path):
|
|
|
18
18
|
if not meta or not meta.get("package"):
|
|
19
19
|
emit_error(
|
|
20
20
|
"Could not read Android metadata from this file.",
|
|
21
|
-
hint="Pass an .apk, or an Android .zip that contains one.
|
|
22
|
-
"bundles have no Android manifest, so there's nothing to read.")
|
|
21
|
+
hint="Pass an .apk, or an Android .zip that contains one.")
|
|
23
22
|
return
|
|
24
23
|
emit_success(meta, human=_print_inspect)
|
|
25
24
|
|
|
@@ -29,27 +29,17 @@ WORKSPACE_FILE = 'nowrun.json' # per-project app binding (stays in the cwd)
|
|
|
29
29
|
|
|
30
30
|
# nowrun-backend topology: the API is served at DOMAIN/api, the dashboard at DOMAIN/console.
|
|
31
31
|
PROD_HOST = 'https://nowrun.io/api'
|
|
32
|
-
QA_HOST = 'https://qa.nowrun.io/api'
|
|
33
|
-
DEV_HOST = 'https://dev.nowrun.io/api'
|
|
34
32
|
LOCAL_HOST = 'http://localhost:3000/api'
|
|
35
33
|
|
|
36
34
|
# Default to prod. Point at another environment with the internal `--host` flag
|
|
37
|
-
# (`nowrun login --host qa`), which
|
|
35
|
+
# (`nowrun login --host https://qa.nowrun.io`), which takes a full URL or a bare host -
|
|
36
|
+
# there are no named shorthands. See resolve_host.
|
|
38
37
|
DEFAULT_HOST = PROD_HOST
|
|
39
38
|
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
'prod': PROD_HOST,
|
|
44
|
-
'qa': QA_HOST,
|
|
45
|
-
'qa1': QA_HOST,
|
|
46
|
-
'dev': DEV_HOST,
|
|
47
|
-
'local': LOCAL_HOST,
|
|
48
|
-
}
|
|
39
|
+
# API host -> console (dashboard) URL where `nowrun login` opens the browser. Only the
|
|
40
|
+
# exceptions live here: local serves its dashboard on a different port. Every other
|
|
41
|
+
# host's console is DOMAIN/console, derived in console_url().
|
|
49
42
|
_CONSOLE_URLS = {
|
|
50
|
-
PROD_HOST: 'https://nowrun.io/console',
|
|
51
|
-
QA_HOST: 'https://qa.nowrun.io/console',
|
|
52
|
-
DEV_HOST: 'https://dev.nowrun.io/console',
|
|
53
43
|
LOCAL_HOST: 'http://localhost:3002',
|
|
54
44
|
}
|
|
55
45
|
|
|
@@ -86,12 +76,26 @@ def ui_host():
|
|
|
86
76
|
|
|
87
77
|
|
|
88
78
|
def resolve_host(value):
|
|
89
|
-
"""Map a --host value to a full API host URL.
|
|
90
|
-
(
|
|
79
|
+
"""Map a --host value to a full API host URL. Takes a full URL
|
|
80
|
+
("https://qa.nowrun.io") or a bare host ("qa.nowrun.io", "localhost:3000") -
|
|
81
|
+
there are no named shorthands. Returns None for an empty value.
|
|
82
|
+
|
|
83
|
+
The API lives at DOMAIN/api and the client appends API paths (/v2/...) to the
|
|
84
|
+
host verbatim, so the resolved host MUST end in /api - otherwise every call
|
|
85
|
+
hits the wrong path and auth looks broken. So we normalise: add a scheme for a
|
|
86
|
+
bare host (http for localhost, else https), then the /api suffix if missing.
|
|
87
|
+
"""
|
|
91
88
|
if not value:
|
|
92
89
|
return None
|
|
93
|
-
v = value.strip()
|
|
94
|
-
|
|
90
|
+
v = value.strip().rstrip('/')
|
|
91
|
+
if not v:
|
|
92
|
+
return None
|
|
93
|
+
if not (v.startswith('http://') or v.startswith('https://')):
|
|
94
|
+
scheme = 'http' if v.split(':', 1)[0] in ('localhost', '127.0.0.1') else 'https'
|
|
95
|
+
v = f'{scheme}://{v}'
|
|
96
|
+
if not v.endswith('/api'):
|
|
97
|
+
v = f'{v}/api'
|
|
98
|
+
return v
|
|
95
99
|
|
|
96
100
|
|
|
97
101
|
def _cache_obj():
|
|
@@ -301,6 +301,12 @@ _CODE_GUIDANCE = {
|
|
|
301
301
|
'BULK_PARTIAL_FAILURE': (
|
|
302
302
|
"Some items in the batch failed.",
|
|
303
303
|
"See each item's error_code/message in `detail.results`; fix those and retry only the failed ids."),
|
|
304
|
+
'DEPLOY_IN_PROGRESS': (
|
|
305
|
+
"A deployment is already in progress for this app, so another can't start yet.",
|
|
306
|
+
"Watch it with `nowrun status`; to replace it, cancel first with `nowrun cancel`, then deploy again."),
|
|
307
|
+
'NO_DEPLOY_IN_PROGRESS': (
|
|
308
|
+
"There's no deployment in progress for this app to cancel.",
|
|
309
|
+
"Check the current state with `nowrun status`."),
|
|
304
310
|
}
|
|
305
311
|
|
|
306
312
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nowrun-publishing
|
|
3
|
-
description: Deploy and manage Android
|
|
3
|
+
description: Deploy and manage Android apps on nowrun using the `nowrun` CLI. Use whenever the user wants to create an app, upload a build, deploy it so people can run it in ChatGPT, check status/readiness, or update its listing metadata.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Publishing to nowrun with the `nowrun` CLI
|
|
7
7
|
|
|
8
|
-
The `nowrun` CLI deploys **Android** apps (compiled `.apk
|
|
9
|
-
|
|
10
|
-
(browser); you operate the CLI, and the
|
|
11
|
-
in the dashboard.
|
|
8
|
+
The `nowrun` CLI deploys **Android** apps (a compiled `.apk`, or an `.apk` inside a `.zip`)
|
|
9
|
+
to nowrun, so people can run them inside ChatGPT (and other AI assistants that connect to
|
|
10
|
+
nowrun). The human signs in once with `nowrun login` (browser); you operate the CLI, and the
|
|
11
|
+
human handles account setup and app configuration in the dashboard.
|
|
12
12
|
|
|
13
13
|
## First: discover the tool (don't guess commands)
|
|
14
14
|
|
|
@@ -85,7 +85,9 @@ Follow this sequence end to end; each step is expanded in the sections below.
|
|
|
85
85
|
the flag that sets it). For Android, also verify the binary (package matches,
|
|
86
86
|
release-signed) - see "Android builds" below.
|
|
87
87
|
5. **Deploy** - `nowrun deploy -f <build> [-av <name> -vc <code>]`, then poll
|
|
88
|
-
`nowrun status -s <submission_id>` until it
|
|
88
|
+
`nowrun status -s <submission_id>` until it goes live (or pass `--wait`). Once live, the
|
|
89
|
+
app is runnable in ChatGPT. `nowrun status` (no `-s`) shows what's live and what's building;
|
|
90
|
+
`nowrun cancel` stops a build in progress so you can start another.
|
|
89
91
|
6. **Delete (end of life)** - `nowrun app delete -a <app_id> --yes` permanently removes the
|
|
90
92
|
app and frees a plan app slot. Destructive and irreversible: only on an explicit user
|
|
91
93
|
request, and confirm the exact `app_id` first.
|
|
@@ -104,12 +106,9 @@ nowrun app list --json # otherwise find an existing app whose app_pkg_name m
|
|
|
104
106
|
`nowrun use <app_id>` (writes nowrun.json).
|
|
105
107
|
- **No match:** create it with all its metadata (see "App metadata" below); this
|
|
106
108
|
**auto-binds** (writes nowrun.json):
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
- **HTML5:** `nowrun app create --title "My Game" --type h5 ...` - no package; it is
|
|
111
|
-
synthesized. (HTML5 apps won't appear in `app list` by package, so reuse the `app_id`
|
|
112
|
-
from nowrun.json / `app list` rather than matching a package name.)
|
|
109
|
+
`nowrun app create --title "My App" -p com.foo.bar ...` (or `--from-apk app.apk` to read
|
|
110
|
+
the package name - and, without `--icon`, the launcher icon - straight from the build
|
|
111
|
+
instead of `-p`).
|
|
113
112
|
|
|
114
113
|
After this, `nowrun.json` holds the app_id and `deploy`/`status`/`validate` default to it.
|
|
115
114
|
Never run `app create` for a package that already has an app.
|
|
@@ -176,25 +175,33 @@ human; only a nowrun admin can re-enable it.
|
|
|
176
175
|
out of an Android build. `validate` flags a missing icon; set one before deploying so the
|
|
177
176
|
live app looks complete. (`--from-apk` is best-effort - if the build has no resolvable
|
|
178
177
|
raster icon it tells you, and you provide an image instead.)
|
|
179
|
-
- **Deploy:**
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
URL itself (no polling loop needed).
|
|
178
|
+
- **Deploy:** `nowrun deploy -f app.apk` - the version name and code are read from the APK
|
|
179
|
+
automatically. Pass `-av <name> -vc <code>` only to override; if they disagree with the APK
|
|
180
|
+
the CLI warns you. First verify the binary (see "Android builds" below). The version code must
|
|
181
|
+
be higher than the currently live one (like the Play Store) or the deploy is rejected.
|
|
182
|
+
|
|
183
|
+
Uploads the build and deploys it, then poll `nowrun status -s <submission_id>` until it goes
|
|
184
|
+
live; once live, the app is runnable in ChatGPT. Add `--wait` to have `deploy` block until it
|
|
185
|
+
settles (no polling loop needed).
|
|
188
186
|
If deploy fails with error code `CARD_NOT_VERIFIED` (HTTP 402), the human hasn't
|
|
189
187
|
verified a payment card yet - tell them to add one in the nowrun dashboard (the error
|
|
190
188
|
message includes the link), then retry. This is required only once, before the first deploy.
|
|
189
|
+
- **See what's deployed / building:** `nowrun status` (no `-s`) shows the current live
|
|
190
|
+
version and URL, any build in progress, and recent deployment history for the app - the same
|
|
191
|
+
view as the dashboard's deploy panel. Use `nowrun status -s <submission_id>` to check one
|
|
192
|
+
specific submission.
|
|
193
|
+
- **Cancel a deployment:** `nowrun cancel` stops the build in progress for the app so you can
|
|
194
|
+
start a new one (there's only one deployment per app at a time). Only one build can run at a
|
|
195
|
+
time: if a deploy fails with `DEPLOY_IN_PROGRESS` (HTTP 429), either wait/poll `nowrun status`
|
|
196
|
+
or `nowrun cancel` first, then deploy again. `NO_DEPLOY_IN_PROGRESS` means nothing was running
|
|
197
|
+
to cancel. A cancelled build won't go live even if its cloud job later finishes.
|
|
191
198
|
|
|
192
199
|
## Android builds: verify the binary before you deploy
|
|
193
200
|
|
|
194
201
|
`nowrun validate` covers the app's title and icon, NOT the binary - so before an **Android**
|
|
195
202
|
deploy (`.apk` or `.zip`), check the actual file yourself. These checks use the Android SDK
|
|
196
203
|
build-tools (`aapt`/`aapt2`, `apksigner`); if they aren't installed, say so instead of
|
|
197
|
-
silently skipping.
|
|
204
|
+
silently skipping.
|
|
198
205
|
|
|
199
206
|
1. **Package name matches the bound app.** The APK's package must equal the app's package
|
|
200
207
|
name (`app_pkg_name` from `nowrun app list`, i.e. what you bound with `nowrun use`). A
|
|
@@ -244,16 +251,20 @@ Common cases:
|
|
|
244
251
|
- **`MODE_REQUIRED` / `ADMIN_MODE_REQUIRED` / `NOT_ADMIN`:** see "Personal vs admin mode".
|
|
245
252
|
- **`NETWORK_ERROR`:** nowrun was unreachable; retry. The session is still valid.
|
|
246
253
|
- **Not signed in (401, `SESSION_EXPIRED`):** the session is gone. Ask the human to run `nowrun login`.
|
|
247
|
-
-
|
|
254
|
+
- **`DEPLOY_IN_PROGRESS` (429):** a deploy is already running for this app. Poll `nowrun status`,
|
|
255
|
+
or `nowrun cancel` to stop it, then deploy again - don't just retry.
|
|
256
|
+
- **`NO_DEPLOY_IN_PROGRESS` (404, on cancel):** nothing was building, so there was nothing to cancel.
|
|
248
257
|
|
|
249
258
|
## Rules & gotchas
|
|
250
259
|
|
|
251
260
|
- **Deploy is async.** After `deploy`, poll `status`; `job_status: progress` means the
|
|
252
|
-
cloud build is running; `success`
|
|
253
|
-
- **`submitted` means "in review," not failed.** A 429
|
|
254
|
-
deployment is already running
|
|
255
|
-
- **There is one deployment per app.** `nowrun deploy` publishes
|
|
256
|
-
public link -
|
|
261
|
+
cloud build is running; `success` means it's live and runnable in ChatGPT.
|
|
262
|
+
- **`submitted` means "in review," not failed.** A 429 (`DEPLOY_IN_PROGRESS`) means a
|
|
263
|
+
deployment is already running: poll `nowrun status`, or `nowrun cancel` and redeploy - don't retry.
|
|
264
|
+
- **There is one deployment per app, one build at a time.** `nowrun deploy` publishes to a
|
|
265
|
+
single live public link - no separate tracks or environments to choose or promote to - and a
|
|
266
|
+
new build can't start (or upload) while one is in progress. `nowrun status` shows what's live
|
|
267
|
+
and what's building; `nowrun cancel` clears an in-progress build so you can deploy again.
|
|
257
268
|
- **`app delete` is permanent.** Only run it on an explicit user request, confirm the exact
|
|
258
269
|
`app_id`, and pass `--yes` when running non-interactively (it otherwise asks to confirm).
|
|
259
270
|
- **Admin tools are for nowrun admins only**, in admin mode, and only when the human asks:
|
|
@@ -267,8 +278,13 @@ Common cases:
|
|
|
267
278
|
exact ids (for `delete`, that they understand it can't be undone).
|
|
268
279
|
- **Provisioning an app from a build link (nowrun admins, admin mode):**
|
|
269
280
|
`nowrun admin apps fetch-apk -a <app_id> --url <link> [--expected-package <pkg>]` downloads
|
|
270
|
-
the
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
281
|
+
the build **onto this machine**, reads its package/version/size/SHA-256 **locally**, and
|
|
282
|
+
reports them - it does NOT deploy. (The download and, on deploy, the upload happen from here,
|
|
283
|
+
not through nowrun's servers - a local machine is faster and metadata is read locally.) The
|
|
284
|
+
link may be a `.apk` or a wrapper `.zip` that contains the APK (e.g. a now.gg apps-content
|
|
285
|
+
package); the version is read from the base APK either way. These are untrusted third-party
|
|
286
|
+
builds: review the reported package/version, then add `--deploy` (which confirms first unless
|
|
287
|
+
`--yes`) to deploy it through the normal upload flow - the same path as `nowrun deploy -f`,
|
|
288
|
+
which also refuses a build whose package doesn't match the bound app. `PACKAGE_MISMATCH` means
|
|
289
|
+
the build's package isn't the one you passed to `--expected-package` - fetch the right build.
|
|
274
290
|
- For any command or argument you're unsure about, **read `nowrun <cmd> -h`**, never invent flags.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: nowrun
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: CLI to deploy and manage Android and HTML5 apps on nowrun.
|
|
5
5
|
Home-page: https://nowrun.io
|
|
6
6
|
Author: nowrun
|
|
@@ -14,12 +14,12 @@ Description-Content-Type: text/markdown
|
|
|
14
14
|
|
|
15
15
|
# nowrun CLI
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Publish Android apps so people can run them inside ChatGPT - driven by your AI coding agent.
|
|
18
18
|
|
|
19
|
-
`nowrun` publishes Android
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
(native, Unity, Godot, Unreal)
|
|
19
|
+
`nowrun` publishes Android apps to [nowrun](https://nowrun.io) so people can run them inside
|
|
20
|
+
ChatGPT (and other AI assistants that connect to nowrun) - streamed into the conversation, no
|
|
21
|
+
install and no app store. You don't run deploy commands by hand: you set the CLI up once, then
|
|
22
|
+
tell your agent to ship. Works with any engine that builds an APK (native, Unity, Godot, Unreal).
|
|
23
23
|
|
|
24
24
|
## Quickstart
|
|
25
25
|
|
|
@@ -38,10 +38,10 @@ copy or paste. (`nowrun update` refreshes it later; `nowrun logout` signs out.)
|
|
|
38
38
|
|
|
39
39
|
**3. Tell your agent.** Open your AI coding tool in your project and give it this prompt:
|
|
40
40
|
|
|
41
|
-
> Deploy my app to nowrun
|
|
41
|
+
> Deploy my app to nowrun so people can run it in ChatGPT using the nowrun CLI. If you need the steps, read https://nowrun.io/docs/skill.md
|
|
42
42
|
|
|
43
|
-
Your agent creates (or reuses) the app, uploads the build, deploys it
|
|
44
|
-
|
|
43
|
+
Your agent creates (or reuses) the app, uploads the build, and deploys it - and once it's live,
|
|
44
|
+
people can run it inside ChatGPT. That's the whole flow.
|
|
45
45
|
|
|
46
46
|
## What your agent runs for you
|
|
47
47
|
|
|
@@ -56,13 +56,14 @@ They're here for reference (and the occasional manual run):
|
|
|
56
56
|
| `nowrun app list` / `nowrun app show` | list your apps / show one, with its visibility and readiness |
|
|
57
57
|
| `nowrun inspect app.apk` | read a build's package + version, no Android SDK needed |
|
|
58
58
|
| `nowrun validate` | check what's missing before deploying |
|
|
59
|
-
| `nowrun deploy -f app.apk` | deploy a build
|
|
59
|
+
| `nowrun deploy -f app.apk` | deploy a build so it's runnable in ChatGPT (version read from the APK; `--wait` blocks until live) |
|
|
60
60
|
| `nowrun status -s <submission_id>` | poll until it's live |
|
|
61
61
|
| `nowrun help --json` | full command list + workflow (what the agent reads) |
|
|
62
62
|
|
|
63
63
|
`deploy` reads the build's package/version and **refuses to deploy a build whose package
|
|
64
|
-
doesn't match the app**, so you never publish to the wrong place.
|
|
65
|
-
|
|
64
|
+
doesn't match the app**, so you never publish to the wrong place. It also **refuses a version
|
|
65
|
+
code that isn't higher than the one currently live** (like the Play Store), so you can't ship a
|
|
66
|
+
downgrade. Add `--json` (in any position) to any command for machine-readable output.
|
|
66
67
|
|
|
67
68
|
New apps are private; a nowrun admin decides which deployed apps are listed in ChatGPT.
|
|
68
69
|
You stay signed in: the CLI refreshes its session automatically (for 30 days after last use).
|
|
@@ -5,7 +5,7 @@ long_description = (this_directory / "README.md").read_text(encoding="utf-8")
|
|
|
5
5
|
|
|
6
6
|
setup(
|
|
7
7
|
name='nowrun',
|
|
8
|
-
version='v2.0.
|
|
8
|
+
version='v2.0.2',
|
|
9
9
|
description='CLI to deploy and manage Android and HTML5 apps on nowrun.',
|
|
10
10
|
long_description=long_description,
|
|
11
11
|
long_description_content_type='text/markdown',
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|