Skip to content

Commit 2027733

Browse files
author
Dave Horner
committed
feat: add configuration & support scripts, update dependencies, and enhance main logic
- Introduced CI/CD configuration files (.github/workflows/release-plz.yml, bacon.toml) for automated builds and local checks. - Updated main logic in src/main.rs to support asynchronous GenAI chat functionality. - Enhanced clipboard integration via the arboard crate using conditional feature flags. - Added support/checks.sh to run comprehensive CI checks, including cargo commands and terminal launching for bacon clippy/cbacon. - Revised Cargo.toml and Cargo.lock with additional dependencies, updated metadata, and adjusted optional features. BREAKING CHANGE: Remove the clipboard dependency in the next release. The clipboard crate (v0.5.0) is unmaintained and has been flagged by cargo audit (RUSTSEC-2022-0056) for security issues, necessitating its removal for improved security and maintainability.
1 parent 1ca1468 commit 2027733

File tree

12 files changed

+1991
-66
lines changed

12 files changed

+1991
-66
lines changed

.aipack/config.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[default_options]
2+
3+
# `model` is required (any model supported by the Rust genai crate)
4+
# By default, it is set in the `~/.aipack-base/config.toml`, but it can be overridden here,
5+
# as with any other properties.
6+
# e.g.,
7+
# Ollama: "llama3.3:70b", "llama3.1:8b", "llama3.2:3b", "deepseek-r1:8b", "deepseek-coder-v2:16b" (or any locally installed Ollama)
8+
# Groq: "deepseek-r1-distill-llama-70b", "llama3-8b-8192", "llama-3.3-70b-versatile"
9+
# xAI: "grok-beta"
10+
# DeepSeek: "deepseek-chat", "deepseek-reasoner" (from deepseek.com)
11+
# Gemini: "gemini-2.0-flash", "gemini-2.0-pro-exp-02-05", "gemini-1.5-pro", "gemini-1.5-flash-8b"
12+
# Anthropic: "claude-3-7-sonnet-latest", "claude-3-5-haiku-latest"
13+
# OpenAI: "o3-mini", "o3-mini-high", "o3-mini-low", "gpt-4o", "gpt-4o-mini"
14+
# model = "gpt-4o-mini" # or an alias below (e.g., "fast", "claude")
15+
16+
# Temperature (by default unset)
17+
# temperature = 0.0
18+
19+
# How many inputs can be processed at the same time (Defaults to 1 if absent)
20+
# input_concurrency = 6
21+
22+
# Add or override model aliases
23+
# model_aliases = { "r1" = "deepseek-reasoner" }

.aipack/gen/crate_recreator.cmd

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@echo off
2+
REM Crate Recreator Batch Wrapper
3+
REM This batch file calls the corresponding Python script with the same base name.
4+
REM For example, if this file is named "crate_recreator.cmd", it will run "crate_recreator.py".
5+
REM All arguments passed to this .cmd file are forwarded to the Python script.
6+
7+
REM Get the name of this script without extension.
8+
set "SCRIPT_NAME=%~n0"
9+
10+
REM Build the path to the Python script (assumed to be in the same directory).
11+
set "PY_SCRIPT=%~dp0%SCRIPT_NAME%.py"
12+
13+
REM Check if the Python script exists.
14+
if not exist "%PY_SCRIPT%" (
15+
echo [ERROR] Python script "%PY_SCRIPT%" not found.
16+
pause
17+
exit /b 1
18+
)
19+
20+
REM Execute the Python script with all passed arguments.
21+
python "%PY_SCRIPT%" %*
22+

.aipack/gen/crate_recreator.py

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
#!/usr/bin/env python3
2+
"""
3+
crate_recreator.py
4+
5+
This script generates a self-contained Python script that, when executed, recreates
6+
the directory structure and file contents of a Rust crate. The process is as follows:
7+
8+
1. Starting from a specified source folder (or using --src-only to restrict to its "src" subfolder),
9+
the script searches upward for a Cargo.toml file. When found, it uses the folder containing
10+
Cargo.toml as the crate root.
11+
2. It extracts the crate name from the Cargo.toml’s [package] section. If not found, it falls back
12+
to using the basename of the originally provided folder.
13+
3. The script then gathers all files recursively from the determined root (or root/src if --src-only),
14+
ignoring directories like ".git", "target", ".aipack", ".github" and files such as ".gitignore",
15+
"Cargo.lock", or any file whose name starts with "LICENSE" or "NOTICE", as well as binary files
16+
(e.g., .webp, .jpg, .jpeg, .png).
17+
4. It generates a self-contained Python script that will recreate the crate’s structure and embedded file contents.
18+
In the generated script each embedded file is annotated with a comment showing its relative path.
19+
5. The generated file is named in the format: <crate_name>_recreate_YYMMDD_HHMM.py.
20+
6. The generated script is set as executable and its content is copied to the clipboard.
21+
(This copied content is the generated recreate script—not this crate recreator.)
22+
23+
Usage:
24+
python crate_recreator.py <source_folder> [--src-only]
25+
"""
26+
27+
import os
28+
import sys
29+
import argparse
30+
import subprocess
31+
import stat
32+
from datetime import datetime
33+
import re
34+
35+
def gather_files(source_folder):
36+
"""
37+
Walk the source folder recursively and return a dictionary mapping
38+
relative file paths to their contents.
39+
40+
Excludes directories named "target" (case-insensitive), ".git", ".aipack", ".github",
41+
and files such as ".gitignore", "Cargo.lock", or any file whose name starts with
42+
"LICENSE" or "NOTICE". Also ignores binary files with extensions like .webp, .jpg, .jpeg, .png.
43+
44+
Provides detailed tracing for debugging.
45+
"""
46+
ignore_dirs = {".git", ".aipack", ".github"}
47+
ignore_files = {".gitignore", "Cargo.lock"}
48+
binary_extensions = (".webp", ".jpg", ".jpeg", ".png")
49+
files_dict = {}
50+
print(f"[TRACE] Starting to traverse source folder: {source_folder}")
51+
for root, dirs, files in os.walk(source_folder):
52+
# Exclude specified directories.
53+
original_dirs = dirs.copy()
54+
dirs[:] = [d for d in dirs if d.lower() != "target" and d not in ignore_dirs]
55+
for excluded in set(original_dirs) - set(dirs):
56+
print(f"[TRACE] Excluding directory: {excluded}")
57+
# Process each file in the current directory.
58+
for file in files:
59+
# Check ignore conditions for file names.
60+
if file in ignore_files or file.endswith(('.bak', '~')):
61+
print(f"[TRACE] Ignoring file: {file}")
62+
continue
63+
if file.startswith("LICENSE") or file.startswith("NOTICE"):
64+
print(f"[TRACE] Ignoring file (starts with LICENSE or NOTICE): {file}")
65+
continue
66+
lower_file = file.lower()
67+
if lower_file.endswith(binary_extensions):
68+
print(f"[TRACE] Ignoring binary file: {file}")
69+
continue
70+
71+
full_path = os.path.join(root, file)
72+
# Compute relative path based on the source folder.
73+
rel_path = os.path.relpath(full_path, source_folder)
74+
print(f"[TRACE] Processing file: {full_path} as {rel_path}")
75+
try:
76+
with open(full_path, "r", encoding="utf-8") as f:
77+
content = f.read()
78+
except Exception as e:
79+
print(f"[WARNING] Skipping file {full_path} due to read error: {e}")
80+
continue
81+
files_dict[rel_path] = content
82+
print(f"[TRACE] Completed traversing. Total files gathered: {len(files_dict)}")
83+
return files_dict
84+
85+
def generate_script(files_dict, crate_name):
86+
"""
87+
Generate a self-contained Python script as a string.
88+
When run, the generated script creates a folder (named after the crate)
89+
and reconstructs all files with their original contents.
90+
91+
This generated script also includes:
92+
- A function to copy its own source code to the clipboard.
93+
- Detailed tracing messages to follow its execution.
94+
95+
Each embedded file is annotated with a trailing comment indicating its relative path.
96+
97+
Returns:
98+
A string containing the full source code of the generated script.
99+
"""
100+
lines = []
101+
lines.append("#!/usr/bin/env python3")
102+
lines.append("import os")
103+
lines.append("import sys")
104+
lines.append("import subprocess")
105+
lines.append("")
106+
lines.append("def copy_to_clipboard(text):")
107+
lines.append(" \"\"\"")
108+
lines.append(" Copies the given text to the system clipboard.")
109+
lines.append(" Uses 'clip' on Windows and 'pbcopy' on macOS.")
110+
lines.append(" \"\"\"")
111+
lines.append(" try:")
112+
lines.append(" if sys.platform.startswith('win'):")
113+
lines.append(" proc = subprocess.Popen(['clip'], stdin=subprocess.PIPE, close_fds=True)")
114+
lines.append(" proc.communicate(input=text.encode('utf-8'))")
115+
lines.append(" elif sys.platform == 'darwin':")
116+
lines.append(" proc = subprocess.Popen(['pbcopy'], stdin=subprocess.PIPE, close_fds=True)")
117+
lines.append(" proc.communicate(input=text.encode('utf-8'))")
118+
lines.append(" else:")
119+
lines.append(" print('[TRACE] Clipboard copy not supported on this platform.')")
120+
lines.append(" except Exception as e:")
121+
lines.append(" print(f'[ERROR] Failed to copy to clipboard: {e}')")
122+
lines.append("")
123+
lines.append("def copy_self_to_clipboard():")
124+
lines.append(" \"\"\"")
125+
lines.append(" Reads its own source file and copies the content to the clipboard.")
126+
lines.append(" Provides detailed tracing for debugging.")
127+
lines.append(" \"\"\"")
128+
lines.append(" try:")
129+
lines.append(" with open(__file__, 'r', encoding='utf-8') as f:")
130+
lines.append(" content = f.read()")
131+
lines.append(" copy_to_clipboard(content)")
132+
lines.append(" print('[TRACE] The script has been copied to the clipboard.')")
133+
lines.append(" except Exception as e:")
134+
lines.append(" print(f'[ERROR] Failed to copy self to clipboard: {e}')")
135+
lines.append("")
136+
lines.append("def create_crate():")
137+
lines.append(" \"\"\"")
138+
lines.append(" Recreates the directory structure and files for the crate.")
139+
lines.append(" Provides detailed tracing for each step.")
140+
lines.append(" \"\"\"")
141+
lines.append(f" base_folder = os.path.join(os.getcwd(), '{crate_name}')")
142+
lines.append(" print(f'[TRACE] Creating base folder: {base_folder}')")
143+
lines.append(" os.makedirs(base_folder, exist_ok=True)")
144+
lines.append(" files = {")
145+
# Embed each file's content along with a comment indicating its relative path.
146+
for path, content in files_dict.items():
147+
lines.append(f" {repr(path)}: {repr(content)}, # File: {path}")
148+
lines.append(" }")
149+
lines.append("")
150+
lines.append(" for relative_path, content in files.items():")
151+
lines.append(" full_path = os.path.join(base_folder, relative_path)")
152+
lines.append(" directory = os.path.dirname(full_path)")
153+
lines.append(" if not os.path.exists(directory):")
154+
lines.append(" os.makedirs(directory, exist_ok=True)")
155+
lines.append(" print(f'[TRACE] Created directory: {directory}')")
156+
lines.append(" with open(full_path, 'w', encoding='utf-8') as f:")
157+
lines.append(" f.write(content)")
158+
lines.append(" print(f'[TRACE] Created file: {full_path}')")
159+
lines.append("")
160+
lines.append("if __name__ == '__main__':")
161+
lines.append(" create_crate()")
162+
lines.append(" # Uncomment the next line to enable self-copy functionality.")
163+
lines.append(" # copy_self_to_clipboard()")
164+
lines.append(" print('[TRACE] Crate creation complete.')")
165+
166+
return "\n".join(lines)
167+
168+
def copy_to_clipboard(text):
169+
"""
170+
Copies the given text to the system clipboard.
171+
Uses 'clip' on Windows and 'pbcopy' on macOS.
172+
Provides tracing information.
173+
"""
174+
try:
175+
if sys.platform.startswith("win"):
176+
print("[TRACE] Using 'clip' for clipboard copy on Windows.")
177+
proc = subprocess.Popen(["clip"], stdin=subprocess.PIPE, close_fds=True)
178+
proc.communicate(input=text.encode("utf-8"))
179+
elif sys.platform == "darwin":
180+
print("[TRACE] Using 'pbcopy' for clipboard copy on macOS.")
181+
proc = subprocess.Popen(["pbcopy"], stdin=subprocess.PIPE, close_fds=True)
182+
proc.communicate(input=text.encode("utf-8"))
183+
else:
184+
print("[TRACE] Clipboard copy not supported on this platform.")
185+
except Exception as e:
186+
print(f"[ERROR] Failed to copy to clipboard: {e}")
187+
188+
def find_cargo_toml(start_dir):
189+
"""
190+
Starting from start_dir, search upward for a Cargo.toml file.
191+
Returns the path to Cargo.toml if found, otherwise None.
192+
"""
193+
current_dir = start_dir
194+
while True:
195+
candidate = os.path.join(current_dir, "Cargo.toml")
196+
if os.path.exists(candidate):
197+
print(f"[TRACE] Found Cargo.toml at: {candidate}")
198+
return candidate
199+
parent = os.path.dirname(current_dir)
200+
if parent == current_dir: # Reached root directory
201+
break
202+
current_dir = parent
203+
print("[TRACE] No Cargo.toml found in the directory hierarchy.")
204+
return None
205+
206+
def get_crate_name_from_cargo_toml(cargo_toml_path):
207+
"""
208+
Parse the Cargo.toml file to extract the crate name from the [package] section.
209+
Returns the crate name if found, otherwise None.
210+
"""
211+
in_package = False
212+
try:
213+
with open(cargo_toml_path, "r", encoding="utf-8") as f:
214+
for line in f:
215+
stripped = line.strip()
216+
if stripped.startswith("[package]"):
217+
in_package = True
218+
elif stripped.startswith("[") and in_package:
219+
# Exiting the [package] section.
220+
break
221+
elif in_package and stripped.startswith("name"):
222+
match = re.search(r'name\s*=\s*["\'](.+?)["\']', stripped)
223+
if match:
224+
crate_name = match.group(1)
225+
print(f"[TRACE] Crate name found in Cargo.toml: {crate_name}")
226+
return crate_name
227+
except Exception as e:
228+
print(f"[ERROR] Failed to parse Cargo.toml: {e}")
229+
return None
230+
231+
def main():
232+
parser = argparse.ArgumentParser(
233+
description="Generate a self-contained Python script that recreates a Rust crate with enhanced features."
234+
)
235+
parser.add_argument("source_folder", help="Path to the source folder (should be within or at the crate root).")
236+
parser.add_argument("--src-only", action="store_true", help="Process only the 'src' folder inside the crate root.")
237+
args = parser.parse_args()
238+
239+
# Resolve the absolute path of the provided source folder.
240+
orig_source_folder = os.path.abspath(args.source_folder)
241+
print(f"[TRACE] Source folder resolved to: {orig_source_folder}")
242+
243+
# Search upward for Cargo.toml from the provided folder.
244+
cargo_toml_path = find_cargo_toml(orig_source_folder)
245+
if cargo_toml_path:
246+
# Use the directory containing Cargo.toml as the crate root.
247+
crate_root = os.path.dirname(cargo_toml_path)
248+
crate_name_from_toml = get_crate_name_from_cargo_toml(cargo_toml_path)
249+
if crate_name_from_toml:
250+
crate_name = crate_name_from_toml
251+
print(f"[TRACE] Using crate name from Cargo.toml: {crate_name}")
252+
else:
253+
crate_name = os.path.basename(orig_source_folder.rstrip(os.sep))
254+
print("[TRACE] Cargo.toml found but crate name could not be extracted; using fallback name.")
255+
else:
256+
crate_root = orig_source_folder
257+
crate_name = os.path.basename(orig_source_folder.rstrip(os.sep))
258+
print("[TRACE] No Cargo.toml found; using fallback crate name.")
259+
260+
# Determine the folder to gather files from.
261+
if args.src_only:
262+
print("[TRACE] --src-only flag is set; processing only the 'src' subfolder.")
263+
source_folder = os.path.join(crate_root, "src")
264+
else:
265+
source_folder = crate_root
266+
267+
if not os.path.exists(source_folder):
268+
print(f"[ERROR] Source folder '{source_folder}' does not exist.")
269+
sys.exit(1)
270+
271+
# Gather files from the determined source folder.
272+
files_dict = gather_files(source_folder)
273+
274+
# Generate the script content (this is the recreate script that will be copied to clipboard).
275+
generated_script = generate_script(files_dict, crate_name)
276+
277+
# Create a timestamped output file name in the format: <crate_name>_recreate_YYMMDD_HHMM.py
278+
timestamp = datetime.now().strftime("%y%m%d_%H%M")
279+
output_file = f"{crate_name}_recreate_{timestamp}.py"
280+
print(f"[TRACE] Writing generated script to: {output_file}")
281+
with open(output_file, "w", encoding="utf-8") as f:
282+
f.write(generated_script)
283+
284+
# Set the generated script to be executable.
285+
try:
286+
st = os.stat(output_file)
287+
os.chmod(output_file, st.st_mode | stat.S_IEXEC)
288+
print(f"[TRACE] Set executable permission for {output_file}.")
289+
except Exception as e:
290+
print(f"[ERROR] Failed to set executable permission for {output_file}: {e}")
291+
292+
print(f"[TRACE] Generated script saved to {output_file}.")
293+
294+
# Copy the generated recreate script's content to the clipboard.
295+
# (This is the script that, when run, will recreate the crate.)
296+
copy_to_clipboard(generated_script)
297+
print("[TRACE] Generated script copied to clipboard.")
298+
299+
if __name__ == "__main__":
300+
main()
301+

0 commit comments

Comments
 (0)