fixdoc 0.0.1__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.
@@ -0,0 +1,298 @@
1
+ """Capture handlers for different input types."""
2
+
3
+ from typing import Optional
4
+
5
+ import click
6
+
7
+ from ..models import Fix
8
+ from ..parsers import (
9
+ detect_error_source,
10
+ detect_and_parse,
11
+ ErrorSource,
12
+ TerraformError,
13
+ KubernetesError,
14
+ )
15
+
16
+
17
+ def handle_piped_input(output: str, tags: Optional[str]) -> Optional[Fix]:
18
+ """
19
+ Handle piped input by detecting the source and routing appropriately.
20
+
21
+ This is the main entry point for piped input handling. It detects
22
+ whether the input is from Terraform, Kubernetes, or another source.
23
+ """
24
+ source = detect_error_source(output)
25
+
26
+ if source == ErrorSource.TERRAFORM:
27
+ return handle_terraform_capture(output, tags)
28
+ elif source in (ErrorSource.KUBERNETES, ErrorSource.HELM):
29
+ return handle_kubernetes_capture(output, tags)
30
+ else:
31
+ return handle_generic_piped_capture(output, tags)
32
+
33
+
34
+ def handle_terraform_capture(output: str, tags: Optional[str]) -> Optional[Fix]:
35
+ """Handle Terraform output with multi-cloud support."""
36
+ errors = detect_and_parse(output)
37
+
38
+ if not errors:
39
+ click.echo("No Terraform errors found in input", err=True)
40
+ return None
41
+
42
+ # Use the first error (or could prompt to select)
43
+ err = errors[0]
44
+
45
+ # Display captured error info
46
+ click.echo("─" * 50)
47
+ click.echo("Captured from Terraform:\n")
48
+ click.echo(f" Provider: {err.cloud_provider.value.upper()}")
49
+ click.echo(f" Resource: {err.resource_address}")
50
+ if err.file:
51
+ click.echo(f" File: {err.file}:{err.line}")
52
+ if err.error_code:
53
+ click.echo(f" Code: {err.error_code}")
54
+ click.echo(f" Error: {err.short_error()}")
55
+
56
+ # Show suggestions if available
57
+ if err.suggestions:
58
+ click.echo("\n Suggestions:")
59
+ for suggestion in err.suggestions[:3]:
60
+ click.echo(f" • {suggestion}")
61
+
62
+ click.echo("─" * 50)
63
+
64
+ # If multiple errors, show count
65
+ if len(errors) > 1:
66
+ click.echo(f"\n ({len(errors) - 1} additional error(s) not shown)\n")
67
+
68
+ # Prompt for resolution
69
+ resolution = click.prompt("\n What fixed this?")
70
+ issue = err.to_issue_string()
71
+
72
+ # Auto-generate tags
73
+ auto_tags = err.generate_tags()
74
+ if tags:
75
+ auto_tags = f"{auto_tags},{tags}"
76
+
77
+ final_tags = click.prompt("Tags", default=auto_tags, show_default=True)
78
+
79
+ # Optional notes
80
+ notes_default = ""
81
+ if err.file:
82
+ notes_default = f"File: {err.file}:{err.line}"
83
+ if err.suggestions:
84
+ notes_default += "\nSuggestions: " + "; ".join(err.suggestions[:2])
85
+
86
+ notes = click.prompt("Notes (optional)", default=notes_default, show_default=False)
87
+
88
+ return Fix(
89
+ issue=issue,
90
+ resolution=resolution,
91
+ error_excerpt=output[:2000],
92
+ tags=final_tags,
93
+ notes=notes or None,
94
+ )
95
+
96
+
97
+ def handle_kubernetes_capture(output: str, tags: Optional[str]) -> Optional[Fix]:
98
+ """Handle Kubernetes (kubectl/Helm) output."""
99
+ errors = detect_and_parse(output)
100
+
101
+ if not errors:
102
+ click.echo("No Kubernetes errors found in input", err=True)
103
+ return None
104
+
105
+ err = errors[0]
106
+
107
+ # Display captured error info
108
+ click.echo("─" * 50)
109
+
110
+ # Determine source label
111
+ if hasattr(err, 'helm_release') and err.helm_release:
112
+ source_label = "Helm"
113
+ else:
114
+ source_label = "Kubernetes"
115
+
116
+ click.echo(f"Captured from {source_label}:\n")
117
+
118
+ if err.namespace:
119
+ click.echo(f" Namespace: {err.namespace}")
120
+ if err.resource_type:
121
+ click.echo(f" Resource: {err.resource_type}/{err.resource_name or 'unknown'}")
122
+
123
+ # Kubernetes-specific fields
124
+ if isinstance(err, KubernetesError):
125
+ if err.helm_release:
126
+ click.echo(f" Release: {err.helm_release}")
127
+ if err.helm_chart:
128
+ click.echo(f" Chart: {err.helm_chart}")
129
+ if err.pod_name:
130
+ click.echo(f" Pod: {err.pod_name}")
131
+ if err.restart_count is not None:
132
+ click.echo(f" Restarts: {err.restart_count}")
133
+ if err.exit_code is not None:
134
+ click.echo(f" Exit Code: {err.exit_code}")
135
+
136
+ if err.error_code:
137
+ click.echo(f" Status: {err.error_code}")
138
+ click.echo(f" Error: {err.short_error()}")
139
+
140
+ # Show suggestions if available
141
+ if err.suggestions:
142
+ click.echo("\n Suggestions:")
143
+ for suggestion in err.suggestions[:3]:
144
+ click.echo(f" • {suggestion}")
145
+
146
+ click.echo("─" * 50)
147
+
148
+ # If multiple errors, show count
149
+ if len(errors) > 1:
150
+ click.echo(f"\n ({len(errors) - 1} additional error(s) not shown)\n")
151
+
152
+ # Prompt for resolution
153
+ resolution = click.prompt("\n What fixed this?")
154
+ issue = err.to_issue_string()
155
+
156
+ # Auto-generate tags
157
+ auto_tags = err.generate_tags()
158
+ if tags:
159
+ auto_tags = f"{auto_tags},{tags}"
160
+
161
+ final_tags = click.prompt("Tags", default=auto_tags, show_default=True)
162
+
163
+ # Optional notes with helpful context
164
+ notes_parts = []
165
+ if isinstance(err, KubernetesError):
166
+ if err.namespace:
167
+ notes_parts.append(f"Namespace: {err.namespace}")
168
+ if err.pod_name:
169
+ notes_parts.append(f"Pod: {err.pod_name}")
170
+ if err.suggestions:
171
+ notes_parts.append("Suggestions: " + "; ".join(err.suggestions[:2]))
172
+
173
+ notes_default = "\n".join(notes_parts)
174
+ notes = click.prompt("Notes (optional)", default=notes_default, show_default=False)
175
+
176
+ return Fix(
177
+ issue=issue,
178
+ resolution=resolution,
179
+ error_excerpt=output[:2000],
180
+ tags=final_tags,
181
+ notes=notes or None,
182
+ )
183
+
184
+
185
+ def handle_generic_piped_capture(piped_input: str, tags: Optional[str]) -> Fix:
186
+ """Handle generic piped input - treat as error excerpt."""
187
+ click.echo("─" * 50)
188
+ click.echo("Captured generic input (unknown source)")
189
+ click.echo("─" * 50)
190
+ click.echo("\nPlease provide fix details:\n")
191
+
192
+ issue = click.prompt("What was the issue?")
193
+ resolution = click.prompt("How was it resolved?")
194
+
195
+ if not tags:
196
+ tags = click.prompt("Tags (optional)", default="", show_default=False)
197
+
198
+ notes = click.prompt("Notes (optional)", default="", show_default=False)
199
+
200
+ return Fix(
201
+ issue=issue,
202
+ resolution=resolution,
203
+ error_excerpt=piped_input[:2000],
204
+ tags=tags or None,
205
+ notes=notes or None,
206
+ )
207
+
208
+
209
+ def handle_quick_capture(quick: str, tags: Optional[str]) -> Fix:
210
+ """Handle quick capture mode."""
211
+ if "|" in quick:
212
+ parts = quick.split("|", 1)
213
+ issue = parts[0].strip()
214
+ resolution = parts[1].strip()
215
+ else:
216
+ issue = quick.strip()
217
+ resolution = click.prompt("Resolution")
218
+
219
+ return Fix(issue=issue, resolution=resolution, tags=tags)
220
+
221
+
222
+ def handle_interactive_capture(tags: Optional[str]) -> Fix:
223
+ """Handle interactive capture mode."""
224
+ click.echo("─" * 50)
225
+ click.echo("Capturing a new fix...")
226
+ click.echo("─" * 50)
227
+ click.echo()
228
+
229
+ issue = click.prompt("What was the issue?")
230
+ resolution = click.prompt("How was it resolved?")
231
+
232
+ error_excerpt = click.prompt(
233
+ "Error excerpt (optional)", default="", show_default=False
234
+ )
235
+
236
+ if not tags:
237
+ tags = click.prompt("Tags (optional)", default="", show_default=False)
238
+
239
+ notes = click.prompt("Notes (optional)", default="", show_default=False)
240
+
241
+ return Fix(
242
+ issue=issue,
243
+ resolution=resolution,
244
+ error_excerpt=error_excerpt or None,
245
+ tags=tags or None,
246
+ notes=notes or None,
247
+ )
248
+
249
+
250
+ def handle_multi_error_capture(output: str, tags: Optional[str]) -> list[Fix]:
251
+ """
252
+ Handle output with multiple errors, creating a fix for each.
253
+
254
+ This is useful for batch processing multiple errors from a single
255
+ Terraform apply or kubectl operation.
256
+ """
257
+ errors = detect_and_parse(output)
258
+
259
+ if not errors:
260
+ click.echo("No errors found in input", err=True)
261
+ return []
262
+
263
+ fixes = []
264
+
265
+ click.echo(f"\nFound {len(errors)} error(s). Processing each:\n")
266
+
267
+ for i, err in enumerate(errors, 1):
268
+ click.echo("─" * 50)
269
+ click.echo(f"Error {i}/{len(errors)}:")
270
+ click.echo(f" Resource: {err.resource_address or err.resource_name or 'unknown'}")
271
+ click.echo(f" Error: {err.short_error()}")
272
+ click.echo("─" * 50)
273
+
274
+ # Ask if user wants to create a fix for this error
275
+ create = click.confirm(f"Create fix for this error?", default=True)
276
+ if not create:
277
+ continue
278
+
279
+ resolution = click.prompt("What fixed this?")
280
+ issue = err.to_issue_string()
281
+
282
+ auto_tags = err.generate_tags()
283
+ if tags:
284
+ auto_tags = f"{auto_tags},{tags}"
285
+
286
+ final_tags = click.prompt("Tags", default=auto_tags, show_default=True)
287
+
288
+ fix = Fix(
289
+ issue=issue,
290
+ resolution=resolution,
291
+ error_excerpt=err.raw_output[:2000],
292
+ tags=final_tags,
293
+ )
294
+ fixes.append(fix)
295
+
296
+ click.echo(f"✓ Fix created\n")
297
+
298
+ return fixes
@@ -0,0 +1,72 @@
1
+ """Delete command for fixdoc CLI."""
2
+
3
+ import click
4
+
5
+ from ..storage import FixRepository
6
+
7
+
8
+ def get_repo() -> FixRepository:
9
+ """Get the fix repository instance."""
10
+ return FixRepository()
11
+
12
+
13
+ @click.command()
14
+ @click.argument("fix_id", required=False)
15
+ @click.option("--purge", is_flag=True, help="Delete all fixes")
16
+ @click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
17
+ def delete(fix_id: str, purge: bool, yes: bool):
18
+ """
19
+ Delete a fix by ID, or purge all fixes.
20
+
21
+ \b
22
+ Examples:
23
+ fixdoc delete a1b2c3d4
24
+ fixdoc delete --purge
25
+ fixdoc delete --purge -y
26
+ """
27
+ repo = get_repo()
28
+
29
+ if purge:
30
+ _purge_all(repo, yes)
31
+ elif fix_id:
32
+ _delete_one(repo, fix_id, yes)
33
+ else:
34
+ click.echo("Provide a fix ID or use --purge to delete all.")
35
+ raise SystemExit(1)
36
+
37
+
38
+ def _purge_all(repo: FixRepository, skip_confirm: bool):
39
+ """Delete all fixes."""
40
+ count = repo.count()
41
+
42
+ if count == 0:
43
+ click.echo("No fixes to delete.")
44
+ return
45
+
46
+ if not skip_confirm:
47
+ if not click.confirm(f"Delete all {count} fixes?"):
48
+ click.echo("Aborted.")
49
+ return
50
+
51
+ repo.purge()
52
+ click.echo(f"✓ Purged {count} fixes.")
53
+
54
+
55
+ def _delete_one(repo: FixRepository, fix_id: str, skip_confirm: bool):
56
+ """Delete a single fix."""
57
+ fix = repo.get(fix_id)
58
+
59
+ if not fix:
60
+ click.echo(f"No fix found with ID starting with '{fix_id}'")
61
+ raise SystemExit(1)
62
+
63
+ if not skip_confirm:
64
+ if not click.confirm(f"Delete fix {fix.id[:8]}?"):
65
+ click.echo("Aborted.")
66
+ return
67
+
68
+ if repo.delete(fix_id):
69
+ click.echo(f"✓ Deleted fix: {fix.id[:8]}")
70
+ else:
71
+ click.echo("Failed to delete fix")
72
+ raise SystemExit(1)
@@ -0,0 +1,118 @@
1
+
2
+ from typing import Optional
3
+
4
+ import click
5
+
6
+ from ..models import Fix
7
+ from ..storage import FixRepository
8
+
9
+
10
+ def get_repo() -> FixRepository:
11
+ return FixRepository()
12
+
13
+
14
+ @click.command()
15
+ @click.argument("fix_id")
16
+ @click.option("--issue", "-i", type=str, help="Update the issue description")
17
+ @click.option("--resolution", "-r", type=str, help="Update the resolution")
18
+ @click.option("--tags", "-t", type=str, help="Update tags")
19
+ @click.option("--notes", "-n", type=str, help="Update notes")
20
+ @click.option("--error", "-e", type=str, help="Update error excerpt")
21
+ @click.option("--interactive", "-I", is_flag=True, help="Edit all fields interactively")
22
+ def edit(
23
+ fix_id: str,
24
+ issue: Optional[str],
25
+ resolution: Optional[str],
26
+ tags: Optional[str],
27
+ notes: Optional[str],
28
+ error: Optional[str],
29
+ interactive: bool,
30
+ ):
31
+ """
32
+ Edit an existing fix.
33
+
34
+ \b
35
+ Examples:
36
+ fixdoc edit a1b2c3d4 --resolution "New fix details"
37
+ fixdoc edit a1b2c3d4 --tags "storage,rbac,new_tag"
38
+ fixdoc edit a1b2c3d4 -I # Interactive mode
39
+ """
40
+ repo = get_repo()
41
+
42
+ fix = repo.get(fix_id)
43
+ if not fix:
44
+ click.echo(f"No fix found with ID starting with '{fix_id}'")
45
+ raise SystemExit(1)
46
+
47
+ if interactive:
48
+ fix = _interactive_edit(fix)
49
+ else:
50
+ fix = _flag_edit(fix, issue, resolution, tags, notes, error)
51
+
52
+ if fix:
53
+ fix.touch()
54
+ repo.save(fix)
55
+ click.echo(f"✓ Updated fix: {fix.id[:8]}")
56
+
57
+
58
+ def _interactive_edit(fix: Fix) -> Fix:
59
+ """Edit all fields interactively."""
60
+ click.echo(f"Editing fix: {fix.id[:8]}\n")
61
+ click.echo("Press Enter to keep current value.\n")
62
+
63
+ new_issue = click.prompt("Issue", default=fix.issue, show_default=True)
64
+ new_resolution = click.prompt("Resolution", default=fix.resolution, show_default=True)
65
+
66
+ new_error = click.prompt(
67
+ "Error excerpt",
68
+ default=fix.error_excerpt or "",
69
+ show_default=True if fix.error_excerpt else False,
70
+ )
71
+
72
+ new_tags = click.prompt(
73
+ "Tags",
74
+ default=fix.tags or "",
75
+ show_default=True if fix.tags else False,
76
+ )
77
+
78
+ new_notes = click.prompt(
79
+ "Notes",
80
+ default=fix.notes or "",
81
+ show_default=True if fix.notes else False,
82
+ )
83
+
84
+ fix.issue = new_issue
85
+ fix.resolution = new_resolution
86
+ fix.error_excerpt = new_error or None
87
+ fix.tags = new_tags or None
88
+ fix.notes = new_notes or None
89
+
90
+ return fix
91
+
92
+
93
+ def _flag_edit(
94
+ fix: Fix,
95
+ issue: Optional[str],
96
+ resolution: Optional[str],
97
+ tags: Optional[str],
98
+ notes: Optional[str],
99
+ error: Optional[str],
100
+ ) -> Optional[Fix]:
101
+ """Edit specific fields via flags."""
102
+ if not any([issue, resolution, tags, notes, error]):
103
+ click.echo("No changes specified. Use flags or -I for interactive mode.")
104
+ click.echo("Run 'fixdoc edit --help' for options.")
105
+ return None
106
+
107
+ if issue:
108
+ fix.issue = issue
109
+ if resolution:
110
+ fix.resolution = resolution
111
+ if tags:
112
+ fix.tags = tags
113
+ if notes:
114
+ fix.notes = notes
115
+ if error:
116
+ fix.error_excerpt = error
117
+
118
+ return fix
@@ -0,0 +1,67 @@
1
+ """List and stats commands for fixdoc CLI."""
2
+
3
+ import click
4
+
5
+ from ..storage import FixRepository
6
+
7
+
8
+ def get_repo() -> FixRepository:
9
+ """Get the fix repository instance."""
10
+ return FixRepository()
11
+
12
+
13
+ @click.command(name="list")
14
+ @click.option("--limit", "-l", type=int, default=20, help="Max fixes to show")
15
+ def list_fixes(limit: int):
16
+ """
17
+ List all captured fixes.
18
+
19
+ Shows a summary of each fix, most recent first.
20
+ """
21
+ repo = get_repo()
22
+ fixes = repo.list_all()
23
+
24
+ if not fixes:
25
+ click.echo("No fixes captured yet. Run `fixdoc capture` to add one.")
26
+ return
27
+
28
+ fixes.sort(key=lambda f: f.created_at, reverse=True)
29
+
30
+ click.echo(f"Total fixes: {len(fixes)}\n")
31
+
32
+ for fix in fixes[:limit]:
33
+ click.echo(f" {fix.summary()}")
34
+
35
+ if len(fixes) > limit:
36
+ click.echo(f"\n ... and {len(fixes) - limit} more. Use --limit to see more.")
37
+
38
+
39
+ @click.command()
40
+ def stats():
41
+ """Show statistics about your fix database."""
42
+ repo = get_repo()
43
+ fixes = repo.list_all()
44
+
45
+ if not fixes:
46
+ click.echo("No fixes captured yet.")
47
+ return
48
+
49
+ all_tags = []
50
+ for fix in fixes:
51
+ if fix.tags:
52
+ all_tags.extend([t.strip() for t in fix.tags.split(",")])
53
+
54
+ tag_counts = {}
55
+ for tag in all_tags:
56
+ tag_counts[tag] = tag_counts.get(tag, 0) + 1
57
+
58
+ click.echo(" Fix Database Statistics\n")
59
+ click.echo(f" Total fixes: {len(fixes)}")
60
+ click.echo(f" Fixes with tags: {sum(1 for f in fixes if f.tags)}")
61
+ click.echo(f" Fixes with error excerpts: {sum(1 for f in fixes if f.error_excerpt)}")
62
+
63
+ if tag_counts:
64
+ click.echo("\n Top tags:")
65
+ sorted_tags = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)
66
+ for tag, count in sorted_tags[:10]:
67
+ click.echo(f" {tag}: {count}")
@@ -0,0 +1,65 @@
1
+ """Search command for fixdoc CLI."""
2
+
3
+ import click
4
+
5
+ from ..storage import FixRepository
6
+ from ..formatter import fix_to_markdown
7
+
8
+
9
+ def get_repo() -> FixRepository:
10
+ """Get the fix repository instance."""
11
+ return FixRepository()
12
+
13
+
14
+ @click.command()
15
+ @click.argument("query")
16
+ @click.option("--limit", "-l", type=int, default=10, help="Max results to show")
17
+ def search(query: str, limit: int):
18
+ """
19
+ Search your fixes by keyword.
20
+
21
+ Searches across issue, resolution, error excerpt, tags, and notes.
22
+
23
+ \b
24
+ Examples:
25
+ fixdoc search "storage account"
26
+ fixdoc search rbac
27
+ """
28
+ repo = get_repo()
29
+ results = repo.search(query)
30
+
31
+ if not results:
32
+ click.echo(f"No fixes found matching '{query}'")
33
+ return
34
+
35
+ click.echo(f"Found {len(results)} fix(es) matching '{query}':\n")
36
+
37
+ for fix in results[:limit]:
38
+ click.echo(f" {fix.summary()}")
39
+
40
+ if len(results) > limit:
41
+ click.echo(f"\n ... and {len(results) - limit} more. Use --limit to see more.")
42
+
43
+ click.echo(f"\nRun `fixdoc show <fix-id>` for full details.")
44
+
45
+
46
+ @click.command()
47
+ @click.argument("fix_id")
48
+ def show(fix_id: str):
49
+ """
50
+ Show full details of a fix.
51
+
52
+ Accepts full or partial fix ID.
53
+
54
+ \b
55
+ Example:
56
+ fixdoc show a1b2c3d4
57
+ """
58
+ repo = get_repo()
59
+ fix = repo.get(fix_id)
60
+
61
+ if not fix:
62
+ click.echo(f"No fix found with ID: '{fix_id}'")
63
+ raise SystemExit(1)
64
+
65
+ click.echo(fix_to_markdown(fix))