feat: harden CASAN production install lifecycle
This commit is contained in:
@@ -56,6 +56,141 @@ VSCODE_EXTENSION_IDS = {
|
||||
}
|
||||
PROMPT_MARKER_START = "<!-- CASAN_PROMPT_ENFORCEMENT_START -->"
|
||||
PROMPT_MARKER_END = "<!-- CASAN_PROMPT_ENFORCEMENT_END -->"
|
||||
CLIENT_LABELS = {
|
||||
"claude": "Claude Code",
|
||||
"codex": "Codex",
|
||||
"vscode-copilot": "VS Code / @casan",
|
||||
}
|
||||
|
||||
|
||||
def _color(code, text):
|
||||
if (not sys.stdout.isatty() or os.environ.get("NO_COLOR") is not None or
|
||||
os.environ.get("TERM") == "dumb"):
|
||||
return text
|
||||
return "\033[%sm%s\033[0m" % (code, text)
|
||||
|
||||
|
||||
def _mark(ok):
|
||||
return _color("32" if ok else "31", "✓" if ok else "✗")
|
||||
|
||||
|
||||
def _warn_mark():
|
||||
return _color("33", "!")
|
||||
|
||||
|
||||
def _heading(text):
|
||||
print(_color("1;36", text))
|
||||
|
||||
|
||||
def _details(rows):
|
||||
width = max((len(label) for label, _value in rows), default=0)
|
||||
for label, value in rows:
|
||||
print(" %-*s %s" % (width, label, value))
|
||||
|
||||
|
||||
def _client_names(clients):
|
||||
return ", ".join(CLIENT_LABELS.get(client, client) for client in clients) or "None"
|
||||
|
||||
|
||||
def _emit_json_or_human(args, payload, renderer):
|
||||
if getattr(args, "json", False):
|
||||
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
||||
else:
|
||||
renderer(payload)
|
||||
|
||||
|
||||
def _render_init(result):
|
||||
_heading("%s CASAN initialized" % _mark(True))
|
||||
_details([
|
||||
("Project", result["project_id"]),
|
||||
("Location", result["target"]),
|
||||
("Level", "%s (%s)" % (
|
||||
result["target_level_name"].capitalize(), result["target_level"])),
|
||||
("Mode", result["enforcement_mode"]),
|
||||
("Clients", _client_names(result["clients"])),
|
||||
("Files", "%d created or updated" % len(result["created"])),
|
||||
("Harness", result["harness_version"]),
|
||||
])
|
||||
print()
|
||||
print(_color("1", "Integrations"))
|
||||
for path, status in result["hook_merges"].items():
|
||||
ok = status not in (None, "absent")
|
||||
print(" %s %-26s %s" % (_mark(ok), path, status or "unavailable"))
|
||||
extension = result["vscode_extension"]
|
||||
if extension.get("status") not in ("skipped",):
|
||||
ok = extension.get("status") == "installed"
|
||||
print(" %s %-26s %s" % (
|
||||
_mark(ok), "VS Code extension", extension.get("status")))
|
||||
print()
|
||||
print(_color("1", "Next steps"))
|
||||
print(" 1. Run `casan doctor`")
|
||||
if "codex" in result["clients"]:
|
||||
print(" 2. In Codex, open `/hooks` and trust this project's hook.")
|
||||
|
||||
|
||||
def _render_verify(result):
|
||||
ok = result["status"] == "ok"
|
||||
_heading("%s Harness integrity %s" % (
|
||||
_mark(ok), "verified" if ok else "check failed"))
|
||||
_details([
|
||||
("Version", str(result.get("harness_version_now") or "unknown")),
|
||||
("Location", str(result.get("harness_root") or "unavailable")),
|
||||
("Expected", str(result.get("expected") or "unavailable")),
|
||||
("Actual", str(result.get("actual") or "unavailable")),
|
||||
])
|
||||
|
||||
|
||||
def _render_level(result):
|
||||
_heading("CASAN packaging level")
|
||||
_details([
|
||||
("Installed", str(result.get("installed_level") or "unknown")),
|
||||
("Project", ("%s (%s)" % (
|
||||
result.get("project_target_level_name"),
|
||||
result.get("project_target_level")))
|
||||
if result.get("project_target_level") else "not initialized"),
|
||||
("Status", str(result.get("project_level_status") or "unknown")),
|
||||
])
|
||||
print()
|
||||
for level, description in result["levels"].items():
|
||||
print(" %-14s %s" % (level, description))
|
||||
|
||||
|
||||
def _render_doctor(result):
|
||||
ready = result["status"] == "ready"
|
||||
_heading("CASAN Doctor")
|
||||
print(" %s Harness integrity" % _mark(result["integrity"]["ok"]))
|
||||
print(" %s Project bootstrap" % _mark(result["bootstrap"]["ok"]))
|
||||
for client, item in result["client_checks"].items():
|
||||
label = CLIENT_LABELS.get(client, client)
|
||||
print(" %s %s" % (_mark(item.get("ready", False)), label))
|
||||
reason = item.get("smoke", {}).get("reason")
|
||||
if reason and not item.get("ready"):
|
||||
print(" %s" % reason)
|
||||
if result["warnings"]:
|
||||
print()
|
||||
print(_color("1;33", "Warnings"))
|
||||
for warning in result["warnings"]:
|
||||
print(" %s %s" % (_warn_mark(), warning))
|
||||
print()
|
||||
status = _color("1;32", "READY") if ready else _color("1;31", "NOT READY")
|
||||
print("Status: %s" % status)
|
||||
|
||||
|
||||
def _render_uninstall(result):
|
||||
_heading("%s CASAN removed from project" % _mark(True))
|
||||
_details([
|
||||
("Project", result.get("project_id") or "unknown"),
|
||||
("Location", result["target"]),
|
||||
("Hooks", "%d integration files cleaned" % len(result["hook_changes"])),
|
||||
("Files", "%d CASAN-owned files removed" % len(result["removed"])),
|
||||
("Evidence", "removed" if result["purged"] else "retained"),
|
||||
("VS Code extension", result["vscode_extension"]["status"]),
|
||||
])
|
||||
if result["retained"]:
|
||||
print()
|
||||
print(_color("1", "Retained for safety"))
|
||||
for path in result["retained"]:
|
||||
print(" - %s" % path)
|
||||
|
||||
|
||||
@contextmanager
|
||||
@@ -225,6 +360,8 @@ def _load_json_or(path, default):
|
||||
|
||||
|
||||
def _backup_once(path, backups):
|
||||
if backups is None:
|
||||
return
|
||||
bak = path + ".casan-bak"
|
||||
if os.path.exists(path) and not os.path.exists(bak):
|
||||
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
||||
@@ -670,10 +807,22 @@ def cmd_init(args):
|
||||
hhash, hsource = compute_harness_hash(harness)
|
||||
created = []
|
||||
backups = []
|
||||
previous_manifest = _load_json_or(
|
||||
os.path.join(target, ".casan", "init-manifest.json"), {})
|
||||
owned_files = {
|
||||
str(path).replace("\\", "/")
|
||||
for path in previous_manifest.get("owned_files", [])
|
||||
if isinstance(path, str)
|
||||
}
|
||||
|
||||
def created_add(p):
|
||||
created.append(os.path.relpath(p, target))
|
||||
|
||||
def mark_owned_if_absent(p):
|
||||
if not os.path.exists(p):
|
||||
owned_files.add(
|
||||
os.path.relpath(p, target).replace(os.sep, "/"))
|
||||
|
||||
# ── .casan/config.json ──
|
||||
cfg_dir = os.path.join(target, ".casan")
|
||||
cfg = {
|
||||
@@ -696,6 +845,7 @@ def cmd_init(args):
|
||||
"target_level_name": lvl_name,
|
||||
}
|
||||
p = os.path.join(cfg_dir, "config.json")
|
||||
mark_owned_if_absent(p)
|
||||
_write(p, json.dumps(cfg, ensure_ascii=False, indent=2) + "\n", backups); created_add(p)
|
||||
|
||||
# ── .casan/version.lock (pin) ──
|
||||
@@ -708,6 +858,7 @@ def cmd_init(args):
|
||||
"recorded_at": now_iso(),
|
||||
}
|
||||
p = os.path.join(cfg_dir, "version.lock")
|
||||
mark_owned_if_absent(p)
|
||||
_write(p, json.dumps(lock, ensure_ascii=False, indent=2) + "\n", backups); created_add(p)
|
||||
|
||||
# ── .casan/agentic.env (Plan-20 flags) ──
|
||||
@@ -720,6 +871,7 @@ def cmd_init(args):
|
||||
"",
|
||||
]
|
||||
p = os.path.join(cfg_dir, "agentic.env")
|
||||
mark_owned_if_absent(p)
|
||||
_write(p, "\n".join(env_lines), backups); created_add(p)
|
||||
|
||||
# Stable project-local bootstrap. It loads the config above, resolves the
|
||||
@@ -730,6 +882,7 @@ def cmd_init(args):
|
||||
if not os.path.isfile(bootstrap_source):
|
||||
sys.stderr.write("casan init: missing project hook bootstrap template\n")
|
||||
return 1
|
||||
mark_owned_if_absent(bootstrap_target)
|
||||
with open(bootstrap_source, "r", encoding="utf-8") as fh:
|
||||
_write(bootstrap_target, fh.read(), backups)
|
||||
try:
|
||||
@@ -746,6 +899,7 @@ def cmd_init(args):
|
||||
os.makedirs(os.path.join(specify, "logs"), exist_ok=True)
|
||||
gi = os.path.join(specify, ".gitignore")
|
||||
if not os.path.exists(gi):
|
||||
mark_owned_if_absent(gi)
|
||||
_write(gi, "# CASAN runtime state — do not commit\nlogs/\nstate/\n", backups); created_add(gi)
|
||||
|
||||
# ── Plan-20 client hooks — MERGED into any existing config, never clobbered.
|
||||
@@ -756,6 +910,7 @@ def cmd_init(args):
|
||||
claude_dst = os.path.join(target, ".claude", "settings.json")
|
||||
if "claude" in clients:
|
||||
dst = claude_dst
|
||||
mark_owned_if_absent(dst)
|
||||
remove_json_hooks(dst, "claude_hook.py", backups)
|
||||
r = merge_json_hooks(dst, os.path.join(ad, "claude-code", "settings.template.json"),
|
||||
"casan-hook.py", backups)
|
||||
@@ -770,6 +925,7 @@ def cmd_init(args):
|
||||
codex_hooks_dst = os.path.join(target, ".codex", "hooks.json")
|
||||
if "codex" in clients:
|
||||
dsth = codex_hooks_dst
|
||||
mark_owned_if_absent(dsth)
|
||||
remove_json_hooks(dsth, "codex_hook.py", backups)
|
||||
r = merge_json_hooks(dsth, os.path.join(ad, "codex", "hooks.template.json"),
|
||||
"casan-hook.py", backups)
|
||||
@@ -787,6 +943,7 @@ def cmd_init(args):
|
||||
os.path.join(target, ".codex", "config.toml"), backups)
|
||||
|
||||
vscode_file = os.path.join(target, ".vscode", "extensions.json")
|
||||
mark_owned_if_absent(vscode_file)
|
||||
vscode_merge = merge_vscode_recommendations(vscode_file, clients, backups)
|
||||
if vscode_merge not in ("unchanged", "absent"):
|
||||
created_add(vscode_file)
|
||||
@@ -812,24 +969,31 @@ def cmd_init(args):
|
||||
ci_src = os.path.join(dk, "templates", "gitea-workflow", "ci.yml")
|
||||
ci_dst = os.path.join(target, ".gitea", "workflows", "casan-ci.yml")
|
||||
if _copy_if_absent(ci_src, ci_dst, None, target, created):
|
||||
owned_files.add(os.path.relpath(
|
||||
ci_dst, target).replace(os.sep, "/"))
|
||||
level_extras.append(".gitea/workflows/casan-ci.yml")
|
||||
dom_src = os.path.join(dk, "templates", "domain-pack")
|
||||
dom_dst = os.path.join(target, "apps", project, "domain")
|
||||
created_before_domain = len(created)
|
||||
n = _copy_tree_missing(dom_src, dom_dst, target, created)
|
||||
if n:
|
||||
owned_files.update(
|
||||
path.replace(os.sep, "/")
|
||||
for path in created[created_before_domain:])
|
||||
level_extras.append("apps/%s/domain (%d files)" % (project, n))
|
||||
|
||||
# ── manifest (so uninstall/verify know what init created) ──
|
||||
manifest = {
|
||||
"created": created,
|
||||
"backups": [os.path.relpath(b, target) for b in backups],
|
||||
"owned_files": sorted(owned_files),
|
||||
"project_id": project,
|
||||
"at": now_iso(),
|
||||
}
|
||||
p = os.path.join(cfg_dir, "init-manifest.json")
|
||||
_write(p, json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", backups)
|
||||
|
||||
print(json.dumps({
|
||||
result = {
|
||||
"status": "initialized",
|
||||
"project_id": project,
|
||||
"target": target,
|
||||
@@ -846,7 +1010,8 @@ def cmd_init(args):
|
||||
"level_extras": level_extras,
|
||||
"note": ("harness NOT copied into repo (hybrid model); selected client hooks "
|
||||
"MERGED and unselected CASAN hooks removed; run `casan doctor`"),
|
||||
}, ensure_ascii=False, indent=2))
|
||||
}
|
||||
_emit_json_or_human(args, result, _render_init)
|
||||
if legacy_migration.get("manual_review"):
|
||||
sys.stderr.write(
|
||||
"casan init: WARNING — legacy CASAN prose outside managed markers "
|
||||
@@ -885,7 +1050,7 @@ def cmd_verify(args):
|
||||
"harness_version_now": harness_version(harness),
|
||||
"harness_root": harness,
|
||||
}
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
_emit_json_or_human(args, result, _render_verify)
|
||||
if not ok:
|
||||
sys.stderr.write("HARNESS_INTEGRITY_DRIFT — the resolved harness does not match the "
|
||||
"project pin. The global harness changed or was tampered.\n")
|
||||
@@ -919,7 +1084,7 @@ def cmd_level(args):
|
||||
"4 enterprise": "future — not shipped",
|
||||
},
|
||||
}
|
||||
print(json.dumps(out, ensure_ascii=False, indent=2))
|
||||
_emit_json_or_human(args, out, _render_level)
|
||||
return 0
|
||||
|
||||
|
||||
@@ -1091,10 +1256,180 @@ def cmd_doctor(args):
|
||||
"with Plan-20 hooks and needs manual review: %s." %
|
||||
", ".join(legacy_conflicts))
|
||||
checks["status"] = "ready" if ready else "not_ready"
|
||||
print(json.dumps(checks, ensure_ascii=False, indent=2))
|
||||
_emit_json_or_human(args, checks, _render_doctor)
|
||||
return 0 if ready else 2
|
||||
|
||||
|
||||
def _clean_integration_json(path, metadata_key, metadata_prefix, remove_if_empty):
|
||||
"""Remove CASAN template metadata and delete a CASAN-created empty file."""
|
||||
if not os.path.isfile(path):
|
||||
return False
|
||||
doc = _load_json_or(path, None)
|
||||
if not isinstance(doc, dict):
|
||||
return False
|
||||
metadata = doc.get(metadata_key)
|
||||
if isinstance(metadata, str) and metadata.startswith(metadata_prefix):
|
||||
doc.pop(metadata_key, None)
|
||||
hooks = doc.get("hooks")
|
||||
if isinstance(hooks, dict) and not hooks:
|
||||
doc.pop("hooks", None)
|
||||
recommendations = doc.get("recommendations")
|
||||
if isinstance(recommendations, list) and not recommendations:
|
||||
doc.pop("recommendations", None)
|
||||
with owner_writable(path):
|
||||
if not doc and remove_if_empty:
|
||||
os.unlink(path)
|
||||
else:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(json.dumps(doc, ensure_ascii=False, indent=2) + "\n")
|
||||
return True
|
||||
|
||||
|
||||
def _remove_owned_file(path, removed, target):
|
||||
if not os.path.isfile(path) and not os.path.islink(path):
|
||||
return
|
||||
with owner_writable(path):
|
||||
os.unlink(path)
|
||||
removed.append(os.path.relpath(path, target).replace(os.sep, "/"))
|
||||
|
||||
|
||||
def _prune_empty_directory(path):
|
||||
try:
|
||||
os.rmdir(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _uninstall_vscode_extension(enabled):
|
||||
if not enabled:
|
||||
return {
|
||||
"status": "retained_shared",
|
||||
"reason": "use --remove-vscode-extension to remove it from this machine",
|
||||
}
|
||||
code = shutil.which("code")
|
||||
if not code:
|
||||
return {"status": "not_removed", "reason": "code_cli_not_found"}
|
||||
result = subprocess.run(
|
||||
[code, "--uninstall-extension", "fpt-casan.casan-governed-chat"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False)
|
||||
return {
|
||||
"status": "uninstalled" if result.returncode == 0 else "failed",
|
||||
"command": code,
|
||||
"detail": (result.stdout or result.stderr).strip(),
|
||||
}
|
||||
|
||||
|
||||
def cmd_uninstall(args):
|
||||
"""Remove one project's CASAN adoption without clobbering user config."""
|
||||
target = os.path.abspath(args.target or os.getcwd())
|
||||
config_path = os.path.join(target, ".casan", "config.json")
|
||||
manifest_path = os.path.join(target, ".casan", "init-manifest.json")
|
||||
config = _load_json_or(config_path, {})
|
||||
manifest = _load_json_or(manifest_path, {})
|
||||
specify_gitignore = os.path.join(target, ".specify", ".gitignore")
|
||||
try:
|
||||
with open(specify_gitignore, "r", encoding="utf-8") as fh:
|
||||
has_casan_evidence = fh.read().startswith(
|
||||
"# CASAN runtime state — do not commit")
|
||||
except OSError:
|
||||
has_casan_evidence = False
|
||||
if (not config and not manifest and
|
||||
not (args.purge and has_casan_evidence)):
|
||||
sys.stderr.write(
|
||||
"casan uninstall: project is not initialized; nothing to remove.\n")
|
||||
return 1
|
||||
|
||||
created = {
|
||||
str(path).replace("\\", "/")
|
||||
for path in manifest.get("created", [])
|
||||
if isinstance(path, str)
|
||||
}
|
||||
owned_files = {
|
||||
str(path).replace("\\", "/")
|
||||
for path in manifest.get("owned_files", [])
|
||||
if isinstance(path, str)
|
||||
}
|
||||
hook_changes = {}
|
||||
integration_files = (
|
||||
(".claude/settings.json", "claude_hook.py", "casan-hook.py"),
|
||||
(".codex/hooks.json", "codex_hook.py", "casan-hook.py"),
|
||||
)
|
||||
for relative, legacy_marker, current_marker in integration_files:
|
||||
path = os.path.join(target, *relative.split("/"))
|
||||
legacy = remove_json_hooks(path, legacy_marker, None)
|
||||
current = remove_json_hooks(path, current_marker, None)
|
||||
status = "removed" if "removed" in (legacy, current) else current
|
||||
hook_changes[relative] = status
|
||||
|
||||
codex_config = os.path.join(target, ".codex", "config.toml")
|
||||
hook_changes[".codex/config.toml"] = clean_legacy_codex_config(
|
||||
codex_config, None)
|
||||
|
||||
vscode_path = os.path.join(target, ".vscode", "extensions.json")
|
||||
hook_changes[".vscode/extensions.json"] = merge_vscode_recommendations(
|
||||
vscode_path, [], None)
|
||||
|
||||
_clean_integration_json(
|
||||
os.path.join(target, ".claude", "settings.json"),
|
||||
"//", "CASAN Plan-20", ".claude/settings.json" in owned_files)
|
||||
_clean_integration_json(
|
||||
os.path.join(target, ".codex", "hooks.json"),
|
||||
"description", "CASAN Plan-20", ".codex/hooks.json" in owned_files)
|
||||
_clean_integration_json(
|
||||
vscode_path, "_casan_unused", "",
|
||||
".vscode/extensions.json" in owned_files)
|
||||
|
||||
removed = []
|
||||
for relative in (
|
||||
".casan/config.json",
|
||||
".casan/version.lock",
|
||||
".casan/agentic.env",
|
||||
".casan/casan-hook.py",
|
||||
".casan/init-manifest.json"):
|
||||
_remove_owned_file(
|
||||
os.path.join(target, *relative.split("/")), removed, target)
|
||||
|
||||
if args.purge:
|
||||
for relative in (".specify/logs", ".specify/state"):
|
||||
path = os.path.join(target, *relative.split("/"))
|
||||
if os.path.isdir(path):
|
||||
with owner_writable(path):
|
||||
shutil.rmtree(path)
|
||||
removed.append(relative)
|
||||
if has_casan_evidence:
|
||||
_remove_owned_file(specify_gitignore, removed, target)
|
||||
|
||||
for relative in (
|
||||
".casan", ".claude", ".codex", ".vscode", ".specify"):
|
||||
_prune_empty_directory(os.path.join(target, relative))
|
||||
|
||||
retained = []
|
||||
if not args.purge and os.path.isdir(os.path.join(target, ".specify")):
|
||||
retained.append(".specify/ runtime evidence (use --purge to remove)")
|
||||
for item in manifest.get("created", []):
|
||||
normalized = str(item).replace("\\", "/")
|
||||
if (normalized.startswith(".gitea/") or
|
||||
normalized.startswith("apps/")):
|
||||
retained.append(normalized + " (may contain project changes)")
|
||||
backups = manifest.get("backups", [])
|
||||
if backups:
|
||||
retained.append("%d .casan-bak backup(s)" % len(backups))
|
||||
|
||||
result = {
|
||||
"status": "uninstalled",
|
||||
"project_id": config.get("project_id") or manifest.get("project_id"),
|
||||
"target": target,
|
||||
"hook_changes": hook_changes,
|
||||
"removed": removed,
|
||||
"purged": bool(args.purge),
|
||||
"vscode_extension": _uninstall_vscode_extension(
|
||||
args.remove_vscode_extension),
|
||||
"retained": retained,
|
||||
}
|
||||
_emit_json_or_human(args, result, _render_uninstall)
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(prog="casan-init", description="CASAN hybrid adoption")
|
||||
sub = parser.add_subparsers(dest="cmd")
|
||||
@@ -1122,21 +1457,42 @@ def main(argv=None):
|
||||
pi.add_argument("--force", action="store_true",
|
||||
help="bypass the source-hub safety guard (adopt CASAN into a CASAN checkout)")
|
||||
pi.add_argument("--harness", help="override harness root")
|
||||
pi.add_argument("--json", action="store_true",
|
||||
help="emit the complete machine-readable result")
|
||||
|
||||
pv = sub.add_parser("verify", help="verify the resolved harness matches the project pin")
|
||||
pv.add_argument("--target", help="project root (default: cwd)")
|
||||
pv.add_argument("--harness", help="override harness root")
|
||||
pv.add_argument("--json", action="store_true",
|
||||
help="emit the complete machine-readable result")
|
||||
|
||||
pl = sub.add_parser("level", help="show installed + project packaging level")
|
||||
pl.add_argument("--show", action="store_true", help="(default) show levels")
|
||||
pl.add_argument("--target", help="project root (default: cwd)")
|
||||
pl.add_argument("--harness", help="override harness root")
|
||||
pl.add_argument("--json", action="store_true",
|
||||
help="emit the complete machine-readable result")
|
||||
|
||||
pd = sub.add_parser("doctor", help="verify selected client integrations end-to-end")
|
||||
pd.add_argument("--target", help="project root (default: cwd)")
|
||||
pd.add_argument("--client", action="append",
|
||||
help="client(s) to check; default: clients enabled in config")
|
||||
pd.add_argument("--harness", help="override harness root")
|
||||
pd.add_argument("--json", action="store_true",
|
||||
help="emit the complete machine-readable result")
|
||||
|
||||
pu = sub.add_parser(
|
||||
"uninstall",
|
||||
help="remove CASAN from a project while preserving user configuration")
|
||||
pu.add_argument("--target", help="project root (default: cwd)")
|
||||
pu.add_argument(
|
||||
"--purge", action="store_true",
|
||||
help="also remove .specify runtime logs and state")
|
||||
pu.add_argument(
|
||||
"--remove-vscode-extension", action="store_true",
|
||||
help="also uninstall the shared @casan VS Code extension from this machine")
|
||||
pu.add_argument("--json", action="store_true",
|
||||
help="emit the complete machine-readable result")
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
if args.cmd == "init":
|
||||
@@ -1153,6 +1509,8 @@ def main(argv=None):
|
||||
return cmd_level(args)
|
||||
if args.cmd == "doctor":
|
||||
return cmd_doctor(args)
|
||||
if args.cmd == "uninstall":
|
||||
return cmd_uninstall(args)
|
||||
parser.print_help()
|
||||
return 64
|
||||
|
||||
|
||||
Reference in New Issue
Block a user