@caddis/cli 0.2.5 → 0.3.0
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.
- package/bundles/antigravity-plugin/agents/claude-md-curator.md +1 -1
- package/bundles/antigravity-plugin/caddis_statusline.py +803 -0
- package/bundles/antigravity-plugin/claude-md/folder-agents.md.tmpl +21 -0
- package/bundles/antigravity-plugin/guard_agy.py +23 -4
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_gate.py +524 -0
- package/bundles/antigravity-plugin/scripts/caddis_inventory.py +369 -0
- package/bundles/antigravity-plugin/scripts/caddis_migrate_dir.py +270 -0
- package/bundles/antigravity-plugin/scripts/caddis_spawn.py +300 -0
- package/bundles/antigravity-plugin/scripts/caddis_statusline.py +803 -0
- package/bundles/antigravity-plugin/scripts/caddis_tidy.py +366 -0
- package/bundles/antigravity-plugin/scripts/check_doc_coverage.py +924 -0
- package/bundles/antigravity-plugin/scripts/claude-deepseek.ps1 +4 -0
- package/bundles/antigravity-plugin/scripts/claude-deepseek.sh +3 -0
- package/bundles/antigravity-plugin/scripts/claude-glm.ps1 +4 -0
- package/bundles/antigravity-plugin/scripts/claude-glm.sh +3 -0
- package/bundles/antigravity-plugin/scripts/claude-oss.ps1 +64 -0
- package/bundles/antigravity-plugin/scripts/claude-oss.sh +60 -0
- package/bundles/antigravity-plugin/scripts/claudster_config.py +126 -0
- package/bundles/antigravity-plugin/scripts/claudster_doctor.py +356 -0
- package/bundles/antigravity-plugin/scripts/claudster_init.py +400 -0
- package/bundles/antigravity-plugin/scripts/claudster_migrate_rules.py +300 -0
- package/bundles/antigravity-plugin/scripts/dream_capture.py +249 -0
- package/bundles/antigravity-plugin/scripts/dream_memory.py +438 -0
- package/bundles/antigravity-plugin/scripts/enforcement_inventory.py +261 -0
- package/bundles/antigravity-plugin/scripts/hook_log.py +156 -0
- package/bundles/antigravity-plugin/scripts/oss_ask.py +98 -0
- package/bundles/antigravity-plugin/scripts/oss_model.py +166 -0
- package/bundles/antigravity-plugin/scripts/oss_review.py +718 -0
- package/bundles/antigravity-plugin/scripts/setup_project_ai.py +1429 -0
- package/bundles/antigravity-plugin/scripts/usage_review.py +1323 -0
- package/bundles/antigravity-plugin/session_end_agy.py +21 -2
- package/bundles/antigravity-plugin/skills/_registry.md +2 -0
- package/bundles/antigravity-plugin/skills/advisory-hub/reference/succession.md +22 -0
- package/bundles/antigravity-plugin/skills/fastapi-dev/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/ship/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/ship-merge/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/ship-pr/SKILL.md +1 -1
- package/bundles/antigravity-plugin/skills/spawn-hub/SKILL.md +33 -0
- package/bundles/antigravity-plugin/skills/spawn-session/SKILL.md +213 -0
- package/bundles/antigravity-plugin/skills/statusline/SKILL.md +105 -0
- package/bundles/antigravity-plugin/skills/use-model/SKILL.md +15 -3
- package/bundles/antigravity-plugin/warm_start_agy.py +21 -2
- package/bundles/antigravity-plugin-extras/plugin.json +1 -1
- package/bundles/antigravity-plugin-extras/skills/architecture-design/SKILL.md +1 -1
- package/bundles/antigravity-plugin-extras/skills/caching-patterns/SKILL.md +1 -1
- package/bundles/antigravity-plugin-extras/skills/webapp-development/SKILL.md +1 -1
- package/bundles/manifest.json +3 -3
- package/dist/claude-deepseek.js +544 -0
- package/dist/claude-glm.js +544 -0
- package/dist/claude-oss.js +544 -0
- package/dist/cli.js +375 -46
- package/package.json +5 -2
- package/bundles/antigravity-plugin/statusline-command-agy.sh +0 -91
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
"""caddis-init — install a caddis toolbox bundle into a project, for any harness.
|
|
2
|
+
|
|
3
|
+
One canonical toolbox, many harnesses: Claude Code gets the plugin/marketplace; every other
|
|
4
|
+
harness (codex, antigravity, copilot, …) gets a per-harness bundle laid into the project by
|
|
5
|
+
this script. See docs/guide/porting-to-a-harness.md for how bundles are produced.
|
|
6
|
+
|
|
7
|
+
Guidance — **repo = rules, user = skills**: install SKILLS once at the user level (`--user`, into
|
|
8
|
+
~/.codex/skills / ~/.agents/skills) so every project sees them; keep RULES (AGENTS.md) per-repo (the
|
|
9
|
+
default `--dest` mode), since rules are project-specific. Teams that want vendored/pinned skills can
|
|
10
|
+
still install per-repo. Every apply is recorded in ~/.caddis/installs.json; `--update-all` refreshes
|
|
11
|
+
them all, `--uninstall` removes only the unmodified files it wrote, and `claudster_doctor.py` reports health.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
python claudster_init.py --target codex # fetch from GitHub (published bundles)
|
|
15
|
+
python claudster_init.py --target antigravity --from E:\\path\\to\\caddis
|
|
16
|
+
python claudster_init.py --target codex --from repo.tar.gz --dest C:\\proj
|
|
17
|
+
|
|
18
|
+
Source resolution (--from may be):
|
|
19
|
+
• a published caddis-plugin checkout → <src>/bundles/<target>/
|
|
20
|
+
• a caddis checkout → <src>/dist/runtime-resources/<target>/
|
|
21
|
+
• a .tar.gz (GitHub codeload shape, single top-level dir) → same roots inside it
|
|
22
|
+
• omitted → download DEFAULT_TARBALL_URL and proceed as tarball
|
|
23
|
+
|
|
24
|
+
Safety contract:
|
|
25
|
+
• a sha256 manifest (.claudster-init.json) records every file this tool wrote;
|
|
26
|
+
• re-runs update only files still matching their manifest hash (unmodified);
|
|
27
|
+
• files the user edited — or pre-existing files never written by this tool — are
|
|
28
|
+
CONFLICTS: reported, left untouched, exit 1. --force overwrites them.
|
|
29
|
+
|
|
30
|
+
Exit codes: 0 ok / up-to-date · 1 conflicts (rest installed) · 2 bad target or source.
|
|
31
|
+
Windows-first: stdlib only, no POSIX assumptions.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import hashlib
|
|
38
|
+
import json
|
|
39
|
+
import os
|
|
40
|
+
import shutil
|
|
41
|
+
import sys
|
|
42
|
+
import tarfile
|
|
43
|
+
import tempfile
|
|
44
|
+
import urllib.request
|
|
45
|
+
from datetime import datetime, timezone
|
|
46
|
+
from pathlib import Path
|
|
47
|
+
|
|
48
|
+
MANIFEST_NAME = ".claudster-init.json" # hidden tool-internal; deliberately NOT renamed this round
|
|
49
|
+
|
|
50
|
+
# Functional identity in one place — every URL below derives from REPO_SLUG, and every env var from
|
|
51
|
+
# ENV_PREFIX, so a future rename is a constant edit. (Mirrored from claudster_config.py rather than
|
|
52
|
+
# imported: this script is downloadable standalone and must stay import-free.)
|
|
53
|
+
REPO_SLUG = "saajunaid/caddis-plugin"
|
|
54
|
+
REPO_URL = f"https://github.com/{REPO_SLUG}"
|
|
55
|
+
DEFAULT_TARBALL_URL = f"https://codeload.github.com/{REPO_SLUG}/tar.gz/refs/heads/main"
|
|
56
|
+
ENV_PREFIX = "CADDIS"
|
|
57
|
+
# Roots (relative to a source checkout) that may hold bundles, in preference order.
|
|
58
|
+
BUNDLE_ROOTS = ("bundles", "dist/runtime-resources")
|
|
59
|
+
|
|
60
|
+
def _home() -> Path:
|
|
61
|
+
"""User home — overridable via CADDIS_FAKE_HOME so tests never touch the real ~/.caddis etc."""
|
|
62
|
+
return Path(os.environ.get(f"{ENV_PREFIX}_FAKE_HOME") or Path.home())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _registry_path() -> Path:
|
|
66
|
+
"""The install registry: every apply is recorded so --update-all can re-run them and --doctor can
|
|
67
|
+
report skew. User-level (survives per-project churn)."""
|
|
68
|
+
return _home() / ".caddis" / "installs.json"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _user_skills(target: str):
|
|
72
|
+
"""Harness user-level skills root + the bundle subdir holding that target's skills (probed
|
|
73
|
+
2026-07-23): the harness merges skills from here for EVERY project, so --user installs once. The
|
|
74
|
+
per-file manifest makes a later --uninstall surgical, so a flat install never disturbs the user's
|
|
75
|
+
own skills (e.g. the azure-* set already under ~/.agents/skills)."""
|
|
76
|
+
home = _home()
|
|
77
|
+
table = {
|
|
78
|
+
"codex": (home / ".codex" / "skills", ".codex/skills"),
|
|
79
|
+
"antigravity": (home / ".agents" / "skills", ".agents/skills"),
|
|
80
|
+
"codex-extras": (home / ".codex" / "skills", ".codex/skills"),
|
|
81
|
+
"antigravity-extras": (home / ".agents" / "skills", ".agents/skills"),
|
|
82
|
+
}
|
|
83
|
+
return table.get(target)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _user_skills_supported() -> list[str]:
|
|
87
|
+
return ["codex", "antigravity", "codex-extras", "antigravity-extras"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _now() -> str:
|
|
91
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _bundle_version(bundle: Path, src_root: Path | None) -> str:
|
|
95
|
+
"""Best-effort caddis version for stamping: the bundle's own plugin.json, else the source
|
|
96
|
+
checkout's canonical claude-plugin version, else 'unknown'."""
|
|
97
|
+
pj = bundle / "plugin.json"
|
|
98
|
+
if pj.is_file():
|
|
99
|
+
try:
|
|
100
|
+
v = json.loads(pj.read_text(encoding="utf-8")).get("version")
|
|
101
|
+
if v:
|
|
102
|
+
return str(v)
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
if src_root is not None:
|
|
106
|
+
rt = src_root / ".github" / "runtime-targets.json"
|
|
107
|
+
if rt.is_file():
|
|
108
|
+
try:
|
|
109
|
+
m = json.loads(rt.read_text(encoding="utf-8"))
|
|
110
|
+
for t in m.get("targets", []):
|
|
111
|
+
v = (t.get("plugin") or {}).get("version")
|
|
112
|
+
if v:
|
|
113
|
+
return str(v)
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
return "unknown"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _read_registry() -> list[dict]:
|
|
120
|
+
rp = _registry_path()
|
|
121
|
+
if rp.is_file():
|
|
122
|
+
try:
|
|
123
|
+
return json.loads(rp.read_text(encoding="utf-8")).get("installs", [])
|
|
124
|
+
except Exception:
|
|
125
|
+
return []
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _record_install(location: Path, target: str, mode: str, version: str) -> None:
|
|
130
|
+
"""Append (or refresh) a registry entry keyed by (location, target). Idempotent per key."""
|
|
131
|
+
installs = _read_registry()
|
|
132
|
+
key = (str(location), target)
|
|
133
|
+
installs = [e for e in installs if (e.get("location"), e.get("target")) != key]
|
|
134
|
+
installs.append({
|
|
135
|
+
"location": str(location), "target": target, "mode": mode,
|
|
136
|
+
"version": version, "timestamp": _now(),
|
|
137
|
+
})
|
|
138
|
+
rp = _registry_path()
|
|
139
|
+
rp.parent.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
rp.write_text(json.dumps({"installs": installs}, indent=2) + "\n", encoding="utf-8")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _sha256(path: Path) -> str:
|
|
144
|
+
h = hashlib.sha256()
|
|
145
|
+
with open(path, "rb") as fh:
|
|
146
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
147
|
+
h.update(chunk)
|
|
148
|
+
return h.hexdigest()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _available_targets(src_root: Path) -> dict[str, Path]:
|
|
152
|
+
"""Map target name -> bundle dir for every bundle the source offers."""
|
|
153
|
+
found: dict[str, Path] = {}
|
|
154
|
+
for rel in BUNDLE_ROOTS:
|
|
155
|
+
root = src_root / rel
|
|
156
|
+
if not root.is_dir():
|
|
157
|
+
continue
|
|
158
|
+
for child in sorted(root.iterdir()):
|
|
159
|
+
if child.is_dir() and child.name not in found:
|
|
160
|
+
found[child.name] = child
|
|
161
|
+
return found
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _resolve_source(from_arg: str | None, scratch: Path) -> Path:
|
|
165
|
+
"""Return a directory that contains one of BUNDLE_ROOTS. Downloads/extracts as needed."""
|
|
166
|
+
if from_arg is None:
|
|
167
|
+
tgz = scratch / "bundle-source.tar.gz"
|
|
168
|
+
print(f"Fetching {DEFAULT_TARBALL_URL} ...")
|
|
169
|
+
urllib.request.urlretrieve(DEFAULT_TARBALL_URL, tgz) # noqa: S310 — fixed https URL
|
|
170
|
+
return _extract_tarball(tgz, scratch)
|
|
171
|
+
src = Path(from_arg)
|
|
172
|
+
if src.is_file(): # tarball path
|
|
173
|
+
return _extract_tarball(src, scratch)
|
|
174
|
+
return src
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _extract_tarball(tgz: Path, scratch: Path) -> Path:
|
|
178
|
+
out = scratch / "extracted"
|
|
179
|
+
with tarfile.open(tgz, "r:gz") as tf:
|
|
180
|
+
tf.extractall(out, filter="data")
|
|
181
|
+
# codeload tarballs have exactly one top-level directory (<repo>-<ref>/)
|
|
182
|
+
entries = [p for p in out.iterdir() if p.is_dir()]
|
|
183
|
+
return entries[0] if len(entries) == 1 else out
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _install(bundle: Path, dest: Path, target: str, force: bool, version: str = "unknown") -> int:
|
|
187
|
+
manifest_path = dest / MANIFEST_NAME
|
|
188
|
+
old_hashes: dict[str, str] = {}
|
|
189
|
+
if manifest_path.exists():
|
|
190
|
+
old_hashes = json.loads(manifest_path.read_text(encoding="utf-8")).get("files", {})
|
|
191
|
+
|
|
192
|
+
new_hashes: dict[str, str] = {}
|
|
193
|
+
installed, updated, conflicts, unchanged = [], [], [], []
|
|
194
|
+
|
|
195
|
+
for src_file in sorted(p for p in bundle.rglob("*") if p.is_file()):
|
|
196
|
+
rel = src_file.relative_to(bundle).as_posix()
|
|
197
|
+
dst_file = dest / rel
|
|
198
|
+
src_hash = _sha256(src_file)
|
|
199
|
+
|
|
200
|
+
if not dst_file.exists():
|
|
201
|
+
dst_file.parent.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
shutil.copy2(src_file, dst_file)
|
|
203
|
+
installed.append(rel)
|
|
204
|
+
new_hashes[rel] = src_hash
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
dst_hash = _sha256(dst_file)
|
|
208
|
+
if dst_hash == src_hash:
|
|
209
|
+
unchanged.append(rel) # identical — adopt (covers pre-existing identical files)
|
|
210
|
+
new_hashes[rel] = src_hash
|
|
211
|
+
elif rel in old_hashes and dst_hash == old_hashes[rel]:
|
|
212
|
+
shutil.copy2(src_file, dst_file) # ours, unmodified — safe to update
|
|
213
|
+
updated.append(rel)
|
|
214
|
+
new_hashes[rel] = src_hash
|
|
215
|
+
elif force:
|
|
216
|
+
shutil.copy2(src_file, dst_file)
|
|
217
|
+
updated.append(rel)
|
|
218
|
+
new_hashes[rel] = src_hash
|
|
219
|
+
else:
|
|
220
|
+
conflicts.append(rel) # user-modified or never ours — do not touch
|
|
221
|
+
new_hashes[rel] = old_hashes.get(rel, dst_hash)
|
|
222
|
+
|
|
223
|
+
manifest_path.write_text(
|
|
224
|
+
json.dumps({"target": target, "version": version, "installed": _now(),
|
|
225
|
+
"files": new_hashes}, indent=2) + "\n",
|
|
226
|
+
encoding="utf-8",
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
if installed:
|
|
230
|
+
print(f"Installed {len(installed)} file(s).")
|
|
231
|
+
if updated:
|
|
232
|
+
print(f"Updated {len(updated)} file(s).")
|
|
233
|
+
if not installed and not updated and not conflicts:
|
|
234
|
+
print(f"Already up to date ({len(unchanged)} file(s) verified).")
|
|
235
|
+
if conflicts:
|
|
236
|
+
print("CONFLICTS — locally modified (or pre-existing) files left untouched;")
|
|
237
|
+
print("re-run with --force to overwrite:")
|
|
238
|
+
for rel in conflicts:
|
|
239
|
+
print(f" {rel}")
|
|
240
|
+
return 1
|
|
241
|
+
return 0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _uninstall(dest: Path, target: str) -> int:
|
|
245
|
+
"""Remove manifest-tracked UNMODIFIED files (+ the registry entry). Modified/pre-existing files
|
|
246
|
+
the user changed are listed and left. Makes trying caddis risk-free."""
|
|
247
|
+
manifest_path = dest / MANIFEST_NAME
|
|
248
|
+
if not manifest_path.exists():
|
|
249
|
+
print(f"[FAIL] no {MANIFEST_NAME} at {dest} — nothing caddis-init installed here.", file=sys.stderr)
|
|
250
|
+
return 2
|
|
251
|
+
files = json.loads(manifest_path.read_text(encoding="utf-8")).get("files", {})
|
|
252
|
+
removed, kept = [], []
|
|
253
|
+
for rel, h in files.items():
|
|
254
|
+
f = dest / rel
|
|
255
|
+
if not f.exists():
|
|
256
|
+
continue
|
|
257
|
+
if _sha256(f) == h:
|
|
258
|
+
f.unlink()
|
|
259
|
+
removed.append(rel)
|
|
260
|
+
p = f.parent # prune now-empty dirs up to dest
|
|
261
|
+
while p != dest and p.is_dir() and not any(p.iterdir()):
|
|
262
|
+
p.rmdir()
|
|
263
|
+
p = p.parent
|
|
264
|
+
else:
|
|
265
|
+
kept.append(rel)
|
|
266
|
+
manifest_path.unlink()
|
|
267
|
+
installs = [e for e in _read_registry() if (e.get("location"), e.get("target")) != (str(dest), target)]
|
|
268
|
+
rp = _registry_path()
|
|
269
|
+
if rp.is_file():
|
|
270
|
+
rp.write_text(json.dumps({"installs": installs}, indent=2) + "\n", encoding="utf-8")
|
|
271
|
+
print(f"Uninstalled {len(removed)} unmodified file(s).")
|
|
272
|
+
if kept:
|
|
273
|
+
print(f"Left {len(kept)} modified file(s) in place:")
|
|
274
|
+
for rel in kept:
|
|
275
|
+
print(f" {rel}")
|
|
276
|
+
return 0
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _apply(src_root: Path, target: str, dest: Path, force: bool, extras: bool, user: bool) -> int:
|
|
280
|
+
"""Install one target into dest (per-repo) or the user-level skills root (--user), record the
|
|
281
|
+
registry, and (optionally) lay the extras tier over the core."""
|
|
282
|
+
targets = _available_targets(src_root)
|
|
283
|
+
if target not in targets:
|
|
284
|
+
avail = ", ".join(sorted(targets)) or "none found"
|
|
285
|
+
print(f"[FAIL] no '{target}' bundle in source. Available: {avail}", file=sys.stderr)
|
|
286
|
+
return 2
|
|
287
|
+
version = _bundle_version(targets[target], src_root)
|
|
288
|
+
|
|
289
|
+
if user:
|
|
290
|
+
# user = skills once for every project. Install only the skills subtree to the harness's
|
|
291
|
+
# user skills root. Flat (the proven discovery path); the manifest tracks exactly what we wrote,
|
|
292
|
+
# so a later --uninstall is surgical and never touches the user's own (e.g. azure-*) skills.
|
|
293
|
+
us = _user_skills(target)
|
|
294
|
+
if us is None:
|
|
295
|
+
print(f"[FAIL] --user not supported for '{target}' (no user skills root). "
|
|
296
|
+
f"Supported: {', '.join(_user_skills_supported())}", file=sys.stderr)
|
|
297
|
+
return 2
|
|
298
|
+
user_root, subdir = us
|
|
299
|
+
skills_src = targets[target] / subdir
|
|
300
|
+
if not skills_src.is_dir():
|
|
301
|
+
print(f"[FAIL] bundle '{target}' has no skills at {subdir}", file=sys.stderr)
|
|
302
|
+
return 2
|
|
303
|
+
user_root.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
rc = _install(skills_src, user_root, target, force, version)
|
|
305
|
+
if rc in (0, 1):
|
|
306
|
+
_record_install(user_root, target, "user", version)
|
|
307
|
+
return rc
|
|
308
|
+
|
|
309
|
+
rc = _install(targets[target], dest, target, force, version)
|
|
310
|
+
if rc in (0, 1):
|
|
311
|
+
_record_install(dest, target, "repo", version)
|
|
312
|
+
if rc == 0 and extras:
|
|
313
|
+
extras_name = f"{target}-extras"
|
|
314
|
+
if extras_name in targets:
|
|
315
|
+
print(f"-- installing extras tier: {extras_name}")
|
|
316
|
+
rc = _install(targets[extras_name], dest, extras_name, force,
|
|
317
|
+
_bundle_version(targets[extras_name], src_root))
|
|
318
|
+
else:
|
|
319
|
+
avail = ", ".join(sorted(t for t in targets if t.endswith("-extras"))) or "none"
|
|
320
|
+
print(f"[WARN] --extras: no '{extras_name}' bundle in source (available extras: {avail}) — "
|
|
321
|
+
"installed core only.", file=sys.stderr)
|
|
322
|
+
return rc
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def main(argv: list[str] | None = None) -> int:
|
|
326
|
+
parser = argparse.ArgumentParser(
|
|
327
|
+
prog="caddis-init", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
|
|
328
|
+
)
|
|
329
|
+
parser.add_argument("--target", help="Harness bundle to install (e.g. codex, antigravity).")
|
|
330
|
+
parser.add_argument("--from", dest="from_", metavar="SRC",
|
|
331
|
+
help="Local checkout dir or .tar.gz. Omit to download from GitHub.")
|
|
332
|
+
parser.add_argument("--dest", default=".", help="Project directory to install into (default: cwd).")
|
|
333
|
+
parser.add_argument("--force", action="store_true",
|
|
334
|
+
help="Overwrite locally modified / pre-existing conflicting files.")
|
|
335
|
+
parser.add_argument("--extras", action="store_true",
|
|
336
|
+
help="Also install the long-tail EXTRAS skills (the '<target>-extras' bundle) on "
|
|
337
|
+
"top of the lean core. Default install is core-only, to keep context lean.")
|
|
338
|
+
parser.add_argument("--user", action="store_true",
|
|
339
|
+
help="Install SKILLS once at the harness's user level (~/.codex/skills, "
|
|
340
|
+
"~/.agents/skills) instead of per-project. Rules (AGENTS.md) stay per-repo.")
|
|
341
|
+
parser.add_argument("--update-all", action="store_true",
|
|
342
|
+
help="Re-run every install recorded in the registry (~/.caddis/installs.json).")
|
|
343
|
+
parser.add_argument("--uninstall", action="store_true",
|
|
344
|
+
help="Remove manifest-tracked UNMODIFIED files at --dest (+ registry entry).")
|
|
345
|
+
args = parser.parse_args(argv)
|
|
346
|
+
|
|
347
|
+
if args.update_all:
|
|
348
|
+
installs = _read_registry()
|
|
349
|
+
if not installs:
|
|
350
|
+
print("No installs recorded — nothing to update.")
|
|
351
|
+
return 0
|
|
352
|
+
with tempfile.TemporaryDirectory(prefix="caddis-init-") as tmp:
|
|
353
|
+
try:
|
|
354
|
+
src_root = _resolve_source(args.from_, Path(tmp))
|
|
355
|
+
except Exception as exc:
|
|
356
|
+
print(f"[FAIL] could not obtain bundle source: {exc}", file=sys.stderr)
|
|
357
|
+
return 2
|
|
358
|
+
worst = 0
|
|
359
|
+
for e in installs:
|
|
360
|
+
loc, tgt, mode = Path(e["location"]), e["target"], e.get("mode", "repo")
|
|
361
|
+
print(f"== update: {tgt} @ {loc} ({mode}) ==")
|
|
362
|
+
if mode == "user":
|
|
363
|
+
rc = _apply(src_root, tgt, loc, args.force, False, True)
|
|
364
|
+
else:
|
|
365
|
+
if not loc.is_dir():
|
|
366
|
+
print(f"[WARN] location gone, skipping: {loc}", file=sys.stderr)
|
|
367
|
+
continue
|
|
368
|
+
rc = _apply(src_root, tgt, loc, args.force, False, False)
|
|
369
|
+
worst = max(worst, rc)
|
|
370
|
+
return worst
|
|
371
|
+
|
|
372
|
+
if args.uninstall:
|
|
373
|
+
if not args.target:
|
|
374
|
+
print("[FAIL] --uninstall needs --target.", file=sys.stderr)
|
|
375
|
+
return 2
|
|
376
|
+
dest = Path(args.dest).resolve()
|
|
377
|
+
if args.user and _user_skills(args.target) is not None:
|
|
378
|
+
dest = _user_skills(args.target)[0]
|
|
379
|
+
return _uninstall(dest, args.target)
|
|
380
|
+
|
|
381
|
+
if not args.target:
|
|
382
|
+
print("[FAIL] --target is required (or use --update-all).", file=sys.stderr)
|
|
383
|
+
return 2
|
|
384
|
+
|
|
385
|
+
dest = Path(args.dest).resolve()
|
|
386
|
+
if not args.user and not dest.is_dir():
|
|
387
|
+
print(f"[FAIL] --dest is not a directory: {dest}", file=sys.stderr)
|
|
388
|
+
return 2
|
|
389
|
+
|
|
390
|
+
with tempfile.TemporaryDirectory(prefix="caddis-init-") as tmp:
|
|
391
|
+
try:
|
|
392
|
+
src_root = _resolve_source(args.from_, Path(tmp))
|
|
393
|
+
except Exception as exc: # download/extract failure — actionable, fail-closed
|
|
394
|
+
print(f"[FAIL] could not obtain bundle source: {exc}", file=sys.stderr)
|
|
395
|
+
return 2
|
|
396
|
+
return _apply(src_root, args.target, dest, args.force, args.extras, args.user)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
if __name__ == "__main__":
|
|
400
|
+
raise SystemExit(main())
|