github-traffic-tracker 0.2.8a0__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.
- ghtraf/__init__.py +9 -0
- ghtraf/__main__.py +8 -0
- ghtraf/_version.py +70 -0
- ghtraf/cli.py +172 -0
- ghtraf/commands/__init__.py +6 -0
- ghtraf/commands/create.py +392 -0
- ghtraf/config.py +182 -0
- ghtraf/configure.py +215 -0
- ghtraf/gh.py +183 -0
- ghtraf/gist.py +132 -0
- ghtraf/output.py +58 -0
- ghtraf/templates/docs/stats/README.md +52 -0
- ghtraf/templates/docs/stats/favicon.svg +6 -0
- ghtraf/templates/docs/stats/index.html +2715 -0
- github_traffic_tracker-0.2.8a0.dist-info/METADATA +159 -0
- github_traffic_tracker-0.2.8a0.dist-info/RECORD +20 -0
- github_traffic_tracker-0.2.8a0.dist-info/WHEEL +5 -0
- github_traffic_tracker-0.2.8a0.dist-info/entry_points.txt +3 -0
- github_traffic_tracker-0.2.8a0.dist-info/licenses/LICENSE +674 -0
- github_traffic_tracker-0.2.8a0.dist-info/top_level.txt +1 -0
ghtraf/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""ghtraf — GitHub Traffic Tracker CLI.
|
|
2
|
+
|
|
3
|
+
Zero-server GitHub traffic analytics. Daily collection via Actions,
|
|
4
|
+
gist-backed storage, client-side dashboard.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ghtraf._version import __version__, __app_name__
|
|
8
|
+
|
|
9
|
+
__all__ = ["__version__", "__app_name__"]
|
ghtraf/__main__.py
ADDED
ghtraf/_version.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Version information for ghtraf (GitHub Traffic Tracker CLI).
|
|
3
|
+
|
|
4
|
+
This file is the canonical source for version numbers.
|
|
5
|
+
The __version__ string is automatically updated by git hooks
|
|
6
|
+
with build metadata (branch, build number, date, commit hash).
|
|
7
|
+
|
|
8
|
+
Format: MAJOR.MINOR.PATCH[-PHASE]_BRANCH_BUILD-YYYYMMDD-COMMITHASH
|
|
9
|
+
Example: 0.2.0-alpha_main_4-20260226-a1b2c3d4
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Version components - edit these for version bumps
|
|
13
|
+
MAJOR = 0
|
|
14
|
+
MINOR = 2
|
|
15
|
+
PATCH = 8
|
|
16
|
+
PHASE = "alpha" # Per-MINOR feature set: None, "alpha", "beta", "rc1", etc.
|
|
17
|
+
|
|
18
|
+
# Auto-updated by git hooks - do not edit manually
|
|
19
|
+
__version__ = "0.2.8-alpha_main_12-20260228-4691eea"
|
|
20
|
+
__app_name__ = "ghtraf"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_version():
|
|
24
|
+
"""Return the full version string including branch and build info."""
|
|
25
|
+
return __version__
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_base_version():
|
|
29
|
+
"""Return the semantic version string (MAJOR.MINOR.PATCH[-PHASE])."""
|
|
30
|
+
if "_" in __version__:
|
|
31
|
+
return __version__.split("_")[0]
|
|
32
|
+
base = f"{MAJOR}.{MINOR}.{PATCH}"
|
|
33
|
+
if PHASE:
|
|
34
|
+
base = f"{base}-{PHASE}"
|
|
35
|
+
return base
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_pip_version():
|
|
39
|
+
"""
|
|
40
|
+
Return PEP 440 compliant version for pip/setuptools.
|
|
41
|
+
|
|
42
|
+
Converts our version format to PEP 440:
|
|
43
|
+
- Main branch: 0.2.0-alpha_main_3-20260226-hash -> 0.2.0a0
|
|
44
|
+
- Dev branch: 0.2.0-alpha_dev_3-20260226-hash -> 0.2.0a0.dev3
|
|
45
|
+
"""
|
|
46
|
+
base = f"{MAJOR}.{MINOR}.{PATCH}"
|
|
47
|
+
|
|
48
|
+
# Map phase to PEP 440 pre-release segment
|
|
49
|
+
phase_map = {"alpha": "a0", "beta": "b0"}
|
|
50
|
+
if PHASE:
|
|
51
|
+
base += phase_map.get(PHASE, PHASE)
|
|
52
|
+
|
|
53
|
+
if "_" not in __version__:
|
|
54
|
+
return base
|
|
55
|
+
|
|
56
|
+
parts = __version__.split("_")
|
|
57
|
+
branch = parts[1] if len(parts) > 1 else "unknown"
|
|
58
|
+
|
|
59
|
+
if branch == "main":
|
|
60
|
+
return base
|
|
61
|
+
else:
|
|
62
|
+
build_info = "_".join(parts[2:]) if len(parts) > 2 else ""
|
|
63
|
+
build_num = build_info.split("-")[0] if "-" in build_info else "0"
|
|
64
|
+
return f"{base}.dev{build_num}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# For convenience in imports
|
|
68
|
+
VERSION = get_version()
|
|
69
|
+
BASE_VERSION = get_base_version()
|
|
70
|
+
PIP_VERSION = get_pip_version()
|
ghtraf/cli.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Main CLI entry point for ghtraf.
|
|
2
|
+
|
|
3
|
+
Implements a Docker-style two-pass argument parser:
|
|
4
|
+
1. First pass: extract global flags (--verbose, --no-color, --config)
|
|
5
|
+
2. Second pass: dispatch to subcommand with shared parent args
|
|
6
|
+
|
|
7
|
+
Global flags can appear before OR after the subcommand:
|
|
8
|
+
ghtraf --verbose create --owner X # works
|
|
9
|
+
ghtraf create --owner X --verbose # also works
|
|
10
|
+
|
|
11
|
+
Subcommands self-register via register(subparsers, parents) convention.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
from ghtraf._version import BASE_VERSION, VERSION
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
# Global flags (Docker-style: can precede the subcommand)
|
|
22
|
+
# ---------------------------------------------------------------------------
|
|
23
|
+
GLOBAL_FLAGS = {
|
|
24
|
+
"--verbose": {"action": "store_true", "default": False,
|
|
25
|
+
"help": "Enable verbose output"},
|
|
26
|
+
"--no-color": {"action": "store_true", "default": False,
|
|
27
|
+
"help": "Disable colored output"},
|
|
28
|
+
"--config": {"metavar": "PATH", "default": None,
|
|
29
|
+
"help": "Path to config file (default: ~/.ghtraf/config.json)"},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _extract_global_flags(argv):
|
|
34
|
+
"""Two-pass parse: pull global flags from anywhere in argv.
|
|
35
|
+
|
|
36
|
+
Returns (global_namespace, remaining_argv).
|
|
37
|
+
"""
|
|
38
|
+
global_parser = argparse.ArgumentParser(add_help=False)
|
|
39
|
+
for flag, kwargs in GLOBAL_FLAGS.items():
|
|
40
|
+
global_parser.add_argument(flag, **kwargs)
|
|
41
|
+
|
|
42
|
+
global_args, remaining = global_parser.parse_known_args(argv)
|
|
43
|
+
return global_args, remaining
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Shared parent parser (inherited by all subcommands via parents=[])
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
def _build_common_parser():
|
|
50
|
+
"""Build the shared argument parser for repo-scoped flags.
|
|
51
|
+
|
|
52
|
+
These are inherited by every subcommand — defined once, zero duplication.
|
|
53
|
+
"""
|
|
54
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
55
|
+
common.add_argument("--owner", metavar="NAME",
|
|
56
|
+
help="GitHub username or organization")
|
|
57
|
+
common.add_argument("--repo", metavar="NAME",
|
|
58
|
+
help="Repository name")
|
|
59
|
+
common.add_argument("--repo-dir", metavar="PATH",
|
|
60
|
+
help="Local repository directory")
|
|
61
|
+
common.add_argument("--dry-run", action="store_true", default=False,
|
|
62
|
+
help="Preview changes without applying them")
|
|
63
|
+
common.add_argument("--non-interactive", action="store_true", default=False,
|
|
64
|
+
help="Never prompt — fail on missing required values")
|
|
65
|
+
return common
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# Subcommand discovery and registration
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
def _discover_commands():
|
|
72
|
+
"""Import and return all command modules.
|
|
73
|
+
|
|
74
|
+
Each module in ghtraf.commands must export:
|
|
75
|
+
register(subparsers, parents) — add itself to the subparser
|
|
76
|
+
run(args, global_args) — execute the command
|
|
77
|
+
"""
|
|
78
|
+
from ghtraf.commands import create
|
|
79
|
+
# Future commands added here:
|
|
80
|
+
# from ghtraf.commands import init, status, list_cmd, upgrade, verify
|
|
81
|
+
return [create]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _build_parser(commands, common_parser):
|
|
85
|
+
"""Build the main argparse parser with subcommand dispatch."""
|
|
86
|
+
parser = argparse.ArgumentParser(
|
|
87
|
+
prog="ghtraf",
|
|
88
|
+
description="ghtraf — GitHub Traffic Tracker CLI",
|
|
89
|
+
epilog=(
|
|
90
|
+
"Run 'ghtraf <command> --help' for details on a specific command.\n"
|
|
91
|
+
"\n"
|
|
92
|
+
"Global flags (--verbose, --no-color, --config) can appear\n"
|
|
93
|
+
"before or after the subcommand."
|
|
94
|
+
),
|
|
95
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
96
|
+
)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--version", "-V",
|
|
99
|
+
action="version",
|
|
100
|
+
version=f"ghtraf {BASE_VERSION} ({VERSION})",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Add global flags to main parser too (for --help display)
|
|
104
|
+
for flag, kwargs in GLOBAL_FLAGS.items():
|
|
105
|
+
parser.add_argument(flag, **kwargs)
|
|
106
|
+
|
|
107
|
+
subparsers = parser.add_subparsers(
|
|
108
|
+
dest="command",
|
|
109
|
+
title="commands",
|
|
110
|
+
metavar="<command>",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Let each command register itself
|
|
114
|
+
for cmd_module in commands:
|
|
115
|
+
cmd_module.register(subparsers, parents=[common_parser])
|
|
116
|
+
|
|
117
|
+
return parser
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# Entry point
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
def main(argv=None):
|
|
124
|
+
"""Main entry point for ghtraf CLI.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
argv: Command-line arguments. None means sys.argv[1:].
|
|
128
|
+
Accepts a list for DazzleCMD integration.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Exit code (0 = success).
|
|
132
|
+
"""
|
|
133
|
+
if argv is None:
|
|
134
|
+
argv = sys.argv[1:]
|
|
135
|
+
|
|
136
|
+
# Pass 1: extract global flags from anywhere in the arg list
|
|
137
|
+
global_args, remaining = _extract_global_flags(argv)
|
|
138
|
+
|
|
139
|
+
# Pass 2: parse subcommand + shared/specific args
|
|
140
|
+
common_parser = _build_common_parser()
|
|
141
|
+
commands = _discover_commands()
|
|
142
|
+
parser = _build_parser(commands, common_parser)
|
|
143
|
+
|
|
144
|
+
# If no args at all, print help
|
|
145
|
+
if not remaining:
|
|
146
|
+
parser.print_help()
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
args = parser.parse_args(remaining)
|
|
150
|
+
|
|
151
|
+
# If subcommand selected but no handler, print help
|
|
152
|
+
if not hasattr(args, "func"):
|
|
153
|
+
parser.print_help()
|
|
154
|
+
return 0
|
|
155
|
+
|
|
156
|
+
# Merge global args into the namespace for convenience
|
|
157
|
+
for key, value in vars(global_args).items():
|
|
158
|
+
if key not in vars(args) or getattr(args, key) is None:
|
|
159
|
+
setattr(args, key, value)
|
|
160
|
+
|
|
161
|
+
# Dispatch
|
|
162
|
+
try:
|
|
163
|
+
return args.func(args) or 0
|
|
164
|
+
except KeyboardInterrupt:
|
|
165
|
+
print("\nInterrupted.")
|
|
166
|
+
return 130
|
|
167
|
+
except SystemExit as e:
|
|
168
|
+
return e.code if isinstance(e.code, int) else 1
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
if __name__ == "__main__":
|
|
172
|
+
sys.exit(main())
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"""ghtraf create — Create gists and configure repository for traffic tracking.
|
|
2
|
+
|
|
3
|
+
This is the bootstrap command. It creates the badge and archive gists,
|
|
4
|
+
sets repository variables/secrets, and optionally configures dashboard
|
|
5
|
+
and workflow files with project-specific values.
|
|
6
|
+
|
|
7
|
+
Equivalent to the standalone setup-gists.py script.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import html as html_module
|
|
11
|
+
import re
|
|
12
|
+
import sys
|
|
13
|
+
from datetime import date
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from ghtraf import gh, gist, configure
|
|
17
|
+
from ghtraf.config import register_repo_globally, save_project_config
|
|
18
|
+
from ghtraf.output import (
|
|
19
|
+
print_dry, print_ok, print_skip, print_step, print_warn, prompt,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register(subparsers, parents):
|
|
24
|
+
"""Register the 'create' subcommand."""
|
|
25
|
+
p = subparsers.add_parser(
|
|
26
|
+
"create",
|
|
27
|
+
parents=parents,
|
|
28
|
+
help="Create gists and set repository variables for traffic tracking",
|
|
29
|
+
description=(
|
|
30
|
+
"Create the public badge gist and unlisted archive gist needed\n"
|
|
31
|
+
"by the traffic-badges.yml workflow, then optionally configure\n"
|
|
32
|
+
"repository variables/secrets and update dashboard files."
|
|
33
|
+
),
|
|
34
|
+
formatter_class=__import__("argparse").RawDescriptionHelpFormatter,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
# create-specific args
|
|
38
|
+
p.add_argument("--created", metavar="DATE",
|
|
39
|
+
help="Repository creation date (YYYY-MM-DD)")
|
|
40
|
+
p.add_argument("--display-name", metavar="NAME",
|
|
41
|
+
help="Display name for dashboard title/banner")
|
|
42
|
+
p.add_argument("--ci-workflows", nargs="*", default=None,
|
|
43
|
+
help="CI workflow names for workflow_run trigger "
|
|
44
|
+
"(omit to comment out trigger)")
|
|
45
|
+
p.add_argument("--configure", action="store_true", dest="configure_files",
|
|
46
|
+
help="Also update dashboard and workflow files with your values")
|
|
47
|
+
p.add_argument("--skip-variables", action="store_true",
|
|
48
|
+
help="Skip setting repository variables/secrets")
|
|
49
|
+
p.add_argument("--gist-token-name", default="TRAFFIC_GIST_TOKEN",
|
|
50
|
+
help="Name for the gist token secret "
|
|
51
|
+
"(default: TRAFFIC_GIST_TOKEN)")
|
|
52
|
+
|
|
53
|
+
p.set_defaults(func=run)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _gather_config(args):
|
|
57
|
+
"""Build config dict, prompting for any missing values."""
|
|
58
|
+
config = {}
|
|
59
|
+
non_interactive = args.non_interactive
|
|
60
|
+
|
|
61
|
+
# Owner
|
|
62
|
+
if args.owner:
|
|
63
|
+
config["owner"] = args.owner
|
|
64
|
+
elif non_interactive:
|
|
65
|
+
print("ERROR: --owner is required in non-interactive mode.")
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
else:
|
|
68
|
+
config["owner"] = prompt("GitHub owner (username or org)")
|
|
69
|
+
|
|
70
|
+
# Repo
|
|
71
|
+
if args.repo:
|
|
72
|
+
config["repo"] = args.repo
|
|
73
|
+
elif non_interactive:
|
|
74
|
+
print("ERROR: --repo is required in non-interactive mode.")
|
|
75
|
+
sys.exit(1)
|
|
76
|
+
else:
|
|
77
|
+
config["repo"] = prompt("Repository name")
|
|
78
|
+
|
|
79
|
+
config["gh_repo"] = f"{config['owner']}/{config['repo']}"
|
|
80
|
+
|
|
81
|
+
# Created date — try auto-detect, fall back to today
|
|
82
|
+
if args.created:
|
|
83
|
+
config["created"] = args.created
|
|
84
|
+
else:
|
|
85
|
+
auto_date = gh.get_repo_created_date(config["gh_repo"])
|
|
86
|
+
if non_interactive:
|
|
87
|
+
config["created"] = auto_date or date.today().isoformat()
|
|
88
|
+
elif auto_date:
|
|
89
|
+
config["created"] = prompt(
|
|
90
|
+
"Repository creation date (YYYY-MM-DD)", default=auto_date)
|
|
91
|
+
else:
|
|
92
|
+
config["created"] = prompt(
|
|
93
|
+
"Repository creation date (YYYY-MM-DD)",
|
|
94
|
+
default=date.today().isoformat())
|
|
95
|
+
|
|
96
|
+
# Display name
|
|
97
|
+
if args.display_name:
|
|
98
|
+
config["display_name"] = args.display_name
|
|
99
|
+
elif non_interactive:
|
|
100
|
+
config["display_name"] = (config["repo"]
|
|
101
|
+
.replace("-", " ")
|
|
102
|
+
.replace("_", " ")
|
|
103
|
+
.title())
|
|
104
|
+
else:
|
|
105
|
+
default_name = (config["repo"]
|
|
106
|
+
.replace("-", " ")
|
|
107
|
+
.replace("_", " ")
|
|
108
|
+
.title())
|
|
109
|
+
config["display_name"] = prompt(
|
|
110
|
+
"Display name for dashboard", default=default_name)
|
|
111
|
+
|
|
112
|
+
# CI workflows
|
|
113
|
+
if args.ci_workflows is not None:
|
|
114
|
+
config["ci_workflows"] = args.ci_workflows
|
|
115
|
+
elif non_interactive:
|
|
116
|
+
config["ci_workflows"] = []
|
|
117
|
+
else:
|
|
118
|
+
ci_input = input(
|
|
119
|
+
" CI workflow names to trigger after "
|
|
120
|
+
"(comma-separated, Enter to skip): "
|
|
121
|
+
).strip()
|
|
122
|
+
if ci_input:
|
|
123
|
+
config["ci_workflows"] = [
|
|
124
|
+
w.strip() for w in ci_input.split(",") if w.strip()
|
|
125
|
+
]
|
|
126
|
+
else:
|
|
127
|
+
config["ci_workflows"] = []
|
|
128
|
+
|
|
129
|
+
config["display_name_html"] = html_module.escape(config["display_name"])
|
|
130
|
+
|
|
131
|
+
return config
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _validate_config(config):
|
|
135
|
+
"""Validate configuration values."""
|
|
136
|
+
if not re.match(r"^\d{4}-\d{2}-\d{2}$", config["created"]):
|
|
137
|
+
print(f"ERROR: Invalid date format '{config['created']}'. "
|
|
138
|
+
"Expected YYYY-MM-DD.")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
repo_exists = gh.check_repo_exists(config["gh_repo"])
|
|
142
|
+
if not repo_exists:
|
|
143
|
+
print_warn(f"Repository {config['gh_repo']} not found on GitHub.")
|
|
144
|
+
print(" This is OK if you haven't created it yet.")
|
|
145
|
+
print(" Repository variables/secrets will be set once it exists.")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _guide_token_setup(config, dry_run=False):
|
|
149
|
+
"""Guide user through PAT creation and offer to set the secret."""
|
|
150
|
+
token_name = config.get("gist_token_name", "TRAFFIC_GIST_TOKEN")
|
|
151
|
+
gh_repo = config["gh_repo"]
|
|
152
|
+
|
|
153
|
+
print()
|
|
154
|
+
print(" The workflow needs a Personal Access Token (PAT) with 'gist' scope")
|
|
155
|
+
print(" to update your gists. This is SEPARATE from your gh CLI token.")
|
|
156
|
+
print()
|
|
157
|
+
print(" To create one:")
|
|
158
|
+
print(f" 1. Go to: https://github.com/settings/tokens/new")
|
|
159
|
+
print(f" 2. Name it: \"Traffic Tracker - {gh_repo}\"")
|
|
160
|
+
print(" 3. Check ONLY the 'gist' scope")
|
|
161
|
+
print(" 4. Set expiration (recommended: no expiration, or 1 year)")
|
|
162
|
+
print(" 5. Click 'Generate token' and copy the value")
|
|
163
|
+
print()
|
|
164
|
+
|
|
165
|
+
if dry_run:
|
|
166
|
+
print_dry(f"Would prompt for PAT and set secret {token_name}")
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
if config.get("non_interactive"):
|
|
170
|
+
print(f" Then run: gh secret set {token_name} -R {gh_repo}")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
token = input(" Paste your PAT here (or press Enter to skip): ").strip()
|
|
174
|
+
if token:
|
|
175
|
+
success = gh.set_repo_secret(token_name, token, gh_repo)
|
|
176
|
+
if not success:
|
|
177
|
+
print_warn("Could not set secret.")
|
|
178
|
+
print(f" Run manually: gh secret set {token_name} -R {gh_repo}")
|
|
179
|
+
else:
|
|
180
|
+
print_ok(f"Secret {token_name} set successfully")
|
|
181
|
+
else:
|
|
182
|
+
print_skip("PAT not provided")
|
|
183
|
+
print(f" Remember to run: gh secret set {token_name} -R {gh_repo}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def run(args):
|
|
187
|
+
"""Execute the create command."""
|
|
188
|
+
dry_run = args.dry_run
|
|
189
|
+
|
|
190
|
+
# Header
|
|
191
|
+
print()
|
|
192
|
+
print("GitHub Traffic Tracker Setup")
|
|
193
|
+
print("=" * 40)
|
|
194
|
+
if dry_run:
|
|
195
|
+
print("[DRY RUN MODE - no changes will be made]")
|
|
196
|
+
|
|
197
|
+
# Prerequisites
|
|
198
|
+
print("\nChecking prerequisites...")
|
|
199
|
+
version = gh.check_gh_installed()
|
|
200
|
+
print_ok(f"gh CLI found ({version})")
|
|
201
|
+
|
|
202
|
+
auth_output = gh.check_gh_authenticated()
|
|
203
|
+
# Extract login line for display
|
|
204
|
+
for line in auth_output.split("\n"):
|
|
205
|
+
if "Logged in to" in line and "account" in line:
|
|
206
|
+
print_ok(line.strip().lstrip("\u2713").strip())
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
has_gist_scope = gh.check_gh_scopes(auth_output)
|
|
210
|
+
if not has_gist_scope:
|
|
211
|
+
print_warn("Your gh CLI token may not have 'gist' scope.")
|
|
212
|
+
print(" Run: gh auth refresh -s gist")
|
|
213
|
+
if not args.non_interactive:
|
|
214
|
+
resp = input(" Continue anyway? (y/N): ").strip().lower()
|
|
215
|
+
if resp != "y":
|
|
216
|
+
sys.exit(1)
|
|
217
|
+
else:
|
|
218
|
+
print_ok("Token has gist access")
|
|
219
|
+
|
|
220
|
+
gh_username = gh.resolve_github_username()
|
|
221
|
+
print_ok(f"GitHub username: {gh_username}")
|
|
222
|
+
|
|
223
|
+
# Configuration
|
|
224
|
+
print("\nGathering configuration...")
|
|
225
|
+
config = _gather_config(args)
|
|
226
|
+
config["gh_username"] = gh_username
|
|
227
|
+
config["gist_token_name"] = args.gist_token_name
|
|
228
|
+
config["non_interactive"] = args.non_interactive
|
|
229
|
+
|
|
230
|
+
_validate_config(config)
|
|
231
|
+
|
|
232
|
+
total_steps = 3
|
|
233
|
+
if args.configure_files:
|
|
234
|
+
total_steps += 1
|
|
235
|
+
if not args.skip_variables:
|
|
236
|
+
total_steps += 1
|
|
237
|
+
|
|
238
|
+
print(f"\n Owner: {config['owner']}")
|
|
239
|
+
print(f" Repository: {config['repo']}")
|
|
240
|
+
print(f" Created: {config['created']}")
|
|
241
|
+
print(f" Display Name: {config['display_name']}")
|
|
242
|
+
if config["ci_workflows"]:
|
|
243
|
+
print(f" CI Workflows: {', '.join(config['ci_workflows'])}")
|
|
244
|
+
else:
|
|
245
|
+
print(" CI Workflows: (none)")
|
|
246
|
+
print(f" Configure: {'yes' if args.configure_files else 'no'}")
|
|
247
|
+
|
|
248
|
+
if not args.non_interactive and not dry_run:
|
|
249
|
+
print()
|
|
250
|
+
resp = input(" Proceed? (Y/n): ").strip().lower()
|
|
251
|
+
if resp == "n":
|
|
252
|
+
print(" Setup cancelled.")
|
|
253
|
+
return 0
|
|
254
|
+
|
|
255
|
+
# Step 1: Create badge gist
|
|
256
|
+
step = 1
|
|
257
|
+
print_step(step, total_steps, "Create badge gist (public)")
|
|
258
|
+
badge_gist_id = gist.create_badge_gist(config, dry_run=dry_run)
|
|
259
|
+
config["badge_gist_id"] = badge_gist_id
|
|
260
|
+
|
|
261
|
+
# Step 2: Create archive gist
|
|
262
|
+
step += 1
|
|
263
|
+
print_step(step, total_steps, "Create archive gist (unlisted)")
|
|
264
|
+
archive_gist_id = gist.create_archive_gist(config, dry_run=dry_run)
|
|
265
|
+
config["archive_gist_id"] = archive_gist_id
|
|
266
|
+
|
|
267
|
+
# Step 3: Set repository variables
|
|
268
|
+
if not args.skip_variables:
|
|
269
|
+
step += 1
|
|
270
|
+
print_step(step, total_steps, "Set repository variables")
|
|
271
|
+
|
|
272
|
+
success = gh.set_repo_variable(
|
|
273
|
+
"TRAFFIC_GIST_ID", badge_gist_id, config["gh_repo"], dry_run)
|
|
274
|
+
if dry_run:
|
|
275
|
+
print_dry(f"Would set variable TRAFFIC_GIST_ID = {badge_gist_id}")
|
|
276
|
+
elif success:
|
|
277
|
+
print_ok(f"TRAFFIC_GIST_ID = {badge_gist_id}")
|
|
278
|
+
else:
|
|
279
|
+
print_warn("Could not set TRAFFIC_GIST_ID")
|
|
280
|
+
print(f" Run manually: gh variable set TRAFFIC_GIST_ID "
|
|
281
|
+
f"--body \"{badge_gist_id}\" -R {config['gh_repo']}")
|
|
282
|
+
|
|
283
|
+
success = gh.set_repo_variable(
|
|
284
|
+
"TRAFFIC_ARCHIVE_GIST_ID", archive_gist_id,
|
|
285
|
+
config["gh_repo"], dry_run)
|
|
286
|
+
if dry_run:
|
|
287
|
+
print_dry(f"Would set variable TRAFFIC_ARCHIVE_GIST_ID = "
|
|
288
|
+
f"{archive_gist_id}")
|
|
289
|
+
elif success:
|
|
290
|
+
print_ok(f"TRAFFIC_ARCHIVE_GIST_ID = {archive_gist_id}")
|
|
291
|
+
else:
|
|
292
|
+
print_warn("Could not set TRAFFIC_ARCHIVE_GIST_ID")
|
|
293
|
+
print(f" Run manually: gh variable set TRAFFIC_ARCHIVE_GIST_ID "
|
|
294
|
+
f"--body \"{archive_gist_id}\" -R {config['gh_repo']}")
|
|
295
|
+
|
|
296
|
+
# PAT guidance
|
|
297
|
+
step += 1
|
|
298
|
+
print_step(step, total_steps,
|
|
299
|
+
f"Repository secret ({config['gist_token_name']})")
|
|
300
|
+
_guide_token_setup(config, dry_run=dry_run)
|
|
301
|
+
|
|
302
|
+
# Step 4: Configure files (optional)
|
|
303
|
+
if args.configure_files:
|
|
304
|
+
step += 1
|
|
305
|
+
print_step(step, total_steps, "Configure project files")
|
|
306
|
+
|
|
307
|
+
repo_dir = Path(args.repo_dir or ".").resolve()
|
|
308
|
+
dashboard_path = repo_dir / "docs" / "stats" / "index.html"
|
|
309
|
+
readme_path = repo_dir / "docs" / "stats" / "README.md"
|
|
310
|
+
workflow_path = repo_dir / ".github" / "workflows" / "traffic-badges.yml"
|
|
311
|
+
|
|
312
|
+
configure.configure_dashboard(config, dashboard_path, dry_run=dry_run)
|
|
313
|
+
configure.configure_readme(config, readme_path, dry_run=dry_run)
|
|
314
|
+
configure.configure_workflow(config, workflow_path, dry_run=dry_run)
|
|
315
|
+
|
|
316
|
+
# Write config files
|
|
317
|
+
if not dry_run:
|
|
318
|
+
repo_dir = Path(args.repo_dir or ".").resolve()
|
|
319
|
+
project_cfg = {
|
|
320
|
+
"owner": config["owner"],
|
|
321
|
+
"repo": config["repo"],
|
|
322
|
+
"created": config["created"],
|
|
323
|
+
"display_name": config["display_name"],
|
|
324
|
+
"badge_gist_id": badge_gist_id,
|
|
325
|
+
"archive_gist_id": archive_gist_id,
|
|
326
|
+
"dashboard_dir": "docs/stats",
|
|
327
|
+
"schema_version": 1,
|
|
328
|
+
}
|
|
329
|
+
if config["ci_workflows"]:
|
|
330
|
+
project_cfg["ci_workflows"] = config["ci_workflows"]
|
|
331
|
+
|
|
332
|
+
save_project_config(project_cfg, repo_dir)
|
|
333
|
+
register_repo_globally(
|
|
334
|
+
owner=config["owner"],
|
|
335
|
+
repo=config["repo"],
|
|
336
|
+
badge_gist_id=badge_gist_id,
|
|
337
|
+
archive_gist_id=archive_gist_id,
|
|
338
|
+
repo_dir=str(repo_dir),
|
|
339
|
+
display_name=config["display_name"],
|
|
340
|
+
created=config["created"],
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Summary
|
|
344
|
+
print()
|
|
345
|
+
print("=" * 40)
|
|
346
|
+
if dry_run:
|
|
347
|
+
print("Dry run complete! Re-run without --dry-run to apply.")
|
|
348
|
+
else:
|
|
349
|
+
print("Setup complete!")
|
|
350
|
+
|
|
351
|
+
print(f"\n Badge Gist ID: {badge_gist_id}")
|
|
352
|
+
print(f" Archive Gist ID: {archive_gist_id}")
|
|
353
|
+
|
|
354
|
+
gist_base = (f"https://gist.githubusercontent.com/"
|
|
355
|
+
f"{gh_username}/{badge_gist_id}/raw")
|
|
356
|
+
print(f"\nBadge URLs:")
|
|
357
|
+
print(f" Installs: https://img.shields.io/endpoint?url="
|
|
358
|
+
f"{gist_base}/installs.json")
|
|
359
|
+
print(f" Downloads: https://img.shields.io/endpoint?url="
|
|
360
|
+
f"{gist_base}/downloads.json")
|
|
361
|
+
print(f" Clones: https://img.shields.io/endpoint?url="
|
|
362
|
+
f"{gist_base}/clones.json")
|
|
363
|
+
print(f" Views: https://img.shields.io/endpoint?url="
|
|
364
|
+
f"{gist_base}/views.json")
|
|
365
|
+
|
|
366
|
+
print(f"\nBadge Markdown (copy-paste for README):")
|
|
367
|
+
shield_base = "https://img.shields.io/endpoint?url=" + gist_base
|
|
368
|
+
owner_lower = config["owner"].lower()
|
|
369
|
+
stats_url = f"https://{owner_lower}.github.io/{config['repo']}/stats/"
|
|
370
|
+
print(f' []({stats_url}#installs)')
|
|
371
|
+
|
|
372
|
+
print(f"\nNext steps:")
|
|
373
|
+
if args.skip_variables:
|
|
374
|
+
print(f" 1. Set repo variables:")
|
|
375
|
+
print(f" gh variable set TRAFFIC_GIST_ID "
|
|
376
|
+
f"--body \"{badge_gist_id}\" -R {config['gh_repo']}")
|
|
377
|
+
print(f" gh variable set TRAFFIC_ARCHIVE_GIST_ID "
|
|
378
|
+
f"--body \"{archive_gist_id}\" -R {config['gh_repo']}")
|
|
379
|
+
print(f" 2. Set repo secret with a PAT (gist scope):")
|
|
380
|
+
print(f" gh secret set {config['gist_token_name']} "
|
|
381
|
+
f"-R {config['gh_repo']}")
|
|
382
|
+
if not args.configure_files:
|
|
383
|
+
print(" - Run again with --configure to update dashboard/workflow files")
|
|
384
|
+
print(" - Commit and push your changes")
|
|
385
|
+
print(" - Enable GitHub Pages (Settings > Pages > Deploy from branch "
|
|
386
|
+
"> main, /docs)")
|
|
387
|
+
print(" - Trigger the workflow manually or wait for the 3am UTC schedule:")
|
|
388
|
+
print(f" gh workflow run \"Track Downloads & Clones\" "
|
|
389
|
+
f"-R {config['gh_repo']}")
|
|
390
|
+
print()
|
|
391
|
+
|
|
392
|
+
return 0
|