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 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
@@ -0,0 +1,8 @@
1
+ """Allow running as: python -m ghtraf"""
2
+
3
+ import sys
4
+
5
+ from ghtraf.cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
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,6 @@
1
+ """ghtraf subcommands.
2
+
3
+ Each command module exports:
4
+ register(subparsers, parents) — add the subcommand to argparse
5
+ run(args) — execute the command
6
+ """
@@ -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' [![Installs]({shield_base}/installs.json)]({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