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.
- aiconfigkit/__init__.py +0 -0
- aiconfigkit/__main__.py +6 -0
- aiconfigkit/ai_tools/__init__.py +0 -0
- aiconfigkit/ai_tools/base.py +236 -0
- aiconfigkit/ai_tools/capability_registry.py +262 -0
- aiconfigkit/ai_tools/claude.py +91 -0
- aiconfigkit/ai_tools/claude_desktop.py +97 -0
- aiconfigkit/ai_tools/cline.py +92 -0
- aiconfigkit/ai_tools/copilot.py +92 -0
- aiconfigkit/ai_tools/cursor.py +109 -0
- aiconfigkit/ai_tools/detector.py +169 -0
- aiconfigkit/ai_tools/kiro.py +85 -0
- aiconfigkit/ai_tools/mcp_syncer.py +291 -0
- aiconfigkit/ai_tools/roo.py +110 -0
- aiconfigkit/ai_tools/translator.py +390 -0
- aiconfigkit/ai_tools/winsurf.py +102 -0
- aiconfigkit/cli/__init__.py +0 -0
- aiconfigkit/cli/delete.py +118 -0
- aiconfigkit/cli/download.py +274 -0
- aiconfigkit/cli/install.py +237 -0
- aiconfigkit/cli/install_new.py +937 -0
- aiconfigkit/cli/list.py +275 -0
- aiconfigkit/cli/main.py +454 -0
- aiconfigkit/cli/mcp_configure.py +232 -0
- aiconfigkit/cli/mcp_install.py +166 -0
- aiconfigkit/cli/mcp_sync.py +165 -0
- aiconfigkit/cli/package.py +383 -0
- aiconfigkit/cli/package_create.py +323 -0
- aiconfigkit/cli/package_install.py +472 -0
- aiconfigkit/cli/template.py +19 -0
- aiconfigkit/cli/template_backup.py +261 -0
- aiconfigkit/cli/template_init.py +499 -0
- aiconfigkit/cli/template_install.py +261 -0
- aiconfigkit/cli/template_list.py +172 -0
- aiconfigkit/cli/template_uninstall.py +146 -0
- aiconfigkit/cli/template_update.py +225 -0
- aiconfigkit/cli/template_validate.py +234 -0
- aiconfigkit/cli/tools.py +47 -0
- aiconfigkit/cli/uninstall.py +125 -0
- aiconfigkit/cli/update.py +309 -0
- aiconfigkit/core/__init__.py +0 -0
- aiconfigkit/core/checksum.py +211 -0
- aiconfigkit/core/component_detector.py +905 -0
- aiconfigkit/core/conflict_resolution.py +329 -0
- aiconfigkit/core/git_operations.py +539 -0
- aiconfigkit/core/mcp/__init__.py +1 -0
- aiconfigkit/core/mcp/credentials.py +279 -0
- aiconfigkit/core/mcp/manager.py +308 -0
- aiconfigkit/core/mcp/set_manager.py +1 -0
- aiconfigkit/core/mcp/validator.py +1 -0
- aiconfigkit/core/models.py +1661 -0
- aiconfigkit/core/package_creator.py +743 -0
- aiconfigkit/core/package_manifest.py +248 -0
- aiconfigkit/core/repository.py +298 -0
- aiconfigkit/core/secret_detector.py +438 -0
- aiconfigkit/core/template_manifest.py +283 -0
- aiconfigkit/core/version.py +201 -0
- aiconfigkit/storage/__init__.py +0 -0
- aiconfigkit/storage/library.py +429 -0
- aiconfigkit/storage/mcp_tracker.py +1 -0
- aiconfigkit/storage/package_tracker.py +234 -0
- aiconfigkit/storage/template_library.py +229 -0
- aiconfigkit/storage/template_tracker.py +296 -0
- aiconfigkit/storage/tracker.py +416 -0
- aiconfigkit/tui/__init__.py +5 -0
- aiconfigkit/tui/installer.py +511 -0
- aiconfigkit/utils/__init__.py +0 -0
- aiconfigkit/utils/atomic_write.py +90 -0
- aiconfigkit/utils/backup.py +169 -0
- aiconfigkit/utils/dotenv.py +128 -0
- aiconfigkit/utils/git_helpers.py +187 -0
- aiconfigkit/utils/logging.py +60 -0
- aiconfigkit/utils/namespace.py +134 -0
- aiconfigkit/utils/paths.py +205 -0
- aiconfigkit/utils/project.py +109 -0
- aiconfigkit/utils/streaming.py +216 -0
- aiconfigkit/utils/ui.py +194 -0
- aiconfigkit/utils/validation.py +187 -0
- devsync-0.5.5.dist-info/LICENSE +21 -0
- devsync-0.5.5.dist-info/METADATA +477 -0
- devsync-0.5.5.dist-info/RECORD +84 -0
- devsync-0.5.5.dist-info/WHEEL +5 -0
- devsync-0.5.5.dist-info/entry_points.txt +2 -0
- 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
|