speculate-cli 0.0.2__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.
speculate/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Speculate CLI package
@@ -0,0 +1 @@
1
+ # CLI package for speculate
@@ -0,0 +1,433 @@
1
+ """
2
+ Command implementations for speculate CLI.
3
+
4
+ Each command is a function with a docstring that serves as CLI help.
5
+ Only copier is lazy-imported (it's a large package).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import shutil
11
+ from datetime import UTC, datetime
12
+ from importlib.metadata import version
13
+ from pathlib import Path
14
+ from typing import Any, cast
15
+
16
+ import yaml
17
+ from prettyfmt import fmt_count_items, fmt_size_human
18
+ from rich import print as rprint
19
+ from strif import atomic_output_file
20
+
21
+ from speculate.cli.cli_ui import (
22
+ print_cancelled,
23
+ print_detail,
24
+ print_error,
25
+ print_error_item,
26
+ print_header,
27
+ print_info,
28
+ print_missing,
29
+ print_note,
30
+ print_success,
31
+ print_warning,
32
+ )
33
+
34
+
35
+ def _load_yaml(path: Path) -> dict[str, Any]:
36
+ """Load a YAML file and return a dictionary."""
37
+ with open(path) as f:
38
+ result = yaml.safe_load(f)
39
+ return cast(dict[str, Any], result) if isinstance(result, dict) else {}
40
+
41
+
42
+ def init(
43
+ destination: str = ".",
44
+ overwrite: bool = False,
45
+ template: str = "gh:jlevy/speculate",
46
+ ref: str = "HEAD",
47
+ ) -> None:
48
+ """Initialize docs in a project using Copier.
49
+
50
+ Copies the docs/ directory from the speculate template into your project.
51
+ Creates a .copier-answers.yml file for future updates.
52
+
53
+ By default, always pulls from the latest commit (HEAD) so docs updates
54
+ don't require new CLI releases. Use --ref to update to a specific version.
55
+
56
+ Examples:
57
+ speculate init # Initialize in current directory
58
+ speculate init ./my-project # Initialize in specific directory
59
+ speculate init --overwrite # Overwrite without confirmation
60
+ speculate init --ref v1.0.0 # Use specific tag/commit
61
+ """
62
+ import copier # Lazy import - large package
63
+
64
+ dst = Path(destination).resolve()
65
+ docs_path = dst / "docs"
66
+
67
+ print_header("Initializing Speculate docs in:", dst)
68
+
69
+ if docs_path.exists() and not overwrite:
70
+ print_note(
71
+ f"{docs_path} already exists", "Use `speculate update` to preserve local changes."
72
+ )
73
+ response = input("Reinitialize anyway? [y/N] ").strip().lower()
74
+ if response != "y":
75
+ print_cancelled()
76
+ raise SystemExit(0)
77
+
78
+ print_header("Docs will be copied to:", f"{docs_path}/")
79
+
80
+ if not overwrite:
81
+ response = input("Proceed? [Y/n] ").strip().lower()
82
+ if response == "n":
83
+ print_cancelled()
84
+ raise SystemExit(0)
85
+
86
+ rprint()
87
+ # vcs_ref=HEAD ensures we always get latest docs without needing CLI releases
88
+ _ = copier.run_copy(template, str(dst), overwrite=overwrite, defaults=overwrite, vcs_ref=ref)
89
+
90
+ # Copy development.sample.md to development.md if it doesn't exist
91
+ sample_dev_md = dst / "docs" / "project" / "development.sample.md"
92
+ dev_md = dst / "docs" / "development.md"
93
+ if sample_dev_md.exists() and not dev_md.exists():
94
+ shutil.copy(sample_dev_md, dev_md)
95
+ print_success("Created docs/development.md from template")
96
+
97
+ # Show summary of what was created
98
+ file_count, total_size = _get_dir_stats(docs_path)
99
+ rprint()
100
+ print_success(
101
+ f"Docs installed ({fmt_count_items(file_count, 'file')}, {fmt_size_human(total_size)})"
102
+ )
103
+ rprint()
104
+
105
+ # Automatically run install to set up tool configs
106
+ install()
107
+
108
+ # Remind user about required project-specific setup
109
+ rprint("[bold yellow]Required next step:[/bold yellow]")
110
+ print_detail("Customize docs/development.md with your project-specific setup.")
111
+ rprint()
112
+ rprint("Other commands:")
113
+ print_detail("speculate status # Check current status")
114
+ print_detail("speculate update # Pull future updates")
115
+ rprint()
116
+
117
+
118
+ def update() -> None:
119
+ """Update docs from the upstream template.
120
+
121
+ Pulls the latest changes from the speculate template and merges them
122
+ with your local docs. Local customizations in docs/project/ are preserved.
123
+
124
+ Automatically runs `install` after update to refresh tool configs.
125
+
126
+ Examples:
127
+ speculate update
128
+ """
129
+ import copier # Lazy import - large package
130
+
131
+ cwd = Path.cwd()
132
+ answers_file = cwd / ".copier-answers.yml"
133
+
134
+ if not answers_file.exists():
135
+ print_error(
136
+ "No .copier-answers.yml found", "Run `speculate init` first to initialize docs."
137
+ )
138
+ raise SystemExit(1)
139
+
140
+ print_header("Updating docs from upstream template...", cwd)
141
+
142
+ _ = copier.run_update(str(cwd), conflict="inline")
143
+
144
+ rprint()
145
+ print_success("Docs updated successfully!")
146
+ rprint()
147
+
148
+ # Automatically run install to refresh tool configs
149
+ install()
150
+
151
+
152
+ def install(
153
+ include: list[str] | None = None,
154
+ exclude: list[str] | None = None,
155
+ ) -> None:
156
+ """Generate tool configs for Cursor, Claude Code, and Codex.
157
+
158
+ Creates or updates:
159
+ - .speculate/settings.yml (install metadata)
160
+ - CLAUDE.md (for Claude Code) — adds speculate header if missing
161
+ - AGENTS.md (for Codex) — adds speculate header if missing
162
+ - .cursor/rules/ (symlinks for Cursor)
163
+
164
+ This command is idempotent and can be run multiple times safely.
165
+ It's automatically called by `init` and `update`.
166
+
167
+ Supports include/exclude patterns with wildcards:
168
+ - `*` matches any characters within a filename
169
+ - `**` matches any path segments
170
+ - Default: include all (["**/*.md"])
171
+
172
+ Examples:
173
+ speculate install
174
+ speculate install --include "general-*.md"
175
+ speculate install --exclude "convex-*.md"
176
+ """
177
+ cwd = Path.cwd()
178
+ docs_path = cwd / "docs"
179
+
180
+ if not docs_path.exists():
181
+ print_error(
182
+ "No docs/ directory found",
183
+ "Run `speculate init` first, or manually copy docs/ to this directory.",
184
+ )
185
+ raise SystemExit(1)
186
+
187
+ print_header("Installing tool configurations...", cwd)
188
+
189
+ # .speculate/settings.yml — track install metadata
190
+ _update_speculate_settings(cwd)
191
+
192
+ # CLAUDE.md — ensure speculate header at top (idempotent)
193
+ _ensure_speculate_header(cwd / "CLAUDE.md")
194
+
195
+ # AGENTS.md — ensure speculate header at top (idempotent)
196
+ _ensure_speculate_header(cwd / "AGENTS.md")
197
+
198
+ # .cursor/rules/
199
+ _setup_cursor_rules(cwd, include=include, exclude=exclude)
200
+
201
+ rprint()
202
+ print_success("Tool configs installed!")
203
+ rprint()
204
+
205
+
206
+ def status() -> None:
207
+ """Show current template version and sync status.
208
+
209
+ Displays:
210
+ - Template version from .copier-answers.yml
211
+ - Last install info from .speculate/settings.yml
212
+ - Whether docs/ exists
213
+ - Whether development.md exists (required)
214
+ - Which tool configs are present
215
+
216
+ Exits with error if development.md is missing (required project setup).
217
+
218
+ Examples:
219
+ speculate status
220
+ """
221
+ cwd = Path.cwd()
222
+ has_errors = False
223
+
224
+ print_header("Speculate Status", cwd)
225
+
226
+ # Check .copier-answers.yml (required for update)
227
+ answers_file = cwd / ".copier-answers.yml"
228
+ if answers_file.exists():
229
+ answers = _load_yaml(answers_file)
230
+ commit = answers.get("_commit", "unknown")
231
+ src = answers.get("_src_path", "unknown")
232
+ print_success(f"Template version: {commit}")
233
+ print_detail(f"Source: {src}")
234
+ else:
235
+ print_error_item(
236
+ ".copier-answers.yml missing (required!)",
237
+ "Run `speculate init` to initialize docs.",
238
+ )
239
+ has_errors = True
240
+
241
+ # Check .speculate/settings.yml
242
+ settings_file = cwd / ".speculate" / "settings.yml"
243
+ if settings_file.exists():
244
+ settings = _load_yaml(settings_file)
245
+ last_update = settings.get("last_update", "unknown")
246
+ cli_version = settings.get("last_cli_version", "unknown")
247
+ print_success(f"Last install: {last_update} (CLI {cli_version})")
248
+ else:
249
+ print_info(".speculate/settings.yml not found")
250
+
251
+ # Check docs/
252
+ docs_path = cwd / "docs"
253
+ if docs_path.exists():
254
+ file_count, total_size = _get_dir_stats(docs_path)
255
+ print_success(
256
+ f"docs/ exists ({fmt_count_items(file_count, 'file')}, {fmt_size_human(total_size)})"
257
+ )
258
+ else:
259
+ print_missing("docs/ not found")
260
+
261
+ # Check development.md (required)
262
+ dev_md = cwd / "docs" / "development.md"
263
+ if dev_md.exists():
264
+ print_success("docs/development.md exists")
265
+ else:
266
+ print_error_item(
267
+ "docs/development.md missing (required!)",
268
+ "Create this file using docs/project/development.sample.md as a template.",
269
+ )
270
+ has_errors = True
271
+
272
+ # Check tool configs
273
+ for name, path in [
274
+ ("CLAUDE.md", cwd / "CLAUDE.md"),
275
+ ("AGENTS.md", cwd / "AGENTS.md"),
276
+ (".cursor/rules/", cwd / ".cursor" / "rules"),
277
+ ]:
278
+ if path.exists():
279
+ print_success(f"{name} exists")
280
+ else:
281
+ print_info(f"{name} not configured")
282
+
283
+ rprint()
284
+
285
+ if has_errors:
286
+ raise SystemExit(1)
287
+
288
+
289
+ # Helper functions
290
+
291
+
292
+ def _update_speculate_settings(project_root: Path) -> None:
293
+ """Create or update .speculate/settings.yml with install metadata."""
294
+ settings_dir = project_root / ".speculate"
295
+ settings_dir.mkdir(parents=True, exist_ok=True)
296
+ settings_file = settings_dir / "settings.yml"
297
+
298
+ # Read existing settings
299
+ settings: dict[str, Any] = _load_yaml(settings_file) if settings_file.exists() else {}
300
+
301
+ # Update with current info
302
+ settings["last_update"] = datetime.now(UTC).isoformat()
303
+ try:
304
+ settings["last_cli_version"] = version("speculate")
305
+ except Exception:
306
+ settings["last_cli_version"] = "unknown"
307
+
308
+ # Get docs version from .copier-answers.yml if available
309
+ answers_file = project_root / ".copier-answers.yml"
310
+ if answers_file.exists():
311
+ answers = _load_yaml(answers_file)
312
+ settings["last_docs_version"] = answers.get("_commit", "unknown")
313
+
314
+ with atomic_output_file(settings_file) as temp_path:
315
+ Path(temp_path).write_text(yaml.dump(settings, default_flow_style=False))
316
+ print_success("Updated .speculate/settings.yml")
317
+
318
+
319
+ def _get_dir_stats(path: Path) -> tuple[int, int]:
320
+ """Return (file_count, total_bytes) for all files in a directory."""
321
+ file_count = 0
322
+ total_size = 0
323
+ for f in path.rglob("*"):
324
+ if f.is_file():
325
+ file_count += 1
326
+ total_size += f.stat().st_size
327
+ return file_count, total_size
328
+
329
+
330
+ SPECULATE_MARKER = "Speculate project structure"
331
+ SPECULATE_HEADER = f"""IMPORTANT: You MUST read ./docs/development.md and ./docs/docs-overview.md for project documentation.
332
+ (This project uses {SPECULATE_MARKER}.)"""
333
+
334
+
335
+ def _ensure_speculate_header(path: Path) -> None:
336
+ """Ensure SPECULATE_HEADER is at the top of the file (idempotent).
337
+
338
+ If file exists and already has the marker, do nothing.
339
+ If file exists without marker, prepend the header.
340
+ If file doesn't exist, create with just the header.
341
+ """
342
+ if path.exists():
343
+ content = path.read_text()
344
+ if SPECULATE_MARKER in content:
345
+ print_info(f"{path.name} already configured")
346
+ return
347
+ # Prepend header to existing content
348
+ new_content = SPECULATE_HEADER + "\n\n" + content
349
+ action = "Updated"
350
+ else:
351
+ new_content = SPECULATE_HEADER + "\n"
352
+ action = "Created"
353
+
354
+ with atomic_output_file(path) as temp_path:
355
+ Path(temp_path).write_text(new_content)
356
+ print_success(f"{action} {path.name}")
357
+
358
+
359
+ def _matches_patterns(
360
+ filename: str,
361
+ include: list[str] | None,
362
+ exclude: list[str] | None,
363
+ ) -> bool:
364
+ """Check if filename matches include patterns and doesn't match exclude patterns.
365
+
366
+ Supports wildcards:
367
+ - `*` matches any characters within a filename
368
+ - `**` is treated same as `*` for simple filename matching
369
+
370
+ Default behavior: include all if no include patterns specified.
371
+ """
372
+ import fnmatch
373
+
374
+ # Normalize ** to * for fnmatch (which doesn't support **)
375
+ def normalize(pattern: str) -> str:
376
+ return pattern.replace("**", "*")
377
+
378
+ # If include patterns specified, file must match at least one
379
+ if include:
380
+ if not any(fnmatch.fnmatch(filename, normalize(p)) for p in include):
381
+ return False
382
+
383
+ # If exclude patterns specified, file must not match any
384
+ if exclude:
385
+ if any(fnmatch.fnmatch(filename, normalize(p)) for p in exclude):
386
+ return False
387
+
388
+ return True
389
+
390
+
391
+ def _setup_cursor_rules(
392
+ project_root: Path,
393
+ include: list[str] | None = None,
394
+ exclude: list[str] | None = None,
395
+ ) -> None:
396
+ """Set up .cursor/rules/ with symlinks to docs/general/agent-rules/.
397
+
398
+ Note: Cursor requires .mdc extension, so we create symlinks with .mdc
399
+ extension pointing to the source .md files.
400
+
401
+ Supports include/exclude patterns for filtering which rules to link.
402
+ """
403
+ cursor_dir = project_root / ".cursor" / "rules"
404
+ cursor_dir.mkdir(parents=True, exist_ok=True)
405
+
406
+ rules_dir = project_root / "docs" / "general" / "agent-rules"
407
+ if not rules_dir.exists():
408
+ print_warning("docs/general/agent-rules/ not found, skipping Cursor setup")
409
+ return
410
+
411
+ linked_count = 0
412
+ skipped_count = 0
413
+ for rule_file in sorted(rules_dir.glob("*.md")):
414
+ # Check include/exclude patterns
415
+ if not _matches_patterns(rule_file.name, include, exclude):
416
+ skipped_count += 1
417
+ continue
418
+
419
+ # Cursor requires .mdc extension
420
+ link_name = rule_file.stem + ".mdc"
421
+ link_path = cursor_dir / link_name
422
+ if link_path.exists() or link_path.is_symlink():
423
+ link_path.unlink()
424
+
425
+ # Create relative symlink
426
+ relative_target = Path("..") / ".." / "docs" / "general" / "agent-rules" / rule_file.name
427
+ link_path.symlink_to(relative_target)
428
+ linked_count += 1
429
+
430
+ msg = f"Linked {linked_count} rules to .cursor/rules/"
431
+ if skipped_count:
432
+ msg += f" ({skipped_count} skipped by pattern)"
433
+ print_success(msg)
@@ -0,0 +1,149 @@
1
+ """
2
+ The `speculate` command installs and syncs structured agent documentation
3
+ into Git-based projects for use with Cursor, Claude Code, Codex, etc.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import argparse
9
+ import sys
10
+ from importlib.metadata import version
11
+ from textwrap import dedent
12
+
13
+ from clideps.utils.readable_argparse import ReadableColorFormatter, get_readable_console_width
14
+ from rich import get_console
15
+ from rich import print as rprint
16
+
17
+ from speculate.cli.cli_commands import init, install, status, update
18
+ from speculate.cli.cli_ui import print_cancelled, print_error
19
+
20
+ APP_NAME = "speculate"
21
+ DESCRIPTION = "speculate: Install and sync agent documentation"
22
+
23
+ ALL_COMMANDS = [init, update, install, status]
24
+
25
+
26
+ def get_version_name() -> str:
27
+ try:
28
+ return f"{APP_NAME} v{version(APP_NAME)}"
29
+ except Exception:
30
+ return f"{APP_NAME} (unknown version)"
31
+
32
+
33
+ def get_short_help(func: object) -> str:
34
+ """Extract the first paragraph from a function's docstring."""
35
+ doc = getattr(func, "__doc__", None)
36
+ if not doc or not isinstance(doc, str):
37
+ return ""
38
+ doc = doc.strip()
39
+ paragraphs = [p.strip() for p in doc.split("\n\n") if p.strip()]
40
+ if paragraphs:
41
+ return " ".join(paragraphs[0].split())
42
+ return ""
43
+
44
+
45
+ def build_parser() -> argparse.ArgumentParser:
46
+ parser = argparse.ArgumentParser(
47
+ formatter_class=ReadableColorFormatter,
48
+ epilog=dedent((__doc__ or "") + "\n\n" + get_version_name()),
49
+ description=DESCRIPTION,
50
+ )
51
+ parser.add_argument("--version", action="store_true", help="show version and exit")
52
+
53
+ subparsers = parser.add_subparsers(dest="subcommand", required=False)
54
+
55
+ for func in ALL_COMMANDS:
56
+ command_name = func.__name__.replace("_", "-")
57
+ subparser = subparsers.add_parser(
58
+ command_name,
59
+ help=get_short_help(func),
60
+ description=func.__doc__,
61
+ formatter_class=ReadableColorFormatter,
62
+ )
63
+
64
+ # Command-specific arguments
65
+ if func is init:
66
+ subparser.add_argument(
67
+ "destination",
68
+ nargs="?",
69
+ default=".",
70
+ help="target directory (default: current directory)",
71
+ )
72
+ subparser.add_argument(
73
+ "--overwrite",
74
+ action="store_true",
75
+ help="skip confirmation prompts",
76
+ )
77
+ subparser.add_argument(
78
+ "--template",
79
+ default="gh:jlevy/speculate",
80
+ help="template source (default: gh:jlevy/speculate)",
81
+ )
82
+ subparser.add_argument(
83
+ "--ref",
84
+ default="HEAD",
85
+ help="git ref for speculate docs files (tag, branch, commit); default: HEAD",
86
+ )
87
+
88
+ if func is install:
89
+ subparser.add_argument(
90
+ "--include",
91
+ action="append",
92
+ help="include only rules matching pattern (supports * and **)",
93
+ )
94
+ subparser.add_argument(
95
+ "--exclude",
96
+ action="append",
97
+ help="exclude rules matching pattern (supports * and **)",
98
+ )
99
+
100
+ return parser
101
+
102
+
103
+ def main() -> None:
104
+ get_console().width = get_readable_console_width()
105
+ parser = build_parser()
106
+ args = parser.parse_args()
107
+
108
+ if args.version:
109
+ rprint(get_version_name())
110
+ return
111
+
112
+ if not args.subcommand:
113
+ parser.print_help()
114
+ return
115
+
116
+ subcommand = args.subcommand.replace("-", "_")
117
+
118
+ try:
119
+ if subcommand == "init":
120
+ init(
121
+ destination=args.destination,
122
+ overwrite=args.overwrite,
123
+ template=args.template,
124
+ ref=args.ref,
125
+ )
126
+ elif subcommand == "update":
127
+ update()
128
+ elif subcommand == "install":
129
+ install(include=args.include, exclude=args.exclude)
130
+ elif subcommand == "status":
131
+ status()
132
+ else:
133
+ raise ValueError(f"Unknown subcommand: {subcommand}")
134
+ sys.exit(0)
135
+ except KeyboardInterrupt:
136
+ rprint()
137
+ print_cancelled()
138
+ sys.exit(130)
139
+ except SystemExit as e:
140
+ sys.exit(e.code)
141
+ except Exception as e:
142
+ rprint()
143
+ print_error(str(e))
144
+ rprint()
145
+ sys.exit(1)
146
+
147
+
148
+ if __name__ == "__main__":
149
+ main()
@@ -0,0 +1,76 @@
1
+ """
2
+ CLI output helpers for consistent formatting.
3
+
4
+ All user-facing output should go through these functions to ensure
5
+ consistent styling across the CLI.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+
12
+ from rich import print as rprint
13
+
14
+ # Standard indentation for continuation lines
15
+ _INDENT = " "
16
+
17
+
18
+ def print_header(title: str, path: Path | str | None = None) -> None:
19
+ """Print a section header with optional path on a new indented line."""
20
+ rprint(f"\n[bold]{title}[/bold]")
21
+ if path is not None:
22
+ rprint(f"{_INDENT}{path}")
23
+ rprint()
24
+
25
+
26
+ def print_success(message: str) -> None:
27
+ """Print a success message with green checkmark."""
28
+ rprint(f"[green]✔︎[/green] {message}")
29
+
30
+
31
+ def print_error(message: str, detail: str | None = None) -> None:
32
+ """Print an error message with red label and optional detail line."""
33
+ rprint(f"[red]Error:[/red] {message}")
34
+ if detail:
35
+ rprint(f"{_INDENT}{detail}")
36
+
37
+
38
+ def print_warning(message: str, detail: str | None = None) -> None:
39
+ """Print a warning message with yellow label and optional detail line."""
40
+ rprint(f"[yellow]Warning:[/yellow] {message}")
41
+ if detail:
42
+ rprint(f"{_INDENT}{detail}")
43
+
44
+
45
+ def print_note(message: str, detail: str | None = None) -> None:
46
+ """Print a note message with yellow label and optional detail line."""
47
+ rprint(f"[yellow]Note:[/yellow] {message}")
48
+ if detail:
49
+ rprint(f"{_INDENT}{detail}")
50
+
51
+
52
+ def print_missing(message: str) -> None:
53
+ """Print a missing/not-found item with yellow X."""
54
+ rprint(f"[yellow]✘[/yellow] {message}")
55
+
56
+
57
+ def print_error_item(message: str, detail: str | None = None) -> None:
58
+ """Print an error item with red X and optional detail line."""
59
+ rprint(f"[red]✘[/red] {message}")
60
+ if detail:
61
+ rprint(f"{_INDENT}{detail}")
62
+
63
+
64
+ def print_info(message: str) -> None:
65
+ """Print an info/neutral message with dim circle."""
66
+ rprint(f"[dim]○[/dim] {message}")
67
+
68
+
69
+ def print_detail(message: str) -> None:
70
+ """Print an indented detail/continuation line."""
71
+ rprint(f"{_INDENT}{message}")
72
+
73
+
74
+ def print_cancelled() -> None:
75
+ """Print cancellation message."""
76
+ rprint("[yellow]Cancelled[/yellow]")
speculate/py.typed ADDED
File without changes
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: speculate-cli
3
+ Version: 0.0.2
4
+ Summary: Structured agent specs, shortcuts, and doc workflows
5
+ Project-URL: Repository, https://github.com/jlevy/speculate-cli
6
+ Author-email: Joshua Levy <joshua@cal.berkeley.edu>
7
+ License-Expression: MIT
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: <4.0,>=3.11
19
+ Requires-Dist: clideps>=0.1.8
20
+ Requires-Dist: copier>=9.4.0
21
+ Requires-Dist: prettyfmt>=0.4.1
22
+ Requires-Dist: pyyaml>=6.0
23
+ Requires-Dist: rich>=14.0.0
24
+ Requires-Dist: strif>=3.0.1
25
+ Description-Content-Type: text/markdown
26
+
27
+ # speculate-cli
28
+
29
+ This is the Python-based CLI for the Speculate project structure.
30
+
31
+ * * *
32
+
33
+ ## Project Docs
34
+
35
+ For how to install uv and Python, see [installation.md](installation.md).
36
+
37
+ For development workflows, see [development.md](development.md).
38
+
39
+ For instructions on publishing to PyPI, see [publishing.md](publishing.md).
40
+
41
+ * * *
42
+
43
+ *This project was built from
44
+ [simple-modern-uv](https://github.com/jlevy/simple-modern-uv).*
@@ -0,0 +1,10 @@
1
+ speculate/__init__.py,sha256=AEYHIp-Is3cMs9OdI6lrfZNMMExks3LYB84cSAPui98,24
2
+ speculate/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ speculate/cli/__init__.py,sha256=fZqkBrsQpAhnVQIaaZUoaz5iQJd6b_qhLZSOSRWvgho,28
4
+ speculate/cli/cli_commands.py,sha256=c17I3zOQeKr2Nvi_-KHOjW5vi1kpEzQCdTTBgPKgRiE,13922
5
+ speculate/cli/cli_main.py,sha256=At-4t9mU8LFXfjMTDDnF4lRpWsVRbg44CtQfiRLPKy8,4397
6
+ speculate/cli/cli_ui.py,sha256=rK8DhefsdpZrOB1YWBcPL0xcUs0EtskrCNTMDb6YWUI,2206
7
+ speculate_cli-0.0.2.dist-info/METADATA,sha256=ESxyLwvyDZ1afX59lZE8HXlYMUJBbV1rsiXIWfgWZ3M,1386
8
+ speculate_cli-0.0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
+ speculate_cli-0.0.2.dist-info/entry_points.txt,sha256=3CpJKf0Lix6H804COTyRIaVw0T_PMItnVh0gWYdENyg,58
10
+ speculate_cli-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ speculate = speculate.cli.cli_main:main