devsync 0.5.5__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.
Files changed (84) hide show
  1. aiconfigkit/__init__.py +0 -0
  2. aiconfigkit/__main__.py +6 -0
  3. aiconfigkit/ai_tools/__init__.py +0 -0
  4. aiconfigkit/ai_tools/base.py +236 -0
  5. aiconfigkit/ai_tools/capability_registry.py +262 -0
  6. aiconfigkit/ai_tools/claude.py +91 -0
  7. aiconfigkit/ai_tools/claude_desktop.py +97 -0
  8. aiconfigkit/ai_tools/cline.py +92 -0
  9. aiconfigkit/ai_tools/copilot.py +92 -0
  10. aiconfigkit/ai_tools/cursor.py +109 -0
  11. aiconfigkit/ai_tools/detector.py +169 -0
  12. aiconfigkit/ai_tools/kiro.py +85 -0
  13. aiconfigkit/ai_tools/mcp_syncer.py +291 -0
  14. aiconfigkit/ai_tools/roo.py +110 -0
  15. aiconfigkit/ai_tools/translator.py +390 -0
  16. aiconfigkit/ai_tools/winsurf.py +102 -0
  17. aiconfigkit/cli/__init__.py +0 -0
  18. aiconfigkit/cli/delete.py +118 -0
  19. aiconfigkit/cli/download.py +274 -0
  20. aiconfigkit/cli/install.py +237 -0
  21. aiconfigkit/cli/install_new.py +937 -0
  22. aiconfigkit/cli/list.py +275 -0
  23. aiconfigkit/cli/main.py +454 -0
  24. aiconfigkit/cli/mcp_configure.py +232 -0
  25. aiconfigkit/cli/mcp_install.py +166 -0
  26. aiconfigkit/cli/mcp_sync.py +165 -0
  27. aiconfigkit/cli/package.py +383 -0
  28. aiconfigkit/cli/package_create.py +323 -0
  29. aiconfigkit/cli/package_install.py +472 -0
  30. aiconfigkit/cli/template.py +19 -0
  31. aiconfigkit/cli/template_backup.py +261 -0
  32. aiconfigkit/cli/template_init.py +499 -0
  33. aiconfigkit/cli/template_install.py +261 -0
  34. aiconfigkit/cli/template_list.py +172 -0
  35. aiconfigkit/cli/template_uninstall.py +146 -0
  36. aiconfigkit/cli/template_update.py +225 -0
  37. aiconfigkit/cli/template_validate.py +234 -0
  38. aiconfigkit/cli/tools.py +47 -0
  39. aiconfigkit/cli/uninstall.py +125 -0
  40. aiconfigkit/cli/update.py +309 -0
  41. aiconfigkit/core/__init__.py +0 -0
  42. aiconfigkit/core/checksum.py +211 -0
  43. aiconfigkit/core/component_detector.py +905 -0
  44. aiconfigkit/core/conflict_resolution.py +329 -0
  45. aiconfigkit/core/git_operations.py +539 -0
  46. aiconfigkit/core/mcp/__init__.py +1 -0
  47. aiconfigkit/core/mcp/credentials.py +279 -0
  48. aiconfigkit/core/mcp/manager.py +308 -0
  49. aiconfigkit/core/mcp/set_manager.py +1 -0
  50. aiconfigkit/core/mcp/validator.py +1 -0
  51. aiconfigkit/core/models.py +1661 -0
  52. aiconfigkit/core/package_creator.py +743 -0
  53. aiconfigkit/core/package_manifest.py +248 -0
  54. aiconfigkit/core/repository.py +298 -0
  55. aiconfigkit/core/secret_detector.py +438 -0
  56. aiconfigkit/core/template_manifest.py +283 -0
  57. aiconfigkit/core/version.py +201 -0
  58. aiconfigkit/storage/__init__.py +0 -0
  59. aiconfigkit/storage/library.py +429 -0
  60. aiconfigkit/storage/mcp_tracker.py +1 -0
  61. aiconfigkit/storage/package_tracker.py +234 -0
  62. aiconfigkit/storage/template_library.py +229 -0
  63. aiconfigkit/storage/template_tracker.py +296 -0
  64. aiconfigkit/storage/tracker.py +416 -0
  65. aiconfigkit/tui/__init__.py +5 -0
  66. aiconfigkit/tui/installer.py +511 -0
  67. aiconfigkit/utils/__init__.py +0 -0
  68. aiconfigkit/utils/atomic_write.py +90 -0
  69. aiconfigkit/utils/backup.py +169 -0
  70. aiconfigkit/utils/dotenv.py +128 -0
  71. aiconfigkit/utils/git_helpers.py +187 -0
  72. aiconfigkit/utils/logging.py +60 -0
  73. aiconfigkit/utils/namespace.py +134 -0
  74. aiconfigkit/utils/paths.py +205 -0
  75. aiconfigkit/utils/project.py +109 -0
  76. aiconfigkit/utils/streaming.py +216 -0
  77. aiconfigkit/utils/ui.py +194 -0
  78. aiconfigkit/utils/validation.py +187 -0
  79. devsync-0.5.5.dist-info/LICENSE +21 -0
  80. devsync-0.5.5.dist-info/METADATA +477 -0
  81. devsync-0.5.5.dist-info/RECORD +84 -0
  82. devsync-0.5.5.dist-info/WHEEL +5 -0
  83. devsync-0.5.5.dist-info/entry_points.txt +2 -0
  84. devsync-0.5.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,416 @@
1
+ """Installation tracking and persistence."""
2
+
3
+ import json
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ from aiconfigkit.core.models import AIToolType, InstallationRecord
9
+ from aiconfigkit.utils.paths import get_installation_tracker_path
10
+ from aiconfigkit.utils.project import get_project_installation_tracker_path
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def _make_path_relative(absolute_path: Path, project_root: Path) -> str:
16
+ """
17
+ Convert absolute path to relative path from project root.
18
+
19
+ Args:
20
+ absolute_path: Absolute file path
21
+ project_root: Project root directory
22
+
23
+ Returns:
24
+ Relative path string (e.g., ".github/instructions/file.md")
25
+ """
26
+ try:
27
+ return str(absolute_path.relative_to(project_root))
28
+ except ValueError:
29
+ # Path is not relative to project root, return as-is
30
+ logger.warning(f"Path {absolute_path} is not relative to project root {project_root}")
31
+ return str(absolute_path)
32
+
33
+
34
+ def _make_path_absolute(relative_path: str, project_root: Path) -> Path:
35
+ """
36
+ Convert relative path to absolute path using project root.
37
+
38
+ Args:
39
+ relative_path: Relative path string
40
+ project_root: Project root directory
41
+
42
+ Returns:
43
+ Absolute Path object
44
+ """
45
+ path = Path(relative_path)
46
+ if path.is_absolute():
47
+ # Already absolute (old format), return as-is
48
+ return path
49
+ # Make relative path absolute
50
+ return project_root / path
51
+
52
+
53
+ class InstallationTracker:
54
+ """
55
+ Manages tracking of installed instructions.
56
+
57
+ Stores installation records in ~/.instructionkit/installations.json
58
+ """
59
+
60
+ def __init__(self, tracker_file: Optional[Path] = None):
61
+ """
62
+ Initialize installation tracker.
63
+
64
+ Args:
65
+ tracker_file: Path to tracker JSON file (defaults to standard location)
66
+ """
67
+ self.tracker_file = tracker_file or get_installation_tracker_path()
68
+ self._ensure_tracker_file()
69
+
70
+ def _ensure_tracker_file(self) -> None:
71
+ """Ensure tracker file and directory exist."""
72
+ self.tracker_file.parent.mkdir(parents=True, exist_ok=True)
73
+
74
+ if not self.tracker_file.exists():
75
+ # Create empty tracker file
76
+ self._write_records([])
77
+
78
+ def _read_records(self) -> list[InstallationRecord]:
79
+ """Read all installation records from file."""
80
+ try:
81
+ with open(self.tracker_file, "r", encoding="utf-8") as f:
82
+ data = json.load(f)
83
+
84
+ records = []
85
+ for item in data:
86
+ try:
87
+ record = InstallationRecord.from_dict(item)
88
+ records.append(record)
89
+ except Exception as e:
90
+ logger.warning(f"Skipping invalid installation record: {e}")
91
+ continue
92
+
93
+ return records
94
+
95
+ except json.JSONDecodeError as e:
96
+ logger.error(f"Invalid JSON in tracker file {self.tracker_file}: {e}")
97
+ return []
98
+ except FileNotFoundError:
99
+ logger.debug(f"Tracker file not found: {self.tracker_file}, will create on first write")
100
+ return []
101
+
102
+ def _write_records(self, records: list[InstallationRecord]) -> None:
103
+ """Write installation records to file."""
104
+ data = [record.to_dict() for record in records]
105
+
106
+ with open(self.tracker_file, "w", encoding="utf-8") as f:
107
+ json.dump(data, f, indent=2, ensure_ascii=False)
108
+
109
+ def add_installation(self, record: InstallationRecord, project_root: Optional[Path] = None) -> None:
110
+ """
111
+ Add an installation record.
112
+
113
+ Args:
114
+ record: Installation record to add
115
+ project_root: Project root for project-scoped installations
116
+ """
117
+ # Determine which tracker file to use
118
+ if project_root:
119
+ tracker_file = get_project_installation_tracker_path(project_root)
120
+ # Ensure project tracker file exists
121
+ tracker_file.parent.mkdir(parents=True, exist_ok=True)
122
+ if not tracker_file.exists():
123
+ tracker_file.write_text("[]", encoding="utf-8")
124
+
125
+ # Convert installed_path to relative for project-scoped installations
126
+ # Create a new record with relative path for storage
127
+ installed_path_abs = Path(record.installed_path)
128
+ relative_path = _make_path_relative(installed_path_abs, project_root)
129
+
130
+ # Create a copy of the record with relative path for storage
131
+ storage_record = InstallationRecord(
132
+ instruction_name=record.instruction_name,
133
+ ai_tool=record.ai_tool,
134
+ source_repo=record.source_repo,
135
+ installed_path=relative_path,
136
+ installed_at=record.installed_at,
137
+ checksum=record.checksum,
138
+ bundle_name=record.bundle_name,
139
+ scope=record.scope,
140
+ source_ref=record.source_ref,
141
+ source_ref_type=record.source_ref_type,
142
+ )
143
+ else:
144
+ tracker_file = self.tracker_file
145
+ storage_record = record # For global scope, use absolute paths
146
+
147
+ # Read records from appropriate file
148
+ try:
149
+ with open(tracker_file, "r", encoding="utf-8") as f:
150
+ data = json.load(f)
151
+ records = [InstallationRecord.from_dict(item) for item in data]
152
+ except (json.JSONDecodeError, FileNotFoundError):
153
+ records = []
154
+
155
+ # Remove any existing record for same instruction + tool + scope
156
+ records = [
157
+ r
158
+ for r in records
159
+ if not (
160
+ r.instruction_name == storage_record.instruction_name
161
+ and r.ai_tool == storage_record.ai_tool
162
+ and r.scope == storage_record.scope
163
+ )
164
+ ]
165
+
166
+ # Add new record (with relative path if project-scoped)
167
+ records.append(storage_record)
168
+
169
+ # Write back to appropriate file
170
+ with open(tracker_file, "w", encoding="utf-8") as f:
171
+ json.dump([r.to_dict() for r in records], f, indent=2, ensure_ascii=False)
172
+
173
+ def remove_installation(
174
+ self,
175
+ instruction_name: str,
176
+ ai_tool: Optional[AIToolType] = None,
177
+ project_root: Optional[Path] = None,
178
+ scope_filter: Optional[str] = None,
179
+ ) -> list[InstallationRecord]:
180
+ """
181
+ Remove installation record(s).
182
+
183
+ Args:
184
+ instruction_name: Name of instruction to remove
185
+ ai_tool: Specific AI tool (if None, removes from all tools)
186
+ project_root: Project root for project-scoped removals
187
+ scope_filter: Filter by scope ('global', 'project', or None for both)
188
+
189
+ Returns:
190
+ List of removed records
191
+ """
192
+
193
+ removed = []
194
+
195
+ # Handle global installations
196
+ if scope_filter is None or scope_filter == "global":
197
+ records = self._read_records()
198
+ global_removed = []
199
+ remaining = []
200
+
201
+ for record in records:
202
+ if record.instruction_name == instruction_name:
203
+ if ai_tool is None or record.ai_tool == ai_tool:
204
+ global_removed.append(record)
205
+ else:
206
+ remaining.append(record)
207
+ else:
208
+ remaining.append(record)
209
+
210
+ self._write_records(remaining)
211
+ removed.extend(global_removed)
212
+
213
+ # Handle project installations
214
+ if project_root and (scope_filter is None or scope_filter == "project"):
215
+ tracker_file = get_project_installation_tracker_path(project_root)
216
+ if tracker_file.exists():
217
+ try:
218
+ with open(tracker_file, "r", encoding="utf-8") as f:
219
+ data = json.load(f)
220
+ records = [InstallationRecord.from_dict(item) for item in data]
221
+
222
+ project_removed = []
223
+ remaining = []
224
+
225
+ for record in records:
226
+ if record.instruction_name == instruction_name:
227
+ if ai_tool is None or record.ai_tool == ai_tool:
228
+ project_removed.append(record)
229
+ else:
230
+ remaining.append(record)
231
+ else:
232
+ remaining.append(record)
233
+
234
+ with open(tracker_file, "w", encoding="utf-8") as f:
235
+ json.dump([r.to_dict() for r in remaining], f, indent=2, ensure_ascii=False)
236
+
237
+ removed.extend(project_removed)
238
+ except (json.JSONDecodeError, FileNotFoundError):
239
+ pass
240
+
241
+ return removed
242
+
243
+ def get_installed_instructions(
244
+ self,
245
+ ai_tool: Optional[AIToolType] = None,
246
+ project_root: Optional[Path] = None,
247
+ include_project: bool = True,
248
+ include_global: bool = True,
249
+ ) -> list[InstallationRecord]:
250
+ """
251
+ Get all installed instructions.
252
+
253
+ Args:
254
+ ai_tool: Filter by AI tool (if None, returns all)
255
+ project_root: Project root to include project-scoped installations
256
+ include_project: Whether to include project-scoped installations
257
+ include_global: Whether to include global installations
258
+
259
+ Returns:
260
+ List of installation records (paths converted to absolute for project-scoped records)
261
+ """
262
+ all_records = []
263
+
264
+ # Get global installations
265
+ if include_global:
266
+ global_records = self._read_records()
267
+ all_records.extend(global_records)
268
+
269
+ # Get project installations
270
+ if include_project and project_root:
271
+ tracker_file = get_project_installation_tracker_path(project_root)
272
+ if tracker_file.exists():
273
+ try:
274
+ with open(tracker_file, "r", encoding="utf-8") as f:
275
+ data = json.load(f)
276
+ project_records = [InstallationRecord.from_dict(item) for item in data]
277
+
278
+ # Convert relative paths to absolute for project-scoped records
279
+ for record in project_records:
280
+ abs_path = _make_path_absolute(record.installed_path, project_root)
281
+ # Update the record with absolute path for compatibility
282
+ record.installed_path = str(abs_path)
283
+
284
+ all_records.extend(project_records)
285
+ except (json.JSONDecodeError, FileNotFoundError):
286
+ pass
287
+
288
+ # Filter by AI tool if specified
289
+ if ai_tool is not None:
290
+ all_records = [r for r in all_records if r.ai_tool == ai_tool]
291
+
292
+ return all_records
293
+
294
+ def is_installed(self, instruction_name: str, ai_tool: Optional[AIToolType] = None) -> bool:
295
+ """
296
+ Check if an instruction is installed.
297
+
298
+ Args:
299
+ instruction_name: Name of instruction
300
+ ai_tool: Specific AI tool (if None, checks any tool)
301
+
302
+ Returns:
303
+ True if instruction is installed
304
+ """
305
+ records = self._read_records()
306
+
307
+ for record in records:
308
+ if record.instruction_name == instruction_name:
309
+ if ai_tool is None or record.ai_tool == ai_tool:
310
+ return True
311
+
312
+ return False
313
+
314
+ def get_installation(
315
+ self, instruction_name: str, ai_tool: AIToolType, project_root: Optional[Path] = None
316
+ ) -> Optional[InstallationRecord]:
317
+ """
318
+ Get installation record for specific instruction and tool.
319
+
320
+ Args:
321
+ instruction_name: Name of instruction
322
+ ai_tool: AI tool type
323
+ project_root: Project root for project-scoped search
324
+
325
+ Returns:
326
+ Installation record if found, None otherwise
327
+ """
328
+ records = self.get_installed_instructions(project_root=project_root)
329
+
330
+ for record in records:
331
+ if record.instruction_name == instruction_name and record.ai_tool == ai_tool:
332
+ return record
333
+
334
+ return None
335
+
336
+ def get_installations_from_repo(self, repo_url: str) -> list[InstallationRecord]:
337
+ """
338
+ Get all installations from a specific repository.
339
+
340
+ Args:
341
+ repo_url: Repository URL
342
+
343
+ Returns:
344
+ List of installation records from this repo
345
+ """
346
+ records = self._read_records()
347
+ return [r for r in records if r.source_repo == repo_url]
348
+
349
+ def get_bundle_installations(self, bundle_name: str) -> list[InstallationRecord]:
350
+ """
351
+ Get all installations that were part of a bundle.
352
+
353
+ Args:
354
+ bundle_name: Name of bundle
355
+
356
+ Returns:
357
+ List of installation records from this bundle
358
+ """
359
+ records = self._read_records()
360
+ return [r for r in records if r.bundle_name == bundle_name]
361
+
362
+ def find_instructions_by_name(
363
+ self, instruction_name: str, project_root: Optional[Path] = None
364
+ ) -> list[InstallationRecord]:
365
+ """
366
+ Find all installations with a specific instruction name.
367
+
368
+ Args:
369
+ instruction_name: Name of instruction to search for
370
+ project_root: Project root for project-scoped search (None for all scopes)
371
+
372
+ Returns:
373
+ List of installation records with this name
374
+ """
375
+ records = self.get_installed_instructions(project_root=project_root)
376
+ return [r for r in records if r.instruction_name == instruction_name]
377
+
378
+ def list_installations(self) -> list[InstallationRecord]:
379
+ """
380
+ Get all installation records.
381
+
382
+ Returns:
383
+ List of all installation records
384
+ """
385
+ return self._read_records()
386
+
387
+ def clear_all(self) -> None:
388
+ """Clear all installation records (for testing)."""
389
+ self._write_records([])
390
+
391
+ def get_updatable_instructions(self, project_root: Optional[Path] = None) -> list[InstallationRecord]:
392
+ """
393
+ Get installations that can be updated (from mutable refs like branches).
394
+
395
+ Args:
396
+ project_root: Project root to include project-scoped installations
397
+
398
+ Returns:
399
+ List of installation records with mutable source refs (branches)
400
+ """
401
+ from aiconfigkit.core.models import RefType
402
+
403
+ all_records = self.get_installed_instructions(project_root=project_root)
404
+
405
+ # Filter to only branch-based installations (mutable)
406
+ updatable = []
407
+ for record in all_records:
408
+ # If no ref type specified, assume it's updatable (old format or default branch)
409
+ if not record.source_ref_type:
410
+ updatable.append(record)
411
+ # Only branches are updatable
412
+ elif record.source_ref_type == RefType.BRANCH:
413
+ updatable.append(record)
414
+ # Tags and commits are immutable, skip them
415
+
416
+ return updatable
@@ -0,0 +1,5 @@
1
+ """TUI (Text User Interface) components for InstructionKit."""
2
+
3
+ from aiconfigkit.tui.installer import show_installer_tui
4
+
5
+ __all__ = ["show_installer_tui"]